@jarenjs/core 0.34.2 → 0.43.3
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/ARCHITECTURE.md +43 -2
- package/README.md +7 -4
- package/dist/types/function.d.ts +25 -0
- package/dist/types/geo/bbox.d.ts +28 -0
- package/dist/types/geo/distance.d.ts +32 -0
- package/dist/types/geo/geojson.d.ts +4 -1
- package/dist/types/geo/simplify.d.ts +28 -0
- package/dist/types/geo/wkt.d.ts +65 -0
- package/dist/types/object.d.ts +18 -0
- package/dist/types/scan.d.ts +1 -0
- package/dist/types/string.d.ts +22 -0
- package/dist/types/text/sse.d.ts +74 -0
- package/docs/GEO.md +77 -4
- package/package.json +1 -1
- package/src/function.js +34 -0
- package/src/geo/bbox.js +41 -0
- package/src/geo/distance.js +48 -0
- package/src/geo/geojson.js +10 -2
- package/src/geo/index.js +1 -1
- package/src/geo/simplify.js +69 -0
- package/src/geo/wkt.js +379 -63
- package/src/object.js +68 -0
- package/src/scan.js +1 -0
- package/src/string.js +36 -0
- package/src/text/sse.js +197 -0
package/src/geo/bbox.js
CHANGED
|
@@ -17,6 +17,14 @@
|
|
|
17
17
|
* The bounding box of a list of positions, as `[west, south, east, north]`.
|
|
18
18
|
* Returns null for an empty list — there is no box that bounds nothing,
|
|
19
19
|
* and an all-Infinity placeholder would silently intersect everything.
|
|
20
|
+
*
|
|
21
|
+
* Null too when any position is non-finite. Narrowing would drop it —
|
|
22
|
+
* every comparison against `NaN` is false — and answer with a finite,
|
|
23
|
+
* plausible box that does not contain the input it was asked to bound.
|
|
24
|
+
* A pre-filter built on such a box loses matching rows silently, which
|
|
25
|
+
* is the one failure a two-stage spatial plan cannot survive, so a
|
|
26
|
+
* position that cannot be bounded refuses the box rather than leaving
|
|
27
|
+
* it too small.
|
|
20
28
|
* @param {Array<number[]>} positions
|
|
21
29
|
* @returns {number[] | null}
|
|
22
30
|
*/
|
|
@@ -31,6 +39,8 @@ export function bboxOfPositions(positions) {
|
|
|
31
39
|
for (let i = 0; i < n; i++) {
|
|
32
40
|
const x = positions[i][0];
|
|
33
41
|
const y = positions[i][1];
|
|
42
|
+
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
43
|
+
return null;
|
|
34
44
|
if (x < west) west = x;
|
|
35
45
|
if (x > east) east = x;
|
|
36
46
|
if (y < south) south = y;
|
|
@@ -77,4 +87,35 @@ export function bboxUnion(a, b) {
|
|
|
77
87
|
];
|
|
78
88
|
}
|
|
79
89
|
|
|
90
|
+
/**
|
|
91
|
+
* A box as the `Polygon` that covers it — the rectangle written
|
|
92
|
+
* counter-clockwise from its south-west corner, which is RFC 7946's
|
|
93
|
+
* exterior winding, and closed.
|
|
94
|
+
*
|
|
95
|
+
* A box is four numbers and a polygon is a value the rest of the
|
|
96
|
+
* language can measure, contain and intersect, so this is the crossing
|
|
97
|
+
* between the two: a geohash cell, an index probe box or a `bbox`
|
|
98
|
+
* member becomes something `$within` and `$area` can take.
|
|
99
|
+
*
|
|
100
|
+
* @param {number[] | null} box - [west, south, east, north]
|
|
101
|
+
* @returns {{ type: string, coordinates: number[][][] } | null} null for no box
|
|
102
|
+
* @example
|
|
103
|
+
* bboxPolygon([4, 52, 5, 53]);
|
|
104
|
+
* // { type: 'Polygon', coordinates: [[[4,52],[5,52],[5,53],[4,53],[4,52]]] }
|
|
105
|
+
*/
|
|
106
|
+
export function bboxPolygon(box) {
|
|
107
|
+
if (box === null || !Array.isArray(box) || box.length < 4)
|
|
108
|
+
return null;
|
|
109
|
+
const west = box[0];
|
|
110
|
+
const south = box[1];
|
|
111
|
+
const east = box[2];
|
|
112
|
+
const north = box[3];
|
|
113
|
+
return {
|
|
114
|
+
type: 'Polygon',
|
|
115
|
+
coordinates: [[
|
|
116
|
+
[west, south], [east, south], [east, north], [west, north], [west, south],
|
|
117
|
+
]],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
80
121
|
//#endregion
|
package/src/geo/distance.js
CHANGED
|
@@ -135,6 +135,54 @@ export function destinationPoint(lon, lat, bearing, distance, radius = EARTH_RAD
|
|
|
135
135
|
return [((l2 * RAD + 540) % 360) - 180, p2 * RAD];
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
/**
|
|
139
|
+
* The bounding box of a geodesic circle: every position within
|
|
140
|
+
* `distance` metres of `(lon, lat)` on the same sphere lies inside it.
|
|
141
|
+
* This is what turns "nearer than r" into a box a range index can seek.
|
|
142
|
+
*
|
|
143
|
+
* The latitude bounds are the circle's due-north and due-south points,
|
|
144
|
+
* but the longitude bounds are **not** its due-east and due-west ones:
|
|
145
|
+
* the circle reaches its extreme meridians where it runs tangent to
|
|
146
|
+
* them, further out than a 90° bearing travels — 0.4 % further at 80°N
|
|
147
|
+
* over 100 km. A box built from four bearings is therefore too small,
|
|
148
|
+
* and too small is the one error a conservative pre-filter cannot
|
|
149
|
+
* survive. The extreme half-width is `asin(sin δ / cos φ)` for angular
|
|
150
|
+
* radius δ at latitude φ.
|
|
151
|
+
*
|
|
152
|
+
* `null` when the circle reaches a pole (`|lat| + δ >= 90°`), where the
|
|
153
|
+
* longitude bound does not exist because the circle spans every
|
|
154
|
+
* meridian, and for any non-finite or negative input.
|
|
155
|
+
*
|
|
156
|
+
* West and east are NOT wrapped into `[-180, 180]`: a circle spanning
|
|
157
|
+
* the antimeridian answers a west below -180 or an east above 180, so
|
|
158
|
+
* a caller can tell that case apart — RFC 7946 asks producers to cut
|
|
159
|
+
* there, and a wrapped box would silently claim the short way round.
|
|
160
|
+
*
|
|
161
|
+
* @param {number} lon - centre longitude, degrees
|
|
162
|
+
* @param {number} lat - centre latitude, degrees
|
|
163
|
+
* @param {number} distance - radius in metres (or in `radius`'s unit)
|
|
164
|
+
* @param {number} [radius] - sphere radius
|
|
165
|
+
* @returns {[number, number, number, number] | null} `[west, south, east, north]`
|
|
166
|
+
* @example
|
|
167
|
+
* circleBounds(5, 52, 1000); // [4.98539…, 51.99101…, 5.01461…, 52.00899…]
|
|
168
|
+
*/
|
|
169
|
+
export function circleBounds(lon, lat, distance, radius = EARTH_RADIUS) {
|
|
170
|
+
if (!Number.isFinite(lon) || !Number.isFinite(lat)
|
|
171
|
+
|| !Number.isFinite(distance) || distance < 0) return null;
|
|
172
|
+
const d = distance / radius;
|
|
173
|
+
const dDeg = d * RAD;
|
|
174
|
+
// at or past a pole the circle covers every meridian, so no box bounds it
|
|
175
|
+
if (Math.abs(lat) + dDeg >= 90) return null;
|
|
176
|
+
// the tangent half-width is never narrower than the angular radius
|
|
177
|
+
// itself (cos φ <= 1), and at the equator the asin(sin …) round trip
|
|
178
|
+
// loses the last bit of exactly that value — taking the wider of the
|
|
179
|
+
// two restores an inequality the real numbers always satisfy rather
|
|
180
|
+
// than padding by a constant nobody could justify
|
|
181
|
+
const tangent = Math.asin(Math.sin(d) / Math.cos(lat * DEG)) * RAD;
|
|
182
|
+
const half = tangent > dDeg ? tangent : dDeg;
|
|
183
|
+
return [lon - half, lat - dDeg, lon + half, lat + dDeg];
|
|
184
|
+
}
|
|
185
|
+
|
|
138
186
|
/**
|
|
139
187
|
* Total great-circle length of a line of positions, in metres. An empty
|
|
140
188
|
* or single-position line has length 0.
|
package/src/geo/geojson.js
CHANGED
|
@@ -105,7 +105,10 @@ export function eachPosition(value, visit) {
|
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
107
|
* The bounding box of any GeoJSON value, as `[west, south, east, north]`.
|
|
108
|
-
* Null when the value contains no positions
|
|
108
|
+
* Null when the value contains no positions, and null when any position
|
|
109
|
+
* it walks is non-finite — the rule `bboxOfPositions` states, for the
|
|
110
|
+
* same reason: a box that does not bound its input is worse than no
|
|
111
|
+
* box.
|
|
109
112
|
*
|
|
110
113
|
* A `bbox` member already present on the value is ignored: it is an
|
|
111
114
|
* optimization the producer may have got wrong, and recomputing is the
|
|
@@ -123,16 +126,21 @@ export function bboxOf(value) {
|
|
|
123
126
|
let east = -Infinity;
|
|
124
127
|
let north = -Infinity;
|
|
125
128
|
let seen = false;
|
|
129
|
+
let bounded = true;
|
|
126
130
|
eachPosition(value, (p) => {
|
|
127
131
|
const x = p[0];
|
|
128
132
|
const y = p[1];
|
|
129
133
|
seen = true;
|
|
134
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
135
|
+
bounded = false;
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
130
138
|
if (x < west) west = x;
|
|
131
139
|
if (x > east) east = x;
|
|
132
140
|
if (y < south) south = y;
|
|
133
141
|
if (y > north) north = y;
|
|
134
142
|
});
|
|
135
|
-
return seen ? [west, south, east, north] : null;
|
|
143
|
+
return seen && bounded ? [west, south, east, north] : null;
|
|
136
144
|
}
|
|
137
145
|
|
|
138
146
|
/**
|
package/src/geo/index.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// geohash.js the string encoding that needs no new vocabulary
|
|
22
22
|
// geojson.js the one layer that knows the `type` discriminator
|
|
23
23
|
// valid.js the one-call structural judgment (rings must close)
|
|
24
|
-
// wkt.js
|
|
24
|
+
// wkt.js the round trip with the databases' text encoding
|
|
25
25
|
// index-tree.js a static packed-Hilbert box index for spatial joins
|
|
26
26
|
// mercator.js the projection out, for anything that draws a map
|
|
27
27
|
// simplify.js dropping the vertices that land on the same pixel
|
package/src/geo/simplify.js
CHANGED
|
@@ -127,4 +127,73 @@ export function simplifyRing(ring, tolerance) {
|
|
|
127
127
|
return simplified.length >= 4 ? simplified : ring.slice();
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/** Every line/ring of a coordinate nest `depth` levels above the leaf. */
|
|
131
|
+
function simplifyNest(value, depth, tolerance, simplify) {
|
|
132
|
+
if (!Array.isArray(value))
|
|
133
|
+
return value;
|
|
134
|
+
if (depth === 0)
|
|
135
|
+
return simplify(value, tolerance);
|
|
136
|
+
const out = new Array(value.length);
|
|
137
|
+
for (let i = 0; i < value.length; i++)
|
|
138
|
+
out[i] = simplifyNest(value[i], depth - 1, tolerance, simplify);
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A whole GeoJSON value with its vertices dropped — the same value,
|
|
144
|
+
* reduced. Lines go through {@link simplifyLine} and rings through
|
|
145
|
+
* {@link simplifyRing}, so **a ring stays closed and a line keeps both
|
|
146
|
+
* endpoints**; points and multipoints have no run to collapse and come
|
|
147
|
+
* back untouched.
|
|
148
|
+
*
|
|
149
|
+
* Accepts what the traversal layer accepts: a geometry, a Feature, a
|
|
150
|
+
* FeatureCollection or a GeometryCollection. Foreign members, ids and
|
|
151
|
+
* properties ride along — the value that comes back is the value that
|
|
152
|
+
* went in, with fewer positions, so it can be stored or sent as-is.
|
|
153
|
+
* Anything this does not recognize is returned unchanged rather than
|
|
154
|
+
* dropped.
|
|
155
|
+
*
|
|
156
|
+
* The tolerance is in the coordinate's own units, which for GeoJSON is
|
|
157
|
+
* **degrees** — a planar vertex-dropping threshold, never a distance. A
|
|
158
|
+
* degree of longitude is not a fixed length, so calling it metres would
|
|
159
|
+
* be the planar-for-geodesic confusion the rest of this module exists to
|
|
160
|
+
* prevent.
|
|
161
|
+
*
|
|
162
|
+
* @param {any} value
|
|
163
|
+
* @param {number} tolerance - in degrees
|
|
164
|
+
* @returns {any} a new value; the input is not modified
|
|
165
|
+
* @example
|
|
166
|
+
* simplifyGeometry({ type: 'LineString', coordinates: [[0,0],[1,0.001],[2,0]] }, 0.01);
|
|
167
|
+
* // { type: 'LineString', coordinates: [[0,0],[2,0]] }
|
|
168
|
+
*/
|
|
169
|
+
export function simplifyGeometry(value, tolerance) {
|
|
170
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
171
|
+
return value;
|
|
172
|
+
if (value.type === 'FeatureCollection') {
|
|
173
|
+
if (!Array.isArray(value.features))
|
|
174
|
+
return value;
|
|
175
|
+
return { ...value, features: value.features.map((f) => simplifyGeometry(f, tolerance)) };
|
|
176
|
+
}
|
|
177
|
+
if (value.type === 'Feature') {
|
|
178
|
+
return { ...value, geometry: simplifyGeometry(value.geometry, tolerance) };
|
|
179
|
+
}
|
|
180
|
+
if (value.type === 'GeometryCollection') {
|
|
181
|
+
if (!Array.isArray(value.geometries))
|
|
182
|
+
return value;
|
|
183
|
+
return { ...value, geometries: value.geometries.map((g) => simplifyGeometry(g, tolerance)) };
|
|
184
|
+
}
|
|
185
|
+
switch (value.type) {
|
|
186
|
+
case 'LineString':
|
|
187
|
+
return { ...value, coordinates: simplifyNest(value.coordinates, 0, tolerance, simplifyLine) };
|
|
188
|
+
case 'MultiLineString':
|
|
189
|
+
return { ...value, coordinates: simplifyNest(value.coordinates, 1, tolerance, simplifyLine) };
|
|
190
|
+
case 'Polygon':
|
|
191
|
+
return { ...value, coordinates: simplifyNest(value.coordinates, 1, tolerance, simplifyRing) };
|
|
192
|
+
case 'MultiPolygon':
|
|
193
|
+
return { ...value, coordinates: simplifyNest(value.coordinates, 2, tolerance, simplifyRing) };
|
|
194
|
+
default: // Point, MultiPoint, and anything unrecognized: nothing to drop
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
130
199
|
//#endregion
|