@dra2020/baseclient 1.0.170 → 1.0.172

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.
@@ -1,5 +1,25 @@
1
1
  import * as Util from '../util/all';
2
- export declare function packCollection(coder: Util.Coder, col: any): ArrayBuffer;
2
+ /**
3
+ * Serialize a collection to the geopack format.
4
+ *
5
+ * The file wants one contiguous coordinate section with every feature's offset an index into it, and
6
+ * that used to be arranged by requiring the whole collection to already sit in ONE buffer at exactly
7
+ * those offsets - packCollection simply blitted that buffer across whole. Which held only as long as
8
+ * nothing had touched the collection since: pack a collection, add a feature, pack again, and the
9
+ * second pack either redid all the work or produced offsets into a buffer that no longer described it.
10
+ *
11
+ * So the coordinates are gathered feature by feature instead. Each one is given its place in the
12
+ * output, and its floats are copied from wherever they happen to live now - one buffer or several. The
13
+ * work is the same (every float is copied either way), the assumption is gone, and featurePack is free
14
+ * to leave already-packed features alone.
15
+ *
16
+ * THE FORMAT IS UNCHANGED. Offsets are assigned in feature order from zero, which is exactly what a
17
+ * single shared buffer produced, so files written before and after this are byte-identical and each
18
+ * reads under the other.
19
+ *
20
+ * `omit` names properties to leave out of the serialized JSON - see the note on collectOmitted.
21
+ */
22
+ export declare function packCollection(coder: Util.Coder, col: any, omit?: string[]): ArrayBuffer;
3
23
  export declare function unpackCollection(coder: Util.Coder, ab: ArrayBuffer): any;
4
24
  export declare function topoToBuffer(coder: Util.Coder, topo: any): ArrayBuffer;
5
25
  export declare function topoFromBuffer(coder: Util.Coder, ab: ArrayBuffer): any;
@@ -19,8 +19,42 @@ export declare function polyPackTopoArcs(topo: any, arcs: any, iPoly?: number):
19
19
  export declare function polyPack(coords: any, prepack?: PolyPack): PolyPack;
20
20
  export declare function polyPackCopy(coords: any): PolyPack;
21
21
  export declare function polyUnpack(prepack: any): any;
22
+ /** Whether this feature's coordinates are already in packed form. */
23
+ export declare function featureIsPacked(f: any): boolean;
24
+ /**
25
+ * Floats this feature would ADD to a buffer if packed now: zero if it is already packed, and zero for
26
+ * the shapes that carry no packed coordinates at all (points, and features with no geometry).
27
+ */
22
28
  export declare function featurePackSize(f: any): number;
23
- export declare function featurePack(f: any, prepack?: PolyPack): any;
29
+ /**
30
+ * Repair packed buffers that have been through JSON and come back as plain objects.
31
+ *
32
+ * A Float64Array does not survive JSON.stringify/parse - it comes back as {"0":n,"1":n,...} - so a
33
+ * collection that has been round-tripped through JSON while packed arrives with something that looks
34
+ * packed but cannot be read. This puts the numbers back into a real Float64Array.
35
+ *
36
+ * Split out of featurePack, where it used to sit inline in the middle of the packing path, because it
37
+ * is not packing: it is a repair for a specific way collections arrive damaged, it applies to
38
+ * collections that are ALREADY packed, and burying it inside meant featurePack had to return early
39
+ * with a different kind of value to signal it had happened. Returns whether anything was repaired.
40
+ */
41
+ export declare function repairPackedBuffers(col: any): boolean;
42
+ /**
43
+ * Ensure a feature or collection is fully packed, and answer with the object handed in.
44
+ *
45
+ * ALWAYS the same object, whatever was passed and whatever it had to do. The previous signature
46
+ * returned three different things - a PolyPack for a feature, a PolyPack for a collection it packed,
47
+ * and the collection itself for one that was already packed - so a caller could not use the result
48
+ * without knowing which case it had fallen into, and every caller but one ignored it. The one that
49
+ * did not assigned a PolyPack to geometry.coordinates.
50
+ *
51
+ * A collection packs its UNPACKED features into one new buffer and leaves packed ones where they are.
52
+ * That is what stops a second call re-packing work already done, which for a large collection means
53
+ * allocating and copying the whole coordinate set again for nothing. Features already packed keep
54
+ * whatever buffer they were in - the collection may end up spanning several - and packCollection no
55
+ * longer minds, because it copies feature by feature rather than assuming one buffer holds them all.
56
+ */
57
+ export declare function featurePack(f: any): any;
24
58
  export declare function featureUnpack(f: any): any;
