var __extends = (this && this.__extends) || (function () { var extendStatics = function (d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; return function (d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); /** * @module ol/renderer/canvas/VectorLayer */ import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js'; import CanvasLayerRenderer, { canvasPool } from './Layer.js'; import ExecutorGroup from '../../render/canvas/ExecutorGroup.js'; import ViewHint from '../../ViewHint.js'; import { HIT_DETECT_RESOLUTION, createHitDetectionImageData, hitDetect, } from '../../render/canvas/hitdetect.js'; import { apply, makeInverse, makeScale, toString as transformToString, } from '../../transform.js'; import { buffer, containsExtent, createEmpty, getWidth, intersects as intersectsExtent, wrapX as wrapExtentX, } from '../../extent.js'; import { createCanvasContext2D, releaseCanvas } from '../../dom.js'; import { defaultOrder as defaultRenderOrder, getTolerance as getRenderTolerance, getSquaredTolerance as getSquaredRenderTolerance, renderFeature, } from '../vector.js'; import { equals } from '../../array.js'; import { fromUserExtent, getTransformFromProjections, getUserProjection, toUserExtent, toUserResolution, } from '../../proj.js'; import { getUid } from '../../util.js'; import { wrapX as wrapCoordinateX } from '../../coordinate.js'; /** * @classdesc * Canvas renderer for vector layers. * @api */ var CanvasVectorLayerRenderer = /** @class */ (function (_super) { __extends(CanvasVectorLayerRenderer, _super); /** * @param {import("../../layer/BaseVector.js").default} vectorLayer Vector layer. */ function CanvasVectorLayerRenderer(vectorLayer) { var _this = _super.call(this, vectorLayer) || this; /** @private */ _this.boundHandleStyleImageChange_ = _this.handleStyleImageChange_.bind(_this); /** * @type {boolean} */ _this.animatingOrInteracting_; /** * @type {ImageData} */ _this.hitDetectionImageData_ = null; /** * @type {Array} */ _this.renderedFeatures_ = null; /** * @private * @type {number} */ _this.renderedRevision_ = -1; /** * @private * @type {number} */ _this.renderedResolution_ = NaN; /** * @private * @type {import("../../extent.js").Extent} */ _this.renderedExtent_ = createEmpty(); /** * @private * @type {import("../../extent.js").Extent} */ _this.wrappedRenderedExtent_ = createEmpty(); /** * @private * @type {number} */ _this.renderedRotation_; /** * @private * @type {import("../../coordinate").Coordinate} */ _this.renderedCenter_ = null; /** * @private * @type {import("../../proj/Projection").default} */ _this.renderedProjection_ = null; /** * @private * @type {function(import("../../Feature.js").default, import("../../Feature.js").default): number|null} */ _this.renderedRenderOrder_ = null; /** * @private * @type {import("../../render/canvas/ExecutorGroup").default} */ _this.replayGroup_ = null; /** * A new replay group had to be created by `prepareFrame()` * @type {boolean} */ _this.replayGroupChanged = true; /** * @type {import("../../render/canvas/ExecutorGroup").default} */ _this.declutterExecutorGroup = null; /** * Clipping to be performed by `renderFrame()` * @type {boolean} */ _this.clipping = true; /** * @private * @type {CanvasRenderingContext2D} */ _this.compositionContext_ = null; /** * @private * @type {number} */ _this.opacity_ = 1; return _this; } /** * @param {ExecutorGroup} executorGroup Executor group. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @param {import("rbush").default} [opt_declutterTree] Declutter tree. */ CanvasVectorLayerRenderer.prototype.renderWorlds = function (executorGroup, frameState, opt_declutterTree) { var extent = frameState.extent; var viewState = frameState.viewState; var center = viewState.center; var resolution = viewState.resolution; var projection = viewState.projection; var rotation = viewState.rotation; var projectionExtent = projection.getExtent(); var vectorSource = this.getLayer().getSource(); var pixelRatio = frameState.pixelRatio; var viewHints = frameState.viewHints; var snapToPixel = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]); var context = this.compositionContext_; var width = Math.round(frameState.size[0] * pixelRatio); var height = Math.round(frameState.size[1] * pixelRatio); var multiWorld = vectorSource.getWrapX() && projection.canWrapX(); var worldWidth = multiWorld ? getWidth(projectionExtent) : null; var endWorld = multiWorld ? Math.ceil((extent[2] - projectionExtent[2]) / worldWidth) + 1 : 1; var world = multiWorld ? Math.floor((extent[0] - projectionExtent[0]) / worldWidth) : 0; do { var transform = this.getRenderTransform(center, resolution, rotation, pixelRatio, width, height, world * worldWidth); executorGroup.execute(context, 1, transform, rotation, snapToPixel, undefined, opt_declutterTree); } while (++world < endWorld); }; CanvasVectorLayerRenderer.prototype.setupCompositionContext_ = function () { if (this.opacity_ !== 1) { var compositionContext = createCanvasContext2D(this.context.canvas.width, this.context.canvas.height, canvasPool); this.compositionContext_ = compositionContext; } else { this.compositionContext_ = this.context; } }; CanvasVectorLayerRenderer.prototype.releaseCompositionContext_ = function () { if (this.opacity_ !== 1) { var alpha = this.context.globalAlpha; this.context.globalAlpha = this.opacity_; this.context.drawImage(this.compositionContext_.canvas, 0, 0); this.context.globalAlpha = alpha; releaseCanvas(this.compositionContext_); canvasPool.push(this.compositionContext_.canvas); this.compositionContext_ = null; } }; /** * Render declutter items for this layer * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. */ CanvasVectorLayerRenderer.prototype.renderDeclutter = function (frameState) { if (this.declutterExecutorGroup) { this.setupCompositionContext_(); this.renderWorlds(this.declutterExecutorGroup, frameState, frameState.declutterTree); this.releaseCompositionContext_(); } }; /** * Render the layer. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @param {HTMLElement} target Target that may be used to render content to. * @return {HTMLElement} The rendered element. */ CanvasVectorLayerRenderer.prototype.renderFrame = function (frameState, target) { var pixelRatio = frameState.pixelRatio; var layerState = frameState.layerStatesArray[frameState.layerIndex]; // set forward and inverse pixel transforms makeScale(this.pixelTransform, 1 / pixelRatio, 1 / pixelRatio); makeInverse(this.inversePixelTransform, this.pixelTransform); var canvasTransform = transformToString(this.pixelTransform); this.useContainer(target, canvasTransform, this.getBackground(frameState)); var context = this.context; var canvas = context.canvas; var replayGroup = this.replayGroup_; var declutterExecutorGroup = this.declutterExecutorGroup; if ((!replayGroup || replayGroup.isEmpty()) && (!declutterExecutorGroup || declutterExecutorGroup.isEmpty())) { return null; } // resize and clear var width = Math.round(frameState.size[0] * pixelRatio); var height = Math.round(frameState.size[1] * pixelRatio); if (canvas.width != width || canvas.height != height) { canvas.width = width; canvas.height = height; if (canvas.style.transform !== canvasTransform) { canvas.style.transform = canvasTransform; } } else if (!this.containerReused) { context.clearRect(0, 0, width, height); } this.preRender(context, frameState); var viewState = frameState.viewState; var projection = viewState.projection; this.opacity_ = layerState.opacity; this.setupCompositionContext_(); // clipped rendering if layer extent is set var clipped = false; var render = true; if (layerState.extent && this.clipping) { var layerExtent = fromUserExtent(layerState.extent, projection); render = intersectsExtent(layerExtent, frameState.extent); clipped = render && !containsExtent(layerExtent, frameState.extent); if (clipped) { this.clipUnrotated(this.compositionContext_, frameState, layerExtent); } } if (render) { this.renderWorlds(replayGroup, frameState); } if (clipped) { this.compositionContext_.restore(); } this.releaseCompositionContext_(); this.postRender(context, frameState); if (this.renderedRotation_ !== viewState.rotation) { this.renderedRotation_ = viewState.rotation; this.hitDetectionImageData_ = null; } return this.container; }; /** * Asynchronous layer level hit detection. * @param {import("../../pixel.js").Pixel} pixel Pixel. * @return {Promise>} Promise that resolves with an array of features. */ CanvasVectorLayerRenderer.prototype.getFeatures = function (pixel) { return new Promise( /** * @param {function(Array): void} resolve Resolver function. * @this {CanvasVectorLayerRenderer} */ function (resolve) { if (!this.hitDetectionImageData_ && !this.animatingOrInteracting_) { var size = [this.context.canvas.width, this.context.canvas.height]; apply(this.pixelTransform, size); var center = this.renderedCenter_; var resolution = this.renderedResolution_; var rotation = this.renderedRotation_; var projection = this.renderedProjection_; var extent = this.wrappedRenderedExtent_; var layer = this.getLayer(); var transforms = []; var width = size[0] * HIT_DETECT_RESOLUTION; var height = size[1] * HIT_DETECT_RESOLUTION; transforms.push(this.getRenderTransform(center, resolution, rotation, HIT_DETECT_RESOLUTION, width, height, 0).slice()); var source = layer.getSource(); var projectionExtent = projection.getExtent(); if (source.getWrapX() && projection.canWrapX() && !containsExtent(projectionExtent, extent)) { var startX = extent[0]; var worldWidth = getWidth(projectionExtent); var world = 0; var offsetX = void 0; while (startX < projectionExtent[0]) { --world; offsetX = worldWidth * world; transforms.push(this.getRenderTransform(center, resolution, rotation, HIT_DETECT_RESOLUTION, width, height, offsetX).slice()); startX += worldWidth; } world = 0; startX = extent[2]; while (startX > projectionExtent[2]) { ++world; offsetX = worldWidth * world; transforms.push(this.getRenderTransform(center, resolution, rotation, HIT_DETECT_RESOLUTION, width, height, offsetX).slice()); startX -= worldWidth; } } this.hitDetectionImageData_ = createHitDetectionImageData(size, transforms, this.renderedFeatures_, layer.getStyleFunction(), extent, resolution, rotation); } resolve(hitDetect(pixel, this.renderedFeatures_, this.hitDetectionImageData_)); }.bind(this)); }; /** * @param {import("../../coordinate.js").Coordinate} coordinate Coordinate. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @param {number} hitTolerance Hit tolerance in pixels. * @param {import("../vector.js").FeatureCallback} callback Feature callback. * @param {Array>} matches The hit detected matches with tolerance. * @return {T|undefined} Callback result. * @template T */ CanvasVectorLayerRenderer.prototype.forEachFeatureAtCoordinate = function (coordinate, frameState, hitTolerance, callback, matches) { var _this = this; if (!this.replayGroup_) { return undefined; } var resolution = frameState.viewState.resolution; var rotation = frameState.viewState.rotation; var layer = this.getLayer(); /** @type {!Object|true>} */ var features = {}; /** * @param {import("../../Feature.js").FeatureLike} feature Feature. * @param {import("../../geom/SimpleGeometry.js").default} geometry Geometry. * @param {number} distanceSq The squared distance to the click position * @return {T|undefined} Callback result. */ var featureCallback = function (feature, geometry, distanceSq) { var key = getUid(feature); var match = features[key]; if (!match) { if (distanceSq === 0) { features[key] = true; return callback(feature, layer, geometry); } matches.push((features[key] = { feature: feature, layer: layer, geometry: geometry, distanceSq: distanceSq, callback: callback, })); } else if (match !== true && distanceSq < match.distanceSq) { if (distanceSq === 0) { features[key] = true; matches.splice(matches.lastIndexOf(match), 1); return callback(feature, layer, geometry); } match.geometry = geometry; match.distanceSq = distanceSq; } return undefined; }; var result; var executorGroups = [this.replayGroup_]; if (this.declutterExecutorGroup) { executorGroups.push(this.declutterExecutorGroup); } executorGroups.some(function (executorGroup) { return (result = executorGroup.forEachFeatureAtCoordinate(coordinate, resolution, rotation, hitTolerance, featureCallback, executorGroup === _this.declutterExecutorGroup && frameState.declutterTree ? frameState.declutterTree.all().map(function (item) { return item.value; }) : null)); }); return result; }; /** * Perform action necessary to get the layer rendered after new fonts have loaded */ CanvasVectorLayerRenderer.prototype.handleFontsChanged = function () { var layer = this.getLayer(); if (layer.getVisible() && this.replayGroup_) { layer.changed(); } }; /** * Handle changes in image style state. * @param {import("../../events/Event.js").default} event Image style change event. * @private */ CanvasVectorLayerRenderer.prototype.handleStyleImageChange_ = function (event) { this.renderIfReadyAndVisible(); }; /** * Determine whether render should be called. * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. * @return {boolean} Layer is ready to be rendered. */ CanvasVectorLayerRenderer.prototype.prepareFrame = function (frameState) { var vectorLayer = this.getLayer(); var vectorSource = vectorLayer.getSource(); if (!vectorSource) { return false; } var animating = frameState.viewHints[ViewHint.ANIMATING]; var interacting = frameState.viewHints[ViewHint.INTERACTING]; var updateWhileAnimating = vectorLayer.getUpdateWhileAnimating(); var updateWhileInteracting = vectorLayer.getUpdateWhileInteracting(); if ((this.ready && !updateWhileAnimating && animating) || (!updateWhileInteracting && interacting)) { this.animatingOrInteracting_ = true; return true; } this.animatingOrInteracting_ = false; var frameStateExtent = frameState.extent; var viewState = frameState.viewState; var projection = viewState.projection; var resolution = viewState.resolution; var pixelRatio = frameState.pixelRatio; var vectorLayerRevision = vectorLayer.getRevision(); var vectorLayerRenderBuffer = vectorLayer.getRenderBuffer(); var vectorLayerRenderOrder = vectorLayer.getRenderOrder(); if (vectorLayerRenderOrder === undefined) { vectorLayerRenderOrder = defaultRenderOrder; } var center = viewState.center.slice(); var extent = buffer(frameStateExtent, vectorLayerRenderBuffer * resolution); var renderedExtent = extent.slice(); var loadExtents = [extent.slice()]; var projectionExtent = projection.getExtent(); if (vectorSource.getWrapX() && projection.canWrapX() && !containsExtent(projectionExtent, frameState.extent)) { // For the replay group, we need an extent that intersects the real world // (-180° to +180°). To support geometries in a coordinate range from -540° // to +540°, we add at least 1 world width on each side of the projection // extent. If the viewport is wider than the world, we need to add half of // the viewport width to make sure we cover the whole viewport. var worldWidth = getWidth(projectionExtent); var gutter = Math.max(getWidth(extent) / 2, worldWidth); extent[0] = projectionExtent[0] - gutter; extent[2] = projectionExtent[2] + gutter; wrapCoordinateX(center, projection); var loadExtent = wrapExtentX(loadExtents[0], projection); // If the extent crosses the date line, we load data for both edges of the worlds if (loadExtent[0] < projectionExtent[0] && loadExtent[2] < projectionExtent[2]) { loadExtents.push([ loadExtent[0] + worldWidth, loadExtent[1], loadExtent[2] + worldWidth, loadExtent[3], ]); } else if (loadExtent[0] > projectionExtent[0] && loadExtent[2] > projectionExtent[2]) { loadExtents.push([ loadExtent[0] - worldWidth, loadExtent[1], loadExtent[2] - worldWidth, loadExtent[3], ]); } } if (this.ready && this.renderedResolution_ == resolution && this.renderedRevision_ == vectorLayerRevision && this.renderedRenderOrder_ == vectorLayerRenderOrder && containsExtent(this.wrappedRenderedExtent_, extent)) { if (!equals(this.renderedExtent_, renderedExtent)) { this.hitDetectionImageData_ = null; this.renderedExtent_ = renderedExtent; } this.renderedCenter_ = center; this.replayGroupChanged = false; return true; } this.replayGroup_ = null; var replayGroup = new CanvasBuilderGroup(getRenderTolerance(resolution, pixelRatio), extent, resolution, pixelRatio); var declutterBuilderGroup; if (this.getLayer().getDeclutter()) { declutterBuilderGroup = new CanvasBuilderGroup(getRenderTolerance(resolution, pixelRatio), extent, resolution, pixelRatio); } var userProjection = getUserProjection(); var userTransform; if (userProjection) { for (var i = 0, ii = loadExtents.length; i < ii; ++i) { var extent_1 = loadExtents[i]; var userExtent_1 = toUserExtent(extent_1, projection); vectorSource.loadFeatures(userExtent_1, toUserResolution(resolution, projection), userProjection); } userTransform = getTransformFromProjections(userProjection, projection); } else { for (var i = 0, ii = loadExtents.length; i < ii; ++i) { vectorSource.loadFeatures(loadExtents[i], resolution, projection); } } var squaredTolerance = getSquaredRenderTolerance(resolution, pixelRatio); var ready = true; var render = /** * @param {import("../../Feature.js").default} feature Feature. * @this {CanvasVectorLayerRenderer} */ function (feature) { var styles; var styleFunction = feature.getStyleFunction() || vectorLayer.getStyleFunction(); if (styleFunction) { styles = styleFunction(feature, resolution); } if (styles) { var dirty = this.renderFeature(feature, squaredTolerance, styles, replayGroup, userTransform, declutterBuilderGroup); ready = ready && !dirty; } }.bind(this); var userExtent = toUserExtent(extent, projection); /** @type {Array} */ var features = vectorSource.getFeaturesInExtent(userExtent); if (vectorLayerRenderOrder) { features.sort(vectorLayerRenderOrder); } for (var i = 0, ii = features.length; i < ii; ++i) { render(features[i]); } this.renderedFeatures_ = features; this.ready = ready; var replayGroupInstructions = replayGroup.finish(); var executorGroup = new ExecutorGroup(extent, resolution, pixelRatio, vectorSource.getOverlaps(), replayGroupInstructions, vectorLayer.getRenderBuffer()); if (declutterBuilderGroup) { this.declutterExecutorGroup = new ExecutorGroup(extent, resolution, pixelRatio, vectorSource.getOverlaps(), declutterBuilderGroup.finish(), vectorLayer.getRenderBuffer()); } this.renderedResolution_ = resolution; this.renderedRevision_ = vectorLayerRevision; this.renderedRenderOrder_ = vectorLayerRenderOrder; this.renderedExtent_ = renderedExtent; this.wrappedRenderedExtent_ = extent; this.renderedCenter_ = center; this.renderedProjection_ = projection; this.replayGroup_ = executorGroup; this.hitDetectionImageData_ = null; this.replayGroupChanged = true; return true; }; /** * @param {import("../../Feature.js").default} feature Feature. * @param {number} squaredTolerance Squared render tolerance. * @param {import("../../style/Style.js").default|Array} styles The style or array of styles. * @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("../../render/canvas/BuilderGroup.js").default} [opt_declutterBuilderGroup] Builder for decluttering. * @return {boolean} `true` if an image is loading. */ CanvasVectorLayerRenderer.prototype.renderFeature = function (feature, squaredTolerance, styles, builderGroup, opt_transform, opt_declutterBuilderGroup) { if (!styles) { return false; } var loading = false; if (Array.isArray(styles)) { for (var i = 0, ii = styles.length; i < ii; ++i) { loading = renderFeature(builderGroup, feature, styles[i], squaredTolerance, this.boundHandleStyleImageChange_, opt_transform, opt_declutterBuilderGroup) || loading; } } else { loading = renderFeature(builderGroup, feature, styles, squaredTolerance, this.boundHandleStyleImageChange_, opt_transform, opt_declutterBuilderGroup); } return loading; }; return CanvasVectorLayerRenderer; }(CanvasLayerRenderer)); export default CanvasVectorLayerRenderer; //# sourceMappingURL=VectorLayer.js.map