Rendering raster tiles with WebGL

This commit is contained in:
Tim Schaub
2021-01-05 14:13:57 -07:00
committed by Andreas Hocevar
parent 2dd212cdac
commit af80477c1d
26 changed files with 2412 additions and 41 deletions

76
src/ol/DataTile.js Normal file
View File

@@ -0,0 +1,76 @@
/**
* @module ol/DataTile
*/
import Tile from './Tile.js';
import TileState from './TileState.js';
/**
* Data that can be used with a DataTile.
* @typedef {Uint8Array|Uint8ClampedArray|DataView} Data
*/
/**
* @typedef {Object} Options
* @property {import("./tilecoord.js").TileCoord} tileCoord Tile coordinate.
* @property {function() : Promise<Data>} loader Data loader.
* @property {number} [transition=250] A duration for tile opacity
* transitions in milliseconds. A duration of 0 disables the opacity transition.
* @api
*/
class DataTile extends Tile {
/**
* @param {Options} options Tile options.
*/
constructor(options) {
const state = TileState.IDLE;
super(options.tileCoord, state, {transition: options.transition});
this.loader_ = options.loader;
this.data_ = null;
this.error_ = null;
}
/**
* Get the data for the tile.
* @return {Data} Tile data.
* @api
*/
getData() {
return this.data_;
}
/**
* Get any loading error.
* @return {Error} Loading error.
* @api
*/
getError() {
return this.error_;
}
/**
* Load not yet loaded URI.
* @api
*/
load() {
this.state = TileState.LOADING;
this.changed();
const self = this;
this.loader_()
.then(function (data) {
self.data_ = data;
self.state = TileState.LOADED;
self.changed();
})
.catch(function (error) {
self.error_ = error;
self.state = TileState.ERROR;
self.changed();
});
}
}
export default DataTile;

289
src/ol/layer/WebGLTile.js Normal file
View File

