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..452f84fccd --- /dev/null +++ b/examples/cog-math.js @@ -0,0 +1,105 @@ +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', + nodata: 0, + 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', + nodata: 0, + 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.html b/examples/cog.html new file mode 100644 index 0000000000..2328e224ec --- /dev/null +++ b/examples/cog.html @@ -0,0 +1,11 @@ +--- +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. The `nodata` property is + used to avoid rendering pixels where all three bands are 0. +tags: "cog" +--- +
diff --git a/examples/cog.js b/examples/cog.js new file mode 100644 index 0000000000..411214f6a7 --- /dev/null +++ b/examples/cog.js @@ -0,0 +1,43 @@ +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', + nodata: 0, + }, + ], + }), + 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..aa34aa406c --- /dev/null +++ b/examples/data-tiles.js @@ -0,0 +1,43 @@ +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); + }, + }), + }), + ], + 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/ +--- +
+ +
+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-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/ +--- +
+
+ + + +
\ 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/package-lock.json b/package-lock.json index d16cab342f..3a67195d2f 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.0-beta.16", "ol-mapbox-style": "^6.4.1", "pbf": "3.2.1", "rbush": "^3.0.1" @@ -1837,6 +1838,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 +2978,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 +3345,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 +3590,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 +4672,27 @@ "node": ">=8" } }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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 +5364,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 +5976,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 +6241,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 +7185,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 +7358,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 +7764,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 +8011,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/observable-fns": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/observable-fns/-/observable-fns-0.5.1.tgz", + "integrity": "sha512-wf7g4Jpo1Wt2KIqZKLGeiuLOEMqpaOZ5gJn7DmSdqXgTdxRwSdBhWegQQpPteQ2gZvzCKqNNpwb853wcpA0j7A==" + }, "node_modules/obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -8172,6 +8241,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 +8258,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 +9005,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", @@ -9965,7 +10043,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 +10051,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 +10390,53 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "node_modules/threads": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/threads/-/threads-1.6.4.tgz", + "integrity": "sha512-A+9MQFAUha9W8MjIPmrvETy98qVmZFr5Unox9D95y7kvz3fBpGiFS7JOZs07B2KvTHoRNI5MrGudRVeCmv4Alw==", + "dependencies": { + "callsites": "^3.1.0", + "debug": "^4.2.0", + "is-observable": "^2.1.0", + "observable-fns": "^0.5.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 +10534,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 +10749,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 +11492,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 +12797,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 +13730,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 +14026,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 +14212,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" } @@ -14675,6 +14804,12 @@ "requires": { "has-flag": "^4.0.0" } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true } } }, @@ -14922,6 +15057,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 +15589,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 +16050,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 +16225,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 +16961,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 +17102,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 +17411,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 +17602,11 @@ "es-abstract": "^1.18.2" } }, + "observable-fns": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/observable-fns/-/observable-fns-0.5.1.tgz", + "integrity": "sha512-wf7g4Jpo1Wt2KIqZKLGeiuLOEMqpaOZ5gJn7DmSdqXgTdxRwSdBhWegQQpPteQ2gZvzCKqNNpwb853wcpA0j7A==" + }, "obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -17599,6 +17765,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 +17779,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 +18354,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", @@ -19015,7 +19190,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 +19197,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 +19447,48 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "threads": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/threads/-/threads-1.6.4.tgz", + "integrity": "sha512-A+9MQFAUha9W8MjIPmrvETy98qVmZFr5Unox9D95y7kvz3fBpGiFS7JOZs07B2KvTHoRNI5MrGudRVeCmv4Alw==", + "requires": { + "callsites": "^3.1.0", + "debug": "^4.2.0", + "is-observable": "^2.1.0", + "observable-fns": "^0.5.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 +19567,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 +19729,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 +20254,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..f51d355612 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "url": "https://opencollective.com/openlayers" }, "dependencies": { + "geotiff": "^1.0.0-beta.16", "ol-mapbox-style": "^6.4.1", "pbf": "3.2.1", "rbush": "^3.0.1" 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/WebGLTile.js b/src/ol/layer/WebGLTile.js new file mode 100644 index 0000000000..820f42dbbd --- /dev/null +++ b/src/ol/layer/WebGLTile.js @@ -0,0 +1,289 @@ +/** + * @module ol/layer/WebGLTile + */ +import BaseTileLayer from './BaseTile.js'; +import WebGLTileLayerRenderer, { + Attributes, + Uniforms, +} from '../renderer/webgl/TileLayer.js'; +import { + ValueTypes, + expressionToGlsl, + getStringNumberEquivalent, + uniformNameForVariable, +} from '../style/expressions.js'; +import {assign} from '../obj.js'; + +/** + * @typedef {Object} Style + * @property {Object} [variables] Style variables. Each variable must hold a number. + * @property {import("../style/expressions.js").ExpressionValue} [color] An expression applied to color values. + * @property {import("../style/expressions.js").ExpressionValue} [brightness=0] Value used to decrease or increase + * the layer brightness. Values range from -1 to 1. + * @property {import("../style/expressions.js").ExpressionValue} [contrast=0] Value used to decrease or increase + * the layer contrast. Values range from -1 to 1. + * @property {import("../style/expressions.js").ExpressionValue} [exposure=0] Value used to decrease or increase + * the layer exposure. Values range from -1 to 1. + * @property {import("../style/expressions.js").ExpressionValue} [saturation=0] Value used to decrease or increase + * the layer saturation. Values range from -1 to 1. + * @property {import("../style/expressions.js").ExpressionValue} [gamma=1] Apply a gamma correction to the layer. + * Values range from 0 to infinity. + */ + +/** + * @typedef {Object} Options + * @property {Style} [style] Style to apply to the layer. + * @property {string} [className='ol-layer'] A CSS class name to set to the layer element. + * @property {number} [opacity=1] Opacity (0, 1). + * @property {boolean} [visible=true] Visibility. + * @property {import("../extent.js").Extent} [extent] The bounding extent for layer rendering. The layer will not be + * rendered outside of this extent. + * @property {number} [zIndex] The z-index for layer rendering. At rendering time, the layers + * will be ordered, first by Z-index and then by position. When `undefined`, a `zIndex` of 0 is assumed + * for layers that are added to the map's `layers` collection, or `Infinity` when the layer's `setMap()` + * method was used. + * @property {number} [minResolution] The minimum resolution (inclusive) at which this layer will be + * visible. + * @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will + * be visible. + * @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be + * visible. + * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will + * be visible. + * @property {number} [preload=0] Preload. Load low-resolution tiles up to `preload` levels. `0` + * means no preloading. + * @property {import("../source/Tile.js").default} [source] Source for this layer. + * @property {import("../PluggableMap.js").default} [map] Sets the layer as overlay on a map. The map will not manage + * this layer in its layers collection, and the layer will be rendered on top. This is useful for + * temporary layers. The standard way to add a layer to a map and have it managed by the map is to + * use {@link module:ol/Map#addLayer}. + * @property {boolean} [useInterimTilesOnError=true] Use interim tiles on error. + */ + +/** + * @typedef {Object} ParsedStyle + * @property {string} vertexShader The vertex shader. + * @property {string} fragmentShader The fragment shader. + * @property {Object} 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}; + + ${uniformDeclarations.join('\n')} + + void main() { + ${colorAssignments.join('\n')} + + vec4 color = color0; + + ${pipeline.join('\n')} + + if (color.a == 0.0) { + discard; + } + + gl_FragColor = color * ${Uniforms.TRANSITION_ALPHA}; + }`; + + return { + vertexShader: vertexShader, + fragmentShader: fragmentShader, + uniforms: uniforms, + }; +} + +/** + * @classdesc + * For layer sources that provide pre-rendered, tiled images in grids that are + * organized by zoom levels for specific resolutions. + * Note that any property set in the options is set as a {@link module:ol/Object~BaseObject} + * property on the layer object; for example, setting `title: 'My Title'` in the + * options means that `title` is observable, and has get/set accessors. + * + * @api + */ +class WebGLTileLayer extends BaseTileLayer { + /** + * @param {Options} opt_options Tile layer options. + */ + constructor(opt_options) { + const options = opt_options ? assign({}, opt_options) : {}; + + const style = options.style || {}; + delete options.style; + super(options); + + const parsedStyle = parseStyle(style || {}, 1); // TODO: get texture count from source + + this.vertexShader_ = parsedStyle.vertexShader; + this.fragmentShader_ = parsedStyle.fragmentShader; + this.uniforms_ = parsedStyle.uniforms; + this.styleVariables_ = style.variables || {}; + } + + /** + * Create a renderer for this layer. + * @return {import("../renderer/Layer.js").default} A layer renderer. + * @protected + */ + createRenderer() { + return new WebGLTileLayerRenderer(this, { + vertexShader: this.vertexShader_, + fragmentShader: this.fragmentShader_, + uniforms: this.uniforms_, + }); + } + + /** + * Update any variables used by the layer style and trigger a re-render. + * @param {Object} variables Variables to update. + */ + 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..951bef91d2 --- /dev/null +++ b/src/ol/renderer/webgl/TileLayer.js @@ -0,0 +1,487 @@ +/** + * @module ol/renderer/webgl/TileLayer + */ +import LRUCache from '../../structs/LRUCache.js'; +import State from '../../source/State.js'; +import TileRange from '../../TileRange.js'; +import TileState from '../../TileState.js'; +import TileTexture from '../../webgl/TileTexture.js'; +import WebGLArrayBuffer from '../../webgl/Buffer.js'; +import WebGLLayerRenderer from './Layer.js'; +import {AttributeType} from '../../webgl/Helper.js'; +import {ELEMENT_ARRAY_BUFFER, STATIC_DRAW} from '../../webgl.js'; +import { + compose as composeTransform, + create as createTransform, +} from '../../transform.js'; +import { + create as createMat4, + fromTransform as mat4FromTransform, +} from '../../vec/mat4.js'; +import { + createOrUpdate as createTileCoord, + getKeyZXY, + getKey as getTileCoordKey, +} from '../../tilecoord.js'; +import {fromUserExtent} from '../../proj.js'; +import {getIntersection} from '../../extent.js'; +import {getUid} from '../../util.js'; +import {numberSafeCompareFunction} from '../../array.js'; +import {toSize} from '../../size.js'; + +export const Uniforms = { + TILE_TEXTURE_PREFIX: 'u_tileTexture', + TILE_TRANSFORM: 'u_tileTransform', + TRANSITION_ALPHA: 'u_transitionAlpha', + DEPTH: 'u_depth', +}; + +export const Attributes = { + TEXTURE_COORD: 'a_textureCoord', +}; + +/** + * @type {Array} + */ +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); +} + +/** + * @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; + } + + /** + * Determine whether render should be called. + * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. + * @return {boolean} Layer is ready to be rendered. + */ + prepareFrame(frameState) { + const source = this.getLayer().getSource(); + if (!source) { + return false; + } + return source.getState() === State.READY; + } + + /** + * Render the layer. + * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. + * @return {HTMLElement} The rendered element. + */ + renderFrame(frameState) { + this.preRender(frameState); + + const layerState = frameState.layerStatesArray[frameState.layerIndex]; + const viewState = frameState.viewState; + + let extent = frameState.extent; + if (layerState.extent) { + extent = getIntersection( + extent, + fromUserExtent(layerState.extent, viewState.projection) + ); + } + + const tileLayer = this.getLayer(); + const tileSource = tileLayer.getSource(); + const tileGrid = tileSource.getTileGridForProjection(viewState.projection); + const z = tileGrid.getZForResolution( + viewState.resolution, + tileSource.zDirection + ); + + /** + * @type {Object>} + */ + const tileTexturesByZ = {}; + + const tileTextureCache = this.tileTextureCache_; + const tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z); + + const tileSourceKey = getUid(tileSource); + if (!(tileSourceKey in frameState.wantedTiles)) { + frameState.wantedTiles[tileSourceKey] = {}; + } + + const wantedTiles = frameState.wantedTiles[tileSourceKey]; + + const tileResolution = tileGrid.getResolution(z); + + for (let x = tileRange.minX; x <= tileRange.maxX; ++x) { + for (let y = tileRange.minY; y <= tileRange.maxY; ++y) { + const tileCoord = createTileCoord(z, x, y, this.tempTileCoord_); + const tileCoordKey = getTileCoordKey(tileCoord); + + let tileTexture; + if (tileTextureCache.containsKey(tileCoordKey)) { + tileTexture = tileTextureCache.get(tileCoordKey); + } else { + const tile = tileSource.getTile( + z, + x, + y, + frameState.pixelRatio, + viewState.projection + ); + tileTexture = new TileTexture(tile, tileGrid, this.helper); + tileTextureCache.set(tileCoordKey, tileTexture); + } + + addTileTextureToLookup(tileTexturesByZ, tileTexture, z); + + const tileQueueKey = tileTexture.tile.getKey(); + wantedTiles[tileQueueKey] = true; + + if (tileTexture.tile.getState() === TileState.IDLE) { + if (!frameState.tileQueue.isKeyQueued(tileQueueKey)) { + frameState.tileQueue.enqueue([ + tileTexture.tile, + tileSourceKey, + tileGrid.getTileCoordCenter(tileCoord), + tileResolution, + ]); + } + } + } + } + + /** + * A lookup of alpha values for tiles at the target rendering resolution + * for tiles that are in transition. If a tile coord key is absent from + * this lookup, the tile should be rendered at alpha 1. + * @type {Object} + */ + const alphaLookup = {}; + + const uid = getUid(this); + const time = frameState.time; + + // look for cached tiles to use if a target tile is not ready + const tileTextures = tileTexturesByZ[z]; + for (let i = 0, ii = tileTextures.length; i < ii; ++i) { + const tileTexture = tileTextures[i]; + const tile = tileTexture.tile; + const tileCoord = tile.tileCoord; + + if (tileTexture.loaded) { + const alpha = tile.getAlpha(uid, time); + if (alpha === 1) { + // no need to look for alt tiles + tile.endTransition(uid); + continue; + } + const tileCoordKey = getTileCoordKey(tileCoord); + alphaLookup[tileCoordKey] = alpha; + } + + // first look for child tiles (at z + 1) + const coveredByChildren = this.findAltTiles_( + tileGrid, + tileCoord, + z + 1, + tileTexturesByZ + ); + + if (coveredByChildren) { + continue; + } + + // next look for parent tiles + for (let parentZ = z - 1; parentZ >= tileGrid.minZoom; --parentZ) { + const coveredByParent = this.findAltTiles_( + tileGrid, + tileCoord, + parentZ, + tileTexturesByZ + ); + + if (coveredByParent) { + break; + } + } + } + + this.helper.useProgram(this.program_); + this.helper.prepareDraw(frameState); + + const zs = Object.keys(tileTexturesByZ) + .map(Number) + .sort(numberSafeCompareFunction); + + const gl = this.helper.getGL(); + + const centerX = viewState.center[0]; + const centerY = viewState.center[1]; + + for (let j = 0, jj = zs.length; j < jj; ++j) { + const tileZ = zs[j]; + const tileResolution = tileGrid.getResolution(tileZ); + const tileSize = toSize(tileGrid.getTileSize(tileZ), this.tempSize_); + const tileOrigin = tileGrid.getOrigin(tileZ); + + const centerI = + (centerX - tileOrigin[0]) / (tileSize[0] * tileResolution); + const centerJ = + (tileOrigin[1] - centerY) / (tileSize[1] * tileResolution); + + const tileScale = viewState.resolution / tileResolution; + + const depth = depthForZ(tileZ); + const tileTextures = tileTexturesByZ[tileZ]; + for (let i = 0, ii = tileTextures.length; i < ii; ++i) { + const tileTexture = tileTextures[i]; + if (!tileTexture.loaded) { + continue; + } + const tile = tileTexture.tile; + const tileCoord = tile.tileCoord; + const tileCoordKey = getTileCoordKey(tileCoord); + + const tileCenterI = tileCoord[1]; + const tileCenterJ = tileCoord[2]; + + composeTransform( + this.tileTransform_, + 0, + 0, + 2 / ((frameState.size[0] * tileScale) / tileSize[0]), + -2 / ((frameState.size[1] * tileScale) / tileSize[1]), + viewState.rotation, + -(centerI - tileCenterI), + -(centerJ - tileCenterJ) + ); + + this.helper.setUniformMatrixValue( + Uniforms.TILE_TRANSFORM, + mat4FromTransform(this.tempMat4_, this.tileTransform_) + ); + + this.helper.bindBuffer(tileTexture.coords); + this.helper.bindBuffer(this.indices_); + this.helper.enableAttributes(attributeDescriptions); + + for ( + let textureIndex = 0; + textureIndex < tileTexture.textures.length; + ++textureIndex + ) { + const textureProperty = 'TEXTURE' + textureIndex; + const uniformName = Uniforms.TILE_TEXTURE_PREFIX + textureIndex; + gl.activeTexture(gl[textureProperty]); + gl.bindTexture(gl.TEXTURE_2D, tileTexture.textures[textureIndex]); + gl.uniform1i(this.helper.getUniformLocation(uniformName), 0); + } + + const alpha = + tileCoordKey in alphaLookup ? alphaLookup[tileCoordKey] : 1; + + if (alpha < 1) { + frameState.animate = true; + } + + this.helper.setUniformFloatValue(Uniforms.TRANSITION_ALPHA, alpha); + this.helper.setUniformFloatValue(Uniforms.DEPTH, depth); + + this.helper.drawElements(0, this.indices_.getSize()); + } + } + + this.helper.finalizeDraw(frameState); + + const canvas = this.helper.getCanvas(); + + const opacity = layerState.opacity; + if (this.renderedOpacity_ !== opacity) { + canvas.style.opacity = String(opacity); + this.renderedOpacity_ = opacity; + } + + while (tileTextureCache.canExpireCache()) { + const tileTexture = tileTextureCache.pop(); + tileTexture.dispose(); + } + + // TODO: let the renderers manage their own cache instead of managing the source cache + if (tileSource.canExpireCache()) { + /** + * @param {import("../../PluggableMap.js").default} map Map. + * @param {import("../../PluggableMap.js").FrameState} frameState Frame state. + */ + const postRenderFunction = function (map, frameState) { + const tileSourceKey = getUid(tileSource); + if (tileSourceKey in frameState.usedTiles) { + tileSource.expireCache( + frameState.viewState.projection, + frameState.usedTiles[tileSourceKey] + ); + } + }; + + frameState.postRenderFunctions.push(postRenderFunction); + } + + this.postRender(frameState); + return canvas; + } + + /** + * Look for tiles covering the provided tile coordinate at an alternate + * zoom level. Loaded tiles will be added to the provided tile texture lookup. + * @param {import("../../tilegrid/TileGrid.js").default} tileGrid The tile grid. + * @param {import("../../tilecoord.js").TileCoord} tileCoord The target tile coordinate. + * @param {number} altZ The alternate zoom level. + * @param {Object>} 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/DataTile.js b/src/ol/source/DataTile.js new file mode 100644 index 0000000000..e96524e737 --- /dev/null +++ b/src/ol/source/DataTile.js @@ -0,0 +1,142 @@ +/** + * @module ol/source/DataTile + */ +import DataTile from '../DataTile.js'; +import EventType from '../events/EventType.js'; +import TileEventType from './TileEventType.js'; +import TileSource, {TileSourceEvent} from './Tile.js'; +import TileState from '../TileState.js'; +import {createXYZ, extentFromProjection} from '../tilegrid.js'; +import {getKeyZXY} from '../tilecoord.js'; +import {getUid} from '../util.js'; + +/** + * @typedef {Object} Options + * @property {function(number, number, number) : Promise} [loader] Data loader. Called with z, x, and y tile coordinates. + * @property {number} [maxZoom=42] Optional max zoom level. Not used if `tileGrid` is provided. + * @property {number} [minZoom=0] Optional min zoom level. Not used if `tileGrid` is provided. + * @property {number|import("../size.js").Size} [tileSize=[256, 256]] The pixel width and height of the tiles. + * @property {number} [maxResolution] Optional tile grid resolution at level zero. Not used if `tileGrid` is provided. + * @property {import("../proj.js").ProjectionLike} [projection='EPSG:3857'] Tile projection. + * @property {import("../tilegrid/TileGrid.js").default} [tileGrid] Tile grid. + * @property {boolean} [opaque=false] Whether the layer is opaque. + * @property {import("./State.js").default} [state] The source state. + * @property {number} [cacheSize] Number of tiles to retain in the cache. + * @property {number} [tilePixelRatio] Tile pixel ratio. + * @property {boolean} [wrapX=true] Render tiles beyond the antimeridian. + * @property {number} [transition] Transition time when fading in new tiles (in miliseconds). + */ + +/** + * @classdesc + * Base class for sources providing tiles divided into a tile grid over http. + * + * @fires import("./Tile.js").TileSourceEvent + */ +class DataTileSource extends TileSource { + /** + * @param {Options} options Image tile options. + */ + constructor(options) { + const projection = + options.projection === undefined ? 'EPSG:3857' : options.projection; + + let tileGrid = options.tileGrid; + if (tileGrid === undefined && projection) { + tileGrid = createXYZ({ + extent: extentFromProjection(projection), + maxResolution: options.maxResolution, + maxZoom: options.maxZoom, + minZoom: options.minZoom, + tileSize: options.tileSize, + }); + } + + super({ + cacheSize: options.cacheSize, + projection: projection, + tileGrid: tileGrid, + opaque: options.opaque, + state: options.state, + tilePixelRatio: options.tilePixelRatio, + wrapX: options.wrapX, + transition: options.transition, + }); + + /** + * @private + * @type {!Object} + */ + this.tileLoadingKeys_ = {}; + + /** + * @private + */ + this.loader_ = options.loader; + + this.handleTileChange_ = this.handleTileChange_.bind(this); + } + + /** + * @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 {!import("../Tile.js").default} Tile. + */ + getTile(z, x, y, pixelRatio, projection) { + const tileCoordKey = getKeyZXY(z, x, y); + if (this.tileCache.containsKey(tileCoordKey)) { + return this.tileCache.get(tileCoordKey); + } + + const sourceLoader = this.loader_; + function loader() { + return sourceLoader(z, x, y); + } + + const tile = new DataTile({tileCoord: [z, x, y], loader: loader}); + tile.addEventListener(EventType.CHANGE, this.handleTileChange_); + + this.tileCache.set(tileCoordKey, tile); + return tile; + } + + /** + * Handle tile change events. + * @param {import("../events/Event.js").default} event Event. + */ + handleTileChange_(event) { + const tile = /** @type {import("../Tile.js").default} */ (event.target); + const uid = getUid(tile); + const tileState = tile.getState(); + let type; + if (tileState == TileState.LOADING) { + this.tileLoadingKeys_[uid] = true; + type = TileEventType.TILELOADSTART; + } else if (uid in this.tileLoadingKeys_) { + delete this.tileLoadingKeys_[uid]; + type = + tileState == TileState.ERROR + ? TileEventType.TILELOADERROR + : tileState == TileState.LOADED + ? TileEventType.TILELOADEND + : undefined; + } + if (type) { + this.dispatchEvent(new TileSourceEvent(type, tile)); + } + } +} + +export default DataTileSource; diff --git a/src/ol/source/GeoTIFF.js b/src/ol/source/GeoTIFF.js new file mode 100644 index 0000000000..9e39ac372e --- /dev/null +++ b/src/ol/source/GeoTIFF.js @@ -0,0 +1,391 @@ +/** + * @module ol/source/GeoTIFF + */ +import DataTile from './DataTile.js'; +import State from './State.js'; +import TileGrid from '../tilegrid/TileGrid.js'; +import {get as getProjection} from '../proj.js'; +import {fromUrl as tiffFromUrl, fromUrls as tiffFromUrls} from 'geotiff'; +import {toSize} from '../size.js'; + +/** + * @typedef SourceInfo + * @property {string} url URL for the source. + * @property {Array} [overviews] List of any overview URLs. + * @property {number} [min=0] The minimum source data value. Rendered values are scaled from 0 to 1 based on + * the configured min and max. + * @property {number} [max] The maximum source data value. Rendered values are scaled from 0 to 1 based on + * the configured min and max. + * @property {number} [nodata] Values to discard. + */ + +/** + * @param {import("geotiff/src/geotiff.js").GeoTIFF|import("geotiff/src/geotiff.js").MultiGeoTIFF} tiff A GeoTIFF. + * @return {Promise>} 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 {string} message The error message. + */ +function assertEqual(expected, got, message) { + if (Array.isArray(expected)) { + const length = expected.length; + if (!Array.isArray(got) || length != got.length) { + throw new Error(message); + } + for (let i = 0; i < length; ++i) { + assertEqual(expected[i], got[i], message); + } + return; + } + + if (expected !== got) { + throw new Error(message); + } +} + +/** + * @param {Array} array The data array. + * @return {number} The minimum value. + */ +function getMinForDataType(array) { + if (array instanceof Int8Array) { + return -128; + } + if (array instanceof Int16Array) { + return -32768; + } + if (array instanceof Int32Array) { + return -2147483648; + } + if (array instanceof Float32Array) { + return 1.2e-38; + } + return 0; +} + +/** + * @param {Array} array The data array. + * @return {number} The maximum value. + */ +function getMaxForDataType(array) { + if (array instanceof Int8Array) { + return 127; + } + if (array instanceof Uint8Array) { + return 255; + } + if (array instanceof Uint8ClampedArray) { + return 255; + } + if (array instanceof Int16Array) { + return 32767; + } + if (array instanceof Uint16Array) { + return 65535; + } + if (array instanceof Int32Array) { + return 2147483647; + } + if (array instanceof Uint32Array) { + return 4294967295; + } + if (array instanceof Float32Array) { + return 3.4e38; + } + return 255; +} + +/** + * @typedef Options + * @property {Array} sources List of information about GeoTIFF sources. + */ + +/** + * @classdesc + * A source for working with GeoTIFF data. + */ +class GeoTIFFSource extends DataTile { + /** + * @param {Options} options Image tile options. + */ + constructor(options) { + super({ + state: State.LOADING, + tileGrid: null, + projection: null, + }); + + /** + * @type {Array} + * @private + */ + this.sourceInfo_ = options.sources; + + const numSources = this.sourceInfo_.length; + + /** + * @type {Array>} + * @private + */ + this.sourceImagery_ = new Array(numSources); + + /** + * @type {number} + * @private + */ + this.samplesPerPixel_; + + /** + * @type {Error} + * @private + */ + this.error_ = null; + + const self = this; + const requests = new Array(numSources); + for (let i = 0; i < numSources; ++i) { + requests[i] = getImagesForSource(this.sourceInfo_[i]); + } + Promise.all(requests) + .then(function (sources) { + self.configure_(sources); + }) + .catch(function (error) { + self.error_ = error; + self.setState(State.ERROR); + }); + } + + /** + * @return {Error} A source loading error. + */ + getError() { + return this.error_; + } + + /** + * Configure the tile grid based on images within the source GeoTIFFs. Each GeoTIFF + * must have the same internal tiled structure. + * @param {Array>} sources Each source is a list of images + * from a single GeoTIFF. + */ + configure_(sources) { + let extent; + let origin; + let tileSizes; + let resolutions; + let samplesPerPixel; + + const sourceCount = sources.length; + for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) { + const images = sources[sourceIndex]; + const imageCount = images.length; + + let sourceExtent; + let sourceOrigin; + const sourceTileSizes = new Array(imageCount); + const sourceResolutions = new Array(imageCount); + + for (let imageIndex = 0; imageIndex < imageCount; ++imageIndex) { + const image = images[imageIndex]; + const imageSamplesPerPixel = image.getSamplesPerPixel(); + if (!samplesPerPixel) { + samplesPerPixel = imageSamplesPerPixel; + } else { + const message = `Band count mismatch for source ${sourceIndex}, got ${imageSamplesPerPixel} but expected ${samplesPerPixel}`; + assertEqual(samplesPerPixel, imageSamplesPerPixel, message); + } + const level = imageCount - (imageIndex + 1); + + if (!sourceExtent) { + sourceExtent = image.getBoundingBox(); + } + + if (!sourceOrigin) { + sourceOrigin = image.getOrigin().slice(0, 2); + } + + sourceResolutions[level] = image.getResolution(images[0])[0]; + sourceTileSizes[level] = [image.getTileWidth(), image.getTileHeight()]; + } + + if (!extent) { + extent = sourceExtent; + } else { + const message = `Extent mismatch for source ${sourceIndex}, got [${sourceExtent}] but expected [${extent}]`; + assertEqual(extent, sourceExtent, message); + } + + if (!origin) { + origin = sourceOrigin; + } else { + const message = `Origin mismatch for source ${sourceIndex}, got [${sourceOrigin}] but expected [${origin}]`; + assertEqual(origin, sourceOrigin, message); + } + + if (!tileSizes) { + tileSizes = sourceTileSizes; + } else { + assertEqual( + tileSizes, + sourceTileSizes, + `Tile size mismatch for source ${sourceIndex}` + ); + } + + if (!resolutions) { + resolutions = sourceResolutions; + } else { + const message = `Resolution mismatch for source ${sourceIndex}, got [${sourceResolutions}] but expected [${resolutions}]`; + assertEqual(resolutions, sourceResolutions, message); + } + + this.sourceImagery_[sourceIndex] = images.reverse(); + } + + if (!this.getProjection()) { + const firstImage = sources[0][0]; + if (firstImage.geoKeys) { + const code = + firstImage.geoKeys.ProjectedCSTypeGeoKey || + firstImage.geoKeys.GeographicTypeGeoKey; + if (code) { + this.projection = getProjection(`EPSG:${code}`); + } + } + } + + if (sourceCount > 1 && samplesPerPixel !== 1) { + throw new Error( + 'Expected single band GeoTIFFs when using multiple sources' + ); + } + + this.samplesPerPixel_ = samplesPerPixel; + + const tileGrid = new TileGrid({ + extent: extent, + origin: origin, + resolutions: resolutions, + tileSizes: tileSizes, + }); + + this.tileGrid = tileGrid; + + this.setLoader(this.loadTile_.bind(this)); + this.setState(State.READY); + } + + loadTile_(z, x, y) { + const size = toSize(this.tileGrid.getTileSize(z)); + const pixelBounds = [ + x * size[0], + y * size[1], + (x + 1) * size[0], + (y + 1) * size[1], + ]; + + const sourceCount = this.sourceImagery_.length; + const requests = new Array(sourceCount); + let addAlpha = false; + const sourceInfo = this.sourceInfo_; + for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) { + const image = this.sourceImagery_[sourceIndex][z]; + requests[sourceIndex] = image.readRasters({window: pixelBounds}); + if (sourceInfo[sourceIndex].nodata !== undefined) { + addAlpha = true; + } + } + + const samplesPerPixel = this.samplesPerPixel_; + let additionalBands = 0; + if (addAlpha) { + if (sourceCount === 2 && samplesPerPixel === 1) { + additionalBands = 2; + } else { + additionalBands = 1; + } + } + const bandCount = samplesPerPixel * sourceCount + additionalBands; + const pixelCount = size[0] * size[1]; + const dataLength = pixelCount * bandCount; + + return Promise.all(requests).then(function (sourceSamples) { + const data = new Uint8ClampedArray(dataLength); + for (let pixelIndex = 0; pixelIndex < pixelCount; ++pixelIndex) { + let transparent = addAlpha; + const sourceOffset = pixelIndex * bandCount; + for (let sourceIndex = 0; sourceIndex < sourceCount; ++sourceIndex) { + const source = sourceInfo[sourceIndex]; + let min = source.min; + if (min === undefined) { + min = getMinForDataType(sourceSamples[sourceIndex][0]); + } + let max = source.max; + if (max === undefined) { + max = getMaxForDataType(sourceSamples[sourceIndex][0]); + } + + const gain = 255 / (max - min); + const bias = -min * gain; + + const nodata = source.nodata; + + const sampleOffset = sourceOffset + sourceIndex * samplesPerPixel; + for ( + let sampleIndex = 0; + sampleIndex < samplesPerPixel; + ++sampleIndex + ) { + const sourceValue = + sourceSamples[sourceIndex][sampleIndex][pixelIndex]; + + const value = gain * sourceValue + bias; + if (!addAlpha) { + data[sampleOffset + sampleIndex] = value; + } else { + if (sourceValue !== nodata) { + transparent = false; + data[sampleOffset + sampleIndex] = value; + } + } + } + + if (addAlpha && !transparent) { + data[sampleOffset + samplesPerPixel] = 255; + } + } + } + + return data; + }); + } +} + +export default GeoTIFFSource; diff --git a/src/ol/style/expressions.js b/src/ol/style/expressions.js index 106190727e..8f9aa6bb1d 100644 --- a/src/ol/style/expressions.js +++ b/src/ol/style/expressions.js @@ -170,6 +170,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 +403,27 @@ Operators['var'] = { }, }; +Operators['band'] = { + getReturnType: function (args) { + return ValueTypes.NUMBER; + }, + toGlsl: function (context, args) { + assertArgsCount(args, 1); + const band = args[0]; + if (typeof band !== 'number') { + throw new Error('Band index must be a number'); + } + const zeroBasedBand = band - 1; + const colorIndex = Math.floor(zeroBasedBand / 4); + let bandIndex = zeroBasedBand % 4; + if (band === context.bandCount && bandIndex === 1) { + // LUMINANCE_ALPHA - band 1 assigned to rgb and band 2 assigned to alpha + bandIndex = 3; + } + return `color${colorIndex}[${bandIndex}]`; + }, +}; + Operators['time'] = { getReturnType: function (args) { return ValueTypes.NUMBER; @@ -748,17 +770,16 @@ Operators['interpolate'] = { assertUniqueInferredType(args, outputType); const input = expressionToGlsl(context, args[1]); - let result = null; + const exponent = numberToGlsl(interpolation); + + let result = ''; for (let i = 2; i < args.length - 2; i += 2) { const stop1 = expressionToGlsl(context, args[i]); - const output1 = expressionToGlsl(context, args[i + 1], outputType); + const output1 = + result || expressionToGlsl(context, args[i + 1], outputType); const stop2 = expressionToGlsl(context, args[i + 2]); const output2 = expressionToGlsl(context, args[i + 3], outputType); - result = `mix(${ - result || output1 - }, ${output2}, pow(clamp((${input} - ${stop1}) / (${stop2} - ${stop1}), 0.0, 1.0), ${numberToGlsl( - interpolation - )}))`; + result = `mix(${output1}, ${output2}, pow(clamp((${input} - ${stop1}) / (${stop2} - ${stop1}), 0.0, 1.0), ${exponent}))`; } return result; }, diff --git a/src/ol/webgl/TileTexture.js b/src/ol/webgl/TileTexture.js new file mode 100644 index 0000000000..cad02dc42e --- /dev/null +++ b/src/ol/webgl/TileTexture.js @@ -0,0 +1,195 @@ +/** + * @module ol/webgl/TileTexture + */ + +import EventTarget from '../events/Target.js'; +import EventType from '../events/EventType.js'; +import ImageTile from '../ImageTile.js'; +import TileState from '../TileState.js'; +import WebGLArrayBuffer from './Buffer.js'; +import {ARRAY_BUFFER, STATIC_DRAW} from '../webgl.js'; +import {toSize} from '../size.js'; + +function bindAndConfigure(gl, texture) { + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); +} + +/** + * @param {WebGLRenderingContext} gl The WebGL context. + * @param {WebGLTexture} texture The texture. + * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} image The image. + */ +function uploadImageTexture(gl, texture, image) { + bindAndConfigure(gl, texture); + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image); +} + +/** + * @param {WebGLRenderingContext} gl The WebGL context. + * @param {WebGLTexture} texture The texture. + * @param {import("../DataTile.js").Data} data The pixel data. + * @param {import("../size.js").Size} size The pixel size. + * @param {number} bandCount The band count. + */ +function uploadDataTexture(gl, texture, data, size, bandCount) { + bindAndConfigure(gl, texture); + + let format; + switch (bandCount) { + case 1: { + format = gl.LUMINANCE; + break; + } + case 2: { + format = gl.LUMINANCE_ALPHA; + break; + } + case 3: { + format = gl.RGB; + break; + } + case 4: { + format = gl.RGBA; + break; + } + default: { + throw new Error(`Unsupported number of bands: ${bandCount}`); + } + } + + gl.texImage2D( + gl.TEXTURE_2D, + 0, + format, + size[0], + size[1], + 0, + format, + gl.UNSIGNED_BYTE, + data + ); +} + +class TileTexture extends EventTarget { + /** + * @param {import("../DataTile.js").default|import("../ImageTile.js").default} tile The tile. + * @param {import("../tilegrid/TileGrid.js").default} grid Tile grid. + * @param {import("../webgl/Helper.js").default} helper WebGL helper. + */ + constructor(tile, grid, helper) { + super(); + + this.tile = tile; + this.size = toSize(grid.getTileSize(tile.tileCoord[0])); + this.loaded = tile.getState() === TileState.LOADED; + + this.bandCount = NaN; + + this.helper_ = helper; + this.handleTileChange_ = this.handleTileChange_.bind(this); + + const coords = new WebGLArrayBuffer(ARRAY_BUFFER, STATIC_DRAW); + coords.fromArray([ + 0, // P0 + 1, + 1, // P1 + 1, + 1, // P2 + 0, + 0, // P3 + 0, + ]); + helper.flushBufferData(coords); + this.coords = coords; + + /** + * @type {Array} + */ + this.textures = []; + + if (this.loaded) { + this.uploadTile_(); + } else { + tile.addEventListener(EventType.CHANGE, this.handleTileChange_); + } + } + + uploadTile_() { + const gl = this.helper_.getGL(); + const tile = this.tile; + + if (tile instanceof ImageTile) { + const texture = gl.createTexture(); + this.textures.push(texture); + this.bandCount = 4; + uploadImageTexture(gl, texture, tile.getImage()); + return; + } + + const data = tile.getData(); + const pixelCount = this.size[0] * this.size[1]; + this.bandCount = data.byteLength / pixelCount; + const textureCount = Math.ceil(this.bandCount / 4); + + if (textureCount === 1) { + const texture = gl.createTexture(); + this.textures.push(texture); + uploadDataTexture(gl, texture, data, this.size, this.bandCount); + return; + } + + const textureDataArrays = new Array(textureCount); + for (let textureIndex = 0; textureIndex < textureCount; ++textureIndex) { + const texture = gl.createTexture(); + this.textures.push(texture); + + const bandCount = + textureIndex < textureCount - 1 ? 4 : this.bandCount % 4; + textureDataArrays[textureIndex] = new Uint8Array(pixelCount * bandCount); + } + + const valueCount = pixelCount * this.bandCount; + for (let dataIndex = 0; dataIndex < valueCount; ++dataIndex) { + const bandIndex = dataIndex % this.bandCount; + const textureBandIndex = bandIndex % 4; + const textureIndex = Math.floor(bandIndex / 4); + const bandCount = + textureIndex < textureCount - 1 ? 4 : this.bandCount % 4; + const pixelIndex = Math.floor(dataIndex / this.bandCount); + textureDataArrays[textureIndex][ + pixelIndex * bandCount + textureBandIndex + ] = data[dataIndex]; + } + + for (let textureIndex = 0; textureIndex < textureCount; ++textureIndex) { + const bandCount = + textureIndex < textureCount - 1 ? 4 : this.bandCount % 4; + const texture = this.textures[textureIndex]; + const data = textureDataArrays[textureIndex]; + uploadDataTexture(gl, texture, data, this.size, bandCount); + } + } + + handleTileChange_() { + if (this.tile.getState() === TileState.LOADED) { + this.loaded = true; + this.uploadTile_(); + this.dispatchEvent(EventType.CHANGE); + } + } + + disposeInternal() { + const gl = this.helper_.getGL(); + this.helper_.deleteBuffer(this.coords); + for (let i = 0; i < this.textures.length; ++i) { + gl.deleteTexture(this.textures[i]); + } + this.tile.removeEventListener(EventType.CHANGE, this.handleTileChange_); + } +} + +export default TileTexture;