Sources and formats
-
Tile sources for
ol/layer/Tile
+
Tile sources for
ol/layer/Tile or
ol/layer/WebGLTile
Image sources for
ol/layer/Image
Vector sources for
ol/layer/Vector
Vector tile sources for
ol/layer/VectorTile
@@ -71,7 +72,7 @@
diff --git a/config/webpack-config-legacy-build.mjs b/config/webpack-config-legacy-build.mjs
index 89786cf0a0..ffc2aef1a8 100644
--- a/config/webpack-config-legacy-build.mjs
+++ b/config/webpack-config-legacy-build.mjs
@@ -32,6 +32,11 @@ export default {
],
},
resolve: {
+ fallback: {
+ fs: false,
+ http: false,
+ https: false,
+ },
alias: {
ol: path.resolve('./build/ol'),
},
diff --git a/examples/cog-math-multisource.html b/examples/cog-math-multisource.html
new file mode 100644
index 0000000000..b5889c5bfe
--- /dev/null
+++ b/examples/cog-math-multisource.html
@@ -0,0 +1,13 @@
+---
+layout: example.html
+title: NDVI+NDWI from two 16-bit COGs
+shortdesc: Calculating NDVI+NDWI as green and blue values.
+docs: >
+ The GeoTIFF layer in this example calculates the Normalized Difference Vegetation Index (NDVI)
+ and Normalized Difference Water Index (NDWI) from two cloud-optimized Sentinel 2 GeoTIFFs: one
+ with 10 m resolution and red and a near infrared bands, and one with 60 m resolution and a short
+ wave infrared channel. The NDVI is shown as green, the NDWI as blue. The 4th band is the alpha
+ band, which gets added when a source has a `nodata` value configured.
+tags: "cog, ndvi, ndwi, sentinel, geotiff"
+---
+
diff --git a/examples/cog-math-multisource.js b/examples/cog-math-multisource.js
new file mode 100644
index 0000000000..1f1d6f3583
--- /dev/null
+++ b/examples/cog-math-multisource.js
@@ -0,0 +1,65 @@
+import GeoTIFF from '../src/ol/source/GeoTIFF.js';
+import Map from '../src/ol/Map.js';
+import TileLayer from '../src/ol/layer/WebGLTile.js';
+import View from '../src/ol/View.js';
+
+const source = new GeoTIFF({
+ sources: [
+ {
+ url: 'https://s2downloads.eox.at/demo/Sentinel-2/3857/R10m.tif',
+ bands: [2, 3],
+ min: 0,
+ nodata: 0,
+ max: 65535,
+ },
+ {
+ url: 'https://s2downloads.eox.at/demo/Sentinel-2/3857/R60m.tif',
+ bands: [8],
+ min: 0,
+ nodata: 0,
+ max: 65535,
+ },
+ ],
+});
+source.setAttributions(
+ "
Sentinel-2 cloudless by
EOX IT Services GmbH (Contains modified Copernicus Sentinel data 2019)"
+);
+
+const ndvi = [
+ '/',
+ ['-', ['band', 2], ['band', 1]],
+ ['+', ['band', 2], ['band', 1]],
+];
+
+const ndwi = [
+ '/',
+ ['-', ['band', 3], ['band', 1]],
+ ['+', ['band', 3], ['band', 1]],
+];
+
+const map = new Map({
+ target: 'map',
+ layers: [
+ new TileLayer({
+ style: {
+ color: [
+ 'color',
+ // red: | NDVI - NDWI |
+ ['*', 255, ['abs', ['-', ndvi, ndwi]]],
+ // green: NDVI
+ ['*', 255, ndvi],
+ // blue: NDWI
+ ['*', 255, ndwi],
+ // alpha
+ ['band', 4],
+ ],
+ },
+ source,
+ }),
+ ],
+ view: new View({
+ center: [1900000, 6100000],
+ zoom: 13,
+ minZoom: 10,
+ }),
+});
diff --git a/examples/cog-math.html b/examples/cog-math.html
new file mode 100644
index 0000000000..318f626e11
--- /dev/null
+++ b/examples/cog-math.html
@@ -0,0 +1,11 @@
+---
+layout: example.html
+title: NDVI from a Sentinel 2 COG
+shortdesc: Calculating NDVI and applying a custom color map.
+docs: >
+ The GeoTIFF layer in this example draws from two Sentinel 2 sources: a red band and a near infrared band.
+ The layer style includes a `color` expression that calculates the Normalized Difference Vegetation Index (NDVI)
+ from values in the two bands. The `interpolate` expression is used to map NDVI values to colors.
+tags: "cog, ndvi"
+---
+
diff --git a/examples/cog-math.js b/examples/cog-math.js
new file mode 100644
index 0000000000..ca9dcdfc9f
--- /dev/null
+++ b/examples/cog-math.js
@@ -0,0 +1,101 @@
+import GeoTIFF from '../src/ol/source/GeoTIFF.js';
+import Map from '../src/ol/Map.js';
+import Projection from '../src/ol/proj/Projection.js';
+import TileLayer from '../src/ol/layer/WebGLTile.js';
+import View from '../src/ol/View.js';
+import proj4 from 'proj4';
+import {getCenter} from '../src/ol/extent.js';
+import {register} from '../src/ol/proj/proj4.js';
+
+proj4.defs('EPSG:32636', '+proj=utm +zone=36 +datum=WGS84 +units=m +no_defs');
+register(proj4);
+
+const projection = new Projection({
+ code: 'EPSG:32636',
+ extent: [166021.44, 0.0, 534994.66, 9329005.18],
+});
+
+// metadata from https://s3.us-west-2.amazonaws.com/sentinel-cogs/sentinel-s2-l2a-cogs/2020/S2A_36QWD_20200701_0_L2A/S2A_36QWD_20200701_0_L2A.json
+const sourceExtent = [499980, 1790220, 609780, 1900020];
+
+const map = new Map({
+ target: 'map',
+ layers: [
+ new TileLayer({
+ style: {
+ color: [
+ 'interpolate',
+ ['linear'],
+ // calculate NDVI, bands come from the sources below
+ [
+ '/',
+ ['-', ['band', 2], ['band', 1]],
+ ['+', ['band', 2], ['band', 1]],
+ ],
+ // color ramp for NDVI values, ranging from -1 to 1
+ -0.2,
+ [191, 191, 191],
+ -0.1,
+ [219, 219, 219],
+ 0,
+ [255, 255, 224],
+ 0.025,
+ [255, 250, 204],
+ 0.05,
+ [237, 232, 181],
+ 0.075,
+ [222, 217, 156],
+ 0.1,
+ [204, 199, 130],
+ 0.125,
+ [189, 184, 107],
+ 0.15,
+ [176, 194, 97],
+ 0.175,
+ [163, 204, 89],
+ 0.2,
+ [145, 191, 82],
+ 0.25,
+ [128, 179, 71],
+ 0.3,
+ [112, 163, 64],
+ 0.35,
+ [97, 150, 54],
+ 0.4,
+ [79, 138, 46],
+ 0.45,
+ [64, 125, 36],
+ 0.5,
+ [48, 110, 28],
+ 0.55,
+ [33, 97, 18],
+ 0.6,
+ [15, 84, 10],
+ 0.65,
+ [0, 69, 0],
+ ],
+ },
+ source: new GeoTIFF({
+ sources: [
+ {
+ // visible red, band 1 in the style expression above
+ url: 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/2020/S2A_36QWD_20200701_0_L2A/B04.tif',
+ max: 10000,
+ },
+ {
+ // near infrared, band 2 in the style expression above
+ url: 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/2020/S2A_36QWD_20200701_0_L2A/B08.tif',
+ max: 10000,
+ },
+ ],
+ }),
+ extent: sourceExtent,
+ }),
+ ],
+ view: new View({
+ center: getCenter(sourceExtent),
+ extent: sourceExtent,
+ zoom: 9,
+ projection: projection,
+ }),
+});
diff --git a/examples/cog-overviews.html b/examples/cog-overviews.html
new file mode 100644
index 0000000000..757d03225d
--- /dev/null
+++ b/examples/cog-overviews.html
@@ -0,0 +1,12 @@
+---
+layout: example.html
+title: GeoTIFF with Overviews
+shortdesc: Rendering a GeoTIFF with external overviews as a layer.
+docs: >
+ In some cases, a GeoTIFF may have external overviews. This example uses the
+ `overviews` property to provide URLs for the external overviews. The example
+ composes a false color composite using shortwave infrared (B6), near infrared (B5),
+ and visible green (B3) bands from a Landsat 8 image.
+tags: "cog"
+---
+
diff --git a/examples/cog-overviews.js b/examples/cog-overviews.js
new file mode 100644
index 0000000000..d0e46697dc
--- /dev/null
+++ b/examples/cog-overviews.js
@@ -0,0 +1,68 @@
+import GeoTIFF from '../src/ol/source/GeoTIFF.js';
+import Map from '../src/ol/Map.js';
+import Projection from '../src/ol/proj/Projection.js';
+import TileLayer from '../src/ol/layer/WebGLTile.js';
+import View from '../src/ol/View.js';
+import proj4 from 'proj4';
+import {getCenter} from '../src/ol/extent.js';
+import {register} from '../src/ol/proj/proj4.js';
+
+proj4.defs('EPSG:32645', '+proj=utm +zone=45 +datum=WGS84 +units=m +no_defs');
+register(proj4);
+
+const projection = new Projection({
+ code: 'EPSG:32645',
+ extent: [166021.44, 0.0, 534994.66, 9329005.18],
+});
+
+const sourceExtent = [382200, 2279370, 610530, 2512500];
+
+const base =
+ 'https://landsat-pds.s3.amazonaws.com/c1/L8/139/045/LC08_L1TP_139045_20170304_20170316_01_T1/LC08_L1TP_139045_20170304_20170316_01_T1';
+
+// scale values in this range to 0 - 1
+const min = 10000;
+const max = 15000;
+
+const map = new Map({
+ target: 'map',
+ layers: [
+ new TileLayer({
+ extent: sourceExtent,
+ style: {
+ saturation: -0.3,
+ },
+ source: new GeoTIFF({
+ sources: [
+ {
+ url: `${base}_B6.TIF`,
+ overviews: [`${base}_B6.TIF.ovr`],
+ min: min,
+ max: max,
+ nodata: 0,
+ },
+ {
+ url: `${base}_B5.TIF`,
+ overviews: [`${base}_B5.TIF.ovr`],
+ min: min,
+ max: max,
+ nodata: 0,
+ },
+ {
+ url: `${base}_B3.TIF`,
+ overviews: [`${base}_B3.TIF.ovr`],
+ min: min,
+ max: max,
+ nodata: 0,
+ },
+ ],
+ }),
+ }),
+ ],
+ view: new View({
+ center: getCenter(sourceExtent),
+ extent: sourceExtent,
+ zoom: 8,
+ projection: projection,
+ }),
+});
diff --git a/examples/cog-pyramid.html b/examples/cog-pyramid.html
new file mode 100644
index 0000000000..3e32e650ad
--- /dev/null
+++ b/examples/cog-pyramid.html
@@ -0,0 +1,12 @@
+---
+layout: example.html
+title: GeoTIFF tile pyramid
+shortdesc: Rendering a COG tile pyramid as layer group.
+docs: >
+ Data from a Cloud Optimized GeoTIFF (COG) tile pyramid can be rendered as a set of layers. In this
+ example, a pyramid of 3-band GeoTIFFs is used to render RGB data. For each tile of the pyramid, a
+ separate layer is created on demand. The lowest resolution layer serves as preview while higher resolutions are
+ loading.
+tags: "cog, tilepyramid, stac"
+---
+
diff --git a/examples/cog-pyramid.js b/examples/cog-pyramid.js
new file mode 100644
index 0000000000..ed63732a49
--- /dev/null
+++ b/examples/cog-pyramid.js
@@ -0,0 +1,71 @@
+import GeoTIFF from '../src/ol/source/GeoTIFF.js';
+import LayerGroup from '../src/ol/layer/Group.js';
+import Map from '../src/ol/Map.js';
+import TileGrid from '../src/ol/tilegrid/TileGrid.js';
+import View from '../src/ol/View.js';
+import WebGLTileLayer from '../src/ol/layer/WebGLTile.js';
+import {getIntersection} from '../src/ol/extent.js';
+
+// Metadata from https://s2downloads.eox.at/demo/EOxCloudless/2019/rgb/2019_EOxCloudless_rgb.json
+
+// Tile grid of the GeoTIFF pyramid layout
+const tileGrid = new TileGrid({
+ origin: [-180, 90],
+ resolutions: [0.703125, 0.3515625, 0.17578125, 8.7890625e-2, 4.39453125e-2],
+ tileSizes: [
+ [512, 256],
+ [1024, 512],
+ [2048, 1024],
+ [4096, 2048],
+ [4096, 4096],
+ ],
+});
+
+const pyramid = new LayerGroup();
+const layerForUrl = {};
+const zs = tileGrid.getResolutions().length;
+
+function useLayer(z, x, y) {
+ const url = `https://s2downloads.eox.at/demo/EOxCloudless/2019/rgb/${z}/${y}/${x}.tif`;
+ if (!(url in layerForUrl)) {
+ pyramid.getLayers().push(
+ new WebGLTileLayer({
+ minZoom: z,
+ maxZoom: z === 0 || z === zs - 1 ? undefined : z + 1,
+ extent: tileGrid.getTileCoordExtent([z, x, y]),
+ source: new GeoTIFF({
+ sources: [
+ {
+ url: url,
+ },
+ ],
+ }),
+ })
+ );
+ layerForUrl[url] = true;
+ }
+}
+
+const map = new Map({
+ target: 'map',
+ layers: [pyramid],
+ view: new View({
+ projection: 'EPSG:4326',
+ center: [0, 0],
+ zoom: 0,
+ showFullExtent: true,
+ }),
+});
+
+// Add overview layer
+useLayer(0, 0, 0);
+
+// Add layer for specific extent on demand
+map.on('moveend', () => {
+ const view = map.getView();
+ tileGrid.forEachTileCoord(
+ getIntersection([-180, -90, 180, 90], view.calculateExtent()),
+ tileGrid.getZForResolution(view.getResolution()),
+ ([z, x, y]) => useLayer(z, x, y)
+ );
+});
diff --git a/examples/cog.html b/examples/cog.html
new file mode 100644
index 0000000000..708b47bdde
--- /dev/null
+++ b/examples/cog.html
@@ -0,0 +1,10 @@
+---
+layout: example.html
+title: Cloud Optimized GeoTIFF (COG)
+shortdesc: Rendering a COG as a tiled layer.
+docs: >
+ Tiled data from a Cloud Optimized GeoTIFF (COG) can be rendered as a layer. In this
+ example, a single 3-band GeoTIFF is used to render RGB data.
+tags: "cog"
+---
+
diff --git a/examples/cog.js b/examples/cog.js
new file mode 100644
index 0000000000..9dc5bc4afb
--- /dev/null
+++ b/examples/cog.js
@@ -0,0 +1,41 @@
+import GeoTIFF from '../src/ol/source/GeoTIFF.js';
+import Map from '../src/ol/Map.js';
+import Projection from '../src/ol/proj/Projection.js';
+import TileLayer from '../src/ol/layer/WebGLTile.js';
+import View from '../src/ol/View.js';
+import proj4 from 'proj4';
+import {getCenter} from '../src/ol/extent.js';
+import {register} from '../src/ol/proj/proj4.js';
+
+proj4.defs('EPSG:32636', '+proj=utm +zone=36 +datum=WGS84 +units=m +no_defs');
+register(proj4);
+
+const projection = new Projection({
+ code: 'EPSG:32636',
+ extent: [166021.44, 0.0, 534994.66, 9329005.18],
+});
+
+// metadata from https://s3.us-west-2.amazonaws.com/sentinel-cogs/sentinel-s2-l2a-cogs/2020/S2A_36QWD_20200701_0_L2A/S2A_36QWD_20200701_0_L2A.json
+const sourceExtent = [499980, 1790220, 609780, 1900020];
+
+const map = new Map({
+ target: 'map',
+ layers: [
+ new TileLayer({
+ source: new GeoTIFF({
+ sources: [
+ {
+ url: 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/2020/S2A_36QWD_20200701_0_L2A/TCI.tif',
+ },
+ ],
+ }),
+ extent: sourceExtent,
+ }),
+ ],
+ view: new View({
+ center: getCenter(sourceExtent),
+ extent: sourceExtent,
+ zoom: 9,
+ projection: projection,
+ }),
+});
diff --git a/examples/data-tiles.html b/examples/data-tiles.html
new file mode 100644
index 0000000000..99ed41bebc
--- /dev/null
+++ b/examples/data-tiles.html
@@ -0,0 +1,9 @@
+---
+layout: example.html
+title: Data Tiles
+shortdesc: Generating tile data from scratch.
+docs: >
+ This example generates RGBA tile data from scratch.
+tags: "data tiles"
+---
+
diff --git a/examples/data-tiles.js b/examples/data-tiles.js
new file mode 100644
index 0000000000..6148d81d0a
--- /dev/null
+++ b/examples/data-tiles.js
@@ -0,0 +1,45 @@
+import DataTile from '../src/ol/source/DataTile.js';
+import Map from '../src/ol/Map.js';
+import TileLayer from '../src/ol/layer/WebGLTile.js';
+import View from '../src/ol/View.js';
+
+const size = 256;
+
+const canvas = document.createElement('canvas');
+canvas.width = size;
+canvas.height = size;
+
+const context = canvas.getContext('2d');
+context.strokeStyle = 'white';
+context.textAlign = 'center';
+context.font = '24px sans-serif';
+const lineHeight = 30;
+
+const map = new Map({
+ target: 'map',
+ layers: [
+ new TileLayer({
+ source: new DataTile({
+ loader: function (z, x, y) {
+ const half = size / 2;
+ context.clearRect(0, 0, size, size);
+ context.fillStyle = 'rgba(100, 100, 100, 0.5)';
+ context.fillRect(0, 0, size, size);
+ context.fillStyle = 'black';
+ context.fillText(`z: ${z}`, half, half - lineHeight);
+ context.fillText(`x: ${x}`, half, half);
+ context.fillText(`y: ${y}`, half, half + lineHeight);
+ context.strokeRect(0, 0, size, size);
+ const data = context.getImageData(0, 0, size, size).data;
+ return Promise.resolve(data);
+ },
+ // disable opacity transition to avoid overlapping labels during tile loading
+ transition: 0,
+ }),
+ }),
+ ],
+ view: new View({
+ center: [0, 0],
+ zoom: 0,
+ }),
+});
diff --git a/examples/webgl-sea-level.css b/examples/webgl-sea-level.css
new file mode 100644
index 0000000000..56b832a021
--- /dev/null
+++ b/examples/webgl-sea-level.css
@@ -0,0 +1,13 @@
+#level {
+ display: inline-block;
+ width: 150px;
+ vertical-align: text-bottom;
+}
+
+a.location {
+ cursor: pointer;
+}
+
+#map {
+ background: #8bd4ff;
+}
diff --git a/examples/webgl-sea-level.html b/examples/webgl-sea-level.html
new file mode 100644
index 0000000000..71dcc94131
--- /dev/null
+++ b/examples/webgl-sea-level.html
@@ -0,0 +1,37 @@
+---
+layout: example.html
+title: Sea Level (with WebGL)
+shortdesc: Render sea level at different elevations
+docs: >
+
+ The style property of a WebGL tile layer accepts a color expression that
+ can be used to modify pixel values before rendering. Here, RGB tiles representing elevation
+ data are loaded and rendered so that values at or below sea level are blue, and values
+ above sea level are transparent. The color expression operates on normalized pixel
+ values ranging from 0 to 1. The band operator is used to select normalized values
+ from a single band.
+
+ After converting the normalized RGB values to elevation, the interpolate expression
+ is used to pick colors to apply at a given elevation. Instead of using constant
+ numeric values as the stops in the colors array, the var operator allows you to
+ use a value that can be modified by your application. When the user drags the
+ sea level slider, the layer.updateStyleVariables() function is called to update
+ the level style variable with the value from the slider.
+
+tags: "webgl, math, flood"
+cloak:
+ - key: get_your_own_D6rA4zTHduk6KOKTXzGB
+ value: Get your own API key at https://www.maptiler.com/cloud/
+---
+
+
+ Sea level
+
+ + m
+
+
+Go to
+
San Francisco ,
+
New York ,
+
Mumbai , or
+
Shanghai
diff --git a/examples/webgl-sea-level.js b/examples/webgl-sea-level.js
new file mode 100644
index 0000000000..7490d17b5f
--- /dev/null
+++ b/examples/webgl-sea-level.js
@@ -0,0 +1,90 @@
+import Map from '../src/ol/Map.js';
+import TileLayer from '../src/ol/layer/WebGLTile.js';
+import View from '../src/ol/View.js';
+import XYZ from '../src/ol/source/XYZ.js';
+import {fromLonLat} from '../src/ol/proj.js';
+
+const key = 'get_your_own_D6rA4zTHduk6KOKTXzGB';
+const attributions =
+ '
© MapTiler ' +
+ '
© OpenStreetMap contributors ';
+
+const elevation = new TileLayer({
+ opacity: 0.6,
+ source: new XYZ({
+ url:
+ 'https://api.maptiler.com/tiles/terrain-rgb/{z}/{x}/{y}.png?key=' + key,
+ maxZoom: 10,
+ tileSize: 512,
+ crossOrigin: 'anonymous',
+ }),
+ style: {
+ variables: {
+ level: 0,
+ },
+ color: [
+ 'interpolate',
+ ['linear'],
+ // band math operates on normalized values from 0-1
+ // so we scale by 255 to align with the elevation formula
+ // from https://cloud.maptiler.com/tiles/terrain-rgb/
+ [
+ '+',
+ -10000,
+ [
+ '*',
+ 0.1 * 255,
+ [
+ '+',
+ ['*', 256 * 256, ['band', 1]],
+ ['+', ['*', 256, ['band', 2]], ['band', 3]],
+ ],
+ ],
+ ],
+ // use the `level` style variable as a stop in the color ramp
+ ['var', 'level'],
+ [139, 212, 255, 1],
+ ['+', 0.01, ['var', 'level']],
+ [139, 212, 255, 0],
+ ],
+ },
+});
+
+const map = new Map({
+ target: 'map',
+ layers: [
+ new TileLayer({
+ source: new XYZ({
+ url: 'https://api.maptiler.com/maps/streets/{z}/{x}/{y}.png?key=' + key,
+ attributions: attributions,
+ crossOrigin: 'anonymous',
+ tileSize: 512,
+ }),
+ }),
+ elevation,
+ ],
+ view: new View({
+ center: fromLonLat([-122.3267, 37.8377]),
+ zoom: 11,
+ }),
+});
+
+const control = document.getElementById('level');
+const output = document.getElementById('output');
+control.addEventListener('input', function () {
+ output.innerText = control.value;
+ elevation.updateStyleVariables({level: parseFloat(control.value)});
+});
+output.innerText = control.value;
+
+const locations = document.getElementsByClassName('location');
+for (let i = 0, ii = locations.length; i < ii; ++i) {
+ locations[i].addEventListener('click', relocate);
+}
+
+function relocate(event) {
+ const data = event.target.dataset;
+ const view = map.getView();
+ view.setCenter(fromLonLat(data.center.split(',').map(Number)));
+ view.setZoom(Number(data.zoom));
+}
diff --git a/examples/webgl-shaded-relief.css b/examples/webgl-shaded-relief.css
new file mode 100644
index 0000000000..71748fd2dc
--- /dev/null
+++ b/examples/webgl-shaded-relief.css
@@ -0,0 +1,7 @@
+table.controls td {
+ padding: 2px 5px;
+}
+table.controls td:nth-child(3) {
+ text-align: right;
+ min-width: 3em;
+}
diff --git a/examples/webgl-shaded-relief.html b/examples/webgl-shaded-relief.html
new file mode 100644
index 0000000000..469918cf52
--- /dev/null
+++ b/examples/webgl-shaded-relief.html
@@ -0,0 +1,32 @@
+---
+layout: example.html
+title: Shaded Relief (with WebGL)
+shortdesc: Calculate shaded relief from elevation data
+docs: >
+
+ For the shaded relief, a single tiled source of elevation data is used as input.
+ The shaded relief is calculated by the layer's style with a color
+ expression. The style variables are updated when the user drags one of the sliders. The
+ band operator is used to sample data from neighboring pixels for calculating slope and
+ aspect, which is done with the ['band', bandIndex, xOffset, yOffset] syntax.
+
+tags: "webgl, shaded relief"
+---
+
+
diff --git a/examples/webgl-shaded-relief.js b/examples/webgl-shaded-relief.js
new file mode 100644
index 0000000000..44cb70c8f0
--- /dev/null
+++ b/examples/webgl-shaded-relief.js
@@ -0,0 +1,91 @@
+import Map from '../src/ol/Map.js';
+import View from '../src/ol/View.js';
+import {OSM, XYZ} from '../src/ol/source.js';
+import {WebGLTile as TileLayer} from '../src/ol/layer.js';
+
+const variables = {};
+
+// The method used to extract elevations from the DEM.
+// In this case the format used is
+// red + green * 2 + blue * 3
+//
+// Other frequently used methods include the Mapbox format
+// (red * 256 * 256 + green * 256 + blue) * 0.1 - 10000
+// and the Terrarium format
+// (red * 256 + green + blue / 256) - 32768
+function elevation(xOffset, yOffset) {
+ return [
+ '+',
+ ['*', 256, ['band', 1, xOffset, yOffset]],
+ [
+ '+',
+ ['*', 2 * 256, ['band', 2, xOffset, yOffset]],
+ ['*', 3 * 256, ['band', 3, xOffset, yOffset]],
+ ],
+ ];
+}
+
+// Generates a shaded relief image given elevation data. Uses a 3x3
+// neighborhood for determining slope and aspect.
+const dp = ['*', 2, ['resolution']];
+const z0x = ['*', ['var', 'vert'], elevation(-1, 0)];
+const z1x = ['*', ['var', 'vert'], elevation(1, 0)];
+const dzdx = ['/', ['-', z1x, z0x], dp];
+const z0y = ['*', ['var', 'vert'], elevation(0, -1)];
+const z1y = ['*', ['var', 'vert'], elevation(0, 1)];
+const dzdy = ['/', ['-', z1y, z0y], dp];
+const slope = ['atan', ['^', ['+', ['^', dzdx, 2], ['^', dzdy, 2]], 0.5]];
+const aspect = ['clamp', ['atan', ['-', 0, dzdx], dzdy], -Math.PI, Math.PI];
+const sunEl = ['*', Math.PI / 180, ['var', 'sunEl']];
+const sunAz = ['*', Math.PI / 180, ['var', 'sunAz']];
+
+const cosIncidence = [
+ '+',
+ ['*', ['sin', sunEl], ['cos', slope]],
+ ['*', ['*', ['cos', sunEl], ['sin', slope]], ['cos', ['-', sunAz, aspect]]],
+];
+const scaled = ['*', 255, cosIncidence];
+
+const shadedRelief = new TileLayer({
+ opacity: 0.3,
+ source: new XYZ({
+ url: 'https://{a-d}.tiles.mapbox.com/v3/aj.sf-dem/{z}/{x}/{y}.png',
+ crossOrigin: 'anonymous',
+ }),
+ style: {
+ variables: variables,
+ color: ['color', scaled, scaled, scaled],
+ },
+});
+
+const controlIds = ['vert', 'sunEl', 'sunAz'];
+controlIds.forEach(function (id) {
+ const control = document.getElementById(id);
+ const output = document.getElementById(id + 'Out');
+ function updateValues() {
+ output.innerText = control.value;
+ variables[id] = Number(control.value);
+ }
+ updateValues();
+ control.addEventListener('input', () => {
+ updateValues();
+ shadedRelief.updateStyleVariables(variables);
+ });
+});
+
+const map = new Map({
+ target: 'map',
+ layers: [
+ new TileLayer({
+ source: new OSM(),
+ }),
+ shadedRelief,
+ ],
+ view: new View({
+ extent: [-13675026, 4439648, -13580856, 4580292],
+ center: [-13615645, 4497969],
+ minZoom: 10,
+ maxZoom: 16,
+ zoom: 13,
+ }),
+});
diff --git a/examples/webgl-tile-style.css b/examples/webgl-tile-style.css
new file mode 100644
index 0000000000..a24af258c9
--- /dev/null
+++ b/examples/webgl-tile-style.css
@@ -0,0 +1,4 @@
+#controls {
+ display: flex;
+ justify-content: space-around;
+}
diff --git a/examples/webgl-tile-style.html b/examples/webgl-tile-style.html
new file mode 100644
index 0000000000..e94e9b17d7
--- /dev/null
+++ b/examples/webgl-tile-style.html
@@ -0,0 +1,31 @@
+---
+layout: example.html
+title: WebGL Tile Layer Styles
+shortdesc: Styling raster tiles with WebGL.
+docs: >
+ The `style` property of a WebGL tile layer can be used to adjust properties like
+ `exposure`, `contrast`, and `saturation`. Typically those values would be set to
+ numeric constants to apply a filter to imagery. In this example, the style properties
+ are set to variables that can be updated based on application state. Adjusting the
+ sliders results in a call to `layer.updateStyleVariables()` to use new values for the
+ associated style properties.
+tags: "webgl, style"
+cloak:
+ - key: get_your_own_D6rA4zTHduk6KOKTXzGB
+ value: Get your own API key at https://www.maptiler.com/cloud/
+---
+
+
+
+
+ exposure
+
+
+
+ contrast
+
+
+
+ saturation
+
+
\ No newline at end of file
diff --git a/examples/webgl-tile-style.js b/examples/webgl-tile-style.js
new file mode 100644
index 0000000000..fce4addba1
--- /dev/null
+++ b/examples/webgl-tile-style.js
@@ -0,0 +1,55 @@
+import Map from '../src/ol/Map.js';
+import TileLayer from '../src/ol/layer/WebGLTile.js';
+import View from '../src/ol/View.js';
+import XYZ from '../src/ol/source/XYZ.js';
+
+const key = 'get_your_own_D6rA4zTHduk6KOKTXzGB';
+const attributions =
+ '
© MapTiler ' +
+ '
© OpenStreetMap contributors ';
+
+const variables = {
+ exposure: 0,
+ contrast: 0,
+ saturation: 0,
+};
+
+const layer = new TileLayer({
+ style: {
+ exposure: ['var', 'exposure'],
+ contrast: ['var', 'contrast'],
+ saturation: ['var', 'saturation'],
+ variables: variables,
+ },
+ source: new XYZ({
+ crossOrigin: 'anonymous', // TODO: determine if we can avoid this
+ attributions: attributions,
+ url: 'https://api.maptiler.com/tiles/satellite/{z}/{x}/{y}.jpg?key=' + key,
+ maxZoom: 20,
+ }),
+});
+
+const map = new Map({
+ target: 'map',
+ layers: [layer],
+ view: new View({
+ center: [0, 0],
+ zoom: 0,
+ }),
+});
+
+for (const name in variables) {
+ const element = document.getElementById(name);
+ const value = variables[name];
+ element.value = value.toString();
+ document.getElementById(`${name}-value`).innerText = `(${value})`;
+
+ element.addEventListener('input', function (event) {
+ const value = parseFloat(event.target.value);
+ document.getElementById(`${name}-value`).innerText = `(${value})`;
+
+ const updates = {};
+ updates[name] = value;
+ layer.updateStyleVariables(updates);
+ });
+}
diff --git a/examples/webgl-tiles.html b/examples/webgl-tiles.html
new file mode 100644
index 0000000000..31b40ffcf0
--- /dev/null
+++ b/examples/webgl-tiles.html
@@ -0,0 +1,9 @@
+---
+layout: example.html
+title: WebGL Tiles
+shortdesc: Rendering raster data with WebGL.
+docs: >
+ This example uses WebGL to raster tiles on a map.
+tags: "webgl, osm"
+---
+
diff --git a/examples/webgl-tiles.js b/examples/webgl-tiles.js
new file mode 100644
index 0000000000..4353f3043c
--- /dev/null
+++ b/examples/webgl-tiles.js
@@ -0,0 +1,17 @@
+import Map from '../src/ol/Map.js';
+import OSM from '../src/ol/source/OSM.js';
+import TileLayer from '../src/ol/layer/WebGLTile.js';
+import View from '../src/ol/View.js';
+
+const map = new Map({
+ target: 'map',
+ layers: [
+ new TileLayer({
+ source: new OSM(),
+ }),
+ ],
+ view: new View({
+ center: [0, 0],
+ zoom: 0,
+ }),
+});
diff --git a/examples/webpack/config.mjs b/examples/webpack/config.mjs
index 4f02305c56..81445dc823 100644
--- a/examples/webpack/config.mjs
+++ b/examples/webpack/config.mjs
@@ -101,6 +101,8 @@ export default {
resolve: {
fallback: {
fs: false,
+ http: false,
+ https: false,
},
alias: {
// allow imports from 'ol/module' instead of specifiying the source path
diff --git a/examples/webpack/worker-loader.cjs b/examples/webpack/worker-loader.cjs
index 15128ea840..891d60bafd 100644
--- a/examples/webpack/worker-loader.cjs
+++ b/examples/webpack/worker-loader.cjs
@@ -9,7 +9,12 @@ module.exports = function loader() {
build(this.resource, {minify})
.then((chunk) => {
for (const filePath in chunk.modules) {
- this.addDependency(filePath);
+ try {
+ const dependency = require.resolve(filePath);
+ this.addDependency(dependency);
+ } catch (e) {
+ // empty catch block
+ }
}
callback(null, chunk.code);
})
diff --git a/package-lock.json b/package-lock.json
index d16cab342f..d681b85f7b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "6.6.2-dev",
"license": "BSD-2-Clause",
"dependencies": {
+ "geotiff": "^1.0.4",
"ol-mapbox-style": "^6.4.1",
"pbf": "3.2.1",
"rbush": "^3.0.1"
@@ -62,6 +63,7 @@
"pngjs": "^6.0.0",
"proj4": "^2.7.5",
"puppeteer": "10.2.0",
+ "regenerator-runtime": "^0.13.9",
"rollup": "^2.42.3",
"rollup-plugin-terser": "^7.0.2",
"serve-static": "^1.14.0",
@@ -1837,6 +1839,15 @@
"eslint": ">=5.1.0"
}
},
+ "node_modules/@petamoriken/float16": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-1.1.1.tgz",
+ "integrity": "sha512-0r8nE5Q60tj3FbWWYLjAdGnWZgP7CMWXNaI5UsNzypRyxLDb/uvOl5SDw8GcPNu6pSTOt+KSI+0oL6fhSpNOFQ==",
+ "dependencies": {
+ "lodash": ">=4.17.5 <5.0.0",
+ "lodash-es": ">=4.17.5 <5.0.0"
+ }
+ },
"node_modules/@rollup/plugin-babel": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz",
@@ -2968,7 +2979,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true,
"engines": {
"node": ">=6"
}
@@ -3336,6 +3346,11 @@
"node": ">= 0.6"
}
},
+ "node_modules/content-type-parser": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/content-type-parser/-/content-type-parser-1.0.2.tgz",
+ "integrity": "sha512-lM4l4CnMEwOLHAHr/P6MEZwZFPJFtAAKgL6pogbXmVZggIqXhdB6RbBtPOTsw2FcXwYhehRGERJmRrjOiIB8pQ=="
+ },
"node_modules/convert-source-map": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz",
@@ -3576,7 +3591,6 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
- "dev": true,
"dependencies": {
"ms": "2.1.2"
},
@@ -4659,6 +4673,15 @@
"node": ">=8"
}
},
+ "node_modules/esm": {
+ "version": "3.2.25",
+ "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
+ "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
+ "optional": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/espree": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
@@ -5330,6 +5353,24 @@
"node": ">=6.9.0"
}
},
+ "node_modules/geotiff": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-1.0.4.tgz",
+ "integrity": "sha512-JmtpvVHlxvyrWgT6Uf0sy7flmXhjWtG0cqVv+G9fMcupV4DAPdTv7tkhsoMnn9RpIIwolveB/VnyII8cRMOD7A==",
+ "dependencies": {
+ "@petamoriken/float16": "^1.0.7",
+ "content-type-parser": "^1.0.2",
+ "lru-cache": "^6.0.0",
+ "pako": "^1.0.11",
+ "parse-headers": "^2.0.2",
+ "threads": "^1.3.1",
+ "txml": "^3.1.2"
+ },
+ "engines": {
+ "browsers": "defaults",
+ "node": ">=10.19"
+ }
+ },
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -5924,8 +5965,7 @@
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/internal-ip": {
"version": "6.2.0",
@@ -6190,6 +6230,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-observable": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-2.1.0.tgz",
+ "integrity": "sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw==",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-path-cwd": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
@@ -7123,8 +7174,12 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ },
+ "node_modules/lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/lodash.clonedeep": {
"version": "4.5.0",
@@ -7292,7 +7347,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
@@ -7699,8 +7753,7 @@
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/multicast-dns": {
"version": "6.2.3",
@@ -7947,6 +8000,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/observable-fns": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/observable-fns/-/observable-fns-0.6.1.tgz",
+ "integrity": "sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg=="
+ },
"node_modules/obuf": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
@@ -8172,6 +8230,11 @@
"node": ">=6"
}
},
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -8184,6 +8247,11 @@
"node": ">=6"
}
},
+ "node_modules/parse-headers": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.3.tgz",
+ "integrity": "sha512-QhhZ+DCCit2Coi2vmAKbq5RGTRcQUOE2+REgv8vdyu7MnYx2eZztegqtTx99TZ86GTIwqiy3+4nQTWZ2tgmdCA=="
+ },
"node_modules/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
@@ -8926,7 +8994,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
- "dev": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -8979,9 +9046,9 @@
}
},
"node_modules/regenerator-runtime": {
- "version": "0.13.7",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
- "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
+ "version": "0.13.9",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
+ "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
"dev": true
},
"node_modules/regenerator-transform": {
@@ -9965,7 +10032,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "dev": true,
"dependencies": {
"safe-buffer": "~5.2.0"
}
@@ -9974,7 +10040,6 @@
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "dev": true,
"funding": [
{
"type": "github",
@@ -10314,18 +10379,53 @@
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
"dev": true
},
+ "node_modules/threads": {
+ "version": "1.6.5",
+ "resolved": "https://registry.npmjs.org/threads/-/threads-1.6.5.tgz",
+ "integrity": "sha512-yL1NN4qZ25crW8wDoGn7TqbENJ69w3zCEjIGXpbqmQ4I+QHrG8+DLaZVKoX74OQUXWCI2lbbrUxDxAbr1xjDGQ==",
+ "dependencies": {
+ "callsites": "^3.1.0",
+ "debug": "^4.2.0",
+ "is-observable": "^2.1.0",
+ "observable-fns": "^0.6.1"
+ },
+ "funding": {
+ "url": "https://github.com/andywer/threads.js?sponsor=1"
+ },
+ "optionalDependencies": {
+ "tiny-worker": ">= 2"
+ }
+ },
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
"dev": true
},
+ "node_modules/through2": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz",
+ "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==",
+ "dependencies": {
+ "inherits": "^2.0.4",
+ "readable-stream": "2 || 3"
+ }
+ },
"node_modules/thunky": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
"dev": true
},
+ "node_modules/tiny-worker": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tiny-worker/-/tiny-worker-2.3.0.tgz",
+ "integrity": "sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==",
+ "optional": true,
+ "dependencies": {
+ "esm": "^3.2.25"
+ }
+ },
"node_modules/tmp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
@@ -10423,6 +10523,14 @@
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
"dev": true
},
+ "node_modules/txml": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/txml/-/txml-3.2.5.tgz",
+ "integrity": "sha512-AtN8AgJLiDanttIXJaQlxH8/R0NOCNwto8kcO7BaxdLgsN9b7itM9lnTD7c2O3TadP+hHB9j7ra5XGFRPNnk/g==",
+ "dependencies": {
+ "through2": "^3.0.1"
+ }
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -10630,8 +10738,7 @@
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
- "dev": true
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"node_modules/utils-merge": {
"version": "1.0.1",
@@ -11374,8 +11481,7 @@
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/yargs": {
"version": "17.1.1",
@@ -12680,6 +12786,15 @@
"minimatch": "^3.0.4"
}
},
+ "@petamoriken/float16": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-1.1.1.tgz",
+ "integrity": "sha512-0r8nE5Q60tj3FbWWYLjAdGnWZgP7CMWXNaI5UsNzypRyxLDb/uvOl5SDw8GcPNu6pSTOt+KSI+0oL6fhSpNOFQ==",
+ "requires": {
+ "lodash": ">=4.17.5 <5.0.0",
+ "lodash-es": ">=4.17.5 <5.0.0"
+ }
+ },
"@rollup/plugin-babel": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz",
@@ -13604,8 +13719,7 @@
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
},
"camelcase": {
"version": "6.2.0",
@@ -13901,6 +14015,11 @@
"integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==",
"dev": true
},
+ "content-type-parser": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/content-type-parser/-/content-type-parser-1.0.2.tgz",
+ "integrity": "sha512-lM4l4CnMEwOLHAHr/P6MEZwZFPJFtAAKgL6pogbXmVZggIqXhdB6RbBtPOTsw2FcXwYhehRGERJmRrjOiIB8pQ=="
+ },
"convert-source-map": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz",
@@ -14082,7 +14201,6 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
- "dev": true,
"requires": {
"ms": "2.1.2"
}
@@ -14922,6 +15040,12 @@
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
"dev": true
},
+ "esm": {
+ "version": "3.2.25",
+ "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
+ "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
+ "optional": true
+ },
"espree": {
"version": "7.3.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz",
@@ -15448,6 +15572,20 @@
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"dev": true
},
+ "geotiff": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-1.0.4.tgz",
+ "integrity": "sha512-JmtpvVHlxvyrWgT6Uf0sy7flmXhjWtG0cqVv+G9fMcupV4DAPdTv7tkhsoMnn9RpIIwolveB/VnyII8cRMOD7A==",
+ "requires": {
+ "@petamoriken/float16": "^1.0.7",
+ "content-type-parser": "^1.0.2",
+ "lru-cache": "^6.0.0",
+ "pako": "^1.0.11",
+ "parse-headers": "^2.0.2",
+ "threads": "^1.3.1",
+ "txml": "^3.1.2"
+ }
+ },
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
@@ -15895,8 +16033,7 @@
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"internal-ip": {
"version": "6.2.0",
@@ -16071,6 +16208,11 @@
"integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==",
"dev": true
},
+ "is-observable": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-2.1.0.tgz",
+ "integrity": "sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw=="
+ },
"is-path-cwd": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
@@ -16802,8 +16944,12 @@
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "dev": true
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
+ },
+ "lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"lodash.clonedeep": {
"version": "4.5.0",
@@ -16939,7 +17085,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
"requires": {
"yallist": "^4.0.0"
}
@@ -17249,8 +17394,7 @@
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"multicast-dns": {
"version": "6.2.3",
@@ -17441,6 +17585,11 @@
"es-abstract": "^1.18.2"
}
},
+ "observable-fns": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/observable-fns/-/observable-fns-0.6.1.tgz",
+ "integrity": "sha512-9gRK4+sRWzeN6AOewNBTLXir7Zl/i3GB6Yl26gK4flxz8BXVpD3kt8amREmWNb0mxYOGDotvE5a4N+PtGGKdkg=="
+ },
"obuf": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
@@ -17599,6 +17748,11 @@
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
"dev": true
},
+ "pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
+ },
"parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -17608,6 +17762,11 @@
"callsites": "^3.0.0"
}
},
+ "parse-headers": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.3.tgz",
+ "integrity": "sha512-QhhZ+DCCit2Coi2vmAKbq5RGTRcQUOE2+REgv8vdyu7MnYx2eZztegqtTx99TZ86GTIwqiy3+4nQTWZ2tgmdCA=="
+ },
"parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
@@ -18178,7 +18337,6 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
- "dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
@@ -18219,9 +18377,9 @@
}
},
"regenerator-runtime": {
- "version": "0.13.7",
- "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
- "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==",
+ "version": "0.13.9",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
+ "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==",
"dev": true
},
"regenerator-transform": {
@@ -19015,7 +19173,6 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
- "dev": true,
"requires": {
"safe-buffer": "~5.2.0"
},
@@ -19023,8 +19180,7 @@
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
- "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
- "dev": true
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}
}
},
@@ -19274,18 +19430,48 @@
"integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
"dev": true
},
+ "threads": {
+ "version": "1.6.5",
+ "resolved": "https://registry.npmjs.org/threads/-/threads-1.6.5.tgz",
+ "integrity": "sha512-yL1NN4qZ25crW8wDoGn7TqbENJ69w3zCEjIGXpbqmQ4I+QHrG8+DLaZVKoX74OQUXWCI2lbbrUxDxAbr1xjDGQ==",
+ "requires": {
+ "callsites": "^3.1.0",
+ "debug": "^4.2.0",
+ "is-observable": "^2.1.0",
+ "observable-fns": "^0.6.1",
+ "tiny-worker": ">= 2"
+ }
+ },
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
"dev": true
},
+ "through2": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz",
+ "integrity": "sha512-enaDQ4MUyP2W6ZyT6EsMzqBPZaM/avg8iuo+l2d3QCs0J+6RaqkHV/2/lOwDTueBHeJ/2LG9lrLW3d5rWPucuQ==",
+ "requires": {
+ "inherits": "^2.0.4",
+ "readable-stream": "2 || 3"
+ }
+ },
"thunky": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
"dev": true
},
+ "tiny-worker": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tiny-worker/-/tiny-worker-2.3.0.tgz",
+ "integrity": "sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==",
+ "optional": true,
+ "requires": {
+ "esm": "^3.2.25"
+ }
+ },
"tmp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
@@ -19364,6 +19550,14 @@
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
"dev": true
},
+ "txml": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/txml/-/txml-3.2.5.tgz",
+ "integrity": "sha512-AtN8AgJLiDanttIXJaQlxH8/R0NOCNwto8kcO7BaxdLgsN9b7itM9lnTD7c2O3TadP+hHB9j7ra5XGFRPNnk/g==",
+ "requires": {
+ "through2": "^3.0.1"
+ }
+ },
"type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -19518,8 +19712,7 @@
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
- "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
- "dev": true
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"utils-merge": {
"version": "1.0.1",
@@ -20044,8 +20237,7 @@
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"yargs": {
"version": "17.1.1",
diff --git a/package.json b/package.json
index 51a215d57e..390c70101f 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
"url": "https://opencollective.com/openlayers"
},
"dependencies": {
+ "geotiff": "^1.0.4",
"ol-mapbox-style": "^6.4.1",
"pbf": "3.2.1",
"rbush": "^3.0.1"
@@ -98,6 +99,7 @@
"pngjs": "^6.0.0",
"proj4": "^2.7.5",
"puppeteer": "10.2.0",
+ "regenerator-runtime": "^0.13.9",
"rollup": "^2.42.3",
"rollup-plugin-terser": "^7.0.2",
"serve-static": "^1.14.0",
diff --git a/src/ol/DataTile.js b/src/ol/DataTile.js
new file mode 100644
index 0000000000..c63eeb5b49
--- /dev/null
+++ b/src/ol/DataTile.js
@@ -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
} 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;
diff --git a/src/ol/layer.js b/src/ol/layer.js
index 790d400f3a..7b51f75fb4 100644
--- a/src/ol/layer.js
+++ b/src/ol/layer.js
@@ -13,3 +13,4 @@ export {default as Vector} from './layer/Vector.js';
export {default as VectorImage} from './layer/VectorImage.js';
export {default as VectorTile} from './layer/VectorTile.js';
export {default as WebGLPoints} from './layer/WebGLPoints.js';
+export {default as WebGLTile} from './layer/WebGLTile.js';
diff --git a/src/ol/layer/WebGLTile.js b/src/ol/layer/WebGLTile.js
new file mode 100644
index 0000000000..7080c7ce3c
--- /dev/null
+++ b/src/ol/layer/WebGLTile.js
@@ -0,0 +1,309 @@
+/**
+ * @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
+ * Translates tile data to rendered pixels.
+ *
+ * @property {Object} [variables] Style variables. Each variable must hold a number. These
+ * variables can be used in the `color`, `brightness`, `contrast`, `exposure`, `saturation` and `gamma`
+ * {@link import("../style/expressions.js").ExpressionValue expressions}, using the `['var', 'varName']` operator.
+ * To update style variables, use the {@link import("./WebGLTile.js").default#updateStyleVariables} method.
+ * @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} 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} */
+ 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};
+ uniform float ${Uniforms.TEXTURE_PIXEL_WIDTH};
+ uniform float ${Uniforms.TEXTURE_PIXEL_HEIGHT};
+ uniform float ${Uniforms.RESOLUTION};
+ uniform float ${Uniforms.ZOOM};
+
+ ${uniformDeclarations.join('\n')}
+
+ void main() {
+ ${colorAssignments.join('\n')}
+
+ vec4 color = color0;
+
+ ${pipeline.join('\n')}
+
+ if (color.a == 0.0) {
+ discard;
+ }
+
+ gl_FragColor = color;
+ gl_FragColor.rgb *= gl_FragColor.a;
+ gl_FragColor *= ${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.
+ *
+ * @extends BaseTileLayer
+ * @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);
+
+ /**
+ * @type {Style}
+ */
+ this.style_ = style;
+ }
+
+ /**
+ * Create a renderer for this layer.
+ * @return {import("../renderer/Layer.js").default} A layer renderer.
+ * @protected
+ */
+ createRenderer() {
+ const source = this.getSource();
+ const parsedStyle = parseStyle(
+ this.style_,
+ 'bandCount' in source ? source.bandCount : 4
+ );
+
+ this.styleVariables_ = this.style_.variables || {};
+
+ return new WebGLTileLayerRenderer(this, {
+ vertexShader: parsedStyle.vertexShader,
+ fragmentShader: parsedStyle.fragmentShader,
+ uniforms: parsedStyle.uniforms,
+ className: this.getClassName(),
+ });
+ }
+
+ /**
+ * Update any variables used by the layer style and trigger a re-render.
+ * @param {Object} variables Variables to update.
+ * @api
+ */
+ updateStyleVariables(variables) {
+ assign(this.styleVariables_, variables);
+ this.changed();
+ }
+}
+
+export default WebGLTileLayer;
diff --git a/src/ol/renderer/webgl/TileLayer.js b/src/ol/renderer/webgl/TileLayer.js
new file mode 100644
index 0000000000..48f0a197d9
--- /dev/null
+++ b/src/ol/renderer/webgl/TileLayer.js
@@ -0,0 +1,543 @@
+/**
+ * @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 {isEmpty} from '../../extent.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',
+ TEXTURE_PIXEL_WIDTH: 'u_texturePixelWidth',
+ TEXTURE_PIXEL_HEIGHT: 'u_texturePixelHeight',
+ RESOLUTION: 'u_resolution',
+ ZOOM: 'u_zoom',
+};
+
+export const Attributes = {
+ TEXTURE_COORD: 'a_textureCoord',
+};
+
+/**
+ * @type {Array}
+ */
+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>} 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.
+ * @return {import("../../extent.js").Extent} Extent.
+ */
+function getRenderExtent(frameState) {
+ const layerState = frameState.layerStatesArray[frameState.layerIndex];
+ let extent = frameState.extent;
+ if (layerState.extent) {
+ extent = getIntersection(
+ extent,
+ fromUserExtent(layerState.extent, frameState.viewState.projection)
+ );
+ }
+ return extent;
+}
+
+/**
+ * @typedef {Object} Options
+ * @property {string} vertexShader Vertex shader source.
+ * @property {string} fragmentShader Fragment shader source.
+ * @property {Object} [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}
+ * @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;
+ }
+
+ /**
+ * @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)
+ );
+ }
+
+ /**
+ * Determine whether render should be called.
+ * @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
+ * @return {boolean} Layer is ready to be rendered.
+ */
+ prepareFrame(frameState) {
+ if (isEmpty(getRenderExtent(frameState))) {
+ return false;
+ }
+ 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 viewState = frameState.viewState;
+ const layerState = frameState.layerStatesArray[frameState.layerIndex];
+ const extent = getRenderExtent(frameState);
+ const tileLayer = this.getLayer();
+ const tileSource = tileLayer.getSource();
+ const tileGrid = tileSource.getTileGridForProjection(viewState.projection);
+ const z = tileGrid.getZForResolution(
+ viewState.resolution,
+ tileSource.zDirection
+ );
+
+ /**
+ * @type {Object>}
+ */
+ 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, tile;
+ if (tileTextureCache.containsKey(tileCoordKey)) {
+ tileTexture = tileTextureCache.get(tileCoordKey);
+ 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, tileGrid, this.helper);
+ tileTextureCache.set(tileCoordKey, tileTexture);
+ } else {
+ tileTexture.setTile(
+ this.isDrawableTile(tile) ? tile : tile.getInterimTile()
+ );
+ }
+ }
+
+ 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,
+ ]);
+ }
+ }
+ }
+ }
+
+ /**
+ * 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}
+ */
+ 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;
+ }
+
+ // 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, !blend);
+
+ 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.setUniformFloatValue(
+ Uniforms.TEXTURE_PIXEL_WIDTH,
+ tileSize[0]
+ );
+ this.helper.setUniformFloatValue(
+ Uniforms.TEXTURE_PIXEL_HEIGHT,
+ tileSize[1]
+ );
+ 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);
+
+ 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>} 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;
diff --git a/src/ol/source.js b/src/ol/source.js
index 155d039c5b..c1efbce95f 100644
--- a/src/ol/source.js
+++ b/src/ol/source.js
@@ -5,6 +5,8 @@
export {default as BingMaps} from './source/BingMaps.js';
export {default as CartoDB} from './source/CartoDB.js';
export {default as Cluster} from './source/Cluster.js';
+export {default as DataTile} from './source/DataTile.js';
+export {default as GeoTIFF} from './source/GeoTIFF.js';
export {default as IIIF} from './source/IIIF.js';
export {default as Image} from './source/Image.js';
export {default as ImageArcGISRest} from './source/ImageArcGISRest.js';
diff --git a/src/ol/source/DataTile.js b/src/ol/source/DataTile.js
new file mode 100644
index 0000000000..3a813aa34e
--- /dev/null
+++ b/src/ol/source/DataTile.js
@@ -0,0 +1,153 @@
+/**
+ * @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 {assign} from '../obj.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} [loader] Data loader. Called with z, x, and y tile coordinates.
+ * Returns a promise that resolves to a {@link import("../DataTile.js").Data}.
+ * @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.
+ *
+ * @fires import("./Tile.js").TileSourceEvent
+ * @api
+ */
+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}
+ */
+ this.tileLoadingKeys_ = {};
+
+ /**
+ * @private
+ */
+ this.loader_ = options.loader;
+
+ this.handleTileChange_ = this.handleTileChange_.bind(this);
+
+ /**
+ * @type {number}
+ */
+ this.bandCount = 4; // assume RGBA
+ }
+
+ /**
+ * @param {function(number, number, number) : Promise} 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 {!DataTile} 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(
+ assign({tileCoord: [z, x, y], loader: loader}, this.tileOptions)
+ );
+ tile.key = this.getKey();
+ 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;
diff --git a/src/ol/source/GeoTIFF.js b/src/ol/source/GeoTIFF.js
new file mode 100644
index 0000000000..af20f311d2
--- /dev/null
+++ b/src/ol/source/GeoTIFF.js
@@ -0,0 +1,517 @@
+/**
+ * @module ol/source/GeoTIFF
+ */
+import DataTile from './DataTile.js';
+import State from './State.js';
+import TileGrid from '../tilegrid/TileGrid.js';
+import {Pool, fromUrl as tiffFromUrl, fromUrls as tiffFromUrls} from 'geotiff';
+import {create as createDecoderWorker} from '../worker/geotiff-decoder.js';
+import {getIntersection} from '../extent.js';
+import {get as getProjection} from '../proj.js';
+import {toSize} from '../size.js';
+
+/**
+ * @typedef {Object} SourceInfo
+ * @property {string} url URL for the source GeoTIFF.
+ * @property {Array} [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. When provided, an additional band (alpha) will be added
+ * to the data.
+ * @property {Array} [bands] Indices of the bands to be read from. If not provided, all bands will
+ * be read. If, for example, a GeoTIFF has red, green, blue and near-infrared bands and you only need the
+ * infrared band, configure `bands: [3]`.
+ */
+
+let workerPool;
+function getWorkerPool() {
+ if (!workerPool) {
+ workerPool = new Pool(undefined, createDecoderWorker());
+ }
+ return workerPool;
+}
+
+/**
+ * @param {import("geotiff/src/geotiff.js").GeoTIFF|import("geotiff/src/geotiff.js").MultiGeoTIFF} tiff A GeoTIFF.
+ * @return {Promise>} 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>} 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|Array>} expected Expected value.
+ * @param {number|Array|Array>} got Actual value.
+ * @param {number} tolerance Accepted tolerance in fraction of expected between expected and got.
+ * @param {string} message The error message.
+ */
+function assertEqual(expected, got, tolerance, 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], tolerance, message);
+ }
+ return;
+ }
+
+ got = /** @type {number} */ (got);
+ if (Math.abs(expected - got) > tolerance * expected) {
+ 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 {Object} Options
+ * @property {Array} sources List of information about GeoTIFF sources.
+ * Multiple sources can be combined when their resolution sets are equal after applying a scale.
+ * The list of sources defines a mapping between input bands as they are read from each GeoTIFF, and
+ * the output bands that are provided by data tiles. To control which bands to read from each GeoTIFF,
+ * use the {@link import("./GeoTIFF.js").SourceInfo bands} property. If, for example, you spedify two
+ * sources, one with 3 bands and {@link import("./GeoTIFF.js").SourceInfo nodata} configured, and
+ * another with 1 band, the resulting data tiles will have 5 bands: 3 from the first source, 1 alpha
+ * band from the first source, and 1 band from the second source.
+ */
+
+/**
+ * @classdesc
+ * A source for working with GeoTIFF data.
+ * @api
+ */
+class GeoTIFFSource extends DataTile {
+ /**
+ * @param {Options} options Data tile options.
+ */
+ constructor(options) {
+ super({
+ state: State.LOADING,
+ tileGrid: null,
+ projection: null,
+ });
+
+ /**
+ * @type {Array}
+ * @private
+ */
+ this.sourceInfo_ = options.sources;
+
+ const numSources = this.sourceInfo_.length;
+
+ /**
+ * @type {Array>}
+ * @private
+ */
+ this.sourceImagery_ = new Array(numSources);
+
+ /**
+ * @type {Array}
+ * @private
+ */
+ this.resolutionFactors_ = new Array(numSources);
+
+ /**
+ * @type {Array}
+ * @private
+ */
+ this.samplesPerPixel_;
+
+ /**
+ * @type {Array>}
+ * @private
+ */
+ this.nodataValues_;
+
+ /**
+ * @type {boolean}
+ * @private
+ */
+ this.addAlpha_ = false;
+
+ /**
+ * @type {Error}
+ * @private
+ */
+ this.error_ = null;
+
+ this.setKey(this.sourceInfo_.map((source) => source.url).join(','));
+
+ 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. When the source state is `error`, use this function
+ * to get more information about the error. To debug a faulty configuration, you may want to use
+ * a listener like
+ * ```js
+ * geotiffSource.on('change', () => {
+ * if (geotiffSource.getState() === 'error') {
+ * console.error(geotiffSource.getError());
+ * }
+ * });
+ * ```
+ */
+ 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>} sources Each source is a list of images
+ * from a single GeoTIFF.
+ * @private
+ */
+ configure_(sources) {
+ let extent;
+ let origin;
+ let tileSizes;
+ let resolutions;
+ const samplesPerPixel = new Array(sources.length);
+ const nodataValues = new Array(sources.length);
+ let minZoom = 0;
+
+ 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);
+
+ nodataValues[sourceIndex] = new Array(imageCount);
+
+ for (let imageIndex = 0; imageIndex < imageCount; ++imageIndex) {
+ const image = images[imageIndex];
+ const nodataValue = image.getGDALNoData();
+ nodataValues[sourceIndex][imageIndex] =
+ nodataValue === null ? NaN : nodataValue;
+
+ const wantedSamples = this.sourceInfo_[sourceIndex].bands;
+ samplesPerPixel[sourceIndex] = wantedSamples
+ ? wantedSamples.length
+ : image.getSamplesPerPixel();
+ 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 {
+ getIntersection(extent, sourceExtent, extent);
+ }
+
+ if (!origin) {
+ origin = sourceOrigin;
+ } else {
+ const message = `Origin mismatch for source ${sourceIndex}, got [${sourceOrigin}] but expected [${origin}]`;
+ assertEqual(origin, sourceOrigin, 0, message);
+ }
+
+ if (!resolutions) {
+ resolutions = sourceResolutions;
+ this.resolutionFactors_[sourceIndex] = 1;
+ } else {
+ if (resolutions.length - minZoom > sourceResolutions.length) {
+ minZoom = resolutions.length - sourceResolutions.length;
+ }
+ const resolutionFactor =
+ resolutions[resolutions.length - 1] /
+ sourceResolutions[sourceResolutions.length - 1];
+ this.resolutionFactors_[sourceIndex] = resolutionFactor;
+ const scaledSourceResolutions = sourceResolutions.map(
+ (resolution) => (resolution *= resolutionFactor)
+ );
+ const message = `Resolution mismatch for source ${sourceIndex}, got [${scaledSourceResolutions}] but expected [${resolutions}]`;
+ assertEqual(
+ resolutions.slice(minZoom, resolutions.length),
+ scaledSourceResolutions,
+ 0.005,
+ message
+ );
+ }
+
+ if (!tileSizes) {
+ tileSizes = sourceTileSizes;
+ } else {
+ assertEqual(
+ tileSizes.slice(minZoom, tileSizes.length),
+ sourceTileSizes,
+ 0,
+ `Tile size mismatch for source ${sourceIndex}`
+ );
+ }
+
+ this.sourceImagery_[sourceIndex] = images.reverse();
+ }
+
+ for (let i = 0, ii = this.sourceImagery_.length; i < ii; ++i) {
+ const sourceImagery = this.sourceImagery_[i];
+ while (sourceImagery.length < resolutions.length) {
+ sourceImagery.unshift(undefined);
+ }
+ }
+
+ 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}`);
+ }
+ }
+ }
+
+ this.samplesPerPixel_ = samplesPerPixel;
+ this.nodataValues_ = nodataValues;
+
+ // decide if we need to add an alpha band to handle nodata
+ outer: for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) {
+ // option 1: source is configured with a nodata value
+ if (this.sourceInfo_[sourceIndex].nodata !== undefined) {
+ this.addAlpha_ = true;
+ break;
+ }
+
+ const values = nodataValues[sourceIndex];
+
+ // option 2: check image metadata for limited bands
+ const bands = this.sourceInfo_[sourceIndex].bands;
+ if (bands) {
+ for (let i = 0; i < bands.length; ++i) {
+ if (!isNaN(values[bands[i]])) {
+ this.addAlpha_ = true;
+ break outer;
+ }
+ }
+ continue;
+ }
+
+ // option 3: check image metadata for all bands
+ for (let imageIndex = 0; imageIndex < values.length; ++imageIndex) {
+ if (!isNaN(values[imageIndex])) {
+ this.addAlpha_ = true;
+ break outer;
+ }
+ }
+ }
+
+ const additionalBands = this.addAlpha_ ? 1 : 0;
+ this.bandCount =
+ samplesPerPixel.reduce((accumulator, value) => {
+ accumulator += value;
+ return accumulator;
+ }, 0) + additionalBands;
+
+ const tileGrid = new TileGrid({
+ extent: extent,
+ minZoom: minZoom,
+ 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 sourceCount = this.sourceImagery_.length;
+ const requests = new Array(sourceCount);
+ const addAlpha = this.addAlpha_;
+ const bandCount = this.bandCount;
+ const samplesPerPixel = this.samplesPerPixel_;
+ const sourceInfo = this.sourceInfo_;
+ for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) {
+ const source = sourceInfo[sourceIndex];
+ const resolutionFactor = this.resolutionFactors_[sourceIndex];
+ const pixelBounds = [
+ Math.round(x * (size[0] * resolutionFactor)),
+ Math.round(y * (size[1] * resolutionFactor)),
+ Math.round((x + 1) * (size[0] * resolutionFactor)),
+ Math.round((y + 1) * (size[1] * resolutionFactor)),
+ ];
+ const image = this.sourceImagery_[sourceIndex][z];
+ requests[sourceIndex] = image.readRasters({
+ window: pixelBounds,
+ width: size[0],
+ height: size[1],
+ samples: source.bands,
+ fillValue: source.nodata,
+ pool: getWorkerPool(),
+ });
+ }
+
+ const pixelCount = size[0] * size[1];
+ const dataLength = pixelCount * bandCount;
+ const nodataValues = this.nodataValues_;
+
+ return Promise.all(requests).then(function (sourceSamples) {
+ const data = new Uint8ClampedArray(dataLength);
+ let dataIndex = 0;
+ for (let pixelIndex = 0; pixelIndex < pixelCount; ++pixelIndex) {
+ let transparent = addAlpha;
+ 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;
+
+ for (
+ let sampleIndex = 0;
+ sampleIndex < samplesPerPixel[sourceIndex];
+ ++sampleIndex
+ ) {
+ const sourceValue =
+ sourceSamples[sourceIndex][sampleIndex][pixelIndex];
+
+ const value = gain * sourceValue + bias;
+ if (!addAlpha) {
+ data[dataIndex] = value;
+ } else {
+ let nodata = source.nodata;
+ if (nodata === undefined) {
+ let bandIndex;
+ if (source.bands) {
+ bandIndex = source.bands[sampleIndex];
+ } else {
+ bandIndex = sampleIndex;
+ }
+ nodata = nodataValues[sourceIndex][bandIndex];
+ }
+
+ if (sourceValue !== nodata) {
+ transparent = false;
+ data[dataIndex] = value;
+ }
+ }
+ dataIndex++;
+ }
+ }
+ if (addAlpha) {
+ if (!transparent) {
+ data[dataIndex] = 255;
+ }
+ dataIndex++;
+ }
+ }
+
+ return data;
+ });
+ }
+}
+
+export default GeoTIFFSource;
diff --git a/src/ol/style/expressions.js b/src/ol/style/expressions.js
index 106190727e..37e371e6f2 100644
--- a/src/ol/style/expressions.js
+++ b/src/ol/style/expressions.js
@@ -3,21 +3,29 @@
* @module ol/style/expressions
*/
+import {Uniforms} from '../renderer/webgl/TileLayer.js';
import {asArray, isStringColor} from '../color.js';
/**
* Base type used for literal style parameters; can be a number literal or the output of an operator,
- * which in turns takes {@link ExpressionValue} arguments.
+ * which in turns takes {@link import("./expressions.js").ExpressionValue} arguments.
*
* The following operators can be used:
*
* * Reading operators:
+ * * `['band', bandIndex, xOffset, yOffset]` For tile layers only. Fetches pixel values from band
+ * `bandIndex` of the source's data. The first `bandIndex` of the source data is `1`. Fetched values
+ * are in the 0..1 range. {@link import("../source/TileImage.js").default} sources have 4 bands: red,
+ * green, blue and alpha. {@link import("../source/DataTile.js").default} sources can have any number
+ * of bands, depending on the underlying data source and
+ * {@link import("../source/GeoTIFF.js").Options configuration}. `xOffset` and `yOffset` are optional
+ * and allow specifying pixel offsets for x and y. This is used for sampling data from neighboring pixels.
* * `['get', 'attributeName']` fetches a feature attribute (it will be prefixed by `a_` in the shader)
* Note: those will be taken from the attributes provided to the renderer
- * * `['var', 'varName']` fetches a value from the style variables, or 0 if undefined
- * * `['time']` returns the time in seconds since the creation of the layer
- * * `['zoom']` returns the current zoom level
* * `['resolution']` returns the current resolution
+ * * `['time']` returns the time in seconds since the creation of the layer
+ * * `['var', 'varName']` fetches a value from the style variables, or 0 if undefined
+ * * `['zoom']` returns the current zoom level
*
* * Math operators:
* * `['*', value1, value2]` multiplies `value1` by `value2`
@@ -27,6 +35,10 @@ import {asArray, isStringColor} from '../color.js';
* * `['clamp', value, low, high]` clamps `value` between `low` and `high`
* * `['%', value1, value2]` returns the result of `value1 % value2` (modulo)
* * `['^', value1, value2]` returns the value of `value1` raised to the `value2` power
+ * * `['abs', value1]` returns the absolute value of `value1`
+ * * `['sin', value1]` returns the sine of `value1`
+ * * `['cos', value1]` returns the cosine of `value1`
+ * * `['atan', value1, value2]` returns `atan2(value1, value2)`. If `value2` is not provided, returns `atan(value1)`
*
* * Transform operators:
* * `['case', condition1, output1, ...conditionN, outputN, fallback]` selects the first output whose corresponding
@@ -73,6 +85,7 @@ import {asArray, isStringColor} from '../color.js';
* * {@link module:ol/color~Color}
*
* @typedef {Array<*>|import("../color.js").Color|string|number|boolean} ExpressionValue
+ * @api
*/
/**
@@ -170,6 +183,7 @@ export function isTypeUnique(valueType) {
* @property {Array} variables List of variables used in the expression; contains **unprefixed names**
* @property {Array} attributes List of attributes used in the expression; contains **unprefixed names**
* @property {Object} stringLiteralsMap This object maps all encountered string values to a number
+ * @property {number} [bandCount] Number of bands per pixel.
*/
/**
@@ -402,6 +416,43 @@ Operators['var'] = {
},
};
+Operators['band'] = {
+ getReturnType: function (args) {
+ return ValueTypes.NUMBER;
+ },
+ toGlsl: function (context, args) {
+ assertArgsMinCount(args, 1);
+ assertArgsMaxCount(args, 3);
+ 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;
+ }
+ if (args.length === 1) {
+ return `color${colorIndex}[${bandIndex}]`;
+ } else {
+ const xOffset = args[1];
+ const yOffset = args[2] || 0;
+ assertNumber(xOffset);
+ assertNumber(yOffset);
+ const uniformName = Uniforms.TILE_TEXTURE_PREFIX + colorIndex;
+ return `texture2D(${uniformName}, v_textureCoord + vec2(${expressionToGlsl(
+ context,
+ xOffset
+ )} / ${Uniforms.TEXTURE_PIXEL_WIDTH}, ${expressionToGlsl(
+ context,
+ yOffset
+ )} / ${Uniforms.TEXTURE_PIXEL_HEIGHT}))[${bandIndex}]`;
+ }
+ },
+};
+
Operators['time'] = {
getReturnType: function (args) {
return ValueTypes.NUMBER;
@@ -529,6 +580,56 @@ Operators['^'] = {
},
};
+Operators['abs'] = {
+ getReturnType: function (args) {
+ return ValueTypes.NUMBER;
+ },
+ toGlsl: function (context, args) {
+ assertArgsCount(args, 1);
+ assertNumbers(args);
+ return `abs(${expressionToGlsl(context, args[0])})`;
+ },
+};
+
+Operators['sin'] = {
+ getReturnType: function (args) {
+ return ValueTypes.NUMBER;
+ },
+ toGlsl: function (context, args) {
+ assertArgsCount(args, 1);
+ assertNumbers(args);
+ return `sin(${expressionToGlsl(context, args[0])})`;
+ },
+};
+
+Operators['cos'] = {
+ getReturnType: function (args) {
+ return ValueTypes.NUMBER;
+ },
+ toGlsl: function (context, args) {
+ assertArgsCount(args, 1);
+ assertNumbers(args);
+ return `cos(${expressionToGlsl(context, args[0])})`;
+ },
+};
+
+Operators['atan'] = {
+ getReturnType: function (args) {
+ return ValueTypes.NUMBER;
+ },
+ toGlsl: function (context, args) {
+ assertArgsMinCount(args, 1);
+ assertArgsMaxCount(args, 2);
+ assertNumbers(args);
+ return args.length === 2
+ ? `atan(${expressionToGlsl(context, args[0])}, ${expressionToGlsl(
+ context,
+ args[1]
+ )})`
+ : `atan(${expressionToGlsl(context, args[0])})`;
+ },
+};
+
Operators['>'] = {
getReturnType: function (args) {
return ValueTypes.BOOLEAN;
@@ -748,17 +849,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;
},
diff --git a/src/ol/webgl/Helper.js b/src/ol/webgl/Helper.js
index 69001218c2..25e21de657 100644
--- a/src/ol/webgl/Helper.js
+++ b/src/ol/webgl/Helper.js
@@ -434,9 +434,10 @@ class WebGLHelper extends Disposable {
* Post process passes will be initialized here, the first one being bound as a render target for
* subsequent draw calls.
* @param {import("../PluggableMap.js").FrameState} frameState current frame state
+ * @param {boolean} [opt_disableAlphaBlend] If true, no alpha blending will happen.
* @api
*/
- prepareDraw(frameState) {
+ prepareDraw(frameState, opt_disableAlphaBlend) {
const gl = this.getGL();
const canvas = this.getCanvas();
const size = frameState.size;
@@ -459,7 +460,10 @@ class WebGLHelper extends Disposable {
gl.clearColor(0.0, 0.0, 0.0, 0.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.enable(gl.BLEND);
- gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+ gl.blendFunc(
+ gl.ONE,
+ opt_disableAlphaBlend ? gl.ZERO : gl.ONE_MINUS_SRC_ALPHA
+ );
gl.useProgram(this.currentProgram_);
this.applyFrameState(frameState);
diff --git a/src/ol/webgl/TileTexture.js b/src/ol/webgl/TileTexture.js
new file mode 100644
index 0000000000..794c646464
--- /dev/null
+++ b/src/ol/webgl/TileTexture.js
@@ -0,0 +1,213 @@
+/**
+ * @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();
+
+ /**
+ * @type {import("../DataTile.js").default|import("../ImageTile.js").default}
+ */
+ this.tile;
+
+ /**
+ * @type {Array}
+ */
+ this.textures = [];
+ this.handleTileChange_ = this.handleTileChange_.bind(this);
+
+ this.size = toSize(grid.getTileSize(tile.tileCoord[0]));
+
+ this.bandCount = NaN;
+
+ this.helper_ = helper;
+
+ 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;
+ this.setTile(tile);
+ }
+
+ /**
+ * @param {import("../DataTile.js").default|import("../ImageTile.js").default} tile Tile.
+ */
+ setTile(tile) {
+ if (tile !== this.tile) {
+ if (this.tile) {
+ this.tile.removeEventListener(EventType.CHANGE, this.handleTileChange_);
+ }
+ this.tile = tile;
+ this.textures.length = 0;
+ this.loaded = tile.getState() === TileState.LOADED;
+ 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;
diff --git a/src/ol/worker/geotiff-decoder.js b/src/ol/worker/geotiff-decoder.js
new file mode 100644
index 0000000000..3dff2cce22
--- /dev/null
+++ b/src/ol/worker/geotiff-decoder.js
@@ -0,0 +1,5 @@
+/* eslint-disable sort-imports-es6-autofix/sort-imports-es6 */
+import 'regenerator-runtime/runtime.js';
+import 'geotiff/src/decoder.worker.js';
+
+export let create;
diff --git a/test/browser/karma.config.cjs b/test/browser/karma.config.cjs
index 296d201efe..9cba92f393 100644
--- a/test/browser/karma.config.cjs
+++ b/test/browser/karma.config.cjs
@@ -71,6 +71,13 @@ module.exports = function (karma) {
webpack: {
devtool: 'inline-source-map',
mode: 'development',
+ resolve: {
+ fallback: {
+ fs: false,
+ http: false,
+ https: false,
+ },
+ },
module: {
rules: [
{
diff --git a/test/browser/spec/ol/datatile.test.js b/test/browser/spec/ol/datatile.test.js
new file mode 100644
index 0000000000..6ec7c2168c
--- /dev/null
+++ b/test/browser/spec/ol/datatile.test.js
@@ -0,0 +1,51 @@
+import DataTile from '../../../../src/ol/DataTile.js';
+import TileState from '../../../../src/ol/TileState.js';
+
+describe('ol.DataTile', function () {
+ /** @type {Promise {
+ const canvas = document.createElement('canvas');
+ canvas.width = 256;
+ canvas.height = 256;
+ const context = canvas.getContext('2d');
+ context.fillStyle = 'red';
+ context.fillRect(0, 0, 256, 256);
+ resolve(context.getImageData(0, 0, 256, 256).data);
+ });
+ };
+ });
+
+ describe('constructor', function () {
+ it('sets options', function () {
+ const tileCoord = [0, 0, 0];
+ const tile = new DataTile({
+ tileCoord: tileCoord,
+ loader: loader,
+ transition: 200,
+ });
+ expect(tile.tileCoord).to.equal(tileCoord);
+ expect(tile.transition_).to.be(200);
+ expect(tile.loader_).to.equal(loader);
+ });
+ });
+
+ describe('#load()', function () {
+ it('handles loading states correctly', function (done) {
+ const tileCoord = [0, 0, 0];
+ const tile = new DataTile({
+ tileCoord: tileCoord,
+ loader: loader,
+ });
+ expect(tile.getState()).to.be(TileState.IDLE);
+ tile.load();
+ expect(tile.getState()).to.be(TileState.LOADING);
+ setTimeout(() => {
+ expect(tile.getState()).to.be(TileState.LOADED);
+ done();
+ }, 16);
+ });
+ });
+});
diff --git a/test/browser/spec/ol/layer/webgltile.test.js b/test/browser/spec/ol/layer/webgltile.test.js
new file mode 100644
index 0000000000..1fae9b1771
--- /dev/null
+++ b/test/browser/spec/ol/layer/webgltile.test.js
@@ -0,0 +1,139 @@
+import DataTileSource from '../../../../../src/ol/source/DataTile.js';
+import Map from '../../../../../src/ol/Map.js';
+import View from '../../../../../src/ol/View.js';
+import WebGLHelper from '../../../../../src/ol/webgl/Helper.js';
+import WebGLTileLayer from '../../../../../src/ol/layer/WebGLTile.js';
+import {createCanvasContext2D} from '../../../../../src/ol/dom.js';
+
+describe('ol.layer.Tile', function () {
+ /** @type {WebGLTileLayer} */
+ let layer;
+ /** @type {Map} */
+ let map, target;
+
+ beforeEach(function (done) {
+ layer = new WebGLTileLayer({
+ className: 'testlayer',
+ source: new DataTileSource({
+ loader(z, x, y) {
+ return new Promise((resolve) => {
+ resolve(new ImageData(256, 256));
+ });
+ },
+ }),
+ style: {
+ variables: {
+ r: 0,
+ g: 255,
+ b: 0,
+ },
+ color: ['color', ['var', 'r'], ['var', 'g'], ['var', 'b']],
+ },
+ });
+ target = document.createElement('div');
+ target.style.width = '100px';
+ target.style.height = '100px';
+ document.body.appendChild(target);
+ map = new Map({
+ target: target,
+ layers: [layer],
+ view: new View({
+ center: [0, 0],
+ zoom: 2,
+ }),
+ });
+ map.once('rendercomplete', () => done());
+ });
+
+ afterEach(function () {
+ map.setTarget(null);
+ document.body.removeChild(target);
+ });
+
+ it('creates fragment and vertex shaders', function () {
+ const compileShaderSpy = sinon.spy(WebGLHelper.prototype, 'compileShader');
+ layer.createRenderer();
+ compileShaderSpy.restore();
+ expect(compileShaderSpy.callCount).to.be(2);
+ expect(compileShaderSpy.getCall(0).args[0].replace(/[ \n]+/g, ' ')).to.be(
+ `
+ #ifdef GL_FRAGMENT_PRECISION_HIGH
+ precision highp float;
+ #else
+ precision mediump float;
+ #endif
+ varying vec2 v_textureCoord;
+ uniform float u_transitionAlpha;
+ uniform float u_texturePixelWidth;
+ uniform float u_texturePixelHeight;
+ uniform float u_resolution;
+ uniform float u_zoom;
+ uniform float u_var_r;
+ uniform float u_var_g;
+ uniform float u_var_b;
+ uniform sampler2D u_tileTexture0;
+ void main() {
+ vec4 color0 = texture2D(u_tileTexture0, v_textureCoord);
+ vec4 color = color0;
+ color = vec4(u_var_r / 255.0, u_var_g / 255.0, u_var_b / 255.0, 1.0);
+ if (color.a == 0.0) {
+ discard;
+ }
+ gl_FragColor = color;
+ gl_FragColor.rgb *= gl_FragColor.a;
+ gl_FragColor *= u_transitionAlpha;
+ }`.replace(/[ \n]+/g, ' ')
+ );
+
+ expect(compileShaderSpy.getCall(1).args[0].replace(/[ \n]+/g, ' ')).to.be(
+ `
+ attribute vec2 a_textureCoord;
+ uniform mat4 u_tileTransform;
+ uniform float u_depth;
+
+ varying vec2 v_textureCoord;
+ void main() {
+ v_textureCoord = a_textureCoord;
+ gl_Position = u_tileTransform * vec4(a_textureCoord, u_depth, 1.0);
+ }
+ `.replace(/[ \n]+/g, ' ')
+ );
+ });
+
+ it('updates style variables', function (done) {
+ layer.updateStyleVariables({
+ r: 255,
+ g: 0,
+ b: 255,
+ });
+ expect(layer.styleVariables_['r']).to.be(255);
+ const targetContext = createCanvasContext2D(100, 100);
+ layer.on('postrender', () => {
+ targetContext.clearRect(0, 0, 100, 100);
+ targetContext.drawImage(target.querySelector('.testlayer'), 0, 0);
+ });
+ map.once('rendercomplete', () => {
+ expect(Array.from(targetContext.getImageData(0, 0, 1, 1).data)).to.eql([
+ 255, 0, 255, 255,
+ ]);
+ done();
+ });
+ });
+
+ it('throws on incorrect style configs', function () {
+ function incorrectStyle() {
+ layer.style_ = {
+ variables: {
+ 'red': 25,
+ 'green': 200,
+ },
+ exposure: 0,
+ contrast: 0,
+ saturation: 0,
+ color: ['color', ['var', 'red'], ['var', 'green'], ['var', 'blue']],
+ };
+ layer.createRenderer();
+ }
+ expect(incorrectStyle).to.throwException(); // missing 'blue' in styleVariables
+ });
+});
diff --git a/test/browser/spec/ol/renderer/webgl/tilelayer.test.js b/test/browser/spec/ol/renderer/webgl/tilelayer.test.js
new file mode 100644
index 0000000000..d1dc885246
--- /dev/null
+++ b/test/browser/spec/ol/renderer/webgl/tilelayer.test.js
@@ -0,0 +1,95 @@
+import TileQueue from '../../../../../../src/ol/TileQueue.js';
+import TileState from '../../../../../../src/ol/TileState.js';
+import WebGLTileLayer from '../../../../../../src/ol/layer/WebGLTile.js';
+import {DataTile} from '../../../../../../src/ol/source.js';
+import {VOID} from '../../../../../../src/ol/functions.js';
+import {create} from '../../../../../../src/ol/transform.js';
+import {createCanvasContext2D} from '../../../../../../src/ol/dom.js';
+import {get} from '../../../../../../src/ol/proj.js';
+
+describe('ol.renderer.webgl.TileLayer', function () {
+ /** @type {import("../../../../../../src/ol/renderer/webgl/TileLayer.js").default} */
+ let renderer;
+ /** @type {WebGLTileLayer} */
+ let tileLayer;
+ /** @type {import('../../../../../../src/ol/PluggableMap.js').FrameState} */
+ let frameState;
+ beforeEach(function () {
+ const size = 256;
+ const context = createCanvasContext2D(size, size);
+
+ tileLayer = new WebGLTileLayer({
+ source: new DataTile({
+ loader: function (z, x, y) {
+ context.clearRect(0, 0, size, size);
+ context.fillStyle = 'rgba(100, 100, 100, 0.5)';
+ context.fillRect(0, 0, size, size);
+ const data = context.getImageData(0, 0, size, size).data;
+ return Promise.resolve(data);
+ },
+ }),
+ });
+
+ renderer = tileLayer.createRenderer();
+
+ const proj = get('EPSG:3857');
+ frameState = {
+ layerStatesArray: [tileLayer.getLayerState()],
+ layerIndex: 0,
+ extent: proj.getExtent(),
+ pixelRatio: 1,
+ pixelToCoordinateTransform: create(),
+ postRenderFunctions: [],
+ time: Date.now(),
+ viewHints: [],
+ viewState: {
+ center: [0, 0],
+ resolution: 156543.03392804097,
+ projection: proj,
+ },
+ size: [256, 256],
+ usedTiles: {},
+ wantedTiles: {},
+ tileQueue: new TileQueue(VOID, VOID),
+ };
+ });
+
+ it('#prepareFrame()', function () {
+ const source = tileLayer.getSource();
+ tileLayer.setSource(null);
+ expect(renderer.prepareFrame(frameState)).to.be(false);
+ tileLayer.setSource(source);
+ expect(renderer.prepareFrame(frameState)).to.be(true);
+ const tileGrid = source.getTileGrid();
+ tileLayer.setExtent(tileGrid.getTileCoordExtent([2, 0, 0]));
+ frameState.resolution = tileGrid.getResolution(2);
+ frameState.extent = tileGrid.getTileCoordExtent([2, 2, 2]);
+ frameState.layerStatesArray = [tileLayer.getLayerState()];
+ expect(renderer.prepareFrame(frameState)).to.be(false);
+ });
+
+ it('#renderFrame()', function () {
+ const rendered = renderer.renderFrame(frameState);
+ expect(rendered).to.be.a(HTMLCanvasElement);
+ expect(frameState.tileQueue.getCount()).to.be(1);
+ expect(Object.keys(frameState.wantedTiles).length).to.be(1);
+ expect(frameState.postRenderFunctions.length).to.be(0); // no tile expired
+ expect(renderer.tileTextureCache_.count_).to.be(1);
+ });
+
+ it('#isDrawableTile()', function (done) {
+ const tile = tileLayer.getSource().getTile(0, 0, 0);
+ expect(renderer.isDrawableTile(tile)).to.be(false);
+ tileLayer.getSource().on('tileloadend', () => {
+ expect(renderer.isDrawableTile(tile)).to.be(true);
+ done();
+ });
+ tile.load();
+ const errorTile = tileLayer.getSource().getTile(1, 0, 1);
+ errorTile.setState(TileState.ERROR);
+ tileLayer.setUseInterimTilesOnError(false);
+ expect(renderer.isDrawableTile(errorTile)).to.be(true);
+ tileLayer.setUseInterimTilesOnError(true);
+ expect(renderer.isDrawableTile(errorTile)).to.be(false);
+ });
+});
diff --git a/test/browser/spec/ol/source/datatile.test.js b/test/browser/spec/ol/source/datatile.test.js
new file mode 100644
index 0000000000..0b3aafd3ef
--- /dev/null
+++ b/test/browser/spec/ol/source/datatile.test.js
@@ -0,0 +1,42 @@
+import DataTile from '../../../../../src/ol/DataTile.js';
+import DataTileSource from '../../../../../src/ol/source/DataTile.js';
+import TileState from '../../../../../src/ol/TileState.js';
+
+describe('ol.source.DataTile', function () {
+ /** @type {DataTileSource} */
+ let source;
+ beforeEach(function () {
+ const loader = function (z, x, y) {
+ return new Promise((resolve) => {
+ const canvas = document.createElement('canvas');
+ canvas.width = 256;
+ canvas.height = 256;
+ const context = canvas.getContext('2d');
+ // encode tile coordinate in rgb
+ context.fillStyle = `rgb(${z}, ${x % 255}, ${y % 255})`;
+ context.fillRect(0, 0, 256, 256);
+ resolve(context.getImageData(0, 0, 256, 256).data);
+ });
+ };
+ source = new DataTileSource({
+ loader: loader,
+ });
+ });
+
+ describe('#getTile()', function () {
+ it('gets tiles and fires a tileloadend event', function (done) {
+ const tile = source.getTile(3, 2, 1);
+ expect(tile).to.be.a(DataTile);
+ expect(tile.state).to.be(TileState.IDLE);
+
+ source.on('tileloadend', () => {
+ expect(tile.state).to.be(TileState.LOADED);
+ // decode tile coordinate from rgb
+ expect(Array.from(tile.getData().slice(0, 3))).to.eql([3, 2, 1]);
+ done();
+ });
+
+ tile.load();
+ });
+ });
+});
diff --git a/test/browser/spec/ol/source/geotiff.test.js b/test/browser/spec/ol/source/geotiff.test.js
new file mode 100644
index 0000000000..de76b67e98
--- /dev/null
+++ b/test/browser/spec/ol/source/geotiff.test.js
@@ -0,0 +1,47 @@
+import GeoTIFFSource from '../../../../../src/ol/source/GeoTIFF.js';
+import State from '../../../../../src/ol/source/State.js';
+import TileState from '../../../../../src/ol/TileState.js';
+
+describe('ol.source.GeoTIFF', function () {
+ /** @type {GeoTIFFSource} */
+ let source;
+ beforeEach(function () {
+ source = new GeoTIFFSource({
+ sources: [
+ {
+ url: 'spec/ol/source/images/0-0-0.tif',
+ },
+ ],
+ });
+ });
+
+ it('manages load states', function (done) {
+ expect(source.getState()).to.be(State.LOADING);
+ source.on('change', () => {
+ expect(source.getState()).to.be(State.READY);
+ done();
+ });
+ });
+
+ it('configures itself from source metadata', function (done) {
+ source.on('change', () => {
+ expect(source.addAlpha_).to.be(true);
+ expect(source.bandCount).to.be(4);
+ expect(source.nodataValues_).to.eql([[0]]);
+ expect(source.getTileGrid().getResolutions().length).to.be(1);
+ expect(source.projection.getCode()).to.be('EPSG:4326');
+ done();
+ });
+ });
+
+ it('loads tiles', function (done) {
+ source.on('change', () => {
+ const tile = source.getTile(0, 0, 0);
+ source.on('tileloadend', () => {
+ expect(tile.getState()).to.be(TileState.LOADED);
+ done();
+ });
+ tile.load();
+ });
+ });
+});
diff --git a/test/browser/spec/ol/source/images/0-0-0.tif b/test/browser/spec/ol/source/images/0-0-0.tif
new file mode 100644
index 0000000000..3507f24029
Binary files /dev/null and b/test/browser/spec/ol/source/images/0-0-0.tif differ
diff --git a/test/browser/spec/ol/style/expressions.test.js b/test/browser/spec/ol/style/expressions.test.js
index caabe63156..d2e6980b0c 100644
--- a/test/browser/spec/ol/style/expressions.test.js
+++ b/test/browser/spec/ol/style/expressions.test.js
@@ -235,6 +235,18 @@ describe('ol.style.expressions', function () {
expect(expressionToGlsl(context, ['^', ['%', ['time'], 10], 2])).to.eql(
'pow(mod(u_time, 10.0), 2.0)'
);
+ expect(
+ expressionToGlsl(context, [
+ 'abs',
+ ['-', ['get', 'attr3'], ['get', 'attr2']],
+ ])
+ ).to.eql('abs((a_attr3 - a_attr2))');
+ expect(expressionToGlsl(context, ['sin', 1])).to.eql('sin(1.0)');
+ expect(expressionToGlsl(context, ['cos', 1])).to.eql('cos(1.0)');
+ expect(expressionToGlsl(context, ['atan', 1])).to.eql('atan(1.0)');
+ expect(expressionToGlsl(context, ['atan', 1, 0.5])).to.eql(
+ 'atan(1.0, 0.5)'
+ );
expect(expressionToGlsl(context, ['>', 10, ['get', 'attr4']])).to.eql(
'(10.0 > a_attr4)'
);
@@ -277,6 +289,10 @@ describe('ol.style.expressions', function () {
expect(
expressionToGlsl(context, ['color', ['get', 'attr4'], 1, 2, 0.5])
).to.eql('vec4(a_attr4 / 255.0, 1.0 / 255.0, 2.0 / 255.0, 0.5)');
+ expect(expressionToGlsl(context, ['band', 1])).to.eql('color0[0]');
+ expect(expressionToGlsl(context, ['band', 1, -1, 2])).to.eql(
+ 'texture2D(u_tileTexture0, v_textureCoord + vec2(-1.0 / u_texturePixelWidth, 2.0 / u_texturePixelHeight))[0]'
+ );
});
it('throws if the value does not match the type', function () {
diff --git a/test/browser/spec/ol/webgl/tiletexture.test.js b/test/browser/spec/ol/webgl/tiletexture.test.js
new file mode 100644
index 0000000000..ef6bc29733
--- /dev/null
+++ b/test/browser/spec/ol/webgl/tiletexture.test.js
@@ -0,0 +1,86 @@
+import DataTile from '../../../../../src/ol/DataTile.js';
+import DataTileSource from '../../../../../src/ol/source/DataTile.js';
+import ImageTile from '../../../../../src/ol/ImageTile.js';
+import TileState from '../../../../../src/ol/TileState.js';
+import TileTexture from '../../../../../src/ol/webgl/TileTexture.js';
+import WebGLArrayBuffer from '../../../../../src/ol/webgl/Buffer.js';
+import WebGLTileLayer from '../../../../../src/ol/layer/WebGLTile.js';
+import {createCanvasContext2D} from '../../../../../src/ol/dom.js';
+
+describe('ol.webgl.TileTexture', function () {
+ /** @type {TileTexture} */
+ let tileTexture;
+
+ beforeEach(function () {
+ const layer = new WebGLTileLayer({
+ source: new DataTileSource({
+ loader(z, x, y) {
+ return new Promise((resolve) => {
+ const context = createCanvasContext2D(256, 256);
+ context.fillStyle = `rgb(${z}, ${x % 255}, ${y % 255})`;
+ context.fillRect(0, 0, 256, 256);
+ resolve(context.getImageData(0, 0, 256, 256).data);
+ });
+ },
+ }),
+ });
+ const renderer =
+ /** @type {import("../../../../../src/ol/renderer/webgl/TileLayer.js").default} */ (
+ layer.createRenderer()
+ );
+ tileTexture = new TileTexture(
+ layer.getSource().getTile(3, 2, 1),
+ layer.getSource().getTileGrid(),
+ renderer.helper
+ );
+ });
+
+ it('constructor', function () {
+ expect(tileTexture.tile.tileCoord).to.eql([3, 2, 1]);
+ expect(tileTexture.coords).to.be.a(WebGLArrayBuffer);
+ });
+
+ it('handles data tiles', function (done) {
+ const dataTile = tileTexture.tile;
+ expect(tileTexture.loaded).to.be(false);
+ expect(dataTile.getState()).to.be(TileState.IDLE);
+ tileTexture.addEventListener('change', () => {
+ if (dataTile.getState() === TileState.LOADED) {
+ expect(tileTexture.loaded).to.be(true);
+ done();
+ }
+ });
+ dataTile.load();
+ });
+
+ it('handles image tiles', function () {
+ const imageTile = new ImageTile([0, 0, 0], TileState.LOADED);
+ tileTexture.setTile(imageTile);
+ expect(tileTexture.loaded).to.be(true);
+ });
+
+ it('registers and unregisters change listener', function () {
+ const tile = tileTexture.tile;
+ expect(tile.getListeners('change').length).to.be(2);
+ tileTexture.dispose();
+ expect(tile.getListeners('change').length).to.be(1);
+ });
+
+ it('updates metadata and unregisters change listener when setting a different tile', function (done) {
+ const tile = tileTexture.tile;
+ expect(tile.getListeners('change').length).to.be(2);
+ const differentTile = new DataTile({
+ tileCoord: [1, 0, 1],
+ loader(z, x, y) {
+ return Promise.resolve(new Uint8Array(256 * 256 * 3));
+ },
+ });
+ tileTexture.setTile(differentTile);
+ expect(tile.getListeners('change').length).to.be(1);
+ tileTexture.addEventListener('change', () => {
+ expect(tileTexture.bandCount).to.be(3);
+ done();
+ });
+ differentTile.load();
+ });
+});
diff --git a/test/rendering/cases/layer-tile-webgl/expected.png b/test/rendering/cases/layer-tile-webgl/expected.png
new file mode 100644
index 0000000000..5d7011ff7d
Binary files /dev/null and b/test/rendering/cases/layer-tile-webgl/expected.png differ
diff --git a/test/rendering/cases/layer-tile-webgl/main.js b/test/rendering/cases/layer-tile-webgl/main.js
new file mode 100644
index 0000000000..6eb87d2786
--- /dev/null
+++ b/test/rendering/cases/layer-tile-webgl/main.js
@@ -0,0 +1,26 @@
+import Map from '../../../../src/ol/Map.js';
+import TileLayer from '../../../../src/ol/layer/WebGLTile.js';
+import View from '../../../../src/ol/View.js';
+import XYZ from '../../../../src/ol/source/XYZ.js';
+import {fromLonLat} from '../../../../src/ol/proj.js';
+
+const center = fromLonLat([8.6, 50.1]);
+
+new Map({
+ layers: [
+ new TileLayer({
+ source: new XYZ({
+ url: '/data/tiles/satellite/{z}/{x}/{y}.jpg',
+ transition: 0,
+ crossOrigin: 'anonymous',
+ }),
+ }),
+ ],
+ target: 'map',
+ view: new View({
+ center: center,
+ zoom: 3,
+ }),
+});
+
+render();