25
59
  export type SavePack = WeakMap<any, any>;
26
60
  export declare function featureUnpackTemporarily(f: any, save?: SavePack): SavePack;
@@ -5,6 +5,15 @@ import * as Q from './quad';
5
5
  export type Topo = any;
6
6
  export declare function topoFromCollection(col: any): Topo;
7
7
  export declare function topoContiguity(topo: Topo): any;
8
+ /** Hits are geometry that would otherwise have been rebuilt from arcs; misses are the rebuilds that
9
+ * were actually necessary. That distinction is the point - a raw conversion count cannot tell them
10
+ * apart. Call as Poly.topoFeatureCacheStats() from the console. */
11
+ export declare function topoFeatureCacheStats(): {
12
+ hits: number;
13
+ misses: number;
14
+ pct: number;
15
+ };
16
+ export declare function topoFeatureCacheReset(): void;
8
17
  export declare function topoToFeature(topo: Topo, geoid: string): any;
9
18
  export type TopoSpliceEntry = {
10
19
  topology: Topo;
@@ -50,20 +50,109 @@ function unpackString(coder: Util.Coder, buf8: Uint8Array, buf32: Int32Array, of
50
50
  return s;
51
51
  }
52
52
 
53
- export function packCollection(coder: Util.Coder, col: any): ArrayBuffer
53
+ /**
54
+ * Properties temporarily removed for the duration of a pack, and where they came from.
55
+ *
56
+ * WHY THIS IS WORTH HAVING. The JSON in a geopack is JSON.stringify of the collection, so anything
57
+ * hanging off a feature goes into the file. A Float64Array does not survive that: it serializes as
58
+ * {"0":n,"1":n,...}, which is both enormous and unreadable on the way back - the same damage
59
+ * repairPackedBuffers exists to undo for coordinates. PackedFields are exactly such arrays, and they
60
+ * are DERIVED, so writing them is waste even when it works. Naming them here means a caller cannot
61
+ * forget, and does not have to strip and rebuild them around every call.
62
+ */
63
+ interface OmittedProps
54
64
  {
55
- // Compute size
56
- let pp = PP.featurePack(col) as PP.PolyPack;
57
- let f: any = col.features.find((f: any) => { return f.geometry.packed ? f.geometry.packed.buffer : null });
58
- let buffer: any = f ? f.geometry.packed.buffer : null;
59
- let size = 16; // int endiness, offset to coordinates, float endiness
60
- col.features.forEach((f: any) => { if (f.geometry.packed) delete f.geometry.packed.buffer; }); // reconstructed when unpacking
65
+ o: any;
66
+ name: string;
67
+ value: any;
68
+ }
69
+
70
+ function collectOmitted(col: any, omit?: string[]): OmittedProps[]
71
+ {
72
+ let saved: OmittedProps[] = [];
73
+ if (! omit || omit.length === 0) return saved;
74
+
75
+ const take = (o: any) => {
76
+ if (! o) return;
77
+ omit.forEach(name => {
78
+ if (o[name] !== undefined) { saved.push({ o, name, value: o[name] }); delete o[name] }
79
+ });
80
+ };
81
+
82
+ take(col);
83
+ if (col.features) col.features.forEach((f: any) => { take(f); take(f.properties) });
84
+ return saved;
85
+ }
86
+
87
+ function restoreOmitted(saved: OmittedProps[]): void
88
+ {
89
+ saved.forEach(s => { s.o[s.name] = s.value });
90
+ }
91
+
92
+ /** One feature's coordinates during a pack: where they are now, and where they are going. */
93
+ interface PackSlot
94
+ {
95
+ packed: any; // the feature's geometry.packed
96
+ buffer: any; // its buffer, to be put back afterwards
97
+ fromOffset: number; // its index within that buffer
98
+ toOffset: number; // its index within the file's coordinate section
99
+ length: number;
100
+ }
101
+
102
+ /**
103
+ * Serialize a collection to the geopack format.
104
+ *
105
+ * The file wants one contiguous coordinate section with every feature's offset an index into it, and
106
+ * that used to be arranged by requiring the whole collection to already sit in ONE buffer at exactly
107
+ * those offsets - packCollection simply blitted that buffer across whole. Which held only as long as
108
+ * nothing had touched the collection since: pack a collection, add a feature, pack again, and the
109
+ * second pack either redid all the work or produced offsets into a buffer that no longer described it.
110
+ *
111
+ * So the coordinates are gathered feature by feature instead. Each one is given its place in the
112
+ * output, and its floats are copied from wherever they happen to live now - one buffer or several. The
113
+ * work is the same (every float is copied either way), the assumption is gone, and featurePack is free
114
+ * to leave already-packed features alone.
115
+ *
116
+ * THE FORMAT IS UNCHANGED. Offsets are assigned in feature order from zero, which is exactly what a
117
+ * single shared buffer produced, so files written before and after this are byte-identical and each
118
+ * reads under the other.
119
+ *
120
+ * `omit` names properties to leave out of the serialized JSON - see the note on collectOmitted.
121
+ */
122
+ export function packCollection(coder: Util.Coder, col: any, omit?: string[]): ArrayBuffer
123
+ {
124
+ // Which features arrive unpacked, so they can be left that way. Packing is how the coordinates get
125
+ // written; it is not something the caller asked for, and a caller that handed over an unpacked
126
+ // collection and got a packed one back would find its own coordinates gone. Equally, a caller whose
127
+ // collection was ALREADY packed must not have it unpacked - that rebuilds every coordinate array
128
+ // for nothing, which on a large collection is the memory this change exists to stop spending.
129
+ let wasUnpacked: any[] = [];
130
+ if (col.features) col.features.forEach((f: any) => { if (! PP.featureIsPacked(f)) wasUnpacked.push(f) });
131
+
132
+ PP.featurePack(col);
133
+
134
+ // Where every feature's coordinates are now, and where they will be in the file.
135
+ let slots: PackSlot[] = [];
136
+ let nFloats = 0;
137
+ col.features.forEach((f: any) => {
138
+ const packed = f.geometry ? f.geometry.packed : null;
139
+ if (! packed) return;
140
+ slots.push({ packed, buffer: packed.buffer, fromOffset: packed.offset, toOffset: nFloats,
141
+ length: packed.length });
142
+ nFloats += packed.length;
143
+ });
144
+
145
+ // The JSON carries each feature's offset, so the offsets have to be the file's before it is built.
146
+ // Buffers come out because they are reconstructed on the way back in.
147
+ slots.forEach(s => { s.packed.offset = s.toOffset; delete s.packed.buffer });
148
+ const omitted = collectOmitted(col, omit);
149
+
150
+ let size = 16; // int endianness, offset to coordinates, float endianness
61
151
  let j = JSON.stringify(col);
62
152
  size += sizeOfString(coder, j);
63
153
  size += pad(size, 8);
64
- let fullsize = size + pp.length * 8; // add space for coordinates
154
+ let fullsize = size + nFloats * 8;
65
155
 
66
- // Now pack it
67
156
  let ab = new ArrayBuffer(fullsize);
68
157
  let buf8 = new Uint8Array(ab);
69
158
  let buf32 = new Int32Array(ab);
@@ -79,14 +168,21 @@ export function packCollection(coder: Util.Coder, col: any): ArrayBuffer
79
168
  offset += pad(offset, 8);
80
169
  if (offset != size)
81
170
  throw 'Oops, packing error.';
82
- let foff = offset >> 3;
83
- let buf = pp.buffer as Float64Array;
84
- for (let i: number = 0; i < pp.length; i++)
85
- buf64[foff++] = buf[i];
86
-
87
- // Now restore
88
- col.features.forEach((f: any) => { if (f.geometry.packed) f.geometry.packed.buffer = buffer; });
89
- PP.featureUnpack(col);
171
+
172
+ const foff = offset >> 3;
173
+ slots.forEach(s => {
174
+ const src = s.buffer as Float64Array;
175
+ if (! src) return; // no coordinates: points, and features with no geometry
176
+ let to = foff + s.toOffset;
177
+ let from = s.fromOffset;
178
+ for (let i = 0; i < s.length; i++)
179
+ buf64[to++] = src[from++];
180
+ });
181
+
182
+ // Put the collection back exactly as it was found.
183
+ slots.forEach(s => { s.packed.offset = s.fromOffset; s.packed.buffer = s.buffer });
184
+ restoreOmitted(omitted);
185
+ wasUnpacked.forEach(f => PP.featureUnpack(f));
90
186
 
91
187
  return ab;
92
188
  }
@@ -330,14 +330,58 @@ export function polyUnpack(prepack: any): any
330
330
  return coords.length > 1 ? coords : coords[0];
331
331
  }
