@dra2020/baseclient 1.0.0

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.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/dist/all/all.d.ts +18 -0
  4. package/dist/baseclient.js +9567 -0
  5. package/dist/baseclient.js.map +1 -0
  6. package/dist/context/all.d.ts +1 -0
  7. package/dist/context/context.d.ts +13 -0
  8. package/dist/filterexpr/all.d.ts +1 -0
  9. package/dist/filterexpr/filterexpr.d.ts +64 -0
  10. package/dist/fsm/all.d.ts +1 -0
  11. package/dist/fsm/fsm.d.ts +118 -0
  12. package/dist/logabstract/all.d.ts +1 -0
  13. package/dist/logabstract/log.d.ts +26 -0
  14. package/dist/logclient/all.d.ts +1 -0
  15. package/dist/logclient/log.d.ts +6 -0
  16. package/dist/ot-editutil/all.d.ts +2 -0
  17. package/dist/ot-editutil/oteditutil.d.ts +14 -0
  18. package/dist/ot-editutil/otmaputil.d.ts +21 -0
  19. package/dist/ot-js/all.d.ts +9 -0
  20. package/dist/ot-js/otarray.d.ts +111 -0
  21. package/dist/ot-js/otclientengine.d.ts +38 -0
  22. package/dist/ot-js/otcomposite.d.ts +37 -0
  23. package/dist/ot-js/otcounter.d.ts +17 -0
  24. package/dist/ot-js/otengine.d.ts +22 -0
  25. package/dist/ot-js/otmap.d.ts +19 -0
  26. package/dist/ot-js/otserverengine.d.ts +38 -0
  27. package/dist/ot-js/otsession.d.ts +111 -0
  28. package/dist/ot-js/ottypes.d.ts +29 -0
  29. package/dist/poly/all.d.ts +15 -0
  30. package/dist/poly/blend.d.ts +1 -0
  31. package/dist/poly/boundbox.d.ts +16 -0
  32. package/dist/poly/cartesian.d.ts +5 -0
  33. package/dist/poly/graham-scan.d.ts +8 -0
  34. package/dist/poly/hash.d.ts +1 -0
  35. package/dist/poly/matrix.d.ts +24 -0
  36. package/dist/poly/minbound.d.ts +1 -0
  37. package/dist/poly/poly.d.ts +52 -0
  38. package/dist/poly/polybin.d.ts +5 -0
  39. package/dist/poly/polylabel.d.ts +7 -0
  40. package/dist/poly/polypack.d.ts +30 -0
  41. package/dist/poly/polyround.d.ts +1 -0
  42. package/dist/poly/polysimplify.d.ts +1 -0
  43. package/dist/poly/quad.d.ts +48 -0
  44. package/dist/poly/selfintersect.d.ts +1 -0
  45. package/dist/poly/shamos.d.ts +1 -0
  46. package/dist/poly/simplify.d.ts +2 -0
  47. package/dist/poly/topo.d.ts +46 -0
  48. package/dist/poly/union.d.ts +48 -0
  49. package/dist/util/all.d.ts +5 -0
  50. package/dist/util/bintrie.d.ts +93 -0
  51. package/dist/util/countedhash.d.ts +19 -0
  52. package/dist/util/gradient.d.ts +15 -0
  53. package/dist/util/indexedarray.d.ts +15 -0
  54. package/dist/util/util.d.ts +68 -0
  55. package/docs/context.md +2 -0
  56. package/docs/fsm.md +243 -0
  57. package/docs/logabstract.md +2 -0
  58. package/docs/logclient.md +2 -0
  59. package/docs/ot-editutil.md +2 -0
  60. package/docs/ot-js.md +95 -0
  61. package/docs/poly.md +103 -0
  62. package/docs/util.md +2 -0
  63. package/lib/all/all.ts +19 -0
  64. package/lib/context/all.ts +1 -0
  65. package/lib/context/context.ts +82 -0
  66. package/lib/filterexpr/all.ts +1 -0
  67. package/lib/filterexpr/filterexpr.ts +625 -0
  68. package/lib/fsm/all.ts +1 -0
  69. package/lib/fsm/fsm.ts +549 -0
  70. package/lib/logabstract/all.ts +1 -0
  71. package/lib/logabstract/log.ts +55 -0
  72. package/lib/logclient/all.ts +1 -0
  73. package/lib/logclient/log.ts +105 -0
  74. package/lib/ot-editutil/all.ts +2 -0
  75. package/lib/ot-editutil/oteditutil.ts +180 -0
  76. package/lib/ot-editutil/otmaputil.ts +209 -0
  77. package/lib/ot-js/all.ts +9 -0
  78. package/lib/ot-js/otarray.ts +1168 -0
  79. package/lib/ot-js/otclientengine.ts +327 -0
  80. package/lib/ot-js/otcomposite.ts +247 -0
  81. package/lib/ot-js/otcounter.ts +145 -0
  82. package/lib/ot-js/otengine.ts +71 -0
  83. package/lib/ot-js/otmap.ts +144 -0
  84. package/lib/ot-js/otserverengine.ts +329 -0
  85. package/lib/ot-js/otsession.ts +199 -0
  86. package/lib/ot-js/ottypes.ts +98 -0
  87. package/lib/poly/all.ts +15 -0
  88. package/lib/poly/blend.ts +27 -0
  89. package/lib/poly/boundbox.ts +102 -0
  90. package/lib/poly/cartesian.ts +130 -0
  91. package/lib/poly/graham-scan.ts +401 -0
  92. package/lib/poly/hash.ts +15 -0
  93. package/lib/poly/matrix.ts +309 -0
  94. package/lib/poly/minbound.ts +211 -0
  95. package/lib/poly/poly.ts +767 -0
  96. package/lib/poly/polybin.ts +218 -0
  97. package/lib/poly/polylabel.ts +204 -0
  98. package/lib/poly/polypack.ts +458 -0
  99. package/lib/poly/polyround.ts +30 -0
  100. package/lib/poly/polysimplify.ts +24 -0
  101. package/lib/poly/quad.ts +272 -0
  102. package/lib/poly/selfintersect.ts +87 -0
  103. package/lib/poly/shamos.ts +297 -0
  104. package/lib/poly/simplify.ts +119 -0
  105. package/lib/poly/topo.ts +525 -0
  106. package/lib/poly/union.ts +371 -0
  107. package/lib/util/all.ts +5 -0
  108. package/lib/util/bintrie.ts +603 -0
  109. package/lib/util/countedhash.ts +83 -0
  110. package/lib/util/gradient.ts +108 -0
  111. package/lib/util/indexedarray.ts +80 -0
  112. package/lib/util/util.ts +695 -0
  113. package/package.json +52 -0
