@macrostrat/cesium-martini 1.5.0 → 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 +5 -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/.idea/cesium-martini.iml +0 -9
- package/.idea/codeStyles/Project.xml +0 -57
- package/.idea/codeStyles/codeStyleConfig.xml +0 -5
- package/.idea/inspectionProfiles/Project_Default.xml +0 -6
- package/.idea/libraries/cache.xml +0 -769
- package/.idea/misc.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/prettier.xml +0 -8
- package/.idea/vcs.xml +0 -20
- package/.idea/workspace.xml +0 -125
- package/.npmrc +0 -1
- package/.prettierrc +0 -3
- package/babel.config.js +0 -3
- package/rollup.config.js +0 -46
- package/tsconfig.json +0 -19
- package/webpack.config.js +0 -75
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**On-the-fly meshing of raster elevation tiles for the CesiumJS virtual globe**
|
|
4
4
|
|
|
5
|
-
[](https://badge.fury.io/js/@macrostrat%2Fcesium-martini)
|
|
5
|
+
[](https://badge.fury.io/js/@macrostrat%2Fcesium-martini)
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
@@ -150,6 +150,10 @@ The configuration also takes a single number and array.
|
|
|
150
150
|
|
|
151
151
|
## Changelog
|
|
152
152
|
|
|
153
|
+
### `[1.5.1]`: February 2025
|
|
154
|
+
|
|
155
|
+
- Remove `.idea` files from bundle
|
|
156
|
+
|
|
153
157
|
### `[1.5.0]`: February 2025
|
|
154
158
|
|
|
155
159
|
- Allow overzooming of tiles when zoom levels are skipped
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@macrostrat/cesium-martini",
|
|
3
|
-
"version": "1.5.
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "1.5.2",
|
|
4
|
+
"description": "On-the-fly meshing of raster elevation tiles for the CesiumJS virtual globe",
|
|
5
5
|
"main": "./dist/index.cjs",
|
|
6
6
|
"module": "./dist/index.js",
|
|
7
7
|
"source": "./src/index.ts",
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
|
-
"
|
|
10
|
+
"source": "./src/index.ts",
|
|
11
11
|
"import": "./dist/index.js",
|
|
12
12
|
"require": "./dist/index.cjs"
|
|
13
13
|
}
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"format": "prettier --write src/**/*.ts examples/**/*.ts",
|
|
27
27
|
"check": "tsc --noEmit"
|
|
28
28
|
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"src"
|
|
32
|
+
],
|
|
29
33
|
"author": "",
|
|
30
34
|
"license": "ISC",
|
|
31
35
|
"dependencies": {
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import "./module.d.ts";
|
|
2
|
+
import DefaultHeightmapResource from "./resources/heightmap-resource";
|
|
3
|
+
import MapboxTerrainResource from "./resources/mapbox-resource";
|
|
4
|
+
import {
|
|
5
|
+
MartiniTerrainProvider,
|
|
6
|
+
StretchedTilingScheme,
|
|
7
|
+
} from "./terrain-provider";
|
|
8
|
+
import MapboxTerrainProvider from "./mapbox-terrain-provider";
|
|
9
|
+
export * from "./worker/decoder";
|
|
10
|
+
export * from "./worker/worker-util";
|
|
11
|
+
|
|
12
|
+
export default MapboxTerrainProvider;
|
|
13
|
+
export {
|
|
14
|
+
MartiniTerrainProvider,
|
|
15
|
+
DefaultHeightmapResource,
|
|
16
|
+
MapboxTerrainResource,
|
|
17
|
+
StretchedTilingScheme,
|
|
18
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { TerrainProvider } from "cesium";
|
|
2
|
+
import MapboxTerrainResource, {
|
|
3
|
+
MapboxTerrainResourceOpts,
|
|
4
|
+
} from "./resources/mapbox-resource";
|
|
5
|
+
import { MartiniTerrainOpts, MartiniTerrainProvider } from "./terrain-provider";
|
|
6
|
+
import WorkerFarmTerrainDecoder from "./worker/decoder";
|
|
7
|
+
import MapboxTerrainWorker from "web-worker:./worker/mapbox-worker";
|
|
8
|
+
|
|
9
|
+
type MapboxTerrainOpts = Omit<MartiniTerrainOpts, "resource"> &
|
|
10
|
+
MapboxTerrainResourceOpts;
|
|
11
|
+
|
|
12
|
+
export default class MapboxTerrainProvider extends MartiniTerrainProvider<TerrainProvider> {
|
|
13
|
+
constructor(opts: MapboxTerrainOpts = {}) {
|
|
14
|
+
const resource = new MapboxTerrainResource(opts);
|
|
15
|
+
const decoder = new WorkerFarmTerrainDecoder({
|
|
16
|
+
worker: new MapboxTerrainWorker(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
super({
|
|
20
|
+
...opts,
|
|
21
|
+
resource,
|
|
22
|
+
decoder,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/module.d.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Resource, Credit } from "cesium";
|
|
2
|
+
import { TileCoordinates } from "../terrain-data";
|
|
3
|
+
|
|
4
|
+
export interface HeightmapResource {
|
|
5
|
+
credit?: Credit;
|
|
6
|
+
tileSize: number;
|
|
7
|
+
getTilePixels: (coords: TileCoordinates) => Promise<ImageData> | undefined;
|
|
8
|
+
getTileDataAvailable: (coords: TileCoordinates) => boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface CanvasRef {
|
|
12
|
+
canvas: HTMLCanvasElement;
|
|
13
|
+
context: CanvasRenderingContext2D;
|
|
14
|
+
}
|
|
15
|
+
export interface DefaultHeightmapResourceOpts {
|
|
16
|
+
url?: string;
|
|
17
|
+
// Legacy option, use skipZoomLevels instead
|
|
18
|
+
skipOddLevels?: boolean;
|
|
19
|
+
skipZoomLevels?: [number] | ((z: number) => boolean);
|
|
20
|
+
maxZoom?: number;
|
|
21
|
+
tileSize?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class DefaultHeightmapResource implements HeightmapResource {
|
|
25
|
+
resource: Resource = null;
|
|
26
|
+
tileSize: number = 256;
|
|
27
|
+
maxZoom: number;
|
|
28
|
+
skipZoomLevel: (z: number) => boolean;
|
|
29
|
+
contextQueue: CanvasRef[];
|
|
30
|
+
|
|
31
|
+
constructor(opts: DefaultHeightmapResourceOpts = {}) {
|
|
32
|
+
if (opts.url) {
|
|
33
|
+
this.resource = new Resource({ url: opts.url });
|
|
34
|
+
}
|
|
35
|
+
this.skipZoomLevel = () => false;
|
|
36
|
+
if (opts.skipZoomLevels) {
|
|
37
|
+
if (Array.isArray(opts.skipZoomLevels)) {
|
|
38
|
+
const _skipZoomLevels = opts.skipZoomLevels as [number];
|
|
39
|
+
this.skipZoomLevel = (z: number) => _skipZoomLevels.includes(z);
|
|
40
|
+
} else {
|
|
41
|
+
this.skipZoomLevel = opts.skipZoomLevels;
|
|
42
|
+
}
|
|
43
|
+
} else if (opts.skipOddLevels) {
|
|
44
|
+
this.skipZoomLevel = (z: number) => z % 2 == 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
this.tileSize = opts.tileSize ?? 256;
|
|
48
|
+
this.maxZoom = opts.maxZoom ?? 15;
|
|
49
|
+
this.contextQueue = [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getCanvas(): CanvasRef {
|
|
53
|
+
let ctx = this.contextQueue.pop();
|
|
54
|
+
if (ctx == null) {
|
|
55
|
+
const canvas = document.createElement("canvas");
|
|
56
|
+
canvas.width = this.tileSize;
|
|
57
|
+
canvas.height = this.tileSize;
|
|
58
|
+
const context = canvas.getContext("2d");
|
|
59
|
+
ctx = {
|
|
60
|
+
canvas,
|
|
61
|
+
context,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return ctx;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
getPixels(img: HTMLImageElement | HTMLCanvasElement): ImageData {
|
|
68
|
+
const canvasRef = this.getCanvas();
|
|
69
|
+
const { context } = canvasRef;
|
|
70
|
+
//context.scale(1, -1);
|
|
71
|
+
// Chrome appears to vertically flip the image for reasons that are unclear
|
|
72
|
+
// We can make it work in Chrome by drawing the image upside-down at this step.
|
|
73
|
+
context.drawImage(img, 0, 0, this.tileSize, this.tileSize);
|
|
74
|
+
const pixels = context.getImageData(0, 0, this.tileSize, this.tileSize);
|
|
75
|
+
context.clearRect(0, 0, this.tileSize, this.tileSize);
|
|
76
|
+
this.contextQueue.push(canvasRef);
|
|
77
|
+
return pixels;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getTileResource(tileCoords: TileCoordinates) {
|
|
81
|
+
// reverseY for TMS tiling (https://gist.github.com/tmcw/4954720)
|
|
82
|
+
// See tiling schemes here: https://www.maptiler.com/google-maps-coordinates-tile-bounds-projection/
|
|
83
|
+
const { z, y } = tileCoords;
|
|
84
|
+
return this.resource.getDerivedResource({
|
|
85
|
+
templateValues: {
|
|
86
|
+
...tileCoords,
|
|
87
|
+
reverseY: Math.pow(2, z) - y - 1,
|
|
88
|
+
},
|
|
89
|
+
preserveQueryParameters: true,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
getTilePixels(coords: TileCoordinates): Promise<ImageData> | undefined {
|
|
94
|
+
const resource = this.getTileResource(coords);
|
|
95
|
+
const request = resource.fetchImage({
|
|
96
|
+
preferImageBitmap: false,
|
|
97
|
+
// @ts-ignore
|
|
98
|
+
retryAttempts: 3,
|
|
99
|
+
});
|
|
100
|
+
if (request == null) return undefined;
|
|
101
|
+
return request.then((img: HTMLImageElement | ImageBitmap) =>
|
|
102
|
+
// @ts-ignore
|
|
103
|
+
this.getPixels(img),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getTileDataAvailable({ z }) {
|
|
108
|
+
if (z == this.maxZoom) return true;
|
|
109
|
+
/* Weird hack:
|
|
110
|
+
For some reason, request render mode breaks if zoom 1 tiles are disabled.
|
|
111
|
+
So we have to make sure that we always report zoom 1 tiles as available.
|
|
112
|
+
*/
|
|
113
|
+
if (z < 2) return true;
|
|
114
|
+
if (this.skipZoomLevel(z)) return false;
|
|
115
|
+
if (z > this.maxZoom) return false;
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export default DefaultHeightmapResource;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Credit, Resource } from "cesium";
|
|
2
|
+
import {
|
|
3
|
+
DefaultHeightmapResource,
|
|
4
|
+
DefaultHeightmapResourceOpts,
|
|
5
|
+
} from "./heightmap-resource";
|
|
6
|
+
|
|
7
|
+
export enum ImageFormat {
|
|
8
|
+
WEBP = "webp",
|
|
9
|
+
PNG = "png",
|
|
10
|
+
PNGRAW = "pngraw",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type MapboxTerrainResourceOpts = {
|
|
14
|
+
highResolution?: boolean;
|
|
15
|
+
imageFormat?: ImageFormat;
|
|
16
|
+
accessToken?: string;
|
|
17
|
+
urlTemplate?: string;
|
|
18
|
+
} & DefaultHeightmapResourceOpts;
|
|
19
|
+
|
|
20
|
+
export class MapboxTerrainResource extends DefaultHeightmapResource {
|
|
21
|
+
resource: Resource = null;
|
|
22
|
+
credit = new Credit("Mapbox");
|
|
23
|
+
|
|
24
|
+
constructor(opts: MapboxTerrainResourceOpts = {}) {
|
|
25
|
+
super(opts);
|
|
26
|
+
const highResolution = opts.highResolution ?? false;
|
|
27
|
+
const format = opts.imageFormat ?? ImageFormat.WEBP;
|
|
28
|
+
const { urlTemplate } = opts;
|
|
29
|
+
|
|
30
|
+
// overrides based on highResolution flag
|
|
31
|
+
if (highResolution) {
|
|
32
|
+
if (opts.maxZoom === undefined) {
|
|
33
|
+
this.maxZoom = 14;
|
|
34
|
+
}
|
|
35
|
+
if (opts.tileSize === undefined) {
|
|
36
|
+
this.tileSize = 512;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const defaultURL = `https://api.mapbox.com/v4/mapbox.terrain-rgb/{z}/{x}/{y}${
|
|
41
|
+
highResolution ? "@2x" : ""
|
|
42
|
+
}.${format}`;
|
|
43
|
+
|
|
44
|
+
this.resource = new Resource({ url: urlTemplate ?? defaultURL });
|
|
45
|
+
if (opts.accessToken) {
|
|
46
|
+
this.resource.setQueryParameters({
|
|
47
|
+
access_token: opts.accessToken,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export default MapboxTerrainResource;
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import {
|
|
2
|
+
QuantizedMeshTerrainData,
|
|
3
|
+
Rectangle,
|
|
4
|
+
Ellipsoid,
|
|
5
|
+
Cartographic,
|
|
6
|
+
BoundingSphere,
|
|
7
|
+
OrientedBoundingBox,
|
|
8
|
+
Cartesian3,
|
|
9
|
+
Credit,
|
|
10
|
+
TilingScheme,
|
|
11
|
+
} from "cesium";
|
|
12
|
+
import {
|
|
13
|
+
TerrainWorkerOutput,
|
|
14
|
+
emptyMesh,
|
|
15
|
+
subsetByWindow,
|
|
16
|
+
TerrainUpscaleInput,
|
|
17
|
+
QuantizedMeshResult,
|
|
18
|
+
} from "./worker/worker-util";
|
|
19
|
+
|
|
20
|
+
export interface TileCoordinates {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
z: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface QuantizedMeshTerrainOptions {
|
|
27
|
+
quantizedVertices: Uint16Array;
|
|
28
|
+
indices: Uint16Array | Uint32Array;
|
|
29
|
+
minimumHeight: number;
|
|
30
|
+
maximumHeight: number;
|
|
31
|
+
boundingSphere: BoundingSphere;
|
|
32
|
+
orientedBoundingBox?: OrientedBoundingBox;
|
|
33
|
+
horizonOcclusionPoint: Cartesian3;
|
|
34
|
+
westIndices: number[];
|
|
35
|
+
southIndices: number[];
|
|
36
|
+
eastIndices: number[];
|
|
37
|
+
northIndices: number[];
|
|
38
|
+
westSkirtHeight: number;
|
|
39
|
+
southSkirtHeight: number;
|
|
40
|
+
eastSkirtHeight: number;
|
|
41
|
+
northSkirtHeight: number;
|
|
42
|
+
childTileMask?: number;
|
|
43
|
+
createdByUpsampling?: boolean;
|
|
44
|
+
encodedNormals?: Uint8Array;
|
|
45
|
+
waterMask?: Uint8Array;
|
|
46
|
+
credits?: Credit[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface TerrainMeshMeta {
|
|
50
|
+
errorLevel: number;
|
|
51
|
+
tileSize: number;
|
|
52
|
+
maxVertexDistance: number | null;
|
|
53
|
+
tileRect: Rectangle;
|
|
54
|
+
ellipsoid: Ellipsoid;
|
|
55
|
+
overscaleFactor: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createTerrainMesh(
|
|
59
|
+
data: TerrainWorkerOutput,
|
|
60
|
+
meta: TerrainMeshMeta,
|
|
61
|
+
) {
|
|
62
|
+
const {
|
|
63
|
+
minimumHeight,
|
|
64
|
+
maximumHeight,
|
|
65
|
+
quantizedVertices,
|
|
66
|
+
indices,
|
|
67
|
+
westIndices,
|
|
68
|
+
southIndices,
|
|
69
|
+
eastIndices,
|
|
70
|
+
northIndices,
|
|
71
|
+
quantizedHeights,
|
|
72
|
+
} = data;
|
|
73
|
+
|
|
74
|
+
const {
|
|
75
|
+
errorLevel,
|
|
76
|
+
tileSize,
|
|
77
|
+
maxVertexDistance,
|
|
78
|
+
tileRect,
|
|
79
|
+
ellipsoid,
|
|
80
|
+
overscaleFactor,
|
|
81
|
+
} = meta;
|
|
82
|
+
|
|
83
|
+
const err = errorLevel;
|
|
84
|
+
const skirtHeight = err * 20;
|
|
85
|
+
|
|
86
|
+
// Check if tileRect is not NaNs
|
|
87
|
+
if (isNaN(tileRect.east) || isNaN(tileRect.north)) {
|
|
88
|
+
throw new Error("Invalid tile rect");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const center = Rectangle.center(tileRect);
|
|
92
|
+
|
|
93
|
+
// Calculating occlusion height is kind of messy currently, but it definitely works
|
|
94
|
+
const halfAngle = tileRect.width / 2;
|
|
95
|
+
const dr = Math.cos(halfAngle); // half tile width since our ref point is at the center
|
|
96
|
+
|
|
97
|
+
let occlusionHeight = dr * ellipsoid.maximumRadius + maximumHeight;
|
|
98
|
+
if (halfAngle > Math.PI / 4) {
|
|
99
|
+
occlusionHeight = (1 + halfAngle) * ellipsoid.maximumRadius;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const occlusionPoint = new Cartographic(
|
|
103
|
+
center.longitude,
|
|
104
|
+
center.latitude,
|
|
105
|
+
occlusionHeight,
|
|
106
|
+
// Scaling factor of two just to be sure.
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const horizonOcclusionPoint = ellipsoid.transformPositionToScaledSpace(
|
|
110
|
+
Cartographic.toCartesian(occlusionPoint),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
let orientedBoundingBox = OrientedBoundingBox.fromRectangle(
|
|
114
|
+
tileRect,
|
|
115
|
+
minimumHeight,
|
|
116
|
+
maximumHeight,
|
|
117
|
+
ellipsoid,
|
|
118
|
+
);
|
|
119
|
+
let boundingSphere =
|
|
120
|
+
BoundingSphere.fromOrientedBoundingBox(orientedBoundingBox);
|
|
121
|
+
|
|
122
|
+
return new RasterTerrainData({
|
|
123
|
+
minimumHeight,
|
|
124
|
+
maximumHeight,
|
|
125
|
+
quantizedVertices,
|
|
126
|
+
indices,
|
|
127
|
+
boundingSphere,
|
|
128
|
+
orientedBoundingBox,
|
|
129
|
+
horizonOcclusionPoint,
|
|
130
|
+
westIndices,
|
|
131
|
+
southIndices,
|
|
132
|
+
eastIndices,
|
|
133
|
+
northIndices,
|
|
134
|
+
westSkirtHeight: skirtHeight,
|
|
135
|
+
southSkirtHeight: skirtHeight,
|
|
136
|
+
eastSkirtHeight: skirtHeight,
|
|
137
|
+
northSkirtHeight: skirtHeight,
|
|
138
|
+
childTileMask: 15,
|
|
139
|
+
createdByUpsampling: overscaleFactor > 0,
|
|
140
|
+
errorLevel: err,
|
|
141
|
+
maxVertexDistance,
|
|
142
|
+
tileSize,
|
|
143
|
+
quantizedHeights,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface EmptyMeshOptions {
|
|
148
|
+
tileRect: Rectangle;
|
|
149
|
+
tileCoord: TileCoordinates;
|
|
150
|
+
ellipsoid: Ellipsoid;
|
|
151
|
+
errorLevel: number;
|
|
152
|
+
tileSize: number;
|
|
153
|
+
maxVertexDistance?: number;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function createEmptyMesh(
|
|
157
|
+
opts: EmptyMeshOptions,
|
|
158
|
+
): QuantizedMeshTerrainData {
|
|
159
|
+
const { tileRect, tileCoord, errorLevel, ellipsoid, maxVertexDistance } =
|
|
160
|
+
opts;
|
|
161
|
+
const center = Rectangle.center(tileRect);
|
|
162
|
+
const { z } = tileCoord;
|
|
163
|
+
|
|
164
|
+
const latScalar = Math.min(Math.abs(Math.sin(center.latitude)), 0.995);
|
|
165
|
+
let v = Math.max(
|
|
166
|
+
Math.ceil((200 / (z + 1)) * Math.pow(1 - latScalar, 0.25)),
|
|
167
|
+
4,
|
|
168
|
+
);
|
|
169
|
+
const output = emptyMesh(v);
|
|
170
|
+
// We use zero for some undefined values
|
|
171
|
+
return createTerrainMesh(output, {
|
|
172
|
+
tileRect,
|
|
173
|
+
ellipsoid,
|
|
174
|
+
errorLevel,
|
|
175
|
+
overscaleFactor: 0,
|
|
176
|
+
maxVertexDistance,
|
|
177
|
+
tileSize: output.tileSize,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
interface RasterParams {
|
|
182
|
+
quantizedHeights?: Float32Array;
|
|
183
|
+
errorLevel: number;
|
|
184
|
+
maxVertexDistance: number;
|
|
185
|
+
tileSize: number;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
type RasterTerrainOptions = QuantizedMeshTerrainOptions & RasterParams;
|
|
189
|
+
|
|
190
|
+
class UpsampleTracker {
|
|
191
|
+
ne: boolean;
|
|
192
|
+
nw: boolean;
|
|
193
|
+
se: boolean;
|
|
194
|
+
sw: boolean;
|
|
195
|
+
|
|
196
|
+
constructor() {
|
|
197
|
+
this.ne = false;
|
|
198
|
+
this.nw = false;
|
|
199
|
+
this.se = false;
|
|
200
|
+
this.sw = false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
finished() {
|
|
204
|
+
return this.ne && this.nw && this.se && this.sw;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export class RasterTerrainData
|
|
209
|
+
extends QuantizedMeshTerrainData
|
|
210
|
+
implements RasterParams
|
|
211
|
+
{
|
|
212
|
+
quantizedHeights: Float32Array;
|
|
213
|
+
errorLevel: number;
|
|
214
|
+
maxVertexDistance: number;
|
|
215
|
+
tileSize: number;
|
|
216
|
+
private upsampleTracker: UpsampleTracker;
|
|
217
|
+
|
|
218
|
+
constructor(opts: RasterTerrainOptions) {
|
|
219
|
+
super(opts);
|
|
220
|
+
this.quantizedHeights = opts.quantizedHeights;
|
|
221
|
+
this.errorLevel = opts.errorLevel;
|
|
222
|
+
this.maxVertexDistance = opts.maxVertexDistance ?? opts.tileSize;
|
|
223
|
+
this.tileSize = opts.tileSize;
|
|
224
|
+
this.upsampleTracker = new UpsampleTracker();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
upsample(
|
|
228
|
+
tilingScheme: TilingScheme,
|
|
229
|
+
thisX: number,
|
|
230
|
+
thisY: number,
|
|
231
|
+
thisLevel: number,
|
|
232
|
+
descendantX: number,
|
|
233
|
+
descendantY: number,
|
|
234
|
+
descendantLevel: number,
|
|
235
|
+
) {
|
|
236
|
+
if (this.quantizedHeights == null) {
|
|
237
|
+
return super.upsample(
|
|
238
|
+
tilingScheme,
|
|
239
|
+
thisX,
|
|
240
|
+
thisY,
|
|
241
|
+
thisLevel,
|
|
242
|
+
descendantX,
|
|
243
|
+
descendantY,
|
|
244
|
+
descendantLevel,
|
|
245
|
+
);
|
|
246
|
+
} // Something wonky about our tiling scheme, perhaps
|
|
247
|
+
// 12/2215/2293 @2x
|
|
248
|
+
//const url = `https://a.tiles.mapbox.com/v4/mapbox.terrain-rgb/${z}/${x}/${y}${hires}.${this.format}?access_token=${this.accessToken}`;
|
|
249
|
+
|
|
250
|
+
const x = descendantX;
|
|
251
|
+
const y = descendantY;
|
|
252
|
+
const z = descendantLevel;
|
|
253
|
+
|
|
254
|
+
const tile = `${z}/${x}/${y}`;
|
|
255
|
+
|
|
256
|
+
//console.log(`Upsampling terrain data from zoom ${thisLevel} to ` + tile);
|
|
257
|
+
|
|
258
|
+
const dz = z - thisLevel;
|
|
259
|
+
|
|
260
|
+
const scalar = Math.pow(2, dz);
|
|
261
|
+
|
|
262
|
+
const ellipsoid = tilingScheme.ellipsoid;
|
|
263
|
+
|
|
264
|
+
const err = this.errorLevel / scalar;
|
|
265
|
+
|
|
266
|
+
const maxVertexDistance = Math.min(
|
|
267
|
+
this.maxVertexDistance * scalar,
|
|
268
|
+
this.tileSize,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const upscaledX = thisX * scalar;
|
|
272
|
+
const upscaledY = thisY * scalar;
|
|
273
|
+
|
|
274
|
+
const dx: number = x - upscaledX;
|
|
275
|
+
const dy: number = y - upscaledY;
|
|
276
|
+
|
|
277
|
+
const x0 = (dx * this.tileSize) / scalar;
|
|
278
|
+
const x1 = ((dx + 1) * this.tileSize) / scalar;
|
|
279
|
+
const y0 = (dy * this.tileSize) / scalar;
|
|
280
|
+
const y1 = ((dy + 1) * this.tileSize) / scalar;
|
|
281
|
+
|
|
282
|
+
const window = { x0, x1, y0, y1 };
|
|
283
|
+
|
|
284
|
+
const res = buildOverscaledTerrainTile({
|
|
285
|
+
tilingScheme,
|
|
286
|
+
heightData: subsetByWindow(this.quantizedHeights, window, true),
|
|
287
|
+
maxVertexDistance,
|
|
288
|
+
x,
|
|
289
|
+
y,
|
|
290
|
+
z,
|
|
291
|
+
errorLevel: err,
|
|
292
|
+
ellipsoidRadius: ellipsoid.maximumRadius,
|
|
293
|
+
tileSize: x1 - x0,
|
|
294
|
+
overscaleFactor: dz,
|
|
295
|
+
});
|
|
296
|
+
if (dz == 1) {
|
|
297
|
+
// If we've got a single child tile, we can track that we've upsampled the parent.
|
|
298
|
+
const quadrant = getQuadrant(dx as 0 | 1, dy as 0 | 1);
|
|
299
|
+
this.upsampleTracker[quadrant] = true;
|
|
300
|
+
}
|
|
301
|
+
if (this.upsampleTracker.finished()) {
|
|
302
|
+
// We've upsampled all child tiles and don't need to keep terrain data around anymore.
|
|
303
|
+
this.quantizedHeights = undefined;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return res;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function getQuadrant(dx: 0 | 1, dy: 0 | 1): "ne" | "nw" | "se" | "sw" {
|
|
311
|
+
if (dx == 0 && dy == 0) return "sw";
|
|
312
|
+
if (dx == 0 && dy == 1) return "nw";
|
|
313
|
+
if (dx == 1 && dy == 0) return "se";
|
|
314
|
+
if (dx == 1 && dy == 1) return "ne";
|
|
315
|
+
throw new Error("Invalid quadrant");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
type OverscaleTerrainOptions = TerrainUpscaleInput & {
|
|
319
|
+
tilingScheme: TilingScheme;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
async function buildOverscaledTerrainTile(opts: OverscaleTerrainOptions) {
|
|
323
|
+
const { tilingScheme, overscaleFactor, ...workerOpts } = opts;
|
|
324
|
+
|
|
325
|
+
const { x, y, z } = workerOpts;
|
|
326
|
+
|
|
327
|
+
const tileRect = tilingScheme.tileXYToRectangle(x, y, z);
|
|
328
|
+
const ellipsoid = tilingScheme.ellipsoid;
|
|
329
|
+
|
|
330
|
+
const { errorLevel, maxVertexDistance, tileSize } = workerOpts;
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
const res = (await upsamplerFarm.scheduleTask(workerOpts, [
|
|
334
|
+
workerOpts.heightData.buffer,
|
|
335
|
+
])) as QuantizedMeshResult;
|
|
336
|
+
|
|
337
|
+
return createTerrainMesh(res, {
|
|
338
|
+
tileRect,
|
|
339
|
+
ellipsoid,
|
|
340
|
+
errorLevel,
|
|
341
|
+
overscaleFactor,
|
|
342
|
+
tileSize,
|
|
343
|
+
// Maximum vertex distance
|
|
344
|
+
maxVertexDistance,
|
|
345
|
+
});
|
|
346
|
+
} catch (err) {
|
|
347
|
+
return createEmptyMesh({
|
|
348
|
+
tileRect,
|
|
349
|
+
errorLevel,
|
|
350
|
+
ellipsoid,
|
|
351
|
+
tileCoord: { x, y, z },
|
|
352
|
+
tileSize: 0,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
import UpsamplerWorker from "web-worker:./worker/upsampler-worker";
|
|
358
|
+
import WorkerFarm from "./worker/worker-farm";
|
|
359
|
+
|
|
360
|
+
const upsamplerFarm = new WorkerFarm({
|
|
361
|
+
worker: new UpsamplerWorker(),
|
|
362
|
+
maxWorkers: 5,
|
|
363
|
+
});
|