332
332
 
333
+ /** Whether this feature's coordinates are already in packed form. */
334
+ export function featureIsPacked(f: any): boolean
335
+ {
336
+ return !! (f && f.geometry && f.geometry.packed !== undefined);
337
+ }
338
+
339
+ /**
340
+ * Floats this feature would ADD to a buffer if packed now: zero if it is already packed, and zero for
341
+ * the shapes that carry no packed coordinates at all (points, and features with no geometry).
342
+ */
333
343
  export function featurePackSize(f: any): number
334
344
  {
345
+ if (featureIsPacked(f)) return 0;
335
346
  if (f && f.geometry && f.geometry.coordinates && f.geometry.type !== 'Point')
336
347
  return polyPackSize(f.geometry.coordinates);
337
348
  return 0;
338
349
  }
339
350
 
340
- export function featurePack(f: any, prepack?: PolyPack): any
351
+ /**
352
+ * Repair packed buffers that have been through JSON and come back as plain objects.
353
+ *
354
+ * A Float64Array does not survive JSON.stringify/parse - it comes back as {"0":n,"1":n,...} - so a
355
+ * collection that has been round-tripped through JSON while packed arrives with something that looks
356
+ * packed but cannot be read. This puts the numbers back into a real Float64Array.
357
+ *
358
+ * Split out of featurePack, where it used to sit inline in the middle of the packing path, because it
359
+ * is not packing: it is a repair for a specific way collections arrive damaged, it applies to
360
+ * collections that are ALREADY packed, and burying it inside meant featurePack had to return early
361
+ * with a different kind of value to signal it had happened. Returns whether anything was repaired.
362
+ */
363
+ export function repairPackedBuffers(col: any): boolean
364
+ {
365
+ if (! col || ! col.features || col.features.length === 0) return false;
366
+
367
+ let ff: any = col.features[0];
368
+ if (! featureIsPacked(ff) || ArrayBuffer.isView(ff.geometry.packed.buffer)) return false;
369
+
370
+ // One buffer is shared by every feature of a collection, so recovering the first recovers all.
371
+ let o: any = ff.geometry.packed.buffer;
372
+ let b = new Float64Array(Util.countKeys(o));
373
+ for (const i in o) b[Number(i)] = o[i];
374
+ col.features.forEach((f: any) => { if (featureIsPacked(f)) f.geometry.packed.buffer = b });
375
+ return true;
376
+ }
377
+
378
+ /**
379
+ * Pack ONE feature's coordinates, into `prepack`'s buffer when given one. Returns the PolyPack that
380
+ * describes where they went, which is how the collection path below advances its offset.
381
+ *
382
+ * Internal: featurePack is the public entry and always answers with the object it was handed.
383
+ */
384
+ function packOne(f: any, prepack?: PolyPack): PolyPack
341
385
  {
342
386
  if (f && f.type === 'Feature')
343
387
  {
@@ -355,45 +399,57 @@ export function featurePack(f: any, prepack?: PolyPack): any
355
399
  }
356
400
  return f.geometry.packed;
357
401
  }
