@dra2020/baseclient 1.0.48 → 1.0.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/geo/geo.d.ts CHANGED
@@ -10,7 +10,14 @@ export declare type GeoCentroidMap = {
10
10
  y: number;
11
11
  };
12
12
  };
13
- export declare function geoEnsureID(col: GeoFeatureCollection): void;
13
+ export interface NormalizeOptions {
14
+ joinPolygons?: boolean;
15
+ checkRewind?: boolean;
16
+ ensureID?: boolean;
17
+ }
18
+ export declare function geoEnsureID(col: GeoFeatureCollection, options?: NormalizeOptions): void;
19
+ export declare function geoNormalizeFeature(f: any, options?: NormalizeOptions): GeoFeature;
20
+ export declare function geoNormalizeCollection(col: GeoFeatureCollection, options?: NormalizeOptions): GeoFeatureCollection;
14
21
  export declare function geoCollectionToMap(col: GeoFeatureCollection): GeoFeatureMap;
15
22
  export declare function geoMapToCollection(map: GeoFeatureMap): GeoFeatureCollection;
16
23
  export declare function geoMapToCollectionNonNull(map: GeoFeatureMap): GeoFeatureCollection;
@@ -8,6 +8,7 @@ export declare function topoToFeature(topo: Topo, geoid: string): any;
8
8
  export declare function topoToCollection(topo: Topo): any;
9
9
  export interface SimplifyOptions {
10
10
  minArea?: number;
11
+ log?: boolean;
11
12
  }
12
13
  export declare function topoSimplifyCollection(col: any, options?: SimplifyOptions): any;
13
14
  export declare function topoMerge(topo: Topo, geoids: string[]): any;
package/lib/geo/geo.ts CHANGED
@@ -8,16 +8,40 @@ export type GeoFeatureArray = GeoFeature[];
8
8
  export type GeoFeatureCollection = geojson.FeatureCollection;
9
9
  export type GeoCentroidMap = { [geoid: string]: { x: number, y: number } };
10
10
 
11
- export function geoEnsureID(col: GeoFeatureCollection): void
11
+ export interface NormalizeOptions
12
12
  {
13
+ joinPolygons?: boolean,
14
+ checkRewind?: boolean,
15
+ ensureID?: boolean,
16
+ }
17
+ const NormalizeAll: NormalizeOptions = { joinPolygons: true, checkRewind: true, ensureID: true };
18
+
19
+ // set the canonical 'id' property from the best property value.
20
+ // if joinPolygons is true, we do not enforce uniqueness.
21
+ //
22
+
23
+ export function geoEnsureID(col: GeoFeatureCollection, options?: NormalizeOptions): void
24
+ {
25
+ options = Util.shallowAssignImmutable({}, options);
26
+
13
27
  let prop: string;
14
- const props = ['id', 'GEOID', 'GEOID10', 'GEOID20', 'GEOID30' ];
28
+ const props = [ 'id', 'GEOID', 'GEOID10', 'GEOID20', 'GEOID30', 'DISTRICT', 'DISTRICTNO', 'DISTRICTNAME' ];
15
29
 
16
30
  if (col && col.features && col.features.length > 0)
17
31
  {
18
32
  let f = col.features[0];
19
- if (f.properties.id !== undefined) return;
20
- props.forEach(p => { if (prop === undefined && f.properties[p] !== undefined) prop = p; });
33
+ if (f.properties.id !== undefined) return; // short-cut - assume if 'id' is set, we're all good.
34
+ props.forEach(p => {
35
+ if (prop === undefined)
36
+ if (f.properties[p] !== undefined)
37
+ prop = p;
38
+ else
39
+ {
40
+ p = p.toLowerCase();
41
+ if (f.properties[p] !== undefined)
42
+ prop = p
43
+ }
44
+ });
21
45
  if (prop)
22
46
  col.features.forEach(f => { f.properties.id = f.properties[prop] });
23
47
  else
@@ -28,6 +52,94 @@ export function geoEnsureID(col: GeoFeatureCollection): void
28
52
  }
29
53
  }
30
54
 
