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/externs/olx.js b/externs/olx.js index 29b2800488..390491c0f4 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,14 @@ olx.layer.VectorOptions.prototype.renderBuffer; olx.layer.VectorOptions.prototype.source; +/** + * Declutter images and text. 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 +4398,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 +4432,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 +4509,14 @@ olx.layer.VectorTileOptions.prototype.preload; olx.layer.VectorTileOptions.prototype.source; +/** + * Declutter images and text. 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..db4b561d43 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 {Array.<*>} + */ + 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 {Array.<*>} */ (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..e426725eab 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 {?} declutter 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, declutter) { + ol.render.canvas.Replay.call(this, + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutter); /** * @private diff --git a/src/ol/render/canvas/polygonreplay.js b/src/ol/render/canvas/polygonreplay.js index 273df81e22..9a3679bc16 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 {?} declutter 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, declutter) { + ol.render.canvas.Replay.call(this, + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutter); /** * @private diff --git a/src/ol/render/canvas/replay.js b/src/ol/render/canvas/replay.js index 0e61911e0c..ad467e3e8b 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 {Array.<*>} 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; @@ -172,38 +180,34 @@ ol.render.canvas.Replay.prototype.replayImage_ = function(context, x, y, image, var w = (width + originX > image.width) ? image.width - originX : width; var h = (height + originY > image.height) ? image.height - originY : height; + var box = this.tmpExtent_; - var box; + var transform = null; if (rotation !== 0) { var centerX = x + anchorX; var centerY = y + anchorY; - ol.transform.compose(localTransform, + transform = ol.transform.compose(localTransform, centerX, centerY, 1, 1, rotation, -centerX, -centerY); - context.setTransform.apply(context, localTransform); - box = ol.extent.createEmpty(); + 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 { - box = [x, y, x + w * scale, y + h * scale]; + ol.extent.createOrUpdate(x, y, x + w * scale, y + h * scale, box); } var canvas = context.canvas; - if (box[0] > canvas.width || box[2] < 0 || box[1] > canvas.height || box[3] < 0) { - return; - } - var alpha = context.globalAlpha; - if (opacity != 1) { - context.globalAlpha = alpha * opacity; - } - - context.drawImage(image, originX, originY, w, h, x, y, w * scale, h * scale); - - if (opacity != 1) { - context.globalAlpha = alpha; - } - if (rotation !== 0) { - context.setTransform.apply(context, this.resetTransform_); + 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); } }; @@ -373,7 +377,30 @@ 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 {Array.<*>} 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) { + if (!this.declutterTree.collides(this.declutterTree.toBBox(declutterGroup))) { + this.declutterTree.insert(declutterGroup.slice(0, 4)); + 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); + } } }; @@ -414,7 +441,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_; @@ -510,24 +537,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 {Array.<*>} */ (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: @@ -536,13 +565,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 {Array.<*>} */ (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); @@ -558,12 +588,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..3ad642adab 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,33 @@ 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; + + /** + * Container for decluttered replay instructions that need to be rendered or + * omitted together, i.e. when styles render both an image and text. The basic + * elements of this array are `[minX, minY, maxX, maxY, count]`, where the + * first four entries are the rendered extent in pixel space. `count` is the + * number of replay instructions in the group. In addition to these basic + * elements, declutter instructions are appended to the array. + * @type {Array.<*>} + * @private + */ + this.declutterGroup_ = null; + /** * @private * @type {number} @@ -170,6 +191,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 = zs.length - 1; z >= 0; --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 = zs.length - 1; z >= 0; --z) { + var replayData = declutterReplays[zs[z].toString()]; + for (var i = 0, ii = replayData.length; i < ii;) { + var replay = replayData[i++]; + var transform = replayData[i++]; + var result = replay.replayHitDetection(context, transform, rotation, {}, + featureCallback, hitExtent); + if (result) { + return result; + } + } + } +}; + + +/** + * @param {boolean} group Group with previous replay. + * @return {Array.<*>} 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 +297,12 @@ 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,7 +332,7 @@ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( var mask = ol.render.canvas.ReplayGroup.getCircleArray_(hitTolerance); - return this.replayHitDetection_(context, transform, rotation, + var result = this.replayHitDetection_(context, transform, rotation, skippedFeaturesHash, /** * @param {ol.Feature|ol.render.Feature} feature Feature. @@ -269,6 +356,11 @@ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( } } }, hitExtent); + if (!result && declutterReplays) { + result = ol.render.canvas.ReplayGroup.replayDeclutterHitDetection( + declutterReplays, context, rotation, callback, hitExtent); + } + return result; }; @@ -303,13 +395,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 +426,10 @@ 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 +437,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,12 +479,13 @@ 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) { @@ -386,14 +494,26 @@ ol.render.canvas.ReplayGroup.prototype.replayHitDetection_ = function( var i, ii, j, replays, replay, result; 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 = 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 +527,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..7148e439bd 100644 --- a/src/ol/render/canvas/textreplay.js +++ b/src/ol/render/canvas/textreplay.js @@ -22,11 +22,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 {Array.<*>} + */ + this.declutterGroup_; /** * @private @@ -238,7 +246,7 @@ ol.render.canvas.TextReplay.prototype.drawText = function(geometry, feature) { } end = this.appendFlatCoordinates(flatCoordinates, flatOffset, flatEnd, stride, false, false); flatOffset = ends[o]; - this.drawChars_(begin, end); + this.drawChars_(begin, end, this.declutterGroup_); begin = end; } this.endGeometry(geometry, feature); @@ -378,12 +386,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 +401,9 @@ ol.render.canvas.TextReplay.prototype.drawTextImage_ = function(label, begin, en * @private * @param {number} begin Begin. * @param {number} end End. + * @param {Array.<*>} 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 +432,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 +449,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 {Array.<*>} */ (declutterGroup); var textFillStyle = textStyle.getFill(); if (!textFillStyle) { fillState = this.textFillState_ = null; diff --git a/src/ol/render/vectorcontext.js b/src/ol/render/vectorcontext.js index 1ea450cd4c..076a2fdb48 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 {Array.<*>=} 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 {Array.<*>=} 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..e8c3a3d0f0 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, ['[0]', '[1]', '[2]', '[3]']) : 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); }; @@ -226,7 +237,7 @@ ol.renderer.canvas.VectorLayer.prototype.forEachFeatureAtCoordinate = function(c var layer = this.getLayer(); /** @type {Object.} */ var features = {}; - return this.replayGroup_.forEachFeatureAtCoordinate(coordinate, resolution, + var result = this.replayGroup_.forEachFeatureAtCoordinate(coordinate, resolution, rotation, hitTolerance, {}, /** * @param {ol.Feature|ol.render.Feature} feature Feature. @@ -238,7 +249,11 @@ ol.renderer.canvas.VectorLayer.prototype.forEachFeatureAtCoordinate = function(c features[key] = true; return callback.call(thisArg, feature, layer); } - }); + }, null); + if (this.declutterTree_) { + this.declutterTree_.clear(); + } + return result; } }; @@ -317,7 +332,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 09769a9dd3..1d2c5827a4 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, ['[0]', '[1]', '[2]', '[3]']) : 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) { @@ -168,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); @@ -217,6 +224,9 @@ ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup_ = function( } } replayGroup.finish(); + for (var r in replayGroup.getReplays()) { + zIndexKeys[r] = true; + } sourceTile.setReplayGroup(layer, tile.tileCoord.toString(), replayGroup); } replayState.renderedRevision = revision; @@ -246,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 = {}; @@ -283,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; }; @@ -335,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 = []; @@ -355,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) { @@ -389,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); }; @@ -462,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..d310285a1f 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, ['[0]', '[1]', '[2]', '[3]']); + /** * @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; } };