402
+ return null;
403
+ }
404
+
405
+ /**
406
+ * Ensure a feature or collection is fully packed, and answer with the object handed in.
407
+ *
408
+ * ALWAYS the same object, whatever was passed and whatever it had to do. The previous signature
409
+ * returned three different things - a PolyPack for a feature, a PolyPack for a collection it packed,
410
+ * and the collection itself for one that was already packed - so a caller could not use the result
411
+ * without knowing which case it had fallen into, and every caller but one ignored it. The one that
412
+ * did not assigned a PolyPack to geometry.coordinates.
413
+ *
414
+ * A collection packs its UNPACKED features into one new buffer and leaves packed ones where they are.
415
+ * That is what stops a second call re-packing work already done, which for a large collection means
416
+ * allocating and copying the whole coordinate set again for nothing. Features already packed keep
417
+ * whatever buffer they were in - the collection may end up spanning several - and packCollection no
418
+ * longer minds, because it copies feature by feature rather than assuming one buffer holds them all.
419
+ */
420
+ export function featurePack(f: any): any
421
+ {
422
+ if (f && f.type === 'Feature')
423
+ {
424
+ if (! featureIsPacked(f)) packOne(f);
425
+ return f;
426
+ }
358
427
  else if (f && f.type === 'FeatureCollection' && f.features)
359
428
  {
360
- // Empty?
361
- if (f.features.length == 0) return f;
429
+ if (f.features.length === 0) return f;
362
430
 
363
- // Already packed or packed incorrectly?
364
- let ff: any = f.features[0];
365
- if (ff && ff.geometry && ff.geometry.packed)
366
- {
367
- // Already packed
368
- if (ArrayBuffer.isView(ff.geometry.packed.buffer))
369
- return f;
370
- // Improperly packed (buffer converted to object - convert back to buffer
371
- let o: any = ff.geometry.packed.buffer;
372
- let b = new Float64Array(Util.countKeys(o));
373
- for (const i in o) b[Number(i)] = o[i];
374
- f.features.forEach((ff: any) => ff.geometry.packed.buffer = b);
375
- return f;
376
- }
431
+ // Before measuring anything: a collection in this state looks packed but is not readable.
432
+ repairPackedBuffers(f);
377
433
 
378
- // Allocate one large buffer
434
+ // One buffer for everything still unpacked. Sized first so it is allocated exactly once.
379
435
  let nFloats: number = 0;
380
436
  let i: number;
381
437
  for (i = 0; i < f.features.length; i++)
382
438
  nFloats += featurePackSize(f.features[i]);
383
- let ab = new ArrayBuffer(nFloats * 8);
384
- prepack = { offset: 0, length: nFloats, buffer: new Float64Array(ab) };
385
439
 
386
- // Now pack
440
+ let prepack: PolyPack = { offset: 0, length: nFloats,
441
+ buffer: new Float64Array(new ArrayBuffer(nFloats * 8)) };
387
442
  for (i = 0; i < f.features.length; i++)
388
443
  {
389
- let postpack = featurePack(f.features[i], prepack);
444
+ if (featureIsPacked(f.features[i])) continue;
445
+ let postpack = packOne(f.features[i], prepack);
390
446
  prepack.offset += postpack ? postpack.length : 0;
391
447
  }
392
448
  if (prepack.offset != nFloats)
393
449
  throw 'oops, packing bug';
394
- return prepack;
450
+ return f;
395
451
  }
396
- return null;
452
+ return f;
397
453
  }
