Skip to content

Commit 065f81f

Browse files
Fix terrain globe Mercator tile warping
1 parent f147137 commit 065f81f

2 files changed

Lines changed: 86 additions & 1 deletion

File tree

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

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ const TILE_OVERLAP_PIXELS = 1;
3333
const MIN_TERRAIN_MESH_MAX_ERROR = 1;
3434
const MAX_LATITUDE = 90;
3535
const MAX_LONGITUDE = 180;
36+
const DEGREES_TO_RADIANS = Math.PI / 180;
37+
const RADIANS_TO_DEGREES = 180 / Math.PI;
3638

3739
const defaultProps: DefaultProps<TerrainLayerProps> = {
3840
...TileLayer.defaultProps,
@@ -255,7 +257,9 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
255257
elevationDecoder,
256258
meshMaxError,
257259
signal
258-
});
260+
})?.then(mesh =>
261+
viewport.resolution && mesh ? remapMeshToWebMercatorTile(mesh, overlappedBounds) : mesh
262+
);
259263
const surface = textureUrl
260264
? // If surface image fails to load, the tile should still be displayed
261265
fetch(textureUrl, {propName: 'texture', layer: this, loaders: [], signal}).catch(_ => null)
@@ -417,3 +421,43 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
417421

418422
const isTileSetURL = (url: string): boolean =>
419423
url.includes('{x}') && (url.includes('{y}') || url.includes('{-y}'));
424+
425+
function remapMeshToWebMercatorTile(mesh: MeshAttributes, bounds: Bounds): MeshAttributes {
426+
const positions = mesh.attributes.POSITION?.value;
427+
const texCoords = mesh.attributes.TEXCOORD_0?.value;
428+
if (!positions || !texCoords) {
429+
return mesh;
430+
}
431+
432+
const [, south, , north] = bounds;
433+
const northY = lngLatToMercatorY(north);
434+
const southY = lngLatToMercatorY(south);
435+
const remappedPositions = new Float32Array(positions);
436+
437+
for (let i = 0; i < texCoords.length / 2; i++) {
438+
const v = texCoords[i * 2 + 1];
439+
const mercatorY = northY + (southY - northY) * v;
440+
remappedPositions[i * 3 + 1] = mercatorYToLat(mercatorY);
441+
}
442+
443+
return {
444+
...mesh,
445+
attributes: {
446+
...mesh.attributes,
447+
POSITION: {
448+
...mesh.attributes.POSITION,
449+
value: remappedPositions
450+
}
451+
}
452+
};
453+
}
454+
455+
function lngLatToMercatorY(latitude: number): number {
456+
const clampedLatitude = Math.max(-85.051129, Math.min(85.051129, latitude));
457+
const sin = Math.sin(clampedLatitude * DEGREES_TO_RADIANS);
458+
return 0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI);
459+
}
460+
461+
function mercatorYToLat(y: number): number {
462+
return Math.atan(Math.sinh(Math.PI * (1 - 2 * y))) * RADIANS_TO_DEGREES;
463+
}

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import {test, expect} from 'vitest';
66
import {generateLayerTests, testLayerAsync} from '@deck.gl/test-utils/vitest';
77
import {TerrainLayer, TileLayer} from '@deck.gl/geo-layers';
8+
import {_GlobeView as GlobeView} from '@deck.gl/core';
89
import {SimpleMeshLayer} from '@deck.gl/mesh-layers';
910
import {TerrainLoader} from '@loaders.gl/terrain';
1011

@@ -47,3 +48,43 @@ test('TerrainLayer', async () => {
4748
onError: err => expect(err).toBeFalsy()
4849
});
4950
});
51+
52+
test('TerrainLayer#globe remaps WebMercator tile rows to lng/lat mesh positions', async () => {
53+
const sourceMesh = {
54+
attributes: {
55+
POSITION: {value: new Float32Array([0, 80, 0, 0.5, 40, 0, 1, 0, 0]), size: 3},
56+
TEXCOORD_0: {value: new Float32Array([0, 0, 0.5, 0.5, 1, 1]), size: 2}
57+
}
58+
};
59+
const layer = new TerrainLayer({
60+
id: 'terrain-globe-mercator',
61+
elevationData: 'terrain/{z}/{x}/{y}.png',
62+
fetch: () => Promise.resolve(sourceMesh)
63+
});
64+
layer.context = {
65+
viewport: new GlobeView().makeViewport({
66+
width: 512,
67+
height: 512,
68+
viewState: {
69+
longitude: 0,
70+
latitude: 0,
71+
zoom: 1
72+
}
73+
})
74+
};
75+
layer.state = {isTiled: true};
76+
77+
const [mesh] = await layer.getTiledTerrainData({
78+
index: {x: 0, y: 0, z: 1},
79+
id: '0-0-1',
80+
bbox: {west: 0, south: 0, east: 1, north: 80},
81+
zoom: 1
82+
});
83+
const positions = mesh.attributes.POSITION.value;
84+
85+
expect(positions[1], 'top row latitude is preserved').toBeGreaterThan(80);
86+
expect(positions[4], 'middle row uses Mercator latitude instead of linear latitude').toBeGreaterThan(
87+
40
88+
);
89+
expect(positions[7], 'bottom row latitude is preserved').toBeLessThan(0);
90+
});

0 commit comments

Comments
 (0)