This commit is contained in:
198
node_modules/ol/src/renderer/Composite.js
generated
vendored
Normal file
198
node_modules/ol/src/renderer/Composite.js
generated
vendored
Normal 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
212
node_modules/ol/src/renderer/Layer.js
generated
vendored
Normal 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
261
node_modules/ol/src/renderer/Map.js
generated
vendored
Normal 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
275
node_modules/ol/src/renderer/canvas/ImageLayer.js
generated
vendored
Normal 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
386
node_modules/ol/src/renderer/canvas/Layer.js
generated
vendored
Normal 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
772
node_modules/ol/src/renderer/canvas/TileLayer.js
generated
vendored
Normal 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
232
node_modules/ol/src/renderer/canvas/VectorImageLayer.js
generated
vendored
Normal 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
800
node_modules/ol/src/renderer/canvas/VectorLayer.js
generated
vendored
Normal 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
859
node_modules/ol/src/renderer/canvas/VectorTileLayer.js
generated
vendored
Normal 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
21
node_modules/ol/src/renderer/canvas/common.js
generated
vendored
Normal 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
484
node_modules/ol/src/renderer/vector.js
generated
vendored
Normal 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
486
node_modules/ol/src/renderer/webgl/Layer.js
generated
vendored
Normal 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
765
node_modules/ol/src/renderer/webgl/PointsLayer.js
generated
vendored
Normal 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
850
node_modules/ol/src/renderer/webgl/TileLayer.js
generated
vendored
Normal 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;
|
||||
Reference in New Issue
Block a user