planning
All checks were successful
Publish To Prod / deploy_and_publish (push) Successful in 35s

This commit is contained in:
2024-10-14 09:15:30 +02:00
parent bcba00a730
commit 6e64e138e2
21059 changed files with 2317811 additions and 1 deletions

198
node_modules/ol/src/renderer/Composite.js generated vendored Normal file
View File

@@ -0,0 +1,198 @@
/**
* @module ol/renderer/Composite
*/
import MapRenderer from './Map.js';
import ObjectEventType from '../ObjectEventType.js';
import RenderEvent from '../render/Event.js';
import RenderEventType from '../render/EventType.js';
import {CLASS_UNSELECTABLE} from '../css.js';
import {checkedFonts} from '../render/canvas.js';
import {inView} from '../layer/Layer.js';
import {listen, unlistenByKey} from '../events.js';
import {replaceChildren} from '../dom.js';
/**
* @classdesc
* Canvas map renderer.
* @api
*/
class CompositeMapRenderer extends MapRenderer {
/**
* @param {import("../PluggableMap.js").default} map Map.
*/
constructor(map) {
super(map);
/**
* @type {import("../events.js").EventsKey}
*/
this.fontChangeListenerKey_ = listen(
checkedFonts,
ObjectEventType.PROPERTYCHANGE,
map.redrawText.bind(map)
);
/**
* @private
* @type {HTMLDivElement}
*/
this.element_ = document.createElement('div');
const style = this.element_.style;
style.position = 'absolute';
style.width = '100%';
style.height = '100%';
style.zIndex = '0';
this.element_.className = CLASS_UNSELECTABLE + ' ol-layers';
const container = map.getViewport();
container.insertBefore(this.element_, container.firstChild || null);
/**
* @private
* @type {Array<HTMLElement>}
*/
this.children_ = [];
/**
* @private
* @type {boolean}
*/
this.renderedVisible_ = true;
}
/**
* @param {import("../render/EventType.js").default} type Event type.
* @param {import("../PluggableMap.js").FrameState} frameState Frame state.
*/
dispatchRenderEvent(type, frameState) {
const map = this.getMap();
if (map.hasListener(type)) {
const event = new RenderEvent(type, undefined, frameState);
map.dispatchEvent(event);
}
}
disposeInternal() {
unlistenByKey(this.fontChangeListenerKey_);
this.element_.parentNode.removeChild(this.element_);
super.disposeInternal();
}
/**
* Render.
* @param {?import("../PluggableMap.js").FrameState} frameState Frame state.
*/
renderFrame(frameState) {
if (!frameState) {
if (this.renderedVisible_) {
this.element_.style.display = 'none';
this.renderedVisible_ = false;
}
return;
}
this.calculateMatrices2D(frameState);
this.dispatchRenderEvent(RenderEventType.PRECOMPOSE, frameState);
const layerStatesArray = frameState.layerStatesArray.sort(function (a, b) {
return a.zIndex - b.zIndex;
});
const viewState = frameState.viewState;
this.children_.length = 0;
/**
* @type {Array<import("../layer/BaseVector.js").default>}
*/
const declutterLayers = [];
let previousElement = null;
for (let i = 0, ii = layerStatesArray.length; i < ii; ++i) {
const layerState = layerStatesArray[i];
frameState.layerIndex = i;
const layer = layerState.layer;
const sourceState = layer.getSourceState();
if (
!inView(layerState, viewState) ||
(sourceState != 'ready' && sourceState != 'undefined')
) {
layer.unrender();
continue;
}
const element = layer.render(frameState, previousElement);
if (!element) {
continue;
}
if (element !== previousElement) {
this.children_.push(element);
previousElement = element;
}
if ('getDeclutter' in layer) {
declutterLayers.push(
/** @type {import("../layer/BaseVector.js").default} */ (layer)
);
}
}
for (let i = declutterLayers.length - 1; i >= 0; --i) {
declutterLayers[i].renderDeclutter(frameState);
}
replaceChildren(this.element_, this.children_);
this.dispatchRenderEvent(RenderEventType.POSTCOMPOSE, frameState);
if (!this.renderedVisible_) {
this.element_.style.display = '';
this.renderedVisible_ = true;
}
this.scheduleExpireIconCache(frameState);
}
/**
* @param {import("../pixel.js").Pixel} pixel Pixel.
* @param {import("../PluggableMap.js").FrameState} frameState FrameState.
* @param {number} hitTolerance Hit tolerance in pixels.
* @param {function(import("../layer/Layer.js").default<import("../source/Source").default>, (Uint8ClampedArray|Uint8Array)): T} callback Layer
* callback.
* @param {function(import("../layer/Layer.js").default<import("../source/Source").default>): boolean} layerFilter Layer filter
* function, only layers which are visible and for which this function
* returns `true` will be tested for features. By default, all visible
* layers will be tested.
* @return {T|undefined} Callback result.
* @template T
*/
forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, layerFilter) {
const viewState = frameState.viewState;
const layerStates = frameState.layerStatesArray;
const numLayers = layerStates.length;
for (let i = numLayers - 1; i >= 0; --i) {
const layerState = layerStates[i];
const layer = layerState.layer;
if (
layer.hasRenderer() &&
inView(layerState, viewState) &&
layerFilter(layer)
) {
const layerRenderer = layer.getRenderer();
const data = layerRenderer.getDataAtPixel(
pixel,
frameState,
hitTolerance
);
if (data) {
const result = callback(layer, data);
if (result) {
return result;
}
}
}
}
return undefined;
}
}
export default CompositeMapRenderer;

212
node_modules/ol/src/renderer/Layer.js generated vendored Normal file
View File

@@ -0,0 +1,212 @@
/**
* @module ol/renderer/Layer
*/
import EventType from '../events/EventType.js';
import ImageState from '../ImageState.js';
import Observable from '../Observable.js';
import {abstract} from '../util.js';
/**
* @template {import("../layer/Layer.js").default} LayerType
*/
class LayerRenderer extends Observable {
/**
* @param {LayerType} layer Layer.
*/
constructor(layer) {
super();
/**
* The renderer is initialized and ready to render.
* @type {boolean}
*/
this.ready = true;
/** @private */
this.boundHandleImageChange_ = this.handleImageChange_.bind(this);
/**
* @protected
* @type {LayerType}
*/
this.layer_ = layer;
/**
* @type {import("../render/canvas/ExecutorGroup").default}
*/
this.declutterExecutorGroup = null;
}
/**
* Asynchronous layer level hit detection.
* @param {import("../pixel.js").Pixel} pixel Pixel.
* @return {Promise<Array<import("../Feature").default>>} Promise that resolves with
* an array of features.
*/
getFeatures(pixel) {
return abstract();
}
/**
* @param {import("../pixel.js").Pixel} pixel Pixel.
* @return {Uint8ClampedArray|Uint8Array|Float32Array|DataView|null} Pixel data.
*/
getData(pixel) {
return null;
}
/**
* Determine whether render should be called.
* @abstract
* @param {import("../PluggableMap.js").FrameState} frameState Frame state.
* @return {boolean} Layer is ready to be rendered.
*/
prepareFrame(frameState) {
return abstract();
}
/**
* Render the layer.
* @abstract
* @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.
*/
renderFrame(frameState, target) {
return abstract();
}
/**
* @param {Object<number, Object<string, import("../Tile.js").default>>} tiles Lookup of loaded tiles by zoom level.
* @param {number} zoom Zoom level.
* @param {import("../Tile.js").default} tile Tile.
* @return {boolean|void} If `false`, the tile will not be considered loaded.
*/
loadedTileCallback(tiles, zoom, tile) {
if (!tiles[zoom]) {
tiles[zoom] = {};
}
tiles[zoom][tile.tileCoord.toString()] = tile;
return undefined;
}
/**
* Create a function that adds loaded tiles to the tile lookup.
* @param {import("../source/Tile.js").default} source Tile source.
* @param {import("../proj/Projection.js").default} projection Projection of the tiles.
* @param {Object<number, Object<string, import("../Tile.js").default>>} tiles Lookup of loaded tiles by zoom level.
* @return {function(number, import("../TileRange.js").default):boolean} A function that can be
* called with a zoom level and a tile range to add loaded tiles to the lookup.
* @protected
*/
createLoadedTileFinder(source, projection, tiles) {
return (
/**
* @param {number} zoom Zoom level.
* @param {import("../TileRange.js").default} tileRange Tile range.
* @return {boolean} The tile range is fully loaded.
* @this {LayerRenderer}
*/
function (zoom, tileRange) {
const callback = this.loadedTileCallback.bind(this, tiles, zoom);
return source.forEachLoadedTile(projection, zoom, tileRange, callback);
}.bind(this)
);
}
/**
* @abstract
* @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<T>} callback Feature callback.
* @param {Array<import("./Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
* @return {T|undefined} Callback result.
* @template T
*/
forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
) {
return undefined;
}
/**
* @abstract
* @param {import("../pixel.js").Pixel} pixel Pixel.
* @param {import("../PluggableMap.js").FrameState} frameState FrameState.
* @param {number} hitTolerance Hit tolerance in pixels.
* @return {Uint8ClampedArray|Uint8Array} The result. If there is no data at the pixel
* location, null will be returned. If there is data, but pixel values cannot be
* returned, and empty array will be returned.
*/
getDataAtPixel(pixel, frameState, hitTolerance) {
return null;
}
/**
* @return {LayerType} Layer.
*/
getLayer() {
return this.layer_;
}
/**
* Perform action necessary to get the layer rendered after new fonts have loaded
* @abstract
*/
handleFontsChanged() {}
/**
* Handle changes in image state.
* @param {import("../events/Event.js").default} event Image change event.
* @private
*/
handleImageChange_(event) {
const image = /** @type {import("../Image.js").default} */ (event.target);
if (image.getState() === ImageState.LOADED) {
this.renderIfReadyAndVisible();
}
}
/**
* Load the image if not already loaded, and register the image change
* listener if needed.
* @param {import("../ImageBase.js").default} image Image.
* @return {boolean} `true` if the image is already loaded, `false` otherwise.
* @protected
*/
loadImage(image) {
let imageState = image.getState();
if (imageState != ImageState.LOADED && imageState != ImageState.ERROR) {
image.addEventListener(EventType.CHANGE, this.boundHandleImageChange_);
}
if (imageState == ImageState.IDLE) {
image.load();
imageState = image.getState();
}
return imageState == ImageState.LOADED;
}
/**
* @protected
*/
renderIfReadyAndVisible() {
const layer = this.getLayer();
if (layer && layer.getVisible() && layer.getSourceState() === 'ready') {
layer.changed();
}
}
/**
* Clean up.
*/
disposeInternal() {
delete this.layer_;
super.disposeInternal();
}
}
export default LayerRenderer;

261
node_modules/ol/src/renderer/Map.js generated vendored Normal file
View File

@@ -0,0 +1,261 @@
/**
* @module ol/renderer/Map
*/
import Disposable from '../Disposable.js';
import {TRUE} from '../functions.js';
import {abstract} from '../util.js';
import {compose as composeTransform, makeInverse} from '../transform.js';
import {getWidth} from '../extent.js';
import {shared as iconImageCache} from '../style/IconImageCache.js';
import {inView} from '../layer/Layer.js';
import {wrapX} from '../coordinate.js';
/**
* @typedef HitMatch
* @property {import("../Feature.js").FeatureLike} feature Feature.
* @property {import("../layer/Layer.js").default} layer Layer.
* @property {import("../geom/SimpleGeometry.js").default} geometry Geometry.
* @property {number} distanceSq Squared distance.
* @property {import("./vector.js").FeatureCallback<T>} callback Callback.
* @template T
*/
/**
* @abstract
*/
class MapRenderer extends Disposable {
/**
* @param {import("../PluggableMap.js").default} map Map.
*/
constructor(map) {
super();
/**
* @private
* @type {import("../PluggableMap.js").default}
*/
this.map_ = map;
}
/**
* @abstract
* @param {import("../render/EventType.js").default} type Event type.
* @param {import("../PluggableMap.js").FrameState} frameState Frame state.
*/
dispatchRenderEvent(type, frameState) {
abstract();
}
/**
* @param {import("../PluggableMap.js").FrameState} frameState FrameState.
* @protected
*/
calculateMatrices2D(frameState) {
const viewState = frameState.viewState;
const coordinateToPixelTransform = frameState.coordinateToPixelTransform;
const pixelToCoordinateTransform = frameState.pixelToCoordinateTransform;
composeTransform(
coordinateToPixelTransform,
frameState.size[0] / 2,
frameState.size[1] / 2,
1 / viewState.resolution,
-1 / viewState.resolution,
-viewState.rotation,
-viewState.center[0],
-viewState.center[1]
);
makeInverse(pixelToCoordinateTransform, coordinateToPixelTransform);
}
/**
* @param {import("../coordinate.js").Coordinate} coordinate Coordinate.
* @param {import("../PluggableMap.js").FrameState} frameState FrameState.
* @param {number} hitTolerance Hit tolerance in pixels.
* @param {boolean} checkWrapped Check for wrapped geometries.
* @param {import("./vector.js").FeatureCallback<T>} callback Feature callback.
* @param {S} thisArg Value to use as `this` when executing `callback`.
* @param {function(this: U, import("../layer/Layer.js").default): boolean} layerFilter Layer filter
* function, only layers which are visible and for which this function
* returns `true` will be tested for features. By default, all visible
* layers will be tested.
* @param {U} thisArg2 Value to use as `this` when executing `layerFilter`.
* @return {T|undefined} Callback result.
* @template S,T,U
*/
forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
checkWrapped,
callback,
thisArg,
layerFilter,
thisArg2
) {
let result;
const viewState = frameState.viewState;
/**
* @param {boolean} managed Managed layer.
* @param {import("../Feature.js").FeatureLike} feature Feature.
* @param {import("../layer/Layer.js").default} layer Layer.
* @param {import("../geom/Geometry.js").default} geometry Geometry.
* @return {T|undefined} Callback result.
*/
function forEachFeatureAtCoordinate(managed, feature, layer, geometry) {
return callback.call(thisArg, feature, managed ? layer : null, geometry);
}
const projection = viewState.projection;
const translatedCoordinate = wrapX(coordinate.slice(), projection);
const offsets = [[0, 0]];
if (projection.canWrapX() && checkWrapped) {
const projectionExtent = projection.getExtent();
const worldWidth = getWidth(projectionExtent);
offsets.push([-worldWidth, 0], [worldWidth, 0]);
}
const layerStates = frameState.layerStatesArray;
const numLayers = layerStates.length;
const matches = /** @type {Array<HitMatch<T>>} */ ([]);
const tmpCoord = [];
for (let i = 0; i < offsets.length; i++) {
for (let j = numLayers - 1; j >= 0; --j) {
const layerState = layerStates[j];
const layer = layerState.layer;
if (
layer.hasRenderer() &&
inView(layerState, viewState) &&
layerFilter.call(thisArg2, layer)
) {
const layerRenderer = layer.getRenderer();
const source = layer.getSource();
if (layerRenderer && source) {
const coordinates = source.getWrapX()
? translatedCoordinate
: coordinate;
const callback = forEachFeatureAtCoordinate.bind(
null,
layerState.managed
);
tmpCoord[0] = coordinates[0] + offsets[i][0];
tmpCoord[1] = coordinates[1] + offsets[i][1];
result = layerRenderer.forEachFeatureAtCoordinate(
tmpCoord,
frameState,
hitTolerance,
callback,
matches
);
}
if (result) {
return result;
}
}
}
}
if (matches.length === 0) {
return undefined;
}
const order = 1 / matches.length;
matches.forEach((m, i) => (m.distanceSq += i * order));
matches.sort((a, b) => a.distanceSq - b.distanceSq);
matches.some((m) => {
return (result = m.callback(m.feature, m.layer, m.geometry));
});
return result;
}
/**
* @abstract
* @param {import("../pixel.js").Pixel} pixel Pixel.
* @param {import("../PluggableMap.js").FrameState} frameState FrameState.
* @param {number} hitTolerance Hit tolerance in pixels.
* @param {function(import("../layer/Layer.js").default<import("../source/Source").default>, (Uint8ClampedArray|Uint8Array)): T} callback Layer
* callback.
* @param {function(import("../layer/Layer.js").default<import("../source/Source").default>): boolean} layerFilter Layer filter
* function, only layers which are visible and for which this function
* returns `true` will be tested for features. By default, all visible
* layers will be tested.
* @return {T|undefined} Callback result.
* @template T
*/
forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, layerFilter) {
return abstract();
}
/**
* @param {import("../coordinate.js").Coordinate} coordinate Coordinate.
* @param {import("../PluggableMap.js").FrameState} frameState FrameState.
* @param {number} hitTolerance Hit tolerance in pixels.
* @param {boolean} checkWrapped Check for wrapped geometries.
* @param {function(this: U, import("../layer/Layer.js").default): boolean} layerFilter Layer filter
* function, only layers which are visible and for which this function
* returns `true` will be tested for features. By default, all visible
* layers will be tested.
* @param {U} thisArg Value to use as `this` when executing `layerFilter`.
* @return {boolean} Is there a feature at the given coordinate?
* @template U
*/
hasFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
checkWrapped,
layerFilter,
thisArg
) {
const hasFeature = this.forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
checkWrapped,
TRUE,
this,
layerFilter,
thisArg
);
return hasFeature !== undefined;
}
/**
* @return {import("../PluggableMap.js").default} Map.
*/
getMap() {
return this.map_;
}
/**
* Render.
* @abstract
* @param {?import("../PluggableMap.js").FrameState} frameState Frame state.
*/
renderFrame(frameState) {
abstract();
}
/**
* @param {import("../PluggableMap.js").FrameState} frameState Frame state.
* @protected
*/
scheduleExpireIconCache(frameState) {
if (iconImageCache.canExpireCache()) {
frameState.postRenderFunctions.push(expireIconCache);
}
}
}
/**
* @param {import("../PluggableMap.js").default} map Map.
* @param {import("../PluggableMap.js").FrameState} frameState Frame state.
*/
function expireIconCache(map, frameState) {
iconImageCache.expire();
}
export default MapRenderer;

275
node_modules/ol/src/renderer/canvas/ImageLayer.js generated vendored Normal file
View File

