Skip to content
Closed
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
79 changes: 62 additions & 17 deletions modules/geo-layers/src/terrain-layer/terrain-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from '@deck.gl/core';
import {SimpleMeshLayer} from '@deck.gl/mesh-layers';
import {COORDINATE_SYSTEM} from '@deck.gl/core';
import type {MeshAttributes} from '@loaders.gl/schema';
import type {Mesh} from '@loaders.gl/schema';
import {TerrainWorkerLoader} from '@loaders.gl/terrain';
import TileLayer, {TileLayerProps} from '../tile-layer/tile-layer';
import type {
Expand All @@ -33,6 +33,8 @@ const TILE_OVERLAP_PIXELS = 1;
const MIN_TERRAIN_MESH_MAX_ERROR = 1;
const MAX_LATITUDE = 90;
const MAX_LONGITUDE = 180;
const DEGREES_TO_RADIANS = Math.PI / 180;
const RADIANS_TO_DEGREES = 180 / Math.PI;

const defaultProps: DefaultProps<TerrainLayerProps> = {
...TileLayer.defaultProps,
Expand Down Expand Up @@ -111,9 +113,9 @@ type TerrainLoadProps = {
signal?: AbortSignal;
};

type MeshAndTexture = [MeshAttributes | null, TextureSource | null];
type MeshAndTexture = [Mesh | null, TextureSource | null];
type MeshBoundingBox = [min: number[], max: number[]];
type MeshWithBoundingBox = MeshAttributes & {
type MeshWithBoundingBox = Mesh & {
header?: {
boundingBox?: MeshBoundingBox;
};
Expand Down Expand Up @@ -165,7 +167,7 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite

state!: {
isTiled?: boolean;
terrain?: MeshAttributes;
terrain?: Mesh;
zRange?: ZRange | null;
};

Expand Down Expand Up @@ -204,7 +206,7 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
elevationDecoder,
meshMaxError,
signal
}: TerrainLoadProps): Promise<MeshAttributes> | null {
}: TerrainLoadProps): Promise<Mesh> | null {
if (!elevationData) {
return null;
}
Expand Down Expand Up @@ -249,13 +251,16 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
Boolean(viewport.resolution && viewport.resolution > 0)
);

const terrain = this.loadTerrain({
elevationData: dataUrl,
bounds: overlappedBounds,
elevationDecoder,
meshMaxError,
signal
});
const terrain =
this.loadTerrain({
elevationData: dataUrl,
bounds: overlappedBounds,
elevationDecoder,
meshMaxError,
signal
})?.then(mesh =>
viewport.resolution && mesh ? remapMeshToWebMercatorTile(mesh, overlappedBounds) : mesh
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure how much this costs - seems heavy.

Easier to handle directly in loadTerrain? Gotta check

) ?? Promise.resolve(null);
const surface = textureUrl
? // If surface image fails to load, the tile should still be displayed
fetch(textureUrl, {propName: 'texture', layer: this, loaders: [], signal}).catch(_ => null)
Expand Down Expand Up @@ -319,11 +324,9 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
const {zRange} = this.state;
const ranges = tiles
.map(tile => tile.content)
.filter(Boolean)
.map(arr => {
// @ts-ignore
const bounds = arr[0].header.boundingBox;
return bounds.map(bound => bound[2]);
.flatMap(arr => {
const bounds = arr?.[0]?.header?.boundingBox;
return bounds ? [bounds.map(bound => bound[2])] : [];
});
if (ranges.length === 0) {
return;
Expand Down Expand Up @@ -417,3 +420,45 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite

const isTileSetURL = (url: string): boolean =>
url.includes('{x}') && (url.includes('{y}') || url.includes('{-y}'));

function remapMeshToWebMercatorTile(mesh: Mesh, bounds: Bounds): Mesh {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

math.gl?

const positionAttribute = mesh.attributes.POSITION;
const texCoordAttribute = mesh.attributes.TEXCOORD_0;
const positions = positionAttribute?.value;
const texCoords = texCoordAttribute?.value;
if (!positions || !texCoords) {
return mesh;
}

const [, south, , north] = bounds;
const northY = lngLatToMercatorY(north);
const southY = lngLatToMercatorY(south);
const remappedPositions = new Float32Array(positions);

for (let i = 0; i < texCoords.length / 2; i++) {
const v = texCoords[i * 2 + 1];
const mercatorY = northY + (southY - northY) * v;
remappedPositions[i * 3 + 1] = mercatorYToLat(mercatorY);
}

return {
...mesh,
attributes: {
...mesh.attributes,
POSITION: {
...positionAttribute,
value: remappedPositions
}
}
};
}

function lngLatToMercatorY(latitude: number): number {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

math.gl?

const clampedLatitude = Math.max(-85.051129, Math.min(85.051129, latitude));
const sin = Math.sin(clampedLatitude * DEGREES_TO_RADIANS);
return 0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI);
}

function mercatorYToLat(y: number): number {
return Math.atan(Math.sinh(Math.PI * (1 - 2 * y))) * RADIANS_TO_DEGREES;
}
29 changes: 25 additions & 4 deletions modules/geo-layers/src/tile-layer/tile-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
GetPickingInfoParams,
DefaultProps,
FilterContext,
COORDINATE_SYSTEM,
_flatten as flatten
} from '@deck.gl/core';
import {GeoJsonLayer} from '@deck.gl/layers';
Expand Down Expand Up @@ -55,6 +56,8 @@ const defaultProps: DefaultProps<TileLayerProps> = {
visibleMaxZoom: null
};

const BITMAP_LAYER_NAME = 'BitmapLayer';

/** All props supported by the TileLayer */
export type TileLayerProps<DataT = unknown> = CompositeLayerProps & _TileLayerProps<DataT>;

Expand Down Expand Up @@ -421,12 +424,14 @@ export default class TileLayer<DataT = any, ExtraPropsT extends {} = {}> extends
_offset: 0,
tile
});
tile.layers = (flatten(layers, Boolean) as Layer<{tile?: Tile2DHeader}>[]).map(layer =>
layer.clone({
tile.layers = (flatten(layers, Boolean) as Layer<{tile?: Tile2DHeader}>[]).map(layer => {
const globeBitmapProps = this._getGlobeBitmapLayerProps(layer);
return layer.clone({
tile,
...globeBitmapProps,
...subLayerProps
})
);
});
});
} else if (
subLayerProps &&
tile.layers[0] &&
Expand All @@ -440,6 +445,22 @@ export default class TileLayer<DataT = any, ExtraPropsT extends {} = {}> extends
});
}

