@dra2020/baseclient 1.0.167 → 1.0.168
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/baseclient.js +87 -15
- package/dist/baseclient.js.map +1 -1
- package/dist/geo/geo.d.ts +9 -3
- package/dist/poly/topovalidate.d.ts +146 -0
- package/lib/geo/geo.ts +65 -16
- package/lib/poly/polybin.ts +27 -0
- package/lib/poly/topovalidate.ts +389 -0
- package/package.json +2 -2
package/dist/geo/geo.d.ts
CHANGED
|
@@ -13,6 +13,9 @@ export type GeoCentroidMap = {
|
|
|
13
13
|
export type HideMap = {
|
|
14
14
|
[id: string]: boolean;
|
|
15
15
|
};
|
|
16
|
+
export interface GeoFeatureMap {
|
|
17
|
+
[id: string]: GeoFeature;
|
|
18
|
+
}
|
|
16
19
|
export declare function dumpMetrics(): void;
|
|
17
20
|
export declare function hidemapConcat(...args: HideMap[]): HideMap;
|
|
18
21
|
export interface NormalizeOptions {
|
|
@@ -32,9 +35,11 @@ export declare function geoCollectionToTopo(col: GeoFeatureCollection): Poly.Top
|
|
|
32
35
|
export declare function geoCollectionToTopoNonNull(col: GeoFeatureCollection): Poly.Topo;
|
|
33
36
|
export declare function geoTopoToCollection(topo: Poly.Topo): GeoFeatureCollection;
|
|
34
37
|
export declare function geoTopoToCollectionNonNull(topo: Poly.Topo): GeoFeatureCollection;
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
export type GeoFilterFunc = (s: string) => boolean | undefined;
|
|
39
|
+
export declare function geoFilterFromHidden(hidden: any): GeoFilterFunc;
|
|
40
|
+
export declare function geoFilteredTopo(topo: Poly.Topo, filter: GeoFilterFunc): Poly.Topo;
|
|
41
|
+
export declare function geoFilteredCollection(col: GeoFeatureCollection, filter: GeoFilterFunc): GeoFeatureCollection;
|
|
42
|
+
export declare function geoFilteredMap(map: GeoFeatureMap, filter: GeoFilterFunc): GeoFeatureMap;
|
|
38
43
|
export type FeatureFunc = (f: GeoFeature) => void;
|
|
39
44
|
interface GeoEntry {
|
|
40
45
|
tag: string;
|
|
@@ -87,6 +92,7 @@ export declare class GeoMultiCollection {
|
|
|
87
92
|
findNoHide(id: string): GeoFeature;
|
|
88
93
|
find(id: string): GeoFeature;
|
|
89
94
|
filter(test: (f: GeoFeature) => boolean): GeoMultiCollection;
|
|
95
|
+
someHidden(): boolean;
|
|
90
96
|
}
|
|
91
97
|
export declare enum geoIntersectOptions {
|
|
92
98
|
Intersects = 0,
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
type Point = [number, number];
|
|
2
|
+
/**
|
|
3
|
+
* Minimal shape this validator needs. A real topology from
|
|
4
|
+
* `topojson-specification` satisfies this. We require both `packed.arcs`
|
|
5
|
+
* (the segment data) and `packed.arcindices` + `objects` (so we can walk
|
|
6
|
+
* only the arcs reachable from the live object set, matching
|
|
7
|
+
* topojson-client's `validateneighbors.js` semantics via `forAllArcPoints`
|
|
8
|
+
* with `onlyOnce: true`).
|
|
9
|
+
*/
|
|
10
|
+
export interface PackedTopologyLike {
|
|
11
|
+
objects: {
|
|
12
|
+
[id: string]: PackedObject;
|
|
13
|
+
};
|
|
14
|
+
packed: {
|
|
15
|
+
arcs: Float64Array | number[];
|
|
16
|
+
arcindices: Int32Array | number[];
|
|
17
|
+
/** Per-topology map from object reference to its offset into `packed.arcindices`.
|
|
18
|
+
* Kept here (not on the object) so the same object can appear in more than one
|
|
19
|
+
* packed topology — see topojson-client/src/packarcindices.js. */
|
|
20
|
+
objectArcs: WeakMap<object, number>;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
interface PackedObject {
|
|
24
|
+
type: string;
|
|
25
|
+
}
|
|
26
|
+
/** A line segment found in more than one arc. Endpoints are reported in their
|
|
27
|
+
* lexicographic-canonical order (smaller endpoint first). */
|
|
28
|
+
export interface DuplicateArcSegment {
|
|
29
|
+
segment: {
|
|
30
|
+
s: Point;
|
|
31
|
+
e: Point;
|
|
32
|
+
};
|
|
33
|
+
/** Arc indices (into `topology.packed.arcs`) that contain this segment, ascending. */
|
|
34
|
+
arcs: number[];
|
|
35
|
+
}
|
|
36
|
+
export interface ValidateArcSegmentsOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Subset of object IDs to validate. If undefined, every object in
|
|
39
|
+
* `topology.objects` is walked (matching `validateneighbors.js` default,
|
|
40
|
+
* which uses `params.topology.objects` when no `objects` field is given).
|
|
41
|
+
*
|
|
42
|
+
* Accepts either a hash whose keys are the object IDs to include (same
|
|
43
|
+
* shape as `topology.objects`), or a Set of IDs.
|
|
44
|
+
*/
|
|
45
|
+
objects?: {
|
|
46
|
+
[id: string]: unknown;
|
|
47
|
+
} | Set<string>;
|
|
48
|
+
}
|
|
49
|
+
export interface ValidateArcSegmentsResult {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
duplicates: DuplicateArcSegment[];
|
|
52
|
+
/** Number of unique arcs actually inspected (reachable from the object set). */
|
|
53
|
+
arcsInspected: number;
|
|
54
|
+
/** Total (arc, consecutive-point-pair) segments inspected across reachable arcs. */
|
|
55
|
+
totalSegments: number;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Verify that no two arcs of a packed TopoJSON topology contain the same line
|
|
59
|
+
* segment — i.e. the same pair of consecutive points, in either direction.
|
|
60
|
+
*
|
|
61
|
+
* Semantics mirror topojson-client's `validateneighbors.js`:
|
|
62
|
+
* - Only arcs **reachable from the current object set** are inspected;
|
|
63
|
+
* orphaned arcs in the packed buffer are ignored.
|
|
64
|
+
* - Each reachable arc is visited at most once (the `onlyOnce: true` flag
|
|
65
|
+
* in the upstream `forAllArcPoints` walker).
|
|
66
|
+
* - Equality is exact Float64 (no epsilon), matching `splice.js`'s
|
|
67
|
+
* `dedup`/`equalArcs`. Endpoint order is normalized lexicographically
|
|
68
|
+
* before keying, so segment `(P→Q)` in one arc and `(Q→P)` in another
|
|
69
|
+
* are treated as the same segment.
|
|
70
|
+
* - Within-arc segment repeats (the same arc containing the same segment
|
|
71
|
+
* twice) are ignored — only cross-arc duplicates are reported.
|
|
72
|
+
*
|
|
73
|
+
* Expected packed layout (same as topojson-client's `forAllArcPoints` / `splice`):
|
|
74
|
+
* topology.packed.arcs (Float64Array)
|
|
75
|
+
* [0] = narcs
|
|
76
|
+
* [1 + 2*a] = npoints for arc `a`
|
|
77
|
+
* [1 + 2*a + 1] = pointoffset for arc `a` (index into the same buffer)
|
|
78
|
+
* at pointoffset: interleaved x, y, x, y, ... (2 floats per point)
|
|
79
|
+
* topology.packed.arcindices (Int32Array)
|
|
80
|
+
* starting at object.packedarcs:
|
|
81
|
+
* Polygon : [nring][narc][arc...]...
|
|
82
|
+
* MultiPolygon : [npoly][nring][narc][arc...]...
|
|
83
|
+
*
|
|
84
|
+
* O(total reachable points) time, O(total reachable unique segments) space.
|
|
85
|
+
*/
|
|
86
|
+
export declare function validateNoDuplicateArcSegments(topology: PackedTopologyLike, options?: ValidateArcSegmentsOptions): ValidateArcSegmentsResult;
|
|
87
|
+
/** One offending point: shared between arcs in a way that violates
|
|
88
|
+
* `ptsToArcs`'s "interior set once" invariant. */
|
|
89
|
+
export interface InteriorPointSharing {
|
|
90
|
+
point: Point;
|
|
91
|
+
/** Arcs in which this point is INTERIOR (not the first or last point). */
|
|
92
|
+
interiorInArcs: number[];
|
|
93
|
+
/** Arcs in which this point is an ENDPOINT (first or last point). */
|
|
94
|
+
endpointInArcs: number[];
|
|
95
|
+
}
|
|
96
|
+
export interface InteriorPointSharingResult {
|
|
97
|
+
ok: boolean;
|
|
98
|
+
/** Points that, in this single topology, are shared across arcs in a way that
|
|
99
|
+
* causes `ptsToArcs` (inside topojson-client's splice) to silently overwrite
|
|
100
|
+
* its `map.set(here, …)` record for that point.
|
|
101
|
+
*
|
|
102
|
+
* Two failure modes are flagged:
|
|
103
|
+
*
|
|
104
|
+
* a) **Interior in 2+ arcs.** `ptsToArcs` says "Interior points will by
|
|
105
|
+
* definition only be set once." If that invariant doesn't hold,
|
|
106
|
+
* whichever arc walks last wins the `map.set`; the earlier arc's
|
|
107
|
+
* record at that point is lost. `newJunctions` then can't detect that
|
|
108
|
+
* the lost arc needed cutting against another topology, so it stays
|
|
109
|
+
* whole and may produce duplicate segments after splice.
|
|
110
|
+
*
|
|
111
|
+
* b) **Interior in one arc AND endpoint in another.** The same overwrite
|
|
112
|
+
* happens regardless of which one walks last:
|
|
113
|
+
* - If the endpoint write lands last, the interior record is lost
|
|
114
|
+
* and the interior arc never gets its junction check.
|
|
115
|
+
* - If the interior write lands last, the endpoint's
|
|
116
|
+
* junction-forcing role at that point is lost.
|
|
117
|
+
* The `ptsToArcs` comment says endpoint overwrites are OK "since ANY
|
|
118
|
+
* endpoint forces a junction" — that only holds when *all* the
|
|
119
|
+
* overwrites are endpoint-on-endpoint. As soon as an interior is in
|
|
120
|
+
* the mix, the invariant is broken.
|
|
121
|
+
*/
|
|
122
|
+
offenses: InteriorPointSharing[];
|
|
123
|
+
/** Number of unique arcs actually inspected (reachable from objects). */
|
|
124
|
+
arcsInspected: number;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Diagnose whether a single input topology to `splice` violates the
|
|
128
|
+
* `ptsToArcs` "interior set once" invariant. This is a candidate explanation
|
|
129
|
+
* for the case where the input topologies report no duplicated arc segments
|
|
130
|
+
* (per `validateNoDuplicateArcSegments`) but the spliced output does — i.e.
|
|
131
|
+
* the inputs share an interior point across arcs, so within ONE topology the
|
|
132
|
+
* pointMap built by `ptsToArcs` silently loses the loser of the overwrite,
|
|
133
|
+
* `newJunctions` then can't find every cut it needs, arcs aren't broken at
|
|
134
|
+
* the right places, and `dedup` (which only catches full-arc matches) can't
|
|
135
|
+
* fuse the resulting partial overlaps.
|
|
136
|
+
*
|
|
137
|
+
* Implementation mirrors `validateNoDuplicateArcSegments`: walk reachable
|
|
138
|
+
* arcs only (matching `forAllArcPoints({onlyOnce: true})`), then for each
|
|
139
|
+
* point of each reachable arc classify it as start, interior, or end and
|
|
140
|
+
* accumulate the per-point arc sets.
|
|
141
|
+
*
|
|
142
|
+
* Exact float equality (no epsilon) — same coordinate semantics as
|
|
143
|
+
* `splice.js`'s `equalArcs`/`pointEqual`.
|
|
144
|
+
*/
|
|
145
|
+
export declare function findCrossArcInteriorPointSharing(topology: PackedTopologyLike, options?: ValidateArcSegmentsOptions): InteriorPointSharingResult;
|
|
146
|
+
export {};
|
package/lib/geo/geo.ts
CHANGED
|
@@ -8,6 +8,8 @@ export type GeoFeatureArray = GeoFeature[];
|
|
|
8
8
|
export type GeoFeatureCollection = geojson.FeatureCollection;
|
|
9
9
|
export type GeoCentroidMap = { [geoid: string]: { x: number, y: number } };
|
|
10
10
|
export type HideMap = { [id: string]: boolean };
|
|
11
|
+
export interface GeoFeatureMap { [id: string]: GeoFeature }
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
// Tracing/debugging aid
|
|
13
15
|
let metrics: { [action: string]: { count: number, total: number } } = {};
|
|
@@ -230,9 +232,43 @@ export function geoTopoToCollectionNonNull(topo: Poly.Topo): GeoFeatureCollectio
|
|
|
230
232
|
return geoTopoToCollection(topo);
|
|
231
233
|
}
|
|
232
234
|
|
|
233
|
-
export
|
|
235
|
+
export type GeoFilterFunc = (s: string) => boolean | undefined;
|
|
236
|
+
|
|
237
|
+
export function geoFilterFromHidden(hidden: any): GeoFilterFunc
|
|
234
238
|
{
|
|
235
|
-
|
|
239
|
+
if (Util.isEmpty(hidden)) return undefined;
|
|
240
|
+
return (s: string) => { return !hidden[s] }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function geoFilteredTopo(topo: Poly.Topo, filter: GeoFilterFunc): Poly.Topo
|
|
244
|
+
{
|
|
245
|
+
if (!topo || !filter)
|
|
246
|
+
return topo;
|
|
247
|
+
let copy: Poly.Topo = Util.shallowCopy(topo);
|
|
248
|
+
copy.objects = {};
|
|
249
|
+
Object.keys(topo.objects).forEach(geoid => {
|
|
250
|
+
if (filter(geoid))
|
|
251
|
+
copy.objects[geoid] = topo.objects[geoid];
|
|
252
|
+
});
|
|
253
|
+
return copy;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function geoFilteredCollection(col: GeoFeatureCollection, filter: GeoFilterFunc): GeoFeatureCollection
|
|
257
|
+
{
|
|
258
|
+
if (!col || !filter)
|
|
259
|
+
return col
|
|
260
|
+
let copy: GeoFeatureCollection = Util.shallowCopy(col);
|
|
261
|
+
copy.features = col.features.filter(f => filter(f.properties.id));
|
|
262
|
+
return copy;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function geoFilteredMap(map: GeoFeatureMap, filter: GeoFilterFunc): GeoFeatureMap
|
|
266
|
+
{
|
|
267
|
+
if (!map || !filter)
|
|
268
|
+
return map;
|
|
269
|
+
let copy: GeoFeatureMap = {};
|
|
270
|
+
Object.keys(map).forEach(geoid => { if (filter(geoid)) copy[geoid] = map[geoid] });
|
|
271
|
+
return copy;
|
|
236
272
|
}
|
|
237
273
|
|
|
238
274
|
export type FeatureFunc = (f: GeoFeature) => void;
|
|
@@ -326,8 +362,11 @@ export class GeoMultiCollection
|
|
|
326
362
|
});
|
|
327
363
|
for (let p in multi.hidden) if (multi.hidden.hasOwnProperty(p))
|
|
328
364
|
{
|
|
329
|
-
this.hidden[p]
|
|
330
|
-
|
|
365
|
+
if (! this.hidden[p])
|
|
366
|
+
{
|
|
367
|
+
this.hidden[p] = true;
|
|
368
|
+
this._onChange();
|
|
369
|
+
}
|
|
331
370
|
}
|
|
332
371
|
}
|
|
333
372
|
|
|
@@ -344,12 +383,17 @@ export class GeoMultiCollection
|
|
|
344
383
|
// Make sure all collection is created
|
|
345
384
|
if (!multi.all.topo && !multi.all.col && !multi.all.map)
|
|
346
385
|
{
|
|
347
|
-
//
|
|
386
|
+
// Make sure at least one all entry is built, preferably topo if there is a base topo
|
|
387
|
+
let e = multi.entries['main'] ?? multi.nthEntry(0);
|
|
348
388
|
if (nEntries > 1)
|
|
349
|
-
|
|
389
|
+
{
|
|
390
|
+
if (e.topo)
|
|
391
|
+
multi.allTopo();
|
|
392
|
+
else
|
|
393
|
+
multi.allCol();
|
|
394
|
+
}
|
|
350
395
|
else
|
|
351
396
|
{
|
|
352
|
-
let e = multi.nthEntry(0);
|
|
353
397
|
multi.all.topo = e.topo;
|
|
354
398
|
multi.all.col = e.col;
|
|
355
399
|
multi.all.map = e.map;
|
|
@@ -538,21 +582,21 @@ export class GeoMultiCollection
|
|
|
538
582
|
{
|
|
539
583
|
// optimise case where one entry
|
|
540
584
|
let n = this.nEntries;
|
|
541
|
-
let e = this.nthEntry(0);
|
|
585
|
+
let e = this.entries['main'] || this.nthEntry(0);
|
|
542
586
|
if (n == 1)
|
|
543
587
|
{
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
this.all.
|
|
547
|
-
this.all.
|
|
548
|
-
this.all.map = this.all.map || e.map;
|
|
588
|
+
let filter = geoFilterFromHidden(this.hidden);
|
|
589
|
+
this.all.topo = geoFilteredTopo(this._topo(e), filter);
|
|
590
|
+
this.all.col = this.all.col || geoFilteredCollection(e.col, filter);
|
|
591
|
+
this.all.map = this.all.map || geoFilteredMap(e.map, filter);
|
|
549
592
|
}
|
|
550
593
|
else if (e.topo)
|
|
551
594
|
{
|
|
552
|
-
// Old-style, goes through map (to filter hidden) and then collection
|
|
553
|
-
// this.all.topo = geoCollectionToTopoNonNull(this.allCol());
|
|
554
595
|
// New style, use splice on packed topologies
|
|
555
|
-
let filterout =
|
|
596
|
+
let filterout = this.someHidden() ? this.hidden : null; // splice function requires NULL for empty
|
|
597
|
+
// DEBUGGING
|
|
598
|
+
filterout = null;
|
|
599
|
+
// DEBUGGING
|
|
556
600
|
let topoarray = Object.values(this.entries).filter((e: GeoEntry) => this._length(e) > 0)
|
|
557
601
|
.map((e: GeoEntry) => { return { topology: this._topo(e), filterout } });
|
|
558
602
|
this.all.topo = topoarray.length == 0 ? null : topoarray.length == 1 ? topoarray[0].topology : Poly.topoSplice(topoarray);
|
|
@@ -726,6 +770,11 @@ export class GeoMultiCollection
|
|
|
726
770
|
});
|
|
727
771
|
return m;
|
|
728
772
|
}
|
|
773
|
+
|
|
774
|
+
someHidden(): boolean
|
|
775
|
+
{
|
|
776
|
+
return !Util.isEmpty(this.hidden)
|
|
777
|
+
}
|
|
729
778
|
}
|
|
730
779
|
|
|
731
780
|
export enum geoIntersectOptions { Intersects, Bounds, BoundsCenter };
|
package/lib/poly/polybin.ts
CHANGED
|
@@ -166,8 +166,21 @@ export function topoToBuffer(coder: Util.Coder, topo: any): ArrayBuffer
|
|
|
166
166
|
// Make sure we're packed
|
|
167
167
|
T.topoPack(topo);
|
|
168
168
|
let savepack = topo.packed;
|
|
169
|
+
// On-disk format predates the topology.packed.objectArcs WeakMap: each object
|
|
170
|
+
// carried its own `packedarcs: number` field in the serialized JSON. Preserve
|
|
171
|
+
// that format. We temporarily project the WeakMap entries back onto each object
|
|
172
|
+
// before stringifying, then strip them so the in-memory topology is unchanged.
|
|
173
|
+
let projected: any[] = [];
|
|
174
|
+
if (savepack.objectArcs)
|
|
175
|
+
for (let id in topo.objects)
|
|
176
|
+
{
|
|
177
|
+
let o = topo.objects[id];
|
|
178
|
+
let off = savepack.objectArcs.get(o);
|
|
179
|
+
if (off !== undefined) { o.packedarcs = off; projected.push(o); }
|
|
180
|
+
}
|
|
169
181
|
delete topo.packed;
|
|
170
182
|
let json = JSON.stringify(topo);
|
|
183
|
+
projected.forEach(o => { delete o.packedarcs; });
|
|
171
184
|
let byteLength = HeaderSize; // 3 lengths + padding
|
|
172
185
|
let stringLength = sizeOfString(coder, json);
|
|
173
186
|
stringLength += pad(stringLength, 8);
|
|
@@ -215,5 +228,19 @@ export function topoFromBuffer(coder: Util.Coder, ab: ArrayBuffer): any
|
|
|
215
228
|
topo.packed = {};
|
|
216
229
|
topo.packed.arcs = new Float64Array(ab, stringLength + HeaderSize, arcsByteLength / 8);
|
|
217
230
|
topo.packed.arcindices = new Int32Array(ab, stringLength + HeaderSize + arcsByteLength, arcindicesByteLength / 4);
|
|
231
|
+
// Reconstruct the WeakMap from the legacy per-object `packedarcs` field, then
|
|
232
|
+
// strip the field so the object representation matches a freshly-packed one.
|
|
233
|
+
let objectArcs = new WeakMap<object, number>();
|
|
234
|
+
if (topo.objects)
|
|
235
|
+
for (let id in topo.objects)
|
|
236
|
+
{
|
|
237
|
+
let o = topo.objects[id];
|
|
238
|
+
if (o && o.packedarcs !== undefined)
|
|
239
|
+
{
|
|
240
|
+
objectArcs.set(o, o.packedarcs);
|
|
241
|
+
delete o.packedarcs;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
topo.packed.objectArcs = objectArcs;
|
|
218
245
|
return topo;
|
|
219
246
|
}
|