@math.gl/geospatial 5.0.0-beta.1 → 5.0.0-beta.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.
Files changed (90) hide show
  1. package/dist/crs/crs-identifier.d.ts +23 -0
  2. package/dist/crs/crs-identifier.d.ts.map +1 -0
  3. package/dist/crs/crs-identifier.js +74 -0
  4. package/dist/crs/crs-identifier.js.map +1 -0
  5. package/dist/crs/index.cjs +48 -0
  6. package/dist/crs/index.cjs.map +2 -2
  7. package/dist/crs/index.d.ts +1 -0
  8. package/dist/crs/index.d.ts.map +1 -1
  9. package/dist/crs/index.js +1 -0
  10. package/dist/crs/index.js.map +1 -1
  11. package/dist/discontinuous/cap-map.d.ts.map +1 -1
  12. package/dist/discontinuous/cap-map.js +197 -13
  13. package/dist/discontinuous/cap-map.js.map +1 -1
  14. package/dist/discontinuous/elastic/geometry.d.ts +1 -1
  15. package/dist/discontinuous/elastic/geometry.d.ts.map +1 -1
  16. package/dist/discontinuous/elastic/geometry.js +7 -7
  17. package/dist/discontinuous/elastic/geometry.js.map +1 -1
  18. package/dist/discontinuous/elastic/map.d.ts.map +1 -1
  19. package/dist/discontinuous/elastic/map.js +263 -23
  20. package/dist/discontinuous/elastic/map.js.map +1 -1
  21. package/dist/discontinuous/elastic/source-arc.d.ts +10 -0
  22. package/dist/discontinuous/elastic/source-arc.d.ts.map +1 -0
  23. package/dist/discontinuous/elastic/source-arc.js +111 -0
  24. package/dist/discontinuous/elastic/source-arc.js.map +1 -0
  25. package/dist/discontinuous/global-polygons.d.ts +1 -1
  26. package/dist/discontinuous/global-polygons.d.ts.map +1 -1
  27. package/dist/discontinuous/global-polygons.js +49 -1
  28. package/dist/discontinuous/global-polygons.js.map +1 -1
  29. package/dist/discontinuous/map-engine.d.ts +9 -0
  30. package/dist/discontinuous/map-engine.d.ts.map +1 -1
  31. package/dist/discontinuous/map-layout.d.ts +21 -0
  32. package/dist/discontinuous/map-layout.d.ts.map +1 -1
  33. package/dist/discontinuous/map-layout.js +116 -1
  34. package/dist/discontinuous/map-layout.js.map +1 -1
  35. package/dist/discontinuous/map-transition.d.ts.map +1 -1
  36. package/dist/discontinuous/map-transition.js +66 -3
  37. package/dist/discontinuous/map-transition.js.map +1 -1
  38. package/dist/discontinuous/mercator-map.d.ts +2 -2
  39. package/dist/discontinuous/mercator-map.d.ts.map +1 -1
  40. package/dist/discontinuous/mercator-map.js +123 -11
  41. package/dist/discontinuous/mercator-map.js.map +1 -1
  42. package/dist/discontinuous/polygon-constraints.d.ts +16 -0
  43. package/dist/discontinuous/polygon-constraints.d.ts.map +1 -0
  44. package/dist/discontinuous/polygon-constraints.js +157 -0
  45. package/dist/discontinuous/polygon-constraints.js.map +1 -0
  46. package/dist/discontinuous/prepare-features.d.ts +27 -0
  47. package/dist/discontinuous/prepare-features.d.ts.map +1 -1
  48. package/dist/discontinuous/prepare-features.js +106 -5
  49. package/dist/discontinuous/prepare-features.js.map +1 -1
  50. package/dist/discontinuous/rotated-map.d.ts +9 -0
  51. package/dist/discontinuous/rotated-map.d.ts.map +1 -1
  52. package/dist/discontinuous/source-arc.d.ts +4 -0
  53. package/dist/discontinuous/source-arc.d.ts.map +1 -0
  54. package/dist/discontinuous/source-arc.js +23 -0
  55. package/dist/discontinuous/source-arc.js.map +1 -0
  56. package/dist/discontinuous/world-kernels.d.ts +1 -1
  57. package/dist/discontinuous.cjs +1047 -75
  58. package/dist/discontinuous.cjs.map +3 -3
  59. package/dist/index.cjs +48 -0
  60. package/dist/index.cjs.map +2 -2
  61. package/dist/index.d.ts +1 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +1 -1
  64. package/dist/index.js.map +1 -1
  65. package/dist/turf.cjs +69 -5
  66. package/dist/turf.cjs.map +2 -2
  67. package/dist/turf.d.ts +9 -0
  68. package/dist/turf.d.ts.map +1 -1
  69. package/dist/turf.js +67 -0
  70. package/dist/turf.js.map +1 -1
  71. package/dist/workers/index.cjs +1075 -79
  72. package/dist/workers/index.cjs.map +3 -3
  73. package/dist/workers/operations.cjs +1049 -77
  74. package/dist/workers/operations.cjs.map +3 -3
  75. package/package.json +5 -5
  76. package/src/crs/crs-identifier.ts +86 -0
  77. package/src/crs/index.ts +1 -0
  78. package/src/discontinuous/cap-map.ts +248 -19
  79. package/src/discontinuous/elastic/geometry.ts +18 -7
  80. package/src/discontinuous/elastic/map.ts +334 -30
  81. package/src/discontinuous/elastic/source-arc.ts +129 -0
  82. package/src/discontinuous/global-polygons.ts +56 -2
  83. package/src/discontinuous/map-layout.ts +165 -2
  84. package/src/discontinuous/map-transition.ts +67 -3
  85. package/src/discontinuous/mercator-map.ts +151 -12
  86. package/src/discontinuous/polygon-constraints.ts +184 -0
  87. package/src/discontinuous/prepare-features.ts +154 -6
  88. package/src/discontinuous/source-arc.ts +31 -0
  89. package/src/index.ts +1 -0
  90. package/src/turf.ts +86 -0