@@ -0,0 +1,289 @@
/**
* @module ol/layer/WebGLTile
*/
import BaseTileLayer from './BaseTile.js';
import WebGLTileLayerRenderer, {
Attributes,
Uniforms,
} from '../renderer/webgl/TileLayer.js';
import {
ValueTypes,
expressionToGlsl,
getStringNumberEquivalent,
uniformNameForVariable,
} from '../style/expressions.js';
import {assign} from '../obj.js';
/**
* @typedef {Object} Style
* @property {Object<string, number>} [variables] Style variables. Each variable must hold a number.
* @property {import("../style/expressions.js").ExpressionValue} [color] An expression applied to color values.
* @property {import("../style/expressions.js").ExpressionValue} [brightness=0] Value used to decrease or increase
* the layer brightness. Values range from -1 to 1.
* @property {import("../style/expressions.js").ExpressionValue} [contrast=0] Value used to decrease or increase
* the layer contrast. Values range from -1 to 1.
* @property {import("../style/expressions.js").ExpressionValue} [exposure=0] Value used to decrease or increase
* the layer exposure. Values range from -1 to 1.
* @property {import("../style/expressions.js").ExpressionValue} [saturation=0] Value used to decrease or increase
* the layer saturation. Values range from -1 to 1.
* @property {import("../style/expressions.js").ExpressionValue} [gamma=1] Apply a gamma correction to the layer.
* Values range from 0 to infinity.
*/
/**
* @typedef {Object} Options
* @property {Style} [style] Style to apply to the layer.
* @property {string} [className='ol-layer'] A CSS class name to set to the layer element.
* @property {number} [opacity=1] Opacity (0, 1).
* @property {boolean} [visible=true] Visibility.
* @property {import("../extent.js").Extent} [extent] The bounding extent for layer rendering. The layer will not be
* rendered outside of this extent.
* @property {number} [zIndex] The z-index for layer rendering. At rendering time, the layers
* will be ordered, first by Z-index and then by position. When `undefined`, a `zIndex` of 0 is assumed
* for layers that are added to the map's `layers` collection, or `Infinity` when the layer's `setMap()`
* method was used.
* @property {number} [minResolution] The minimum resolution (inclusive) at which this layer will be
* visible.
* @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
* be visible.
* @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be
* visible.
* @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will
* be visible.
* @property {number} [preload=0] Preload. Load low-resolution tiles up to `preload` levels. `0`
* means no preloading.
* @property {import("../source/Tile.js").default} [source] Source for this layer.
* @property {import("../PluggableMap.js").default} [map] Sets the layer as overlay on a map. The map will not manage
* this layer in its layers collection, and the layer will be rendered on top. This is useful for
* temporary layers. The standard way to add a layer to a map and have it managed by the map is to
* use {@link module:ol/Map#addLayer}.
* @property {boolean} [useInterimTilesOnError=true] Use interim tiles on error.
*/
/**
* @typedef {Object} ParsedStyle
* @property {string} vertexShader The vertex shader.
* @property {string} fragmentShader The fragment shader.
* @property {Object<string,import("../webgl/Helper.js").UniformValue>} uniforms Uniform definitions.
*/
/**
* @param {Style} style The layer style.
* @param {number} bandCount The number of bands.
* @return {ParsedStyle} Shaders and uniforms generated from the style.
*/
function parseStyle(style, bandCount) {
const vertexShader = `
attribute vec2 ${Attributes.TEXTURE_COORD};
uniform mat4 ${Uniforms.TILE_TRANSFORM};
uniform float ${Uniforms.DEPTH};
varying vec2 v_textureCoord;
void main() {
v_textureCoord = ${Attributes.TEXTURE_COORD};
gl_Position = ${Uniforms.TILE_TRANSFORM} * vec4(${Attributes.TEXTURE_COORD}, ${Uniforms.DEPTH}, 1.0);
}
`;
/**
* @type {import("../style/expressions.js").ParsingContext}
*/
const context = {
inFragmentShader: true,
variables: [],
attributes: [],
stringLiteralsMap: {},
bandCount: bandCount,
};
const pipeline = [];
if (style.color !== undefined) {
const color = expressionToGlsl(context, style.color, ValueTypes.COLOR);
pipeline.push(`color = ${color};`);
}
if (style.contrast !== undefined) {
const contrast = expressionToGlsl(
context,
style.contrast,
ValueTypes.NUMBER
);
pipeline.push(
`color.rgb = clamp((${contrast} + 1.0) * color.rgb - (${contrast} / 2.0), vec3(0.0, 0.0, 0.0), vec3(1.0, 1.0, 1.0));`
);
}
if (style.exposure !== undefined) {
const exposure = expressionToGlsl(
context,
style.exposure,
ValueTypes.NUMBER
);
pipeline.push(
`color.rgb = clamp((${exposure} + 1.0) * color.rgb, vec3(0.0, 0.0, 0.0), vec3(1.0, 1.0, 1.0));`
);
}
if (style.saturation !== undefined) {
const saturation = expressionToGlsl(
context,
style.saturation,
ValueTypes.NUMBER
);
pipeline.push(`
float saturation = ${saturation} + 1.0;
float sr = (1.0 - saturation) * 0.2126;
float sg = (1.0 - saturation) * 0.7152;
float sb = (1.0 - saturation) * 0.0722;
mat3 saturationMatrix = mat3(
sr + saturation, sr, sr,
sg, sg + saturation, sg,
sb, sb, sb + saturation
);
color.rgb = clamp(saturationMatrix * color.rgb, vec3(0.0, 0.0, 0.0), vec3(1.0, 1.0, 1.0));
`);
}
if (style.gamma !== undefined) {
const gamma = expressionToGlsl(context, style.gamma, ValueTypes.NUMBER);
pipeline.push(`color.rgb = pow(color.rgb, vec3(1.0 / ${gamma}));`);
}
if (style.brightness !== undefined) {
const brightness = expressionToGlsl(
context,
style.brightness,
ValueTypes.NUMBER
);
pipeline.push(
`color.rgb = clamp(color.rgb + ${brightness}, vec3(0.0, 0.0, 0.0), vec3(1.0, 1.0, 1.0));`
);
}
/** @type {Object<string,import("../webgl/Helper").UniformValue>} */
const uniforms = {};
const numVariables = context.variables.length;
if (numVariables > 1 && !style.variables) {
throw new Error(
`Missing variables in style (expected ${context.variables})`
);
}
for (let i = 0; i < numVariables; ++i) {
const variableName = context.variables[i];
if (!(variableName in style.variables)) {
throw new Error(`Missing '${variableName}' in style variables`);
}
const uniformName = uniformNameForVariable(variableName);
uniforms[uniformName] = function () {
let value = style.variables[variableName];
if (typeof value === 'string') {
value = getStringNumberEquivalent(context, value);
}
return value !== undefined ? value : -9999999; // to avoid matching with the first string literal
};
}
const uniformDeclarations = Object.keys(uniforms).map(function (name) {
return `uniform float ${name};`;
});
const textureCount = Math.ceil(bandCount / 4);
const colorAssignments = new Array(textureCount);
for (let textureIndex = 0; textureIndex < textureCount; ++textureIndex) {
const uniformName = Uniforms.TILE_TEXTURE_PREFIX + textureIndex;
uniformDeclarations.push(`uniform sampler2D ${uniformName};`);
colorAssignments[
textureIndex
] = `vec4 color${textureIndex} = texture2D(${uniformName}, v_textureCoord);`;
}
const fragmentShader = `
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
varying vec2 v_textureCoord;
uniform float ${Uniforms.TRANSITION_ALPHA};
${uniformDeclarations.join('\n')}
void main() {
${colorAssignments.join('\n')}
vec4 color = color0;
${pipeline.join('\n')}
if (color.a == 0.0) {
discard;
}
gl_FragColor = color * ${Uniforms.TRANSITION_ALPHA};
}`;
return {
vertexShader: vertexShader,
fragmentShader: fragmentShader,
uniforms: uniforms,
};
}
/**
* @classdesc
* For layer sources that provide pre-rendered, tiled images in grids that are
* organized by zoom levels for specific resolutions.
* Note that any property set in the options is set as a {@link module:ol/Object~BaseObject}
* property on the layer object; for example, setting `title: 'My Title'` in the
* options means that `title` is observable, and has get/set accessors.
*
* @api
*/
class WebGLTileLayer extends BaseTileLayer {
/**
* @param {Options} opt_options Tile layer options.
*/
constructor(opt_options) {
const options = opt_options ? assign({}, opt_options) : {};
const style = options.style || {};
delete options.style;
super(options);
const parsedStyle = parseStyle(style || {}, 1); // TODO: get texture count from source
this.vertexShader_ = parsedStyle.vertexShader;
this.fragmentShader_ = parsedStyle.fragmentShader;
this.uniforms_ = parsedStyle.uniforms;
this.styleVariables_ = style.variables || {};
}
/**
* Create a renderer for this layer.
* @return {import("../renderer/Layer.js").default} A layer renderer.
* @protected
*/
createRenderer() {
return new WebGLTileLayerRenderer(this, {
vertexShader: this.vertexShader_,
fragmentShader: this.fragmentShader_,
uniforms: this.uniforms_,
});
}
/**
* Update any variables used by the layer style and trigger a re-render.
* @param {Object<string, number>} variables Variables to update.
*/
updateStyleVariables(variables) {
assign(this.styleVariables_, variables);
this.changed();
}
}
export default WebGLTileLayer;

