map-zero 0.1.0

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +220 -0
  4. package/docs/api.md +66 -0
  5. package/docs/architecture.md +87 -0
  6. package/docs/cartography.md +77 -0
  7. package/docs/cesium.md +107 -0
  8. package/docs/openlayers.md +98 -0
  9. package/docs/styles.md +103 -0
  10. package/package.json +51 -0
  11. package/packages/cesium/package.json +13 -0
  12. package/packages/cesium/src/index.js +405 -0
  13. package/packages/ol/package.json +14 -0
  14. package/packages/ol/src/index.js +1705 -0
  15. package/packages/ol/src/labels.js +977 -0
  16. package/src/3dtiles/b3dm.js +38 -0
  17. package/src/3dtiles/clipper-surfaces.js +317 -0
  18. package/src/3dtiles/export.js +768 -0
  19. package/src/3dtiles/extrude.js +301 -0
  20. package/src/3dtiles/flat.js +531 -0
  21. package/src/3dtiles/glb.js +178 -0
  22. package/src/3dtiles/gpkg-buildings.js +240 -0
  23. package/src/3dtiles/gpkg-features.js +157 -0
  24. package/src/3dtiles/tileset.js +75 -0
  25. package/src/build.js +134 -0
  26. package/src/cli.js +656 -0
  27. package/src/export-pmtiles.js +962 -0
  28. package/src/geometry-read.js +50 -0
  29. package/src/gpkg-read.js +460 -0
  30. package/src/gpkg.js +567 -0
  31. package/src/html.js +593 -0
  32. package/src/layers.js +357 -0
  33. package/src/manifest.js +29 -0
  34. package/src/mvt.js +2593 -0
  35. package/src/ol.js +5 -0
  36. package/src/osm.js +2110 -0
  37. package/src/pmtiles-worker.js +70 -0
  38. package/src/pmtiles.js +260 -0
  39. package/src/server.js +720 -0
  40. package/src/style-command.js +78 -0
  41. package/src/style-filters.js +76 -0
  42. package/src/style-presets.js +93 -0
  43. package/src/style-themes.js +235 -0
  44. package/src/style.js +13 -0
  45. package/src/tile-cache.js +59 -0
  46. package/src/utils.js +222 -0
  47. package/styles/presets/light.json +4655 -0
  48. package/styles/presets/monochrome.json +4655 -0
  49. package/styles/presets/neon-dark-3d.json +90 -0
  50. package/styles/presets/neon-dark.json +4690 -0
  51. package/styles/presets/tactical.json +4690 -0
  52. package/styles/themes/neon-dark.theme.json +20 -0
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Wrap a GLB buffer in a minimal valid B3DM container.
3
+ *
4
+ * @param {Buffer} glb
5
+ * @returns {Buffer}
6
+ */
7
+ export function buildB3dm(glb) {
8
+ const featureTableJson = padJsonForSection({ BATCH_LENGTH: 0 }, 28);
9
+ const header = Buffer.alloc(28);
10
+ header.write('b3dm', 0, 4, 'ascii');
11
+ header.writeUInt32LE(1, 4);
12
+ header.writeUInt32LE(28 + featureTableJson.length + glb.length, 8);
13
+ header.writeUInt32LE(featureTableJson.length, 12);
14
+ header.writeUInt32LE(0, 16);
15
+ header.writeUInt32LE(0, 20);
16
+ header.writeUInt32LE(0, 24);
17
+ return Buffer.concat([header, featureTableJson, glb]);
18
+ }
19
+
20
+ /**
21
+ * @param {unknown} value
22
+ * @param {number} sectionOffset
23
+ * @returns {Buffer}
24
+ */
25
+ function padJsonForSection(value, sectionOffset) {
26
+ const buffer = Buffer.from(JSON.stringify(value), 'utf8');
27
+ const targetLength = align(sectionOffset + buffer.length, 8) - sectionOffset;
28
+ return Buffer.concat([buffer, Buffer.alloc(targetLength - buffer.length, 0x20)]);
29
+ }
30
+
31
+ /**
32
+ * @param {number} value
33
+ * @param {number} alignment
34
+ * @returns {number}
35
+ */
36
+ function align(value, alignment) {
37
+ return Math.ceil(value / alignment) * alignment;
38
+ }
@@ -0,0 +1,317 @@
1
+ import earcut from 'earcut';
2
+ import {
3
+ EndType,
4
+ JoinType,
5
+ NativeClipperLibRequestedFormat,
6
+ loadNativeClipperLibInstanceAsync
7
+ } from 'js-angusj-clipper';
8
+
9
+ import { wgs84SurfaceNormal, wgs84ToEcef } from './extrude.js';
10
+
11
+ const DEFAULT_SCALE = 100;
12
+ const DEFAULT_ARC_TOLERANCE_METERS = 0.25;
13
+ const DEFAULT_CLEAN_DISTANCE_METERS = 0.03;
14
+ let clipperPromise;
15
+
16
+ /**
17
+ * Build a flat 3D mesh from linework using Clipper's open-path offsetter.
18
+ *
19
+ * This is intended to replace custom ribbon joins/caps for exported Cesium
20
+ * cartography. It produces dissolved polygonal road surfaces that can be
21
+ * triangulated and written to 3D Tiles.
22
+ *
23
+ * @param {Array<Array<[number, number]>>} lines
24
+ * @param {{
25
+ * widthMeters: number,
26
+ * height?: number,
27
+ * scale?: number,
28
+ * arcToleranceMeters?: number,
29
+ * cleanDistanceMeters?: number,
30
+ * minSegmentMeters?: number,
31
+ * miterLimit?: number
32
+ * }} options
33
+ * @returns {Promise<{
34
+ * positions: Float32Array,
35
+ * normals: Float32Array,
36
+ * indices: Uint16Array,
37
+ * min: [number, number, number],
38
+ * max: [number, number, number],
39
+ * bbox: [number, number, number, number],
40
+ * maxHeight: number,
41
+ * featureCount: number
42
+ * } | null>}
43
+ */
44
+ export async function buildClipperLineSurfaceMesh(lines, options) {
45
+ const cleanLines = lines.map(cleanLine).filter((line) => line.length >= 2);
46
+ if (cleanLines.length === 0) return null;
47
+
48
+ const clipper = await getClipper();
49
+ const scale = positiveNumber(options.scale, DEFAULT_SCALE);
50
+ const projection = createLocalProjection(cleanLines.flat());
51
+ const openPaths = [];
52
+ const closedPaths = [];
53
+ const minSegmentMeters = positiveNumber(options.minSegmentMeters, 0.05);
54
+
55
+ for (const line of cleanLines) {
56
+ const local = removeRedundantLocalPoints(line.map((point) => projectPoint(point, projection)), minSegmentMeters);
57
+ if (local.length < 2) continue;
58
+ const closed = local.length > 2 && samePoint(local[0], local[local.length - 1]);
59
+ const path = local.slice(0, closed ? -1 : undefined).map((point) => toIntPoint(point, scale));
60
+ if (path.length < (closed ? 3 : 2)) continue;
61
+ if (closed) closedPaths.push(path);
62
+ else openPaths.push(path);
63
+ }
64
+
65
+ const offsetInputs = [];
66
+ if (openPaths.length > 0) {
67
+ offsetInputs.push({
68
+ data: openPaths,
69
+ joinType: JoinType.Round,
70
+ endType: EndType.OpenRound
71
+ });
72
+ }
73
+ if (closedPaths.length > 0) {
74
+ offsetInputs.push({
75
+ data: closedPaths,
76
+ joinType: JoinType.Round,
77
+ endType: EndType.ClosedLine
78
+ });
79
+ }
80
+ if (offsetInputs.length === 0) return null;
81
+
82
+ const offsetTree = clipper.offsetToPolyTree({
83
+ delta: options.widthMeters * scale / 2,
84
+ arcTolerance: positiveNumber(options.arcToleranceMeters, DEFAULT_ARC_TOLERANCE_METERS) * scale,
85
+ miterLimit: positiveNumber(options.miterLimit, 2),
86
+ offsetInputs
87
+ });
88
+
89
+ if (!offsetTree || offsetTree.total === 0) return null;
90
+
91
+ const polygons = collectPolyTreePolygons(offsetTree);
92
+ return buildSurfaceMeshFromClipperPolygons(polygons, projection, scale, options.height ?? 1);
93
+ }
94
+
95
+ function getClipper() {
96
+ clipperPromise ??= loadNativeClipperLibInstanceAsync(
97
+ NativeClipperLibRequestedFormat.WasmWithAsmJsFallback
98
+ );
99
+ return clipperPromise;
100
+ }
101
+
102
+ function collectPolyTreePolygons(polyTree) {
103
+ const polygons = [];
104
+ for (const child of polyTree.childs) {
105
+ collectNode(child, polygons);
106
+ }
107
+ return polygons;
108
+ }
109
+
110
+ function collectNode(node, polygons) {
111
+ if (node.isOpen) return;
112
+ if (!node.isHole) {
113
+ const holes = node.childs.filter((child) => child.isHole && !child.isOpen);
114
+ polygons.push([node.contour, ...holes.map((child) => child.contour)]);
115
+ for (const hole of holes) {
116
+ for (const nested of hole.childs) {
117
+ collectNode(nested, polygons);
118
+ }
119
+ }
120
+ } else {
121
+ for (const child of node.childs) {
122
+ collectNode(child, polygons);
123
+ }
124
+ }
125
+ }
126
+
127
+ function buildSurfaceMeshFromClipperPolygons(polygons, projection, scale, height) {
128
+ const positions = [];
129
+ const normals = [];
130
+ const indices = [];
131
+ const bboxes = [];
132
+ let featureCount = 0;
133
+
134
+ for (const polygon of polygons) {
135
+ const vertices = [];
136
+ const holes = [];
137
+ const localPoints = [];
138
+ let cursor = 0;
139
+
140
+ for (let ringIndex = 0; ringIndex < polygon.length; ringIndex++) {
141
+ const ring = cleanPathRing(polygon[ringIndex], scale);
142
+ if (ring.length < 3) continue;
143
+ if (ringIndex > 0) holes.push(cursor);
144
+ for (const point of ring) {
145
+ vertices.push(point[0], point[1]);
146
+ localPoints.push(point);
147
+ cursor++;
148
+ }
149
+ }
150
+
151
+ if (vertices.length < 6) continue;
152
+ const triangles = earcut(vertices, holes, 2);
153
+ if (triangles.length === 0) continue;
154
+
155
+ const lonLatPoints = localPoints.map((point) => unprojectPoint(point, projection));
156
+ const centroid = polygonCentroid(lonLatPoints);
157
+ const normal = wgs84SurfaceNormal(centroid[0], centroid[1]);
158
+ const ecef = lonLatPoints.map(([lon, lat]) => wgs84ToEcef(lon, lat, height));
159
+ const vertexOffset = positions.length / 3;
160
+
161
+ for (const point of ecef) {
162
+ positions.push(...point);
163
+ normals.push(...normal);
164
+ }
165
+ for (const index of triangles) {
166
+ indices.push(vertexOffset + index);
167
+ }
168
+ bboxes.push(lineBbox(lonLatPoints));
169
+ featureCount++;
170
+ }
171
+
172
+ if (positions.length === 0 || featureCount === 0) return null;
173
+ const positionArray = new Float32Array(positions);
174
+ const normalArray = new Float32Array(normals);
175
+ const indexArray = positionArray.length / 3 > 65535
176
+ ? new Uint32Array(indices)
177
+ : new Uint16Array(indices);
178
+ const bounds = minMaxVec3(positionArray);
179
+ return {
180
+ positions: positionArray,
181
+ normals: normalArray,
182
+ indices: indexArray,
183
+ min: bounds.min,
184
+ max: bounds.max,
185
+ bbox: mergeBboxes(bboxes),
186
+ maxHeight: height,
187
+ featureCount
188
+ };
189
+ }
190
+
191
+ function cleanPathRing(path, scale) {
192
+ const ring = [];
193
+ for (const point of path) {
194
+ const local = [point.x / scale, point.y / scale];
195
+ const last = ring[ring.length - 1];
196
+ if (!last || !samePoint(last, local)) {
197
+ ring.push(local);
198
+ }
199
+ }
200
+ if (ring.length > 1 && samePoint(ring[0], ring[ring.length - 1])) {
201
+ ring.pop();
202
+ }
203
+ return ring;
204
+ }
205
+
206
+ function cleanLine(line) {
207
+ return line
208
+ .map((point) => [Number(point?.[0]), Number(point?.[1])])
209
+ .filter(([lon, lat]) => Number.isFinite(lon) && Number.isFinite(lat));
210
+ }
211
+
212
+ function removeRedundantLocalPoints(points, minDistance) {
213
+ const out = [];
214
+ for (const point of points) {
215
+ const last = out[out.length - 1];
216
+ if (!last || distance2(last, point) >= minDistance) {
217
+ out.push(point);
218
+ }
219
+ }
220
+ return out;
221
+ }
222
+
223
+ function createLocalProjection(points) {
224
+ const origin = polygonCentroid(points);
225
+ const meanLat = origin[1] * Math.PI / 180;
226
+ return {
227
+ origin,
228
+ metersPerLon: Math.max(1, 111320 * Math.cos(meanLat)),
229
+ metersPerLat: 110540
230
+ };
231
+ }
232
+
233
+ function projectPoint(point, projection) {
234
+ return [
235
+ (point[0] - projection.origin[0]) * projection.metersPerLon,
236
+ (point[1] - projection.origin[1]) * projection.metersPerLat
237
+ ];
238
+ }
239
+
240
+ function unprojectPoint(point, projection) {
241
+ return [
242
+ projection.origin[0] + point[0] / projection.metersPerLon,
243
+ projection.origin[1] + point[1] / projection.metersPerLat
244
+ ];
245
+ }
246
+
247
+ function toIntPoint(point, scale) {
248
+ return {
249
+ x: Math.round(point[0] * scale),
250
+ y: Math.round(point[1] * scale)
251
+ };
252
+ }
253
+
254
+ function polygonCentroid(points) {
255
+ let lon = 0;
256
+ let lat = 0;
257
+ for (const point of points) {
258
+ lon += point[0];
259
+ lat += point[1];
260
+ }
261
+ return [lon / points.length, lat / points.length];
262
+ }
263
+
264
+ function lineBbox(points) {
265
+ let minLon = Infinity;
266
+ let minLat = Infinity;
267
+ let maxLon = -Infinity;
268
+ let maxLat = -Infinity;
269
+ for (const [lon, lat] of points) {
270
+ minLon = Math.min(minLon, lon);
271
+ minLat = Math.min(minLat, lat);
272
+ maxLon = Math.max(maxLon, lon);
273
+ maxLat = Math.max(maxLat, lat);
274
+ }
275
+ return [minLon, minLat, maxLon, maxLat];
276
+ }
277
+
278
+ function mergeBboxes(bboxes) {
279
+ let minLon = Infinity;
280
+ let minLat = Infinity;
281
+ let maxLon = -Infinity;
282
+ let maxLat = -Infinity;
283
+ for (const bbox of bboxes) {
284
+ minLon = Math.min(minLon, bbox[0]);
285
+ minLat = Math.min(minLat, bbox[1]);
286
+ maxLon = Math.max(maxLon, bbox[2]);
287
+ maxLat = Math.max(maxLat, bbox[3]);
288
+ }
289
+ return [minLon, minLat, maxLon, maxLat];
290
+ }
291
+
292
+ function minMaxVec3(positions) {
293
+ const min = [Infinity, Infinity, Infinity];
294
+ const max = [-Infinity, -Infinity, -Infinity];
295
+ for (let i = 0; i < positions.length; i += 3) {
296
+ min[0] = Math.min(min[0], positions[i]);
297
+ min[1] = Math.min(min[1], positions[i + 1]);
298
+ min[2] = Math.min(min[2], positions[i + 2]);
299
+ max[0] = Math.max(max[0], positions[i]);
300
+ max[1] = Math.max(max[1], positions[i + 1]);
301
+ max[2] = Math.max(max[2], positions[i + 2]);
302
+ }
303
+ return { min, max };
304
+ }
305
+
306
+ function samePoint(a, b) {
307
+ return Math.abs(a[0] - b[0]) < 1e-9 && Math.abs(a[1] - b[1]) < 1e-9;
308
+ }
309
+
310
+ function distance2(a, b) {
311
+ return Math.hypot(a[0] - b[0], a[1] - b[1]);
312
+ }
313
+
314
+ function positiveNumber(value, fallback) {
315
+ const number = Number(value);
316
+ return Number.isFinite(number) && number > 0 ? number : fallback;
317
+ }