@@ -0,0 +1,275 @@
/**
* @module ol/renderer/canvas/ImageLayer
*/
import CanvasLayerRenderer from './Layer.js';
import ImageState from '../../ImageState.js';
import ViewHint from '../../ViewHint.js';
import {ENABLE_RASTER_REPROJECTION} from '../../reproj/common.js';
import {IMAGE_SMOOTHING_DISABLED, IMAGE_SMOOTHING_ENABLED} from './common.js';
import {
apply as applyTransform,
compose as composeTransform,
makeInverse,
toString as toTransformString,
} from '../../transform.js';
import {assign} from '../../obj.js';
import {
containsCoordinate,
containsExtent,
getHeight,
getIntersection,
getWidth,
intersects as intersectsExtent,
isEmpty,
} from '../../extent.js';
import {fromUserExtent} from '../../proj.js';
/**
* @classdesc
* Canvas renderer for image layers.
* @api
*/
class CanvasImageLayerRenderer extends CanvasLayerRenderer {
/**
* @param {import("../../layer/Image.js").default} imageLayer Image layer.
*/
constructor(imageLayer) {
super(imageLayer);
/**
* @protected
* @type {?import("../../ImageBase.js").default}
*/
this.image_ = null;
}
/**
* @return {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} Image.
*/
getImage() {
return !this.image_ ? null : this.image_.getImage();
}
/**
* Determine whether render should be called.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @return {boolean} Layer is ready to be rendered.
*/
prepareFrame(frameState) {
const layerState = frameState.layerStatesArray[frameState.layerIndex];
const pixelRatio = frameState.pixelRatio;
const viewState = frameState.viewState;
const viewResolution = viewState.resolution;
const imageSource = this.getLayer().getSource();
const hints = frameState.viewHints;
let renderedExtent = frameState.extent;
if (layerState.extent !== undefined) {
renderedExtent = getIntersection(
renderedExtent,
fromUserExtent(layerState.extent, viewState.projection)
);
}
if (
!hints[ViewHint.ANIMATING] &&
!hints[ViewHint.INTERACTING] &&
!isEmpty(renderedExtent)
) {
if (imageSource) {
let projection = viewState.projection;
if (!ENABLE_RASTER_REPROJECTION) {
const sourceProjection = imageSource.getProjection();
if (sourceProjection) {
projection = sourceProjection;
}
}
const image = imageSource.getImage(
renderedExtent,
viewResolution,
pixelRatio,
projection
);
if (image) {
if (this.loadImage(image)) {
this.image_ = image;
} else if (image.getState() === ImageState.EMPTY) {
this.image_ = null;
}
}
} else {
this.image_ = null;
}
}
return !!this.image_;
}
/**
* @param {import("../../pixel.js").Pixel} pixel Pixel.
* @return {Uint8ClampedArray} Data at the pixel location.
*/
getData(pixel) {
const frameState = this.frameState;
if (!frameState) {
return null;
}
const layer = this.getLayer();
const coordinate = applyTransform(
frameState.pixelToCoordinateTransform,
pixel.slice()
);
const layerExtent = layer.getExtent();
if (layerExtent) {
if (!containsCoordinate(layerExtent, coordinate)) {
return null;
}
}
const imageExtent = this.image_.getExtent();
const img = this.image_.getImage();
const imageMapWidth = getWidth(imageExtent);
const col = Math.floor(
img.width * ((coordinate[0] - imageExtent[0]) / imageMapWidth)
);
if (col < 0 || col >= img.width) {
return null;
}
const imageMapHeight = getHeight(imageExtent);
const row = Math.floor(
img.height * ((imageExtent[3] - coordinate[1]) / imageMapHeight)
);
if (row < 0 || row >= img.height) {
return null;
}
return this.getImageData(img, col, row);
}
/**
* 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.
*/
renderFrame(frameState, target) {
const image = this.image_;
const imageExtent = image.getExtent();
const imageResolution = image.getResolution();
const imagePixelRatio = image.getPixelRatio();
const layerState = frameState.layerStatesArray[frameState.layerIndex];
const pixelRatio = frameState.pixelRatio;
const viewState = frameState.viewState;
const viewCenter = viewState.center;
const viewResolution = viewState.resolution;
const scale =
(pixelRatio * imageResolution) / (viewResolution * imagePixelRatio);
const extent = frameState.extent;
const resolution = viewState.resolution;
const rotation = viewState.rotation;
// desired dimensions of the canvas in pixels
const width = Math.round((getWidth(extent) / resolution) * pixelRatio);
const height = Math.round((getHeight(extent) / resolution) * pixelRatio);
// set forward and inverse pixel transforms
composeTransform(
this.pixelTransform,
frameState.size[0] / 2,
frameState.size[1] / 2,
1 / pixelRatio,
1 / pixelRatio,
rotation,
-width / 2,
-height / 2
);
makeInverse(this.inversePixelTransform, this.pixelTransform);
const canvasTransform = toTransformString(this.pixelTransform);
this.useContainer(target, canvasTransform, this.getBackground(frameState));
const context = this.context;
const canvas = context.canvas;
if (canvas.width != width || canvas.height != height) {
canvas.width = width;
canvas.height = height;
} else if (!this.containerReused) {
context.clearRect(0, 0, width, height);
}
// clipped rendering if layer extent is set
let clipped = false;
let render = true;
if (layerState.extent) {
const layerExtent = fromUserExtent(
layerState.extent,
viewState.projection
);
render = intersectsExtent(layerExtent, frameState.extent);
clipped = render && !containsExtent(layerExtent, frameState.extent);
if (clipped) {
this.clipUnrotated(context, frameState, layerExtent);
}
}
const img = image.getImage();
const transform = composeTransform(
this.tempTransform,
width / 2,
height / 2,
scale,
scale,
0,
(imagePixelRatio * (imageExtent[0] - viewCenter[0])) / imageResolution,
(imagePixelRatio * (viewCenter[1] - imageExtent[3])) / imageResolution
);
this.renderedResolution = (imageResolution * pixelRatio) / imagePixelRatio;
const dw = img.width * transform[0];
const dh = img.height * transform[3];
if (!this.getLayer().getSource().getInterpolate()) {
assign(context, IMAGE_SMOOTHING_DISABLED);
}
this.preRender(context, frameState);
if (render && dw >= 0.5 && dh >= 0.5) {
const dx = transform[4];
const dy = transform[5];
const opacity = layerState.opacity;
let previousAlpha;
if (opacity !== 1) {
previousAlpha = context.globalAlpha;
context.globalAlpha = opacity;
}
context.drawImage(img, 0, 0, +img.width, +img.height, dx, dy, dw, dh);
if (opacity !== 1) {
context.globalAlpha = previousAlpha;
}
}
this.postRender(context, frameState);
if (clipped) {
context.restore();
}
assign(context, IMAGE_SMOOTHING_ENABLED);
if (canvasTransform !== canvas.style.transform) {
canvas.style.transform = canvasTransform;
}
return this.container;
}
}
export default CanvasImageLayerRenderer;

386
node_modules/ol/src/renderer/canvas/Layer.js generated vendored Normal file
View File

@@ -0,0 +1,386 @@
/**
* @module ol/renderer/canvas/Layer
*/
import LayerRenderer from '../Layer.js';
import RenderEvent from '../../render/Event.js';
import RenderEventType from '../../render/EventType.js';
import {
apply as applyTransform,
compose as composeTransform,
create as createTransform,
} from '../../transform.js';
import {asArray} from '../../color.js';
import {
containsCoordinate,
getBottomLeft,
getBottomRight,
getTopLeft,
getTopRight,
} from '../../extent.js';
import {createCanvasContext2D} from '../../dom.js';
import {equals} from '../../array.js';
/**
* @type {Array<HTMLCanvasElement>}
*/
export const canvasPool = [];
/**
* @type {CanvasRenderingContext2D}
*/
let pixelContext = null;
function createPixelContext() {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
pixelContext = canvas.getContext('2d');
}
/**
* @abstract
* @template {import("../../layer/Layer.js").default} LayerType
* @extends {LayerRenderer<LayerType>}
*/
class CanvasLayerRenderer extends LayerRenderer {
/**
* @param {LayerType} layer Layer.
*/
constructor(layer) {
super(layer);
/**
* @protected
* @type {HTMLElement}
*/
this.container = null;
/**
* @protected
* @type {number}
*/
this.renderedResolution;
/**
* A temporary transform. The values in this transform should only be used in a
* function that sets the values.
* @protected
* @type {import("../../transform.js").Transform}
*/
this.tempTransform = createTransform();
/**
* The transform for rendered pixels to viewport CSS pixels. This transform must
* be set when rendering a frame and may be used by other functions after rendering.
* @protected
* @type {import("../../transform.js").Transform}
*/
this.pixelTransform = createTransform();
/**
* The transform for viewport CSS pixels to rendered pixels. This transform must
* be set when rendering a frame and may be used by other functions after rendering.
* @protected
* @type {import("../../transform.js").Transform}
*/
this.inversePixelTransform = createTransform();
/**
* @type {CanvasRenderingContext2D}
*/
this.context = null;
/**
* @type {boolean}
*/
this.containerReused = false;
/**
* @private
* @type {CanvasRenderingContext2D}
*/
this.pixelContext_ = null;
/**
* @protected
* @type {import("../../PluggableMap.js").FrameState|null}
*/
this.frameState = null;
}
/**
* @param {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} image Image.
* @param {number} col The column index.
* @param {number} row The row index.
* @return {Uint8ClampedArray|null} The image data.
*/
getImageData(image, col, row) {
if (!pixelContext) {
createPixelContext();
}
pixelContext.clearRect(0, 0, 1, 1);
let data;
try {
pixelContext.drawImage(image, col, row, 1, 1, 0, 0, 1, 1);
data = pixelContext.getImageData(0, 0, 1, 1).data;
} catch (err) {
pixelContext = null;
return null;
}
return data;
}
/**
* @param {import('../../PluggableMap.js').FrameState} frameState Frame state.
* @return {string} Background color.
*/
getBackground(frameState) {
const layer = this.getLayer();
let background = layer.getBackground();
if (typeof background === 'function') {
background = background(frameState.viewState.resolution);
}
return background || undefined;
}
/**
* Get a rendering container from an existing target, if compatible.
* @param {HTMLElement} target Potential render target.
* @param {string} transform CSS Transform.
* @param {string} [opt_backgroundColor] Background color.
*/
useContainer(target, transform, opt_backgroundColor) {
const layerClassName = this.getLayer().getClassName();
let container, context;
if (
target &&
target.className === layerClassName &&
(!opt_backgroundColor ||
(target &&
target.style.backgroundColor &&
equals(
asArray(target.style.backgroundColor),
asArray(opt_backgroundColor)
)))
) {
const canvas = target.firstElementChild;
if (canvas instanceof HTMLCanvasElement) {
context = canvas.getContext('2d');
}
}
if (context && context.canvas.style.transform === transform) {
// Container of the previous layer renderer can be used.
this.container = target;
this.context = context;
this.containerReused = true;
} else if (this.containerReused) {
// Previously reused container cannot be used any more.
this.container = null;
this.context = null;
this.containerReused = false;
}
if (!this.container) {
container = document.createElement('div');
container.className = layerClassName;
let style = container.style;
style.position = 'absolute';
style.width = '100%';
style.height = '100%';
context = createCanvasContext2D();
const canvas = context.canvas;
container.appendChild(canvas);
style = canvas.style;
style.position = 'absolute';
style.left = '0';
style.transformOrigin = 'top left';
this.container = container;
this.context = context;
}
if (
!this.containerReused &&
opt_backgroundColor &&
!this.container.style.backgroundColor
) {
this.container.style.backgroundColor = opt_backgroundColor;
}
}
/**
* @param {CanvasRenderingContext2D} context Context.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {import("../../extent.js").Extent} extent Clip extent.
* @protected
*/
clipUnrotated(context, frameState, extent) {
const topLeft = getTopLeft(extent);
const topRight = getTopRight(extent);
const bottomRight = getBottomRight(extent);
const bottomLeft = getBottomLeft(extent);
applyTransform(frameState.coordinateToPixelTransform, topLeft);
applyTransform(frameState.coordinateToPixelTransform, topRight);
applyTransform(frameState.coordinateToPixelTransform, bottomRight);
applyTransform(frameState.coordinateToPixelTransform, bottomLeft);
const inverted = this.inversePixelTransform;
applyTransform(inverted, topLeft);
applyTransform(inverted, topRight);
applyTransform(inverted, bottomRight);
applyTransform(inverted, bottomLeft);
context.save();
context.beginPath();
context.moveTo(Math.round(topLeft[0]), Math.round(topLeft[1]));
context.lineTo(Math.round(topRight[0]), Math.round(topRight[1]));
context.lineTo(Math.round(bottomRight[0]), Math.round(bottomRight[1]));
context.lineTo(Math.round(bottomLeft[0]), Math.round(bottomLeft[1]));
context.clip();
}
/**
* @param {import("../../render/EventType.js").default} type Event type.
* @param {CanvasRenderingContext2D} context Context.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @private
*/
dispatchRenderEvent_(type, context, frameState) {
const layer = this.getLayer();
if (layer.hasListener(type)) {
const event = new RenderEvent(
type,
this.inversePixelTransform,
frameState,
context
);
layer.dispatchEvent(event);
}
}
/**
* @param {CanvasRenderingContext2D} context Context.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @protected
*/
preRender(context, frameState) {
this.frameState = frameState;
this.dispatchRenderEvent_(RenderEventType.PRERENDER, context, frameState);
}
/**
* @param {CanvasRenderingContext2D} context Context.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @protected
*/
postRender(context, frameState) {
this.dispatchRenderEvent_(RenderEventType.POSTRENDER, context, frameState);
}
/**
* Creates a transform for rendering to an element that will be rotated after rendering.
* @param {import("../../coordinate.js").Coordinate} center Center.
* @param {number} resolution Resolution.
* @param {number} rotation Rotation.
* @param {number} pixelRatio Pixel ratio.
* @param {number} width Width of the rendered element (in pixels).
* @param {number} height Height of the rendered element (in pixels).
* @param {number} offsetX Offset on the x-axis in view coordinates.
* @protected
* @return {!import("../../transform.js").Transform} Transform.
*/
getRenderTransform(
center,
resolution,
rotation,
pixelRatio,
width,
height,
offsetX
) {
const dx1 = width / 2;
const dy1 = height / 2;
const sx = pixelRatio / resolution;
const sy = -sx;
const dx2 = -center[0] + offsetX;
const dy2 = -center[1];
return composeTransform(
this.tempTransform,
dx1,
dy1,
sx,
sy,
-rotation,
dx2,
dy2
);
}
/**
* @param {import("../../pixel.js").Pixel} pixel Pixel.
* @param {import("../../PluggableMap.js").FrameState} frameState FrameState.
* @param {number} hitTolerance Hit tolerance in pixels.
* @return {Uint8ClampedArray|Uint8Array} The result. If there is no data at the pixel
* location, null will be returned. If there is data, but pixel values cannot be
* returned, and empty array will be returned.
*/
getDataAtPixel(pixel, frameState, hitTolerance) {
const renderPixel = applyTransform(
this.inversePixelTransform,
pixel.slice()
);
const context = this.context;
const layer = this.getLayer();
const layerExtent = layer.getExtent();
if (layerExtent) {
const renderCoordinate = applyTransform(
frameState.pixelToCoordinateTransform,
pixel.slice()
);
/** get only data inside of the layer extent */
if (!containsCoordinate(layerExtent, renderCoordinate)) {
return null;
}
}
const x = Math.round(renderPixel[0]);
const y = Math.round(renderPixel[1]);
let pixelContext = this.pixelContext_;
if (!pixelContext) {
const pixelCanvas = document.createElement('canvas');
pixelCanvas.width = 1;
pixelCanvas.height = 1;
pixelContext = pixelCanvas.getContext('2d');
this.pixelContext_ = pixelContext;
}
pixelContext.clearRect(0, 0, 1, 1);
let data;
try {
pixelContext.drawImage(context.canvas, x, y, 1, 1, 0, 0, 1, 1);
data = pixelContext.getImageData(0, 0, 1, 1).data;
} catch (err) {
if (err.name === 'SecurityError') {
// tainted canvas, we assume there is data at the given pixel (although there might not be)
this.pixelContext_ = null;
return new Uint8Array();
}
return data;
}
if (data[3] === 0) {
return null;
}
return data;
}
/**
* Clean up.
*/
disposeInternal() {
delete this.frameState;
super.disposeInternal();
}
}
export default CanvasLayerRenderer;

772
node_modules/ol/src/renderer/canvas/TileLayer.js generated vendored Normal file
View File