View File

@@ -0,0 +1,487 @@
/**
* @module ol/renderer/webgl/TileLayer
*/
import LRUCache from '../../structs/LRUCache.js';
import State from '../../source/State.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 {
compose as composeTransform,
create as createTransform,
} from '../../transform.js';
import {
create as createMat4,
fromTransform as mat4FromTransform,
} from '../../vec/mat4.js';
import {
createOrUpdate as createTileCoord,
getKeyZXY,
getKey as getTileCoordKey,
} from '../../tilecoord.js';
import {fromUserExtent} from '../../proj.js';
import {getIntersection} from '../../extent.js';
import {getUid} from '../../util.js';
import {numberSafeCompareFunction} from '../../array.js';
import {toSize} from '../../size.js';
export const Uniforms = {
TILE_TEXTURE_PREFIX: 'u_tileTexture',
TILE_TRANSFORM: 'u_tileTransform',
TRANSITION_ALPHA: 'u_transitionAlpha',
DEPTH: 'u_depth',
};
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,
},
];
/**
* 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<string, 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);
}
/**
* @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 {string} [className='ol-layer'] A CSS class name to set to the canvas element.
*/
/**
* @classdesc
* WebGL renderer for tile layers.
* @api
*/
class WebGLTileLayerRenderer extends WebGLLayerRenderer {
/**
* @param {import("../../layer/WebGLTile.js").default} tileLayer Tile layer.
* @param {Options} options Options.
*/
constructor(tileLayer, options) {
super(tileLayer, {
uniforms: options.uniforms,
className: options.className,
});
/**
* This transform converts tile i, j 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];
this.program_ = this.helper.getProgram(
options.fragmentShader,
options.vertexShader
);
/**
* 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
*/
const indices = new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, STATIC_DRAW);
indices.fromArray([0, 1, 3, 1, 2, 3]);
this.helper.flushBufferData(indices);
this.indices_ = indices;
this.tileTextureCache_ = new LRUCache(512);
this.renderedOpacity_ = NaN;
}
/**
* 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 source = this.getLayer().getSource();
if (!source) {
return false;
}
return source.getState() === State.READY;
}
/**
* Render the layer.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
* @return {HTMLElement} The rendered element.
*/
renderFrame(frameState) {
this.preRender(frameState);
const layerState = frameState.layerStatesArray[frameState.layerIndex];
const viewState = frameState.viewState;
let extent = frameState.extent;
if (layerState.extent) {
extent = getIntersection(
extent,
fromUserExtent(layerState.extent, viewState.projection)
);
}
const tileLayer = this.getLayer();
const tileSource = tileLayer.getSource();
const tileGrid = tileSource.getTileGridForProjection(viewState.projection);
const z = tileGrid.getZForResolution(
viewState.resolution,
tileSource.zDirection
);
/**
* @type {Object<string, Array<import("../../webgl/TileTexture.js").default>>}
*/
const tileTexturesByZ = {};
const tileTextureCache = this.tileTextureCache_;
const tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z);
const tileSourceKey = getUid(tileSource);
if (!(tileSourceKey in frameState.wantedTiles)) {
frameState.wantedTiles[tileSourceKey] = {};
}
const wantedTiles = frameState.wantedTiles[tileSourceKey];
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 tileCoordKey = getTileCoordKey(tileCoord);
let tileTexture;
if (tileTextureCache.containsKey(tileCoordKey)) {
tileTexture = tileTextureCache.get(tileCoordKey);
} else {
const tile = tileSource.getTile(
z,
x,
y,
frameState.pixelRatio,
viewState.projection
);
tileTexture = new TileTexture(tile, tileGrid, this.helper);
tileTextureCache.set(tileCoordKey, tileTexture);
}
addTileTextureToLookup(tileTexturesByZ, tileTexture, z);
const tileQueueKey = tileTexture.tile.getKey();
wantedTiles[tileQueueKey] = true;
if (tileTexture.tile.getState() === TileState.IDLE) {
if (!frameState.tileQueue.isKeyQueued(tileQueueKey)) {
frameState.tileQueue.enqueue([
tileTexture.tile,
tileSourceKey,
tileGrid.getTileCoordCenter(tileCoord),
tileResolution,
]);
}
}
}
}
/**
* 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;
// 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;
}
const tileCoordKey = getTileCoordKey(tileCoord);
alphaLookup[tileCoordKey] = alpha;
}
// 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
for (let parentZ = z - 1; parentZ >= tileGrid.minZoom; --parentZ) {
const coveredByParent = this.findAltTiles_(
tileGrid,
tileCoord,
parentZ,
tileTexturesByZ
);
if (coveredByParent) {
break;
}
}
}
this.helper.useProgram(this.program_);
this.helper.prepareDraw(frameState);
const zs = Object.keys(tileTexturesByZ)
.map(Number)
.sort(numberSafeCompareFunction);
const gl = this.helper.getGL();
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 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];
composeTransform(
this.tileTransform_,
0,
0,
2 / ((frameState.size[0] * tileScale) / tileSize[0]),
-2 / ((frameState.size[1] * tileScale) / tileSize[1]),
viewState.rotation,
-(centerI - tileCenterI),
-(centerJ - tileCenterJ)
);
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);
for (
let textureIndex = 0;
textureIndex < tileTexture.textures.length;
++textureIndex
) {
const textureProperty = 'TEXTURE' + textureIndex;
const uniformName = Uniforms.TILE_TEXTURE_PREFIX + textureIndex;
gl.activeTexture(gl[textureProperty]);
gl.bindTexture(gl.TEXTURE_2D, tileTexture.textures[textureIndex]);
gl.uniform1i(this.helper.getUniformLocation(uniformName), 0);
}
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.drawElements(0, this.indices_.getSize());
}
}
this.helper.finalizeDraw(frameState);
const canvas = this.helper.getCanvas();
const opacity = layerState.opacity;
if (this.renderedOpacity_ !== opacity) {
canvas.style.opacity = String(opacity);
this.renderedOpacity_ = opacity;
}
while (tileTextureCache.canExpireCache()) {
const tileTexture = tileTextureCache.pop();
tileTexture.dispose();
}
// TODO: let the renderers manage their own cache instead of managing the source cache
if (tileSource.canExpireCache()) {
/**
* @param {import("../../PluggableMap.js").default} map Map.
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
*/
const postRenderFunction = function (map, frameState) {
const tileSourceKey = getUid(tileSource);
if (tileSourceKey in frameState.usedTiles) {
tileSource.expireCache(
frameState.viewState.projection,
frameState.usedTiles[tileSourceKey]
);
}
};
frameState.postRenderFunctions.push(postRenderFunction);
}
this.postRender(frameState);
return canvas;
}
/**
* 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<string, 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_;
for (let x = tileRange.minX; x <= tileRange.maxX; ++x) {
for (let y = tileRange.minY; y <= tileRange.maxY; ++y) {
const cacheKey = getKeyZXY(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;
}
}
/**
* @function
* @return {import("../../layer/WebGLTile.js").default}
*/
WebGLTileLayerRenderer.prototype.getLayer;
export default WebGLTileLayerRenderer;

