Merge pull request #12008 from tschaub/gl

Rendering raster tiles with WebGL
This commit is contained in:
Tim Schaub
2021-08-20 10:14:48 -06:00
committed by GitHub
51 changed files with 3540 additions and 57 deletions

View File

@@ -27,7 +27,8 @@
<a href="module-ol_layer_Tile-TileLayer.html">ol/layer/Tile</a><br>
<a href="module-ol_layer_Image-ImageLayer.html">ol/layer/Image</a><br>
<a href="module-ol_layer_Vector-VectorLayer.html">ol/layer/Vector</a><br>
<a href="module-ol_layer_VectorTile-VectorTileLayer.html">ol/layer/VectorTile</a>
<a href="module-ol_layer_VectorTile-VectorTileLayer.html">ol/layer/VectorTile</a><br>
<a href="module-ol_layer_WebGLTile-WebGLTileLayer.html">ol/layer/WebGLTile</a>
</div>
</div>
</div>
@@ -58,7 +59,7 @@
<div class="card h-100 bg-light">
<div class="card-body">
<h4 class="card-title">Sources and formats</h4>
<a href="module-ol_source_Tile-TileSource.html">Tile sources</a> for <a href="module-ol_layer_Tile-TileLayer.html">ol/layer/Tile</a>
<a href="module-ol_source_Tile-TileSource.html">Tile sources</a> for <a href="module-ol_layer_Tile-TileLayer.html">ol/layer/Tile</a> or <a href="module-ol_layer_WebGLTile-WebGLTileLayer.html">ol/layer/WebGLTile</a>
<br><a href="module-ol_source_Image-ImageSource.html">Image sources</a> for <a href="module-ol_layer_Image-ImageLayer.html">ol/layer/Image</a>
<br><a href="module-ol_source_Vector-VectorSource.html">Vector sources</a> for <a href="module-ol_layer_Vector-VectorLayer.html">ol/layer/Vector</a>
<br><a href="module-ol_source_VectorTile-VectorTile.html">Vector tile sources</a> for <a href="module-ol_layer_VectorTile-VectorTileLayer.html">ol/layer/VectorTile</a>
@@ -71,7 +72,7 @@
<div class="card h-100 bg-light">
<div class="card-body">
<h4 class="card-title">Projections</h4>
<p>All coordinates and extents need to be provided in view projection (default: EPSG:3857). To transform, use <a href="module-ol_proj.html#.transform">ol/proj#transform()</a> and <a href="module-ol_proj.html#.transformExtent">ol/proj#transformExtent()</a>.</p>
<p>All coordinates and extents need to be provided in view projection (default: EPSG:3857). To transform coordinates from and to geographic, use <a href="module-ol_proj.html#.fromLonLat">ol/proj#fromLonLat()</a> and <a href="module-ol_proj.html#.toLonLat">ol/proj#toLonLat()</a>. For extents and other projections, use <a href="module-ol_proj.html#.transformExtent">ol/proj#transformExtent()</a> and <a href="module-ol_proj.html#.transform">ol/proj#transform()</a>.<p>
<a href="module-ol_proj.html">ol/proj</a>
</div>
</div>

View File

@@ -32,6 +32,11 @@ export default {
],
},
resolve: {
fallback: {
fs: false,
http: false,
https: false,
},
alias: {
ol: path.resolve('./build/ol'),
},

View File

@@ -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"
---
<div id="map" class="map"></div>

View File

@@ -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(
"<a href='https://s2maps.eu'>Sentinel-2 cloudless</a> by <a href='https://eox.at/'>EOX IT Services GmbH</a> (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,
}),
});

11
examples/cog-math.html Normal file
View File

@@ -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"
---
<div id="map" class="map"></div>

101
examples/cog-math.js Normal file
View File

@@ -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,
}),
});

View File

@@ -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"
---
<div id="map" class="map"></div>

68
examples/cog-overviews.js Normal file
View File

@@ -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,
}),
});

12
examples/cog-pyramid.html Normal file
View File