@@ -0,0 +1,772 @@
/**
* @module ol/renderer/canvas/TileLayer
*/
import CanvasLayerRenderer from './Layer.js';
import ImageTile from '../../ImageTile.js';
import ReprojTile from '../../reproj/Tile.js';
import TileRange from '../../TileRange.js';
import TileState from '../../TileState.js';
import {IMAGE_SMOOTHING_DISABLED, IMAGE_SMOOTHING_ENABLED} from './common.js';
import {
apply as applyTransform,
compose as composeTransform,
makeInverse,
toString as toTransformString,
} from '../../transform.js';
import {assign} from '../../obj.js';
import {
containsCoordinate,
createEmpty,
equals,
getHeight,
getIntersection,
getRotatedViewport,
getTopLeft,
getWidth,
intersects,
} from '../../extent.js';
import {fromUserExtent} from '../../proj.js';
import {getUid} from '../../util.js';
import {numberSafeCompareFunction} from '../../array.js';
import {toSize} from '../../size.js';
/**
* @classdesc
* Canvas renderer for tile layers.
* @api
* @template {import("../../layer/Tile.js").default<import("../../source/Tile.js").default>|import("../../layer/VectorTile.js").default} [LayerType=import("../../layer/Tile.js").default<import("../../source/Tile.js").default>|import("../../layer/VectorTile.js").default]
* @extends {CanvasLayerRenderer<LayerType>}
*/
class CanvasTileLayerRenderer extends CanvasLayerRenderer {
/**
* @param {LayerType} tileLayer Tile layer.
*/
constructor(tileLayer) {
super(tileLayer);
/**
* Rendered extent has changed since the previous `renderFrame()` call
* @type {boolean}
*/
this.extentChanged = true;
/**
* @private
* @type {?import("../../extent.js").Extent}
*/
this.renderedExtent_ = null;
/**
* @protected
* @type {number}
*/
this.renderedPixelRatio;
/**
* @protected
* @type {import("../../proj/Projection.js").default}
*/
this.renderedProjection = null;
/**
* @protected
* @type {number}
*/
this.renderedRevision;
/**
* @protected
* @type {!Array<import("../../Tile.js").default>}
*/
this.renderedTiles = [];
/**
* @private
* @type {boolean}
*/
this.newTiles_ = false;
/**
* @protected
* @type {import("../../extent.js").Extent}
*/
this.tmpExtent = createEmpty();
/**
* @private
* @type {import("../../TileRange.js").default}
*/
this.tmpTileRange_ = new TileRange(0, 0, 0, 0);
}
/**
* @protected
* @param {import("../../Tile.js").default} tile Tile.
* @return {boolean} Tile is drawable.
*/
isDrawableTile(tile) {
const tileLayer = this.getLayer();
const tileState = tile.getState();
const useInterimTilesOnError = tileLayer.getUseInterimTilesOnError();
return (
tileState == TileState.LOADED ||
tileState == TileState.EMPTY ||
(tileState == TileState.ERROR && !useInterimTilesOnError)
);
}
/**
* @param {number} z Tile coordinate z.
* @param {number} x Tile coordinate x.
* @param {number} y Tile coordinate y.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @return {!import("../../Tile.js").default} Tile.
*/
getTile(z, x, y, frameState) {
const pixelRatio = frameState.pixelRatio;
const projection = frameState.viewState.projection;
const tileLayer = this.getLayer();
const tileSource = tileLayer.getSource();
let tile = tileSource.getTile(z, x, y, pixelRatio, projection);
if (tile.getState() == TileState.ERROR) {
if (!tileLayer.getUseInterimTilesOnError()) {
// When useInterimTilesOnError is false, we consider the error tile as loaded.
tile.setState(TileState.LOADED);
} else if (tileLayer.getPreload() > 0) {
// Preloaded tiles for lower resolutions might have finished loading.
this.newTiles_ = true;
}
}
if (!this.isDrawableTile(tile)) {
tile = tile.getInterimTile();
}
return tile;
}
/**
* @param {import("../../pixel.js").Pixel} pixel Pixel.
* @return {Uint8ClampedArray} Data at the pixel location.
*/
getData(pixel) {
const frameState = this.frameState;
if (!frameState) {
return null;
}
const layer = this.getLayer();
const coordinate = applyTransform(
frameState.pixelToCoordinateTransform,
pixel.slice()
);
const layerExtent = layer.getExtent();
if (layerExtent) {
if (!containsCoordinate(layerExtent, coordinate)) {
return null;
}
}
const pixelRatio = frameState.pixelRatio;
const projection = frameState.viewState.projection;
const viewState = frameState.viewState;
const source = layer.getRenderSource();
const tileGrid = source.getTileGridForProjection(viewState.projection);
const tilePixelRatio = source.getTilePixelRatio(frameState.pixelRatio);
for (
let z = tileGrid.getZForResolution(viewState.resolution);
z >= tileGrid.getMinZoom();
--z
) {
const tileCoord = tileGrid.getTileCoordForCoordAndZ(coordinate, z);
const tile = source.getTile(
z,
tileCoord[1],
tileCoord[2],
pixelRatio,
projection
);
if (!(tile instanceof ImageTile || tile instanceof ReprojTile)) {
return null;
}
if (tile.getState() !== TileState.LOADED) {
continue;
}
const tileOrigin = tileGrid.getOrigin(z);
const tileSize = toSize(tileGrid.getTileSize(z));
const tileResolution = tileGrid.getResolution(z);
const col = Math.floor(
tilePixelRatio *
((coordinate[0] - tileOrigin[0]) / tileResolution -
tileCoord[1] * tileSize[0])
);
const row = Math.floor(
tilePixelRatio *
((tileOrigin[1] - coordinate[1]) / tileResolution -
tileCoord[2] * tileSize[1])
);
const gutter = Math.round(
tilePixelRatio * source.getGutterForProjection(viewState.projection)
);
return this.getImageData(tile.getImage(), col + gutter, row + gutter);
}
return null;
}
/**
* @param {Object<number, Object<string, import("../../Tile.js").default>>} tiles Lookup of loaded tiles by zoom level.
* @param {number} zoom Zoom level.
* @param {import("../../Tile.js").default} tile Tile.
* @return {boolean|void} If `false`, the tile will not be considered loaded.
*/
loadedTileCallback(tiles, zoom, tile) {
if (this.isDrawableTile(tile)) {
return super.loadedTileCallback(tiles, zoom, tile);
}
return false;
}
/**
* Determine whether render should be called.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @return {boolean} Layer is ready to be rendered.
*/
prepareFrame(frameState) {
return !!this.getLayer().getSource();
}
/**
* 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.
*/
renderFrame(frameState, target) {
const layerState = frameState.layerStatesArray[frameState.layerIndex];
const viewState = frameState.viewState;
const projection = viewState.projection;
const viewResolution = viewState.resolution;
const viewCenter = viewState.center;
const rotation = viewState.rotation;
const pixelRatio = frameState.pixelRatio;
const tileLayer = this.getLayer();
const tileSource = tileLayer.getSource();
const sourceRevision = tileSource.getRevision();
const tileGrid = tileSource.getTileGridForProjection(projection);
const z = tileGrid.getZForResolution(viewResolution, tileSource.zDirection);
const tileResolution = tileGrid.getResolution(z);
let extent = frameState.extent;
const resolution = frameState.viewState.resolution;
const tilePixelRatio = tileSource.getTilePixelRatio(pixelRatio);
// desired dimensions of the canvas in pixels
const width = Math.round((getWidth(extent) / resolution) * pixelRatio);
const height = Math.round((getHeight(extent) / resolution) * pixelRatio);
const layerExtent =
layerState.extent && fromUserExtent(layerState.extent, projection);
if (layerExtent) {
extent = getIntersection(
extent,
fromUserExtent(layerState.extent, projection)
);
}
const dx = (tileResolution * width) / 2 / tilePixelRatio;
const dy = (tileResolution * height) / 2 / tilePixelRatio;
const canvasExtent = [
viewCenter[0] - dx,
viewCenter[1] - dy,
viewCenter[0] + dx,
viewCenter[1] + dy,
];
const tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z);
/**
* @type {Object<number, Object<string, import("../../Tile.js").default>>}
*/
const tilesToDrawByZ = {};
tilesToDrawByZ[z] = {};
const findLoadedTiles = this.createLoadedTileFinder(
tileSource,
projection,
tilesToDrawByZ
);
const tmpExtent = this.tmpExtent;
const tmpTileRange = this.tmpTileRange_;
this.newTiles_ = false;
const viewport = rotation
? getRotatedViewport(
viewState.center,
resolution,
rotation,
frameState.size
)
: undefined;
for (let x = tileRange.minX; x <= tileRange.maxX; ++x) {
for (let y = tileRange.minY; y <= tileRange.maxY; ++y) {
if (
rotation &&
!tileGrid.tileCoordIntersectsViewport([z, x, y], viewport)
) {
continue;
}
const tile = this.getTile(z, x, y, frameState);
if (this.isDrawableTile(tile)) {
const uid = getUid(this);
if (tile.getState() == TileState.LOADED) {
tilesToDrawByZ[z][tile.tileCoord.toString()] = tile;
let inTransition = tile.inTransition(uid);
if (inTransition && layerState.opacity !== 1) {
// Skipping transition when layer is not fully opaque avoids visual artifacts.
tile.endTransition(uid);
inTransition = false;
}
if (
!this.newTiles_ &&
(inTransition || this.renderedTiles.indexOf(tile) === -1)
) {
this.newTiles_ = true;
}
}
if (tile.getAlpha(uid, frameState.time) === 1) {
// don't look for alt tiles if alpha is 1
continue;
}
}
const childTileRange = tileGrid.getTileCoordChildTileRange(
tile.tileCoord,
tmpTileRange,
tmpExtent
);
let covered = false;
if (childTileRange) {
covered = findLoadedTiles(z + 1, childTileRange);
}
if (!covered) {
tileGrid.forEachTileCoordParentTileRange(
tile.tileCoord,
findLoadedTiles,
tmpTileRange,
tmpExtent
);
}
}
}
const canvasScale =
((tileResolution / viewResolution) * pixelRatio) / tilePixelRatio;
// set forward and inverse pixel transforms
composeTransform(
this.pixelTransform,
frameState.size[0] / 2,
frameState.size[1] / 2,
1 / pixelRatio,
1 / pixelRatio,
rotation,
-width / 2,
-height / 2
);
const canvasTransform = toTransformString(this.pixelTransform);
this.useContainer(target, canvasTransform, this.getBackground(frameState));
const context = this.context;
const canvas = context.canvas;
makeInverse(this.inversePixelTransform, this.pixelTransform);
// set scale transform for calculating tile positions on the canvas
composeTransform(
this.tempTransform,
width / 2,
height / 2,
canvasScale,
canvasScale,
0,
-width / 2,
-height / 2
);
if (canvas.width != width || canvas.height != height) {
canvas.width = width;
canvas.height = height;
} else if (!this.containerReused) {
context.clearRect(0, 0, width, height);
}
if (layerExtent) {
this.clipUnrotated(context, frameState, layerExtent);
}
if (!tileSource.getInterpolate()) {
assign(context, IMAGE_SMOOTHING_DISABLED);
}
this.preRender(context, frameState);
this.renderedTiles.length = 0;
/** @type {Array<number>} */
let zs = Object.keys(tilesToDrawByZ).map(Number);
zs.sort(numberSafeCompareFunction);
let clips, clipZs, currentClip;
if (
layerState.opacity === 1 &&
(!this.containerReused ||
tileSource.getOpaque(frameState.viewState.projection))
) {
zs = zs.reverse();
} else {
clips = [];
clipZs = [];
}
for (let i = zs.length - 1; i >= 0; --i) {
const currentZ = zs[i];
const currentTilePixelSize = tileSource.getTilePixelSize(
currentZ,
pixelRatio,
projection
);
const currentResolution = tileGrid.getResolution(currentZ);
const currentScale = currentResolution / tileResolution;
const dx = currentTilePixelSize[0] * currentScale * canvasScale;
const dy = currentTilePixelSize[1] * currentScale * canvasScale;
const originTileCoord = tileGrid.getTileCoordForCoordAndZ(
getTopLeft(canvasExtent),
currentZ
);
const originTileExtent = tileGrid.getTileCoordExtent(originTileCoord);
const origin = applyTransform(this.tempTransform, [
(tilePixelRatio * (originTileExtent[0] - canvasExtent[0])) /
tileResolution,
(tilePixelRatio * (canvasExtent[3] - originTileExtent[3])) /
tileResolution,
]);
const tileGutter =
tilePixelRatio * tileSource.getGutterForProjection(projection);
const tilesToDraw = tilesToDrawByZ[currentZ];
for (const tileCoordKey in tilesToDraw) {
const tile = /** @type {import("../../ImageTile.js").default} */ (
tilesToDraw[tileCoordKey]
);
const tileCoord = tile.tileCoord;
// Calculate integer positions and sizes so that tiles align
const xIndex = originTileCoord[1] - tileCoord[1];
const nextX = Math.round(origin[0] - (xIndex - 1) * dx);
const yIndex = originTileCoord[2] - tileCoord[2];
const nextY = Math.round(origin[1] - (yIndex - 1) * dy);
const x = Math.round(origin[0] - xIndex * dx);
const y = Math.round(origin[1] - yIndex * dy);
const w = nextX - x;
const h = nextY - y;
const transition = z === currentZ;
const inTransition =
transition && tile.getAlpha(getUid(this), frameState.time) !== 1;
let contextSaved = false;
if (!inTransition) {
if (clips) {
// Clip mask for regions in this tile that already filled by a higher z tile
currentClip = [x, y, x + w, y, x + w, y + h, x, y + h];
for (let i = 0, ii = clips.length; i < ii; ++i) {
if (z !== currentZ && currentZ < clipZs[i]) {
const clip = clips[i];
if (
intersects(
[x, y, x + w, y + h],
[clip[0], clip[3], clip[4], clip[7]]
)
) {
if (!contextSaved) {
context.save();
contextSaved = true;
}
context.beginPath();
// counter-clockwise (outer ring) for current tile
context.moveTo(currentClip[0], currentClip[1]);
context.lineTo(currentClip[2], currentClip[3]);
context.lineTo(currentClip[4], currentClip[5]);
context.lineTo(currentClip[6], currentClip[7]);
// clockwise (inner ring) for higher z tile
context.moveTo(clip[6], clip[7]);
context.lineTo(clip[4], clip[5]);
context.lineTo(clip[2], clip[3]);
context.lineTo(clip[0], clip[1]);
context.clip();
}
}
}
clips.push(currentClip);
clipZs.push(currentZ);
} else {
context.clearRect(x, y, w, h);
}
}
this.drawTileImage(
tile,
frameState,
x,
y,
w,
h,
tileGutter,
transition
);
if (clips && !inTransition) {
if (contextSaved) {
context.restore();
}
this.renderedTiles.unshift(tile);
} else {
this.renderedTiles.push(tile);
}
this.updateUsedTiles(frameState.usedTiles, tileSource, tile);
}
}
this.renderedRevision = sourceRevision;
this.renderedResolution = tileResolution;
this.extentChanged =
!this.renderedExtent_ || !equals(this.renderedExtent_, canvasExtent);
this.renderedExtent_ = canvasExtent;
this.renderedPixelRatio = pixelRatio;
this.renderedProjection = projection;
this.manageTilePyramid(
frameState,
tileSource,
tileGrid,
pixelRatio,
projection,
extent,
z,
tileLayer.getPreload()
);
this.scheduleExpireCache(frameState, tileSource);
this.postRender(context, frameState);
if (layerState.extent) {
context.restore();
}
assign(context, IMAGE_SMOOTHING_ENABLED);
if (canvasTransform !== canvas.style.transform) {
canvas.style.transform = canvasTransform;
}
return this.container;
}
/**
* @param {import("../../ImageTile.js").default} tile Tile.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {number} x Left of the tile.
* @param {number} y Top of the tile.
* @param {number} w Width of the tile.
* @param {number} h Height of the tile.
* @param {number} gutter Tile gutter.
* @param {boolean} transition Apply an alpha transition.
*/
drawTileImage(tile, frameState, x, y, w, h, gutter, transition) {
const image = this.getTileImage(tile);
if (!image) {
return;
}
const uid = getUid(this);
const layerState = frameState.layerStatesArray[frameState.layerIndex];
const alpha =
layerState.opacity *
(transition ? tile.getAlpha(uid, frameState.time) : 1);
const alphaChanged = alpha !== this.context.globalAlpha;
if (alphaChanged) {
this.context.save();
this.context.globalAlpha = alpha;
}
this.context.drawImage(
image,
gutter,
gutter,
image.width - 2 * gutter,
image.height - 2 * gutter,
x,
y,
w,
h
);
if (alphaChanged) {
this.context.restore();
}
if (alpha !== layerState.opacity) {
frameState.animate = true;
} else if (transition) {
tile.endTransition(uid);
}
}
/**
* @return {HTMLCanvasElement} Image
*/
getImage() {
const context = this.context;
return context ? context.canvas : null;
}
/**
* Get the image from a tile.
* @param {import("../../ImageTile.js").default} tile Tile.
* @return {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} Image.
* @protected
*/
getTileImage(tile) {
return tile.getImage();
}
/**
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {import("../../source/Tile.js").default} tileSource Tile source.
* @protected
*/
scheduleExpireCache(frameState, tileSource) {
if (tileSource.canExpireCache()) {
/**
* @param {import("../../source/Tile.js").default} tileSource Tile source.
* @param {import("../../PluggableMap.js").default} map Map.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
*/
const postRenderFunction = function (tileSource, map, frameState) {
const tileSourceKey = getUid(tileSource);
if (tileSourceKey in frameState.usedTiles) {
tileSource.expireCache(
frameState.viewState.projection,
frameState.usedTiles[tileSourceKey]
);
}
}.bind(null, tileSource);
frameState.postRenderFunctions.push(
/** @type {import("../../PluggableMap.js").PostRenderFunction} */ (
postRenderFunction
)
);
}
}
/**
* @param {!Object<string, !Object<string, boolean>>} usedTiles Used tiles.
* @param {import("../../source/Tile.js").default} tileSource Tile source.
* @param {import('../../Tile.js').default} tile Tile.
* @protected
*/
updateUsedTiles(usedTiles, tileSource, tile) {
// FIXME should we use tilesToDrawByZ instead?
const tileSourceKey = getUid(tileSource);
if (!(tileSourceKey in usedTiles)) {
usedTiles[tileSourceKey] = {};
}
usedTiles[tileSourceKey][tile.getKey()] = true;
}
/**
* Manage tile pyramid.
* This function performs a number of functions related to the tiles at the
* current zoom and lower zoom levels:
* - registers idle tiles in frameState.wantedTiles so that they are not
* discarded by the tile queue
* - enqueues missing tiles
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {import("../../source/Tile.js").default} tileSource Tile source.
* @param {import("../../tilegrid/TileGrid.js").default} tileGrid Tile grid.
* @param {number} pixelRatio Pixel ratio.
* @param {import("../../proj/Projection.js").default} projection Projection.
* @param {import("../../extent.js").Extent} extent Extent.
* @param {number} currentZ Current Z.
* @param {number} preload Load low resolution tiles up to `preload` levels.
* @param {function(import("../../Tile.js").default):void} [opt_tileCallback] Tile callback.
* @protected
*/
manageTilePyramid(
frameState,
tileSource,
tileGrid,
pixelRatio,
projection,
extent,
currentZ,
preload,
opt_tileCallback
) {
const tileSourceKey = getUid(tileSource);
if (!(tileSourceKey in frameState.wantedTiles)) {
frameState.wantedTiles[tileSourceKey] = {};
}
const wantedTiles = frameState.wantedTiles[tileSourceKey];
const tileQueue = frameState.tileQueue;
const minZoom = tileGrid.getMinZoom();
const rotation = frameState.viewState.rotation;
const viewport = rotation
? getRotatedViewport(
frameState.viewState.center,
frameState.viewState.resolution,
rotation,
frameState.size
)
: undefined;
let tileCount = 0;
let tile, tileRange, tileResolution, x, y, z;
for (z = minZoom; z <= currentZ; ++z) {
tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z, tileRange);
tileResolution = tileGrid.getResolution(z);
for (x = tileRange.minX; x <= tileRange.maxX; ++x) {
for (y = tileRange.minY; y <= tileRange.maxY; ++y) {
if (
rotation &&
!tileGrid.tileCoordIntersectsViewport([z, x, y], viewport)
) {
continue;
}
if (currentZ - z <= preload) {
++tileCount;
tile = tileSource.getTile(z, x, y, pixelRatio, projection);
if (tile.getState() == TileState.IDLE) {
wantedTiles[tile.getKey()] = true;
if (!tileQueue.isKeyQueued(tile.getKey())) {
tileQueue.enqueue([
tile,
tileSourceKey,
tileGrid.getTileCoordCenter(tile.tileCoord),
tileResolution,
]);
}
}
if (opt_tileCallback !== undefined) {
opt_tileCallback(tile);
}
} else {
tileSource.useTile(z, x, y, projection);
}
}
}
}
tileSource.updateCacheSize(tileCount, projection);
}
}
export default CanvasTileLayerRenderer;

232
node_modules/ol/src/renderer/canvas/VectorImageLayer.js generated vendored Normal file
View File

@@ -0,0 +1,232 @@
/**
* @module ol/renderer/canvas/VectorImageLayer
*/
import CanvasImageLayerRenderer from './ImageLayer.js';
import CanvasVectorLayerRenderer from './VectorLayer.js';
import EventType from '../../events/EventType.js';
import ImageCanvas from '../../ImageCanvas.js';
import ImageState from '../../ImageState.js';
import RBush from 'rbush';
import ViewHint from '../../ViewHint.js';
import {apply, compose, create} from '../../transform.js';
import {assign} from '../../obj.js';
import {getHeight, getWidth, isEmpty, scaleFromCenter} from '../../extent.js';
/**
* @classdesc
* Canvas renderer for image layers.
* @api
*/
class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer {
/**
* @param {import("../../layer/VectorImage.js").default} layer Vector image layer.
*/
constructor(layer) {
super(layer);
/**
* @private
* @type {import("./VectorLayer.js").default}
*/
this.vectorRenderer_ = new CanvasVectorLayerRenderer(layer);
/**
* @private
* @type {number}
*/
this.layerImageRatio_ = layer.getImageRatio();
/**
* @private
* @type {import("../../transform.js").Transform}
*/
this.coordinateToVectorPixelTransform_ = create();
/**
* @private
* @type {import("../../transform.js").Transform}
*/
this.renderedPixelToCoordinateTransform_ = null;
}
/**
* Clean up.
*/
disposeInternal() {
this.vectorRenderer_.dispose();
super.disposeInternal();
}
/**
* Asynchronous layer level hit detection.
* @param {import("../../pixel.js").Pixel} pixel Pixel.
* @return {Promise<Array<import("../../Feature").default>>} Promise that resolves with an array of features.
*/
getFeatures(pixel) {
if (!this.vectorRenderer_) {
return new Promise((resolve) => resolve([]));
}
const vectorPixel = apply(
this.coordinateToVectorPixelTransform_,
apply(this.renderedPixelToCoordinateTransform_, pixel.slice())
);
return this.vectorRenderer_.getFeatures(vectorPixel);
}
/**
* Perform action necessary to get the layer rendered after new fonts have loaded
*/
handleFontsChanged() {
this.vectorRenderer_.handleFontsChanged();
}
/**
* Determine whether render should be called.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @return {boolean} Layer is ready to be rendered.
*/
prepareFrame(frameState) {
const pixelRatio = frameState.pixelRatio;
const viewState = frameState.viewState;
const viewResolution = viewState.resolution;
const hints = frameState.viewHints;
const vectorRenderer = this.vectorRenderer_;
let renderedExtent = frameState.extent;
if (this.layerImageRatio_ !== 1) {
renderedExtent = renderedExtent.slice(0);
scaleFromCenter(renderedExtent, this.layerImageRatio_);
}
const width = getWidth(renderedExtent) / viewResolution;
const height = getHeight(renderedExtent) / viewResolution;
if (
!hints[ViewHint.ANIMATING] &&
!hints[ViewHint.INTERACTING] &&
!isEmpty(renderedExtent)
) {
vectorRenderer.useContainer(null, null);
const context = vectorRenderer.context;
const layerState = frameState.layerStatesArray[frameState.layerIndex];
context.globalAlpha = layerState.opacity;
const imageLayerState = assign({}, layerState, {opacity: 1});
const imageFrameState =
/** @type {import("../../PluggableMap.js").FrameState} */ (
assign({}, frameState, {
declutterTree: new RBush(9),
extent: renderedExtent,
size: [width, height],
viewState: /** @type {import("../../View.js").State} */ (
assign({}, frameState.viewState, {
rotation: 0,
})
),
layerStatesArray: [imageLayerState],
layerIndex: 0,
})
);
let emptyImage = true;
const image = new ImageCanvas(
renderedExtent,
viewResolution,
pixelRatio,
context.canvas,
function (callback) {
if (
vectorRenderer.prepareFrame(imageFrameState) &&
vectorRenderer.replayGroupChanged
) {
vectorRenderer.clipping = false;
if (vectorRenderer.renderFrame(imageFrameState, null)) {
vectorRenderer.renderDeclutter(imageFrameState);
emptyImage = false;
}
callback();
}
}
);
image.addEventListener(
EventType.CHANGE,
function () {
if (image.getState() !== ImageState.LOADED) {
return;
}
this.image_ = emptyImage ? null : image;
const imageResolution = image.getResolution();
const imagePixelRatio = image.getPixelRatio();
const renderedResolution =
(imageResolution * pixelRatio) / imagePixelRatio;
this.renderedResolution = renderedResolution;
this.coordinateToVectorPixelTransform_ = compose(
this.coordinateToVectorPixelTransform_,
width / 2,
height / 2,
1 / renderedResolution,
-1 / renderedResolution,
0,
-viewState.center[0],
-viewState.center[1]
);
}.bind(this)
);
image.load();
}
if (this.image_) {
this.renderedPixelToCoordinateTransform_ =
frameState.pixelToCoordinateTransform.slice();
}
return !!this.image_;
}
/**
*/
preRender() {}
/**
*/
postRender() {}
/**
*/
renderDeclutter() {}
/**
* @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<T>} callback Feature callback.
* @param {Array<import("../Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
* @return {T|undefined} Callback result.
* @template T
*/
forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
) {
if (this.vectorRenderer_) {
return this.vectorRenderer_.forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
);
} else {
return super.forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
);
}
}
}
export default CanvasVectorImageLayerRenderer;

800
node_modules/ol/src/renderer/canvas/VectorLayer.js generated vendored Normal file
View File

