@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
|
@@ -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,
|
|
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:
|
|
200
|
-
const
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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.
|
|
208
|
-
|
|
209
|
-
part.coordinates[i
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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]
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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(
|
|
383
|
+
snapPolygon(prepared),
|
|
237
384
|
c.domain.map(snapPolygon)
|
|
238
385
|
) as Polygons;
|
|
239
386
|
for (const polygon of clipped) {
|
|
240
|
-
const
|
|
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
|
|
244
|
-
sourceCoordinates
|
|
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(
|
|
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
|
-
|
|
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
|
}
|