private _getGlobeBitmapLayerProps(layer: Layer): Record<string, unknown> | null {
if (
!this.context.viewport.resolution ||
(layer.constructor as typeof Layer).layerName !== BITMAP_LAYER_NAME ||
(layer.props as Record<string, unknown>)._imageCoordinateSystem !== 'default'
) {
return null;
}

return {
// XYZ/slippy tile imagery is Web Mercator encoded. In GlobeView, BitmapLayer
// positions the mesh in lng/lat, so the image needs Mercator-to-lnglat UV conversion.
_imageCoordinateSystem: COORDINATE_SYSTEM.CARTESIAN
};
}

filterSubLayer({layer, cullRect}: FilterContext) {
const {tile} = (layer as Layer<{tile: Tile2DHeader}>).props;
const {modelMatrix} = this.props;
Expand Down
42 changes: 42 additions & 0 deletions test/modules/geo-layers/terrain-layer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import {test, expect} from 'vitest';
import {generateLayerTests, testLayerAsync} from '@deck.gl/test-utils/vitest';
import {TerrainLayer, TileLayer} from '@deck.gl/geo-layers';
import {_GlobeView as GlobeView} from '@deck.gl/core';
import {SimpleMeshLayer} from '@deck.gl/mesh-layers';
import {TerrainLoader} from '@loaders.gl/terrain';

Expand Down Expand Up @@ -47,3 +48,44 @@ test('TerrainLayer', async () => {
onError: err => expect(err).toBeFalsy()
});
});

