@mailwoman/spatial 4.9.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.
package/h3/index.ts ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ */
6
+
7
+ import { cellToLatLng } from "h3-js"
8
+ import type { Tagged } from "type-fest"
9
+ import { GeoPoint, type PointLiteral } from "../geometries/point.js"
10
+
11
+ /**
12
+ * A H3 cell index, full 64 bits.
13
+ *
14
+ * @type {string}
15
+ * @title H3 Cell Index
16
+ * @pattern ^[0-9a-f]{15}$
17
+ */
18
+ export type H3Cell = Tagged<string, "H3Cell">
19
+
20
+ export function isH3Cell(value: string): value is H3Cell {
21
+ return /^[0-9a-f]{15}$/.test(value)
22
+ }
23
+
24
+ /**
25
+ * A H3 cell index, shortened to 48 bits.
26
+ *
27
+ * @type {string}
28
+ * @title H3 Cell Index (Short)
29
+ * @pattern ^[0-9a-f]{12}$
30
+ */
31
+ export type H3CellShort = Tagged<string, "H3CellShort">
32
+
33
+ /**
34
+ * Given a full H3 cell index, shorten it to 48 bits.
35
+ */
36
+ export function shortenH3Cell(cell: H3Cell): H3CellShort {
37
+ // ...and convert it to a 48-bit cell address.
38
+ const cellBigInt = BigInt(`0x${cell}`)
39
+ // 8 f 2 aa 84 5a 18 ac 6b
40
+ // aa 84 5a 18 ac 6b
41
+
42
+ // Extract the cell address without the resolution
43
+ const h3CellShortBigInt = cellBigInt & 0xfffffffffffffn
44
+
45
+ const h3CellShortHex = h3CellShortBigInt.toString(16)
46
+
47
+ return h3CellShortHex as H3CellShort
48
+ }
49
+
50
+ //2 aa 84 5a 18 ac 6b
51
+
52
+ // 8 f2 aa 84 5a 18 ac 6b
53
+ /**
54
+ * Given a short cell address, expand it to a full H3 cell index.
55
+ */
56
+ export function expandH3Cell(h3CellShort: H3CellShort, resolution = 15): H3Cell {
57
+ // Convert the short cell address back to BigInt
58
+ const h3CellShortBigInt = BigInt(`0x${h3CellShort}`)
59
+
60
+ const resolutionHex = resolution.toString(16)
61
+ // Reassemble the H3 cell index portion...
62
+ const cellBigInt = h3CellShortBigInt << BigInt(8 * (15 - resolution))
63
+ // Back to a string...
64
+ const partialCell = cellBigInt.toString(16)
65
+ // Finally, we add the resolution back to the cell index.
66
+ const cell = `8${resolutionHex}${partialCell}`
67
+
68
+ return cell as H3Cell
69
+ }
70
+
71
+ /**
72
+ * Given a geographic point, return a short cell address.
73
+ */
74
+ export function shortCellToPoint(shortCell: H3CellShort, resolution = 15): GeoPoint {
75
+ const cell = expandH3Cell(shortCell, resolution)
76
+
77
+ // Convert the H3 cell index back to latitude and longitude
78
+ const [latitude, longitude] = cellToLatLng(cell)
79
+
80
+ return new GeoPoint([longitude, latitude])
81
+ }
82
+
83
+ export function cellToPointLiteral(cell: H3Cell): PointLiteral {
84
+ const [latitude, longitude] = cellToLatLng(cell)
85
+
86
+ return {
87
+ type: "Point",
88
+ coordinates: [longitude, latitude],
89
+ }
90
+ }
package/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ */
6
+
7
+ export * from "./bbox.js"
8
+ export * from "./countries/index.js"
9
+ export * from "./feature.js"
10
+ export * from "./geometries/index.js"
11
+ export * from "./google/index.js"
12
+ export * from "./h3/index.js"
13
+ export * from "./objects.js"
14
+ export * from "./position.js"
15
+ export * from "./projection.js"
16
+ export * from "./regions/index.js"
package/objects.ts ADDED
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ */
6
+
7
+ import { type BBox2DLiteral, type BBox3DLiteral, is2DBBox } from "./bbox.js"
8
+
9
+ /**
10
+ * GeoJSON object types.
11
+ */
12
+ export type GeometryType =
13
+ | "Point"
14
+ | "MultiPoint"
15
+ | "LineString"
16
+ | "MultiLineString"
17
+ | "Polygon"
18
+ | "MultiPolygon"
19
+ | "GeometryCollection"
20
+ | "FeatureCollection"
21
+
22
+ /**
23
+ * Shadow enum-like record of valid GeoJSON object types.
24
+ */
25
+ export const GeometryType = {
26
+ Point: "Point",
27
+ MultiPoint: "MultiPoint",
28
+ LineString: "LineString",
29
+ MultiLineString: "MultiLineString",
30
+ Polygon: "Polygon",
31
+ MultiPolygon: "MultiPolygon",
32
+ GeometryCollection: "GeometryCollection",
33
+ FeatureCollection: "FeatureCollection",
34
+ } as const satisfies { [K in GeometryType]: K }
35
+
36
+ /**
37
+ * The base GeoJSON object.
38
+ *
39
+ * The GeoJSON specification also allows foreign members
40
+ * (https://tools.ietf.org/html/rfc7946#section-6.1) to be included in the object.
41
+ *
42
+ * @see {@link https://tools.ietf.org/html/rfc7946#section-3 GeoJSON Object}
43
+ * @title Geo Object
44
+ * @public
45
+ */
46
+ export interface GeoObjectLiteral {
47
+ /**
48
+ * Specifies the type of GeoJSON object.
49
+ */
50
+ type: GeometryType
51
+
52
+ /**
53
+ * A unique identifier for the feature, such as a UUID, a serial number, or a name.
54
+ */
55
+ id?: string | number | undefined | null
56
+
57
+ /**
58
+ * A bounding box of the coordinate range of the object's Geometries, Features, or Feature
59
+ * Collections.
60
+ *
61
+ * This is useful when defining the extent of a GeoJSON object, i.e. the minimum and maximum
62
+ * coordinates of the object's Geometries, Features, or Feature Collections.
63
+ *
64
+ * @see {@link https://tools.ietf.org/html/rfc7946#section-5 GeoJSON Bounding Boxes}
65
+ */
66
+ bbox?: BBox2DLiteral | BBox3DLiteral
67
+
68
+ /**
69
+ * Coordinate reference system for GeoJSON objects.
70
+ *
71
+ * @see {@link https://tools.ietf.org/html/rfc7946#section-4 Coordinate Reference Systems}
72
+ * @title Coordinate Reference System
73
+ */
74
+ crs?: {
75
+ type: "name"
76
+
77
+ properties: {
78
+ /**
79
+ * The name of the coordinate reference system.
80
+ *
81
+ * @default "EPSG:4326"
82
+ */
83
+ name: string
84
+ }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Abstract base-class for all GeoJSON class constructors.
90
+ */
91
+ export abstract class GeoObject implements GeoObjectLiteral {
92
+ /**
93
+ * The JSON literal type of the GeoJSON object.
94
+ */
95
+ abstract type: GeometryType
96
+
97
+ /**
98
+ * A bounding box of the coordinate range of the object's Geometries, Features, or Feature.
99
+ *
100
+ * @see {@linkcode BBox2DLiteral} for 2-dimensional bounding boxes.
101
+ * @see {@linkcode BBox3DLiteral} for 3-dimensional bounding boxes.
102
+ * @see
103
+ */
104
+ bbox?: BBox2DLiteral | BBox3DLiteral
105
+
106
+ protected constructor(bbox?: BBox2DLiteral | BBox3DLiteral) {
107
+ this.bbox = bbox
108
+ }
109
+
110
+ /**
111
+ * Predicate to determine if the GeoJSON object is 2-dimensional.
112
+ */
113
+ public is2D() {
114
+ return is2DBBox(this.bbox)
115
+ }
116
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@mailwoman/spatial",
3
+ "version": "4.9.0",
4
+ "description": "Spatial analysis, geocoding, and other geo-related utilities.",
5
+ "license": "AGPL-3.0",
6
+ "contributors": [
7
+ {
8
+ "name": "Teffen Ellis",
9
+ "email": "teffen@sister.software"
10
+ }
11
+ ],
12
+ "type": "module",
13
+ "exports": {
14
+ "./package.json": "./package.json",
15
+ "./schema/*.json": "./dist/schema/*.json",
16
+ "./sdk": "./out/sdk/index.js",
17
+ ".": "./out/index.js"
18
+ },
19
+ "dependencies": {
20
+ "@mailwoman/core": "workspace:*",
21
+ "geo-coordinates-parser": "^1.7.4",
22
+ "h3-js": "^4.4.0",
23
+ "wkx": "^0.5.0"
24
+ },
25
+ "devDependencies": {
26
+ "@googlemaps/google-maps-services-js": "^3.4.2",
27
+ "@types/google.maps": "^3.65.1",
28
+ "type-fest": "^5.7.0"
29
+ },
30
+ "optionalDependencies": {
31
+ "@googlemaps/google-maps-services-js": "^3.4.2",
32
+ "@types/google.maps": "^3.65.1"
33
+ },
34
+ "engines": {
35
+ "node": ">=22.5.1"
36
+ },
37
+ "keywords": [
38
+ "geo",
39
+ "spatial"
40
+ ],
41
+ "publishConfig": {
42
+ "access": "public"
43
+ }
44
+ }
package/position.ts ADDED
@@ -0,0 +1,269 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * This file contains types and utilities for working with geographic positions.
7
+ */
8
+
9
+ import { type LatLngLiteral } from "@googlemaps/google-maps-services-js"
10
+ import { GeoPoint, type GeoPointInput } from "@mailwoman/spatial"
11
+
12
+ /**
13
+ * An ordered pair of coordinates in the form of [longitude, latitude].
14
+ *
15
+ * Note that unlike the typical order, GeoJSON coordinates are in the order of [longitude, latitude]
16
+ * to match the order of [x, y] in Cartesian coordinates.
17
+ *
18
+ * @category Position
19
+ * @category GeoJSON
20
+ * @see {@linkcode Coordinates3D} for 3D coordinates.
21
+ */
22
+ export type Coordinates2D = [
23
+ /**
24
+ * The longitude of the point, i.e. the x-coordinate.
25
+ *
26
+ * @minimum -180
27
+ * @maximum 180
28
+ */
29
+ longitude: number,
30
+ /**
31
+ * The latitude of the point, i.e. the y-coordinate.
32
+ *
33
+ * @minimum -90
34
+ * @maximum 90
35
+ */
36
+ latitude: number,
37
+ ]
38
+
39
+ /**
40
+ * Orders the given coordinates as [longitude, latitude].
41
+ *
42
+ * This is useful when converting into GeoJSON format.
43
+ *
44
+ * @category GeoJSON
45
+ * @category Position
46
+ */
47
+ export function orderCoordPairToGeoJSON([latitude, longitude]: [number, number]): Coordinates2D {
48
+ return [longitude, latitude]
49
+ }
50
+
51
+ /**
52
+ * Orders the given coordinates as [latitude, longitude].
53
+ *
54
+ * This is useful when converting into Google Maps format.
55
+ *
56
+ * @category GeoJSON
57
+ * @category Position
58
+ */
59
+ export function orderGeoJSONToCoordPair([longitude, latitude]: Coordinates2D): [number, number] {
60
+ return [latitude, longitude]
61
+ }
62
+
63
+ /**
64
+ * Given an input which appears to be reversed GeoJSON coordinates (i.e. [latitude, longitude]),
65
+ * returns the coordinates in the correct order of [longitude, latitude].
66
+ *
67
+ * Note that this is a heuristic and is only accurate for North American coordinates.
68
+ *
69
+ * @category GeoJSON
70
+ * @category Position
71
+ */
72
+ export function inferGeoJSONCoordOrder([coordA, coordB]: [number, number]): Coordinates2D {
73
+ // Latitude values typically range from -90 to 90
74
+ const isCoordALat = coordA >= -90 && coordA <= 90
75
+ const isCoordBLat = coordB >= -90 && coordB <= 90
76
+
77
+ if (isCoordALat && !isCoordBLat) {
78
+ // coordA is latitude, coordB is longitude
79
+ return [coordB, coordA]
80
+ }
81
+
82
+ if (!isCoordALat && isCoordBLat) {
83
+ // coordB is latitude, coordA is longitude
84
+ return [coordA, coordB]
85
+ }
86
+
87
+ // In case both appear to be latitudes (unlikely) or longitudes (out of range for US),
88
+ // assume coordA is longitude and coordB is latitude as default.
89
+ return [coordA, coordB]
90
+ }
91
+
92
+ /**
93
+ * An ordered triple of coordinates in the form of [longitude, latitude, altitude].
94
+ *
95
+ * @category Position
96
+ * @category GeoJSON
97
+ * @see {@linkcode Coordinates2D} for 2D coordinates.
98
+ */
99
+ export type Coordinates3D = [
100
+ /**
101
+ * The longitude of the point, i.e. the x-coordinate.
102
+ *
103
+ * @minimum -180
104
+ * @maximum 180
105
+ */
106
+ longitude: number,
107
+ /**
108
+ * The latitude of the point, i.e. the y-coordinate.
109
+ *
110
+ * @minimum -90
111
+ * @maximum 90
112
+ */
113
+ latitude: number,
114
+ /**
115
+ * The altitude of the point, i.e. the z-coordinate.
116
+ */
117
+ altitude: number,
118
+ ]
119
+
120
+ /**
121
+ * A record of internal coordinates, typically used by the US Census.
122
+ */
123
+ export type InternalPointCoordinates = {
124
+ /**
125
+ * Internal Longitude (X) Coordinates
126
+ *
127
+ * @minimum -180
128
+ * @maximum 180
129
+ */
130
+ x: number
131
+ /**
132
+ * Internal Latitude (Y) Coordinates
133
+ *
134
+ * @minimum -90
135
+ * @maximum 90
136
+ */
137
+ y: number
138
+ }
139
+
140
+ /**
141
+ * Type-predicate to determine if the given input is a GeoJSON Point geometry.
142
+ *
143
+ * @category Type Predicates
144
+ * @category GeoJSON
145
+ */
146
+ export function isCoordPairLiteral(input: unknown): input is [number, number] | [number, number, number] {
147
+ if (!Array.isArray(input)) return false
148
+
149
+ if (input.length !== 2 && input.length !== 3) return false
150
+
151
+ return input.every((coord) => typeof coord === "number")
152
+ }
153
+
154
+ /**
155
+ * Type-predicate to determine if the given input is a {@linkcode LatLngLiteral} object.
156
+ *
157
+ * @category Position
158
+ * @category Type Predicates
159
+ * @see {@link https://developers.google.com/maps/documentation/javascript/reference/coordinates#LatLngLiteral Google Maps API Documentation}
160
+ */
161
+ export function isGoogleMapsLatLngLiteral(input: unknown): input is LatLngLiteral {
162
+ if (!input || typeof input !== "object") return false
163
+
164
+ if (!Object.hasOwn(input, "lat") || !Object.hasOwn(input, "lng")) return false
165
+
166
+ return true
167
+ }
168
+
169
+ /**
170
+ * Type-predicate to determine if the given input is a {@linkcode InternalPointCoordinates} object.
171
+ *
172
+ * @category Position
173
+ * @category Type Predicates
174
+ */
175
+ export function isInterpolatedCoordinates(input: unknown): input is InternalPointCoordinates {
176
+ if (!input || typeof input !== "object") return false
177
+
178
+ if (!("x" in input)) return false
179
+ if (!("y" in input)) return false
180
+
181
+ return typeof input.x === "number" && typeof input.y === "number"
182
+ }
183
+
184
+ /**
185
+ * Given a longitude value, wraps it to the range of [-180, 180].
186
+ *
187
+ * This is useful when normalizing longitude values.
188
+ *
189
+ * @category Position
190
+ * @param longitude The longitude value to wrap.
191
+ */
192
+ export function wrapLongitude(longitude: number): number {
193
+ return ((((longitude + 180) % 360) + 360) % 360) - 180
194
+ }
195
+
196
+ /**
197
+ * Given a latitude value, clamps it to the range of [-90, 90].
198
+ *
199
+ * This is useful when normalizing latitude values.
200
+ *
201
+ * @category Position
202
+ * @param value The latitude value to clamp.
203
+ */
204
+ export function clampLatitude(value: number): number {
205
+ return Math.min(90, Math.max(-90, value))
206
+ }
207
+
208
+ /**
209
+ * Conversion factors for converting between degrees and radians.
210
+ *
211
+ * @category Position
212
+ * @see {@link https://en.wikipedia.org/wiki/Radian Wikipedia: Radian}
213
+ * @see {@link https://en.wikipedia.org/wiki/Degree_(angle) Wikipedia: Degree (angle)}
214
+ */
215
+ export const ConversionFactor = {
216
+ DegreesToRadians: (Math.PI / 180) as unknown as 0.01745329251,
217
+ RadiansToDegrees: (180 / Math.PI) as unknown as 57.2957795131,
218
+ } as const
219
+
220
+ /**
221
+ * Available conversion units for the radius of the Earth.
222
+ */
223
+ export type EarthRadiusUnit = "km" | "miles" | "meters"
224
+
225
+ /**
226
+ * Radius of the Earth in various units
227
+ */
228
+ const RADII = {
229
+ km: 6371,
230
+ miles: 3958.8,
231
+ meters: 6371000,
232
+ } as const satisfies Record<EarthRadiusUnit, number>
233
+
234
+ /**
235
+ * Calculate the distance between two points on the Earth's surface.
236
+ *
237
+ * @category Position
238
+ * @param point1 The first point to calculate the distance from.
239
+ * @param point2 The second point to calculate the distance to.
240
+ * @param unit The unit of measurement to return the distance in.
241
+ *
242
+ * @returns The distance between the two points in the specified unit.
243
+ */
244
+ export function haversine(point1: GeoPointInput, point2: GeoPointInput, unit: EarthRadiusUnit = "km"): number {
245
+ const p1 = GeoPoint.from(point1)
246
+ const p2 = GeoPoint.from(point2)
247
+
248
+ if (!p1 || !p2) return NaN
249
+
250
+ const lat1 = p1.latitude
251
+ const lon1 = p1.longitude
252
+ const lat2 = p2.latitude
253
+ const lon2 = p2.longitude
254
+
255
+ const dLat = (lat2 - lat1) * ConversionFactor.DegreesToRadians
256
+
257
+ const dLon = (lon2 - lon1) * ConversionFactor.DegreesToRadians
258
+
259
+ const a =
260
+ Math.pow(Math.sin(dLat / 2), 2) +
261
+ Math.cos(lat1 * ConversionFactor.DegreesToRadians) *
262
+ Math.cos(lat2 * ConversionFactor.DegreesToRadians) *
263
+ Math.pow(Math.sin(dLon / 2), 2)
264
+
265
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
266
+ const radius = RADII[unit]
267
+
268
+ return radius * c
269
+ }
package/projection.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ */
6
+
7
+ /**
8
+ * A coordinate system used to represent the Earth's surface.
9
+ *
10
+ * @category Geo
11
+ */
12
+ export enum CoordinateProjection {
13
+ /**
14
+ * Coordinate system used in Google Earth and GSP systems.
15
+ *
16
+ * It represents Earth as a three-dimensional ellipsoid.
17
+ */
18
+ WGS84 = "4326",
19
+ /**
20
+ * North American Datum 1983, a geodetic reference system used in the TIGER/Line data.
21
+ */
22
+ NAD83 = "4269",
23
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ */
6
+
7
+ import type { RegionName } from "./names.js"
8
+
9
+ /**
10
+ * M.49 region codes for continents.
11
+ */
12
+ export const RegionCodes = [
13
+ // ---
14
+ "AF",
15
+ "AN",
16
+ "AS",
17
+ "EU",
18
+ "NA",
19
+ "OC",
20
+ "SA",
21
+ ] as const satisfies readonly string[]
22
+
23
+ /**
24
+ * M.49 region code for a specific continent.
25
+ *
26
+ * @public
27
+ */
28
+ export type RegionCode = (typeof RegionCodes)[number]
29
+
30
+ /**
31
+ * Continent codes to their full names.
32
+ *
33
+ * @internal
34
+ */
35
+ export const RegionCodeToNameRecord = {
36
+ AF: "Africa",
37
+ AN: "Antarctica",
38
+ AS: "Asia",
39
+ EU: "Europe",
40
+ NA: "North America",
41
+ OC: "Oceania",
42
+ SA: "South America",
43
+ } as const satisfies Record<RegionCode, RegionName>
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ */
6
+
7
+ export * from "./codes.js"
8
+ export * from "./names.js"
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ */
6
+
7
+ /**
8
+ * A list of geographic regions, i.e. continents.
9
+ *
10
+ * @category Geographic
11
+ */
12
+ export const RegionNames = [
13
+ "Africa",
14
+ "Antarctica",
15
+ "Asia",
16
+ "Europe",
17
+ "North America",
18
+ "Oceania",
19
+ "South America",
20
+ ] as const satisfies readonly string[]
21
+
22
+ /**
23
+ * A region of the world, i.e. a continent.
24
+ *
25
+ * @category Geographic
26
+ * @title Geographic Region
27
+ * @public
28
+ */
29
+ export type RegionName = (typeof RegionNames)[number]
package/sdk/index.ts ADDED
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ */
6
+
7
+ export * from "./well-known-text.js"