@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/README.md +3 -0
- package/bbox.ts +445 -0
- package/countries/codes.ts +531 -0
- package/countries/index.ts +8 -0
- package/countries/names.ts +274 -0
- package/feature.ts +90 -0
- package/geometries/collection.ts +37 -0
- package/geometries/index.ts +12 -0
- package/geometries/line-string.ts +47 -0
- package/geometries/point.ts +419 -0
- package/geometries/polygon.ts +237 -0
- package/google/index.ts +7 -0
- package/google/place-id.ts +44 -0
- package/h3/index.ts +90 -0
- package/index.ts +16 -0
- package/objects.ts +116 -0
- package/package.json +44 -0
- package/position.ts +269 -0
- package/projection.ts +23 -0
- package/regions/codes.ts +43 -0
- package/regions/index.ts +8 -0
- package/regions/names.ts +29 -0
- package/sdk/index.ts +7 -0
- package/sdk/well-known-text.ts +86 -0
- package/tsconfig.json +15 -0
- package/typedoc.json +4 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software.
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type google from "@googlemaps/google-maps-services-js"
|
|
8
|
+
import type { LatLng, LatLngLiteral } from "@googlemaps/google-maps-services-js"
|
|
9
|
+
import { tryParsingJSON } from "@mailwoman/core/objects"
|
|
10
|
+
import { convert as convertCoords } from "geo-coordinates-parser"
|
|
11
|
+
import { latLngToCell } from "h3-js"
|
|
12
|
+
import { type BBox2DLiteral, type BBox3DLiteral, GeoBoundingBox, isBBox } from "../bbox.js"
|
|
13
|
+
import { type H3Cell, shortenH3Cell } from "../h3/index.js"
|
|
14
|
+
import { type GeoObjectLiteral, GeometryType } from "../objects.js"
|
|
15
|
+
import {
|
|
16
|
+
type InternalPointCoordinates,
|
|
17
|
+
type Coordinates2D as Point2DCoordinates,
|
|
18
|
+
type Coordinates3D as Point3DCoordinates,
|
|
19
|
+
clampLatitude,
|
|
20
|
+
inferGeoJSONCoordOrder,
|
|
21
|
+
isCoordPairLiteral,
|
|
22
|
+
isGoogleMapsLatLngLiteral,
|
|
23
|
+
isInterpolatedCoordinates,
|
|
24
|
+
wrapLongitude,
|
|
25
|
+
} from "../position.js"
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A JSON-serializeable single point geometry, such as a specific location, address, or longitude,
|
|
29
|
+
* latitude pair.
|
|
30
|
+
*
|
|
31
|
+
* ```js
|
|
32
|
+
* {
|
|
33
|
+
* "type": "Point",
|
|
34
|
+
* "coordinates": [100, 0]
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @title Point Geometry
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
41
|
+
export interface PointLiteral extends GeoObjectLiteral {
|
|
42
|
+
/**
|
|
43
|
+
* Declares the type of GeoJSON object as a `Point` geometry.
|
|
44
|
+
*/
|
|
45
|
+
type: "Point"
|
|
46
|
+
/**
|
|
47
|
+
* A pair of coordinates in the form of [longitude, latitude].
|
|
48
|
+
*
|
|
49
|
+
* @see {@linkcode Point2DCoordinates} for more information.
|
|
50
|
+
*/
|
|
51
|
+
coordinates: Point2DCoordinates | Point3DCoordinates
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Type-predicate to determine if the given input is a GeoJSON Point geometry.
|
|
56
|
+
*/
|
|
57
|
+
export function isPointLiteral(input: PointLiteral | null | undefined | unknown): input is PointLiteral {
|
|
58
|
+
if (!input || typeof input !== "object") return false
|
|
59
|
+
|
|
60
|
+
if (!("type" in input)) return false
|
|
61
|
+
if (!("coordinates" in input)) return false
|
|
62
|
+
if (input.type !== GeometryType.Point) return false
|
|
63
|
+
|
|
64
|
+
return isCoordPairLiteral(input.coordinates)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Common interface for Browser Geolocation API coordinates.
|
|
69
|
+
*
|
|
70
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GeolocationCoordinates MDN Web Docs}
|
|
71
|
+
*/
|
|
72
|
+
export interface GeolocationCoordinatesLike {
|
|
73
|
+
latitude: number
|
|
74
|
+
longitude: number
|
|
75
|
+
altitude: number
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Type-predicate to determine if the given input appears to be a {@linkcode GeolocationCoordinates}
|
|
80
|
+
* object.
|
|
81
|
+
*/
|
|
82
|
+
export function isGeolocationCoordinatesLike(input: unknown): input is GeolocationCoordinatesLike {
|
|
83
|
+
if (!input || typeof input !== "object") return false
|
|
84
|
+
|
|
85
|
+
if (!("latitude" in input)) return false
|
|
86
|
+
if (!("longitude" in input)) return false
|
|
87
|
+
if (!("altitude" in input)) return false
|
|
88
|
+
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type GeoPointInput =
|
|
93
|
+
| PointLiteral
|
|
94
|
+
| Point2DCoordinates
|
|
95
|
+
| Point3DCoordinates
|
|
96
|
+
| GeolocationCoordinatesLike
|
|
97
|
+
| InternalPointCoordinates
|
|
98
|
+
| LatLngLiteral
|
|
99
|
+
| LatLng
|
|
100
|
+
|
|
101
|
+
//#region GeoPoint
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* A single point geometry, such as a specific location, address, or longitude, latitude pair.
|
|
105
|
+
*/
|
|
106
|
+
export class GeoPoint implements PointLiteral {
|
|
107
|
+
//#region Properties
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Declares the type of GeoJSON object as a `Point` geometry.
|
|
111
|
+
*/
|
|
112
|
+
readonly type = GeometryType.Point
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The bounding box literal of the GeoPoint.
|
|
116
|
+
*
|
|
117
|
+
* @see {@linkcode GeoPoint.bbox} for the actual bounding box object.
|
|
118
|
+
* @see {@linkcode GeoBoundingBox} for creating a bounding box.
|
|
119
|
+
*/
|
|
120
|
+
public boundingBox: GeoBoundingBox | null = null
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The bounding box literal of the GeoPoint.
|
|
124
|
+
*
|
|
125
|
+
* @see {@linkcode BBox2DLiteral} for 2D bounding boxes.
|
|
126
|
+
* @see {@linkcode BBox3DLiteral} for 3D bounding boxes.
|
|
127
|
+
* @see {@linkcode GeoBoundingBox} for creating a bounding box.
|
|
128
|
+
*/
|
|
129
|
+
public get bbox(): BBox2DLiteral | BBox3DLiteral | undefined {
|
|
130
|
+
return this.boundingBox?.toJSON()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public set bbox(bbox: BBox2DLiteral | BBox3DLiteral | undefined) {
|
|
134
|
+
this.boundingBox = bbox ? new GeoBoundingBox(bbox) : null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public get coordinates(): Point2DCoordinates | Point3DCoordinates {
|
|
138
|
+
if (this.is3D()) {
|
|
139
|
+
return [this.#longitude, this.#latitude, this.#altitude]
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return [this.#longitude, this.#latitude]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public set coordinates(coords: Point2DCoordinates | Point3DCoordinates) {
|
|
146
|
+
const [longitude, latitude, altitude] = coords
|
|
147
|
+
|
|
148
|
+
this.#longitude = longitude
|
|
149
|
+
this.#latitude = latitude
|
|
150
|
+
this.#altitude = typeof altitude === "number" ? altitude : 0
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#latitude: number = 0
|
|
154
|
+
#longitude: number = 0
|
|
155
|
+
#altitude: number = 0
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The longitude of the point in degrees, i.e. the x-coordinate.
|
|
159
|
+
*
|
|
160
|
+
* Values outside the range will be wrapped around to the opposite side of the globe.
|
|
161
|
+
*
|
|
162
|
+
* @minimum -180
|
|
163
|
+
* @maximum 180
|
|
164
|
+
*/
|
|
165
|
+
public get longitude(): number {
|
|
166
|
+
return this.#longitude
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
public set longitude(value: number) {
|
|
170
|
+
this.#longitude = wrapLongitude(value)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The latitude of the point in degrees, i.e. the y-coordinate.
|
|
175
|
+
*
|
|
176
|
+
* Values outside the range will be clamped to the poles.
|
|
177
|
+
*
|
|
178
|
+
* @minimum -90
|
|
179
|
+
* @maximum 90
|
|
180
|
+
*/
|
|
181
|
+
public get latitude(): number {
|
|
182
|
+
return this.#latitude
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public set latitude(value: number) {
|
|
186
|
+
this.#latitude = clampLatitude(value)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The altitude of the point, i.e. the z-coordinate.
|
|
191
|
+
*
|
|
192
|
+
* This is optional and is typically measured in meters.
|
|
193
|
+
*/
|
|
194
|
+
public get altitude(): number {
|
|
195
|
+
return this.#altitude
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
public set altitude(value: number) {
|
|
199
|
+
this.#altitude = value
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
//#endregion
|
|
203
|
+
|
|
204
|
+
//#region Constructors
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Create a new GeoPoint object with default coordinates.
|
|
208
|
+
*/
|
|
209
|
+
constructor()
|
|
210
|
+
/**
|
|
211
|
+
* Create a new GeoPoint instance from another {@linkcode GeoJSONPosition} coordinates.
|
|
212
|
+
*/
|
|
213
|
+
constructor(
|
|
214
|
+
geoJSONPosition: Point2DCoordinates | Point3DCoordinates,
|
|
215
|
+
bbox?: BBox2DLiteral | BBox3DLiteral | GeoBoundingBox
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Create a new GeoPoint instance from the browser's Geolocation API coordinates.
|
|
220
|
+
*
|
|
221
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/GeolocationCoordinates MDN Web Docs}
|
|
222
|
+
*/
|
|
223
|
+
constructor(geoLocationCoordinates: GeolocationCoordinatesLike, bbox?: BBox2DLiteral | BBox3DLiteral | GeoBoundingBox)
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Creates a new GeoPoint instance from a Google Maps API
|
|
227
|
+
* {@linkcode google.LatLngLiteral | LatLngLiteral} object.
|
|
228
|
+
*/
|
|
229
|
+
constructor(latLngLiteral: google.LatLngLiteral, bbox?: BBox2DLiteral | BBox3DLiteral | GeoBoundingBox)
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Creates a new GeoPoint instance from internal coordinates.
|
|
233
|
+
*/
|
|
234
|
+
constructor(interpolatedCoords: InternalPointCoordinates, bbox?: BBox2DLiteral | BBox3DLiteral | GeoBoundingBox)
|
|
235
|
+
/**
|
|
236
|
+
* Creates a new GeoPoint instance from a Google Maps API {@linkcode google.LatLng | LatLng}
|
|
237
|
+
* object.
|
|
238
|
+
*/
|
|
239
|
+
constructor(latLng: LatLngLiteral, bbox?: BBox2DLiteral | BBox3DLiteral | GeoBoundingBox)
|
|
240
|
+
/**
|
|
241
|
+
* Create a new GeoPoint instance from another {@linkcode PointLiteral}.
|
|
242
|
+
*/
|
|
243
|
+
constructor(geoPointJSON: PointLiteral, bbox?: BBox2DLiteral | BBox3DLiteral | GeoBoundingBox)
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Create a new GeoPoint instance.
|
|
247
|
+
*/
|
|
248
|
+
constructor(input: GeoPointInput, bbox?: BBox2DLiteral | BBox3DLiteral | GeoBoundingBox)
|
|
249
|
+
constructor(input?: GeoPointInput, bbox?: BBox2DLiteral | BBox3DLiteral | GeoBoundingBox) {
|
|
250
|
+
if (isCoordPairLiteral(input)) {
|
|
251
|
+
if (input.length === 2) {
|
|
252
|
+
this.coordinates = inferGeoJSONCoordOrder(input)
|
|
253
|
+
} else {
|
|
254
|
+
this.coordinates = input
|
|
255
|
+
}
|
|
256
|
+
} else if (isPointLiteral(input)) {
|
|
257
|
+
this.coordinates = [...input.coordinates]
|
|
258
|
+
|
|
259
|
+
if (isBBox(input.bbox)) {
|
|
260
|
+
this.bbox = [...input.bbox]
|
|
261
|
+
}
|
|
262
|
+
} else if (isGoogleMapsLatLngLiteral(input)) {
|
|
263
|
+
this.coordinates = [input.lng, input.lat]
|
|
264
|
+
} else if (isGeolocationCoordinatesLike(input)) {
|
|
265
|
+
this.#longitude = input.longitude
|
|
266
|
+
this.#latitude = input.latitude
|
|
267
|
+
this.#altitude = input.altitude || 0
|
|
268
|
+
} else if (isInterpolatedCoordinates(input)) {
|
|
269
|
+
this.#longitude = input.x
|
|
270
|
+
this.#latitude = input.y
|
|
271
|
+
this.#altitude = 0
|
|
272
|
+
} else {
|
|
273
|
+
this.coordinates = [0, 0]
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (isBBox(bbox)) {
|
|
277
|
+
this.bbox = bbox
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Attempts to create a new GeoPoint instance from the given input.
|
|
283
|
+
*/
|
|
284
|
+
static from(input: unknown): GeoPoint | null {
|
|
285
|
+
if (!input) return null
|
|
286
|
+
if (input instanceof GeoPoint) return input
|
|
287
|
+
|
|
288
|
+
if (typeof input === "string") {
|
|
289
|
+
const coordinates = tryParsingJSON<GeoPointInput>(input) || tryParsingJSON<GeoPointInput>(`[${input}]`)
|
|
290
|
+
|
|
291
|
+
if (coordinates) {
|
|
292
|
+
input = coordinates
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const point = new GeoPoint(input as GeoPointInput)
|
|
298
|
+
if (point.isNullIsland()) return null
|
|
299
|
+
|
|
300
|
+
return point
|
|
301
|
+
} catch (_error) {
|
|
302
|
+
return null
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
//#endregion
|
|
307
|
+
|
|
308
|
+
//#region Predicates
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Whether the GeoPoint is 2-dimensional.
|
|
312
|
+
*/
|
|
313
|
+
public is2D() {
|
|
314
|
+
return !this.is3D()
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Whether the GeoPoint is 3-dimensional.
|
|
319
|
+
*/
|
|
320
|
+
public is3D() {
|
|
321
|
+
return this.#altitude !== 0
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Whether the GeoPoint is the null island at 0, 0.
|
|
326
|
+
*/
|
|
327
|
+
public isNullIsland(): boolean {
|
|
328
|
+
return this.#latitude === 0 && this.#longitude === 0
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
//#endregion
|
|
332
|
+
|
|
333
|
+
//#region Conversion
|
|
334
|
+
|
|
335
|
+
public toJSON(): PointLiteral {
|
|
336
|
+
return {
|
|
337
|
+
type: this.type,
|
|
338
|
+
coordinates: this.coordinates,
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
public to2DCoordinates(): Point2DCoordinates {
|
|
343
|
+
return [this.#longitude, this.#latitude]
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
public to3DCoordinates(): Point3DCoordinates {
|
|
347
|
+
return [this.#longitude, this.#latitude, this.#altitude]
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Converts the GeoPoint to a Google Maps API {@linkcode google.LatLngLiteral | LatLngLiteral}
|
|
352
|
+
* object.
|
|
353
|
+
*/
|
|
354
|
+
public toGoogleLatLngLiteral(): google.LatLngLiteral {
|
|
355
|
+
return {
|
|
356
|
+
lat: this.#latitude,
|
|
357
|
+
lng: this.#longitude,
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Converts the GeoPoint to DMS (Degrees, Minutes, Seconds) format.
|
|
363
|
+
*/
|
|
364
|
+
public toDMS(): string {
|
|
365
|
+
const converter = convertCoords(`${this.#latitude},${this.#longitude}`)
|
|
366
|
+
|
|
367
|
+
return converter.toCoordinateFormat("DMS")
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Converts the GeoPoint to a H3 short cell address.
|
|
372
|
+
*/
|
|
373
|
+
public toH3Cell(resolution = 15) {
|
|
374
|
+
const cell = latLngToCell(this.#latitude, this.#longitude, resolution) as H3Cell
|
|
375
|
+
|
|
376
|
+
return cell
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Converts the GeoPoint to a H3 short cell address.
|
|
381
|
+
*/
|
|
382
|
+
public toH3ShortCell(resolution = 15) {
|
|
383
|
+
const cell = this.toH3Cell(resolution)
|
|
384
|
+
|
|
385
|
+
return shortenH3Cell(cell)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
public toString(): string {
|
|
389
|
+
return JSON.stringify(this.toJSON())
|
|
390
|
+
}
|
|
391
|
+
//#endregion
|
|
392
|
+
|
|
393
|
+
public [Symbol.iterator](): Iterator<number> {
|
|
394
|
+
return this.coordinates[Symbol.iterator]()
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* An array of positions for each point in the geometry.
|
|
400
|
+
*
|
|
401
|
+
* @see {@linkcode GeoJSONPosition} for more information.
|
|
402
|
+
*/
|
|
403
|
+
export type MultiPointPath = [...points: Array<Point2DCoordinates | Point3DCoordinates>]
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* A collection of points, such as a constellation or a set of locations.
|
|
407
|
+
*/
|
|
408
|
+
export interface MultiPointLiteral extends GeoObjectLiteral {
|
|
409
|
+
/**
|
|
410
|
+
* Declares the type of GeoJSON object as a `MultiPoint` geometry.
|
|
411
|
+
*/
|
|
412
|
+
type: "MultiPoint"
|
|
413
|
+
/**
|
|
414
|
+
* An array of positions for each point in the geometry.
|
|
415
|
+
*
|
|
416
|
+
* @see {@linkcode GeoJSONPosition} for more information.
|
|
417
|
+
*/
|
|
418
|
+
coordinates: MultiPointPath
|
|
419
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software.
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ResourceError } from "@mailwoman/core/errors"
|
|
8
|
+
import type { GeoObjectLiteral } from "../objects.js"
|
|
9
|
+
import type { LineStringPath } from "./line-string.js"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* An array of positions forming a closed shape, such as a country or a lake.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
*
|
|
16
|
+
* A polygon without holes:
|
|
17
|
+
*
|
|
18
|
+
* ```json
|
|
19
|
+
* {
|
|
20
|
+
* "type": "Polygon",
|
|
21
|
+
* "coordinates": [
|
|
22
|
+
* [
|
|
23
|
+
* [100, 0],
|
|
24
|
+
* [101, 0],
|
|
25
|
+
* [101, 1],
|
|
26
|
+
* [100, 1],
|
|
27
|
+
* [100, 0]
|
|
28
|
+
* ]
|
|
29
|
+
* ]
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export type SolidPolygonPath = [
|
|
34
|
+
/**
|
|
35
|
+
* - A linear ring is a closed LineString with four or more positions.
|
|
36
|
+
* - The first and last positions are equivalent (they represent equivalent points).
|
|
37
|
+
*/
|
|
38
|
+
exteriorRing: LineStringPath,
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* An array of positions forming a closed shape with holes, such as a country with islands or a lake
|
|
43
|
+
* with islands.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
*
|
|
47
|
+
* A polygon with holes:
|
|
48
|
+
*
|
|
49
|
+
* ```json
|
|
50
|
+
* {
|
|
51
|
+
* "type": "Polygon",
|
|
52
|
+
* "coordinates": [
|
|
53
|
+
* [
|
|
54
|
+
* [100.0, 0.0],
|
|
55
|
+
* [101.0, 0.0],
|
|
56
|
+
* [101.0, 1.0],
|
|
57
|
+
* [100.0, 1.0],
|
|
58
|
+
* [100.0, 0.0]
|
|
59
|
+
* ],
|
|
60
|
+
* [
|
|
61
|
+
* [100.8, 0.8],
|
|
62
|
+
* [100.8, 0.2],
|
|
63
|
+
* [100.2, 0.2],
|
|
64
|
+
* [100.2, 0.8],
|
|
65
|
+
* [100.8, 0.8]
|
|
66
|
+
* ]
|
|
67
|
+
* ]
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export type NestedPolygonPath = [
|
|
72
|
+
/**
|
|
73
|
+
* - A linear ring is a closed LineString with four or more positions.
|
|
74
|
+
* - The first and last positions are equivalent (they represent equivalent points).
|
|
75
|
+
*/
|
|
76
|
+
exteriorRing: LineStringPath,
|
|
77
|
+
/**
|
|
78
|
+
* - The interior rings are arrays of positions forming holes in the polygon.
|
|
79
|
+
*/
|
|
80
|
+
...interiorRings: LineStringPath[],
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A polygon geometry.
|
|
85
|
+
*
|
|
86
|
+
* @see {@linkcode PolygonLiteral} for applicable JSON schema.
|
|
87
|
+
* @see {@linkcode SolidPolygonPath} for more information.
|
|
88
|
+
* @see {@linkcode NestedPolygonPath} for more information.
|
|
89
|
+
*/
|
|
90
|
+
export type PolygonPath = SolidPolygonPath | NestedPolygonPath
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* An array of positions forming a closed shape, such as a country or a lake.
|
|
94
|
+
*/
|
|
95
|
+
export interface PolygonLiteral<P extends PolygonPath = SolidPolygonPath> extends GeoObjectLiteral {
|
|
96
|
+
/**
|
|
97
|
+
* Declares the type of GeoJSON object as a `Polygon` geometry.
|
|
98
|
+
*/
|
|
99
|
+
type: "Polygon"
|
|
100
|
+
/**
|
|
101
|
+
* An array of positions for each point in the geometry.
|
|
102
|
+
*
|
|
103
|
+
* @see {@link https://datatracker.ietf.org/doc/html/rfc7946#section-3.1.6 | RFC 7946 Section 3.1.6}
|
|
104
|
+
* @see {@linkcode SolidPolygonPath}
|
|
105
|
+
* @see {@linkcode NestedPolygonPath}
|
|
106
|
+
*/
|
|
107
|
+
coordinates: P
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Predicate for checking if a GeoJSON object is a `Polygon` geometry.
|
|
112
|
+
*/
|
|
113
|
+
export function isPolygonLiteral<P extends PolygonPath = PolygonPath>(input: unknown): input is PolygonLiteral<P> {
|
|
114
|
+
if (typeof input !== "object" || input === null) return false
|
|
115
|
+
|
|
116
|
+
return "type" in input && input.type === "Polygon" && "coordinates" in input && Array.isArray(input.coordinates)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Predicate for checking if a polygon geometry is a solid, i.e. it has no holes.
|
|
121
|
+
*/
|
|
122
|
+
export function isSolidPolygonPath(input: PolygonLiteral): boolean {
|
|
123
|
+
return input.coordinates.length === 1
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A collection of polygons, such as a country with islands or a lake with islands.
|
|
128
|
+
*/
|
|
129
|
+
export interface MultiPolygonLiteral<P extends PolygonPath = SolidPolygonPath> extends GeoObjectLiteral {
|
|
130
|
+
type: "MultiPolygon"
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* An array of polygons.
|
|
134
|
+
*/
|
|
135
|
+
coordinates: P[][]
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Predicate for checking if a GeoJSON object is a `MultiPolygon` geometry.
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Given a polygon geometry, return an OSM filter string.
|
|
143
|
+
*
|
|
144
|
+
* This is useful when working with the Overpass API.
|
|
145
|
+
*/
|
|
146
|
+
export function polygonToOSMFilter(input: PolygonLiteral): string {
|
|
147
|
+
if (!isPolygonLiteral(input)) return ""
|
|
148
|
+
|
|
149
|
+
const [exteriorRing] = input.coordinates
|
|
150
|
+
|
|
151
|
+
const filter = exteriorRing.map(([lon, lat]) => `${lat} ${lon}`).join(" ")
|
|
152
|
+
|
|
153
|
+
return `poly:'${filter}'`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Tags returned by the Overpass API for a node.
|
|
158
|
+
*
|
|
159
|
+
* @category OSM
|
|
160
|
+
*/
|
|
161
|
+
export enum OSMNodeTag {
|
|
162
|
+
HouseNumber = "addr:housenumber",
|
|
163
|
+
PostCode = "addr:postcode",
|
|
164
|
+
Street = "addr:street",
|
|
165
|
+
State = "addr:state",
|
|
166
|
+
City = "addr:city",
|
|
167
|
+
Website = "website",
|
|
168
|
+
Email = "email",
|
|
169
|
+
Phone = "phone",
|
|
170
|
+
Shop = "shop",
|
|
171
|
+
Brand = "brand",
|
|
172
|
+
Cuisine = "cuisine",
|
|
173
|
+
Name = "name",
|
|
174
|
+
Healthcare = "healthcare",
|
|
175
|
+
Office = "office",
|
|
176
|
+
Amenity = "amenity",
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const ForbiddenResidentialOSMNodeTags: ReadonlySet<OSMNodeTag> = new Set<OSMNodeTag>([
|
|
180
|
+
OSMNodeTag.Shop,
|
|
181
|
+
OSMNodeTag.Brand,
|
|
182
|
+
OSMNodeTag.Cuisine,
|
|
183
|
+
OSMNodeTag.Office,
|
|
184
|
+
OSMNodeTag.Healthcare,
|
|
185
|
+
])
|
|
186
|
+
|
|
187
|
+
export type OSMNodeTagRecord = Record<OSMNodeTag, string | undefined>
|
|
188
|
+
|
|
189
|
+
export interface OSMOverpassElement {
|
|
190
|
+
type: "node"
|
|
191
|
+
id: number
|
|
192
|
+
lat: number
|
|
193
|
+
lon: number
|
|
194
|
+
tags: OSMNodeTagRecord
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface OSMOverpassResponseBody {
|
|
198
|
+
version: string
|
|
199
|
+
generator: string
|
|
200
|
+
osm3s: {
|
|
201
|
+
timestamp_osm_base: string
|
|
202
|
+
copyright: string
|
|
203
|
+
}
|
|
204
|
+
elements: OSMOverpassElement[]
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Given an OSM element, attempts to infer if the result is a residential address.
|
|
209
|
+
*/
|
|
210
|
+
export function isResidentialElement(element: OSMOverpassElement): boolean {
|
|
211
|
+
for (const key in element.tags) {
|
|
212
|
+
if (ForbiddenResidentialOSMNodeTags.has(key as OSMNodeTag)) return false
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (element.tags[OSMNodeTag.Amenity] === "restaurant") return false
|
|
216
|
+
|
|
217
|
+
return true
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function fetchOSMElementViaOverpassAPI(input: PolygonLiteral): Promise<OSMOverpassElement[]> {
|
|
221
|
+
const filter = polygonToOSMFilter(input)
|
|
222
|
+
|
|
223
|
+
const url = new URL("http://overpass-api.de/api/interpreter")
|
|
224
|
+
url.searchParams.set("data", `[out:json];(node['addr:housenumber'](${filter}););out body;>;out skel qt;`)
|
|
225
|
+
|
|
226
|
+
return fetch(url)
|
|
227
|
+
.then((response) => {
|
|
228
|
+
if (!response.ok) throw ResourceError.from(response.status, response.statusText, "osm", "overpass-api", "fetch")
|
|
229
|
+
|
|
230
|
+
return response
|
|
231
|
+
})
|
|
232
|
+
.then((response) => response.json() as Promise<OSMOverpassResponseBody>)
|
|
233
|
+
.then((body) => body.elements)
|
|
234
|
+
.catch((error) => {
|
|
235
|
+
throw ResourceError.wrap(error, "osm", "overpass-api", "fetch")
|
|
236
|
+
})
|
|
237
|
+
}
|
package/google/index.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software.
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Google Place ID utilities.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Tagged } from "type-fest"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A Place ID uniquely identifies a place in the Google Places database and on Google Maps.
|
|
13
|
+
*
|
|
14
|
+
* The length of the identifier may vary. Generally, the identifier is a 27-character string,
|
|
15
|
+
* however, more specific places may have longer identifiers.
|
|
16
|
+
*
|
|
17
|
+
* Place IDs appear to be base64-encoded strings, delimited by underscores and dashes.
|
|
18
|
+
*
|
|
19
|
+
* Note that Place IDs do change. Consider a them stale after a few days.
|
|
20
|
+
*
|
|
21
|
+
* @category Google
|
|
22
|
+
* @category Geocoding
|
|
23
|
+
* @type {string}
|
|
24
|
+
* @minLength 1
|
|
25
|
+
* @pattern ^[A-Za-z0-9_-]+$
|
|
26
|
+
* @title Google Place ID
|
|
27
|
+
*/
|
|
28
|
+
export type GooglePlaceID = Tagged<string, "GooglePlaceID">
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Pattern for validating a Google Place ID.
|
|
32
|
+
*/
|
|
33
|
+
export const GOOGLE_PLACE_ID_PATTERN = /^[A-Za-z0-9_-]+$/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Type-predicate for checking if a value appears to be a valid Google Place ID.
|
|
37
|
+
*
|
|
38
|
+
* @category Google
|
|
39
|
+
* @category Geocoding
|
|
40
|
+
* @internal
|
|
41
|
+
*/
|
|
42
|
+
export function isGooglePlaceID(input: string): input is GooglePlaceID {
|
|
43
|
+
return GOOGLE_PLACE_ID_PATTERN.test(input.toString())
|
|
44
|
+
}
|