@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 ADDED
@@ -0,0 +1,3 @@
1
+ # Mailwoman Spatial
2
+
3
+ This package contains a collection of utilities for working with spatial data.
package/bbox.ts ADDED
@@ -0,0 +1,445 @@
1
+ /**
2
+ * @copyright Sister Software.
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * GeoJSON Bounding Boxes
7
+ */
8
+
9
+ import type { PolygonLiteral, SolidPolygonPath } from "./geometries/polygon.js"
10
+ import { clampLatitude, wrapLongitude } from "./position.js"
11
+ import { CoordinateProjection } from "./projection.js"
12
+
13
+ //#region Bounding Box Literals
14
+
15
+ /**
16
+ * A 2-dimensional rectangular area that can be determined by two longitudes and two latitudes.
17
+ *
18
+ * @category GeoJSON
19
+ * @category Bounding Box
20
+ * @see {@linkcode BBox} for additional information.
21
+ * @see {@linkcode BBox3DLiteral} for 3-dimensional bounding boxes.
22
+ * @see {@linkcode isBBox} for type-predicates.
23
+ * @see {@link https://tools.ietf.org/html/rfc7946#section-5 GeoJSON Bounding Boxes}
24
+ */
25
+ export type BBox2DLiteral = [
26
+ /**
27
+ * The most western longitude (in decimal degrees) of the coordinate range.
28
+ *
29
+ * @minimum -180.0
30
+ * @maximum 180.0
31
+ */
32
+ minLongitude: number,
33
+ /**
34
+ * The most southern latitude (in decimal degrees) of the coordinate range.
35
+ *
36
+ * @minimum -90.0
37
+ * @maximum 90.0
38
+ */
39
+ minLatitude: number,
40
+ /**
41
+ * The most eastern longitude (in decimal degrees) of the coordinate range.
42
+ *
43
+ * @minimum -180.0
44
+ * @maximum 180.0
45
+ */
46
+ maxLongitude: number,
47
+ /**
48
+ * The most northern latitude (in decimal degrees) of the coordinate range.
49
+ *
50
+ * @minimum -90.0
51
+ * @maximum 90.0
52
+ */
53
+ maxLatitude: number,
54
+ ]
55
+
56
+ /**
57
+ * A 3-dimensional rectangular area that can be determined by two longitudes, two latitudes, and two
58
+ * altitudes.
59
+ *
60
+ * @category GeoJSON
61
+ * @category Bounding Box
62
+ * @see {@linkcode BBox} for additional information.
63
+ * @see {@linkcode BBox2DLiteral} for a 2-dimensional bounding box.
64
+ * @see {@link https://tools.ietf.org/html/rfc7946#section-5 GeoJSON Bounding Boxes}
65
+ */
66
+ export type BBox3DLiteral = [
67
+ /**
68
+ * The most western longitude (in decimal degrees) of the coordinate range.
69
+ *
70
+ * @minimum -180.0
71
+ * @maximum 180.0
72
+ */
73
+ minLongitude: number,
74
+ /**
75
+ * The most southern latitude (in decimal degrees) of the coordinate range.
76
+ *
77
+ * @minimum -90.0
78
+ * @maximum 90.0
79
+ */
80
+ minLatitude: number,
81
+
82
+ /**
83
+ * Altitude of the most western longitude (in decimal degrees) of the coordinate range.
84
+ */
85
+ minAltitude: number,
86
+ /**
87
+ * The most eastern longitude (in decimal degrees) of the coordinate range.
88
+ *
89
+ * @minimum -180.0
90
+ * @maximum 180.0
91
+ */
92
+ maxLongitude: number,
93
+ /**
94
+ * The most northern latitude (in decimal degrees) of the coordinate range.
95
+ *
96
+ * @minimum -90.0
97
+ * @maximum 90.0
98
+ */
99
+ maxLatitude: number,
100
+
101
+ /**
102
+ * Altitude of the most eastern longitude (in decimal degrees) of the coordinate range.
103
+ */
104
+ maxAltitude: number,
105
+ ]
106
+
107
+ //#endregion
108
+
109
+ //#region Type Predicates
110
+
111
+ /**
112
+ * Type-predicate for 3-dimensional bounding boxes.
113
+ *
114
+ * This is useful when determining the type of bounding box in a GeoJSON object.
115
+ *
116
+ * @category GeoJSON
117
+ * @category Bounding Box
118
+ */
119
+ export function is2DBBox(input: unknown): input is BBox3DLiteral {
120
+ return Array.isArray(input) && input.length === 6
121
+ }
122
+
123
+ /**
124
+ * Type-predicate for 3-dimensional bounding boxes.
125
+ *
126
+ * This is useful when determining the type of bounding box in a GeoJSON object.
127
+ *
128
+ * @category GeoJSON
129
+ * @category Bounding Box
130
+ */
131
+ export function is3DBBox(input: unknown): input is BBox3DLiteral {
132
+ return Array.isArray(input) && input.length === 6
133
+ }
134
+
135
+ /**
136
+ * Type-predicate to determine if the given input is a bounding box.
137
+ *
138
+ * @category GeoJSON
139
+ * @category Bounding Box
140
+ */
141
+ export function isBBox(input: unknown): input is BBox2DLiteral | BBox3DLiteral {
142
+ return Array.isArray(input) && (input.length === 4 || input.length === 6)
143
+ }
144
+
145
+ /**
146
+ * Type-predicate to determine if the given input is a GeoBoundingBox instance.
147
+ *
148
+ * Note that this function only checks if the input is an instance of the GeoBoundingBox class.
149
+ * `instanceof` checks are not reliable in JavaScript, so this function should be used with
150
+ * caution.
151
+ *
152
+ * @category GeoJSON
153
+ */
154
+ export function isGeoBoundingBox(input: unknown): input is GeoBoundingBox {
155
+ return input instanceof GeoBoundingBox
156
+ }
157
+
158
+ //#endregion
159
+
160
+ /**
161
+ * Input for creating a GeoBoundingBox instance.
162
+ */
163
+ export type GeoBoundingBoxInput = BBox2DLiteral | BBox3DLiteral | GeoBoundingBox
164
+
165
+ // MARK: - GeoBoundingBox
166
+
167
+ /**
168
+ * A bounding box to represent the coordinate range of a GeoJSON object.
169
+ *
170
+ * This is useful when defining the extent of a GeoJSON object, such as the minimum and maximum
171
+ * coordinates of the object's Geometries, Features, or Feature Collections.
172
+ */
173
+ export class GeoBoundingBox {
174
+ //#region Properties
175
+
176
+ public projection: CoordinateProjection
177
+
178
+ /**
179
+ * The most western longitude (in decimal degrees) of the coordinate range.
180
+ *
181
+ * @minimum -180.0
182
+ * @maximum 180.0
183
+ */
184
+ #minLongitude: number
185
+ /**
186
+ * The most southern latitude (in decimal degrees) of the coordinate range.
187
+ *
188
+ * @minimum -90.0
189
+ * @maximum 90.0
190
+ */
191
+ #minLatitude: number
192
+
193
+ /**
194
+ * Altitude of the most western longitude (in decimal degrees) of the coordinate range.
195
+ */
196
+ #minAltitude: number
197
+ /**
198
+ * The most eastern longitude (in decimal degrees) of the coordinate range.
199
+ *
200
+ * @minimum -180.0
201
+ * @maximum 180.0
202
+ */
203
+ #maxLongitude: number
204
+ /**
205
+ * The most northern latitude (in decimal degrees) of the coordinate range.
206
+ *
207
+ * @minimum -90.0
208
+ * @maximum 90.0
209
+ */
210
+ #maxLatitude: number
211
+
212
+ /**
213
+ * Altitude of the most eastern longitude (in decimal degrees) of the coordinate range.
214
+ */
215
+ #maxAltitude: number
216
+
217
+ //#endregion
218
+
219
+ //#region Accessors
220
+
221
+ public get minLongitude() {
222
+ return this.#minLongitude
223
+ }
224
+ public set minLongitude(value: number) {
225
+ this.#minLongitude = wrapLongitude(value)
226
+ }
227
+
228
+ public get minLatitude() {
229
+ return this.#minLatitude
230
+ }
231
+ public set minLatitude(value: number) {
232
+ this.#minLatitude = clampLatitude(value)
233
+ }
234
+
235
+ public get maxLongitude() {
236
+ return this.#maxLongitude
237
+ }
238
+ public set maxLongitude(value: number) {
239
+ this.#maxLongitude = wrapLongitude(value)
240
+ }
241
+
242
+ public get maxLatitude() {
243
+ return this.#maxLatitude
244
+ }
245
+ public set maxLatitude(value: number) {
246
+ this.#maxLatitude = clampLatitude(value)
247
+ }
248
+
249
+ public get minAltitude() {
250
+ return this.#minAltitude
251
+ }
252
+ public set minAltitude(value: number) {
253
+ this.#minAltitude = value
254
+ }
255
+
256
+ public get maxAltitude() {
257
+ return this.#maxAltitude
258
+ }
259
+
260
+ public set maxAltitude(value: number) {
261
+ this.#maxAltitude = value
262
+ }
263
+
264
+ public get east(): number {
265
+ return this.#maxLongitude
266
+ }
267
+
268
+ public set east(value: number) {
269
+ this.#maxLongitude = wrapLongitude(value)
270
+ }
271
+
272
+ public get north(): number {
273
+ return this.#maxLatitude
274
+ }
275
+
276
+ public set north(value: number) {
277
+ this.#maxLatitude = clampLatitude(value)
278
+ }
279
+
280
+ public get south(): number {
281
+ return this.#minLatitude
282
+ }
283
+
284
+ public set south(value: number) {
285
+ this.#minLatitude = clampLatitude(value)
286
+ }
287
+
288
+ public get west(): number {
289
+ return this.#minLongitude
290
+ }
291
+
292
+ public set west(value: number) {
293
+ this.#minLongitude = wrapLongitude(value)
294
+ }
295
+
296
+ public get elevation(): number {
297
+ return this.#maxAltitude
298
+ }
299
+
300
+ public set elevation(value: number) {
301
+ this.#maxAltitude = value
302
+ }
303
+
304
+ public get depth(): number {
305
+ return this.#minAltitude
306
+ }
307
+
308
+ public set depth(value: number) {
309
+ this.#minAltitude = value
310
+ }
311
+
312
+ public [Symbol.iterator]() {
313
+ return this.toJSON()[Symbol.iterator]()
314
+ }
315
+
316
+ //#endregion
317
+
318
+ //#region Constructors
319
+
320
+ /**
321
+ * Creates a blank GeoBoundingBox instance.
322
+ */
323
+ constructor()
324
+ /**
325
+ * Creates a new GeoBoundingBox instance from the given 2D bounding box.
326
+ */
327
+ constructor(bbox: BBox2DLiteral, projection?: CoordinateProjection)
328
+
329
+ /**
330
+ * Creates a new GeoBoundingBox instance from the given 3D bounding box.
331
+ */
332
+ constructor(bbox: BBox3DLiteral, projection?: CoordinateProjection)
333
+
334
+ constructor(input?: GeoBoundingBoxInput, projection?: CoordinateProjection)
335
+ constructor(input?: GeoBoundingBoxInput, projection?: CoordinateProjection) {
336
+ let bbox: BBox2DLiteral | BBox3DLiteral
337
+
338
+ if (input instanceof GeoBoundingBox) {
339
+ bbox = input.toJSON()
340
+ } else if (isBBox(input)) {
341
+ bbox = input
342
+ } else {
343
+ bbox = [0, 0, 0, 0]
344
+ }
345
+
346
+ const [
347
+ // ---
348
+ minLongitude = 0,
349
+ minLatitude = 0,
350
+ maxLongitude = 0,
351
+ maxLatitude = 0,
352
+ minAltitude = 0,
353
+ maxAltitude = 0,
354
+ ] = bbox
355
+
356
+ this.#minLongitude = minLongitude
357
+ this.#minLatitude = minLatitude
358
+ this.#maxLongitude = maxLongitude
359
+ this.#maxLatitude = maxLatitude
360
+ this.#minAltitude = minAltitude
361
+ this.#maxAltitude = maxAltitude
362
+ this.projection = projection ?? CoordinateProjection.WGS84
363
+ }
364
+
365
+ //#endregion
366
+
367
+ //#region Predicates
368
+
369
+ public is3D() {
370
+ return this.#minAltitude !== 0 || this.#maxAltitude !== 0
371
+ }
372
+
373
+ public is2D() {
374
+ return !this.is3D()
375
+ }
376
+
377
+ //#endregion
378
+
379
+ //#region Conversion
380
+
381
+ /**
382
+ * Converts the 2D GeoBoundingBox to an array literal.
383
+ */
384
+ public toJSON2D(): BBox2DLiteral {
385
+ return [
386
+ // ---
387
+ this.#minLongitude,
388
+ this.#minLatitude,
389
+ this.#maxLongitude,
390
+ this.#maxLatitude,
391
+ ]
392
+ }
393
+
394
+ /**
395
+ * Converts the 3D GeoBoundingBox to an array literal.
396
+ */
397
+ public toJSON3D(): BBox3DLiteral {
398
+ return [
399
+ // ---
400
+ this.#minLongitude,
401
+ this.#minLatitude,
402
+ this.#minAltitude,
403
+ this.#maxLongitude,
404
+ this.#maxLatitude,
405
+ this.#maxAltitude,
406
+ ]
407
+ }
408
+
409
+ public toJSON(): BBox2DLiteral | BBox3DLiteral {
410
+ if (this.is3D()) return this.toJSON3D()
411
+
412
+ return this.toJSON2D()
413
+ }
414
+
415
+ /**
416
+ * Converts the GeoBoundingBox to a GeoJSON Polygon, omitting the altitude.
417
+ */
418
+ public to2DPolygon(): PolygonLiteral {
419
+ const { minLatitude, maxLatitude, minLongitude, maxLongitude } = this
420
+ const path: SolidPolygonPath = [
421
+ [
422
+ [minLongitude, minLatitude],
423
+ [maxLongitude, minLatitude],
424
+ [maxLongitude, maxLatitude],
425
+ [minLongitude, maxLatitude],
426
+ [minLongitude, minLatitude],
427
+ ],
428
+ ]
429
+
430
+ return { type: "Polygon", coordinates: path }
431
+ }
432
+
433
+ /**
434
+ * Converts the GeoBoundingBox to a Well-Known-Text (WKT) string.
435
+ *
436
+ * This is useful when building Spatialite query parameters.
437
+ */
438
+ public to2DEWKT(): string {
439
+ const { minLatitude, maxLatitude, minLongitude, maxLongitude } = this
440
+
441
+ return `SRID=${this.projection};POLYGON((${minLongitude} ${minLatitude}, ${maxLongitude} ${minLatitude}, ${maxLongitude} ${maxLatitude}, ${minLongitude} ${maxLatitude}, ${minLongitude} ${minLatitude}))`
442
+ }
443
+
444
+ //#endregion
445
+ }