@@ -0,0 +1,800 @@
/**
* @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
*/
class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
/**
* @param {import("../../layer/BaseVector.js").default} vectorLayer Vector layer.
*/
constructor(vectorLayer) {
super(vectorLayer);
/** @private */
this.boundHandleStyleImageChange_ = this.handleStyleImageChange_.bind(this);
/**
* @type {boolean}
*/
this.animatingOrInteracting_;
/**
* @type {ImageData}
*/
this.hitDetectionImageData_ = null;
/**
* @type {Array<import("../../Feature.js").default>}
*/
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;
}
/**
* @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.compositionContext_;
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);
}
setupCompositionContext_() {
if (this.opacity_ !== 1) {
const compositionContext = createCanvasContext2D(
this.context.canvas.width,
this.context.canvas.height,
canvasPool
);
this.compositionContext_ = compositionContext;
} else {
this.compositionContext_ = this.context;
}
}
releaseCompositionContext_() {
if (this.opacity_ !== 1) {
const 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.
*/
renderDeclutter(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.
*/
renderFrame(frameState, target) {
const pixelRatio = frameState.pixelRatio;
const layerState = frameState.layerStatesArray[frameState.layerIndex];
// set forward and inverse pixel transforms
makeScale(this.pixelTransform, 1 / pixelRatio, 1 / pixelRatio);
makeInverse(this.inversePixelTransform, this.pixelTransform);
const canvasTransform = transformToString(this.pixelTransform);
this.useContainer(target, canvasTransform, this.getBackground(frameState));
const context = this.context;
const canvas = context.canvas;
const replayGroup = this.replayGroup_;
const declutterExecutorGroup = this.declutterExecutorGroup;
if (
(!replayGroup || replayGroup.isEmpty()) &&
(!declutterExecutorGroup || declutterExecutorGroup.isEmpty())
) {
return null;
}
// resize and clear
const width = Math.round(frameState.size[0] * pixelRatio);
const 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);
const viewState = frameState.viewState;
const projection = viewState.projection;
this.opacity_ = layerState.opacity;
this.setupCompositionContext_();
// clipped rendering if layer extent is set
let clipped = false;
let render = true;
if (layerState.extent && this.clipping) {
const 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<Array<import("../../Feature").default>>} Promise that resolves with an array of features.
*/
getFeatures(pixel) {
return new Promise(
/**
* @param {function(Array<import("../../Feature").default|import("../../render/Feature").default>): void} resolve Resolver function.
* @this {CanvasVectorLayerRenderer}
*/
function (resolve) {
if (!this.hitDetectionImageData_ && !this.animatingOrInteracting_) {
const size = [this.context.canvas.width, this.context.canvas.height];
apply(this.pixelTransform, size);
const center = this.renderedCenter_;
const resolution = this.renderedResolution_;
const rotation = this.renderedRotation_;
const projection = this.renderedProjection_;
const extent = this.wrappedRenderedExtent_;
const layer = this.getLayer();
const transforms = [];
const width = size[0] * HIT_DETECT_RESOLUTION;
const height = size[1] * HIT_DETECT_RESOLUTION;
transforms.push(
this.getRenderTransform(
center,
resolution,
rotation,
HIT_DETECT_RESOLUTION,
width,
height,
0
).slice()
);
const source = layer.getSource();
const projectionExtent = projection.getExtent();
if (
source.getWrapX() &&
projection.canWrapX() &&
!containsExtent(projectionExtent, extent)
) {
let startX = extent[0];
const worldWidth = getWidth(projectionExtent);
let world = 0;
let offsetX;
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<T>} callback Feature callback.
* @param {Array<import("../Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
* @return {T|undefined} Callback result.
* @template T
*/
forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
) {
if (!this.replayGroup_) {
return undefined;
}
const resolution = frameState.viewState.resolution;
const rotation = frameState.viewState.rotation;
const layer = this.getLayer();
/** @type {!Object<string, import("../Map.js").HitMatch<T>|true>} */
const 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.
*/
const featureCallback = function (feature, geometry, distanceSq) {
const key = getUid(feature);
const 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;
};
let result;
const executorGroups = [this.replayGroup_];
if (this.declutterExecutorGroup) {
executorGroups.push(this.declutterExecutorGroup);
}
executorGroups.some((executorGroup) => {
return (result = executorGroup.forEachFeatureAtCoordinate(
coordinate,
resolution,
rotation,
hitTolerance,
featureCallback,
executorGroup === this.declutterExecutorGroup &&
frameState.declutterTree
? frameState.declutterTree.all().map((item) => item.value)
: null
));
});
return result;
}
/**
* Perform action necessary to get the layer rendered after new fonts have loaded
*/
handleFontsChanged() {
const 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
*/
handleStyleImageChange_(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.
*/
prepareFrame(frameState) {
const vectorLayer = this.getLayer();
const vectorSource = vectorLayer.getSource();
if (!vectorSource) {
return false;
}
const animating = frameState.viewHints[ViewHint.ANIMATING];
const interacting = frameState.viewHints[ViewHint.INTERACTING];
const updateWhileAnimating = vectorLayer.getUpdateWhileAnimating();
const updateWhileInteracting = vectorLayer.getUpdateWhileInteracting();
if (
(this.ready && !updateWhileAnimating && animating) ||
(!updateWhileInteracting && interacting)
) {
this.animatingOrInteracting_ = true;
return true;
}
this.animatingOrInteracting_ = false;
const frameStateExtent = frameState.extent;
const viewState = frameState.viewState;
const projection = viewState.projection;
const resolution = viewState.resolution;
const pixelRatio = frameState.pixelRatio;
const vectorLayerRevision = vectorLayer.getRevision();
const vectorLayerRenderBuffer = vectorLayer.getRenderBuffer();
let vectorLayerRenderOrder = vectorLayer.getRenderOrder();
if (vectorLayerRenderOrder === undefined) {
vectorLayerRenderOrder = defaultRenderOrder;
}
const center = viewState.center.slice();
const extent = buffer(
frameStateExtent,
vectorLayerRenderBuffer * resolution
);
const renderedExtent = extent.slice();
const loadExtents = [extent.slice()];
const 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.
const worldWidth = getWidth(projectionExtent);
const gutter = Math.max(getWidth(extent) / 2, worldWidth);
extent[0] = projectionExtent[0] - gutter;
extent[2] = projectionExtent[2] + gutter;
wrapCoordinateX(center, projection);
const 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;
const replayGroup = new CanvasBuilderGroup(
getRenderTolerance(resolution, pixelRatio),
extent,
resolution,
pixelRatio
);
let declutterBuilderGroup;
if (this.getLayer().getDeclutter()) {
declutterBuilderGroup = new CanvasBuilderGroup(
getRenderTolerance(resolution, pixelRatio),
extent,
resolution,
pixelRatio
);
}
const userProjection = getUserProjection();
let userTransform;
if (userProjection) {
for (let i = 0, ii = loadExtents.length; i < ii; ++i) {
const extent = loadExtents[i];
const userExtent = toUserExtent(extent, projection);
vectorSource.loadFeatures(
userExtent,
toUserResolution(resolution, projection),
userProjection
);
}
userTransform = getTransformFromProjections(userProjection, projection);
} else {
for (let i = 0, ii = loadExtents.length; i < ii; ++i) {
vectorSource.loadFeatures(loadExtents[i], resolution, projection);
}
}
const squaredTolerance = getSquaredRenderTolerance(resolution, pixelRatio);
let ready = true;
const render =
/**
* @param {import("../../Feature.js").default} feature Feature.
* @this {CanvasVectorLayerRenderer}
*/
function (feature) {
let styles;
const styleFunction =
feature.getStyleFunction() || vectorLayer.getStyleFunction();
if (styleFunction) {
styles = styleFunction(feature, resolution);
}
if (styles) {
const dirty = this.renderFeature(
feature,
squaredTolerance,
styles,
replayGroup,
userTransform,
declutterBuilderGroup
);
ready = ready && !dirty;
}
}.bind(this);
const userExtent = toUserExtent(extent, projection);
/** @type {Array<import("../../Feature.js").default>} */
const features = vectorSource.getFeaturesInExtent(userExtent);
if (vectorLayerRenderOrder) {
features.sort(vectorLayerRenderOrder);
}
for (let i = 0, ii = features.length; i < ii; ++i) {
render(features[i]);
}
this.renderedFeatures_ = features;
this.ready = ready;
const replayGroupInstructions = replayGroup.finish();
const 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<import("../../style/Style.js").default>} 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.
*/
renderFeature(
feature,
squaredTolerance,
styles,
builderGroup,
opt_transform,
opt_declutterBuilderGroup
) {
if (!styles) {
return false;
}
let loading = false;
if (Array.isArray(styles)) {
for (let 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;
}
}
export default CanvasVectorLayerRenderer;

859
node_modules/ol/src/renderer/canvas/VectorTileLayer.js generated vendored Normal file
View File

@@ -0,0 +1,859 @@
/**
* @module ol/renderer/canvas/VectorTileLayer
*/
import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js';
import CanvasExecutorGroup from '../../render/canvas/ExecutorGroup.js';
import CanvasTileLayerRenderer from './TileLayer.js';
import TileState from '../../TileState.js';
import VectorTileRenderType from '../../layer/VectorTileRenderType.js';
import ViewHint from '../../ViewHint.js';
import {
HIT_DETECT_RESOLUTION,
createHitDetectionImageData,
hitDetect,
} from '../../render/canvas/hitdetect.js';
import {
apply as applyTransform,
create as createTransform,
multiply,
reset as resetTransform,
scale,
scale as scaleTransform,
translate as translateTransform,
} from '../../transform.js';
import {
boundingExtent,
buffer,
containsExtent,
equals,
getIntersection,
getTopLeft,
intersects,
} from '../../extent.js';
import {
getSquaredTolerance as getSquaredRenderTolerance,
renderFeature,
} from '../vector.js';
import {getUid} from '../../util.js';
import {toSize} from '../../size.js';
import {wrapX} from '../../coordinate.js';
/**
* @type {!Object<string, Array<import("../../render/canvas.js").BuilderType>>}
*/
const IMAGE_REPLAYS = {
'image': ['Polygon', 'Circle', 'LineString', 'Image', 'Text'],
'hybrid': ['Polygon', 'LineString'],
'vector': [],
};
/**
* @type {!Object<string, Array<import("../../render/canvas.js").BuilderType>>}
*/
const VECTOR_REPLAYS = {
'hybrid': ['Image', 'Text', 'Default'],
'vector': ['Polygon', 'Circle', 'LineString', 'Image', 'Text', 'Default'],
};
/**
* @classdesc
* Canvas renderer for vector tile layers.
* @api
* @extends {CanvasTileLayerRenderer<import("../../layer/VectorTile.js").default>}
*/
class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
/**
* @param {import("../../layer/VectorTile.js").default} layer VectorTile layer.
*/
constructor(layer) {
super(layer);
/** @private */
this.boundHandleStyleImageChange_ = this.handleStyleImageChange_.bind(this);
/**
* @private
* @type {number}
*/
this.renderedLayerRevision_;
/**
* @private
* @type {import("../../transform").Transform}
*/
this.renderedPixelToCoordinateTransform_ = null;
/**
* @private
* @type {number}
*/
this.renderedRotation_;
/**
* @private
* @type {import("../../transform.js").Transform}
*/
this.tmpTransform_ = createTransform();
}
/**
* @param {import("../../VectorRenderTile.js").default} tile Tile.
* @param {number} pixelRatio Pixel ratio.
* @param {import("../../proj/Projection").default} projection Projection.
* @return {boolean|undefined} Tile needs to be rendered.
*/
prepareTile(tile, pixelRatio, projection) {
let render;
const state = tile.getState();
if (state === TileState.LOADED || state === TileState.ERROR) {
this.updateExecutorGroup_(tile, pixelRatio, projection);
if (this.tileImageNeedsRender_(tile)) {
render = true;
}
}
return render;
}
/**
* @param {number} z Tile coordinate z.
* @param {number} x Tile coordinate x.
* @param {number} y Tile coordinate y.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @return {!import("../../Tile.js").default} Tile.
*/
getTile(z, x, y, frameState) {
const pixelRatio = frameState.pixelRatio;
const viewState = frameState.viewState;
const resolution = viewState.resolution;
const projection = viewState.projection;
const layer = this.getLayer();
const tile = layer.getSource().getTile(z, x, y, pixelRatio, projection);
const viewHints = frameState.viewHints;
const hifi = !(
viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]
);
if (hifi || !tile.wantedResolution) {
tile.wantedResolution = resolution;
}
const render = this.prepareTile(tile, pixelRatio, projection);
if (
render &&
(hifi || Date.now() - frameState.time < 8) &&
layer.getRenderMode() !== VectorTileRenderType.VECTOR
) {
this.renderTileImage_(tile, frameState);
}
return super.getTile(z, x, y, frameState);
}
/**
* @param {import("../../VectorRenderTile.js").default} tile Tile.
* @return {boolean} Tile is drawable.
*/
isDrawableTile(tile) {
const layer = this.getLayer();
return (
super.isDrawableTile(tile) &&
(layer.getRenderMode() === VectorTileRenderType.VECTOR
? getUid(layer) in tile.executorGroups
: tile.hasContext(layer))
);
}
/**
* @inheritDoc
*/
getTileImage(tile) {
return tile.getImage(this.getLayer());
}
/**
* Determine whether render should be called.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @return {boolean} Layer is ready to be rendered.
*/
prepareFrame(frameState) {
const layerRevision = this.getLayer().getRevision();
if (this.renderedLayerRevision_ !== layerRevision) {
this.renderedLayerRevision_ = layerRevision;
this.renderedTiles.length = 0;
}
return super.prepareFrame(frameState);
}
/**
* @param {import("../../VectorRenderTile.js").default} tile Tile.
* @param {number} pixelRatio Pixel ratio.
* @param {import("../../proj/Projection.js").default} projection Projection.
* @private
*/
updateExecutorGroup_(tile, pixelRatio, projection) {
const layer = /** @type {import("../../layer/VectorTile.js").default} */ (
this.getLayer()
);
const revision = layer.getRevision();
const renderOrder = layer.getRenderOrder() || null;
const resolution = tile.wantedResolution;
const builderState = tile.getReplayState(layer);
if (
!builderState.dirty &&
builderState.renderedResolution === resolution &&
builderState.renderedRevision == revision &&
builderState.renderedRenderOrder == renderOrder
) {
return;
}
const source = layer.getSource();
const declutter = layer.getDeclutter();
const sourceTileGrid = source.getTileGrid();
const tileGrid = source.getTileGridForProjection(projection);
const tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord);
const sourceTiles = source.getSourceTiles(pixelRatio, projection, tile);
const layerUid = getUid(layer);
delete tile.hitDetectionImageData[layerUid];
tile.executorGroups[layerUid] = [];
if (declutter) {
tile.declutterExecutorGroups[layerUid] = [];
}
builderState.dirty = false;
for (let t = 0, tt = sourceTiles.length; t < tt; ++t) {
const sourceTile = sourceTiles[t];
if (sourceTile.getState() != TileState.LOADED) {
continue;
}
const sourceTileCoord = sourceTile.tileCoord;
const sourceTileExtent =
sourceTileGrid.getTileCoordExtent(sourceTileCoord);
const sharedExtent = getIntersection(tileExtent, sourceTileExtent);
const builderExtent = buffer(
sharedExtent,
layer.getRenderBuffer() * resolution,
this.tmpExtent
);
const bufferedExtent = equals(sourceTileExtent, sharedExtent)
? null
: builderExtent;
const builderGroup = new CanvasBuilderGroup(
0,
builderExtent,
resolution,
pixelRatio
);
const declutterBuilderGroup = declutter
? new CanvasBuilderGroup(0, sharedExtent, resolution, pixelRatio)
: undefined;
const squaredTolerance = getSquaredRenderTolerance(
resolution,
pixelRatio
);
/**
* @param {import("../../Feature.js").FeatureLike} feature Feature.
* @this {CanvasVectorTileLayerRenderer}
*/
const render = function (feature) {
let styles;
const styleFunction =
feature.getStyleFunction() || layer.getStyleFunction();
if (styleFunction) {
styles = styleFunction(feature, resolution);
}
if (styles) {
const dirty = this.renderFeature(
feature,
squaredTolerance,
styles,
builderGroup,
declutterBuilderGroup
);
builderState.dirty = builderState.dirty || dirty;
}
};
const features = sourceTile.getFeatures();
if (renderOrder && renderOrder !== builderState.renderedRenderOrder) {
features.sort(renderOrder);
}
for (let i = 0, ii = features.length; i < ii; ++i) {
const feature = features[i];
if (
!bufferedExtent ||
intersects(bufferedExtent, feature.getGeometry().getExtent())
) {
render.call(this, feature);
}
}
const executorGroupInstructions = builderGroup.finish();
// no need to clip when the render tile is covered by a single source tile
const replayExtent =
layer.getRenderMode() !== VectorTileRenderType.VECTOR &&
declutter &&
sourceTiles.length === 1
? null
: sharedExtent;
const renderingReplayGroup = new CanvasExecutorGroup(
replayExtent,
resolution,
pixelRatio,
source.getOverlaps(),
executorGroupInstructions,
layer.getRenderBuffer()
);
tile.executorGroups[layerUid].push(renderingReplayGroup);
if (declutterBuilderGroup) {
const declutterExecutorGroup = new CanvasExecutorGroup(
null,
resolution,
pixelRatio,
source.getOverlaps(),
declutterBuilderGroup.finish(),
layer.getRenderBuffer()
);
tile.declutterExecutorGroups[layerUid].push(declutterExecutorGroup);
}
}
builderState.renderedRevision = revision;
builderState.renderedRenderOrder = renderOrder;
builderState.renderedResolution = resolution;
}
/**
* @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<T>} callback Feature callback.
* @param {Array<import("../Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
* @return {T|undefined} Callback result.
* @template T
*/
forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
) {
const resolution = frameState.viewState.resolution;
const rotation = frameState.viewState.rotation;
hitTolerance = hitTolerance == undefined ? 0 : hitTolerance;
const layer = this.getLayer();
const source = layer.getSource();
const tileGrid = source.getTileGridForProjection(
frameState.viewState.projection
);
const hitExtent = boundingExtent([coordinate]);
buffer(hitExtent, resolution * hitTolerance, hitExtent);
/** @type {!Object<string, import("../Map.js").HitMatch<T>|true>} */
const 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.
*/
const featureCallback = function (feature, geometry, distanceSq) {
let key = feature.getId();
if (key === undefined) {
key = getUid(feature);
}
const 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;
};
const renderedTiles =
/** @type {Array<import("../../VectorRenderTile.js").default>} */ (
this.renderedTiles
);
let found;
for (let i = 0, ii = renderedTiles.length; !found && i < ii; ++i) {
const tile = renderedTiles[i];
const tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord);
if (!intersects(tileExtent, hitExtent)) {
continue;
}
const layerUid = getUid(layer);
const executorGroups = [tile.executorGroups[layerUid]];
const declutterExecutorGroups = tile.declutterExecutorGroups[layerUid];
if (declutterExecutorGroups) {
executorGroups.push(declutterExecutorGroups);
}
executorGroups.some((executorGroups) => {
const declutteredFeatures =
executorGroups === declutterExecutorGroups
? frameState.declutterTree.all().map((item) => item.value)
: null;
for (let t = 0, tt = executorGroups.length; t < tt; ++t) {
const executorGroup = executorGroups[t];
found = executorGroup.forEachFeatureAtCoordinate(
coordinate,
resolution,
rotation,
hitTolerance,
featureCallback,
declutteredFeatures
);
if (found) {
return true;
}
}
});
}
return found;
}
/**
* Asynchronous layer level hit detection.
* @param {import("../../pixel.js").Pixel} pixel Pixel.
* @return {Promise<Array<import("../../Feature").default>>} Promise that resolves with an array of features.
*/
getFeatures(pixel) {
return new Promise(
function (resolve, reject) {
const layer =
/** @type {import("../../layer/VectorTile.js").default} */ (
this.getLayer()
);
const layerUid = getUid(layer);
const source = layer.getSource();
const projection = this.renderedProjection;
const projectionExtent = projection.getExtent();
const resolution = this.renderedResolution;
const tileGrid = source.getTileGridForProjection(projection);
const coordinate = applyTransform(
this.renderedPixelToCoordinateTransform_,
pixel.slice()
);
const tileCoord = tileGrid.getTileCoordForCoordAndResolution(
coordinate,
resolution
);
let tile;
for (let i = 0, ii = this.renderedTiles.length; i < ii; ++i) {
if (
tileCoord.toString() === this.renderedTiles[i].tileCoord.toString()
) {
tile = this.renderedTiles[i];
if (tile.getState() === TileState.LOADED) {
const extent = tileGrid.getTileCoordExtent(tile.tileCoord);
if (
source.getWrapX() &&
projection.canWrapX() &&
!containsExtent(projectionExtent, extent)
) {
wrapX(coordinate, projection);
}
break;
}
tile = undefined;
}
}
if (!tile || tile.loadingSourceTiles > 0) {
resolve([]);
return;
}
const extent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord);
const corner = getTopLeft(extent);
const tilePixel = [
(coordinate[0] - corner[0]) / resolution,
(corner[1] - coordinate[1]) / resolution,
];
const features = tile
.getSourceTiles()
.reduce(function (accumulator, sourceTile) {
return accumulator.concat(sourceTile.getFeatures());
}, []);
let hitDetectionImageData = tile.hitDetectionImageData[layerUid];
if (!hitDetectionImageData && !this.animatingOrInteracting_) {
const tileSize = toSize(
tileGrid.getTileSize(
tileGrid.getZForResolution(resolution, source.zDirection)
)
);
const rotation = this.renderedRotation_;
const transforms = [
this.getRenderTransform(
tileGrid.getTileCoordCenter(tile.wrappedTileCoord),
resolution,
0,
HIT_DETECT_RESOLUTION,
tileSize[0] * HIT_DETECT_RESOLUTION,
tileSize[1] * HIT_DETECT_RESOLUTION,
0
),
];
hitDetectionImageData = createHitDetectionImageData(
tileSize,
transforms,
features,
layer.getStyleFunction(),
tileGrid.getTileCoordExtent(tile.wrappedTileCoord),
tile.getReplayState(layer).renderedResolution,
rotation
);
tile.hitDetectionImageData[layerUid] = hitDetectionImageData;
}
resolve(hitDetect(tilePixel, features, hitDetectionImageData));
}.bind(this)
);
}
/**
* Perform action necessary to get the layer rendered after new fonts have loaded
*/
handleFontsChanged() {
const layer = this.getLayer();
if (layer.getVisible() && this.renderedLayerRevision_ !== undefined) {
layer.changed();
}
}
/**
* Handle changes in image style state.
* @param {import("../../events/Event.js").default} event Image style change event.
* @private
*/
handleStyleImageChange_(event) {
this.renderIfReadyAndVisible();
}
/**
* Render declutter items for this layer
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
*/
renderDeclutter(frameState) {
const context = this.context;
const alpha = context.globalAlpha;
context.globalAlpha = this.getLayer().getOpacity();
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
);
}
}
}
context.globalAlpha = alpha;
}
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.
* @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.
*/
renderFrame(frameState, target) {
const viewHints = frameState.viewHints;
const hifi = !(
viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]
);
super.renderFrame(frameState, target);
this.renderedPixelToCoordinateTransform_ =
frameState.pixelToCoordinateTransform.slice();
this.renderedRotation_ = frameState.viewState.rotation;
const layer = /** @type {import("../../layer/VectorTile.js").default} */ (
this.getLayer()
);
const renderMode = layer.getRenderMode();
const context = this.context;
const alpha = context.globalAlpha;
context.globalAlpha = layer.getOpacity();
const replayTypes = VECTOR_REPLAYS[renderMode];
const viewState = frameState.viewState;
const rotation = viewState.rotation;
const tileSource = layer.getSource();
const tileGrid = tileSource.getTileGridForProjection(viewState.projection);
const z = tileGrid.getZForResolution(
viewState.resolution,
tileSource.zDirection
);
const tiles = this.renderedTiles;
const clips = [];
const clipZs = [];
let ready = true;
for (let i = tiles.length - 1; i >= 0; --i) {
const tile = /** @type {import("../../VectorRenderTile.js").default} */ (
tiles[i]
);
ready = ready && !tile.getReplayState(layer).dirty;
const executorGroups = tile.executorGroups[getUid(layer)].filter(
(group) => group.hasExecutors(replayTypes)
);
if (executorGroups.length === 0) {
continue;
}
const transform = this.getTileRenderTransform(tile, frameState);
const currentZ = tile.tileCoord[0];
let contextSaved = false;
// Clip mask for regions in this tile that already filled by a higher z tile
const currentClip = executorGroups[0].getClipCoords(transform);
if (currentClip) {
for (let j = 0, jj = clips.length; j < jj; ++j) {
if (z !== currentZ && currentZ < clipZs[j]) {
const clip = clips[j];
if (
intersects(
[
currentClip[0],
currentClip[3],
currentClip[4],
currentClip[7],
],
[clip[0], clip[3], clip[4], clip[7]]
)
) {
if (!contextSaved) {
context.save();
contextSaved = true;
}
context.beginPath();
// counter-clockwise (outer ring) for current tile
context.moveTo(currentClip[0], currentClip[1]);
context.lineTo(currentClip[2], currentClip[3]);
context.lineTo(currentClip[4], currentClip[5]);
context.lineTo(currentClip[6], currentClip[7]);
// clockwise (inner ring) for higher z tile
context.moveTo(clip[6], clip[7]);
context.lineTo(clip[4], clip[5]);
context.lineTo(clip[2], clip[3]);
context.lineTo(clip[0], clip[1]);
context.clip();
}
}
}
clips.push(currentClip);
clipZs.push(currentZ);
}
for (let t = 0, tt = executorGroups.length; t < tt; ++t) {
const executorGroup = executorGroups[t];
executorGroup.execute(
context,
1,
transform,
rotation,
hifi,
replayTypes
);
}
if (contextSaved) {
context.restore();
}
}
context.globalAlpha = alpha;
this.ready = ready;
return this.container;
}
/**
* @param {import("../../Feature.js").FeatureLike} feature Feature.
* @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("../../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.
*/
renderFeature(
feature,
squaredTolerance,
styles,
builderGroup,
opt_declutterBuilderGroup
) {
if (!styles) {
return false;
}
let loading = false;
if (Array.isArray(styles)) {
for (let i = 0, ii = styles.length; i < ii; ++i) {
loading =
renderFeature(
builderGroup,
feature,
styles[i],
squaredTolerance,
this.boundHandleStyleImageChange_,
undefined,
opt_declutterBuilderGroup
) || loading;
}
} else {
loading = renderFeature(
builderGroup,
feature,
styles,
squaredTolerance,
this.boundHandleStyleImageChange_,
undefined,
opt_declutterBuilderGroup
);
}
return loading;
}
/**
* @param {import("../../VectorRenderTile.js").default} tile Tile.
* @return {boolean} A new tile image was rendered.
* @private
*/
tileImageNeedsRender_(tile) {
const layer = /** @type {import("../../layer/VectorTile.js").default} */ (
this.getLayer()
);
if (layer.getRenderMode() === VectorTileRenderType.VECTOR) {
return false;
}
const replayState = tile.getReplayState(layer);
const revision = layer.getRevision();
const resolution = tile.wantedResolution;
return (
replayState.renderedTileResolution !== resolution ||
replayState.renderedTileRevision !== revision
);
}
/**
* @param {import("../../VectorRenderTile.js").default} tile Tile.
* @param {import("../../PluggableMap").FrameState} frameState Frame state.
* @private
*/
renderTileImage_(tile, frameState) {
const layer = /** @type {import("../../layer/VectorTile.js").default} */ (
this.getLayer()
);
const replayState = tile.getReplayState(layer);
const revision = layer.getRevision();
const executorGroups = tile.executorGroups[getUid(layer)];
replayState.renderedTileRevision = revision;
const tileCoord = tile.wrappedTileCoord;
const z = tileCoord[0];
const source = layer.getSource();
let pixelRatio = frameState.pixelRatio;
const viewState = frameState.viewState;
const projection = viewState.projection;
const tileGrid = source.getTileGridForProjection(projection);
const tileResolution = tileGrid.getResolution(tile.tileCoord[0]);
const renderPixelRatio =
(frameState.pixelRatio / tile.wantedResolution) * tileResolution;
const resolution = tileGrid.getResolution(z);
const context = tile.getContext(layer);
// Increase tile size when overzooming for low pixel ratio, to avoid blurry tiles
pixelRatio = Math.round(
Math.max(pixelRatio, renderPixelRatio / pixelRatio)
);
const size = source.getTilePixelSize(z, pixelRatio, projection);
context.canvas.width = size[0];
context.canvas.height = size[1];
const renderScale = pixelRatio / renderPixelRatio;
if (renderScale !== 1) {
const canvasTransform = resetTransform(this.tmpTransform_);
scaleTransform(canvasTransform, renderScale, renderScale);
context.setTransform.apply(context, canvasTransform);
}
const tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent);
const pixelScale = renderPixelRatio / resolution;
const transform = resetTransform(this.tmpTransform_);
scaleTransform(transform, pixelScale, -pixelScale);
translateTransform(transform, -tileExtent[0], -tileExtent[3]);
for (let i = 0, ii = executorGroups.length; i < ii; ++i) {
const executorGroup = executorGroups[i];
executorGroup.execute(
context,
renderScale,
transform,
0,
true,
IMAGE_REPLAYS[layer.getRenderMode()]
);
}
replayState.renderedTileResolution = tile.wantedResolution;
}
}
export default CanvasVectorTileLayerRenderer;

