diff --git a/changelog/upgrade-notes.md b/changelog/upgrade-notes.md index fd31ad4fec..d150d3c48a 100644 --- a/changelog/upgrade-notes.md +++ b/changelog/upgrade-notes.md @@ -98,6 +98,14 @@ If you were previously using `VectorTile` layers with `renderMode: 'vector'`, yo If you were previously using `Vector` layers with `renderMode: 'image'`, you have to remove this configuration option. Instead, use the new `ol/layer/VectorImage` layer with your `ol/source/Vector`. +##### New declutter behavior + +If a map has more than one layer with `declutter` set to true, decluttering now considers all `Vector` and `VectorTile` layers, instead of decluttering each layer separately. Only `VectorImage` layers continue to be decluttered separately. The higher the z-index of a layer, the higher the priority of its decluttered items. + +Within a layer, the declutter order has changed. Previously, styles with a lower `zIndex` were prioritized over those with a higher `zIndex`. Now the opposite order is used. + +On vector layers, even if decluttered images or texts have a lower z-Index than polygons or lines, they will now be rendered on top of the polygons or lines. For vector tile layers, this was the case already in previous releases. + ##### New `prerender` and `postrender` layer events replace old `precompose`, `render` and `postcompose` events If you were previously registering for `precompose` and `postcompose` events, you should now register for `prerender` and `postrender` events on layers. Instead of the previous `render` event, you should now listen for `postrender`. Layers are no longer composed to a single Canvas element. Instead, they are added to the map viewport as individual elements. diff --git a/rendering/cases/layer-vector-decluttering/expected.png b/rendering/cases/layer-vector-decluttering/expected.png index 7c067a9489..55ef4b2926 100644 Binary files a/rendering/cases/layer-vector-decluttering/expected.png and b/rendering/cases/layer-vector-decluttering/expected.png differ diff --git a/rendering/cases/layer-vector-decluttering/main.js b/rendering/cases/layer-vector-decluttering/main.js index 65e809b1e8..d551f268dc 100644 --- a/rendering/cases/layer-vector-decluttering/main.js +++ b/rendering/cases/layer-vector-decluttering/main.js @@ -7,6 +7,7 @@ import Point from '../../../src/ol/geom/Point.js'; import Style from '../../../src/ol/style/Style.js'; import Text from '../../../src/ol/style/Text.js'; import CircleStyle from '../../../src/ol/style/Circle.js'; +import Fill from '../../../src/ol/style/Fill.js'; import Stroke from '../../../src/ol/style/Stroke.js'; import LineString from '../../../src/ol/geom/LineString.js'; @@ -59,9 +60,10 @@ source1.addFeature(new Feature({ })); layer1.setStyle(function(feature) { return new Style({ + zIndex: feature.get('zIndex'), image: new CircleStyle({ radius: 15, - stroke: new Stroke({ + fill: new Fill({ color: 'blue' }) }) @@ -69,7 +71,7 @@ layer1.setStyle(function(feature) { }); map.addLayer(layer1); -center = [center[0] + 500, center[1] + 500]; +center = [center[0] + 500, center[1] + 700]; const feature2 = new Feature({ geometry: new Point(center), text: 'center', @@ -88,15 +90,16 @@ source2.addFeature(new Feature({ })); layer2.setStyle(function(feature) { return new Style({ + zIndex: feature.get('zIndex'), text: new Text({ text: feature.get('text'), - font: '16px Ubuntu' + font: 'italic bold 18px Ubuntu' }) }); }); map.addLayer(layer2); -center = [center[0] + 500, center[1] + 500]; +center = [center[0] + 500, center[1] + 300]; source3.addFeature(new Feature({ geometry: new Point(center), text: 'center' @@ -112,16 +115,17 @@ source3.addFeature(new Feature({ layer3.setStyle(function(feature) { return new Style({ image: new CircleStyle({ - radius: 5, + radius: 10, stroke: new Stroke({ - color: 'red' + color: 'red', + width: 8 }) }), text: new Text({ text: feature.get('text'), - font: '16px Ubuntu', + font: 'italic bold 18px Ubuntu', textBaseline: 'bottom', - offsetY: -5 + offsetY: -12 }) }); }); @@ -130,11 +134,12 @@ map.addLayer(layer3); center = [center[0] - 2000, center[1] - 2000]; const point = new Feature(new Point(center)); point.setStyle(new Style({ - zIndex: 2, + zIndex: 1, image: new CircleStyle({ radius: 8, stroke: new Stroke({ - color: 'blue' + color: 'blue', + width: 4 }) }) })); @@ -143,7 +148,7 @@ const line = new Feature(new LineString([ [center[0] + 650, center[1] - 200] ])); line.setStyle(new Style({ - zIndex: 1, + zIndex: 2, stroke: new Stroke({ color: '#CCC', width: 12 @@ -151,7 +156,7 @@ line.setStyle(new Style({ text: new Text({ placement: 'line', text: 'east-west', - font: '16px Ubuntu', + font: 'italic bold 18px Ubuntu', overflow: true }) })); @@ -159,4 +164,4 @@ source4.addFeature(point); source4.addFeature(line); map.addLayer(layer4); -render({tolerance: 0.02}); +render({tolerance: 0.007}); diff --git a/rendering/cases/layer-vectorimage-decluttering/expected.png b/rendering/cases/layer-vectorimage-decluttering/expected.png index 5c3a16c967..73be4ebb8e 100644 Binary files a/rendering/cases/layer-vectorimage-decluttering/expected.png and b/rendering/cases/layer-vectorimage-decluttering/expected.png differ diff --git a/rendering/cases/layer-vectorimage-decluttering/main.js b/rendering/cases/layer-vectorimage-decluttering/main.js index 05cb3406c0..83428d97f0 100644 --- a/rendering/cases/layer-vectorimage-decluttering/main.js +++ b/rendering/cases/layer-vectorimage-decluttering/main.js @@ -1,72 +1,19 @@ -import Feature from '../../../src/ol/Feature.js'; import Map from '../../../src/ol/Map.js'; import View from '../../../src/ol/View.js'; import VectorSource from '../../../src/ol/source/Vector.js'; -import Style from '../../../src/ol/style/Style.js'; -import Stroke from '../../../src/ol/style/Stroke.js'; import VectorImageLayer from '../../../src/ol/layer/VectorImage.js'; -import CircleStyle from '../../../src/ol/style/Circle.js'; +import Feature from '../../../src/ol/Feature.js'; import Point from '../../../src/ol/geom/Point.js'; -import LineString from '../../../src/ol/geom/LineString.js'; +import Style from '../../../src/ol/style/Style.js'; import Text from '../../../src/ol/style/Text.js'; +import CircleStyle from '../../../src/ol/style/Circle.js'; +import Fill from '../../../src/ol/style/Fill.js'; +import Stroke from '../../../src/ol/style/Stroke.js'; +import LineString from '../../../src/ol/geom/LineString.js'; -const center = [1825927.7316762917, 6143091.089223046]; - -const source = new VectorSource(); -const vectorLayer1 = new VectorImageLayer({ - source: source, - style: function(feature) { - return new Style({ - image: new CircleStyle({ - radius: 15, - stroke: new Stroke({ - color: 'blue' - }) - }), - text: new Text({ - text: feature.get('text'), - font: '16px Ubuntu' - }) - }); - } -}); - -const centerFeature = new Feature({ - geometry: new Point(center), - text: 'center' -}); -source.addFeature(centerFeature); -source.addFeature(new Feature({ - geometry: new Point([center[0] - 540, center[1]]), - text: 'west' -})); -source.addFeature(new Feature({ - geometry: new Point([center[0] + 540, center[1]]), - text: 'east' -})); - -const line = new Feature(new LineString([ - [center[0] - 650, center[1] - 200], - [center[0] + 650, center[1] - 200] -])); -line.setStyle(new Style({ - stroke: new Stroke({ - color: '#CCC', - width: 12 - }), - text: new Text({ - placement: 'line', - text: 'east-west', - font: '16px Ubuntu' - }) -})); -source.addFeature(line); - +let center = [1825927.7316762917, 6143091.089223046]; const map = new Map({ pixelRatio: 1, - layers: [ - vectorLayer1 - ], target: 'map', view: new View({ center: center, @@ -74,6 +21,147 @@ const map = new Map({ }) }); -map.getView().fit(source.getExtent()); +const source1 = new VectorSource(); +const layer1 = new VectorImageLayer({ + declutter: true, + source: source1 +}); -render({tolerance: 0.02}); +const source2 = new VectorSource(); +const layer2 = new VectorImageLayer({ + declutter: true, + source: source2 +}); + +const source3 = new VectorSource(); +const layer3 = new VectorImageLayer({ + declutter: true, + source: source3 +}); + +const source4 = new VectorSource(); +const layer4 = new VectorImageLayer({ + declutter: true, + source: source4 +}); + +const feature1 = new Feature({ + geometry: new Point(center), + zIndex: 2 +}); +source1.addFeature(feature1); +source1.addFeature(new Feature({ + geometry: new Point([center[0] - 540, center[1]]), + zIndex: 3 +})); +source1.addFeature(new Feature({ + geometry: new Point([center[0] + 540, center[1]]), + zIndex: 1 +})); +layer1.setStyle(function(feature) { + return new Style({ + zIndex: feature.get('zIndex'), + image: new CircleStyle({ + radius: 15, + fill: new Fill({ + color: 'blue' + }) + }) + }); +}); +map.addLayer(layer1); + +center = [center[0] + 500, center[1] + 700]; +const feature2 = new Feature({ + geometry: new Point(center), + text: 'center', + zIndex: 2 +}); +source2.addFeature(feature2); +source2.addFeature(new Feature({ + geometry: new Point([center[0] - 540, center[1]]), + text: 'west', + zIndex: 3 +})); +source2.addFeature(new Feature({ + geometry: new Point([center[0] + 540, center[1]]), + text: 'east', + zIndex: 1 +})); +layer2.setStyle(function(feature) { + return new Style({ + zIndex: feature.get('zIndex'), + text: new Text({ + text: feature.get('text'), + font: 'italic bold 18px Ubuntu' + }) + }); +}); +map.addLayer(layer2); + +center = [center[0] + 500, center[1] + 300]; +source3.addFeature(new Feature({ + geometry: new Point(center), + text: 'center' +})); +source3.addFeature(new Feature({ + geometry: new Point([center[0] - 540, center[1]]), + text: 'west' +})); +source3.addFeature(new Feature({ + geometry: new Point([center[0] + 540, center[1]]), + text: 'east' +})); +layer3.setStyle(function(feature) { + return new Style({ + image: new CircleStyle({ + radius: 10, + stroke: new Stroke({ + color: 'red', + width: 8 + }) + }), + text: new Text({ + text: feature.get('text'), + font: 'italic bold 18px Ubuntu', + textBaseline: 'bottom', + offsetY: -12 + }) + }); +}); +map.addLayer(layer3); + +center = [center[0] - 2000, center[1] - 2000]; +const point = new Feature(new Point(center)); +point.setStyle(new Style({ + zIndex: 1, + image: new CircleStyle({ + radius: 8, + stroke: new Stroke({ + color: 'blue', + width: 4 + }) + }) +})); +const line = new Feature(new LineString([ + [center[0] - 650, center[1] - 200], + [center[0] + 650, center[1] - 200] +])); +line.setStyle(new Style({ + zIndex: 2, + stroke: new Stroke({ + color: '#CCC', + width: 12 + }), + text: new Text({ + placement: 'line', + text: 'east-west', + font: 'italic bold 18px Ubuntu', + overflow: true + }) +})); +source4.addFeature(point); +source4.addFeature(line); +map.addLayer(layer4); + +render({tolerance: 0.007}); diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js index 6cf34cbf21..9ab36d6b55 100644 --- a/src/ol/PluggableMap.js +++ b/src/ol/PluggableMap.js @@ -39,6 +39,7 @@ import {create as createTransform, apply as applyTransform} from './transform.js * @property {boolean} animate * @property {import("./transform.js").Transform} coordinateToPixelTransform * @property {null|import("./extent.js").Extent} extent + * @property {Array<*>} declutterItems * @property {import("./coordinate.js").Coordinate} focus * @property {number} index * @property {Array} layerStatesArray @@ -1175,6 +1176,7 @@ class PluggableMap extends BaseObject { frameState = /** @type {FrameState} */ ({ animate: false, coordinateToPixelTransform: this.coordinateToPixelTransform_, + declutterItems: previousFrameState ? previousFrameState.declutterItems : [], extent: extent, focus: this.focus_ ? this.focus_ : viewState.center, index: this.frameIndex_++, diff --git a/src/ol/layer/BaseVector.js b/src/ol/layer/BaseVector.js index fd6255f4a9..b612b5f762 100644 --- a/src/ol/layer/BaseVector.js +++ b/src/ol/layer/BaseVector.js @@ -32,8 +32,10 @@ import {createDefaultStyle, toFunction as toStyleFunction} from '../style/Style. * temporary layers. The standard way to add a layer to a map and have it managed by the map is to * use {@link module:ol/Map#addLayer}. * @property {boolean} [declutter=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. Lower z-index - * means higher priority. + * image and text styles of all Vector and VectorTile layers that have set this to `true`. The priority + * is defined by the z-index of the layer, the `zIndex` of the style and the render order of features. + * Higher z-index means higher priority. Within the same z-index, a feature rendered before another has + * higher priority. * @property {import("../style/Style.js").StyleLike} [style] Layer style. See * {@link module:ol/style} for default style which will be used if this is not defined. * @property {boolean} [updateWhileAnimating=false] When set to `true`, feature batches will diff --git a/src/ol/layer/VectorImage.js b/src/ol/layer/VectorImage.js index 9775ae251e..1b948ce832 100644 --- a/src/ol/layer/VectorImage.js +++ b/src/ol/layer/VectorImage.js @@ -30,9 +30,9 @@ import CanvasVectorImageLayerRenderer from '../renderer/canvas/VectorImageLayer. * this layer in its layers collection, and the layer will be rendered on top. This is useful for * temporary layers. The standard way to add a layer to a map and have it managed by the map is to * use {@link module:ol/Map#addLayer}. - * @property {boolean} [declutter=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. Lower z-index - * means higher priority. + * @property {boolean} [declutter=false] Declutter images and text on this layer. The priority is defined + * by the `zIndex` of the style and the render order of features. Higher z-index means higher priority. + * Within the same z-index, a feature rendered before another has higher priority. * @property {import("../style/Style.js").StyleLike} [style] Layer style. See * {@link module:ol/style} for default style which will be used if this is not defined. * @property {boolean} [updateWhileAnimating=false] When set to `true`, feature batches will diff --git a/src/ol/layer/VectorTile.js b/src/ol/layer/VectorTile.js index 9bf96d8128..067ba0d8e1 100644 --- a/src/ol/layer/VectorTile.js +++ b/src/ol/layer/VectorTile.js @@ -46,8 +46,10 @@ import {assign} from '../obj.js'; * temporary layers. The standard way to add a layer to a map and have it managed by the map is to * use {@link module:ol/Map#addLayer}. * @property {boolean} [declutter=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. Lower z-index - * means higher priority. + * image and text styles of all Vector and VectorTile layers that have set this to `true`. The priority + * is defined by the z-index of the layer, the `zIndex` of the style and the render order of features. + * Higher z-index means higher priority. Within the same z-index, a feature rendered before another has + * higher priority. * @property {import("../style/Style.js").StyleLike} [style] Layer style. See * {@link module:ol/style} for default style which will be used if this is not defined. * @property {boolean} [updateWhileAnimating=false] When set to `true`, feature batches will be diff --git a/src/ol/render.js b/src/ol/render.js index 330e5aafa6..c413506596 100644 --- a/src/ol/render.js +++ b/src/ol/render.js @@ -110,3 +110,23 @@ export function getRenderPixel(event, pixel) { applyTransform(event.inversePixelTransform.slice(), result); return result; } + +/** + * @param {import("./PluggableMap.js").FrameState} frameState Frame state. + * @param {?} declutterTree Declutter tree. + * @returns {?} Declutter tree. + */ +export function renderDeclutterItems(frameState, declutterTree) { + if (declutterTree) { + declutterTree.clear(); + } + const items = frameState.declutterItems; + for (let z = items.length - 1; z >= 0; --z) { + const zIndexItems = items[z]; + for (let i = 0, ii = zIndexItems.length; i < ii; i += 3) { + declutterTree = zIndexItems[i].renderDeclutter(zIndexItems[i + 1], zIndexItems[i + 2], declutterTree); + } + } + items.length = 0; + return declutterTree; +} diff --git a/src/ol/render/canvas/Executor.js b/src/ol/render/canvas/Executor.js index 162eac03a9..be254c38e3 100644 --- a/src/ol/render/canvas/Executor.js +++ b/src/ol/render/canvas/Executor.js @@ -21,6 +21,7 @@ import { import {createCanvasContext2D} from '../../dom.js'; import {labelCache, defaultTextAlign, measureTextHeight, measureAndCacheTextWidth, measureTextWidths} from '../canvas.js'; import Disposable from '../../Disposable.js'; +import rbush from 'rbush'; /** @@ -58,15 +59,10 @@ class Executor extends Disposable { * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. * @param {boolean} overlaps The replay can have overlapping geometries. - * @param {?} declutterTree Declutter tree. * @param {SerializableInstructions} instructions The serializable instructions */ - constructor(resolution, pixelRatio, overlaps, declutterTree, instructions) { + constructor(resolution, pixelRatio, overlaps, instructions) { super(); - /** - * @type {?} - */ - this.declutterTree = declutterTree; /** * @protected @@ -93,6 +89,11 @@ class Executor extends Disposable { */ this.alignFill_; + /** + * @type {Array<*>} + */ + this.declutterItems = []; + /** * @protected * @type {Array<*>} @@ -412,8 +413,10 @@ class Executor extends Disposable { /** * @param {import("../canvas.js").DeclutterGroup} declutterGroup Declutter group. * @param {import("../../Feature.js").FeatureLike} feature Feature. + * @param {?} declutterTree Declutter tree. + * @return {?} Declutter tree. */ - renderDeclutter_(declutterGroup, feature) { + renderDeclutter(declutterGroup, feature, declutterTree) { if (declutterGroup && declutterGroup.length > 5) { const groupCount = declutterGroup[4]; if (groupCount == 1 || groupCount == declutterGroup.length - 5) { @@ -425,8 +428,11 @@ class Executor extends Disposable { maxY: /** @type {number} */ (declutterGroup[3]), value: feature }; - if (!this.declutterTree.collides(box)) { - this.declutterTree.insert(box); + if (!declutterTree) { + declutterTree = rbush(9, undefined); + } + if (!declutterTree.collides(box)) { + declutterTree.insert(box); for (let j = 5, jj = declutterGroup.length; j < jj; ++j) { const declutterData = /** @type {Array} */ (declutterGroup[j]); if (declutterData) { @@ -443,6 +449,7 @@ class Executor extends Disposable { createOrUpdateEmpty(declutterGroup); } } + return declutterTree; } /** @@ -497,6 +504,7 @@ class Executor extends Disposable { featureCallback, opt_hitExtent ) { + this.declutterItems.length = 0; /** @type {Array} */ let pixelCoordinates; if (this.pixelCoordinates_ && equals(transform, this.renderedTransform_)) { @@ -670,7 +678,7 @@ class Executor extends Disposable { backgroundFill ? /** @type {Array<*>} */ (lastFillInstruction) : null, backgroundStroke ? /** @type {Array<*>} */ (lastStrokeInstruction) : null); } - this.renderDeclutter_(declutterGroup, feature); + this.declutterItems.push(this, declutterGroup, feature); ++i; break; case CanvasInstruction.DRAW_CHARS: @@ -739,7 +747,7 @@ class Executor extends Disposable { } } } - this.renderDeclutter_(declutterGroup, feature); + this.declutterItems.push(this, declutterGroup, feature); ++i; break; case CanvasInstruction.END_GEOMETRY: diff --git a/src/ol/render/canvas/ExecutorGroup.js b/src/ol/render/canvas/ExecutorGroup.js index 362de136f1..371f3f3c5d 100644 --- a/src/ol/render/canvas/ExecutorGroup.js +++ b/src/ol/render/canvas/ExecutorGroup.js @@ -35,18 +35,12 @@ class ExecutorGroup extends Disposable { * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. * @param {boolean} overlaps The executor group can have overlapping geometries. - * @param {?} declutterTree Declutter tree for declutter processing in postrender. * @param {!Object>} allInstructions * The serializable instructions. * @param {number=} opt_renderBuffer Optional rendering buffer. */ - constructor(maxExtent, resolution, pixelRatio, overlaps, declutterTree, allInstructions, opt_renderBuffer) { + constructor(maxExtent, resolution, pixelRatio, overlaps, allInstructions, opt_renderBuffer) { super(); - /** - * Declutter tree. - * @private - */ - this.declutterTree_ = declutterTree; /** * @private @@ -128,7 +122,7 @@ class ExecutorGroup extends Disposable { for (const builderType in instructionByZindex) { const instructions = instructionByZindex[builderType]; executors[builderType] = new Executor( - this.resolution_, this.pixelRatio_, this.overlaps_, this.declutterTree_, instructions); + this.resolution_, this.pixelRatio_, this.overlaps_, instructions); } } } @@ -172,7 +166,7 @@ class ExecutorGroup extends Disposable { * @param {number} hitTolerance Hit tolerance in pixels. * @param {Object} skippedFeaturesHash Ids of features to skip. * @param {function(import("../../Feature.js").FeatureLike): T} callback Feature callback. - * @param {Object} declutterReplays Declutter replays. + * @param {Array} declutteredFeatures Decluttered features. * @return {T|undefined} Callback result. * @template T */ @@ -183,7 +177,7 @@ class ExecutorGroup extends Disposable { hitTolerance, skippedFeaturesHash, callback, - declutterReplays + declutteredFeatures ) { hitTolerance = Math.round(hitTolerance); @@ -213,12 +207,6 @@ class ExecutorGroup extends Disposable { } const mask = getCircleArray(hitTolerance); - let declutteredFeatures; - if (this.declutterTree_) { - declutteredFeatures = this.declutterTree_.all().map(function(entry) { - return entry.value; - }); - } let builderType; @@ -261,20 +249,10 @@ class ExecutorGroup extends Disposable { builderType = ORDER[j]; executor = executors[builderType]; if (executor !== undefined) { - if (declutterReplays && - (builderType == BuilderType.IMAGE || builderType == BuilderType.TEXT)) { - const declutter = declutterReplays[zIndexKey]; - if (!declutter) { - declutterReplays[zIndexKey] = [executor, transform.slice(0)]; - } else { - declutter.push(executor, transform.slice(0)); - } - } else { - result = executor.executeHitDetection(context, transform, rotation, - skippedFeaturesHash, featureCallback, hitExtent); - if (result) { - return result; - } + result = executor.executeHitDetection(context, transform, rotation, + skippedFeaturesHash, featureCallback, hitExtent); + if (result) { + return result; } } } @@ -446,14 +424,20 @@ export function getCircleArray(radius) { * @param {CanvasRenderingContext2D} context Context. * @param {number} rotation Rotation. * @param {boolean} snapToPixel Snap point symbols and text to integer pixels. + * @param {Array>} declutterItems Declutter items. */ -export function replayDeclutter(declutterReplays, context, rotation, snapToPixel) { +export function replayDeclutter(declutterReplays, context, rotation, snapToPixel, declutterItems) { const zs = Object.keys(declutterReplays).map(Number).sort(numberSafeCompareFunction); const skippedFeatureUids = {}; for (let z = 0, zz = zs.length; z < zz; ++z) { const executorData = declutterReplays[zs[z].toString()]; + let currentExecutor; for (let i = 0, ii = executorData.length; i < ii;) { const executor = executorData[i++]; + if (executor !== currentExecutor) { + currentExecutor = executor; + declutterItems.push(executor.declutterItems); + } const transform = executorData[i++]; executor.execute(context, transform, rotation, skippedFeatureUids, snapToPixel); } diff --git a/src/ol/renderer/Composite.js b/src/ol/renderer/Composite.js index 6d2ef225ab..2d0908c1fa 100644 --- a/src/ol/renderer/Composite.js +++ b/src/ol/renderer/Composite.js @@ -99,6 +99,7 @@ class CompositeMapRenderer extends MapRenderer { this.children_.push(element); } } + super.renderFrame(frameState); replaceChildren(this.element_, this.children_); diff --git a/src/ol/renderer/Layer.js b/src/ol/renderer/Layer.js index b65c917e73..b63ea42195 100644 --- a/src/ol/renderer/Layer.js +++ b/src/ol/renderer/Layer.js @@ -89,10 +89,11 @@ class LayerRenderer extends Observable { * @param {import("../PluggableMap.js").FrameState} frameState Frame state. * @param {number} hitTolerance Hit tolerance in pixels. * @param {function(import("../Feature.js").FeatureLike, import("../layer/Layer.js").default): T} callback Feature callback. + * @param {Array} declutteredFeatures Decluttered features. * @return {T|void} Callback result. * @template T */ - forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) {} + forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, declutteredFeatures) {} /** * @abstract diff --git a/src/ol/renderer/Map.js b/src/ol/renderer/Map.js index 093a6fe85f..9955be7a51 100644 --- a/src/ol/renderer/Map.js +++ b/src/ol/renderer/Map.js @@ -10,6 +10,7 @@ import {TRUE} from '../functions.js'; import {visibleAtResolution} from '../layer/Layer.js'; import {shared as iconImageCache} from '../style/IconImageCache.js'; import {compose as composeTransform, makeInverse} from '../transform.js'; +import {renderDeclutterItems} from '../render.js'; /** * @abstract @@ -28,6 +29,11 @@ class MapRenderer extends Disposable { */ this.map_ = map; + /** + * @private + */ + this.declutterTree_ = null; + /** * @private * @type {!Object} @@ -133,6 +139,12 @@ class MapRenderer extends Disposable { const layerStates = frameState.layerStatesArray; const numLayers = layerStates.length; + let declutteredFeatures; + if (this.declutterTree_) { + declutteredFeatures = this.declutterTree_.all().map(function(entry) { + return entry.value; + }); + } let i; for (i = numLayers - 1; i >= 0; --i) { const layerState = layerStates[i]; @@ -144,7 +156,7 @@ class MapRenderer extends Disposable { const callback = forEachFeatureAtCoordinate.bind(null, layerState.managed); result = layerRenderer.forEachFeatureAtCoordinate( source.getWrapX() ? translatedCoordinate : coordinate, - frameState, hitTolerance, callback); + frameState, hitTolerance, callback, declutteredFeatures); } if (result) { return result; @@ -252,11 +264,10 @@ class MapRenderer extends Disposable { /** * Render. - * @abstract * @param {?import("../PluggableMap.js").FrameState} frameState Frame state. */ renderFrame(frameState) { - abstract(); + this.declutterTree_ = renderDeclutterItems(frameState, this.declutterTree_); } /** diff --git a/src/ol/renderer/canvas/VectorImageLayer.js b/src/ol/renderer/canvas/VectorImageLayer.js index f35397bbdb..8d7e7b20b3 100644 --- a/src/ol/renderer/canvas/VectorImageLayer.js +++ b/src/ol/renderer/canvas/VectorImageLayer.js @@ -11,6 +11,7 @@ import CanvasVectorLayerRenderer from './VectorLayer.js'; import {listen} from '../../events.js'; import EventType from '../../events/EventType.js'; import ImageState from '../../ImageState.js'; +import {renderDeclutterItems} from '../../render.js'; /** * @classdesc @@ -72,6 +73,7 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer { let skippedFeatures = this.skippedFeatures_; const context = vectorRenderer.context; const imageFrameState = /** @type {import("../../PluggableMap.js").FrameState} */ (assign({}, frameState, { + declutterItems: [], size: [ getWidth(renderedExtent) / viewResolution, getHeight(renderedExtent) / viewResolution @@ -86,6 +88,7 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer { (vectorRenderer.replayGroupChanged || !equals(skippedFeatures, newSkippedFeatures))) { vectorRenderer.renderFrame(imageFrameState, layerState); + renderDeclutterItems(imageFrameState, null); skippedFeatures = newSkippedFeatures; callback(); } @@ -123,11 +126,11 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer { /** * @inheritDoc */ - forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) { + forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, declutteredFeatures) { if (this.vectorRenderer_) { - return this.vectorRenderer_.forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback); + return this.vectorRenderer_.forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, declutteredFeatures); } else { - return super.forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback); + return super.forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, declutteredFeatures); } } } diff --git a/src/ol/renderer/canvas/VectorLayer.js b/src/ol/renderer/canvas/VectorLayer.js index 26271e7f41..aa79c35644 100644 --- a/src/ol/renderer/canvas/VectorLayer.js +++ b/src/ol/renderer/canvas/VectorLayer.js @@ -5,11 +5,10 @@ import {getUid} from '../../util.js'; import ViewHint from '../../ViewHint.js'; import {listen, unlisten} from '../../events.js'; import EventType from '../../events/EventType.js'; -import rbush from 'rbush'; import {buffer, createEmpty, containsExtent, getWidth} from '../../extent.js'; import {labelCache} from '../../render/canvas.js'; import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js'; -import ExecutorGroup from '../../render/canvas/ExecutorGroup.js'; +import ExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js'; import CanvasLayerRenderer from './Layer.js'; import {defaultOrder as defaultRenderOrder, getTolerance as getRenderTolerance, getSquaredTolerance as getSquaredRenderTolerance, renderFeature} from '../vector.js'; import {toString as transformToString, makeScale, makeInverse} from '../../transform.js'; @@ -28,12 +27,6 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { super(vectorLayer); - /** - * Declutter tree. - * @private - */ - this.declutterTree_ = vectorLayer.getDeclutter() ? rbush(9, undefined) : null; - /** * @private * @type {boolean} @@ -138,17 +131,14 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { this.clip(context, frameState, clipExtent); } - if (this.declutterTree_) { - this.declutterTree_.clear(); - } - const viewHints = frameState.viewHints; const snapToPixel = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]); const transform = this.getRenderTransform(frameState, width, height, 0); const skippedFeatureUids = layerState.managed ? frameState.skippedFeatureUids : {}; - replayGroup.execute(context, transform, rotation, skippedFeatureUids, snapToPixel); + const declutterReplays = /** @type {import("../../layer/Vector.js").default} */ (this.getLayer()).getDeclutter() ? {} : null; + replayGroup.execute(context, transform, rotation, skippedFeatureUids, snapToPixel, undefined, declutterReplays); if (vectorSource.getWrapX() && projection.canWrapX() && !containsExtent(projectionExtent, extent)) { let startX = extent[0]; @@ -159,7 +149,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { --world; offsetX = worldWidth * world; const transform = this.getRenderTransform(frameState, width, height, offsetX); - replayGroup.execute(context, transform, rotation, skippedFeatureUids, snapToPixel); + replayGroup.execute(context, transform, rotation, skippedFeatureUids, snapToPixel, undefined, declutterReplays); startX += worldWidth; } world = 0; @@ -168,10 +158,15 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { ++world; offsetX = worldWidth * world; const transform = this.getRenderTransform(frameState, width, height, offsetX); - replayGroup.execute(context, transform, rotation, skippedFeatureUids, snapToPixel); + replayGroup.execute(context, transform, rotation, skippedFeatureUids, snapToPixel, undefined, declutterReplays); startX -= worldWidth; } } + if (declutterReplays) { + const viewHints = frameState.viewHints; + const hifi = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]); + replayDeclutter(declutterReplays, context, rotation, hifi, frameState.declutterItems); + } if (clipped) { context.restore(); @@ -190,7 +185,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { /** * @inheritDoc */ - forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg) { + forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, declutteredFeatures) { if (!this.replayGroup_) { return undefined; } else { @@ -208,9 +203,9 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { const key = getUid(feature); if (!(key in features)) { features[key] = true; - return callback.call(thisArg, feature, layer); + return callback(feature, layer); } - }, null); + }, declutteredFeatures); return result; } } @@ -299,7 +294,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { const replayGroup = new CanvasBuilderGroup( getRenderTolerance(resolution, pixelRatio), extent, resolution, - pixelRatio, !!this.declutterTree_); + pixelRatio, vectorLayer.getDeclutter()); vectorSource.loadFeatures(extent, resolution, projection); @@ -339,7 +334,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { const replayGroupInstructions = replayGroup.finish(); const executorGroup = new ExecutorGroup(extent, resolution, - pixelRatio, vectorSource.getOverlaps(), this.declutterTree_, + pixelRatio, vectorSource.getOverlaps(), replayGroupInstructions, vectorLayer.getRenderBuffer()); this.renderedResolution_ = resolution; diff --git a/src/ol/renderer/canvas/VectorTileLayer.js b/src/ol/renderer/canvas/VectorTileLayer.js index 454b9b0ca8..1b74d9e4ba 100644 --- a/src/ol/renderer/canvas/VectorTileLayer.js +++ b/src/ol/renderer/canvas/VectorTileLayer.js @@ -7,7 +7,6 @@ import TileState from '../../TileState.js'; import ViewHint from '../../ViewHint.js'; import {listen, unlisten, unlistenByKey} from '../../events.js'; import EventType from '../../events/EventType.js'; -import rbush from 'rbush'; import {buffer, containsCoordinate, equals, getIntersection, getTopLeft, intersects} from '../../extent.js'; import VectorTileRenderType from '../../layer/VectorTileRenderType.js'; import ReplayType from '../../render/canvas/BuilderType.js'; @@ -103,12 +102,6 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { */ this.inverseOverlayPixelTransform_ = createTransform(); - /** - * Declutter tree. - * @private - */ - this.declutterTree_ = layer.getDeclutter() ? rbush(9, undefined) : null; - /** * @private * @type {boolean} @@ -274,7 +267,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { buffer(sharedExtent, layer.getRenderBuffer() * resolution, this.tmpExtent); builderState.dirty = false; const builderGroup = new CanvasBuilderGroup(0, sharedExtent, resolution, - pixelRatio, !!this.declutterTree_); + pixelRatio, layer.getDeclutter()); const squaredTolerance = getSquaredRenderTolerance(resolution, pixelRatio); /** @@ -310,7 +303,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { null : sharedExtent; const renderingReplayGroup = new CanvasExecutorGroup(replayExtent, resolution, - pixelRatio, source.getOverlaps(), this.declutterTree_, executorGroupInstructions, layer.getRenderBuffer()); + pixelRatio, source.getOverlaps(), executorGroupInstructions, layer.getRenderBuffer()); tile.executorGroups[layerUid].push(renderingReplayGroup); } builderState.renderedRevision = revision; @@ -322,11 +315,12 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { /** * @inheritDoc */ - forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg) { + forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, declutteredFeatures) { const resolution = frameState.viewState.resolution; const rotation = frameState.viewState.rotation; hitTolerance = hitTolerance == undefined ? 0 : hitTolerance; - const layer = this.getLayer(); + const layer = /** @type {import("../../layer/VectorTile.js").default} */ (this.getLayer()); + const declutter = layer.getDeclutter(); const source = layer.getSource(); const tileGrid = source.getTileGridForProjection(frameState.viewState.projection); /** @type {!Object} */ @@ -338,7 +332,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { let i, ii; for (i = 0, ii = renderedTiles.length; i < ii; ++i) { const tile = renderedTiles[i]; - if (!this.declutterTree_) { + if (!declutter) { // When not decluttering, we only need to consider the tile that contains the given // coordinate, because each feature will be rendered for each tile that contains it. const tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord); @@ -361,9 +355,9 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { } if (!(key in features)) { features[key] = true; - return callback.call(thisArg, feature, layer); + return callback(feature, layer); } - }, null); + }, declutteredFeatures); } } return found; @@ -464,9 +458,6 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { context.clearRect(0, 0, width, height); } - if (declutterReplays) { - this.declutterTree_.clear(); - } const tiles = this.renderedTiles; const tileGrid = source.getTileGridForProjection(frameState.viewState.projection); const clips = []; @@ -522,7 +513,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { } } if (declutterReplays) { - replayDeclutter(declutterReplays, context, rotation, hifi); + replayDeclutter(declutterReplays, context, rotation, hifi, frameState.declutterItems); } const opacity = layerState.opacity; @@ -552,9 +543,6 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer { frameState.animate = true; delete this.renderTileImageQueue_[uid]; const layer = /** @type {import("../../layer/VectorTile.js").default} */ (this.getLayer()); - if (this.declutterTree_ && layer.getRenderMode() === VectorTileRenderType.IMAGE) { - this.declutterTree_.clear(); - } const viewState = frameState.viewState; const tileGrid = layer.getSource().getTileGridForProjection(viewState.projection); const tileResolution = tileGrid.getResolution(tile.tileCoord[0]); diff --git a/src/ol/source/Raster.js b/src/ol/source/Raster.js index 558e426d96..ed44849ff8 100644 --- a/src/ol/source/Raster.js +++ b/src/ol/source/Raster.js @@ -227,7 +227,8 @@ class RasterSource extends ImageSource { rotation: 0 }), viewHints: [], - wantedTiles: {} + wantedTiles: {}, + declutterItems: [] }; this.setAttributions(function(frameState) { diff --git a/test/spec/ol/render/canvas/textbuilder.test.js b/test/spec/ol/render/canvas/textbuilder.test.js index 18f61732f3..850134b675 100644 --- a/test/spec/ol/render/canvas/textbuilder.test.js +++ b/test/spec/ol/render/canvas/textbuilder.test.js @@ -28,7 +28,7 @@ function createContext() { function executeInstructions(builder, expectedDrawTextImageCalls, expectedBuilderImageCalls) { const transform = createTransform(); const context = createContext(); - const executor = new Executor(0.02, 1, false, null, builder.finish()); + const executor = new Executor(0.02, 1, false, builder.finish()); sinon.spy(executor, 'drawTextImageWithPointPlacement_'); const replayImageStub = sinon.stub(executor, 'replayImage_'); executor.execute(context, transform); diff --git a/test/spec/ol/renderer/canvas/builder.test.js b/test/spec/ol/renderer/canvas/builder.test.js index da26600c2f..754cf7b3e2 100644 --- a/test/spec/ol/renderer/canvas/builder.test.js +++ b/test/spec/ol/renderer/canvas/builder.test.js @@ -35,7 +35,7 @@ describe('ol.render.canvas.BuilderGroup', function() { */ function execute(builder, skippedUids, pixelRatio, overlaps) { const executor = new ExecutorGroup([-180, -90, 180, 90], 1, - pixelRatio || 1, !!overlaps, null, builder.finish()); + pixelRatio || 1, !!overlaps, builder.finish()); executor.execute(context, transform, 0, skippedUids || {}); }