Merge pull request #13566 from CNS-Solutions/decluttering

Decluttering mode by style
This commit is contained in:
Andreas Hocevar
2022-05-06 22:23:59 +02:00
committed by GitHub
11 changed files with 333 additions and 63 deletions

View File

@@ -808,17 +808,21 @@ class Executor {
instruction[12]
);
let width = /** @type {number} */ (instruction[13]);
const declutterImageWithText =
/** @type {import("../canvas.js").DeclutterImageWithText} */ (
const declutterMode =
/** @type {"declutter"|"obstacle"|"none"|undefined} */ (
instruction[14]
);
const declutterImageWithText =
/** @type {import("../canvas.js").DeclutterImageWithText} */ (
instruction[15]
);
if (!image && instruction.length >= 19) {
if (!image && instruction.length >= 20) {
// create label images
text = /** @type {string} */ (instruction[18]);
textKey = /** @type {string} */ (instruction[19]);
strokeKey = /** @type {string} */ (instruction[20]);
fillKey = /** @type {string} */ (instruction[21]);
text = /** @type {string} */ (instruction[19]);
textKey = /** @type {string} */ (instruction[20]);
strokeKey = /** @type {string} */ (instruction[21]);
fillKey = /** @type {string} */ (instruction[22]);
const labelWithAnchor = this.drawLabelWithPointPlacement_(
text,
textKey,
@@ -827,10 +831,10 @@ class Executor {
);
image = labelWithAnchor.label;
instruction[3] = image;
const textOffsetX = /** @type {number} */ (instruction[22]);
const textOffsetX = /** @type {number} */ (instruction[23]);
anchorX = (labelWithAnchor.anchorX - textOffsetX) * this.pixelRatio;
instruction[4] = anchorX;
const textOffsetY = /** @type {number} */ (instruction[23]);
const textOffsetY = /** @type {number} */ (instruction[24]);
anchorY = (labelWithAnchor.anchorY - textOffsetY) * this.pixelRatio;
instruction[5] = anchorY;
height = image.height;
@@ -840,15 +844,15 @@ class Executor {
}
let geometryWidths;
if (instruction.length > 24) {
geometryWidths = /** @type {number} */ (instruction[24]);
if (instruction.length > 25) {
geometryWidths = /** @type {number} */ (instruction[25]);
}
let padding, backgroundFill, backgroundStroke;
if (instruction.length > 16) {
padding = /** @type {Array<number>} */ (instruction[15]);
backgroundFill = /** @type {boolean} */ (instruction[16]);
backgroundStroke = /** @type {boolean} */ (instruction[17]);
if (instruction.length > 17) {
padding = /** @type {Array<number>} */ (instruction[16]);
backgroundFill = /** @type {boolean} */ (instruction[17]);
backgroundStroke = /** @type {boolean} */ (instruction[18]);
} else {
padding = defaultPadding;
backgroundFill = false;
@@ -902,39 +906,43 @@ class Executor {
? /** @type {Array<*>} */ (lastStrokeInstruction)
: null,
];
let imageArgs;
let imageDeclutterBox;
if (opt_declutterTree && declutterImageWithText) {
const index = dd - d;
if (!declutterImageWithText[index]) {
// We now have the image for an image+text combination.
declutterImageWithText[index] = args;
// Don't render anything for now, wait for the text.
continue;
}
imageArgs = declutterImageWithText[index];
delete declutterImageWithText[index];
imageDeclutterBox = getDeclutterBox(imageArgs);
if (opt_declutterTree.collides(imageDeclutterBox)) {
continue;
}
}
if (
opt_declutterTree &&
opt_declutterTree.collides(dimensions.declutterBox)
) {
continue;
}
if (imageArgs) {
// We now have image and text for an image+text combination.
if (opt_declutterTree) {
opt_declutterTree.insert(imageDeclutterBox);
}
// Render the image before we render the text.
this.replayImageOrLabel_.apply(this, imageArgs);
}
if (opt_declutterTree) {
opt_declutterTree.insert(dimensions.declutterBox);
if (declutterMode === 'none') {
// not rendered in declutter group
continue;
} else if (declutterMode === 'obstacle') {
// will always be drawn, thus no collision detection, but insert as obstacle
opt_declutterTree.insert(dimensions.declutterBox);
continue;
} else {
let imageArgs;
let imageDeclutterBox;
if (declutterImageWithText) {
const index = dd - d;
if (!declutterImageWithText[index]) {
// We now have the image for an image+text combination.
declutterImageWithText[index] = args;
// Don't render anything for now, wait for the text.
continue;
}
imageArgs = declutterImageWithText[index];
delete declutterImageWithText[index];
imageDeclutterBox = getDeclutterBox(imageArgs);
if (opt_declutterTree.collides(imageDeclutterBox)) {
continue;
}
}
if (opt_declutterTree.collides(dimensions.declutterBox)) {
continue;
}
if (imageArgs) {
// We now have image and text for an image+text combination.
opt_declutterTree.insert(imageDeclutterBox);
// Render the image before we render the text.
this.replayImageOrLabel_.apply(this, imageArgs);
}
opt_declutterTree.insert(dimensions.declutterBox);
}
}
this.replayImageOrLabel_.apply(this, args);
}

View File

@@ -92,6 +92,12 @@ class CanvasImageBuilder extends CanvasBuilder {
*/
this.width_ = undefined;
/**
* @private
* @type {"declutter"|"obstacle"|"none"|undefined}
*/
this.declutterMode_ = undefined;
/**
* Data shared with a text builder for combined decluttering.
* @private
@@ -132,6 +138,7 @@ class CanvasImageBuilder extends CanvasBuilder {
(this.scale_[1] * this.pixelRatio) / this.imagePixelRatio_,
],
Math.ceil(this.width_ * this.imagePixelRatio_),
this.declutterMode_,
this.declutterImageWithText_,
]);
this.hitDetectionInstructions.push([
@@ -150,6 +157,7 @@ class CanvasImageBuilder extends CanvasBuilder {
this.rotation_,
this.scale_,
this.width_,
this.declutterMode_,
this.declutterImageWithText_,
]);
this.endGeometry(feature);
@@ -187,6 +195,7 @@ class CanvasImageBuilder extends CanvasBuilder {
(this.scale_[1] * this.pixelRatio) / this.imagePixelRatio_,
],
Math.ceil(this.width_ * this.imagePixelRatio_),
this.declutterMode_,
this.declutterImageWithText_,
]);
this.hitDetectionInstructions.push([
@@ -205,6 +214,7 @@ class CanvasImageBuilder extends CanvasBuilder {
this.rotation_,
this.scale_,
this.width_,
this.declutterMode_,
this.declutterImageWithText_,
]);
this.endGeometry(feature);
@@ -255,6 +265,7 @@ class CanvasImageBuilder extends CanvasBuilder {
this.rotation_ = imageStyle.getRotation();
this.scale_ = imageStyle.getScaleArray();
this.width_ = size[0];
this.declutterMode_ = imageStyle.getDeclutterMode();
this.declutterImageWithText_ = opt_sharedData;
}
}

View File

@@ -374,6 +374,7 @@ class CanvasTextBuilder extends CanvasBuilder {
this.textRotation_,
[1, 1],
NaN,
undefined,
this.declutterImageWithText_,
padding == defaultPadding
? defaultPadding
@@ -406,6 +407,7 @@ class CanvasTextBuilder extends CanvasBuilder {
this.textRotation_,
[scale, scale],
NaN,
undefined,
this.declutterImageWithText_,
padding,
!!textState.backgroundFill,

View File

@@ -362,16 +362,29 @@ function renderPointGeometry(
const textStyle = style.getText();
/** @type {import("../render/canvas.js").DeclutterImageWithText} */
let declutterImageWithText;
if (opt_declutterBuilderGroup) {
builderGroup = opt_declutterBuilderGroup;
declutterImageWithText =
imageStyle && textStyle && textStyle.getText() ? {} : undefined;
}
if (imageStyle) {
if (imageStyle.getImageState() != ImageState.LOADED) {
return;
}
const imageReplay = builderGroup.getBuilder(
let imageBuilderGroup = builderGroup;
if (opt_declutterBuilderGroup) {
const declutterMode = imageStyle.getDeclutterMode();
if (declutterMode !== 'none') {
imageBuilderGroup = opt_declutterBuilderGroup;
if (declutterMode === 'obstacle') {
// draw in non-declutter group:
const imageReplay = builderGroup.getBuilder(
style.getZIndex(),
BuilderType.IMAGE
);
imageReplay.setImageStyle(imageStyle, declutterImageWithText);
imageReplay.drawPoint(geometry, feature);
} else if (textStyle && textStyle.getText()) {
declutterImageWithText = {};
}
}
}
const imageReplay = imageBuilderGroup.getBuilder(
style.getZIndex(),
BuilderType.IMAGE
);
@@ -379,7 +392,11 @@ function renderPointGeometry(
imageReplay.drawPoint(geometry, feature);
}
if (textStyle && textStyle.getText()) {
const textReplay = builderGroup.getBuilder(
let textBuilderGroup = builderGroup;
if (opt_declutterBuilderGroup) {
textBuilderGroup = opt_declutterBuilderGroup;
}
const textReplay = textBuilderGroup.getBuilder(
style.getZIndex(),
BuilderType.TEXT
);
@@ -406,16 +423,29 @@ function renderMultiPointGeometry(
const textStyle = style.getText();
/** @type {import("../render/canvas.js").DeclutterImageWithText} */
let declutterImageWithText;
if (opt_declutterBuilderGroup) {
builderGroup = opt_declutterBuilderGroup;
declutterImageWithText =
imageStyle && textStyle && textStyle.getText() ? {} : undefined;
}
if (imageStyle) {
if (imageStyle.getImageState() != ImageState.LOADED) {
return;
}
const imageReplay = builderGroup.getBuilder(
let imageBuilderGroup = builderGroup;
if (opt_declutterBuilderGroup) {
const declutterMode = imageStyle.getDeclutterMode();
if (declutterMode !== 'none') {
imageBuilderGroup = opt_declutterBuilderGroup;
if (declutterMode === 'obstacle') {
// draw in non-declutter group:
const imageReplay = builderGroup.getBuilder(
style.getZIndex(),
BuilderType.IMAGE
);
imageReplay.setImageStyle(imageStyle, declutterImageWithText);
imageReplay.drawMultiPoint(geometry, feature);
} else if (textStyle && textStyle.getText()) {
declutterImageWithText = {};
}
}
}
const imageReplay = imageBuilderGroup.getBuilder(
style.getZIndex(),
BuilderType.IMAGE
);
@@ -423,7 +453,11 @@ function renderMultiPointGeometry(
imageReplay.drawMultiPoint(geometry, feature);
}
if (textStyle && textStyle.getText()) {
const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder(
let textBuilderGroup = builderGroup;
if (opt_declutterBuilderGroup) {
textBuilderGroup = opt_declutterBuilderGroup;
}
const textReplay = textBuilderGroup.getBuilder(
style.getZIndex(),
BuilderType.TEXT
);

View File

@@ -16,6 +16,7 @@ import RegularShape from './RegularShape.js';
* (positive rotation clockwise, meaningful only when used in conjunction with a two dimensional scale).
* @property {boolean} [rotateWithView=false] Whether to rotate the shape with the view
* (meaningful only when used in conjunction with a two dimensional scale).
* @property {"declutter"|"obstacle"|"none"|undefined} [declutterMode] Declutter mode
*/
/**
@@ -41,6 +42,7 @@ class CircleStyle extends RegularShape {
options.rotateWithView !== undefined ? options.rotateWithView : false,
displacement:
options.displacement !== undefined ? options.displacement : [0, 0],
declutterMode: options.declutterMode,
});
}
@@ -59,6 +61,7 @@ class CircleStyle extends RegularShape {
rotation: this.getRotation(),
rotateWithView: this.getRotateWithView(),
displacement: this.getDisplacement().slice(),
declutterMode: this.getDeclutterMode(),
});
style.setOpacity(this.getOpacity());
return style;

View File

@@ -44,6 +44,7 @@ import {getUid} from '../util.js';
* @property {import("../size.js").Size} [imgSize] Image size in pixels. Only required if `img` is set and `src` is not, and
* for SVG images in Internet Explorer 11. The provided `imgSize` needs to match the actual size of the image.
* @property {string} [src] Image source URI.
* @property {"declutter"|"obstacle"|"none"|undefined} [declutterMode] Declutter mode
*/
/**
@@ -86,6 +87,7 @@ class Icon extends ImageStyle {
displacement:
options.displacement !== undefined ? options.displacement : [0, 0],
rotateWithView: rotateWithView,
declutterMode: options.declutterMode,
});
/**

View File

@@ -11,7 +11,7 @@ import {toSize} from '../size.js';
* @property {number} rotation Rotation.
* @property {number|import("../size.js").Size} scale Scale.
* @property {Array<number>} displacement Displacement.
*/
* @property {"declutter"|"obstacle"|"none"|undefined} declutterMode Declutter mode: `declutter`, `obstacle`, 'none */
/**
* @classdesc
@@ -61,6 +61,12 @@ class ImageStyle {
* @type {Array<number>}
*/
this.displacement_ = options.displacement;
/**
* @private
* @type {"declutter"|"obstacle"|"none"|undefined}
*/
this.declutterMode_ = options.declutterMode;
}
/**
@@ -76,6 +82,7 @@ class ImageStyle {
rotation: this.getRotation(),
rotateWithView: this.getRotateWithView(),
displacement: this.getDisplacement().slice(),
declutterMode: this.getDeclutterMode(),
});
}
@@ -132,6 +139,15 @@ class ImageStyle {
return this.displacement_;
}
/**
* Get the declutter mode of the shape
* @return {"declutter"|"obstacle"|"none"|undefined} Shape's declutter mode
* @api
*/
getDeclutterMode() {
return this.declutterMode_;
}
/**
* Get the anchor point in pixels. The anchor determines the center point for the
* symbolizer.

View File

@@ -31,6 +31,7 @@ import {
* @property {boolean} [rotateWithView=false] Whether to rotate the shape with the view.
* @property {number|import("../size.js").Size} [scale=1] Scale. Unless two dimensional scaling is required a better
* result may be obtained with appropriate settings for `radius`, `radius1` and `radius2`.
* @property {"declutter"|"obstacle"|"none"|undefined} [declutterMode] Declutter mode
*/
/**
@@ -69,6 +70,7 @@ class RegularShape extends ImageStyle {
scale: options.scale !== undefined ? options.scale : 1,
displacement:
options.displacement !== undefined ? options.displacement : [0, 0],
declutterMode: options.declutterMode,
});
/**
@@ -159,6 +161,7 @@ class RegularShape extends ImageStyle {
rotateWithView: this.getRotateWithView(),
scale: Array.isArray(scale) ? scale.slice() : scale,
displacement: this.getDisplacement().slice(),
declutterMode: this.getDeclutterMode(),
});
style.setOpacity(this.getOpacity());
return style;