@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
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
9
- "version": "5.0.0-beta.1",
9
+ "version": "5.0.0-beta.2",
10
10
  "keywords": [
11
11
  "webgl",
12
12
  "javascript",
@@ -1046,9 +1046,9 @@
1046
1046
  ],
1047
1047
  "dependencies": {
1048
1048
  "@mapbox/martini": "0.2.0",
1049
- "@math.gl/core": "5.0.0-beta.1",
1050
- "@math.gl/geometry": "5.0.0-beta.1",
1051
- "@math.gl/web-mercator": "5.0.0-beta.1",
1049
+ "@math.gl/core": "5.0.0-beta.2",
1050
+ "@math.gl/geometry": "5.0.0-beta.2",
1051
+ "@math.gl/web-mercator": "5.0.0-beta.2",
1052
1052
  "polygon-clipping": "0.15.7"
1053
1053
  },
1054
1054
  "devDependencies": {
@@ -1060,5 +1060,5 @@
1060
1060
  "proj4": "2.22.0"
1061
1061
  },
1062
1062
  "sideEffects": false,
1063
- "gitHead": "13b9fae96e2df2010f5528a199316f47c044200b"
1063
+ "gitHead": "f5c7bfdb4d9232e793237f08a86b513aedccc0a0"
1064
1064
  }
@@ -0,0 +1,86 @@
1
+ // math.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
+
5
+ import type {CRSIdentifier} from './crs';
6
+
7
+ /** WMS 1.3 `CRS:` codes and the OGC register entries they abbreviate. */
8
+ const WMS_CRS_CODES = new Map<string, string>([
9
+ ['84', 'CRS84'],
10
+ ['83', 'CRS83'],
11
+ ['27', 'CRS27']
12
+ ]);
13
+ /** Explicit input conveniences, not a case-insensitivity rule for all registry names. */
14
+ const KNOWN_AUTHORITIES = new Set(['EPSG', 'OGC', 'ESRI', 'IAU']);
15
+ const OGC_CRS_CODES = new Map<string, string>([
16
+ ['CRS84', 'CRS84'],
17
+ ['CRS83', 'CRS83'],
18
+ ['CRS27', 'CRS27'],
19
+ ['CRS84H', 'CRS84h']
20
+ ]);
21
+
22
+ const AUTHORITY_CODE = /^([A-Z][A-Z0-9_-]*):([A-Z0-9_.-]+)$/i;
23
+ const OGC_URN = /^urn:ogc:def:crs:([A-Z][A-Z0-9_-]*):([^:]*):([A-Z0-9_.-]+)$/i;
24
+ const OGC_URI =
25
+ /^https?:\/\/www\.opengis\.net\/def\/crs\/([A-Z][A-Z0-9_-]*)\/([^/]+)\/([A-Z0-9_.-]+)\/?$/i;
26
+
27
+ /**
28
+ * Normalizes recognized spellings of an authority CRS identifier to `AUTHORITY:CODE`.
29
+ *
30
+ * Recognized forms are `AUTHORITY:CODE`, the WMS 1.3 codes `CRS:84`, `CRS:83` and `CRS:27`,
31
+ * OGC URNs (`urn:ogc:def:crs:EPSG::32619`) and OGC URIs
32
+ * (`http://www.opengis.net/def/crs/EPSG/0/32619`). Known authority prefixes (EPSG, OGC, ESRI and IAU) and explicitly known OGC CRS codes
33
+ * accept case-insensitive input aliases. Other authority and code spellings are preserved.
34
+ *
35
+ * A version is removed only when it is empty or `0`, or when it is `1.3` on an OGC `CRS84`,
36
+ * `CRS83` or `CRS27` code, which the OGC register publishes only at that version. Any other
37
+ * versioned identifier is returned unchanged, so it does not compare equal to an unversioned one.
38
+ * Unrecognized strings are also returned unchanged.
39
+ *
40
+ * The result identifies a registry entry only. Different codes are never equated: `EPSG:4326` and
41
+ * `OGC:CRS84` stay distinct, as do Web Mercator aliases. No datum equivalence, axis-order
42
+ * convention or registry lookup is applied.
43
+ *
44
+ * @param identifier - CRS identifier as written by a source or service.
45
+ * @returns The canonical `AUTHORITY:CODE` identifier, or `identifier` unchanged.
46
+ */
47
+ export function normalizeCRSIdentifier(identifier: CRSIdentifier): CRSIdentifier {
48
+ const text = identifier.trim();
49
+
50
+ const authorityCode = AUTHORITY_CODE.exec(text);
51
+ if (authorityCode) {
52
+ const [, authority, code] = authorityCode;
53
+ if (authority.toUpperCase() === 'CRS') {
54
+ const ogcCode = WMS_CRS_CODES.get(code);
55
+ return ogcCode ? `OGC:${ogcCode}` : identifier;
56
+ }
57
+ return formatIdentifier(authority, code);
58
+ }
59
+
60
+ const versioned = OGC_URN.exec(text) || OGC_URI.exec(text);
61
+ if (versioned) {
62
+ const [, authority, version, code] = versioned;
63
+ return isUnversioned(authority, version, code) ? formatIdentifier(authority, code) : identifier;
64
+ }
65
+
66
+ return identifier;
67
+ }
68
+
69
+ /** Whether a URN or URI version names no particular revision of the registry entry. */
70
+ function isUnversioned(authority: string, version: string, code: string): boolean {
71
+ if (version === '' || version === '0') return true;
72
+ const ogcCode = code.toUpperCase();
73
+ return (
74
+ authority.toUpperCase() === 'OGC' &&
75
+ version === '1.3' &&
76
+ Array.from(WMS_CRS_CODES.values()).includes(ogcCode)
77
+ );
78
+ }
79
+
80
+ function formatIdentifier(authority: string, code: string): CRSIdentifier {
81
+ const upperAuthority = authority.toUpperCase();
82
+ const canonicalAuthority = KNOWN_AUTHORITIES.has(upperAuthority) ? upperAuthority : authority;
83
+ const canonicalCode =
84
+ canonicalAuthority === 'OGC' ? OGC_CRS_CODES.get(code.toUpperCase()) || code : code;
85
+ return `${canonicalAuthority}:${canonicalCode}`;
86
+ }
package/src/crs/index.ts CHANGED
@@ -32,6 +32,7 @@ export type {
32
32
  UnresolvedCRSReference
33
33
  } from './spatial-reference';