21
node_modules/ol/src/renderer/canvas/common.js generated vendored Normal file
View File

@@ -0,0 +1,21 @@
/**
* @module ol/renderer/canvas/common
*/
/**
* Context options to disable image smoothing.
* @type {Object}
*/
export const IMAGE_SMOOTHING_DISABLED = {
imageSmoothingEnabled: false,
msImageSmoothingEnabled: false,
};
/**
* Context options to enable image smoothing.
* @type {Object}
*/
export const IMAGE_SMOOTHING_ENABLED = {
imageSmoothingEnabled: true,
msImageSmoothingEnabled: true,
};

484
node_modules/ol/src/renderer/vector.js generated vendored Normal file
View File

@@ -0,0 +1,484 @@
/**
* @module ol/renderer/vector
*/
import ImageState from '../ImageState.js';
import {getUid} from '../util.js';
/**
* Feature callback. The callback will be called with three arguments. The first
* argument is one {@link module:ol/Feature~Feature feature} or {@link module:ol/render/Feature~RenderFeature render feature}
* at the pixel, the second is the {@link module:ol/layer/Layer~Layer layer} of the feature and will be null for
* unmanaged layers. The third is the {@link module:ol/geom/SimpleGeometry~SimpleGeometry} of the feature. For features
* with a GeometryCollection geometry, it will be the first detected geometry from the collection.
* @template T
* @typedef {function(import("../Feature.js").FeatureLike, import("../layer/Layer.js").default<import("../source/Source").default>, import("../geom/SimpleGeometry.js").default): T} FeatureCallback
*/
/**
* Tolerance for geometry simplification in device pixels.
* @type {number}
*/
const SIMPLIFY_TOLERANCE = 0.5;
/**
* @const
* @type {Object<import("../geom/Geometry.js").Type,
* function(import("../render/canvas/BuilderGroup.js").default, import("../geom/Geometry.js").default,
* import("../style/Style.js").default, Object): void>}
*/
const GEOMETRY_RENDERERS = {
'Point': renderPointGeometry,
'LineString': renderLineStringGeometry,
'Polygon': renderPolygonGeometry,
'MultiPoint': renderMultiPointGeometry,
'MultiLineString': renderMultiLineStringGeometry,
'MultiPolygon': renderMultiPolygonGeometry,
'GeometryCollection': renderGeometryCollectionGeometry,
'Circle': renderCircleGeometry,
};
/**
* @param {import("../Feature.js").FeatureLike} feature1 Feature 1.
* @param {import("../Feature.js").FeatureLike} feature2 Feature 2.
* @return {number} Order.
*/
export function defaultOrder(feature1, feature2) {
return parseInt(getUid(feature1), 10) - parseInt(getUid(feature2), 10);
}
/**
* @param {number} resolution Resolution.
* @param {number} pixelRatio Pixel ratio.
* @return {number} Squared pixel tolerance.
*/
export function getSquaredTolerance(resolution, pixelRatio) {
const tolerance = getTolerance(resolution, pixelRatio);
return tolerance * tolerance;
}
/**
* @param {number} resolution Resolution.
* @param {number} pixelRatio Pixel ratio.
* @return {number} Pixel tolerance.
*/
export function getTolerance(resolution, pixelRatio) {
return (SIMPLIFY_TOLERANCE * resolution) / pixelRatio;
}
/**
* @param {import("../render/canvas/BuilderGroup.js").default} builderGroup Builder group.
* @param {import("../geom/Circle.js").default} geometry Geometry.
* @param {import("../style/Style.js").default} style Style.
* @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,
opt_declutterBuilderGroup
) {
const fillStyle = style.getFill();
const strokeStyle = style.getStroke();
if (fillStyle || strokeStyle) {
const circleReplay = builderGroup.getBuilder(style.getZIndex(), 'Circle');
circleReplay.setFillStrokeStyle(fillStyle, strokeStyle);
circleReplay.drawCircle(geometry, feature);
}
const textStyle = style.getText();
if (textStyle && textStyle.getText()) {
const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder(
style.getZIndex(),
'Text'
);
textReplay.setTextStyle(textStyle);
textReplay.drawText(geometry, feature);
}
}
/**
* @param {import("../render/canvas/BuilderGroup.js").default} replayGroup Replay group.
* @param {import("../Feature.js").FeatureLike} feature Feature.
* @param {import("../style/Style.js").default} style Style.
* @param {number} squaredTolerance Squared tolerance.
* @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("../render/canvas/BuilderGroup.js").default} [opt_declutterBuilderGroup] Builder for decluttering.
* @return {boolean} `true` if style is loading.
*/
export function renderFeature(
replayGroup,
feature,
style,
squaredTolerance,
listener,
opt_transform,
opt_declutterBuilderGroup
) {
let loading = false;
const imageStyle = style.getImage();
if (imageStyle) {
const imageState = imageStyle.getImageState();
if (imageState == ImageState.LOADED || imageState == ImageState.ERROR) {
imageStyle.unlistenImageChange(listener);
} else {
if (imageState == ImageState.IDLE) {
imageStyle.load();
}
imageStyle.listenImageChange(listener);
loading = true;
}
}
renderFeatureInternal(
replayGroup,
feature,
style,
squaredTolerance,
opt_transform,
opt_declutterBuilderGroup
);
return loading;
}
/**
* @param {import("../render/canvas/BuilderGroup.js").default} replayGroup Replay group.
* @param {import("../Feature.js").FeatureLike} feature Feature.
* @param {import("../style/Style.js").default} style Style.
* @param {number} squaredTolerance Squared tolerance.
* @param {import("../proj.js").TransformFunction} [opt_transform] Optional transform function.
* @param {import("../render/canvas/BuilderGroup.js").default} [opt_declutterBuilderGroup] Builder for decluttering.
*/
function renderFeatureInternal(
replayGroup,
feature,
style,
squaredTolerance,
opt_transform,
opt_declutterBuilderGroup
) {
const geometry = style.getGeometryFunction()(feature);
if (!geometry) {
return;
}
const simplifiedGeometry = geometry.simplifyTransformed(
squaredTolerance,
opt_transform
);
const renderer = style.getRenderer();
if (renderer) {
renderGeometry(replayGroup, simplifiedGeometry, style, feature);
} else {
const geometryRenderer = GEOMETRY_RENDERERS[simplifiedGeometry.getType()];
geometryRenderer(
replayGroup,
simplifiedGeometry,
style,
feature,
opt_declutterBuilderGroup
);
}
}
/**
* @param {import("../render/canvas/BuilderGroup.js").default} replayGroup Replay group.
* @param {import("../geom/Geometry.js").default|import("../render/Feature.js").default} geometry Geometry.
* @param {import("../style/Style.js").default} style Style.
* @param {import("../Feature.js").FeatureLike} feature Feature.
*/
function renderGeometry(replayGroup, geometry, style, feature) {
if (geometry.getType() == 'GeometryCollection') {
const geometries =
/** @type {import("../geom/GeometryCollection.js").default} */ (
geometry
).getGeometries();
for (let i = 0, ii = geometries.length; i < ii; ++i) {
renderGeometry(replayGroup, geometries[i], style, feature);
}
return;
}
const replay = replayGroup.getBuilder(style.getZIndex(), 'Default');
replay.drawCustom(
/** @type {import("../geom/SimpleGeometry.js").default} */ (geometry),
feature,
style.getRenderer(),
style.getHitDetectionRenderer()
);
}
/**
* @param {import("../render/canvas/BuilderGroup.js").default} replayGroup Replay group.
* @param {import("../geom/GeometryCollection.js").default} geometry Geometry.
* @param {import("../style/Style.js").default} style Style.
* @param {import("../Feature.js").default} feature Feature.
* @param {import("../render/canvas/BuilderGroup.js").default} [opt_declutterBuilderGroup] Builder for decluttering.
*/
function renderGeometryCollectionGeometry(
replayGroup,
geometry,
style,
feature,
opt_declutterBuilderGroup
) {
const geometries = geometry.getGeometriesArray();
let i, ii;
for (i = 0, ii = geometries.length; i < ii; ++i) {
const geometryRenderer = GEOMETRY_RENDERERS[geometries[i].getType()];
geometryRenderer(
replayGroup,
geometries[i],
style,
feature,
opt_declutterBuilderGroup
);
}
}
/**
* @param {import("../render/canvas/BuilderGroup.js").default} builderGroup Replay group.
* @param {import("../geom/LineString.js").default|import("../render/Feature.js").default} geometry Geometry.
* @param {import("../style/Style.js").default} style Style.
* @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,
opt_declutterBuilderGroup
) {
const strokeStyle = style.getStroke();
if (strokeStyle) {
const lineStringReplay = builderGroup.getBuilder(
style.getZIndex(),
'LineString'
);
lineStringReplay.setFillStrokeStyle(null, strokeStyle);
lineStringReplay.drawLineString(geometry, feature);
}
const textStyle = style.getText();
if (textStyle && textStyle.getText()) {
const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder(
style.getZIndex(),
'Text'
);
textReplay.setTextStyle(textStyle);
textReplay.drawText(geometry, feature);
}
}
/**
* @param {import("../render/canvas/BuilderGroup.js").default} builderGroup Replay group.
* @param {import("../geom/MultiLineString.js").default|import("../render/Feature.js").default} geometry Geometry.
* @param {import("../style/Style.js").default} style Style.
* @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,
opt_declutterBuilderGroup
) {
const strokeStyle = style.getStroke();
if (strokeStyle) {
const lineStringReplay = builderGroup.getBuilder(
style.getZIndex(),
'LineString'
);
lineStringReplay.setFillStrokeStyle(null, strokeStyle);
lineStringReplay.drawMultiLineString(geometry, feature);
}
const textStyle = style.getText();
if (textStyle && textStyle.getText()) {
const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder(
style.getZIndex(),
'Text'
);
textReplay.setTextStyle(textStyle);
textReplay.drawText(geometry, feature);
}
}
/**
* @param {import("../render/canvas/BuilderGroup.js").default} builderGroup Replay group.
* @param {import("../geom/MultiPolygon.js").default} geometry Geometry.
* @param {import("../style/Style.js").default} style Style.
* @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,
opt_declutterBuilderGroup
) {
const fillStyle = style.getFill();
const strokeStyle = style.getStroke();
if (strokeStyle || fillStyle) {
const polygonReplay = builderGroup.getBuilder(style.getZIndex(), 'Polygon');
polygonReplay.setFillStrokeStyle(fillStyle, strokeStyle);
polygonReplay.drawMultiPolygon(geometry, feature);
}
const textStyle = style.getText();
if (textStyle && textStyle.getText()) {
const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder(
style.getZIndex(),
'Text'
);
textReplay.setTextStyle(textStyle);
textReplay.drawText(geometry, feature);
}
}
/**
* @param {import("../render/canvas/BuilderGroup.js").default} builderGroup Replay group.
* @param {import("../geom/Point.js").default|import("../render/Feature.js").default} geometry Geometry.
* @param {import("../style/Style.js").default} style Style.
* @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,
opt_declutterBuilderGroup
) {
const imageStyle = style.getImage();
const textStyle = style.getText();
/** @type {import("../render/canvas.js").DeclutterImageWithText} */
let declutterImageWithText;
if (imageStyle) {
if (imageStyle.getImageState() != ImageState.LOADED) {
return;
}
let imageBuilderGroup = builderGroup;
if (opt_declutterBuilderGroup) {
const declutterMode = imageStyle.getDeclutterMode();
if (declutterMode !== 'none') {
imageBuilderGroup = opt_declutterBuilderGroup;
if (declutterMode === 'obstacle') {
// draw in non-declutter group:
const imageReplay = builderGroup.getBuilder(
style.getZIndex(),
'Image'
);
imageReplay.setImageStyle(imageStyle, declutterImageWithText);
imageReplay.drawPoint(geometry, feature);
} else if (textStyle && textStyle.getText()) {
declutterImageWithText = {};
}
}
}
const imageReplay = imageBuilderGroup.getBuilder(
style.getZIndex(),
'Image'
);
imageReplay.setImageStyle(imageStyle, declutterImageWithText);
imageReplay.drawPoint(geometry, feature);
}
if (textStyle && textStyle.getText()) {
let textBuilderGroup = builderGroup;
if (opt_declutterBuilderGroup) {
textBuilderGroup = opt_declutterBuilderGroup;
}
const textReplay = textBuilderGroup.getBuilder(style.getZIndex(), 'Text');
textReplay.setTextStyle(textStyle, declutterImageWithText);
textReplay.drawText(geometry, feature);
}
}
/**
* @param {import("../render/canvas/BuilderGroup.js").default} builderGroup Replay group.
* @param {import("../geom/MultiPoint.js").default|import("../render/Feature.js").default} geometry Geometry.
* @param {import("../style/Style.js").default} style Style.
* @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,
opt_declutterBuilderGroup
) {
const imageStyle = style.getImage();
const textStyle = style.getText();
/** @type {import("../render/canvas.js").DeclutterImageWithText} */
let declutterImageWithText;
if (imageStyle) {
if (imageStyle.getImageState() != ImageState.LOADED) {
return;
}
let imageBuilderGroup = builderGroup;
if (opt_declutterBuilderGroup) {
const declutterMode = imageStyle.getDeclutterMode();
if (declutterMode !== 'none') {
imageBuilderGroup = opt_declutterBuilderGroup;
if (declutterMode === 'obstacle') {
// draw in non-declutter group:
const imageReplay = builderGroup.getBuilder(
style.getZIndex(),
'Image'
);
imageReplay.setImageStyle(imageStyle, declutterImageWithText);
imageReplay.drawMultiPoint(geometry, feature);
} else if (textStyle && textStyle.getText()) {
declutterImageWithText = {};
}
}
}
const imageReplay = imageBuilderGroup.getBuilder(
style.getZIndex(),
'Image'
);
imageReplay.setImageStyle(imageStyle, declutterImageWithText);
imageReplay.drawMultiPoint(geometry, feature);
}
if (textStyle && textStyle.getText()) {
let textBuilderGroup = builderGroup;
if (opt_declutterBuilderGroup) {
textBuilderGroup = opt_declutterBuilderGroup;
}
const textReplay = textBuilderGroup.getBuilder(style.getZIndex(), 'Text');
textReplay.setTextStyle(textStyle, declutterImageWithText);
textReplay.drawText(geometry, feature);
}
}
/**
* @param {import("../render/canvas/BuilderGroup.js").default} builderGroup Replay group.
* @param {import("../geom/Polygon.js").default|import("../render/Feature.js").default} geometry Geometry.
* @param {import("../style/Style.js").default} style Style.
* @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,
opt_declutterBuilderGroup
) {
const fillStyle = style.getFill();
const strokeStyle = style.getStroke();
if (fillStyle || strokeStyle) {
const polygonReplay = builderGroup.getBuilder(style.getZIndex(), 'Polygon');
polygonReplay.setFillStrokeStyle(fillStyle, strokeStyle);
polygonReplay.drawPolygon(geometry, feature);
}
const textStyle = style.getText();
if (textStyle && textStyle.getText()) {
const textReplay = (opt_declutterBuilderGroup || builderGroup).getBuilder(
style.getZIndex(),
'Text'
);
textReplay.setTextStyle(textStyle);
textReplay.drawText(geometry, feature);
}
}

486
node_modules/ol/src/renderer/webgl/Layer.js generated vendored Normal file
View File

@@ -0,0 +1,486 @@
/**
* @module ol/renderer/webgl/Layer
*/
import LayerProperty from '../../layer/Property.js';
import LayerRenderer from '../Layer.js';
import RenderEvent from '../../render/Event.js';
import RenderEventType from '../../render/EventType.js';
import WebGLHelper from '../../webgl/Helper.js';
import {
apply as applyTransform,
compose as composeTransform,
create as createTransform,
} from '../../transform.js';
import {containsCoordinate} from '../../extent.js';
/**
* @enum {string}
*/
export const WebGLWorkerMessageType = {
GENERATE_BUFFERS: 'GENERATE_BUFFERS',
};
/**
* @typedef {Object} WebGLWorkerGenerateBuffersMessage
* This message will trigger the generation of a vertex and an index buffer based on the given render instructions.
* When the buffers are generated, the worked will send a message of the same type to the main thread, with
* the generated buffers in it.
* Note that any addition properties present in the message *will* be sent back to the main thread.
* @property {WebGLWorkerMessageType} type Message type
* @property {ArrayBuffer} renderInstructions Render instructions raw binary buffer.
* @property {ArrayBuffer} [vertexBuffer] Vertices array raw binary buffer (sent by the worker).
* @property {ArrayBuffer} [indexBuffer] Indices array raw binary buffer (sent by the worker).
* @property {number} [customAttributesCount] Amount of custom attributes count in the render instructions.
*/
/**
* @typedef {Object} PostProcessesOptions
* @property {number} [scaleRatio] Scale ratio; if < 1, the post process will render to a texture smaller than
* the main canvas that will then be sampled up (useful for saving resource on blur steps).
* @property {string} [vertexShader] Vertex shader source
* @property {string} [fragmentShader] Fragment shader source
* @property {Object<string,import("../../webgl/Helper").UniformValue>} [uniforms] Uniform definitions for the post process step
*/
/**
* @typedef {Object} Options
* @property {Object<string,import("../../webgl/Helper").UniformValue>} [uniforms] Uniform definitions for the post process steps
* @property {Array<PostProcessesOptions>} [postProcesses] Post-processes definitions
*/
/**
* @classdesc
* Base WebGL renderer class.
* Holds all logic related to data manipulation & some common rendering logic
* @template {import("../../layer/Layer.js").default} LayerType
* @extends {LayerRenderer<LayerType>}
*/
class WebGLLayerRenderer extends LayerRenderer {
/**
* @param {LayerType} layer Layer.
* @param {Options} [opt_options] Options.
*/
constructor(layer, opt_options) {
super(layer);
const options = opt_options || {};
/**
* The transform for viewport CSS pixels to rendered pixels. This transform is only
* set before dispatching rendering events.
* @private
* @type {import("../../transform.js").Transform}
*/
this.inversePixelTransform_ = createTransform();
/**
* @private
* @type {CanvasRenderingContext2D}
*/
this.pixelContext_ = null;
/**
* @private
*/
this.postProcesses_ = options.postProcesses;
/**
* @private
*/
this.uniforms_ = options.uniforms;
/**
* @type {WebGLHelper}
* @protected
*/
this.helper;
layer.addChangeListener(LayerProperty.MAP, this.removeHelper.bind(this));
this.dispatchPreComposeEvent = this.dispatchPreComposeEvent.bind(this);
this.dispatchPostComposeEvent = this.dispatchPostComposeEvent.bind(this);
}
/**
* @param {WebGLRenderingContext} context The WebGL rendering context.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @protected
*/
dispatchPreComposeEvent(context, frameState) {
const layer = this.getLayer();
if (layer.hasListener(RenderEventType.PRECOMPOSE)) {
const event = new RenderEvent(
RenderEventType.PRECOMPOSE,
undefined,
frameState,
context
);
layer.dispatchEvent(event);
}
}
/**
* @param {WebGLRenderingContext} context The WebGL rendering context.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @protected
*/
dispatchPostComposeEvent(context, frameState) {
const layer = this.getLayer();
if (layer.hasListener(RenderEventType.POSTCOMPOSE)) {
const event = new RenderEvent(
RenderEventType.POSTCOMPOSE,
undefined,
frameState,
context
);
layer.dispatchEvent(event);
}
}
/**
* Reset options (only handles uniforms).
* @param {Options} options Options.
*/
reset(options) {
this.uniforms_ = options.uniforms;
if (this.helper) {
this.helper.setUniforms(this.uniforms_);
}
}
/**
* @protected
*/
removeHelper() {
if (this.helper) {
this.helper.dispose();
delete this.helper;
}
}
/**
* Determine whether renderFrame should be called.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @return {boolean} Layer is ready to be rendered.
*/
prepareFrame(frameState) {
if (this.getLayer().getRenderSource()) {
let incrementGroup = true;
let groupNumber = -1;
let className;
for (let i = 0, ii = frameState.layerStatesArray.length; i < ii; i++) {
const layer = frameState.layerStatesArray[i].layer;
const renderer = layer.getRenderer();
if (!(renderer instanceof WebGLLayerRenderer)) {
incrementGroup = true;
continue;
}
const layerClassName = layer.getClassName();
if (incrementGroup || layerClassName !== className) {
groupNumber += 1;
incrementGroup = false;
}
className = layerClassName;
if (renderer === this) {
break;
}
}
const canvasCacheKey =
'map/' + frameState.mapId + '/group/' + groupNumber;
if (!this.helper || !this.helper.canvasCacheKeyMatches(canvasCacheKey)) {
this.removeHelper();
this.helper = new WebGLHelper({
postProcesses: this.postProcesses_,
uniforms: this.uniforms_,
canvasCacheKey: canvasCacheKey,
});
if (className) {
this.helper.getCanvas().className = className;
}
this.afterHelperCreated();
}
}
return this.prepareFrameInternal(frameState);
}
/**
* @protected
*/
afterHelperCreated() {}
/**
* Determine whether renderFrame should be called.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @return {boolean} Layer is ready to be rendered.
* @protected
*/
prepareFrameInternal(frameState) {
return true;
}
/**
* Clean up.
*/
disposeInternal() {
this.removeHelper();
super.disposeInternal();
}
/**
* @param {import("../../render/EventType.js").default} type Event type.
* @param {WebGLRenderingContext} context The rendering context.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @private
*/
dispatchRenderEvent_(type, context, frameState) {
const layer = this.getLayer();
if (layer.hasListener(type)) {
composeTransform(
this.inversePixelTransform_,
0,
0,
frameState.pixelRatio,
-frameState.pixelRatio,
0,
0,
-frameState.size[1]
);
const event = new RenderEvent(
type,
this.inversePixelTransform_,
frameState,
context
);
layer.dispatchEvent(event);
}
}
/**
* @param {WebGLRenderingContext} context The rendering context.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @protected
*/
preRender(context, frameState) {
this.dispatchRenderEvent_(RenderEventType.PRERENDER, context, frameState);
}
/**
* @param {WebGLRenderingContext} context The rendering context.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @protected
*/
postRender(context, frameState) {
this.dispatchRenderEvent_(RenderEventType.POSTRENDER, context, frameState);
}
/**
* @param {import("../../pixel.js").Pixel} pixel Pixel.
* @param {import("../../PluggableMap.js").FrameState} frameState FrameState.
* @param {number} hitTolerance Hit tolerance in pixels.
* @return {Uint8ClampedArray|Uint8Array} The result. If there is no data at the pixel
* location, null will be returned. If there is data, but pixel values cannot be
* returned, and empty array will be returned.
*/
getDataAtPixel(pixel, frameState, hitTolerance) {
const renderPixel = applyTransform(
[frameState.pixelRatio, 0, 0, frameState.pixelRatio, 0, 0],
pixel.slice()
);
const gl = this.helper.getGL();
if (!gl) {
return null;
}
const layer = this.getLayer();
const layerExtent = layer.getExtent();
if (layerExtent) {
const renderCoordinate = applyTransform(
frameState.pixelToCoordinateTransform,
pixel.slice()
);
/** get only data inside of the layer extent */
if (!containsCoordinate(layerExtent, renderCoordinate)) {
return null;
}
}
const attributes = gl.getContextAttributes();
if (!attributes || !attributes.preserveDrawingBuffer) {
// we assume there is data at the given pixel (although there might not be)
return new Uint8Array();
}
const x = Math.round(renderPixel[0]);
const y = Math.round(renderPixel[1]);
let pixelContext = this.pixelContext_;
if (!pixelContext) {
const pixelCanvas = document.createElement('canvas');
pixelCanvas.width = 1;
pixelCanvas.height = 1;
pixelContext = pixelCanvas.getContext('2d');
this.pixelContext_ = pixelContext;
}
pixelContext.clearRect(0, 0, 1, 1);
let data;
try {
pixelContext.drawImage(gl.canvas, x, y, 1, 1, 0, 0, 1, 1);
data = pixelContext.getImageData(0, 0, 1, 1).data;
} catch (err) {
return data;
}
if (data[3] === 0) {
return null;
}
return data;
}
}
const tmpArray_ = [];
const bufferPositions_ = {vertexPosition: 0, indexPosition: 0};
function writePointVertex(buffer, pos, x, y, index) {
buffer[pos + 0] = x;
buffer[pos + 1] = y;
buffer[pos + 2] = index;
}
/**
* An object holding positions both in an index and a vertex buffer.
* @typedef {Object} BufferPositions
* @property {number} vertexPosition Position in the vertex buffer
* @property {number} indexPosition Position in the index buffer
*/
/**
* Pushes a quad (two triangles) based on a point geometry
* @param {Float32Array} instructions Array of render instructions for points.
* @param {number} elementIndex Index from which render instructions will be read.
* @param {Float32Array} vertexBuffer Buffer in the form of a typed array.
* @param {Uint32Array} indexBuffer Buffer in the form of a typed array.
* @param {number} customAttributesCount Amount of custom attributes for each element.
* @param {BufferPositions} [bufferPositions] Buffer write positions; if not specified, positions will be set at 0.
* @return {BufferPositions} New buffer positions where to write next
* @property {number} vertexPosition New position in the vertex buffer where future writes should start.
* @property {number} indexPosition New position in the index buffer where future writes should start.
* @private
*/
export function writePointFeatureToBuffers(
instructions,
elementIndex,
vertexBuffer,
indexBuffer,
customAttributesCount,
bufferPositions
) {
// This is for x, y and index
const baseVertexAttrsCount = 3;
const baseInstructionsCount = 2;
const stride = baseVertexAttrsCount + customAttributesCount;
const x = instructions[elementIndex + 0];
const y = instructions[elementIndex + 1];
// read custom numerical attributes on the feature
const customAttrs = tmpArray_;
customAttrs.length = customAttributesCount;
for (let i = 0; i < customAttrs.length; i++) {
customAttrs[i] = instructions[elementIndex + baseInstructionsCount + i];
}
let vPos = bufferPositions ? bufferPositions.vertexPosition : 0;
let iPos = bufferPositions ? bufferPositions.indexPosition : 0;
const baseIndex = vPos / stride;
// push vertices for each of the four quad corners (first standard then custom attributes)
writePointVertex(vertexBuffer, vPos, x, y, 0);
customAttrs.length &&
vertexBuffer.set(customAttrs, vPos + baseVertexAttrsCount);
vPos += stride;
writePointVertex(vertexBuffer, vPos, x, y, 1);
customAttrs.length &&
vertexBuffer.set(customAttrs, vPos + baseVertexAttrsCount);
vPos += stride;
writePointVertex(vertexBuffer, vPos, x, y, 2);
customAttrs.length &&
vertexBuffer.set(customAttrs, vPos + baseVertexAttrsCount);
vPos += stride;
writePointVertex(vertexBuffer, vPos, x, y, 3);
customAttrs.length &&
vertexBuffer.set(customAttrs, vPos + baseVertexAttrsCount);
vPos += stride;
indexBuffer[iPos++] = baseIndex;
indexBuffer[iPos++] = baseIndex + 1;
indexBuffer[iPos++] = baseIndex + 3;
indexBuffer[iPos++] = baseIndex + 1;
indexBuffer[iPos++] = baseIndex + 2;
indexBuffer[iPos++] = baseIndex + 3;
bufferPositions_.vertexPosition = vPos;
bufferPositions_.indexPosition = iPos;
return bufferPositions_;
}
/**
* Returns a texture of 1x1 pixel, white
* @private
* @return {ImageData} Image data.
*/
export function getBlankImageData() {
const canvas = document.createElement('canvas');
const image = canvas.getContext('2d').createImageData(1, 1);
image.data[0] = 255;
image.data[1] = 255;
image.data[2] = 255;
image.data[3] = 255;
return image;
}
/**
* Generates a color array based on a numerical id
* Note: the range for each component is 0 to 1 with 256 steps
* @param {number} id Id
* @param {Array<number>} [opt_array] Reusable array
* @return {Array<number>} Color array containing the encoded id
*/
export function colorEncodeId(id, opt_array) {
const array = opt_array || [];
const radix = 256;
const divide = radix - 1;
array[0] = Math.floor(id / radix / radix / radix) / divide;
array[1] = (Math.floor(id / radix / radix) % radix) / divide;
array[2] = (Math.floor(id / radix) % radix) / divide;
array[3] = (id % radix) / divide;
return array;
}
/**
* Reads an id from a color-encoded array
* Note: the expected range for each component is 0 to 1 with 256 steps.
* @param {Array<number>} color Color array containing the encoded id
* @return {number} Decoded id
*/
export function colorDecodeId(color) {
let id = 0;
const radix = 256;
const mult = radix - 1;
id += Math.round(color[0] * radix * radix * radix * mult);
id += Math.round(color[1] * radix * radix * mult);
id += Math.round(color[2] * radix * mult);
id += Math.round(color[3] * mult);
return id;
}
export default WebGLLayerRenderer;

