Skip to content

Commit dd962f8

Browse files
Address terrain globe Mercator review comments
1 parent 8c3cac8 commit dd962f8

2 files changed

Lines changed: 75 additions & 31 deletions

File tree

modules/geo-layers/src/terrain-layer/terrain-layer.ts

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ import {SimpleMeshLayer} from '@deck.gl/mesh-layers';
1818
import {COORDINATE_SYSTEM} from '@deck.gl/core';
1919
import type {Mesh} from '@loaders.gl/schema';
2020
import {TerrainWorkerLoader} from '@loaders.gl/terrain';
21+
import {
22+
MAX_LATITUDE as MAX_WEB_MERCATOR_LATITUDE,
23+
lngLatToWorld,
24+
worldToLngLat
25+
} from '@math.gl/web-mercator';
2126
import TileLayer, {TileLayerProps} from '../tile-layer/tile-layer';
2227
import type {
2328
Bounds,
@@ -33,8 +38,6 @@ const TILE_OVERLAP_PIXELS = 1;
3338
const MIN_TERRAIN_MESH_MAX_ERROR = 1;
3439
const MAX_LATITUDE = 90;
3540
const MAX_LONGITUDE = 180;
36-
const DEGREES_TO_RADIANS = Math.PI / 180;
37-
const RADIANS_TO_DEGREES = 180 / Math.PI;
3841

3942
const defaultProps: DefaultProps<TerrainLayerProps> = {
4043
...TileLayer.defaultProps,
@@ -110,6 +113,7 @@ type TerrainLoadProps = {
110113
elevationData: string | null;
111114
elevationDecoder: ElevationDecoder;
112115
meshMaxError: number;
116+
remapToWebMercatorTile?: boolean;
113117
signal?: AbortSignal;
114118
};
115119

@@ -205,6 +209,7 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
205209
bounds,
206210
elevationDecoder,
207211
meshMaxError,
212+
remapToWebMercatorTile,
208213
signal
209214
}: TerrainLoadProps): Promise<Mesh> | null {
210215
if (!elevationData) {
@@ -223,7 +228,16 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
223228
}
224229
};
225230
const {fetch} = this.props;
226-
return fetch(elevationData, {propName: 'elevationData', layer: this, loadOptions, signal});
231+
const terrain = fetch(elevationData, {
232+
propName: 'elevationData',
233+
layer: this,
234+
loadOptions,
235+
signal
236+
});
237+
238+
return remapToWebMercatorTile
239+
? terrain.then(mesh => (mesh ? remapMeshToWebMercatorTile(mesh, bounds) : mesh))
240+
: terrain;
227241
}
228242

229243
getTiledTerrainData(tile: TileLoadProps): Promise<MeshAndTexture> {
@@ -245,22 +259,18 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
245259
topRight = [bbox.right, bbox.top];
246260
}
247261
const bounds: Bounds = [bottomLeft[0], bottomLeft[1], topRight[0], topRight[1]];
248-
const overlappedBounds = getOverlappedBounds(
249-
bounds,
250-
this.props.tileSize,
251-
Boolean(viewport.resolution && viewport.resolution > 0)
252-
);
262+
const isGlobe = Boolean(viewport.resolution && viewport.resolution > 0);
263+
const overlappedBounds = getOverlappedBounds(bounds, this.props.tileSize, isGlobe);
253264

254265
const terrain =
255266
this.loadTerrain({
256267
elevationData: dataUrl,
257268
bounds: overlappedBounds,
258269
elevationDecoder,
259270
meshMaxError,
271+
remapToWebMercatorTile: isGlobe,
260272
signal
261-
})?.then(mesh =>
262-
viewport.resolution && mesh ? remapMeshToWebMercatorTile(mesh, overlappedBounds) : mesh
263-
) ?? Promise.resolve(null);
273+
}) ?? Promise.resolve(null);
264274
const surface = textureUrl
265275
? // If surface image fails to load, the tile should still be displayed
266276
fetch(textureUrl, {propName: 'texture', layer: this, loaders: [], signal}).catch(_ => null)
@@ -431,14 +441,14 @@ function remapMeshToWebMercatorTile(mesh: Mesh, bounds: Bounds): Mesh {
431441
}
432442

433443
const [, south, , north] = bounds;
434-
const northY = lngLatToMercatorY(north);
435-
const southY = lngLatToMercatorY(south);
444+
const northY = lngLatToMercatorWorldY(north);
445+
const southY = lngLatToMercatorWorldY(south);
436446
const remappedPositions = new Float32Array(positions);
437447

438448
for (let i = 0; i < texCoords.length / 2; i++) {
439449
const v = texCoords[i * 2 + 1];
440450
const mercatorY = northY + (southY - northY) * v;
441-
remappedPositions[i * 3 + 1] = mercatorYToLat(mercatorY);
451+
remappedPositions[i * 3 + 1] = worldToLngLat([0, mercatorY])[1];
442452
}
443453

444454
return {
@@ -453,12 +463,10 @@ function remapMeshToWebMercatorTile(mesh: Mesh, bounds: Bounds): Mesh {
453463
};
454464
}
455465