398
454
 
399
455
  export function featureUnpack(f: any): any
package/lib/poly/topo.ts CHANGED
@@ -36,6 +36,20 @@ function getGEOID(f: any): string
36
36
  export function topoFromCollection(col: any): Topo
37
37
  {
38
38
  if (col == null) return null;
39
+
40
+ // The objects map below is keyed per feature, so every feature needs an id that is present, unique
41
+ // and a string - and the topology is meaningless without one. A collection that has no usable id
42
+ // would otherwise key EVERY feature to undefined and collapse the whole thing onto a single object,
43
+ // silently: the topology comes back with one object in it and nothing anywhere reports a problem.
44
+ // An imported GeoJSON file is the ordinary way to arrive here in that state, because GeoJSON puts a
45
+ // feature's id at the TOP level and most exporters write it there rather than in properties.
46
+ //
47
+ // geoEnsureID also NORMALIZES, which matters as much: it copies GEOID/GEOID10 into properties.id,
48
+ // so the key used here is properties.id whatever the collection called it. Callers that read the
49
+ // objects map back - topoSimplifyCollection does, by properties.id - would otherwise be looking up
50
+ // a key this function never used.
51
+ G.geoEnsureID(col);
52
+
39
53
  let save = PP.featureUnpackTemporarily(col);
40
54
  let prop = getGEOID(col);
41
55
  let objects: any = {};
@@ -91,9 +105,67 @@ export function topoContiguity(topo: Topo): any
91
105
  return result;
92
106
  }
93
107
 