765
node_modules/ol/src/renderer/webgl/PointsLayer.js generated vendored Normal file
View File

@@ -0,0 +1,765 @@
/**
* @module ol/renderer/webgl/PointsLayer
*/
import BaseVector from '../../layer/BaseVector.js';
import VectorEventType from '../../source/VectorEventType.js';
import ViewHint from '../../ViewHint.js';
import WebGLArrayBuffer from '../../webgl/Buffer.js';
import WebGLLayerRenderer, {
WebGLWorkerMessageType,
colorDecodeId,
colorEncodeId,
} from './Layer.js';
import WebGLRenderTarget from '../../webgl/RenderTarget.js';
import {ARRAY_BUFFER, DYNAMIC_DRAW, ELEMENT_ARRAY_BUFFER} from '../../webgl.js';
import {AttributeType, DefaultUniform} from '../../webgl/Helper.js';
import {
apply as applyTransform,
create as createTransform,
makeInverse as makeInverseTransform,
multiply as multiplyTransform,
translate as translateTransform,
} from '../../transform.js';
import {assert} from '../../asserts.js';
import {buffer, createEmpty, equals, getWidth} from '../../extent.js';
import {create as createWebGLWorker} from '../../worker/webgl.js';
import {getUid} from '../../util.js';
import {listen, unlistenByKey} from '../../events.js';
/**
* @typedef {Object} CustomAttribute A description of a custom attribute to be passed on to the GPU, with a value different
* for each feature.
* @property {string} name Attribute name.
* @property {function(import("../../Feature").default, Object<string, *>):number} callback This callback computes the numerical value of the
* attribute for a given feature (properties are available as 2nd arg for quicker access).
*/
/**
* @typedef {Object} FeatureCacheItem Object that holds a reference to a feature, its geometry and properties. Used to optimize
* rebuildBuffers by accessing these objects quicker.
* @property {import("../../Feature").default} feature Feature
* @property {Object<string, *>} properties Feature properties
* @property {import("../../geom").Geometry} geometry Feature geometry
*/
/**
* @typedef {Object} Options
* @property {string} [className='ol-layer'] A CSS class name to set to the canvas element.
* @property {Array<CustomAttribute>} [attributes] These attributes will be read from the features in the source and then
* passed to the GPU. The `name` property of each attribute will serve as its identifier:
* * In the vertex shader as an `attribute` by prefixing it with `a_`
* * In the fragment shader as a `varying` by prefixing it with `v_`
* Please note that these can only be numerical values.
* @property {string} vertexShader Vertex shader source, mandatory.
* @property {string} fragmentShader Fragment shader source, mandatory.
* @property {string} [hitVertexShader] Vertex shader source for hit detection rendering.
* @property {string} [hitFragmentShader] Fragment shader source for hit detection rendering.
* @property {Object<string,import("../../webgl/Helper").UniformValue>} [uniforms] Uniform definitions for the post process steps
* Please note that `u_texture` is reserved for the main texture slot and `u_opacity` is reserved for the layer opacity.
* @property {Array<import("./Layer").PostProcessesOptions>} [postProcesses] Post-processes definitions
*/
/**
* @classdesc
* WebGL vector renderer optimized for points.
* All features will be rendered as quads (two triangles forming a square). New data will be flushed to the GPU
* every time the vector source changes.
*
* You need to provide vertex and fragment shaders for rendering. This can be done using
* {@link module:ol/webgl/ShaderBuilder~ShaderBuilder} utilities. These shaders shall expect a `a_position` attribute
* containing the screen-space projected center of the quad, as well as a `a_index` attribute
* whose value (0, 1, 2 or 3) indicates which quad vertex is currently getting processed (see structure below).
*
* To include variable attributes in the shaders, you need to declare them using the `attributes` property of
* the options object like so:
* ```js
* new WebGLPointsLayerRenderer(layer, {
* attributes: [
* {
* name: 'size',
* callback: function(feature) {
* // compute something with the feature
* }
* },
* {
* name: 'weight',
* callback: function(feature) {
* // compute something with the feature
* }
* },
* ],
* vertexShader:
* // shader using attribute a_weight and a_size
* fragmentShader:
* // shader using varying v_weight and v_size
* ```
*
* To enable hit detection, you must as well provide dedicated shaders using the `hitVertexShader`
* and `hitFragmentShader` properties. These shall expect the `a_hitColor` attribute to contain
* the final color that will have to be output for hit detection to work.
*
* The following uniform is used for the main texture: `u_texture`.
* The following uniform is used for the layer opacity: `u_opacity`.
*
* Please note that the main shader output should have premultiplied alpha, otherwise visual anomalies may occur.
*
* Points are rendered as quads with the following structure:
*
* ```
* (u0, v1) (u1, v1)
* [3]----------[2]
* |` |
* | ` |
* | ` |
* | ` |
* | ` |
* | ` |
* [0]----------[1]
* (u0, v0) (u1, v0)
* ```
*
* This uses {@link module:ol/webgl/Helper~WebGLHelper} internally.
*
* @api
*/
class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
/**
* @param {import("../../layer/Layer.js").default} layer Layer.
* @param {Options} options Options.
*/
constructor(layer, options) {
const uniforms = options.uniforms || {};
const projectionMatrixTransform = createTransform();
uniforms[DefaultUniform.PROJECTION_MATRIX] = projectionMatrixTransform;
super(layer, {
uniforms: uniforms,
postProcesses: options.postProcesses,
});
this.ready = false;
this.sourceRevision_ = -1;
this.verticesBuffer_ = new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW);
this.hitVerticesBuffer_ = new WebGLArrayBuffer(ARRAY_BUFFER, DYNAMIC_DRAW);
this.indicesBuffer_ = new WebGLArrayBuffer(
ELEMENT_ARRAY_BUFFER,
DYNAMIC_DRAW
);
/**
* @private
*/
this.vertexShader_ = options.vertexShader;
/**
* @private
*/
this.fragmentShader_ = options.fragmentShader;
/**
* @type {WebGLProgram}
* @private
*/
this.program_;
/**
* @type {boolean}
* @private
*/
this.hitDetectionEnabled_ =
options.hitFragmentShader && options.hitVertexShader ? true : false;
/**
* @private
*/
this.hitVertexShader_ = options.hitVertexShader;
/**
* @private
*/
this.hitFragmentShader_ = options.hitFragmentShader;
/**
* @type {WebGLProgram}
* @private
*/
this.hitProgram_;
const customAttributes = options.attributes
? options.attributes.map(function (attribute) {
return {
name: 'a_' + attribute.name,
size: 1,
type: AttributeType.FLOAT,
};
})
: [];
/**
* A list of attributes used by the renderer. By default only the position and
* index of the vertex (0 to 3) are required.
* @type {Array<import('../../webgl/Helper.js').AttributeDescription>}
*/
this.attributes = [
{
name: 'a_position',
size: 2,
type: AttributeType.FLOAT,
},
{
name: 'a_index',
size: 1,
type: AttributeType.FLOAT,
},
].concat(customAttributes);
/**
* A list of attributes used for hit detection.
* @type {Array<import('../../webgl/Helper.js').AttributeDescription>}
*/
this.hitDetectionAttributes = [
{
name: 'a_position',
size: 2,
type: AttributeType.FLOAT,
},
{
name: 'a_index',
size: 1,
type: AttributeType.FLOAT,
},
{
name: 'a_hitColor',
size: 4,
type: AttributeType.FLOAT,
},
{
name: 'a_featureUid',
size: 1,
type: AttributeType.FLOAT,
},
].concat(customAttributes);
this.customAttributes = options.attributes ? options.attributes : [];
this.previousExtent_ = createEmpty();
/**
* This transform is updated on every frame and is the composition of:
* - invert of the world->screen transform that was used when rebuilding buffers (see `this.renderTransform_`)
* - current world->screen transform
* @type {import("../../transform.js").Transform}
* @private
*/
this.currentTransform_ = projectionMatrixTransform;
/**
* This transform is updated when buffers are rebuilt and converts world space coordinates to screen space
* @type {import("../../transform.js").Transform}
* @private
*/
this.renderTransform_ = createTransform();
/**
* @type {import("../../transform.js").Transform}
* @private
*/
this.invertRenderTransform_ = createTransform();
/**
* @type {Float32Array}
* @private
*/
this.renderInstructions_ = new Float32Array(0);
/**
* These instructions are used for hit detection
* @type {Float32Array}
* @private
*/
this.hitRenderInstructions_ = new Float32Array(0);
/**
* @type {WebGLRenderTarget}
* @private
*/
this.hitRenderTarget_;
/**
* Keep track of latest message sent to worker
* @type {number}
* @private
*/
this.generateBuffersRun_ = 0;
this.worker_ = createWebGLWorker();
this.worker_.addEventListener(
'message',
/**
* @param {*} event Event.
* @this {WebGLPointsLayerRenderer}
*/
function (event) {
const received = event.data;
if (received.type === WebGLWorkerMessageType.GENERATE_BUFFERS) {
const projectionTransform = received.projectionTransform;
if (received.hitDetection) {
this.hitVerticesBuffer_.fromArrayBuffer(received.vertexBuffer);
this.helper.flushBufferData(this.hitVerticesBuffer_);
} else {
this.verticesBuffer_.fromArrayBuffer(received.vertexBuffer);
this.helper.flushBufferData(this.verticesBuffer_);
}
this.indicesBuffer_.fromArrayBuffer(received.indexBuffer);
this.helper.flushBufferData(this.indicesBuffer_);
this.renderTransform_ = projectionTransform;
makeInverseTransform(
this.invertRenderTransform_,
this.renderTransform_
);
if (received.hitDetection) {
this.hitRenderInstructions_ = new Float32Array(
event.data.renderInstructions
);
} else {
this.renderInstructions_ = new Float32Array(
event.data.renderInstructions
);
if (received.generateBuffersRun === this.generateBuffersRun_) {
this.ready = true;
}
}
this.getLayer().changed();
}
}.bind(this)
);
/**
* This object will be updated when the source changes. Key is uid.
* @type {Object<string, FeatureCacheItem>}
* @private
*/
this.featureCache_ = {};
/**
* Amount of features in the cache.
* @type {number}
* @private
*/
this.featureCount_ = 0;
const source = this.getLayer().getSource();
this.sourceListenKeys_ = [
listen(
source,
VectorEventType.ADDFEATURE,
this.handleSourceFeatureAdded_,
this
),
listen(
source,
VectorEventType.CHANGEFEATURE,
this.handleSourceFeatureChanged_,
this
),
listen(
source,
VectorEventType.REMOVEFEATURE,
this.handleSourceFeatureDelete_,
this
),
listen(
source,
VectorEventType.CLEAR,
this.handleSourceFeatureClear_,
this
),
];
source.forEachFeature(
function (feature) {
this.featureCache_[getUid(feature)] = {
feature: feature,
properties: feature.getProperties(),
geometry: feature.getGeometry(),
};
this.featureCount_++;
}.bind(this)
);
}
afterHelperCreated() {
this.program_ = this.helper.getProgram(
this.fragmentShader_,
this.vertexShader_
);
if (this.hitDetectionEnabled_) {
this.hitProgram_ = this.helper.getProgram(
this.hitFragmentShader_,
this.hitVertexShader_
);
this.hitRenderTarget_ = new WebGLRenderTarget(this.helper);
}
}
/**
* @param {import("../../source/Vector.js").VectorSourceEvent} event Event.
* @private
*/
handleSourceFeatureAdded_(event) {
const feature = event.feature;
this.featureCache_[getUid(feature)] = {
feature: feature,
properties: feature.getProperties(),
geometry: feature.getGeometry(),
};
this.featureCount_++;
}
/**
* @param {import("../../source/Vector.js").VectorSourceEvent} event Event.
* @private
*/
handleSourceFeatureChanged_(event) {
const feature = event.feature;
this.featureCache_[getUid(feature)] = {
feature: feature,
properties: feature.getProperties(),
geometry: feature.getGeometry(),
};
}
/**
* @param {import("../../source/Vector.js").VectorSourceEvent} event Event.
* @private
*/
handleSourceFeatureDelete_(event) {
const feature = event.feature;
delete this.featureCache_[getUid(feature)];
this.featureCount_--;
}
/**
* @private
*/
handleSourceFeatureClear_() {
this.featureCache_ = {};
this.featureCount_ = 0;
}
/**
* Render the layer.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @return {HTMLElement} The rendered element.
*/
renderFrame(frameState) {
const gl = this.helper.getGL();
this.preRender(gl, frameState);
const projection = frameState.viewState.projection;
const layer = this.getLayer();
const vectorSource = layer.getSource();
// FIXME fix hit detection isn't reliable when rendering multiple worlds
const multiWorld = vectorSource.getWrapX() && projection.canWrapX();
const projectionExtent = projection.getExtent();
const extent = frameState.extent;
const worldWidth = multiWorld ? getWidth(projectionExtent) : null;
const endWorld = multiWorld
? Math.ceil((extent[2] - projectionExtent[2]) / worldWidth) + 1
: 1;
const startWorld = multiWorld
? Math.floor((extent[0] - projectionExtent[0]) / worldWidth)
: 0;
let world = startWorld;
const renderCount = this.indicesBuffer_.getSize();
do {
// apply the current projection transform with the invert of the one used to fill buffers
this.helper.makeProjectionTransform(frameState, this.currentTransform_);
translateTransform(this.currentTransform_, world * worldWidth, 0);
multiplyTransform(this.currentTransform_, this.invertRenderTransform_);
this.helper.applyUniforms(frameState);
this.helper.drawElements(0, renderCount);
} while (++world < endWorld);
this.helper.finalizeDraw(
frameState,
this.dispatchPreComposeEvent,
this.dispatchPostComposeEvent
);
const canvas = this.helper.getCanvas();
if (this.hitDetectionEnabled_) {
this.renderHitDetection(frameState, startWorld, endWorld, worldWidth);
this.hitRenderTarget_.clearCachedData();
}
this.postRender(gl, frameState);
return canvas;
}
/**
* Determine whether renderFrame should be called.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @return {boolean} Layer is ready to be rendered.
*/
prepareFrameInternal(frameState) {
const layer = this.getLayer();
const vectorSource = layer.getSource();
const viewState = frameState.viewState;
const viewNotMoving =
!frameState.viewHints[ViewHint.ANIMATING] &&
!frameState.viewHints[ViewHint.INTERACTING];
const extentChanged = !equals(this.previousExtent_, frameState.extent);
const sourceChanged = this.sourceRevision_ < vectorSource.getRevision();
if (sourceChanged) {
this.sourceRevision_ = vectorSource.getRevision();
}
if (viewNotMoving && (extentChanged || sourceChanged)) {
const projection = viewState.projection;
const resolution = viewState.resolution;
const renderBuffer =
layer instanceof BaseVector ? layer.getRenderBuffer() : 0;
const extent = buffer(frameState.extent, renderBuffer * resolution);
vectorSource.loadFeatures(extent, resolution, projection);
this.rebuildBuffers_(frameState);
this.previousExtent_ = frameState.extent.slice();
}
this.helper.useProgram(this.program_);
this.helper.prepareDraw(frameState);
// write new data
this.helper.bindBuffer(this.verticesBuffer_);
this.helper.bindBuffer(this.indicesBuffer_);
this.helper.enableAttributes(this.attributes);
return true;
}
/**
* Rebuild internal webgl buffers based on current view extent; costly, should not be called too much
* @param {import("../../PluggableMap").FrameState} frameState Frame state.
* @private
*/
rebuildBuffers_(frameState) {
// saves the projection transform for the current frame state
const projectionTransform = createTransform();
this.helper.makeProjectionTransform(frameState, projectionTransform);
// here we anticipate the amount of render instructions that we well generate
// this can be done since we know that for normal render we only have x, y as base instructions,
// and x, y, r, g, b, a and featureUid for hit render instructions
// and we also know the amount of custom attributes to append to these
const totalInstructionsCount =
(2 + this.customAttributes.length) * this.featureCount_;
if (
!this.renderInstructions_ ||
this.renderInstructions_.length !== totalInstructionsCount
) {
this.renderInstructions_ = new Float32Array(totalInstructionsCount);
}
if (this.hitDetectionEnabled_) {
const totalHitInstructionsCount =
(7 + this.customAttributes.length) * this.featureCount_;
if (
!this.hitRenderInstructions_ ||
this.hitRenderInstructions_.length !== totalHitInstructionsCount
) {
this.hitRenderInstructions_ = new Float32Array(
totalHitInstructionsCount
);
}
}
// loop on features to fill the buffer
let featureCache, geometry;
const tmpCoords = [];
const tmpColor = [];
let renderIndex = 0;
let hitIndex = 0;
let hitColor;
for (const featureUid in this.featureCache_) {
featureCache = this.featureCache_[featureUid];
geometry = /** @type {import("../../geom").Point} */ (
featureCache.geometry
);
if (!geometry || geometry.getType() !== 'Point') {
continue;
}
tmpCoords[0] = geometry.getFlatCoordinates()[0];
tmpCoords[1] = geometry.getFlatCoordinates()[1];
applyTransform(projectionTransform, tmpCoords);
hitColor = colorEncodeId(hitIndex + 6, tmpColor);
this.renderInstructions_[renderIndex++] = tmpCoords[0];
this.renderInstructions_[renderIndex++] = tmpCoords[1];
// for hit detection, the feature uid is saved in the opacity value
// and the index of the opacity value is encoded in the color values
if (this.hitDetectionEnabled_) {
this.hitRenderInstructions_[hitIndex++] = tmpCoords[0];
this.hitRenderInstructions_[hitIndex++] = tmpCoords[1];
this.hitRenderInstructions_[hitIndex++] = hitColor[0];
this.hitRenderInstructions_[hitIndex++] = hitColor[1];
this.hitRenderInstructions_[hitIndex++] = hitColor[2];
this.hitRenderInstructions_[hitIndex++] = hitColor[3];
this.hitRenderInstructions_[hitIndex++] = Number(featureUid);
}
// pushing custom attributes
let value;
for (let j = 0; j < this.customAttributes.length; j++) {
value = this.customAttributes[j].callback(
featureCache.feature,
featureCache.properties
);
this.renderInstructions_[renderIndex++] = value;
if (this.hitDetectionEnabled_) {
this.hitRenderInstructions_[hitIndex++] = value;
}
}
}
/** @type {import('./Layer').WebGLWorkerGenerateBuffersMessage} */
const message = {
type: WebGLWorkerMessageType.GENERATE_BUFFERS,
renderInstructions: this.renderInstructions_.buffer,
customAttributesCount: this.customAttributes.length,
};
// additional properties will be sent back as-is by the worker
message['projectionTransform'] = projectionTransform;
message['generateBuffersRun'] = ++this.generateBuffersRun_;
this.ready = false;
this.worker_.postMessage(message, [this.renderInstructions_.buffer]);
this.renderInstructions_ = null;
/** @type {import('./Layer').WebGLWorkerGenerateBuffersMessage} */
if (this.hitDetectionEnabled_) {
const hitMessage = {
type: WebGLWorkerMessageType.GENERATE_BUFFERS,
renderInstructions: this.hitRenderInstructions_.buffer,
customAttributesCount: 5 + this.customAttributes.length,
};
hitMessage['projectionTransform'] = projectionTransform;
hitMessage['hitDetection'] = true;
this.worker_.postMessage(hitMessage, [
this.hitRenderInstructions_.buffer,
]);
this.hitRenderInstructions_ = null;
}
}
/**
* @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<T>} callback Feature callback.
* @param {Array<import("../Map.js").HitMatch<T>>} matches The hit detected matches with tolerance.
* @return {T|undefined} Callback result.
* @template T
*/
forEachFeatureAtCoordinate(
coordinate,
frameState,
hitTolerance,
callback,
matches
) {
assert(this.hitDetectionEnabled_, 66);
if (!this.hitRenderInstructions_) {
return undefined;
}
const pixel = applyTransform(
frameState.coordinateToPixelTransform,
coordinate.slice()
);
const data = this.hitRenderTarget_.readPixel(pixel[0] / 2, pixel[1] / 2);
const color = [data[0] / 255, data[1] / 255, data[2] / 255, data[3] / 255];
const index = colorDecodeId(color);
const opacity = this.hitRenderInstructions_[index];
const uid = Math.floor(opacity).toString();
const source = this.getLayer().getSource();
const feature = source.getFeatureByUid(uid);
if (feature) {
return callback(feature, this.getLayer(), null);
}
return undefined;
}
/**
* Render the hit detection data to the corresponding render target
* @param {import("../../PluggableMap.js").FrameState} frameState current frame state
* @param {number} startWorld the world to render in the first iteration
* @param {number} endWorld the last world to render
* @param {number} worldWidth the width of the worlds being rendered
*/
renderHitDetection(frameState, startWorld, endWorld, worldWidth) {
// skip render entirely if vertex buffers not ready/generated yet
if (!this.hitVerticesBuffer_.getSize()) {
return;
}
let world = startWorld;
this.hitRenderTarget_.setSize([
Math.floor(frameState.size[0] / 2),
Math.floor(frameState.size[1] / 2),
]);
this.helper.useProgram(this.hitProgram_);
this.helper.prepareDrawToRenderTarget(
frameState,
this.hitRenderTarget_,
true
);
this.helper.bindBuffer(this.hitVerticesBuffer_);
this.helper.bindBuffer(this.indicesBuffer_);
this.helper.enableAttributes(this.hitDetectionAttributes);
do {
this.helper.makeProjectionTransform(frameState, this.currentTransform_);
translateTransform(this.currentTransform_, world * worldWidth, 0);
multiplyTransform(this.currentTransform_, this.invertRenderTransform_);
this.helper.applyUniforms(frameState);
const renderCount = this.indicesBuffer_.getSize();
this.helper.drawElements(0, renderCount);
} while (++world < endWorld);
}
/**
* Clean up.
*/
disposeInternal() {
this.worker_.terminate();
this.layer_ = null;
this.sourceListenKeys_.forEach(function (key) {
unlistenByKey(key);
});
this.sourceListenKeys_ = null;
super.disposeInternal();
}
}
export default WebGLPointsLayerRenderer;

