diff --git a/examples/mapbox-vector-tiles.js b/examples/mapbox-vector-tiles.js index c6a5329f90..c2262324e0 100644 --- a/examples/mapbox-vector-tiles.js +++ b/examples/mapbox-vector-tiles.js @@ -15,6 +15,7 @@ var key = 'pk.eyJ1IjoiYWhvY2V2YXIiLCJhIjoiRk1kMWZaSSJ9.E5BkluenyWQMsBLsuByrmg'; var map = new ol.Map({ layers: [ new ol.layer.VectorTile({ + declutter: true, source: new ol.source.VectorTile({ attributions: '© Mapbox ' + '© ' + diff --git a/examples/street-labels.js b/examples/street-labels.js index 1e3242b373..83cdfd25c1 100644 --- a/examples/street-labels.js +++ b/examples/street-labels.js @@ -28,6 +28,7 @@ var map = new ol.Map({ imagerySet: 'Aerial' }) }), new ol.layer.Vector({ + declutter: true, source: new ol.source.Vector({ format: new ol.format.GeoJSON(), url: 'data/geojson/vienna-streets.geojson' diff --git a/examples/vector-label-decluttering.html b/examples/vector-label-decluttering.html index cc45ca1d74..f070b32af6 100644 --- a/examples/vector-label-decluttering.html +++ b/examples/vector-label-decluttering.html @@ -4,10 +4,8 @@ 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" + Decluttering is used to avoid overlapping labels with `exceedLength: true` set on the text style. For MultiPolygon geometries, only the widest polygon is selected in a custom `geometry` function. +tags: "vector, decluttering, labels" ---
diff --git a/examples/vector-label-decluttering.js b/examples/vector-label-decluttering.js index d3c366449a..6f58b4862e 100644 --- a/examples/vector-label-decluttering.js +++ b/examples/vector-label-decluttering.js @@ -1,5 +1,3 @@ -// NOCOMPILE -/* global labelgun */ goog.require('ol.Map'); goog.require('ol.View'); goog.require('ol.extent'); @@ -9,26 +7,7 @@ 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 = {}; +goog.require('ol.style.Text'); var map = new ol.Map({ target: 'map', @@ -38,30 +17,35 @@ var map = new ol.Map({ }) }); -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 labelStyle = new ol.style.Style({ - renderer: function(coords, state) { - var text = state.feature.get('name'); - createLabel(textCache[text], text, coords); - } + geometry: function(feature) { + var geometry = feature.getGeometry(); + if (geometry.getType() == 'MultiPolygon') { + // Only render label for the widest polygon of a multipolygon + var polygons = geometry.getPolygons(); + var widest = 0; + for (var i = 0, ii = polygons.length; i < ii; ++i) { + var polygon = polygons[i]; + var width = ol.extent.getWidth(polygon.getExtent()); + if (width > widest) { + widest = width; + geometry = polygon; + } + } + } + return geometry; + }, + text: new ol.style.Text({ + font: '12px Calibri,sans-serif', + exceedLength: true, + fill: new ol.style.Fill({ + color: '#000' + }), + stroke: new ol.style.Stroke({ + color: '#fff', + width: 3 + }) + }) }); var countryStyle = new ol.style.Style({ fill: new ol.style.Fill({ @@ -72,54 +56,18 @@ var countryStyle = new ol.style.Style({ width: 1 }) }); -var styleWithLabel = [countryStyle, labelStyle]; -var styleWithoutLabel = [countryStyle]; +var style = [countryStyle, labelStyle]; -var pixelRatio; // This is set by the map's precompose listener var vectorLayer = new ol.layer.Vector({ source: new ol.source.Vector({ url: 'data/geojson/countries.geojson', format: new ol.format.GeoJSON() }), - style: function(feature, resolution) { - var text = feature.get('name'); - var width = textMeasureContext.measureText(text).width; - var geometry = feature.getGeometry(); - if (geometry.getType() == 'MultiPolygon') { - geometry = geometry.getPolygons().sort(sortByWidth)[0]; - } - var extentWidth = ol.extent.getWidth(geometry.getExtent()); - if (extentWidth / resolution > width) { - // Only consider label when it fits its geometry's extent - if (!(text in textCache)) { - // Draw the label to its own canvas and cache it. - var 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); - } - labelStyle.setGeometry(geometry.getInteriorPoint()); - return styleWithLabel; - } else { - return styleWithoutLabel; - } - } -}); -vectorLayer.on('precompose', function(e) { - pixelRatio = e.frameState.pixelRatio; - 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); - } + style: function(feature) { + labelStyle.getText().setText(feature.get('name')); + return style; + }, + declutter: true }); map.addLayer(vectorLayer); diff --git a/externs/olx.js b/externs/olx.js index 2cf800c2d4..5ed63f1848 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -4250,6 +4250,7 @@ olx.layer.TileOptions.prototype.zIndex; * renderBuffer: (number|undefined), * source: (ol.source.Vector|undefined), * map: (ol.PluggableMap|undefined), + * declutter: (boolean|undefined), * style: (ol.style.Style|Array.|ol.StyleFunction|undefined), * updateWhileAnimating: (boolean|undefined), * updateWhileInteracting: (boolean|undefined), @@ -4332,6 +4333,16 @@ olx.layer.VectorOptions.prototype.renderBuffer; olx.layer.VectorOptions.prototype.source; +/** + * Declutter images and text. Decluttering is applied to all image and text + * styles, and the priority is defined by the z-index of the style. Lower + * z-index means higher priority. Default is `false`. + * @type {boolean|undefined} + * @api + */ +olx.layer.VectorOptions.prototype.declutter; + + /** * Layer style. See {@link ol.style} for default style which will be used if * this is not defined. @@ -4389,6 +4400,7 @@ olx.layer.VectorOptions.prototype.zIndex; * renderMode: (ol.layer.VectorTileRenderType|string|undefined), * renderOrder: (ol.RenderOrderFunction|undefined), * source: (ol.source.VectorTile|undefined), + * declutter: (boolean|undefined), * style: (ol.style.Style|Array.|ol.StyleFunction|undefined), * updateWhileAnimating: (boolean|undefined), * updateWhileInteracting: (boolean|undefined), @@ -4422,7 +4434,8 @@ olx.layer.VectorTileOptions.prototype.renderBuffer; * * `'vector'`: Vector tiles are rendered as vectors. Most accurate rendering * even during animations, but slower performance than the other options. * - * The default is `'hybrid'`. + * When `declutter` is set to `true`, `'hybrid'` will be used instead of + * `'image'`. The default is `'hybrid'`. * @type {ol.layer.VectorTileRenderType|string|undefined} * @api */ @@ -4498,6 +4511,17 @@ olx.layer.VectorTileOptions.prototype.preload; olx.layer.VectorTileOptions.prototype.source; +/** + * Declutter images and text. Decluttering is applied to all image and text + * styles, and the priority is defined by the z-index of the style. Lower + * z-index means higher priority. When set to `true`, a `renderMode` of + * `'image'` will be overridden with `'hybrid'`. Default is `false`. + * @type {boolean|undefined} + * @api + */ +olx.layer.VectorTileOptions.prototype.declutter; + + /** * Layer style. See {@link ol.style} for default style which will be used if * this is not defined. diff --git a/src/ol/layer/vector.js b/src/ol/layer/vector.js index 2d315e9c92..a2e60decd8 100644 --- a/src/ol/layer/vector.js +++ b/src/ol/layer/vector.js @@ -32,6 +32,12 @@ ol.layer.Vector = function(opt_options) { delete baseOptions.updateWhileInteracting; ol.layer.Layer.call(this, /** @type {olx.layer.LayerOptions} */ (baseOptions)); + /** + * @private + * @type {boolean} + */ + this.declutter_ = options.declutter !== undefined ? options.declutter : false; + /** * @type {number} * @private @@ -80,6 +86,22 @@ ol.layer.Vector = function(opt_options) { ol.inherits(ol.layer.Vector, ol.layer.Layer); +/** + * @return {boolean} Declutter. + */ +ol.layer.Vector.prototype.getDeclutter = function() { + return this.declutter_; +}; + + +/** + * @param {boolean} declutter Declutter. + */ +ol.layer.Vector.prototype.setDeclutter = function(declutter) { + this.declutter_ = declutter; +}; + + /** * @return {number|undefined} Render buffer. */ diff --git a/src/ol/layer/vectortile.js b/src/ol/layer/vectortile.js index c7f96569ef..fdb536f554 100644 --- a/src/ol/layer/vectortile.js +++ b/src/ol/layer/vectortile.js @@ -34,17 +34,23 @@ ol.layer.VectorTile = function(opt_options) { this.setUseInterimTilesOnError(options.useInterimTilesOnError ? options.useInterimTilesOnError : true); - ol.asserts.assert(options.renderMode == undefined || - options.renderMode == ol.layer.VectorTileRenderType.IMAGE || - options.renderMode == ol.layer.VectorTileRenderType.HYBRID || - options.renderMode == ol.layer.VectorTileRenderType.VECTOR, + var renderMode = options.renderMode; + + ol.asserts.assert(renderMode == undefined || + renderMode == ol.layer.VectorTileRenderType.IMAGE || + renderMode == ol.layer.VectorTileRenderType.HYBRID || + renderMode == ol.layer.VectorTileRenderType.VECTOR, 28); // `renderMode` must be `'image'`, `'hybrid'` or `'vector'` + if (options.declutter && renderMode == ol.layer.VectorTileRenderType.IMAGE) { + renderMode = ol.layer.VectorTileRenderType.HYBRID; + } + /** * @private * @type {ol.layer.VectorTileRenderType|string} */ - this.renderMode_ = options.renderMode || ol.layer.VectorTileRenderType.HYBRID; + this.renderMode_ = renderMode || ol.layer.VectorTileRenderType.HYBRID; /** * The layer type. diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index 8384b2e952..d2ca4d8764 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -1,6 +1,9 @@ goog.provide('ol.render.canvas'); +goog.require('ol.transform'); + + /** * @const * @type {string} @@ -91,3 +94,41 @@ ol.render.canvas.rotateAtOffset = function(context, rotation, offsetX, offsetY) context.translate(-offsetX, -offsetY); } }; + + +ol.render.canvas.resetTransform_ = ol.transform.create(); + + +/** + * @param {CanvasRenderingContext2D} context Context. + * @param {ol.Transform|null} transform Transform. + * @param {number} opacity Opacity. + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} image Image. + * @param {number} originX Origin X. + * @param {number} originY Origin Y. + * @param {number} w Width. + * @param {number} h Height. + * @param {number} x X. + * @param {number} y Y. + * @param {number} scale Scale. + */ +ol.render.canvas.drawImage = function(context, + transform, opacity, image, originX, originY, w, h, x, y, scale) { + var alpha; + if (opacity != 1) { + alpha = context.globalAlpha; + context.globalAlpha = alpha * opacity; + } + if (transform) { + context.setTransform.apply(context, transform); + } + + context.drawImage(image, originX, originY, w, h, x, y, w * scale, h * scale); + + if (alpha) { + context.globalAlpha = alpha; + } + if (transform) { + context.setTransform.apply(context, ol.render.canvas.resetTransform_); + } +}; diff --git a/src/ol/render/canvas/imagereplay.js b/src/ol/render/canvas/imagereplay.js index 5df2a79264..fb021f27cc 100644 --- a/src/ol/render/canvas/imagereplay.js +++ b/src/ol/render/canvas/imagereplay.js @@ -13,10 +13,19 @@ goog.require('ol.render.canvas.Replay'); * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. * @param {boolean} overlaps The replay can have overlapping geometries. + * @param {?} declutterTree Declutter tree. * @struct */ -ol.render.canvas.ImageReplay = function(tolerance, maxExtent, resolution, pixelRatio, overlaps) { - ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution, pixelRatio, overlaps); +ol.render.canvas.ImageReplay = function( + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { + ol.render.canvas.Replay.call(this, + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree); + + /** + * @private + * @type {ol.DeclutterGroup} + */ + this.declutterGroup_ = null; /** * @private @@ -130,7 +139,7 @@ ol.render.canvas.ImageReplay.prototype.drawPoint = function(pointGeometry, featu this.instructions.push([ ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, this.image_, // Remaining arguments to DRAW_IMAGE are in alphabetical order - this.anchorX_, this.anchorY_, this.height_, this.opacity_, + this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_, this.originX_, this.originY_, this.rotateWithView_, this.rotation_, this.scale_ * this.pixelRatio, this.snapToPixel_, this.width_ ]); @@ -138,7 +147,7 @@ ol.render.canvas.ImageReplay.prototype.drawPoint = function(pointGeometry, featu ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, this.hitDetectionImage_, // Remaining arguments to DRAW_IMAGE are in alphabetical order - this.anchorX_, this.anchorY_, this.height_, this.opacity_, + this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_, this.originX_, this.originY_, this.rotateWithView_, this.rotation_, this.scale_, this.snapToPixel_, this.width_ ]); @@ -162,7 +171,7 @@ ol.render.canvas.ImageReplay.prototype.drawMultiPoint = function(multiPointGeome this.instructions.push([ ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, this.image_, // Remaining arguments to DRAW_IMAGE are in alphabetical order - this.anchorX_, this.anchorY_, this.height_, this.opacity_, + this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_, this.originX_, this.originY_, this.rotateWithView_, this.rotation_, this.scale_ * this.pixelRatio, this.snapToPixel_, this.width_ ]); @@ -170,7 +179,7 @@ ol.render.canvas.ImageReplay.prototype.drawMultiPoint = function(multiPointGeome ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, this.hitDetectionImage_, // Remaining arguments to DRAW_IMAGE are in alphabetical order - this.anchorX_, this.anchorY_, this.height_, this.opacity_, + this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_, this.originX_, this.originY_, this.rotateWithView_, this.rotation_, this.scale_, this.snapToPixel_, this.width_ ]); @@ -203,7 +212,7 @@ ol.render.canvas.ImageReplay.prototype.finish = function() { /** * @inheritDoc */ -ol.render.canvas.ImageReplay.prototype.setImageStyle = function(imageStyle) { +ol.render.canvas.ImageReplay.prototype.setImageStyle = function(imageStyle, declutterGroup) { var anchor = imageStyle.getAnchor(); var size = imageStyle.getSize(); var hitDetectionImage = imageStyle.getHitDetectionImage(1); @@ -211,6 +220,7 @@ ol.render.canvas.ImageReplay.prototype.setImageStyle = function(imageStyle) { var origin = imageStyle.getOrigin(); this.anchorX_ = anchor[0]; this.anchorY_ = anchor[1]; + this.declutterGroup_ = /** @type {ol.DeclutterGroup} */ (declutterGroup); this.hitDetectionImage_ = hitDetectionImage; this.image_ = image; this.height_ = size[1]; diff --git a/src/ol/render/canvas/linestringreplay.js b/src/ol/render/canvas/linestringreplay.js index fccaa66553..9862317eea 100644 --- a/src/ol/render/canvas/linestringreplay.js +++ b/src/ol/render/canvas/linestringreplay.js @@ -17,11 +17,13 @@ goog.require('ol.render.canvas.Replay'); * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. * @param {boolean} overlaps The replay can have overlapping geometries. + * @param {?} declutterTree Declutter tree. * @struct */ -ol.render.canvas.LineStringReplay = function(tolerance, maxExtent, resolution, pixelRatio, overlaps) { - - ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution, pixelRatio, overlaps); +ol.render.canvas.LineStringReplay = function( + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { + ol.render.canvas.Replay.call(this, + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree); /** * @private diff --git a/src/ol/render/canvas/polygonreplay.js b/src/ol/render/canvas/polygonreplay.js index 273df81e22..8d6f4192af 100644 --- a/src/ol/render/canvas/polygonreplay.js +++ b/src/ol/render/canvas/polygonreplay.js @@ -19,11 +19,13 @@ goog.require('ol.render.canvas.Replay'); * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. * @param {boolean} overlaps The replay can have overlapping geometries. + * @param {?} declutterTree Declutter tree. * @struct */ -ol.render.canvas.PolygonReplay = function(tolerance, maxExtent, resolution, pixelRatio, overlaps) { - - ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution, pixelRatio, overlaps); +ol.render.canvas.PolygonReplay = function( + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { + ol.render.canvas.Replay.call(this, + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree); /** * @private diff --git a/src/ol/render/canvas/replay.js b/src/ol/render/canvas/replay.js index 37a88ca06b..4550547397 100644 --- a/src/ol/render/canvas/replay.js +++ b/src/ol/render/canvas/replay.js @@ -12,6 +12,7 @@ goog.require('ol.geom.flat.transform'); goog.require('ol.has'); goog.require('ol.obj'); goog.require('ol.render.VectorContext'); +goog.require('ol.render.canvas'); goog.require('ol.render.canvas.Instruction'); goog.require('ol.transform'); @@ -24,11 +25,23 @@ goog.require('ol.transform'); * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. * @param {boolean} overlaps The replay can have overlapping geometries. + * @param {?} declutterTree Declutter tree. * @struct */ -ol.render.canvas.Replay = function(tolerance, maxExtent, resolution, pixelRatio, overlaps) { +ol.render.canvas.Replay = function(tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { ol.render.VectorContext.call(this); + /** + * @type {?} + */ + this.declutterTree = declutterTree; + + /** + * @private + * @type {ol.Extent} + */ + this.tmpExtent_ = ol.extent.createEmpty(); + /** * @protected * @type {number} @@ -127,12 +140,6 @@ ol.render.canvas.Replay = function(tolerance, maxExtent, resolution, pixelRatio, */ this.tmpLocalTransform_ = ol.transform.create(); - /** - * @private - * @type {!ol.Transform} - */ - this.resetTransform_ = ol.transform.create(); - /** * @private * @type {Array.>} @@ -149,6 +156,7 @@ ol.inherits(ol.render.canvas.Replay, ol.render.VectorContext); * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} image Image. * @param {number} anchorX Anchor X. * @param {number} anchorY Anchor Y. + * @param {ol.DeclutterGroup} declutterGroup Declutter group. * @param {number} height Height. * @param {number} opacity Opacity. * @param {number} originX Origin X. @@ -159,7 +167,7 @@ ol.inherits(ol.render.canvas.Replay, ol.render.VectorContext); * @param {number} width Width. */ ol.render.canvas.Replay.prototype.replayImage_ = function(context, x, y, image, anchorX, anchorY, - height, opacity, originX, originY, rotation, scale, snapToPixel, width) { + declutterGroup, height, opacity, originX, originY, rotation, scale, snapToPixel, width) { var localTransform = this.tmpLocalTransform_; anchorX *= scale; anchorY *= scale; @@ -169,28 +177,37 @@ ol.render.canvas.Replay.prototype.replayImage_ = function(context, x, y, image, x = Math.round(x); y = Math.round(y); } - if (rotation !== 0) { - var centerX = x + anchorX; - var centerY = y + anchorY; - ol.transform.compose(localTransform, - centerX, centerY, 1, 1, rotation, -centerX, -centerY); - context.setTransform.apply(context, localTransform); - } - var alpha = context.globalAlpha; - if (opacity != 1) { - context.globalAlpha = alpha * opacity; - } var w = (width + originX > image.width) ? image.width - originX : width; var h = (height + originY > image.height) ? image.height - originY : height; + var box = this.tmpExtent_; - context.drawImage(image, originX, originY, w, h, x, y, w * scale, h * scale); - - if (opacity != 1) { - context.globalAlpha = alpha; - } + var transform = null; if (rotation !== 0) { - context.setTransform.apply(context, this.resetTransform_); + var centerX = x + anchorX; + var centerY = y + anchorY; + transform = ol.transform.compose(localTransform, + centerX, centerY, 1, 1, rotation, -centerX, -centerY); + ol.extent.createOrUpdateEmpty(box); + ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, [x, y])); + ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, [x + w, y])); + ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, [x + w, y + h])); + ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, [x, y + w])); + } else { + ol.extent.createOrUpdate(x, y, x + w * scale, y + h * scale, box); + } + var canvas = context.canvas; + var intersects = box[0] <= canvas.width && box[2] >= 0 && box[1] <= canvas.height && box[3] >= 0; + if (declutterGroup) { + if (!intersects && declutterGroup[4] == 1) { + return; + } + ol.extent.extend(declutterGroup, box); + declutterGroup.push(intersects ? + [context, transform ? transform.slice(0) : null, opacity, image, originX, originY, w, h, x, y, scale] : + null); + } else if (intersects) { + ol.render.canvas.drawImage(context, transform, opacity, image, originX, originY, w, h, x, y, scale); } }; @@ -360,7 +377,37 @@ ol.render.canvas.Replay.prototype.fill_ = function(context, rotation) { } context.fill(); if (this.fillOrigin_) { - context.setTransform.apply(context, this.resetTransform_); + context.setTransform.apply(context, ol.render.canvas.resetTransform_); + } +}; + + +/** + * @param {ol.DeclutterGroup} declutterGroup Declutter group. + */ +ol.render.canvas.Replay.prototype.renderDeclutter_ = function(declutterGroup) { + if (declutterGroup && declutterGroup.length > 5) { + var groupCount = declutterGroup[4]; + if (groupCount == 1 || groupCount == declutterGroup.length - 5) { + /** @type {ol.RBushEntry} */ + var box = { + minX: /** @type {number} */ (declutterGroup[0]), + minY: /** @type {number} */ (declutterGroup[1]), + maxX: /** @type {number} */ (declutterGroup[2]), + maxY: /** @type {number} */ (declutterGroup[3]) + }; + if (!this.declutterTree.collides(box)) { + this.declutterTree.insert(box); + var drawImage = ol.render.canvas.drawImage; + for (var j = 5, jj = declutterGroup.length; j < jj; ++j) { + if (declutterGroup[j]) { + drawImage.apply(undefined, /** @type {Array} */ (declutterGroup[j])); + } + } + } + declutterGroup.length = 5; + ol.extent.createOrUpdateEmpty(declutterGroup); + } } }; @@ -401,7 +448,7 @@ ol.render.canvas.Replay.prototype.replay_ = function( var ii = instructions.length; // end of instructions var d = 0; // data index var dd; // end of per-instruction data - var anchorX, anchorY, prevX, prevY, roundX, roundY; + var anchorX, anchorY, prevX, prevY, roundX, roundY, declutterGroup; var pendingFill = 0; var pendingStroke = 0; var coordinateCache = this.coordinateCache_; @@ -497,24 +544,26 @@ ol.render.canvas.Replay.prototype.replay_ = function( // Remaining arguments in DRAW_IMAGE are in alphabetical order anchorX = /** @type {number} */ (instruction[4]); anchorY = /** @type {number} */ (instruction[5]); - var height = /** @type {number} */ (instruction[6]); - var opacity = /** @type {number} */ (instruction[7]); - var originX = /** @type {number} */ (instruction[8]); - var originY = /** @type {number} */ (instruction[9]); - var rotateWithView = /** @type {boolean} */ (instruction[10]); - var rotation = /** @type {number} */ (instruction[11]); - var scale = /** @type {number} */ (instruction[12]); - var snapToPixel = /** @type {boolean} */ (instruction[13]); - var width = /** @type {number} */ (instruction[14]); + declutterGroup = /** @type {ol.DeclutterGroup} */ (instruction[6]); + var height = /** @type {number} */ (instruction[7]); + var opacity = /** @type {number} */ (instruction[8]); + var originX = /** @type {number} */ (instruction[9]); + var originY = /** @type {number} */ (instruction[10]); + var rotateWithView = /** @type {boolean} */ (instruction[11]); + var rotation = /** @type {number} */ (instruction[12]); + var scale = /** @type {number} */ (instruction[13]); + var snapToPixel = /** @type {boolean} */ (instruction[14]); + var width = /** @type {number} */ (instruction[15]); if (rotateWithView) { rotation += viewRotation; } for (; d < dd; d += 2) { - this.replayImage_(context, pixelCoordinates[d], pixelCoordinates[d + 1], - image, anchorX, anchorY, height, opacity, originX, originY, - rotation, scale, snapToPixel, width); + this.replayImage_(context, + pixelCoordinates[d], pixelCoordinates[d + 1], image, anchorX, anchorY, + declutterGroup, height, opacity, originX, originY, rotation, scale, snapToPixel, width); } + this.renderDeclutter_(declutterGroup); ++i; break; case ol.render.canvas.Instruction.DRAW_CHARS: @@ -523,13 +572,14 @@ ol.render.canvas.Replay.prototype.replay_ = function( var images = /** @type {Array.} */ (instruction[3]); // Remaining arguments in DRAW_CHARS are in alphabetical order var baseline = /** @type {number} */ (instruction[4]); - var exceedLength = /** @type {number} */ (instruction[5]); - var maxAngle = /** @type {number} */ (instruction[6]); - var measure = /** @type {function(string):number} */ (instruction[7]); - var offsetY = /** @type {number} */ (instruction[8]); - var text = /** @type {string} */ (instruction[9]); - var align = /** @type {number} */ (instruction[10]); - var textScale = /** @type {number} */ (instruction[11]); + declutterGroup = /** @type {ol.DeclutterGroup} */ (instruction[5]); + var exceedLength = /** @type {number} */ (instruction[6]); + var maxAngle = /** @type {number} */ (instruction[7]); + var measure = /** @type {function(string):number} */ (instruction[8]); + var offsetY = /** @type {number} */ (instruction[9]); + var text = /** @type {string} */ (instruction[10]); + var align = /** @type {number} */ (instruction[11]); + var textScale = /** @type {number} */ (instruction[12]); var pathLength = ol.geom.flat.length.lineString(pixelCoordinates, begin, end, 2); var textLength = measure(text); @@ -545,12 +595,12 @@ ol.render.canvas.Replay.prototype.replay_ = function( var label = images[c]; anchorX = label.width / 2; anchorY = baseline * label.height + (0.5 - baseline) * (label.height - fillHeight) - offsetY; - this.replayImage_(context, char[0], char[1], label, anchorX, anchorY, - label.height, 1, 0, 0, char[2], textScale, false, label.width); + this.replayImage_(context, char[0], char[1], label, + anchorX, anchorY, declutterGroup, label.height, 1, 0, 0, char[2], textScale, false, label.width); } } } - + this.renderDeclutter_(declutterGroup); ++i; break; case ol.render.canvas.Instruction.END_GEOMETRY: diff --git a/src/ol/render/canvas/replaygroup.js b/src/ol/render/canvas/replaygroup.js index c110ddb8b7..c3ec7b4666 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.ReplayType'); goog.require('ol.render.canvas.Replay'); goog.require('ol.render.canvas.ImageReplay'); goog.require('ol.render.canvas.LineStringReplay'); @@ -24,13 +25,27 @@ goog.require('ol.transform'); * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. * @param {boolean} overlaps The replay group can have overlapping geometries. + * @param {?} declutterTree Declutter tree + * for declutter processing in postrender. * @param {number=} opt_renderBuffer Optional rendering buffer. * @struct */ ol.render.canvas.ReplayGroup = function( - tolerance, maxExtent, resolution, pixelRatio, overlaps, opt_renderBuffer) { + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree, opt_renderBuffer) { ol.render.ReplayGroup.call(this); + /** + * Declutter tree. + * @private + */ + this.declutterTree_ = declutterTree; + + /** + * @type {ol.DeclutterGroup} + * @private + */ + this.declutterGroup_ = null; + /** * @private * @type {number} @@ -170,6 +185,71 @@ ol.render.canvas.ReplayGroup.getCircleArray_ = function(radius) { }; +ol.render.canvas.ReplayGroup.replayDeclutter = function(declutterReplays, context, rotation) { + var zs = Object.keys(declutterReplays).map(Number).sort(ol.array.numberSafeCompareFunction); + for (var z = 0, zz = zs.length; z < zz; ++z) { + var replayData = declutterReplays[zs[z].toString()]; + for (var i = 0, ii = replayData.length; i < ii;) { + var replay = replayData[i++]; + var transform = replayData[i++]; + replay.replay(context, transform, rotation, {}); + } + } +}; + + +ol.render.canvas.ReplayGroup.replayDeclutterHitDetection = function( + declutterReplays, context, rotation, featureCallback, hitExtent) { + var zs = Object.keys(declutterReplays).map(Number).sort(ol.array.numberSafeCompareFunction); + for (var z = 0, zz = zs.length; z < zz; ++z) { + var replayData = declutterReplays[zs[z].toString()]; + for (var i = replayData.length - 1; i >= 0;) { + var transform = replayData[i--]; + var replay = replayData[i--]; + var result = replay.replayHitDetection(context, transform, rotation, {}, + featureCallback, hitExtent); + if (result) { + return result; + } + } + } +}; + + +/** + * @param {boolean} group Group with previous replay. + * @return {ol.DeclutterGroup} Declutter instruction group. + */ +ol.render.canvas.ReplayGroup.prototype.addDeclutter = function(group) { + var declutter = null; + if (this.declutterTree_) { + if (group) { + declutter = this.declutterGroup_; + /** @type {number} */ (declutter[4])++; + } else { + declutter = this.declutterGroup_ = ol.extent.createEmpty(); + declutter.push(1); + } + } + return declutter; +}; + + +/** + * @param {CanvasRenderingContext2D} context Context. + * @param {ol.Transform} transform Transform. + */ +ol.render.canvas.ReplayGroup.prototype.clip = function(context, transform) { + var flatClipCoords = this.getClipCoords(transform); + context.beginPath(); + context.moveTo(flatClipCoords[0], flatClipCoords[1]); + context.lineTo(flatClipCoords[2], flatClipCoords[3]); + context.lineTo(flatClipCoords[4], flatClipCoords[5]); + context.lineTo(flatClipCoords[6], flatClipCoords[7]); + context.clip(); +}; + + /** * @param {Array.} replays Replays. * @return {boolean} Has replays of the provided types. @@ -211,11 +291,13 @@ ol.render.canvas.ReplayGroup.prototype.finish = function() { * to skip. * @param {function((ol.Feature|ol.render.Feature)): T} callback Feature * callback. + * @param {Object.} declutterReplays Declutter + * replays. * @return {T|undefined} Callback result. * @template T */ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( - coordinate, resolution, rotation, hitTolerance, skippedFeaturesHash, callback) { + coordinate, resolution, rotation, hitTolerance, skippedFeaturesHash, callback, declutterReplays) { hitTolerance = Math.round(hitTolerance); var contextSize = hitTolerance * 2 + 1; @@ -245,30 +327,36 @@ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( var mask = ol.render.canvas.ReplayGroup.getCircleArray_(hitTolerance); - return this.replayHitDetection_(context, transform, rotation, - skippedFeaturesHash, - /** - * @param {ol.Feature|ol.render.Feature} feature Feature. - * @return {?} Callback result. - */ - function(feature) { - var imageData = context.getImageData(0, 0, contextSize, contextSize).data; - for (var i = 0; i < contextSize; i++) { - for (var j = 0; j < contextSize; j++) { - if (mask[i][j]) { - if (imageData[(j * contextSize + i) * 4 + 3] > 0) { - var result = callback(feature); - if (result) { - return result; - } else { - context.clearRect(0, 0, contextSize, contextSize); - return undefined; - } - } + /** + * @param {ol.Feature|ol.render.Feature} feature Feature. + * @return {?} Callback result. + */ + function hitDetectionCallback(feature) { + var imageData = context.getImageData(0, 0, contextSize, contextSize).data; + for (var i = 0; i < contextSize; i++) { + for (var j = 0; j < contextSize; j++) { + if (mask[i][j]) { + if (imageData[(j * contextSize + i) * 4 + 3] > 0) { + var result = callback(feature); + if (result) { + return result; + } else { + context.clearRect(0, 0, contextSize, contextSize); + return undefined; } } } - }, hitExtent); + } + } + } + + var result = this.replayHitDetection_(context, transform, rotation, + skippedFeaturesHash, hitDetectionCallback, hitExtent, declutterReplays); + if (!result && declutterReplays) { + result = ol.render.canvas.ReplayGroup.replayDeclutterHitDetection( + declutterReplays, context, rotation, hitDetectionCallback, hitExtent); + } + return result; }; @@ -303,13 +391,21 @@ ol.render.canvas.ReplayGroup.prototype.getReplay = function(zIndex, replayType) if (replay === undefined) { var Constructor = ol.render.canvas.ReplayGroup.BATCH_CONSTRUCTORS_[replayType]; replay = new Constructor(this.tolerance_, this.maxExtent_, - this.resolution_, this.pixelRatio_, this.overlaps_); + this.resolution_, this.pixelRatio_, this.overlaps_, this.declutterTree_); replays[replayType] = replay; } return replay; }; +/** + * @return {Object.>} Replays. + */ +ol.render.canvas.ReplayGroup.prototype.getReplays = function() { + return this.replaysByZIndex_; +}; + + /** * @inheritDoc */ @@ -326,9 +422,11 @@ ol.render.canvas.ReplayGroup.prototype.isEmpty = function() { * to skip. * @param {Array.=} opt_replayTypes Ordered replay types * to replay. Default is {@link ol.render.replay.ORDER} + * @param {Object.=} opt_declutterReplays Declutter + * replays. */ ol.render.canvas.ReplayGroup.prototype.replay = function(context, - transform, viewRotation, skippedFeaturesHash, opt_replayTypes) { + transform, viewRotation, skippedFeaturesHash, opt_replayTypes, opt_declutterReplays) { /** @type {Array.} */ var zs = Object.keys(this.replaysByZIndex_).map(Number); @@ -336,23 +434,29 @@ ol.render.canvas.ReplayGroup.prototype.replay = function(context, // setup clipping so that the parts of over-simplified geometries are not // visible outside the current extent when panning - var flatClipCoords = this.getClipCoords(transform); context.save(); - context.beginPath(); - context.moveTo(flatClipCoords[0], flatClipCoords[1]); - context.lineTo(flatClipCoords[2], flatClipCoords[3]); - context.lineTo(flatClipCoords[4], flatClipCoords[5]); - context.lineTo(flatClipCoords[6], flatClipCoords[7]); - context.clip(); + this.clip(context, transform); var replayTypes = opt_replayTypes ? opt_replayTypes : ol.render.replay.ORDER; var i, ii, j, jj, replays, replay; for (i = 0, ii = zs.length; i < ii; ++i) { - replays = this.replaysByZIndex_[zs[i].toString()]; + var zIndexKey = zs[i].toString(); + replays = this.replaysByZIndex_[zIndexKey]; for (j = 0, jj = replayTypes.length; j < jj; ++j) { - replay = replays[replayTypes[j]]; + var replayType = replayTypes[j]; + replay = replays[replayType]; if (replay !== undefined) { - replay.replay(context, transform, viewRotation, skippedFeaturesHash); + if (opt_declutterReplays && + (replayType == ol.render.ReplayType.IMAGE || replayType == ol.render.ReplayType.TEXT)) { + var declutter = opt_declutterReplays[zIndexKey]; + if (!declutter) { + opt_declutterReplays[zIndexKey] = [replay, transform.slice(0)]; + } else { + declutter.push(replay, transform.slice(0)); + } + } else { + replay.replay(context, transform, viewRotation, skippedFeaturesHash); + } } } } @@ -372,28 +476,40 @@ ol.render.canvas.ReplayGroup.prototype.replay = function(context, * Feature callback. * @param {ol.Extent=} opt_hitExtent Only check features that intersect this * extent. + * @param {Object.=} opt_declutterReplays Declutter + * replays. * @return {T|undefined} Callback result. * @template T */ ol.render.canvas.ReplayGroup.prototype.replayHitDetection_ = function( context, transform, viewRotation, skippedFeaturesHash, - featureCallback, opt_hitExtent) { + featureCallback, opt_hitExtent, opt_declutterReplays) { /** @type {Array.} */ var zs = Object.keys(this.replaysByZIndex_).map(Number); - zs.sort(function(a, b) { - return b - a; - }); + zs.sort(ol.array.numberSafeCompareFunction); - var i, ii, j, replays, replay, result; - for (i = 0, ii = zs.length; i < ii; ++i) { - replays = this.replaysByZIndex_[zs[i].toString()]; + var i, j, replays, replay, result; + for (i = zs.length - 1; i >= 0; --i) { + var zIndexKey = zs[i].toString(); + replays = this.replaysByZIndex_[zIndexKey]; for (j = ol.render.replay.ORDER.length - 1; j >= 0; --j) { - replay = replays[ol.render.replay.ORDER[j]]; + var replayType = ol.render.replay.ORDER[j]; + replay = replays[replayType]; if (replay !== undefined) { - result = replay.replayHitDetection(context, transform, viewRotation, - skippedFeaturesHash, featureCallback, opt_hitExtent); - if (result) { - return result; + if (opt_declutterReplays && + (replayType == ol.render.ReplayType.IMAGE || replayType == ol.render.ReplayType.TEXT)) { + var declutter = opt_declutterReplays[zIndexKey]; + if (!declutter) { + opt_declutterReplays[zIndexKey] = [replay, transform.slice(0)]; + } else { + declutter.push(replay, transform.slice(0)); + } + } else { + result = replay.replayHitDetection(context, transform, viewRotation, + skippedFeaturesHash, featureCallback, opt_hitExtent); + if (result) { + return result; + } } } } @@ -407,7 +523,7 @@ ol.render.canvas.ReplayGroup.prototype.replayHitDetection_ = function( * @private * @type {Object.} + * number, number, boolean, Array.)>} */ ol.render.canvas.ReplayGroup.BATCH_CONSTRUCTORS_ = { 'Circle': ol.render.canvas.PolygonReplay, diff --git a/src/ol/render/canvas/textreplay.js b/src/ol/render/canvas/textreplay.js index fc1dea771b..5220b4a3d5 100644 --- a/src/ol/render/canvas/textreplay.js +++ b/src/ol/render/canvas/textreplay.js @@ -3,6 +3,7 @@ goog.provide('ol.render.canvas.TextReplay'); goog.require('ol'); goog.require('ol.colorlike'); goog.require('ol.dom'); +goog.require('ol.extent'); goog.require('ol.geom.flat.straightchunk'); goog.require('ol.geom.GeometryType'); goog.require('ol.has'); @@ -22,11 +23,19 @@ goog.require('ol.style.TextPlacement'); * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. * @param {boolean} overlaps The replay can have overlapping geometries. + * @param {?} declutterTree Declutter tree. * @struct */ -ol.render.canvas.TextReplay = function(tolerance, maxExtent, resolution, pixelRatio, overlaps) { +ol.render.canvas.TextReplay = function( + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { + ol.render.canvas.Replay.call(this, + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree); - ol.render.canvas.Replay.call(this, tolerance, maxExtent, resolution, pixelRatio, overlaps); + /** + * @private + * @type {ol.DeclutterGroup} + */ + this.declutterGroup_; /** * @private @@ -127,18 +136,22 @@ ol.render.canvas.TextReplay.labelCache_ = new ol.structs.LRUCache(); */ ol.render.canvas.TextReplay.measureTextHeight = (function() { var span; - return function(font, lines, widths) { - if (!span) { - span = document.createElement('span'); - span.textContent = 'M'; - span.style.margin = span.style.padding = '0 !important'; - span.style.position = 'absolute !important'; - span.style.left = '-99999px !important'; + var heights = {}; + return function(font) { + var height = heights[font]; + if (height == undefined) { + if (!span) { + span = document.createElement('span'); + span.textContent = 'M'; + span.style.margin = span.style.padding = '0 !important'; + span.style.position = 'absolute !important'; + span.style.left = '-99999px !important'; + } + span.style.font = font; + document.body.appendChild(span); + height = heights[font] = span.offsetHeight; + document.body.removeChild(span); } - span.style.font = font; - document.body.appendChild(span); - var height = span.offsetHeight; - document.body.removeChild(span); return height; }; })(); @@ -207,6 +220,9 @@ ol.render.canvas.TextReplay.prototype.drawText = function(geometry, feature) { var i, ii; if (this.textState_.placement === ol.style.TextPlacement.LINE) { + if (!ol.extent.intersects(this.getBufferedMaxExtent(), geometry.getExtent())) { + return; + } var ends; flatCoordinates = geometry.getFlatCoordinates(); stride = geometry.getStride(); @@ -236,9 +252,12 @@ ol.render.canvas.TextReplay.prototype.drawText = function(geometry, feature) { } else { flatEnd = ends[o]; } - end = this.appendFlatCoordinates(flatCoordinates, flatOffset, flatEnd, stride, false, false); + for (i = flatOffset; i < flatEnd; i += stride) { + this.coordinates.push(flatCoordinates[i], flatCoordinates[i + 1]); + } + end = this.coordinates.length; flatOffset = ends[o]; - this.drawChars_(begin, end); + this.drawChars_(begin, end, this.declutterGroup_); begin = end; } this.endGeometry(geometry, feature); @@ -303,8 +322,6 @@ ol.render.canvas.TextReplay.prototype.getImage_ = function(text, fill, stroke) { var label; var key = (stroke ? this.strokeKey_ : '') + this.textKey_ + text + (fill ? this.fillKey_ : ''); - var lines = text.split('\n'); - var numLines = lines.length; if (!ol.render.canvas.TextReplay.labelCache_.containsKey(key)) { var strokeState = this.textStrokeState_; var fillState = this.textFillState_; @@ -314,6 +331,8 @@ ol.render.canvas.TextReplay.prototype.getImage_ = function(text, fill, stroke) { var align = ol.render.replay.TEXT_ALIGN[textState.textAlign || ol.render.canvas.defaultTextAlign]; var strokeWidth = stroke && strokeState.lineWidth ? strokeState.lineWidth : 0; + var lines = text.split('\n'); + var numLines = lines.length; var widths = []; var width = ol.render.canvas.TextReplay.measureTextWidths(textState.font, lines, widths); var lineHeight = ol.render.canvas.TextReplay.measureTextHeight(textState.font); @@ -332,7 +351,7 @@ ol.render.canvas.TextReplay.prototype.getImage_ = function(text, fill, stroke) { context.lineCap = strokeState.lineCap; context.lineJoin = strokeState.lineJoin; context.miterLimit = strokeState.miterLimit; - if (ol.has.CANVAS_LINE_DASH) { + if (ol.has.CANVAS_LINE_DASH && strokeState.lineDash.length) { context.setLineDash(strokeState.lineDash); context.lineDashOffset = strokeState.lineDashOffset; } @@ -378,12 +397,12 @@ ol.render.canvas.TextReplay.prototype.drawTextImage_ = function(label, begin, en var anchorY = baseline * label.height / pixelRatio + 2 * (0.5 - baseline) * strokeWidth; this.instructions.push([ol.render.canvas.Instruction.DRAW_IMAGE, begin, end, label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio, - label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_, + this.declutterGroup_, label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_, 1, true, label.width ]); this.hitDetectionInstructions.push([ol.render.canvas.Instruction.DRAW_IMAGE, begin, end, label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio, - label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_, + this.declutterGroup_, label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_, 1 / pixelRatio, true, label.width ]); }; @@ -393,8 +412,9 @@ ol.render.canvas.TextReplay.prototype.drawTextImage_ = function(label, begin, en * @private * @param {number} begin Begin. * @param {number} end End. + * @param {ol.DeclutterGroup} declutterGroup Declutter group. */ -ol.render.canvas.TextReplay.prototype.drawChars_ = function(begin, end) { +ol.render.canvas.TextReplay.prototype.drawChars_ = function(begin, end, declutterGroup) { var pixelRatio = this.pixelRatio; var strokeState = this.textStrokeState_; var fill = !!this.textFillState_; @@ -423,13 +443,13 @@ ol.render.canvas.TextReplay.prototype.drawChars_ = function(begin, end) { var align = ol.render.replay.TEXT_ALIGN[textState.textAlign || ol.render.canvas.defaultTextAlign]; var widths = {}; this.instructions.push([ol.render.canvas.Instruction.DRAW_CHARS, - begin, end, labels, baseline, + begin, end, labels, baseline, declutterGroup, textState.exceedLength, textState.maxAngle, ol.render.canvas.TextReplay.getTextWidth.bind(widths, context, pixelRatio * this.textScale_), offsetY, this.text_, align, 1 ]); this.hitDetectionInstructions.push([ol.render.canvas.Instruction.DRAW_CHARS, - begin, end, labels, baseline, + begin, end, labels, baseline, declutterGroup, textState.exceedLength, textState.maxAngle, ol.render.canvas.TextReplay.getTextWidth.bind(widths, context, this.textScale_), offsetY, this.text_, align, 1 / pixelRatio @@ -440,11 +460,12 @@ ol.render.canvas.TextReplay.prototype.drawChars_ = function(begin, end) { /** * @inheritDoc */ -ol.render.canvas.TextReplay.prototype.setTextStyle = function(textStyle) { +ol.render.canvas.TextReplay.prototype.setTextStyle = function(textStyle, declutterGroup) { var textState, fillState, strokeState; if (!textStyle) { this.text_ = ''; } else { + this.declutterGroup_ = /** @type {ol.DeclutterGroup} */ (declutterGroup); var textFillStyle = textStyle.getFill(); if (!textFillStyle) { fillState = this.textFillState_ = null; diff --git a/src/ol/render/feature.js b/src/ol/render/feature.js index d45e085111..a3d187fc1a 100644 --- a/src/ol/render/feature.js +++ b/src/ol/render/feature.js @@ -1,8 +1,12 @@ goog.provide('ol.render.Feature'); goog.require('ol'); +goog.require('ol.array'); goog.require('ol.extent'); goog.require('ol.geom.GeometryType'); +goog.require('ol.geom.flat.center'); +goog.require('ol.geom.flat.interiorpoint'); +goog.require('ol.geom.flat.interpolate'); goog.require('ol.geom.flat.transform'); goog.require('ol.transform'); @@ -45,6 +49,18 @@ ol.render.Feature = function(type, flatCoordinates, ends, properties, id) { */ this.flatCoordinates_ = flatCoordinates; + /** + * @private + * @type {Array.} + */ + this.flatInteriorPoints_ = null; + + /** + * @private + * @type {Array.} + */ + this.flatMidpoints_ = null; + /** * @private * @type {Array.|Array.>} @@ -102,6 +118,66 @@ ol.render.Feature.prototype.getExtent = function() { return this.extent_; }; + +/** + * @return {Array.} Flat interior points. + */ +ol.render.Feature.prototype.getFlatInteriorPoint = function() { + if (!this.flatInteriorPoints_) { + var flatCenter = ol.extent.getCenter(this.getExtent()); + this.flatInteriorPoints_ = ol.geom.flat.interiorpoint.linearRings( + this.flatCoordinates_, 0, this.ends_, 2, flatCenter, 0); + } + return this.flatInteriorPoints_; +}; + + +/** + * @return {Array.} Flat interior points. + */ +ol.render.Feature.prototype.getFlatInteriorPoints = function() { + if (!this.flatInteriorPoints_) { + var flatCenters = ol.geom.flat.center.linearRingss( + this.flatCoordinates_, 0, this.ends_, 2); + this.flatInteriorPoints_ = ol.geom.flat.interiorpoint.linearRingss( + this.flatCoordinates_, 0, this.ends_, 2, flatCenters); + } + return this.flatInteriorPoints_; +}; + + +/** + * @return {Array.} Flat midpoint. + */ +ol.render.Feature.prototype.getFlatMidpoint = function() { + if (!this.flatMidpoints_) { + this.flatMidpoints_ = ol.geom.flat.interpolate.lineString( + this.flatCoordinates_, 0, this.flatCoordinates_.length, 2, 0.5); + } + return this.flatMidpoints_; +}; + + +/** + * @return {Array.} Flat midpoints. + */ +ol.render.Feature.prototype.getFlatMidpoints = function() { + if (!this.flatMidpoints_) { + this.flatMidpoints_ = []; + var flatCoordinates = this.flatCoordinates_; + var offset = 0; + var ends = this.ends_; + for (var i = 0, ii = ends.length; i < ii; ++i) { + var end = ends[i]; + var midpoint = ol.geom.flat.interpolate.lineString( + flatCoordinates, offset, end, 2, 0.5); + ol.array.extend(this.flatMidpoints_, midpoint); + offset = end; + } + } + return this.flatMidpoints_; +}; + /** * Get the feature identifier. This is a stable identifier for the feature and * is set when reading data from a remote source. diff --git a/src/ol/render/vectorcontext.js b/src/ol/render/vectorcontext.js index 1ea450cd4c..bfeab295e7 100644 --- a/src/ol/render/vectorcontext.js +++ b/src/ol/render/vectorcontext.js @@ -123,11 +123,13 @@ ol.render.VectorContext.prototype.setFillStrokeStyle = function(fillStyle, strok /** * @param {ol.style.Image} imageStyle Image style. + * @param {ol.DeclutterGroup=} opt_declutterGroup Declutter. */ -ol.render.VectorContext.prototype.setImageStyle = function(imageStyle) {}; +ol.render.VectorContext.prototype.setImageStyle = function(imageStyle, opt_declutterGroup) {}; /** * @param {ol.style.Text} textStyle Text style. + * @param {ol.DeclutterGroup=} opt_declutterGroup Declutter. */ -ol.render.VectorContext.prototype.setTextStyle = function(textStyle) {}; +ol.render.VectorContext.prototype.setTextStyle = function(textStyle, opt_declutterGroup) {}; diff --git a/src/ol/render/webgl/replaygroup.js b/src/ol/render/webgl/replaygroup.js index 98c4f74191..0d29d5dcb1 100644 --- a/src/ol/render/webgl/replaygroup.js +++ b/src/ol/render/webgl/replaygroup.js @@ -53,6 +53,13 @@ ol.render.webgl.ReplayGroup = function(tolerance, maxExtent, opt_renderBuffer) { ol.inherits(ol.render.webgl.ReplayGroup, ol.render.ReplayGroup); +/** + * @param {ol.style.Style} style Style. + * @param {boolean} group Group with previous replay. + */ +ol.render.webgl.ReplayGroup.prototype.addDeclutter = function(style, group) {}; + + /** * @param {ol.webgl.Context} context WebGL context. * @return {function()} Delete resources function. diff --git a/src/ol/renderer/canvas/vectorlayer.js b/src/ol/renderer/canvas/vectorlayer.js index a50421a5f0..dc25ba9dac 100644 --- a/src/ol/renderer/canvas/vectorlayer.js +++ b/src/ol/renderer/canvas/vectorlayer.js @@ -4,6 +4,7 @@ goog.require('ol'); goog.require('ol.LayerType'); goog.require('ol.ViewHint'); goog.require('ol.dom'); +goog.require('ol.ext.rbush'); goog.require('ol.extent'); goog.require('ol.render.EventType'); goog.require('ol.render.canvas'); @@ -23,6 +24,13 @@ ol.renderer.canvas.VectorLayer = function(vectorLayer) { ol.renderer.canvas.Layer.call(this, vectorLayer); + /** + * Declutter tree. + * @private + */ + this.declutterTree_ = vectorLayer.getDeclutter() ? + ol.ext.rbush(9) : null; + /** * @private * @type {boolean} @@ -209,6 +217,9 @@ ol.renderer.canvas.VectorLayer.prototype.composeFrame = function(frameState, lay if (clipped) { context.restore(); } + if (this.declutterTree_) { + this.declutterTree_.clear(); + } this.postCompose(context, frameState, layerState, transform); }; @@ -223,10 +234,11 @@ ol.renderer.canvas.VectorLayer.prototype.forEachFeatureAtCoordinate = function(c } else { var resolution = frameState.viewState.resolution; var rotation = frameState.viewState.rotation; - var layer = this.getLayer(); + var layer = /** @type {ol.layer.Vector} */ (this.getLayer()); /** @type {Object.} */ var features = {}; - return this.replayGroup_.forEachFeatureAtCoordinate(coordinate, resolution, + var declutterReplays = layer.getDeclutter() ? {} : null; + var result = this.replayGroup_.forEachFeatureAtCoordinate(coordinate, resolution, rotation, hitTolerance, {}, /** * @param {ol.Feature|ol.render.Feature} feature Feature. @@ -238,7 +250,11 @@ ol.renderer.canvas.VectorLayer.prototype.forEachFeatureAtCoordinate = function(c features[key] = true; return callback.call(thisArg, feature, layer); } - }); + }, declutterReplays); + if (this.declutterTree_) { + this.declutterTree_.clear(); + } + return result; } }; @@ -317,7 +333,7 @@ ol.renderer.canvas.VectorLayer.prototype.prepareFrame = function(frameState, lay var replayGroup = new ol.render.canvas.ReplayGroup( ol.renderer.vector.getTolerance(resolution, pixelRatio), extent, resolution, - pixelRatio, vectorSource.getOverlaps(), vectorLayer.getRenderBuffer()); + pixelRatio, vectorSource.getOverlaps(), this.declutterTree_, vectorLayer.getRenderBuffer()); vectorSource.loadFeatures(extent, resolution, projection); /** * @param {ol.Feature} feature Feature. diff --git a/src/ol/renderer/canvas/vectortilelayer.js b/src/ol/renderer/canvas/vectortilelayer.js index 99af6a4bee..cdf2186d18 100644 --- a/src/ol/renderer/canvas/vectortilelayer.js +++ b/src/ol/renderer/canvas/vectortilelayer.js @@ -4,6 +4,7 @@ goog.require('ol'); goog.require('ol.LayerType'); goog.require('ol.TileState'); goog.require('ol.dom'); +goog.require('ol.ext.rbush'); goog.require('ol.extent'); goog.require('ol.layer.VectorTileRenderType'); goog.require('ol.proj'); @@ -33,6 +34,12 @@ ol.renderer.canvas.VectorTileLayer = function(layer) { ol.renderer.canvas.TileLayer.call(this, layer); + /** + * Declutter tree. + * @private + */ + this.declutterTree_ = layer.getDeclutter() ? ol.ext.rbush(9) : null; + /** * @private * @type {boolean} @@ -54,7 +61,6 @@ ol.renderer.canvas.VectorTileLayer = function(layer) { // Use lower resolution for pure vector rendering. Closest resolution otherwise. this.zDirection = layer.getRenderMode() == ol.layer.VectorTileRenderType.VECTOR ? 1 : 0; - }; ol.inherits(ol.renderer.canvas.VectorTileLayer, ol.renderer.canvas.TileLayer); @@ -150,6 +156,7 @@ ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup_ = function( var resolution = tileGrid.getResolution(tile.tileCoord[0]); var tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord); + var zIndexKeys = {}; for (var t = 0, tt = tile.tileKeys.length; t < tt; ++t) { var sourceTile = tile.getTile(tile.tileKeys[t]); if (sourceTile.getState() == ol.TileState.ERROR) { @@ -159,6 +166,8 @@ ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup_ = function( var sourceTileCoord = sourceTile.tileCoord; var sourceTileExtent = sourceTileGrid.getTileCoordExtent(sourceTileCoord); var sharedExtent = ol.extent.getIntersection(tileExtent, sourceTileExtent); + var bufferedExtent = ol.extent.equals(sourceTileExtent, sharedExtent) ? null : + ol.extent.buffer(sharedExtent, layer.getRenderBuffer() * resolution); var tileProjection = sourceTile.getProjection(); var reproject = false; if (!ol.proj.equivalent(projection, tileProjection)) { @@ -166,8 +175,8 @@ ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup_ = function( sourceTile.setProjection(projection); } replayState.dirty = false; - var replayGroup = new ol.render.canvas.ReplayGroup(0, sharedExtent, - resolution, pixelRatio, source.getOverlaps(), layer.getRenderBuffer()); + var replayGroup = new ol.render.canvas.ReplayGroup(0, sharedExtent, resolution, + pixelRatio, source.getOverlaps(), this.declutterTree_, layer.getRenderBuffer()); var squaredTolerance = ol.renderer.vector.getSquaredTolerance( resolution, pixelRatio); @@ -210,9 +219,14 @@ ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup_ = function( } feature.getGeometry().transform(tileProjection, projection); } - renderFeature.call(this, feature); + if (!bufferedExtent || ol.extent.intersects(bufferedExtent, feature.getExtent())) { + renderFeature.call(this, feature); + } } replayGroup.finish(); + for (var r in replayGroup.getReplays()) { + zIndexKeys[r] = true; + } sourceTile.setReplayGroup(layer, tile.tileCoord.toString(), replayGroup); } replayState.renderedRevision = revision; @@ -242,6 +256,7 @@ ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = functi var rotation = frameState.viewState.rotation; hitTolerance = hitTolerance == undefined ? 0 : hitTolerance; var layer = this.getLayer(); + var declutterReplays = layer.getDeclutter() ? {} : null; /** @type {Object.} */ var features = {}; @@ -279,9 +294,12 @@ ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = functi features[key] = true; return callback.call(thisArg, feature, layer); } - }); + }, declutterReplays); } } + if (this.declutterTree_) { + this.declutterTree_.clear(); + } return found; }; @@ -331,14 +349,16 @@ ol.renderer.canvas.VectorTileLayer.prototype.handleStyleImageChange_ = function( */ ol.renderer.canvas.VectorTileLayer.prototype.postCompose = function(context, frameState, layerState) { var layer = this.getLayer(); + var declutterReplays = layer.getDeclutter() ? {} : null; var source = /** @type {ol.source.VectorTile} */ (layer.getSource()); var renderMode = layer.getRenderMode(); - var replays = ol.renderer.canvas.VectorTileLayer.VECTOR_REPLAYS[renderMode]; + var replayTypes = ol.renderer.canvas.VectorTileLayer.VECTOR_REPLAYS[renderMode]; 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); + ol.render.canvas.rotateAtOffset(context, -rotation, offsetX, offsetY); var tiles = this.renderedTiles; var tileGrid = source.getTileGridForProjection(frameState.viewState.projection); var clips = []; @@ -351,21 +371,23 @@ ol.renderer.canvas.VectorTileLayer.prototype.postCompose = function(context, fra var tileCoord = tile.tileCoord; var worldOffset = tileGrid.getTileCoordExtent(tileCoord)[0] - tileGrid.getTileCoordExtent(tile.wrappedTileCoord)[0]; + var transform = undefined; for (var t = 0, tt = tile.tileKeys.length; t < tt; ++t) { var sourceTile = tile.getTile(tile.tileKeys[t]); if (sourceTile.getState() == ol.TileState.ERROR) { continue; } var replayGroup = sourceTile.getReplayGroup(layer, tileCoord.toString()); - if (renderMode != ol.layer.VectorTileRenderType.VECTOR && !replayGroup.hasReplays(replays)) { + if (renderMode != ol.layer.VectorTileRenderType.VECTOR && !replayGroup.hasReplays(replayTypes)) { continue; } + if (!transform) { + transform = this.getTransform(frameState, worldOffset); + } var currentZ = sourceTile.tileCoord[0]; - var transform = this.getTransform(frameState, worldOffset); 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) { @@ -385,12 +407,16 @@ ol.renderer.canvas.VectorTileLayer.prototype.postCompose = function(context, fra context.clip(); } } - replayGroup.replay(context, transform, rotation, {}, replays); + replayGroup.replay(context, transform, rotation, {}, replayTypes, declutterReplays); context.restore(); clips.push(currentClip); zs.push(currentZ); } } + if (declutterReplays) { + ol.render.canvas.ReplayGroup.replayDeclutter(declutterReplays, context, rotation); + this.declutterTree_.clear(); + } ol.renderer.canvas.TileLayer.prototype.postCompose.apply(this, arguments); }; @@ -458,7 +484,7 @@ ol.renderer.canvas.VectorTileLayer.prototype.renderTileImage_ = function( ol.transform.scale(transform, pixelScale, -pixelScale); ol.transform.translate(transform, -tileExtent[0], -tileExtent[3]); var replayGroup = sourceTile.getReplayGroup(layer, tile.tileCoord.toString()); - replayGroup.replay(context, transform, 0, {}, replays, true); + replayGroup.replay(context, transform, 0, {}, replays); } } }; diff --git a/src/ol/renderer/vector.js b/src/ol/renderer/vector.js index e1e055fc89..13cd33f827 100644 --- a/src/ol/renderer/vector.js +++ b/src/ol/renderer/vector.js @@ -57,7 +57,7 @@ ol.renderer.vector.renderCircleGeometry_ = function(replayGroup, geometry, style if (textStyle) { var textReplay = replayGroup.getReplay( style.getZIndex(), ol.render.ReplayType.TEXT); - textReplay.setTextStyle(textStyle); + textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false)); textReplay.drawText(geometry, feature); } }; @@ -180,7 +180,7 @@ ol.renderer.vector.renderLineStringGeometry_ = function(replayGroup, geometry, s if (textStyle) { var textReplay = replayGroup.getReplay( style.getZIndex(), ol.render.ReplayType.TEXT); - textReplay.setTextStyle(textStyle); + textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false)); textReplay.drawText(geometry, feature); } }; @@ -205,7 +205,7 @@ ol.renderer.vector.renderMultiLineStringGeometry_ = function(replayGroup, geomet if (textStyle) { var textReplay = replayGroup.getReplay( style.getZIndex(), ol.render.ReplayType.TEXT); - textReplay.setTextStyle(textStyle); + textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false)); textReplay.drawText(geometry, feature); } }; @@ -231,7 +231,7 @@ ol.renderer.vector.renderMultiPolygonGeometry_ = function(replayGroup, geometry, if (textStyle) { var textReplay = replayGroup.getReplay( style.getZIndex(), ol.render.ReplayType.TEXT); - textReplay.setTextStyle(textStyle); + textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false)); textReplay.drawText(geometry, feature); } }; @@ -252,14 +252,14 @@ ol.renderer.vector.renderPointGeometry_ = function(replayGroup, geometry, style, } var imageReplay = replayGroup.getReplay( style.getZIndex(), ol.render.ReplayType.IMAGE); - imageReplay.setImageStyle(imageStyle); + imageReplay.setImageStyle(imageStyle, replayGroup.addDeclutter(false)); imageReplay.drawPoint(geometry, feature); } var textStyle = style.getText(); if (textStyle) { var textReplay = replayGroup.getReplay( style.getZIndex(), ol.render.ReplayType.TEXT); - textReplay.setTextStyle(textStyle); + textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(!!imageStyle)); textReplay.drawText(geometry, feature); } }; @@ -280,14 +280,14 @@ ol.renderer.vector.renderMultiPointGeometry_ = function(replayGroup, geometry, s } var imageReplay = replayGroup.getReplay( style.getZIndex(), ol.render.ReplayType.IMAGE); - imageReplay.setImageStyle(imageStyle); + imageReplay.setImageStyle(imageStyle, replayGroup.addDeclutter(false)); imageReplay.drawMultiPoint(geometry, feature); } var textStyle = style.getText(); if (textStyle) { var textReplay = replayGroup.getReplay( style.getZIndex(), ol.render.ReplayType.TEXT); - textReplay.setTextStyle(textStyle); + textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(!!imageStyle)); textReplay.drawText(geometry, feature); } }; @@ -313,7 +313,7 @@ ol.renderer.vector.renderPolygonGeometry_ = function(replayGroup, geometry, styl if (textStyle) { var textReplay = replayGroup.getReplay( style.getZIndex(), ol.render.ReplayType.TEXT); - textReplay.setTextStyle(textStyle); + textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false)); textReplay.drawText(geometry, feature); } }; diff --git a/src/ol/source/imagevector.js b/src/ol/source/imagevector.js index 68a4f8f565..66a15f2fe6 100644 --- a/src/ol/source/imagevector.js +++ b/src/ol/source/imagevector.js @@ -4,6 +4,7 @@ goog.require('ol'); goog.require('ol.dom'); goog.require('ol.events'); goog.require('ol.events.EventType'); +goog.require('ol.ext.rbush'); goog.require('ol.extent'); goog.require('ol.render.canvas.ReplayGroup'); goog.require('ol.renderer.vector'); @@ -55,6 +56,12 @@ ol.source.ImageVector = function(options) { */ this.canvasSize_ = [0, 0]; + /** + * Declutter tree. + * @private + */ + this.declutterTree_ = ol.ext.rbush(9); + /** * @private * @type {number} @@ -113,7 +120,7 @@ ol.source.ImageVector.prototype.canvasFunctionInternal_ = function(extent, resol var replayGroup = new ol.render.canvas.ReplayGroup( ol.renderer.vector.getTolerance(resolution, pixelRatio), extent, - resolution, pixelRatio, this.source_.getOverlaps(), this.renderBuffer_); + resolution, pixelRatio, this.source_.getOverlaps(), this.declutterTree_, this.renderBuffer_); this.source_.loadFeatures(extent, resolution, projection); @@ -146,6 +153,7 @@ ol.source.ImageVector.prototype.canvasFunctionInternal_ = function(extent, resol replayGroup.replay(this.canvasContext_, transform, 0, {}); this.replayGroup_ = replayGroup; + this.declutterTree_.clear(); return this.canvasContext_.canvas; }; @@ -161,7 +169,7 @@ ol.source.ImageVector.prototype.forEachFeatureAtCoordinate = function( } else { /** @type {Object.} */ var features = {}; - return this.replayGroup_.forEachFeatureAtCoordinate( + var result = this.replayGroup_.forEachFeatureAtCoordinate( coordinate, resolution, 0, hitTolerance, skippedFeatureUids, /** * @param {ol.Feature|ol.render.Feature} feature Feature. @@ -173,7 +181,9 @@ ol.source.ImageVector.prototype.forEachFeatureAtCoordinate = function( features[key] = true; return callback(feature); } - }); + }, null); + this.declutterTree_.clear(); + return result; } }; diff --git a/src/ol/typedefs.js b/src/ol/typedefs.js index 8d6c599715..761e0d0e1d 100644 --- a/src/ol/typedefs.js +++ b/src/ol/typedefs.js @@ -167,6 +167,20 @@ ol.Coordinate; ol.CoordinateFormatType; +/** + * Container for decluttered replay instructions that need to be rendered or + * omitted together, i.e. when styles render both an image and text, or for the + * characters that form text along lines. The basic elements of this array are + * `[minX, minY, maxX, maxY, count]`, where the first four entries are the + * rendered extent of the group in pixel space. `count` is the number of styles + * in the group, i.e. 2 when an image and a text are grouped, or 1 otherwise. + * In addition to these four elements, declutter instruction arrays (i.e. the + * arguments to @{link ol.render.canvas.drawImage} are appended to the array. + * @typedef {Array.<*>} + */ +ol.DeclutterGroup; + + /** * A function that takes a {@link ol.MapBrowserEvent} and two * {@link ol.Pixel}s and returns a `{boolean}`. If the condition is met, diff --git a/test/rendering/ol/layer/expected/vector-canvas-declutter-group.png b/test/rendering/ol/layer/expected/vector-canvas-declutter-group.png new file mode 100644 index 0000000000..584b381e7e Binary files /dev/null and b/test/rendering/ol/layer/expected/vector-canvas-declutter-group.png differ diff --git a/test/rendering/ol/layer/expected/vector-canvas-declutter-image-zindex.png b/test/rendering/ol/layer/expected/vector-canvas-declutter-image-zindex.png new file mode 100644 index 0000000000..f049e6fc7d Binary files /dev/null and b/test/rendering/ol/layer/expected/vector-canvas-declutter-image-zindex.png differ diff --git a/test/rendering/ol/layer/expected/vector-canvas-declutter-image.png b/test/rendering/ol/layer/expected/vector-canvas-declutter-image.png new file mode 100644 index 0000000000..e361809630 Binary files /dev/null and b/test/rendering/ol/layer/expected/vector-canvas-declutter-image.png differ diff --git a/test/rendering/ol/layer/expected/vector-canvas-declutter-line-zindex.png b/test/rendering/ol/layer/expected/vector-canvas-declutter-line-zindex.png new file mode 100644 index 0000000000..b4a115521f Binary files /dev/null and b/test/rendering/ol/layer/expected/vector-canvas-declutter-line-zindex.png differ diff --git a/test/rendering/ol/layer/expected/vector-canvas-declutter-line.png b/test/rendering/ol/layer/expected/vector-canvas-declutter-line.png new file mode 100644 index 0000000000..7cb8c929d0 Binary files /dev/null and b/test/rendering/ol/layer/expected/vector-canvas-declutter-line.png differ diff --git a/test/rendering/ol/layer/expected/vector-canvas-declutter-zindex.png b/test/rendering/ol/layer/expected/vector-canvas-declutter-zindex.png new file mode 100644 index 0000000000..dd44e26090 Binary files /dev/null and b/test/rendering/ol/layer/expected/vector-canvas-declutter-zindex.png differ diff --git a/test/rendering/ol/layer/expected/vector-canvas-declutter.png b/test/rendering/ol/layer/expected/vector-canvas-declutter.png new file mode 100644 index 0000000000..d92be539df Binary files /dev/null and b/test/rendering/ol/layer/expected/vector-canvas-declutter.png differ diff --git a/test/rendering/ol/layer/expected/vectortile-canvas-declutter.png b/test/rendering/ol/layer/expected/vectortile-canvas-declutter.png new file mode 100644 index 0000000000..2f44b10f35 Binary files /dev/null and b/test/rendering/ol/layer/expected/vectortile-canvas-declutter.png differ diff --git a/test/rendering/ol/layer/vector.test.js b/test/rendering/ol/layer/vector.test.js index 3dc4319f58..bca13ea6ad 100644 --- a/test/rendering/ol/layer/vector.test.js +++ b/test/rendering/ol/layer/vector.test.js @@ -6,12 +6,15 @@ goog.require('ol.View'); goog.require('ol.format.GeoJSON'); goog.require('ol.geom.Circle'); goog.require('ol.geom.LineString'); +goog.require('ol.geom.Point'); goog.require('ol.geom.Polygon'); goog.require('ol.layer.Vector'); goog.require('ol.source.Vector'); +goog.require('ol.style.Circle'); goog.require('ol.style.Fill'); goog.require('ol.style.Stroke'); goog.require('ol.style.Style'); +goog.require('ol.style.Text'); describe('ol.rendering.layer.Vector', function() { @@ -461,4 +464,304 @@ describe('ol.rendering.layer.Vector', function() { }); + describe('decluttering', function() { + + beforeEach(function() { + source = new ol.source.Vector(); + }); + + it('declutters text', function(done) { + createMap('canvas'); + var layer = new ol.layer.Vector({ + source: source + }); + map.addLayer(layer); + + var centerFeature = new ol.Feature({ + geometry: new ol.geom.Point(center), + text: 'center' + }); + source.addFeature(centerFeature); + source.addFeature(new ol.Feature({ + geometry: new ol.geom.Point([center[0] - 550, center[1]]), + text: 'west' + })); + source.addFeature(new ol.Feature({ + geometry: new ol.geom.Point([center[0] + 550, center[1]]), + text: 'east' + })); + + layer.setDeclutter(true); + layer.setStyle(function(feature) { + return new ol.style.Style({ + text: new ol.style.Text({ + text: feature.get('text'), + font: '12px sans-serif' + }) + }); + }); + + map.once('postrender', function() { + var hitDetected = map.getFeaturesAtPixel([42, 42]); + expect(hitDetected).to.have.length(1); + expect(hitDetected[0]).to.equal(centerFeature); + expectResemble(map, 'rendering/ol/layer/expected/vector-canvas-declutter.png', + 2.2, done); + }); + }); + + it('declutters text and respects z-index', function(done) { + createMap('canvas'); + var layer = new ol.layer.Vector({ + source: source + }); + map.addLayer(layer); + + source.addFeature(new ol.Feature({ + geometry: new ol.geom.Point(center), + text: 'center', + zIndex: 2 + })); + source.addFeature(new ol.Feature({ + geometry: new ol.geom.Point([center[0] - 550, center[1]]), + text: 'west', + zIndex: 3 + })); + source.addFeature(new ol.Feature({ + geometry: new ol.geom.Point([center[0] + 550, center[1]]), + text: 'east', + zIndex: 1 + })); + + layer.setDeclutter(true); + layer.setStyle(function(feature) { + return new ol.style.Style({ + zIndex: feature.get('zIndex'), + text: new ol.style.Text({ + text: feature.get('text'), + font: '12px sans-serif' + }) + }); + }); + + map.once('postrender', function() { + expectResemble(map, 'rendering/ol/layer/expected/vector-canvas-declutter-zindex.png', + 3.9, done); + }); + }); + + it('declutters images', function(done) { + createMap('canvas'); + var layer = new ol.layer.Vector({ + source: source + }); + map.addLayer(layer); + + var centerFeature = new ol.Feature({ + geometry: new ol.geom.Point(center) + }); + source.addFeature(centerFeature); + source.addFeature(new ol.Feature({ + geometry: new ol.geom.Point([center[0] - 550, center[1]]) + })); + source.addFeature(new ol.Feature({ + geometry: new ol.geom.Point([center[0] + 550, center[1]]) + })); + + layer.setDeclutter(true); + layer.setStyle(function(feature) { + return new ol.style.Style({ + image: new ol.style.Circle({ + radius: 15, + stroke: new ol.style.Stroke({ + color: 'blue' + }) + }) + }); + }); + + map.once('postrender', function() { + var hitDetected = map.getFeaturesAtPixel([40, 40]); + expect(hitDetected).to.have.length(1); + expect(hitDetected[0]).to.equal(centerFeature); + expectResemble(map, 'rendering/ol/layer/expected/vector-canvas-declutter-image.png', + IMAGE_TOLERANCE, done); + }); + }); + + it('declutters images and respects z-index', function(done) { + createMap('canvas'); + var layer = new ol.layer.Vector({ + source: source + }); + map.addLayer(layer); + + source.addFeature(new ol.Feature({ + geometry: new ol.geom.Point(center), + zIndex: 2 + })); + source.addFeature(new ol.Feature({ + geometry: new ol.geom.Point([center[0] - 550, center[1]]), + zIndex: 3 + })); + source.addFeature(new ol.Feature({ + geometry: new ol.geom.Point([center[0] + 550, center[1]]), + zIndex: 1 + })); + + layer.setDeclutter(true); + layer.setStyle(function(feature) { + return new ol.style.Style({ + zIndex: feature.get('zIndex'), + image: new ol.style.Circle({ + radius: 15, + stroke: new ol.style.Stroke({ + color: 'blue' + }) + }) + }); + }); + + map.once('postrender', function() { + expectResemble(map, 'rendering/ol/layer/expected/vector-canvas-declutter-image-zindex.png', + IMAGE_TOLERANCE, done); + }); + }); + + it('declutters image & text groups', function(done) { + createMap('canvas'); + var layer = new ol.layer.Vector({ + source: source + }); + map.addLayer(layer); + + source.addFeature(new ol.Feature({ + geometry: new ol.geom.Point(center), + text: 'center' + })); + source.addFeature(new ol.Feature({ + geometry: new ol.geom.Point([center[0] - 550, center[1]]), + text: 'west' + })); + source.addFeature(new ol.Feature({ + geometry: new ol.geom.Point([center[0] + 550, center[1]]), + text: 'east' + })); + + layer.setDeclutter(true); + layer.setStyle(function(feature) { + return new ol.style.Style({ + image: new ol.style.Circle({ + radius: 5, + stroke: new ol.style.Stroke({ + color: 'blue' + }) + }), + text: new ol.style.Text({ + text: feature.get('text'), + font: '12px sans-serif', + textBaseline: 'bottom', + offsetY: -5 + }) + }); + }); + + map.once('postrender', function() { + expectResemble(map, 'rendering/ol/layer/expected/vector-canvas-declutter-group.png', + 2.2, done); + }); + }); + + it('declutters text along lines and images', function(done) { + createMap('canvas'); + var layer = new ol.layer.Vector({ + source: source + }); + map.addLayer(layer); + + var point = new ol.Feature(new ol.geom.Point(center)); + point.setStyle(new ol.style.Style({ + image: new ol.style.Circle({ + radius: 8, + stroke: new ol.style.Stroke({ + color: 'blue' + }) + }) + })); + var line = new ol.Feature(new ol.geom.LineString([ + [center[0] - 650, center[1] - 200], + [center[0] + 650, center[1] - 200] + ])); + line.setStyle(new ol.style.Style({ + stroke: new ol.style.Stroke({ + color: '#CCC', + width: 12 + }), + text: new ol.style.Text({ + placement: 'line', + text: 'east-west', + font: '12px sans-serif' + }) + })); + + source.addFeature(point); + source.addFeature(line); + + layer.setDeclutter(true); + + map.once('postrender', function() { + expectResemble(map, 'rendering/ol/layer/expected/vector-canvas-declutter-line.png', + IMAGE_TOLERANCE, done); + }); + }); + + it('declutters text along lines and images with z-index', function(done) { + createMap('canvas'); + var layer = new ol.layer.Vector({ + source: source + }); + map.addLayer(layer); + + var point = new ol.Feature(new ol.geom.Point(center)); + point.setStyle(new ol.style.Style({ + zIndex: 2, + image: new ol.style.Circle({ + radius: 8, + stroke: new ol.style.Stroke({ + color: 'blue' + }) + }) + })); + var line = new ol.Feature(new ol.geom.LineString([ + [center[0] - 650, center[1] - 200], + [center[0] + 650, center[1] - 200] + ])); + line.setStyle(new ol.style.Style({ + zIndex: 1, + stroke: new ol.style.Stroke({ + color: '#CCC', + width: 12 + }), + text: new ol.style.Text({ + placement: 'line', + text: 'east-west', + font: '12px sans-serif' + }) + })); + + source.addFeature(point); + source.addFeature(line); + + layer.setDeclutter(true); + + map.once('postrender', function() { + var hitDetected = map.getFeaturesAtPixel([35, 46]); + expect(hitDetected).to.have.length(1); + expect(hitDetected[0]).to.equal(line); + expectResemble(map, 'rendering/ol/layer/expected/vector-canvas-declutter-line-zindex.png', + 4.1, done); + }); + }); + }); + }); diff --git a/test/rendering/ol/layer/vectortile.test.js b/test/rendering/ol/layer/vectortile.test.js index 0568e46701..12291b135e 100644 --- a/test/rendering/ol/layer/vectortile.test.js +++ b/test/rendering/ol/layer/vectortile.test.js @@ -6,6 +6,10 @@ goog.require('ol.format.MVT'); goog.require('ol.layer.VectorTile'); goog.require('ol.obj'); goog.require('ol.source.VectorTile'); +goog.require('ol.style.Circle'); +goog.require('ol.style.Fill'); +goog.require('ol.style.Style'); +goog.require('ol.style.Text'); goog.require('ol.tilegrid'); @@ -13,10 +17,11 @@ describe('ol.rendering.layer.VectorTile', function() { var map; - function createMap(renderer, opt_pixelRatio) { + function createMap(renderer, opt_pixelRatio, opt_size) { + var size = opt_size || 50; map = new ol.Map({ pixelRatio: opt_pixelRatio || 1, - target: createMapDiv(50, 50), + target: createMapDiv(size, size), renderer: renderer, view: new ol.View({ center: [1825927.7316762917, 6143091.089223046], @@ -104,6 +109,34 @@ describe('ol.rendering.layer.VectorTile', function() { }); }); + it('declutters text and images', function(done) { + createMap('canvas', 1, 100); + map.getView().setZoom(13.8); + var style = function(feature, resolution) { + var geom = feature.getGeometry(); + if (geom.getType() == 'Point') { + return new ol.style.Style({ + image: new ol.style.Circle({ + radius: 7, + fill: new ol.style.Fill({ + color: 'red' + }) + }), + text: new ol.style.Text({ + text: feature.get('name_en'), + font: '12px sans-serif', + textBaseline: 'bottom', + offsetY: -7 + }) + }); + } + }; + waitForTiles(source, {declutter: true, style: style}, function() { + expectResemble(map, 'rendering/ol/layer/expected/vectortile-canvas-declutter.png', + 8.5, done); + }); + }); + }); }); diff --git a/test/spec/ol/render/feature.test.js b/test/spec/ol/render/feature.test.js index c73dea5171..2618c54234 100644 --- a/test/spec/ol/render/feature.test.js +++ b/test/spec/ol/render/feature.test.js @@ -1,5 +1,7 @@ - - +goog.require('ol.geom.LineString'); +goog.require('ol.geom.MultiLineString'); +goog.require('ol.geom.MultiPolygon'); +goog.require('ol.geom.Polygon'); goog.require('ol.render.Feature'); @@ -51,6 +53,51 @@ describe('ol.render.Feature', function() { }); }); + describe('#getFlatInteriorPoint()', function() { + it('returns correct point and caches it', function() { + var polygon = new ol.geom.Polygon([[[0, 0], [0, 10], [10, 10], [10, 0], [0, 0]]]); + var feature = new ol.render.Feature('Polygon', polygon.getOrientedFlatCoordinates(), + polygon.getEnds()); + expect(feature.getFlatInteriorPoint()).to.eql([5, 5, 10]); + expect(feature.getFlatInteriorPoint()).to.be(feature.flatInteriorPoints_); + }); + }); + + describe('#getFlatInteriorPoints()', function() { + it('returns correct points and caches them', function() { + var polygon = new ol.geom.MultiPolygon([ + [[[0, 0], [0, 10], [10, 10], [10, 0], [0, 0]]], + [[[10, 0], [10, 10], [20, 10], [20, 0], [10, 0]]] + ]); + var feature = new ol.render.Feature('MultiPolygon', polygon.getOrientedFlatCoordinates(), + polygon.getEndss()); + expect(feature.getFlatInteriorPoints()).to.eql([5, 5, 10, 15, 5, 10]); + expect(feature.getFlatInteriorPoints()).to.be(feature.flatInteriorPoints_); + }); + }); + + describe('#getFlatMidpoint()', function() { + it('returns correct point', function() { + var line = new ol.geom.LineString([[0, 0], [0, 10], [10, 10], [10, 0], [0, 0]]); + var feature = new ol.render.Feature('LineString', line.getFlatCoordinates()); + expect(feature.getFlatMidpoint()).to.eql([10, 10]); + expect(feature.getFlatMidpoint()).to.eql(feature.flatMidpoints_); + }); + }); + + describe('#getFlatMidpoints()', function() { + it('returns correct points and caches them', function() { + var line = new ol.geom.MultiLineString([ + [[0, 0], [0, 10], [10, 10], [10, 0], [0, 0]], + [[10, 0], [10, 10], [20, 10], [20, 0], [10, 0]] + ]); + var feature = new ol.render.Feature('MultiLineString', line.getFlatCoordinates(), + line.getEnds()); + expect(feature.getFlatMidpoints()).to.eql([10, 10, 20, 10]); + expect(feature.getFlatMidpoints()).to.be(feature.flatMidpoints_); + }); + }); + describe('#getGeometry()', function() { it('returns itself as geometry', function() { expect(renderFeature.getGeometry()).to.equal(renderFeature);