diff --git a/debug/hillshade.html b/debug/hillshade.html index 68965655cca..6285c11b7e3 100644 --- a/debug/hillshade.html +++ b/debug/hillshade.html @@ -12,6 +12,52 @@ + + + +
@@ -26,19 +72,69 @@ }); map.on('load', function () { - map.addSource('dem', { + + map.addSource('mapbox-dem', { "type": "raster-dem", - "url": "mapbox://mapbox.terrain-rgb" + "url": "mapbox://mapbox.terrain-rgb", + "tileSize": 256 }); map.addLayer({ - "id": "hillshading", - "source": "dem", + "id": "Mapbox data", + "source": "mapbox-dem", "type": "hillshade" // insert below waterway-river-canal-shadow; // where hillshading sits in the Mapbox Outdoors style }, 'waterway-river-canal-shadow'); + + map.addSource('terrarium-dem', { + "type": "raster-dem", + "encoding": "terrarium", + "tiles": [ + "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png" + ], + "tileSize": 256 + }); + map.addLayer({ + "id": "Terrarium data", + "source": "terrarium-dem", + "type": "hillshade", + "layout": { + "visibility": "none" + } + // insert below waterway-river-canal-shadow; + // where hillshading sits in the Mapbox Outdoors style + }, 'waterway-river-canal-shadow'); + }); +var toggleableLayerIds = ['Mapbox data', 'Terrarium data']; + +for (var i = 0; i < toggleableLayerIds.length; i++) { + var id = toggleableLayerIds[i]; + + var link = document.createElement('a'); + link.href = '#'; + link.className = (i === 0) ? 'active' : ''; + link.textContent = id; + + link.onclick = function (e) { + var clickedLayer = this.textContent; + e.preventDefault(); + e.stopPropagation(); + + if (this.className === '') { + var activeLayer = document.getElementsByClassName('active')[0]; + activeLayer.className = ''; + map.setLayoutProperty(activeLayer.textContent, 'visibility', 'none'); + this.className = 'active'; + map.setLayoutProperty(clickedLayer, 'visibility', 'visible'); + } + }; + + var layers = document.getElementById('menu'); + layers.appendChild(link); +} + diff --git a/flow-typed/style-spec.js b/flow-typed/style-spec.js index 7ece7672149..b00732c7959 100644 --- a/flow-typed/style-spec.js +++ b/flow-typed/style-spec.js @@ -102,7 +102,8 @@ declare type RasterDEMSourceSpecification = { "minzoom"?: number, "maxzoom"?: number, "tileSize"?: number, - "attribution"?: string + "attribution"?: string, + "encoding"?: "terrarium" | "mapbox" } declare type GeojsonSourceSpecification = {| diff --git a/src/data/dem_data.js b/src/data/dem_data.js index ae2d1f7fa3f..4dc472749e0 100644 --- a/src/data/dem_data.js +++ b/src/data/dem_data.js @@ -57,22 +57,14 @@ class DEMData { this.loaded = !!data; } - loadFromImage(data: RGBAImage) { + loadFromImage(data: RGBAImage, encoding: "mapbox" | "terrarium") { if (data.height !== data.width) throw new RangeError('DEM tiles must be square'); - + if (encoding && encoding !== "mapbox" && encoding !== "terrarium") return util.warnOnce(`"${encoding}" is not a valid encoding type. Valid types include "mapbox" and "terrarium".`); // Build level 0 const level = this.level = new Level(data.width, data.width / 2); const pixels = data.data; - // unpack - for (let y = 0; y < level.dim; y++) { - for (let x = 0; x < level.dim; x++) { - const i = y * level.dim + x; - const j = i * 4; - // decoding per https://blog.mapbox.com/global-elevation-data-6689f1d0ba65 - level.set(x, y, this.scale * ((pixels[j] * 256 * 256 + pixels[j + 1] * 256.0 + pixels[j + 2]) / 10.0 - 10000.0)); - } - } + this._unpackData(level, pixels, encoding || "mapbox"); // in order to avoid flashing seams between tiles, here we are initially populating a 1px border of pixels around the image // with the data of the nearest pixel from the image. this data is eventually replaced when the tile's neighboring @@ -95,6 +87,30 @@ class DEMData { this.loaded = true; } + _unpackMapbox(r: number, g: number, b: number) { + // unpacking formula for mapbox.terrain-rgb: + // https://www.mapbox.com/help/access-elevation-data/#mapbox-terrain-rgb + return ((r * 256 * 256 + g * 256.0 + b) / 10.0 - 10000.0); + } + + _unpackTerrarium(r: number, g: number, b: number) { + // unpacking formula for mapzen terrarium: + // https://aws.amazon.com/public-datasets/terrain/ + return ((r * 256 + g + b / 256) - 32768.0); + } + + _unpackData(level: Level, pixels: Uint8Array | Uint8ClampedArray, encoding: string) { + const unpackFunctions = {"mapbox": this._unpackMapbox, "terrarium": this._unpackTerrarium}; + const unpack = unpackFunctions[encoding]; + for (let y = 0; y < level.dim; y++) { + for (let x = 0; x < level.dim; x++) { + const i = y * level.dim + x; + const j = i * 4; + level.set(x, y, this.scale * unpack(pixels[j], pixels[j + 1], pixels[j + 2])); + } + } + } + getPixels() { return new RGBAImage({width: this.level.dim + 2 * this.level.border, height: this.level.dim + 2 * this.level.border}, new Uint8Array(this.level.data.buffer)); } diff --git a/src/source/raster_dem_tile_source.js b/src/source/raster_dem_tile_source.js index d8ed8ba8121..666802dac69 100644 --- a/src/source/raster_dem_tile_source.js +++ b/src/source/raster_dem_tile_source.js @@ -15,11 +15,14 @@ import type {Callback} from '../types/callback'; class RasterDEMTileSource extends RasterTileSource implements Source { + encoding: "mapbox" | "terrarium"; + constructor(id: string, options: RasterDEMSourceSpecification, dispatcher: Dispatcher, eventedParent: Evented) { super(id, options, dispatcher, eventedParent); this.type = 'raster-dem'; this.maxzoom = 22; this._options = util.extend({}, options); + this.encoding = options.encoding || "mapbox"; } serialize() { @@ -29,6 +32,7 @@ class RasterDEMTileSource extends RasterTileSource implements Source { tileSize: this.tileSize, tiles: this.tiles, bounds: this.bounds, + encoding: this.encoding }; } @@ -55,7 +59,8 @@ class RasterDEMTileSource extends RasterTileSource implements Source { uid: tile.uid, coord: tile.tileID, source: this.id, - rawImageData: rawImageData + rawImageData: rawImageData, + encoding: this.encoding }; if (!tile.workerID || tile.state === 'expired') { diff --git a/src/source/raster_dem_tile_worker_source.js b/src/source/raster_dem_tile_worker_source.js index cebe046e05d..d6051e0d1c6 100644 --- a/src/source/raster_dem_tile_worker_source.js +++ b/src/source/raster_dem_tile_worker_source.js @@ -21,11 +21,12 @@ class RasterDEMTileWorkerSource { } loadTile(params: WorkerDEMTileParameters, callback: WorkerDEMTileCallback) { - const uid = params.uid; + const uid = params.uid, + encoding = params.encoding; const dem = new DEMData(uid); this.loading[uid] = dem; - dem.loadFromImage(params.rawImageData); + dem.loadFromImage(params.rawImageData, encoding); delete this.loading[uid]; this.loaded = this.loaded || {}; diff --git a/src/source/worker_source.js b/src/source/worker_source.js index 02049003e5c..2c0bd57e221 100644 --- a/src/source/worker_source.js +++ b/src/source/worker_source.js @@ -28,7 +28,8 @@ export type WorkerTileParameters = TileParameters & { export type WorkerDEMTileParameters = TileParameters & { coord: { z: number, x: number, y: number, w: number }, - rawImageData: RGBAImage + rawImageData: RGBAImage, + encoding: "mapbox" | "terrarium" }; export type WorkerTileResult = { diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index 6acae51edc7..10a68f28bd9 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -231,7 +231,7 @@ "type": "enum", "values": { "raster-dem": { - "doc": "A raster DEM source using Mapbox Terrain RGB" + "doc": "A RGB-encoded raster DEM source" } }, "doc": "The type of the source." @@ -272,6 +272,19 @@ "type": "string", "doc": "Contains an attribution to be displayed when the map is shown to a user." }, + "encoding": { + "type": "enum", + "values": { + "terrarium": { + "doc": "Terrarium format PNG tiles. See https://aws.amazon.com/es/public-datasets/terrain/ for more info." + }, + "mapbox": { + "doc": "Mapbox Terrain RGB tiles. See https://www.mapbox.com/help/access-elevation-data/#mapbox-terrain-rgb for more info." + } + }, + "default": "mapbox", + "doc": "The encoding used by this source. Mapbox Terrain RGB is used by default" + }, "*": { "type": "*", "doc": "Other keys to configure the data source." @@ -502,7 +515,7 @@ } }, "hillshade": { - "doc": "Client-side hillshading visualization based on DEM data. Currently, the implementation only supports Mapbox Terrain RGB tiles", + "doc": "Client-side hillshading visualization based on DEM data. Currently, the implementation only supports Mapbox Terrain RGB and Mapzen Terrarium tiles.", "sdk-support": { "basic functionality": { "js": "0.43.0" diff --git a/test/integration/render-tests/hillshade-accent-color/terrarium/expected.png b/test/integration/render-tests/hillshade-accent-color/terrarium/expected.png new file mode 100644 index 00000000000..bfa642090da Binary files /dev/null and b/test/integration/render-tests/hillshade-accent-color/terrarium/expected.png differ diff --git a/test/integration/render-tests/hillshade-accent-color/terrarium/style.json b/test/integration/render-tests/hillshade-accent-color/terrarium/style.json new file mode 100644 index 00000000000..ca14a4da901 --- /dev/null +++ b/test/integration/render-tests/hillshade-accent-color/terrarium/style.json @@ -0,0 +1,36 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 64, + "width": 64 + } + }, + "center": [-113.26903, 35.995], + "zoom": 11, + "sources": { + "source": { + "type": "raster-dem", + "tiles": [ + "local://tiles/{z}-{x}-{y}.terrarium.png" + ], + "maxzoom": 15, + "tileSize": 256, + "encoding": "terrarium" + } + }, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "white" + } + }, + { + "id": "hillshade", + "type": "hillshade", + "source": "source" + } + ] +} diff --git a/test/integration/tiles/12-759-1608.terrarium.png b/test/integration/tiles/12-759-1608.terrarium.png new file mode 100644 index 00000000000..8587a6663d1 Binary files /dev/null and b/test/integration/tiles/12-759-1608.terrarium.png differ diff --git a/test/unit/data/dem_data.test.js b/test/unit/data/dem_data.test.js index ff2b2017851..74e12f59d6f 100644 --- a/test/unit/data/dem_data.test.js +++ b/test/unit/data/dem_data.test.js @@ -1,6 +1,7 @@ 'use strict'; const test = require('mapbox-gl-js-test').test; +const util = require('../../../src/util/util'); const {DEMData, Level} = require('../../../src/data/dem_data'); const {RGBAImage} = require('../../../src/util/image'); const {serialize, deserialize} = require('../../../src/util/web_worker_transfer'); @@ -48,7 +49,7 @@ test('Level', (t)=>{ }); -test('DEMData constructor', (t) => { +test('DEMData', (t) => { t.test('constructor', (t) => { const dem = new DEMData(0, 1); t.false(dem.loaded); @@ -77,6 +78,18 @@ test('DEMData constructor', (t) => { t.end(); }); + t.test('loadFromImage with invalid encoding', (t) => { + const dem = new DEMData(0, 1); + t.stub(util, 'warnOnce'); + t.false(dem.loaded); + t.equal(dem.uid, 0); + + dem.loadFromImage({width: 4, height: 4, data: new Uint8ClampedArray(4 * 4 * 4)}, "derp"); + t.ok(util.warnOnce.calledOnce); + t.ok(util.warnOnce.getCall(0).calledWithMatch(/"derp" is not a valid encoding type/)); + t.end(); + }); + t.end(); });