55
+ export function geoNormalizeFeature(f: any, options?: NormalizeOptions): GeoFeature
56
+ {
57
+ options = Util.shallowAssignImmutable({}, options);
58
+
59
+ if (f && f.geometry && f.geometry.type === 'MultiPolygon' && f.geometry.coordinates)
60
+ {
61
+ let multiPoly = f.geometry.coordinates;
62
+
63
+ // Convert degenerate MultiPolygon to Polygon
64
+ if (multiPoly.length == 1)
65
+ {
66
+ f.geometry.type = 'Polygon';
67
+ f.geometry.coordinates = multiPoly[0];
68
+ }
69
+ }
70
+ else if (f && f.geometry && f.geometry.type === 'Point' && f.geometry.coordinates)
71
+ {
72
+ while (Array.isArray(f.geometry.coordinates[0]))
73
+ f.geometry.coordinates = f.geometry.coordinates[0];
74
+ }
75
+ else if (options.checkRewind)
76
+ // Various tools do not guarantee valid GeoJSON winding rules. Verify it since internal processing
77
+ // assumes it is correct.
78
+ Poly.featureRewind(f);
79
+
80
+ return f;
81
+ }
82
+
83
+ function onlyPolygons(col: GeoFeatureCollection): boolean
84
+ {
85
+ if (col && Array.isArray(col.features))
86
+ for (let i = 0; i < col.features.length; i++)
87
+ {
88
+ let f = col.features[i];
89
+ if (f.geometry && f.geometry.type === 'MultiPolygon')
90
+ return false;
91
+ }
92
+
93
+ return true;
94
+ }
95
+
96
+ function mergePolygon(f1: any, f2: any): any
97
+ {
98
+ if (!f1) return f2;
99
+ if (!f2) return f1;
100
+ if (f1.geometry.type !== 'Polygon' && f1.geometry.type !== 'MultiPolygon')
101
+ return f1;
102
+ if (f2.geometry.type !== 'Polygon' && f2.geometry.type !== 'MultiPolygon')
103
+ return f2;
104
+ if (f1.geometry.type === 'Polygon')
105
+ {
106
+ f1.geometry.type = 'MultiPolygon';
107
+ if (f2.geometry.type === 'Polygon')
108
+ f1.geometry.coordinates = [ f1.geometry.coordinates, f2.geometry.coordinates ];
109
+ else
110
+ f1.geometry.coordinates = [ f1.geometry.coordinates, ...f2.geometry.coordinates ];
111
+ }
112
+ else
113
+ {
114
+ if (f2.geometry.type === 'Polygon')
115
+ f1.geometry.coordinates.push(f2.geometry.coordinates);
116
+ else
117
+ f1.geometry.coordinates = [...f1.geometry.coordinates, ...f2.geometry.coordinates];
118
+ }
119
+ return f1;
120
+ }
121
+
122
+ export function geoNormalizeCollection(col: GeoFeatureCollection, options?: NormalizeOptions): GeoFeatureCollection
123
+ {
124
+ options = Util.shallowAssignImmutable(NormalizeAll, options);
125
+
126
+ // Normalize individual features
127
+ if (col && Array.isArray(col.features)) col.features.forEach((f: GeoFeature) => geoNormalizeFeature(f, options));
128
+
129
+ // Ensure ID
130
+ if (options.ensureID)
131
+ geoEnsureID(col, options);
132
+
133
+ // Merge polygons into multi-polygons based on id?
134
+ if (options.ensureID && options.joinPolygons && onlyPolygons(col))
135
+ {
136
+ let map: GeoFeatureMap = {};
137
+ col.features.forEach(f => { let id = f.properties.id; map[id] = mergePolygon(map[id], f) });
138
+ col.features = Object.values(map);
139
+ }
140
+ return col;
141
+ }
142
+
31
143
  export function geoCollectionToMap(col: GeoFeatureCollection): GeoFeatureMap
