@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.
@@ -8994,19 +8994,79 @@ function unpackString(coder, buf8, buf32, offset) {
8994
8994
  let s = Util.u82s(coder, buf8, offset + 4, size);
8995
8995
  return s;
8996
8996
  }
8997
- function packCollection(coder, col) {
8998
- // Compute size
8999
- let pp = PP.featurePack(col);
9000
- let f = col.features.find((f) => { return f.geometry.packed ? f.geometry.packed.buffer : null; });
9001
- let buffer = f ? f.geometry.packed.buffer : null;
9002
- let size = 16; // int endiness, offset to coordinates, float endiness
9003
- col.features.forEach((f) => { if (f.geometry.packed)
9004
- delete f.geometry.packed.buffer; }); // reconstructed when unpacking
8997
+ function collectOmitted(col, omit) {
8998
+ let saved = [];
8999
+ if (!omit || omit.length === 0)
9000
+ return saved;
9001
+ const take = (o) => {
9002
+ if (!o)
9003
+ return;
9004
+ omit.forEach(name => {
9005
+ if (o[name] !== undefined) {
9006
+ saved.push({ o, name, value: o[name] });
9007
+ delete o[name];
9008
+ }
9009
+ });
9010
+ };
9011
+ take(col);
9012
+ if (col.features)
9013
+ col.features.forEach((f) => { take(f); take(f.properties); });
9014
+ return saved;
9015
+ }
9016
+ function restoreOmitted(saved) {
9017
+ saved.forEach(s => { s.o[s.name] = s.value; });
9018
+ }
9019
+ /**
9020
+ * Serialize a collection to the geopack format.
9021
+ *
9022
+ * The file wants one contiguous coordinate section with every feature's offset an index into it, and
9023
+ * that used to be arranged by requiring the whole collection to already sit in ONE buffer at exactly
9024
+ * those offsets - packCollection simply blitted that buffer across whole. Which held only as long as
9025
+ * nothing had touched the collection since: pack a collection, add a feature, pack again, and the
9026
+ * second pack either redid all the work or produced offsets into a buffer that no longer described it.
9027
+ *
9028
+ * So the coordinates are gathered feature by feature instead. Each one is given its place in the
9029
+ * output, and its floats are copied from wherever they happen to live now - one buffer or several. The
9030
+ * work is the same (every float is copied either way), the assumption is gone, and featurePack is free
9031
+ * to leave already-packed features alone.
9032
+ *
9033
+ * THE FORMAT IS UNCHANGED. Offsets are assigned in feature order from zero, which is exactly what a
9034
+ * single shared buffer produced, so files written before and after this are byte-identical and each
9035
+ * reads under the other.
9036
+ *
9037
+ * `omit` names properties to leave out of the serialized JSON - see the note on collectOmitted.
9038
+ */
9039
+ function packCollection(coder, col, omit) {
9040
+ // Which features arrive unpacked, so they can be left that way. Packing is how the coordinates get
9041
+ // written; it is not something the caller asked for, and a caller that handed over an unpacked
9042
+ // collection and got a packed one back would find its own coordinates gone. Equally, a caller whose
9043
+ // collection was ALREADY packed must not have it unpacked - that rebuilds every coordinate array
9044
+ // for nothing, which on a large collection is the memory this change exists to stop spending.
9045
+ let wasUnpacked = [];
9046
+ if (col.features)
9047
+ col.features.forEach((f) => { if (!PP.featureIsPacked(f))
9048
+ wasUnpacked.push(f); });
9049
+ PP.featurePack(col);
9050
+ // Where every feature's coordinates are now, and where they will be in the file.
9051
+ let slots = [];
9052
+ let nFloats = 0;
9053
+ col.features.forEach((f) => {
9054
+ const packed = f.geometry ? f.geometry.packed : null;
9055
+ if (!packed)
9056
+ return;
9057
+ slots.push({ packed, buffer: packed.buffer, fromOffset: packed.offset, toOffset: nFloats,
9058
+ length: packed.length });
9059
+ nFloats += packed.length;
9060
+ });
9061
+ // The JSON carries each feature's offset, so the offsets have to be the file's before it is built.
9062
+ // Buffers come out because they are reconstructed on the way back in.
9063
+ slots.forEach(s => { s.packed.offset = s.toOffset; delete s.packed.buffer; });
9064
+ const omitted = collectOmitted(col, omit);
9065
+ let size = 16; // int endianness, offset to coordinates, float endianness
9005
9066
  let j = JSON.stringify(col);
9006
9067
  size += sizeOfString(coder, j);
9007
9068
  size += pad(size, 8);
9008
- let fullsize = size + pp.length * 8; // add space for coordinates
9009
- // Now pack it
9069
+ let fullsize = size + nFloats * 8;
9010
9070
  let ab = new ArrayBuffer(fullsize);
9011
9071
  let buf8 = new Uint8Array(ab);
9012
9072
  let buf32 = new Int32Array(ab);
@@ -9022,14 +9082,20 @@ function packCollection(coder, col) {
9022
9082
  offset += pad(offset, 8);
9023
9083
  if (offset != size)
9024
9084
  throw 'Oops, packing error.';
9025
- let foff = offset >> 3;
9026
- let buf = pp.buffer;
9027
- for (let i = 0; i < pp.length; i++)
9028
- buf64[foff++] = buf[i];
9029
- // Now restore
9030
- col.features.forEach((f) => { if (f.geometry.packed)
9031
- f.geometry.packed.buffer = buffer; });
9032
- PP.featureUnpack(col);
9085
+ const foff = offset >> 3;
9086
+ slots.forEach(s => {
9087
+ const src = s.buffer;
9088
+ if (!src)
9089
+ return; // no coordinates: points, and features with no geometry
9090
+ let to = foff + s.toOffset;
9091
+ let from = s.fromOffset;
9092
+ for (let i = 0; i < s.length; i++)
9093
+ buf64[to++] = src[from++];
9094
+ });
9095
+ // Put the collection back exactly as it was found.
9096
+ slots.forEach(s => { s.packed.offset = s.fromOffset; s.packed.buffer = s.buffer; });
9097
+ restoreOmitted(omitted);
9098
+ wasUnpacked.forEach(f => PP.featureUnpack(f));
9033
9099
  return ab;
9034
9100
  }
9035
9101
  function reverse(buf8, s, n) {
@@ -9749,7 +9815,9 @@ exports.polyPackTopoArcs = polyPackTopoArcs;
9749
9815
  exports.polyPack = polyPack;
9750
9816
  exports.polyPackCopy = polyPackCopy;
9751
9817
  exports.polyUnpack = polyUnpack;
9818
+ exports.featureIsPacked = featureIsPacked;
9752
9819
  exports.featurePackSize = featurePackSize;
9820
+ exports.repairPackedBuffers = repairPackedBuffers;
9753
9821
  exports.featurePack = featurePack;
9754
9822
  exports.featureUnpack = featureUnpack;
9755
9823
  exports.featureUnpackTemporarily = featureUnpackTemporarily;
@@ -10038,12 +10106,55 @@ function polyUnpack(prepack) {
10038
10106
  }
10039
10107
  return coords.length > 1 ? coords : coords[0];
10040
10108
  }
10109
+ /** Whether this feature's coordinates are already in packed form. */
10110
+ function featureIsPacked(f) {
10111
+ return !!(f && f.geometry && f.geometry.packed !== undefined);
10112
+ }
10113
+ /**
10114
+ * Floats this feature would ADD to a buffer if packed now: zero if it is already packed, and zero for
10115
+ * the shapes that carry no packed coordinates at all (points, and features with no geometry).
10116
+ */
10041
10117
  function featurePackSize(f) {
10118
+ if (featureIsPacked(f))
10119
+ return 0;
10042
10120
  if (f && f.geometry && f.geometry.coordinates && f.geometry.type !== 'Point')
10043
10121
  return polyPackSize(f.geometry.coordinates);
10044
10122
  return 0;
10045
10123
  }
10046
- function featurePack(f, prepack) {
10124
+ /**
10125
+ * Repair packed buffers that have been through JSON and come back as plain objects.
10126
+ *
10127
+ * A Float64Array does not survive JSON.stringify/parse - it comes back as {"0":n,"1":n,...} - so a
10128
+ * collection that has been round-tripped through JSON while packed arrives with something that looks
10129
+ * packed but cannot be read. This puts the numbers back into a real Float64Array.
10130
+ *
10131
+ * Split out of featurePack, where it used to sit inline in the middle of the packing path, because it
10132
+ * is not packing: it is a repair for a specific way collections arrive damaged, it applies to
10133
+ * collections that are ALREADY packed, and burying it inside meant featurePack had to return early
10134
+ * with a different kind of value to signal it had happened. Returns whether anything was repaired.
10135
+ */
10136
+ function repairPackedBuffers(col) {
10137
+ if (!col || !col.features || col.features.length === 0)
10138
+ return false;
10139
+ let ff = col.features[0];
10140
+ if (!featureIsPacked(ff) || ArrayBuffer.isView(ff.geometry.packed.buffer))
10141
+ return false;
10142
+ // One buffer is shared by every feature of a collection, so recovering the first recovers all.
10143
+ let o = ff.geometry.packed.buffer;
10144
+ let b = new Float64Array(Util.countKeys(o));
10145
+ for (const i in o)
10146
+ b[Number(i)] = o[i];
10147
+ col.features.forEach((f) => { if (featureIsPacked(f))
10148
+ f.geometry.packed.buffer = b; });
10149
+ return true;
10150
+ }
10151
+ /**
10152
+ * Pack ONE feature's coordinates, into `prepack`'s buffer when given one. Returns the PolyPack that
10153
+ * describes where they went, which is how the collection path below advances its offset.
10154
+ *
10155
+ * Internal: featurePack is the public entry and always answers with the object it was handed.
10156
+ */
10157
+ function packOne(f, prepack) {
10047
10158
  if (f && f.type === 'Feature') {
10048
10159
  if (f.geometry && f.geometry.type === 'Point')
10049
10160
  return prepack ? { offset: prepack.offset, length: 0, buffer: prepack.buffer } : { offset: 0, length: 0, buffer: null };
@@ -10057,41 +10168,52 @@ function featurePack(f, prepack) {
10057
10168
  }
10058
10169
  return f.geometry.packed;
10059
10170
  }
10171
+ return null;
10172
+ }
10173
+ /**
10174
+ * Ensure a feature or collection is fully packed, and answer with the object handed in.
10175
+ *
10176
+ * ALWAYS the same object, whatever was passed and whatever it had to do. The previous signature
10177
+ * returned three different things - a PolyPack for a feature, a PolyPack for a collection it packed,
10178
+ * and the collection itself for one that was already packed - so a caller could not use the result
10179
+ * without knowing which case it had fallen into, and every caller but one ignored it. The one that
10180
+ * did not assigned a PolyPack to geometry.coordinates.
10181
+ *
10182
+ * A collection packs its UNPACKED features into one new buffer and leaves packed ones where they are.
10183
+ * That is what stops a second call re-packing work already done, which for a large collection means
10184
+ * allocating and copying the whole coordinate set again for nothing. Features already packed keep
10185
+ * whatever buffer they were in - the collection may end up spanning several - and packCollection no
10186
+ * longer minds, because it copies feature by feature rather than assuming one buffer holds them all.
10187
+ */
10188
+ function featurePack(f) {
10189
+ if (f && f.type === 'Feature') {
10190
+ if (!featureIsPacked(f))
10191
+ packOne(f);
10192
+ return f;
10193
+ }
10060
10194
  else if (f && f.type === 'FeatureCollection' && f.features) {
10061
- // Empty?
10062
- if (f.features.length == 0)
10195
+ if (f.features.length === 0)
10063
10196
  return f;
10064
- // Already packed or packed incorrectly?
10065
- let ff = f.features[0];
10066
- if (ff && ff.geometry && ff.geometry.packed) {
10067
- // Already packed
10068
- if (ArrayBuffer.isView(ff.geometry.packed.buffer))
10069
- return f;
10070
- // Improperly packed (buffer converted to object - convert back to buffer
10071
- let o = ff.geometry.packed.buffer;
10072
- let b = new Float64Array(Util.countKeys(o));
10073
- for (const i in o)
10074
- b[Number(i)] = o[i];
10075
- f.features.forEach((ff) => ff.geometry.packed.buffer = b);
10076
- return f;
10077
- }
10078
- // Allocate one large buffer
10197
+ // Before measuring anything: a collection in this state looks packed but is not readable.
10198
+ repairPackedBuffers(f);
10199
+ // One buffer for everything still unpacked. Sized first so it is allocated exactly once.
10079
10200
  let nFloats = 0;
10080
10201
  let i;
10081
10202
  for (i = 0; i < f.features.length; i++)
10082
10203
  nFloats += featurePackSize(f.features[i]);
10083
- let ab = new ArrayBuffer(nFloats * 8);
10084
- prepack = { offset: 0, length: nFloats, buffer: new Float64Array(ab) };
10085
- // Now pack
10204
+ let prepack = { offset: 0, length: nFloats,
10205
+ buffer: new Float64Array(new ArrayBuffer(nFloats * 8)) };
10086
10206
  for (i = 0; i < f.features.length; i++) {
10087
- let postpack = featurePack(f.features[i], prepack);
10207
+ if (featureIsPacked(f.features[i]))
10208
+ continue;
10209
+ let postpack = packOne(f.features[i], prepack);
10088
10210
  prepack.offset += postpack ? postpack.length : 0;
10089
10211
  }
10090
10212
  if (prepack.offset != nFloats)
10091
10213
  throw 'oops, packing bug';
10092
- return prepack;
10214
+ return f;
10093
10215
  }
10094
- return null;
10216
+ return f;
10095
10217
  }
10096
10218
  function featureUnpack(f) {
10097
10219
  if (f && f.geometry && f.geometry.packed !== undefined) {
@@ -11082,6 +11204,8 @@ Object.defineProperty(exports, "__esModule", ({ value: true }));
11082
11204
  exports.FsmTopoUnion = void 0;
11083
11205
  exports.topoFromCollection = topoFromCollection;
11084
11206
  exports.topoContiguity = topoContiguity;
11207
+ exports.topoFeatureCacheStats = topoFeatureCacheStats;
11208
+ exports.topoFeatureCacheReset = topoFeatureCacheReset;
11085
11209
  exports.topoToFeature = topoToFeature;
11086
11210
  exports.topoSplice = topoSplice;
11087
11211
  exports.topoToCollection = topoToCollection;
@@ -11099,6 +11223,7 @@ const TopoServer = __importStar(__webpack_require__(/*! @dra2020/topojson-server
11099
11223
  const TopoSimplify = __importStar(__webpack_require__(/*! @dra2020/topojson-simplify */ "@dra2020/topojson-simplify"));
11100
11224
  const Util = __importStar(__webpack_require__(/*! ../util/all */ "./lib/util/all.ts"));
11101
11225
  const FSM = __importStar(__webpack_require__(/*! ../fsm/all */ "./lib/fsm/all.ts"));
11226
+ const G = __importStar(__webpack_require__(/*! ../geo/all */ "./lib/geo/all.ts"));
11102
11227
  const P = __importStar(__webpack_require__(/*! ./poly */ "./lib/poly/poly.ts"));
11103
11228
  const PP = __importStar(__webpack_require__(/*! ./polypack */ "./lib/poly/polypack.ts"));
11104
11229
  const PL = __importStar(__webpack_require__(/*! ./polylabel */ "./lib/poly/polylabel.ts"));
@@ -11121,6 +11246,18 @@ function getGEOID(f) {
11121
11246
  function topoFromCollection(col) {
11122
11247
  if (col == null)
11123
11248
  return null;
11249
+ // The objects map below is keyed per feature, so every feature needs an id that is present, unique
11250
+ // and a string - and the topology is meaningless without one. A collection that has no usable id
11251
+ // would otherwise key EVERY feature to undefined and collapse the whole thing onto a single object,
11252
+ // silently: the topology comes back with one object in it and nothing anywhere reports a problem.
11253
+ // An imported GeoJSON file is the ordinary way to arrive here in that state, because GeoJSON puts a
11254
+ // feature's id at the TOP level and most exporters write it there rather than in properties.
11255
+ //
11256
+ // geoEnsureID also NORMALIZES, which matters as much: it copies GEOID/GEOID10 into properties.id,
11257
+ // so the key used here is properties.id whatever the collection called it. Callers that read the
11258
+ // objects map back - topoSimplifyCollection does, by properties.id - would otherwise be looking up
11259
+ // a key this function never used.
11260
+ G.geoEnsureID(col);
11124
11261
  let save = PP.featureUnpackTemporarily(col);
11125
11262
  let prop = getGEOID(col);
11126
11263
  let objects = {};
@@ -11170,8 +11307,64 @@ function topoContiguity(topo) {
11170
11307
  });
11171
11308
  return result;
11172
11309
  }
11310
+ /**
11311
+ * Resolved polygon geometry, keyed on the TOPOLOGY OBJECT and held at module level.
11312
+ *
11313
+ * The two things a topology object stands for have opposite natures, and the split matters:
11314
+ *
11315
+ * ARCS are a property of the topology the object appears in. The same object shares different arcs
11316
+ * in different topologies - a precinct and a block do not break at the same places - which is why
11317
+ * arc offsets live in a per-topology WeakMap (topology.packed.objectArcs) rather than on the object.
11318
+ *
11319
+ * GEOMETRY is not. The points are the same wherever the object appears. It may end up PACKED into
11320
+ * different buffers depending on which collection packed it, but the coordinates are identical, and
11321
+ * a collection packed from several buffers is fine by construction (see packCollection).
11322
+ *
11323
+ * So geometry can be cached once, globally, for the life of the object. Module level rather than a
11324
+ * field on the object precisely to avoid the project-then-strip dance that packedarcs still needs
11325
+ * around JSON.stringify in topoToBuffer - a trick that has produced real bugs by leaving values
11326
+ * persisted where they should not be. A WeakMap is invisible to serialization and dies with the
11327
+ * object.
11328
+ *
11329
+ * THE RULE THAT KEEPS THIS CORRECT. Any operation that changes an object's resolved geometry must
11330
+ * produce a NEW object; operations that preserve geometry may share identity. Both hold today: splice
11331
+ * renumbers and subdivides arcs, neither of which moves a point, and it now shares objects
11332
+ * deliberately; simplification goes through topoFromCollection and so builds new objects. DRA
11333
+ * topologies are unquantized, so there is no transform that could decode the same arcs differently.
11334
+ */
11335
+ const builtGeometry = new WeakMap();
11336
+ let cacheHits = 0;
11337
+ let cacheMisses = 0;
11338
+ /** Hits are geometry that would otherwise have been rebuilt from arcs; misses are the rebuilds that
11339
+ * were actually necessary. That distinction is the point - a raw conversion count cannot tell them
11340
+ * apart. Call as Poly.topoFeatureCacheStats() from the console. */
11341
+ function topoFeatureCacheStats() {
11342
+ const total = cacheHits + cacheMisses;
11343
+ return { hits: cacheHits, misses: cacheMisses, pct: total ? Math.round((cacheHits / total) * 100) : 0 };
11344
+ }
11345
+ function topoFeatureCacheReset() { cacheHits = 0; cacheMisses = 0; }
11173
11346
  function topoToFeature(topo, geoid) {
11174
- return correctGeometry(TopoClient.feature(topo, topo.objects[geoid]));
11347
+ const o = topo.objects[geoid];
11348
+ const geometry = o == null ? undefined : builtGeometry.get(o);
11349
+ if (geometry !== undefined) {
11350
+ cacheHits++;
11351
+ // A fresh Feature wrapper each call, matching what TopoClient.feature returns - including sharing
11352
+ // the properties object with the topology object, which it already did. Only the geometry, the
11353
+ // expensive and immutable part, is reused.
11354
+ const f = { type: 'Feature', properties: o.properties == null ? {} : o.properties, geometry };
11355
+ if (o.id != null)
11356
+ f.id = o.id;
11357
+ if (o.bbox != null)
11358
+ f.bbox = o.bbox;
11359
+ return f;
11360
+ }
11361
+ cacheMisses++;
11362
+ const built = correctGeometry(TopoClient.feature(topo, o));
11363
+ // Cached AFTER correctGeometry, so every later reader gets the rewound rings rather than repeating
11364
+ // the correction.
11365
+ if (o != null && built && built.geometry)
11366
+ builtGeometry.set(o, built.geometry);
11367
+ return built;
11175
11368
  }
11176
11369
  function topoSplice(topoarray) {
11177
11370
  if (topoarray)
@@ -11305,6 +11498,13 @@ function topoSimplifyCollection(col, options) {
11305
11498
  col.features.forEach((f) => {
11306
11499
  let oOld = topo.objects[f.properties.id];
11307
11500
  let oNew = testtopo.objects[f.properties.id];
11501
+ // Every feature must have found its own object. topoFromCollection guarantees it, so reaching
11502
+ // here means the keying has diverged - and the two ways that goes wrong are both silent or
11503
+ // worse: a collection with no ids used to put every feature on ONE object and come back
11504
+ // simplified to a single feature, and a mismatched key dereferences undefined a few lines
11505
+ // below. Neither is worth guessing through, so stop and leave the collection alone.
11506
+ if (oOld === undefined || oNew === undefined)
11507
+ throw `topoSimplifyCollection: feature id ${f.properties.id} has no topology object`;
11308
11508
  // Ignore points
11309
11509
  if (f.geometry && f.geometry.type === 'Point')
11310
11510
  keepTiny.set(f, f);