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:
- 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"
---
<div id="map" class="map"></div>

View File

@@ -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,

View File

@@ -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;

View File

@@ -62,7 +62,6 @@ import LabelCache from './canvas/LabelCache.js';
* @property {Array<number>} [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<DeclutterGroup>} DeclutterGroups
*/
/**
* @const
* @type {string}

View File

@@ -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;

View File

@@ -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];
}
}
++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]);

View File

@@ -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];

View File

@@ -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) {