map-zero 0.1.0 → 0.2.1
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/CHANGELOG.md +14 -0
- package/README.md +31 -2
- package/docs/cesium.md +24 -9
- package/package.json +4 -3
- package/packages/cesium/package.json +6 -3
- package/packages/cesium/src/imagery-worker.js +604 -0
- package/packages/cesium/src/imagery.js +434 -0
- package/packages/cesium/src/index.js +199 -35
- package/packages/ol/package.json +1 -1
- package/packages/ol/src/index.js +349 -16
- package/src/3dtiles/b3dm.js +18 -2
- package/src/3dtiles/clipper-surfaces.js +121 -20
- package/src/3dtiles/export.js +298 -25
- package/src/3dtiles/extrude.js +78 -23
- package/src/3dtiles/flat.js +8 -20
- package/src/3dtiles/glb.js +78 -27
- package/src/3dtiles/gpkg-features.js +4 -4
- package/src/3dtiles/precision.js +47 -0
- package/src/cli.js +100 -2
- package/src/export-pmtiles.js +17 -16
- package/src/from-bbox.js +335 -0
- package/src/gpkg-read.js +15 -2
- package/src/html.js +28 -17
- package/src/manifest.js +1 -8
- package/src/mvt.js +35 -3
- package/src/package.js +343 -0
- package/src/server.js +38 -10
- package/src/style-command.js +1 -1
- package/src/style-filters.js +2 -3
- package/styles/presets/neon-dark-3d.json +0 -90
package/src/3dtiles/extrude.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import earcut from 'earcut';
|
|
2
|
+
import { localizeEcefPositions } from './precision.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Converts 2D building footprints in lon/lat coordinates into localized ECEF
|
|
6
|
+
* meshes suitable for Cesium 3D Tiles. This module owns the building-specific
|
|
7
|
+
* extrusion path: roof/bottom triangulation, wall generation, normals, and
|
|
8
|
+
* WGS84 coordinate conversion.
|
|
9
|
+
*/
|
|
2
10
|
|
|
3
11
|
const WGS84_A = 6378137.0;
|
|
4
12
|
const WGS84_F = 1 / 298.257223563;
|
|
@@ -12,6 +20,7 @@ const WGS84_E2 = WGS84_F * (2 - WGS84_F);
|
|
|
12
20
|
* indices: Uint16Array | Uint32Array,
|
|
13
21
|
* min: [number, number, number],
|
|
14
22
|
* max: [number, number, number],
|
|
23
|
+
* rtcCenter: [number, number, number],
|
|
15
24
|
* bbox: [number, number, number, number],
|
|
16
25
|
* maxHeight: number,
|
|
17
26
|
* featureCount: number
|
|
@@ -19,7 +28,11 @@ const WGS84_E2 = WGS84_F * (2 - WGS84_F);
|
|
|
19
28
|
*/
|
|
20
29
|
|
|
21
30
|
/**
|
|
22
|
-
* Build one
|
|
31
|
+
* Build one localized mesh from many independent building footprints.
|
|
32
|
+
*
|
|
33
|
+
* Each footprint is extruded separately, then all vertices are merged before
|
|
34
|
+
* being shifted around a shared RTC center. The shared center keeps float32
|
|
35
|
+
* positions precise enough for Cesium at city scale.
|
|
23
36
|
*
|
|
24
37
|
* @param {Footprint[]} footprints
|
|
25
38
|
* @param {{ baseHeight?: number }} [options]
|
|
@@ -55,19 +68,19 @@ export function buildMergedExtrudedPolygonMesh(footprints, options = {}) {
|
|
|
55
68
|
return null;
|
|
56
69
|
}
|
|
57
70
|
|
|
58
|
-
const
|
|
71
|
+
const localized = localizeEcefPositions(positions);
|
|
59
72
|
const normalArray = new Float32Array(normals);
|
|
60
|
-
const indexArray =
|
|
73
|
+
const indexArray = localized.positions.length / 3 > 65535
|
|
61
74
|
? new Uint32Array(indices)
|
|
62
75
|
: new Uint16Array(indices);
|
|
63
|
-
const bounds = minMaxVec3(positionArray);
|
|
64
76
|
|
|
65
77
|
return {
|
|
66
|
-
positions:
|
|
78
|
+
positions: localized.positions,
|
|
67
79
|
normals: normalArray,
|
|
68
80
|
indices: indexArray,
|
|
69
|
-
min:
|
|
70
|
-
max:
|
|
81
|
+
min: localized.min,
|
|
82
|
+
max: localized.max,
|
|
83
|
+
rtcCenter: localized.rtcCenter,
|
|
71
84
|
bbox: mergeBboxes(bboxes),
|
|
72
85
|
maxHeight,
|
|
73
86
|
featureCount
|
|
@@ -75,6 +88,12 @@ export function buildMergedExtrudedPolygonMesh(footprints, options = {}) {
|
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
/**
|
|
91
|
+
* Extrude one footprint ring into roof, bottom, and wall triangles.
|
|
92
|
+
*
|
|
93
|
+
* The input ring is expected in lon/lat degrees. Heights are meters above the
|
|
94
|
+
* ellipsoid. The returned positions remain absolute ECEF numbers; localization
|
|
95
|
+
* happens in buildMergedExtrudedPolygonMesh.
|
|
96
|
+
*
|
|
78
97
|
* @param {Array<[number, number]>} ring
|
|
79
98
|
* @param {number} baseHeight
|
|
80
99
|
* @param {number} topHeight
|
|
@@ -137,6 +156,9 @@ export function buildExtrudedPolygonMesh(ring, baseHeight, topHeight) {
|
|
|
137
156
|
}
|
|
138
157
|
|
|
139
158
|
/**
|
|
159
|
+
* Normalize a polygon ring to finite lon/lat pairs and remove the repeated
|
|
160
|
+
* closing coordinate when present.
|
|
161
|
+
*
|
|
140
162
|
* @param {Array<[number, number]>} ring
|
|
141
163
|
* @returns {Array<[number, number]>}
|
|
142
164
|
*/
|
|
@@ -162,6 +184,9 @@ export function cleanRing(ring) {
|
|
|
162
184
|
}
|
|
163
185
|
|
|
164
186
|
/**
|
|
187
|
+
* Project lon/lat points around a local centroid into an approximate meter
|
|
188
|
+
* plane for earcut triangulation.
|
|
189
|
+
*
|
|
165
190
|
* @param {Array<[number, number]>} points
|
|
166
191
|
* @param {[number, number]} centroid
|
|
167
192
|
* @returns {{ flat: number[] }}
|
|
@@ -178,6 +203,8 @@ function projectRing(points, centroid) {
|
|
|
178
203
|
}
|
|
179
204
|
|
|
180
205
|
/**
|
|
206
|
+
* Return a simple arithmetic lon/lat centroid for small local rings.
|
|
207
|
+
*
|
|
181
208
|
* @param {Array<[number, number]>} points
|
|
182
209
|
* @returns {[number, number]}
|
|
183
210
|
*/
|
|
@@ -192,6 +219,9 @@ function polygonCentroid(points) {
|
|
|
192
219
|
}
|
|
193
220
|
|
|
194
221
|
/**
|
|
222
|
+
* Compute a lon/lat bbox for a footprint and pad it slightly so child tile
|
|
223
|
+
* bounding volumes are not exactly coplanar with feature edges.
|
|
224
|
+
*
|
|
195
225
|
* @param {Array<[number, number]>} points
|
|
196
226
|
* @returns {[number, number, number, number]}
|
|
197
227
|
*/
|
|
@@ -211,6 +241,8 @@ function polygonBbox(points) {
|
|
|
211
241
|
}
|
|
212
242
|
|
|
213
243
|
/**
|
|
244
|
+
* Merge multiple lon/lat bounding boxes.
|
|
245
|
+
*
|
|
214
246
|
* @param {Array<[number, number, number, number]>} bboxes
|
|
215
247
|
* @returns {[number, number, number, number]}
|
|
216
248
|
*/
|
|
@@ -229,31 +261,31 @@ function mergeBboxes(bboxes) {
|
|
|
229
261
|
}
|
|
230
262
|
|
|
231
263
|
/**
|
|
232
|
-
*
|
|
233
|
-
*
|
|
264
|
+
* Compute a unit normal from three ECEF triangle vertices.
|
|
265
|
+
*
|
|
266
|
+
* @param {[number, number, number]} a
|
|
267
|
+
* @param {[number, number, number]} b
|
|
268
|
+
* @param {[number, number, number]} c
|
|
269
|
+
* @returns {[number, number, number]}
|
|
234
270
|
*/
|
|
235
|
-
function minMaxVec3(positions) {
|
|
236
|
-
const min = [Infinity, Infinity, Infinity];
|
|
237
|
-
const max = [-Infinity, -Infinity, -Infinity];
|
|
238
|
-
for (let i = 0; i < positions.length; i += 3) {
|
|
239
|
-
min[0] = Math.min(min[0], positions[i]);
|
|
240
|
-
min[1] = Math.min(min[1], positions[i + 1]);
|
|
241
|
-
min[2] = Math.min(min[2], positions[i + 2]);
|
|
242
|
-
max[0] = Math.max(max[0], positions[i]);
|
|
243
|
-
max[1] = Math.max(max[1], positions[i + 1]);
|
|
244
|
-
max[2] = Math.max(max[2], positions[i + 2]);
|
|
245
|
-
}
|
|
246
|
-
return { min, max };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
271
|
function triangleNormal(a, b, c) {
|
|
250
272
|
return normalize(cross(subtract(b, a), subtract(c, a)));
|
|
251
273
|
}
|
|
252
274
|
|
|
275
|
+
/**
|
|
276
|
+
* @param {[number, number, number]} a
|
|
277
|
+
* @param {[number, number, number]} b
|
|
278
|
+
* @returns {[number, number, number]}
|
|
279
|
+
*/
|
|
253
280
|
function subtract(a, b) {
|
|
254
281
|
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
255
282
|
}
|
|
256
283
|
|
|
284
|
+
/**
|
|
285
|
+
* @param {[number, number, number]} a
|
|
286
|
+
* @param {[number, number, number]} b
|
|
287
|
+
* @returns {[number, number, number]}
|
|
288
|
+
*/
|
|
257
289
|
function cross(a, b) {
|
|
258
290
|
return [
|
|
259
291
|
a[1] * b[2] - a[2] * b[1],
|
|
@@ -262,6 +294,10 @@ function cross(a, b) {
|
|
|
262
294
|
];
|
|
263
295
|
}
|
|
264
296
|
|
|
297
|
+
/**
|
|
298
|
+
* @param {[number, number, number]} vector
|
|
299
|
+
* @returns {[number, number, number]}
|
|
300
|
+
*/
|
|
265
301
|
function normalize(vector) {
|
|
266
302
|
const length = Math.hypot(vector[0], vector[1], vector[2]);
|
|
267
303
|
if (!Number.isFinite(length) || length === 0) {
|
|
@@ -270,6 +306,13 @@ function normalize(vector) {
|
|
|
270
306
|
return [vector[0] / length, vector[1] / length, vector[2] / length];
|
|
271
307
|
}
|
|
272
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Return the geodetic up vector for a lon/lat coordinate.
|
|
311
|
+
*
|
|
312
|
+
* @param {number} lonDeg
|
|
313
|
+
* @param {number} latDeg
|
|
314
|
+
* @returns {[number, number, number]}
|
|
315
|
+
*/
|
|
273
316
|
export function wgs84SurfaceNormal(lonDeg, latDeg) {
|
|
274
317
|
const lonRad = degToRad(lonDeg);
|
|
275
318
|
const latRad = degToRad(latDeg);
|
|
@@ -281,6 +324,14 @@ export function wgs84SurfaceNormal(lonDeg, latDeg) {
|
|
|
281
324
|
];
|
|
282
325
|
}
|
|
283
326
|
|
|
327
|
+
/**
|
|
328
|
+
* Convert lon/lat/height to Earth-Centered, Earth-Fixed coordinates.
|
|
329
|
+
*
|
|
330
|
+
* @param {number} lonDeg
|
|
331
|
+
* @param {number} latDeg
|
|
332
|
+
* @param {number} h
|
|
333
|
+
* @returns {[number, number, number]}
|
|
334
|
+
*/
|
|
284
335
|
export function wgs84ToEcef(lonDeg, latDeg, h) {
|
|
285
336
|
const lonRad = degToRad(lonDeg);
|
|
286
337
|
const latRad = degToRad(latDeg);
|
|
@@ -296,6 +347,10 @@ export function wgs84ToEcef(lonDeg, latDeg, h) {
|
|
|
296
347
|
];
|
|
297
348
|
}
|
|
298
349
|
|
|
350
|
+
/**
|
|
351
|
+
* @param {number} value
|
|
352
|
+
* @returns {number}
|
|
353
|
+
*/
|
|
299
354
|
function degToRad(value) {
|
|
300
355
|
return value * Math.PI / 180;
|
|
301
356
|
}
|
package/src/3dtiles/flat.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import earcut from 'earcut';
|
|
2
2
|
|
|
3
3
|
import { cleanRing, wgs84SurfaceNormal, wgs84ToEcef } from './extrude.js';
|
|
4
|
+
import { localizeEcefPositions } from './precision.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* @typedef {{
|
|
@@ -9,6 +10,7 @@ import { cleanRing, wgs84SurfaceNormal, wgs84ToEcef } from './extrude.js';
|
|
|
9
10
|
* indices: Uint16Array,
|
|
10
11
|
* min: [number, number, number],
|
|
11
12
|
* max: [number, number, number],
|
|
13
|
+
* rtcCenter: [number, number, number],
|
|
12
14
|
* bbox: [number, number, number, number],
|
|
13
15
|
* maxHeight: number,
|
|
14
16
|
* featureCount: number
|
|
@@ -176,20 +178,20 @@ function finishMesh(positions, normals, bboxes, height, featureCount, indices =
|
|
|
176
178
|
return null;
|
|
177
179
|
}
|
|
178
180
|
|
|
179
|
-
const
|
|
181
|
+
const localized = localizeEcefPositions(positions);
|
|
180
182
|
const normalArray = new Float32Array(normals);
|
|
181
183
|
const indexArray = indices.length > 0
|
|
182
|
-
?
|
|
184
|
+
? localized.positions.length / 3 > 65535
|
|
183
185
|
? new Uint32Array(indices)
|
|
184
186
|
: new Uint16Array(indices)
|
|
185
187
|
: new Uint16Array(0);
|
|
186
|
-
const bounds = minMaxVec3(positionArray);
|
|
187
188
|
return {
|
|
188
|
-
positions:
|
|
189
|
+
positions: localized.positions,
|
|
189
190
|
normals: normalArray,
|
|
190
191
|
indices: indexArray,
|
|
191
|
-
min:
|
|
192
|
-
max:
|
|
192
|
+
min: localized.min,
|
|
193
|
+
max: localized.max,
|
|
194
|
+
rtcCenter: localized.rtcCenter,
|
|
193
195
|
bbox: mergeBboxes(bboxes),
|
|
194
196
|
maxHeight: height,
|
|
195
197
|
featureCount
|
|
@@ -515,17 +517,3 @@ function mergeBboxes(bboxes) {
|
|
|
515
517
|
}
|
|
516
518
|
return [minLon, minLat, maxLon, maxLat];
|
|
517
519
|
}
|
|
518
|
-
|
|
519
|
-
function minMaxVec3(positions) {
|
|
520
|
-
const min = [Infinity, Infinity, Infinity];
|
|
521
|
-
const max = [-Infinity, -Infinity, -Infinity];
|
|
522
|
-
for (let i = 0; i < positions.length; i += 3) {
|
|
523
|
-
min[0] = Math.min(min[0], positions[i]);
|
|
524
|
-
min[1] = Math.min(min[1], positions[i + 1]);
|
|
525
|
-
min[2] = Math.min(min[2], positions[i + 2]);
|
|
526
|
-
max[0] = Math.max(max[0], positions[i]);
|
|
527
|
-
max[1] = Math.max(max[1], positions[i + 1]);
|
|
528
|
-
max[2] = Math.max(max[2], positions[i + 2]);
|
|
529
|
-
}
|
|
530
|
-
return { min, max };
|
|
531
|
-
}
|
package/src/3dtiles/glb.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* Minimal glTF/GLB writer used by the 3D Tiles exporter.
|
|
3
|
+
*
|
|
4
|
+
* It receives already-built mesh buffers and writes a single-scene, single-mesh
|
|
5
|
+
* GLB. The writer intentionally stays small: no animations, textures, feature
|
|
6
|
+
* tables, or material variants. Cesium styling is applied outside the GLB when
|
|
7
|
+
* possible.
|
|
8
|
+
*
|
|
2
9
|
* @typedef {{
|
|
3
10
|
* positions: Float32Array,
|
|
4
11
|
* normals?: Float32Array,
|
|
@@ -9,19 +16,19 @@
|
|
|
9
16
|
*/
|
|
10
17
|
|
|
11
18
|
/**
|
|
12
|
-
* Build a minimal
|
|
19
|
+
* Build a minimal GLB for one merged mesh.
|
|
13
20
|
*
|
|
14
21
|
* @param {Mesh} mesh
|
|
15
|
-
* @param {{ color?: [number, number, number, number], generator?: string, includeNormals?: boolean }} [options]
|
|
22
|
+
* @param {{ color?: [number, number, number, number], generator?: string, includeNormals?: boolean, quantizeNormals?: boolean, doubleSided?: boolean, unlit?: boolean }} [options]
|
|
16
23
|
* @returns {Buffer}
|
|
17
24
|
*/
|
|
18
25
|
export function buildGlbFromMesh(mesh, options = {}) {
|
|
19
|
-
const
|
|
26
|
+
const binBuilder = { chunks: [], byteLength: 0 };
|
|
20
27
|
const bufferViews = [];
|
|
21
28
|
const accessors = [];
|
|
22
29
|
const attributes = {};
|
|
23
30
|
|
|
24
|
-
const positionView = appendBuffer(
|
|
31
|
+
const positionView = appendBuffer(binBuilder, bufferFromTypedArray(mesh.positions), 4);
|
|
25
32
|
bufferViews.push({ buffer: 0, byteOffset: positionView.byteOffset, byteLength: positionView.byteLength, target: 34962 });
|
|
26
33
|
attributes.POSITION = accessors.length;
|
|
27
34
|
accessors.push({
|
|
@@ -38,22 +45,26 @@ export function buildGlbFromMesh(mesh, options = {}) {
|
|
|
38
45
|
&& mesh.normals
|
|
39
46
|
&& mesh.normals.length === mesh.positions.length;
|
|
40
47
|
if (includeNormals) {
|
|
41
|
-
const
|
|
48
|
+
const normals = options.quantizeNormals === false
|
|
49
|
+
? mesh.normals
|
|
50
|
+
: quantizeNormals(mesh.normals);
|
|
51
|
+
const normalView = appendBuffer(binBuilder, bufferFromTypedArray(normals), 4);
|
|
42
52
|
bufferViews.push({ buffer: 0, byteOffset: normalView.byteOffset, byteLength: normalView.byteLength, target: 34962 });
|
|
43
53
|
attributes.NORMAL = accessors.length;
|
|
44
54
|
accessors.push({
|
|
45
55
|
bufferView: bufferViews.length - 1,
|
|
46
56
|
byteOffset: 0,
|
|
47
|
-
componentType: 5126,
|
|
48
|
-
count:
|
|
49
|
-
type: 'VEC3'
|
|
57
|
+
componentType: normals instanceof Float32Array ? 5126 : 5120,
|
|
58
|
+
count: normals.length / 3,
|
|
59
|
+
type: 'VEC3',
|
|
60
|
+
...(normals instanceof Float32Array ? {} : { normalized: true })
|
|
50
61
|
});
|
|
51
62
|
}
|
|
52
63
|
|
|
53
64
|
const hasIndices = mesh.indices.length > 0;
|
|
54
65
|
let indexAccessor = null;
|
|
55
66
|
if (hasIndices) {
|
|
56
|
-
const indexView = appendBuffer(
|
|
67
|
+
const indexView = appendBuffer(binBuilder, bufferFromTypedArray(mesh.indices), 4);
|
|
57
68
|
bufferViews.push({ buffer: 0, byteOffset: indexView.byteOffset, byteLength: indexView.byteLength, target: 34963 });
|
|
58
69
|
indexAccessor = accessors.length;
|
|
59
70
|
accessors.push({
|
|
@@ -65,7 +76,7 @@ export function buildGlbFromMesh(mesh, options = {}) {
|
|
|
65
76
|
});
|
|
66
77
|
}
|
|
67
78
|
|
|
68
|
-
const bin = Buffer.concat(chunks);
|
|
79
|
+
const bin = Buffer.concat(binBuilder.chunks);
|
|
69
80
|
const primitive = {
|
|
70
81
|
attributes,
|
|
71
82
|
material: 0
|
|
@@ -74,38 +85,47 @@ export function buildGlbFromMesh(mesh, options = {}) {
|
|
|
74
85
|
primitive.indices = indexAccessor;
|
|
75
86
|
}
|
|
76
87
|
|
|
88
|
+
const unlit = options.unlit !== false;
|
|
89
|
+
const material = {
|
|
90
|
+
pbrMetallicRoughness: {
|
|
91
|
+
baseColorFactor: options.color ?? [0, 1, 1, 1],
|
|
92
|
+
metallicFactor: 0,
|
|
93
|
+
roughnessFactor: 1
|
|
94
|
+
},
|
|
95
|
+
doubleSided: options.doubleSided === true
|
|
96
|
+
};
|
|
97
|
+
if (unlit) {
|
|
98
|
+
material.extensions = {
|
|
99
|
+
KHR_materials_unlit: {}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
77
103
|
const gltf = {
|
|
78
104
|
asset: {
|
|
79
105
|
version: '2.0',
|
|
80
106
|
generator: options.generator ?? 'map-zero'
|
|
81
107
|
},
|
|
82
|
-
extensionsUsed: ['KHR_materials_unlit'],
|
|
83
108
|
scene: 0,
|
|
84
109
|
scenes: [{ nodes: [0] }],
|
|
85
110
|
nodes: [{ mesh: 0 }],
|
|
86
111
|
meshes: [{
|
|
87
112
|
primitives: [primitive]
|
|
88
113
|
}],
|
|
89
|
-
materials: [
|
|
90
|
-
pbrMetallicRoughness: {
|
|
91
|
-
baseColorFactor: options.color ?? [0, 1, 1, 1],
|
|
92
|
-
metallicFactor: 0,
|
|
93
|
-
roughnessFactor: 1
|
|
94
|
-
},
|
|
95
|
-
extensions: {
|
|
96
|
-
KHR_materials_unlit: {}
|
|
97
|
-
},
|
|
98
|
-
doubleSided: true
|
|
99
|
-
}],
|
|
114
|
+
materials: [material],
|
|
100
115
|
buffers: [{ byteLength: bin.length }],
|
|
101
116
|
bufferViews,
|
|
102
117
|
accessors
|
|
103
118
|
};
|
|
119
|
+
if (unlit) {
|
|
120
|
+
gltf.extensionsUsed = ['KHR_materials_unlit'];
|
|
121
|
+
}
|
|
104
122
|
|
|
105
123
|
return buildGlb(gltf, bin);
|
|
106
124
|
}
|
|
107
125
|
|
|
108
126
|
/**
|
|
127
|
+
* Convert any typed array view into a Buffer over the same underlying memory.
|
|
128
|
+
*
|
|
109
129
|
* @param {ArrayBufferView} typedArray
|
|
110
130
|
* @returns {Buffer}
|
|
111
131
|
*/
|
|
@@ -114,6 +134,25 @@ function bufferFromTypedArray(typedArray) {
|
|
|
114
134
|
}
|
|
115
135
|
|
|
116
136
|
/**
|
|
137
|
+
* Store unit normals as normalized signed bytes. This keeps the lighting
|
|
138
|
+
* direction while cutting the normal attribute from 12 bytes to 3 bytes per
|
|
139
|
+
* vertex, which matters for dense building tiles.
|
|
140
|
+
*
|
|
141
|
+
* @param {Float32Array} normals
|
|
142
|
+
* @returns {Int8Array}
|
|
143
|
+
*/
|
|
144
|
+
function quantizeNormals(normals) {
|
|
145
|
+
const quantized = new Int8Array(normals.length);
|
|
146
|
+
for (let i = 0; i < normals.length; i++) {
|
|
147
|
+
const value = Math.max(-1, Math.min(1, Number.isFinite(normals[i]) ? normals[i] : 0));
|
|
148
|
+
quantized[i] = Math.round(value * 127);
|
|
149
|
+
}
|
|
150
|
+
return quantized;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Serialize a glTF JSON document and binary payload into GLB v2 layout.
|
|
155
|
+
*
|
|
117
156
|
* @param {Record<string, unknown>} gltf
|
|
118
157
|
* @param {Buffer} binBuffer
|
|
119
158
|
* @returns {Buffer}
|
|
@@ -136,19 +175,27 @@ export function buildGlb(gltf, binBuffer) {
|
|
|
136
175
|
}
|
|
137
176
|
|
|
138
177
|
/**
|
|
139
|
-
*
|
|
178
|
+
* Append one binary buffer to the BIN chunk with the requested alignment.
|
|
179
|
+
*
|
|
180
|
+
* Tracking byteLength avoids repeatedly scanning all previous chunks while
|
|
181
|
+
* assembling large city tiles.
|
|
182
|
+
*
|
|
183
|
+
* @param {{ chunks: Buffer[], byteLength: number }} builder
|
|
140
184
|
* @param {Buffer} buffer
|
|
141
185
|
* @param {number} alignment
|
|
142
186
|
* @returns {{ byteOffset: number, byteLength: number }}
|
|
143
187
|
*/
|
|
144
|
-
function appendBuffer(
|
|
145
|
-
const byteOffset =
|
|
188
|
+
function appendBuffer(builder, buffer, alignment) {
|
|
189
|
+
const byteOffset = builder.byteLength;
|
|
146
190
|
const paddedOffset = align(byteOffset, alignment);
|
|
147
191
|
if (paddedOffset > byteOffset) {
|
|
148
|
-
|
|
192
|
+
const padding = Buffer.alloc(paddedOffset - byteOffset);
|
|
193
|
+
builder.chunks.push(padding);
|
|
194
|
+
builder.byteLength += padding.length;
|
|
149
195
|
}
|
|
150
196
|
|
|
151
|
-
chunks.push(buffer);
|
|
197
|
+
builder.chunks.push(buffer);
|
|
198
|
+
builder.byteLength += buffer.length;
|
|
152
199
|
return {
|
|
153
200
|
byteOffset: paddedOffset,
|
|
154
201
|
byteLength: buffer.length
|
|
@@ -156,6 +203,8 @@ function appendBuffer(chunks, buffer, alignment) {
|
|
|
156
203
|
}
|
|
157
204
|
|
|
158
205
|
/**
|
|
206
|
+
* Pad a GLB chunk to the required byte alignment.
|
|
207
|
+
*
|
|
159
208
|
* @param {Buffer} buffer
|
|
160
209
|
* @param {number} alignment
|
|
161
210
|
* @param {number} fill
|
|
@@ -169,6 +218,8 @@ function padBuffer(buffer, alignment, fill) {
|
|
|
169
218
|
}
|
|
170
219
|
|
|
171
220
|
/**
|
|
221
|
+
* Round a byte length up to the next multiple of alignment.
|
|
222
|
+
*
|
|
172
223
|
* @param {number} value
|
|
173
224
|
* @param {number} alignment
|
|
174
225
|
* @returns {number}
|
|
@@ -24,14 +24,14 @@ const LAYER_ALIASES = {
|
|
|
24
24
|
*/
|
|
25
25
|
export function readLayerMetadata(db, manifest, layerId) {
|
|
26
26
|
const manifestLayer = Array.isArray(manifest.layers)
|
|
27
|
-
? manifest.layers.find((layer) => layer
|
|
28
|
-
manifest.layers.find((layer) => layer
|
|
27
|
+
? manifest.layers.find((layer) => layer === layerId) ??
|
|
28
|
+
manifest.layers.find((layer) => layer === LAYER_ALIASES[layerId])
|
|
29
29
|
: null;
|
|
30
|
-
if (!manifestLayer
|
|
30
|
+
if (!manifestLayer) {
|
|
31
31
|
throw new Error(`manifest does not contain layer: ${layerId}`);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const table = String(manifestLayer
|
|
34
|
+
const table = String(manifestLayer);
|
|
35
35
|
const geometry = db.prepare(`
|
|
36
36
|
SELECT column_name
|
|
37
37
|
FROM gpkg_geometry_columns
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert absolute ECEF positions to local tile coordinates before storing them
|
|
3
|
+
* as Float32. Absolute ECEF values are around millions of meters, where Float32
|
|
4
|
+
* precision is too coarse for flat terrain meshes.
|
|
5
|
+
*
|
|
6
|
+
* @param {number[]} positions
|
|
7
|
+
* @returns {{ positions: Float32Array, min: [number, number, number], max: [number, number, number], rtcCenter: [number, number, number] }}
|
|
8
|
+
*/
|
|
9
|
+
export function localizeEcefPositions(positions) {
|
|
10
|
+
const absoluteBounds = minMaxVec3(positions);
|
|
11
|
+
const rtcCenter = [
|
|
12
|
+
(absoluteBounds.min[0] + absoluteBounds.max[0]) / 2,
|
|
13
|
+
(absoluteBounds.min[1] + absoluteBounds.max[1]) / 2,
|
|
14
|
+
(absoluteBounds.min[2] + absoluteBounds.max[2]) / 2
|
|
15
|
+
];
|
|
16
|
+
const local = new Float32Array(positions.length);
|
|
17
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
18
|
+
local[i] = positions[i] - rtcCenter[0];
|
|
19
|
+
local[i + 1] = positions[i + 1] - rtcCenter[1];
|
|
20
|
+
local[i + 2] = positions[i + 2] - rtcCenter[2];
|
|
21
|
+
}
|
|
22
|
+
const localBounds = minMaxVec3(local);
|
|
23
|
+
return {
|
|
24
|
+
positions: local,
|
|
25
|
+
min: localBounds.min,
|
|
26
|
+
max: localBounds.max,
|
|
27
|
+
rtcCenter
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {ArrayLike<number>} positions
|
|
33
|
+
* @returns {{ min: [number, number, number], max: [number, number, number] }}
|
|
34
|
+
*/
|
|
35
|
+
export function minMaxVec3(positions) {
|
|
36
|
+
const min = [Infinity, Infinity, Infinity];
|
|
37
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
38
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
39
|
+
min[0] = Math.min(min[0], positions[i]);
|
|
40
|
+
min[1] = Math.min(min[1], positions[i + 1]);
|
|
41
|
+
min[2] = Math.min(min[2], positions[i + 2]);
|
|
42
|
+
max[0] = Math.max(max[0], positions[i]);
|
|
43
|
+
max[1] = Math.max(max[1], positions[i + 1]);
|
|
44
|
+
max[2] = Math.max(max[2], positions[i + 2]);
|
|
45
|
+
}
|
|
46
|
+
return { min, max };
|
|
47
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -6,7 +6,9 @@ import { Command, InvalidArgumentError } from 'commander';
|
|
|
6
6
|
import { buildPackage } from './build.js';
|
|
7
7
|
import { export3dTiles } from './3dtiles/export.js';
|
|
8
8
|
import { exportPmtiles } from './export-pmtiles.js';
|
|
9
|
+
import { createPackageFromBbox } from './from-bbox.js';
|
|
9
10
|
import { LAYER_ALIASES, SUPPORTED_LAYERS } from './layers.js';
|
|
11
|
+
import { packageMapZero } from './package.js';
|
|
10
12
|
import { serveMapZero } from './server.js';
|
|
11
13
|
import { availableStylePresets, availableStyleThemes, writePackageStyle } from './style-command.js';
|
|
12
14
|
import { parseBbox, parseLayerList } from './utils.js';
|
|
@@ -55,6 +57,77 @@ program
|
|
|
55
57
|
}
|
|
56
58
|
});
|
|
57
59
|
|
|
60
|
+
program
|
|
61
|
+
.command('from-bbox')
|
|
62
|
+
.description('Download an OSM extract and build a complete map-zero package for a bbox.')
|
|
63
|
+
.requiredOption('--bbox <bbox>', 'minLon,minLat,maxLon,maxLat', parseBboxOption)
|
|
64
|
+
.requiredOption('--out <output.mapzero>', 'output package folder')
|
|
65
|
+
.option('--layers <layers>', 'comma-separated logical layers; defaults to all supported layers', parseLayersOption)
|
|
66
|
+
.option('--minzoom <zoom>', 'minimum PMTiles zoom to export', parseZoomOption, 8)
|
|
67
|
+
.option('--maxzoom <zoom>', 'maximum PMTiles zoom to export', parseZoomOption, 16)
|
|
68
|
+
.option('--workers <count>', 'parallel PMTiles tile generation workers', parsePositiveIntegerOption, 1)
|
|
69
|
+
.option('--force-pmtiles', 'allow very large PMTiles exports')
|
|
70
|
+
.option('--cache-dir <dir>', 'OSM extract cache directory; defaults to ~/.cache/map-zero/osm')
|
|
71
|
+
.option('--provider-index-url <url>', 'Geofabrik-compatible index URL')
|
|
72
|
+
.option('--force-download', 're-download the selected OSM extract even if cached')
|
|
73
|
+
.option('--batch-size <count>', 'geometry build batch size', parsePositiveIntegerOption, 5000)
|
|
74
|
+
.option('--keep-temp', 'keep the temporary SQLite build database')
|
|
75
|
+
.option('--debug-build', 'show build memory usage in progress logs')
|
|
76
|
+
.option('--no-pmtiles', 'skip PMTiles export')
|
|
77
|
+
.option('--no-3dtiles', 'skip 3D Tiles export')
|
|
78
|
+
.option('--no-zip', 'skip portable zip creation')
|
|
79
|
+
.option('--include-gpkg', 'include data.gpkg in the portable zip')
|
|
80
|
+
.action(async (options) => {
|
|
81
|
+
const buildProgress = createBuildProgressReporter();
|
|
82
|
+
try {
|
|
83
|
+
const result = await createPackageFromBbox({
|
|
84
|
+
bbox: options.bbox,
|
|
85
|
+
out: options.out,
|
|
86
|
+
layers: options.layers ?? [...SUPPORTED_LAYERS],
|
|
87
|
+
minZoom: options.minzoom,
|
|
88
|
+
maxZoom: options.maxzoom,
|
|
89
|
+
workers: options.workers,
|
|
90
|
+
forcePmtiles: Boolean(options.forcePmtiles),
|
|
91
|
+
cacheDir: options.cacheDir,
|
|
92
|
+
providerIndexUrl: options.providerIndexUrl,
|
|
93
|
+
forceDownload: Boolean(options.forceDownload),
|
|
94
|
+
batchSize: options.batchSize,
|
|
95
|
+
keepTemp: Boolean(options.keepTemp),
|
|
96
|
+
debugBuild: Boolean(options.debugBuild),
|
|
97
|
+
pmtiles: options.pmtiles,
|
|
98
|
+
tiles3d: options.tiles3d,
|
|
99
|
+
zip: options.zip,
|
|
100
|
+
includeGpkg: Boolean(options.includeGpkg),
|
|
101
|
+
onStage(message) {
|
|
102
|
+
console.log(message);
|
|
103
|
+
},
|
|
104
|
+
onBuildProgress: buildProgress.update,
|
|
105
|
+
onPmtilesProgress: reportPmtilesProgress,
|
|
106
|
+
on3dTilesProgress: report3dTilesProgress
|
|
107
|
+
});
|
|
108
|
+
buildProgress.finish();
|
|
109
|
+
|
|
110
|
+
console.log(`Built ${result.outDir}`);
|
|
111
|
+
console.log(` source: ${result.source.name} (${result.source.path})`);
|
|
112
|
+
for (const [layer, count] of Object.entries(result.counts)) {
|
|
113
|
+
console.log(` ${layer}: ${count}`);
|
|
114
|
+
}
|
|
115
|
+
if (result.pmtiles) {
|
|
116
|
+
console.log(` PMTiles: ${result.pmtiles.outPath} (${formatBytes(result.pmtiles.outputBytes)})`);
|
|
117
|
+
}
|
|
118
|
+
if (result.tiles3d) {
|
|
119
|
+
console.log(` 3D Tiles: ${result.tiles3d.tilesetPath} (${formatBytes(result.tiles3d.outputBytes)})`);
|
|
120
|
+
}
|
|
121
|
+
if (result.zip) {
|
|
122
|
+
console.log(` ZIP: ${result.zip.outPath} (${formatBytes(result.zip.outputBytes)})`);
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
buildProgress.finish();
|
|
126
|
+
console.error(`map-zero: ${error instanceof Error ? error.message : String(error)}`);
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
58
131
|
program
|
|
59
132
|
.command('pmtiles')
|
|
60
133
|
.description('Export a .mapzero package to a static vector PMTiles archive.')
|
|
@@ -92,8 +165,8 @@ program
|
|
|
92
165
|
.argument('<package.mapzero>', 'map-zero package folder')
|
|
93
166
|
.option('--out <dir>', 'output 3D Tiles folder; defaults to <package>/3dtiles')
|
|
94
167
|
.option('--layers <layers>', 'comma-separated 3D layers; defaults to all supported 3D layers', parse3dTilesLayersOption)
|
|
95
|
-
.option('--max-depth <count>', 'quadtree depth for building tiles', parseNonNegativeIntegerOption,
|
|
96
|
-
.option('--max-features <count>', 'maximum features per leaf tile before subdivision', parsePositiveIntegerOption,
|
|
168
|
+
.option('--max-depth <count>', 'quadtree depth for building tiles', parseNonNegativeIntegerOption, 8)
|
|
169
|
+
.option('--max-features <count>', 'maximum features per leaf tile before subdivision', parsePositiveIntegerOption, 1500)
|
|
97
170
|
.option('--default-height <meters>', 'fallback building height in meters', parsePositiveNumberOption, 8)
|
|
98
171
|
.action(async (packageDir, options) => {
|
|
99
172
|
try {
|
|
@@ -152,6 +225,31 @@ program
|
|
|
152
225
|
}
|
|
153
226
|
});
|
|
154
227
|
|
|
228
|
+
program
|
|
229
|
+
.command('package')
|
|
230
|
+
.description('Create a portable zip with manifest, styles, PMTiles, and 3D Tiles.')
|
|
231
|
+
.argument('<package.mapzero>', 'map-zero package folder')
|
|
232
|
+
.option('--out <file.zip>', 'output zip path; defaults to <package>.zip')
|
|
233
|
+
.option('--include-gpkg', 'include the source GeoPackage in the zip')
|
|
234
|
+
.action(async (packageDir, options) => {
|
|
235
|
+
try {
|
|
236
|
+
const result = await packageMapZero({
|
|
237
|
+
packageDir,
|
|
238
|
+
out: options.out,
|
|
239
|
+
includeGpkg: Boolean(options.includeGpkg)
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
console.log(`Packaged ${result.outPath}`);
|
|
243
|
+
console.log(` files: ${formatInteger(result.fileCount)}`);
|
|
244
|
+
console.log(` input size: ${formatBytes(result.inputBytes)}`);
|
|
245
|
+
console.log(` zip size: ${formatBytes(result.outputBytes)}`);
|
|
246
|
+
console.log(` data.gpkg: ${result.includedGpkg ? 'included' : 'excluded'}`);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.error(`map-zero: ${error instanceof Error ? error.message : String(error)}`);
|
|
249
|
+
process.exitCode = 1;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
155
253
|
program
|
|
156
254
|
.command('serve')
|
|
157
255
|
.description('Serve a .mapzero package with a readonly HTTP API and OpenLayers viewer.')
|