Merge pull request #11592 from ahocevar/new-declutter

New decluttering implementation
This commit is contained in:
Andreas Hocevar
2020-10-07 10:33:33 +02:00
committed by GitHub
27 changed files with 889 additions and 808 deletions
+1 -2
View File
@@ -6,7 +6,6 @@ import stringify from 'json-stringify-safe';
import styleFunction from 'ol-mapbox-style/dist/stylefunction.js'; import styleFunction from 'ol-mapbox-style/dist/stylefunction.js';
import {Projection} from '../src/ol/proj.js'; import {Projection} from '../src/ol/proj.js';
import {inView} from '../src/ol/layer/Layer.js'; import {inView} from '../src/ol/layer/Layer.js';
import {renderDeclutterItems} from '../src/ol/render.js';
import {getTilePriority as tilePriorityFunction} from '../src/ol/TileQueue.js'; import {getTilePriority as tilePriorityFunction} from '../src/ol/TileQueue.js';
/** @type {any} */ /** @type {any} */
@@ -145,7 +144,7 @@ worker.addEventListener('message', (event) => {
renderer.renderFrame(frameState, canvas); renderer.renderFrame(frameState, canvas);
} }
}); });
renderDeclutterItems(frameState, null); layers.forEach((layer) => layer.renderDeclutter(frameState));
if (tileQueue.getTilesLoading() < maxTotalLoading) { if (tileQueue.getTilesLoading() < maxTotalLoading) {
tileQueue.reprioritize(); tileQueue.reprioritize();
tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads); tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads);
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1,53 @@
import Feature from '../../../src/ol/Feature.js';
import Map from '../../../src/ol/Map.js';
import MultiPoint from '../../../src/ol/geom/MultiPoint.js';
import VectorSource from '../../../src/ol/source/Vector.js';
import View from '../../../src/ol/View.js';
import {Icon, Stroke, Style, Text} from '../../../src/ol/style.js';
import {Vector as VectorLayer} from '../../../src/ol/layer.js';
const vectorLayer = new VectorLayer({
declutter: true,
renderBuffer: 0,
source: new VectorSource({
features: [
new Feature(
new MultiPoint([
[0, 0],
[0, 1],
[0.5, 0.5],
[0.9, 0.85],
[1, 0],
[0.3, 0.5],
])
),
],
}),
style: new Style({
image: new Icon({
anchor: [0.5, 46],
anchorXUnits: 'fraction',
anchorYUnits: 'pixels',
src: '/data/icon.png',
}),
text: new Text({
text: 'Test',
font: 'italic 700 20px Ubuntu',
stroke: new Stroke({
color: 'red',
width: 20,
}),
}),
}),
});
new Map({
layers: [vectorLayer],
target: document.getElementById('map'),
view: new View({
center: [0.5, 0.5],
resolution: 0.006679631467570084,
}),
});
render({tolerance: 0.007});
+2 -10
View File
@@ -50,8 +50,8 @@ import {removeNode} from './dom.js';
* @property {import("./View.js").State} viewState The state of the current view. * @property {import("./View.js").State} viewState The state of the current view.
* @property {boolean} animate * @property {boolean} animate
* @property {import("./transform.js").Transform} coordinateToPixelTransform * @property {import("./transform.js").Transform} coordinateToPixelTransform
* @property {import("rbush").default} declutterTree
* @property {null|import("./extent.js").Extent} extent * @property {null|import("./extent.js").Extent} extent
* @property {Array<DeclutterItems>} declutterItems
* @property {number} index * @property {number} index
* @property {Array<import("./layer/Layer.js").State>} layerStatesArray * @property {Array<import("./layer/Layer.js").State>} layerStatesArray
* @property {number} layerIndex * @property {number} layerIndex
@@ -64,12 +64,6 @@ import {removeNode} from './dom.js';
* @property {!Object<string, Object<string, boolean>>} wantedTiles * @property {!Object<string, Object<string, boolean>>} wantedTiles
*/ */
/**
* @typedef {Object} DeclutterItems
* @property {Array<*>} items Declutter items of an executor.
* @property {number} opacity Layer opacity.
*/
/** /**
* @typedef {function(PluggableMap, ?FrameState): any} PostRenderFunction * @typedef {function(PluggableMap, ?FrameState): any} PostRenderFunction
*/ */
@@ -1387,9 +1381,7 @@ class PluggableMap extends BaseObject {
frameState = { frameState = {
animate: false, animate: false,
coordinateToPixelTransform: this.coordinateToPixelTransform_, coordinateToPixelTransform: this.coordinateToPixelTransform_,
declutterItems: previousFrameState declutterTree: null,
? previousFrameState.declutterItems
: [],
extent: getForViewAndSize( extent: getForViewAndSize(
viewState.center, viewState.center,
viewState.resolution, viewState.resolution,
+6
View File
@@ -45,6 +45,12 @@ class VectorRenderTile extends Tile {
*/ */
this.executorGroups = {}; this.executorGroups = {};
/**
* Executor groups for decluttering, by layer uid. Entries are read/written by the renderer.
* @type {Object<string, Array<import("./render/canvas/ExecutorGroup.js").default>>}
*/
this.declutterExecutorGroups = {};
/** /**
* Number of loading source tiles. Read/written by the source. * Number of loading source tiles. Read/written by the source.
* @type {number} * @type {number}
+12
View File
@@ -2,6 +2,7 @@
* @module ol/layer/BaseVector * @module ol/layer/BaseVector
*/ */
import Layer from './Layer.js'; import Layer from './Layer.js';
import RBush from 'rbush';
import {assign} from '../obj.js'; import {assign} from '../obj.js';
import { import {
createDefaultStyle, createDefaultStyle,
@@ -214,6 +215,17 @@ class BaseVectorLayer extends Layer {
return this.updateWhileInteracting_; return this.updateWhileInteracting_;
} }
/**
* Render declutter items for this layer
* @param {import("../PluggableMap.js").FrameState} frameState Frame state.
*/
renderDeclutter(frameState) {
if (!frameState.declutterTree) {
frameState.declutterTree = new RBush(9);
}
/** @type {*} */ (this.getRenderer()).renderDeclutter(frameState);
}
/** /**
* @param {import("../render.js").OrderFunction|null|undefined} renderOrder * @param {import("../render.js").OrderFunction|null|undefined} renderOrder
* Render order. * Render order.
+2
View File
@@ -309,6 +309,8 @@ class Heatmap extends VectorLayer {
], ],
}); });
} }
renderDeclutter() {}
} }
/** /**
-26
View File
@@ -129,29 +129,3 @@ export function getRenderPixel(event, pixel) {
applyTransform(event.inversePixelTransform.slice(), result); applyTransform(event.inversePixelTransform.slice(), result);
return result; return result;
} }
/**
* @param {import("./PluggableMap.js").FrameState} frameState Frame state.
* @param {?} declutterTree Declutter tree.
* @returns {?} Declutter tree.
*/
export function renderDeclutterItems(frameState, declutterTree) {
if (declutterTree) {
declutterTree.clear();
}
const items = frameState.declutterItems;
for (let z = items.length - 1; z >= 0; --z) {
const item = items[z];
const zIndexItems = item.items;
for (let i = 0, ii = zIndexItems.length; i < ii; i += 3) {
declutterTree = zIndexItems[i].renderDeclutter(
zIndexItems[i + 1],
zIndexItems[i + 2],
item.opacity,
declutterTree
);
}
}
items.length = 0;
return declutterTree;
}
+4 -4
View File
@@ -100,15 +100,15 @@ class VectorContext {
/** /**
* @param {import("../style/Image.js").default} imageStyle Image style. * @param {import("../style/Image.js").default} imageStyle Image style.
* @param {import("./canvas.js").DeclutterGroup=} opt_declutterGroup Declutter. * @param {import("../render/canvas.js").DeclutterImageWithText=} opt_declutterImageWithText Shared data for combined decluttering with a text style.
*/ */
setImageStyle(imageStyle, opt_declutterGroup) {} setImageStyle(imageStyle, opt_declutterImageWithText) {}
/** /**
* @param {import("../style/Text.js").default} textStyle Text style. * @param {import("../style/Text.js").default} textStyle Text style.
* @param {import("./canvas.js").DeclutterGroups=} opt_declutterGroups Declutter. * @param {import("../render/canvas.js").DeclutterImageWithText=} opt_declutterImageWithText Shared data for combined decluttering with an image style.
*/ */
setTextStyle(textStyle, opt_declutterGroups) {} setTextStyle(textStyle, opt_declutterImageWithText) {}
} }
export default VectorContext; export default VectorContext;
+11 -15
View File
@@ -68,20 +68,17 @@ import {toString} from '../transform.js';
*/ */
/** /**
* Container for decluttered replay instructions that need to be rendered or * @typedef {Object} SerializableInstructions
* omitted together, i.e. when styles render both an image and text, or for the * @property {Array<*>} instructions The rendering instructions.
* characters that form text along lines. The basic elements of this array are * @property {Array<*>} hitDetectionInstructions The rendering hit detection instructions.
* `[minX, minY, maxX, maxY, count]`, where the first four entries are the * @property {Array<number>} coordinates The array of all coordinates.
* rendered extent of the group in pixel space. `count` is the number of styles * @property {!Object<string, TextState>} [textStates] The text states (decluttering).
* in the group, i.e. 2 when an image and a text are grouped, or 1 otherwise. * @property {!Object<string, FillState>} [fillStates] The fill states (decluttering).
* In addition to these four elements, declutter instruction arrays (i.e. the * @property {!Object<string, StrokeState>} [strokeStates] The stroke states (decluttering).
* arguments to {@link module:ol/render/canvas~drawImage} are appended to the array.
* @typedef {Array<*>} DeclutterGroup
*/ */
/** /**
* Declutter groups for support of multi geometries. * @typedef {Object<number, import("./canvas/Executor.js").ReplayImageOrLabelArgs>} DeclutterImageWithText
* @typedef {Array<DeclutterGroup>} DeclutterGroups
*/ */
/** /**
@@ -293,9 +290,8 @@ export const measureTextHeight = (function () {
* @type {HTMLDivElement} * @type {HTMLDivElement}
*/ */
let div; let div;
const heights = textHeights;
return function (fontSpec) { return function (fontSpec) {
let height = heights[fontSpec]; let height = textHeights[fontSpec];
if (height == undefined) { if (height == undefined) {
if (WORKER_OFFSCREEN_CANVAS) { if (WORKER_OFFSCREEN_CANVAS) {
const font = getFontParameters(fontSpec); const font = getFontParameters(fontSpec);
@@ -303,7 +299,7 @@ export const measureTextHeight = (function () {
const lineHeight = isNaN(Number(font.lineHeight)) const lineHeight = isNaN(Number(font.lineHeight))
? 1.2 ? 1.2
: Number(font.lineHeight); : Number(font.lineHeight);
textHeights[fontSpec] = height =
lineHeight * lineHeight *
(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent); (metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent);
} else { } else {
@@ -318,9 +314,9 @@ export const measureTextHeight = (function () {
div.style.font = fontSpec; div.style.font = fontSpec;
document.body.appendChild(div); document.body.appendChild(div);
height = div.offsetHeight; height = div.offsetHeight;
heights[fontSpec] = height;
document.body.removeChild(div); document.body.removeChild(div);
} }
textHeights[fontSpec] = height;
} }
return height; return height;
}; };
+1 -11
View File
@@ -29,16 +29,6 @@ import {
inflateMultiCoordinatesArray, inflateMultiCoordinatesArray,
} from '../../geom/flat/inflate.js'; } from '../../geom/flat/inflate.js';
/**
* @typedef {Object} SerializableInstructions
* @property {Array<*>} instructions The rendering instructions.
* @property {Array<*>} hitDetectionInstructions The rendering hit detection instructions.
* @property {Array<number>} coordinates The array of all coordinates.
* @property {!Object<string, import("../canvas.js").TextState>} [textStates] The text states (decluttering).
* @property {!Object<string, import("../canvas.js").FillState>} [fillStates] The fill states (decluttering).
* @property {!Object<string, import("../canvas.js").StrokeState>} [strokeStates] The stroke states (decluttering).
*/
class CanvasBuilder extends VectorContext { class CanvasBuilder extends VectorContext {
/** /**
* @param {number} tolerance Tolerance. * @param {number} tolerance Tolerance.
@@ -383,7 +373,7 @@ class CanvasBuilder extends VectorContext {
} }
/** /**
* @return {SerializableInstructions} the serializable instructions. * @return {import("../canvas.js").SerializableInstructions} the serializable instructions.
*/ */
finish() { finish() {
return { return {
+1 -33
View File
@@ -26,21 +26,8 @@ class BuilderGroup {
* @param {import("../../extent.js").Extent} maxExtent Max extent. * @param {import("../../extent.js").Extent} maxExtent Max extent.
* @param {number} resolution Resolution. * @param {number} resolution Resolution.
* @param {number} pixelRatio Pixel ratio. * @param {number} pixelRatio Pixel ratio.
* @param {boolean} declutter Decluttering enabled.
*/ */
constructor(tolerance, maxExtent, resolution, pixelRatio, declutter) { constructor(tolerance, maxExtent, resolution, pixelRatio) {
/**
* @type {boolean}
* @private
*/
this.declutter_ = declutter;
/**
* @type {import("../canvas.js").DeclutterGroups}
* @private
*/
this.declutterGroups_ = null;
/** /**
* @private * @private
* @type {number} * @type {number}
@@ -72,25 +59,6 @@ class BuilderGroup {
this.buildersByZIndex_ = {}; this.buildersByZIndex_ = {};
} }
/**
* @param {boolean} group Group with previous builder.
* @return {import("../canvas").DeclutterGroups} The resulting instruction groups.
*/
addDeclutter(group) {
/** @type {Array<*>} */
let declutter = null;
if (this.declutter_) {
if (group) {
declutter = this.declutterGroups_;
/** @type {number} */ (declutter[0][0])++;
} else {
declutter = [[1]];
this.declutterGroups_ = declutter;
}
}
return declutter;
}
/** /**
* @return {!Object<string, !Object<import("./BuilderType").default, import("./Builder.js").SerializableInstructions>>} The serializable instructions * @return {!Object<string, !Object<import("./BuilderType").default, import("./Builder.js").SerializableInstructions>>} The serializable instructions
*/ */
+290 -311
View File
@@ -2,7 +2,6 @@
* @module ol/render/canvas/Executor * @module ol/render/canvas/Executor
*/ */
import CanvasInstruction from './Instruction.js'; import CanvasInstruction from './Instruction.js';
import RBush from 'rbush';
import {TEXT_ALIGN} from './TextBuilder.js'; import {TEXT_ALIGN} from './TextBuilder.js';
import {WORKER_OFFSCREEN_CANVAS} from '../../has.js'; import {WORKER_OFFSCREEN_CANVAS} from '../../has.js';
import { import {
@@ -11,13 +10,7 @@ import {
create as createTransform, create as createTransform,
setFromArray as transformSetFromArray, setFromArray as transformSetFromArray,
} from '../../transform.js'; } from '../../transform.js';
import { import {createEmpty, createOrUpdate, intersects} from '../../extent.js';
createEmpty,
createOrUpdate,
getHeight,
getWidth,
intersects,
} from '../../extent.js';
import { import {
defaultPadding, defaultPadding,
defaultTextBaseline, defaultTextBaseline,
@@ -35,13 +28,29 @@ import {lineStringLength} from '../../geom/flat/length.js';
import {transform2D} from '../../geom/flat/transform.js'; import {transform2D} from '../../geom/flat/transform.js';
/** /**
* @typedef {Object} SerializableInstructions * @typedef {Object} BBox
* @property {Array<*>} instructions The rendering instructions. * @property {number} minX
* @property {Array<*>} hitDetectionInstructions The rendering hit detection instructions. * @property {number} minY
* @property {Array<number>} coordinates The array of all coordinates. * @property {number} maxX
* @property {!Object<string, import("../canvas.js").TextState>} textStates The text states (decluttering). * @property {number} maxY
* @property {!Object<string, import("../canvas.js").FillState>} fillStates The fill states (decluttering). * @property {*} value
* @property {!Object<string, import("../canvas.js").StrokeState>} strokeStates The stroke states (decluttering). */
/**
* @typedef {Object} ImageOrLabelDimensions
* @property {number} drawImageX
* @property {number} drawImageY
* @property {number} drawImageW
* @property {number} drawImageH
* @property {number} originX
* @property {number} originY
* @property {Array<number>} scale
* @property {BBox} declutterBox
* @property {import("../../transform.js").Transform} canvasTransform
*/
/**
* @typedef {{0: CanvasRenderingContext2D, 1: number, 2: import("../canvas.js").Label|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement, 3: ImageOrLabelDimensions, 4: number, 5: Array<*>, 6: Array<*>}} ReplayImageOrLabelArgs
*/ */
/** /**
@@ -49,11 +58,6 @@ import {transform2D} from '../../geom/flat/transform.js';
*/ */
const tmpExtent = createEmpty(); const tmpExtent = createEmpty();
/**
* @type {!import("../../transform.js").Transform}
*/
const tmpTransform = createTransform();
/** @type {import("../../coordinate.js").Coordinate} */ /** @type {import("../../coordinate.js").Coordinate} */
const p1 = []; const p1 = [];
/** @type {import("../../coordinate.js").Coordinate} */ /** @type {import("../../coordinate.js").Coordinate} */
@@ -63,12 +67,20 @@ const p3 = [];
/** @type {import("../../coordinate.js").Coordinate} */ /** @type {import("../../coordinate.js").Coordinate} */
const p4 = []; const p4 = [];
/**
* @param {ReplayImageOrLabelArgs} replayImageOrLabelArgs Arguments to replayImageOrLabel
* @return {BBox} Declutter bbox.
*/
function getDeclutterBox(replayImageOrLabelArgs) {
return replayImageOrLabelArgs[3].declutterBox;
}
class Executor { class Executor {
/** /**
* @param {number} resolution Resolution. * @param {number} resolution Resolution.
* @param {number} pixelRatio Pixel ratio. * @param {number} pixelRatio Pixel ratio.
* @param {boolean} overlaps The replay can have overlapping geometries. * @param {boolean} overlaps The replay can have overlapping geometries.
* @param {SerializableInstructions} instructions The serializable instructions * @param {import("../canvas.js").SerializableInstructions} instructions The serializable instructions
* @param {import("../../size.js").Size} renderBuffer Render buffer (width/height) in pixels. * @param {import("../../size.js").Size} renderBuffer Render buffer (width/height) in pixels.
*/ */
constructor(resolution, pixelRatio, overlaps, instructions, renderBuffer) { constructor(resolution, pixelRatio, overlaps, instructions, renderBuffer) {
@@ -97,11 +109,6 @@ class Executor {
*/ */
this.alignFill_; this.alignFill_;
/**
* @type {Array<*>}
*/
this.declutterItems = [];
/** /**
* @protected * @protected
* @type {Array<*>} * @type {Array<*>}
@@ -274,7 +281,6 @@ class Executor {
* @param {import("../../coordinate.js").Coordinate} p4 4th point of the background box. * @param {import("../../coordinate.js").Coordinate} p4 4th point of the background box.
* @param {Array<*>} fillInstruction Fill instruction. * @param {Array<*>} fillInstruction Fill instruction.
* @param {Array<*>} strokeInstruction Stroke instruction. * @param {Array<*>} strokeInstruction Stroke instruction.
* @param {boolean} declutter Declutter.
*/ */
replayTextBackground_( replayTextBackground_(
context, context,
@@ -283,8 +289,7 @@ class Executor {
p3, p3,
p4, p4,
fillInstruction, fillInstruction,
strokeInstruction, strokeInstruction
declutter
) { ) {
context.beginPath(); context.beginPath();
context.moveTo.apply(context, p1); context.moveTo.apply(context, p1);
@@ -294,9 +299,6 @@ class Executor {
context.lineTo.apply(context, p1); context.lineTo.apply(context, p1);
if (fillInstruction) { if (fillInstruction) {
this.alignFill_ = /** @type {boolean} */ (fillInstruction[2]); this.alignFill_ = /** @type {boolean} */ (fillInstruction[2]);
if (declutter) {
context.fillStyle = /** @type {import("../../colorlike.js").ColorLike} */ (fillInstruction[1]);
}
this.fill_(context); this.fill_(context);
} }
if (strokeInstruction) { if (strokeInstruction) {
@@ -310,62 +312,49 @@ class Executor {
/** /**
* @private * @private
* @param {CanvasRenderingContext2D} context Context. * @param {number} sheetWidth Width of the sprite sheet.
* @param {number} contextScale Scale of the context. * @param {number} sheetHeight Height of the sprite sheet.
* @param {number} x X. * @param {number} centerX X.
* @param {number} y Y. * @param {number} centerY Y.
* @param {import("../canvas.js").Label|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} imageOrLabel Image. * @param {number} width Width.
* @param {number} height Height.
* @param {number} anchorX Anchor X. * @param {number} anchorX Anchor X.
* @param {number} anchorY Anchor Y. * @param {number} anchorY Anchor Y.
* @param {import("../canvas.js").DeclutterGroup} declutterGroup Declutter group.
* @param {number} height Height.
* @param {number} opacity Opacity.
* @param {number} originX Origin X. * @param {number} originX Origin X.
* @param {number} originY Origin Y. * @param {number} originY Origin Y.
* @param {number} rotation Rotation. * @param {number} rotation Rotation.
* @param {import("../../size.js").Size} scale Scale. * @param {import("../../size.js").Size} scale Scale.
* @param {boolean} snapToPixel Snap to pixel. * @param {boolean} snapToPixel Snap to pixel.
* @param {number} width Width.
* @param {Array<number>} padding Padding. * @param {Array<number>} padding Padding.
* @param {Array<*>} fillInstruction Fill instruction. * @param {boolean} fillStroke Background fill or stroke.
* @param {Array<*>} strokeInstruction Stroke instruction. * @param {import("../../Feature.js").FeatureLike} feature Feature.
* @return {boolean} The image or label was rendered. * @return {ImageOrLabelDimensions} Dimensions for positioning and decluttering the image or label.
*/ */
replayImageOrLabel_( calculateImageOrLabelDimensions_(
context, sheetWidth,
contextScale, sheetHeight,
x, centerX,
y, centerY,
imageOrLabel, width,
height,
anchorX, anchorX,
anchorY, anchorY,
declutterGroup,
height,
opacity,
originX, originX,
originY, originY,
rotation, rotation,
scale, scale,
snapToPixel, snapToPixel,
width,
padding, padding,
fillInstruction, fillStroke,
strokeInstruction feature
) { ) {
const fillStroke = fillInstruction || strokeInstruction;
anchorX *= scale[0]; anchorX *= scale[0];
anchorY *= scale[1]; anchorY *= scale[1];
x -= anchorX; let x = centerX - anchorX;
y -= anchorY; let y = centerY - anchorY;
const w = const w = width + originX > sheetWidth ? sheetWidth - originX : width;
width + originX > imageOrLabel.width const h = height + originY > sheetHeight ? sheetHeight - originY : height;
? imageOrLabel.width - originX
: width;
const h =
height + originY > imageOrLabel.height
? imageOrLabel.height - originY
: height;
const boxW = padding[3] + w * scale[0] + padding[1]; const boxW = padding[3] + w * scale[0] + padding[1];
const boxH = padding[0] + h * scale[1] + padding[2]; const boxH = padding[0] + h * scale[1] + padding[2];
const boxX = x - padding[3]; const boxX = x - padding[3];
@@ -382,12 +371,10 @@ class Executor {
p4[1] = p3[1]; p4[1] = p3[1];
} }
let transform = null; let transform;
if (rotation !== 0) { if (rotation !== 0) {
const centerX = x + anchorX;
const centerY = y + anchorY;
transform = composeTransform( transform = composeTransform(
tmpTransform, createTransform(),
centerX, centerX,
centerY, centerY,
1, 1,
@@ -397,10 +384,10 @@ class Executor {
-centerY -centerY
); );
applyTransform(tmpTransform, p1); applyTransform(transform, p1);
applyTransform(tmpTransform, p2); applyTransform(transform, p2);
applyTransform(tmpTransform, p3); applyTransform(transform, p3);
applyTransform(tmpTransform, p4); applyTransform(transform, p4);
createOrUpdate( createOrUpdate(
Math.min(p1[0], p2[0], p3[0], p4[0]), Math.min(p1[0], p2[0], p3[0], p4[0]),
Math.min(p1[1], p2[1], p3[1], p4[1]), Math.min(p1[1], p2[1], p3[1], p4[1]),
@@ -417,66 +404,63 @@ class Executor {
tmpExtent tmpExtent
); );
} }
let renderBufferX = 0;
let renderBufferY = 0;
if (declutterGroup) {
const renderBuffer = this.renderBuffer_;
renderBuffer[0] = Math.max(renderBuffer[0], getWidth(tmpExtent));
renderBufferX = renderBuffer[0];
renderBuffer[1] = Math.max(renderBuffer[1], getHeight(tmpExtent));
renderBufferY = renderBuffer[1];
}
const canvas = context.canvas;
const strokePadding = strokeInstruction
? (strokeInstruction[2] * scale[0]) / 2
: 0;
const intersects =
tmpExtent[0] - strokePadding <=
(canvas.width + renderBufferX) / contextScale &&
tmpExtent[2] + strokePadding >= -renderBufferX / contextScale &&
tmpExtent[1] - strokePadding <=
(canvas.height + renderBufferY) / contextScale &&
tmpExtent[3] + strokePadding >= -renderBufferY / contextScale;
if (snapToPixel) { if (snapToPixel) {
x = Math.round(x); x = Math.round(x);
y = Math.round(y); y = Math.round(y);
} }
return {
drawImageX: x,
drawImageY: y,
drawImageW: w,
drawImageH: h,
originX: originX,
originY: originY,
declutterBox: {
minX: tmpExtent[0],
minY: tmpExtent[1],
maxX: tmpExtent[2],
maxY: tmpExtent[3],
value: feature,
},
canvasTransform: transform,
scale: scale,
};
}
if (declutterGroup) { /**
if (!intersects && declutterGroup[0] == 1) { * @private
return false; * @param {CanvasRenderingContext2D} context Context.
} * @param {number} contextScale Scale of the context.
const declutterArgs = intersects * @param {import("../canvas.js").Label|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} imageOrLabel Image.
? [ * @param {ImageOrLabelDimensions} dimensions Dimensions.
context, * @param {number} opacity Opacity.
transform ? transform.slice(0) : null, * @param {Array<*>} fillInstruction Fill instruction.
opacity, * @param {Array<*>} strokeInstruction Stroke instruction.
imageOrLabel, * @return {boolean} The image or label was rendered.
originX, */
originY, replayImageOrLabel_(
w, context,
h, contextScale,
x, imageOrLabel,
y, dimensions,
scale, opacity,
tmpExtent.slice(), fillInstruction,
] strokeInstruction
: null; ) {
if (declutterArgs) { const fillStroke = !!(fillInstruction || strokeInstruction);
if (fillStroke) {
declutterArgs.push( const box = dimensions.declutterBox;
fillInstruction, const canvas = context.canvas;
strokeInstruction, const strokePadding = strokeInstruction
p1.slice(0), ? (strokeInstruction[2] * dimensions.scale[0]) / 2
p2.slice(0), : 0;
p3.slice(0), const intersects =
p4.slice(0) box.minX - strokePadding <= canvas.width / contextScale &&
); box.maxX + strokePadding >= 0 &&
} box.minY - strokePadding <= canvas.height / contextScale &&
declutterGroup.push(declutterArgs); box.maxY + strokePadding >= 0;
}
} else if (intersects) { if (intersects) {
if (fillStroke) { if (fillStroke) {
this.replayTextBackground_( this.replayTextBackground_(
context, context,
@@ -485,22 +469,21 @@ class Executor {
p3, p3,
p4, p4,
/** @type {Array<*>} */ (fillInstruction), /** @type {Array<*>} */ (fillInstruction),
/** @type {Array<*>} */ (strokeInstruction), /** @type {Array<*>} */ (strokeInstruction)
false
); );
} }
drawImageOrLabel( drawImageOrLabel(
context, context,
transform, dimensions.canvasTransform,
opacity, opacity,
imageOrLabel, imageOrLabel,
originX, dimensions.originX,
originY, dimensions.originY,
w, dimensions.drawImageW,
h, dimensions.drawImageH,
x, dimensions.drawImageX,
y, dimensions.drawImageY,
scale dimensions.scale
); );
} }
return true; return true;
@@ -530,7 +513,9 @@ class Executor {
* @param {Array<*>} instruction Instruction. * @param {Array<*>} instruction Instruction.
*/ */
setStrokeStyle_(context, instruction) { setStrokeStyle_(context, instruction) {
context.strokeStyle = /** @type {import("../../colorlike.js").ColorLike} */ (instruction[1]); context[
'strokeStyle'
] = /** @type {import("../../colorlike.js").ColorLike} */ (instruction[1]);
context.lineWidth = /** @type {number} */ (instruction[2]); context.lineWidth = /** @type {number} */ (instruction[2]);
context.lineCap = /** @type {CanvasLineCap} */ (instruction[3]); context.lineCap = /** @type {CanvasLineCap} */ (instruction[3]);
context.lineJoin = /** @type {CanvasLineJoin} */ (instruction[4]); context.lineJoin = /** @type {CanvasLineJoin} */ (instruction[4]);
@@ -541,68 +526,6 @@ class Executor {
} }
} }
/**
* @param {import("../canvas.js").DeclutterGroup} declutterGroup Declutter group.
* @param {import("../../Feature.js").FeatureLike} feature Feature.
* @param {number} opacity Layer opacity.
* @param {?} declutterTree Declutter tree.
* @return {?} Declutter tree.
*/
renderDeclutter(declutterGroup, feature, opacity, declutterTree) {
/** @type {Array<import("../../structs/RBush.js").Entry>} */
const boxes = [];
for (let i = 1, ii = declutterGroup.length; i < ii; ++i) {
const declutterData = declutterGroup[i];
const box = declutterData[11];
boxes.push({
minX: box[0],
minY: box[1],
maxX: box[2],
maxY: box[3],
value: feature,
});
}
if (!declutterTree) {
declutterTree = new RBush(9);
}
let collides = false;
for (let i = 0, ii = boxes.length; i < ii; ++i) {
if (declutterTree.collides(boxes[i])) {
collides = true;
break;
}
}
if (!collides) {
declutterTree.load(boxes);
for (let j = 1, jj = declutterGroup.length; j < jj; ++j) {
const declutterData = /** @type {Array} */ (declutterGroup[j]);
const context = declutterData[0];
const currentAlpha = context.globalAlpha;
if (currentAlpha !== opacity) {
context.globalAlpha = opacity;
}
if (declutterData.length > 12) {
this.replayTextBackground_(
declutterData[0],
declutterData[14],
declutterData[15],
declutterData[16],
declutterData[17],
declutterData[12],
declutterData[13],
true
);
}
drawImageOrLabel.apply(undefined, declutterData);
if (currentAlpha !== opacity) {
context.globalAlpha = currentAlpha;
}
}
}
declutterGroup.length = 1;
return declutterTree;
}
/** /**
* @private * @private
* @param {string} text The text to draw. * @param {string} text The text to draw.
@@ -647,6 +570,7 @@ class Executor {
* @param {function(import("../../Feature.js").FeatureLike): T|undefined} featureCallback Feature callback. * @param {function(import("../../Feature.js").FeatureLike): T|undefined} featureCallback Feature callback.
* @param {import("../../extent.js").Extent=} opt_hitExtent Only check features that intersect this * @param {import("../../extent.js").Extent=} opt_hitExtent Only check features that intersect this
* extent. * extent.
* @param {import("rbush").default=} opt_declutterTree Declutter tree.
* @return {T|undefined} Callback result. * @return {T|undefined} Callback result.
* @template T * @template T
*/ */
@@ -657,9 +581,9 @@ class Executor {
instructions, instructions,
snapToPixel, snapToPixel,
featureCallback, featureCallback,
opt_hitExtent opt_hitExtent,
opt_declutterTree
) { ) {
this.declutterItems.length = 0;
/** @type {Array<number>} */ /** @type {Array<number>} */
let pixelCoordinates; let pixelCoordinates;
if (this.pixelCoordinates_ && equals(transform, this.renderedTransform_)) { if (this.pixelCoordinates_ && equals(transform, this.renderedTransform_)) {
@@ -688,12 +612,11 @@ class Executor {
prevY, prevY,
roundX, roundX,
roundY, roundY,
declutterGroup,
declutterGroups,
image, image,
text, text,
textKey; textKey,
let strokeKey, fillKey; strokeKey,
fillKey;
let pendingFill = 0; let pendingFill = 0;
let pendingStroke = 0; let pendingStroke = 0;
let lastFillInstruction = null; let lastFillInstruction = null;
@@ -796,15 +719,15 @@ class Executor {
// 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]);
declutterGroups = featureCallback ? null : instruction[6]; let height = /** @type {number} */ (instruction[6]);
let height = /** @type {number} */ (instruction[7]); const opacity = /** @type {number} */ (instruction[7]);
const opacity = /** @type {number} */ (instruction[8]); const originX = /** @type {number} */ (instruction[8]);
const originX = /** @type {number} */ (instruction[9]); const originY = /** @type {number} */ (instruction[9]);
const originY = /** @type {number} */ (instruction[10]); const rotateWithView = /** @type {boolean} */ (instruction[10]);
const rotateWithView = /** @type {boolean} */ (instruction[11]); let rotation = /** @type {number} */ (instruction[11]);
let rotation = /** @type {number} */ (instruction[12]); const scale = /** @type {import("../../size.js").Size} */ (instruction[12]);
const scale = /** @type {import("../../size.js").Size} */ (instruction[13]); let width = /** @type {number} */ (instruction[13]);
let width = /** @type {number} */ (instruction[14]); const declutterImageWithText = /** @type {import("../canvas.js").DeclutterImageWithText} */ (instruction[14]);
if (!image && instruction.length >= 19) { if (!image && instruction.length >= 19) {
// create label images // create label images
@@ -827,9 +750,9 @@ class Executor {
anchorY = (labelWithAnchor.anchorY - textOffsetY) * this.pixelRatio; anchorY = (labelWithAnchor.anchorY - textOffsetY) * this.pixelRatio;
instruction[5] = anchorY; instruction[5] = anchorY;
height = image.height; height = image.height;
instruction[7] = height; instruction[6] = height;
width = image.width; width = image.width;
instruction[14] = width; instruction[13] = width;
} }
let geometryWidths; let geometryWidths;
@@ -856,7 +779,6 @@ class Executor {
rotation -= viewRotation; rotation -= viewRotation;
} }
let widthIndex = 0; let widthIndex = 0;
let declutterGroupIndex = 0;
for (; d < dd; d += 2) { for (; d < dd; d += 2) {
if ( if (
geometryWidths && geometryWidths &&
@@ -864,51 +786,72 @@ class Executor {
) { ) {
continue; continue;
} }
if (declutterGroups) { const dimensions = this.calculateImageOrLabelDimensions_(
const index = Math.floor(declutterGroupIndex); image.width,
declutterGroup = image.height,
declutterGroups.length < index + 1
? [declutterGroups[0][0]]
: declutterGroups[index];
}
const rendered = this.replayImageOrLabel_(
context,
contextScale,
pixelCoordinates[d], pixelCoordinates[d],
pixelCoordinates[d + 1], pixelCoordinates[d + 1],
image, width,
height,
anchorX, anchorX,
anchorY, anchorY,
declutterGroup,
height,
opacity,
originX, originX,
originY, originY,
rotation, rotation,
scale, scale,
snapToPixel, snapToPixel,
width,
padding, padding,
backgroundFill || backgroundStroke,
feature
);
/** @type {ReplayImageOrLabelArgs} */
const args = [
context,
contextScale,
image,
dimensions,
opacity,
backgroundFill backgroundFill
? /** @type {Array<*>} */ (lastFillInstruction) ? /** @type {Array<*>} */ (lastFillInstruction)
: null, : null,
backgroundStroke backgroundStroke
? /** @type {Array<*>} */ (lastStrokeInstruction) ? /** @type {Array<*>} */ (lastStrokeInstruction)
: null : null,
); ];
if ( let imageArgs;
rendered && let imageDeclutterBox;
declutterGroup && if (opt_declutterTree && declutterImageWithText) {
declutterGroups[declutterGroups.length - 1] !== declutterGroup if (!declutterImageWithText[d]) {
) { // We now have the image for an image+text combination.
declutterGroups.push(declutterGroup); declutterImageWithText[d] = args;
} // Don't render anything for now, wait for the text.
if (declutterGroup) { continue;
if (declutterGroup.length - 1 === declutterGroup[0]) { }
this.declutterItems.push(this, declutterGroup, feature); imageArgs = declutterImageWithText[d];
delete declutterImageWithText[d];
imageDeclutterBox = getDeclutterBox(imageArgs);
if (opt_declutterTree.collides(imageDeclutterBox)) {
continue;
} }
declutterGroupIndex += 1 / declutterGroup[0];
} }
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);
}
this.replayImageOrLabel_.apply(this, args);
} }
++i; ++i;
break; break;
@@ -916,19 +859,18 @@ class Executor {
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 : instruction[4]; const overflow = /** @type {number} */ (instruction[4]);
const overflow = /** @type {number} */ (instruction[5]); fillKey = /** @type {string} */ (instruction[5]);
fillKey = /** @type {string} */ (instruction[6]); const maxAngle = /** @type {number} */ (instruction[6]);
const maxAngle = /** @type {number} */ (instruction[7]); const measurePixelRatio = /** @type {number} */ (instruction[7]);
const measurePixelRatio = /** @type {number} */ (instruction[8]); const offsetY = /** @type {number} */ (instruction[8]);
const offsetY = /** @type {number} */ (instruction[9]); strokeKey = /** @type {string} */ (instruction[9]);
strokeKey = /** @type {string} */ (instruction[10]); const strokeWidth = /** @type {number} */ (instruction[10]);
const strokeWidth = /** @type {number} */ (instruction[11]); text = /** @type {string} */ (instruction[11]);
text = /** @type {string} */ (instruction[12]); textKey = /** @type {string} */ (instruction[12]);
textKey = /** @type {string} */ (instruction[13]);
const pixelRatioScale = [ const pixelRatioScale = [
/** @type {number} */ (instruction[14]), /** @type {number} */ (instruction[13]),
/** @type {number} */ (instruction[14]), /** @type {number} */ (instruction[13]),
]; ];
const textState = this.textStates[textKey]; const textState = this.textStates[textKey];
@@ -967,8 +909,9 @@ class Executor {
cachedWidths, cachedWidths,
viewRotationFromTransform ? 0 : this.viewRotation_ viewRotationFromTransform ? 0 : this.viewRotation_
); );
if (parts) { drawChars: if (parts) {
let rendered = false; /** @type {Array<ReplayImageOrLabelArgs>} */
const replayImageOrLabelArgs = [];
let c, cc, chars, label, part; let c, cc, chars, label, part;
if (strokeKey) { if (strokeKey) {
for (c = 0, cc = parts.length; c < cc; ++c) { for (c = 0, cc = parts.length; c < cc; ++c) {
@@ -981,28 +924,39 @@ class Executor {
((0.5 - baseline) * 2 * strokeWidth * textScale[1]) / ((0.5 - baseline) * 2 * strokeWidth * textScale[1]) /
textScale[0] - textScale[0] -
offsetY; offsetY;
rendered = const dimensions = this.calculateImageOrLabelDimensions_(
this.replayImageOrLabel_( label.width,
context, label.height,
contextScale, part[0],
/** @type {number} */ (part[0]), part[1],
/** @type {number} */ (part[1]), label.width,
label, label.height,
anchorX, anchorX,
anchorY, anchorY,
declutterGroup, 0,
label.height, 0,
1, part[3],
0, pixelRatioScale,
0, false,
/** @type {number} */ (part[3]), defaultPadding,
pixelRatioScale, false,
false, feature
label.width, );
defaultPadding, if (
null, opt_declutterTree &&
null opt_declutterTree.collides(dimensions.declutterBox)
) || rendered; ) {
break drawChars;
}
replayImageOrLabelArgs.push([
context,
contextScale,
label,
dimensions,
1,
null,
null,
]);
} }
} }
if (fillKey) { if (fillKey) {
@@ -1012,32 +966,48 @@ class Executor {
label = this.createLabel(chars, textKey, fillKey, ''); label = this.createLabel(chars, textKey, fillKey, '');
anchorX = /** @type {number} */ (part[2]); anchorX = /** @type {number} */ (part[2]);
anchorY = baseline * label.height - offsetY; anchorY = baseline * label.height - offsetY;
rendered = const dimensions = this.calculateImageOrLabelDimensions_(
this.replayImageOrLabel_( label.width,
context, label.height,
contextScale, part[0],
/** @type {number} */ (part[0]), part[1],
/** @type {number} */ (part[1]), label.width,
label, label.height,
anchorX, anchorX,
anchorY, anchorY,
declutterGroup, 0,
label.height, 0,
1, part[3],
0, pixelRatioScale,
0, false,
/** @type {number} */ (part[3]), defaultPadding,
pixelRatioScale, false,
false, feature
label.width, );
defaultPadding, if (
null, opt_declutterTree &&
null opt_declutterTree.collides(dimensions.declutterBox)
) || rendered; ) {
break drawChars;
}
replayImageOrLabelArgs.push([
context,
contextScale,
label,
dimensions,
1,
null,
null,
]);
} }
} }
if (rendered) { if (opt_declutterTree) {
this.declutterItems.push(this, declutterGroup, feature); opt_declutterTree.load(
replayImageOrLabelArgs.map(getDeclutterBox)
);
}
for (let i = 0, ii = replayImageOrLabelArgs.length; i < ii; ++i) {
this.replayImageOrLabel_.apply(this, replayImageOrLabelArgs[i]);
} }
} }
} }
@@ -1139,8 +1109,16 @@ class Executor {
* @param {import("../../transform.js").Transform} transform Transform. * @param {import("../../transform.js").Transform} transform Transform.
* @param {number} viewRotation View rotation. * @param {number} viewRotation View rotation.
* @param {boolean} snapToPixel Snap point symbols and text to integer pixels. * @param {boolean} snapToPixel Snap point symbols and text to integer pixels.
* @param {import("rbush").default=} opt_declutterTree Declutter tree.
*/ */
execute(context, contextScale, transform, viewRotation, snapToPixel) { execute(
context,
contextScale,
transform,
viewRotation,
snapToPixel,
opt_declutterTree
) {
this.viewRotation_ = viewRotation; this.viewRotation_ = viewRotation;
this.execute_( this.execute_(
context, context,
@@ -1149,7 +1127,8 @@ class Executor {
this.instructions, this.instructions,
snapToPixel, snapToPixel,
undefined, undefined,
undefined undefined,
opt_declutterTree
); );
} }
+15 -61
View File
@@ -36,7 +36,7 @@ class ExecutorGroup {
* @param {number} resolution Resolution. * @param {number} resolution Resolution.
* @param {number} pixelRatio Pixel ratio. * @param {number} pixelRatio Pixel ratio.
* @param {boolean} overlaps The executor group can have overlapping geometries. * @param {boolean} overlaps The executor group can have overlapping geometries.
* @param {!Object<string, !Object<import("./BuilderType.js").default, import("./Builder.js").SerializableInstructions>>} allInstructions * @param {!Object<string, !Object<import("./BuilderType.js").default, import("../canvas.js").SerializableInstructions>>} allInstructions
* The serializable instructions. * The serializable instructions.
* @param {number=} opt_renderBuffer Optional rendering buffer. * @param {number=} opt_renderBuffer Optional rendering buffer.
*/ */
@@ -116,7 +116,7 @@ class ExecutorGroup {
/** /**
* Create executors and populate them using the provided instructions. * Create executors and populate them using the provided instructions.
* @private * @private
* @param {!Object<string, !Object<import("./BuilderType.js").default, import("./Builder.js").SerializableInstructions>>} allInstructions The serializable instructions * @param {!Object<string, !Object<import("./BuilderType.js").default, import("../canvas.js").SerializableInstructions>>} allInstructions The serializable instructions
*/ */
createExecutors_(allInstructions) { createExecutors_(allInstructions) {
for (const zIndex in allInstructions) { for (const zIndex in allInstructions) {
@@ -318,7 +318,7 @@ class ExecutorGroup {
* @param {boolean} snapToPixel Snap point symbols and test to integer pixel. * @param {boolean} snapToPixel Snap point symbols and test to integer pixel.
* @param {Array<import("./BuilderType.js").default>=} opt_builderTypes Ordered replay types to replay. * @param {Array<import("./BuilderType.js").default>=} opt_builderTypes Ordered replay types to replay.
* Default is {@link module:ol/render/replay~ORDER} * Default is {@link module:ol/render/replay~ORDER}
* @param {Object<string, import("../canvas.js").DeclutterGroup>=} opt_declutterReplays Declutter replays. * @param {import("rbush").default=} opt_declutterTree Declutter tree.
*/ */
execute( execute(
context, context,
@@ -327,7 +327,7 @@ class ExecutorGroup {
viewRotation, viewRotation,
snapToPixel, snapToPixel,
opt_builderTypes, opt_builderTypes,
opt_declutterReplays opt_declutterTree
) { ) {
/** @type {Array<number>} */ /** @type {Array<number>} */
const zs = Object.keys(this.executorsByZIndex_).map(Number); const zs = Object.keys(this.executorsByZIndex_).map(Number);
@@ -342,6 +342,9 @@ class ExecutorGroup {
const builderTypes = opt_builderTypes ? opt_builderTypes : ORDER; const builderTypes = opt_builderTypes ? opt_builderTypes : ORDER;
let i, ii, j, jj, replays, replay; let i, ii, j, jj, replays, replay;
if (opt_declutterTree) {
zs.reverse();
}
for (i = 0, ii = zs.length; i < ii; ++i) { for (i = 0, ii = zs.length; i < ii; ++i) {
const zIndexKey = zs[i].toString(); const zIndexKey = zs[i].toString();
replays = this.executorsByZIndex_[zIndexKey]; replays = this.executorsByZIndex_[zIndexKey];
@@ -349,26 +352,14 @@ class ExecutorGroup {
const builderType = builderTypes[j]; const builderType = builderTypes[j];
replay = replays[builderType]; replay = replays[builderType];
if (replay !== undefined) { if (replay !== undefined) {
if ( replay.execute(
opt_declutterReplays && context,
(builderType == BuilderType.IMAGE || contextScale,
builderType == BuilderType.TEXT) transform,
) { viewRotation,
const declutter = opt_declutterReplays[zIndexKey]; snapToPixel,
if (!declutter) { opt_declutterTree
opt_declutterReplays[zIndexKey] = [replay, transform.slice(0)]; );
} else {
declutter.push(replay, transform.slice(0));
}
} else {
replay.execute(
context,
contextScale,
transform,
viewRotation,
snapToPixel
);
}
} }
} }
} }
@@ -454,41 +445,4 @@ export function getCircleArray(radius) {
return arr; return arr;
} }
/**
* @param {!Object<string, Array<*>>} declutterReplays Declutter replays.
* @param {CanvasRenderingContext2D} context Context.
* @param {number} rotation Rotation.
* @param {number} opacity Opacity.
* @param {boolean} snapToPixel Snap point symbols and text to integer pixels.
* @param {Array<import("../../PluggableMap.js").DeclutterItems>} declutterItems Declutter items.
*/
export function replayDeclutter(
declutterReplays,
context,
rotation,
opacity,
snapToPixel,
declutterItems
) {
const zs = Object.keys(declutterReplays)
.map(Number)
.sort(numberSafeCompareFunction);
for (let z = 0, zz = zs.length; z < zz; ++z) {
const executorData = declutterReplays[zs[z].toString()];
let currentExecutor;
for (let i = 0, ii = executorData.length; i < ii; ) {
const executor = executorData[i++];
const transform = executorData[i++];
executor.execute(context, 1, transform, rotation, snapToPixel);
if (executor !== currentExecutor && executor.declutterItems.length > 0) {
currentExecutor = executor;
declutterItems.push({
items: executor.declutterItems,
opacity: opacity,
});
}
}
}
}
export default ExecutorGroup; export default ExecutorGroup;
+15 -14
View File
@@ -14,12 +14,6 @@ class CanvasImageBuilder extends CanvasBuilder {
constructor(tolerance, maxExtent, resolution, pixelRatio) { constructor(tolerance, maxExtent, resolution, pixelRatio) {
super(tolerance, maxExtent, resolution, pixelRatio); super(tolerance, maxExtent, resolution, pixelRatio);
/**
* @private
* @type {import("../canvas.js").DeclutterGroups}
*/
this.declutterGroups_ = null;
/** /**
* @private * @private
* @type {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement} * @type {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement}
@@ -97,6 +91,13 @@ class CanvasImageBuilder extends CanvasBuilder {
* @type {number|undefined} * @type {number|undefined}
*/ */
this.width_ = undefined; this.width_ = undefined;
/**
* Data shared with a text builder for combined decluttering.
* @private
* @type {import("../canvas.js").DeclutterImageWithText}
*/
this.declutterImageWithText_ = undefined;
} }
/** /**
@@ -120,7 +121,6 @@ class CanvasImageBuilder extends CanvasBuilder {
// Remaining arguments to DRAW_IMAGE are in alphabetical order // Remaining arguments to DRAW_IMAGE are in alphabetical order
this.anchorX_ * this.imagePixelRatio_, this.anchorX_ * this.imagePixelRatio_,
this.anchorY_ * this.imagePixelRatio_, this.anchorY_ * this.imagePixelRatio_,
this.declutterGroups_,
Math.ceil(this.height_ * this.imagePixelRatio_), Math.ceil(this.height_ * this.imagePixelRatio_),
this.opacity_, this.opacity_,
this.originX_, this.originX_,
@@ -132,6 +132,7 @@ class CanvasImageBuilder extends CanvasBuilder {
(this.scale_[1] * this.pixelRatio) / this.imagePixelRatio_, (this.scale_[1] * this.pixelRatio) / this.imagePixelRatio_,
], ],
Math.ceil(this.width_ * this.imagePixelRatio_), Math.ceil(this.width_ * this.imagePixelRatio_),
this.declutterImageWithText_,
]); ]);
this.hitDetectionInstructions.push([ this.hitDetectionInstructions.push([
CanvasInstruction.DRAW_IMAGE, CanvasInstruction.DRAW_IMAGE,
@@ -141,7 +142,6 @@ class CanvasImageBuilder extends CanvasBuilder {
// Remaining arguments to DRAW_IMAGE are in alphabetical order // Remaining arguments to DRAW_IMAGE are in alphabetical order
this.anchorX_, this.anchorX_,
this.anchorY_, this.anchorY_,
this.declutterGroups_,
this.height_, this.height_,
this.opacity_, this.opacity_,
this.originX_, this.originX_,
@@ -150,6 +150,7 @@ class CanvasImageBuilder extends CanvasBuilder {
this.rotation_, this.rotation_,
this.scale_, this.scale_,
this.width_, this.width_,
this.declutterImageWithText_,
]); ]);
this.endGeometry(feature); this.endGeometry(feature);
} }
@@ -175,7 +176,6 @@ class CanvasImageBuilder extends CanvasBuilder {
// Remaining arguments to DRAW_IMAGE are in alphabetical order // Remaining arguments to DRAW_IMAGE are in alphabetical order
this.anchorX_ * this.imagePixelRatio_, this.anchorX_ * this.imagePixelRatio_,
this.anchorY_ * this.imagePixelRatio_, this.anchorY_ * this.imagePixelRatio_,
this.declutterGroups_,
Math.ceil(this.height_ * this.imagePixelRatio_), Math.ceil(this.height_ * this.imagePixelRatio_),
this.opacity_, this.opacity_,
this.originX_, this.originX_,
@@ -187,6 +187,7 @@ class CanvasImageBuilder extends CanvasBuilder {
(this.scale_[1] * this.pixelRatio) / this.imagePixelRatio_, (this.scale_[1] * this.pixelRatio) / this.imagePixelRatio_,
], ],
Math.ceil(this.width_ * this.imagePixelRatio_), Math.ceil(this.width_ * this.imagePixelRatio_),
this.declutterImageWithText_,
]); ]);
this.hitDetectionInstructions.push([ this.hitDetectionInstructions.push([
CanvasInstruction.DRAW_IMAGE, CanvasInstruction.DRAW_IMAGE,
@@ -196,7 +197,6 @@ class CanvasImageBuilder extends CanvasBuilder {
// Remaining arguments to DRAW_IMAGE are in alphabetical order // Remaining arguments to DRAW_IMAGE are in alphabetical order
this.anchorX_, this.anchorX_,
this.anchorY_, this.anchorY_,
this.declutterGroups_,
this.height_, this.height_,
this.opacity_, this.opacity_,
this.originX_, this.originX_,
@@ -205,12 +205,13 @@ class CanvasImageBuilder extends CanvasBuilder {
this.rotation_, this.rotation_,
this.scale_, this.scale_,
this.width_, this.width_,
this.declutterImageWithText_,
]); ]);
this.endGeometry(feature); this.endGeometry(feature);
} }
/** /**
* @return {import("./Builder.js").SerializableInstructions} the serializable instructions. * @return {import("../canvas.js").SerializableInstructions} the serializable instructions.
*/ */
finish() { finish() {
this.reverseHitDetectionInstructions(); this.reverseHitDetectionInstructions();
@@ -233,9 +234,9 @@ class CanvasImageBuilder extends CanvasBuilder {
/** /**
* @param {import("../../style/Image.js").default} imageStyle Image style. * @param {import("../../style/Image.js").default} imageStyle Image style.
* @param {import("../canvas.js").DeclutterGroup} declutterGroups Declutter. * @param {Object=} opt_sharedData Shared data.
*/ */
setImageStyle(imageStyle, declutterGroups) { setImageStyle(imageStyle, opt_sharedData) {
const anchor = imageStyle.getAnchor(); const anchor = imageStyle.getAnchor();
const size = imageStyle.getSize(); const size = imageStyle.getSize();
const hitDetectionImage = imageStyle.getHitDetectionImage(); const hitDetectionImage = imageStyle.getHitDetectionImage();
@@ -244,7 +245,6 @@ class CanvasImageBuilder extends CanvasBuilder {
this.imagePixelRatio_ = imageStyle.getPixelRatio(this.pixelRatio); this.imagePixelRatio_ = imageStyle.getPixelRatio(this.pixelRatio);
this.anchorX_ = anchor[0]; this.anchorX_ = anchor[0];
this.anchorY_ = anchor[1]; this.anchorY_ = anchor[1];
this.declutterGroups_ = declutterGroups;
this.hitDetectionImage_ = hitDetectionImage; this.hitDetectionImage_ = hitDetectionImage;
this.image_ = image; this.image_ = image;
this.height_ = size[1]; this.height_ = size[1];
@@ -255,6 +255,7 @@ class CanvasImageBuilder extends CanvasBuilder {
this.rotation_ = imageStyle.getRotation(); this.rotation_ = imageStyle.getRotation();
this.scale_ = imageStyle.getScaleArray(); this.scale_ = imageStyle.getScaleArray();
this.width_ = size[0]; this.width_ = size[0];
this.declutterImageWithText_ = opt_sharedData;
} }
} }
+1 -1
View File
@@ -127,7 +127,7 @@ class CanvasLineStringBuilder extends CanvasBuilder {
} }
/** /**
* @return {import("./Builder.js").SerializableInstructions} the serializable instructions. * @return {import("../canvas.js").SerializableInstructions} the serializable instructions.
*/ */
finish() { finish() {
const state = this.state; const state = this.state;
+1 -1
View File
@@ -220,7 +220,7 @@ class CanvasPolygonBuilder extends CanvasBuilder {
} }
/** /**
* @return {import("./Builder.js").SerializableInstructions} the serializable instructions. * @return {import("../canvas.js").SerializableInstructions} the serializable instructions.
*/ */
finish() { finish() {
this.reverseHitDetectionInstructions(); this.reverseHitDetectionInstructions();
+15 -23
View File
@@ -52,12 +52,6 @@ class CanvasTextBuilder extends CanvasBuilder {
constructor(tolerance, maxExtent, resolution, pixelRatio) { constructor(tolerance, maxExtent, resolution, pixelRatio) {
super(tolerance, maxExtent, resolution, pixelRatio); super(tolerance, maxExtent, resolution, pixelRatio);
/**
* @private
* @type {import("../canvas.js").DeclutterGroups}
*/
this.declutterGroups_;
/** /**
* @private * @private
* @type {Array<HTMLCanvasElement>} * @type {Array<HTMLCanvasElement>}
@@ -144,10 +138,17 @@ class CanvasTextBuilder extends CanvasBuilder {
* @type {string} * @type {string}
*/ */
this.strokeKey_ = ''; this.strokeKey_ = '';
/**
* Data shared with an image builder for combined decluttering.
* @private
* @type {import("../canvas.js").DeclutterImageWithText}
*/
this.declutterImageWithText_ = undefined;
} }
/** /**
* @return {import("./Builder.js").SerializableInstructions} the serializable instructions. * @return {import("../canvas.js").SerializableInstructions} the serializable instructions.
*/ */
finish() { finish() {
const instructions = super.finish(); const instructions = super.finish();
@@ -226,12 +227,7 @@ class CanvasTextBuilder extends CanvasBuilder {
} }
const end = coordinates.length; const end = coordinates.length;
flatOffset = ends[o]; flatOffset = ends[o];
const declutterGroup = this.declutterGroups_ this.drawChars_(begin, end);
? 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);
@@ -331,7 +327,6 @@ class CanvasTextBuilder extends CanvasBuilder {
null, null,
NaN, NaN,
NaN, NaN,
this.declutterGroups_,
NaN, NaN,
1, 1,
0, 0,
@@ -340,6 +335,7 @@ class CanvasTextBuilder extends CanvasBuilder {
this.textRotation_, this.textRotation_,
[1, 1], [1, 1],
NaN, NaN,
this.declutterImageWithText_,
padding == defaultPadding padding == defaultPadding
? defaultPadding ? defaultPadding
: padding.map(function (p) { : padding.map(function (p) {
@@ -363,7 +359,6 @@ class CanvasTextBuilder extends CanvasBuilder {
null, null,
NaN, NaN,
NaN, NaN,
this.declutterGroups_,
NaN, NaN,
1, 1,
0, 0,
@@ -372,6 +367,7 @@ class CanvasTextBuilder extends CanvasBuilder {
this.textRotation_, this.textRotation_,
[scale, scale], [scale, scale],
NaN, NaN,
this.declutterImageWithText_,
padding, padding,
!!textState.backgroundFill, !!textState.backgroundFill,
!!textState.backgroundStroke, !!textState.backgroundStroke,
@@ -433,9 +429,8 @@ class CanvasTextBuilder extends CanvasBuilder {
* @private * @private
* @param {number} begin Begin. * @param {number} begin Begin.
* @param {number} end End. * @param {number} end End.
* @param {import("../canvas.js").DeclutterGroup} declutterGroup Declutter group.
*/ */
drawChars_(begin, end, declutterGroup) { drawChars_(begin, end) {
const strokeState = this.textStrokeState_; const strokeState = this.textStrokeState_;
const textState = this.textState_; const textState = this.textState_;
@@ -458,7 +453,6 @@ class CanvasTextBuilder extends CanvasBuilder {
begin, begin,
end, end,
baseline, baseline,
declutterGroup,
textState.overflow, textState.overflow,
fillKey, fillKey,
textState.maxAngle, textState.maxAngle,
@@ -475,7 +469,6 @@ class CanvasTextBuilder extends CanvasBuilder {
begin, begin,
end, end,
baseline, baseline,
declutterGroup,
textState.overflow, textState.overflow,
fillKey, fillKey,
textState.maxAngle, textState.maxAngle,
@@ -491,15 +484,13 @@ class CanvasTextBuilder extends CanvasBuilder {
/** /**
* @param {import("../../style/Text.js").default} textStyle Text style. * @param {import("../../style/Text.js").default} textStyle Text style.
* @param {import("../canvas.js").DeclutterGroups} declutterGroups Declutter. * @param {Object=} opt_sharedData Shared data.
*/ */
setTextStyle(textStyle, declutterGroups) { setTextStyle(textStyle, opt_sharedData) {
let textState, fillState, strokeState; let textState, fillState, strokeState;
if (!textStyle) { if (!textStyle) {
this.text_ = ''; this.text_ = '';
} else { } else {
this.declutterGroups_ = declutterGroups;
const textFillStyle = textStyle.getFill(); const textFillStyle = textStyle.getFill();
if (!textFillStyle) { if (!textFillStyle) {
fillState = null; fillState = null;
@@ -595,6 +586,7 @@ class CanvasTextBuilder extends CanvasBuilder {
: '|' + getUid(fillState.fillStyle) : '|' + getUid(fillState.fillStyle)
: ''; : '';
} }
this.declutterImageWithText_ = opt_sharedData;
} }
} }
+10 -1
View File
@@ -102,6 +102,10 @@ class CompositeMapRenderer extends MapRenderer {
const viewState = frameState.viewState; const viewState = frameState.viewState;
this.children_.length = 0; this.children_.length = 0;
/**
* @type {Array<import("../layer/BaseVector.js").default>}
*/
const declutterLayers = [];
let previousElement = null; let previousElement = null;
for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) { for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) {
const layerState = layerStatesArray[i]; const layerState = layerStatesArray[i];
@@ -123,8 +127,13 @@ class CompositeMapRenderer extends MapRenderer {
this.children_.push(element); this.children_.push(element);
previousElement = element; previousElement = element;
} }
if ('getDeclutter' in layer) {
declutterLayers.push(layer);
}
}
for (let i = declutterLayers.length - 1; i >= 0; --i) {
declutterLayers[i].renderDeclutter(frameState);
} }
super.renderFrame(frameState);
replaceChildren(this.element_, this.children_); replaceChildren(this.element_, this.children_);
+6 -8
View File
@@ -25,6 +25,11 @@ class LayerRenderer extends Observable {
* @type {LayerType} * @type {LayerType}
*/ */
this.layer_ = layer; this.layer_ = layer;
/**
* @type {import("../render/canvas/ExecutorGroup").default}
*/
this.declutterExecutorGroup = null;
} }
/** /**
@@ -101,17 +106,10 @@ class LayerRenderer extends Observable {
* @param {import("../PluggableMap.js").FrameState} frameState Frame state. * @param {import("../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels. * @param {number} hitTolerance Hit tolerance in pixels.
* @param {function(import("../Feature.js").FeatureLike, import("../layer/Layer.js").default): T} callback Feature callback. * @param {function(import("../Feature.js").FeatureLike, import("../layer/Layer.js").default): T} callback Feature callback.
* @param {Array<import("../Feature.js").FeatureLike>} declutteredFeatures Decluttered features.
* @return {T|void} Callback result. * @return {T|void} Callback result.
* @template T * @template T
*/ */
forEachFeatureAtCoordinate( forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) {}
coordinate,
frameState,
hitTolerance,
callback,
declutteredFeatures
) {}
/** /**
* @abstract * @abstract
+3 -15
View File
@@ -8,7 +8,6 @@ import {compose as composeTransform, makeInverse} from '../transform.js';
import {getWidth} from '../extent.js'; import {getWidth} from '../extent.js';
import {shared as iconImageCache} from '../style/IconImageCache.js'; import {shared as iconImageCache} from '../style/IconImageCache.js';
import {inView} from '../layer/Layer.js'; import {inView} from '../layer/Layer.js';
import {renderDeclutterItems} from '../render.js';
import {wrapX} from '../coordinate.js'; import {wrapX} from '../coordinate.js';
/** /**
@@ -26,11 +25,6 @@ class MapRenderer extends Disposable {
* @type {import("../PluggableMap.js").default} * @type {import("../PluggableMap.js").default}
*/ */
this.map_ = map; this.map_ = map;
/**
* @private
*/
this.declutterTree_ = null;
} }
/** /**
@@ -116,12 +110,6 @@ class MapRenderer extends Disposable {
const layerStates = frameState.layerStatesArray; const layerStates = frameState.layerStatesArray;
const numLayers = layerStates.length; const numLayers = layerStates.length;
let declutteredFeatures;
if (this.declutterTree_) {
declutteredFeatures = this.declutterTree_.all().map(function (entry) {
return entry.value;
});
}
const tmpCoord = []; const tmpCoord = [];
for (let i = 0; i < offsets.length; i++) { for (let i = 0; i < offsets.length; i++) {
@@ -149,8 +137,7 @@ class MapRenderer extends Disposable {
tmpCoord, tmpCoord,
frameState, frameState,
hitTolerance, hitTolerance,
callback, callback
declutteredFeatures
); );
} }
if (result) { if (result) {
@@ -224,10 +211,11 @@ class MapRenderer extends Disposable {
/** /**
* Render. * Render.
* @abstract
* @param {?import("../PluggableMap.js").FrameState} frameState Frame state. * @param {?import("../PluggableMap.js").FrameState} frameState Frame state.
*/ */
renderFrame(frameState) { renderFrame(frameState) {
this.declutterTree_ = renderDeclutterItems(frameState, this.declutterTree_); abstract();
} }
/** /**
+10 -15
View File
@@ -6,11 +6,11 @@ import CanvasVectorLayerRenderer from './VectorLayer.js';
import EventType from '../../events/EventType.js'; import EventType from '../../events/EventType.js';
import ImageCanvas from '../../ImageCanvas.js'; import ImageCanvas from '../../ImageCanvas.js';
import ImageState from '../../ImageState.js'; import ImageState from '../../ImageState.js';
import RBush from 'rbush';
import ViewHint from '../../ViewHint.js'; import ViewHint from '../../ViewHint.js';
import {apply, compose, create} from '../../transform.js'; import {apply, compose, create} from '../../transform.js';
import {assign} from '../../obj.js'; import {assign} from '../../obj.js';
import {getHeight, getWidth, isEmpty, scaleFromCenter} from '../../extent.js'; import {getHeight, getWidth, isEmpty, scaleFromCenter} from '../../extent.js';
import {renderDeclutterItems} from '../../render.js';
/** /**
* @classdesc * @classdesc
@@ -115,7 +115,7 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer {
{}, {},
frameState, frameState,
{ {
declutterItems: [], declutterTree: new RBush(9),
extent: renderedExtent, extent: renderedExtent,
size: [width, height], size: [width, height],
viewState: /** @type {import("../../View.js").State} */ (assign( viewState: /** @type {import("../../View.js").State} */ (assign(
@@ -139,7 +139,7 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer {
) { ) {
vectorRenderer.clipping = false; vectorRenderer.clipping = false;
vectorRenderer.renderFrame(imageFrameState, null); vectorRenderer.renderFrame(imageFrameState, null);
renderDeclutterItems(imageFrameState, null); vectorRenderer.renderDeclutter(imageFrameState);
callback(); callback();
} }
} }
@@ -186,37 +186,32 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer {
*/ */
postRender() {} postRender() {}
/**
*/
renderDeclutter() {}
/** /**
* @param {import("../../coordinate.js").Coordinate} coordinate Coordinate. * @param {import("../../coordinate.js").Coordinate} coordinate Coordinate.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels. * @param {number} hitTolerance Hit tolerance in pixels.
* @param {function(import("../../Feature.js").FeatureLike, import("../../layer/Layer.js").default): T} callback Feature callback. * @param {function(import("../../Feature.js").FeatureLike, import("../../layer/Layer.js").default): T} callback Feature callback.
* @param {Array<import("../../Feature.js").FeatureLike>} declutteredFeatures Decluttered features.
* @return {T|void} Callback result. * @return {T|void} Callback result.
* @template T * @template T
*/ */
forEachFeatureAtCoordinate( forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) {
coordinate,
frameState,
hitTolerance,
callback,
declutteredFeatures
) {
if (this.vectorRenderer_) { if (this.vectorRenderer_) {
return this.vectorRenderer_.forEachFeatureAtCoordinate( return this.vectorRenderer_.forEachFeatureAtCoordinate(
coordinate, coordinate,
frameState, frameState,
hitTolerance, hitTolerance,
callback, callback
declutteredFeatures
); );
} else { } else {
return super.forEachFeatureAtCoordinate( return super.forEachFeatureAtCoordinate(
coordinate, coordinate,
frameState, frameState,
hitTolerance, hitTolerance,
callback, callback
declutteredFeatures
); );
} }
} }
+143 -92
View File
@@ -3,9 +3,7 @@
*/ */
import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js'; import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js';
import CanvasLayerRenderer from './Layer.js'; import CanvasLayerRenderer from './Layer.js';
import ExecutorGroup, { import ExecutorGroup from '../../render/canvas/ExecutorGroup.js';
replayDeclutter,
} from '../../render/canvas/ExecutorGroup.js';
import ViewHint from '../../ViewHint.js'; import ViewHint from '../../ViewHint.js';
import { import {
apply, apply,
@@ -130,6 +128,11 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
*/ */
this.replayGroupChanged = true; this.replayGroupChanged = true;
/**
* @type {import("../../render/canvas/ExecutorGroup").default}
*/
this.declutterExecutorGroup = null;
/** /**
* Clipping to be performed by `renderFrame()` * Clipping to be performed by `renderFrame()`
* @type {boolean} * @type {boolean}
@@ -150,6 +153,73 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
super.useContainer(target, transform, opacity); super.useContainer(target, transform, opacity);
} }
/**
* @param {ExecutorGroup} executorGroup Executor group.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {import("rbush").default=} opt_declutterTree Declutter tree.
*/
renderWorlds(executorGroup, frameState, opt_declutterTree) {
const extent = frameState.extent;
const viewState = frameState.viewState;
const center = viewState.center;
const resolution = viewState.resolution;
const projection = viewState.projection;
const rotation = viewState.rotation;
const projectionExtent = projection.getExtent();
const vectorSource = this.getLayer().getSource();
const pixelRatio = frameState.pixelRatio;
const viewHints = frameState.viewHints;
const snapToPixel = !(
viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]
);
const context = this.context;
const width = Math.round(frameState.size[0] * pixelRatio);
const height = Math.round(frameState.size[1] * pixelRatio);
const multiWorld = vectorSource.getWrapX() && projection.canWrapX();
const worldWidth = multiWorld ? getWidth(projectionExtent) : null;
const endWorld = multiWorld
? Math.ceil((extent[2] - projectionExtent[2]) / worldWidth) + 1
: 1;
let world = multiWorld
? Math.floor((extent[0] - projectionExtent[0]) / worldWidth)
: 0;
do {
const transform = this.getRenderTransform(
center,
resolution,
rotation,
pixelRatio,
width,
height,
world * worldWidth
);
executorGroup.execute(
context,
1,
transform,
rotation,
snapToPixel,
undefined,
opt_declutterTree
);
} while (++world < endWorld);
}
/**
* Render declutter items for this layer
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
*/
renderDeclutter(frameState) {
if (this.declutterExecutorGroup) {
this.renderWorlds(
this.declutterExecutorGroup,
frameState,
frameState.declutterTree
);
}
}
/** /**
* Render the layer. * Render the layer.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
@@ -171,7 +241,11 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
const canvas = context.canvas; const canvas = context.canvas;
const replayGroup = this.replayGroup_; const replayGroup = this.replayGroup_;
if (!replayGroup || replayGroup.isEmpty()) { const declutterExecutorGroup = this.declutterExecutorGroup;
if (
(!replayGroup || replayGroup.isEmpty()) &&
(!declutterExecutorGroup || declutterExecutorGroup.isEmpty())
) {
if (!this.containerReused && canvas.width > 0) { if (!this.containerReused && canvas.width > 0) {
canvas.width = 0; canvas.width = 0;
} }
@@ -193,14 +267,8 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
this.preRender(context, frameState); this.preRender(context, frameState);
const extent = frameState.extent;
const viewState = frameState.viewState; const viewState = frameState.viewState;
const center = viewState.center;
const resolution = viewState.resolution;
const projection = viewState.projection; const projection = viewState.projection;
const rotation = viewState.rotation;
const projectionExtent = projection.getExtent();
const vectorSource = this.getLayer().getSource();
// clipped rendering if layer extent is set // clipped rendering if layer extent is set
let clipped = false; let clipped = false;
@@ -214,56 +282,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
} }
} }
const viewHints = frameState.viewHints; this.renderWorlds(replayGroup, frameState);
const snapToPixel = !(
viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]
);
const declutterReplays = this.getLayer().getDeclutter() ? {} : null;
const multiWorld = vectorSource.getWrapX() && projection.canWrapX();
const worldWidth = multiWorld ? getWidth(projectionExtent) : null;
const endWorld = multiWorld
? Math.ceil((extent[2] - projectionExtent[2]) / worldWidth) + 1
: 1;
let world = multiWorld
? Math.floor((extent[0] - projectionExtent[0]) / worldWidth)
: 0;
do {
const transform = this.getRenderTransform(
center,
resolution,
rotation,
pixelRatio,
width,
height,
world * worldWidth
);
replayGroup.execute(
context,
1,
transform,
rotation,
snapToPixel,
undefined,
declutterReplays
);
} while (++world < endWorld);
if (declutterReplays) {
const viewHints = frameState.viewHints;
const hifi = !(
viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]
);
replayDeclutter(
declutterReplays,
context,
rotation,
1,
hifi,
frameState.declutterItems
);
}
if (clipped) { if (clipped) {
context.restore(); context.restore();
@@ -388,44 +407,51 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels. * @param {number} hitTolerance Hit tolerance in pixels.
* @param {function(import("../../Feature.js").FeatureLike, import("../../layer/Layer.js").default): T} callback Feature callback. * @param {function(import("../../Feature.js").FeatureLike, import("../../layer/Layer.js").default): T} callback Feature callback.
* @param {Array<import("../../Feature.js").FeatureLike>} declutteredFeatures Decluttered features.
* @return {T|void} Callback result. * @return {T|void} Callback result.
* @template T * @template T
*/ */
forEachFeatureAtCoordinate( forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) {
coordinate,
frameState,
hitTolerance,
callback,
declutteredFeatures
) {
if (!this.replayGroup_) { if (!this.replayGroup_) {
return undefined; return undefined;
} else { } else {
const resolution = frameState.viewState.resolution; const resolution = frameState.viewState.resolution;
const rotation = frameState.viewState.rotation; const rotation = frameState.viewState.rotation;
const layer = this.getLayer(); const layer = this.getLayer();
/** @type {!Object<string, boolean>} */ /** @type {!Object<string, boolean>} */
const features = {}; const features = {};
const result = this.replayGroup_.forEachFeatureAtCoordinate( /**
coordinate, * @param {import("../../Feature.js").FeatureLike} feature Feature.
resolution, * @return {?} Callback result.
rotation, */
hitTolerance, const featureCallback = function (feature) {
/** const key = getUid(feature);
* @param {import("../../Feature.js").FeatureLike} feature Feature. if (!(key in features)) {
* @return {?} Callback result. features[key] = true;
*/ return callback(feature, layer);
function (feature) { }
const key = getUid(feature); };
if (!(key in features)) {
features[key] = true; let result;
return callback(feature, layer); const executorGroups = [this.replayGroup_];
} if (this.declutterExecutorGroup) {
}, executorGroups.push(this.declutterExecutorGroup);
layer.getDeclutter() ? declutteredFeatures : null }
); executorGroups.forEach((executorGroup) => {
result =
result ||
executorGroup.forEachFeatureAtCoordinate(
coordinate,
resolution,
rotation,
hitTolerance,
featureCallback,
executorGroup === this.declutterExecutorGroup
? frameState.declutterTree.all().map((item) => item.value)
: null
);
});
return result; return result;
} }
@@ -556,10 +582,19 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
getRenderTolerance(resolution, pixelRatio), getRenderTolerance(resolution, pixelRatio),
extent, extent,
resolution, resolution,
pixelRatio, pixelRatio
vectorLayer.getDeclutter()
); );
let declutterBuilderGroup;
if (this.getLayer().getDeclutter()) {
declutterBuilderGroup = new CanvasBuilderGroup(
getRenderTolerance(resolution, pixelRatio),
extent,
resolution,
pixelRatio
);
}
const userProjection = getUserProjection(); const userProjection = getUserProjection();
let userTransform; let userTransform;
if (userProjection) { if (userProjection) {
@@ -597,7 +632,8 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
squaredTolerance, squaredTolerance,
styles, styles,
replayGroup, replayGroup,
userTransform userTransform,
declutterBuilderGroup
); );
this.dirty_ = this.dirty_ || dirty; this.dirty_ = this.dirty_ || dirty;
} }
@@ -624,6 +660,17 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
vectorLayer.getRenderBuffer() vectorLayer.getRenderBuffer()
); );
if (declutterBuilderGroup) {
this.declutterExecutorGroup = new ExecutorGroup(
extent,
resolution,
pixelRatio,
vectorSource.getOverlaps(),
declutterBuilderGroup.finish(),
vectorLayer.getRenderBuffer()
);
}
this.renderedResolution_ = resolution; this.renderedResolution_ = resolution;
this.renderedRevision_ = vectorLayerRevision; this.renderedRevision_ = vectorLayerRevision;
this.renderedRenderOrder_ = vectorLayerRenderOrder; this.renderedRenderOrder_ = vectorLayerRenderOrder;
@@ -643,6 +690,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
* @param {import("../../style/Style.js").default|Array<import("../../style/Style.js").default>} styles The style or array of styles. * @param {import("../../style/Style.js").default|Array<import("../../style/Style.js").default>} styles The style or array of styles.
* @param {import("../../render/canvas/BuilderGroup.js").default} builderGroup Builder group. * @param {import("../../render/canvas/BuilderGroup.js").default} builderGroup Builder group.
* @param {import("../../proj.js").TransformFunction=} opt_transform Transform from user to view projection. * @param {import("../../proj.js").TransformFunction=} opt_transform Transform from user to view projection.
* @param {import("../../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering.
* @return {boolean} `true` if an image is loading. * @return {boolean} `true` if an image is loading.
*/ */
renderFeature( renderFeature(
@@ -650,7 +698,8 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
squaredTolerance, squaredTolerance,
styles, styles,
builderGroup, builderGroup,
opt_transform opt_transform,
opt_declutterBuilderGroup
) { ) {
if (!styles) { if (!styles) {
return false; return false;
@@ -665,7 +714,8 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
styles[i], styles[i],
squaredTolerance, squaredTolerance,
this.boundHandleStyleImageChange_, this.boundHandleStyleImageChange_,
opt_transform opt_transform,
opt_declutterBuilderGroup
) || loading; ) || loading;
} }
} else { } else {
@@ -675,7 +725,8 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
styles, styles,
squaredTolerance, squaredTolerance,
this.boundHandleStyleImageChange_, this.boundHandleStyleImageChange_,
opt_transform opt_transform,
opt_declutterBuilderGroup
); );
} }
return loading; return loading;
+166 -118
View File
@@ -2,9 +2,7 @@
* @module ol/renderer/canvas/VectorTileLayer * @module ol/renderer/canvas/VectorTileLayer
*/ */
import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js'; import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js';
import CanvasExecutorGroup, { import CanvasExecutorGroup from '../../render/canvas/ExecutorGroup.js';
replayDeclutter,
} from '../../render/canvas/ExecutorGroup.js';
import CanvasTileLayerRenderer from './TileLayer.js'; import CanvasTileLayerRenderer from './TileLayer.js';
import EventType from '../../events/EventType.js'; import EventType from '../../events/EventType.js';
import ReplayType from '../../render/canvas/BuilderType.js'; import ReplayType from '../../render/canvas/BuilderType.js';
@@ -262,6 +260,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
} }
const source = layer.getSource(); const source = layer.getSource();
const declutter = layer.getDeclutter();
const sourceTileGrid = source.getTileGrid(); const sourceTileGrid = source.getTileGrid();
const tileGrid = source.getTileGridForProjection(projection); const tileGrid = source.getTileGridForProjection(projection);
const tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord); const tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord);
@@ -270,6 +269,9 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
const layerUid = getUid(layer); const layerUid = getUid(layer);
delete tile.hitDetectionImageData[layerUid]; delete tile.hitDetectionImageData[layerUid];
tile.executorGroups[layerUid] = []; tile.executorGroups[layerUid] = [];
if (declutter) {
tile.declutterExecutorGroups[layerUid] = [];
}
for (let t = 0, tt = sourceTiles.length; t < tt; ++t) { for (let t = 0, tt = sourceTiles.length; t < tt; ++t) {
const sourceTile = sourceTiles[t]; const sourceTile = sourceTiles[t];
if (sourceTile.getState() != TileState.LOADED) { if (sourceTile.getState() != TileState.LOADED) {
@@ -292,9 +294,11 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
0, 0,
sharedExtent, sharedExtent,
resolution, resolution,
pixelRatio, pixelRatio
layer.getDeclutter()
); );
const declutterBuilderGroup = declutter
? new CanvasBuilderGroup(0, sharedExtent, resolution, pixelRatio)
: undefined;
const squaredTolerance = getSquaredRenderTolerance( const squaredTolerance = getSquaredRenderTolerance(
resolution, resolution,
pixelRatio pixelRatio
@@ -316,7 +320,8 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
feature, feature,
squaredTolerance, squaredTolerance,
styles, styles,
builderGroup builderGroup,
declutterBuilderGroup
); );
this.dirty_ = this.dirty_ || dirty; this.dirty_ = this.dirty_ || dirty;
builderState.dirty = builderState.dirty || dirty; builderState.dirty = builderState.dirty || dirty;
@@ -340,7 +345,7 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
// no need to clip when the render tile is covered by a single source tile // no need to clip when the render tile is covered by a single source tile
const replayExtent = const replayExtent =
layer.getRenderMode() !== VectorTileRenderType.VECTOR && layer.getRenderMode() !== VectorTileRenderType.VECTOR &&
layer.getDeclutter() && declutter &&
sourceTiles.length === 1 sourceTiles.length === 1
? null ? null
: sharedExtent; : sharedExtent;
@@ -353,6 +358,17 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
layer.getRenderBuffer() layer.getRenderBuffer()
); );
tile.executorGroups[layerUid].push(renderingReplayGroup); tile.executorGroups[layerUid].push(renderingReplayGroup);
if (declutterBuilderGroup) {
const declutterExecutorGroup = new CanvasExecutorGroup(
replayExtent,
resolution,
pixelRatio,
source.getOverlaps(),
declutterBuilderGroup.finish(),
layer.getRenderBuffer()
);
tile.declutterExecutorGroups[layerUid].push(declutterExecutorGroup);
}
} }
builderState.renderedRevision = revision; builderState.renderedRevision = revision;
builderState.renderedZ = tile.sourceZ; builderState.renderedZ = tile.sourceZ;
@@ -365,17 +381,10 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels. * @param {number} hitTolerance Hit tolerance in pixels.
* @param {function(import("../../Feature.js").FeatureLike, import("../../layer/Layer.js").default): T} callback Feature callback. * @param {function(import("../../Feature.js").FeatureLike, import("../../layer/Layer.js").default): T} callback Feature callback.
* @param {Array<import("../../Feature.js").FeatureLike>} declutteredFeatures Decluttered features.
* @return {T|void} Callback result. * @return {T|void} Callback result.
* @template T * @template T
*/ */
forEachFeatureAtCoordinate( forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) {
coordinate,
frameState,
hitTolerance,
callback,
declutteredFeatures
) {
const resolution = frameState.viewState.resolution; const resolution = frameState.viewState.resolution;
const rotation = frameState.viewState.rotation; const rotation = frameState.viewState.rotation;
hitTolerance = hitTolerance == undefined ? 0 : hitTolerance; hitTolerance = hitTolerance == undefined ? 0 : hitTolerance;
@@ -405,39 +414,44 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
continue; continue;
} }
} }
const executorGroups = tile.executorGroups[getUid(layer)]; const layerUid = getUid(layer);
for (let t = 0, tt = executorGroups.length; t < tt; ++t) { const executorGroups = [tile.executorGroups[layerUid]];
const executorGroup = executorGroups[t]; const declutterExecutorGroups = tile.declutterExecutorGroups[layerUid];
found = if (declutterExecutorGroups) {
found || executorGroups.push(declutterExecutorGroups);
executorGroup.forEachFeatureAtCoordinate(
coordinate,
resolution,
rotation,
hitTolerance,
/**
* @param {import("../../Feature.js").FeatureLike} feature Feature.
* @return {?} Callback result.
*/
function (feature) {
if (
tileContainsCoordinate ||
(declutteredFeatures &&
declutteredFeatures.indexOf(feature) !== -1)
) {
let key = feature.getId();
if (key === undefined) {
key = getUid(feature);
}
if (!(key in features)) {
features[key] = true;
return callback(feature, layer);
}
}
},
layer.getDeclutter() ? declutteredFeatures : null
);
} }
executorGroups.forEach((executorGroups) => {
for (let t = 0, tt = executorGroups.length; t < tt; ++t) {
const executorGroup = executorGroups[t];
found =
found ||
executorGroup.forEachFeatureAtCoordinate(
coordinate,
resolution,
rotation,
hitTolerance,
/**
* @param {import("../../Feature.js").FeatureLike} feature Feature.
* @return {?} Callback result.
*/
function (feature) {
if (tileContainsCoordinate) {
let key = feature.getId();
if (key === undefined) {
key = getUid(feature);
}
if (!(key in features)) {
features[key] = true;
return callback(feature, layer);
}
}
},
executorGroups === declutterExecutorGroups
? frameState.declutterTree.all().map((item) => item.value)
: null
);
}
});
} }
return found; return found;
} }
@@ -554,6 +568,70 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
this.renderIfReadyAndVisible(); this.renderIfReadyAndVisible();
} }
/**
* Render declutter items for this layer
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
*/
renderDeclutter(frameState) {
const viewHints = frameState.viewHints;
const hifi = !(
viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]
);
const tiles = /** @type {Array<import("../../VectorRenderTile.js").default>} */ (this
.renderedTiles);
for (let i = 0, ii = tiles.length; i < ii; ++i) {
const tile = tiles[i];
const declutterExecutorGroups =
tile.declutterExecutorGroups[getUid(this.getLayer())];
if (declutterExecutorGroups) {
for (let j = declutterExecutorGroups.length - 1; j >= 0; --j) {
declutterExecutorGroups[j].execute(
this.context,
1,
this.getTileRenderTransform(tile, frameState),
frameState.viewState.rotation,
hifi,
undefined,
frameState.declutterTree
);
}
}
}
}
getTileRenderTransform(tile, frameState) {
const pixelRatio = frameState.pixelRatio;
const viewState = frameState.viewState;
const center = viewState.center;
const resolution = viewState.resolution;
const rotation = viewState.rotation;
const size = frameState.size;
const width = Math.round(size[0] * pixelRatio);
const height = Math.round(size[1] * pixelRatio);
const source = this.getLayer().getSource();
const tileGrid = source.getTileGridForProjection(
frameState.viewState.projection
);
const tileCoord = tile.tileCoord;
const tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord);
const worldOffset =
tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent)[0] - tileExtent[0];
const transform = multiply(
scale(this.inversePixelTransform.slice(), 1 / pixelRatio, 1 / pixelRatio),
this.getRenderTransform(
center,
resolution,
rotation,
pixelRatio,
width,
height,
worldOffset
)
);
return transform;
}
/** /**
* Render the layer. * Render the layer.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
@@ -587,49 +665,18 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
} }
const context = this.context; const context = this.context;
const declutterReplays = layer.getDeclutter() ? {} : null;
const replayTypes = VECTOR_REPLAYS[renderMode]; const replayTypes = VECTOR_REPLAYS[renderMode];
const pixelRatio = frameState.pixelRatio;
const viewState = frameState.viewState; const viewState = frameState.viewState;
const center = viewState.center;
const resolution = viewState.resolution;
const rotation = viewState.rotation; const rotation = viewState.rotation;
const size = frameState.size;
const width = Math.round(size[0] * pixelRatio);
const height = Math.round(size[1] * pixelRatio);
const tiles = this.renderedTiles; const tiles = this.renderedTiles;
const tileGrid = source.getTileGridForProjection(
frameState.viewState.projection
);
const clips = []; const clips = [];
const clipZs = []; const clipZs = [];
for (let i = tiles.length - 1; i >= 0; --i) { for (let i = tiles.length - 1; i >= 0; --i) {
const tile = /** @type {import("../../VectorRenderTile.js").default} */ (tiles[ const tile = /** @type {import("../../VectorRenderTile.js").default} */ (tiles[
i i
]); ]);
const tileCoord = tile.tileCoord; const transform = this.getTileRenderTransform(tile, frameState);
const tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord);
const worldOffset =
tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent)[0] -
tileExtent[0];
const transform = multiply(
scale(
this.inversePixelTransform.slice(),
1 / pixelRatio,
1 / pixelRatio
),
this.getRenderTransform(
center,
resolution,
rotation,
pixelRatio,
width,
height,
worldOffset
)
);
const executorGroups = tile.executorGroups[getUid(layer)]; const executorGroups = tile.executorGroups[getUid(layer)];
let clipped = false; let clipped = false;
for (let t = 0, tt = executorGroups.length; t < tt; ++t) { for (let t = 0, tt = executorGroups.length; t < tt; ++t) {
@@ -640,27 +687,29 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
} }
const currentZ = tile.tileCoord[0]; const currentZ = tile.tileCoord[0];
let currentClip; let currentClip;
if (!declutterReplays && !clipped) { if (!clipped) {
currentClip = executorGroup.getClipCoords(transform); currentClip = executorGroup.getClipCoords(transform);
context.save(); if (currentClip) {
context.save();
// Create a clip mask for regions in this low resolution tile that are // Create a clip mask for regions in this low resolution tile that are
// already filled by a higher resolution tile // already filled by a higher resolution tile
for (let j = 0, jj = clips.length; j < jj; ++j) { for (let j = 0, jj = clips.length; j < jj; ++j) {
const clip = clips[j]; const clip = clips[j];
if (currentZ < clipZs[j]) { if (currentZ < clipZs[j]) {
context.beginPath(); context.beginPath();
// counter-clockwise (outer ring) for current tile // counter-clockwise (outer ring) for current tile
context.moveTo(currentClip[0], currentClip[1]); context.moveTo(currentClip[0], currentClip[1]);
context.lineTo(currentClip[2], currentClip[3]); context.lineTo(currentClip[2], currentClip[3]);
context.lineTo(currentClip[4], currentClip[5]); context.lineTo(currentClip[4], currentClip[5]);
context.lineTo(currentClip[6], currentClip[7]); context.lineTo(currentClip[6], currentClip[7]);
// clockwise (inner ring) for higher resolution tile // clockwise (inner ring) for higher resolution tile
context.moveTo(clip[6], clip[7]); context.moveTo(clip[6], clip[7]);
context.lineTo(clip[4], clip[5]); context.lineTo(clip[4], clip[5]);
context.lineTo(clip[2], clip[3]); context.lineTo(clip[2], clip[3]);
context.lineTo(clip[0], clip[1]); context.lineTo(clip[0], clip[1]);
context.clip(); context.clip();
}
} }
} }
} }
@@ -670,10 +719,9 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
transform, transform,
rotation, rotation,
hifi, hifi,
replayTypes, replayTypes
declutterReplays
); );
if (!declutterReplays && !clipped) { if (!clipped && currentClip) {
context.restore(); context.restore();
clips.push(currentClip); clips.push(currentClip);
clipZs.push(currentZ); clipZs.push(currentZ);
@@ -681,17 +729,6 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
} }
} }
} }
if (declutterReplays) {
const layerState = frameState.layerStatesArray[frameState.layerIndex];
replayDeclutter(
declutterReplays,
context,
rotation,
layerState.opacity,
hifi,
frameState.declutterItems
);
}
return this.container; return this.container;
} }
@@ -718,10 +755,17 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
* @param {import("../../Feature.js").FeatureLike} feature Feature. * @param {import("../../Feature.js").FeatureLike} feature Feature.
* @param {number} squaredTolerance Squared tolerance. * @param {number} squaredTolerance Squared tolerance.
* @param {import("../../style/Style.js").default|Array<import("../../style/Style.js").default>} styles The style or array of styles. * @param {import("../../style/Style.js").default|Array<import("../../style/Style.js").default>} styles The style or array of styles.
* @param {import("../../render/canvas/BuilderGroup.js").default} executorGroup Replay group. * @param {import("../../render/canvas/BuilderGroup.js").default} builderGroup Replay group.
* @param {import("../../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder group for decluttering.
* @return {boolean} `true` if an image is loading. * @return {boolean} `true` if an image is loading.
*/ */
renderFeature(feature, squaredTolerance, styles, executorGroup) { renderFeature(
feature,
squaredTolerance,
styles,
builderGroup,
opt_declutterBuilderGroup
) {
if (!styles) { if (!styles) {
return false; return false;
} }
@@ -730,20 +774,24 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
for (let i = 0, ii = styles.length; i < ii; ++i) { for (let i = 0, ii = styles.length; i < ii; ++i) {
loading = loading =
renderFeature( renderFeature(
executorGroup, builderGroup,
feature, feature,
styles[i], styles[i],
squaredTolerance, squaredTolerance,
this.boundHandleStyleImageChange_ this.boundHandleStyleImageChange_,
undefined,
opt_declutterBuilderGroup
) || loading; ) || loading;
} }
} else { } else {
loading = renderFeature( loading = renderFeature(
executorGroup, builderGroup,
feature, feature,
styles, styles,
squaredTolerance, squaredTolerance,
this.boundHandleStyleImageChange_ this.boundHandleStyleImageChange_,
undefined,
opt_declutterBuilderGroup
); );
} }
return loading; return loading;
+119 -38
View File
@@ -62,8 +62,15 @@ export function getTolerance(resolution, pixelRatio) {
* @param {import("../geom/Circle.js").default} geometry Geometry. * @param {import("../geom/Circle.js").default} geometry Geometry.
* @param {import("../style/Style.js").default} style Style. * @param {import("../style/Style.js").default} style Style.
* @param {import("../Feature.js").default} feature Feature. * @param {import("../Feature.js").default} feature Feature.
* @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering.
*/ */
function renderCircleGeometry(builderGroup, geometry, style, feature) { function renderCircleGeometry(
builderGroup,
geometry,
style,
feature,
opt_declutterBuilderGroup
) {
const fillStyle = style.getFill(); const fillStyle = style.getFill();
const strokeStyle = style.getStroke(); const strokeStyle = style.getStroke();
if (fillStyle || strokeStyle) { if (fillStyle || strokeStyle) {
@@ -75,12 +82,12 @@ function renderCircleGeometry(builderGroup, geometry, style, feature) {
circleReplay.drawCircle(geometry, feature); circleReplay.drawCircle(geometry, feature);
} }
const textStyle = style.getText(); const textStyle = style.getText();
if (textStyle) { if (textStyle && textStyle.getText()) {
const textReplay = builderGroup.getBuilder( const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder(
style.getZIndex(), style.getZIndex(),
BuilderType.TEXT BuilderType.TEXT
); );
textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); textReplay.setTextStyle(textStyle);
textReplay.drawText(geometry, feature); textReplay.drawText(geometry, feature);
} }
} }
@@ -92,8 +99,8 @@ function renderCircleGeometry(builderGroup, geometry, style, feature) {
* @param {number} squaredTolerance Squared tolerance. * @param {number} squaredTolerance Squared tolerance.
* @param {function(import("../events/Event.js").default): void} listener Listener function. * @param {function(import("../events/Event.js").default): void} listener Listener function.
* @param {import("../proj.js").TransformFunction} [opt_transform] Transform from user to view projection. * @param {import("../proj.js").TransformFunction} [opt_transform] Transform from user to view projection.
* @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering.
* @return {boolean} `true` if style is loading. * @return {boolean} `true` if style is loading.
* @template T
*/ */
export function renderFeature( export function renderFeature(
replayGroup, replayGroup,
@@ -101,7 +108,8 @@ export function renderFeature(
style, style,
squaredTolerance, squaredTolerance,
listener, listener,
opt_transform opt_transform,
opt_declutterBuilderGroup
) { ) {
let loading = false; let loading = false;
const imageStyle = style.getImage(); const imageStyle = style.getImage();
@@ -123,7 +131,8 @@ export function renderFeature(
feature, feature,
style, style,
squaredTolerance, squaredTolerance,
opt_transform opt_transform,
opt_declutterBuilderGroup
); );
return loading; return loading;
@@ -135,13 +144,15 @@ export function renderFeature(
* @param {import("../style/Style.js").default} style Style. * @param {import("../style/Style.js").default} style Style.
* @param {number} squaredTolerance Squared tolerance. * @param {number} squaredTolerance Squared tolerance.
* @param {import("../proj.js").TransformFunction} [opt_transform] Optional transform function. * @param {import("../proj.js").TransformFunction} [opt_transform] Optional transform function.
* @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering.
*/ */
function renderFeatureInternal( function renderFeatureInternal(
replayGroup, replayGroup,
feature, feature,
style, style,
squaredTolerance, squaredTolerance,
opt_transform opt_transform,
opt_declutterBuilderGroup
) { ) {
const geometry = style.getGeometryFunction()(feature); const geometry = style.getGeometryFunction()(feature);
if (!geometry) { if (!geometry) {
@@ -156,7 +167,13 @@ function renderFeatureInternal(
renderGeometry(replayGroup, simplifiedGeometry, style, feature); renderGeometry(replayGroup, simplifiedGeometry, style, feature);
} else { } else {
const geometryRenderer = GEOMETRY_RENDERERS[simplifiedGeometry.getType()]; const geometryRenderer = GEOMETRY_RENDERERS[simplifiedGeometry.getType()];
geometryRenderer(replayGroup, simplifiedGeometry, style, feature); geometryRenderer(
replayGroup,
simplifiedGeometry,
style,
feature,
opt_declutterBuilderGroup
);
} }
} }
@@ -187,18 +204,26 @@ function renderGeometry(replayGroup, geometry, style, feature) {
* @param {import("../geom/GeometryCollection.js").default} geometry Geometry. * @param {import("../geom/GeometryCollection.js").default} geometry Geometry.
* @param {import("../style/Style.js").default} style Style. * @param {import("../style/Style.js").default} style Style.
* @param {import("../Feature.js").default} feature Feature. * @param {import("../Feature.js").default} feature Feature.
* @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering.
*/ */
function renderGeometryCollectionGeometry( function renderGeometryCollectionGeometry(
replayGroup, replayGroup,
geometry, geometry,
style, style,
feature feature,
opt_declutterBuilderGroup
) { ) {
const geometries = geometry.getGeometriesArray(); const geometries = geometry.getGeometriesArray();
let i, ii; let i, ii;
for (i = 0, ii = geometries.length; i < ii; ++i) { for (i = 0, ii = geometries.length; i < ii; ++i) {
const geometryRenderer = GEOMETRY_RENDERERS[geometries[i].getType()]; const geometryRenderer = GEOMETRY_RENDERERS[geometries[i].getType()];
geometryRenderer(replayGroup, geometries[i], style, feature); geometryRenderer(
replayGroup,
geometries[i],
style,
feature,
opt_declutterBuilderGroup
);
} }
} }
@@ -207,8 +232,15 @@ function renderGeometryCollectionGeometry(
* @param {import("../geom/LineString.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../geom/LineString.js").default|import("../render/Feature.js").default} geometry Geometry.
* @param {import("../style/Style.js").default} style Style. * @param {import("../style/Style.js").default} style Style.
* @param {import("../Feature.js").FeatureLike} feature Feature. * @param {import("../Feature.js").FeatureLike} feature Feature.
* @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering.
*/ */
function renderLineStringGeometry(builderGroup, geometry, style, feature) { function renderLineStringGeometry(
builderGroup,
geometry,
style,
feature,
opt_declutterBuilderGroup
) {
const strokeStyle = style.getStroke(); const strokeStyle = style.getStroke();
if (strokeStyle) { if (strokeStyle) {
const lineStringReplay = builderGroup.getBuilder( const lineStringReplay = builderGroup.getBuilder(
@@ -219,12 +251,12 @@ function renderLineStringGeometry(builderGroup, geometry, style, feature) {
lineStringReplay.drawLineString(geometry, feature); lineStringReplay.drawLineString(geometry, feature);
} }
const textStyle = style.getText(); const textStyle = style.getText();
if (textStyle) { if (textStyle && textStyle.getText()) {
const textReplay = builderGroup.getBuilder( const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder(
style.getZIndex(), style.getZIndex(),
BuilderType.TEXT BuilderType.TEXT
); );
textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); textReplay.setTextStyle(textStyle);
textReplay.drawText(geometry, feature); textReplay.drawText(geometry, feature);
} }
} }
@@ -234,8 +266,15 @@ function renderLineStringGeometry(builderGroup, geometry, style, feature) {
* @param {import("../geom/MultiLineString.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../geom/MultiLineString.js").default|import("../render/Feature.js").default} geometry Geometry.
* @param {import("../style/Style.js").default} style Style. * @param {import("../style/Style.js").default} style Style.
* @param {import("../Feature.js").FeatureLike} feature Feature. * @param {import("../Feature.js").FeatureLike} feature Feature.
* @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering.
*/ */
function renderMultiLineStringGeometry(builderGroup, geometry, style, feature) { function renderMultiLineStringGeometry(
builderGroup,
geometry,
style,
feature,
opt_declutterBuilderGroup
) {
const strokeStyle = style.getStroke(); const strokeStyle = style.getStroke();
if (strokeStyle) { if (strokeStyle) {
const lineStringReplay = builderGroup.getBuilder( const lineStringReplay = builderGroup.getBuilder(
@@ -246,12 +285,12 @@ function renderMultiLineStringGeometry(builderGroup, geometry, style, feature) {
lineStringReplay.drawMultiLineString(geometry, feature); lineStringReplay.drawMultiLineString(geometry, feature);
} }
const textStyle = style.getText(); const textStyle = style.getText();
if (textStyle) { if (textStyle && textStyle.getText()) {
const textReplay = builderGroup.getBuilder( const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder(
style.getZIndex(), style.getZIndex(),
BuilderType.TEXT BuilderType.TEXT
); );
textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); textReplay.setTextStyle(textStyle);
textReplay.drawText(geometry, feature); textReplay.drawText(geometry, feature);
} }
} }
@@ -261,8 +300,15 @@ function renderMultiLineStringGeometry(builderGroup, geometry, style, feature) {
* @param {import("../geom/MultiPolygon.js").default} geometry Geometry. * @param {import("../geom/MultiPolygon.js").default} geometry Geometry.
* @param {import("../style/Style.js").default} style Style. * @param {import("../style/Style.js").default} style Style.
* @param {import("../Feature.js").default} feature Feature. * @param {import("../Feature.js").default} feature Feature.
* @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering.
*/ */
function renderMultiPolygonGeometry(builderGroup, geometry, style, feature) { function renderMultiPolygonGeometry(
builderGroup,
geometry,
style,
feature,
opt_declutterBuilderGroup
) {
const fillStyle = style.getFill(); const fillStyle = style.getFill();
const strokeStyle = style.getStroke(); const strokeStyle = style.getStroke();
if (strokeStyle || fillStyle) { if (strokeStyle || fillStyle) {
@@ -274,12 +320,12 @@ function renderMultiPolygonGeometry(builderGroup, geometry, style, feature) {
polygonReplay.drawMultiPolygon(geometry, feature); polygonReplay.drawMultiPolygon(geometry, feature);
} }
const textStyle = style.getText(); const textStyle = style.getText();
if (textStyle) { if (textStyle && textStyle.getText()) {
const textReplay = builderGroup.getBuilder( const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder(
style.getZIndex(), style.getZIndex(),
BuilderType.TEXT BuilderType.TEXT
); );
textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); textReplay.setTextStyle(textStyle);
textReplay.drawText(geometry, feature); textReplay.drawText(geometry, feature);
} }
} }
@@ -289,9 +335,24 @@ function renderMultiPolygonGeometry(builderGroup, geometry, style, feature) {
* @param {import("../geom/Point.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../geom/Point.js").default|import("../render/Feature.js").default} geometry Geometry.
* @param {import("../style/Style.js").default} style Style. * @param {import("../style/Style.js").default} style Style.
* @param {import("../Feature.js").FeatureLike} feature Feature. * @param {import("../Feature.js").FeatureLike} feature Feature.
* @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering.
*/ */
function renderPointGeometry(builderGroup, geometry, style, feature) { function renderPointGeometry(
builderGroup,
geometry,
style,
feature,
opt_declutterBuilderGroup
) {
const imageStyle = style.getImage(); const imageStyle = style.getImage();
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) {
if (imageStyle.getImageState() != ImageState.LOADED) { if (imageStyle.getImageState() != ImageState.LOADED) {
return; return;
@@ -300,16 +361,15 @@ function renderPointGeometry(builderGroup, geometry, style, feature) {
style.getZIndex(), style.getZIndex(),
BuilderType.IMAGE BuilderType.IMAGE
); );
imageReplay.setImageStyle(imageStyle, builderGroup.addDeclutter(false)); imageReplay.setImageStyle(imageStyle, declutterImageWithText);
imageReplay.drawPoint(geometry, feature); imageReplay.drawPoint(geometry, feature);
} }
const textStyle = style.getText(); if (textStyle && textStyle.getText()) {
if (textStyle) {
const textReplay = builderGroup.getBuilder( const textReplay = builderGroup.getBuilder(
style.getZIndex(), style.getZIndex(),
BuilderType.TEXT BuilderType.TEXT
); );
textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(!!imageStyle)); textReplay.setTextStyle(textStyle, declutterImageWithText);
textReplay.drawText(geometry, feature); textReplay.drawText(geometry, feature);
} }
} }
@@ -319,9 +379,24 @@ function renderPointGeometry(builderGroup, geometry, style, feature) {
* @param {import("../geom/MultiPoint.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../geom/MultiPoint.js").default|import("../render/Feature.js").default} geometry Geometry.
* @param {import("../style/Style.js").default} style Style. * @param {import("../style/Style.js").default} style Style.
* @param {import("../Feature.js").FeatureLike} feature Feature. * @param {import("../Feature.js").FeatureLike} feature Feature.
* @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering.
*/ */
function renderMultiPointGeometry(builderGroup, geometry, style, feature) { function renderMultiPointGeometry(
builderGroup,
geometry,
style,
feature,
opt_declutterBuilderGroup
) {
const imageStyle = style.getImage(); const imageStyle = style.getImage();
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) {
if (imageStyle.getImageState() != ImageState.LOADED) { if (imageStyle.getImageState() != ImageState.LOADED) {
return; return;
@@ -330,16 +405,15 @@ function renderMultiPointGeometry(builderGroup, geometry, style, feature) {
style.getZIndex(), style.getZIndex(),
BuilderType.IMAGE BuilderType.IMAGE
); );
imageReplay.setImageStyle(imageStyle, builderGroup.addDeclutter(false)); imageReplay.setImageStyle(imageStyle, declutterImageWithText);
imageReplay.drawMultiPoint(geometry, feature); imageReplay.drawMultiPoint(geometry, feature);
} }
const textStyle = style.getText(); if (textStyle && textStyle.getText()) {
if (textStyle) { const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder(
const textReplay = builderGroup.getBuilder(
style.getZIndex(), style.getZIndex(),
BuilderType.TEXT BuilderType.TEXT
); );
textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(!!imageStyle)); textReplay.setTextStyle(textStyle, declutterImageWithText);
textReplay.drawText(geometry, feature); textReplay.drawText(geometry, feature);
} }
} }
@@ -349,8 +423,15 @@ function renderMultiPointGeometry(builderGroup, geometry, style, feature) {
* @param {import("../geom/Polygon.js").default|import("../render/Feature.js").default} geometry Geometry. * @param {import("../geom/Polygon.js").default|import("../render/Feature.js").default} geometry Geometry.
* @param {import("../style/Style.js").default} style Style. * @param {import("../style/Style.js").default} style Style.
* @param {import("../Feature.js").FeatureLike} feature Feature. * @param {import("../Feature.js").FeatureLike} feature Feature.
* @param {import("../render/canvas/BuilderGroup.js").default=} opt_declutterBuilderGroup Builder for decluttering.
*/ */
function renderPolygonGeometry(builderGroup, geometry, style, feature) { function renderPolygonGeometry(
builderGroup,
geometry,
style,
feature,
opt_declutterBuilderGroup
) {
const fillStyle = style.getFill(); const fillStyle = style.getFill();
const strokeStyle = style.getStroke(); const strokeStyle = style.getStroke();
if (fillStyle || strokeStyle) { if (fillStyle || strokeStyle) {
@@ -362,12 +443,12 @@ function renderPolygonGeometry(builderGroup, geometry, style, feature) {
polygonReplay.drawPolygon(geometry, feature); polygonReplay.drawPolygon(geometry, feature);
} }
const textStyle = style.getText(); const textStyle = style.getText();
if (textStyle) { if (textStyle && textStyle.getText()) {
const textReplay = builderGroup.getBuilder( const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder(
style.getZIndex(), style.getZIndex(),
BuilderType.TEXT BuilderType.TEXT
); );
textReplay.setTextStyle(textStyle, builderGroup.addDeclutter(false)); textReplay.setTextStyle(textStyle);
textReplay.drawText(geometry, feature); textReplay.drawText(geometry, feature);
} }
} }
+1 -8
View File
@@ -598,17 +598,10 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} hitTolerance Hit tolerance in pixels. * @param {number} hitTolerance Hit tolerance in pixels.
* @param {function(import("../../Feature.js").FeatureLike, import("../../layer/Layer.js").default): T} callback Feature callback. * @param {function(import("../../Feature.js").FeatureLike, import("../../layer/Layer.js").default): T} callback Feature callback.
* @param {Array<import("../../Feature.js").FeatureLike>} declutteredFeatures Decluttered features.
* @return {T|void} Callback result. * @return {T|void} Callback result.
* @template T * @template T
*/ */
forEachFeatureAtCoordinate( forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback) {
coordinate,
frameState,
hitTolerance,
callback,
declutteredFeatures
) {
assert(this.hitDetectionEnabled_, 66); assert(this.hitDetectionEnabled_, 66);
if (!this.hitRenderInstructions_) { if (!this.hitRenderInstructions_) {
return; return;
+1 -1
View File
@@ -549,6 +549,7 @@ class RasterSource extends ImageSource {
this.frameState_ = { this.frameState_ = {
animate: false, animate: false,
coordinateToPixelTransform: createTransform(), coordinateToPixelTransform: createTransform(),
declutterTree: null,
extent: null, extent: null,
index: 0, index: 0,
layerIndex: 0, layerIndex: 0,
@@ -565,7 +566,6 @@ class RasterSource extends ImageSource {
}), }),
viewHints: [], viewHints: [],
wantedTiles: {}, wantedTiles: {},
declutterItems: [],
}; };
this.setAttributions(function (frameState) { this.setAttributions(function (frameState) {