142
src/ol/source/DataTile.js Normal file
View File

@@ -0,0 +1,142 @@
/**
* @module ol/source/DataTile
*/
import DataTile from '../DataTile.js';
import EventType from '../events/EventType.js';
import TileEventType from './TileEventType.js';
import TileSource, {TileSourceEvent} from './Tile.js';
import TileState from '../TileState.js';
import {createXYZ, extentFromProjection} from '../tilegrid.js';
import {getKeyZXY} from '../tilecoord.js';
import {getUid} from '../util.js';
/**
* @typedef {Object} Options
* @property {function(number, number, number) : Promise<import("../DataTile.js").Data>} [loader] Data loader. Called with z, x, and y tile coordinates.
* @property {number} [maxZoom=42] Optional max zoom level. Not used if `tileGrid` is provided.
* @property {number} [minZoom=0] Optional min zoom level. Not used if `tileGrid` is provided.
* @property {number|import("../size.js").Size} [tileSize=[256, 256]] The pixel width and height of the tiles.
* @property {number} [maxResolution] Optional tile grid resolution at level zero. Not used if `tileGrid` is provided.
* @property {import("../proj.js").ProjectionLike} [projection='EPSG:3857'] Tile projection.
* @property {import("../tilegrid/TileGrid.js").default} [tileGrid] Tile grid.
* @property {boolean} [opaque=false] Whether the layer is opaque.
* @property {import("./State.js").default} [state] The source state.
* @property {number} [cacheSize] Number of tiles to retain in the cache.
* @property {number} [tilePixelRatio] Tile pixel ratio.
* @property {boolean} [wrapX=true] Render tiles beyond the antimeridian.
* @property {number} [transition] Transition time when fading in new tiles (in miliseconds).
*/
/**
* @classdesc
* Base class for sources providing tiles divided into a tile grid over http.
*
* @fires import("./Tile.js").TileSourceEvent
*/
class DataTileSource extends TileSource {
/**
* @param {Options} options Image tile options.
*/
constructor(options) {
const projection =
options.projection === undefined ? 'EPSG:3857' : options.projection;
let tileGrid = options.tileGrid;
if (tileGrid === undefined && projection) {
tileGrid = createXYZ({
extent: extentFromProjection(projection),
maxResolution: options.maxResolution,
maxZoom: options.maxZoom,
minZoom: options.minZoom,
tileSize: options.tileSize,
});
}
super({
cacheSize: options.cacheSize,
projection: projection,
tileGrid: tileGrid,
opaque: options.opaque,
state: options.state,
tilePixelRatio: options.tilePixelRatio,
wrapX: options.wrapX,
transition: options.transition,
});
/**
* @private
* @type {!Object<string, boolean>}
*/
this.tileLoadingKeys_ = {};
/**
* @private
*/
this.loader_ = options.loader;
this.handleTileChange_ = this.handleTileChange_.bind(this);
}
/**
* @param {function(number, number, number) : Promise<import("../DataTile.js").Data>} loader The data loader.
* @protected
*/
setLoader(loader) {
this.loader_ = loader;
}
/**
* @abstract
* @param {number} z Tile coordinate z.
* @param {number} x Tile coordinate x.
* @param {number} y Tile coordinate y.
* @param {number} pixelRatio Pixel ratio.
* @param {import("../proj/Projection.js").default} projection Projection.
* @return {!import("../Tile.js").default} Tile.
*/
getTile(z, x, y, pixelRatio, projection) {
const tileCoordKey = getKeyZXY(z, x, y);
if (this.tileCache.containsKey(tileCoordKey)) {
return this.tileCache.get(tileCoordKey);
}
const sourceLoader = this.loader_;
function loader() {
return sourceLoader(z, x, y);
}
const tile = new DataTile({tileCoord: [z, x, y], loader: loader});
tile.addEventListener(EventType.CHANGE, this.handleTileChange_);
this.tileCache.set(tileCoordKey, tile);
return tile;
}
/**
* Handle tile change events.
* @param {import("../events/Event.js").default} event Event.
*/
handleTileChange_(event) {
const tile = /** @type {import("../Tile.js").default} */ (event.target);
const uid = getUid(tile);
const tileState = tile.getState();
let type;
if (tileState == TileState.LOADING) {
this.tileLoadingKeys_[uid] = true;
type = TileEventType.TILELOADSTART;
} else if (uid in this.tileLoadingKeys_) {
delete this.tileLoadingKeys_[uid];
type =
tileState == TileState.ERROR
? TileEventType.TILELOADERROR
: tileState == TileState.LOADED
? TileEventType.TILELOADEND
: undefined;
}
if (type) {
this.dispatchEvent(new TileSourceEvent(type, tile));
}
}
}
export default DataTileSource;