456-
function lngLatToMercatorY(latitude: number): number {
457-
const clampedLatitude = Math.max(-85.051129, Math.min(85.051129, latitude));
458-
const sin = Math.sin(clampedLatitude * DEGREES_TO_RADIANS);
459-
return 0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI);
460-
}
461-
462-
function mercatorYToLat(y: number): number {
463-
return Math.atan(Math.sinh(Math.PI * (1 - 2 * y))) * RADIANS_TO_DEGREES;
466+
function lngLatToMercatorWorldY(latitude: number): number {
467+
const clampedLatitude = Math.max(
468+
-MAX_WEB_MERCATOR_LATITUDE,
469+
Math.min(MAX_WEB_MERCATOR_LATITUDE, latitude)
470+
);
471+
return lngLatToWorld([0, clampedLatitude])[1];
464472
}

test/modules/geo-layers/terrain-layer.spec.ts

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import {TerrainLayer, TileLayer} from '@deck.gl/geo-layers';
88
import {_GlobeView as GlobeView} from '@deck.gl/core';
99
import {SimpleMeshLayer} from '@deck.gl/mesh-layers';
1010
import {TerrainLoader} from '@loaders.gl/terrain';
11+
import {
12+
MAX_LATITUDE as MAX_WEB_MERCATOR_LATITUDE,
13+
lngLatToWorld,
14+
worldToLngLat
15+
} from '@math.gl/web-mercator';
1116

1217
test('TerrainLayer', async () => {
1318
const testCases = generateLayerTests({
@@ -50,15 +55,27 @@ test('TerrainLayer', async () => {
5055
});
5156

5257
test('TerrainLayer#globe remaps WebMercator tile rows to lng/lat mesh positions', async () => {
58+
const sourcePositions = new Float32Array([0, 80, 0, 0.5, 40, 0, 1, 0, 0]);
59+
const tileSize = 512;
60+
const bbox = {west: 0, south: 0, east: 1, north: 80};
61+
const yPad = ((bbox.north - bbox.south) / tileSize) * 1;
62+
const overlappedSouth = bbox.south - yPad;
63+
const overlappedNorth = bbox.north + yPad;
64+
const expectedMiddleLatitude = worldToLngLat([
65+
0,
66+
(lngLatToMercatorWorldY(overlappedNorth) + lngLatToMercatorWorldY(overlappedSouth)) / 2
67+
])[1];
68+
5369
const sourceMesh = {
5470
attributes: {
55-
POSITION: {value: new Float32Array([0, 80, 0, 0.5, 40, 0, 1, 0, 0]), size: 3},
71+
POSITION: {value: sourcePositions, size: 3},
5672
TEXCOORD_0: {value: new Float32Array([0, 0, 0.5, 0.5, 1, 1]), size: 2}
5773
}
5874
};
5975
const layer = new TerrainLayer({
6076
id: 'terrain-globe-mercator',
6177
elevationData: 'terrain/{z}/{x}/{y}.png',
78+
tileSize,
6279
fetch: () => Promise.resolve(sourceMesh)
6380
});
6481
layer.context = {
@@ -77,15 +94,34 @@ test('TerrainLayer#globe remaps WebMercator tile rows to lng/lat mesh positions'
7794
const [mesh] = await layer.getTiledTerrainData({
7895
index: {x: 0, y: 0, z: 1},
7996
id: '0-0-1',
80-
bbox: {west: 0, south: 0, east: 1, north: 80},
97+
bbox,
8198
zoom: 1
8299
});
83-
const positions = mesh.attributes.POSITION.value;
100+
const positions = mesh!.attributes.POSITION.value;
101+
102+
expect(positions, 'remap copies the loader positions').not.toBe(sourcePositions);
103+
expect(sourcePositions[1], 'source top row is unchanged').toBe(80);
104+
expect(sourcePositions[4], 'source middle row is unchanged').toBe(40);
105+
expect(sourcePositions[7], 'source bottom row is unchanged').toBe(0);
84106

85-
expect(positions[1], 'top row latitude is preserved').toBeGreaterThan(80);
86-
expect(
87-
positions[4],
88-
'middle row uses Mercator latitude instead of linear latitude'
89-
).toBeGreaterThan(40);
90-
expect(positions[7], 'bottom row latitude is preserved').toBeLessThan(0);
107+
expect(positions[1], 'top row latitude follows the overlapped tile north').toBeCloseTo(
108+
overlappedNorth,
109+
5
110+
);
111+
expect(positions[4], 'middle row uses Mercator latitude instead of linear latitude').toBeCloseTo(
112+
expectedMiddleLatitude,
113+
5
114+
);
115+
expect(positions[7], 'bottom row latitude follows the overlapped tile south').toBeCloseTo(
116+
overlappedSouth,
117+
5
118+
);
91119
});
120+
121+
function lngLatToMercatorWorldY(latitude: number): number {
122+
const clampedLatitude = Math.max(
123+
-MAX_WEB_MERCATOR_LATITUDE,
124+
Math.min(MAX_WEB_MERCATOR_LATITUDE, latitude)
125+
);
126+
return lngLatToWorld([0, clampedLatitude])[1];
127+
}

0 commit comments

Comments
 (0)