Skip to content

Commit 10c68f5

Browse files
committed
added(display): camera now always stays above terrain, preventing users from zooming below ground with smooth adaptive clamping
added(display): auto-adjust camera zoom to maintain altitude above terrain when terrain data becomes available prepare TerrainWorker integration and updates in TerrainTileProvider Signed-off-by: Tim Deubler <[email protected]>
1 parent fa49346 commit 10c68f5

File tree

21 files changed

+478
-82
lines changed

21 files changed

+478
-82
lines changed

packages/core/src/features/TerrainFeature.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,18 @@
1818
*/
1919
import {Feature} from './Feature';
2020
import {GeoJSONCoordinate} from './GeoJSON';
21+
import {TerrainTileProvider, TileLayer} from '@here/xyz-maps-core';
2122

2223

2324
export type TerrainTileMesh = {
24-
vertices: Uint16Array|Uint32Array|Float32Array;
25-
indices: Uint16Array|Uint32Array;
26-
normals?: Int8Array|Int16Array|Float32Array;
25+
vertices: Uint16Array | Uint32Array | Float32Array;
26+
indices: Uint16Array | Uint32Array;
27+
normals?: Int8Array | Int16Array | Float32Array;
2728
edgeIndices?: {
28-
left: Uint16Array|Uint32Array;
29-
right: Uint16Array|Uint32Array;
30-
top: Uint16Array|Uint32Array;
31-
bottom: Uint16Array|Uint32Array;
29+
left: Uint16Array | Uint32Array;
30+
right: Uint16Array | Uint32Array;
31+
top: Uint16Array | Uint32Array;
32+
bottom: Uint16Array | Uint32Array;
3233
}
3334
}
3435