850
node_modules/ol/src/renderer/webgl/TileLayer.js generated vendored Normal file
View File

@@ -0,0 +1,850 @@
/**
* @module ol/renderer/webgl/TileLayer
*/
import LRUCache from '../../structs/LRUCache.js';
import TileRange from '../../TileRange.js';
import TileState from '../../TileState.js';
import TileTexture from '../../webgl/TileTexture.js';
import WebGLArrayBuffer from '../../webgl/Buffer.js';
import WebGLLayerRenderer from './Layer.js';
import {AttributeType} from '../../webgl/Helper.js';
import {ELEMENT_ARRAY_BUFFER, STATIC_DRAW} from '../../webgl.js';
import {
apply as applyTransform,
create as createTransform,
reset as resetTransform,
rotate as rotateTransform,
scale as scaleTransform,
translate as translateTransform,
} from '../../transform.js';
import {
boundingExtent,
containsCoordinate,
getIntersection,
isEmpty,
} from '../../extent.js';
import {
create as createMat4,
fromTransform as mat4FromTransform,
} from '../../vec/mat4.js';
import {
createOrUpdate as createTileCoord,
getKey as getTileCoordKey,
} from '../../tilecoord.js';
import {fromUserExtent} from '../../proj.js';
import {getUid} from '../../util.js';
import {numberSafeCompareFunction} from '../../array.js';
import {toSize} from '../../size.js';
export const Uniforms = {
TILE_TEXTURE_ARRAY: 'u_tileTextures',
TILE_TRANSFORM: 'u_tileTransform',
TRANSITION_ALPHA: 'u_transitionAlpha',
DEPTH: 'u_depth',
TEXTURE_PIXEL_WIDTH: 'u_texturePixelWidth',
TEXTURE_PIXEL_HEIGHT: 'u_texturePixelHeight',
TEXTURE_RESOLUTION: 'u_textureResolution', // map units per texture pixel
TEXTURE_ORIGIN_X: 'u_textureOriginX', // map x coordinate of left edge of texture
TEXTURE_ORIGIN_Y: 'u_textureOriginY', // map y coordinate of top edge of texture
RENDER_EXTENT: 'u_renderExtent', // intersection of layer, source, and view extent
RESOLUTION: 'u_resolution',
ZOOM: 'u_zoom',
};
export const Attributes = {
TEXTURE_COORD: 'a_textureCoord',
};
/**
* @type {Array<import('../../webgl/Helper.js').AttributeDescription>}
*/
const attributeDescriptions = [
{
name: Attributes.TEXTURE_COORD,
size: 2,
type: AttributeType.FLOAT,
},
];
/**
* @type {Object<string, boolean>}
*/
const empty = {};
/**
* Transform a zoom level into a depth value ranging from -1 to 1.
* @param {number} z A zoom level.
* @return {number} A depth value.
*/
function depthForZ(z) {
return 2 * (1 - 1 / (z + 1)) - 1;
}
/**
* Add a tile texture to the lookup.
* @param {Object<number, Array<import("../../webgl/TileTexture.js").default>>} tileTexturesByZ Lookup of
* tile textures by zoom level.
* @param {import("../../webgl/TileTexture.js").default} tileTexture A tile texture.
* @param {number} z The zoom level.
*/
function addTileTextureToLookup(tileTexturesByZ, tileTexture, z) {
if (!(z in tileTexturesByZ)) {
tileTexturesByZ[z] = [];
}
tileTexturesByZ[z].push(tileTexture);
}
/**
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {import("../../extent.js").Extent} extent The frame extent.
* @return {import("../../extent.js").Extent} Frame extent intersected with layer extents.
*/
function getRenderExtent(frameState, extent) {
const layerState = frameState.layerStatesArray[frameState.layerIndex];
if (layerState.extent) {
extent = getIntersection(
extent,
fromUserExtent(layerState.extent, frameState.viewState.projection)
);
}
const source = /** @type {import("../../source/Tile.js").default} */ (
layerState.layer.getRenderSource()
);
if (!source.getWrapX()) {
const gridExtent = source
.getTileGridForProjection(frameState.viewState.projection)
.getExtent();
if (gridExtent) {
extent = getIntersection(extent, gridExtent);
}
}
return extent;
}
function getCacheKey(source, tileCoord) {
return `${source.getKey()},${getTileCoordKey(tileCoord)}`;
}
/**
* @typedef {Object} Options
* @property {string} vertexShader Vertex shader source.
* @property {string} fragmentShader Fragment shader source.
* @property {Object<string, import("../../webgl/Helper").UniformValue>} [uniforms] Additional uniforms
* made available to shaders.
* @property {Array<import("../../webgl/PaletteTexture.js").default>} [paletteTextures] Palette textures.
* @property {number} [cacheSize=512] The texture cache size.
*/
/**
* @typedef {import("../../layer/WebGLTile.js").default} LayerType
*/
/**
* @classdesc
* WebGL renderer for tile layers.
* @extends {WebGLLayerRenderer<LayerType>}
* @api
*/
class WebGLTileLayerRenderer extends WebGLLayerRenderer {
/**
* @param {LayerType} tileLayer Tile layer.
* @param {Options} options Options.
*/
constructor(tileLayer, options) {
super(tileLayer, {
uniforms: options.uniforms,
});
/**
* The last call to `renderFrame` was completed with all tiles loaded
* @type {boolean}
*/
this.renderComplete = false;
/**
* This transform converts texture coordinates to screen coordinates.
* @type {import("../../transform.js").Transform}
* @private
*/
this.tileTransform_ = createTransform();
/**
* @type {Array<number>}
* @private
*/
this.tempMat4_ = createMat4();
/**
* @type {import("../../TileRange.js").default}
* @private
*/
this.tempTileRange_ = new TileRange(0, 0, 0, 0);
/**
* @type {import("../../tilecoord.js").TileCoord}
* @private
*/
this.tempTileCoord_ = createTileCoord(0, 0, 0);
/**
* @type {import("../../size.js").Size}
* @private
*/
this.tempSize_ = [0, 0];
/**
* @type {WebGLProgram}
* @private
*/
this.program_;
/**
* @private
*/
this.vertexShader_ = options.vertexShader;
/**
* @private
*/
this.fragmentShader_ = options.fragmentShader;
/**
* Tiles are rendered as a quad with the following structure:
*
* [P3]---------[P2]
* |` |
* | ` B |
* | ` |
* | ` |
* | A ` |
* | ` |
* [P0]---------[P1]
*
* Triangle A: P0, P1, P3
* Triangle B: P1, P2, P3
*
* @private
*/
this.indices_ = new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, STATIC_DRAW);
this.indices_.fromArray([0, 1, 3, 1, 2, 3]);
const cacheSize = options.cacheSize !== undefined ? options.cacheSize : 512;
/**
* @type {import("../../structs/LRUCache.js").default<import("../../webgl/TileTexture.js").default>}
* @private
*/
this.tileTextureCache_ = new LRUCache(cacheSize);
/**
* @type {Array<import("../../webgl/PaletteTexture.js").default>}
* @private
*/
this.paletteTextures_ = options.paletteTextures || [];
/**
* @private
* @type {import("../../PluggableMap.js").FrameState|null}
*/
this.frameState_ = null;
}
/**
* @param {Options} options Options.
*/
reset(options) {
super.reset({
uniforms: options.uniforms,
});
this.vertexShader_ = options.vertexShader;
this.fragmentShader_ = options.fragmentShader;
this.paletteTextures_ = options.paletteTextures || [];
if (this.helper) {
this.program_ = this.helper.getProgram(
this.fragmentShader_,
this.vertexShader_
);
}
}
afterHelperCreated() {
this.program_ = this.helper.getProgram(
this.fragmentShader_,
this.vertexShader_
);
this.helper.flushBufferData(this.indices_);
}
/**
* @param {import("../../webgl/TileTexture").TileType} tile Tile.
* @return {boolean} Tile is drawable.
* @private
*/
isDrawableTile_(tile) {
const tileLayer = this.getLayer();
const tileState = tile.getState();
const useInterimTilesOnError = tileLayer.getUseInterimTilesOnError();
return (
tileState == TileState.LOADED ||
tileState == TileState.EMPTY ||
(tileState == TileState.ERROR && !useInterimTilesOnError)
);
}
/**
* Determine whether renderFrame should be called.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @return {boolean} Layer is ready to be rendered.
*/
prepareFrameInternal(frameState) {
const layer = this.getLayer();
const source = layer.getRenderSource();
if (!source) {
return false;
}
if (isEmpty(getRenderExtent(frameState, frameState.extent))) {
return false;
}
return source.getState() === 'ready';
}
/**
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @param {import("../../extent.js").Extent} extent The extent to be rendered.
* @param {number} initialZ The zoom level.
* @param {Object<number, Array<TileTexture>>} tileTexturesByZ The zoom level.
*/
enqueueTiles(frameState, extent, initialZ, tileTexturesByZ) {
const viewState = frameState.viewState;
const tileLayer = this.getLayer();
const tileSource = tileLayer.getRenderSource();
const tileGrid = tileSource.getTileGridForProjection(viewState.projection);
const gutter = tileSource.getGutterForProjection(viewState.projection);
const tileSourceKey = getUid(tileSource);
if (!(tileSourceKey in frameState.wantedTiles)) {
frameState.wantedTiles[tileSourceKey] = {};
}
const wantedTiles = frameState.wantedTiles[tileSourceKey];
const tileTextureCache = this.tileTextureCache_;
const minZ = Math.max(
initialZ - tileLayer.getPreload(),
tileGrid.getMinZoom(),
tileLayer.getMinZoom()
);
for (let z = initialZ; z >= minZ; --z) {
const tileRange = tileGrid.getTileRangeForExtentAndZ(
extent,
z,
this.tempTileRange_
);
const tileResolution = tileGrid.getResolution(z);
for (let x = tileRange.minX; x <= tileRange.maxX; ++x) {
for (let y = tileRange.minY; y <= tileRange.maxY; ++y) {
const tileCoord = createTileCoord(z, x, y, this.tempTileCoord_);
const cacheKey = getCacheKey(tileSource, tileCoord);
/** @type {TileTexture} */
let tileTexture;
/** @type {import("../../webgl/TileTexture").TileType} */
let tile;
if (tileTextureCache.containsKey(cacheKey)) {
tileTexture = tileTextureCache.get(cacheKey);
tile = tileTexture.tile;
}
if (!tileTexture || tileTexture.tile.key !== tileSource.getKey()) {
tile = tileSource.getTile(
z,
x,
y,
frameState.pixelRatio,
viewState.projection
);
if (!tileTexture) {
tileTexture = new TileTexture({
tile: tile,
grid: tileGrid,
helper: this.helper,
gutter: gutter,
});
tileTextureCache.set(cacheKey, tileTexture);
} else {
if (this.isDrawableTile_(tile)) {
tileTexture.setTile(tile);
} else {
const interimTile =
/** @type {import("../../webgl/TileTexture").TileType} */ (
tile.getInterimTile()
);
tileTexture.setTile(interimTile);
}
}
}
addTileTextureToLookup(tileTexturesByZ, tileTexture, z);
const tileQueueKey = tile.getKey();
wantedTiles[tileQueueKey] = true;
if (tile.getState() === TileState.IDLE) {
if (!frameState.tileQueue.isKeyQueued(tileQueueKey)) {
frameState.tileQueue.enqueue([
tile,
tileSourceKey,
tileGrid.getTileCoordCenter(tileCoord),
tileResolution,
]);
}
}
}
}
}
}
/**
* Render the layer.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @return {HTMLElement} The rendered element.
*/
renderFrame(frameState) {
this.frameState_ = frameState;
this.renderComplete = true;
const gl = this.helper.getGL();
this.preRender(gl, frameState);
const viewState = frameState.viewState;
const tileLayer = this.getLayer();
const tileSource = tileLayer.getRenderSource();
const tileGrid = tileSource.getTileGridForProjection(viewState.projection);
const gutter = tileSource.getGutterForProjection(viewState.projection);
const extent = getRenderExtent(frameState, frameState.extent);
const z = tileGrid.getZForResolution(
viewState.resolution,
tileSource.zDirection
);
/**
* @type {Object<number, Array<import("../../webgl/TileTexture.js").default>>}
*/
const tileTexturesByZ = {};
if (frameState.nextExtent) {
const targetZ = tileGrid.getZForResolution(
viewState.nextResolution,
tileSource.zDirection
);
const nextExtent = getRenderExtent(frameState, frameState.nextExtent);
this.enqueueTiles(frameState, nextExtent, targetZ, tileTexturesByZ);
}
this.enqueueTiles(frameState, extent, z, tileTexturesByZ);
/**
* A lookup of alpha values for tiles at the target rendering resolution
* for tiles that are in transition. If a tile coord key is absent from
* this lookup, the tile should be rendered at alpha 1.
* @type {Object<string, number>}
*/
const alphaLookup = {};
const uid = getUid(this);
const time = frameState.time;
let blend = false;
// look for cached tiles to use if a target tile is not ready
const tileTextures = tileTexturesByZ[z];
for (let i = 0, ii = tileTextures.length; i < ii; ++i) {
const tileTexture = tileTextures[i];
const tile = tileTexture.tile;
const tileCoord = tile.tileCoord;
if (tileTexture.loaded) {
const alpha = tile.getAlpha(uid, time);
if (alpha === 1) {
// no need to look for alt tiles
tile.endTransition(uid);
continue;
}
blend = true;
const tileCoordKey = getTileCoordKey(tileCoord);
alphaLookup[tileCoordKey] = alpha;
}
this.renderComplete = false;
// first look for child tiles (at z + 1)
const coveredByChildren = this.findAltTiles_(
tileGrid,
tileCoord,
z + 1,
tileTexturesByZ
);
if (coveredByChildren) {
continue;
}
// next look for parent tiles
const minZoom = tileGrid.getMinZoom();
for (let parentZ = z - 1; parentZ >= minZoom; --parentZ) {
const coveredByParent = this.findAltTiles_(
tileGrid,
tileCoord,
parentZ,
tileTexturesByZ
);
if (coveredByParent) {
break;
}
}
}
this.helper.useProgram(this.program_);
this.helper.prepareDraw(frameState, !blend);
const zs = Object.keys(tileTexturesByZ)
.map(Number)
.sort(numberSafeCompareFunction);
const centerX = viewState.center[0];
const centerY = viewState.center[1];
for (let j = 0, jj = zs.length; j < jj; ++j) {
const tileZ = zs[j];
const tileResolution = tileGrid.getResolution(tileZ);
const tileSize = toSize(tileGrid.getTileSize(tileZ), this.tempSize_);
const tileOrigin = tileGrid.getOrigin(tileZ);
const tileWidthWithGutter = tileSize[0] + 2 * gutter;
const tileHeightWithGutter = tileSize[1] + 2 * gutter;
const aspectRatio = tileWidthWithGutter / tileHeightWithGutter;
const centerI =
(centerX - tileOrigin[0]) / (tileSize[0] * tileResolution);
const centerJ =
(tileOrigin[1] - centerY) / (tileSize[1] * tileResolution);
const tileScale = viewState.resolution / tileResolution;
const depth = depthForZ(tileZ);
const tileTextures = tileTexturesByZ[tileZ];
for (let i = 0, ii = tileTextures.length; i < ii; ++i) {
const tileTexture = tileTextures[i];
if (!tileTexture.loaded) {
continue;
}
const tile = tileTexture.tile;
const tileCoord = tile.tileCoord;
const tileCoordKey = getTileCoordKey(tileCoord);
const tileCenterI = tileCoord[1];
const tileCenterJ = tileCoord[2];
resetTransform(this.tileTransform_);
scaleTransform(
this.tileTransform_,
2 / ((frameState.size[0] * tileScale) / tileWidthWithGutter),
-2 / ((frameState.size[1] * tileScale) / tileWidthWithGutter)
);
rotateTransform(this.tileTransform_, viewState.rotation);
scaleTransform(this.tileTransform_, 1, 1 / aspectRatio);
translateTransform(
this.tileTransform_,
(tileSize[0] * (tileCenterI - centerI) - gutter) /
tileWidthWithGutter,
(tileSize[1] * (tileCenterJ - centerJ) - gutter) /
tileHeightWithGutter
);
this.helper.setUniformMatrixValue(
Uniforms.TILE_TRANSFORM,
mat4FromTransform(this.tempMat4_, this.tileTransform_)
);
this.helper.bindBuffer(tileTexture.coords);
this.helper.bindBuffer(this.indices_);
this.helper.enableAttributes(attributeDescriptions);
let textureSlot = 0;
while (textureSlot < tileTexture.textures.length) {
const textureProperty = 'TEXTURE' + textureSlot;
const uniformName = `${Uniforms.TILE_TEXTURE_ARRAY}[${textureSlot}]`;
gl.activeTexture(gl[textureProperty]);
gl.bindTexture(gl.TEXTURE_2D, tileTexture.textures[textureSlot]);
gl.uniform1i(
this.helper.getUniformLocation(uniformName),
textureSlot
);
++textureSlot;
}
for (
let paletteIndex = 0;
paletteIndex < this.paletteTextures_.length;
++paletteIndex
) {
const paletteTexture = this.paletteTextures_[paletteIndex];
gl.activeTexture(gl['TEXTURE' + textureSlot]);
const texture = paletteTexture.getTexture(gl);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.uniform1i(
this.helper.getUniformLocation(paletteTexture.name),
textureSlot
);
++textureSlot;
}
const alpha =
tileCoordKey in alphaLookup ? alphaLookup[tileCoordKey] : 1;
if (alpha < 1) {
frameState.animate = true;
}
this.helper.setUniformFloatValue(Uniforms.TRANSITION_ALPHA, alpha);
this.helper.setUniformFloatValue(Uniforms.DEPTH, depth);
this.helper.setUniformFloatValue(
Uniforms.TEXTURE_PIXEL_WIDTH,
tileWidthWithGutter
);
this.helper.setUniformFloatValue(
Uniforms.TEXTURE_PIXEL_HEIGHT,
tileHeightWithGutter
);
this.helper.setUniformFloatValue(
Uniforms.TEXTURE_RESOLUTION,
tileResolution
);
this.helper.setUniformFloatValue(
Uniforms.TEXTURE_ORIGIN_X,
tileOrigin[0] +
tileCenterI * tileSize[0] * tileResolution -
gutter * tileResolution
);
this.helper.setUniformFloatValue(
Uniforms.TEXTURE_ORIGIN_Y,
tileOrigin[1] -
tileCenterJ * tileSize[1] * tileResolution +
gutter * tileResolution
);
let gutterExtent = extent;
if (gutter > 0) {
gutterExtent = tileGrid.getTileCoordExtent(tileCoord);
getIntersection(gutterExtent, extent, gutterExtent);
}
this.helper.setUniformFloatVec4(Uniforms.RENDER_EXTENT, gutterExtent);
this.helper.setUniformFloatValue(
Uniforms.RESOLUTION,
viewState.resolution
);
this.helper.setUniformFloatValue(Uniforms.ZOOM, viewState.zoom);
this.helper.drawElements(0, this.indices_.getSize());
}
}
this.helper.finalizeDraw(
frameState,
this.dispatchPreComposeEvent,
this.dispatchPostComposeEvent
);
const canvas = this.helper.getCanvas();
const tileTextureCache = this.tileTextureCache_;
while (tileTextureCache.canExpireCache()) {
const tileTexture = tileTextureCache.pop();
tileTexture.dispose();
}
// TODO: let the renderers manage their own cache instead of managing the source cache
/**
* Here we unconditionally expire the source cache since the renderer maintains
* its own cache.
* @param {import("../../PluggableMap.js").default} map Map.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
*/
const postRenderFunction = function (map, frameState) {
tileSource.expireCache(frameState.viewState.projection, empty);
};
frameState.postRenderFunctions.push(postRenderFunction);
this.postRender(gl, frameState);
return canvas;
}
/**
* @param {import("../../pixel.js").Pixel} pixel Pixel.
* @return {Uint8ClampedArray|Uint8Array|Float32Array|DataView} Data at the pixel location.
*/
getData(pixel) {
const gl = this.helper.getGL();
if (!gl) {
return null;
}
const frameState = this.frameState_;
if (!frameState) {
return null;
}
const layer = this.getLayer();
const coordinate = applyTransform(
frameState.pixelToCoordinateTransform,
pixel.slice()
);
const viewState = frameState.viewState;
const layerExtent = layer.getExtent();
if (layerExtent) {
if (
!containsCoordinate(
fromUserExtent(layerExtent, viewState.projection),
coordinate
)
) {
return null;
}
}
// determine last source suitable for rendering at coordinate
const sources = layer.getSources(
boundingExtent([coordinate]),
viewState.resolution
);
let i, source, tileGrid;
for (i = sources.length - 1; i >= 0; --i) {
source = sources[i];
if (source.getState() === 'ready') {
tileGrid = source.getTileGridForProjection(viewState.projection);
if (source.getWrapX()) {
break;
}
const gridExtent = tileGrid.getExtent();
if (!gridExtent || containsCoordinate(gridExtent, coordinate)) {
break;
}
}
}
if (i < 0) {
return null;
}
const tileTextureCache = this.tileTextureCache_;
for (
let z = tileGrid.getZForResolution(viewState.resolution);
z >= tileGrid.getMinZoom();
--z
) {
const tileCoord = tileGrid.getTileCoordForCoordAndZ(coordinate, z);
const cacheKey = getCacheKey(source, tileCoord);
if (!tileTextureCache.containsKey(cacheKey)) {
continue;
}
const tileTexture = tileTextureCache.get(cacheKey);
if (!tileTexture.loaded) {
continue;
}
const tileOrigin = tileGrid.getOrigin(z);
const tileSize = toSize(tileGrid.getTileSize(z));
const tileResolution = tileGrid.getResolution(z);
const col =
(coordinate[0] - tileOrigin[0]) / tileResolution -
tileCoord[1] * tileSize[0];
const row =
(tileOrigin[1] - coordinate[1]) / tileResolution -
tileCoord[2] * tileSize[1];
return tileTexture.getPixelData(col, row);
}
return null;
}
/**
* Look for tiles covering the provided tile coordinate at an alternate
* zoom level. Loaded tiles will be added to the provided tile texture lookup.
* @param {import("../../tilegrid/TileGrid.js").default} tileGrid The tile grid.
* @param {import("../../tilecoord.js").TileCoord} tileCoord The target tile coordinate.
* @param {number} altZ The alternate zoom level.
* @param {Object<number, Array<import("../../webgl/TileTexture.js").default>>} tileTexturesByZ Lookup of
* tile textures by zoom level.
* @return {boolean} The tile coordinate is covered by loaded tiles at the alternate zoom level.
* @private
*/
findAltTiles_(tileGrid, tileCoord, altZ, tileTexturesByZ) {
const tileRange = tileGrid.getTileRangeForTileCoordAndZ(
tileCoord,
altZ,
this.tempTileRange_
);
if (!tileRange) {
return false;
}
let covered = true;
const tileTextureCache = this.tileTextureCache_;
const source = this.getLayer().getRenderSource();
for (let x = tileRange.minX; x <= tileRange.maxX; ++x) {
for (let y = tileRange.minY; y <= tileRange.maxY; ++y) {
const cacheKey = getCacheKey(source, [altZ, x, y]);
let loaded = false;
if (tileTextureCache.containsKey(cacheKey)) {
const tileTexture = tileTextureCache.get(cacheKey);
if (tileTexture.loaded) {
addTileTextureToLookup(tileTexturesByZ, tileTexture, altZ);
loaded = true;
}
}
if (!loaded) {
covered = false;
}
}
}
return covered;
}
removeHelper() {
if (this.helper) {
const tileTextureCache = this.tileTextureCache_;
tileTextureCache.forEach((tileTexture) => tileTexture.dispose());
tileTextureCache.clear();
}
super.removeHelper();
}
/**
* Clean up.
*/
disposeInternal() {
const helper = this.helper;
if (helper) {
const gl = helper.getGL();
gl.deleteProgram(this.program_);
delete this.program_;
helper.deleteBuffer(this.indices_);
}
super.disposeInternal();
delete this.indices_;
delete this.tileTextureCache_;
delete this.frameState_;
}
}
export default WebGLTileLayerRenderer;