391
src/ol/source/GeoTIFF.js Normal file
View File

@@ -0,0 +1,391 @@
/**
* @module ol/source/GeoTIFF
*/
import DataTile from './DataTile.js';
import State from './State.js';
import TileGrid from '../tilegrid/TileGrid.js';
import {get as getProjection} from '../proj.js';
import {fromUrl as tiffFromUrl, fromUrls as tiffFromUrls} from 'geotiff';
import {toSize} from '../size.js';
/**
* @typedef SourceInfo
* @property {string} url URL for the source.
* @property {Array<string>} [overviews] List of any overview URLs.
* @property {number} [min=0] The minimum source data value. Rendered values are scaled from 0 to 1 based on
* the configured min and max.
* @property {number} [max] The maximum source data value. Rendered values are scaled from 0 to 1 based on
* the configured min and max.
* @property {number} [nodata] Values to discard.
*/
/**
* @param {import("geotiff/src/geotiff.js").GeoTIFF|import("geotiff/src/geotiff.js").MultiGeoTIFF} tiff A GeoTIFF.
* @return {Promise<Array<import("geotiff/src/geotiffimage.js").GeoTIFFImage>>} Resolves to a list of images.
*/
function getImagesForTIFF(tiff) {
return tiff.getImageCount().then(function (count) {
const requests = new Array(count);
for (let i = 0; i < count; ++i) {
requests[i] = tiff.getImage(i);
}
return Promise.all(requests);
});
}
/**
* @param {SourceInfo} source The GeoTIFF source.
* @return {Promise<Array<import("geotiff/src/geotiffimage.js").GeoTIFFImage>>} Resolves to a list of images.
*/
function getImagesForSource(source) {
let request;
if (source.overviews) {
request = tiffFromUrls(source.url, source.overviews);
} else {
request = tiffFromUrl(source.url);
}
return request.then(getImagesForTIFF);
}
/**
* @param {number|Array<number>|Array<Array<number>>} expected Expected value.
* @param {number|Array<number>|Array<Array<number>>} got Actual value.
* @param {string} message The error message.
*/
function assertEqual(expected, got, message) {
if (Array.isArray(expected)) {
const length = expected.length;
if (!Array.isArray(got) || length != got.length) {
throw new Error(message);
}
for (let i = 0; i < length; ++i) {
assertEqual(expected[i], got[i], message);
}
return;
}
if (expected !== got) {
throw new Error(message);
}
}
/**
* @param {Array} array The data array.
* @return {number} The minimum value.
*/
function getMinForDataType(array) {
if (array instanceof Int8Array) {
return -128;
}
if (array instanceof Int16Array) {
return -32768;
}
if (array instanceof Int32Array) {
return -2147483648;
}
if (array instanceof Float32Array) {
return 1.2e-38;
}
return 0;
}
/**
* @param {Array} array The data array.
* @return {number} The maximum value.
*/
function getMaxForDataType(array) {
if (array instanceof Int8Array) {
return 127;
}
if (array instanceof Uint8Array) {
return 255;
}
if (array instanceof Uint8ClampedArray) {
return 255;
}
if (array instanceof Int16Array) {
return 32767;
}
if (array instanceof Uint16Array) {
return 65535;
}
if (array instanceof Int32Array) {
return 2147483647;
}
if (array instanceof Uint32Array) {
return 4294967295;
}
if (array instanceof Float32Array) {
return 3.4e38;
}
return 255;
}
/**
* @typedef Options
* @property {Array<SourceInfo>} sources List of information about GeoTIFF sources.
*/
/**
* @classdesc
* A source for working with GeoTIFF data.
*/
class GeoTIFFSource extends DataTile {
/**
* @param {Options} options Image tile options.
*/
constructor(options) {
super({
state: State.LOADING,
tileGrid: null,
projection: null,
});
/**
* @type {Array<SourceInfo>}
* @private
*/
this.sourceInfo_ = options.sources;
const numSources = this.sourceInfo_.length;
/**
* @type {Array<Array<import("geotiff/src/geotiffimage.js").GeoTIFFImage>>}
* @private
*/
this.sourceImagery_ = new Array(numSources);
/**
* @type {number}
* @private
*/
this.samplesPerPixel_;
/**
* @type {Error}
* @private
*/
this.error_ = null;
const self = this;
const requests = new Array(numSources);
for (let i = 0; i < numSources; ++i) {
requests[i] = getImagesForSource(this.sourceInfo_[i]);
}
Promise.all(requests)
.then(function (sources) {
self.configure_(sources);
})
.catch(function (error) {
self.error_ = error;
self.setState(State.ERROR);
});
}
/**
* @return {Error} A source loading error.
*/
getError() {
return this.error_;
}
/**
* Configure the tile grid based on images within the source GeoTIFFs. Each GeoTIFF
* must have the same internal tiled structure.
* @param {Array<Array<import("geotiff/src/geotiffimage.js").GeoTIFFImage>>} sources Each source is a list of images
* from a single GeoTIFF.
*/
configure_(sources) {
let extent;
let origin;
let tileSizes;
let resolutions;
let samplesPerPixel;
const sourceCount = sources.length;
for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) {
const images = sources[sourceIndex];
const imageCount = images.length;
let sourceExtent;
let sourceOrigin;
const sourceTileSizes = new Array(imageCount);
const sourceResolutions = new Array(imageCount);
for (let imageIndex = 0; imageIndex < imageCount; ++imageIndex) {
const image = images[imageIndex];
const imageSamplesPerPixel = image.getSamplesPerPixel();
if (!samplesPerPixel) {
samplesPerPixel = imageSamplesPerPixel;
} else {
const message = `Band count mismatch for source ${sourceIndex}, got ${imageSamplesPerPixel} but expected ${samplesPerPixel}`;
assertEqual(samplesPerPixel, imageSamplesPerPixel, message);
}
const level = imageCount - (imageIndex + 1);
if (!sourceExtent) {
sourceExtent = image.getBoundingBox();
}
if (!sourceOrigin) {
sourceOrigin = image.getOrigin().slice(0, 2);
}
sourceResolutions[level] = image.getResolution(images[0])[0];
sourceTileSizes[level] = [image.getTileWidth(), image.getTileHeight()];
}
if (!extent) {
extent = sourceExtent;
} else {
const message = `Extent mismatch for source ${sourceIndex}, got [${sourceExtent}] but expected [${extent}]`;
assertEqual(extent, sourceExtent, message);
}
if (!origin) {
origin = sourceOrigin;
} else {
const message = `Origin mismatch for source ${sourceIndex}, got [${sourceOrigin}] but expected [${origin}]`;
assertEqual(origin, sourceOrigin, message);
}
if (!tileSizes) {
tileSizes = sourceTileSizes;
} else {
assertEqual(
tileSizes,
sourceTileSizes,
`Tile size mismatch for source ${sourceIndex}`
);
}
if (!resolutions) {
resolutions = sourceResolutions;
} else {
const message = `Resolution mismatch for source ${sourceIndex}, got [${sourceResolutions}] but expected [${resolutions}]`;
assertEqual(resolutions, sourceResolutions, message);
}
this.sourceImagery_[sourceIndex] = images.reverse();
}
if (!this.getProjection()) {
const firstImage = sources[0][0];
if (firstImage.geoKeys) {
const code =
firstImage.geoKeys.ProjectedCSTypeGeoKey ||
firstImage.geoKeys.GeographicTypeGeoKey;
if (code) {
this.projection = getProjection(`EPSG:${code}`);
}
}
}
if (sourceCount > 1 && samplesPerPixel !== 1) {
throw new Error(
'Expected single band GeoTIFFs when using multiple sources'
);
}
this.samplesPerPixel_ = samplesPerPixel;
const tileGrid = new TileGrid({
extent: extent,
origin: origin,
resolutions: resolutions,
tileSizes: tileSizes,
});
this.tileGrid = tileGrid;
this.setLoader(this.loadTile_.bind(this));
this.setState(State.READY);
}
loadTile_(z, x, y) {
const size = toSize(this.tileGrid.getTileSize(z));
const pixelBounds = [
x * size[0],
y * size[1],
(x + 1) * size[0],
(y + 1) * size[1],
];
const sourceCount = this.sourceImagery_.length;
const requests = new Array(sourceCount);
let addAlpha = false;
const sourceInfo = this.sourceInfo_;
for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) {
const image = this.sourceImagery_[sourceIndex][z];
requests[sourceIndex] = image.readRasters({window: pixelBounds});
if (sourceInfo[sourceIndex].nodata !== undefined) {
addAlpha = true;
}
}
const samplesPerPixel = this.samplesPerPixel_;
let additionalBands = 0;
if (addAlpha) {
if (sourceCount === 2 && samplesPerPixel === 1) {
additionalBands = 2;
} else {
additionalBands = 1;
}
}
const bandCount = samplesPerPixel * sourceCount + additionalBands;
const pixelCount = size[0] * size[1];
const dataLength = pixelCount * bandCount;
return Promise.all(requests).then(function (sourceSamples) {
const data = new Uint8ClampedArray(dataLength);
for (let pixelIndex = 0; pixelIndex < pixelCount; ++pixelIndex) {
let transparent = addAlpha;
const sourceOffset = pixelIndex * bandCount;
for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) {
const source = sourceInfo[sourceIndex];
let min = source.min;
if (min === undefined) {
min = getMinForDataType(sourceSamples[sourceIndex][0]);
}
let max = source.max;
if (max === undefined) {
max = getMaxForDataType(sourceSamples[sourceIndex][0]);
}
const gain = 255 / (max - min);
const bias = -min * gain;
const nodata = source.nodata;
const sampleOffset = sourceOffset + sourceIndex * samplesPerPixel;
for (
let sampleIndex = 0;
sampleIndex < samplesPerPixel;
++sampleIndex
) {
const sourceValue =
sourceSamples[sourceIndex][sampleIndex][pixelIndex];
const value = gain * sourceValue + bias;
if (!addAlpha) {
data[sampleOffset + sampleIndex] = value;
} else {
if (sourceValue !== nodata) {
transparent = false;
data[sampleOffset + sampleIndex] = value;
}
}
}
if (addAlpha && !transparent) {
data[sampleOffset + samplesPerPixel] = 255;
}
}
}
return data;
});
}
}
export default GeoTIFFSource;

