Skip to content

Commit 2ff156c

Browse files
fix(geo-layers): TerrainLayer GlobeView support (#10250)
1 parent 0c4b7d1 commit 2ff156c

6 files changed

Lines changed: 124 additions & 34 deletions

File tree

examples/website/google-3d-tiles/app.jsx

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: MIT
33
// Copyright (c) vis.gl contributors
44

5-
import React, {useState, useMemo} from 'react';
5+
import React, {useState, useMemo, useCallback} from 'react';
66
import {scaleLinear} from 'd3-scale';
77
import {createRoot} from 'react-dom/client';
88
import {DeckGL} from '@deck.gl/react';
@@ -48,9 +48,10 @@ function getTooltip({object}) {
4848
);
4949
}
5050

51-
export default function App({data = TILESET_URL, distance = 0, opacity = 0.2}) {
51+
export default function App({data = TILESET_URL, distance = 0, opacity = 0.2, globeView = false}) {
5252
const [credits, setCredits] = useState('');
53-
const [useGlobe, setUseGlobe] = useState(false);
53+
const [viewState, setViewState] = useState(INITIAL_VIEW_STATE);
54+
const onViewStateChange = useCallback(({viewState: vs}) => setViewState(vs), []);
5455

5556
const onTraversalComplete = selectedTiles => {
5657
const uniqueCredits = new Set();
@@ -94,40 +95,26 @@ export default function App({data = TILESET_URL, distance = 0, opacity = 0.2}) {
9495

9596
const view = useMemo(
9697
() =>
97-
useGlobe
98+
globeView
9899
? new GlobeView({id: 'view', controller: true})
99100
: new MapView({
100101
id: 'view',
101102
controller: {type: TerrainController, touchRotate: true, inertia: 500}
102103
}),
103-
[useGlobe]
104+
[globeView]
104105
);
105106

106107
return (
107108
<div>
108109
<DeckGL
109-
key={useGlobe ? 'globe' : 'map'}
110+
key={globeView ? 'globe' : 'map'}
110111
style={{backgroundColor: '#061714'}}
111112
views={view}
112-
initialViewState={INITIAL_VIEW_STATE}
113+
viewState={viewState}
114+
onViewStateChange={onViewStateChange}
113115
layers={layers}
114116
getTooltip={getTooltip}
115117
/>
116-
<button
117-
onClick={() => setUseGlobe(v => !v)}
118-
style={{
119-
position: 'absolute',
120-
top: '8px',
121-
left: '8px',
122-
padding: '6px 10px',
123-
fontFamily: 'sans-serif',
124-
fontSize: '12px',
125-
border: 'none',
126-
cursor: 'pointer'
127-
}}
128-
>
129-
{useGlobe ? 'Map' : 'Globe'}
130-
</button>
131118
<div
132119
style={{position: 'absolute', left: '8px', bottom: '4px', color: 'white', fontSize: '10px'}}
133120
>

examples/website/terrain/app.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
// SPDX-License-Identifier: MIT
33
// Copyright (c) vis.gl contributors
44

5-
import React from 'react';
5+
import React, {useState, useCallback} from 'react';
66
import {createRoot} from 'react-dom/client';
77
import {DeckGL} from '@deck.gl/react';
88

99
import {TerrainLayer, TerrainLayerProps} from '@deck.gl/geo-layers';
10+
import {MapView, _GlobeView as GlobeView} from '@deck.gl/core';
1011
import type {MapViewState} from '@deck.gl/core';
1112

1213
// Set your mapbox token here
@@ -36,17 +37,22 @@ const ELEVATION_DECODER: TerrainLayerProps['elevationDecoder'] = {
3637
export default function App({
3738
texture = SURFACE_IMAGE,
3839
wireframe = false,
40+
globeView = false,
3941
initialViewState = INITIAL_VIEW_STATE
4042
}: {
4143
texture?: string;
4244
wireframe?: boolean;
45+
globeView?: boolean;
4346
initialViewState?: MapViewState;
4447
}) {
48+
const [viewState, setViewState] = useState(initialViewState);
49+
const onViewStateChange = useCallback(({viewState: vs}) => setViewState(vs), []);
50+
4551
const layer = new TerrainLayer({
4652
id: 'terrain',
4753
minZoom: 0,
48-
maxZoom: 23,
49-
strategy: 'no-overlap',
54+
maxZoom: 14,
55+
refinementStrategy: 'best-available',
5056
elevationDecoder: ELEVATION_DECODER,
5157
elevationData: TERRAIN_IMAGE,
5258
texture,
@@ -57,8 +63,11 @@ export default function App({
5763

5864
return (
5965
<DeckGL
60-
initialViewState={initialViewState}
66+
views={globeView ? new GlobeView() : new MapView()}
67+
viewState={viewState}
68+
onViewStateChange={onViewStateChange}
6169
controller={true}
70+
parameters={{cull: true}}
6271
layers={[layer]}
6372
getTooltip={info => {
6473
if (info.picked && info.coordinate && info.coordinate.length === 3) {

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

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ import type {
2929
import {Tile2DHeader, urlType, getURLFromTemplate, URLTemplate} from '../tileset-2d/index';
3030

3131
const DUMMY_DATA = [1];
32+
const TILE_OVERLAP_PIXELS = 1;
33+
const MIN_TERRAIN_MESH_MAX_ERROR = 1;
34+
const MAX_LATITUDE = 90;
35+
const MAX_LONGITUDE = 180;
3236

3337
const defaultProps: DefaultProps<TerrainLayerProps> = {
3438
...TileLayer.defaultProps,
@@ -69,6 +73,35 @@ function urlTemplateToUpdateTrigger(template: URLTemplate): string {
6973
return template || '';
7074
}
7175

76+
function getOverlappedBounds(bounds: Bounds, tileSize: number, clampLngLat: boolean): Bounds {
77+
const xPad = ((bounds[2] - bounds[0]) / tileSize) * TILE_OVERLAP_PIXELS;
78+
const yPad = ((bounds[3] - bounds[1]) / tileSize) * TILE_OVERLAP_PIXELS;
79+
const overlappedBounds: Bounds = [
80+
bounds[0] - xPad,
81+
bounds[1] - yPad,
82+
bounds[2] + xPad,
83+
bounds[3] + yPad
84+
];
85+
86+
if (!clampLngLat) {
87+
return overlappedBounds;
88+
}
89+
90+
return [
91+
Math.max(overlappedBounds[0], -MAX_LONGITUDE),
92+
Math.max(overlappedBounds[1], -MAX_LATITUDE),
93+
Math.min(overlappedBounds[2], MAX_LONGITUDE),
94+
Math.min(overlappedBounds[3], MAX_LATITUDE)
95+
];
96+
}
97+
98+
function getEffectiveMeshMaxError(meshMaxError: number): number {
99+
if (!Number.isFinite(meshMaxError) || meshMaxError <= 0) {
100+
return MIN_TERRAIN_MESH_MAX_ERROR;
101+
}
102+
return Math.max(meshMaxError, MIN_TERRAIN_MESH_MAX_ERROR);
103+
}
104+
72105
type ElevationDecoder = {rScaler: number; gScaler: number; bScaler: number; offset: number};
73106
type TerrainLoadProps = {
74107
bounds: Bounds;
@@ -79,6 +112,12 @@ type TerrainLoadProps = {
79112
};
80113

81114
type MeshAndTexture = [MeshAttributes | null, TextureSource | null];
115+
type MeshBoundingBox = [min: number[], max: number[]];
116+
type MeshWithBoundingBox = MeshAttributes & {
117+
header?: {
118+
boundingBox?: MeshBoundingBox;
119+
};
120+
};
82121

83122
/** All properties supported by TerrainLayer */
84123
export type TerrainLayerProps = _TerrainLayerProps &
@@ -169,14 +208,15 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
169208
if (!elevationData) {
170209
return null;
171210
}
211+
const effectiveMeshMaxError = getEffectiveMeshMaxError(meshMaxError);
172212
let loadOptions = this.getLoadOptions();
173213
loadOptions = {
174214
...loadOptions,
175215
terrain: {
176-
skirtHeight: this.state.isTiled ? meshMaxError * 2 : 0,
216+
skirtHeight: this.state.isTiled ? effectiveMeshMaxError * 2 : 0,
177217
...loadOptions?.terrain,
178218
bounds,
179-
meshMaxError,
219+
meshMaxError: effectiveMeshMaxError,
180220
elevationDecoder
181221
}
182222
};
@@ -203,10 +243,15 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
203243
topRight = [bbox.right, bbox.top];
204244
}
205245
const bounds: Bounds = [bottomLeft[0], bottomLeft[1], topRight[0], topRight[1]];
246+
const overlappedBounds = getOverlappedBounds(
247+
bounds,
248+
this.props.tileSize,
249+
Boolean(viewport.resolution && viewport.resolution > 0)
250+
);
206251

207252
const terrain = this.loadTerrain({
208253
elevationData: dataUrl,
209-
bounds,
254+
bounds: overlappedBounds,
210255
elevationDecoder,
211256
meshMaxError,
212257
signal
@@ -237,12 +282,27 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
237282

238283
const [mesh, texture] = data;
239284

285+
const {viewport} = this.context;
286+
// Bounds are baked with projectFlat. In GlobeView projectFlat is identity,
287+
// so tiled terrain meshes are in lng/lat degrees instead of common-space
288+
// web-mercator units.
289+
const isGlobe = Boolean(viewport.resolution && viewport.resolution > 0);
290+
const boundingBox = (mesh as MeshWithBoundingBox | null)?.header?.boundingBox;
291+
const hasLngLatBounds =
292+
boundingBox &&
293+
boundingBox.every(
294+
([x, y]) =>
295+
x >= -MAX_LONGITUDE && x <= MAX_LONGITUDE && y >= -MAX_LATITUDE && y <= MAX_LATITUDE
296+
);
297+
const coordinateSystem =
298+
isGlobe && hasLngLatBounds ? COORDINATE_SYSTEM.LNGLAT : COORDINATE_SYSTEM.CARTESIAN;
299+
240300
return new SubLayerClass(props, {
241301
data: DUMMY_DATA,
242302
mesh,
243303
texture,
244304
_instanced: false,
245-
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN,
305+
coordinateSystem,
246306
getPosition: d => [0, 0, 0],
247307
getColor: color,
248308
wireframe,
@@ -311,7 +371,8 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
311371
elevationData: urlTemplateToUpdateTrigger(elevationData),
312372
texture: urlTemplateToUpdateTrigger(texture),
313373
meshMaxError,
314-
elevationDecoder
374+
elevationDecoder,
375+
projectionMode: this.context.viewport.projectionMode
315376
}
316377
},
317378
onViewportLoad: this.onViewportLoad.bind(this),

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

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// Copyright (c) vis.gl contributors
44

55
import {test, expect} from 'vitest';
6-
import {MapView} from '@deck.gl/core';
6+
import {COORDINATE_SYSTEM, MapView, _GlobeView as GlobeView} from '@deck.gl/core';
77
import {TerrainLayer} from '@deck.gl/geo-layers';
88
import {testInitializeLayerAsync} from '@deck.gl/test-utils/vitest';
99
import {TruncatedConeGeometry} from '@luma.gl/engine';
@@ -26,6 +26,12 @@ const TEST_VIEWPORT = new MapView().makeViewport({
2626
viewState: {longitude: 0, latitude: 0, zoom: 0}
2727
});
2828

29+
const TEST_GLOBE_VIEWPORT = new GlobeView().makeViewport({
30+
width: 100,
31+
height: 100,
32+
viewState: {longitude: 0, latitude: 0, zoom: 0}
33+
});
34+
2935
function createTestMesh() {
3036
const mesh = new TruncatedConeGeometry({
3137
topRadius: 1,
@@ -125,3 +131,26 @@ test('TerrainLayer#isLoaded waits for elevation and texture in tiled mode', asyn
125131
expect(layer.isLoaded, 'tiled terrain layer is loaded after both resources resolve').toBe(true);
126132
handle?.finalize();
127133
});
134+
135+
test('TerrainLayer renders tiled Martini meshes in lng/lat coordinates on GlobeView', async () => {
136+
const layer = new TerrainLayer({
137+
id: 'terrain-tiled-globe',
138+
elevationData: 'https://example.com/elevation/{z}/{x}/{y}.png',
139+
minZoom: 0,
140+
maxZoom: 0,
141+
fetch: () => Promise.resolve(createTestMesh())
142+
});
143+
144+
const handle = await testInitializeLayerAsync({
145+
layer,
146+
viewport: TEST_GLOBE_VIEWPORT,
147+
finalize: false
148+
});
149+
150+
const tileLayer = layer.getSubLayers()[0];
151+
const meshLayer = tileLayer.getSubLayers()[0];
152+
expect(meshLayer.props.coordinateSystem, 'Globe terrain mesh uses lng/lat').toBe(
153+
COORDINATE_SYSTEM.LNGLAT
154+
);
155+
handle?.finalize();
156+
});

website/src/examples/google-3d-tiles.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class Google3dTilesDemo extends Component {
1414
static code = `${GITHUB_TREE}/examples/website/google-3d-tiles`;
1515

1616
static parameters = {
17+
globeView: {displayName: 'Globe View', type: 'checkbox', value: false},
1718
distance: {displayName: 'Distance to tree', type: 'range', value: 0, step: 1, min: 0, max: 400},
1819
opacity: {displayName: 'Opacity', type: 'range', value: 0.2, step: 0.01, min: 0, max: 0.5}
1920
};
@@ -70,8 +71,9 @@ class Google3dTilesDemo extends Component {
7071
const {params} = this.props;
7172
const distance = params.distance.value;
7273
const opacity = params.opacity.value;
74+
const globeView = params.globeView.value;
7375

74-
return <App {...this.props} distance={distance} opacity={opacity} />;
76+
return <App {...this.props} distance={distance} opacity={opacity} globeView={globeView} />;
7577
}
7678
}
7779

website/src/examples/terrain-layer.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ class TerrainDemo extends Component {
6262
static code = `${GITHUB_TREE}/examples/website/terrain`;
6363

6464
static parameters = {
65+
globeView: {displayName: 'Globe View', type: 'checkbox', value: false},
6566
location: {
6667
displayName: 'Location',
6768
type: 'select',
@@ -105,7 +106,7 @@ class TerrainDemo extends Component {
105106

106107
render() {
107108
const {params, data, ...otherProps} = this.props;
108-
const {location, surface, wireframe} = params;
109+
const {location, surface, wireframe, globeView} = params;
109110

110111
const initialViewState = LOCATIONS[location.value];
111112
initialViewState.pitch = 45;
@@ -118,6 +119,7 @@ class TerrainDemo extends Component {
118119
initialViewState={initialViewState}
119120
texture={SURFACE_IMAGES[surface.value]}
120121
wireframe={wireframe.value}
122+
globeView={globeView.value}
121123
/>
122124
</div>
123125
);

0 commit comments

Comments
 (0)