Declutter multi geometries per geometry instead of per feature

This commit is contained in:
ahocevar
2019-08-17 18:41:54 +02:00
parent 189ad24528
commit bd3f35eef0
8 changed files with 54 additions and 49 deletions

View File

@@ -5,7 +5,7 @@ shortdesc: Label decluttering with a custom renderer.
resources: resources:
- https://cdn.polyfill.io/v2/polyfill.min.js?features=Set" - https://cdn.polyfill.io/v2/polyfill.min.js?features=Set"
docs: > 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" tags: "vector, decluttering, labels"
--- ---
<div id="map" class="map"></div> <div id="map" class="map"></div>

View File

@@ -1,6 +1,5 @@
import Map from '../src/ol/Map.js'; import Map from '../src/ol/Map.js';
import View from '../src/ol/View.js'; import View from '../src/ol/View.js';
import {getWidth} from '../src/ol/extent.js';
import GeoJSON from '../src/ol/format/GeoJSON.js'; import GeoJSON from '../src/ol/format/GeoJSON.js';
import VectorLayer from '../src/ol/layer/Vector.js'; import VectorLayer from '../src/ol/layer/Vector.js';
import VectorSource from '../src/ol/source/Vector.js'; import VectorSource from '../src/ol/source/Vector.js';
@@ -15,23 +14,6 @@ const map = new Map({
}); });
const labelStyle = new Style({ 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({ text: new Text({
font: '12px Calibri,sans-serif', font: '12px Calibri,sans-serif',
overflow: true, overflow: true,

View File

@@ -106,9 +106,9 @@ class VectorContext {
/** /**
* @param {import("../style/Text.js").default} textStyle Text style. * @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; export default VectorContext;

View File

@@ -62,7 +62,6 @@ import LabelCache from './canvas/LabelCache.js';
* @property {Array<number>} [padding] * @property {Array<number>} [padding]
*/ */
/** /**
* Container for decluttered replay instructions that need to be rendered or * 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 * 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<DeclutterGroup>} DeclutterGroups
*/
/** /**
* @const * @const
* @type {string} * @type {string}

View File

@@ -40,10 +40,10 @@ class BuilderGroup {
this.declutter_ = declutter; this.declutter_ = declutter;
/** /**
* @type {import("../canvas.js").DeclutterGroup} * @type {import("../canvas.js").DeclutterGroups}
* @private * @private
*/ */
this.declutterGroup_ = null; this.declutterGroups_ = null;
/** /**
* @private * @private
@@ -78,17 +78,17 @@ class BuilderGroup {
/** /**
* @param {boolean} group Group with previous builder. * @param {boolean} group Group with previous builder.
* @return {Array<*>} The resulting instruction group. * @return {import("../canvas").DeclutterGroups} The resulting instruction groups.
*/ */
addDeclutter(group) { addDeclutter(group) {
let declutter = null; let declutter = null;
if (this.declutter_) { if (this.declutter_) {
if (group) { if (group) {
declutter = this.declutterGroup_; declutter = this.declutterGroups_;
/** @type {number} */ (declutter[4])++; /** @type {number} */ (declutter[0][4])++;
} else { } else {
declutter = this.declutterGroup_ = createEmpty(); declutter = this.declutterGroups_ = [createEmpty()];
declutter.push(1); declutter[0].push(1);
} }
} }
return declutter; return declutter;

View File

@@ -535,7 +535,7 @@ class Executor extends Disposable {
const ii = instructions.length; // end of instructions const ii = instructions.length; // end of instructions
let d = 0; // data index let d = 0; // data index
let dd; // end of per-instruction data 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 strokeKey, fillKey;
let pendingFill = 0; let pendingFill = 0;
let pendingStroke = 0; let pendingStroke = 0;
@@ -633,7 +633,7 @@ class Executor extends Disposable {
// Remaining arguments in DRAW_IMAGE are in alphabetical order // Remaining arguments in DRAW_IMAGE are in alphabetical order
anchorX = /** @type {number} */ (instruction[4]); anchorX = /** @type {number} */ (instruction[4]);
anchorY = /** @type {number} */ (instruction[5]); 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]); let height = /** @type {number} */ (instruction[7]);
const opacity = /** @type {number} */ (instruction[8]); const opacity = /** @type {number} */ (instruction[8]);
const originX = /** @type {number} */ (instruction[9]); const originX = /** @type {number} */ (instruction[9]);
@@ -643,7 +643,6 @@ class Executor extends Disposable {
const scale = /** @type {number} */ (instruction[13]); const scale = /** @type {number} */ (instruction[13]);
let width = /** @type {number} */ (instruction[14]); let width = /** @type {number} */ (instruction[14]);
if (!image && instruction.length >= 19) { if (!image && instruction.length >= 19) {
// create label images // create label images
text = /** @type {string} */ (instruction[18]); text = /** @type {string} */ (instruction[18]);
@@ -679,25 +678,41 @@ class Executor extends Disposable {
rotation += viewRotation; rotation += viewRotation;
} }
let widthIndex = 0; let widthIndex = 0;
let declutterGroupIndex = 0;
for (; d < dd; d += 2) { for (; d < dd; d += 2) {
if (geometryWidths && geometryWidths[widthIndex++] < width / this.pixelRatio) { if (geometryWidths && geometryWidths[widthIndex++] < width / this.pixelRatio) {
continue; 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, this.replayImage_(context,
pixelCoordinates[d], pixelCoordinates[d + 1], image, anchorX, anchorY, pixelCoordinates[d], pixelCoordinates[d + 1], image, anchorX, anchorY,
declutterGroup, height, opacity, originX, originY, rotation, scale, declutterGroup, height, opacity, originX, originY, rotation, scale,
snapToPixel, width, padding, snapToPixel, width, padding,
backgroundFill ? /** @type {Array<*>} */ (lastFillInstruction) : null, backgroundFill ? /** @type {Array<*>} */ (lastFillInstruction) : null,
backgroundStroke ? /** @type {Array<*>} */ (lastStrokeInstruction) : 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; ++i;
break; break;
case CanvasInstruction.DRAW_CHARS: case CanvasInstruction.DRAW_CHARS:
const begin = /** @type {number} */ (instruction[1]); const begin = /** @type {number} */ (instruction[1]);
const end = /** @type {number} */ (instruction[2]); const end = /** @type {number} */ (instruction[2]);
const baseline = /** @type {number} */ (instruction[3]); 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]); const overflow = /** @type {number} */ (instruction[5]);
fillKey = /** @type {string} */ (instruction[6]); fillKey = /** @type {string} */ (instruction[6]);
const maxAngle = /** @type {number} */ (instruction[7]); const maxAngle = /** @type {number} */ (instruction[7]);

View File

@@ -16,9 +16,9 @@ class CanvasImageBuilder extends CanvasBuilder {
/** /**
* @private * @private
* @type {import("../canvas.js").DeclutterGroup} * @type {import("../canvas.js").DeclutterGroups}
*/ */
this.declutterGroup_ = null; this.declutterGroups_ = null;
/** /**
* @private * @private
@@ -121,14 +121,14 @@ class CanvasImageBuilder extends CanvasBuilder {
this.instructions.push([ this.instructions.push([
CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.image_, CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.image_,
// Remaining arguments to DRAW_IMAGE are in alphabetical order // 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.originX_, this.originY_, this.rotateWithView_, this.rotation_,
this.scale_ * this.pixelRatio, this.width_ this.scale_ * this.pixelRatio, this.width_
]); ]);
this.hitDetectionInstructions.push([ this.hitDetectionInstructions.push([
CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.hitDetectionImage_, CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.hitDetectionImage_,
// Remaining arguments to DRAW_IMAGE are in alphabetical order // 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.originX_, this.originY_, this.rotateWithView_, this.rotation_,
this.scale_, this.width_ this.scale_, this.width_
]); ]);
@@ -151,14 +151,14 @@ class CanvasImageBuilder extends CanvasBuilder {
this.instructions.push([ this.instructions.push([
CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.image_, CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.image_,
// Remaining arguments to DRAW_IMAGE are in alphabetical order // 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.originX_, this.originY_, this.rotateWithView_, this.rotation_,
this.scale_ * this.pixelRatio, this.width_ this.scale_ * this.pixelRatio, this.width_
]); ]);
this.hitDetectionInstructions.push([ this.hitDetectionInstructions.push([
CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.hitDetectionImage_, CanvasInstruction.DRAW_IMAGE, myBegin, myEnd, this.hitDetectionImage_,
// Remaining arguments to DRAW_IMAGE are in alphabetical order // 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.originX_, this.originY_, this.rotateWithView_, this.rotation_,
this.scale_, this.width_ this.scale_, this.width_
]); ]);
@@ -189,7 +189,7 @@ class CanvasImageBuilder extends CanvasBuilder {
/** /**
* @inheritDoc * @inheritDoc
*/ */
setImageStyle(imageStyle, declutterGroup) { setImageStyle(imageStyle, declutterGroups) {
const anchor = imageStyle.getAnchor(); const anchor = imageStyle.getAnchor();
const size = imageStyle.getSize(); const size = imageStyle.getSize();
const hitDetectionImage = imageStyle.getHitDetectionImage(1); const hitDetectionImage = imageStyle.getHitDetectionImage(1);
@@ -197,7 +197,7 @@ class CanvasImageBuilder extends CanvasBuilder {
const origin = imageStyle.getOrigin(); const origin = imageStyle.getOrigin();
this.anchorX_ = anchor[0]; this.anchorX_ = anchor[0];
this.anchorY_ = anchor[1]; this.anchorY_ = anchor[1];
this.declutterGroup_ = /** @type {import("../canvas.js").DeclutterGroup} */ (declutterGroup); this.declutterGroups_ = /** @type {import("../canvas.js").DeclutterGroups} */ (declutterGroups);
this.hitDetectionImage_ = hitDetectionImage; this.hitDetectionImage_ = hitDetectionImage;
this.image_ = image; this.image_ = image;
this.height_ = size[1]; this.height_ = size[1];

View File

@@ -40,9 +40,9 @@ class CanvasTextBuilder extends CanvasBuilder {
/** /**
* @private * @private
* @type {import("../canvas.js").DeclutterGroup} * @type {import("../canvas.js").DeclutterGroups}
*/ */
this.declutterGroup_; this.declutterGroups_;
/** /**
* @private * @private
@@ -201,7 +201,10 @@ class CanvasTextBuilder extends CanvasBuilder {
} }
end = this.coordinates.length; end = this.coordinates.length;
flatOffset = ends[o]; 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; begin = end;
} }
this.endGeometry(feature); this.endGeometry(feature);
@@ -274,7 +277,7 @@ class CanvasTextBuilder extends CanvasBuilder {
// render time. // render time.
const pixelRatio = this.pixelRatio; const pixelRatio = this.pixelRatio;
this.instructions.push([CanvasInstruction.DRAW_IMAGE, begin, end, 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, this.textRotateWithView_, this.textRotation_, 1, NaN,
textState.padding == defaultPadding ? textState.padding == defaultPadding ?
defaultPadding : textState.padding.map(function(p) { defaultPadding : textState.padding.map(function(p) {
@@ -285,7 +288,7 @@ class CanvasTextBuilder extends CanvasBuilder {
this.textOffsetX_, this.textOffsetY_, geometryWidths this.textOffsetX_, this.textOffsetY_, geometryWidths
]); ]);
this.hitDetectionInstructions.push([CanvasInstruction.DRAW_IMAGE, begin, end, 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, this.textRotateWithView_, this.textRotation_, 1 / this.pixelRatio, NaN,
textState.padding, textState.padding,
!!textState.backgroundFill, !!textState.backgroundStroke, !!textState.backgroundFill, !!textState.backgroundStroke,
@@ -379,12 +382,12 @@ class CanvasTextBuilder extends CanvasBuilder {
/** /**
* @inheritDoc * @inheritDoc
*/ */
setTextStyle(textStyle, declutterGroup) { setTextStyle(textStyle, declutterGroups) {
let textState, fillState, strokeState; let textState, fillState, strokeState;
if (!textStyle) { if (!textStyle) {
this.text_ = ''; this.text_ = '';
} else { } else {
this.declutterGroup_ = /** @type {import("../canvas.js").DeclutterGroup} */ (declutterGroup); this.declutterGroups_ = /** @type {import("../canvas.js").DeclutterGroups} */ (declutterGroups);
const textFillStyle = textStyle.getFill(); const textFillStyle = textStyle.getFill();
if (!textFillStyle) { if (!textFillStyle) {