Declutter multi geometries per geometry instead of per feature
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user