package.reproj.DataTile.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ol Show documentation
Show all versions of ol Show documentation
OpenLayers mapping library
The newest version!
/**
* @module ol/reproj/DataTile
*/
import {ERROR_THRESHOLD} from './common.js';
import DataTile, {asArrayLike, asImageLike, toArray} from '../DataTile.js';
import EventType from '../events/EventType.js';
import TileState from '../TileState.js';
import Triangulation from './Triangulation.js';
import {
calculateSourceExtentResolution,
canvasPool,
render as renderReprojected,
} from '../reproj.js';
import {clamp} from '../math.js';
import {createCanvasContext2D, releaseCanvas} from '../dom.js';
import {getArea, getIntersection, getWidth, wrapAndSliceX} from '../extent.js';
import {listen, unlistenByKey} from '../events.js';
/**
* @typedef {function(number, number, number, number) : import("../DataTile.js").default} TileGetter
*/
/**
* @typedef {Object} TileOffset
* @property {DataTile} tile Tile.
* @property {number} offset Offset.
*/
/**
* @typedef {Object} Options
* @property {import("../proj/Projection.js").default} sourceProj Source projection.
* @property {import("../tilegrid/TileGrid.js").default} sourceTileGrid Source tile grid.
* @property {import("../proj/Projection.js").default} targetProj Target projection.
* @property {import("../tilegrid/TileGrid.js").default} targetTileGrid Target tile grid.
* @property {import("../tilecoord.js").TileCoord} tileCoord Coordinate of the tile.
* @property {import("../tilecoord.js").TileCoord} [wrappedTileCoord] Coordinate of the tile wrapped in X.
* @property {number} pixelRatio Pixel ratio.
* @property {number} gutter Gutter of the source tiles.
* @property {TileGetter} getTileFunction Function returning source tiles (z, x, y, pixelRatio).
* @property {boolean} [interpolate=false] Use interpolated values when resampling. By default,
* the nearest neighbor is used when resampling.
* @property {number} [errorThreshold] Acceptable reprojection error (in px).
* @property {number} [transition=250] A duration for tile opacity
* transitions in milliseconds. A duration of 0 disables the opacity transition.
*/
/**
* @classdesc
* Class encapsulating single reprojected data tile.
* See {@link module:ol/source/DataTile~DataTileSource}.
*
*/
class ReprojDataTile extends DataTile {
/**
* @param {Options} options Tile options.
*/
constructor(options) {
super({
tileCoord: options.tileCoord,
loader: () => Promise.resolve(new Uint8ClampedArray(4)),
interpolate: options.interpolate,
transition: options.transition,
});
/**
* @private
* @type {number}
*/
this.pixelRatio_ = options.pixelRatio;
/**
* @private
* @type {number}
*/
this.gutter_ = options.gutter;
/**
* @type {import("../DataTile.js").Data}
* @private
*/
this.reprojData_ = null;
/**
* @type {Error}
* @private
*/
this.reprojError_ = null;
/**
* @type {import('../size.js').Size}
* @private
*/
this.reprojSize_ = undefined;
/**
* @private
* @type {import("../tilegrid/TileGrid.js").default}
*/
this.sourceTileGrid_ = options.sourceTileGrid;
/**
* @private
* @type {import("../tilegrid/TileGrid.js").default}
*/
this.targetTileGrid_ = options.targetTileGrid;
/**
* @private
* @type {import("../tilecoord.js").TileCoord}
*/
this.wrappedTileCoord_ = options.wrappedTileCoord || options.tileCoord;
/**
* @private
* @type {!Array}
*/
this.sourceTiles_ = [];
/**
* @private
* @type {?Array}
*/
this.sourcesListenerKeys_ = null;
/**
* @private
* @type {number}
*/
this.sourceZ_ = 0;
const sourceProj = options.sourceProj;
const sourceProjExtent = sourceProj.getExtent();
const sourceTileGridExtent = options.sourceTileGrid.getExtent();
/**
* @private
* @type {import("../extent.js").Extent}
*/
this.clipExtent_ = sourceProj.canWrapX()
? sourceTileGridExtent
? getIntersection(sourceProjExtent, sourceTileGridExtent)
: sourceProjExtent
: sourceTileGridExtent;
const targetExtent = this.targetTileGrid_.getTileCoordExtent(
this.wrappedTileCoord_,
);
const maxTargetExtent = this.targetTileGrid_.getExtent();
let maxSourceExtent = this.sourceTileGrid_.getExtent();
const limitedTargetExtent = maxTargetExtent
? getIntersection(targetExtent, maxTargetExtent)
: targetExtent;
if (getArea(limitedTargetExtent) === 0) {
// Tile is completely outside range -> EMPTY
// TODO: is it actually correct that the source even creates the tile ?
this.state = TileState.EMPTY;
return;
}
if (sourceProjExtent) {
if (!maxSourceExtent) {
maxSourceExtent = sourceProjExtent;
} else {
maxSourceExtent = getIntersection(maxSourceExtent, sourceProjExtent);
}
}
const targetResolution = this.targetTileGrid_.getResolution(
this.wrappedTileCoord_[0],
);
const targetProj = options.targetProj;
const sourceResolution = calculateSourceExtentResolution(
sourceProj,
targetProj,
limitedTargetExtent,
targetResolution,
);
if (!isFinite(sourceResolution) || sourceResolution <= 0) {
// invalid sourceResolution -> EMPTY
// probably edges of the projections when no extent is defined
this.state = TileState.EMPTY;
return;
}
const errorThresholdInPixels =
options.errorThreshold !== undefined
? options.errorThreshold
: ERROR_THRESHOLD;
/**
* @private
* @type {!import("./Triangulation.js").default}
*/
this.triangulation_ = new Triangulation(
sourceProj,
targetProj,
limitedTargetExtent,
maxSourceExtent,
sourceResolution * errorThresholdInPixels,
targetResolution,
);
if (this.triangulation_.getTriangles().length === 0) {
// no valid triangles -> EMPTY
this.state = TileState.EMPTY;
return;
}
this.sourceZ_ = this.sourceTileGrid_.getZForResolution(sourceResolution);
let sourceExtent = this.triangulation_.calculateSourceExtent();
if (maxSourceExtent) {
if (sourceProj.canWrapX()) {
sourceExtent[1] = clamp(
sourceExtent[1],
maxSourceExtent[1],
maxSourceExtent[3],
);
sourceExtent[3] = clamp(
sourceExtent[3],
maxSourceExtent[1],
maxSourceExtent[3],
);
} else {
sourceExtent = getIntersection(sourceExtent, maxSourceExtent);
}
}
if (!getArea(sourceExtent)) {
this.state = TileState.EMPTY;
} else {
let worldWidth = 0;
let worldsAway = 0;
if (sourceProj.canWrapX()) {
worldWidth = getWidth(sourceProjExtent);
worldsAway = Math.floor(
(sourceExtent[0] - sourceProjExtent[0]) / worldWidth,
);
}
const sourceExtents = wrapAndSliceX(
sourceExtent.slice(),
sourceProj,
true,
);
sourceExtents.forEach((extent) => {
const sourceRange = this.sourceTileGrid_.getTileRangeForExtentAndZ(
extent,
this.sourceZ_,
);
const getTile = options.getTileFunction;
for (let srcX = sourceRange.minX; srcX <= sourceRange.maxX; srcX++) {
for (let srcY = sourceRange.minY; srcY <= sourceRange.maxY; srcY++) {
const tile = getTile(this.sourceZ_, srcX, srcY, this.pixelRatio_);
if (tile) {
const offset = worldsAway * worldWidth;
this.sourceTiles_.push({tile, offset});
}
}
}
++worldsAway;
});
if (this.sourceTiles_.length === 0) {
this.state = TileState.EMPTY;
}
}
}
/**
* Get the tile size.
* @return {import('../size.js').Size} Tile size.
* @override
*/
getSize() {
return this.reprojSize_;
}
/**
* Get the data for the tile.
* @return {import("../DataTile.js").Data} Tile data.
* @override
*/
getData() {
return this.reprojData_;
}
/**
* Get any loading error.
* @return {Error} Loading error.
* @override
*/
getError() {
return this.reprojError_;
}
/**
* @private
*/
reproject_() {
const dataSources = [];
let imageLike = false;
this.sourceTiles_.forEach((source) => {
const tile = source.tile;
if (!tile || tile.getState() !== TileState.LOADED) {
return;
}
const size = tile.getSize();
const gutter = this.gutter_;
/**
* @type {import("../DataTile.js").ArrayLike}
*/
let tileData;
const arrayData = asArrayLike(tile.getData());
if (arrayData) {
tileData = arrayData;
} else {
imageLike = true;
tileData = toArray(asImageLike(tile.getData()));
}
const pixelSize = [size[0] + 2 * gutter, size[1] + 2 * gutter];
const isFloat = tileData instanceof Float32Array;
const pixelCount = pixelSize[0] * pixelSize[1];
const DataType = isFloat ? Float32Array : Uint8ClampedArray;
const tileDataR = new DataType(tileData.buffer);
const bytesPerElement = DataType.BYTES_PER_ELEMENT;
const bytesPerPixel = (bytesPerElement * tileDataR.length) / pixelCount;
const bytesPerRow = tileDataR.byteLength / pixelSize[1];
const bandCount = Math.floor(
bytesPerRow / bytesPerElement / pixelSize[0],
);
const packedLength = pixelCount * bandCount;
let packedData = tileDataR;
if (tileDataR.length !== packedLength) {
packedData = new DataType(packedLength);
let dataIndex = 0;
let rowOffset = 0;
const colCount = pixelSize[0] * bandCount;
for (let rowIndex = 0; rowIndex < pixelSize[1]; ++rowIndex) {
for (let colIndex = 0; colIndex < colCount; ++colIndex) {
packedData[dataIndex++] = tileDataR[rowOffset + colIndex];
}
rowOffset += bytesPerRow / bytesPerElement;
}
}
const extent = this.sourceTileGrid_.getTileCoordExtent(tile.tileCoord);
extent[0] += source.offset;
extent[2] += source.offset;
const clipExtent = this.clipExtent_?.slice();
if (clipExtent) {
clipExtent[0] += source.offset;
clipExtent[2] += source.offset;
}
dataSources.push({
extent: extent,
clipExtent: clipExtent,
data: new Uint8ClampedArray(packedData.buffer),
dataType: DataType,
bytesPerPixel: bytesPerPixel,
pixelSize: pixelSize,
});
});
this.sourceTiles_.length = 0;
if (dataSources.length === 0) {
this.state = TileState.ERROR;
this.changed();
return;
}
const z = this.wrappedTileCoord_[0];
const size = this.targetTileGrid_.getTileSize(z);
const targetWidth = typeof size === 'number' ? size : size[0];
const targetHeight = typeof size === 'number' ? size : size[1];
const targetResolution = this.targetTileGrid_.getResolution(z);
const sourceResolution = this.sourceTileGrid_.getResolution(this.sourceZ_);
const targetExtent = this.targetTileGrid_.getTileCoordExtent(
this.wrappedTileCoord_,
);
let dataR, dataU;
const bytesPerPixel = dataSources[0].bytesPerPixel;
const reprojs = Math.ceil(bytesPerPixel / 3);
for (let reproj = reprojs - 1; reproj >= 0; --reproj) {
const sources = [];
for (let i = 0, len = dataSources.length; i < len; ++i) {
const dataSource = dataSources[i];
const buffer = dataSource.data;
const pixelSize = dataSource.pixelSize;
const width = pixelSize[0];
const height = pixelSize[1];
const context = createCanvasContext2D(width, height, canvasPool);
const imageData = context.createImageData(width, height);
const data = imageData.data;
let offset = reproj * 3;
for (let j = 0, len = data.length; j < len; j += 4) {
data[j] = buffer[offset];
data[j + 1] = buffer[offset + 1];
data[j + 2] = buffer[offset + 2];
data[j + 3] = 255;
offset += bytesPerPixel;
}
context.putImageData(imageData, 0, 0);
sources.push({
extent: dataSource.extent,
clipExtent: dataSource.clipExtent,
image: context.canvas,
});
}
const canvas = renderReprojected(
targetWidth,
targetHeight,
this.pixelRatio_,
sourceResolution,
this.sourceTileGrid_.getExtent(),
targetResolution,
targetExtent,
this.triangulation_,
sources,
this.gutter_,
false,
false,
false,
);
for (let i = 0, len = sources.length; i < len; ++i) {
const canvas = sources[i].image;
const context = canvas.getContext('2d');
releaseCanvas(context);
canvasPool.push(context.canvas);
}
const context = canvas.getContext('2d');
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
releaseCanvas(context);
canvasPool.push(canvas);
if (!dataR) {
dataU = new Uint8ClampedArray(
bytesPerPixel * imageData.width * imageData.height,
);
dataR = new dataSources[0].dataType(dataU.buffer);
}
const data = imageData.data;
let offset = reproj * 3;
for (let i = 0, len = data.length; i < len; i += 4) {
if (data[i + 3] === 255) {
dataU[offset] = data[i];
dataU[offset + 1] = data[i + 1];
dataU[offset + 2] = data[i + 2];
} else {
dataU[offset] = 0;
dataU[offset + 1] = 0;
dataU[offset + 2] = 0;
}
offset += bytesPerPixel;
}
}
if (imageLike) {
const context = createCanvasContext2D(targetWidth, targetHeight);
const imageData = new ImageData(dataR, targetWidth);
context.putImageData(imageData, 0, 0);
this.reprojData_ = context.canvas;
} else {
this.reprojData_ = dataR;
}
this.reprojSize_ = [
Math.round(targetWidth * this.pixelRatio_),
Math.round(targetHeight * this.pixelRatio_),
];
this.state = TileState.LOADED;
this.changed();
}
/**
* Load not yet loaded URI.
* @override
*/
load() {
if (this.state !== TileState.IDLE && this.state !== TileState.ERROR) {
return;
}
this.state = TileState.LOADING;
this.changed();
let leftToLoad = 0;
this.sourcesListenerKeys_ = [];
this.sourceTiles_.forEach(({tile}) => {
const state = tile.getState();
if (state !== TileState.IDLE && state !== TileState.LOADING) {
return;
}
leftToLoad++;
const sourceListenKey = listen(tile, EventType.CHANGE, () => {
const state = tile.getState();
if (
state == TileState.LOADED ||
state == TileState.ERROR ||
state == TileState.EMPTY
) {
unlistenByKey(sourceListenKey);
leftToLoad--;
if (leftToLoad === 0) {
this.unlistenSources_();
this.reproject_();
}
}
});
this.sourcesListenerKeys_.push(sourceListenKey);
});
if (leftToLoad === 0) {
setTimeout(this.reproject_.bind(this), 0);
} else {
this.sourceTiles_.forEach(function ({tile}) {
const state = tile.getState();
if (state == TileState.IDLE) {
tile.load();
}
});
}
}
/**
* @private
*/
unlistenSources_() {
this.sourcesListenerKeys_.forEach(unlistenByKey);
this.sourcesListenerKeys_ = null;
}
}
export default ReprojDataTile;