From bd3f35eef0cc5b659f8ef838f53581b18c5a7e6f Mon Sep 17 00:00:00 2001 From: ahocevar Date: Sat, 17 Aug 2019 18:41:54 +0200 Subject: [PATCH] Declutter multi geometries per geometry instead of per feature --- examples/vector-label-decluttering.html | 2 +- examples/vector-label-decluttering.js | 18 ------------------ src/ol/render/VectorContext.js | 4 ++-- src/ol/render/canvas.js | 7 ++++++- src/ol/render/canvas/BuilderGroup.js | 14 +++++++------- src/ol/render/canvas/Executor.js | 25 ++++++++++++++++++++----- src/ol/render/canvas/ImageBuilder.js | 16 ++++++++-------- src/ol/render/canvas/TextBuilder.js | 17 ++++++++++------- 8 files changed, 54 insertions(+), 49 deletions(-) diff --git a/examples/vector-label-decluttering.html b/examples/vector-label-decluttering.html index 9bbc13b512..19ae8ab16a 100644 --- a/examples/vector-label-decluttering.html +++ b/examples/vector-label-decluttering.html @@ -5,7 +5,7 @@ shortdesc: Label decluttering with a custom renderer. resources: - https://cdn.polyfill.io/v2/polyfill.min.js?features=Set" docs: > - Decluttering is used to avoid overlapping labels with `overflow: true` set on the text style. For MultiPolygon geometries, only the widest polygon is selected in a custom `geometry` function. + Decluttering is used to avoid overlapping labels. The `overflow: true` setting on the text style makes it so labels that do not fit within the bounds of a polygon are also included. tags: "vector, decluttering, labels" ---
diff --git a/examples/vector-label-decluttering.js b/examples/vector-label-decluttering.js index dfc6afc65e..5f8ad18bf3 100644 --- a/examples/vector-label-decluttering.js +++ b/examples/vector-label-decluttering.js @@ -1,6 +1,5 @@ import Map from '../src/ol/Map.js'; import View from '../src/ol/View.js'; -import {getWidth} from '../src/ol/extent.js'; import GeoJSON from '../src/ol/format/GeoJSON.js'; import VectorLayer from '../src/ol/layer/Vector.js'; import VectorSource from '../src/ol/source/Vector.js'; @@ -15,23 +14,6 @@ const map = new Map({ }); const labelStyle = new Style({ - geometry: function(feature) { - let geometry = feature.getGeometry(); - if (geometry.getType() == 'MultiPolygon') { - // Only render label for the widest polygon of a multipolygon - const polygons = geometry.getPolygons(); - let widest = 0; - for (let i = 0, ii = polygons.length; i < ii; ++i) { - const polygon = polygons[i]; - const width = getWidth(polygon.getExtent()); - if (width > widest) { - widest = width; - geometry = polygon; - } - } - } - return geometry; - }, text: new Text({ font: '12px Calibri,sans-serif', overflow: true, diff --git a/src/ol/render/VectorContext.js b/src/ol/render/VectorContext.js index bca30ff6d9..838996200d 100644 --- a/src/ol/render/VectorContext.js +++ b/src/ol/render/VectorContext.js @@ -106,9 +106,9 @@ class VectorContext { /** * @param {import("../style/Text.js").default} textStyle Text style. - * @param {import("./canvas.js").DeclutterGroup=} opt_declutterGroup Declutter. + * @param {import("./canvas.js").DeclutterGroups=} opt_declutterGroups Declutter. */ - setTextStyle(textStyle, opt_declutterGroup) {} + setTextStyle(textStyle, opt_declutterGroups) {} } export default VectorContext; diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index b6160472ac..2d995c624b 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -62,7 +62,6 @@ import LabelCache from './canvas/LabelCache.js'; * @property {Array} [padding] */ - /** * 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 @@ -76,6 +75,12 @@ import LabelCache from './canvas/LabelCache.js'; */ +/** + * Declutter groups for support of multi geometries. + * @typedef {Array} DeclutterGroups + */ + + /** * @const * @type {string} diff --git a/src/ol/render/canvas/BuilderGroup.js b/src/ol/render/canvas/BuilderGroup.js index 9038045dc2..9f90c83dd8 100644 --- a/src/ol/render/canvas/BuilderGroup.js +++ b/src/ol/render/canvas/BuilderGroup.js @@ -40,10 +40,10 @@ class BuilderGroup { this.declutter_ = declutter; /** - * @type {import("../canvas.js").DeclutterGroup} + * @type {import("../canvas.js").DeclutterGroups} * @private */ - this.declutterGroup_ = null; + this.declutterGroups_ = null; /** * @private @@ -78,17 +78,17 @@ class BuilderGroup { /** * @param {boolean} group Group with previous builder. - * @return {Array<*>} The resulting instruction group. + * @return {import("../canvas").DeclutterGroups} The resulting instruction groups. */ addDeclutter(group) { let declutter = null; if (this.declutter_) { if (group) { - declutter = this.declutterGroup_; - /** @type {number} */ (declutter[4])++; + declutter = this.declutterGroups_; + /** @type {number} */ (declutter[0][4])++; } else { - declutter = this.declutterGroup_ = createEmpty(); - declutter.push(1); + declutter = this.declutterGroups_ = [createEmpty()]; + declutter[0].push(1); } } return declutter; diff --git a/src/ol/render/canvas/Executor.js b/src/ol/render/canvas/Executor.js index 8c911a89db..52b5ebc546 100644 --- a/src/ol/render/canvas/Executor.js +++ b/src/ol/render/canvas/Executor.js @@ -535,7 +535,7 @@ class Executor extends Disposable { const ii = instructions.length; // end of instructions let d = 0; // data index let dd; // end of per-instruction data - let anchorX, anchorY, prevX, prevY, roundX, roundY, declutterGroup, image, text, textKey; + let anchorX, anchorY, prevX, prevY, roundX, roundY, declutterGroup, declutterGroups, image, text, textKey; let strokeKey, fillKey; let pendingFill = 0; let pendingStroke = 0; @@ -633,7 +633,7 @@ class Executor extends Disposable { // Remaining arguments in DRAW_IMAGE are in alphabetical order anchorX = /** @type {number} */ (instruction[4]); anchorY = /** @type {number} */ (instruction[5]); - declutterGroup = featureCallback ? null : /** @type {import("../canvas.js").DeclutterGroup} */ (instruction[6]); + declutterGroups = featureCallback ? null : instruction[6]; let height = /** @type {number} */ (instruction[7]); const opacity = /** @type {number} */ (instruction[8]); const originX = /** @type {number} */ (instruction[9]); @@ -643,7 +643,6 @@ class Executor extends Disposable { const scale = /** @type {number} */ (instruction[13]); let width = /** @type {number} */ (instruction[14]); - if (!image && instruction.length >= 19) { // create label images text = /** @type {string} */ (instruction[18]); @@ -679,25 +678,41 @@ class Executor extends Disposable { rotation += viewRotation; } let widthIndex = 0; + let declutterGroupIndex = 0; for (; d < dd; d += 2) { if (geometryWidths && geometryWidths[widthIndex++] < width / this.pixelRatio) { continue; } + if (declutterGroups) { + const index = Math.floor(declutterGroupIndex); + if (declutterGroups.length < index + 1) { + declutterGroup = createEmpty(); + declutterGroup.push(declutterGroups[0][4]); + declutterGroups.push(declutterGroup); + } + declutterGroup = declutterGroups[index]; + } this.replayImage_(context, pixelCoordinates[d], pixelCoordinates[d + 1], image, anchorX, anchorY, declutterGroup, height, opacity, originX, originY, rotation, scale, snapToPixel, width, padding, backgroundFill ? /** @type {Array<*>} */ (lastFillInstruction) : null, backgroundStroke ? /** @type {Array<*>} */ (lastStrokeInstruction) : null); + if (declutterGroup) { + if (declutterGroupIndex === Math.floor(declutterGroupIndex)) { + this.declutterItems.push(this, declutterGroup, feature); + } + declutterGroupIndex += 1 / declutterGroup[4]; + + } } - this.declutterItems.push(this, declutterGroup, feature); ++i; break; case CanvasInstruction.DRAW_CHARS: const begin = /** @type {number} */ (instruction[1]); const end = /** @type {number} */ (instruction[2]); const baseline = /** @type {number} */ (instruction[3]); - declutterGroup = featureCallback ? null : /** @type {import("../canvas.js").DeclutterGroup} */ (instruction[4]); + declutterGroup = featureCallback ? null : instruction[4]; const overflow = /** @type {number} */ (instruction[5]); fillKey = /** @type {string} */ (instruction[6]); const maxAngle = /** @type {number} */ (instruction[7]); diff --git a/src/ol/render/canvas/ImageBuilder.js b/src/ol/render/canvas/ImageBuilder.js index c29e4a1e3d..d7cd75a5fb 100644 --- a/src/ol/render/canvas/ImageBuilder.js +++ b/src/ol/render/canvas/ImageBuilder.js @@ -16,9 +16,9 @@ class CanvasImageBuilder extends CanvasBuilder { /** * @private - * @type {import("../canvas.js").DeclutterGroup} + * @type {import("../canvas.js").DeclutterGroups} */ - this.declutterGroup_ = null; + this.declutterGroups_ = null; /** * @private @@ -121,14 +121,14 @@ class CanvasImageBuilder extends CanvasBuilder { this.instructions.push([ CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.image_, // Remaining arguments to DRAW_IMAGE are in alphabetical order - this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_, + this.anchorX_, this.anchorY_, this.declutterGroups_, this.height_, this.opacity_, this.originX_, this.originY_, this.rotateWithView_, this.rotation_, this.scale_ * this.pixelRatio, this.width_ ]); this.hitDetectionInstructions.push([ CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.hitDetectionImage_, // Remaining arguments to DRAW_IMAGE are in alphabetical order - this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_, + this.anchorX_, this.anchorY_, this.declutterGroups_, this.height_, this.opacity_, this.originX_, this.originY_, this.rotateWithView_, this.rotation_, this.scale_, this.width_ ]); @@ -151,14 +151,14 @@ class CanvasImageBuilder extends CanvasBuilder { this.instructions.push([ CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.image_, // Remaining arguments to DRAW_IMAGE are in alphabetical order - this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_, + this.anchorX_, this.anchorY_, this.declutterGroups_, this.height_, this.opacity_, this.originX_, this.originY_, this.rotateWithView_, this.rotation_, this.scale_ * this.pixelRatio, this.width_ ]); this.hitDetectionInstructions.push([ CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.hitDetectionImage_, // Remaining arguments to DRAW_IMAGE are in alphabetical order - this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_, + this.anchorX_, this.anchorY_, this.declutterGroups_, this.height_, this.opacity_, this.originX_, this.originY_, this.rotateWithView_, this.rotation_, this.scale_, this.width_ ]); @@ -189,7 +189,7 @@ class CanvasImageBuilder extends CanvasBuilder { /** * @inheritDoc */ - setImageStyle(imageStyle, declutterGroup) { + setImageStyle(imageStyle, declutterGroups) { const anchor = imageStyle.getAnchor(); const size = imageStyle.getSize(); const hitDetectionImage = imageStyle.getHitDetectionImage(1); @@ -197,7 +197,7 @@ class CanvasImageBuilder extends CanvasBuilder { const origin = imageStyle.getOrigin(); this.anchorX_ = anchor[0]; this.anchorY_ = anchor[1]; - this.declutterGroup_ = /** @type {import("../canvas.js").DeclutterGroup} */ (declutterGroup); + this.declutterGroups_ = /** @type {import("../canvas.js").DeclutterGroups} */ (declutterGroups); this.hitDetectionImage_ = hitDetectionImage; this.image_ = image; this.height_ = size[1]; diff --git a/src/ol/render/canvas/TextBuilder.js b/src/ol/render/canvas/TextBuilder.js index ab9cfc14c8..d2399edab0 100644 --- a/src/ol/render/canvas/TextBuilder.js +++ b/src/ol/render/canvas/TextBuilder.js @@ -40,9 +40,9 @@ class CanvasTextBuilder extends CanvasBuilder { /** * @private - * @type {import("../canvas.js").DeclutterGroup} + * @type {import("../canvas.js").DeclutterGroups} */ - this.declutterGroup_; + this.declutterGroups_; /** * @private @@ -201,7 +201,10 @@ class CanvasTextBuilder extends CanvasBuilder { } end = this.coordinates.length; flatOffset = ends[o]; - this.drawChars_(begin, end, this.declutterGroup_); + const declutterGroup = this.declutterGroups_ ? + (o === 0 ? this.declutterGroups_[0] : [].concat(this.declutterGroups_[0])) : + null; + this.drawChars_(begin, end, declutterGroup); begin = end; } this.endGeometry(feature); @@ -274,7 +277,7 @@ class CanvasTextBuilder extends CanvasBuilder { // render time. const pixelRatio = this.pixelRatio; this.instructions.push([CanvasInstruction.DRAW_IMAGE, begin, end, - null, NaN, NaN, this.declutterGroup_, NaN, 1, 0, 0, + null, NaN, NaN, this.declutterGroups_, NaN, 1, 0, 0, this.textRotateWithView_, this.textRotation_, 1, NaN, textState.padding == defaultPadding ? defaultPadding : textState.padding.map(function(p) { @@ -285,7 +288,7 @@ class CanvasTextBuilder extends CanvasBuilder { this.textOffsetX_, this.textOffsetY_, geometryWidths ]); this.hitDetectionInstructions.push([CanvasInstruction.DRAW_IMAGE, begin, end, - null, NaN, NaN, this.declutterGroup_, NaN, 1, 0, 0, + null, NaN, NaN, this.declutterGroups_, NaN, 1, 0, 0, this.textRotateWithView_, this.textRotation_, 1 / this.pixelRatio, NaN, textState.padding, !!textState.backgroundFill, !!textState.backgroundStroke, @@ -379,12 +382,12 @@ class CanvasTextBuilder extends CanvasBuilder { /** * @inheritDoc */ - setTextStyle(textStyle, declutterGroup) { + setTextStyle(textStyle, declutterGroups) { let textState, fillState, strokeState; if (!textStyle) { this.text_ = ''; } else { - this.declutterGroup_ = /** @type {import("../canvas.js").DeclutterGroup} */ (declutterGroup); + this.declutterGroups_ = /** @type {import("../canvas.js").DeclutterGroups} */ (declutterGroups); const textFillStyle = textStyle.getFill(); if (!textFillStyle) {