@@ -0,0 +1,119 @@
1
+ /*
2
+ Adapted from:
3
+ (c) 2017, Vladimir Agafonkin
4
+ Simplify.js, a high-performance JS polyline simplification library
5
+ mourner.github.io/simplify-js
6
+ */
7
+
8
+ export type Point = [ number, number];
9
+
10
+ // square distance between 2 points
11
+ function getSqDist(p1: Point, p2: Point): number
12
+ {
13
+ var dx = p1[0] - p2[0],
14
+ dy = p1[1] - p2[1];
15
+
16
+ return dx * dx + dy * dy;
17
+ }
18
+
19
+ // square distance from a point to a segment
20
+ function getSqSegDist(p: Point, p1: Point, p2: Point): number
21
+ {
22
+ var x = p1[0],
23
+ y = p1[1],
24
+ dx = p2[0] - x,
25
+ dy = p2[1] - y;
26
+
27
+ if (dx !== 0 || dy !== 0) {
28
+
29
+ var t = ((p[0] - x) * dx + (p[1] - y) * dy) / (dx * dx + dy * dy);
30
+
31
+ if (t > 1) {
32
+ x = p2[0];
33
+ y = p2[1];
34
+
35
+ } else if (t > 0) {
36
+ x += dx * t;
37
+ y += dy * t;
38
+ }
39
+ }
40
+
41
+ dx = p[0] - x;
42
+ dy = p[1] - y;
43
+
44
+ return dx * dx + dy * dy;
45
+ }
46
+
47
+
48
+ // rest of the code doesn't care about point format
49
+
50
+ // basic distance-based simplification
51
+ function simplifyRadialDist(points: Point[], sqTolerance: number): Point[]
52
+ {
53
+ let prevPoint: Point = points[0],
54
+ newPoints: Point[] = [prevPoint],
55
+ point: Point;
56
+
57
+ for (let i = 1, len = points.length; i < len; i++) {
58
+ point = points[i];
59
+
60
+ if (getSqDist(point, prevPoint) > sqTolerance) {
61
+ newPoints.push(point);
62
+ prevPoint = point;
63
+ }
64
+ }
65
+
66
+ if (prevPoint !== point) newPoints.push(point);
67
+
68
+ return newPoints;
69
+ }
70
+
71
+ function simplifyDPStep(points: Point[], first: number, last: number, sqTolerance: number, simplified: Point[]): void
72
+ {
73
+ let maxSqDist: number = sqTolerance,
74
+ index: number;
75
+
76
+ for (let i = first + 1; i < last; i++)
77
+ {
78
+ let sqDist = getSqSegDist(points[i], points[first], points[last]);
79
+
80
+ if (sqDist > maxSqDist)
81
+ {
82
+ index = i;
83
+ maxSqDist = sqDist;
84
+ }
85
+ }
86
+
87
+ if (maxSqDist > sqTolerance)
88
+ {
89
+ if (index - first > 1) simplifyDPStep(points, first, index, sqTolerance, simplified);
90
+ simplified.push(points[index]);
91
+ if (last - index > 1) simplifyDPStep(points, index, last, sqTolerance, simplified);
92
+ }
93
+ }
94
+
95
+ // simplification using Ramer-Douglas-Peucker algorithm
96
+ function simplifyDouglasPeucker(points: Point[], sqTolerance: number): Point[]
97
+ {
98
+ let last: number = points.length - 1;
99
+
100
+ let simplified: Point[] = [points[0]];
101
+ simplifyDPStep(points, 0, last, sqTolerance, simplified);
102
+ simplified.push(points[last]);
103
+
104
+ return simplified;
105
+ }
106
+
107
+ // both algorithms combined for awesome performance
108
+ export function simplify(points: Point[], tolerance: number = 1, highestQuality: boolean = false): Point[]
109
+ {
110
+ if (tolerance == 0 || points.length <= 2)
111
+ return points;
112
+
113
+ let sqTolerance: number = tolerance * tolerance;
114
+
115
+ points = highestQuality ? points : simplifyRadialDist(points, sqTolerance);
116
+ points = simplifyDouglasPeucker(points, sqTolerance);
117
+
118
+ return points;
119
+ }
@@ -0,0 +1,525 @@
1
+
2
+ // Forked version that supports packing
3
+ import * as TopoClient from '@dra2020/topojson-client';
4
+ // Forked version that fixes self-looping hole problem
5
+ import * as TopoServer from '@dra2020/topojson-server';
6
+ // Forked version that fixes performance problem
7
+ import * as TopoSimplify from '@dra2020/topojson-simplify';
8
+
9
+ import * as Util from '../util/all';
10
+ import * as FSM from '../fsm/all';
11
+
12
+ import * as P from './poly';
13
+ import * as Q from './quad';
14
+ import * as PP from './polypack';
15
+ import * as PL from './polylabel';
16
+ import { selfIntersectFast } from './shamos';
17
+
18
+ export type Topo = any;
19
+
20
+ function getGEOID(f: any): string
21
+ {
22
+ if (f.features && f.features.length) f = f.features[0];
23
+ else if (Array.isArray(f)) f = f[0];
24
+ if (f.properties.id !== undefined) return 'id';
25
+ if (f.properties.GEOID !== undefined) return 'GEOID';
26
+ if (f.properties.GEOID10 !== undefined) return 'GEOID10';
27
+ }
28
+
29
+ export function topoFromCollection(col: any): Topo
30
+ {
31
+ if (col == null) return null;
32
+ let save = PP.featureUnpackTemporarily(col);
33
+ let prop = getGEOID(col);
34
+ let objects: any = {};
35
+ col.features.forEach((f: any) => objects[f.properties[prop]] = f);
36
+ let topo = TopoServer.topology(objects);
37
+ PP.featureRepack(col, save);
38
+ if (col.datasets)
39
+ (topo as any).datasets = col.datasets;
40
+ return topo;
41
+ }
42
+
43
+ function ringsCancel(outerPoly: any, innerRing: any): boolean
44
+ {
45
+ if (outerPoly.length != 1) return false;
46
+ let outerRing = outerPoly[0];
47
+ if (outerRing.length !== innerRing.length) return false;
48
+ let n = outerRing.length-1;
49
+ let i = 0;
50
+ for (; i <= n; i++, n--)
51
+ if (! Util.shallowEqual(outerRing[i], innerRing[n]))
52
+ return false;
53
+ return true;
54
+ }
55
+
56
+ function correctGeometry(f: any): any
57
+ {
58
+ if (f && f.geometry && f.geometry.type === 'MultiPolygon' && f.geometry.coordinates)
59
+ {
60
+ let multiPoly = f.geometry.coordinates;
61
+
62
+ /* Comment this out right now - might have been do to not rewinding merge output
63
+ // topojson will under certain circumstances return a MultiPolygon with the first Polygon containing holes
64
+ // that are precisely filled by a subsequent polygon. We make a secondary union pass to try to correct for this.
65
+ // If we have a really degenerate multipolygon (test above some number of polygons) omit this expensive pass
66
+ // since cleanup is unlikely.
67
+ if (multiPoly.length > 1 && multiPoly.length < 50)
68
+ {
69
+ let result = Q.unionPolys(multiPoly);
70
+ if (Util.depthof(result) == 4) result = [ result ];
71
+ multiPoly = result;
72
+ }
73
+ */
74
+
75
+ // Convert degenerate MultiPolygon to Polygon
76
+ if (multiPoly.length == 1)
77
+ {
78
+ f.geometry.type = 'Polygon';
79
+ f.geometry.coordinates = multiPoly[0];
80
+ }
81
+ }
82
+
83
+ return f;
84
+ }
85
+
86
+ export function topoContiguity(topo: Topo): any
87
+ {
88
+ let objects: any[] = Object.values(topo.objects);
89
+ let geoid = getGEOID(objects);
90
+ objects.forEach((o: any) => { o.properties.id = o.properties[geoid] });
91
+ let neighbors = TopoClient.neighbors(objects, true);
92
+ let result: any = {};
93
+ result['OUT_OF_STATE'] = [];
94
+ objects.forEach((o: any, i: number) => {
95
+ result[o.properties.id] = neighbors[i].map((j: any) => {
96
+ if (j >= 0)
97
+ return objects[j].properties.id;
98
+ else
99
+ {
100
+ result['OUT_OF_STATE'].push(o.properties.id);
101
+ return 'OUT_OF_STATE';
102
+ }
103
+ });
104
+ });
105
+ return result;
106
+ }
107
+
108
+ export function topoToFeature(topo: Topo, geoid: string): any
109
+ {
110
+ return correctGeometry(TopoClient.feature(topo, topo.objects[geoid]));
111
+ }
112
+
113
+ export function topoToCollection(topo: Topo): any
114
+ {
115
+ let col: any = { type: 'FeatureCollection', features: [] };
116
+ Object.keys(topo.objects).forEach((geoid: string) => {
117
+ col.features.push(topoToFeature(topo, geoid));
118
+ });
119
+ if (topo.datasets) col.datasets = topo.datasets;
120
+ return col;
121
+ }
122
+
123
+ function keepArcs(topo: any, arcs: any, keepweight: number): void
124
+ {
125
+ arcs.forEach((a: any) => {
126
+ if (Array.isArray(a))
127
+ keepArcs(topo, a, keepweight);
128
+ else
129
+ {
130
+ let arc = topo.arcs[a < 0 ? ~a : a];
131
+ arc.forEach((pt: any) => {
132
+ if (pt[2] >= keepweight)
133
+ pt[2] = Infinity;
134
+ });
135
+ }
136
+ });
137
+ }
138
+
139
+ function fullFidelity(topo: any, arcs: any): boolean
140
+ {
141
+ let bFull = true;
142
+
143
+ arcs.forEach((a: any) => {
144
+ if (Array.isArray(a))
145
+ bFull = bFull && fullFidelity(topo, a);
146
+ else
147
+ {
148
+ let arc = topo.arcs[a < 0 ? ~a : a];
149
+ arc.forEach((pt: any) => {
150
+ if (bFull && pt[2] !== Infinity)
151
+ bFull = false;
152
+ });
153
+ }
154
+ });
155
+ return bFull;
156
+ }
157
+
158
+ function misMatchPoly(p1: any, p2: any): boolean
159
+ {
160
+ if (p1 == null || p2 == null || p1.length != p2.length) return true;
161
+ for (let i = 0; i < p1.length; i++)
162
+ if (p1[i] == null || p2[i] == null) return true;
163
+ return false;
164
+ }
165
+
166
+ function misMatchMulti(m1: any, m2: any): boolean
167
+ {
168
+ if (m1 == null || m2 == null || m1.length != m2.length) return true;
169
+ for (let i = 0; i < m1.length; i++)
170
+ if (misMatchPoly(m1[i], m2[i])) return true;
171
+ return false;
172
+ }
173
+
174
+ function misMatchObject(o1: any, o2: any): boolean
175
+ {
176
+ if (o1 == null || o2 == null || o1.type !== o2.type) return true;
177
+ return (o1.type === 'MultiPolygon') ? misMatchMulti(o1.arcs, o2.arcs) : misMatchPoly(o1.arcs, o2.arcs);
178
+ }
179
+
180
+ const MAX_TRIES = 25;
181
+
182
+ function bigTimeString(ms: number): string
183
+ {
184
+ let seconds = Math.trunc(ms / 1000);
185
+ let minutes = Math.trunc(seconds / 60);
186
+ seconds -= minutes * 60;
187
+ return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
188
+ }
189
+
190
+ export interface SimplifyOptions
191
+ {
192
+ minArea?: number,
193
+ }
194
+
195
+ const DefaultSimplifyOptions: SimplifyOptions = { minArea: 500 };
196
+
197
+ //
198
+ // topoSimplifyCollection:
199
+ // This implements our simplification strategy for block/precinct level shapes. The basic idea is to
200
+ // ensure that all shapes generated at the block level have non-zero area and do not introduce self-crossing
201
+ // edges. Since topological simplification happens at the edge level, self-crossing is a risk when opposite
202
+ // edges make simplification decisions that cause the edges to cross. Crossing edges result in visual anomalies.
203
+ // In addition, our basic strategy is that if we have well-formed block shapes, all other shape processing
204
+ // (virtual features, precincts, counties, districts) can be done by fast merges that generate well-formed
205
+ // shapes without holes, overlaps or crossing edges.
206
+ // The strategy here is to first just do topological simplification at the default simplification level. We then
207
+ // scan for problematic shapes (zero area, too thin and narrow or self-crossing) and keep additional points on
208
+ // the edges (arcs) for those shapes, adding additional points on each iteration step until we have a good set.
209
+ // For tiny shapes (zero area or short and thin) we just immediately say keep all points since these are mostly
210
+ // shapes with a small number of points anyway.
211
+ //
212
+
213
+ export function topoSimplifyCollection(col: any, options?: SimplifyOptions): any
214
+ {
215
+ options = Util.shallowAssignImmutable(DefaultSimplifyOptions, options);
216
+
217
+ if (col == null) return null;
218
+ let elapsedTotal = new Util.Elapsed();
219
+ let elapsed = new Util.Elapsed();
220
+ let topo = topoFromCollection(col);
221
+ console.log(`topoSimplifyCollection: fromCollection: ${Math.round(elapsed.ms())}ms`);
222
+ elapsed.start();
223
+ topo = TopoSimplify.presimplify(topo, TopoSimplify['sphericalTriangleArea']);
224
+ console.log(`topoSimplifyCollection: presimplify: ${Math.round(elapsed.ms())}ms`);
225
+ elapsed.start();
226
+
227
+ // Keep iterating on removing simplification from degenerate shapes
228
+ let nTries = 1;
229
+ let nBadLast = Number.MAX_VALUE;
230
+ let keepweight = 1E-11;
231
+ let nLabelTolerance = 1E-4;
232
+ let minArea = options.minArea;
233
+ let keepTiny = new WeakMap<any, any>();
234
+ while (true)
235
+ {
236
+ let testtopo = TopoSimplify.simplify(topo, 1E-10);
237
+ elapsed.start();
238
+ let nBad = 0;
239
+ let nTiny = 0;
240
+ col.features.forEach((f: any) => {
241
+ let oOld: any = topo.objects[f.properties.id];
242
+ let oNew: any = testtopo.objects[f.properties.id];
243
+ if (! keepTiny.has(f))
244
+ {
245
+ // Walk through each polygon of a multipolygon separately since I may have a large non-degenerate
246
+ // shape combined with degenerate smaller shapes that I want to identify. I do not examine holes
247
+ // separately under the assumption that holes will have matching polygons that would be identified
248
+ // and would cause those shared edges (between the hole and the filling polygon) to be preserved if necessary.
249
+ //
250
+ // I do make a final pass of testing for self-intersection that looks at both polygon and hole edges.
251
+ //
252
+ let arcs = PP.normalizeArcs(oNew.arcs);
253
+ let npoly = PP.countArcPolygons(arcs);
254
+ let bDecided = false;
255
+ let iPoly = 0;
256
+ for (; !bDecided && iPoly < npoly; iPoly++)
257
+ {
258
+ let pp = PP.polyPackTopoArcs(testtopo, arcs, iPoly);
259
+ P.polyRewindRings(pp);
260
+ let a = P.polyArea(pp);
261
+ if (a <= 0)
262
+ {
263
+ keepTiny.set(f, f);
264
+ keepArcs(topo, oOld.arcs, 0); // keeps all points to avoid reprocessing these tiny shapes
265
+ nBad++, nTiny++, bDecided = true;
266
+ }
267
+ else if (a < minArea)
268
+ {
269
+ let d = PL.polyLabel(pp);
270
+ if (d.d < nLabelTolerance)
271
+ {
272
+ keepTiny.set(f, f);
273
+ keepArcs(topo, oOld.arcs, 0); // keeps all points to avoid reprocessing these tiny shapes
274
+ nBad++, nTiny++, bDecided = true;
275
+ }
276
+ }
277
+ }
278
+ if (! bDecided)
279
+ {
280
+ let pp = PP.polyPackTopoArcs(testtopo, arcs);
281
+ if (selfIntersectFast(pp))
282
+ {
283
+ keepArcs(topo, oOld.arcs, keepweight);
284
+ nBad++;
285
+ }
286
+ }
287
+ }
288
+ });
289
+ console.log(`topoSimplifyCollection: pass ${nTries}: ${nBad} (${nTiny} tiny) of ${col.features.length} features are degenerate`);
290
+
291
+ // If not making progress, keep more points
292
+ if (nBad >= nBadLast)
293
+ {
294
+ keepweight /= 10;
295
+ console.log(`topoSimplifyCollection: pass ${nTries}: reducing weight limit to ${keepweight}`);
296
+ }
297
+ nBadLast = nBad;
298
+
299
+ if (nBad && nTries > MAX_TRIES)
300
+ console.error(`topoSimplifyCollection: failed to finalize simplify down to zero degenerate features`);
301
+ // If no bad block shapes, or finished trying, just return result
302
+ if (nBad == 0 || nTries > MAX_TRIES)
303
+ {
304
+ col = topoToCollection(testtopo);
305
+ break;
306
+ }
307
+
308
+ nTries++;
309
+ }
310
+
311
+ console.log(`topoSimplifyCollection: total elapsed time: ${bigTimeString(elapsedTotal.ms())}`);
312
+
313
+ return col;
314
+ }
315
+
316
+ export function topoMerge(topo: Topo, geoids: string[]): any
317
+ {
318
+ if (geoids == null || geoids.length == 0) return null;
319
+ let objects: any[] = [];
320
+ geoids.forEach((geoid) => objects.push(topo.objects[geoid]));
321
+ let f: any = correctGeometry({ type: 'Feature', properties: {}, geometry: TopoClient.merge(topo, objects) });
322
+ P.featureRewind(f);
323
+
324
+ /* Comment out for now - may be due to merge output needing to be rewound
325
+ // If I get a bad output from topoMerge, just do more expensive poly union. This can happen if input polygons
326
+ // are a little funky (in particular, if they double back along the same edge.
327
+ if (selfIntersectFast(f))
328
+ {
329
+ //console.log('topoMerge: patching selfIntersect');
330
+ let polys: any[] = [];
331
+ geoids.forEach((geoid) => polys.push(topoToFeature(topo, geoid).geometry.coordinates));
332
+ let result = Q.unionPolys(polys);
333
+ let depth = Util.depthof(result);
334
+ if (depth === 5 && result.length === 1)
335
+ {
336
+ depth = 4;
337
+ result = result[0];
338
+ }
339
+ f = { type: 'feature', properties: {}, geometry: { type: depth == 4 ? 'Polygon' : 'MultiPolygon', coordinates: result } };
340
+ }
341
+ */
342
+ return f;
343
+ }
344
+
345
+ export function topoMergeFeatures(topo: Topo, features: any[]): any
346
+ {
347
+ if (features == null || features.length == 0) return null;
348
+ let prop = getGEOID(features);
349
+ return topoMerge(topo, features.map(f => f.properties[prop]));
350
+ }
351
+
352
+ let UniqueState = FSM.FSM_CUSTOM1;
353
+ let FSM_COMPUTING = UniqueState++;
354
+
355
+ class FsmIncrementalUnion extends FSM.Fsm
356
+ {
357
+ options: P.TickOptions;
358
+ key: any;
359
+ map: any; // { [geoid: string]: Feature }
360
+ result: any;
361
+ work: Q.WorkDone;
362
+
363
+ constructor(env: FSM.FsmEnvironment, options: P.TickOptions, key: any, map?: any)
364
+ {
365
+ super(env);
366
+ this.options = options;
367
+ this.key = key;
368
+ this.result = null;
369
+ this.map = null;
370
+ if (map) this.recompute(map);
371
+ }
372
+
373
+ matches(key: any): boolean
374
+ {
375
+ return Util.shallowEqual(this.key, key);
376
+ }
377
+
378
+ recompute(map: any): void
379
+ {
380
+ if (this.map != null && map != null && Util.shallowEqual(map, this.map))
381
+ {
382
+ this.work = { nUnion: 0, nDifference: 0, ms: 0 };
383
+ }
384
+ else if (map == null || Util.isEmpty(map))
385
+ {
386
+ this.work = { nUnion: 0, nDifference: 0, ms: 0 };
387
+ this.result = null;
388
+ this.map = map;
389
+ }
390
+ else
391
+ {
392
+ let values = Object.values(map);
393
+ this.work = { nUnion: values.length, nDifference: 0, ms: 0 };
394
+ let elapsed = new Util.Elapsed();
395
+ this.result = topoMergeFeatures(this.key.topo, values);
396
+ this.work.ms = elapsed.ms();
397
+ this.map = map;
398
+ }
399
+ this.setState(FSM.FSM_DONE);
400
+ }
401
+
402
+ cancel(): void
403
+ {
404
+ this.result = null;
405
+ this.map = null;
406
+ this.setState(FSM.FSM_DONE);
407
+ }
408
+
409
+ tick(): void
410
+ {
411
+ if (this.ready)
412
+ {
413
+ switch (this.state)
414
+ {
415
+ case FSM.FSM_STARTING:
416
+ // never initialized to do work (see recompute())
417
+ this.setState(FSM.FSM_DONE);
418
+ break;
419
+ }
420
+ }
421
+ }
422
+ }
423
+
424
+ export interface TopoUnionResult
425
+ {
426
+ key: any;
427
+ poly: any;
428
+ work: Q.WorkDone;
429
+ }
430
+
431
+ export class FsmTopoUnion extends FSM.Fsm
432
+ {
433
+ options: P.TickOptions;
434
+ unions: FsmIncrementalUnion[];
435
+ work: Q.WorkDone;
436
+
437
+ constructor(env: FSM.FsmEnvironment, options?: P.TickOptions)
438
+ {
439
+ super(env);
440
+ this.options = Util.shallowAssignImmutable(P.DefaultTickOptions, options);
441
+ this.unions = [];
442
+ this.work = { nUnion: 0, nDifference: 0, ms: 0 };
443
+ }
444
+
445
+ get result(): TopoUnionResult[]
446
+ {
447
+ if (this.unions.length > 0 && this.state === FSM.FSM_DONE)
448
+ return this.unions.map((i: FsmIncrementalUnion) => ({ key: i.key, poly: i.result, work: i.work }) );
449
+ else
450
+ return null;
451
+ }
452
+
453
+ cancel(): void
454
+ {
455
+ this.unions.forEach((i: FsmIncrementalUnion) => {
456
+ i.cancel();
457
+ });
458
+ this.unions = [];
459
+ this.setState(FSM.FSM_DONE);
460
+ }
461
+
462
+ cancelOne(key: any): void
463
+ {
464
+ for (let i = 0; i < this.unions.length; i++)
465
+ {
466
+ let u = this.unions[i];
467
+ if (u.matches(key))
468
+ {
469
+ u.cancel();
470
+ return;
471
+ }
472
+ }
473
+ }
474
+
475
+ recompute(key: any, map: any): void
476
+ {
477
+ let fsm: FsmIncrementalUnion = this.unions.find((i: FsmIncrementalUnion) => i.matches(key));
478
+ if (fsm == null)
479
+ {
480
+ fsm = new FsmIncrementalUnion(this.env, this.options, key, map);
481
+ this.unions.push(fsm);
482
+ }
483
+ else
484
+ fsm.recompute(map);
485
+ this.work = { nUnion: 0, nDifference: 0, ms: 0 };
486
+ this.unions.forEach((u) => { this.work.nUnion += u.work.nUnion; this.work.nDifference += u.work.nDifference });
487
+ this.waitOn(fsm);
488
+ this.setState(FSM_COMPUTING);
489
+ }
490
+
491
+ tick(): void
492
+ {
493
+ if (this.ready)
494
+ {
495
+ switch (this.state)
496
+ {
497
+ case FSM.FSM_STARTING:
498
+ case FSM_COMPUTING:
499
+ if (this.unions) this.unions.forEach((u) => { this.work.ms += u.work.ms });
500
+ this.setState(FSM.FSM_DONE);
501
+ break;
502
+ }
503
+ }
504
+ }
505
+ }
506
+
507
+ export function topoPacked(topo: any): boolean
508
+ {
509
+ return topo.packed !== undefined;
510
+ }
511
+
512
+ export function topoPack(topo: any): any
513
+ {
514
+ let tc = TopoClient;
515
+ TopoClient.packArcs(topo);
516
+ TopoClient.packArcIndices(topo);
517
+ return topo;
518
+ }
519
+
520
+ export function topoUnpack(topo: any): any
521
+ {
522
+ TopoClient.unpackArcs(topo);
523
+ TopoClient.unpackArcIndices(topo);
524
+ return topo;
525
+ }