@@ -51,6 +52,8 @@ export type TerrainTileFeatureProperties = TerrainTileMesh & {
5152
* This Feature represents a Terrain Tile.
5253
*/
5354
export class TerrainTileFeature extends Feature<'Polygon'> {
55+
_provider: TerrainTileProvider;
56+
5457
/**
5558
* The Properties of the Terrain Tile feature.
5659
*/
@@ -64,4 +67,39 @@ export class TerrainTileFeature extends Feature<'Polygon'> {
6467
type: 'Polygon',
6568
coordinates: GeoJSONCoordinate[][]
6669
};
70+
71+
72+
/**
73+
* @hidden
74+
* @internal
75+
*
76+
* Returns the height map array for the terrain tile.
77+
* The height map contains elevation data for each point in the tile.
78+
*
79+
* @returns {Uint16Array | Uint32Array | Float32Array | null}
80+
* The height map array, or null if not available.
81+
*/
82+
getHeightMap(): Uint16Array | Uint32Array | Float32Array | null {
83+
return this.properties.heightMap || null;
84+
}
85+
86+
/**
87+
* @hidden
88+
* @internal
89+
*
90+
* Returns the height value from the height map at the given normalized local tile coordinates.
91+
*
92+
* @param normalizedX - The normalized X coordinate (0.0 to 1.0).
93+
* @param nomralizedY - The normalized Y coordinate (0.0 to 1.0).
94+
*
95+
* @returns The height value at the specified coordinates, or null if no height map is available.
96+
*/
97+
getHeightAt(normalizedX: number, nomralizedY: number, interpolate?: boolean = false) {
98+
const heightMap = this.getHeightMap();
99+
if (!heightMap) return null;
100+
const size = Math.sqrt(heightMap.length);
101+
const x = Math.floor(normalizedX * size);
102+
const y = Math.floor(nomralizedY * size);
103+
return heightMap[y * size + x];
104+
}
67105
}

packages/core/src/layers/terrain/TerrainStyle.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ export class TerrainTileLayerStyle implements LayerStyle {
153153
};
154154
}
155155
return {
156+
id: `Terrain-${id}-${Math.random()*1e6^0}`,
156157
textures,
157158
materials: {
158159
terrain: {

packages/core/src/loaders/webworker/HTTPLoader.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,13 @@ import Tile from '../../tile/Tile';
2828
// return worker;
2929
// };
3030

31+
type TileResponse = { tile: Tile, success: any, error: any };
32+
type CustomResponse = { resolve: (data: unknown) => void, reject(reason?: any): void };
33+
3134
class WorkerHTTPLoader extends HTTPLoader {
3235
private worker: Worker;
3336

34-
private requests: Map<string, { tile: Tile, success: any, error: any }> = new Map();
37+
private pendingResponses: Map<string, TileResponse | CustomResponse> = new Map();
3538

3639
constructor(worker: string, options: HTTPLoaderOptions, payload?) {
3740
super(options);
@@ -56,19 +59,22 @@ class WorkerHTTPLoader extends HTTPLoader {
5659
private receiveMessage(e) {
5760
const data = e.data;
5861
const msg = data.msg;
59-
const quadkey = data.quadkey;
60-
const cb = this.requests.get(quadkey);
61-
62+
const key = data.key || msg;
63+
const cb = this.pendingResponses.get(key);
6264
if (cb) {
63-
this.requests.delete(quadkey);
65+
this.pendingResponses.delete(key);
6466
switch (msg) {
6567
case 'success':
66-
cb.success(this.processData(data.data));
68+
(cb as TileResponse).success(this.processData(data.data));
6769
break;
6870
case 'error':
6971
const {tile} = cb;
7072
tile.error = new NetworkError(data.data);
71-
cb.error?.(tile.error, tile);
73+
(cb as TileResponse).error?.(tile.error, tile);
74+
break;
75+
default:
76+
// custom messages
77+
(cb as CustomResponse).resolve(data.data);
7278
break;
7379
}
7480
}
@@ -78,7 +84,7 @@ class WorkerHTTPLoader extends HTTPLoader {
7884
load(tile, success, error?) {
7985
const url = this.getUrl(tile);
8086
const {quadkey, x, y, z} = tile;
81-
this.requests.set(quadkey, {tile, success, error});
87+
this.pendingResponses.set(quadkey, {tile, success, error});
8288
this.worker.postMessage({msg: 'load', url, quadkey, x, y, z});
8389
}
8490

@@ -87,11 +93,26 @@ class WorkerHTTPLoader extends HTTPLoader {
8793
const url = this.getUrl(tile);
8894
const {quadkey, x, y, z} = tile;
8995

90-
if (this.requests.get(quadkey)) {
91-
this.requests.delete(quadkey);
96+
if (this.pendingResponses.get(quadkey)) {
97+
this.pendingResponses.delete(quadkey);
9298
this.worker.postMessage({msg: 'abort', url, quadkey, x, y, z});
9399
}
94100
}
101+
102+
103+
private addPendingResponse(key: string) {
104+
return new Promise(async (resolve, reject) => {
105+
this.pendingResponses.set(key, {resolve, reject});
106+
});
107+
}
108+
109+
async call(args: { method: string, key?: string, data?: any, transfer?: any[] }): Promise<any> {
110+
const {method, data, transfer} = args;
111+
const key = args.method + args.key ?? '';
112+
const promise = this.addPendingResponse(key);
113+
this.worker.postMessage({msg: method, key, custom: data}, transfer);
114+
return promise;
115+
}
95116
}
96117

97118
export default WorkerHTTPLoader;

packages/core/src/loaders/webworker/HTTPWorker.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,14 @@ export class HTTPWorker {
2626
loader;
2727

2828
id: string;
29+
private customHandlers: Set<string>;
2930

3031
constructor(options: HTTPLoaderOptions) {
3132
const {responseType} = options;
3233
const loader = this.loader = new HTTPLoader({responseType});
3334

35+
this.customHandlers = new Set<string>();
36+
3437
self.addEventListener('message', (e) => {
3538
const {msg, quadkey, url, x, y, z} = e.data;
3639
switch (msg) {
@@ -40,23 +43,37 @@ export class HTTPWorker {
4043
case 'load':
4144
loader.baseUrl = url;
4245
this.load(x, y, z, quadkey, url);
46+
break;
47+
default:
48+
if (this.customHandlers.has(msg)) {
49+
const result : {data:any, transfer?: Transferable[]} = this[msg](e.data.custom);
50+
if (result?.data) {
51+
self.postMessage({msg, key: e.data.key, data: result.data}, result.transfer);
52+
}
53+
}
4354
}
4455
});
4556
}
4657

58+
protected registerCustomMsgHandler(customHandler: string) {
59+
if (typeof this[customHandler] == 'function') {
60+
this.customHandlers.add(customHandler);
61+
}
62+
}
63+
4764
load(x: number, y: number, z: number, quadkey: string, url: string) {
4865
const worker = this;
4966
worker.loader.load({quadkey}, (arrayBuffer) => {
5067
const {data, transfer} = worker.process(arrayBuffer, x, y, z);
5168
self.postMessage({
5269
msg: 'success',
5370
url,
54-
quadkey,
71+
key: quadkey,
5572
data
5673
}, transfer);
5774
},
5875
(e) => {
59-
self.postMessage({msg: 'error', url, quadkey, data: e});
76+
self.postMessage({msg: 'error', url, key: quadkey, data: e});
6077
});
6178
}
6279

packages/core/src/providers/TerrainProvider/RTINMeshBuilder.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,8 +262,6 @@ export class RTINMeshBuilder {
262262
const maxXY = this.size - 1;
263263
const meshIndices: Uint32Array | Uint16Array = this.meshIndices;
264264

265-
console.log('---');
266-
267265
this.resetTmpMesh();
268266

269267
const processTriangle = (ax: number, ay: number, bx: number, by: number, cx: number, cy: number) => {

packages/core/src/providers/TerrainProvider/TerrainTileProvider.ts

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,21 +41,30 @@ type TerrainTileProviderOptions = Omit<RemoteTileProviderOptions, 'level'> & {
4141
loader?: any;
4242
}
4343

44+
45+
const createGeometricErrorMap = (maxGeometricError: number | StyleZoomRange<number>) => {
46+
if (typeof maxGeometricError == 'number') {
47+
maxGeometricError = Object.fromEntries(
48+
Array.from({length: 30}, (_, i) => [i, maxGeometricError as number])
49+
);
50+
}
51+
return maxGeometricError;
52+
};
53+
54+
4455
export class TerrainTileProvider extends RemoteTileProvider {
4556
dataType = 'json';
4657

4758
Feature = TerrainTileFeature;
59+
private _twl: TerrainWorkerLoader; // TerrainWorkerLoader
60+
61+
private maxGeometricError: { [zoom: number]: number };
62+
4863
constructor(options: TerrainTileProviderOptions) {
4964
options ||= {};
5065

51-
const attribution: (DataSourceAttribution|string)[] = [];
52-
let {maxGeometricError} = options;
53-
54-
if (typeof maxGeometricError == 'number') {
55-
maxGeometricError = Object.fromEntries(
56-
Array.from({length: 30}, (_, i) => [i, maxGeometricError as number])
57-
);
58-
}
66+
const attribution: (DataSourceAttribution | string)[] = [];
67+
const maxGeometricError = createGeometricErrorMap(options.maxGeometricError);
5968

6069
const addAttribution = (attr: string | DataSourceAttribution | DataSourceAttribution[]) => {
6170
if (attr) {
@@ -65,6 +74,9 @@ export class TerrainTileProvider extends RemoteTileProvider {
6574

6675
addAttribution(options.attribution);
6776

77+
78+
let terrainWorkderLoader;
79+
6880
if (!options.loader) {
6981
const tileLoadersConfig = {};
7082
for (let key of ['terrain', 'imagery']) {
@@ -79,6 +91,9 @@ export class TerrainTileProvider extends RemoteTileProvider {
7991
...loaderOptions,
8092
maxGeometricError
8193
});
94+
if (key == 'terrain') {
95+
terrainWorkderLoader = tileLoadersConfig[key];
96+
}
8297
addAttribution(loaderOptions.attribution);
8398
}
8499
}
@@ -97,6 +112,8 @@ export class TerrainTileProvider extends RemoteTileProvider {
97112

98113
const provider = this;
99114

115+
this._twl = terrainWorkderLoader;
116+
100117
this.remoteTileLoader = new TileLoadDelegator({
101118
provider,
102119
loader: options.loader,
@@ -146,7 +163,8 @@ export class TerrainTileProvider extends RemoteTileProvider {
146163
const oppositeSide = getOppositeNeighbor(side);
147164
updatedTiles.push(neighborTile);
148165

149-
if (heightMap) {
166+
if (!properties.edgeIndices) {
167+
// fully uses heightmaps
150168
const neighborHeightMap = neighborProperties.heightMap;
151169
if (side === Neighbor.RIGHT || side === Neighbor.BOTTOM) {
152170
stitchHeightmapBorders(heightMap, neighborHeightMap, side, 1, 2);

0 commit comments

Comments
 (0)