@macrostrat/cesium-martini 1.5.1 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +7 -3
- package/src/index.ts +18 -0
- package/src/mapbox-terrain-provider.ts +25 -0
- package/src/module.d.ts +4 -0
- package/src/resources/heightmap-resource.ts +120 -0
- package/src/resources/mapbox-resource.ts +53 -0
- package/src/terrain-data.ts +363 -0
- package/src/terrain-provider.ts +343 -0
- package/src/worker/decoder.ts +57 -0
- package/src/worker/mapbox-worker.ts +86 -0
- package/src/worker/upsampler-worker.ts +64 -0
- package/src/worker/worker-farm.ts +85 -0
- package/src/worker/worker-util.ts +267 -0
- package/.env.example +0 -1
- package/.prettierrc +0 -3
- package/babel.config.js +0 -3
- package/rollup.config.js +0 -46
- package/tsconfig.json +0 -19
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Cartographic,
|
|
3
|
+
OrientedBoundingBox,
|
|
4
|
+
BoundingSphere,
|
|
5
|
+
Rectangle,
|
|
6
|
+
Ellipsoid,
|
|
7
|
+
WebMercatorTilingScheme,
|
|
8
|
+
Math as CMath,
|
|
9
|
+
Event as CEvent,
|
|
10
|
+
TerrainProvider,
|
|
11
|
+
Credit,
|
|
12
|
+
TilingScheme,
|
|
13
|
+
QuantizedMeshTerrainData,
|
|
14
|
+
} from "cesium";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
TerrainWorkerInput,
|
|
18
|
+
emptyMesh as _emptyMesh,
|
|
19
|
+
} from "./worker/worker-util";
|
|
20
|
+
import { HeightmapResource } from "./resources/heightmap-resource";
|
|
21
|
+
import WorkerFarmTerrainDecoder, {
|
|
22
|
+
TerrainDecoder,
|
|
23
|
+
DefaultTerrainDecoder,
|
|
24
|
+
} from "./worker/decoder";
|
|
25
|
+
import {
|
|
26
|
+
createEmptyMesh,
|
|
27
|
+
createTerrainMesh,
|
|
28
|
+
TerrainMeshMeta,
|
|
29
|
+
} from "./terrain-data";
|
|
30
|
+
import { TileCoordinates } from "./terrain-data";
|
|
31
|
+
|
|
32
|
+
// https://github.com/CesiumGS/cesium/blob/1.68/Source/Scene/MapboxImageryProvider.js#L42
|
|
33
|
+
|
|
34
|
+
export interface MartiniTerrainOpts {
|
|
35
|
+
resource: HeightmapResource;
|
|
36
|
+
decoder?: TerrainDecoder;
|
|
37
|
+
|
|
38
|
+
ellipsoid?: Ellipsoid;
|
|
39
|
+
tilingScheme?: TilingScheme;
|
|
40
|
+
// workerURL: string;
|
|
41
|
+
detailScalar?: number;
|
|
42
|
+
minimumErrorLevel?: number;
|
|
43
|
+
maxWorkers?: number;
|
|
44
|
+
minZoomLevel?: number;
|
|
45
|
+
fillPoles?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class StretchedTilingScheme extends WebMercatorTilingScheme {
|
|
49
|
+
tileXYToRectangle(
|
|
50
|
+
x: number,
|
|
51
|
+
y: number,
|
|
52
|
+
level: number,
|
|
53
|
+
res: Rectangle,
|
|
54
|
+
): Rectangle {
|
|
55
|
+
let result = super.tileXYToRectangle(x, y, level);
|
|
56
|
+
if (y == 0) {
|
|
57
|
+
result.north = Math.PI / 2;
|
|
58
|
+
}
|
|
59
|
+
if (y + 1 == Math.pow(2, level)) {
|
|
60
|
+
result.south = -Math.PI / 2;
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class MartiniTerrainProvider<TerrainProvider> {
|
|
67
|
+
hasWaterMask = false;
|
|
68
|
+
hasVertexNormals = false;
|
|
69
|
+
credit = new Credit("Mapbox");
|
|
70
|
+
ready: boolean;
|
|
71
|
+
readyPromise: Promise<boolean>;
|
|
72
|
+
availability = null;
|
|
73
|
+
errorEvent = new CEvent();
|
|
74
|
+
tilingScheme: TilingScheme;
|
|
75
|
+
ellipsoid: Ellipsoid;
|
|
76
|
+
levelOfDetailScalar: number | null = null;
|
|
77
|
+
maxWorkers: number = 5;
|
|
78
|
+
minError: number = 0.1;
|
|
79
|
+
minZoomLevel: number;
|
|
80
|
+
fillPoles: boolean = true;
|
|
81
|
+
_errorAtMinZoom: number = 1000;
|
|
82
|
+
|
|
83
|
+
resource: HeightmapResource = null;
|
|
84
|
+
decoder: TerrainDecoder = null;
|
|
85
|
+
|
|
86
|
+
RADIUS_SCALAR = 1.0;
|
|
87
|
+
|
|
88
|
+
// @ts-ignore
|
|
89
|
+
constructor(opts: MartiniTerrainOpts = {}) {
|
|
90
|
+
//this.martini = new Martini(257);
|
|
91
|
+
this.resource = opts.resource;
|
|
92
|
+
this.credit = this.resource.credit ?? new Credit("Mapbox");
|
|
93
|
+
|
|
94
|
+
this.decoder = opts.decoder;
|
|
95
|
+
this.maxWorkers = opts.maxWorkers ?? 5;
|
|
96
|
+
|
|
97
|
+
if (!this.decoder) {
|
|
98
|
+
const maxWorkers = this.maxWorkers;
|
|
99
|
+
if (maxWorkers > 0) {
|
|
100
|
+
this.decoder = new WorkerFarmTerrainDecoder({ maxWorkers });
|
|
101
|
+
} else {
|
|
102
|
+
this.decoder = new DefaultTerrainDecoder();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
this.minZoomLevel = opts.minZoomLevel ?? 3;
|
|
106
|
+
this.fillPoles = opts.fillPoles ?? true;
|
|
107
|
+
|
|
108
|
+
this.ready = true;
|
|
109
|
+
this.readyPromise = Promise.resolve(true);
|
|
110
|
+
this.minError = opts.minimumErrorLevel ?? 0.1;
|
|
111
|
+
|
|
112
|
+
this.errorEvent.addEventListener(console.log, this);
|
|
113
|
+
this.ellipsoid = opts.ellipsoid ?? Ellipsoid.WGS84;
|
|
114
|
+
|
|
115
|
+
if (opts.tilingScheme == null) {
|
|
116
|
+
let scheme = WebMercatorTilingScheme;
|
|
117
|
+
if (this.fillPoles) {
|
|
118
|
+
scheme = StretchedTilingScheme;
|
|
119
|
+
}
|
|
120
|
+
this.tilingScheme = new scheme({
|
|
121
|
+
numberOfLevelZeroTilesX: 1,
|
|
122
|
+
numberOfLevelZeroTilesY: 1,
|
|
123
|
+
ellipsoid: this.ellipsoid,
|
|
124
|
+
});
|
|
125
|
+
} else {
|
|
126
|
+
this.tilingScheme = opts.tilingScheme;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.levelOfDetailScalar = (opts.detailScalar ?? 2.0) + CMath.EPSILON5;
|
|
130
|
+
|
|
131
|
+
//this.errorEvent.addEventListener(console.log, this);
|
|
132
|
+
this.ellipsoid =
|
|
133
|
+
opts.tilingScheme?.ellipsoid ?? opts.ellipsoid ?? Ellipsoid.WGS84;
|
|
134
|
+
|
|
135
|
+
this._errorAtMinZoom = this.errorAtZoom(this.minZoomLevel);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
requestTileGeometry(x, y, z, request) {
|
|
139
|
+
// Look for tiles both below the zoom level and below the error threshold for the zoom level at the equator...
|
|
140
|
+
if (
|
|
141
|
+
this.minZoomLevel != 0 &&
|
|
142
|
+
(z < this.minZoomLevel ||
|
|
143
|
+
this.scaledErrorForTile(x, y, z) > this._errorAtMinZoom)
|
|
144
|
+
) {
|
|
145
|
+
// If we are below the minimum zoom level, we return empty heightmaps
|
|
146
|
+
// to avoid unnecessary requests for low-resolution data.
|
|
147
|
+
return Promise.resolve(this.emptyMesh(x, y, z));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Note: we still load a TON of tiles near the poles. We might need to do some overzooming here...
|
|
151
|
+
return this.decoder.requestTileGeometry(
|
|
152
|
+
{ x, y, z },
|
|
153
|
+
this.processTile.bind(this),
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async processTile({ x, y, z }: TileCoordinates) {
|
|
158
|
+
// Something wonky about our tiling scheme, perhaps
|
|
159
|
+
// 12/2215/2293 @2x
|
|
160
|
+
//const url = `https://a.tiles.mapbox.com/v4/mapbox.terrain-rgb/${z}/${x}/${y}${hires}.${this.format}?access_token=${this.accessToken}`;
|
|
161
|
+
const tileRect = this.tilingScheme.tileXYToRectangle(x, y, z);
|
|
162
|
+
const errorLevel = this.errorAtZoom(z);
|
|
163
|
+
const maxVertexDistance = this.maxVertexDistance(tileRect);
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const { tileSize, getTilePixels } = this.resource;
|
|
167
|
+
const r1 = getTilePixels.bind(this.resource, { x, y, z });
|
|
168
|
+
|
|
169
|
+
if (r1 == null) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const px = await r1();
|
|
173
|
+
|
|
174
|
+
let pixelData = px.data;
|
|
175
|
+
if (pixelData == null) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
///const center = Rectangle.center(tileRect);
|
|
180
|
+
|
|
181
|
+
const params: TerrainWorkerInput = {
|
|
182
|
+
imageData: pixelData,
|
|
183
|
+
maxVertexDistance,
|
|
184
|
+
x,
|
|
185
|
+
y,
|
|
186
|
+
z,
|
|
187
|
+
errorLevel,
|
|
188
|
+
ellipsoidRadius: this.tilingScheme.ellipsoid.maximumRadius,
|
|
189
|
+
tileSize,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const res = await this.decoder.decodeTerrain(params, pixelData.buffer);
|
|
193
|
+
|
|
194
|
+
const meta: TerrainMeshMeta = {
|
|
195
|
+
ellipsoid: this.tilingScheme.ellipsoid,
|
|
196
|
+
errorLevel,
|
|
197
|
+
overscaleFactor: 0,
|
|
198
|
+
maxVertexDistance,
|
|
199
|
+
tileRect,
|
|
200
|
+
tileSize,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/** This builds a final terrain mesh object that can optionally
|
|
204
|
+
* be upscaled to a higher resolution.
|
|
205
|
+
*/
|
|
206
|
+
return createTerrainMesh(res, meta);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
//console.log(err);
|
|
209
|
+
return createEmptyMesh({
|
|
210
|
+
tileRect,
|
|
211
|
+
errorLevel,
|
|
212
|
+
ellipsoid: this.tilingScheme.ellipsoid,
|
|
213
|
+
tileCoord: { x, y, z },
|
|
214
|
+
tileSize: 0,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
errorAtZoom(zoom: number) {
|
|
220
|
+
return Math.max(
|
|
221
|
+
this.getLevelMaximumGeometricError(zoom) / this.levelOfDetailScalar,
|
|
222
|
+
this.minError,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
scaledErrorForTile(x: number, y: number, z: number) {
|
|
227
|
+
const tileRect = this.tilingScheme.tileXYToRectangle(x, y, z);
|
|
228
|
+
const center = Rectangle.center(tileRect);
|
|
229
|
+
return this.errorAtZoom(z) / Math.pow(1 - Math.sin(center.latitude), 2);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
maxVertexDistance(tileRect: Rectangle) {
|
|
233
|
+
return Math.ceil(2 / tileRect.height);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
emptyMesh(x: number, y: number, z: number) {
|
|
237
|
+
const tileRect = this.tilingScheme.tileXYToRectangle(x, y, z);
|
|
238
|
+
const center = Rectangle.center(tileRect);
|
|
239
|
+
|
|
240
|
+
const latScalar = Math.min(Math.abs(Math.sin(center.latitude)), 0.995);
|
|
241
|
+
let v = Math.max(
|
|
242
|
+
Math.ceil((200 / (z + 1)) * Math.pow(1 - latScalar, 0.25)),
|
|
243
|
+
4,
|
|
244
|
+
);
|
|
245
|
+
const output = _emptyMesh(v);
|
|
246
|
+
const err = this.errorAtZoom(z);
|
|
247
|
+
return this.createQuantizedMeshData(tileRect, err, output);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
createQuantizedMeshData(tileRect, errorLevel, workerOutput) {
|
|
251
|
+
const {
|
|
252
|
+
minimumHeight,
|
|
253
|
+
maximumHeight,
|
|
254
|
+
quantizedVertices,
|
|
255
|
+
indices,
|
|
256
|
+
westIndices,
|
|
257
|
+
southIndices,
|
|
258
|
+
eastIndices,
|
|
259
|
+
northIndices,
|
|
260
|
+
} = workerOutput;
|
|
261
|
+
|
|
262
|
+
const err = errorLevel;
|
|
263
|
+
const skirtHeight = err * 20;
|
|
264
|
+
|
|
265
|
+
const center = Rectangle.center(tileRect);
|
|
266
|
+
|
|
267
|
+
// Calculating occlusion height is kind of messy currently, but it definitely works
|
|
268
|
+
const halfAngle = tileRect.width / 2;
|
|
269
|
+
const dr = Math.cos(halfAngle); // half tile width since our ref point is at the center
|
|
270
|
+
|
|
271
|
+
let occlusionHeight = dr * this.ellipsoid.maximumRadius + maximumHeight;
|
|
272
|
+
if (halfAngle > Math.PI / 4) {
|
|
273
|
+
occlusionHeight = (1 + halfAngle) * this.ellipsoid.maximumRadius;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const occlusionPoint = new Cartographic(
|
|
277
|
+
center.longitude,
|
|
278
|
+
center.latitude,
|
|
279
|
+
occlusionHeight,
|
|
280
|
+
// Scaling factor of two just to be sure.
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const horizonOcclusionPoint = this.ellipsoid.transformPositionToScaledSpace(
|
|
284
|
+
Cartographic.toCartesian(occlusionPoint),
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
let orientedBoundingBox = OrientedBoundingBox.fromRectangle(
|
|
288
|
+
tileRect,
|
|
289
|
+
minimumHeight,
|
|
290
|
+
maximumHeight,
|
|
291
|
+
this.tilingScheme.ellipsoid,
|
|
292
|
+
);
|
|
293
|
+
let boundingSphere =
|
|
294
|
+
BoundingSphere.fromOrientedBoundingBox(orientedBoundingBox);
|
|
295
|
+
|
|
296
|
+
// SE NW NE
|
|
297
|
+
// NE NW SE
|
|
298
|
+
|
|
299
|
+
/** TODO: we need to create raster terrain data. */
|
|
300
|
+
return new QuantizedMeshTerrainData({
|
|
301
|
+
minimumHeight,
|
|
302
|
+
maximumHeight,
|
|
303
|
+
quantizedVertices,
|
|
304
|
+
indices,
|
|
305
|
+
boundingSphere,
|
|
306
|
+
orientedBoundingBox,
|
|
307
|
+
horizonOcclusionPoint,
|
|
308
|
+
westIndices,
|
|
309
|
+
southIndices,
|
|
310
|
+
eastIndices,
|
|
311
|
+
northIndices,
|
|
312
|
+
westSkirtHeight: skirtHeight,
|
|
313
|
+
southSkirtHeight: skirtHeight,
|
|
314
|
+
eastSkirtHeight: skirtHeight,
|
|
315
|
+
northSkirtHeight: skirtHeight,
|
|
316
|
+
childTileMask: 15,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
getLevelMaximumGeometricError(level) {
|
|
321
|
+
const levelZeroMaximumGeometricError =
|
|
322
|
+
TerrainProvider.getEstimatedLevelZeroGeometricErrorForAHeightmap(
|
|
323
|
+
this.tilingScheme.ellipsoid,
|
|
324
|
+
65,
|
|
325
|
+
this.tilingScheme.getNumberOfXTilesAtLevel(0),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
/*
|
|
329
|
+
Scalar to control overzooming
|
|
330
|
+
- also seems to control zooming for imagery layers
|
|
331
|
+
- This scalar was causing trouble for non-256 tile sizes,
|
|
332
|
+
and we've removed it for now. It could be reintroduced
|
|
333
|
+
if it seems necessary
|
|
334
|
+
*/
|
|
335
|
+
const scalar = 1; //this.resource.tileSize / 256 ;
|
|
336
|
+
|
|
337
|
+
return levelZeroMaximumGeometricError / scalar / (1 << level);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
getTileDataAvailable(x, y, z) {
|
|
341
|
+
return this.resource.getTileDataAvailable({ x, y, z });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { HeightmapTerrainData, QuantizedMeshTerrainData } from "cesium";
|
|
2
|
+
import { TileCoordinates } from "../terrain-data";
|
|
3
|
+
import WorkerFarm from "./worker-farm";
|
|
4
|
+
import { TerrainWorkerInput, QuantizedMeshResult } from "./worker-util";
|
|
5
|
+
|
|
6
|
+
export interface TerrainDecoder {
|
|
7
|
+
requestTileGeometry: (
|
|
8
|
+
coords: TileCoordinates,
|
|
9
|
+
processFunction: (
|
|
10
|
+
coords: TileCoordinates,
|
|
11
|
+
) => Promise<HeightmapTerrainData | QuantizedMeshTerrainData>,
|
|
12
|
+
) => Promise<HeightmapTerrainData | QuantizedMeshTerrainData> | undefined;
|
|
13
|
+
decodeTerrain: (
|
|
14
|
+
params: TerrainWorkerInput,
|
|
15
|
+
data: ArrayBufferLike,
|
|
16
|
+
) => Promise<QuantizedMeshResult>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class DefaultTerrainDecoder implements TerrainDecoder {
|
|
20
|
+
inProgress: number = 0;
|
|
21
|
+
maxRequests: number = 2;
|
|
22
|
+
|
|
23
|
+
requestTileGeometry(coords, processFunction) {
|
|
24
|
+
if (this.inProgress > this.maxRequests) return undefined;
|
|
25
|
+
this.inProgress += 1;
|
|
26
|
+
return processFunction(coords).finally(() => {
|
|
27
|
+
this.inProgress -= 1;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
decodeTerrain(params, data) {
|
|
32
|
+
return Promise.resolve(null);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface WorkerFarmDecoderOpts {
|
|
37
|
+
maxWorkers?: number;
|
|
38
|
+
worker?: Worker;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class WorkerFarmTerrainDecoder extends DefaultTerrainDecoder {
|
|
42
|
+
farm: WorkerFarm;
|
|
43
|
+
|
|
44
|
+
constructor(opts: WorkerFarmDecoderOpts) {
|
|
45
|
+
super();
|
|
46
|
+
this.farm = new WorkerFarm({ worker: opts.worker });
|
|
47
|
+
this.maxRequests = opts.maxWorkers ?? 5;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
decodeTerrain(params, data) {
|
|
51
|
+
return this.farm.scheduleTask(params, [
|
|
52
|
+
data,
|
|
53
|
+
]) as Promise<QuantizedMeshResult>;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export default WorkerFarmTerrainDecoder;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
rgbTerrainToGrid,
|
|
3
|
+
createQuantizedMeshData,
|
|
4
|
+
TerrainWorkerInput,
|
|
5
|
+
} from "./worker-util";
|
|
6
|
+
import ndarray from "ndarray";
|
|
7
|
+
import Martini from "@mapbox/martini";
|
|
8
|
+
// https://github.com/CesiumGS/cesium/blob/1.76/Source/WorkersES6/createVerticesFromQuantizedTerrainMesh.js
|
|
9
|
+
|
|
10
|
+
let martiniCache = {};
|
|
11
|
+
|
|
12
|
+
function decodeTerrain(
|
|
13
|
+
parameters: TerrainWorkerInput,
|
|
14
|
+
transferableObjects?: Transferable[],
|
|
15
|
+
) {
|
|
16
|
+
const {
|
|
17
|
+
imageData,
|
|
18
|
+
tileSize = 256,
|
|
19
|
+
errorLevel,
|
|
20
|
+
maxVertexDistance,
|
|
21
|
+
} = parameters;
|
|
22
|
+
|
|
23
|
+
// Height data can be either an array of numbers (for pre-existing terrain data)
|
|
24
|
+
// or an image data array (for decoding from an image)
|
|
25
|
+
|
|
26
|
+
const heightData = {
|
|
27
|
+
type: "image",
|
|
28
|
+
array: imageData,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
let terrain: Float32Array;
|
|
32
|
+
if (heightData.type === "image") {
|
|
33
|
+
const { array } = heightData;
|
|
34
|
+
const pixels = ndarray(
|
|
35
|
+
new Uint8Array(array),
|
|
36
|
+
[tileSize, tileSize, 4],
|
|
37
|
+
[4, 4 * tileSize, 1],
|
|
38
|
+
0,
|
|
39
|
+
);
|
|
40
|
+
terrain = rgbTerrainToGrid(pixels);
|
|
41
|
+
} else {
|
|
42
|
+
// @ts-ignore
|
|
43
|
+
terrain = heightData.array;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Tile size must be maintained through the life of the worker
|
|
47
|
+
martiniCache[tileSize] ??= new Martini(tileSize + 1);
|
|
48
|
+
|
|
49
|
+
const tile = martiniCache[tileSize].createTile(terrain);
|
|
50
|
+
|
|
51
|
+
const canUpscaleTile = true; //heightData.type === "image";
|
|
52
|
+
|
|
53
|
+
// get a mesh (vertices and triangles indices) for a 10m error
|
|
54
|
+
const mesh = tile.getMesh(errorLevel, Math.min(maxVertexDistance, tileSize));
|
|
55
|
+
const res = createQuantizedMeshData(
|
|
56
|
+
tile,
|
|
57
|
+
mesh,
|
|
58
|
+
tileSize,
|
|
59
|
+
// Only include vertex data if anticipate upscaling tile
|
|
60
|
+
canUpscaleTile ? terrain : null,
|
|
61
|
+
);
|
|
62
|
+
transferableObjects.push(res.indices.buffer);
|
|
63
|
+
transferableObjects.push(res.quantizedVertices.buffer);
|
|
64
|
+
if (res.quantizedHeights) {
|
|
65
|
+
transferableObjects.push(res.quantizedHeights.buffer);
|
|
66
|
+
}
|
|
67
|
+
return res;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
self.onmessage = function (msg) {
|
|
71
|
+
const { id, payload } = msg.data;
|
|
72
|
+
if (id == null) return;
|
|
73
|
+
let objects: Transferable[] = [];
|
|
74
|
+
let res = null;
|
|
75
|
+
try {
|
|
76
|
+
res = decodeTerrain(payload, objects);
|
|
77
|
+
// @ts-ignore
|
|
78
|
+
self.postMessage({ id, payload: res }, objects);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
const msg = err.message ?? err;
|
|
81
|
+
self.postMessage({ id, err: msg.toString() });
|
|
82
|
+
} finally {
|
|
83
|
+
res = null;
|
|
84
|
+
objects = null;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Worker to upsample terrain meshes */
|
|
2
|
+
import { createQuantizedMeshData, TerrainUpscaleInput } from "./worker-util";
|
|
3
|
+
import Martini from "@mapbox/martini";
|
|
4
|
+
// https://github.com/CesiumGS/cesium/blob/1.76/Source/WorkersES6/createVerticesFromQuantizedTerrainMesh.js
|
|
5
|
+
|
|
6
|
+
let martiniCache: Record<number, Martini> = {};
|
|
7
|
+
|
|
8
|
+
function decodeTerrain(
|
|
9
|
+
parameters: TerrainUpscaleInput,
|
|
10
|
+
transferableObjects?: Transferable[],
|
|
11
|
+
) {
|
|
12
|
+
const {
|
|
13
|
+
heightData,
|
|
14
|
+
tileSize = 256,
|
|
15
|
+
errorLevel,
|
|
16
|
+
maxVertexDistance,
|
|
17
|
+
} = parameters;
|
|
18
|
+
|
|
19
|
+
// Height data can be either an array of numbers (for pre-existing terrain data)
|
|
20
|
+
// or an image data array (for decoding from an image)
|
|
21
|
+
|
|
22
|
+
let terrain: Float32Array = heightData;
|
|
23
|
+
|
|
24
|
+
// Tile size must be maintained through the life of the worker
|
|
25
|
+
martiniCache[tileSize] ??= new Martini(tileSize + 1);
|
|
26
|
+
|
|
27
|
+
const tile = martiniCache[tileSize].createTile(terrain);
|
|
28
|
+
|
|
29
|
+
const canUpscaleTile = true; //heightData.type === "image";
|
|
30
|
+
|
|
31
|
+
// get a mesh (vertices and triangles indices) for a 10m error
|
|
32
|
+
const mesh = tile.getMesh(errorLevel, Math.min(maxVertexDistance, tileSize));
|
|
33
|
+
const res = createQuantizedMeshData(
|
|
34
|
+
tile,
|
|
35
|
+
mesh,
|
|
36
|
+
tileSize,
|
|
37
|
+
// Only include vertex data if anticipate upscaling tile
|
|
38
|
+
canUpscaleTile ? terrain : null,
|
|
39
|
+
);
|
|
40
|
+
transferableObjects.push(res.indices.buffer);
|
|
41
|
+
transferableObjects.push(res.quantizedVertices.buffer);
|
|
42
|
+
if (res.quantizedHeights) {
|
|
43
|
+
transferableObjects.push(res.quantizedHeights.buffer);
|
|
44
|
+
}
|
|
45
|
+
return res;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
self.onmessage = function (msg) {
|
|
49
|
+
const { id, payload } = msg.data;
|
|
50
|
+
if (id == null) return;
|
|
51
|
+
let objects: Transferable[] = [];
|
|
52
|
+
let res = null;
|
|
53
|
+
try {
|
|
54
|
+
res = decodeTerrain(payload, objects);
|
|
55
|
+
// @ts-ignore
|
|
56
|
+
self.postMessage({ id, payload: res }, objects);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
const msg = err.message ?? err;
|
|
59
|
+
self.postMessage({ id, err: msg.toString() });
|
|
60
|
+
} finally {
|
|
61
|
+
res = null;
|
|
62
|
+
objects = null;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const resolves = {};
|
|
2
|
+
const rejects = {};
|
|
3
|
+
let globalMsgId = 0; // Activate calculation in the worker, returning a promise
|
|
4
|
+
|
|
5
|
+
async function sendMessage(
|
|
6
|
+
worker: Worker,
|
|
7
|
+
payload: any,
|
|
8
|
+
transferableObjects: Transferable[],
|
|
9
|
+
) {
|
|
10
|
+
const msgId = globalMsgId++;
|
|
11
|
+
const msg = {
|
|
12
|
+
id: msgId,
|
|
13
|
+
payload,
|
|
14
|
+
};
|
|
15
|
+
return new Promise(function (resolve, reject) {
|
|
16
|
+
// save callbacks for later
|
|
17
|
+
resolves[msgId] = resolve;
|
|
18
|
+
rejects[msgId] = reject;
|
|
19
|
+
worker.postMessage(msg, transferableObjects);
|
|
20
|
+
});
|
|
21
|
+
} // Handle incoming calculation result
|
|
22
|
+
|
|
23
|
+
function handleMessage(msg) {
|
|
24
|
+
const { id, err, payload } = msg.data;
|
|
25
|
+
if (payload) {
|
|
26
|
+
const resolve = resolves[id];
|
|
27
|
+
if (resolve) {
|
|
28
|
+
resolve(payload);
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
// error condition
|
|
32
|
+
const reject = rejects[id];
|
|
33
|
+
if (reject) {
|
|
34
|
+
if (err) {
|
|
35
|
+
reject(err);
|
|
36
|
+
} else {
|
|
37
|
+
reject("Got nothing");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// purge used callbacks
|
|
43
|
+
delete resolves[id];
|
|
44
|
+
delete rejects[id];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class WorkerFarm {
|
|
48
|
+
worker: Worker;
|
|
49
|
+
inProgressWorkers: number = 0;
|
|
50
|
+
maxWorkers: number = 5;
|
|
51
|
+
processingQueue: Function[] = [];
|
|
52
|
+
|
|
53
|
+
constructor(opts) {
|
|
54
|
+
this.worker = opts.worker;
|
|
55
|
+
this.worker.onmessage = handleMessage;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async scheduleTask(params, transferableObjects) {
|
|
59
|
+
const res = await sendMessage(this.worker, params, transferableObjects);
|
|
60
|
+
this.releaseWorker();
|
|
61
|
+
return res;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async holdForAvailableWorker(): Promise<void> {
|
|
65
|
+
let resultPromise: Promise<void>;
|
|
66
|
+
if (this.inProgressWorkers > this.maxWorkers) {
|
|
67
|
+
resultPromise = new Promise((resolve, reject) => {
|
|
68
|
+
this.processingQueue.push(resolve);
|
|
69
|
+
});
|
|
70
|
+
} else {
|
|
71
|
+
resultPromise = Promise.resolve(null);
|
|
72
|
+
}
|
|
73
|
+
await resultPromise;
|
|
74
|
+
this.inProgressWorkers += 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
releaseWorker() {
|
|
78
|
+
this.inProgressWorkers -= 1;
|
|
79
|
+
if (this.processingQueue.length > 0) {
|
|
80
|
+
this.processingQueue.shift()();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default WorkerFarm;
|