Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 9 additions & 22 deletions examples/website/google-3d-tiles/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import React, {useState, useMemo} from 'react';
import React, {useState, useMemo, useCallback} from 'react';
import {scaleLinear} from 'd3-scale';
import {createRoot} from 'react-dom/client';
import {DeckGL} from '@deck.gl/react';
Expand Down Expand Up @@ -48,9 +48,10 @@ function getTooltip({object}) {
);
}

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

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

const view = useMemo(
() =>
useGlobe
globeView
? new GlobeView({id: 'view', controller: true})
: new MapView({
id: 'view',
controller: {type: TerrainController, touchRotate: true, inertia: 500}
}),
[useGlobe]
[globeView]
);

return (
<div>
<DeckGL
key={useGlobe ? 'globe' : 'map'}
key={globeView ? 'globe' : 'map'}
style={{backgroundColor: '#061714'}}
views={view}
initialViewState={INITIAL_VIEW_STATE}
viewState={viewState}
onViewStateChange={onViewStateChange}
layers={layers}
getTooltip={getTooltip}
/>
<button
onClick={() => setUseGlobe(v => !v)}
style={{
position: 'absolute',
top: '8px',
left: '8px',
padding: '6px 10px',
fontFamily: 'sans-serif',
fontSize: '12px',
border: 'none',
cursor: 'pointer'
}}
>
{useGlobe ? 'Map' : 'Globe'}
</button>
<div
style={{position: 'absolute', left: '8px', bottom: '4px', color: 'white', fontSize: '10px'}}
>
Expand Down
17 changes: 13 additions & 4 deletions examples/website/terrain/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

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

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

// Set your mapbox token here
Expand Down Expand Up @@ -36,17 +37,22 @@ const ELEVATION_DECODER: TerrainLayerProps['elevationDecoder'] = {
export default function App({
texture = SURFACE_IMAGE,
wireframe = false,
globeView = false,
initialViewState = INITIAL_VIEW_STATE
}: {
texture?: string;
wireframe?: boolean;
globeView?: boolean;
initialViewState?: MapViewState;
}) {
const [viewState, setViewState] = useState(initialViewState);
const onViewStateChange = useCallback(({viewState: vs}) => setViewState(vs), []);

const layer = new TerrainLayer({
id: 'terrain',
minZoom: 0,
maxZoom: 23,
strategy: 'no-overlap',
maxZoom: 14,
refinementStrategy: 'best-available',
elevationDecoder: ELEVATION_DECODER,
elevationData: TERRAIN_IMAGE,
texture,
Expand All @@ -57,8 +63,11 @@ export default function App({

return (
<DeckGL
initialViewState={initialViewState}
views={globeView ? new GlobeView() : new MapView()}
viewState={viewState}
onViewStateChange={onViewStateChange}
controller={true}
parameters={{cull: true}}
layers={[layer]}
getTooltip={info => {
if (info.picked && info.coordinate && info.coordinate.length === 3) {
Expand Down
71 changes: 66 additions & 5 deletions modules/geo-layers/src/terrain-layer/terrain-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import type {
import {Tile2DHeader, urlType, getURLFromTemplate, URLTemplate} from '../tileset-2d/index';

const DUMMY_DATA = [1];
const TILE_OVERLAP_PIXELS = 1;
const MIN_TERRAIN_MESH_MAX_ERROR = 1;
const MAX_LATITUDE = 90;
const MAX_LONGITUDE = 180;

const defaultProps: DefaultProps<TerrainLayerProps> = {
...TileLayer.defaultProps,
Expand Down Expand Up @@ -69,6 +73,35 @@ function urlTemplateToUpdateTrigger(template: URLTemplate): string {
return template || '';
}

function getOverlappedBounds(bounds: Bounds, tileSize: number, clampLngLat: boolean): Bounds {
const xPad = ((bounds[2] - bounds[0]) / tileSize) * TILE_OVERLAP_PIXELS;
const yPad = ((bounds[3] - bounds[1]) / tileSize) * TILE_OVERLAP_PIXELS;
const overlappedBounds: Bounds = [
bounds[0] - xPad,
bounds[1] - yPad,
bounds[2] + xPad,
bounds[3] + yPad
];

if (!clampLngLat) {
return overlappedBounds;
}

return [
Math.max(overlappedBounds[0], -MAX_LONGITUDE),
Math.max(overlappedBounds[1], -MAX_LATITUDE),
Math.min(overlappedBounds[2], MAX_LONGITUDE),
Math.min(overlappedBounds[3], MAX_LATITUDE)
];
}

function getEffectiveMeshMaxError(meshMaxError: number): number {
if (!Number.isFinite(meshMaxError) || meshMaxError <= 0) {
return MIN_TERRAIN_MESH_MAX_ERROR;
}
return Math.max(meshMaxError, MIN_TERRAIN_MESH_MAX_ERROR);
}

type ElevationDecoder = {rScaler: number; gScaler: number; bScaler: number; offset: number};
type TerrainLoadProps = {
bounds: Bounds;
Expand All @@ -79,6 +112,12 @@ type TerrainLoadProps = {
};

type MeshAndTexture = [MeshAttributes | null, TextureSource | null];
type MeshBoundingBox = [min: number[], max: number[]];
type MeshWithBoundingBox = MeshAttributes & {
header?: {
boundingBox?: MeshBoundingBox;
};
};

/** All properties supported by TerrainLayer */
export type TerrainLayerProps = _TerrainLayerProps &
Expand Down Expand Up @@ -169,14 +208,15 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
if (!elevationData) {
return null;
}
const effectiveMeshMaxError = getEffectiveMeshMaxError(meshMaxError);
let loadOptions = this.getLoadOptions();
loadOptions = {
...loadOptions,
terrain: {
skirtHeight: this.state.isTiled ? meshMaxError * 2 : 0,
skirtHeight: this.state.isTiled ? effectiveMeshMaxError * 2 : 0,
...loadOptions?.terrain,
bounds,
meshMaxError,
meshMaxError: effectiveMeshMaxError,
elevationDecoder
}
};
Expand All @@ -203,10 +243,15 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
topRight = [bbox.right, bbox.top];
}
const bounds: Bounds = [bottomLeft[0], bottomLeft[1], topRight[0], topRight[1]];
const overlappedBounds = getOverlappedBounds(
bounds,
this.props.tileSize,
Boolean(viewport.resolution && viewport.resolution > 0)
);

const terrain = this.loadTerrain({
elevationData: dataUrl,
bounds,
bounds: overlappedBounds,
elevationDecoder,
meshMaxError,
signal
Expand Down Expand Up @@ -237,12 +282,27 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite

const [mesh, texture] = data;

const {viewport} = this.context;
// Bounds are baked with projectFlat. In GlobeView projectFlat is identity,
// so tiled terrain meshes are in lng/lat degrees instead of common-space
// web-mercator units.
const isGlobe = Boolean(viewport.resolution && viewport.resolution > 0);
const boundingBox = (mesh as MeshWithBoundingBox | null)?.header?.boundingBox;
const hasLngLatBounds =
boundingBox &&
boundingBox.every(
([x, y]) =>
x >= -MAX_LONGITUDE && x <= MAX_LONGITUDE && y >= -MAX_LATITUDE && y <= MAX_LATITUDE
);
const coordinateSystem =
isGlobe && hasLngLatBounds ? COORDINATE_SYSTEM.LNGLAT : COORDINATE_SYSTEM.CARTESIAN;

return new SubLayerClass(props, {
data: DUMMY_DATA,
mesh,
texture,
_instanced: false,
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN,
coordinateSystem,
getPosition: d => [0, 0, 0],
getColor: color,
wireframe,
Expand Down Expand Up @@ -311,7 +371,8 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
elevationData: urlTemplateToUpdateTrigger(elevationData),
texture: urlTemplateToUpdateTrigger(texture),
meshMaxError,
elevationDecoder
elevationDecoder,
projectionMode: this.context.viewport.projectionMode
}
},
onViewportLoad: this.onViewportLoad.bind(this),
Expand Down
31 changes: 30 additions & 1 deletion test/modules/geo-layers/terrain-layer-loading.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// Copyright (c) vis.gl contributors

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

const TEST_GLOBE_VIEWPORT = new GlobeView().makeViewport({
width: 100,
height: 100,
viewState: {longitude: 0, latitude: 0, zoom: 0}
});

function createTestMesh() {
const mesh = new TruncatedConeGeometry({
topRadius: 1,
Expand Down Expand Up @@ -125,3 +131,26 @@ test('TerrainLayer#isLoaded waits for elevation and texture in tiled mode', asyn
expect(layer.isLoaded, 'tiled terrain layer is loaded after both resources resolve').toBe(true);
handle?.finalize();
});

test('TerrainLayer renders tiled Martini meshes in lng/lat coordinates on GlobeView', async () => {
const layer = new TerrainLayer({
id: 'terrain-tiled-globe',
elevationData: 'https://example.com/elevation/{z}/{x}/{y}.png',
minZoom: 0,
maxZoom: 0,
fetch: () => Promise.resolve(createTestMesh())
});

const handle = await testInitializeLayerAsync({
layer,
viewport: TEST_GLOBE_VIEWPORT,
finalize: false
});

const tileLayer = layer.getSubLayers()[0];
const meshLayer = tileLayer.getSubLayers()[0];
expect(meshLayer.props.coordinateSystem, 'Globe terrain mesh uses lng/lat').toBe(
COORDINATE_SYSTEM.LNGLAT
);
handle?.finalize();
});
4 changes: 3 additions & 1 deletion website/src/examples/google-3d-tiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class Google3dTilesDemo extends Component {
static code = `${GITHUB_TREE}/examples/website/google-3d-tiles`;

static parameters = {
globeView: {displayName: 'Globe View', type: 'checkbox', value: false},
distance: {displayName: 'Distance to tree', type: 'range', value: 0, step: 1, min: 0, max: 400},
opacity: {displayName: 'Opacity', type: 'range', value: 0.2, step: 0.01, min: 0, max: 0.5}
};
Expand Down Expand Up @@ -70,8 +71,9 @@ class Google3dTilesDemo extends Component {
const {params} = this.props;
const distance = params.distance.value;
const opacity = params.opacity.value;
const globeView = params.globeView.value;

return <App {...this.props} distance={distance} opacity={opacity} />;
return <App {...this.props} distance={distance} opacity={opacity} globeView={globeView} />;
}
}

Expand Down
4 changes: 3 additions & 1 deletion website/src/examples/terrain-layer.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class TerrainDemo extends Component {
static code = `${GITHUB_TREE}/examples/website/terrain`;

static parameters = {
globeView: {displayName: 'Globe View', type: 'checkbox', value: false},
location: {
displayName: 'Location',
type: 'select',
Expand Down Expand Up @@ -105,7 +106,7 @@ class TerrainDemo extends Component {

render() {
const {params, data, ...otherProps} = this.props;
const {location, surface, wireframe} = params;
const {location, surface, wireframe, globeView} = params;

const initialViewState = LOCATIONS[location.value];
initialViewState.pitch = 45;
Expand All @@ -118,6 +119,7 @@ class TerrainDemo extends Component {
initialViewState={initialViewState}
texture={SURFACE_IMAGES[surface.value]}
wireframe={wireframe.value}
globeView={globeView.value}
/>
</div>
);
Expand Down
Loading