32
144
  {
33
145
  if (col == null) return null;
@@ -21,16 +21,66 @@ export function polyDistance(poly: any, x: number, y: number): number
21
21
  let pp = P.polyNormalize(poly);
22
22
  if (pp == null) return 0;
23
23
 
24
- let forEachPointPair = (iter: any) => {
25
- PP.polyPackEachRing(pp, (b: Float64Array, iPoly, iRing: number, iOffset: number, nPoints: number) => {
26
- let iFirst = iOffset;
27
- let iLast = iOffset + nPoints * 2;
28
- let iSecond = iLast - 2;
29
- for (; iFirst < iLast; iSecond = iFirst, iFirst += 2)
30
- iter(iPoly, iRing, b[iFirst], b[iFirst+1], b[iSecond], b[iSecond+1]);
31
- });
32
- };
33
- return pointToPolygonDist(x, y, forEachPointPair);
24
+ // First find if it is contained in one of the outer polygons, or outside all outer polygons
25
+ let iContaining = -1;
26
+ let maxOutside = - Infinity;
27
+ let minInside = Infinity;
28
+ let noholes = true;
29
+ PP.polyPackEachRing(pp, (b: Float64Array, iPoly: number, iRing: number, iOffset: number, nPoints: number) => {
30
+ // If we have determined we are inside this polygon, keep track of whether it has holes
31
+ if (iContaining == iPoly && iRing > 0)
32
+ noholes = false;
33
+ // Don't process rings
34
+ if (iRing > 0) return;
35
+ // OK, get distance
36
+ let forEachPointPair = (iter: any) => {
37
+ PP.polyPackEachRing(pp, (b: Float64Array, iInteriorPoly: number, iRing: number, iOffset: number, nPoints: number) => {
38
+ if (iRing || iInteriorPoly != iPoly) return;
39
+ let iFirst = iOffset;
40
+ let iLast = iOffset + nPoints * 2;
41
+ let iSecond = iLast - 2;
42
+ for (; iFirst < iLast; iSecond = iFirst, iFirst += 2)
43
+ iter(b[iFirst], b[iFirst+1], b[iSecond], b[iSecond+1]);
44
+ });
45
+ };
46
+ let dist = pointToPolygonDist(x, y, forEachPointPair);
47
+ // If inside, is it closest inside (deal with multipolygons that self-contain - think filled donut)
48
+ if (dist > 0 && dist < minInside)
49
+ {
50
+ iContaining = iPoly;
51
+ minInside = dist;
52
+ noholes = true;
53
+ }
54
+ else if (dist < 0)
55
+ maxOutside = Math.max(maxOutside, dist);
56
+ });
57
+ if (iContaining < 0)
58
+ return maxOutside;
59
+ if (noholes)
60
+ return minInside;
61
+
62
+ // OK, now need to worry about holes in the polygon it is contained in
63
+ PP.polyPackEachRing(pp, (b: Float64Array, iPoly: number, iRing: number, iOffset: number, nPoints: number) => {
64
+ // Only want to look at the holes for the containing polygon
65
+ if (iPoly != iContaining || iRing == 0) return;
66
+ // Compute distance to those holes
67
+ let forEachPointPair = (iter: any) => {
68
+ PP.polyPackEachRing(pp, (b: Float64Array, iInteriorPoly: number, iRing: number, iOffset: number, nPoints: number) => {
69
+ if (iInteriorPoly != iContaining || iRing == 0) return;
70
+ let iFirst = iOffset;
71
+ let iLast = iOffset + nPoints * 2;
72
+ let iSecond = iLast - 2;
73
+ for (; iFirst < iLast; iSecond = iFirst, iFirst += 2)
74
+ iter(b[iFirst], b[iFirst+1], b[iSecond], b[iSecond+1]);
75
+ });
76
+ };
77
+ // Negate distance since dealing with holes
78
+ let dist = - pointToPolygonDist(x, y, forEachPointPair);
79
+ // We take the min to either get a negative value (inside hole) or a smaller positive value
80
+ // (outside hole but close to hole boundary).
81
+ minInside = Math.min(minInside, dist);
82
+ });
83
+ return minInside;
34
84
  }
35
85
 
36
86
  //
@@ -69,7 +119,7 @@ export function polyLabel(poly: any, precision?: number, debug?: boolean): PolyL
69
119
  let iLast = iOffset + nPoints * 2;
70
120
  let iSecond = iLast - 2;
71
121
  for (; iFirst < iLast; iSecond = iFirst, iFirst += 2)
72
- iter(iPoly, iRing, b[iFirst], b[iFirst+1], b[iSecond], b[iSecond+1]);
122
+ iter(b[iFirst], b[iFirst+1], b[iSecond], b[iSecond+1]);
73
123
  });
74
124
  };
75
125
 
@@ -164,38 +214,15 @@ class Cell
164
214
  // signed distance from point to polygon outline (negative if point is outside)
165
215
  function pointToPolygonDist(x: number, y: number, forEachPointPair: any): number
