@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.
- package/dist/baseclient.js +243 -43
- package/dist/baseclient.js.map +1 -1
- package/dist/poly/polybin.d.ts +21 -1
- package/dist/poly/polypack.d.ts +35 -1
- package/dist/poly/topo.d.ts +9 -0
- package/lib/poly/polybin.ts +113 -17
- package/lib/poly/polypack.ts +80 -24
- package/lib/poly/topo.ts +81 -1
- package/package.json +2 -2
package/dist/poly/polybin.d.ts
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
import * as Util from '../util/all';
|
|
2
|
-
|
|
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;
|
package/dist/poly/polypack.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/poly/topo.d.ts
CHANGED
|
@@ -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;
|
package/lib/poly/polybin.ts
CHANGED
|
@@ -50,20 +50,109 @@ function unpackString(coder: Util.Coder, buf8: Uint8Array, buf32: Int32Array, of
|
|
|
50
50
|
return s;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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 +
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
}
|
package/lib/poly/polypack.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
361
|
-
if (f.features.length == 0) return f;
|
|
429
|
+
if (f.features.length === 0) return f;
|
|
362
430
|
|
|
363
|
-
//
|
|
364
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
450
|
+
return f;
|
|
395
451
|
}
|
|
396
|
-
return
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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",
|