@mailwoman/spatial 4.15.0 → 4.16.1
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/bbox.test.ts +39 -0
- package/bbox.ts +3 -3
- package/coordinate-formats.test.ts +84 -0
- package/coordinate-formats.ts +215 -0
- package/feature.ts +3 -3
- package/geometries/point.test.ts +144 -0
- package/geometries/polygon.test.ts +62 -0
- package/google/place-id.test.ts +54 -0
- package/index.ts +1 -0
- package/out/bbox.d.ts +2 -2
- package/out/bbox.js +2 -2
- package/out/coordinate-formats.d.ts +43 -0
- package/out/coordinate-formats.d.ts.map +1 -0
- package/out/coordinate-formats.js +194 -0
- package/out/coordinate-formats.js.map +1 -0
- package/out/feature.d.ts +3 -3
- package/out/feature.d.ts.map +1 -1
- package/out/index.d.ts +1 -0
- package/out/index.d.ts.map +1 -1
- package/out/index.js +1 -0
- package/out/index.js.map +1 -1
- package/out/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
- package/position.test.ts +48 -0
- package/sdk/well-known-text.test.ts +133 -0
- package/tsconfig.json +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mailwoman/spatial",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.16.1",
|
|
4
4
|
"description": "Spatial analysis, geocoding, and other geo-related utilities.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"contributors": [
|
|
@@ -22,13 +22,14 @@
|
|
|
22
22
|
".": "./out/index.js"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@mailwoman/core": "4.
|
|
25
|
+
"@mailwoman/core": "4.16.1",
|
|
26
26
|
"geo-coordinates-parser": "^1.7.4",
|
|
27
27
|
"h3-js": "^4.4.0",
|
|
28
28
|
"wkx": "^0.5.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@googlemaps/google-maps-services-js": "^3.4.2",
|
|
32
|
+
"@mailwoman/annotations": "4.16.1",
|
|
32
33
|
"@types/google.maps": "^3.65.1",
|
|
33
34
|
"type-fest": "^5.7.0"
|
|
34
35
|
},
|
|
@@ -37,7 +38,7 @@
|
|
|
37
38
|
"@types/google.maps": "^3.65.1"
|
|
38
39
|
},
|
|
39
40
|
"engines": {
|
|
40
|
-
"node": ">=
|
|
41
|
+
"node": ">=24.18.0"
|
|
41
42
|
},
|
|
42
43
|
"keywords": [
|
|
43
44
|
"geo",
|
package/position.test.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { haversineKm } from "@mailwoman/spatial"
|
|
8
|
+
import { expect, test } from "vitest"
|
|
9
|
+
|
|
10
|
+
// Earth mean radius the formula uses (RADII.km). Reference distances below are derived from it, not
|
|
11
|
+
// looked up — so they pin the exact constant + formula, not an approximation.
|
|
12
|
+
const R = 6371
|
|
13
|
+
|
|
14
|
+
test("haversineKm: a point to itself is zero", () => {
|
|
15
|
+
expect(haversineKm(40.7128, -74.006, 40.7128, -74.006)).toBe(0)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test("haversineKm: one degree along the equator / a meridian is R·(π/180)", () => {
|
|
19
|
+
const oneDegree = (R * Math.PI) / 180 // ≈ 111.195 km
|
|
20
|
+
expect(haversineKm(0, 0, 0, 1)).toBeCloseTo(oneDegree, 3) // 1° longitude at the equator
|
|
21
|
+
expect(haversineKm(0, 0, 1, 0)).toBeCloseTo(oneDegree, 3) // 1° latitude along a meridian
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test("haversineKm: antipodal points are half the great circle (R·π)", () => {
|
|
25
|
+
expect(haversineKm(0, 0, 0, 180)).toBeCloseTo(R * Math.PI, 2) // ≈ 20015.09 km
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test("haversineKm: a real-world pair lands in the right ballpark (NYC ↔ LA)", () => {
|
|
29
|
+
const d = haversineKm(40.7128, -74.006, 34.0522, -118.2437)
|
|
30
|
+
expect(d).toBeGreaterThan(3900)
|
|
31
|
+
expect(d).toBeLessThan(3970) // ~3936 km
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test("haversineKm: symmetric in its arguments", () => {
|
|
35
|
+
const ab = haversineKm(51.5074, -0.1278, 48.8566, 2.3522) // London → Paris
|
|
36
|
+
const ba = haversineKm(48.8566, 2.3522, 51.5074, -0.1278)
|
|
37
|
+
expect(ab).toBeCloseTo(ba, 10)
|
|
38
|
+
expect(ab).toBeGreaterThan(330)
|
|
39
|
+
expect(ab).toBeLessThan(355) // ~343 km
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test("haversineKm: (0,0) is a real point (Gulf of Guinea), not a missing-coordinate sentinel", () => {
|
|
43
|
+
// Unlike the object-form `haversine`, the raw-scalar form has no Null-Island sentinel — 0/0 is a
|
|
44
|
+
// real coordinate, so this returns a finite distance rather than NaN.
|
|
45
|
+
const d = haversineKm(0, 0, 0.5, 0.5)
|
|
46
|
+
expect(Number.isNaN(d)).toBe(false)
|
|
47
|
+
expect(d).toBeGreaterThan(0)
|
|
48
|
+
})
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { GeometryLiteral } from "@mailwoman/spatial"
|
|
8
|
+
import { expect, test } from "vitest"
|
|
9
|
+
import wkx from "wkx"
|
|
10
|
+
import {
|
|
11
|
+
geometryToEWKB,
|
|
12
|
+
geometryToEWKH,
|
|
13
|
+
geometryToSQL,
|
|
14
|
+
geometryToWKB,
|
|
15
|
+
geometryToWKT,
|
|
16
|
+
wellKnownGeometryToGeoJSON,
|
|
17
|
+
} from "./well-known-text.js"
|
|
18
|
+
|
|
19
|
+
const POINT: GeometryLiteral = { type: "Point", coordinates: [30, 10] }
|
|
20
|
+
|
|
21
|
+
// A closed exterior ring (last vertex repeats the first), as GeoJSON requires.
|
|
22
|
+
const POLYGON: GeometryLiteral = {
|
|
23
|
+
type: "Polygon",
|
|
24
|
+
coordinates: [
|
|
25
|
+
[
|
|
26
|
+
[30, 10],
|
|
27
|
+
[40, 40],
|
|
28
|
+
[20, 40],
|
|
29
|
+
[10, 20],
|
|
30
|
+
[30, 10],
|
|
31
|
+
],
|
|
32
|
+
],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The 4-level MultiPolygon literal doesn't narrow against the GeometryLiteral union; cast it.
|
|
36
|
+
const MULTIPOLYGON = {
|
|
37
|
+
type: "MultiPolygon",
|
|
38
|
+
coordinates: [
|
|
39
|
+
[
|
|
40
|
+
[
|
|
41
|
+
[30, 20],
|
|
42
|
+
[45, 40],
|
|
43
|
+
[10, 40],
|
|
44
|
+
[30, 20],
|
|
45
|
+
],
|
|
46
|
+
],
|
|
47
|
+
[
|
|
48
|
+
[
|
|
49
|
+
[15, 5],
|
|
50
|
+
[40, 10],
|
|
51
|
+
[10, 20],
|
|
52
|
+
[5, 10],
|
|
53
|
+
[15, 5],
|
|
54
|
+
],
|
|
55
|
+
],
|
|
56
|
+
],
|
|
57
|
+
} as unknown as GeometryLiteral
|
|
58
|
+
|
|
59
|
+
test("wellKnownGeometryToGeoJSON: parses a WKT POINT into exact GeoJSON coordinates", () => {
|
|
60
|
+
const geo = wellKnownGeometryToGeoJSON<GeometryLiteral>("POINT(30 10)")
|
|
61
|
+
expect(geo).toEqual(POINT)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test("wellKnownGeometryToGeoJSON: parses a WKT POLYGON, preserving ring order and closure", () => {
|
|
65
|
+
const geo = wellKnownGeometryToGeoJSON<GeometryLiteral>("POLYGON((30 10,40 40,20 40,10 20,30 10))")
|
|
66
|
+
expect(geo).toEqual(POLYGON)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test("wellKnownGeometryToGeoJSON: parses a WKT MULTIPOLYGON into two distinct polygons", () => {
|
|
70
|
+
const geo = wellKnownGeometryToGeoJSON<GeometryLiteral>(
|
|
71
|
+
"MULTIPOLYGON(((30 20,45 40,10 40,30 20)),((15 5,40 10,10 20,5 10,15 5)))"
|
|
72
|
+
)
|
|
73
|
+
expect(geo).toEqual(MULTIPOLYGON)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test("wellKnownGeometryToGeoJSON: parses an EWKB buffer back into the source geometry", () => {
|
|
77
|
+
// Produce a known EWKB buffer, then round-trip it through the parser.
|
|
78
|
+
const ewkb = wkx.Geometry.parseGeoJSON(POINT).toEwkb()
|
|
79
|
+
const geo = wellKnownGeometryToGeoJSON<GeometryLiteral>(ewkb)
|
|
80
|
+
expect(geo).toEqual(POINT)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test("wellKnownGeometryToGeoJSON: throws on malformed WKT", () => {
|
|
84
|
+
expect(() => wellKnownGeometryToGeoJSON("NOT A GEOMETRY")).toThrow()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test("geometryToWKT: serializes GeoJSON back to its canonical WKT string", () => {
|
|
88
|
+
expect(geometryToWKT(POINT)).toBe("POINT(30 10)")
|
|
89
|
+
expect(geometryToWKT(POLYGON)).toBe("POLYGON((30 10,40 40,20 40,10 20,30 10))")
|
|
90
|
+
expect(geometryToWKT(MULTIPOLYGON)).toBe("MULTIPOLYGON(((30 20,45 40,10 40,30 20)),((15 5,40 10,10 20,5 10,15 5)))")
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test("geometryToWKT ↔ wellKnownGeometryToGeoJSON: lossless round-trip for a polygon", () => {
|
|
94
|
+
const wkt = geometryToWKT(POLYGON)
|
|
95
|
+
const back = wellKnownGeometryToGeoJSON<GeometryLiteral>(wkt)
|
|
96
|
+
expect(back).toEqual(POLYGON)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test("geometryToWKB: produces the exact little-endian WKB byte string for a POINT", () => {
|
|
100
|
+
// 01 (LE) | 01000000 (type=Point) | 30 as float64 LE | 10 as float64 LE
|
|
101
|
+
const wkb = geometryToWKB(POINT)
|
|
102
|
+
expect(wkb.toString("hex")).toBe("01010000000000000000003e400000000000002440")
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test("geometryToWKB: round-trips through the parser back to source GeoJSON", () => {
|
|
106
|
+
const wkb = geometryToWKB(MULTIPOLYGON)
|
|
107
|
+
const back = wkx.Geometry.parse(wkb).toGeoJSON()
|
|
108
|
+
expect(back).toEqual(MULTIPOLYGON)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test("geometryToEWKB: tags the geometry with the SRID-flag the plain WKB lacks", () => {
|
|
112
|
+
// EWKB sets the 0x20000000 SRID flag on the type word, so the hex differs from plain WKB.
|
|
113
|
+
const ewkb = geometryToEWKB(POINT)
|
|
114
|
+
expect(ewkb.toString("hex")).toBe("0101000020e61000000000000000003e400000000000002440")
|
|
115
|
+
// Distinct from plain WKB.
|
|
116
|
+
expect(ewkb.toString("hex")).not.toBe(geometryToWKB(POINT).toString("hex"))
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test("geometryToEWKH: is the hex-encoded form of the EWKB buffer", () => {
|
|
120
|
+
expect(geometryToEWKH(POINT)).toBe(geometryToEWKB(POINT).toString("hex"))
|
|
121
|
+
expect(geometryToEWKH(POINT)).toBe("0101000020e61000000000000000003e400000000000002440")
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test("geometryToSQL: returns a thunk emitting a GeomFromEWKB literal for a real geometry", () => {
|
|
125
|
+
const thunk = geometryToSQL(POINT)
|
|
126
|
+
expect(typeof thunk).toBe("function")
|
|
127
|
+
expect(thunk()).toBe(`GeomFromEWKB('${geometryToEWKH(POINT)}')`)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test("geometryToSQL: null/undefined geometry yields a thunk that emits the SQL literal NULL", () => {
|
|
131
|
+
expect(geometryToSQL(null)()).toBe("NULL")
|
|
132
|
+
expect(geometryToSQL(undefined)()).toBe("NULL")
|
|
133
|
+
})
|
package/tsconfig.json
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
"emitDeclarationOnly": false,
|
|
8
8
|
"allowImportingTsExtensions": false
|
|
9
9
|
},
|
|
10
|
-
"exclude": ["./out/**/*"],
|
|
10
|
+
"exclude": ["./out/**/*", "./**/*.test.ts", "./**/*.test.tsx"],
|
|
11
11
|
"references": [
|
|
12
12
|
// ---
|
|
13
|
+
{ "path": "../annotations" },
|
|
13
14
|
{ "path": "../core" }
|
|
14
15
|
]
|
|
15
16
|
}
|