View File

@@ -170,6 +170,7 @@ export function isTypeUnique(valueType) {
* @property {Array<string>} variables List of variables used in the expression; contains **unprefixed names**
* @property {Array<string>} attributes List of attributes used in the expression; contains **unprefixed names**
* @property {Object<string, number>} stringLiteralsMap This object maps all encountered string values to a number
* @property {number} [bandCount] Number of bands per pixel.
*/
/**
@@ -402,6 +403,27 @@ Operators['var'] = {
},
};
Operators['band'] = {
getReturnType: function (args) {
return ValueTypes.NUMBER;
},
toGlsl: function (context, args) {
assertArgsCount(args, 1);
const band = args[0];
if (typeof band !== 'number') {
throw new Error('Band index must be a number');
}
const zeroBasedBand = band - 1;
const colorIndex = Math.floor(zeroBasedBand / 4);
let bandIndex = zeroBasedBand % 4;
if (band === context.bandCount && bandIndex === 1) {
// LUMINANCE_ALPHA - band 1 assigned to rgb and band 2 assigned to alpha
bandIndex = 3;
}
return `color${colorIndex}[${bandIndex}]`;
},
};
Operators['time'] = {
getReturnType: function (args) {
return ValueTypes.NUMBER;
@@ -748,17 +770,16 @@ Operators['interpolate'] = {
assertUniqueInferredType(args, outputType);
const input = expressionToGlsl(context, args[1]);
let result = null;
const exponent = numberToGlsl(interpolation);
let result = '';
for (let i = 2; i < args.length - 2; i += 2) {
const stop1 = expressionToGlsl(context, args[i]);
const output1 = expressionToGlsl(context, args[i + 1], outputType);
const output1 =
result || expressionToGlsl(context, args[i + 1], outputType);
const stop2 = expressionToGlsl(context, args[i + 2]);
const output2 = expressionToGlsl(context, args[i + 3], outputType);
result = `mix(${
result || output1
}, ${output2}, pow(clamp((${input} - ${stop1}) / (${stop2} - ${stop1}), 0.0, 1.0), ${numberToGlsl(
interpolation
)}))`;
result = `mix(${output1}, ${output2}, pow(clamp((${input} - ${stop1}) / (${stop2} - ${stop1}), 0.0, 1.0), ${exponent}))`;
}
return result;
},

195
src/ol/webgl/TileTexture.js Normal file
View File

@@ -0,0 +1,195 @@
/**
* @module ol/webgl/TileTexture
*/
import EventTarget from '../events/Target.js';
import EventType from '../events/EventType.js';
import ImageTile from '../ImageTile.js';
import TileState from '../TileState.js';
import WebGLArrayBuffer from './Buffer.js';
import {ARRAY_BUFFER, STATIC_DRAW} from '../webgl.js';
import {toSize} from '../size.js';
function bindAndConfigure(gl, texture) {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
}
/**
* @param {WebGLRenderingContext} gl The WebGL context.
* @param {WebGLTexture} texture The texture.
* @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} image The image.
*/
function uploadImageTexture(gl, texture, image) {
bindAndConfigure(gl, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
}
/**
* @param {WebGLRenderingContext} gl The WebGL context.
* @param {WebGLTexture} texture The texture.
* @param {import("../DataTile.js").Data} data The pixel data.
* @param {import("../size.js").Size} size The pixel size.
* @param {number} bandCount The band count.
*/
function uploadDataTexture(gl, texture, data, size, bandCount) {
bindAndConfigure(gl, texture);
let format;
switch (bandCount) {
case 1: {
format = gl.LUMINANCE;
break;
}
case 2: {
format = gl.LUMINANCE_ALPHA;
break;
}
case 3: {
format = gl.RGB;
break;
}
case 4: {
format = gl.RGBA;
break;
}
default: {
throw new Error(`Unsupported number of bands: ${bandCount}`);
}
}
gl.texImage2D(
gl.TEXTURE_2D,
0,
format,
size[0],
size[1],
0,
format,
gl.UNSIGNED_BYTE,
data
);
}
class TileTexture extends EventTarget {
/**
* @param {import("../DataTile.js").default|import("../ImageTile.js").default} tile The tile.
* @param {import("../tilegrid/TileGrid.js").default} grid Tile grid.
* @param {import("../webgl/Helper.js").default} helper WebGL helper.
*/
constructor(tile, grid, helper) {
super();
this.tile = tile;
this.size = toSize(grid.getTileSize(tile.tileCoord[0]));
this.loaded = tile.getState() === TileState.LOADED;
this.bandCount = NaN;
this.helper_ = helper;
this.handleTileChange_ = this.handleTileChange_.bind(this);
const coords = new WebGLArrayBuffer(ARRAY_BUFFER, STATIC_DRAW);
coords.fromArray([
0, // P0
1,
1, // P1
1,
1, // P2
0,
0, // P3
0,
]);
helper.flushBufferData(coords);
this.coords = coords;
/**
* @type {Array<WebGLTexture>}
*/
this.textures = [];
if (this.loaded) {
this.uploadTile_();
} else {
tile.addEventListener(EventType.CHANGE, this.handleTileChange_);
}
}
uploadTile_() {
const gl = this.helper_.getGL();
const tile = this.tile;
if (tile instanceof ImageTile) {
const texture = gl.createTexture();
this.textures.push(texture);
this.bandCount = 4;
uploadImageTexture(gl, texture, tile.getImage());
return;
}
const data = tile.getData();
const pixelCount = this.size[0] * this.size[1];
this.bandCount = data.byteLength / pixelCount;
const textureCount = Math.ceil(this.bandCount / 4);
if (textureCount === 1) {
const texture = gl.createTexture();
this.textures.push(texture);
uploadDataTexture(gl, texture, data, this.size, this.bandCount);
return;
}
const textureDataArrays = new Array(textureCount);
for (let textureIndex = 0; textureIndex < textureCount; ++textureIndex) {
const texture = gl.createTexture();
this.textures.push(texture);
const bandCount =
textureIndex < textureCount - 1 ? 4 : this.bandCount % 4;
textureDataArrays[textureIndex] = new Uint8Array(pixelCount * bandCount);
}
const valueCount = pixelCount * this.bandCount;
for (let dataIndex = 0; dataIndex < valueCount; ++dataIndex) {
const bandIndex = dataIndex % this.bandCount;
const textureBandIndex = bandIndex % 4;
const textureIndex = Math.floor(bandIndex / 4);
const bandCount =
textureIndex < textureCount - 1 ? 4 : this.bandCount % 4;
const pixelIndex = Math.floor(dataIndex / this.bandCount);
textureDataArrays[textureIndex][
pixelIndex * bandCount + textureBandIndex
] = data[dataIndex];
}
for (let textureIndex = 0; textureIndex < textureCount; ++textureIndex) {
const bandCount =
textureIndex < textureCount - 1 ? 4 : this.bandCount % 4;
const texture = this.textures[textureIndex];
const data = textureDataArrays[textureIndex];
uploadDataTexture(gl, texture, data, this.size, bandCount);
}
}
handleTileChange_() {
if (this.tile.getState() === TileState.LOADED) {
this.loaded = true;
this.uploadTile_();
this.dispatchEvent(EventType.CHANGE);
}
}
disposeInternal() {
const gl = this.helper_.getGL();
this.helper_.deleteBuffer(this.coords);
for (let i = 0; i < this.textures.length; ++i) {
gl.deleteTexture(this.textures[i]);
}
this.tile.removeEventListener(EventType.CHANGE, this.handleTileChange_);
}
}
export default TileTexture;