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.
@@ -7,6 +7,14 @@ import {
7
7
  } from 'js-angusj-clipper';
8
8
 
9
9
  import { wgs84SurfaceNormal, wgs84ToEcef } from './extrude.js';
10
+ import { localizeEcefPositions } from './precision.js';
11
+
12
+ /**
13
+ * Builds flat elevated cartographic surfaces from linework by offsetting lines
14
+ * with Clipper, dissolving joins/caps, and triangulating the resulting polygons.
15
+ * This path is used for roads, railways, boundaries, and aviation linework where
16
+ * hand-built ribbons produced visible join artifacts.
17
+ */
10
18
 
11
19
  const DEFAULT_SCALE = 100;
12
20
  const DEFAULT_ARC_TOLERANCE_METERS = 0.25;
@@ -92,6 +100,11 @@ export async function buildClipperLineSurfaceMesh(lines, options) {
92
100
  return buildSurfaceMeshFromClipperPolygons(polygons, projection, scale, options.height ?? 1);
93
101
  }
94
102
 
103
+ /**
104
+ * Lazily load the Clipper WASM/ASM implementation once per process.
105
+ *
106
+ * @returns {Promise<any>}
107
+ */
95
108
  function getClipper() {
96
109
  clipperPromise ??= loadNativeClipperLibInstanceAsync(
97
110
  NativeClipperLibRequestedFormat.WasmWithAsmJsFallback
@@ -99,6 +112,12 @@ function getClipper() {
99
112
  return clipperPromise;
100
113
  }
101
114
 
115
+ /**
116
+ * Flatten a Clipper PolyTree into polygon rings, preserving first-level holes.
117
+ *
118
+ * @param {any} polyTree
119
+ * @returns {Array<Array<Array<{ x: number, y: number }>>>}
120
+ */
102
121
  function collectPolyTreePolygons(polyTree) {
103
122
  const polygons = [];
104
123
  for (const child of polyTree.childs) {
@@ -107,6 +126,12 @@ function collectPolyTreePolygons(polyTree) {
107
126
  return polygons;
108
127
  }
109
128
 
129
+ /**
130
+ * Recursively collect closed non-hole nodes and their hole contours.
131
+ *
132
+ * @param {any} node
133
+ * @param {Array<Array<Array<{ x: number, y: number }>>>} polygons
134
+ */
110
135
  function collectNode(node, polygons) {
111
136
  if (node.isOpen) return;
112
137
  if (!node.isHole) {
@@ -124,6 +149,15 @@ function collectNode(node, polygons) {
124
149
  }
125
150
  }
126
151
 
152
+ /**
153
+ * Convert integer Clipper polygon rings back into localized ECEF mesh data.
154
+ *
155
+ * @param {Array<Array<Array<{ x: number, y: number }>>>} polygons
156
+ * @param {{ origin: [number, number], metersPerLon: number, metersPerLat: number }} projection
157
+ * @param {number} scale
158
+ * @param {number} height
159
+ * @returns {import('./extrude.js').ExtrudedMesh | null}
160
+ */
127
161
  function buildSurfaceMeshFromClipperPolygons(polygons, projection, scale, height) {
128
162
  const positions = [];
129
163
  const normals = [];
@@ -170,24 +204,32 @@ function buildSurfaceMeshFromClipperPolygons(polygons, projection, scale, height
170
204
  }
171
205
 
172
206
  if (positions.length === 0 || featureCount === 0) return null;
173
- const positionArray = new Float32Array(positions);
207
+ const localized = localizeEcefPositions(positions);
174
208
  const normalArray = new Float32Array(normals);
175
- const indexArray = positionArray.length / 3 > 65535
209
+ const indexArray = localized.positions.length / 3 > 65535
176
210
  ? new Uint32Array(indices)
177
211
  : new Uint16Array(indices);
178
- const bounds = minMaxVec3(positionArray);
179
212
  return {
180
- positions: positionArray,
213
+ positions: localized.positions,
181
214
  normals: normalArray,
182
215
  indices: indexArray,
183
- min: bounds.min,
184
- max: bounds.max,
216
+ min: localized.min,
217
+ max: localized.max,
218
+ rtcCenter: localized.rtcCenter,
185
219
  bbox: mergeBboxes(bboxes),
186
220
  maxHeight: height,
187
221
  featureCount
188
222
  };
189
223
  }
190
224
 
225
+ /**
226
+ * Convert a Clipper path to local meter coordinates and remove duplicate close
227
+ * points before triangulation.
228
+ *
229
+ * @param {Array<{ x: number, y: number }>} path
230
+ * @param {number} scale
231
+ * @returns {Array<[number, number]>}
232
+ */
191
233
  function cleanPathRing(path, scale) {
192
234
  const ring = [];
193
235
  for (const point of path) {
@@ -203,12 +245,25 @@ function cleanPathRing(path, scale) {
203
245
  return ring;
204
246
  }
205
247
 
248
+ /**
249
+ * Normalize a lon/lat line to finite coordinate pairs.
250
+ *
251
+ * @param {Array<[number, number]>} line
252
+ * @returns {Array<[number, number]>}
253
+ */
206
254
  function cleanLine(line) {
207
255
  return line
208
256
  .map((point) => [Number(point?.[0]), Number(point?.[1])])
209
257
  .filter(([lon, lat]) => Number.isFinite(lon) && Number.isFinite(lat));
210
258
  }
211
259
 
260
+ /**
261
+ * Drop adjacent projected points that are closer than minDistance meters.
262
+ *
263
+ * @param {Array<[number, number]>} points
264
+ * @param {number} minDistance
265
+ * @returns {Array<[number, number]>}
266
+ */
212
267
  function removeRedundantLocalPoints(points, minDistance) {
213
268
  const out = [];
214
269
  for (const point of points) {
@@ -220,6 +275,12 @@ function removeRedundantLocalPoints(points, minDistance) {
220
275
  return out;
221
276
  }
222
277
 
278
+ /**
279
+ * Create a local lon/lat <-> meter projection centered on the data extent.
280
+ *
281
+ * @param {Array<[number, number]>} points
282
+ * @returns {{ origin: [number, number], metersPerLon: number, metersPerLat: number }}
283
+ */
223
284
  function createLocalProjection(points) {
224
285
  const origin = polygonCentroid(points);
225
286
  const meanLat = origin[1] * Math.PI / 180;
@@ -230,6 +291,13 @@ function createLocalProjection(points) {
230
291
  };
231
292
  }
232
293
 
294
+ /**
295
+ * Project one lon/lat point into local meters.
296
+ *
297
+ * @param {[number, number]} point
298
+ * @param {{ origin: [number, number], metersPerLon: number, metersPerLat: number }} projection
299
+ * @returns {[number, number]}
300
+ */
233
301
  function projectPoint(point, projection) {
234
302
  return [
235
303
  (point[0] - projection.origin[0]) * projection.metersPerLon,
@@ -237,6 +305,13 @@ function projectPoint(point, projection) {
237
305
  ];
238
306
  }
239
307
 
308
+ /**
309
+ * Convert local meters back to lon/lat.
310
+ *
311
+ * @param {[number, number]} point
312
+ * @param {{ origin: [number, number], metersPerLon: number, metersPerLat: number }} projection
313
+ * @returns {[number, number]}
314
+ */
240
315
  function unprojectPoint(point, projection) {
241
316
  return [
242
317
  projection.origin[0] + point[0] / projection.metersPerLon,
@@ -244,6 +319,13 @@ function unprojectPoint(point, projection) {
244
319
  ];
245
320
  }
246
321
 
322
+ /**
323
+ * Convert projected meter coordinates to Clipper integer coordinates.
324
+ *
325
+ * @param {[number, number]} point
326
+ * @param {number} scale
327
+ * @returns {{ x: number, y: number }}
328
+ */
247
329
  function toIntPoint(point, scale) {
248
330
  return {
249
331
  x: Math.round(point[0] * scale),
@@ -251,6 +333,12 @@ function toIntPoint(point, scale) {
251
333
  };
252
334
  }
253
335
 
336
+ /**
337
+ * Return a simple arithmetic lon/lat centroid for small local geometries.
338
+ *
339
+ * @param {Array<[number, number]>} points
340
+ * @returns {[number, number]}
341
+ */
254
342
  function polygonCentroid(points) {
255
343
  let lon = 0;
256
344
  let lat = 0;
@@ -261,6 +349,12 @@ function polygonCentroid(points) {
261
349
  return [lon / points.length, lat / points.length];
262
350
  }
263
351
 
352
+ /**
353
+ * Compute a lon/lat bbox for a set of line or polygon vertices.
354
+ *
355
+ * @param {Array<[number, number]>} points
356
+ * @returns {[number, number, number, number]}
357
+ */
264
358
  function lineBbox(points) {
265
359
  let minLon = Infinity;
266
360
  let minLat = Infinity;
@@ -275,6 +369,12 @@ function lineBbox(points) {
275
369
  return [minLon, minLat, maxLon, maxLat];
276
370
  }
277
371
 
372
+ /**
373
+ * Merge multiple lon/lat bounding boxes.
374
+ *
375
+ * @param {Array<[number, number, number, number]>} bboxes
376
+ * @returns {[number, number, number, number]}
377
+ */
278
378
  function mergeBboxes(bboxes) {
279
379
  let minLon = Infinity;
280
380
  let minLat = Infinity;
@@ -289,28 +389,29 @@ function mergeBboxes(bboxes) {
289
389
  return [minLon, minLat, maxLon, maxLat];
290
390
  }
291
391
 
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
-
392
+ /**
393
+ * @param {[number, number]} a
394
+ * @param {[number, number]} b
395
+ * @returns {boolean}
396
+ */
306
397
  function samePoint(a, b) {
307
398
  return Math.abs(a[0] - b[0]) < 1e-9 && Math.abs(a[1] - b[1]) < 1e-9;
308
399
  }
309
400
 
401
+ /**
402
+ * @param {[number, number]} a
403
+ * @param {[number, number]} b
404
+ * @returns {number}
405
+ */
310
406
  function distance2(a, b) {
311
407
  return Math.hypot(a[0] - b[0], a[1] - b[1]);
312
408
  }
313
409
 
410
+ /**
411
+ * @param {unknown} value
412
+ * @param {number} fallback
413
+ * @returns {number}
414
+ */
314
415
  function positiveNumber(value, fallback) {
315
416
  const number = Number(value);
316
417
  return Number.isFinite(number) && number > 0 ? number : fallback;