From 742a71cd4b489f31a16d7d3e14c4ce742b61c9ff Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 14 Sep 2017 09:33:35 +0200 Subject: [PATCH 01/17] Only render images when visible --- src/ol/render/canvas/replay.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/ol/render/canvas/replay.js b/src/ol/render/canvas/replay.js index 37a88ca06b..0e61911e0c 100644 --- a/src/ol/render/canvas/replay.js +++ b/src/ol/render/canvas/replay.js @@ -169,21 +169,34 @@ ol.render.canvas.Replay.prototype.replayImage_ = function(context, x, y, image, x = Math.round(x); y = Math.round(y); } + + var w = (width + originX > image.width) ? image.width - originX : width; + var h = (height + originY > image.height) ? image.height - originY : height; + + var box; 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); + box = ol.extent.createEmpty(); + 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]; + } + 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; } - var w = (width + originX > image.width) ? image.width - originX : width; - var h = (height + originY > image.height) ? image.height - originY : height; - context.drawImage(image, originX, originY, w, h, x, y, w * scale, h * scale); if (opacity != 1) { From dbeb3d2795bd25bb5dba0a94840370f138615cbd Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 8 Oct 2017 19:41:46 +0200 Subject: [PATCH 02/17] Only render parts of source tile that are needed for the view tile --- src/ol/renderer/canvas/vectortilelayer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/ol/renderer/canvas/vectortilelayer.js b/src/ol/renderer/canvas/vectortilelayer.js index 99af6a4bee..09769a9dd3 100644 --- a/src/ol/renderer/canvas/vectortilelayer.js +++ b/src/ol/renderer/canvas/vectortilelayer.js @@ -159,6 +159,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)) { @@ -210,7 +212,9 @@ 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(); sourceTile.setReplayGroup(layer, tile.tileCoord.toString(), replayGroup); From 5ebc969599a304ee03638a25b385d52122d8297e Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Mon, 9 Oct 2017 18:24:27 +0200 Subject: [PATCH 03/17] Support line and polygon label points for ol.render.Feature --- src/ol/render/feature.js | 76 +++++++++++++++++++++++++++++ test/spec/ol/render/feature.test.js | 51 ++++++++++++++++++- 2 files changed, 125 insertions(+), 2 deletions(-) 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/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); From 08af207724dcc8488b6e156788c2289106371984 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Mon, 9 Oct 2017 18:27:31 +0200 Subject: [PATCH 04/17] Add decluttering for images and text --- examples/mapbox-vector-tiles.js | 1 + examples/street-labels.js | 1 + externs/olx.js | 21 ++- src/ol/layer/vector.js | 22 +++ src/ol/layer/vectortile.js | 16 ++- src/ol/render/canvas.js | 41 ++++++ src/ol/render/canvas/imagereplay.js | 24 +++- src/ol/render/canvas/linestringreplay.js | 8 +- src/ol/render/canvas/polygonreplay.js | 8 +- src/ol/render/canvas/replay.js | 134 ++++++++++------- src/ol/render/canvas/replaygroup.js | 166 +++++++++++++++++++--- src/ol/render/canvas/textreplay.js | 28 ++-- src/ol/render/vectorcontext.js | 6 +- src/ol/render/webgl/replaygroup.js | 7 + src/ol/renderer/canvas/vectorlayer.js | 21 ++- src/ol/renderer/canvas/vectortilelayer.js | 42 ++++-- src/ol/renderer/vector.js | 18 +-- src/ol/source/imagevector.js | 16 ++- 18 files changed, 450 insertions(+), 130 deletions(-) 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; } }; From b3f9e4e8ef7224abc6cd986ee9e0851ec2f31d7f Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Wed, 11 Oct 2017 23:42:22 +0200 Subject: [PATCH 05/17] Improve documentation --- externs/olx.js | 9 +++++++-- src/ol/render/canvas/imagereplay.js | 4 ++-- src/ol/render/canvas/replay.js | 8 ++++---- src/ol/render/canvas/replaygroup.js | 21 +++++++++------------ src/ol/render/canvas/textreplay.js | 6 +++--- src/ol/render/vectorcontext.js | 4 ++-- src/ol/typedefs.js | 14 ++++++++++++++ 7 files changed, 41 insertions(+), 25 deletions(-) diff --git a/externs/olx.js b/externs/olx.js index 390491c0f4..1096b44768 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -4334,7 +4334,9 @@ olx.layer.VectorOptions.prototype.source; /** - * Declutter images and text. Default is `false`. + * 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. Higher + * z-index means higher priority. Default is `false`. * @type {boolean|undefined} * @api */ @@ -4510,7 +4512,10 @@ olx.layer.VectorTileOptions.prototype.source; /** - * Declutter images and text. Default is `false`. + * 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. Higher + * 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 */ diff --git a/src/ol/render/canvas/imagereplay.js b/src/ol/render/canvas/imagereplay.js index db4b561d43..fb021f27cc 100644 --- a/src/ol/render/canvas/imagereplay.js +++ b/src/ol/render/canvas/imagereplay.js @@ -23,7 +23,7 @@ ol.render.canvas.ImageReplay = function( /** * @private - * @type {Array.<*>} + * @type {ol.DeclutterGroup} */ this.declutterGroup_ = null; @@ -220,7 +220,7 @@ ol.render.canvas.ImageReplay.prototype.setImageStyle = function(imageStyle, decl var origin = imageStyle.getOrigin(); this.anchorX_ = anchor[0]; this.anchorY_ = anchor[1]; - this.declutterGroup_ = /** @type {Array.<*>} */ (declutterGroup); + this.declutterGroup_ = /** @type {ol.DeclutterGroup} */ (declutterGroup); this.hitDetectionImage_ = hitDetectionImage; this.image_ = image; this.height_ = size[1]; diff --git a/src/ol/render/canvas/replay.js b/src/ol/render/canvas/replay.js index ad467e3e8b..a063e33355 100644 --- a/src/ol/render/canvas/replay.js +++ b/src/ol/render/canvas/replay.js @@ -156,7 +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 {ol.DeclutterGroup} declutterGroup Declutter group. * @param {number} height Height. * @param {number} opacity Opacity. * @param {number} originX Origin X. @@ -383,7 +383,7 @@ ol.render.canvas.Replay.prototype.fill_ = function(context, rotation) { /** - * @param {Array.<*>} declutterGroup Declutter group. + * @param {ol.DeclutterGroup} declutterGroup Declutter group. */ ol.render.canvas.Replay.prototype.renderDeclutter_ = function(declutterGroup) { if (declutterGroup && declutterGroup.length > 5) { @@ -537,7 +537,7 @@ 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]); - declutterGroup = /** @type {Array.<*>} */ (instruction[6]); + declutterGroup = /** @type {ol.DeclutterGroup} */ (instruction[6]); var height = /** @type {number} */ (instruction[7]); var opacity = /** @type {number} */ (instruction[8]); var originX = /** @type {number} */ (instruction[9]); @@ -565,7 +565,7 @@ 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]); - declutterGroup = /** @type {Array.<*>} */ (instruction[5]); + 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]); diff --git a/src/ol/render/canvas/replaygroup.js b/src/ol/render/canvas/replaygroup.js index 3ad642adab..92cc0f595c 100644 --- a/src/ol/render/canvas/replaygroup.js +++ b/src/ol/render/canvas/replaygroup.js @@ -41,13 +41,7 @@ ol.render.canvas.ReplayGroup = function( 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.<*>} + * @type {ol.DeclutterGroup} * @private */ this.declutterGroup_ = null; @@ -224,7 +218,7 @@ ol.render.canvas.ReplayGroup.replayDeclutterHitDetection = function( /** * @param {boolean} group Group with previous replay. - * @return {Array.<*>} Declutter instruction group. + * @return {ol.DeclutterGroup} Declutter instruction group. */ ol.render.canvas.ReplayGroup.prototype.addDeclutter = function(group) { var declutter = null; @@ -297,7 +291,8 @@ 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. + * @param {Object.} declutterReplays Declutter + * replays. * @return {T|undefined} Callback result. * @template T */ @@ -426,7 +421,8 @@ 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. + * @param {Object.=} opt_declutterReplays Declutter + * replays. */ ol.render.canvas.ReplayGroup.prototype.replay = function(context, transform, viewRotation, skippedFeaturesHash, opt_replayTypes, opt_declutterReplays) { @@ -479,7 +475,8 @@ 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. + * @param {Object.=} opt_declutterReplays Declutter + * replays. * @return {T|undefined} Callback result. * @template T */ @@ -527,7 +524,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 7148e439bd..e480dffe0f 100644 --- a/src/ol/render/canvas/textreplay.js +++ b/src/ol/render/canvas/textreplay.js @@ -32,7 +32,7 @@ ol.render.canvas.TextReplay = function( /** * @private - * @type {Array.<*>} + * @type {ol.DeclutterGroup} */ this.declutterGroup_; @@ -401,7 +401,7 @@ ol.render.canvas.TextReplay.prototype.drawTextImage_ = function(label, begin, en * @private * @param {number} begin Begin. * @param {number} end End. - * @param {Array.<*>} declutterGroup Declutter group. + * @param {ol.DeclutterGroup} declutterGroup Declutter group. */ ol.render.canvas.TextReplay.prototype.drawChars_ = function(begin, end, declutterGroup) { var pixelRatio = this.pixelRatio; @@ -454,7 +454,7 @@ ol.render.canvas.TextReplay.prototype.setTextStyle = function(textStyle, declutt if (!textStyle) { this.text_ = ''; } else { - this.declutterGroup_ = /** @type {Array.<*>} */ (declutterGroup); + this.declutterGroup_ = /** @type {ol.DeclutterGroup} */ (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 076a2fdb48..bfeab295e7 100644 --- a/src/ol/render/vectorcontext.js +++ b/src/ol/render/vectorcontext.js @@ -123,13 +123,13 @@ ol.render.VectorContext.prototype.setFillStrokeStyle = function(fillStyle, strok /** * @param {ol.style.Image} imageStyle Image style. - * @param {Array.<*>=} opt_declutterGroup Declutter. + * @param {ol.DeclutterGroup=} opt_declutterGroup Declutter. */ ol.render.VectorContext.prototype.setImageStyle = function(imageStyle, opt_declutterGroup) {}; /** * @param {ol.style.Text} textStyle Text style. - * @param {Array.<*>=} opt_declutterGroup Declutter. + * @param {ol.DeclutterGroup=} opt_declutterGroup Declutter. */ ol.render.VectorContext.prototype.setTextStyle = function(textStyle, opt_declutterGroup) {}; 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, From 80e67bac7ae16bbfca31ab15f05c7af741954f74 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sat, 21 Oct 2017 21:12:24 +0200 Subject: [PATCH 06/17] Give lower z-index priority (painter's order) --- externs/olx.js | 4 ++-- src/ol/render/canvas/replaygroup.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/externs/olx.js b/externs/olx.js index 1096b44768..2165836267 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -4335,7 +4335,7 @@ 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. Higher + * 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 @@ -4513,7 +4513,7 @@ 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. Higher + * 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} diff --git a/src/ol/render/canvas/replaygroup.js b/src/ol/render/canvas/replaygroup.js index 92cc0f595c..f811872488 100644 --- a/src/ol/render/canvas/replaygroup.js +++ b/src/ol/render/canvas/replaygroup.js @@ -187,7 +187,7 @@ 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) { + 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++]; From 22e4ba06a6694a75b09c1491efe2fe8f7fdc17d8 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sat, 21 Oct 2017 21:10:25 +0200 Subject: [PATCH 07/17] Add rendering tests --- .../vector-canvas-declutter-group.png | Bin 0 -> 1402 bytes .../vector-canvas-declutter-image-zindex.png | Bin 0 -> 1086 bytes .../vector-canvas-declutter-image.png | Bin 0 -> 1038 bytes .../vector-canvas-declutter-line-zindex.png | Bin 0 -> 1484 bytes .../expected/vector-canvas-declutter-line.png | Bin 0 -> 968 bytes .../vector-canvas-declutter-zindex.png | Bin 0 -> 1369 bytes .../expected/vector-canvas-declutter.png | Bin 0 -> 1178 bytes test/rendering/ol/layer/vector.test.js | 292 ++++++++++++++++++ 8 files changed, 292 insertions(+) create mode 100644 test/rendering/ol/layer/expected/vector-canvas-declutter-group.png create mode 100644 test/rendering/ol/layer/expected/vector-canvas-declutter-image-zindex.png create mode 100644 test/rendering/ol/layer/expected/vector-canvas-declutter-image.png create mode 100644 test/rendering/ol/layer/expected/vector-canvas-declutter-line-zindex.png create mode 100644 test/rendering/ol/layer/expected/vector-canvas-declutter-line.png create mode 100644 test/rendering/ol/layer/expected/vector-canvas-declutter-zindex.png create mode 100644 test/rendering/ol/layer/expected/vector-canvas-declutter.png 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 0000000000000000000000000000000000000000..584b381e7ec19bc0eb0ea6f888809255332ad315 GIT binary patch literal 1402 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8V0H9#aSW-5dwVmtBP3LU{lkCD z@8`NNc`RA8d)KM3?zQKPtjqQG>L`g`Ygd?H=dwO0X{k`!rS1#u8;pd0DF})RM#?WT zSF&#ti4vQZ`uxnBTit9&y$((jFzbA1@%^6qIWz5~Sw`o5&Kxt|Fu&~I^K(D%0xh^F z$y&LQMMPUSkwalK3zJ5J4`W)46h%}`l<9^DowyZKqGI%>e`iv7b|+eBeptiOpiIMt z{#_ii zm^Z)wr78V#%Kj_U@VHQhU)?k4Yi<4dpjF%lW*?g|(P`m@_rL#Td#y}Vs#5P-^ffs- zxxM@7Bd)cjat+P4r|2tlIVxU*9!Xo!6LCnI!!jAs_=WQA0u@opbFf-IKeMpiy zzP@_icF_Yh$v$=*O@eE`nw7^WRh^33ogBp^)_ru*^UC?mFU*dGu(PqHou6a*`DfJH zZKku&)>S?_(&_#E{r&&R5^b7Fzrr-PYIZFOy#HOBK};%Htw|wb(N?qLeX_58mtQUp zT6x9&*o=D$9AQ~&=NIZ4AG>ncAco^Xz}`6d4>tKLgR;7<0~r>TJ=|G%`_=&ip7+X& zre3?1$nf!sOIuspr*)~z{d+phjurmd_x`uMRI=C(pT~RSEf4spUAFzYUxDLY)|NM4 z5~rzp7R|i&D>3=ZgO$f~*;Aq!@5Vi;yl0l|vpu_McURV0bCxmR-^ebVN1T#L?*=R91X+2GIE5-4)@9Lo%q&gey- zw_MvjStZiRrFP!)w|$E;UcPwoVxI!XwVkT5>#skrm{-2~TGis_dDnPLyqAS&-CZ)} z*1ZV3cU`;o1g)$I(-B+$-R6Ds%2ZdE*K4-wn@oF?a@sHUP@&9sm%u5fQ;hBlUzERg zt8!`3OUI>CZnNLj?t1)VmeKsj>QLnEalPFK}%agsF$LU|2P=0!n-}3Y+r=Q;U z+gtzo{rmGzigwQZge)ZGwLv3Iaq(LJCALHSiot?;=If)hZ)qTzh!jTTly{Xk{ZK)rdLa+ zd6`&7EN9%6{$S$%>2Ghwm6$dhXDrQ~3{<`5GDG>i>dHygN|qceBz31|6m9ThzH|M? z;~tX@D~-MS&Ax39|Id1%R{n?ag1Ed_0=j&q5zKertl)W&>9DuDZS{xTUfWM?V%8?M zG3-o!O7B=cbpHO!+^~CtPEr3}hA`$=pO&THZcAtf%B^E9;AZ&ld`XevJ)`>m%1mb0 yV&^i0gks@e>w}q2h=@Bdrf?ZNzCoa$L-*>+H+}xwZ4~pJD`ZK@y-~QkG-`l>g>=!ue z>JalvK$u~!B1Z$`1Ws^6gd`Z|vdsD9k-$(QRB%eUkz`3pp%vW?rU!1# zx-Ng<@Bxm3)xYeRjGoPLpZ%n+KbYZ4dxP)+?E~fqu20tgDR!k{9rLyD`)@xRc;wI4 z5Ruqs(_hL^%KpLZgV2fTXV3eXGfr=in{fH;>suE$$MlCbSn>bhkzlP{yF=HO^$**n z@I7u?sZ#w+S=YA&CUBpK{Qu=@-Kpb^?&;^Z8qRfR{$YNC$$rNzu3r`(SWiqUdM&mx z%=6y-l{Qibrl0nV+iQ83bq(9i*dt|?Qnt)rmVcU2p}2N><93I2)7Y0x^_ptmlXt(g z_SwfE<`}L&44c-7-hTRJtxjkiQ)GsvC>f3;r8k`%o|nIa(6Pwv4l0q z_`Q1e|B)5L9hRR8Un_Pp)UnKA{%Nsw;m2P;-yJSy{3CaRtw>$?!19Lg4)gqMiSMd{nt|UKW*s*-qimh7{j)g^X^EJkJT4p3Q{5tWYSu=9pr6XUq zewFL^%QR{06^2~-3t|_pPqv(~aYNoZz4WS}-hAszdlkgKPnvzpuSEUCy-D2LBmC~O z=Pmh^!SJ^6lCgsQHjB-YyRy|2%9(S2y}ahH3yivq{}F5-HBNZiA34SLYPJLWiT(eo zQbRnRZ+s@T;_m_B17Q>HN6+6DrCZ(b*|@_tU{>qen*Oiq!t+?|WcPnCW>|DKFkts_ zTl+d07rT7F%j@EzRvpj$*ZMW~fZo5d=LUs#$L_rixOnQ-&KbO>&y(349dE=C%e`xgumfjz9IvavI{h$ b{b%M`^yA0M@9fdQa)H6q)z4*}Q$iB}k+b7V literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e3618096304af672eceedcbc80d950b200eaef96 GIT binary patch literal 1038 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8V7}_<;uum9_jXp~3g1A9Z-5Y<~?zY zi26OR_+24y`TWmsw|;y1ZLQVY;Pk)yx9*Gozx7;Hw*JAcu7(v?)Q&M|`6e*%E|}C1 zl*eb%;Kk&%)A9m?7N^G3XAaCOR2EE`%}lbS=`)Ax!V>&G8oz7Uelhenw6{K2p78$E z9pj1DIUKG_Tkz;G%wy<}w*MqDhuunWMo7i4_r40+#tRp41Tiz*Q z1E~!hPhzTKx0x2Ryx7H*-jFsS^y>1b=a}Rj!@}hkSEc$f%$0zB!JzP-_=I;=1y2RezcWFCl{Ao;djbG6hv z$qhVDT*Czai>Zqp@IBBW;k?S!^EyWiW9BOk2I+=qh2zd@pR04Wvr2Gn(oLDYQRW3d z`?O|ziH^`aISJw??o7}<##CWiqEzoxbhzQVf~^&kLw(tPt$1cKKN2`NpLR`p(-V zPdCOktXH_}q!zr5?F~x}^T)Srx)T|G$;^r8Hfy@WF)L8$POgT|oxf2A8J!#ko^RAU z%H0n;lscOd>!(aB*mZm zFLH0rIb-p`{_s-&|c<`-Ctj*rZ=1{n8!IMZ|0Ac?1e8i zEcPGYd#6a&MQ_>Vl=#(b4AbtE2z;2l|GuTj`{O^B?+-EAQ=p$#bo1{Fwh5*;l~?K{ z@iOW(%0!nrpT2)Zig}OzoikMzs+_gg25U11WxkbSiOA&odCk1R>mM7X`2aNs{o~$~ XusqPlUQ8F5&lx;j{an^LB{Ts5`W?C2 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b4a115521f92e48f988faf736d55a43eb9efae97 GIT binary patch literal 1484 zcmcIk{W}u~9N#powWXo+GDSV-^@wD>IAdejO4%c>*p#UWQ||#d5p<;qHg;=lj$5`#hhQ=aWM?;|+rtLjV8(43ES5 zsW;)@0c)u-AHG@-0B9ECu^4~a1z9l>cPCJvw@o}P>_b>|hrp+@?m}I%{c$Im7hc^e zBAY=n%yg%v7>v1(^&sv@wP8s~&in!J`|id?j8mbDHpt%k?3}(9DhMAcJywS_+>c_F z??rZx+H0dWx9gv$O~fxRr)oK4K}@UqP%Cs52toitc7i3*l^XiGdwM^iHLbokW7aYt z1RUD%+>X7KH?KQ1XxX3-07FSI|Lg7=T`k6s0a>8TE6D%nkc&iTzhMgu3ms4>Rqo2D zTY$K~pCWFm+ut@e?v@f-0i!yX;aOxA&}`MaE|p5vuRZsyx9`pmTq0eE@-3FB6kcjh z^n2Jll>iE41(2D78ZMXkr5O^)$||(<{oKLW@mqfWw?lSdN|5?X9Mjs7!{Km^!|wx9 zi$|54UzV?|&75|jtpMO~IG@k&c$K)IaFj?SE!Skm<*1Y!@7@t2$mGK&(e3s1u)MrH zm*sa=9yHZgL(EX^=0@n78?YFoKz#V8rvY1m{{ETs z^U+i)wX(Welg(!H1%j#V+1c4OlO$D)xCM?vp$@I{sm}3~=xEm=QrBRluPPf5RGt|L z7!cOf)HK1$%F3YRON0-go38f3ueyI4Y!u1bRfjIVW};Fy9OP;%*WRNuY#%PqdBH;* z6M6ajQez#TG&Jmv9Jt@9PL1KD7&ByoKwN#2v@(eSX=+X_jx<+LlDn=u?nfv-_v{+K zEtN{ov%@r*hucv@b%7ZI0sh-IJDjHwr)+N6#b4GqjzsqKDwWD9HSd<6rH~gfBVCbF zXorre>!gHN&~D*4ED|APGFeOF!gnnDivv8+2+kXDk2cMR+kPzIa@|a=n-_X`yfvXh z3owE%7(l1{kw|ZKfX?1grGpQ^`kjwFb+OJAzQw3l1i0+Z*w~nrogEo57qbfrjZhpd zh2in|mDSY}OAzn@D0N(Qqw!u9l{U6d7yKevh literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7cb8c929d0cac52d9316dab71c1ab312a8ee7894 GIT binary patch literal 968 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8VD9&HaSW-5dppB>g>RzB@%q@@9rd5 zsiN9?$#vbFrL`{izS(8(e!hE6{+_z~|E}f#|90#%;`k7nsg4i8X{2 zn+85lVdT8hc_1=D@x;^k?~CUi&`e-CvD)hQIbLp;1l`J$k1f?|(|<8auq=8x^N+T( zpak>l`uTg_&%VC&fYOAgKeIjUlNp$@<$InK-kDim!2P`Kj z-;BPbmmqWE*qaisC?4_V};MJvOOmEpUa)#RJ&&` zmt(>v$2jguDu$0Q>^yMoSH8os(|?!Pur#ta$W3UzdHW*o0f`B|H*eN-2(YYUIo6zW z{@$c_zqhQMmXdsT!pG(V7JbXRPJGGQYPD;B*p?K}+g(Sy=f_5Ddvo*emg{%+*Ox^5 z`xj@Ie_tz{Dd`;>9DG?gbZ>-?*!p_g^Wu$Dy;8TWl4DYf)C=K{UVErVQgFJS>iWYa ziz9kE9)J1zGR-%%G~DHyOJIoCD%HNniK}*byG-KEJuLU~-aR!dt6MiWKDL`XSHJ$| z$)BIkmX!J~3fbkf@WO`AETLDKl{Q75dRjEQLuGqWf!6=}jUJ-ga%LVlG4Xw7W~F=f z>u5Fqy2Fo;JMXA0);K?(|JkwLg>UXwzt>-<^z}-ugQ4ex#DvhSHS%2EOB1J@K6__p z@PmWROpD#V8m6AgzID5LroFwT98a2zCyO1+q{{;GH+gNmSj@gyX#AdaSjnbw+wVZ{ zgo5`D(f)tEuHS16ZhYdf`C6MF+i-f}+j zPie^f|IPo&@1y3A;wuz5{yg6Q!68HGh^g6|`{pi^U)P*uy_Wn|`xq-#lJpw1?Er&Pgg&ebxsLQ0M3xP=>Px# literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..dd44e26090a2b211d9aa4f7af11288e8bbb59e29 GIT binary patch literal 1369 zcmchX`&ZI;9L7Ir6sH|iS`rb3N47$28V1zJ%*@&=&?bl+uRilI(V9?HwQ zIkTpjMe4DyMnQ<$piR@7X|4oQlggD{5f1I(W+zyTd((*fS1fsV@VCcTBbj&aiJ9(~ZthO6 zy)6<2$SG90Dk1s}IvBelp&`vn$rE@5^OWDjddG{=F(hgi?+mUZ27QsjbJxShj93>R zf}3*SRO@XOj5$AYlpL3AT9JNCEK6_3K`4fth)eF${%k6M! z&i(*iGlJv00uku1KB}l`HocwdN>LRl@RrQkrGLDu z^1x(?k*Tv1{i``RsB@vZXl(Z(~w?#0huL^S3q8b)~bbZ;Q5*?jrT$2Lym26Ek9$IE(ze%|%< z_4(;AobL{Xf8Th7f+^Iijdn$vr_84}b$fbV_AiR^c*)eXd;tv+*VQqM!>(%MUQ|+~ zqj0@Lf4!cGfp)Dd=~}&~k$Y0{gUYzp<`3&Q_j;Z93%DYkqcF#ZIwU0^nS+?|PA$1d%YI*(Y)!B27voTudKoq!15G<3`Xd}F& z9ef|(yA#v(D}57w4Xb|`>ULoFm(NWR%Ildz;ooe;dKz3ijC&9z=*==o;Vj|=*>m^_ zwR=eVWUzev2y2r!HshLc?E{{fHEjf7!1SP9@o%7px6l3O?v^TvK1aVqR%2T8Y1#wR z2f3cE+QB#7I-xjamC0Eb`a{{O8*%yh3+P5Jm44?DuI;BAsc$AHJHouITG~r<)rHWO zE#>3tiA-IZ_Q#~8+JlD<&0I+9*Ug^aoYdr@9ws{Vbda4W1-nopKH;;WXVIK%X^1eC Tal5+GF_|DdI0|YGN-F#hdRJ3t literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d92be539df00973d822ac49cb1ada2d4b3388ee3 GIT binary patch literal 1178 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdoh8U`h0JaSW-5dwVT5Lnc&$?ZNjs z#@4%e*LCRr%)BgQnlX!a_DtCTXAO>5a~1A&dg**!uCP?BzDm7GltsU*qbl{IlgQqe zOGQ|>dTjkSXY=eOD>%H4bT=rJ@=o;ocBZCCWnzE`cf43n4RiI@&+}$}K2v?Jr%6VK z@u-)#1k(wfMg}g0;0DR*Y;>aThw=JMdLp&-%D=MRwQ>xG&-bZ*%w@fiwe=hGj~kZe zObhM^F+NkMWuG%^)-CDdAEUiiGP{`YC!Dj~ej&qT-Kwiu`ivDE4zoMt3eVk4+guqd z;;P>9qIm7s(+ql|u7<(WQq8*-efC=E$dDhM{e0HhXB8r@`5H^pI$p%S|9i2uwRQev zm7USqcNqj0q@rrmxcX#)49$wyB{s%G*>4*9|-W8mCdvennXP4;=GC^K< z*&m#=IltcgSd6UeMOLLvUMrJbzHAA~N~)~1l$DgMlsHh#@Q&$&Zo_eRo{B8thBV$r0;R;RIjJX*>5vu zEo0w$O}}kgWOjArw71M3lGk6KZE)<2*3vi*ey)c1;&Bq&*Ylp)^u|!h>cyPdi~%`5 zDOFXw1f$A+9_e^d5h(KBHSkO0*0TH)Yv%tBWmpro`mpr>rJ27TRGrq7f94Q5%{+7M z{7u*Va`)xgEBxk?_@DGSEFDcgPY!Gi}qL9e9W z|K0kZsp3|Y^~}_pi46VayYDVDRO@?e(Y5HswW(95^1uH2i}^qI0k_45F{`gmW4x2U zws!f|thoVRvgbbU30xaCz2n7-;&)$lPNy-}Fe!Y#_q?7xVp?j%qTO4srJvq3M`e*= z@|no@f8YH1^JjYIEICf^UDsc~H9B@@^;SE%1=~xR4^PV68ueXcso#42`5)Z&?!El7 zW|qpyxyNRFH=iadC|h`L@2-6*|8s6WQ;iWh+VOo~+}g1H%8TyLDc&8u_L{KQ$_@N& zhs9kBqnBT{oWAU(;@Yo^uig5P@b8w@w!1?2c0BxWQt0@{zOvnaPd|IV{o}`vS$pH| zuiX0YYSi!FqJ9^8dV52sO`CQ|S|%}C`vkN&q oJ1pqHn8Ic7_!9jnwFmN>`f3v-r=9%-EOQt Date: Sun, 22 Oct 2017 12:51:40 +0200 Subject: [PATCH 08/17] Use existing sort function --- src/ol/render/canvas/replaygroup.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/ol/render/canvas/replaygroup.js b/src/ol/render/canvas/replaygroup.js index f811872488..f06ce60b7b 100644 --- a/src/ol/render/canvas/replaygroup.js +++ b/src/ol/render/canvas/replaygroup.js @@ -485,12 +485,10 @@ ol.render.canvas.ReplayGroup.prototype.replayHitDetection_ = function( 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) { + 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) { From 53a2cf55f134f52f48e665e106802a419cdde0d4 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 22 Oct 2017 12:55:23 +0200 Subject: [PATCH 09/17] Fix hit detection for decluttered layers --- src/ol/render/canvas/replaygroup.js | 53 ++++++++++++++------------- src/ol/renderer/canvas/vectorlayer.js | 5 ++- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/ol/render/canvas/replaygroup.js b/src/ol/render/canvas/replaygroup.js index f06ce60b7b..c3ec7b4666 100644 --- a/src/ol/render/canvas/replaygroup.js +++ b/src/ol/render/canvas/replaygroup.js @@ -201,11 +201,11 @@ ol.render.canvas.ReplayGroup.replayDeclutter = function(declutterReplays, contex 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) { + 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++]; + 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) { @@ -327,33 +327,34 @@ ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function( var mask = ol.render.canvas.ReplayGroup.getCircleArray_(hitTolerance); - var result = 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, callback, hitExtent); + declutterReplays, context, rotation, hitDetectionCallback, hitExtent); } return result; }; diff --git a/src/ol/renderer/canvas/vectorlayer.js b/src/ol/renderer/canvas/vectorlayer.js index e8c3a3d0f0..574f2b98fa 100644 --- a/src/ol/renderer/canvas/vectorlayer.js +++ b/src/ol/renderer/canvas/vectorlayer.js @@ -234,9 +234,10 @@ 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 = {}; + var declutterReplays = layer.getDeclutter() ? {} : null; var result = this.replayGroup_.forEachFeatureAtCoordinate(coordinate, resolution, rotation, hitTolerance, {}, /** @@ -249,7 +250,7 @@ ol.renderer.canvas.VectorLayer.prototype.forEachFeatureAtCoordinate = function(c features[key] = true; return callback.call(thisArg, feature, layer); } - }, null); + }, declutterReplays); if (this.declutterTree_) { this.declutterTree_.clear(); } From 912e9080320ed4cf44173e2ea518e7df3239fb5e Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 22 Oct 2017 14:30:35 +0200 Subject: [PATCH 10/17] Test decluttered hit detection --- test/rendering/ol/layer/vector.test.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/test/rendering/ol/layer/vector.test.js b/test/rendering/ol/layer/vector.test.js index 644391235f..bca13ea6ad 100644 --- a/test/rendering/ol/layer/vector.test.js +++ b/test/rendering/ol/layer/vector.test.js @@ -477,10 +477,11 @@ describe('ol.rendering.layer.Vector', function() { }); map.addLayer(layer); - source.addFeature(new ol.Feature({ + 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' @@ -501,6 +502,9 @@ describe('ol.rendering.layer.Vector', function() { }); 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); }); @@ -553,9 +557,10 @@ describe('ol.rendering.layer.Vector', function() { }); map.addLayer(layer); - source.addFeature(new ol.Feature({ + 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]]) })); @@ -576,6 +581,9 @@ describe('ol.rendering.layer.Vector', function() { }); 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); }); @@ -747,6 +755,9 @@ describe('ol.rendering.layer.Vector', function() { 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); }); From 29ebdde3142aac39b2c766ea9f1869986d365642 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 22 Oct 2017 15:02:55 +0200 Subject: [PATCH 11/17] Test decluttering for vector tile layers --- .../expected/vectortile-canvas-declutter.png | Bin 0 -> 2981 bytes test/rendering/ol/layer/vectortile.test.js | 37 +++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 test/rendering/ol/layer/expected/vectortile-canvas-declutter.png 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 0000000000000000000000000000000000000000..2f44b10f35a3a449df22e8f0c8d5dbb94cfab942 GIT binary patch literal 2981 zcmds3`8U)J7yo7*`%KnknHdiu5nlT|*6)){5gvdQd(XZ1e9p~wb#asuR~82VK+5SPh~e9S z-vSZg>qvI72LMQ1bpq`?5-zTg30GBmYXHf*A527h2RedxMBZ)seUBlal(NC=R$W#j_+ zd2{VfQEw6iXbM2Xq3h@|1W@5q(!mw-S2encFe$ffef7*DfauyQ1z{TA)D2Y?NJ%7a z9Iw8YZw8HbQUGpixE>P)z9cm~IbRtie2^Y+2$*~(sx8TJgnXNTrIX*C?o~Yrxl#7Y zUxs=TD+=^~ojO$P_WWGBhrWWo!U)RbhP&73wavP@4c!8#KgZrJ&g&H@i+RlJwG zcvYtT57ECHdpj_$Cgq($tyP(E;2_aMw`YxdX4ty}H7Pvu<$ElMz1tVC`RhN!_fuwU zBmjMuw?S}3#Ed$iY-wfHb|Ps%Ae?Di4y2DYkB*(43G(sraX_gN)B%_G-Zo~X5XPE; zPO&#DmYv;q7G$TxzW%3w;$w?su2y8cI5k=JtJ;e^tO~Qr2d*?#JR13; zp~_6uE$LsWaMn@>(ldz|Do?u*a}fX(`Lo<%bg005h#1M|+svm^^7L4zIJMA-hzj+b zo{NE09g2%U`ufQezQ-nFV`H0X4Q){;D^SU$&_QtxhRVzt&3sS6aovnoI~|jf2v;;s z2F5-5!M*?Udi+@2*5RXr@_x5yjE)XM7!HT4U7{0Yh71{NjkeuZHORh*8O95fVstRl z@>o$Hs{a=c6+wftjYN=?-2C}q|FrCX6cXrD#wGMF z>Xh`qiYgs}a^LtrZT0KADHV}V%p$JeMypK;XJpZ#+<1zI1VF4QvJ^x-tk2^tYh=ux ztCLa2OlL{$ViNzZgJ9pQWJ{bQjgs>qeBvA z&?sOO6XfudfdaK^D#`hV*aMoRV?IFSD=C@d^2HydQ1#=ZRl6v*1w8TaZ5q?7Eku2y?^7xaIg4h;{V zSNYDrB8E~lJ%t#+7RE1a6#LpRH8pi17LQk~2`|NQf{8sgT}m;gR&|w? z95^#d4msV;>{#%pJF{zG_=t3mSlWJ&OY*&Xt)1UP6o6? zBwf+GM4(d+O!=VH#Y}d9*bFT2WO_MK4UwWeZM2U19N5sp=;O6F-GFXnoV$#ehqA#x@Gr?D{mRdPWUkXcm*s}k0ArU1p`M9Ngz*t)553rOpaK-oc9z#Mx zYU+NCZZbNI-d2Y02T}%e{=`Wdyt96Mae07jW>Dy?6$o|vcYJ)@-Q;{}B5%IOG-CSg zWxZx9wJLsflC-wEx*Ezq2j1)PXP5g4uZHoXgJo7bZFKr}t>ah>EFmSE^192(S0ux-VTDb35cb{>0*cc!?^96 zvNWuIqk?^k49x14N>ZKS74i}trJ;hznAK7BBL7$#i%`9H#DW>cj=!*h&r0ynJb`=r zp5#0)GMIfy?S<}}d>0W_d$2LUh}3e)Y3~Sdo;5LH)kLFhNrKoHIA)%~O4}M^O&RC% zWo>Oi7gp5w$sT+JhtN4(^EUPcB>S&rKI6Q`>f?jl-Q7bLxtQu$I-lUz{{w@4D`lTNY+TeJP##S`RO} z2qK_^m17#I+q5HunkH-iHR@4|fJl}urCk6>NR7=L-QKM^Yz?AGn#ifKF)EVeY$^+z z8OjB}w?o;qXj=Pm@<8}c)LJZ}dw_!Z5-=7wCiSP9ke#JMjv*?oE~bRI4gqGXge%`;L7Y6RQR)H}r6RfU<*Ld7S= zyUep=zL{1HEonw&F6s;BgEKWBeIK>VSIJduoxQ!i0F2A~w%*!2LAfV>^ra1wJ&vtE z$a;rXQx(rJc%wN<`IR|ylFK1^_4pfh1dnJn_{Z;|hB)Gp;y}t{;zzG7=4xJy3)wX6 zqa>o6`F8ByMxC7y2r@4piG#?@T#Y%C{@5;6*cb8u&pA zG@&^)MGd-I+kiKgy7Q)Dwdhkcbv9>bN?gybx_IAq8EEgHFftCY~0f9Oc2WsCC(o_ z!zBWgmgmr^_Rphz;BDMXi^La<;t7HAuI^SUl@H0@u8t?TNpuTj-xYvKFfISDoQ013 XC&@VoSd|6;{RW)=aseCc{jU8N%kgzc literal 0 HcmV?d00001 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); + }); + }); + }); }); From dfa3cbf4e263c9532c46e59749039822ba1bfded Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 22 Oct 2017 15:51:22 +0200 Subject: [PATCH 12/17] Use native decluttering in vector-label-decluttering example --- examples/vector-label-decluttering.html | 6 +- examples/vector-label-decluttering.js | 122 +++++++----------------- 2 files changed, 37 insertions(+), 91 deletions(-) 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); From 4a73754b93ca43b66d1c7a2678eec829930b7cec Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Mon, 23 Oct 2017 11:38:53 +0200 Subject: [PATCH 13/17] Entertain the compiler --- src/ol/render/canvas/replay.js | 11 +++++++++-- src/ol/renderer/canvas/vectorlayer.js | 2 +- src/ol/renderer/canvas/vectortilelayer.js | 2 +- src/ol/source/imagevector.js | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/ol/render/canvas/replay.js b/src/ol/render/canvas/replay.js index a063e33355..4550547397 100644 --- a/src/ol/render/canvas/replay.js +++ b/src/ol/render/canvas/replay.js @@ -389,8 +389,15 @@ 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)); + /** @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]) { diff --git a/src/ol/renderer/canvas/vectorlayer.js b/src/ol/renderer/canvas/vectorlayer.js index 574f2b98fa..dc25ba9dac 100644 --- a/src/ol/renderer/canvas/vectorlayer.js +++ b/src/ol/renderer/canvas/vectorlayer.js @@ -29,7 +29,7 @@ ol.renderer.canvas.VectorLayer = function(vectorLayer) { * @private */ this.declutterTree_ = vectorLayer.getDeclutter() ? - ol.ext.rbush(9, ['[0]', '[1]', '[2]', '[3]']) : null; + ol.ext.rbush(9) : null; /** * @private diff --git a/src/ol/renderer/canvas/vectortilelayer.js b/src/ol/renderer/canvas/vectortilelayer.js index 1d2c5827a4..cdf2186d18 100644 --- a/src/ol/renderer/canvas/vectortilelayer.js +++ b/src/ol/renderer/canvas/vectortilelayer.js @@ -38,7 +38,7 @@ ol.renderer.canvas.VectorTileLayer = function(layer) { * Declutter tree. * @private */ - this.declutterTree_ = layer.getDeclutter() ? ol.ext.rbush(9, ['[0]', '[1]', '[2]', '[3]']) : null; + this.declutterTree_ = layer.getDeclutter() ? ol.ext.rbush(9) : null; /** * @private diff --git a/src/ol/source/imagevector.js b/src/ol/source/imagevector.js index d310285a1f..66a15f2fe6 100644 --- a/src/ol/source/imagevector.js +++ b/src/ol/source/imagevector.js @@ -60,7 +60,7 @@ ol.source.ImageVector = function(options) { * Declutter tree. * @private */ - this.declutterTree_ = ol.ext.rbush(9, ['[0]', '[1]', '[2]', '[3]']); + this.declutterTree_ = ol.ext.rbush(9); /** * @private From b29e74d1abe79c8ed66dc289bbe9f42dce356c49 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 31 Oct 2017 08:48:02 +0100 Subject: [PATCH 14/17] Measure height only once per font --- src/ol/render/canvas/textreplay.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/ol/render/canvas/textreplay.js b/src/ol/render/canvas/textreplay.js index e480dffe0f..43462563f9 100644 --- a/src/ol/render/canvas/textreplay.js +++ b/src/ol/render/canvas/textreplay.js @@ -135,18 +135,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; }; })(); From 6ce201c429a79c6418082f9d2d694c230600b055 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 31 Oct 2017 08:48:42 +0100 Subject: [PATCH 15/17] Do not render text along oversimplified geometries --- src/ol/render/canvas/textreplay.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/ol/render/canvas/textreplay.js b/src/ol/render/canvas/textreplay.js index 43462563f9..6677bc2b05 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'); @@ -219,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(); @@ -248,7 +252,10 @@ 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.declutterGroup_); begin = end; From e6ae029d08df8d2dd7d43ce2d40d74d700485573 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 31 Oct 2017 08:51:15 +0100 Subject: [PATCH 16/17] Improve text rendering performance --- src/ol/render/canvas/textreplay.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ol/render/canvas/textreplay.js b/src/ol/render/canvas/textreplay.js index 6677bc2b05..5220b4a3d5 100644 --- a/src/ol/render/canvas/textreplay.js +++ b/src/ol/render/canvas/textreplay.js @@ -322,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_; @@ -333,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); @@ -351,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; } From 306851d6ee4c802f5dece7250919884acc2a8ea5 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 3 Nov 2017 10:36:28 +0100 Subject: [PATCH 17/17] Consistent naming of declutterTree --- src/ol/render/canvas/linestringreplay.js | 6 +++--- src/ol/render/canvas/polygonreplay.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ol/render/canvas/linestringreplay.js b/src/ol/render/canvas/linestringreplay.js index e426725eab..9862317eea 100644 --- a/src/ol/render/canvas/linestringreplay.js +++ b/src/ol/render/canvas/linestringreplay.js @@ -17,13 +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. + * @param {?} declutterTree Declutter tree. * @struct */ ol.render.canvas.LineStringReplay = function( - tolerance, maxExtent, resolution, pixelRatio, overlaps, declutter) { + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { ol.render.canvas.Replay.call(this, - tolerance, maxExtent, resolution, pixelRatio, overlaps, declutter); + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree); /** * @private diff --git a/src/ol/render/canvas/polygonreplay.js b/src/ol/render/canvas/polygonreplay.js index 9a3679bc16..8d6f4192af 100644 --- a/src/ol/render/canvas/polygonreplay.js +++ b/src/ol/render/canvas/polygonreplay.js @@ -19,13 +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. + * @param {?} declutterTree Declutter tree. * @struct */ ol.render.canvas.PolygonReplay = function( - tolerance, maxExtent, resolution, pixelRatio, overlaps, declutter) { + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) { ol.render.canvas.Replay.call(this, - tolerance, maxExtent, resolution, pixelRatio, overlaps, declutter); + tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree); /** * @private