Skip to content

Commit

Permalink
Merge pull request #14971 from ahocevar/flush-declutter
Browse files Browse the repository at this point in the history
New flushDeclutterItems() map method to control declutter stack
  • Loading branch information
ahocevar committed Aug 11, 2023
2 parents 69f433e + 9042545 commit c274b4e
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 7 deletions.
13 changes: 13 additions & 0 deletions 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%;
}
16 changes: 16 additions & 0 deletions 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/
---
<div id="map" class="map"></div>
35 changes: 35 additions & 0 deletions 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());
15 changes: 15 additions & 0 deletions src/ol/Map.js
Expand Up @@ -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.
Expand Down
29 changes: 22 additions & 7 deletions src/ol/renderer/Composite.js
Expand Up @@ -59,6 +59,11 @@ class CompositeMapRenderer extends MapRenderer {
* @type {boolean}
*/
this.renderedVisible_ = true;

/**
* @type {Array<import("../layer/BaseVector.js").default>}
*/
this.declutterLayers_ = [];
}

/**
Expand Down Expand Up @@ -101,10 +106,10 @@ class CompositeMapRenderer extends MapRenderer {
const viewState = frameState.viewState;

this.children_.length = 0;
/**
* @type {Array<import("../layer/BaseVector.js").default>}
*/
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];
Expand Down Expand Up @@ -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_);

Expand All @@ -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;
5 changes: 5 additions & 0 deletions src/ol/renderer/Map.js
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions test/browser/spec/ol/Map.test.js
Expand Up @@ -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;

Expand Down

0 comments on commit c274b4e

Please sign in to comment.