Allow styles to configure a custom renderer
Two new examples show how custom renderers can be used to render text along paths, and to declutter labels using 3rd party libraries.
This commit is contained in:
20
examples/street-labels.html
Normal file
20
examples/street-labels.html
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
layout: example.html
|
||||
title: Street Labels
|
||||
shortdesc: Render street names with a custom render.
|
||||
docs: >
|
||||
Example showing the use of a custom renderer to render text along a path. [Labelgun](https://github.com/Geovation/labelgun) is used to avoid label collisions, and [linelabel](https://github.com/naturalatlas/linelabel) makes sure that labels are placed on suitable street segments. The data is fetched from OSM using the [Overpass API](https://overpass-api.de).
|
||||
tags: "vector, label, collision detection, labelgun, linelabel, overpass"
|
||||
resources:
|
||||
- https://cdn.polyfill.io/v2/polyfill.min.js?features=Set"
|
||||
- https://unpkg.com/rbush@2.0.1/rbush.min.js
|
||||
- https://unpkg.com/labelgun@0.1.1/lib/labelgun.min.js
|
||||
cloak:
|
||||
As1HiMj1PvLPlqc_gtM7AqZfBL8ZL3VrjaS3zIb22Uvb9WKhuJObROC-qUpa81U5: Your Bing Maps Key from http://www.bingmapsportal.com/ here
|
||||
---
|
||||
<!-- Wrap https://npmjs.com/package/linelabel -->
|
||||
<script>var module = {};</script>
|
||||
<script src="https://unpkg.com/linelabel@0.1.1"></script>
|
||||
<script>var linelabel = module.exports;</script>
|
||||
|
||||
<div id="map" class="map"></div>
|
||||
177
examples/street-labels.js
Normal file
177
examples/street-labels.js
Normal file
@@ -0,0 +1,177 @@
|
||||
// NOCOMPILE
|
||||
goog.require('ol.Map');
|
||||
goog.require('ol.View');
|
||||
goog.require('ol.extent');
|
||||
goog.require('ol.format.OSMXML');
|
||||
goog.require('ol.geom.LineString');
|
||||
goog.require('ol.layer.Tile');
|
||||
goog.require('ol.layer.Vector');
|
||||
goog.require('ol.source.BingMaps');
|
||||
goog.require('ol.source.Vector');
|
||||
goog.require('ol.style.Style');
|
||||
|
||||
/* global labelgun */
|
||||
var labelEngine = new labelgun['default'](function() {}, function() {});
|
||||
|
||||
function segmentSort(a, b) {
|
||||
return a.length - b.length;
|
||||
}
|
||||
|
||||
function dist2D(p1, p2) {
|
||||
var dx = p2[0] - p1[0];
|
||||
var dy = p2[1] - p1[1];
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
// Modified from https://github.com/Viglino/ol3-ext/blob/7d17eef5720970fd36798ebc889ea44d9f04b059/style/settextpathstyle.js
|
||||
function textPath(ctx, text, path, fid, pixelRatio) {
|
||||
var di, dpos = 0;
|
||||
var pos = 1;
|
||||
var letterPadding = ctx.measureText(' ').width * pixelRatio * 0.25;
|
||||
var d = 0;
|
||||
for (var i = 1; i < path.length; ++i) {
|
||||
d += dist2D(path[i - 1], path[i]);
|
||||
}
|
||||
var nbspace = text.split(' ').length - 1;
|
||||
var start = (d - ctx.measureText(text).width * pixelRatio - (text.length + nbspace) * letterPadding) / 2;
|
||||
var extent = ol.extent.createEmpty();
|
||||
var letters = [];
|
||||
for (var t = 0; t < text.length; t++) {
|
||||
var letter = text[t];
|
||||
var wl = ctx.measureText(letter).width * pixelRatio;
|
||||
var dl = start + wl / 2;
|
||||
if (!di || dpos + di < dl) {
|
||||
for (; pos < path.length;) {
|
||||
di = dist2D(path[pos - 1], path[pos]);
|
||||
if (dpos + di > dl) {
|
||||
break;
|
||||
}
|
||||
pos += 1;
|
||||
if (pos >= path.length) {
|
||||
break;
|
||||
}
|
||||
dpos += di;
|
||||
}
|
||||
}
|
||||
var x, y, a, dt = dl - dpos;
|
||||
if (pos >= path.length) {
|
||||
pos = path.length - 1;
|
||||
}
|
||||
a = Math.atan2(path[pos][1] - path[pos - 1][1], path[pos][0] - path[pos - 1][0]);
|
||||
if (!dt) {
|
||||
x = path[pos - 1][0];
|
||||
y = path[pos - 1][1];
|
||||
} else {
|
||||
x = path[pos - 1][0] + (path[pos][0] - path[pos - 1][0]) * dt / di;
|
||||
y = path[pos - 1][1] + (path[pos][1] - path[pos - 1][1]) * dt / di;
|
||||
}
|
||||
ol.extent.extendCoordinate(extent, [x, y]);
|
||||
letters.push([x, y, a, letter]);
|
||||
start += wl + letterPadding * (letter == ' ' ? 2 : 1);
|
||||
}
|
||||
ol.extent.buffer(extent, 5 * pixelRatio, extent);
|
||||
var bounds = {
|
||||
bottomLeft: ol.extent.getBottomLeft(extent),
|
||||
topRight: ol.extent.getTopRight(extent)
|
||||
};
|
||||
labelEngine.ingestLabel(bounds, fid, 1, letters, text, false);
|
||||
}
|
||||
|
||||
var style = new ol.style.Style({
|
||||
geometry: function(feature) {
|
||||
// Use the longest, straight enough segment of the geometry
|
||||
var coords = feature.getGeometry().getCoordinates();
|
||||
/* global linelabel */
|
||||
var segment = linelabel(coords, Math.PI / 8).sort(segmentSort)[0];
|
||||
return new ol.geom.LineString(coords.slice(segment.beginIndex, segment.endIndex));
|
||||
},
|
||||
renderer: function(coords, geometry, feature, state) {
|
||||
var context = state.context;
|
||||
var pixelRatio = state.pixelRatio;
|
||||
var text = feature.get('name');
|
||||
if (text) {
|
||||
// Only consider label when the segment is long enough
|
||||
var labelLength = context.measureText(text).width * pixelRatio;
|
||||
var pathLength = 0;
|
||||
for (var i = 1, ii = coords.length; i < ii; ++i) {
|
||||
pathLength += dist2D(coords[i - 1], coords[i]);
|
||||
if (pathLength >= labelLength) {
|
||||
if (coords[0][0] > coords[coords.length - 1][0]) {
|
||||
// Attempt to make text upright
|
||||
coords.reverse();
|
||||
}
|
||||
textPath(context, text, coords, feature.getId(), pixelRatio);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var rasterLayer = new ol.layer.Tile({
|
||||
source: new ol.source.BingMaps({
|
||||
key: 'As1HiMj1PvLPlqc_gtM7AqZfBL8ZL3VrjaS3zIb22Uvb9WKhuJObROC-qUpa81U5',
|
||||
imagerySet: 'Aerial'
|
||||
})
|
||||
});
|
||||
|
||||
var source = new ol.source.Vector();
|
||||
// Request streets from OSM, using the Overpass API
|
||||
var client = new XMLHttpRequest();
|
||||
client.open('POST', 'https://overpass-api.de/api/interpreter');
|
||||
client.addEventListener('load', function() {
|
||||
var features = new ol.format.OSMXML().readFeatures(client.responseText, {
|
||||
featureProjection: 'EPSG:3857'
|
||||
});
|
||||
source.addFeatures(features);
|
||||
});
|
||||
client.send('(way["highway"](48.19642,16.32580,48.22050,16.41986));(._;>;);out meta;');
|
||||
|
||||
var vectorLayer = new ol.layer.Vector({
|
||||
source: source,
|
||||
style: function(feature) {
|
||||
if (feature.getGeometry().getType() == 'LineString') {
|
||||
return style;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var extent = [1817379, 6139595, 1827851, 6143616];
|
||||
var map = new ol.Map({
|
||||
layers: [rasterLayer, vectorLayer],
|
||||
target: 'map',
|
||||
view: new ol.View({
|
||||
extent: extent,
|
||||
center: ol.extent.getCenter(extent),
|
||||
zoom: 17,
|
||||
minZoom: 14
|
||||
})
|
||||
});
|
||||
|
||||
vectorLayer.on('precompose', function() {
|
||||
labelEngine.destroy();
|
||||
});
|
||||
vectorLayer.on('postcompose', function(e) {
|
||||
var context = e.context;
|
||||
var pixelRatio = e.frameState.pixelRatio;
|
||||
context.save();
|
||||
context.font = 'normal 11px "Open Sans", "Arial Unicode MS"';
|
||||
context.fillStyle = 'white';
|
||||
context.textBaseline = 'middle';
|
||||
context.textAlign = 'center';
|
||||
var labels = labelEngine.getShown();
|
||||
for (var i = 0, ii = labels.length; i < ii; ++i) {
|
||||
// Render label letter by letter
|
||||
var letters = labels[i].labelObject;
|
||||
for (var j = 0, jj = letters.length; j < jj; ++j) {
|
||||
var labelData = letters[j];
|
||||
context.save();
|
||||
context.translate(labelData[0], labelData[1]);
|
||||
context.rotate(labelData[2]);
|
||||
context.scale(pixelRatio, pixelRatio);
|
||||
context.fillText(labelData[3], 0, 0);
|
||||
context.restore();
|
||||
}
|
||||
}
|
||||
context.restore();
|
||||
});
|
||||
14
examples/vector-label-decluttering.html
Normal file
14
examples/vector-label-decluttering.html
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
layout: example.html
|
||||
title: Vector Label Decluttering
|
||||
shortdesc: Label decluttering with a custom renderer.
|
||||
resources:
|
||||
- https://cdn.polyfill.io/v2/polyfill.min.js?features=Set"
|
||||
- https://unpkg.com/rbush@2.0.1/rbush.min.js
|
||||
- https://unpkg.com/labelgun@0.1.1/lib/labelgun.min.js
|
||||
docs: >
|
||||
A custom `renderer` function is used instead of an `ol.style.Text` to label the countries of the world. Only texts that are not wider than their country's bounding box are considered and handed over to [Labelgun](https://github.com/Geovation/labelgun) for decluttering.
|
||||
tags: "vector, renderer, labelgun, label"
|
||||
---
|
||||
<div id="map" class="map"></div>
|
||||
<div id="info"> </div>
|
||||
130
examples/vector-label-decluttering.js
Normal file
130
examples/vector-label-decluttering.js
Normal file
@@ -0,0 +1,130 @@
|
||||
// NOCOMPILE
|
||||
goog.require('ol.Map');
|
||||
goog.require('ol.View');
|
||||
goog.require('ol.extent');
|
||||
goog.require('ol.format.GeoJSON');
|
||||
goog.require('ol.geom.Point');
|
||||
goog.require('ol.layer.Vector');
|
||||
goog.require('ol.source.Vector');
|
||||
goog.require('ol.style.Fill');
|
||||
goog.require('ol.style.Stroke');
|
||||
goog.require('ol.style.Style');
|
||||
|
||||
// Style for labels
|
||||
function setStyle(context) {
|
||||
context.font = '12px Calibri,sans-serif';
|
||||
context.fillStyle = '#000';
|
||||
context.strokeStyle = '#fff';
|
||||
context.lineWidth = 3;
|
||||
context.textBaseline = 'hanging';
|
||||
context.textAlign = 'start';
|
||||
}
|
||||
|
||||
// A separate canvas context for measuring label width and height.
|
||||
var textMeasureContext = document.createElement('CANVAS').getContext('2d');
|
||||
setStyle(textMeasureContext);
|
||||
|
||||
// The label height is approximated by the width of the text 'WI'.
|
||||
var height = textMeasureContext.measureText('WI').width;
|
||||
|
||||
// A cache for reusing label images once they have been created.
|
||||
var textCache = {};
|
||||
|
||||
var map = new ol.Map({
|
||||
target: 'map',
|
||||
view: new ol.View({
|
||||
center: [0, 0],
|
||||
zoom: 1
|
||||
})
|
||||
});
|
||||
|
||||
/*global labelgun*/
|
||||
var labelEngine = new labelgun['default'](function() {}, function() {});
|
||||
|
||||
function createLabel(canvas, text, coord) {
|
||||
var halfWidth = canvas.width / 2;
|
||||
var halfHeight = canvas.height / 2;
|
||||
var bounds = {
|
||||
bottomLeft: [Math.round(coord[0] - halfWidth), Math.round(coord[1] - halfHeight)],
|
||||
topRight: [Math.round(coord[0] + halfWidth), Math.round(coord[1] + halfHeight)]
|
||||
};
|
||||
labelEngine.ingestLabel(bounds, coord.toString(), 1, canvas, text, false);
|
||||
}
|
||||
|
||||
// For multi-polygons, we only label the widest polygon. This is done by sorting
|
||||
// by extent width in descending order, and take the first from the array.
|
||||
function sortByWidth(a, b) {
|
||||
return ol.extent.getWidth(b.getExtent()) - ol.extent.getWidth(a.getExtent());
|
||||
}
|
||||
|
||||
var resolution;
|
||||
var styles = [
|
||||
new ol.style.Style({
|
||||
fill: new ol.style.Fill({
|
||||
color: 'rgba(255, 255, 255, 0.6)'
|
||||
}),
|
||||
stroke: new ol.style.Stroke({
|
||||
color: '#319FD3',
|
||||
width: 1
|
||||
})
|
||||
}),
|
||||
new ol.style.Style({
|
||||
renderer: function(coord, geometry, feature, state) {
|
||||
var pixelRatio = state.pixelRatio;
|
||||
var text = feature.get('name');
|
||||
var canvas = textCache[text];
|
||||
if (!canvas) {
|
||||
// Draw the label to its own canvas and cache it.
|
||||
var width = textMeasureContext.measureText(text).width;
|
||||
canvas = textCache[text] = document.createElement('CANVAS');
|
||||
canvas.width = width * pixelRatio;
|
||||
canvas.height = height * pixelRatio;
|
||||
var context = canvas.getContext('2d');
|
||||
context.scale(pixelRatio, pixelRatio);
|
||||
setStyle(context);
|
||||
context.strokeText(text, 0, 0);
|
||||
context.fillText(text, 0, 0);
|
||||
}
|
||||
var extentWidth = geometry.getCoordinates()[2] / resolution * pixelRatio;
|
||||
if (extentWidth > canvas.width) {
|
||||
// Only consider labels not wider than their country's bounding box
|
||||
createLabel(canvas, text, coord);
|
||||
}
|
||||
},
|
||||
// Geometry function to determine label positions
|
||||
geometry: function(feature) {
|
||||
var geometry = feature.getGeometry();
|
||||
if (geometry.getType() == 'MultiPolygon') {
|
||||
var geometries = geometry.getPolygons();
|
||||
geometry = geometries.sort(sortByWidth)[0];
|
||||
}
|
||||
var coordinates = geometry.getInteriorPoint().getCoordinates();
|
||||
var extentWidth = ol.extent.getWidth(geometry.getExtent());
|
||||
coordinates.push(extentWidth);
|
||||
return new ol.geom.Point(coordinates, 'XYM');
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
var vectorLayer = new ol.layer.Vector({
|
||||
source: new ol.source.Vector({
|
||||
url: 'data/geojson/countries.geojson',
|
||||
format: new ol.format.GeoJSON()
|
||||
}),
|
||||
style: styles
|
||||
});
|
||||
vectorLayer.on('precompose', function(e) {
|
||||
resolution = e.frameState.viewState.resolution;
|
||||
labelEngine.destroy();
|
||||
});
|
||||
vectorLayer.on('postcompose', function(e) {
|
||||
var labels = labelEngine.getShown();
|
||||
for (var i = 0, ii = labels.length; i < ii; ++i) {
|
||||
var label = labels[i];
|
||||
// Draw label to the map canvas
|
||||
e.context.drawImage(label.labelObject, label.minX, label.minY);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
map.addLayer(vectorLayer);
|
||||
Reference in New Issue
Block a user