UNPKG

heli-agri

Version:

HeliAgri is a high-performance, feature-packed library for creating interactive maps on the web. It can display map tiles, vector data and markers loaded from any source on any web page. OpenLayers has been developed to further the use of geographic infor

425 lines (389 loc) 12.8 kB
/** * @module ol/render/canvas/ExecutorGroup */ import Executor from './Executor.js'; import {ascending} from '../../array.js'; import {buffer, createEmpty, extendCoordinate} from '../../extent.js'; import { compose as composeTransform, create as createTransform, } from '../../transform.js'; import {createCanvasContext2D} from '../../dom.js'; import {isEmpty} from '../../obj.js'; import {transform2D} from '../../geom/flat/transform.js'; /** * @const * @type {Array<import("../canvas.js").BuilderType>} */ const ORDER = ['Polygon', 'Circle', 'LineString', 'Image', 'Text', 'Default']; class ExecutorGroup { /** * @param {import("../../extent.js").Extent} maxExtent Max extent for clipping. When a * `maxExtent` was set on the Builder for this executor group, the same `maxExtent` * should be set here, unless the target context does not exceed that extent (which * can be the case when rendering to tiles). * @param {number} resolution Resolution. * @param {number} pixelRatio Pixel ratio. * @param {boolean} overlaps The executor group can have overlapping geometries. * @param {!Object<string, !Object<import("../canvas.js").BuilderType, import("../canvas.js").SerializableInstructions>>} allInstructions * The serializable instructions. * @param {number} [renderBuffer] Optional rendering buffer. */ constructor( maxExtent, resolution, pixelRatio, overlaps, allInstructions, renderBuffer ) { /** * @private * @type {import("../../extent.js").Extent} */ this.maxExtent_ = maxExtent; /** * @private * @type {boolean} */ this.overlaps_ = overlaps; /** * @private * @type {number} */ this.pixelRatio_ = pixelRatio; /** * @private * @type {number} */ this.resolution_ = resolution; /** * @private * @type {number|undefined} */ this.renderBuffer_ = renderBuffer; /** * @private * @type {!Object<string, !Object<import("../canvas.js").BuilderType, import("./Executor").default>>} */ this.executorsByZIndex_ = {}; /** * @private * @type {CanvasRenderingContext2D} */ this.hitDetectionContext_ = null; /** * @private * @type {import("../../transform.js").Transform} */ this.hitDetectionTransform_ = createTransform(); this.createExecutors_(allInstructions); } /** * @param {CanvasRenderingContext2D} context Context. * @param {import("../../transform.js").Transform} transform Transform. */ clip(context, transform) { const flatClipCoords = this.getClipCoords(transform); context.beginPath(); context.moveTo(flatClipCoords[0], flatClipCoords[1]); context.lineTo(flatClipCoords[2], flatClipCoords[3]); context.lineTo(flatClipCoords[4], flatClipCoords[5]); context.lineTo(flatClipCoords[6], flatClipCoords[7]); context.clip(); } /** * Create executors and populate them using the provided instructions. * @private * @param {!Object<string, !Object<import("../canvas.js").BuilderType, import("../canvas.js").SerializableInstructions>>} allInstructions The serializable instructions */ createExecutors_(allInstructions) { for (const zIndex in allInstructions) { let executors = this.executorsByZIndex_[zIndex]; if (executors === undefined) { executors = {}; this.executorsByZIndex_[zIndex] = executors; } const instructionByZindex = allInstructions[zIndex]; for (const builderType in instructionByZindex) { const instructions = instructionByZindex[builderType]; executors[builderType] = new Executor( this.resolution_, this.pixelRatio_, this.overlaps_, instructions ); } } } /** * @param {Array<import("../canvas.js").BuilderType>} executors Executors. * @return {boolean} Has executors of the provided types. */ hasExecutors(executors) { for (const zIndex in this.executorsByZIndex_) { const candidates = this.executorsByZIndex_[zIndex]; for (let i = 0, ii = executors.length; i < ii; ++i) { if (executors[i] in candidates) { return true; } } } return false; } /** * @param {import("../../coordinate.js").Coordinate} coordinate Coordinate. * @param {number} resolution Resolution. * @param {number} rotation Rotation. * @param {number} hitTolerance Hit tolerance in pixels. * @param {function(import("../../Feature.js").FeatureLike, import("../../geom/SimpleGeometry.js").default, number): T} callback Feature callback. * @param {Array<import("../../Feature.js").FeatureLike>} declutteredFeatures Decluttered features. * @return {T|undefined} Callback result. * @template T */ forEachFeatureAtCoordinate( coordinate, resolution, rotation, hitTolerance, callback, declutteredFeatures ) { hitTolerance = Math.round(hitTolerance); const contextSize = hitTolerance * 2 + 1; const transform = composeTransform( this.hitDetectionTransform_, hitTolerance + 0.5, hitTolerance + 0.5, 1 / resolution, -1 / resolution, -rotation, -coordinate[0], -coordinate[1] ); const newContext = !this.hitDetectionContext_; if (newContext) { this.hitDetectionContext_ = createCanvasContext2D( contextSize, contextSize, undefined, {willReadFrequently: true} ); } const context = this.hitDetectionContext_; if ( context.canvas.width !== contextSize || context.canvas.height !== contextSize ) { context.canvas.width = contextSize; context.canvas.height = contextSize; } else if (!newContext) { context.clearRect(0, 0, contextSize, contextSize); } /** * @type {import("../../extent.js").Extent} */ let hitExtent; if (this.renderBuffer_ !== undefined) { hitExtent = createEmpty(); extendCoordinate(hitExtent, coordinate); buffer( hitExtent, resolution * (this.renderBuffer_ + hitTolerance), hitExtent ); } const indexes = getPixelIndexArray(hitTolerance); let builderType; /** * @param {import("../../Feature.js").FeatureLike} feature Feature. * @param {import("../../geom/SimpleGeometry.js").default} geometry Geometry. * @return {T|undefined} Callback result. */ function featureCallback(feature, geometry) { const imageData = context.getImageData( 0, 0, contextSize, contextSize ).data; for (let i = 0, ii = indexes.length; i < ii; i++) { if (imageData[indexes[i]] > 0) { if ( !declutteredFeatures || (builderType !== 'Image' && builderType !== 'Text') || declutteredFeatures.includes(feature) ) { const idx = (indexes[i] - 3) / 4; const x = hitTolerance - (idx % contextSize); const y = hitTolerance - ((idx / contextSize) | 0); const result = callback(feature, geometry, x * x + y * y); if (result) { return result; } } context.clearRect(0, 0, contextSize, contextSize); break; } } return undefined; } /** @type {Array<number>} */ const zs = Object.keys(this.executorsByZIndex_).map(Number); zs.sort(ascending); let i, j, executors, executor, result; for (i = zs.length - 1; i >= 0; --i) { const zIndexKey = zs[i].toString(); executors = this.executorsByZIndex_[zIndexKey]; for (j = ORDER.length - 1; j >= 0; --j) { builderType = ORDER[j]; executor = executors[builderType]; if (executor !== undefined) { result = executor.executeHitDetection( context, transform, rotation, featureCallback, hitExtent ); if (result) { return result; } } } } return undefined; } /** * @param {import("../../transform.js").Transform} transform Transform. * @return {Array<number>|null} Clip coordinates. */ getClipCoords(transform) { const maxExtent = this.maxExtent_; if (!maxExtent) { return null; } const minX = maxExtent[0]; const minY = maxExtent[1]; const maxX = maxExtent[2]; const maxY = maxExtent[3]; const flatClipCoords = [minX, minY, minX, maxY, maxX, maxY, maxX, minY]; transform2D(flatClipCoords, 0, 8, 2, transform, flatClipCoords); return flatClipCoords; } /** * @return {boolean} Is empty. */ isEmpty() { return isEmpty(this.executorsByZIndex_); } /** * @param {CanvasRenderingContext2D} context Context. * @param {number} contextScale Scale of the context. * @param {import("../../transform.js").Transform} transform Transform. * @param {number} viewRotation View rotation. * @param {boolean} snapToPixel Snap point symbols and test to integer pixel. * @param {Array<import("../canvas.js").BuilderType>} [builderTypes] Ordered replay types to replay. * Default is {@link module:ol/render/replay~ORDER} * @param {import("rbush").default} [declutterTree] Declutter tree. */ execute( context, contextScale, transform, viewRotation, snapToPixel, builderTypes, declutterTree ) { /** @type {Array<number>} */ const zs = Object.keys(this.executorsByZIndex_).map(Number); zs.sort(ascending); // setup clipping so that the parts of over-simplified geometries are not // visible outside the current extent when panning if (this.maxExtent_) { context.save(); this.clip(context, transform); } builderTypes = builderTypes ? builderTypes : ORDER; let i, ii, j, jj, replays, replay; if (declutterTree) { zs.reverse(); } for (i = 0, ii = zs.length; i < ii; ++i) { const zIndexKey = zs[i].toString(); replays = this.executorsByZIndex_[zIndexKey]; for (j = 0, jj = builderTypes.length; j < jj; ++j) { const builderType = builderTypes[j]; replay = replays[builderType]; if (replay !== undefined) { replay.execute( context, contextScale, transform, viewRotation, snapToPixel, declutterTree ); } } } if (this.maxExtent_) { context.restore(); } } } /** * This cache is used to store arrays of indexes for calculated pixel circles * to increase performance. * It is a static property to allow each Replaygroup to access it. * @type {Object<number, Array<number>>} */ const circlePixelIndexArrayCache = {}; /** * This methods creates an array with indexes of all pixels within a circle, * ordered by how close they are to the center. * A cache is used to increase performance. * @param {number} radius Radius. * @return {Array<number>} An array with indexes within a circle. */ export function getPixelIndexArray(radius) { if (circlePixelIndexArrayCache[radius] !== undefined) { return circlePixelIndexArrayCache[radius]; } const size = radius * 2 + 1; const maxDistanceSq = radius * radius; const distances = new Array(maxDistanceSq + 1); for (let i = 0; i <= radius; ++i) { for (let j = 0; j <= radius; ++j) { const distanceSq = i * i + j * j; if (distanceSq > maxDistanceSq) { break; } let distance = distances[distanceSq]; if (!distance) { distance = []; distances[distanceSq] = distance; } distance.push(((radius + i) * size + (radius + j)) * 4 + 3); if (i > 0) { distance.push(((radius - i) * size + (radius + j)) * 4 + 3); } if (j > 0) { distance.push(((radius + i) * size + (radius - j)) * 4 + 3); if (i > 0) { distance.push(((radius - i) * size + (radius - j)) * 4 + 3); } } } } const pixelIndex = []; for (let i = 0, ii = distances.length; i < ii; ++i) { if (distances[i]) { pixelIndex.push(...distances[i]); } } circlePixelIndexArrayCache[radius] = pixelIndex; return pixelIndex; } export default ExecutorGroup;