@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/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 interface GeoFeatureMap {
36
- [id: string]: GeoFeature;
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 interface GeoFeatureMap
235
+ export type GeoFilterFunc = (s: string) => boolean | undefined;
236
+
237
+ export function geoFilterFromHidden(hidden: any): GeoFilterFunc
234
238
  {
235
- [id: string]: GeoFeature; // Maps id to GeoFeature
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] = true;
330
- this._onChange();
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
- // Create cheapest one (collection if I need to create, otherwise copy from single entry)
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
- multi.allCol();
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
- // Note that this is potentially invalid shortcut when some features are hidden from base geometry.
545
- // Could test overall count of objects vs. this count to check.
546
- this.all.topo = this._topo(e);
547
- this.all.col = this.all.col || e.col;
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 = Util.isEmpty(this.hidden) ? null : this.hidden; // splice function requires NULL for empty
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 };
@@ -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
  }