@@ -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"
---
<div id="map" class="map"></div>

71
examples/cog-pyramid.js Normal file
View File

@@ -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)
);
});

10
examples/cog.html Normal file
View File

@@ -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"
---
<div id="map" class="map"></div>

41
examples/cog.js Normal file
View File

@@ -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,
}),
});

9
examples/data-tiles.html Normal file
View File

@@ -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"
---
<div id="map" class="map"></div>

45
examples/data-tiles.js Normal file
View File

@@ -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,
}),
});

View File

@@ -0,0 +1,13 @@
#level {
display: inline-block;
width: 150px;
vertical-align: text-bottom;
}
a.location {
cursor: pointer;
}
#map {
background: #8bd4ff;
}

View File

@@ -0,0 +1,37 @@
---
layout: example.html
title: Sea Level (with WebGL)
shortdesc: Render sea level at different elevations
docs: >
<p>
The <code>style</code> property of a WebGL tile layer accepts a <code>color</code> 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 <code>color</code> expression operates on normalized pixel
values ranging from 0 to 1. The <code>band</code> operator is used to select normalized values
from a single band.
</p><p>
After converting the normalized RGB values to elevation, the <code>interpolate</code> 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 <code>var</code> operator allows you to
use a value that can be modified by your application. When the user drags the
sea level slider, the <code>layer.updateStyleVariables()</code> function is called to update
the <code>level</code> style variable with the value from the slider.
</p>
tags: "webgl, math, flood"
cloak:
- key: get_your_own_D6rA4zTHduk6KOKTXzGB
value: Get your own API key at https://www.maptiler.com/cloud/
---
<div id="map" class="map"></div>
<label>
Sea level
<input id="level" type="range" min="0" max="100" value="1"/>
+<span id="output"></span> m
</label>
<br>
Go to
<a class="location" data-center="-122.3267,37.8377" data-zoom="11">San Francisco</a>,
<a class="location" data-center="-73.9338,40.6861" data-zoom="11">New York</a>,
<a class="location" data-center="72.9481,18.9929" data-zoom="11">Mumbai</a>, or
<a class="location" data-center="120.831,31.160" data-zoom="9">Shanghai</a>

View File

@@ -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 =
'<a href="https://www.maptiler.com/copyright/" target="_blank">&copy; MapTiler</a> ' +
'<a href="https://www.openstreetmap.org/copyright" target="_blank">&copy; OpenStreetMap contributors</a>';
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));
}

View File

@@ -0,0 +1,7 @@
table.controls td {
padding: 2px 5px;
}
table.controls td:nth-child(3) {
text-align: right;
min-width: 3em;
}

View File

@@ -0,0 +1,32 @@
---
layout: example.html
title: Shaded Relief (with WebGL)
shortdesc: Calculate shaded relief from elevation data
docs: >
<p>
For the shaded relief, a single tiled source of elevation data is used as input.
The shaded relief is calculated by the layer's <code>style</code> with a <code>color</code>
expression. The style variables are updated when the user drags one of the sliders. The
<code>band</code> operator is used to sample data from neighboring pixels for calculating slope and
aspect, which is done with the <code>['band', bandIndex, xOffset, yOffset]</code> syntax.
</p>
tags: "webgl, shaded relief"
---
<div id="map" class="map"></div>
<table class="controls">
<tr>
<td><label for="vert">vertical exaggeration:</label></td>
<td><input id="vert" type="range" min="1" max="5" value="1"/></td>
<td><span id="vertOut"></span> x</td>
</tr>
<tr>
<td><label for="sunEl">sun elevation:</label></td>
<td><input id="sunEl" type="range" min="0" max="90" value="45"/></td>
<td><span id="sunElOut"></span> °</td>
</tr>
<tr>
<td><label for="sunAz">sun azimuth:</label></td>
<td><input id="sunAz" type="range" min="0" max="360" value="45"/></td>
<td><span id="sunAzOut"></span> °</td>
</tr>
</table>

View File

