Skip to content

Commit 379d83d

Browse files
asfordchrisgervang
authored andcommitted
fix(geo-layers): Pass zoomOffset through TerrainLayer to child TileLayer. (#10382)
1 parent fc68fa4 commit 379d83d

6 files changed

Lines changed: 218 additions & 18 deletions

File tree

examples/website/map-tile/app.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export default function App({
5757
maxZoom = 7,
5858
visibleMinZoom,
5959
visibleMaxZoom = 7,
60+
zoomOffset = 0,
6061
useExtent = false
6162
}: {
6263
showBorder?: boolean;
@@ -66,6 +67,7 @@ export default function App({
6667
maxZoom?: number;
6768
visibleMinZoom?: number;
6869
visibleMaxZoom?: number;
70+
zoomOffset?: number;
6971
useExtent?: boolean;
7072
}) {
7173
const [zoom, setZoom] = useState(INITIAL_VIEW_STATE.zoom);
@@ -95,6 +97,7 @@ export default function App({
9597
tileSize: 512,
9698
visibleMinZoom,
9799
visibleMaxZoom,
100+
zoomOffset,
98101
extent: useExtent ? FRANCE_EXTENT : undefined,
99102
renderSubLayers: props => {
100103
const [[west, south], [east, north]] = props.tile.boundingBox;

examples/website/terrain/app.tsx

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

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

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

1213
// Set your mapbox token here
1314
const MAPBOX_TOKEN = process.env.MapboxAccessToken; // eslint-disable-line
@@ -36,29 +37,58 @@ const ELEVATION_DECODER: TerrainLayerProps['elevationDecoder'] = {
3637
export default function App({
3738
texture = SURFACE_IMAGE,
3839
wireframe = false,
39-
initialViewState = INITIAL_VIEW_STATE
40+
globeView = false,
41+
zoomOffset = 0,
42+
minZoom = 0,
43+
maxZoom = 14,
44+
visibleMinZoom = 0,
45+
visibleMaxZoom = 14,
46+
initialViewState = INITIAL_VIEW_STATE,
47+
onZoomChange
4048
}: {
4149
texture?: string;
4250
wireframe?: boolean;
51+
globeView?: boolean;
52+
zoomOffset?: number;
53+
minZoom?: number;
54+
maxZoom?: number;
55+
visibleMinZoom?: number;
56+
visibleMaxZoom?: number;
4357
initialViewState?: MapViewState;
58+
onZoomChange?: (zoom: number) => void;
4459
}) {
60+
const [viewState, setViewState] = useState(initialViewState);
61+
const onViewStateChange = useCallback(
62+
({viewState: vs}) => {
63+
setViewState(vs);
64+
onZoomChange?.(vs.zoom);
65+
},
66+
[onZoomChange]
67+
);
68+
4569
const layer = new TerrainLayer({
4670
id: 'terrain',
47-
minZoom: 0,
48-
maxZoom: 23,
49-
strategy: 'no-overlap',
71+
minZoom,
72+
maxZoom,
73+
visibleMinZoom,
74+
visibleMaxZoom,
75+
refinementStrategy: 'best-available',
5076
elevationDecoder: ELEVATION_DECODER,
5177
elevationData: TERRAIN_IMAGE,
5278
texture,
5379
wireframe,
80+
zoomOffset,
5481
color: [255, 255, 255],
5582
pickable: '3d'
5683
});
5784

5885
return (
5986
<DeckGL
60-
initialViewState={initialViewState}
87+
views={globeView ? new GlobeView() : new MapView()}
88+
viewState={viewState}
89+
onViewStateChange={onViewStateChange}
6190
controller={true}
91+
parameters={{cull: true}}
6292
layers={[layer]}
6393
getTooltip={info => {
6494
if (info.picked && info.coordinate && info.coordinate.length === 3) {

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import {
66
Color,
77
CompositeLayer,
88
CompositeLayerProps,
9+
COORDINATE_SYSTEM,
910
DefaultProps,
11+
_GlobeViewport as GlobeViewport,
1012
Layer,
1113
LayersList,
1214
log,
@@ -15,7 +17,6 @@ import {
1517
UpdateParameters
1618
} from '@deck.gl/core';
1719
import {SimpleMeshLayer} from '@deck.gl/mesh-layers';
18-
import {COORDINATE_SYSTEM} from '@deck.gl/core';
1920
import type {MeshAttributes} from '@loaders.gl/schema';
2021
import {TerrainWorkerLoader} from '@loaders.gl/terrain';
2122
import TileLayer, {TileLayerProps} from '../tile-layer/tile-layer';
@@ -26,7 +27,7 @@ import type {
2627
TileLoadProps,
2728
ZRange
2829
} from '../tileset-2d/index';
29-
import {Tile2DHeader, urlType, getURLFromTemplate, URLTemplate} from '../tileset-2d/index';
30+
import {getURLFromTemplate, Tile2DHeader, URLTemplate, urlType} from '../tileset-2d/index';
3031

3132
const DUMMY_DATA = [1];
3233

@@ -295,7 +296,8 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
295296
onTileError,
296297
maxCacheSize,
297298
maxCacheByteSize,
298-
refinementStrategy
299+
refinementStrategy,
300+
zoomOffset
299301
} = this.props;
300302

301303
if (this.state.isTiled) {
@@ -311,7 +313,9 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
311313
elevationData: urlTemplateToUpdateTrigger(elevationData),
312314
texture: urlTemplateToUpdateTrigger(texture),
313315
meshMaxError,
314-
elevationDecoder
316+
elevationDecoder,
317+
projectionMode: this.context.viewport.projectionMode,
318+
zoomOffset
315319
}
316320
},
317321
onViewportLoad: this.onViewportLoad.bind(this),
@@ -326,7 +330,8 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
326330
onTileError,
327331
maxCacheSize,
328332
maxCacheByteSize,
329-
refinementStrategy
333+
refinementStrategy,
334+
zoomOffset
330335
}
331336
);
332337
}

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

Lines changed: 90 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, _GlobeView as GlobeView, MapView} 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,86 @@ 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 fetches tiles at correct zoom levels for different zoomOffset values', async () => {
136+
// Create a viewport at zoom 3 to test various zoomOffset values
137+
const viewportZoom3 = new MapView().makeViewport({
138+
width: 100,
139+
height: 100,
140+
viewState: {longitude: 0, latitude: 0, zoom: 3}
141+
});
142+
143+
const testCases = [
144+
{zoomOffset: -1, expectedZ: 2, description: 'zoomOffset: -1 (lower detail)'},
145+
{zoomOffset: 0, expectedZ: 3, description: 'zoomOffset: 0 (normal)'},
146+
{zoomOffset: 1, expectedZ: 4, description: 'zoomOffset: +1 (higher detail)'}
147+
];
148+
149+
for (const testCase of testCases) {
150+
const fetchedUrls: {elevation: string[]; texture: string[]} = {
151+
elevation: [],
152+
texture: []
153+
};
154+
155+
const layer = new TerrainLayer({
156+
id: `terrain-zoom-offset-${testCase.zoomOffset}`,
157+
elevationData: 'https://example.com/elevation/{z}/{x}/{y}.png',
158+
texture: 'https://example.com/texture/{z}/{x}/{y}.png',
159+
zoomOffset: testCase.zoomOffset,
160+
minZoom: 0,
161+
maxZoom: 6,
162+
fetch: (url, {propName}) => {
163+
if (propName === 'elevationData') {
164+
fetchedUrls.elevation.push(url);
165+
} else if (propName === 'texture') {
166+
fetchedUrls.texture.push(url);
167+
}
168+
return Promise.resolve(createTestMesh());
169+
}
170+
});
171+
172+
const handle = await testInitializeLayerAsync({
173+
layer,
174+
viewport: viewportZoom3,
175+
finalize: false
176+
});
177+
178+
await sleep();
179+
180+
// Verify tiles are fetched at the expected zoom level for both elevation and texture
181+
const expectedZoomPath = `/${testCase.expectedZ}/`;
182+
expect(
183+
fetchedUrls.elevation.some(url => url.includes(expectedZoomPath)),
184+
`${testCase.description}: elevation tiles fetched at z=${testCase.expectedZ}`
185+
).toBe(true);
186+
expect(
187+
fetchedUrls.texture.some(url => url.includes(expectedZoomPath)),
188+
`${testCase.description}: texture tiles fetched at z=${testCase.expectedZ}`
189+
).toBe(true);
190+
191+
handle?.finalize();
192+
}
193+
});
194+
195+
test('TerrainLayer renders tiled Martini meshes in lng/lat coordinates on GlobeView', async () => {
196+
const layer = new TerrainLayer({
197+
id: 'terrain-tiled-globe',
198+
elevationData: 'https://example.com/elevation/{z}/{x}/{y}.png',
199+
minZoom: 0,
200+
maxZoom: 0,
201+
fetch: () => Promise.resolve(createTestMesh())
202+
});
203+
204+
const handle = await testInitializeLayerAsync({
205+
layer,
206+
viewport: TEST_GLOBE_VIEWPORT,
207+
finalize: false
208+
});
209+
210+
const tileLayer = layer.getSubLayers()[0];
211+
const meshLayer = tileLayer.getSubLayers()[0];
212+
expect(meshLayer.props.coordinateSystem, 'Globe terrain mesh uses lng/lat').toBe(
213+
COORDINATE_SYSTEM.LNGLAT
214+
);
215+
handle?.finalize();
216+
});

website/src/examples/terrain-layer.js

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

5-
import React, {Component} from 'react';
6-
import {MAPBOX_STYLES, GITHUB_TREE} from '../constants/defaults';
5+
import {Component} from 'react';
76
import App from 'website-examples/terrain/app';
7+
import {GITHUB_TREE, MAPBOX_STYLES} from '../constants/defaults';
88

99
import {makeExample} from '../components';
1010

@@ -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',
@@ -74,7 +75,51 @@ class TerrainDemo extends Component {
7475
options: Object.keys(SURFACE_IMAGES),
7576
value: 'Satellite'
7677
},
77-
wireframe: {displayName: 'Wireframe', type: 'checkbox', value: false}
78+
wireframe: {displayName: 'Wireframe', type: 'checkbox', value: false},
79+
minZoom: {
80+
displayName: 'Min Zoom',
81+
type: 'range',
82+
value: 0,
83+
step: 1,
84+
min: 0,
85+
max: 19,
86+
accentColor: '#0275ff'
87+
},
88+
maxZoom: {
89+
displayName: 'Max Zoom',
90+
type: 'range',
91+
value: 14,
92+
step: 1,
93+
min: 0,
94+
max: 19,
95+
accentColor: '#0275ff'
96+
},
97+
visibleMinZoom: {
98+
displayName: 'Visible Min Zoom',
99+
type: 'range',
100+
value: 0,
101+
step: 1,
102+
min: 0,
103+
max: 19,
104+
accentColor: '#1a2b4a'
105+
},
106+
visibleMaxZoom: {
107+
displayName: 'Visible Max Zoom',
108+
type: 'range',
109+
value: 19,
110+
step: 1,
111+
min: 0,
112+
max: 19,
113+
accentColor: '#1a2b4a'
114+
},
115+
zoomOffset: {
116+
displayName: 'Zoom Offset',
117+
type: 'range',
118+
value: 0,
119+
min: -2,
120+
max: 2,
121+
step: 1
122+
}
78123
};
79124

80125
static mapStyle = MAPBOX_STYLES.BLANK;
@@ -99,13 +144,32 @@ class TerrainDemo extends Component {
99144
<a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>
100145
</div>
101146
</p>
147+
<div className="layout">
148+
<div className="stat col-1-2">
149+
Viewport Zoom<b>{meta.zoom != null ? meta.zoom.toFixed(1) : '-'}</b>
150+
</div>
151+
</div>
102152
</div>
103153
);
104154
}
105155

156+
_onZoomChange = zoom => {
157+
this.props.onStateChange({zoom});
158+
};
159+
106160
render() {
107161
const {params, data, ...otherProps} = this.props;
108-
const {location, surface, wireframe} = params;
162+
const {
163+
location,
164+
surface,
165+
wireframe,
166+
globeView,
167+
minZoom,
168+
maxZoom,
169+
visibleMinZoom,
170+
visibleMaxZoom,
171+
zoomOffset
172+
} = params;
109173

110174
const initialViewState = LOCATIONS[location.value];
111175
initialViewState.pitch = 45;
@@ -118,6 +182,13 @@ class TerrainDemo extends Component {
118182
initialViewState={initialViewState}
119183
texture={SURFACE_IMAGES[surface.value]}
120184
wireframe={wireframe.value}
185+
globeView={globeView.value}
186+
minZoom={minZoom.value}
187+
maxZoom={maxZoom.value}
188+
visibleMinZoom={visibleMinZoom.value}
189+
visibleMaxZoom={visibleMaxZoom.value}
190+
zoomOffset={zoomOffset.value}
191+
onZoomChange={this._onZoomChange}
121192
/>
122193
</div>
123194
);

0 commit comments

Comments
 (0)