diff --git a/examples/street-labels.html b/examples/street-labels.html
new file mode 100644
index 0000000000..bab0d1b061
--- /dev/null
+++ b/examples/street-labels.html
@@ -0,0 +1,17 @@
+---
+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. [label-segment](https://github.com/ahocevar/label-segment) makes sure that labels are placed on suitable street segments. [textpath](https://github.com/ahocevar/textpath) arranges the letters of a label along the geometry. 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
+ - https://unpkg.com/textpath@1.0.1/dist/textpath.js
+ - https://unpkg.com/label-segment@1.0.0/dist/label-segment.js
+cloak:
+ As1HiMj1PvLPlqc_gtM7AqZfBL8ZL3VrjaS3zIb22Uvb9WKhuJObROC-qUpa81U5: Your Bing Maps Key from http://www.bingmapsportal.com/ here
+---
+
diff --git a/examples/street-labels.js b/examples/street-labels.js
new file mode 100644
index 0000000000..464cd2785d
--- /dev/null
+++ b/examples/street-labels.js
@@ -0,0 +1,116 @@
+// NOCOMPILE
+/* global labelgun, labelSegment, textPath */
+goog.require('ol.Map');
+goog.require('ol.View');
+goog.require('ol.extent');
+goog.require('ol.format.OSMXML');
+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');
+
+var emptyFn = function() {};
+var labelEngine = new labelgun['default'](emptyFn, emptyFn);
+
+var context, pixelRatio; // Will be set in the map's postcompose listener
+function measureText(text) {
+ return context.measureText(text).width * pixelRatio;
+}
+
+var extent, letters; // Will be set in the style's renderer function
+function collectDrawData(letter, x, y, angle) {
+ ol.extent.extendCoordinate(extent, [x, y]);
+ letters.push([x, y, angle, letter]);
+}
+
+var style = new ol.style.Style({
+ renderer: function(coords, geometry, feature) {
+ var text = feature.get('name');
+ if (text) {
+ // Only create label when geometry has a long and straight segment
+ var path = labelSegment(coords, Math.PI / 8, measureText(text));
+ if (path) {
+ extent = ol.extent.createEmpty();
+ letters = [];
+ textPath(text, path, measureText, collectDrawData);
+ ol.extent.buffer(extent, 5 * pixelRatio, extent);
+ var bounds = {
+ bottomLeft: ol.extent.getBottomLeft(extent),
+ topRight: ol.extent.getTopRight(extent)
+ };
+ labelEngine.ingestLabel(bounds, feature.getId(), 1, letters, text, false);
+ }
+ }
+ }
+});
+
+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
+fetch('https://overpass-api.de/api/interpreter', {
+ method: 'POST',
+ body: '(way["highway"](48.19642,16.32580,48.22050,16.41986));(._;>;);out meta;'
+}).then(function(response) {
+ return response.text();
+}).then(function(responseText) {
+ var features = new ol.format.OSMXML().readFeatures(responseText, {
+ featureProjection: 'EPSG:3857'
+ });
+ source.addFeatures(features);
+});
+
+var vectorLayer = new ol.layer.Vector({
+ source: source,
+ style: function(feature) {
+ if (feature.getGeometry().getType() == 'LineString') {
+ return style;
+ }
+ }
+});
+
+var viewExtent = [1817379, 6139595, 1827851, 6143616];
+var map = new ol.Map({
+ layers: [rasterLayer, vectorLayer],
+ target: 'map',
+ view: new ol.View({
+ extent: viewExtent,
+ center: ol.extent.getCenter(viewExtent),
+ zoom: 17,
+ minZoom: 14
+ })
+});
+
+vectorLayer.on('precompose', function() {
+ labelEngine.destroy();
+});
+vectorLayer.on('postcompose', function(e) {
+ context = e.context;
+ 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();
+});
diff --git a/examples/vector-label-decluttering.html b/examples/vector-label-decluttering.html
new file mode 100644
index 0000000000..cc45ca1d74
--- /dev/null
+++ b/examples/vector-label-decluttering.html
@@ -0,0 +1,13 @@
+---
+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"
+---
+
diff --git a/examples/vector-label-decluttering.js b/examples/vector-label-decluttering.js
new file mode 100644
index 0000000000..9a50d04dd9
--- /dev/null
+++ b/examples/vector-label-decluttering.js
@@ -0,0 +1,132 @@
+// NOCOMPILE
+/* global labelgun */
+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
+ })
+});
+
+var emptyFn = function() {};
+var labelEngine = new labelgun['default'](emptyFn, emptyFn);
+
+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; // This is set by the map's precompose listener
+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);
+ }
+ // The 3rd value of the coordinate is the measure of the extent width
+ 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());
+ // We are using the extentWidth as measure value of the geometry
+ 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);
diff --git a/externs/olx.js b/externs/olx.js
index e909141fb5..e07514a3ea 100644
--- a/externs/olx.js
+++ b/externs/olx.js
@@ -4400,6 +4400,48 @@ olx.layer.VectorTileOptions.prototype.visible;
olx.render;
+/**
+ * @typedef {{context: CanvasRenderingContext2D,
+ * pixelRatio: number,
+ * resolution: number,
+ * rotation: number}}
+ */
+olx.render.State;
+
+
+/**
+ * Canvas context that the layer is being rendered to.
+ * @type {CanvasRenderingContext2D}
+ * @api
+ */
+olx.render.State.prototype.context;
+
+
+/**
+ * Pixel ratio used by the layer renderer.
+ * @type {number}
+ * @api
+ */
+olx.render.State.prototype.pixelRatio;
+
+
+/**
+ * Resolution that the render batch was created and optimized for. This is
+ * not the view's resolution that is being rendered.
+ * @type {number}
+ * @api
+ */
+olx.render.State.prototype.resolution;
+
+
+/**
+ * Rotation of the rendered layer in radians.
+ * @type {number}
+ * @api
+ */
+olx.render.State.prototype.rotation;
+
+
/**
* @typedef {{size: (ol.Size|undefined),
* pixelRatio: (number|undefined)}}
@@ -7621,6 +7663,7 @@ olx.style.TextOptions.prototype.stroke;
* @typedef {{geometry: (undefined|string|ol.geom.Geometry|ol.StyleGeometryFunction),
* fill: (ol.style.Fill|undefined),
* image: (ol.style.Image|undefined),
+ * renderer: (ol.StyleRenderFunction|undefined),
* stroke: (ol.style.Stroke|undefined),
* text: (ol.style.Text|undefined),
* zIndex: (number|undefined)}}
@@ -7653,6 +7696,16 @@ olx.style.StyleOptions.prototype.fill;
olx.style.StyleOptions.prototype.image;
+/**
+ * Custom renderer. When configured, `fill`, `stroke` and `image` will be
+ * ignored, and the provided function will be called with each render frame for
+ * each geometry.
+ *
+ * @type {ol.StyleRenderFunction|undefined}
+ */
+olx.style.StyleOptions.prototype.renderer;
+
+
/**
* Stroke style.
* @type {ol.style.Stroke|undefined}
diff --git a/src/ol/geom/simplegeometry.js b/src/ol/geom/simplegeometry.js
index ecd5626144..f8821f1192 100644
--- a/src/ol/geom/simplegeometry.js
+++ b/src/ol/geom/simplegeometry.js
@@ -41,6 +41,12 @@ ol.geom.SimpleGeometry = function() {
*/
this.flatCoordinates = null;
+ /**
+ * @private
+ * @type {Array.|Array.>|Array.>>}
+ */
+ this.renderCoordinates_ = null;
+
};
ol.inherits(ol.geom.SimpleGeometry, ol.geom.Geometry);
@@ -141,6 +147,18 @@ ol.geom.SimpleGeometry.prototype.getLayout = function() {
};
+/**
+ * @return {Array.|Array.>|Array.>>}
+ * Render coordinates.
+ */
+ol.geom.SimpleGeometry.prototype.getRenderCoordinates = function() {
+ if (!this.renderCoordinates_) {
+ this.renderCoordinates_ = [];
+ }
+ return this.renderCoordinates_;
+};
+
+
/**
* @inheritDoc
*/
diff --git a/src/ol/render/canvas/instruction.js b/src/ol/render/canvas/instruction.js
index 8985e59b97..78419e7692 100644
--- a/src/ol/render/canvas/instruction.js
+++ b/src/ol/render/canvas/instruction.js
@@ -8,13 +8,14 @@ ol.render.canvas.Instruction = {
BEGIN_PATH: 1,
CIRCLE: 2,
CLOSE_PATH: 3,
- DRAW_IMAGE: 4,
- DRAW_TEXT: 5,
- END_GEOMETRY: 6,
- FILL: 7,
- MOVE_TO_LINE_TO: 8,
- SET_FILL_STYLE: 9,
- SET_STROKE_STYLE: 10,
- SET_TEXT_STYLE: 11,
- STROKE: 12
+ CUSTOM: 4,
+ DRAW_IMAGE: 5,
+ DRAW_TEXT: 6,
+ END_GEOMETRY: 7,
+ FILL: 8,
+ MOVE_TO_LINE_TO: 9,
+ SET_FILL_STYLE: 10,
+ SET_STROKE_STYLE: 11,
+ SET_TEXT_STYLE: 12,
+ STROKE: 13
};
diff --git a/src/ol/render/canvas/linestringreplay.js b/src/ol/render/canvas/linestringreplay.js
index 91e86dce8f..218b18c1ea 100644
--- a/src/ol/render/canvas/linestringreplay.js
+++ b/src/ol/render/canvas/linestringreplay.js
@@ -37,7 +37,7 @@ ol.render.canvas.LineStringReplay = function(tolerance, maxExtent, resolution, o
* currentLineJoin: (string|undefined),
* currentLineWidth: (number|undefined),
* currentMiterLimit: (number|undefined),
- * lastStroke: number,
+ * lastStroke: (number|undefined),
* strokeStyle: (ol.ColorLike|undefined),
* lineCap: (string|undefined),
* lineDash: Array.,
@@ -54,7 +54,7 @@ ol.render.canvas.LineStringReplay = function(tolerance, maxExtent, resolution, o
currentLineJoin: undefined,
currentLineWidth: undefined,
currentMiterLimit: undefined,
- lastStroke: 0,
+ lastStroke: undefined,
strokeStyle: undefined,
lineCap: undefined,
lineDash: null,
@@ -122,10 +122,11 @@ ol.render.canvas.LineStringReplay.prototype.setStrokeStyle_ = function() {
state.currentLineJoin != lineJoin ||
state.currentLineWidth != lineWidth ||
state.currentMiterLimit != miterLimit) {
- if (state.lastStroke != this.coordinates.length) {
+ if (state.lastStroke != undefined && state.lastStroke != this.coordinates.length) {
this.instructions.push([ol.render.canvas.Instruction.STROKE]);
state.lastStroke = this.coordinates.length;
}
+ state.lastStroke = 0;
this.instructions.push([
ol.render.canvas.Instruction.SET_STROKE_STYLE,
strokeStyle, lineWidth, lineCap, lineJoin, miterLimit, lineDash, lineDashOffset, true, 1
@@ -208,7 +209,7 @@ ol.render.canvas.LineStringReplay.prototype.drawMultiLineString = function(multi
*/
ol.render.canvas.LineStringReplay.prototype.finish = function() {
var state = this.state_;
- if (state.lastStroke != this.coordinates.length) {
+ if (state.lastStroke != undefined && state.lastStroke != this.coordinates.length) {
this.instructions.push([ol.render.canvas.Instruction.STROKE]);
}
this.reverseHitDetectionInstructions();
diff --git a/src/ol/render/canvas/replay.js b/src/ol/render/canvas/replay.js
index 9550eda872..309ce70586 100644
--- a/src/ol/render/canvas/replay.js
+++ b/src/ol/render/canvas/replay.js
@@ -4,6 +4,8 @@ goog.require('ol');
goog.require('ol.array');
goog.require('ol.extent');
goog.require('ol.extent.Relationship');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.flat.inflate');
goog.require('ol.geom.flat.transform');
goog.require('ol.has');
goog.require('ol.obj');
@@ -174,6 +176,75 @@ ol.render.canvas.Replay.prototype.appendFlatCoordinates = function(flatCoordinat
};
+/**
+ * @param {Array.} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {Array.} ends Ends.
+ * @param {number} stride Stride.
+ * @param {Array.} replayEnds Replay ends.
+ * @return {number} Offset.
+ */
+ol.render.canvas.Replay.prototype.drawCustomCoordinates_ = function(flatCoordinates, offset, ends, stride, replayEnds) {
+ for (var i = 0, ii = ends.length; i < ii; ++i) {
+ var end = ends[i];
+ var replayEnd = this.appendFlatCoordinates(flatCoordinates, offset, end, stride, false, false);
+ replayEnds.push(replayEnd);
+ offset = end;
+ }
+ return offset;
+};
+
+
+/**
+ * @inheritDoc.
+ */
+ol.render.canvas.Replay.prototype.drawCustom = function(geometry, feature, renderer) {
+ this.beginGeometry(geometry, feature);
+ var type = geometry.getType();
+ var stride = geometry.getStride();
+ var replayBegin = this.coordinates.length;
+ var flatCoordinates, replayEnd, replayEnds, replayEndss;
+ var offset;
+ if (type == ol.geom.GeometryType.MULTI_POLYGON) {
+ geometry = /** @type {ol.geom.MultiPolygon} */ (geometry);
+ flatCoordinates = geometry.getOrientedFlatCoordinates();
+ replayEndss = [];
+ var endss = geometry.getEndss();
+ offset = 0;
+ for (var i = 0, ii = endss.length; i < ii; ++i) {
+ var myEnds = [];
+ offset = this.drawCustomCoordinates_(flatCoordinates, offset, endss[i], stride, myEnds);
+ replayEndss.push(myEnds);
+ }
+ this.instructions.push([ol.render.canvas.Instruction.CUSTOM,
+ replayBegin, replayEndss, geometry, renderer, ol.geom.flat.inflate.coordinatesss]);
+ } else if (type == ol.geom.GeometryType.POLYGON || type == ol.geom.GeometryType.MULTI_LINE_STRING) {
+ replayEnds = [];
+ flatCoordinates = (type == ol.geom.GeometryType.POLYGON) ?
+ /** @type {ol.geom.Polygon} */ (geometry).getOrientedFlatCoordinates() :
+ geometry.getFlatCoordinates();
+ offset = this.drawCustomCoordinates_(flatCoordinates, 0,
+ /** @type {ol.geom.Polygon|ol.geom.MultiLineString} */ (geometry).getEnds(),
+ stride, replayEnds);
+ this.instructions.push([ol.render.canvas.Instruction.CUSTOM,
+ replayBegin, replayEnds, geometry, renderer, ol.geom.flat.inflate.coordinatess]);
+ } else if (type == ol.geom.GeometryType.LINE_STRING || type == ol.geom.GeometryType.MULTI_POINT) {
+ flatCoordinates = geometry.getFlatCoordinates();
+ replayEnd = this.appendFlatCoordinates(
+ flatCoordinates, 0, flatCoordinates.length, stride, false, false);
+ this.instructions.push([ol.render.canvas.Instruction.CUSTOM,
+ replayBegin, replayEnd, geometry, renderer, ol.geom.flat.inflate.coordinates]);
+ } else if (type == ol.geom.GeometryType.POINT) {
+ flatCoordinates = geometry.getFlatCoordinates();
+ this.coordinates.push(flatCoordinates[0], flatCoordinates[1]);
+ replayEnd = this.coordinates.length;
+ this.instructions.push([ol.render.canvas.Instruction.CUSTOM,
+ replayBegin, replayEnd, geometry, renderer]);
+ }
+ this.endGeometry(geometry, feature);
+};
+
+
/**
* @protected
* @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry.
@@ -249,6 +320,17 @@ ol.render.canvas.Replay.prototype.replay_ = function(
var prevX, prevY, roundX, roundY;
var pendingFill = 0;
var pendingStroke = 0;
+
+ /**
+ * @type {olx.render.State}
+ */
+ var state = {
+ context: context,
+ pixelRatio: pixelRatio,
+ resolution: this.resolution,
+ rotation: viewRotation
+ };
+
// When the batch size gets too big, performance decreases. 200 is a good
// balance between batch size and number of fill/stroke instructions.
var batchSize =
@@ -303,6 +385,21 @@ ol.render.canvas.Replay.prototype.replay_ = function(
context.closePath();
++i;
break;
+ case ol.render.canvas.Instruction.CUSTOM:
+ d = /** @type {number} */ (instruction[1]);
+ dd = instruction[2];
+ var geometry = /** @type {ol.geom.SimpleGeometry} */ (instruction[3]);
+ var renderer = instruction[4];
+ var coords;
+ if (instruction.length == 6) {
+ var fn = instruction[5];
+ coords = fn(pixelCoordinates, d, dd, 2, geometry.getRenderCoordinates());
+ } else {
+ coords = pixelCoordinates.slice(d, dd);
+ }
+ renderer(coords, geometry, feature, state);
+ ++i;
+ break;
case ol.render.canvas.Instruction.DRAW_IMAGE:
d = /** @type {number} */ (instruction[1]);
dd = /** @type {number} */ (instruction[2]);
diff --git a/src/ol/render/canvas/replaygroup.js b/src/ol/render/canvas/replaygroup.js
index 356b982799..a4ee79a49a 100644
--- a/src/ol/render/canvas/replaygroup.js
+++ b/src/ol/render/canvas/replaygroup.js
@@ -7,6 +7,7 @@ goog.require('ol.extent');
goog.require('ol.geom.flat.transform');
goog.require('ol.obj');
goog.require('ol.render.ReplayGroup');
+goog.require('ol.render.canvas.Replay');
goog.require('ol.render.canvas.ImageReplay');
goog.require('ol.render.canvas.LineStringReplay');
goog.require('ol.render.canvas.PolygonReplay');
@@ -161,6 +162,24 @@ ol.render.canvas.ReplayGroup.getCircleArray_ = function(radius) {
return arr;
};
+
+/**
+ * @param {Array.} replays Replays.
+ * @return {boolean} Has replays of the provided types.
+ */
+ol.render.canvas.ReplayGroup.prototype.hasReplays = function(replays) {
+ for (var zIndex in this.replaysByZIndex_) {
+ var candidates = this.replaysByZIndex_[zIndex];
+ for (var i = 0, ii = replays.length; i < ii; ++i) {
+ if (replays[i] in candidates) {
+ return true;
+ }
+ }
+ }
+ return false;
+};
+
+
/**
* FIXME empty description for jsdoc
*/
@@ -387,6 +406,7 @@ ol.render.canvas.ReplayGroup.prototype.replayHitDetection_ = function(
*/
ol.render.canvas.ReplayGroup.BATCH_CONSTRUCTORS_ = {
'Circle': ol.render.canvas.PolygonReplay,
+ 'Default': ol.render.canvas.Replay,
'Image': ol.render.canvas.ImageReplay,
'LineString': ol.render.canvas.LineStringReplay,
'Polygon': ol.render.canvas.PolygonReplay,
diff --git a/src/ol/render/feature.js b/src/ol/render/feature.js
index 702dbeb2c2..8bbec70671 100644
--- a/src/ol/render/feature.js
+++ b/src/ol/render/feature.js
@@ -43,6 +43,12 @@ ol.render.Feature = function(type, flatCoordinates, ends, properties, id) {
*/
this.flatCoordinates_ = flatCoordinates;
+ /**
+ * @private
+ * @type {Array.|Array.>|Array.>>}
+ */
+ this.renderCoordinates_ = null;
+
/**
* @private
* @type {Array.|Array.>}
@@ -111,6 +117,18 @@ ol.render.Feature.prototype.getOrientedFlatCoordinates = function() {
};
+/**
+ * @return {Array.|Array.>|Array.>>}
+ * Render coordinates.
+ */
+ol.render.Feature.prototype.getRenderCoordinates = function() {
+ if (!this.renderCoordinates_) {
+ this.renderCoordinates_ = [];
+ }
+ return this.renderCoordinates_;
+};
+
+
/**
* @return {Array.} Flat coordinates.
*/
diff --git a/src/ol/render/replay.js b/src/ol/render/replay.js
index a0a8e0dc7e..3737e5ef03 100644
--- a/src/ol/render/replay.js
+++ b/src/ol/render/replay.js
@@ -12,5 +12,6 @@ ol.render.replay.ORDER = [
ol.render.ReplayType.CIRCLE,
ol.render.ReplayType.LINE_STRING,
ol.render.ReplayType.IMAGE,
- ol.render.ReplayType.TEXT
+ ol.render.ReplayType.TEXT,
+ ol.render.ReplayType.DEFAULT
];
diff --git a/src/ol/render/replaytype.js b/src/ol/render/replaytype.js
index 5423cee151..066bcef098 100644
--- a/src/ol/render/replaytype.js
+++ b/src/ol/render/replaytype.js
@@ -6,6 +6,7 @@ goog.provide('ol.render.ReplayType');
*/
ol.render.ReplayType = {
CIRCLE: 'Circle',
+ DEFAULT: 'Default',
IMAGE: 'Image',
LINE_STRING: 'LineString',
POLYGON: 'Polygon',
diff --git a/src/ol/render/vectorcontext.js b/src/ol/render/vectorcontext.js
index a8974f1cc7..4428177138 100644
--- a/src/ol/render/vectorcontext.js
+++ b/src/ol/render/vectorcontext.js
@@ -13,6 +13,16 @@ ol.render.VectorContext = function() {
};
+/**
+ * Render a geometry with a custom renderer.
+ *
+ * @param {ol.geom.SimpleGeometry} geometry Geometry.
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ * @param {Function} renderer Renderer.
+ */
+ol.render.VectorContext.prototype.drawCustom = function(geometry, feature, renderer) {};
+
+
/**
* Render a geometry.
*
diff --git a/src/ol/renderer/canvas/vectortilelayer.js b/src/ol/renderer/canvas/vectortilelayer.js
index e0482fcec5..ee6c12ac60 100644
--- a/src/ol/renderer/canvas/vectortilelayer.js
+++ b/src/ol/renderer/canvas/vectortilelayer.js
@@ -61,7 +61,8 @@ ol.inherits(ol.renderer.canvas.VectorTileLayer, ol.renderer.canvas.TileLayer);
* @type {!Object.>}
*/
ol.renderer.canvas.VectorTileLayer.IMAGE_REPLAYS = {
- 'image': ol.render.replay.ORDER,
+ 'image': [ol.render.ReplayType.POLYGON, ol.render.ReplayType.CIRCLE,
+ ol.render.ReplayType.LINE_STRING, ol.render.ReplayType.IMAGE, ol.render.ReplayType.TEXT],
'hybrid': [ol.render.ReplayType.POLYGON, ol.render.ReplayType.LINE_STRING]
};
@@ -71,7 +72,8 @@ ol.renderer.canvas.VectorTileLayer.IMAGE_REPLAYS = {
* @type {!Object.>}
*/
ol.renderer.canvas.VectorTileLayer.VECTOR_REPLAYS = {
- 'hybrid': [ol.render.ReplayType.IMAGE, ol.render.ReplayType.TEXT],
+ 'image': [ol.render.ReplayType.DEFAULT],
+ 'hybrid': [ol.render.ReplayType.IMAGE, ol.render.ReplayType.TEXT, ol.render.ReplayType.DEFAULT],
'vector': ol.render.replay.ORDER
};
@@ -332,61 +334,62 @@ ol.renderer.canvas.VectorTileLayer.prototype.postCompose = function(context, fra
var source = layer.getSource();
var renderMode = layer.getRenderMode();
var replays = ol.renderer.canvas.VectorTileLayer.VECTOR_REPLAYS[renderMode];
- if (replays) {
- var pixelRatio = frameState.pixelRatio;
- var rotation = frameState.viewState.rotation;
- var size = frameState.size;
- var offsetX = Math.round(pixelRatio * size[0] / 2);
- var offsetY = Math.round(pixelRatio * size[1] / 2);
- var tiles = this.renderedTiles;
- var tilePixelRatio = layer.getSource().getTilePixelRatio();
- var sourceTileGrid = source.getTileGrid();
- var tileGrid = source.getTileGridForProjection(frameState.viewState.projection);
- var clips = [];
- var zs = [];
- for (var i = tiles.length - 1; i >= 0; --i) {
- var tile = /** @type {ol.VectorImageTile} */ (tiles[i]);
- if (tile.getState() == ol.TileState.ABORT) {
+ var pixelRatio = frameState.pixelRatio;
+ var rotation = frameState.viewState.rotation;
+ var size = frameState.size;
+ var offsetX = Math.round(pixelRatio * size[0] / 2);
+ var offsetY = Math.round(pixelRatio * size[1] / 2);
+ var tiles = this.renderedTiles;
+ var tilePixelRatio = layer.getSource().getTilePixelRatio();
+ var sourceTileGrid = source.getTileGrid();
+ var tileGrid = source.getTileGridForProjection(frameState.viewState.projection);
+ var clips = [];
+ var zs = [];
+ for (var i = tiles.length - 1; i >= 0; --i) {
+ var tile = /** @type {ol.VectorImageTile} */ (tiles[i]);
+ if (tile.getState() == ol.TileState.ABORT) {
+ continue;
+ }
+ var tileCoord = tile.tileCoord;
+ var worldOffset = tileGrid.getTileCoordExtent(tileCoord)[0] -
+ tileGrid.getTileCoordExtent(tile.wrappedTileCoord)[0];
+ for (var t = 0, tt = tile.tileKeys.length; t < tt; ++t) {
+ var sourceTile = tile.getTile(tile.tileKeys[t]);
+ var replayGroup = sourceTile.getReplayGroup(layer, tileCoord.toString());
+ if (renderMode != ol.layer.VectorTileRenderType.VECTOR && !replayGroup.hasReplays(replays)) {
continue;
}
- var tileCoord = tile.tileCoord;
- var worldOffset = tileGrid.getTileCoordExtent(tileCoord)[0] -
- tileGrid.getTileCoordExtent(tile.wrappedTileCoord)[0];
- for (var t = 0, tt = tile.tileKeys.length; t < tt; ++t) {
- var sourceTile = tile.getTile(tile.tileKeys[t]);
- var currentZ = sourceTile.tileCoord[0];
- var sourceResolution = sourceTileGrid.getResolution(currentZ);
- var transform = this.getReplayTransform_(sourceTile, frameState);
- ol.transform.translate(transform, worldOffset * tilePixelRatio / sourceResolution, 0);
- var replayGroup = sourceTile.getReplayGroup(layer, tileCoord.toString());
- var currentClip = replayGroup.getClipCoords(transform);
- context.save();
- context.globalAlpha = layerState.opacity;
- ol.render.canvas.rotateAtOffset(context, -rotation, offsetX, offsetY);
- // Create a clip mask for regions in this low resolution tile that are
- // already filled by a higher resolution tile
- for (var j = 0, jj = clips.length; j < jj; ++j) {
- var clip = clips[j];
- if (currentZ < zs[j]) {
- context.beginPath();
- // counter-clockwise (outer ring) for current tile
- context.moveTo(currentClip[0], currentClip[1]);
- context.lineTo(currentClip[2], currentClip[3]);
- context.lineTo(currentClip[4], currentClip[5]);
- context.lineTo(currentClip[6], currentClip[7]);
- // clockwise (inner ring) for higher resolution tile
- context.moveTo(clip[6], clip[7]);
- context.lineTo(clip[4], clip[5]);
- context.lineTo(clip[2], clip[3]);
- context.lineTo(clip[0], clip[1]);
- context.clip();
- }
+ var currentZ = sourceTile.tileCoord[0];
+ var sourceResolution = sourceTileGrid.getResolution(currentZ);
+ var transform = this.getReplayTransform_(sourceTile, frameState);
+ ol.transform.translate(transform, worldOffset * tilePixelRatio / sourceResolution, 0);
+ var currentClip = replayGroup.getClipCoords(transform);
+ context.save();
+ context.globalAlpha = layerState.opacity;
+ ol.render.canvas.rotateAtOffset(context, -rotation, offsetX, offsetY);
+ // Create a clip mask for regions in this low resolution tile that are
+ // already filled by a higher resolution tile
+ for (var j = 0, jj = clips.length; j < jj; ++j) {
+ var clip = clips[j];
+ if (currentZ < zs[j]) {
+ context.beginPath();
+ // counter-clockwise (outer ring) for current tile
+ context.moveTo(currentClip[0], currentClip[1]);
+ context.lineTo(currentClip[2], currentClip[3]);
+ context.lineTo(currentClip[4], currentClip[5]);
+ context.lineTo(currentClip[6], currentClip[7]);
+ // clockwise (inner ring) for higher resolution tile
+ context.moveTo(clip[6], clip[7]);
+ context.lineTo(clip[4], clip[5]);
+ context.lineTo(clip[2], clip[3]);
+ context.lineTo(clip[0], clip[1]);
+ context.clip();
}
- replayGroup.replay(context, pixelRatio, transform, rotation, {}, replays);
- context.restore();
- clips.push(currentClip);
- zs.push(currentZ);
}
+ replayGroup.replay(context, pixelRatio, transform, rotation, {}, replays);
+ context.restore();
+ clips.push(currentClip);
+ zs.push(currentZ);
}
}
ol.renderer.canvas.TileLayer.prototype.postCompose.apply(this, arguments);
@@ -466,7 +469,7 @@ ol.renderer.canvas.VectorTileLayer.prototype.renderTileImage_ = function(
ol.transform.translate(transform, -tileExtent[0], -tileExtent[3]);
}
var replayGroup = sourceTile.getReplayGroup(layer, tile.tileCoord.toString());
- replayGroup.replay(context, pixelRatio, transform, 0, {}, replays);
+ replayGroup.replay(context, pixelRatio, transform, 0, {}, replays, true);
}
}
};
diff --git a/src/ol/renderer/vector.js b/src/ol/renderer/vector.js
index 14f522c659..b0efa64a47 100644
--- a/src/ol/renderer/vector.js
+++ b/src/ol/renderer/vector.js
@@ -2,6 +2,7 @@ goog.provide('ol.renderer.vector');
goog.require('ol');
goog.require('ol.ImageState');
+goog.require('ol.geom.GeometryType');
goog.require('ol.render.ReplayType');
@@ -111,9 +112,34 @@ ol.renderer.vector.renderFeature_ = function(
return;
}
var simplifiedGeometry = geometry.getSimplifiedGeometry(squaredTolerance);
- var geometryRenderer =
- ol.renderer.vector.GEOMETRY_RENDERERS_[simplifiedGeometry.getType()];
- geometryRenderer(replayGroup, simplifiedGeometry, style, feature);
+ var renderer = style.getRenderer();
+ if (renderer) {
+ ol.renderer.vector.renderGeometry_(replayGroup, simplifiedGeometry, style, feature);
+ } else {
+ var geometryRenderer =
+ ol.renderer.vector.GEOMETRY_RENDERERS_[simplifiedGeometry.getType()];
+ geometryRenderer(replayGroup, simplifiedGeometry, style, feature);
+ }
+};
+
+
+/**
+ * @param {ol.render.ReplayGroup} replayGroup Replay group.
+ * @param {ol.geom.Geometry} geometry Geometry.
+ * @param {ol.style.Style} style Style.
+ * @param {ol.Feature|ol.render.Feature} feature Feature.
+ * @private
+ */
+ol.renderer.vector.renderGeometry_ = function(replayGroup, geometry, style, feature) {
+ if (geometry.getType() == ol.geom.GeometryType.GEOMETRY_COLLECTION) {
+ var geometries = /** @type {ol.geom.GeometryCollection} */ (geometry).getGeometries();
+ for (var i = 0, ii = geometries.length; i < ii; ++i) {
+ ol.renderer.vector.renderGeometry_(replayGroup, geometries[i], style, feature);
+ }
+ return;
+ }
+ var replay = replayGroup.getReplay(style.getZIndex(), ol.render.ReplayType.DEFAULT);
+ replay.drawCustom(/** @type {ol.geom.SimpleGeometry} */ (geometry), feature, style.getRenderer());
};
diff --git a/src/ol/style/style.js b/src/ol/style/style.js
index b3f076ec5e..09dd93407f 100644
--- a/src/ol/style/style.js
+++ b/src/ol/style/style.js
@@ -50,6 +50,12 @@ ol.style.Style = function(opt_options) {
*/
this.image_ = options.image !== undefined ? options.image : null;
+ /**
+ * @private
+ * @type {ol.StyleRenderFunction|null}
+ */
+ this.renderer_ = options.renderer !== undefined ? options.renderer : null;
+
/**
* @private
* @type {ol.style.Stroke}
@@ -92,6 +98,28 @@ ol.style.Style.prototype.clone = function() {
};
+/**
+ * Get the custom renderer function that was configured with
+ * {@link #setRenderer} or the `renderer` constructor option.
+ * @return {ol.StyleRenderFunction|null} Custom renderer function.
+ * @api
+ */
+ol.style.Style.prototype.getRenderer = function() {
+ return this.renderer_;
+};
+
+
+/**
+ * Sets a custom renderer function for this style. When set, `fill`, `stroke`
+ * and `image` options of the style will be ignored.
+ * @param {ol.StyleRenderFunction|null} renderer Custom renderer function.
+ * @api
+ */
+ol.style.Style.prototype.setRenderer = function(renderer) {
+ this.renderer_ = renderer;
+};
+
+
/**
* Get the geometry to be rendered.
* @return {string|ol.geom.Geometry|ol.StyleGeometryFunction}
diff --git a/src/ol/typedefs.js b/src/ol/typedefs.js
index d9300c5912..ec24160a3e 100644
--- a/src/ol/typedefs.js
+++ b/src/ol/typedefs.js
@@ -626,6 +626,19 @@ ol.StyleFunction;
ol.StyleGeometryFunction;
+/**
+ * Custom renderer function. Takes 4 arguments:
+ *
+ * 1. The pixel coordinates of the geometry in GeoJSON notation.
+ * 2. The original {@link ol.geom.SimpleGeometry}.
+ * 3. The underlying {@link ol.Feature} or {@link ol.render.Feature}.
+ * 4. The {@link olx.render.State} of the layer renderer.
+ *
+ * @typedef {function(Array,ol.geom.SimpleGeometry,(ol.Feature|ol.render.Feature),olx.render.State)}
+ */
+ol.StyleRenderFunction;
+
+
/**
* @typedef {{opacity: number,
* rotateWithView: boolean,
diff --git a/tasks/build-examples.js b/tasks/build-examples.js
index 2275e0bf85..47054ea603 100644
--- a/tasks/build-examples.js
+++ b/tasks/build-examples.js
@@ -10,7 +10,7 @@ var markupRegEx = /([^\/^\.]*)\.html$/;
var cleanupJSRegEx = /.*(\/\/ NOCOMPILE|goog\.require\(.*\);)[\r\n]*/g;
var requiresRegEx = /.*goog\.require\('(ol\.\S*)'\);/g;
var isCssRegEx = /\.css$/;
-var isJsRegEx = /\.js$/;
+var isJsRegEx = /\.js(\?.*)?$/;
var srcDir = path.join(__dirname, '..', 'examples');
var destDir = path.join(__dirname, '..', 'build', 'examples');
diff --git a/test/spec/ol/renderer/canvas/replay.test.js b/test/spec/ol/renderer/canvas/replay.test.js
index 306367dd69..6398f5b55b 100644
--- a/test/spec/ol/renderer/canvas/replay.test.js
+++ b/test/spec/ol/renderer/canvas/replay.test.js
@@ -205,6 +205,53 @@ describe('ol.render.canvas.ReplayGroup', function() {
expect(style2.getStroke().getLineDashOffset()).to.be(2);
expect(lineDashOffset).to.be(4);
});
+
+ it('calls the renderer function configured for the style', function() {
+ var spy = sinon.spy();
+ var style = new ol.style.Style({
+ renderer: spy
+ });
+ var point = new ol.Feature(new ol.geom.Point([45, 90]));
+ var multipoint = new ol.Feature(new ol.geom.MultiPoint(
+ [[45, 90], [90, 45]]));
+ var linestring = new ol.Feature(new ol.geom.LineString(
+ [[45, 90], [45, 45], [90, 45]]));
+ var multilinestring = new ol.Feature(new ol.geom.MultiLineString(
+ [linestring.getGeometry().getCoordinates(), linestring.getGeometry().getCoordinates()]));
+ var polygon = feature1;
+ var multipolygon = new ol.Feature(new ol.geom.MultiPolygon(
+ [polygon.getGeometry().getCoordinates(), polygon.getGeometry().getCoordinates()]));
+ var geometrycollection = new ol.Feature(new ol.geom.GeometryCollection(
+ [point.getGeometry(), linestring.getGeometry(), polygon.getGeometry()]));
+ replay = new ol.render.canvas.ReplayGroup(1, [-180, -90, 180, 90], 1, true);
+ ol.renderer.vector.renderFeature(replay, point, style, 1);
+ ol.renderer.vector.renderFeature(replay, multipoint, style, 1);
+ ol.renderer.vector.renderFeature(replay, linestring, style, 1);
+ ol.renderer.vector.renderFeature(replay, multilinestring, style, 1);
+ ol.renderer.vector.renderFeature(replay, polygon, style, 1);
+ ol.renderer.vector.renderFeature(replay, multipolygon, style, 1);
+ ol.renderer.vector.renderFeature(replay, geometrycollection, style, 1);
+ ol.transform.scale(transform, 0.1, 0.1);
+ replay.replay(context, 1, transform, 0, {});
+ expect(spy.callCount).to.be(9);
+ expect(spy.firstCall.args.length).to.be(4);
+ expect(spy.firstCall.args[1]).to.be(point.getGeometry());
+ expect(spy.firstCall.args[2]).to.be(point);
+ expect(spy.firstCall.args[3].context).to.be(context);
+ expect(spy.firstCall.args[3].pixelRatio).to.be(1);
+ expect(spy.firstCall.args[3].rotation).to.be(0);
+ expect(spy.firstCall.args[3].resolution).to.be(1);
+ expect(spy.getCall(0).args[0]).to.eql([4.5, 9]);
+ expect(spy.getCall(1).args[0][0]).to.eql([4.5, 9]);
+ expect(spy.getCall(2).args[0][0]).to.eql([4.5, 9]);
+ expect(spy.getCall(3).args[0][0][0]).to.eql([4.5, 9]);
+ expect(spy.getCall(4).args[0][0][0]).to.eql([-9, -4.5]);
+ expect(spy.getCall(5).args[0][0][0][0]).to.eql([-9, -4.5]);
+ expect(spy.getCall(6).args[2]).to.be(geometrycollection);
+ expect(spy.getCall(6).args[1].getCoordinates()).to.eql([45, 90]);
+ expect(spy.getCall(7).args[1].getCoordinates()[0]).to.eql([45, 90]);
+ expect(spy.getCall(8).args[1].getCoordinates()[0][0]).to.eql([-90, -45]);
+ });
});
});
diff --git a/test/spec/ol/renderer/canvas/vectortilelayer.test.js b/test/spec/ol/renderer/canvas/vectortilelayer.test.js
index 0b9664f200..69fbfa7b45 100644
--- a/test/spec/ol/renderer/canvas/vectortilelayer.test.js
+++ b/test/spec/ol/renderer/canvas/vectortilelayer.test.js
@@ -121,6 +121,18 @@ describe('ol.renderer.canvas.VectorTileLayer', function() {
spy2.restore();
});
+ it('renders replays with custom renderers as direct replays', function() {
+ layer.renderMode_ = 'image';
+ layer.setStyle(new ol.style.Style({
+ renderer: function() {}
+ }));
+ var spy = sinon.spy(ol.renderer.canvas.VectorTileLayer.prototype,
+ 'getReplayTransform_');
+ map.renderSync();
+ expect(spy.callCount).to.be(1);
+ spy.restore();
+ });
+
it('gives precedence to feature styles over layer styles', function() {
var spy = sinon.spy(map.getRenderer().getLayerRenderer(layer),
'renderFeature');