@@ -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,
}),
});

View File

@@ -0,0 +1,4 @@
#controls {
display: flex;
justify-content: space-around;
}

View File

@@ -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/
---
<div id="map" class="map"></div>
<div id="controls">
<label>
<input id="exposure" type="range" min="-0.5" max="0.5" step="0.01">
<br>exposure <span id="exposure-value"></span>
</label>
<label>
<input id="contrast" type="range" min="-0.5" max="0.5" step="0.01">
<br>contrast <span id="contrast-value"></span>
</label>
<label>
<input id="saturation" type="range" min="-0.5" max="0.5" step="0.01">
<br>saturation <span id="saturation-value"></span>
</label>
</div>

View File

@@ -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 =
'<a href="https://www.maptiler.com/copyright/" target="_blank">&copy; MapTiler</a> ' +
'<a href="https://www.openstreetmap.org/copyright" target="_blank">&copy; OpenStreetMap contributors</a>';
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);
});
}

View File

@@ -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"
---
<div id="map" class="map"></div>

17
examples/webgl-tiles.js Normal file
View File

@@ -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,
}),
});

View File

@@ -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

View File

@@ -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);
})

272
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

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

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

View File

@@ -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';

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

@@ -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<string, number>} [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<string,import("../webgl/Helper.js").UniformValue>} uniforms Uniform definitions.
*/
/**
* @param {Style} style The layer style.
* @param {number} [bandCount] The number of bands.
* @return {ParsedStyle} Shaders and uniforms generated from the style.
*/
function parseStyle(style, bandCount) {
const vertexShader = `
attribute vec2 ${Attributes.TEXTURE_COORD};
uniform mat4 ${Uniforms.TILE_TRANSFORM};
uniform float ${Uniforms.DEPTH};
varying vec2 v_textureCoord;
void main() {
v_textureCoord = ${Attributes.TEXTURE_COORD};
gl_Position = ${Uniforms.TILE_TRANSFORM} * vec4(${Attributes.TEXTURE_COORD}, ${Uniforms.DEPTH}, 1.0);
}
`;
/**
* @type {import("../style/expressions.js").ParsingContext}
*/
const context = {
inFragmentShader: true,
variables: [],
attributes: [],
stringLiteralsMap: {},
bandCount: bandCount,
};
const pipeline = [];
if (style.color !== undefined) {
const color = expressionToGlsl(context, style.color, ValueTypes.COLOR);
pipeline.push(`color = ${color};`);
}
if (style.contrast !== undefined) {
const contrast = expressionToGlsl(
context,
style.contrast,
ValueTypes.NUMBER
);
pipeline.push(
`color.rgb = clamp((${contrast} + 1.0) * color.rgb - (${contrast} / 2.0), vec3(0.0, 0.0, 0.0), vec3(1.0, 1.0, 1.0));`
);
}
if (style.exposure !== undefined) {
const exposure = expressionToGlsl(
context,
style.exposure,
ValueTypes.NUMBER
);
pipeline.push(
`color.rgb = clamp((${exposure} + 1.0) * color.rgb, vec3(0.0, 0.0, 0.0), vec3(1.0, 1.0, 1.0));`
);
}
if (style.saturation !== undefined) {
const saturation = expressionToGlsl(
context,
style.saturation,
ValueTypes.NUMBER
);
pipeline.push(`
float saturation = ${saturation} + 1.0;
float sr = (1.0 - saturation) * 0.2126;
float sg = (1.0 - saturation) * 0.7152;
float sb = (1.0 - saturation) * 0.0722;
mat3 saturationMatrix = mat3(
sr + saturation, sr, sr,
sg, sg + saturation, sg,
sb, sb, sb + saturation
);
color.rgb = clamp(saturationMatrix * color.rgb, vec3(0.0, 0.0, 0.0), vec3(1.0, 1.0, 1.0));
`);
}
if (style.gamma !== undefined) {
const gamma = expressionToGlsl(context, style.gamma, ValueTypes.NUMBER);
pipeline.push(`color.rgb = pow(color.rgb, vec3(1.0 / ${gamma}));`);
}
if (style.brightness !== undefined) {
const brightness = expressionToGlsl(
context,
style.brightness,
ValueTypes.NUMBER
);
pipeline.push(
`color.rgb = clamp(color.rgb + ${brightness}, vec3(0.0, 0.0, 0.0), vec3(1.0, 1.0, 1.0));`
);
}
/** @type {Object<string,import("../webgl/Helper").UniformValue>} */
const uniforms = {};
const numVariables = context.variables.length;
if (numVariables > 1 && !style.variables) {
throw new Error(
`Missing variables in style (expected ${context.variables})`
);
}
for (let i = 0; i < numVariables; ++i) {
const variableName = context.variables[i];
if (!(variableName in style.variables)) {
throw new Error(`Missing '${variableName}' in style variables`);
}
const uniformName = uniformNameForVariable(variableName);
uniforms[uniformName] = function () {
let value = style.variables[variableName];
if (typeof value === 'string') {
value = getStringNumberEquivalent(context, value);
}
return value !== undefined ? value : -9999999; // to avoid matching with the first string literal
};
}
const uniformDeclarations = Object.keys(uniforms).map(function (name) {
return `uniform float ${name};`;
});
const textureCount = Math.ceil(bandCount / 4);
const colorAssignments = new Array(textureCount);
for (let textureIndex = 0; textureIndex < textureCount; ++textureIndex) {
const uniformName = Uniforms.TILE_TEXTURE_PREFIX + textureIndex;
uniformDeclarations.push(`uniform sampler2D ${uniformName};`);
colorAssignments[
textureIndex
] = `vec4 color${textureIndex} = texture2D(${uniformName}, v_textureCoord);`;
}
const fragmentShader = `
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
varying vec2 v_textureCoord;
uniform float ${Uniforms.TRANSITION_ALPHA};
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<import("../source/DataTile.js").default|import("../source/TileImage.js").default>
* @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<string, number>} variables Variables to update.
* @api
*/
updateStyleVariables(variables) {
assign(this.styleVariables_, variables);
this.changed();
}
}
export default WebGLTileLayer;

View File

@@ -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<import('../../webgl/Helper.js').AttributeDescription>}
*/
const attributeDescriptions = [
{
name: Attributes.TEXTURE_COORD,
size: 2,
type: AttributeType.FLOAT,
},
];
/**
* Transform a zoom level into a depth value ranging from -1 to 1.
* @param {number} z A zoom level.
* @return {number} A depth value.
*/
function depthForZ(z) {
return 2 * (1 - 1 / (z + 1)) - 1;
}
/**
* Add a tile texture to the lookup.
* @param {Object<string, Array<import("../../webgl/TileTexture.js").default>>} tileTexturesByZ Lookup of
* tile textures by zoom level.
* @param {import("../../webgl/TileTexture.js").default} tileTexture A tile texture.
* @param {number} z The zoom level.
*/
function addTileTextureToLookup(tileTexturesByZ, tileTexture, z) {
if (!(z in tileTexturesByZ)) {
tileTexturesByZ[z] = [];
}
tileTexturesByZ[z].push(tileTexture);
}
/**
*
* @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<string, import("../../webgl/Helper").UniformValue>} [uniforms] Additional uniforms
* made available to shaders.
* @property {string} [className='ol-layer'] A CSS class name to set to the canvas element.
*/
/**
* @classdesc
* WebGL renderer for tile layers.
* @api
*/
class WebGLTileLayerRenderer extends WebGLLayerRenderer {
/**
* @param {import("../../layer/WebGLTile.js").default} tileLayer Tile layer.
* @param {Options} options Options.
*/
constructor(tileLayer, options) {
super(tileLayer, {
uniforms: options.uniforms,
className: options.className,
});
/**
* This transform converts tile i, j coordinates to screen coordinates.
* @type {import("../../transform.js").Transform}
* @private
*/
this.tileTransform_ = createTransform();
/**
* @type {Array<number>}
* @private
*/
this.tempMat4_ = createMat4();
/**
* @type {import("../../TileRange.js").default}
* @private
*/
this.tempTileRange_ = new TileRange(0, 0, 0, 0);
/**
* @type {import("../../tilecoord.js").TileCoord}
* @private
*/
this.tempTileCoord_ = createTileCoord(0, 0, 0);
/**
* @type {import("../../size.js").Size}
* @private
*/
this.tempSize_ = [0, 0];
this.program_ = this.helper.getProgram(
options.fragmentShader,
options.vertexShader
);
/**
* Tiles are rendered as a quad with the following structure:
*
* [P3]---------[P2]
* |` |
* | ` B |
* | ` |
* | ` |
* | A ` |
* | ` |
* [P0]---------[P1]
*
* Triangle A: P0, P1, P3
* Triangle B: P1, P2, P3
*/
const indices = new WebGLArrayBuffer(ELEMENT_ARRAY_BUFFER, STATIC_DRAW);
indices.fromArray([0, 1, 3, 1, 2, 3]);
this.helper.flushBufferData(indices);
this.indices_ = indices;
this.tileTextureCache_ = new LRUCache(512);
this.renderedOpacity_ = NaN;
}
/**
* @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<string, Array<import("../../webgl/TileTexture.js").default>>}
*/
const tileTexturesByZ = {};
const tileTextureCache = this.tileTextureCache_;
const tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z);
const tileSourceKey = getUid(tileSource);
if (!(tileSourceKey in frameState.wantedTiles)) {
frameState.wantedTiles[tileSourceKey] = {};
}
const wantedTiles = frameState.wantedTiles[tileSourceKey];
const tileResolution = tileGrid.getResolution(z);
for (let x = tileRange.minX; x <= tileRange.maxX; ++x) {
for (let y = tileRange.minY; y <= tileRange.maxY; ++y) {
const tileCoord = createTileCoord(z, x, y, this.tempTileCoord_);
const tileCoordKey = getTileCoordKey(tileCoord);
let tileTexture, 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<string, number>}
*/
const alphaLookup = {};
const uid = getUid(this);
const time = frameState.time;
let blend = false;
// look for cached tiles to use if a target tile is not ready
const tileTextures = tileTexturesByZ[z];
for (let i = 0, ii = tileTextures.length; i < ii; ++i) {
const tileTexture = tileTextures[i];
const tile = tileTexture.tile;
const tileCoord = tile.tileCoord;
if (tileTexture.loaded) {
const alpha = tile.getAlpha(uid, time);
if (alpha === 1) {
// no need to look for alt tiles
tile.endTransition(uid);
continue;
}
blend = true;
const tileCoordKey = getTileCoordKey(tileCoord);
alphaLookup[tileCoordKey] = alpha;
}
// 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<string, Array<import("../../webgl/TileTexture.js").default>>} tileTexturesByZ Lookup of
* tile textures by zoom level.
* @return {boolean} The tile coordinate is covered by loaded tiles at the alternate zoom level.
* @private
*/
findAltTiles_(tileGrid, tileCoord, altZ, tileTexturesByZ) {
const tileRange = tileGrid.getTileRangeForTileCoordAndZ(
tileCoord,
altZ,
this.tempTileRange_
);
if (!tileRange) {
return false;
}
let covered = true;
const tileTextureCache = this.tileTextureCache_;
for (let x = tileRange.minX; x <= tileRange.maxX; ++x) {
for (let y = tileRange.minY; y <= tileRange.maxY; ++y) {
const cacheKey = getKeyZXY(altZ, x, y);
let loaded = false;
if (tileTextureCache.containsKey(cacheKey)) {
const tileTexture = tileTextureCache.get(cacheKey);
if (tileTexture.loaded) {
addTileTextureToLookup(tileTexturesByZ, tileTexture, altZ);
loaded = true;
}
}
if (!loaded) {
covered = false;
}
}
}
return covered;
}
}
/**
* @function
* @return {import("../../layer/WebGLTile.js").default}
*/
WebGLTileLayerRenderer.prototype.getLayer;
export default WebGLTileLayerRenderer;

View File

@@ -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';

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

@@ -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<import("../DataTile.js").Data>} [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<string, boolean>}
*/
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<import("../DataTile.js").Data>} loader The data loader.
* @protected
*/
setLoader(loader) {
this.loader_ = loader;
}
/**
* @abstract
* @param {number} z Tile coordinate z.
* @param {number} x Tile coordinate x.
* @param {number} y Tile coordinate y.
* @param {number} pixelRatio Pixel ratio.
* @param {import("../proj/Projection.js").default} projection Projection.
* @return {!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;

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

@@ -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<string>} [overviews] List of any overview URLs.
* @property {number} [min=0] The minimum source data value. Rendered values are scaled from 0 to 1 based on
* the configured min and max.
* @property {number} [max] The maximum source data value. Rendered values are scaled from 0 to 1 based on
* the configured min and max.
* @property {number} [nodata] Values to discard. When provided, an additional band (alpha) will be added
* to the data.
* @property {Array<number>} [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<Array<import("geotiff/src/geotiffimage.js").GeoTIFFImage>>} Resolves to a list of images.
*/
function getImagesForTIFF(tiff) {
return tiff.getImageCount().then(function (count) {
const requests = new Array(count);
for (let i = 0; i < count; ++i) {
requests[i] = tiff.getImage(i);
}
return Promise.all(requests);
});
}
/**
* @param {SourceInfo} source The GeoTIFF source.
* @return {Promise<Array<import("geotiff/src/geotiffimage.js").GeoTIFFImage>>} Resolves to a list of images.
*/
function getImagesForSource(source) {
let request;
if (source.overviews) {
request = tiffFromUrls(source.url, source.overviews);
} else {
request = tiffFromUrl(source.url);
}
return request.then(getImagesForTIFF);
}
/**
* @param {number|Array<number>|Array<Array<number>>} expected Expected value.
* @param {number|Array<number>|Array<Array<number>>} got Actual value.
* @param {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<SourceInfo>} 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<SourceInfo>}
* @private
*/
this.sourceInfo_ = options.sources;
const numSources = this.sourceInfo_.length;
/**
* @type {Array<Array<import("geotiff/src/geotiffimage.js").GeoTIFFImage>>}
* @private
*/
this.sourceImagery_ = new Array(numSources);
/**
* @type {Array<number>}
* @private
*/
this.resolutionFactors_ = new Array(numSources);
/**
* @type {Array<number>}
* @private
*/
this.samplesPerPixel_;
/**
* @type {Array<Array<number>>}
* @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<Array<import("geotiff/src/geotiffimage.js").GeoTIFFImage>>} 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;

View File

@@ -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<string>} variables List of variables used in the expression; contains **unprefixed names**
* @property {Array<string>} attributes List of attributes used in the expression; contains **unprefixed names**
* @property {Object<string, number>} stringLiteralsMap This object maps all encountered string values to a number
* @property {number} [bandCount] Number of bands per pixel.
*/
/**
@@ -402,6 +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;
},

View File

@@ -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);

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

@@ -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<WebGLTexture>}
*/
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;

View File

@@ -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;

View File

@@ -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: [
{

View File

@@ -0,0 +1,51 @@
import DataTile from '../../../../src/ol/DataTile.js';
import TileState from '../../../../src/ol/TileState.js';
describe('ol.DataTile', function () {
/** @type {Promise<import('../../../../src/ol/DataTile.js').Data} */
let loader;
beforeEach(function () {
loader = function () {
return new Promise((resolve) => {
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);
});
});
});

View File

@@ -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
});
});

View File

@@ -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);
});
});

View File

@@ -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();
});
});
});

View File

@@ -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();
});
});
});

Binary file not shown.

View File

@@ -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 () {

View File

@@ -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();
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -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();