test('TerrainLayer#globe remaps WebMercator tile rows to lng/lat mesh positions', async () => {
const sourceMesh = {
attributes: {
POSITION: {value: new Float32Array([0, 80, 0, 0.5, 40, 0, 1, 0, 0]), size: 3},
TEXCOORD_0: {value: new Float32Array([0, 0, 0.5, 0.5, 1, 1]), size: 2}
}
};
const layer = new TerrainLayer({
id: 'terrain-globe-mercator',
elevationData: 'terrain/{z}/{x}/{y}.png',
fetch: () => Promise.resolve(sourceMesh)
});
layer.context = {
viewport: new GlobeView().makeViewport({
width: 512,
height: 512,
viewState: {
longitude: 0,
latitude: 0,
zoom: 1
}
})
};
layer.state = {isTiled: true};

const [mesh] = await layer.getTiledTerrainData({
index: {x: 0, y: 0, z: 1},
id: '0-0-1',
bbox: {west: 0, south: 0, east: 1, north: 80},
zoom: 1
});
const positions = mesh.attributes.POSITION.value;

expect(positions[1], 'top row latitude is preserved').toBeGreaterThan(80);
expect(
positions[4],
'middle row uses Mercator latitude instead of linear latitude'
).toBeGreaterThan(40);
expect(positions[7], 'bottom row latitude is preserved').toBeLessThan(0);
});
87 changes: 85 additions & 2 deletions test/modules/geo-layers/tile-layer/tile-layer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
// Copyright (c) vis.gl contributors

import {test, expect} from 'vitest';
import {WebMercatorViewport} from '@deck.gl/core';
import {ScatterplotLayer} from '@deck.gl/layers';
import {COORDINATE_SYSTEM, WebMercatorViewport, _GlobeView as GlobeView} from '@deck.gl/core';
import {BitmapLayer, ScatterplotLayer} from '@deck.gl/layers';
import {generateLayerTests, testLayerAsync, testLayer} from '@deck.gl/test-utils/vitest';
import {TileLayer} from '@deck.gl/geo-layers';

Expand Down Expand Up @@ -208,6 +208,89 @@ test('TileLayer#MapView:repeat', async () => {
});
});

test('TileLayer#GlobeView:BitmapLayer image coordinate system', async () => {
const testViewport = new GlobeView().makeViewport({
width: 100,
height: 100,
viewState: {
longitude: 0,
latitude: 0,
zoom: 2
}
});

const renderSubLayers = props => {
const {west, south, east, north} = props.tile.bbox;
return new BitmapLayer(props, {
id: `${props.id}-bitmap`,
image: '/test/data/icon-atlas.png',
bounds: [west, south, east, north]
});
};

await testLayerAsync({
Layer: TileLayer,
viewport: testViewport,
testCases: [
{
title: 'defaults BitmapLayer image coordinates to Web Mercator',
props: {
getTileData: () => ({}),
renderSubLayers
},
onAfterUpdate: ({layer, subLayers}) => {
if (layer.isLoaded) {
expect(subLayers[0].props._imageCoordinateSystem).toBe(COORDINATE_SYSTEM.CARTESIAN);
}
}
}
],
onError: err => expect(err).toBeFalsy()
});
});

test('TileLayer#GlobeView:preserves explicit BitmapLayer image coordinate system', async () => {
const testViewport = new GlobeView().makeViewport({
width: 100,
height: 100,
viewState: {
longitude: 0,
latitude: 0,
zoom: 2
}
});

const renderSubLayersWithExplicitImageCoordinateSystem = props => {
const {west, south, east, north} = props.tile.bbox;
return new BitmapLayer(props, {
id: `${props.id}-bitmap`,
image: '/test/data/icon-atlas.png',
bounds: [west, south, east, north],
_imageCoordinateSystem: COORDINATE_SYSTEM.LNGLAT
});
};

await testLayerAsync({
Layer: TileLayer,
viewport: testViewport,
testCases: [
{
title: 'preserves explicit BitmapLayer image coordinate system',
props: {
getTileData: () => ({}),
renderSubLayers: renderSubLayersWithExplicitImageCoordinateSystem
},
onAfterUpdate: ({layer, subLayers}) => {
if (layer.isLoaded) {
expect(subLayers[0].props._imageCoordinateSystem).toBe(COORDINATE_SYSTEM.LNGLAT);
}
}
}
],
onError: err => expect(err).toBeFalsy()
});
});

test('TileLayer#error tiles do not block isLoaded', async () => {
let tileErrorCalled = 0;

Expand Down
Loading