108
+ /**
109
+ * Resolved polygon geometry, keyed on the TOPOLOGY OBJECT and held at module level.
110
+ *
111
+ * The two things a topology object stands for have opposite natures, and the split matters:
112
+ *
113
+ * ARCS are a property of the topology the object appears in. The same object shares different arcs
114
+ * in different topologies - a precinct and a block do not break at the same places - which is why
115
+ * arc offsets live in a per-topology WeakMap (topology.packed.objectArcs) rather than on the object.
116
+ *
117
+ * GEOMETRY is not. The points are the same wherever the object appears. It may end up PACKED into
118
+ * different buffers depending on which collection packed it, but the coordinates are identical, and
119
+ * a collection packed from several buffers is fine by construction (see packCollection).
120
+ *
121
+ * So geometry can be cached once, globally, for the life of the object. Module level rather than a
122
+ * field on the object precisely to avoid the project-then-strip dance that packedarcs still needs
123
+ * around JSON.stringify in topoToBuffer - a trick that has produced real bugs by leaving values
124
+ * persisted where they should not be. A WeakMap is invisible to serialization and dies with the
125
+ * object.
126
+ *
127
+ * THE RULE THAT KEEPS THIS CORRECT. Any operation that changes an object's resolved geometry must
128
+ * produce a NEW object; operations that preserve geometry may share identity. Both hold today: splice
129
+ * renumbers and subdivides arcs, neither of which moves a point, and it now shares objects
130
+ * deliberately; simplification goes through topoFromCollection and so builds new objects. DRA
131
+ * topologies are unquantized, so there is no transform that could decode the same arcs differently.
132
+ */
133
+ const builtGeometry = new WeakMap<any, any>();
134
+ let cacheHits = 0;
135
+ let cacheMisses = 0;
136
+
137
+ /** Hits are geometry that would otherwise have been rebuilt from arcs; misses are the rebuilds that
138
+ * were actually necessary. That distinction is the point - a raw conversion count cannot tell them
139
+ * apart. Call as Poly.topoFeatureCacheStats() from the console. */
140
+ export function topoFeatureCacheStats(): { hits: number, misses: number, pct: number }
141
+ {
142
+ const total = cacheHits + cacheMisses;
143
+ return { hits: cacheHits, misses: cacheMisses, pct: total ? Math.round((cacheHits / total) * 100) : 0 };
144
+ }
145
+
146
+ export function topoFeatureCacheReset(): void { cacheHits = 0; cacheMisses = 0 }
147
+
94
148
  export function topoToFeature(topo: Topo, geoid: string): any
95
149
  {
96
- return correctGeometry(TopoClient.feature(topo, topo.objects[geoid]));
150
+ const o: any = topo.objects[geoid];
151
+ const geometry = o == null ? undefined : builtGeometry.get(o);
152
+ if (geometry !== undefined)
153
+ {
154
+ cacheHits++;
155
+ // A fresh Feature wrapper each call, matching what TopoClient.feature returns - including sharing
156
+ // the properties object with the topology object, which it already did. Only the geometry, the
157
+ // expensive and immutable part, is reused.
158
+ const f: any = { type: 'Feature', properties: o.properties == null ? {} : o.properties, geometry };
159
+ if (o.id != null) f.id = o.id;
160
+ if (o.bbox != null) f.bbox = o.bbox;
161
+ return f;
162
+ }
163
+ cacheMisses++;
164
+ const built = correctGeometry(TopoClient.feature(topo, o));
165
+ // Cached AFTER correctGeometry, so every later reader gets the rewound rings rather than repeating
166
+ // the correction.
167
+ if (o != null && built && built.geometry) builtGeometry.set(o, built.geometry);
168
+ return built;
97
169
  }
98
170
 
99
171
  export type TopoSpliceEntry = { topology: Topo, filterout?: any };
@@ -260,6 +332,14 @@ export function topoSimplifyCollection(col: any, options?: SimplifyOptions): any
260
332
  let oOld: any = topo.objects[f.properties.id];
261
333
  let oNew: any = testtopo.objects[f.properties.id];
262
334
 
335
+ // Every feature must have found its own object. topoFromCollection guarantees it, so reaching
336
+ // here means the keying has diverged - and the two ways that goes wrong are both silent or
337
+ // worse: a collection with no ids used to put every feature on ONE object and come back
338
+ // simplified to a single feature, and a mismatched key dereferences undefined a few lines
339
+ // below. Neither is worth guessing through, so stop and leave the collection alone.
340
+ if (oOld === undefined || oNew === undefined)
341
+ throw `topoSimplifyCollection: feature id ${f.properties.id} has no topology object`;
342
+
263
343
  // Ignore points
264
344
  if (f.geometry && f.geometry.type === 'Point')
265
345
  keepTiny.set(f, f);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dra2020/baseclient",
3
- "version": "1.0.170",
3
+ "version": "1.0.172",
4
4
  "description": "Utility functions for Javascript projects.",
5
5
  "main": "dist/baseclient.js",
6
6
  "types": "./dist/all/all.d.ts",
@@ -44,7 +44,7 @@
44
44
  "webpack-cli": "^5.1.4"
45
45
  },
46
46
  "dependencies": {
47
- "@dra2020/topojson-client": "^3.2.17",
47
+ "@dra2020/topojson-client": "^3.2.18",
48
48
  "@dra2020/topojson-server": "^3.0.103",
49
49
  "@dra2020/topojson-simplify": "^3.0.102",
50
50
  "diff-match-patch": "^1.0.5",