@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.
- package/dist/crs/crs-identifier.d.ts +23 -0
- package/dist/crs/crs-identifier.d.ts.map +1 -0
- package/dist/crs/crs-identifier.js +74 -0
- package/dist/crs/crs-identifier.js.map +1 -0
- package/dist/crs/index.cjs +48 -0
- package/dist/crs/index.cjs.map +2 -2
- package/dist/crs/index.d.ts +1 -0
- package/dist/crs/index.d.ts.map +1 -1
- package/dist/crs/index.js +1 -0
- package/dist/crs/index.js.map +1 -1
- package/dist/discontinuous/cap-map.d.ts.map +1 -1
- package/dist/discontinuous/cap-map.js +197 -13
- package/dist/discontinuous/cap-map.js.map +1 -1
- package/dist/discontinuous/elastic/geometry.d.ts +1 -1
- package/dist/discontinuous/elastic/geometry.d.ts.map +1 -1
- package/dist/discontinuous/elastic/geometry.js +7 -7
- package/dist/discontinuous/elastic/geometry.js.map +1 -1
- package/dist/discontinuous/elastic/map.d.ts.map +1 -1
- package/dist/discontinuous/elastic/map.js +263 -23
- package/dist/discontinuous/elastic/map.js.map +1 -1
- package/dist/discontinuous/elastic/source-arc.d.ts +10 -0
- package/dist/discontinuous/elastic/source-arc.d.ts.map +1 -0
- package/dist/discontinuous/elastic/source-arc.js +111 -0
- package/dist/discontinuous/elastic/source-arc.js.map +1 -0
- package/dist/discontinuous/global-polygons.d.ts +1 -1
- package/dist/discontinuous/global-polygons.d.ts.map +1 -1
- package/dist/discontinuous/global-polygons.js +49 -1
- package/dist/discontinuous/global-polygons.js.map +1 -1
- package/dist/discontinuous/map-engine.d.ts +9 -0
- package/dist/discontinuous/map-engine.d.ts.map +1 -1
- package/dist/discontinuous/map-layout.d.ts +21 -0
- package/dist/discontinuous/map-layout.d.ts.map +1 -1
- package/dist/discontinuous/map-layout.js +116 -1
- package/dist/discontinuous/map-layout.js.map +1 -1
- package/dist/discontinuous/map-transition.d.ts.map +1 -1
- package/dist/discontinuous/map-transition.js +66 -3
- package/dist/discontinuous/map-transition.js.map +1 -1
- package/dist/discontinuous/mercator-map.d.ts +2 -2
- package/dist/discontinuous/mercator-map.d.ts.map +1 -1
- package/dist/discontinuous/mercator-map.js +123 -11
- package/dist/discontinuous/mercator-map.js.map +1 -1
- package/dist/discontinuous/polygon-constraints.d.ts +16 -0
- package/dist/discontinuous/polygon-constraints.d.ts.map +1 -0
- package/dist/discontinuous/polygon-constraints.js +157 -0
- package/dist/discontinuous/polygon-constraints.js.map +1 -0
- package/dist/discontinuous/prepare-features.d.ts +27 -0
- package/dist/discontinuous/prepare-features.d.ts.map +1 -1
- package/dist/discontinuous/prepare-features.js +106 -5
- package/dist/discontinuous/prepare-features.js.map +1 -1
- package/dist/discontinuous/rotated-map.d.ts +9 -0
- package/dist/discontinuous/rotated-map.d.ts.map +1 -1
- package/dist/discontinuous/source-arc.d.ts +4 -0
- package/dist/discontinuous/source-arc.d.ts.map +1 -0
- package/dist/discontinuous/source-arc.js +23 -0
- package/dist/discontinuous/source-arc.js.map +1 -0
- package/dist/discontinuous/world-kernels.d.ts +1 -1
- package/dist/discontinuous.cjs +1047 -75
- package/dist/discontinuous.cjs.map +3 -3
- package/dist/index.cjs +48 -0
- package/dist/index.cjs.map +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/turf.cjs +69 -5
- package/dist/turf.cjs.map +2 -2
- package/dist/turf.d.ts +9 -0
- package/dist/turf.d.ts.map +1 -1
- package/dist/turf.js +67 -0
- package/dist/turf.js.map +1 -1
- package/dist/workers/index.cjs +1075 -79
- package/dist/workers/index.cjs.map +3 -3
- package/dist/workers/operations.cjs +1049 -77
- package/dist/workers/operations.cjs.map +3 -3
- package/package.json +5 -5
- package/src/crs/crs-identifier.ts +86 -0
- package/src/crs/index.ts +1 -0
- package/src/discontinuous/cap-map.ts +248 -19
- package/src/discontinuous/elastic/geometry.ts +18 -7
- package/src/discontinuous/elastic/map.ts +334 -30
- package/src/discontinuous/elastic/source-arc.ts +129 -0
- package/src/discontinuous/global-polygons.ts +56 -2
- package/src/discontinuous/map-layout.ts +165 -2
- package/src/discontinuous/map-transition.ts +67 -3
- package/src/discontinuous/mercator-map.ts +151 -12
- package/src/discontinuous/polygon-constraints.ts +184 -0
- package/src/discontinuous/prepare-features.ts +154 -6
- package/src/discontinuous/source-arc.ts +31 -0
- package/src/index.ts +1 -0
- package/src/turf.ts +86 -0
|
@@ -54,15 +54,40 @@ export type MapSeam = {
|
|
|
54
54
|
kind: 'join' | 'cut' | 'boundary';
|
|
55
55
|
};
|
|
56
56
|
export type MapPoint = {regionId: string; position: Position};
|
|
57
|
-
export type MapLine = {
|
|
57
|
+
export type MapLine = {
|
|
58
|
+
regionId: string;
|
|
59
|
+
coordinates: Position[];
|
|
60
|
+
sourceCoordinates: Position[];
|
|
61
|
+
provenance?: {
|
|
62
|
+
segments: number[];
|
|
63
|
+
parameters: number[];
|
|
64
|
+
vertices: number[];
|
|
65
|
+
angularDistances: number[];
|
|
66
|
+
startReason?: string;
|
|
67
|
+
endReason?: string;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
export type MapPolygonEdge = {
|
|
71
|
+
kind: 'source-boundary' | 'coverage-boundary' | 'chart-cut' | 'continuous-chart-join';
|
|
72
|
+
ring: number;
|
|
73
|
+
segment: number;
|
|
74
|
+
t0: number;
|
|
75
|
+
t1: number;
|
|
76
|
+
};
|
|
58
77
|
export type MapPolygon = {
|
|
59
78
|
regionId: string;
|
|
60
79
|
coordinates: Position[][];
|
|
61
80
|
sourceCoordinates: Position[][];
|
|
81
|
+
edgeProvenance?: MapPolygonEdge[][];
|
|
62
82
|
};
|
|
63
83
|
export type MapSamplingOptions = {
|
|
64
84
|
/** Maximum angular sampling interval in degrees. Default 2, range (0, 90]. */
|
|
65
85
|
maxAngularStep?: number;
|
|
86
|
+
/** Opt-in minor-great-circle source lineage, recorded during clipping/sampling. */
|
|
87
|
+
pathProvenance?: boolean;
|
|
88
|
+
pathDistances?: boolean;
|
|
89
|
+
pathBoundaries?: boolean;
|
|
90
|
+
polygonEdgeProvenance?: boolean;
|
|
66
91
|
/** Midpoint chord-error target in placed output units. Default 1e-4. */
|
|
67
92
|
tolerance?: number;
|
|
68
93
|
};
|
|
@@ -243,6 +268,10 @@ export class DiscontinuousMap {
|
|
|
243
268
|
const points = line.map(geographic);
|
|
244
269
|
for (let i = 1; i < points.length; i++) validateArc(points[i - 1], points[i]);
|
|
245
270
|
const results: MapLine[] = [];
|
|
271
|
+
const cumulative = options.pathDistances ? [0] : undefined;
|
|
272
|
+
if (cumulative)
|
|
273
|
+
for (let i = 1; i < points.length; i++)
|
|
274
|
+
cumulative.push(cumulative[i - 1] + angle(points[i - 1], points[i]));
|
|
246
275
|
for (const r of this.compiled) {
|
|
247
276
|
let current: MapLine | null = null;
|
|
248
277
|
for (let i = 1; i < points.length; i++) {
|
|
@@ -251,6 +280,25 @@ export class DiscontinuousMap {
|
|
|
251
280
|
current = null;
|
|
252
281
|
continue;
|
|
253
282
|
}
|
|
283
|
+
const reason = (p: Vector, endpoint: Vector, isEndpoint: boolean) => {
|
|
284
|
+
if (isEndpoint && angle(p, endpoint) < 1e-12) return 'source-endpoint';
|
|
285
|
+
const kinds = r.normals
|
|
286
|
+
.map((normal, edge) =>
|
|
287
|
+
Math.abs(dot(normal, p)) < 1e-9
|
|
288
|
+
? this.seams.find(s =>
|
|
289
|
+
s.regions.some((id, i) => id === r.definition.id && s.edges[i] === edge)
|
|
290
|
+
)?.kind
|
|
291
|
+
: undefined
|
|
292
|
+
)
|
|
293
|
+
.filter(Boolean);
|
|
294
|
+
return kinds.includes('boundary')
|
|
295
|
+
? 'coverage-boundary'
|
|
296
|
+
: kinds.includes('cut')
|
|
297
|
+
? 'chart-cut'
|
|
298
|
+
: kinds.includes('join')
|
|
299
|
+
? 'continuous-chart-join'
|
|
300
|
+
: 'coverage-boundary';
|
|
301
|
+
};
|
|
254
302
|
const sampled = this.sample(r, clipped[0], clipped[1], settings);
|
|
255
303
|
if (sampled.length < 2) continue;
|
|
256
304
|
if (
|
|
@@ -265,12 +313,33 @@ export class DiscontinuousMap {
|
|
|
265
313
|
)
|
|
266
314
|
) {
|
|
267
315
|
current = {regionId: r.definition.id, coordinates: [], sourceCoordinates: []};
|
|
316
|
+
if (options.pathProvenance)
|
|
317
|
+
current.provenance = {segments: [], parameters: [], vertices: [], angularDistances: []};
|
|
318
|
+
if (current.provenance && options.pathBoundaries)
|
|
319
|
+
current.provenance.startReason = reason(clipped[0], points[0], i === 1);
|
|
268
320
|
results.push(current);
|
|
269
321
|
} else sampled.shift();
|
|
270
322
|
for (const p of sampled) {
|
|
271
323
|
current.coordinates.push(this.project(r, p));
|
|
272
324
|
current.sourceCoordinates.push(coordinates(p));
|
|
325
|
+
if (current.provenance) {
|
|
326
|
+
const length = angle(points[i - 1], points[i]);
|
|
327
|
+
const t = length ? Math.min(1, Math.max(0, angle(points[i - 1], p) / length)) : 0;
|
|
328
|
+
current.provenance.segments.push(i - 1);
|
|
329
|
+
current.provenance.parameters.push(t);
|
|
330
|
+
current.provenance.vertices.push(
|
|
331
|
+
angle(points[i - 1], p) < 1e-12 ? i - 1 : angle(points[i], p) < 1e-12 ? i : 0xffffffff
|
|
332
|
+
);
|
|
333
|
+
if (cumulative)
|
|
334
|
+
current.provenance.angularDistances.push(cumulative[i - 1] + t * length);
|
|
335
|
+
}
|
|
273
336
|
}
|
|
337
|
+
if (current.provenance && options.pathBoundaries)
|
|
338
|
+
current.provenance.endReason = reason(
|
|
339
|
+
clipped[1],
|
|
340
|
+
points[points.length - 1],
|
|
341
|
+
i === points.length - 1
|
|
342
|
+
);
|
|
274
343
|
// Even an excursion returning to the same seam point starts a new part.
|
|
275
344
|
if (!this.contains(r, points[i])) current = null;
|
|
276
345
|
}
|
|
@@ -392,24 +461,118 @@ export class DiscontinuousMap {
|
|
|
392
461
|
coordinates: [],
|
|
393
462
|
sourceCoordinates: []
|
|
394
463
|
};
|
|
464
|
+
if (options.polygonEdgeProvenance) output.edgeProvenance = [];
|
|
395
465
|
polygon.forEach((ring, index) => {
|
|
396
466
|
const sphere = ring.map(p => unit(add(center, add(scale(x, p[0]), scale(y, p[1])))));
|
|
397
467
|
const points: Vector[] = [];
|
|
468
|
+
const edges: MapPolygonEdge[] = [];
|
|
469
|
+
const classify = (a: Vector, b: Vector): MapPolygonEdge => {
|
|
470
|
+
// Directional pole copies can differ in floating-point coordinates
|
|
471
|
+
// while describing the same source direction. Their zero-length
|
|
472
|
+
// connector is computational, never an original boundary.
|
|
473
|
+
if (angle(a, b) < 64 * Number.EPSILON)
|
|
474
|
+
return {
|
|
475
|
+
kind: 'continuous-chart-join',
|
|
476
|
+
ring: 0xffffffff,
|
|
477
|
+
segment: 0xffffffff,
|
|
478
|
+
t0: 0,
|
|
479
|
+
t1: 0
|
|
480
|
+
};
|
|
481
|
+
// Attribute the boolean boundary in the source clipping chart, before
|
|
482
|
+
// projection/resampling. Original constraints win on coincident cuts.
|
|
483
|
+
const candidates: {ring: number; segment: number}[] = [];
|
|
484
|
+
vectors.forEach((source, ring) =>
|
|
485
|
+
source.forEach((u, segment) => {
|
|
486
|
+
const v = source[(segment + 1) % source.length],
|
|
487
|
+
length = angle(u, v);
|
|
488
|
+
if (
|
|
489
|
+
length > 1e-12 &&
|
|
490
|
+
[a, b].every(
|
|
491
|
+
p =>
|
|
492
|
+
Math.abs(dot(unit(cross(u, v)), p)) < 1e-12 &&
|
|
493
|
+
Math.abs(angle(u, p) + angle(p, v) - length) < 1e-10
|
|
494
|
+
)
|
|
495
|
+
)
|
|
496
|
+
candidates.push({ring, segment});
|
|
497
|
+
})
|
|
498
|
+
);
|
|
499
|
+
if (candidates.length > 1) throw new Error('Ambiguous polygon boundary provenance');
|
|
500
|
+
if (candidates.length) {
|
|
501
|
+
const {ring, segment} = candidates[0],
|
|
502
|
+
source = vectors[ring],
|
|
503
|
+
u = source[segment],
|
|
504
|
+
length = angle(u, source[(segment + 1) % source.length]);
|
|
505
|
+
return {
|
|
506
|
+
kind: 'source-boundary',
|
|
507
|
+
ring,
|
|
508
|
+
segment,
|
|
509
|
+
t0: Math.min(1, angle(u, a) / length),
|
|
510
|
+
t1: Math.min(1, angle(u, b) / length)
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
const kinds = r.normals
|
|
514
|
+
.map((n, edge) =>
|
|
515
|
+
[a, b].every(p => Math.abs(dot(n, p)) < 1e-9)
|
|
516
|
+
? this.seams.find(s =>
|
|
517
|
+
s.regions.some((id, i) => id === r.definition.id && s.edges[i] === edge)
|
|
518
|
+
)?.kind
|
|
519
|
+
: undefined
|
|
520
|
+
)
|
|
521
|
+
.filter(Boolean);
|
|
522
|
+
if (!kinds.length) throw new Error('Unknown polygon clipping constraint');
|
|
523
|
+
return {
|
|
524
|
+
kind: kinds.includes('boundary')
|
|
525
|
+
? 'coverage-boundary'
|
|
526
|
+
: kinds.includes('cut')
|
|
527
|
+
? 'chart-cut'
|
|
528
|
+
: 'continuous-chart-join',
|
|
529
|
+
ring: 0xffffffff,
|
|
530
|
+
segment: 0xffffffff,
|
|
531
|
+
t0: 0,
|
|
532
|
+
t1: 0
|
|
533
|
+
};
|
|
534
|
+
};
|
|
398
535
|
for (let i = 1; i < sphere.length; i++) {
|
|
536
|
+
const origin =
|
|
537
|
+
options.polygonEdgeProvenance &&
|
|
538
|
+
!sphere[i - 1].every((value, k) => value === sphere[i][k])
|
|
539
|
+
? classify(sphere[i - 1], sphere[i])
|
|
540
|
+
: undefined;
|
|
399
541
|
const sampled = this.sample(r, sphere[i - 1], sphere[i], settings);
|
|
400
542
|
for (const point of sampled) {
|
|
401
543
|
const previous = points[points.length - 1];
|
|
402
|
-
if (!previous || !point.every((value, k) => value === previous[k]))
|
|
544
|
+
if (!previous || !point.every((value, k) => value === previous[k])) {
|
|
545
|
+
if (previous && origin) {
|
|
546
|
+
const edge = {...origin};
|
|
547
|
+
if (edge.kind === 'source-boundary') {
|
|
548
|
+
const source = vectors[edge.ring],
|
|
549
|
+
u = source[edge.segment],
|
|
550
|
+
length = angle(u, source[(edge.segment + 1) % source.length]);
|
|
551
|
+
edge.t0 = Math.min(1, angle(u, previous) / length);
|
|
552
|
+
edge.t1 = Math.min(1, angle(u, point) / length);
|
|
553
|
+
}
|
|
554
|
+
edges.push(edge);
|
|
555
|
+
}
|
|
403
556
|
points.push(point);
|
|
557
|
+
}
|
|
404
558
|
}
|
|
405
559
|
}
|
|
406
560
|
if (points.length < 3) return;
|
|
407
561
|
if (points[points.length - 1].every((value, k) => value === points[0][k])) points.pop();
|
|
562
|
+
if (options.polygonEdgeProvenance && edges.length < points.length)
|
|
563
|
+
edges.push(classify(points[points.length - 1], points[0]));
|
|
408
564
|
points.push(points[0]);
|
|
409
565
|
const xy = points.map(p => this.project(r, p));
|
|
410
566
|
if (ringOrientation(xy) > 0 !== (index === 0)) {
|
|
411
567
|
xy.reverse();
|
|
412
568
|
points.reverse();
|
|
569
|
+
edges.reverse();
|
|
570
|
+
for (const e of edges) [e.t0, e.t1] = [e.t1, e.t0];
|
|
571
|
+
}
|
|
572
|
+
if (output.edgeProvenance) {
|
|
573
|
+
if (edges.length !== points.length - 1)
|
|
574
|
+
throw new Error('Mismatched polygon edge provenance');
|
|
575
|
+
output.edgeProvenance.push(edges);
|
|
413
576
|
}
|
|
414
577
|
output.coordinates.push(xy);
|
|
415
578
|
output.sourceCoordinates.push(points.map(coordinates));
|
|
@@ -7,6 +7,71 @@ import type {GeographicPosition, MapSamplingOptions, MapRegion} from './map-layo
|
|
|
7
7
|
import {geographic, type Position} from './math';
|
|
8
8
|
import {rectangle} from './elastic/geometry';
|
|
9
9
|
|
|
10
|
+
// Qualified default world formulas with finite coverage and no interior cuts.
|
|
11
|
+
// Keep this explicit: a newly registered kernel is not automatically qualified.
|
|
12
|
+
// Bertin and Sinu-Mollweide need rotated-chart coverage fixes; Gringorten has
|
|
13
|
+
// interior cuts. Singular or folded formulas and parameter overrides remain
|
|
14
|
+
// conservative until their endpoint coverage and correspondence are qualified.
|
|
15
|
+
const directWorldProjections = new Set([
|
|
16
|
+
'gilbert',
|
|
17
|
+
'aitoff',
|
|
18
|
+
'august',
|
|
19
|
+
'baker',
|
|
20
|
+
'boggs',
|
|
21
|
+
'bromley',
|
|
22
|
+
'collignon',
|
|
23
|
+
'craster',
|
|
24
|
+
'eckert1',
|
|
25
|
+
'eckert2',
|
|
26
|
+
'eckert3',
|
|
27
|
+
'eckert4',
|
|
28
|
+
'eckert5',
|
|
29
|
+
'eckert6',
|
|
30
|
+
'fahey',
|
|
31
|
+
'foucaut',
|
|
32
|
+
'ginzburg4',
|
|
33
|
+
'ginzburg5',
|
|
34
|
+
'ginzburg6',
|
|
35
|
+
'ginzburg8',
|
|
36
|
+
'ginzburg9',
|
|
37
|
+
'homolosine',
|
|
38
|
+
'kavrayskiy7',
|
|
39
|
+
'larrivee',
|
|
40
|
+
'laskowski',
|
|
41
|
+
'miller',
|
|
42
|
+
'mollweide',
|
|
43
|
+
'mt-flat-polar-parabolic',
|
|
44
|
+
'mt-flat-polar-quartic',
|
|
45
|
+
'mt-flat-polar-sinusoidal',
|
|
46
|
+
'natural-earth2',
|
|
47
|
+
'nell-hammer',
|
|
48
|
+
'patterson',
|
|
49
|
+
'robinson',
|
|
50
|
+
'sinusoidal',
|
|
51
|
+
'times',
|
|
52
|
+
'van-der-grinten',
|
|
53
|
+
'van-der-grinten2',
|
|
54
|
+
'van-der-grinten3',
|
|
55
|
+
'van-der-grinten4',
|
|
56
|
+
'wagner4',
|
|
57
|
+
'wagner6',
|
|
58
|
+
'winkel3',
|
|
59
|
+
'bonne',
|
|
60
|
+
'bottomley',
|
|
61
|
+
'cylindrical-equal-area',
|
|
62
|
+
'cylindrical-stereographic',
|
|
63
|
+
'foucaut-sinusoidal',
|
|
64
|
+
'hammer',
|
|
65
|
+
'hill',
|
|
66
|
+
'hufnagel',
|
|
67
|
+
'hyperelliptical',
|
|
68
|
+
'lagrange',
|
|
69
|
+
'loximuthal',
|
|
70
|
+
'wagner7',
|
|
71
|
+
'quartic-authalic',
|
|
72
|
+
'wagner'
|
|
73
|
+
]);
|
|
74
|
+
|
|
10
75
|
export type MapTransitionOptions = {strategy?: 'auto' | 'direct' | 'via-globe'};
|
|
11
76
|
export type MapTransitionPart = Readonly<{
|
|
12
77
|
kind: 'line' | 'polygon';
|
|
@@ -316,9 +381,8 @@ function fullCoverage(map: MapEngine): boolean {
|
|
|
316
381
|
covered =
|
|
317
382
|
completeSections && !overlaps && polygonClipping.difference(world, ...domains).length === 0;
|
|
318
383
|
} else if (d.type === 'world') {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
covered = ['sinusoidal', 'mollweide', 'robinson'].includes(d.projection);
|
|
384
|
+
covered =
|
|
385
|
+
directWorldProjections.has(d.projection) && Object.keys(d.parameters ?? {}).length === 0;
|
|
322
386
|
}
|
|
323
387
|
coverageCache.set(map, covered);
|
|
324
388
|
return covered;
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
|
|
4
4
|
|
|
5
5
|
import polygonClipping from 'polygon-clipping';
|
|
6
|
+
import {tracePolygonConstraints, composePolygonEdge} from './polygon-constraints';
|
|
7
|
+
import {sourceArc} from './source-arc';
|
|
6
8
|
import {
|
|
7
9
|
DiscontinuousMap,
|
|
8
10
|
type GeographicPosition,
|
|
@@ -10,7 +12,7 @@ import {
|
|
|
10
12
|
type MapRegion,
|
|
11
13
|
type MapSamplingOptions
|
|
12
14
|
} from './map-layout';
|
|
13
|
-
import {geographic, RAD, type Vector} from './math';
|
|
15
|
+
import {angle, coordinates, geographic, RAD, unit, type Vector} from './math';
|
|
14
16
|
|
|
15
17
|
/** Spherical Mercator, clipped to a finite latitude band after seam splitting. */
|
|
16
18
|
export class MercatorMap extends DiscontinuousMap {
|
|
@@ -117,18 +119,83 @@ export class MercatorMap extends DiscontinuousMap {
|
|
|
117
119
|
continue;
|
|
118
120
|
}
|
|
119
121
|
const at = (t: number): [number, number] => [a[0] + (b[0] - a[0]) * t, a[1] + dy * t];
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
// Locate the latitude cut on the sampled source great-circle arc, not
|
|
123
|
+
// on its projected chord. Retain the parent segment throughout this stage.
|
|
124
|
+
const endpoint = (t: number) => {
|
|
125
|
+
if (t === 0 || t === 1) return {xy: at(t), source: part.sourceCoordinates[i - 1 + t], t};
|
|
126
|
+
const va = geographic(part.sourceCoordinates[i - 1]),
|
|
127
|
+
vb = geographic(part.sourceCoordinates[i]),
|
|
128
|
+
arc = angle(va, vb);
|
|
129
|
+
const sourceAt = (t: number) => {
|
|
130
|
+
if (!arc) return part.sourceCoordinates[i - 1];
|
|
131
|
+
const a = Math.sin((1 - t) * arc) / Math.sin(arc),
|
|
132
|
+
b = Math.sin(t * arc) / Math.sin(arc);
|
|
133
|
+
return coordinates(unit(va.map((v, k) => a * v + b * vb[k]) as Vector));
|
|
134
|
+
};
|
|
135
|
+
const latitude = Math.sign(at(t)[1]) * this.limit;
|
|
136
|
+
let lo = 0,
|
|
137
|
+
hi = 1;
|
|
138
|
+
for (let j = 0; j < 52; j++) {
|
|
139
|
+
const mid = (lo + hi) / 2;
|
|
140
|
+
if (sourceAt(mid)[1] < latitude === part.sourceCoordinates[i - 1][1] < latitude)
|
|
141
|
+
lo = mid;
|
|
142
|
+
else hi = mid;
|
|
143
|
+
}
|
|
144
|
+
const f = (lo + hi) / 2,
|
|
145
|
+
source = sourceAt(f);
|
|
146
|
+
const xy = super.projectPoint(source, part.regionId)!.position;
|
|
147
|
+
return {xy, source, t: f};
|
|
148
|
+
};
|
|
149
|
+
const start = endpoint(t0),
|
|
150
|
+
end = endpoint(t1);
|
|
151
|
+
const append = (point: typeof start, index: number) => {
|
|
152
|
+
current!.coordinates.push(point.xy);
|
|
153
|
+
current!.sourceCoordinates.push(point.source);
|
|
154
|
+
const source = part.provenance,
|
|
155
|
+
target = current!.provenance;
|
|
156
|
+
if (!source || !target) return;
|
|
157
|
+
const segment = source.segments[i];
|
|
158
|
+
const from = source.segments[i - 1] === segment ? source.parameters[i - 1] : 0;
|
|
159
|
+
target.segments.push(point.t === 0 || point.t === 1 ? source.segments[index] : segment);
|
|
160
|
+
target.parameters.push(
|
|
161
|
+
point.t === 0 || point.t === 1
|
|
162
|
+
? source.parameters[index]
|
|
163
|
+
: from + point.t * (source.parameters[i] - from)
|
|
164
|
+
);
|
|
165
|
+
target.vertices.push(
|
|
166
|
+
point.t === 0 || point.t === 1 ? source.vertices[index] : 0xffffffff
|
|
167
|
+
);
|
|
168
|
+
if (options?.pathDistances)
|
|
169
|
+
target.angularDistances.push(
|
|
170
|
+
source.angularDistances[i - 1] +
|
|
171
|
+
point.t * (source.angularDistances[i] - source.angularDistances[i - 1])
|
|
172
|
+
);
|
|
173
|
+
};
|
|
122
174
|
if (!current || t0 > 0) {
|
|
123
175
|
current = {
|
|
124
176
|
regionId: part.regionId,
|
|
125
|
-
coordinates: [
|
|
126
|
-
sourceCoordinates: [
|
|
177
|
+
coordinates: [],
|
|
178
|
+
sourceCoordinates: [],
|
|
179
|
+
...(part.provenance
|
|
180
|
+
? {
|
|
181
|
+
provenance: {
|
|
182
|
+
segments: [],
|
|
183
|
+
parameters: [],
|
|
184
|
+
vertices: [],
|
|
185
|
+
angularDistances: [],
|
|
186
|
+
...(options?.pathBoundaries
|
|
187
|
+
? {startReason: t0 > 0 ? 'coverage-boundary' : part.provenance.startReason}
|
|
188
|
+
: {})
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
: {})
|
|
127
192
|
};
|
|
128
193
|
result.push(current);
|
|
194
|
+
append(start, i - 1);
|
|
129
195
|
}
|
|
130
|
-
|
|
131
|
-
current.
|
|
196
|
+
append(end, i);
|
|
197
|
+
if (current.provenance && options?.pathBoundaries)
|
|
198
|
+
current.provenance.endReason = t1 < 1 ? 'coverage-boundary' : part.provenance!.endReason;
|
|
132
199
|
if (t1 < 1) current = undefined;
|
|
133
200
|
}
|
|
134
201
|
}
|
|
@@ -150,11 +217,83 @@ export class MercatorMap extends DiscontinuousMap {
|
|
|
150
217
|
]
|
|
151
218
|
];
|
|
152
219
|
return super.projectPolygon(rings, options).flatMap(part =>
|
|
153
|
-
polygonClipping.intersection(part.coordinates, box).map(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
220
|
+
polygonClipping.intersection(part.coordinates, box).map(polygon => {
|
|
221
|
+
const traced = tracePolygonConstraints(
|
|
222
|
+
part.coordinates,
|
|
223
|
+
polygon,
|
|
224
|
+
1e-10,
|
|
225
|
+
part.edgeProvenance
|
|
226
|
+
);
|
|
227
|
+
const sources = traced.rings.map(r => r.map(p => this.source(p)));
|
|
228
|
+
traced.owners.forEach((owners, r) =>
|
|
229
|
+
owners.forEach((owner, i) => {
|
|
230
|
+
if (!owner) return;
|
|
231
|
+
const [parentRing, parentEdge] = owner,
|
|
232
|
+
a = part.sourceCoordinates[parentRing][parentEdge],
|
|
233
|
+
b = part.sourceCoordinates[parentRing][parentEdge + 1],
|
|
234
|
+
at = sourceArc(a, b);
|
|
235
|
+
for (const j of [i, i + 1]) {
|
|
236
|
+
const q = traced.rings[r][j],
|
|
237
|
+
first = part.coordinates[parentRing][parentEdge],
|
|
238
|
+
last = part.coordinates[parentRing][parentEdge + 1];
|
|
239
|
+
if (Math.hypot(q[0] - first[0], q[1] - first[1]) < 1e-10 * this.radius)
|
|
240
|
+
sources[r][j] = [...a];
|
|
241
|
+
else if (Math.hypot(q[0] - last[0], q[1] - last[1]) < 1e-10 * this.radius)
|
|
242
|
+
sources[r][j] = [...b];
|
|
243
|
+
else {
|
|
244
|
+
const latitude = Math.sign(q[1]) * this.limit;
|
|
245
|
+
let lo = 0,
|
|
246
|
+
hi = 1;
|
|
247
|
+
if (a[1] < latitude === b[1] < latitude)
|
|
248
|
+
throw new Error('Unresolved Mercator source constraint intersection');
|
|
249
|
+
for (let k = 0; k < 52; k++) {
|
|
250
|
+
const mid = (lo + hi) / 2;
|
|
251
|
+
if (at(mid)[1] < latitude === a[1] < latitude) lo = mid;
|
|
252
|
+
else hi = mid;
|
|
253
|
+
}
|
|
254
|
+
sources[r][j] = at((lo + hi) / 2);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
);
|
|
259
|
+
for (const r of sources) r[r.length - 1] = [...r[0]];
|
|
260
|
+
return {
|
|
261
|
+
regionId: part.regionId,
|
|
262
|
+
sourceCoordinates: sources,
|
|
263
|
+
coordinates: sources.map(r => r.map(p => super.projectPoint(p, part.regionId)!.position)),
|
|
264
|
+
...(options?.polygonEdgeProvenance
|
|
265
|
+
? {
|
|
266
|
+
edgeProvenance: traced.owners.map((owners, r) =>
|
|
267
|
+
owners.map((owner, i) => {
|
|
268
|
+
if (owner) {
|
|
269
|
+
const [pr, pe] = owner;
|
|
270
|
+
return composePolygonEdge(
|
|
271
|
+
part.edgeProvenance![pr][pe],
|
|
272
|
+
part.sourceCoordinates[pr][pe],
|
|
273
|
+
part.sourceCoordinates[pr][pe + 1],
|
|
274
|
+
sources[r][i],
|
|
275
|
+
sources[r][i + 1]
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
if (
|
|
279
|
+
!sources[r]
|
|
280
|
+
.slice(i, i + 2)
|
|
281
|
+
.every(p => Math.abs(Math.abs(p[1]) - this.limit) < 1e-7)
|
|
282
|
+
)
|
|
283
|
+
throw new Error('Unknown Mercator clipping constraint');
|
|
284
|
+
return {
|
|
285
|
+
kind: 'coverage-boundary' as const,
|
|
286
|
+
ring: 0xffffffff,
|
|
287
|
+
segment: 0xffffffff,
|
|
288
|
+
t0: 0,
|
|
289
|
+
t1: 0
|
|
290
|
+
};
|
|
291
|
+
})
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
: {})
|
|
295
|
+
};
|
|
296
|
+
})
|
|
158
297
|
);
|
|
159
298
|
}
|
|
160
299
|
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
// math.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// SPDX-FileCopyrightText: Copyright (c) vis.gl contributors
|
|
4
|
+
import {angle, geographic, type Position} from './math';
|
|
5
|
+
import type {MapPolygonEdge} from './map-layout';
|
|
6
|
+
export type ConstraintOwner = readonly [ring: number, edge: number] | null;
|
|
7
|
+
/** Attribute boolean output to indexed input constraints before final map
|
|
8
|
+
* projection/resampling. Restore vertices erased by collinear-edge merging.
|
|
9
|
+
* Ambiguous ancestry rejects; target rendering coordinates never identify an
|
|
10
|
+
* original source segment. The caller owns this computational clipping chart.
|
|
11
|
+
*/
|
|
12
|
+
export function tracePolygonConstraints(
|
|
13
|
+
input: Position[][],
|
|
14
|
+
output: Position[][],
|
|
15
|
+
tolerance = 1e-10,
|
|
16
|
+
priorities?: MapPolygonEdge[][],
|
|
17
|
+
accept?: (point: Position) => boolean,
|
|
18
|
+
degenerate?: (a: Position, b: Position) => boolean
|
|
19
|
+
) {
|
|
20
|
+
const constraints = input.flatMap((ring, r) =>
|
|
21
|
+
ring.slice(0, -1).map((a, i) => ({a, b: ring[i + 1], r, i}))
|
|
22
|
+
);
|
|
23
|
+
let work = 0;
|
|
24
|
+
const tick = () => {
|
|
25
|
+
if (++work > 20000000) throw new Error('Polygon constraint work limit exceeded');
|
|
26
|
+
};
|
|
27
|
+
const fraction = (p: Position, a: Position, b: Position) => {
|
|
28
|
+
const dx = b[0] - a[0],
|
|
29
|
+
dy = b[1] - a[1],
|
|
30
|
+
l = dx * dx + dy * dy;
|
|
31
|
+
return l ? ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / l : NaN;
|
|
32
|
+
};
|
|
33
|
+
const on = (p: Position, a: Position, b: Position) => {
|
|
34
|
+
tick();
|
|
35
|
+
const t = fraction(p, a, b),
|
|
36
|
+
length = Math.hypot(b[0] - a[0], b[1] - a[1]);
|
|
37
|
+
return (
|
|
38
|
+
length > 0 &&
|
|
39
|
+
t >= -tolerance &&
|
|
40
|
+
t <= 1 + tolerance &&
|
|
41
|
+
Math.hypot(p[0] - a[0] - t * (b[0] - a[0]), p[1] - a[1] - t * (b[1] - a[1])) <=
|
|
42
|
+
tolerance * Math.max(1, length)
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
type Box = readonly [number, number, number, number];
|
|
46
|
+
type Node = {box: Box; ids?: number[]; left?: Node; right?: Node};
|
|
47
|
+
const boxes: Box[] = constraints.map(c => [
|
|
48
|
+
Math.min(c.a[0], c.b[0]),
|
|
49
|
+
Math.min(c.a[1], c.b[1]),
|
|
50
|
+
Math.max(c.a[0], c.b[0]),
|
|
51
|
+
Math.max(c.a[1], c.b[1])
|
|
52
|
+
]);
|
|
53
|
+
const build = (ids: number[]): Node => {
|
|
54
|
+
const box: [number, number, number, number] = [Infinity, Infinity, -Infinity, -Infinity];
|
|
55
|
+
for (const id of ids) {
|
|
56
|
+
tick();
|
|
57
|
+
const b = boxes[id];
|
|
58
|
+
box[0] = Math.min(box[0], b[0]);
|
|
59
|
+
box[1] = Math.min(box[1], b[1]);
|
|
60
|
+
box[2] = Math.max(box[2], b[2]);
|
|
61
|
+
box[3] = Math.max(box[3], b[3]);
|
|
62
|
+
}
|
|
63
|
+
if (ids.length <= 16) return {box, ids};
|
|
64
|
+
const axis = box[2] - box[0] >= box[3] - box[1] ? 0 : 1;
|
|
65
|
+
ids.sort((a, b) => {
|
|
66
|
+
tick();
|
|
67
|
+
return boxes[a][axis] + boxes[a][axis + 2] - boxes[b][axis] - boxes[b][axis + 2];
|
|
68
|
+
});
|
|
69
|
+
const middle = Math.floor(ids.length / 2);
|
|
70
|
+
return {box, left: build(ids.slice(0, middle)), right: build(ids.slice(middle))};
|
|
71
|
+
};
|
|
72
|
+
const root = build(constraints.map((_, i) => i));
|
|
73
|
+
const padding = tolerance * Math.max(1, root.box[2] - root.box[0], root.box[3] - root.box[1]);
|
|
74
|
+
const query = (a: Position, b: Position) => {
|
|
75
|
+
const box: Box = [
|
|
76
|
+
Math.min(a[0], b[0]) - padding,
|
|
77
|
+
Math.min(a[1], b[1]) - padding,
|
|
78
|
+
Math.max(a[0], b[0]) + padding,
|
|
79
|
+
Math.max(a[1], b[1]) + padding
|
|
80
|
+
],
|
|
81
|
+
ids: number[] = [];
|
|
82
|
+
const overlaps = (b: Box) =>
|
|
83
|
+
b[0] <= box[2] && b[2] >= box[0] && b[1] <= box[3] && b[3] >= box[1];
|
|
84
|
+
const visit = (node: Node) => {
|
|
85
|
+
tick();
|
|
86
|
+
if (!overlaps(node.box)) return;
|
|
87
|
+
if (node.ids) {
|
|
88
|
+
for (const id of node.ids) {
|
|
89
|
+
tick();
|
|
90
|
+
if (overlaps(boxes[id])) ids.push(id);
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
visit(node.left!);
|
|
94
|
+
visit(node.right!);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
visit(root);
|
|
98
|
+
return ids.map(id => constraints[id]);
|
|
99
|
+
};
|
|
100
|
+
const rings: Position[][] = [],
|
|
101
|
+
owners: ConstraintOwner[][] = [];
|
|
102
|
+
for (const ring of output) {
|
|
103
|
+
const points: Position[] = [];
|
|
104
|
+
for (let i = 1; i < ring.length; i++) {
|
|
105
|
+
const a = ring[i - 1],
|
|
106
|
+
b = ring[i];
|
|
107
|
+
if (a.every((v, k) => v === b[k])) continue;
|
|
108
|
+
const breaks: {t: number; p: Position}[] = [{t: 0, p: a}];
|
|
109
|
+
for (const c of query(a, b))
|
|
110
|
+
if (on(c.a, a, b) && (!accept || accept(c.a))) {
|
|
111
|
+
const t = fraction(c.a, a, b);
|
|
112
|
+
if (t > 1e-12 && t < 1 - 1e-12) breaks.push({t, p: c.a});
|
|
113
|
+
}
|
|
114
|
+
breaks.sort((a, b) => a.t - b.t);
|
|
115
|
+
for (const item of breaks)
|
|
116
|
+
if (
|
|
117
|
+
!points.length ||
|
|
118
|
+
Math.hypot(...points[points.length - 1].map((v, k) => v - item.p[k])) > 1e-14
|
|
119
|
+
)
|
|
120
|
+
points.push([...item.p]);
|
|
121
|
+
}
|
|
122
|
+
if (points.length < 3) continue;
|
|
123
|
+
points.push([...points[0]]);
|
|
124
|
+
const tags: ConstraintOwner[] = [];
|
|
125
|
+
for (let i = 1; i < points.length; i++) {
|
|
126
|
+
const a = points[i - 1],
|
|
127
|
+
b = points[i];
|
|
128
|
+
if (degenerate?.(a, b)) {
|
|
129
|
+
tags.push(null);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
let candidates = query(a, b).filter(c => on(a, c.a, c.b) && on(b, c.a, c.b));
|
|
133
|
+
const same = (p: Position, q: Position) =>
|
|
134
|
+
Math.hypot(...p.map((v, k) => v - q[k])) <=
|
|
135
|
+
64 * Number.EPSILON * Math.max(1, ...p.map(Math.abs), ...q.map(Math.abs));
|
|
136
|
+
const exact = candidates.filter(
|
|
137
|
+
c => (same(a, c.a) && same(b, c.b)) || (same(a, c.b) && same(b, c.a))
|
|
138
|
+
);
|
|
139
|
+
if (exact.length) candidates = exact;
|
|
140
|
+
if (candidates.some(c => priorities?.[c.r]?.[c.i]?.kind === 'source-boundary'))
|
|
141
|
+
candidates = candidates.filter(c => priorities?.[c.r]?.[c.i]?.kind === 'source-boundary');
|
|
142
|
+
if (candidates.length > 1) {
|
|
143
|
+
const score = (c: (typeof candidates)[number]) =>
|
|
144
|
+
Math.max(
|
|
145
|
+
...[a, b].map(p => {
|
|
146
|
+
const t = Math.min(1, Math.max(0, fraction(p, c.a, c.b)));
|
|
147
|
+
return Math.hypot(
|
|
148
|
+
p[0] - c.a[0] - t * (c.b[0] - c.a[0]),
|
|
149
|
+
p[1] - c.a[1] - t * (c.b[1] - c.a[1])
|
|
150
|
+
);
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
candidates.sort((a, b) => score(a) - score(b));
|
|
154
|
+
const epsilon = 64 * Number.EPSILON * Math.max(1, ...a.map(Math.abs), ...b.map(Math.abs));
|
|
155
|
+
if (score(candidates[1]) - score(candidates[0]) > epsilon) candidates = [candidates[0]];
|
|
156
|
+
else if (Math.hypot(a[0] - b[0], a[1] - b[1]) <= 2 * epsilon) candidates = [];
|
|
157
|
+
else throw new Error('Ambiguous polygon clipping constraint ancestry');
|
|
158
|
+
}
|
|
159
|
+
tags.push(candidates.length ? [candidates[0].r, candidates[0].i] : null);
|
|
160
|
+
}
|
|
161
|
+
rings.push(points);
|
|
162
|
+
owners.push(tags);
|
|
163
|
+
}
|
|
164
|
+
return {rings, owners};
|
|
165
|
+
}
|
|
166
|
+
/** Compose directed fractions through a known parent edge; never search raw
|
|
167
|
+
* input geometry for an original segment after resampling or projection. */
|
|
168
|
+
export function composePolygonEdge(
|
|
169
|
+
parent: MapPolygonEdge,
|
|
170
|
+
a: Position,
|
|
171
|
+
b: Position,
|
|
172
|
+
start: Position,
|
|
173
|
+
end: Position
|
|
174
|
+
): MapPolygonEdge {
|
|
175
|
+
if (parent.kind !== 'source-boundary') return {...parent};
|
|
176
|
+
const v = geographic(a),
|
|
177
|
+
length = angle(v, geographic(b));
|
|
178
|
+
const parameter = (p: Position) => (length ? Math.min(1, angle(v, geographic(p)) / length) : 0);
|
|
179
|
+
return {
|
|
180
|
+
...parent,
|
|
181
|
+
t0: parent.t0 + parameter(start) * (parent.t1 - parent.t0),
|
|
182
|
+
t1: parent.t0 + parameter(end) * (parent.t1 - parent.t0)
|
|
183
|
+
};
|
|
184
|
+
}
|