@@ -16,7 +16,10 @@ import type {
16
16
  } from '../map-layout';
17
17
  import type {ElasticMapData} from './data';
18
18
  import {ElasticGrid} from './grid';
19
- import {indexDomain, rectangle, clipSegment, sample, sampling, type Polygons} from './geometry';
19
+ import {indexDomain, rectangle, onSegment, sample, sampling, type Polygons} from './geometry';
20
+ import {clipSourceArc} from './source-arc';
21
+ import {sourceArc} from '../source-arc';
22
+ import {tracePolygonConstraints, composePolygonEdge} from '../polygon-constraints';
20
23
 
21
24
  // This chart splitter retains directional pole limits and antimeridian sides.
22
25
  const kernel = Object.assign((x: number, y: number) => [x, y], {
@@ -182,7 +185,7 @@ class ElasticMap implements MapSurface {
182
185
  }
183
186
  private sourceOptions(options: MapSamplingOptions) {
184
187
  const {step} = sampling(options);
185
- return {maxAngularStep: Math.min(step, 1), tolerance: 1e-5};
188
+ return {...options, maxAngularStep: Math.min(step, 1), tolerance: 1e-5};
186
189
  }
187
190
  private sampled(c: Chart, line: Position[], options: MapSamplingOptions) {
188
191
  return sample(
@@ -196,35 +199,105 @@ class ElasticMap implements MapSurface {
196
199
  const out: MapLine[] = [];
197
200
  for (const part of splitter.projectLine(line, this.sourceOptions(options)))
198
201
  for (const c of this.charts.filter(c => c.baseId === part.regionId)) {
199
- let current: Position[] = [];
200
- const flush = () => {
201
- if (current.length > 1) {
202
- const {source, xy} = this.sampled(c, current, options);
203
- out.push({regionId: c.region.id, coordinates: xy, sourceCoordinates: source});
204
- }
205
- current = [];
202
+ let current: MapLine | undefined;
203
+ const boundary = (point: Position, vertex: number) => {
204
+ if (vertex === 0 || vertex === line.length - 1) return 'source-endpoint';
205
+ let edge = 0;
206
+ const kinds: string[] = [];
207
+ for (const polygon of c.domain)
208
+ for (const ring of polygon)
209
+ for (let i = 1; i < ring.length; i++, edge++)
210
+ if (onSegment(point, ring[i - 1], ring[i])) {
211
+ const seam = this.seams.find(s =>
212
+ s.regions.some((id, j) => id === c.region.id && s.edges[j] === edge)
213
+ );
214
+ kinds.push(seam?.kind ?? 'boundary');
215
+ }
216
+ return kinds.includes('boundary')
217
+ ? 'coverage-boundary'
218
+ : kinds.includes('cut')
219
+ ? 'chart-cut'
220
+ : kinds.includes('join')
221
+ ? 'continuous-chart-join'
222
+ : 'coverage-boundary';
206
223
  };
207
- for (let i = 1; i < part.coordinates.length; i++)
208
- for (const [a, b] of clipSegment(
209
- part.coordinates[i - 1],
210
- part.coordinates[i],
211
- c.domain
212
- )) {
213
- if (
214
- current.length &&
224
+ for (let i = 1; i < part.sourceCoordinates.length; i++) {
225
+ const a = part.coordinates[i - 1],
226
+ b = part.coordinates[i];
227
+ const intervals = clipSourceArc(a, b, c.domain);
228
+ if (!intervals.length) {
229
+ current = undefined;
230
+ continue;
231
+ }
232
+ if (Math.hypot(intervals[0].a[0] - a[0], intervals[0].a[1] - a[1]) > 1e-8)
233
+ current = undefined;
234
+ for (const clipped of intervals) {
235
+ const arc = sourceArc(clipped.a, clipped.b);
236
+ const samples = sample(
237
+ [clipped.a, clipped.b],
238
+ p => c.grid.project(p),
239
+ options,
240
+ Math.min(180 / (c.grid.section.rows - 1), 360 / (c.grid.section.columns - 1)),
241
+ (_, t) => arc(t)
242
+ );
243
+ const first = samples.source[0],
244
+ last = samples.source[samples.source.length - 1];
245
+ const joined =
246
+ current &&
215
247
  Math.hypot(
216
- current[current.length - 1][0] - a[0],
217
- current[current.length - 1][1] - a[1]
218
- ) > 1e-8
219
- )
220
- flush();
221
- if (!current.length) current.push(a);
222
- current.push(b);
248
+ ...current.sourceCoordinates[current.sourceCoordinates.length - 1].map(
249
+ (v, k) => v - first[k]
250
+ )
251
+ ) < 1e-8;
252
+ if (!joined) {
253
+ current = {regionId: c.region.id, coordinates: [], sourceCoordinates: []};
254
+ if (part.provenance)
255
+ current.provenance = {
256
+ segments: [],
257
+ parameters: [],
258
+ vertices: [],
259
+ angularDistances: []
260
+ };
261
+ out.push(current);
262
+ }
263
+ for (let j = joined ? 1 : 0; j < samples.source.length; j++) {
264
+ const p = samples.source[j];
265
+ current!.sourceCoordinates.push(p);
266
+ current!.coordinates.push(samples.xy[j]);
267
+ const parent = part.provenance,
268
+ target = current!.provenance;
269
+ if (parent && target) {
270
+ const length = angle(geographic(a), geographic(b)),
271
+ t = length ? Math.min(1, angle(geographic(a), geographic(p)) / length) : 0;
272
+ const endpoint = t < 1e-12 ? i - 1 : t > 1 - 1e-12 ? i : -1,
273
+ segment = parent.segments[i],
274
+ from = parent.segments[i - 1] === segment ? parent.parameters[i - 1] : 0;
275
+ target.segments.push(endpoint >= 0 ? parent.segments[endpoint] : segment);
276
+ target.parameters.push(
277
+ endpoint >= 0
278
+ ? parent.parameters[endpoint]
279
+ : from + t * (parent.parameters[i] - from)
280
+ );
281
+ target.vertices.push(endpoint >= 0 ? parent.vertices[endpoint] : 0xffffffff);
282
+ if (options.pathDistances)
283
+ target.angularDistances.push(
284
+ parent.angularDistances[i - 1] +
285
+ t * (parent.angularDistances[i] - parent.angularDistances[i - 1])
286
+ );
287
+ if (options.pathBoundaries) {
288
+ if (target.vertices.length === 1)
289
+ target.startReason = boundary(p, target.vertices[0]);
290
+ target.endReason = boundary(p, target.vertices[target.vertices.length - 1]);
291
+ }
292
+ }
293
+ }
294
+ if (Math.hypot(last[0] - b[0], last[1] - b[1]) > 1e-8) current = undefined;
223
295
  }
224
- flush();
296
+ }
225
297
  }
226
298
  return out;
227
299
  }
300
+
228
301
  projectPolygon(
229
302
  rings: readonly (readonly GeographicPosition[])[],
230
303
  options: MapSamplingOptions = {}
@@ -232,16 +305,244 @@ class ElasticMap implements MapSurface {
232
305
  const out: MapPolygon[] = [];
233
306
  for (const part of splitter.projectPolygon(rings, this.sourceOptions(options)))
234
307
  for (const c of this.charts.filter(c => c.baseId === part.regionId)) {
308
+ // Insert exact domain crossings and interval midpoints before the
309
+ // boolean operation. Otherwise an arc can bow outside a source chart
310
+ // while its sampled chord remains inside, silently bridging the cut.
311
+ const prepared: Position[][] = [],
312
+ preparedEdges: NonNullable<MapPolygon['edgeProvenance']> = [];
313
+ part.coordinates.forEach((ring, r) => {
314
+ const points: Position[] = [],
315
+ edges: NonNullable<MapPolygon['edgeProvenance']>[number] = [];
316
+ for (let i = 1; i < ring.length; i++) {
317
+ const a = ring[i - 1],
318
+ b = ring[i],
319
+ arc = sourceArc(a, b),
320
+ length = angle(geographic(a), geographic(b)),
321
+ fractions = [0, 1];
322
+ for (const interval of clipSourceArc(a, b, c.domain))
323
+ for (const p of [interval.a, interval.b]) {
324
+ const t =
325
+ length > 1e-14
326
+ ? angle(geographic(a), geographic(p)) / length
327
+ : Math.abs(b[0] - a[0]) > 1e-14
328
+ ? (p[0] - a[0]) / (b[0] - a[0])
329
+ : 0;
330
+ if (t > 1e-12 && t < 1 - 1e-12) fractions.push(t);
331
+ }
332
+ fractions.sort((a, b) => a - b);
333
+ const split = fractions.filter((t, i) => !i || t - fractions[i - 1] > 1e-12);
334
+ const append = (point: Position) => {
335
+ if (points.length >= 100000)
336
+ throw new Error('Elastic polygon constraint vertex limit exceeded');
337
+ if (points.length) {
338
+ const previous = points[points.length - 1];
339
+ if (options.polygonEdgeProvenance)
340
+ edges.push(
341
+ composePolygonEdge(part.edgeProvenance![r][i - 1], a, b, previous, point)
342
+ );
343
+ }
344
+ points.push(point);
345
+ };
346
+ const first = arc(0);
347
+ if (!points.length) points.push(first);
348
+ else if (points[points.length - 1].some((v, k) => v !== first[k])) {
349
+ if (angle(geographic(points[points.length - 1]), geographic(first)) > 1e-10)
350
+ throw new Error('Discontinuous Elastic input source constraint');
351
+ if (options.polygonEdgeProvenance)
352
+ edges.push({
353
+ kind: 'continuous-chart-join',
354
+ ring: 0xffffffff,
355
+ segment: 0xffffffff,
356
+ t0: 0,
357
+ t1: 0
358
+ });
359
+ points.push(first);
360
+ }
361
+ for (let j = 1; j < split.length; j++) {
362
+ append(arc((split[j - 1] + split[j]) / 2));
363
+ append(arc(split[j]));
364
+ }
365
+ }
366
+ if (points[points.length - 1].some((v, k) => v !== points[0][k])) {
367
+ if (angle(geographic(points[points.length - 1]), geographic(points[0])) > 1e-10)
368
+ throw new Error('Unclosed Elastic input source constraint');
369
+ if (options.polygonEdgeProvenance)
370
+ edges.push({
371
+ kind: 'continuous-chart-join',
372
+ ring: 0xffffffff,
373
+ segment: 0xffffffff,
374
+ t0: 0,
375
+ t1: 0
376
+ });
377
+ points.push([...points[0]]);
378
+ }
379
+ prepared.push(points);
380
+ preparedEdges.push(edges);
381
+ });
235
382
  const clipped = polygonClipping.intersection(
236
- snapPolygon(part.coordinates),
383
+ snapPolygon(prepared),
237
384
  c.domain.map(snapPolygon)
238
385
  ) as Polygons;
239
386
  for (const polygon of clipped) {
240
- const samples = polygon.map(r => this.sampled(c, r, options));
387
+ const input = snapPolygon(prepared),
388
+ traced = tracePolygonConstraints(
389
+ input,
390
+ polygon,
391
+ 1e-9,
392
+ options.polygonEdgeProvenance ? preparedEdges : undefined,
393
+ undefined,
394
+ (a, b) => angle(geographic(a), geographic(b)) < 64 * Number.EPSILON
395
+ ),
396
+ resolved = traced.rings.map(r => r.map(p => [...p] as Position));
397
+ traced.owners.forEach((owners, r) =>
398
+ owners.forEach((owner, i) => {
399
+ if (!owner) return;
400
+ const [pr, pe] = owner,
401
+ a = prepared[pr][pe],
402
+ b = prepared[pr][pe + 1],
403
+ length = angle(geographic(a), geographic(b)),
404
+ arc = sourceArc(a, b);
405
+ for (const j of [i, i + 1]) {
406
+ const q = traced.rings[r][j],
407
+ start = input[pr][pe],
408
+ end = input[pr][pe + 1];
409
+ if (Math.hypot(q[0] - start[0], q[1] - start[1]) < 1e-8) resolved[r][j] = [...a];
410
+ else if (Math.hypot(q[0] - end[0], q[1] - end[1]) < 1e-8) resolved[r][j] = [...b];
411
+ else {
412
+ const candidates = clipSourceArc(a, b, c.domain).flatMap(interval => [
413
+ interval.a,
414
+ interval.b
415
+ ]);
416
+ if (!candidates.length)
417
+ throw new Error('Unresolved Elastic polygon source intersection');
418
+ const reference = length ? angle(geographic(a), geographic(q)) / length : 0;
419
+ candidates.sort(
420
+ (x, y) =>
421
+ Math.abs(
422
+ (length ? angle(geographic(a), geographic(x)) / length : 0) - reference
423
+ ) -
424
+ Math.abs(
425
+ (length ? angle(geographic(a), geographic(y)) / length : 0) - reference
426
+ )
427
+ );
428
+ const fraction = length
429
+ ? angle(geographic(a), geographic(candidates[0])) / length
430
+ : 0;
431
+ resolved[r][j] = arc(Math.min(1, Math.max(0, fraction)));
432
+ }
433
+ }
434
+ })
435
+ );
436
+ for (const ring of resolved) ring[ring.length - 1] = [...ring[0]];
437
+ const generated = (a: Position, b: Position) => {
438
+ if (angle(geographic(a), geographic(b)) < 64 * Number.EPSILON)
439
+ return 'continuous-chart-join' as const;
440
+ let edge = 0;
441
+ const kinds: string[] = [];
442
+ for (const polygon of c.domain)
443
+ for (const ring of polygon)
444
+ for (let j = 1; j < ring.length; j++, edge++)
445
+ if (onSegment(a, ring[j - 1], ring[j]) && onSegment(b, ring[j - 1], ring[j])) {
446
+ const seam = this.seams.find(s =>
447
+ s.regions.some((id, k) => id === c.region.id && s.edges[k] === edge)
448
+ );
449
+ kinds.push(seam?.kind ?? 'boundary');
450
+ }
451
+ if (!kinds.length) throw new Error('Unknown Elastic polygon clipping constraint');
452
+ return kinds.includes('boundary')
453
+ ? ('coverage-boundary' as const)
454
+ : kinds.includes('cut')
455
+ ? ('chart-cut' as const)
456
+ : ('continuous-chart-join' as const);
457
+ };
458
+ const coordinates: Position[][] = [],
459
+ sourceCoordinates: Position[][] = [],
460
+ edgeProvenance: NonNullable<MapPolygon['edgeProvenance']> = [];
461
+ resolved.forEach((ring, r) => {
462
+ const source: Position[] = [],
463
+ xy: Position[] = [],
464
+ edges: NonNullable<MapPolygon['edgeProvenance']>[number] = [];
465
+ for (let i = 1; i < ring.length; i++) {
466
+ const owner = traced.owners[r][i - 1],
467
+ arc = owner ? sourceArc(ring[i - 1], ring[i]) : undefined;
468
+ const samples = sample(
469
+ arc ? [arc(0), arc(1)] : [ring[i - 1], ring[i]],
470
+ p => c.grid.project(p),
471
+ options,
472
+ Math.min(180 / (c.grid.section.rows - 1), 360 / (c.grid.section.columns - 1)),
473
+ arc ? (_, t) => arc(t) : undefined
474
+ );
475
+ if (!source.length) {
476
+ source.push(samples.source[0]);
477
+ xy.push(samples.xy[0]);
478
+ } else if (
479
+ source[source.length - 1].some((v, k) => Math.abs(v - samples.source[0][k]) > 1e-10)
480
+ ) {
481
+ if (
482
+ angle(geographic(source[source.length - 1]), geographic(samples.source[0])) >
483
+ 1e-10
484
+ )
485
+ throw new Error('Discontinuous Elastic polygon source constraint');
486
+ if (options.polygonEdgeProvenance)
487
+ edges.push({
488
+ kind: 'continuous-chart-join',
489
+ ring: 0xffffffff,
490
+ segment: 0xffffffff,
491
+ t0: 0,
492
+ t1: 0
493
+ });
494
+ source.push(samples.source[0]);
495
+ xy.push(samples.xy[0]);
496
+ }
497
+ for (let j = 1; j < samples.source.length; j++) {
498
+ if (options.polygonEdgeProvenance) {
499
+ if (owner) {
500
+ const [pr, pe] = owner;
501
+ edges.push(
502
+ composePolygonEdge(
503
+ preparedEdges[pr][pe],
504
+ prepared[pr][pe],
505
+ prepared[pr][pe + 1],
506
+ source[source.length - 1],
507
+ samples.source[j]
508
+ )
509
+ );
510
+ } else
511
+ edges.push({
512
+ kind: generated(ring[i - 1], ring[i]),
513
+ ring: 0xffffffff,
514
+ segment: 0xffffffff,
515
+ t0: 0,
516
+ t1: 0
517
+ });
518
+ }
519
+ source.push(samples.source[j]);
520
+ xy.push(samples.xy[j]);
521
+ }
522
+ }
523
+ if (source.length && source[source.length - 1].some((v, k) => v !== source[0][k])) {
524
+ if (angle(geographic(source[source.length - 1]), geographic(source[0])) > 1e-10)
525
+ throw new Error('Unclosed Elastic polygon source constraint');
526
+ if (options.polygonEdgeProvenance)
527
+ edges.push({
528
+ kind: 'continuous-chart-join',
529
+ ring: 0xffffffff,
530
+ segment: 0xffffffff,
531
+ t0: 0,
532
+ t1: 0
533
+ });
534
+ source.push([...source[0]]);
535
+ xy.push([...xy[0]]);
536
+ }
537
+ coordinates.push(xy);
538
+ sourceCoordinates.push(source);
539
+ edgeProvenance.push(edges);
540
+ });
241
541
  out.push({
242
542
  regionId: c.region.id,
243
- coordinates: samples.map(s => s.xy),
244
- sourceCoordinates: samples.map(s => s.source)
543
+ coordinates,
544
+ sourceCoordinates,
545
+ ...(options.polygonEdgeProvenance ? {edgeProvenance} : {})
245
546
  });
246
547
  }
247
548
  }
@@ -312,6 +613,9 @@ function wrap(lon: number) {
312
613
  // Spherical chart clipping leaves sub-ulp variations along coincident chart
313
614
  // edges. Quantize only Boolean operands, far below source sampling tolerance,
314
615
  // so the sweep-line operation sees identical endpoints on shared boundaries.
616
+
617
+ // Normalize sub-ulp differences only for the boolean operation. Exact source
618
+ // constraints and ancestry remain in the unsnapped prepared arrays.
315
619
  function snapPolygon(rings: readonly (readonly GeographicPosition[])[]): Position[][] {
316
- return rings.map(ring => ring.map(p => p.map(v => Math.round(v * 1e10) / 1e10) as Position));
620
+ return rings.map(r => r.map(p => p.map(v => Math.round(v * 1e10) / 1e10) as Position));
317
621
  }
@@ -0,0 +1,129 @@
1
+ // math.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
+ import {angle, geographic, RAD, type Position} from '../math';
5
+ import {contains, type Polygons} from './geometry';
6
+ import {sourceArc} from '../source-arc';
7
+
8
+ /** Clip a source minor arc against straight lon/lat domain constraints. Analytic
9
+ * derivative bounds isolate roots; unresolved tangencies/pole conditioning reject
10
+ * rather than trusting a fixed set of midpoint probes. */
11
+ export function clipSourceArc(
12
+ a: Position,
13
+ b: Position,
14
+ polygons: Polygons
15
+ ): {a: Position; b: Position}[] {
16
+ const at = sourceArc(a, b),
17
+ va = geographic(a),
18
+ vb = geographic(b),
19
+ omega = angle(va, vb),
20
+ ts = [0, 1];
21
+ let work = 0;
22
+ const tick = () => {
23
+ if (++work > 1000000) throw new Error('Elastic source clipping limit exceeded');
24
+ };
25
+ const linear =
26
+ omega < 1e-14 ||
27
+ (Math.abs(a[1]) < 1e-12 && Math.abs(b[1]) < 1e-12) ||
28
+ Math.abs(at(0)[0] - at(1)[0]) < 1e-12;
29
+ const tangent =
30
+ omega > 1e-14 ? va.map((v, k) => (vb[k] - v * Math.cos(omega)) / Math.sin(omega)) : [0, 0, 0];
31
+ // Longitude angular momentum is conserved along a great circle. Using its
32
+ // actual magnitude keeps nearly meridional arcs from exhausting loose bounds.
33
+ const momentum =
34
+ Math.abs(omega * (va[0] * tangent[1] - va[1] * tangent[0])) + 64 * Number.EPSILON * omega;
35
+ const vector = (t: number) =>
36
+ va.map((v, k) => v * Math.cos(omega * t) + tangent[k] * Math.sin(omega * t));
37
+ const derivative = (t: number) =>
38
+ va.map((v, k) => omega * (-v * Math.sin(omega * t) + tangent[k] * Math.cos(omega * t)));
39
+ for (const rings of polygons)
40
+ for (const ring of rings)
41
+ for (let edge = 1; edge < ring.length; edge++) {
42
+ const c = ring[edge - 1],
43
+ d = ring[edge],
44
+ dx = d[0] - c[0],
45
+ dy = d[1] - c[1],
46
+ length = Math.hypot(dx, dy);
47
+ if (!length) continue;
48
+ const angular = omega / RAD,
49
+ latitude = Math.min(90, Math.max(Math.abs(a[1]), Math.abs(b[1])) + angular),
50
+ cosine = Math.cos(latitude * RAD),
51
+ longitude = cosine > 1e-10 ? angular / cosine : 360;
52
+ if (
53
+ Math.max(c[1], d[1]) < Math.min(a[1], b[1]) - angular - 1e-10 ||
54
+ Math.min(c[1], d[1]) > Math.max(a[1], b[1]) + angular + 1e-10 ||
55
+ Math.max(c[0], d[0]) < Math.min(a[0], b[0]) - longitude - 1e-10 ||
56
+ Math.min(c[0], d[0]) > Math.max(a[0], b[0]) + longitude + 1e-10
57
+ )
58
+ continue;
59
+ const value = (t: number) => {
60
+ tick();
61
+ const p = at(t);
62
+ return ((p[0] - c[0]) * dy - (p[1] - c[1]) * dx) / length;
63
+ };
64
+ const put = (t: number) => {
65
+ const p = at(t),
66
+ u = ((p[0] - c[0]) * dx + (p[1] - c[1]) * dy) / (length * length);
67
+ if (u >= -1e-10 && u <= 1 + 1e-10) ts.push(t);
68
+ };
69
+ const bisect = (lo: number, hi: number, f: number) => {
70
+ for (let k = 0; k < 52; k++) {
71
+ const mid = (lo + hi) / 2;
72
+ if (value(mid) >= 0 === f >= 0) lo = mid;
73
+ else hi = mid;
74
+ }
75
+ put((lo + hi) / 2);
76
+ };
77
+ const visit = (lo: number, hi: number, depth: number) => {
78
+ tick();
79
+ const f = value(lo),
80
+ g = value(hi),
81
+ mid = (lo + hi) / 2,
82
+ h = (hi - lo) / 2;
83
+ if (Math.abs(f) < 1e-12) put(lo);
84
+ if (Math.abs(g) < 1e-12) put(hi);
85
+ if (linear) {
86
+ if (f * g < 0) bisect(lo, hi, f);
87
+ return;
88
+ }
89
+ const v = vector(mid),
90
+ dv = derivative(mid),
91
+ r2 = v[0] * v[0] + v[1] * v[1];
92
+ const df =
93
+ (((v[0] * dv[1] - v[1] * dv[0]) / r2) * dy - (dv[2] / Math.sqrt(r2)) * dx) /
94
+ (RAD * length);
95
+ // Bound |latitude| over the complete finite arc, including interior extrema.
96
+ let z = Math.max(Math.abs(vector(lo)[2]), Math.abs(vector(hi)[2]));
97
+ const phase = Math.atan2(tangent[2], va[2]);
98
+ for (let k = -2; k <= 2; k++) {
99
+ const theta = phase + k * Math.PI;
100
+ if (theta >= omega * lo && theta <= omega * hi)
101
+ z = Math.max(z, Math.hypot(va[2], tangent[2]));
102
+ }
103
+ const r = Math.sqrt(Math.max(0, 1 - Math.min(1, z) ** 2));
104
+ const second = r
105
+ ? ((2 * momentum * omega * Math.abs(dy)) / r ** 4 +
106
+ omega ** 2 * Math.abs(dx) * (1 / r + 1 / r ** 3)) /
107
+ (RAD * length)
108
+ : Infinity;
109
+ if (Number.isFinite(df) && Math.abs(df) > second * h + 1e-12) {
110
+ if (f * g < 0) bisect(lo, hi, f);
111
+ return;
112
+ }
113
+ const speed = r
114
+ ? ((momentum * Math.abs(dy)) / r ** 2 + omega * Math.abs(dx)) / (RAD * length)
115
+ : Infinity;
116
+ if (Math.abs(value(mid)) > speed * h + 1e-12) return;
117
+ if (depth >= 24) throw new Error('Unresolved Elastic source clipping tangent or pole');
118
+ visit(lo, mid, depth + 1);
119
+ visit(mid, hi, depth + 1);
120
+ };
121
+ visit(0, 1, 0);
122
+ }
123
+ ts.sort((a, b) => a - b);
124
+ const result: {a: Position; b: Position}[] = [];
125
+ for (let i = 1; i < ts.length; i++)
126
+ if (ts[i] - ts[i - 1] > 1e-12 && contains(polygons, at((ts[i] + ts[i - 1]) / 2)))
127
+ result.push({a: at(ts[i - 1]), b: at(ts[i])});
128
+ return result;
129
+ }
@@ -262,12 +262,66 @@ export function cutSphericalPolygon(
262
262
 
263
263
  /** Use existing map-specific lobe, face, cap and placement clipping after atlas partitioning. */
264
264
  export function projectSphericalPolygon(
265
- map: Pick<MapSurface, 'projectPolygon'>,
265
+ map: Pick<MapSurface, 'projectPolygon'> & Partial<Pick<MapSurface, 'regions' | 'seams'>>,
266
266
  rings: readonly (readonly GeographicPosition[])[],
267
267
  options: SphericalCutOptions & MapSamplingOptions
268
268
  ): MapPolygon[] {
269
269
  const result: MapPolygon[] = [];
270
+ if (options.polygonEdgeProvenance && (!map.regions || !map.seams))
271
+ throw new Error('Atlas provenance requires map region and seam metadata');
270
272
  for (const part of cutSphericalPolygon(rings, options))
271
- result.push(...map.projectPolygon(part.rings, options));
273
+ for (const polygon of map.projectPolygon(part.rings, options)) {
274
+ if (options.polygonEdgeProvenance) {
275
+ if (!polygon.edgeProvenance)
276
+ throw new Error('Map backend does not support polygon edge provenance');
277
+ const region = map.regions!.find(r => r.id === polygon.regionId)!;
278
+ polygon.edgeProvenance = polygon.edgeProvenance.map((edges, ring) =>
279
+ edges.map((edge, i) => {
280
+ if (edge.kind !== 'source-boundary') return edge;
281
+ const parent = part.edges[edge.ring][edge.segment];
282
+ if (parent.kind === 'source')
283
+ return {
284
+ ...edge,
285
+ ring: parent.ringIndex,
286
+ segment: parent.edgeIndex,
287
+ t0: parent.startFraction + edge.t0 * (parent.endFraction - parent.startFraction),
288
+ t1: parent.startFraction + edge.t1 * (parent.endFraction - parent.startFraction)
289
+ };
290
+ // Atlas edges are computational joins unless they coincide with an
291
+ // actual map cut/coverage constraint. Never promote an atlas connector
292
+ // to a real source boundary or an extrusion wall.
293
+ const a = geographic(polygon.sourceCoordinates[ring][i]),
294
+ b = geographic(polygon.sourceCoordinates[ring][i + 1]),
295
+ kinds: string[] = [];
296
+ region.boundary.forEach((p, j) => {
297
+ const normal = cross(
298
+ geographic(p),
299
+ geographic(region.boundary[(j + 1) % region.boundary.length])
300
+ );
301
+ const length = Math.hypot(...normal);
302
+ if (length < 1e-12) return;
303
+ if ([a, b].every(v => Math.abs(dot(normal, v)) / length < 1e-10)) {
304
+ const seam = map.seams!.find(s =>
305
+ s.regions.some((id, k) => id === region.id && s.edges[k] === j)
306
+ );
307
+ if (seam) kinds.push(seam.kind);
308
+ }
309
+ });
310
+ return {
311
+ kind: kinds.includes('boundary')
312
+ ? ('coverage-boundary' as const)
313
+ : kinds.includes('cut')
314
+ ? ('chart-cut' as const)
315
+ : ('continuous-chart-join' as const),
316
+ ring: 0xffffffff,
317
+ segment: 0xffffffff,
318
+ t0: 0,
319
+ t1: 0
320
+ };
321
+ })
322
+ );
323
+ }
324
+ result.push(polygon);
325
+ }
272
326
  return result;
273
327
  }