@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/ARCHITECTURE.md
CHANGED
|
@@ -120,7 +120,8 @@ flowchart TB
|
|
|
120
120
|
GeoBbox["bbox.js<br/>Bounding boxes"]
|
|
121
121
|
GeoHash["geohash.js<br/>Base-32 cells"]
|
|
122
122
|
GeoJson["geojson.js<br/>Typed-geometry traversal"]
|
|
123
|
-
GeoValid["valid.js
|
|
123
|
+
GeoValid["valid.js<br/>Structural judgment"]
|
|
124
|
+
GeoWkt["wkt.js<br/>Well-Known Text round trip"]
|
|
124
125
|
GeoTree["index-tree.js<br/>Packed-Hilbert box index"]
|
|
125
126
|
GeoMerc["mercator.js + simplify.js<br/>Projection out, simplification"]
|
|
126
127
|
end
|
|
@@ -194,6 +195,7 @@ flowchart TB
|
|
|
194
195
|
GeoIndex --> GeoHash
|
|
195
196
|
GeoIndex --> GeoJson
|
|
196
197
|
GeoIndex --> GeoValid
|
|
198
|
+
GeoIndex --> GeoWkt
|
|
197
199
|
GeoIndex --> GeoTree
|
|
198
200
|
GeoIndex --> GeoMerc
|
|
199
201
|
|
|
@@ -549,7 +551,7 @@ passed straight in without the `type` discriminator being involved.
|
|
|
549
551
|
| `geohash.js` | the base-32 cell encoding, and its validity tester |
|
|
550
552
|
| `geojson.js` | the one layer that knows the `type` discriminator |
|
|
551
553
|
| `valid.js` | `isValidGeoJson` — the one-call structural judgment (rings must close), the boolean twin of the schema artifacts in `@jarenjs/json` |
|
|
552
|
-
| `wkt.js` | `
|
|
554
|
+
| `wkt.js` | the Well-Known Text round trip — `wktToGeoJson`, `geoJsonToWkt` and `isValidWkt`, one grammar walk with and without a builder |
|
|
553
555
|
| `index-tree.js` | a static packed-Hilbert box index for spatial joins |
|
|
554
556
|
| `mercator.js` | Web Mercator, the projection *out* for anything that draws |
|
|
555
557
|
| `simplify.js` | Douglas-Peucker — dropping the vertices a drawing cannot show |
|
|
@@ -557,6 +559,45 @@ passed straight in without the `type` discriminator being involved.
|
|
|
557
559
|
The three validity testers back the `geoFormats` group in
|
|
558
560
|
[`@jarenjs/formats`](../formats) (`geohash`, `wkt`, `geojson`).
|
|
559
561
|
|
|
562
|
+
**WKT is one grammar walk with two entry points.** `isValidWkt` is a format
|
|
563
|
+
tester — it runs per value in the validator and per keystroke in the form
|
|
564
|
+
layer — so it must not allocate a geometry to answer a boolean; and two
|
|
565
|
+
hand-maintained grammars for one syntax would drift apart. Both are avoided by
|
|
566
|
+
giving every scan function a sink: absent, it validates; present, it appends
|
|
567
|
+
the value it just recognized, and `wktToGeoJson` reads the result. A committed
|
|
568
|
+
corpus of 181 strings, its malformed half included, asserts
|
|
569
|
+
`isValidWkt(s) === (wktToGeoJson(s) !== null)` for every entry — a divergence
|
|
570
|
+
is a failing test, not a note. `geoJsonToWkt` writes the string back, and the
|
|
571
|
+
parse-write-parse direction is asserted over the corpus; the write-parse-write
|
|
572
|
+
direction is *not* claimed, because WKT whitespace, the `M` measure and number
|
|
573
|
+
spelling are normalized on the way through.
|
|
574
|
+
|
|
575
|
+
Against [`wellknown`](https://www.npmjs.com/package/wellknown), the established
|
|
576
|
+
WKT↔GeoJSON converter (`npm run benchmark:geo`, Node v24.19.0):
|
|
577
|
+
|
|
578
|
+
| scenario | Jaren | wellknown | ratio |
|
|
579
|
+
|---|---|---|---|
|
|
580
|
+
| parse a `POINT` | 439 ns | 1.34 µs | **3.1×** |
|
|
581
|
+
| parse a 2000-vertex `POLYGON` | 820 µs | 2.20 ms | **2.7×** |
|
|
582
|
+
| write that polygon | 160 µs | 213 µs | **1.3×** |
|
|
583
|
+
| validate a `POINT` | 241 ns | 1.29 µs | **5.4×** |
|
|
584
|
+
| validate that polygon | 493 µs | 1.94 ms | **3.9×** |
|
|
585
|
+
|
|
586
|
+
The last two rows are the ones the sink exists for: `wellknown` has no
|
|
587
|
+
predicate, so validating means parsing and throwing the geometry away. And the
|
|
588
|
+
comparison is *not* like-for-like on work done — **wellknown validates less**
|
|
589
|
+
and is still slower. Over the corpus it accepts 38 strings this grammar
|
|
590
|
+
refuses (no ring closure, no four-point ring or two-point line minimum, no
|
|
591
|
+
coordinate count checked against the modifier, no tag list, and text after the
|
|
592
|
+
geometry ignored); it refuses 22 this one accepts (`M`/`ZM` geometries, `EMPTY`
|
|
593
|
+
for POINT/MULTIPOINT/GEOMETRYCOLLECTION); and on 10 more the two produce
|
|
594
|
+
*different geometries*, because wellknown answers an EMPTY geometry for a
|
|
595
|
+
POLYGON/MULTILINESTRING/MULTIPOLYGON carrying a modifier and writes a
|
|
596
|
+
MULTIPOINT's measure into the altitude slot. Every one of those classes has a
|
|
597
|
+
pinned size in the benchmark's equivalence table, so a difference cannot be
|
|
598
|
+
quietly absorbed, and the entries behind each are listed under it. The
|
|
599
|
+
stringify output is byte-identical on the polygon.
|
|
600
|
+
|
|
560
601
|
Two decisions carry the module. **Orientation is computed exactly**, through
|
|
561
602
|
Shewchuk's adaptive precision arithmetic: a naive floating-point determinant
|
|
562
603
|
returns the *wrong sign* on near-collinear input, which makes a containment
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ None of it depends on JSON Schema: every module can be used standalone in any Ja
|
|
|
10
10
|
|---|---|
|
|
11
11
|
| `@jarenjs/core` | type guards and getters (`isStringType`, `isObjectClass`, `getIntegerType`, ...) |
|
|
12
12
|
| `@jarenjs/core/array` | array helpers (`isUniqueArray`, `getUniqueArray`, `includesAll`, ...) |
|
|
13
|
-
| `@jarenjs/core/object` | deep equality (`equalsDeep`, JSON-only `equalsJson`), the `isJsonObject`
|
|
13
|
+
| `@jarenjs/core/object` | deep equality (`equalsDeep`, JSON-only `equalsJson`), the `isJsonObject` and deep `isJsonValue` predicates, `__proto__`-safe `setObjectMember`, `deepFreeze`, map/set merging |
|
|
14
14
|
| `@jarenjs/core/string` | Unicode string helpers (`countCodePoints`, `compareCodePoints`, ...), cached regex compilation, the suite's one content hash (`fnv1a` and the `hashContent` fingerprint over it) and `kebabCase` |
|
|
15
15
|
| `@jarenjs/core/cache` | the bounded LRU (`createBoundedCache`), the reference-keyed `createWeakCache`, and `createSemanticCache` — keyed by what a value IS, for caches whose entries decide a result |
|
|
16
16
|
| `@jarenjs/core/chunk` | cutting a value down to size: `sizeOf` (the suite's one size rule — a string is its length, anything else its JSON), `excerpt`, `truncate`, and `chunkText` by size, line or separator with offsets that locate a piece in its source |
|
|
@@ -75,7 +75,7 @@ Grouped by file: `email` (RFC 5321 + internationalized addresses), `host` (hostn
|
|
|
75
75
|
```javascript
|
|
76
76
|
import {
|
|
77
77
|
orient2d, haversineDistance, ringWinding, bboxOf,
|
|
78
|
-
geohashEncode,
|
|
78
|
+
geohashEncode, wktToGeoJson, geoJsonToWkt, createBboxIndex,
|
|
79
79
|
} from '@jarenjs/core/geo';
|
|
80
80
|
|
|
81
81
|
orient2d(0, 0, 1, 0, 0, 1); // > 0 — counter-clockwise, exactly
|
|
@@ -83,17 +83,20 @@ haversineDistance(4.9041, 52.3676, 2.3522, 48.8566); // 429_862 m (Amsterdam–P
|
|
|
83
83
|
ringWinding([[0,0],[1,0],[1,1],[0,1],[0,0]]); // 1 — an RFC 7946 exterior ring
|
|
84
84
|
bboxOf({ type: 'Polygon', coordinates: [[[4,52],[5,52],[5,53],[4,52]]] }); // [4, 52, 5, 53]
|
|
85
85
|
geohashEncode(4.9041, 52.3676, 5); // 'u173z' — a string, so a prefix test is proximity
|
|
86
|
-
|
|
86
|
+
|
|
87
|
+
wktToGeoJson('POINT (4.9041 52.3676)'); // { type: 'Point', coordinates: [4.9041, 52.3676] }
|
|
88
|
+
geoJsonToWkt({ type: 'Point', coordinates: [4.9041, 52.3676] }); // 'POINT (4.9041 52.3676)'
|
|
87
89
|
|
|
88
90
|
const index = createBboxIndex(regions.map(bboxOf)); // packed-Hilbert, build once
|
|
89
91
|
for (const i of index.search(...bboxOf(point))) confirm(regions[i]); // candidates, then the exact test
|
|
90
92
|
```
|
|
91
93
|
|
|
92
|
-
|
|
94
|
+
Four design decisions carry the module:
|
|
93
95
|
|
|
94
96
|
- **Orientation is computed exactly** (Shewchuk's adaptive predicates): a naive floating-point determinant returns the *wrong sign* on near-collinear input, which makes containment contradict itself. Every ring winding and point-in-polygon answer rests on this sign, and the deliberate cost is on the [benchmark page](https://jklarenbeek.github.io/jarenjs/#/benchmarks?suite=geo).
|
|
95
97
|
- **Measurement is spherical, drawing is projected, and the two never mix.** A Euclidean norm on raw degrees is 64% wrong over 1 km at Dutch latitudes, so `haversineDistance`/`sphericalRingArea` work on the sphere (`equirectDistance` is the cheap screening form for rejecting candidates first), while `projectMercator`/`fitMercator` and `simplifyLine`/`simplifyRing` exist for renderers — never measure on a projected coordinate.
|
|
96
98
|
- **Validity is a separate concern from traversal.** `eachPosition`, `bboxOf`, `centroidOf` and friends measure without judging; `isValidGeoJson` (structure plus the ring closure a JSON Schema provably cannot express), `isValidWkt` and `isValidGeohash` are the one-call judgments that back the `geoFormats` group in [`@jarenjs/formats`](../formats), next to the full GeoJSON meta-schema artifacts in [`@jarenjs/json`](../json).
|
|
99
|
+
- **WKT is one grammar walk with two entry points.** `isValidWkt` and `wktToGeoJson` run the *same* scan, parameterized by a sink that is absent for the predicate and present for the parser — so the `wkt` format tester (which runs per value in the validator and per keystroke in the form layer) allocates nothing, and the two cannot drift apart. A committed corpus asserts `isValidWkt(s) === (wktToGeoJson(s) !== null)` for all 181 entries, malformed half included. `geoJsonToWkt` writes the string back, and `wktToGeoJson(geoJsonToWkt(g))` returns `g`; the text direction is *not* claimed, because whitespace, the `M` measure and number spelling are normalized. Against [`wellknown`](https://www.npmjs.com/package/wellknown) the parse is 3.1× faster on a `POINT` and the yes-or-no answer 5.4× — and wellknown *validates less* while being slower, which is the honest framing; the table and every language difference are in [ARCHITECTURE](./ARCHITECTURE.md).
|
|
97
100
|
|
|
98
101
|
The spatial query operators (`$distance`, `$within`, `$geohash`, spatial joins over the box index) live in the query engine in [`@jarenjs/json`](../json); the streaming map chart that draws a FeatureCollection with bounded memory lives in [`@jarenjs/charts`](../../components/charts). The full module reference is [docs/GEO.md](./docs/GEO.md).
|
|
99
102
|
|
package/dist/types/function.d.ts
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when a value is a thenable — the shape `await` and the sync-capable
|
|
3
|
+
* async helpers below key on. A `then` that is not a function is data.
|
|
4
|
+
* @param {any} value
|
|
5
|
+
* @returns {boolean}
|
|
6
|
+
*/
|
|
7
|
+
export declare function isThenable(value: any): boolean;
|
|
8
|
+
/**
|
|
9
|
+
* Sync-capable-async composition: apply `next` to a result that may be
|
|
10
|
+
* a value or a promise, WITHOUT allocating a promise when it is already
|
|
11
|
+
* a value. This is how a seam whose implementations may answer either
|
|
12
|
+
* way (a database driver, an idempotency ledger) keeps its synchronous
|
|
13
|
+
* fast path exact while the asynchronous one composes naturally.
|
|
14
|
+
* @param {any} value - A value or a promise of one
|
|
15
|
+
* @param {(value: any) => any} next
|
|
16
|
+
* @returns {any} `next`'s result, promise-wrapped only if the input was
|
|
17
|
+
*/
|
|
18
|
+
export declare function chain(value: any, next: (value: any) => any): any;
|
|
19
|
+
/**
|
|
20
|
+
* Lift a value-or-promise into a promise — the ONE allocation a public
|
|
21
|
+
* asynchronous surface pays per call over a sync-capable seam.
|
|
22
|
+
* @param {any} value
|
|
23
|
+
* @returns {Promise<any>}
|
|
24
|
+
*/
|
|
25
|
+
export declare function toPromise(value: any): Promise<any>;
|
|
1
26
|
/**
|
|
2
27
|
* trueThat
|
|
3
28
|
* acts as a dummy validator that always returns true
|
package/dist/types/geo/bbox.d.ts
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
* The bounding box of a list of positions, as `[west, south, east, north]`.
|
|
3
3
|
* Returns null for an empty list — there is no box that bounds nothing,
|
|
4
4
|
* and an all-Infinity placeholder would silently intersect everything.
|
|
5
|
+
*
|
|
6
|
+
* Null too when any position is non-finite. Narrowing would drop it —
|
|
7
|
+
* every comparison against `NaN` is false — and answer with a finite,
|
|
8
|
+
* plausible box that does not contain the input it was asked to bound.
|
|
9
|
+
* A pre-filter built on such a box loses matching rows silently, which
|
|
10
|
+
* is the one failure a two-stage spatial plan cannot survive, so a
|
|
11
|
+
* position that cannot be bounded refuses the box rather than leaving
|
|
12
|
+
* it too small.
|
|
5
13
|
* @param {Array<number[]>} positions
|
|
6
14
|
* @returns {number[] | null}
|
|
7
15
|
*/
|
|
@@ -30,3 +38,23 @@ export declare function bboxContains(box: number[], x: number, y: number): boole
|
|
|
30
38
|
* @returns {number[]}
|
|
31
39
|
*/
|
|
32
40
|
export declare function bboxUnion(a: number[], b: number[]): number[];
|
|
41
|
+
/**
|
|
42
|
+
* A box as the `Polygon` that covers it — the rectangle written
|
|
43
|
+
* counter-clockwise from its south-west corner, which is RFC 7946's
|
|
44
|
+
* exterior winding, and closed.
|
|
45
|
+
*
|
|
46
|
+
* A box is four numbers and a polygon is a value the rest of the
|
|
47
|
+
* language can measure, contain and intersect, so this is the crossing
|
|
48
|
+
* between the two: a geohash cell, an index probe box or a `bbox`
|
|
49
|
+
* member becomes something `$within` and `$area` can take.
|
|
50
|
+
*
|
|
51
|
+
* @param {number[] | null} box - [west, south, east, north]
|
|
52
|
+
* @returns {{ type: string, coordinates: number[][][] } | null} null for no box
|
|
53
|
+
* @example
|
|
54
|
+
* bboxPolygon([4, 52, 5, 53]);
|
|
55
|
+
* // { type: 'Polygon', coordinates: [[[4,52],[5,52],[5,53],[4,53],[4,52]]] }
|
|
56
|
+
*/
|
|
57
|
+
export declare function bboxPolygon(box: number[] | null): {
|
|
58
|
+
type: string;
|
|
59
|
+
coordinates: number[][][];
|
|
60
|
+
} | null;
|
|
@@ -57,6 +57,38 @@ export declare function initialBearing(lon1: number, lat1: number, lon2: number,
|
|
|
57
57
|
* @returns {[number, number]} the destination as a GeoJSON position
|
|
58
58
|
*/
|
|
59
59
|
export declare function destinationPoint(lon: number, lat: number, bearing: number, distance: number, radius?: number): [number, number];
|
|
60
|
+
/**
|
|
61
|
+
* The bounding box of a geodesic circle: every position within
|
|
62
|
+
* `distance` metres of `(lon, lat)` on the same sphere lies inside it.
|
|
63
|
+
* This is what turns "nearer than r" into a box a range index can seek.
|
|
64
|
+
*
|
|
65
|
+
* The latitude bounds are the circle's due-north and due-south points,
|
|
66
|
+
* but the longitude bounds are **not** its due-east and due-west ones:
|
|
67
|
+
* the circle reaches its extreme meridians where it runs tangent to
|
|
68
|
+
* them, further out than a 90° bearing travels — 0.4 % further at 80°N
|
|
69
|
+
* over 100 km. A box built from four bearings is therefore too small,
|
|
70
|
+
* and too small is the one error a conservative pre-filter cannot
|
|
71
|
+
* survive. The extreme half-width is `asin(sin δ / cos φ)` for angular
|
|
72
|
+
* radius δ at latitude φ.
|
|
73
|
+
*
|
|
74
|
+
* `null` when the circle reaches a pole (`|lat| + δ >= 90°`), where the
|
|
75
|
+
* longitude bound does not exist because the circle spans every
|
|
76
|
+
* meridian, and for any non-finite or negative input.
|
|
77
|
+
*
|
|
78
|
+
* West and east are NOT wrapped into `[-180, 180]`: a circle spanning
|
|
79
|
+
* the antimeridian answers a west below -180 or an east above 180, so
|
|
80
|
+
* a caller can tell that case apart — RFC 7946 asks producers to cut
|
|
81
|
+
* there, and a wrapped box would silently claim the short way round.
|
|
82
|
+
*
|
|
83
|
+
* @param {number} lon - centre longitude, degrees
|
|
84
|
+
* @param {number} lat - centre latitude, degrees
|
|
85
|
+
* @param {number} distance - radius in metres (or in `radius`'s unit)
|
|
86
|
+
* @param {number} [radius] - sphere radius
|
|
87
|
+
* @returns {[number, number, number, number] | null} `[west, south, east, north]`
|
|
88
|
+
* @example
|
|
89
|
+
* circleBounds(5, 52, 1000); // [4.98539…, 51.99101…, 5.01461…, 52.00899…]
|
|
90
|
+
*/
|
|
91
|
+
export declare function circleBounds(lon: number, lat: number, distance: number, radius?: number): [number, number, number, number] | null;
|
|
60
92
|
/**
|
|
61
93
|
* Total great-circle length of a line of positions, in metres. An empty
|
|
62
94
|
* or single-position line has length 0.
|
|
@@ -27,7 +27,10 @@ export declare function geometryOf(value: any): object | null;
|
|
|
27
27
|
export declare function eachPosition(value: any, visit: (position: number[]) => void): void;
|
|
28
28
|
/**
|
|
29
29
|
* The bounding box of any GeoJSON value, as `[west, south, east, north]`.
|
|
30
|
-
* Null when the value contains no positions
|
|
30
|
+
* Null when the value contains no positions, and null when any position
|
|
31
|
+
* it walks is non-finite — the rule `bboxOfPositions` states, for the
|
|
32
|
+
* same reason: a box that does not bound its input is worse than no
|
|
33
|
+
* box.
|
|
31
34
|
*
|
|
32
35
|
* A `bbox` member already present on the value is ignored: it is an
|
|
33
36
|
* optimization the producer may have got wrong, and recomputing is the
|
|
@@ -29,3 +29,31 @@ export declare function simplifyLine(line: Array<number[]>, tolerance: number):
|
|
|
29
29
|
* @returns {Array<number[]>}
|
|
30
30
|
*/
|
|
31
31
|
export declare function simplifyRing(ring: Array<number[]>, tolerance: number): Array<number[]>;
|
|
32
|
+
/**
|
|
33
|
+
* A whole GeoJSON value with its vertices dropped — the same value,
|
|
34
|
+
* reduced. Lines go through {@link simplifyLine} and rings through
|
|
35
|
+
* {@link simplifyRing}, so **a ring stays closed and a line keeps both
|
|
36
|
+
* endpoints**; points and multipoints have no run to collapse and come
|
|
37
|
+
* back untouched.
|
|
38
|
+
*
|
|
39
|
+
* Accepts what the traversal layer accepts: a geometry, a Feature, a
|
|
40
|
+
* FeatureCollection or a GeometryCollection. Foreign members, ids and
|
|
41
|
+
* properties ride along — the value that comes back is the value that
|
|
42
|
+
* went in, with fewer positions, so it can be stored or sent as-is.
|
|
43
|
+
* Anything this does not recognize is returned unchanged rather than
|
|
44
|
+
* dropped.
|
|
45
|
+
*
|
|
46
|
+
* The tolerance is in the coordinate's own units, which for GeoJSON is
|
|
47
|
+
* **degrees** — a planar vertex-dropping threshold, never a distance. A
|
|
48
|
+
* degree of longitude is not a fixed length, so calling it metres would
|
|
49
|
+
* be the planar-for-geodesic confusion the rest of this module exists to
|
|
50
|
+
* prevent.
|
|
51
|
+
*
|
|
52
|
+
* @param {any} value
|
|
53
|
+
* @param {number} tolerance - in degrees
|
|
54
|
+
* @returns {any} a new value; the input is not modified
|
|
55
|
+
* @example
|
|
56
|
+
* simplifyGeometry({ type: 'LineString', coordinates: [[0,0],[1,0.001],[2,0]] }, 0.01);
|
|
57
|
+
* // { type: 'LineString', coordinates: [[0,0],[2,0]] }
|
|
58
|
+
*/
|
|
59
|
+
export declare function simplifyGeometry(value: any, tolerance: number): any;
|
package/dist/types/geo/wkt.d.ts
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* coordinate count consistent with the `Z`/`M`/`ZM` modifier and with
|
|
5
5
|
* itself, closed polygon rings, and nothing before or after.
|
|
6
6
|
*
|
|
7
|
+
* This is the non-allocating half of the walk {@link wktToGeoJson}
|
|
8
|
+
* builds with, so the two accept exactly the same language.
|
|
9
|
+
*
|
|
7
10
|
* @param {string} text
|
|
8
11
|
* @returns {boolean}
|
|
9
12
|
* @example
|
|
@@ -12,5 +15,67 @@
|
|
|
12
15
|
* isValidWkt('POLYGON ((0 0, 4 0, 4 4, 1 1))'); // false (open ring)
|
|
13
16
|
* isValidWkt('POINT Z (1 2)'); // false (Z wants 3)
|
|
14
17
|
* isValidWkt('LINESTRING (0 0, 1 1 1)'); // false (mixed dimension)
|
|
18
|
+
* isValidWkt('CIRCLE EMPTY'); // false (not one of the seven)
|
|
15
19
|
*/
|
|
16
20
|
export declare function isValidWkt(text: string): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* The GeoJSON geometry a WKT string names, or `null` when the text is
|
|
23
|
+
* not well-formed WKT — the same judgment {@link isValidWkt} makes,
|
|
24
|
+
* from the same walk. `null` rather than a throw is the kernel's
|
|
25
|
+
* posture for "no answer" (`bboxOf`, `centroidOf`, `geohashBounds` all
|
|
26
|
+
* answer it), so no call site has to wrap this in a try.
|
|
27
|
+
*
|
|
28
|
+
* Three rules decide what a lenient parser would silently lose:
|
|
29
|
+
*
|
|
30
|
+
* - `EMPTY` becomes an empty coordinate array (`POINT EMPTY` →
|
|
31
|
+
* `{ type: 'Point', coordinates: [] }`), never `null`: unparseable
|
|
32
|
+
* and validly empty are different answers.
|
|
33
|
+
* - The `M` measure is dropped and `Z` is kept. RFC 7946 §3.1.1 defines
|
|
34
|
+
* a position's third element as altitude, and a measure is not one —
|
|
35
|
+
* writing it there would be a lie. `POINT ZM (1 2 3 4)` → `[1, 2, 3]`,
|
|
36
|
+
* `POINT M (1 2 3)` → `[1, 2]`.
|
|
37
|
+
* - Coordinate ranges are not judged. `POINT (999 999)` parses;
|
|
38
|
+
* `isValidGeoJson` is the value gate.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} text
|
|
41
|
+
* @returns {object | null}
|
|
42
|
+
* @example
|
|
43
|
+
* wktToGeoJson('POINT (4.9041 52.3676)');
|
|
44
|
+
* // { type: 'Point', coordinates: [4.9041, 52.3676] }
|
|
45
|
+
* wktToGeoJson('POLYGON ((0 0, 4 0, 4 4, 0 0))');
|
|
46
|
+
* // { type: 'Polygon', coordinates: [[[0,0],[4,0],[4,4],[0,0]]] }
|
|
47
|
+
* wktToGeoJson('POINT EMPTY'); // { type: 'Point', coordinates: [] }
|
|
48
|
+
* wktToGeoJson('POLYGON ((0 0))'); // null
|
|
49
|
+
*/
|
|
50
|
+
export declare function wktToGeoJson(text: string): object | null;
|
|
51
|
+
/**
|
|
52
|
+
* A GeoJSON value as a WKT string, or `null` for a value with no
|
|
53
|
+
* geometry to write. Accepts what the traversal layer accepts: a bare
|
|
54
|
+
* position, a geometry, a Feature or a FeatureCollection.
|
|
55
|
+
*
|
|
56
|
+
* `options.dim` is 2 (the default) or 3. At 3, a geometry whose every
|
|
57
|
+
* position carries a third element is written with a `Z` modifier;
|
|
58
|
+
* anything else is written 2D with the third elements dropped, because
|
|
59
|
+
* one WKT geometry carries one modifier for all of its coordinates. The
|
|
60
|
+
* decision is made per tagged geometry, so a GeometryCollection may mix
|
|
61
|
+
* 2D and 3D members — each writes its own modifier. Invalid WKT is
|
|
62
|
+
* never emitted.
|
|
63
|
+
*
|
|
64
|
+
* A non-finite coordinate yields `null`: a value that cannot be written
|
|
65
|
+
* is not written approximately.
|
|
66
|
+
*
|
|
67
|
+
* @param {any} value
|
|
68
|
+
* @param {{ dim?: number }} [options]
|
|
69
|
+
* @returns {string | null}
|
|
70
|
+
* @example
|
|
71
|
+
* geoJsonToWkt({ type: 'Point', coordinates: [4.9041, 52.3676] });
|
|
72
|
+
* // 'POINT (4.9041 52.3676)'
|
|
73
|
+
* geoJsonToWkt([4.9041, 52.3676]); // 'POINT (4.9041 52.3676)'
|
|
74
|
+
* geoJsonToWkt({ type: 'Point', coordinates: [] }); // 'POINT EMPTY'
|
|
75
|
+
* geoJsonToWkt({ type: 'Point', coordinates: [1, 2, 3] }, { dim: 3 });
|
|
76
|
+
* // 'POINT Z (1 2 3)'
|
|
77
|
+
* geoJsonToWkt({ type: 'Point', coordinates: [NaN, 2] }); // null
|
|
78
|
+
*/
|
|
79
|
+
export declare function geoJsonToWkt(value: any, options?: {
|
|
80
|
+
dim?: number;
|
|
81
|
+
}): string | null;
|
package/dist/types/object.d.ts
CHANGED
|
@@ -136,6 +136,24 @@ export declare function semanticKey(value: any): string;
|
|
|
136
136
|
* @returns {boolean}
|
|
137
137
|
*/
|
|
138
138
|
export declare function isJsonObject(value: any): boolean;
|
|
139
|
+
/**
|
|
140
|
+
* True when a value is representable as JSON as it stands: `null`, a
|
|
141
|
+
* boolean, a finite number, a string, or an array/plain object (own
|
|
142
|
+
* enumerable members, prototype `Object.prototype` or `null`) whose
|
|
143
|
+
* members all are. Everything else — `undefined`, `bigint`, `symbol`,
|
|
144
|
+
* functions, `NaN`/`±Infinity`, class instances (`Error`, `Date`, `Map`,
|
|
145
|
+
* `Response`, DOM nodes ...) and any cycle — is not.
|
|
146
|
+
*
|
|
147
|
+
* This is the boundary predicate for "JSON only crosses": a host that
|
|
148
|
+
* hands a value into application state asks it once, before the value
|
|
149
|
+
* can carry an object the state validator would have to reject. It is
|
|
150
|
+
* TOTAL — a hostile accessor that throws makes the value not-JSON rather
|
|
151
|
+
* than propagating.
|
|
152
|
+
*
|
|
153
|
+
* @param {any} value
|
|
154
|
+
* @returns {boolean}
|
|
155
|
+
*/
|
|
156
|
+
export declare function isJsonValue(value: any): boolean;
|
|
139
157
|
/**
|
|
140
158
|
* Assign a member so that a key named `__proto__` becomes an own data
|
|
141
159
|
* property instead of reassigning the object's prototype. Every builder
|
package/dist/types/scan.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ export declare const CC_0 = 48;
|
|
|
21
21
|
export declare const CC_1 = 49;
|
|
22
22
|
export declare const CC_9 = 57;
|
|
23
23
|
export declare const CC_COLON = 58;
|
|
24
|
+
export declare const CC_SEMICOLON = 59;
|
|
24
25
|
export declare const CC_LT = 60;
|
|
25
26
|
export declare const CC_EQ = 61;
|
|
26
27
|
export declare const CC_GT = 62;
|
package/dist/types/string.d.ts
CHANGED
|
@@ -94,6 +94,28 @@ export declare function fromCodePoints(codePoints: number[]): string;
|
|
|
94
94
|
* @returns {number} -1 when a < b, 0 when equal, 1 when a > b
|
|
95
95
|
*/
|
|
96
96
|
export declare function compareCodePoints(a: string, b: string): number;
|
|
97
|
+
/**
|
|
98
|
+
* The exclusive upper bound of the strings beginning with `prefix`: the
|
|
99
|
+
* smallest string that sorts above every one of them. For a
|
|
100
|
+
* well-formed `value`, `value` begins with `prefix` **iff**
|
|
101
|
+
* `prefix <= value < codePointPrefixSuccessor(prefix)` under
|
|
102
|
+
* {@link compareCodePoints} — which is what lets a caller spell a
|
|
103
|
+
* prefix test as a half-open range, the form an ordered index can seek
|
|
104
|
+
* rather than test row by row.
|
|
105
|
+
*
|
|
106
|
+
* Null when there is no such string, and the caller then has no range
|
|
107
|
+
* to offer: the empty prefix (every string begins with it) and a
|
|
108
|
+
* prefix of nothing but U+10FFFF (nothing sorts above it).
|
|
109
|
+
*
|
|
110
|
+
* The bound never lands in the surrogate range, because it has to be a
|
|
111
|
+
* string the caller can hand on — to a comparator, a database
|
|
112
|
+
* parameter, a serializer. No well-formed string sorts inside that
|
|
113
|
+
* range, so stepping over it leaves the equivalence above intact.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} prefix - The prefix to bound
|
|
116
|
+
* @returns {string | null} The exclusive upper bound, or null when none exists
|
|
117
|
+
*/
|
|
118
|
+
export declare function codePointPrefixSuccessor(prefix: string): string | null;
|
|
97
119
|
/** FNV-1a 32-bit offset basis — the seed a fresh hash starts from. */
|
|
98
120
|
export declare const FNV1A_OFFSET_BASIS = 2166136261;
|
|
99
121
|
/**
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file An incremental Server-Sent-Events codec (WHATWG HTML §9.2).
|
|
3
|
+
* `createSseEventDecoder` yields complete events `{ event, id, data,
|
|
4
|
+
* retry }` from network chunks fed in any split — mid-line, mid-event,
|
|
5
|
+
* CR, LF or CRLF line endings; `createSseDecoder` is the data-only view
|
|
6
|
+
* of the same machine (the shape every OpenAI-compatible streaming
|
|
7
|
+
* endpoint needs: just the `data:` payloads, in order); and
|
|
8
|
+
* `encodeSseEvent` renders one event back to wire text.
|
|
9
|
+
*
|
|
10
|
+
* Dispatch follows the specification: field lines are `name: value`
|
|
11
|
+
* with one optional leading space in the value, `:` lines are comments,
|
|
12
|
+
* an event is dispatched on the blank line exactly when its data buffer
|
|
13
|
+
* is non-empty (multi-line data joins with `\n`), the last-event-id is
|
|
14
|
+
* a stream attribute that persists across events (an `id` field whose
|
|
15
|
+
* value carries U+0000 is ignored), and `retry` must be ASCII digits.
|
|
16
|
+
* `end()` flushes a final event from a stream that never sent its
|
|
17
|
+
* closing blank line.
|
|
18
|
+
*/
|
|
19
|
+
export type SseEvent = {
|
|
20
|
+
event: string | null;
|
|
21
|
+
id: string | null;
|
|
22
|
+
data: string;
|
|
23
|
+
retry: number | null;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* One decoded event. `event` is the `event:` field of the block, `null`
|
|
27
|
+
* when the block had none (the specification's default type "message");
|
|
28
|
+
* `id` is the stream's last-event-id at dispatch, `null` while the
|
|
29
|
+
* stream has not set one; `data` is the joined data payload; `retry` is
|
|
30
|
+
* the reconnection time a `retry:` field set since the previous
|
|
31
|
+
* dispatch, `null` otherwise.
|
|
32
|
+
* @typedef {Object} SseEvent
|
|
33
|
+
* @property {string | null} event
|
|
34
|
+
* @property {string | null} id
|
|
35
|
+
* @property {string} data
|
|
36
|
+
* @property {number | null} retry
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* An incremental decoder yielding complete events.
|
|
40
|
+
* @returns {{ feed: (chunk: string) => SseEvent[], end: () => SseEvent[] }}
|
|
41
|
+
* `feed` returns the events completed by this chunk; `end` flushes a
|
|
42
|
+
* final event from a stream that never sent its closing blank line.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createSseEventDecoder(): {
|
|
45
|
+
feed: (chunk: string) => SseEvent[];
|
|
46
|
+
end: () => SseEvent[];
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* The data-only view of the event decoder: `feed` yields the complete
|
|
50
|
+
* `data:` payloads in order and every other field is ignored — the
|
|
51
|
+
* shape a chat-completion stream consumer needs.
|
|
52
|
+
* @returns {{ feed: (chunk: string) => string[], end: () => string[] }}
|
|
53
|
+
*/
|
|
54
|
+
export declare function createSseDecoder(): {
|
|
55
|
+
feed: (chunk: string) => string[];
|
|
56
|
+
end: () => string[];
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Render one event as wire text: the optional `event:`, `id:` and
|
|
60
|
+
* `retry:` lines, the data split on `\n` into one `data:` line each,
|
|
61
|
+
* and the dispatching blank line. Throws `TypeError` for text the frame
|
|
62
|
+
* cannot carry: a line terminator inside `event` or `id`, U+0000 inside
|
|
63
|
+
* `id`, a bare carriage return inside `data` (a decoder would read it
|
|
64
|
+
* as a line break and corrupt the framing), or a `retry` that is not a
|
|
65
|
+
* non-negative integer.
|
|
66
|
+
* @param {{ event?: string | null, id?: string | null, data: string, retry?: number | null }} fields
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
export declare function encodeSseEvent(fields: {
|
|
70
|
+
event?: string | null;
|
|
71
|
+
id?: string | null;
|
|
72
|
+
data: string;
|
|
73
|
+
retry?: number | null;
|
|
74
|
+
}): string;
|
package/docs/GEO.md
CHANGED
|
@@ -35,6 +35,20 @@ rejecting candidates before the real test. Also `initialBearing`,
|
|
|
35
35
|
`destinationPoint`, `lineLength(positions)`. The ellipsoid is
|
|
36
36
|
deliberately not modelled.
|
|
37
37
|
|
|
38
|
+
`circleBounds(lon, lat, metres)` answers the `[west, south, east,
|
|
39
|
+
north]` box a "nearer than r" test can be seeked with — the shape a
|
|
40
|
+
range index needs before the exact distance runs. Its longitude bounds
|
|
41
|
+
are **not** the circle's due-east and due-west points: the circle
|
|
42
|
+
reaches its extreme meridians where it runs tangent to them, which is
|
|
43
|
+
`asin(sin δ / cos φ)` from the centre and 0.4 % further than a 90°
|
|
44
|
+
bearing travels at 80°N over 100 km. A box built from four bearings is
|
|
45
|
+
too small, and a pre-filter that is too small drops matching rows
|
|
46
|
+
silently. It answers `null` when the circle reaches a pole (there is no
|
|
47
|
+
longitude bound to give), and it does not wrap west/east into
|
|
48
|
+
`[-180, 180]` — a circle spanning the antimeridian answers a west below
|
|
49
|
+
-180, which is how a caller detects the case RFC 7946 asks producers to
|
|
50
|
+
cut.
|
|
51
|
+
|
|
38
52
|
## Rings — `ring.js`
|
|
39
53
|
|
|
40
54
|
`isRingClosed`, `ringSignedArea` (shoelace over `orient2d` terms —
|
|
@@ -56,6 +70,15 @@ never-matching, keeping positions aligned with the caller's array.
|
|
|
56
70
|
Build and probe are level with Flatbush (`npm run benchmark:geo`; the
|
|
57
71
|
table lives in [ARCHITECTURE](../ARCHITECTURE.md)).
|
|
58
72
|
|
|
73
|
+
**A box is refused rather than made too small.** `bboxOfPositions` and
|
|
74
|
+
`bboxOf` answer `null` for an empty input *and* whenever any position
|
|
75
|
+
they are asked to bound has a non-finite coordinate — they never skip
|
|
76
|
+
the position and bound the rest. A narrowing loop would drop it
|
|
77
|
+
silently (every comparison against `NaN` is false) and return a finite,
|
|
78
|
+
plausible box that does not contain its input, and a candidate filter
|
|
79
|
+
built on such a box loses matching entries with nothing to show for it.
|
|
80
|
+
`null` is the answer every caller already handles.
|
|
81
|
+
|
|
59
82
|
## GeoJSON traversal — `geojson.js`
|
|
60
83
|
|
|
61
84
|
The one layer that reads the `type` discriminator; every function takes
|
|
@@ -66,7 +89,7 @@ mass), `containsPosition`, `geoDistance` (between representative
|
|
|
66
89
|
positions), `ringsClosed`. Nothing here validates — malformed input
|
|
67
90
|
yields null or 0 — because judgment lives one module over.
|
|
68
91
|
|
|
69
|
-
## Validity — `valid.js`, `
|
|
92
|
+
## Validity — `valid.js`, `geohash.js`
|
|
70
93
|
|
|
71
94
|
The one-call judgments backing the `geoFormats` group in
|
|
72
95
|
`@jarenjs/formats`:
|
|
@@ -76,11 +99,61 @@ The one-call judgments backing the `geoFormats` group in
|
|
|
76
99
|
invariant a JSON Schema provably cannot express. The shallow twin of
|
|
77
100
|
the meta-schema artifacts in `@jarenjs/json`, which locate failures
|
|
78
101
|
and (via `$query`) also check winding.
|
|
79
|
-
- `isValidWkt(text)` — strict ISO 19125 grammar: seven tags, `Z`/`M`/
|
|
80
|
-
`ZM` modifiers (unmodified accepts 2 or 3 coordinates, as PostGIS
|
|
81
|
-
does), consistent counts, closed rings, `EMPTY`, no surrounding text.
|
|
82
102
|
- `isValidGeohash(hash)` — non-empty, lowercase base-32 alphabet.
|
|
83
103
|
|
|
104
|
+
## Well-Known Text — `wkt.js`
|
|
105
|
+
|
|
106
|
+
The round trip with the text encoding every spatial database emits.
|
|
107
|
+
`wktToGeoJson(text)` returns a geometry or `null`; `geoJsonToWkt(value,
|
|
108
|
+
options?)` returns a string or `null`; `isValidWkt(text)` answers
|
|
109
|
+
yes-or-no. **All three are one grammar walk**: each scan function takes
|
|
110
|
+
a sink that is absent for the predicate and present for the parser, so
|
|
111
|
+
the format tester allocates nothing and the two entry points cannot
|
|
112
|
+
drift. A committed corpus asserts `isValidWkt(s) === (wktToGeoJson(s)
|
|
113
|
+
!== null)` for every entry, malformed half included — a divergence is a
|
|
114
|
+
failing test.
|
|
115
|
+
|
|
116
|
+
The grammar is strict ISO 19125: the seven tags and no others,
|
|
117
|
+
`Z`/`M`/`ZM` modifiers (unmodified accepts 2 or 3 coordinates, as
|
|
118
|
+
PostGIS does), coordinate counts consistent with the modifier and with
|
|
119
|
+
the geometry's first point, rings of four or more positions that close,
|
|
120
|
+
`EMPTY`, and no text before or after.
|
|
121
|
+
|
|
122
|
+
Four rules decide what a lenient converter loses:
|
|
123
|
+
|
|
124
|
+
- **`EMPTY` is an empty coordinate array**, not `null`: `POINT EMPTY` →
|
|
125
|
+
`{ type: 'Point', coordinates: [] }`. Unparseable and validly empty
|
|
126
|
+
are different answers.
|
|
127
|
+
- **`Z` is kept and `M` is dropped.** RFC 7946 §3.1.1 defines a
|
|
128
|
+
position's third element as altitude; a WKT measure is not one, so
|
|
129
|
+
writing it there would be a lie. `POINT ZM (1 2 3 4)` → `[1, 2, 3]`,
|
|
130
|
+
`POINT M (1 2 3)` → `[1, 2]`. The measure is discarded, and it is
|
|
131
|
+
discarded on purpose.
|
|
132
|
+
- **The parser does not judge ranges.** `POINT (999 999)` parses;
|
|
133
|
+
`isValidGeoJson` is the value gate, and duplicating it here would make
|
|
134
|
+
the two entry points disagree.
|
|
135
|
+
- **A value that cannot be written is not written approximately.**
|
|
136
|
+
`geoJsonToWkt` answers `null` for a non-finite coordinate, and writes
|
|
137
|
+
numbers with the shortest round-tripping spelling (`String(n)`, what
|
|
138
|
+
`JSON.stringify` uses) — never a fixed precision, which would silently
|
|
139
|
+
move the point.
|
|
140
|
+
|
|
141
|
+
`geoJsonToWkt` accepts what the traversal layer accepts: a Feature
|
|
142
|
+
writes its geometry, a FeatureCollection a `GEOMETRYCOLLECTION` of its
|
|
143
|
+
features' geometries, a bare position a `POINT`. `options.dim` is 2 (the
|
|
144
|
+
default) or 3; at 3 a geometry whose every position carries a third
|
|
145
|
+
element gets a `Z` modifier, and anything else is written 2D with the
|
|
146
|
+
third elements dropped, because one WKT geometry carries one modifier
|
|
147
|
+
for all of its coordinates. The decision is made per tagged geometry, so
|
|
148
|
+
a `GEOMETRYCOLLECTION` may mix 2D and 3D members — invalid WKT is never
|
|
149
|
+
emitted.
|
|
150
|
+
|
|
151
|
+
**One direction round-trips and the other does not.**
|
|
152
|
+
`wktToGeoJson(geoJsonToWkt(g))` returns `g`, and that is asserted over
|
|
153
|
+
the whole corpus. `geoJsonToWkt(wktToGeoJson(s)) === s` is false in
|
|
154
|
+
general and is not claimed: whitespace, the `M` measure and number
|
|
155
|
+
spelling are all normalized.
|
|
156
|
+
|
|
84
157
|
## Geohash — `geohash.js`
|
|
85
158
|
|
|
86
159
|
`geohashEncode(lon, lat, precision)`, `geohashDecode`, `geohashBounds`,
|
package/package.json
CHANGED
package/src/function.js
CHANGED
|
@@ -5,6 +5,40 @@ import {
|
|
|
5
5
|
|
|
6
6
|
// export * from './function-tools-xtra';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* True when a value is a thenable — the shape `await` and the sync-capable
|
|
10
|
+
* async helpers below key on. A `then` that is not a function is data.
|
|
11
|
+
* @param {any} value
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
export function isThenable(value) {
|
|
15
|
+
return value !== null && typeof value === 'object' && typeof value.then === 'function';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Sync-capable-async composition: apply `next` to a result that may be
|
|
20
|
+
* a value or a promise, WITHOUT allocating a promise when it is already
|
|
21
|
+
* a value. This is how a seam whose implementations may answer either
|
|
22
|
+
* way (a database driver, an idempotency ledger) keeps its synchronous
|
|
23
|
+
* fast path exact while the asynchronous one composes naturally.
|
|
24
|
+
* @param {any} value - A value or a promise of one
|
|
25
|
+
* @param {(value: any) => any} next
|
|
26
|
+
* @returns {any} `next`'s result, promise-wrapped only if the input was
|
|
27
|
+
*/
|
|
28
|
+
export function chain(value, next) {
|
|
29
|
+
return isThenable(value) ? value.then(next) : next(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Lift a value-or-promise into a promise — the ONE allocation a public
|
|
34
|
+
* asynchronous surface pays per call over a sync-capable seam.
|
|
35
|
+
* @param {any} value
|
|
36
|
+
* @returns {Promise<any>}
|
|
37
|
+
*/
|
|
38
|
+
export function toPromise(value) {
|
|
39
|
+
return isThenable(value) ? value : Promise.resolve(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
8
42
|
/**
|
|
9
43
|
* trueThat
|
|
10
44
|
* acts as a dummy validator that always returns true
|