34
34
  export {createSpatialReference, inferCRSRepresentation} from './spatial-reference';
35
+ export {normalizeCRSIdentifier} from './crs-identifier';
35
36
  export type {
36
37
  EncodeWKTCRSOptions,
37
38
  ParseWKTCRSOptions,
@@ -3,6 +3,8 @@
3
3
  // SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
4
4
 
5
5
  import clipCircle from './spherical-clip/clip-circle.js';
6
+ import {tracePolygonConstraints, composePolygonEdge} from './polygon-constraints';
7
+ import {sourceArc} from './source-arc';
6
8
  import {circleStream} from './spherical-clip/circle.js';
7
9
  import {
8
10
  DiscontinuousMap,
@@ -91,11 +93,15 @@ export class CapMap extends DiscontinuousMap {
91
93
  rings: readonly (readonly GeographicPosition[])[],
92
94
  polygon: boolean,
93
95
  options?: MapSamplingOptions,
94
- regionId?: string
96
+ regionId?: string,
97
+ segmentIndices?: number[][]
95
98
  ): Position[][] {
96
99
  const output: Position[][] = [];
97
100
  const originals = new Map<string, Position>();
98
- let ring: Position[] = [];
101
+ let ring: Position[] = [],
102
+ indices: number[] = [],
103
+ activeSegment = 0;
104
+ let previousInput: GeographicPosition | undefined, currentInput: GeographicPosition | undefined;
99
105
  const precision =
100
106
  Math.min(
101
107
  options?.maxAngularStep ?? 2,
@@ -184,13 +190,60 @@ export class CapMap extends DiscontinuousMap {
184
190
  point: (x: number, y: number) => {
185
191
  if (++emitted > 1_000_000)
186
192
  throw new Error('Projection sampling limit exceeded; relax tolerance');
187
- ring.push(originals.get(`${x},${y}`) ?? this.source([x / RAD, y / RAD]));
193
+ let source = originals.get(`${x},${y}`);
194
+ if (!source && !polygon && this.nonlinear && previousInput && currentInput) {
195
+ // The local nonlinear transform bends source arcs. A circle/chord
196
+ // intersection must be refined on its owning original source arc.
197
+ const a = geographic(previousInput),
198
+ b = geographic(currentInput),
199
+ length = angle(a, b);
200
+ const at = (t: number): Position =>
201
+ length < 1e-14
202
+ ? ([...currentInput!] as Position)
203
+ : coordinates(
204
+ unit(
205
+ a.map(
206
+ (v, k) =>
207
+ (Math.sin((1 - t) * length) * v + Math.sin(t * length) * b[k]) /
208
+ Math.sin(length)
209
+ ) as Vector
210
+ )
211
+ );
212
+ const value = (t: number) => geographic(local(at(t)))[0] - Math.cos(this.cap);
213
+ const approximate = this.source([x / RAD, y / RAD]);
214
+ const reference = length ? Math.min(1, angle(a, geographic(approximate)) / length) : 0;
215
+ const roots: number[] = [];
216
+ for (let j = 0; j < 32; j++) {
217
+ let lo = j / 32,
218
+ hi = (j + 1) / 32,
219
+ f = value(lo),
220
+ g = value(hi);
221
+ if (Math.abs(f) < 1e-14) roots.push(lo);
222
+ if (f * g >= 0) continue;
223
+ for (let k = 0; k < 52; k++) {
224
+ const mid = (lo + hi) / 2;
225
+ if (value(mid) >= 0 === f >= 0) lo = mid;
226
+ else hi = mid;
227
+ }
228
+ roots.push((lo + hi) / 2);
229
+ }
230
+ if (Math.abs(value(1)) < 1e-14) roots.push(1);
231
+ if (!roots.length) throw new Error('Unresolved nonlinear cap source intersection');
232
+ roots.sort((a, b) => Math.abs(a - reference) - Math.abs(b - reference));
233
+ source = at(roots[0]);
234
+ }
235
+ ring.push(source ?? this.source([x / RAD, y / RAD]));
236
+ if (segmentIndices) indices.push(activeSegment);
188
237
  },
189
238
  lineStart: () => {
190
239
  ring = [];
240
+ indices = [];
191
241
  },
192
242
  lineEnd: () => {
193
- if (ring.length > 1) output.push(ring);
243
+ if (ring.length > 1) {
244
+ output.push(ring);
245
+ segmentIndices?.push(indices);
246
+ }
194
247
  },
195
248
  polygonStart: () => {},
196
249
  polygonEnd: () => {}
@@ -207,7 +260,9 @@ export class CapMap extends DiscontinuousMap {
207
260
  const x = q[0] * RAD,
208
261
  y = q[1] * RAD;
209
262
  originals.set(`${x},${y}`, [...p]);
263
+ currentInput = p;
210
264
  stream.point(x, y);
265
+ previousInput = p;
211
266
  };
212
267
  const subdivide = (a: GeographicPosition, b: GeographicPosition, depth = 0) => {
213
268
  if (this.nonlinear && depth < 20) {
@@ -232,9 +287,13 @@ export class CapMap extends DiscontinuousMap {
232
287
  emit(b);
233
288
  };
234
289
  for (const points of rings) {
290
+ previousInput = undefined;
235
291
  stream.lineStart();
236
292
  if (points.length) emit(points[0]);
237
- for (let i = 1; i < points.length; i++) subdivide(points[i - 1], points[i]);
293
+ for (let i = 1; i < points.length; i++) {
294
+ activeSegment = i - 1;
295
+ subdivide(points[i - 1], points[i]);
296
+ }
238
297
  if (polygon && points.length) subdivide(points[points.length - 1], points[0]);
239
298
  stream.lineEnd();
240
299
  }
@@ -246,15 +305,70 @@ export class CapMap extends DiscontinuousMap {
246
305
  options?: MapSamplingOptions
247
306
  ): MapLine[] {
248
307
  if (this.cap === Math.PI / 2 && !this.nonlinear) return super.projectLine(line, options);
249
- // Super validates antipodal edges and sampling options even for invisible input.
250
- return super
251
- .projectLine(line, options)
252
- .flatMap(part =>
253
- this.clip([part.sourceCoordinates], false, options, part.regionId).flatMap(line =>
254
- super.projectLine(line, options).filter(q => q.regionId === part.regionId)
255
- )
308
+ // Attribute streaming intersections while the owning sampled edge is active.
309
+ // Rigid spherical rotations preserve the original great-circle semantics.
310
+ return super.projectLine(line, options).flatMap(part => {
311
+ const indices: number[][] | undefined = part.provenance ? [] : undefined;
312
+ const clipped = this.clip([part.sourceCoordinates], false, options, part.regionId, indices);
313
+ return clipped.flatMap((points, piece) =>
314
+ super
315
+ .projectLine(points, options)
316
+ .filter(q => q.regionId === part.regionId)
317
+ .map(q => {
318
+ const parent = part.provenance,
319
+ child = q.provenance;
320
+ if (!parent || !child) return q;
321
+ const provenance: NonNullable<MapLine['provenance']> = {
322
+ segments: [],
323
+ parameters: [],
324
+ vertices: [],
325
+ angularDistances: []
326
+ };
327
+ for (let j = 0; j < q.sourceCoordinates.length; j++) {
328
+ const k = child.segments[j];
329
+ // The incoming endpoint owns the clipped edge, including a newly
330
+ // generated entry point followed by its original end vertex.
331
+ const edge = indices![piece][k + 1];
332
+ const a = geographic(part.sourceCoordinates[edge]),
333
+ b = geographic(part.sourceCoordinates[edge + 1]),
334
+ p = geographic(q.sourceCoordinates[j]);
335
+ const length = angle(a, b),
336
+ t = length ? Math.min(1, angle(a, p) / length) : 0;
337
+ const endpoint = t < 1e-12 ? edge : t > 1 - 1e-12 ? edge + 1 : -1;
338
+ const segment = parent.segments[edge + 1];
339
+ const from = parent.segments[edge] === segment ? parent.parameters[edge] : 0;
340
+ provenance.segments.push(endpoint >= 0 ? parent.segments[endpoint] : segment);
341
+ provenance.parameters.push(
342
+ endpoint >= 0
343
+ ? parent.parameters[endpoint]
344
+ : from + t * (parent.parameters[edge + 1] - from)
345
+ );
346
+ provenance.vertices.push(endpoint >= 0 ? parent.vertices[endpoint] : 0xffffffff);
347
+ if (options?.pathDistances)
348
+ provenance.angularDistances.push(
349
+ parent.angularDistances[edge] +
350
+ t * (parent.angularDistances[edge + 1] - parent.angularDistances[edge])
351
+ );
352
+ }
353
+ if (options?.pathBoundaries) {
354
+ const same = (a: GeographicPosition, b: GeographicPosition) =>
355
+ angle(geographic(a), geographic(b)) < 1e-12;
356
+ provenance.startReason = same(q.sourceCoordinates[0], part.sourceCoordinates[0])
357
+ ? parent.startReason
358
+ : 'coverage-boundary';
359
+ provenance.endReason = same(
360
+ q.sourceCoordinates[q.sourceCoordinates.length - 1],
361
+ part.sourceCoordinates[part.sourceCoordinates.length - 1]
362
+ )
363
+ ? parent.endReason
364
+ : 'coverage-boundary';
365
+ }
366
+ return {...q, provenance};
367
+ })
256
368
  );
369
+ });
257
370
  }
371
+
258
372
  override projectPolygon(
259
373
  rings: readonly (readonly GeographicPosition[])[],
260
374
  options?: MapSamplingOptions
@@ -356,19 +470,134 @@ export class CapMap extends DiscontinuousMap {
356
470
  for (const ring of outer) {
357
471
  const plane = ring.map(planar),
358
472
  sources = [ring, ...holes.filter(h => contains(plane, planar(h[0])))];
473
+ const parentPlane = part.sourceCoordinates.map(r => r.map(planar));
474
+ const fromPlane = (p: Position): Position =>
475
+ coordinates(unit(center.map((v, k) => v + x[k] * p[0] + y[k] * p[1]) as Vector));
476
+ const traced = tracePolygonConstraints(
477
+ parentPlane,
478
+ sources.map(r => r.map(planar)),
479
+ this.nonlinear ? 2e-6 : 1e-10,
480
+ part.edgeProvenance,
481
+ p => this.visible(fromPlane(p))
482
+ );
483
+ const resolved = traced.rings.map(r => r.map(fromPlane));
484
+ traced.owners.forEach((owners, r) =>
485
+ owners.forEach((owner, i) => {
486
+ if (!owner) return;
487
+ const [pr, pe] = owner,
488
+ a = part.sourceCoordinates[pr][pe],
489
+ b = part.sourceCoordinates[pr][pe + 1],
490
+ at = sourceArc(a, b);
491
+ for (const j of [i, i + 1]) {
492
+ const point = resolved[r][j],
493
+ distance = angle(geographic(a), geographic(b)),
494
+ fraction = distance ? angle(geographic(a), geographic(point)) / distance : 0;
495
+ if (fraction < 1e-10) resolved[r][j] = [...a];
496
+ else if (fraction > 1 - 1e-10) resolved[r][j] = [...b];
497
+ else if (
498
+ Math.hypot(...cross(geographic(a), geographic(b))) > 1e-14 &&
499
+ Math.abs(dot(unit(cross(geographic(a), geographic(b))), geographic(point))) < 1e-12
500
+ ) {
501
+ resolved[r][j] = at(Math.min(1, Math.max(0, fraction)));
502
+ } else {
503
+ const value = (t: number) => geographic(this.local(at(t)))[0] - Math.cos(this.cap),
504
+ roots: number[] = [];
505
+ for (let k = 0; k < 32; k++) {
506
+ let lo = k / 32,
507
+ hi = (k + 1) / 32,
508
+ f = value(lo),
509
+ g = value(hi);
510
+ if (Math.abs(f) < 1e-13) roots.push(lo);
511
+ if (f * g >= 0) continue;
512
+ for (let n = 0; n < 52; n++) {
513
+ const mid = (lo + hi) / 2;
514
+ if (value(mid) >= 0 === f >= 0) lo = mid;
515
+ else hi = mid;
516
+ }
517
+ roots.push((lo + hi) / 2);
518
+ }
519
+ if (Math.abs(value(1)) < 1e-13) roots.push(1);
520
+ if (!roots.length) throw new Error('Unresolved cap polygon source intersection');
521
+ roots.sort((a, b) => Math.abs(a - fraction) - Math.abs(b - fraction));
522
+ resolved[r][j] = at(roots[0]);
523
+ }
524
+ }
525
+ })
526
+ );
527
+ for (const ring of resolved) ring[ring.length - 1] = [...ring[0]];
359
528
  result.push({
360
529
  regionId: part.regionId,
361
- sourceCoordinates: sources,
362
- coordinates: sources.map(r =>
530
+ sourceCoordinates: resolved,
531
+ coordinates: resolved.map(r =>
363
532
  r.map(p => {
364
533
  const q = super.projectPoint(p, part.regionId);
365
- if (!q)
366
- throw new Error(
367
- `Cap point outside chart ${part.regionId}: ${p}, owner ${super.findRegions(p)}`
368
- );
534
+ if (!q) throw new Error('Cap polygon source constraint left its owning chart');
369
535
  return q.position;
370
536
  })
371
- )
537
+ ),
538
+ ...(options?.polygonEdgeProvenance
539
+ ? {
540
+ edgeProvenance: traced.owners.map((owners, r) =>
541
+ owners.map((owner, i) => {
542
+ const a = resolved[r][i],
543
+ b = resolved[r][i + 1];
544
+ if (owner) {
545
+ const [pr, pe] = owner;
546
+ return composePolygonEdge(
547
+ part.edgeProvenance![pr][pe],
548
+ part.sourceCoordinates[pr][pe],
549
+ part.sourceCoordinates[pr][pe + 1],
550
+ a,
551
+ b
552
+ );
553
+ }
554
+ if (angle(geographic(a), geographic(b)) < 64 * Number.EPSILON)
555
+ return {
556
+ kind: 'continuous-chart-join' as const,
557
+ ring: 0xffffffff,
558
+ segment: 0xffffffff,
559
+ t0: 0,
560
+ t1: 0
561
+ };
562
+ const kinds: string[] = [];
563
+ region.boundary.forEach((p, j) => {
564
+ const n = cross(
565
+ geographic(p),
566
+ geographic(region.boundary[(j + 1) % region.boundary.length])
567
+ ),
568
+ length = Math.hypot(...n);
569
+ if (
570
+ length &&
571
+ [a, b].every(p => Math.abs(dot(n, geographic(p))) / length < 1e-9)
572
+ ) {
573
+ const seam = this.seams.find(s =>
574
+ s.regions.some((id, k) => id === region.id && s.edges[k] === j)
575
+ );
576
+ if (seam) kinds.push(seam.kind);
577
+ }
578
+ });
579
+ const horizon = [a, b].every(
580
+ p => Math.abs(geographic(this.local(p))[0] - Math.cos(this.cap)) < 1e-7
581
+ );
582
+ if (!horizon && !kinds.length)
583
+ throw new Error('Unknown cap polygon clipping constraint');
584
+ return {
585
+ kind: horizon
586
+ ? ('coverage-boundary' as const)
587
+ : kinds.includes('boundary')
588
+ ? ('coverage-boundary' as const)
589
+ : kinds.includes('cut')
590
+ ? ('chart-cut' as const)
591
+ : ('continuous-chart-join' as const),
592
+ ring: 0xffffffff,
593
+ segment: 0xffffffff,
594
+ t0: 0,
595
+ t1: 0
596
+ };
597
+ })
598
+ )
599
+ }
600
+ : {})
372
601
  });
373
602
  }
374
603
  }
@@ -89,7 +89,8 @@ export function sample(
89
89
  line: Position[],
90
90
  project: (p: Position) => Position | null,
91
91
  options: MapSamplingOptions,
92
- cellStep: number
92
+ cellStep: number,
93
+ interpolate?: (segment: number, t: number) => Position
93
94
  ) {
94
95
  const {step, tolerance} = sampling(options),
95
96
  source: Position[] = [],
@@ -101,11 +102,21 @@ export function sample(
101
102
  };
102
103
  const at = (p: Position) => {
103
104
  const q = project(p);
104
- if (!q) throw new Error('Elastic grid is undefined inside its coverage');
105
+ if (!q) throw new Error(`Elastic grid is undefined inside its coverage at ${p}`);
105
106
  return q;
106
107
  };
107
- const visit = (a: Position, b: Position, qa: Position, qb: Position, depth: number) => {
108
- const mid = lerp(a, b, 0.5),
108
+ const visit = (
109
+ a: Position,
110
+ b: Position,
111
+ qa: Position,
112
+ qb: Position,
113
+ depth: number,
114
+ segment: number,
115
+ t0: number,
116
+ t1: number
117
+ ) => {
118
+ const tm = (t0 + t1) / 2,
119
+ mid = interpolate ? interpolate(segment, tm) : lerp(a, b, 0.5),
109
120
  qm = at(mid),
110
121
  chord = lerp(qa, qb, 0.5);
111
122
  if (
@@ -113,14 +124,14 @@ export function sample(
113
124
  Math.hypot(qm[0] - chord[0], qm[1] - chord[1]) > tolerance
114
125
  ) {
115
126
  if (depth >= 24) throw new Error('Elastic sampling limit exceeded');
116
- visit(a, mid, qa, qm, depth + 1);
117
- visit(mid, b, qm, qb, depth + 1);
127
+ visit(a, mid, qa, qm, depth + 1, segment, t0, tm);
128
+ visit(mid, b, qm, qb, depth + 1, segment, tm, t1);
118
129
  } else emit(b, qb);
119
130
  };
120
131
  if (line.length) {
121
132
  emit(line[0], at(line[0]));
122
133
  for (let i = 1; i < line.length; i++)
123
- visit(line[i - 1], line[i], at(line[i - 1]), at(line[i]), 0);
134
+ visit(line[i - 1], line[i], at(line[i - 1]), at(line[i]), 0, i - 1, 0, 1);
124
135
  }
125
136
  return {source, xy};
126
137
  }