166
216
  {
167
- let iPolyLast = -1;
168
- let iRingLast = -1;
169
217
  let inside = false;
170
- let thisInside = false;
171
- let useInside = false;
172
- let isHole = false;
173
- let minDistSq: number = Infinity;
218
+ let minDistSq = Infinity;
174
219
 
175
- forEachPointPair((iPoly: number, iRing: number, ax: number, ay: number, bx: number, by: number) => {
176
- if (iPoly != iPolyLast || iRing != iRingLast)
177
- {
178
- if (useInside)
179
- inside = isHole ? ! thisInside : thisInside;
180
- iPolyLast = iPoly;
181
- iRingLast = iRing;
182
- thisInside = false;
183
- useInside = false;
184
- isHole = false;
185
- }
220
+ forEachPointPair((ax: number, ay: number, bx: number, by: number) => {
186
221
  if ((ay > y !== by > y) && (x < (bx - ax) * (y - ay) / (by - ay) + ax))
187
- thisInside = !thisInside;
222
+ inside = !inside;
188
223
 
189
- let thisDistSq = getSegDistSq(x, y, ax, ay, bx, by);
190
- if (thisDistSq < minDistSq)
191
- {
192
- minDistSq = thisDistSq;
193
- useInside = true;
194
- isHole = iRing != 0;
195
- }
224
+ minDistSq = Math.min(minDistSq, getSegDistSq(x, y, ax, ay, bx, by));
196
225
  });
197
- if (useInside)
198
- inside = isHole ? ! thisInside : thisInside;
199
226
 
200
227
  return (inside ? 1 : -1) * Math.sqrt(minDistSq);
201
228
  }
@@ -209,7 +236,7 @@ function getCentroidCell(forEachPointPair: any): Cell
209
236
  let fx: number;
210
237
  let fy: number;
211
238
 
212
- forEachPointPair((iPoly: number, iRing: number, ax: number, ay: number, bx: number, by: number) => {
239
+ forEachPointPair((ax: number, ay: number, bx: number, by: number) => {
213
240
  if (fx === undefined) fx = ax, fy = ay;
214
241
  let f: number = ax * by - bx * ay;
215
242
  x += (ax + bx) * f;
package/lib/poly/topo.ts CHANGED
@@ -215,13 +215,14 @@ function intpt(f: any): { x: number, y: number }
215
215
  export interface SimplifyOptions
216
216
  {
217
217
  minArea?: number,
218
+ log?: boolean,
218
219
  }
219
220
 
220
221
  const DefaultSimplifyOptions: SimplifyOptions = { minArea: 500 };
221
222
 
222
- function log(s: string): void
223
+ function log(emitlog: boolean, s: string): void
223
224
  {
224
- console.log(s);
225
+ if (emitlog) console.log(s);
225
226
  }
226
227
 
227
228
  //
@@ -248,10 +249,10 @@ export function topoSimplifyCollection(col: any, options?: SimplifyOptions): any
248
249
  let elapsedTotal = new Util.Elapsed();
249
250
  let elapsed = new Util.Elapsed();
250
251
  let topo = topoFromCollection(col);
251
- log(`topoSimplifyCollection: fromCollection: ${Math.round(elapsed.ms())}ms`);
252
+ log(options.log, `topoSimplifyCollection: fromCollection: ${Math.round(elapsed.ms())}ms`);
252
253
  elapsed.start();
253
254
  topo = TopoSimplify.presimplify(topo, TopoSimplify['sphericalTriangleArea']);
254
- log(`topoSimplifyCollection: presimplify: ${Math.round(elapsed.ms())}ms`);
255
+ log(options.log, `topoSimplifyCollection: presimplify: ${Math.round(elapsed.ms())}ms`);
255
256
  elapsed.start();
256
257
 
257
258
  // Keep iterating on removing simplification from degenerate shapes
@@ -328,19 +329,19 @@ export function topoSimplifyCollection(col: any, options?: SimplifyOptions): any
328
329
  keepTiny.set(f, f);
329
330
  keepArcs(topo, oOld.arcs, 0); // keeps all points to avoid reprocessing these tiny shapes
330
331
  nBad++, nTiny++;
331
- log(`topoSimplifyCollection: ${f.properties.id}: increasing feature fidelity because intpt dist is ${d}`);
332
+ log(options.log, `topoSimplifyCollection: ${f.properties.id}: increasing feature fidelity because intpt dist is ${d}`);
332
333
  }
333
334
  }
334
335
  }
335
336
  }
336
337
  });
337
- log(`topoSimplifyCollection: pass ${nTries}: ${nBad} (${nTiny} tiny) of ${col.features.length} features are degenerate`);
338
+ log(options.log, `topoSimplifyCollection: pass ${nTries}: ${nBad} (${nTiny} tiny) of ${col.features.length} features are degenerate`);
338
339
 
339
340
  // If not making progress, keep more points
340
341
  if (nBad >= nBadLast)
341
342
  {
342
343
  keepweight /= 10;
343
- log(`topoSimplifyCollection: pass ${nTries}: reducing weight limit to ${keepweight}`);
344
+ log(options.log, `topoSimplifyCollection: pass ${nTries}: reducing weight limit to ${keepweight}`);
344
345
  }
345
346
  nBadLast = nBad;
346
347
 
@@ -356,7 +357,7 @@ export function topoSimplifyCollection(col: any, options?: SimplifyOptions): any
356
357
  nTries++;
357
358
  }
358
359
 
359
- log(`topoSimplifyCollection: total elapsed time: ${bigTimeString(elapsedTotal.ms())}`);
360
+ log(options.log, `topoSimplifyCollection: total elapsed time: ${bigTimeString(elapsedTotal.ms())}`);
360
361
 
361
362
  return col;
362
363
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dra2020/baseclient",
3
- "version": "1.0.48",
3
+ "version": "1.0.51",
4
4
  "description": "Utility functions for Javascript projects.",
5
5
  "main": "dist/baseclient.js",
6
6
  "types": "./dist/all/all.d.ts",