From 9042545af2c6f5bb558f6a938ea0d52e455f66dd Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Thu, 3 Aug 2023 16:42:06 +0200 Subject: [PATCH] New flushDeclutterItems() map method to control declutter stack --- examples/declutter-group.css | 13 ++++++++ examples/declutter-group.html | 16 +++++++++ examples/declutter-group.js | 35 ++++++++++++++++++++ src/ol/Map.js | 15 +++++++++ src/ol/renderer/Composite.js | 29 +++++++++++++---- src/ol/renderer/Map.js | 5 +++ test/browser/spec/ol/Map.test.js | 56 ++++++++++++++++++++++++++++++++ 7 files changed, 162 insertions(+), 7 deletions(-) create mode 100644 examples/declutter-group.css create mode 100644 examples/declutter-group.html create mode 100644 examples/declutter-group.js diff --git a/examples/declutter-group.css b/examples/declutter-group.css new file mode 100644 index 00000000000..c1535b9531c --- /dev/null +++ b/examples/declutter-group.css @@ -0,0 +1,13 @@ +.map .ol-rotate { + left: .5em; + bottom: .5em; + top: auto; + right: auto; +} +.map:-webkit-full-screen { + height: 100%; + margin: 0; +} +.map:fullscreen { + height: 100%; +} diff --git a/examples/declutter-group.html b/examples/declutter-group.html new file mode 100644 index 00000000000..b02626806ab --- /dev/null +++ b/examples/declutter-group.html @@ -0,0 +1,16 @@ +--- +layout: example.html +title: Declutter Group +shortdesc: Declutter vector layers by groups +docs: > + This shows how to specify when vector(tile) layers are decluttered if `declutter` is set to true. By default, + all decluttering will happen after all layers have been rendered. Calling the map's `flushDeclutter()` method + makes decluttering occur immediately. This is useful for layers that need to be entirely rendered above the declutter items + of layers lower in the layer stack. In the example, the blue square overlay displays above the decluttered vector symbols + and labels. +tags: "mapbox, declutter, vector" +cloak: + - key: get_your_own_D6rA4zTHduk6KOKTXzGB + value: Get your own API key at https://www.maptiler.com/cloud/ +--- +
\ No newline at end of file diff --git a/examples/declutter-group.js b/examples/declutter-group.js new file mode 100644 index 00000000000..2e7520fecdf --- /dev/null +++ b/examples/declutter-group.js @@ -0,0 +1,35 @@ +import {Feature, Map, View} from '../src/ol/index.js'; +import {Group as LayerGroup, Vector as VectorLayer} from '../src/ol/layer.js'; +import {Vector as VectorSource} from '../src/ol/source.js'; +import {apply} from 'ol-mapbox-style'; +import {fromExtent} from '../src/ol/geom/Polygon.js'; +import {getCenter} from '../src/ol/extent.js'; + +const square = [-12e6, 3.5e6, -10e6, 5.5e6]; +const overlay = new VectorLayer({ + source: new VectorSource({ + features: [new Feature(fromExtent(square))], + }), + style: { + 'stroke-color': 'rgba(180, 180, 255, 1)', + 'stroke-width': 1, + 'fill-color': 'rgba(200, 200, 255, 0.85)', + }, +}); + +const layer = new LayerGroup(); +apply( + layer, + 'https://api.maptiler.com/maps/topo-v2/style.json?key=get_your_own_D6rA4zTHduk6KOKTXzGB' +); + +const map = new Map({ + target: 'map', + view: new View({ + center: getCenter(square), + zoom: 4, + }), + layers: [layer, overlay], +}); + +overlay.on('prerender', () => map.flushDeclutterItems()); diff --git a/src/ol/Map.js b/src/ol/Map.js index c21125f5dc8..36352b12d47 100644 --- a/src/ol/Map.js +++ b/src/ol/Map.js @@ -1457,6 +1457,21 @@ class Map extends BaseObject { } } + /** + * This method is meant to be called in a layer's `prerender` listener. It causes all collected + * declutter items to be decluttered and rendered on the map immediately. This is useful for + * layers that need to appear entirely above the decluttered items of layers lower in the layer + * stack. + * @api + */ + flushDeclutterItems() { + const frameState = this.frameState_; + if (!frameState) { + return; + } + this.renderer_.flushDeclutterItems(frameState); + } + /** * Remove the given control from the map. * @param {import("./control/Control.js").default} control Control. diff --git a/src/ol/renderer/Composite.js b/src/ol/renderer/Composite.js index 8947c3655d6..78357c8b4c9 100644 --- a/src/ol/renderer/Composite.js +++ b/src/ol/renderer/Composite.js @@ -59,6 +59,11 @@ class CompositeMapRenderer extends MapRenderer { * @type {boolean} */ this.renderedVisible_ = true; + + /** + * @type {Array} + */ + this.declutterLayers_ = []; } /** @@ -101,10 +106,10 @@ class CompositeMapRenderer extends MapRenderer { const viewState = frameState.viewState; this.children_.length = 0; - /** - * @type {Array} - */ - const declutterLayers = []; + + const declutterLayers = this.declutterLayers_; + declutterLayers.length = 0; + let previousElement = null; for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) { const layerState = layerStatesArray[i]; @@ -134,9 +139,7 @@ class CompositeMapRenderer extends MapRenderer { ); } } - for (let i = declutterLayers.length - 1; i >= 0; --i) { - declutterLayers[i].renderDeclutter(frameState); - } + this.flushDeclutterItems(frameState); replaceChildren(this.element_, this.children_); @@ -149,6 +152,18 @@ class CompositeMapRenderer extends MapRenderer { this.scheduleExpireIconCache(frameState); } + + /** + * @param {import("../Map.js").FrameState} frameState Frame state. + */ + flushDeclutterItems(frameState) { + const layers = this.declutterLayers_; + for (let i = layers.length - 1; i >= 0; --i) { + layers[i].renderDeclutter(frameState); + } + frameState.declutterTree = null; + layers.length = 0; + } } export default CompositeMapRenderer; diff --git a/src/ol/renderer/Map.js b/src/ol/renderer/Map.js index c7145b543d2..f9301512f49 100644 --- a/src/ol/renderer/Map.js +++ b/src/ol/renderer/Map.js @@ -221,6 +221,11 @@ class MapRenderer extends Disposable { abstract(); } + /** + * @param {import("../Map.js").FrameState} frameState Frame state. + */ + flushDeclutterItems(frameState) {} + /** * @param {import("../Map.js").FrameState} frameState Frame state. * @protected diff --git a/test/browser/spec/ol/Map.test.js b/test/browser/spec/ol/Map.test.js index ecbc206b957..18fb5fed029 100644 --- a/test/browser/spec/ol/Map.test.js +++ b/test/browser/spec/ol/Map.test.js @@ -1122,6 +1122,62 @@ describe('ol/Map', function () { }); }); + describe('#fushDeclutterItems()', function () { + let map; + + beforeEach(function () { + map = new Map({ + target: createMapDiv(100, 100), + view: new View({ + projection: 'EPSG:4326', + center: [0, 0], + resolution: 1, + }), + }); + }); + + afterEach(function () { + disposeMap(map); + }); + + it('calls renderDeclutter() on all layers with a lower layer index', function () { + const createFeatures = () => [ + new Feature(new Point([0, 0])), + new Feature(new Point([-1, 0])), + new Feature(new Point([1, 0])), + ]; + const layer1 = new VectorLayer({ + source: new VectorSource({features: createFeatures()}), + }); + const layer2 = new VectorLayer({ + source: new VectorSource({features: createFeatures()}), + }); + const layer3 = new VectorLayer({ + source: new VectorSource({features: createFeatures()}), + }); + map.addLayer(layer1); + map.addLayer(layer2); + map.addLayer(layer3); + + const spy1 = sinon.spy(layer1, 'renderDeclutter'); + const spy2 = sinon.spy(layer2, 'renderDeclutter'); + const spy3 = sinon.spy(layer3, 'renderDeclutter'); + + layer3.on('prerender', () => { + map.flushDeclutterItems(); + expect(spy1.callCount).to.be(1); + expect(spy2.callCount).to.be(1); + expect(spy3.callCount).to.be(0); + }); + + map.renderSync(); + + expect(spy1.callCount).to.be(1); + expect(spy2.callCount).to.be(1); + expect(spy3.callCount).to.be(1); + }); + }); + describe('dispose', function () { let map;