@trackunit/geo-json-utils 0.0.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/README.md +21 -0
- package/index.cjs.d.ts +1 -0
- package/index.cjs.js +520 -0
- package/index.esm.d.ts +1 -0
- package/index.esm.js +491 -0
- package/package.json +16 -0
- package/src/GeoJsonSchemas.d.ts +176 -0
- package/src/GeoJsonUtils.d.ts +73 -0
- package/src/TUGeoJsonObjectBridgeUtils.d.ts +33 -0
- package/src/TuGeoJsonSchemas.d.ts +58 -0
- package/src/index.d.ts +4 -0
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Trackunit geo-json-utils
|
|
2
|
+
|
|
3
|
+
The `@trackunit/geo-json-utils` package is the home of Trackunits public react components used for...
|
|
4
|
+
|
|
5
|
+
This library is exposed publicly for use in the the Trackunit [Iris App SDK](https://www.npmjs.com/package/@trackunit/iris-app).
|
|
6
|
+
|
|
7
|
+
To browse all avaliable components visit our [Public Storybook](https://apps.iris.trackunit.com/storybook/).
|
|
8
|
+
|
|
9
|
+
For more info and a full guide on Iris App SDK Development, please visit our [Developer Hub](https://developers.trackunit.com/).
|
|
10
|
+
|
|
11
|
+
## Development
|
|
12
|
+
|
|
13
|
+
At this point this library is only developed by Trackunit Employees.
|
|
14
|
+
For development related information see the [development readme](https://github.com/Trackunit/manager/blob/master/libs/react/components/DEVELOPMENT.md).
|
|
15
|
+
|
|
16
|
+
## Trackunit
|
|
17
|
+
|
|
18
|
+
This package was developed by Trackunit ApS.
|
|
19
|
+
Trackunit is the leading SaaS-based IoT solution for the construction industry, offering an ecosystem of hardware, fleet management software & telematics.
|
|
20
|
+
|
|
21
|
+

|
package/index.cjs.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/index";
|
package/index.cjs.js
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var zod = require('zod');
|
|
4
|
+
var polygonClipping = require('polygon-clipping');
|
|
5
|
+
|
|
6
|
+
// * NOTE: For simplicity these tools are built for 2D coordinate space only!
|
|
7
|
+
/**
|
|
8
|
+
* A Position is an array of coordinates. [x, y]
|
|
9
|
+
* https://tools.ietf.org/html/rfc7946#section-3.1.1
|
|
10
|
+
*/
|
|
11
|
+
const geoJsonPositionSchema = zod.z.tuple([zod.z.number(), zod.z.number()]);
|
|
12
|
+
/**
|
|
13
|
+
* Point geometry object.
|
|
14
|
+
* https://tools.ietf.org/html/rfc7946#section-3.1.2
|
|
15
|
+
*/
|
|
16
|
+
const geoJsonPointSchema = zod.z.strictObject({
|
|
17
|
+
type: zod.z.literal("Point"),
|
|
18
|
+
coordinates: geoJsonPositionSchema,
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* MultiPoint geometry object.
|
|
22
|
+
* https://tools.ietf.org/html/rfc7946#section-3.1.3
|
|
23
|
+
*/
|
|
24
|
+
const geoJsonMultiPointSchema = zod.z.strictObject({
|
|
25
|
+
type: zod.z.literal("MultiPoint"),
|
|
26
|
+
coordinates: zod.z.array(geoJsonPositionSchema),
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* LineString geometry object.
|
|
30
|
+
* Minimum length of 2 positions.
|
|
31
|
+
* https://tools.ietf.org/html/rfc7946#section-3.1.4
|
|
32
|
+
*/
|
|
33
|
+
const geoJsonLineStringSchema = zod.z.strictObject({
|
|
34
|
+
type: zod.z.literal("LineString"),
|
|
35
|
+
coordinates: zod.z.array(geoJsonPositionSchema).min(2),
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* MultiLineString geometry object.
|
|
39
|
+
* https://tools.ietf.org/html/rfc7946#section-3.1.5
|
|
40
|
+
*/
|
|
41
|
+
const geoJsonMultiLineStringSchema = zod.z.strictObject({
|
|
42
|
+
type: zod.z.literal("MultiLineString"),
|
|
43
|
+
coordinates: zod.z.array(zod.z.array(geoJsonPositionSchema)),
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* Helper type for reuse across polygon schemas.
|
|
47
|
+
*
|
|
48
|
+
* - A linear ring is a closed LineString with four or more positions.
|
|
49
|
+
* - The first and last positions are equivalent, and they MUST contain
|
|
50
|
+
identical values; their representation SHOULD also be identical
|
|
51
|
+
* - A linear ring is the boundary of a surface or the boundary of a
|
|
52
|
+
hole in a surface
|
|
53
|
+
* - A linear ring MUST follow the right-hand rule with respect to the
|
|
54
|
+
area it bounds, i.e., exterior rings are counterclockwise, and
|
|
55
|
+
holes are clockwise
|
|
56
|
+
*/
|
|
57
|
+
const geoJsonLinearRingSchema = zod.z
|
|
58
|
+
.array(geoJsonPositionSchema)
|
|
59
|
+
.min(4, {
|
|
60
|
+
message: "Coordinates array must contain at least 4 positions. 3 to make a non-line shape and 1 to close the shape (duplicate of first)",
|
|
61
|
+
})
|
|
62
|
+
.superRefine((coords, ctx) => {
|
|
63
|
+
const first = coords[0];
|
|
64
|
+
const last = coords[coords.length - 1];
|
|
65
|
+
// Check if first and last coordinates match
|
|
66
|
+
if (JSON.stringify(first) !== JSON.stringify(last)) {
|
|
67
|
+
ctx.addIssue({
|
|
68
|
+
code: zod.z.ZodIssueCode.custom,
|
|
69
|
+
message: "First and last coordinate positions must be identical (to close the linear ring aka polygon).",
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// Check if consecutive points are identical (excluding first and last)
|
|
73
|
+
for (let i = 1; i < coords.length - 1; i++) {
|
|
74
|
+
if (JSON.stringify(coords[i]) === JSON.stringify(coords[i - 1])) {
|
|
75
|
+
ctx.addIssue({
|
|
76
|
+
code: zod.z.ZodIssueCode.custom,
|
|
77
|
+
message: `Consecutive coordinates at index ${i - 1} and ${i} should not be identical.`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
/**
|
|
83
|
+
* Polygon geometry object.
|
|
84
|
+
* https://tools.ietf.org/html/rfc7946#section-3.1.6
|
|
85
|
+
*/
|
|
86
|
+
const geoJsonPolygonSchema = zod.z.strictObject({
|
|
87
|
+
type: zod.z.literal("Polygon"),
|
|
88
|
+
coordinates: zod.z.array(geoJsonLinearRingSchema),
|
|
89
|
+
});
|
|
90
|
+
/**
|
|
91
|
+
* MultiPolygon geometry object.
|
|
92
|
+
* https://tools.ietf.org/html/rfc7946#section-3.1.7
|
|
93
|
+
*/
|
|
94
|
+
const geoJsonMultiPolygonSchema = zod.z.strictObject({
|
|
95
|
+
type: zod.z.literal("MultiPolygon"),
|
|
96
|
+
coordinates: zod.z.array(zod.z.array(geoJsonLinearRingSchema)),
|
|
97
|
+
});
|
|
98
|
+
// The same for Geometry, GeometryCollection, GeoJsonProperties, Feature, FeatureCollection, etc.
|
|
99
|
+
const geoJsonGeometrySchema = zod.z.union([
|
|
100
|
+
geoJsonPointSchema,
|
|
101
|
+
geoJsonMultiPointSchema,
|
|
102
|
+
geoJsonLineStringSchema,
|
|
103
|
+
geoJsonMultiLineStringSchema,
|
|
104
|
+
geoJsonPolygonSchema,
|
|
105
|
+
geoJsonMultiPolygonSchema,
|
|
106
|
+
]);
|
|
107
|
+
//* -------- Bbox -------- *//
|
|
108
|
+
/**
|
|
109
|
+
* 2D bounding box of the GeoJSON object.
|
|
110
|
+
* The value of the Bbox member is an array of length 4.
|
|
111
|
+
*
|
|
112
|
+
* [min_lon, min_lat, max_lon, max_lat]
|
|
113
|
+
*/
|
|
114
|
+
const geoJsonBboxSchema = zod.z
|
|
115
|
+
.tuple([zod.z.number(), zod.z.number(), zod.z.number(), zod.z.number()])
|
|
116
|
+
.refine(([minLng, minLat, maxLng, maxLat]) => maxLng > minLng && maxLat > minLat, {
|
|
117
|
+
message: "Invalid bounding box: maxLng should be greater than minLng, and maxLat should be greater than minLat.",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const EARTH_RADIUS = 6378137; // Earth’s mean radius in meters
|
|
121
|
+
/**
|
|
122
|
+
* @description Creates a polygon (with no holes) from a bounding box.
|
|
123
|
+
*/
|
|
124
|
+
const getPolygonFromBbox = (bbox) => {
|
|
125
|
+
const [minLon, minLat, maxLon, maxLat] = bbox;
|
|
126
|
+
return {
|
|
127
|
+
type: "Polygon",
|
|
128
|
+
coordinates: [
|
|
129
|
+
[
|
|
130
|
+
[minLon, minLat],
|
|
131
|
+
[maxLon, minLat],
|
|
132
|
+
[maxLon, maxLat],
|
|
133
|
+
[minLon, maxLat],
|
|
134
|
+
[minLon, minLat],
|
|
135
|
+
],
|
|
136
|
+
],
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* @description Creates a bounding box from a GeoJSON Polygon.
|
|
141
|
+
*/
|
|
142
|
+
const getBboxFromGeoJsonPolygon = (polygon) => {
|
|
143
|
+
const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
|
|
144
|
+
if (!polygonParsed.success) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const points = polygonParsed.data.coordinates[0];
|
|
148
|
+
if (!points) {
|
|
149
|
+
// Should never happen since the schema checks for it
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const latitudes = points.map(point => point[1]);
|
|
153
|
+
const longitudes = points.map(point => point[0]);
|
|
154
|
+
return [Math.min(...longitudes), Math.min(...latitudes), Math.max(...longitudes), Math.max(...latitudes)];
|
|
155
|
+
};
|
|
156
|
+
/**
|
|
157
|
+
* @description Creates a round polygon from a point and a radius.
|
|
158
|
+
*/
|
|
159
|
+
const getPolygonFromPointAndRadius = (point, radius) => {
|
|
160
|
+
const [lon, lat] = point.coordinates;
|
|
161
|
+
// Adjust the number of points based on radius (resolution)
|
|
162
|
+
const pointsCount = Math.max(32, Math.floor(radius / 100)); // More points for larger radius
|
|
163
|
+
const angleStep = (2 * Math.PI) / pointsCount;
|
|
164
|
+
const coordinates = [];
|
|
165
|
+
for (let i = 0; i <= pointsCount; i++) {
|
|
166
|
+
const angle = i * angleStep;
|
|
167
|
+
// Calculate offset in latitude and longitude
|
|
168
|
+
const deltaLat = (radius / EARTH_RADIUS) * (180 / Math.PI);
|
|
169
|
+
const deltaLon = deltaLat / Math.cos((lat * Math.PI) / 180);
|
|
170
|
+
// Calculate new coordinates based on angle
|
|
171
|
+
const newLat = lat + deltaLat * Math.sin(angle);
|
|
172
|
+
const newLon = lon + deltaLon * Math.cos(angle);
|
|
173
|
+
coordinates.push([newLon, newLat]);
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
type: "Polygon",
|
|
177
|
+
coordinates: [coordinates],
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* @description Creates a TU bounding box from a GeoJson Polygon.
|
|
182
|
+
*/
|
|
183
|
+
const getBoundingBoxFromGeoJsonPolygon = (polygon) => {
|
|
184
|
+
const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
|
|
185
|
+
if (!polygonParsed.success) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const points = polygonParsed.data.coordinates[0];
|
|
189
|
+
if (!points) {
|
|
190
|
+
// Should never happen since the schema checks for it
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const latitudes = points.map(point => point[1]);
|
|
194
|
+
const longitudes = points.map(point => point[0]);
|
|
195
|
+
return {
|
|
196
|
+
nw: {
|
|
197
|
+
latitude: Math.max(...latitudes),
|
|
198
|
+
longitude: Math.min(...longitudes),
|
|
199
|
+
},
|
|
200
|
+
se: {
|
|
201
|
+
latitude: Math.min(...latitudes),
|
|
202
|
+
longitude: Math.max(...longitudes),
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
};
|
|
206
|
+
/**
|
|
207
|
+
* @description Creates a GeoJSON Polygon from a TU bounding box.
|
|
208
|
+
*/
|
|
209
|
+
const getGeoJsonPolygonFromBoundingBox = (boundingBox) => {
|
|
210
|
+
const { nw, se } = boundingBox;
|
|
211
|
+
return {
|
|
212
|
+
type: "Polygon",
|
|
213
|
+
coordinates: [
|
|
214
|
+
[
|
|
215
|
+
[nw.longitude, nw.latitude], // Northwest corner
|
|
216
|
+
[se.longitude, nw.latitude], // Northeast corner
|
|
217
|
+
[se.longitude, se.latitude], // Southeast corner
|
|
218
|
+
[nw.longitude, se.latitude], // Southwest corner
|
|
219
|
+
[nw.longitude, nw.latitude], // Close the loop back to Northwest corner
|
|
220
|
+
],
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
/**
|
|
225
|
+
* @description Creates TU point coordinate from a GeoJSON Point.
|
|
226
|
+
*/
|
|
227
|
+
const getPointCoordinateFromGeoJsonPoint = (point) => {
|
|
228
|
+
return { latitude: point.coordinates[1], longitude: point.coordinates[0] };
|
|
229
|
+
};
|
|
230
|
+
/**
|
|
231
|
+
* @description Gets the extreme point of a polygon in a given direction.
|
|
232
|
+
* @param {object} params - The parameters object
|
|
233
|
+
* @param {GeoJsonPolygon} params.polygon - The polygon to get the extreme point from
|
|
234
|
+
* @param {("top" | "right" | "bottom" | "left")} params.direction - The direction to get the extreme point in
|
|
235
|
+
* @returns {GeoJsonPoint} The extreme point in the given direction
|
|
236
|
+
*/
|
|
237
|
+
const getExtremeGeoJsonPointFromPolygon = ({ polygon, direction, }) => {
|
|
238
|
+
var _a, _b, _c;
|
|
239
|
+
const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
|
|
240
|
+
if (!polygonParsed.success) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
const firstPoint = (_a = polygonParsed.data.coordinates[0]) === null || _a === void 0 ? void 0 : _a[0];
|
|
244
|
+
if (!firstPoint) {
|
|
245
|
+
// Should never happen since the schema checks for it
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const extremePosition = (_b = polygonParsed.data.coordinates[0]) === null || _b === void 0 ? void 0 : _b.reduce((extremePoint, currentPoint) => {
|
|
249
|
+
var _a, _b, _c, _d;
|
|
250
|
+
switch (direction) {
|
|
251
|
+
case "top":
|
|
252
|
+
return currentPoint[1] > ((_a = extremePoint === null || extremePoint === void 0 ? void 0 : extremePoint[1]) !== null && _a !== void 0 ? _a : -Infinity) ? currentPoint : extremePoint !== null && extremePoint !== void 0 ? extremePoint : currentPoint;
|
|
253
|
+
case "right":
|
|
254
|
+
return currentPoint[0] > ((_b = extremePoint === null || extremePoint === void 0 ? void 0 : extremePoint[0]) !== null && _b !== void 0 ? _b : -Infinity) ? currentPoint : extremePoint !== null && extremePoint !== void 0 ? extremePoint : currentPoint;
|
|
255
|
+
case "bottom":
|
|
256
|
+
return currentPoint[1] < ((_c = extremePoint === null || extremePoint === void 0 ? void 0 : extremePoint[1]) !== null && _c !== void 0 ? _c : Infinity) ? currentPoint : extremePoint !== null && extremePoint !== void 0 ? extremePoint : currentPoint;
|
|
257
|
+
case "left":
|
|
258
|
+
return currentPoint[0] < ((_d = extremePoint === null || extremePoint === void 0 ? void 0 : extremePoint[0]) !== null && _d !== void 0 ? _d : Infinity) ? currentPoint : extremePoint !== null && extremePoint !== void 0 ? extremePoint : currentPoint;
|
|
259
|
+
default: {
|
|
260
|
+
throw new Error(`${direction} is not known`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}, (_c = polygonParsed.data.coordinates[0]) === null || _c === void 0 ? void 0 : _c[0]);
|
|
264
|
+
return extremePosition
|
|
265
|
+
? {
|
|
266
|
+
type: "Point",
|
|
267
|
+
coordinates: extremePosition,
|
|
268
|
+
}
|
|
269
|
+
: null; // Should never happen since the schema checks for it
|
|
270
|
+
};
|
|
271
|
+
/**
|
|
272
|
+
* Checks if a position is inside a linear ring. On edge is considered inside.
|
|
273
|
+
*/
|
|
274
|
+
const isGeoJsonPositionInLinearRing = ({ position, linearRing, }) => {
|
|
275
|
+
const linearRingParsed = geoJsonLinearRingSchema.safeParse(linearRing);
|
|
276
|
+
if (!linearRingParsed.success) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
let inside = false;
|
|
280
|
+
const [x, y] = position;
|
|
281
|
+
for (let i = 0, j = linearRingParsed.data.length - 1; i < linearRingParsed.data.length; j = i++) {
|
|
282
|
+
const point1 = linearRingParsed.data[i];
|
|
283
|
+
const point2 = linearRingParsed.data[j];
|
|
284
|
+
if (!point1 || !point2) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const [xi, yi] = point1;
|
|
288
|
+
const [xj, yj] = point2;
|
|
289
|
+
const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
|
|
290
|
+
if (intersect) {
|
|
291
|
+
inside = !inside;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return inside;
|
|
295
|
+
};
|
|
296
|
+
/**
|
|
297
|
+
* @description Checks if a point is inside a polygon.
|
|
298
|
+
*/
|
|
299
|
+
const isGeoJsonPointInPolygon = ({ point, polygon, }) => {
|
|
300
|
+
const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
|
|
301
|
+
if (!polygonParsed.success) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
return polygonParsed.data.coordinates.some(linearRing => isGeoJsonPositionInLinearRing({ position: point.coordinates, linearRing }));
|
|
305
|
+
};
|
|
306
|
+
/**
|
|
307
|
+
* Checks if polygon1 is fully contained within polygon2
|
|
308
|
+
*/
|
|
309
|
+
const isFullyContainedInGeoJsonPolygon = (polygon1, polygon2) => {
|
|
310
|
+
const polygon1Parsed = geoJsonPolygonSchema.safeParse(polygon1);
|
|
311
|
+
const polygon2Parsed = geoJsonPolygonSchema.safeParse(polygon2);
|
|
312
|
+
// The schema checks more than a TypeScript type can represent
|
|
313
|
+
if (!polygon1Parsed.success || !polygon2Parsed.success) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
return polygon1Parsed.data.coordinates.every(linearRing => polygon2Parsed.data.coordinates.some(lr => linearRing.every(position => isGeoJsonPositionInLinearRing({ position, linearRing: lr }))));
|
|
317
|
+
};
|
|
318
|
+
/**
|
|
319
|
+
* @description Gets the intersection between two GeoJSON polygons. If one polygon is fully contained within the other,
|
|
320
|
+
* returns the contained polygon. Otherwise returns a MultiPolygon representing the intersection.
|
|
321
|
+
* @param polygon1 The first polygon to check intersection
|
|
322
|
+
* @param polygon2 The second polygon to check intersection
|
|
323
|
+
* @returns {(GeoJsonMultiPolygon | GeoJsonPolygon)} The intersection as either a Polygon (if one contains the other) or MultiPolygon
|
|
324
|
+
*/
|
|
325
|
+
const getGeoJsonPolygonIntersection = (polygon1, polygon2) => {
|
|
326
|
+
const polygon1Parsed = geoJsonPolygonSchema.safeParse(polygon1);
|
|
327
|
+
const polygon2Parsed = geoJsonPolygonSchema.safeParse(polygon2);
|
|
328
|
+
if (!polygon1Parsed.success || !polygon2Parsed.success) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
if (isFullyContainedInGeoJsonPolygon(polygon1, polygon2)) {
|
|
332
|
+
return polygon1;
|
|
333
|
+
}
|
|
334
|
+
if (isFullyContainedInGeoJsonPolygon(polygon2, polygon1)) {
|
|
335
|
+
return polygon2;
|
|
336
|
+
}
|
|
337
|
+
const intersectionResult = polygonClipping.intersection(polygon1.coordinates, polygon2.coordinates);
|
|
338
|
+
if (intersectionResult.length === 1 && intersectionResult[0]) {
|
|
339
|
+
return {
|
|
340
|
+
type: "Polygon",
|
|
341
|
+
coordinates: intersectionResult[0],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
type: "MultiPolygon",
|
|
346
|
+
coordinates: polygonClipping.intersection(polygon1.coordinates, polygon2.coordinates),
|
|
347
|
+
};
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
//! These tools are used to bridge the gap with out poorly typed graphql types
|
|
351
|
+
// Should be ideally be avoided but are needed until we fix the graphql types
|
|
352
|
+
const isDoubleNestedCoords = (coords) => Array.isArray(coords) &&
|
|
353
|
+
Array.isArray(coords[0]) &&
|
|
354
|
+
Array.isArray(coords[0][0]) &&
|
|
355
|
+
typeof coords[0][0][0] === "number";
|
|
356
|
+
const isSingleCoords = (coords) => typeof coords[0] === "number";
|
|
357
|
+
/**
|
|
358
|
+
* @description Returns coordinates in consistent format
|
|
359
|
+
* @param inconsistentCoordinates Single point, array of points or nested array of points
|
|
360
|
+
* @returns {GeoJsonPosition[]} Array of standardized coordinates
|
|
361
|
+
*/
|
|
362
|
+
const coordinatesToStandardFormat = (inconsistentCoordinates) => {
|
|
363
|
+
if (!inconsistentCoordinates) {
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
if (isSingleCoords(inconsistentCoordinates)) {
|
|
367
|
+
return [inconsistentCoordinates];
|
|
368
|
+
}
|
|
369
|
+
if (isDoubleNestedCoords(inconsistentCoordinates)) {
|
|
370
|
+
return inconsistentCoordinates[0] || [];
|
|
371
|
+
}
|
|
372
|
+
if (inconsistentCoordinates[0] && typeof inconsistentCoordinates[0][0] === "number") {
|
|
373
|
+
return inconsistentCoordinates;
|
|
374
|
+
}
|
|
375
|
+
return [];
|
|
376
|
+
};
|
|
377
|
+
/**
|
|
378
|
+
* @description Extracts a point coordinate from a GeoJSON object.
|
|
379
|
+
* @param geoObject A GeoJSON object.
|
|
380
|
+
* @returns {PointCoordinate} A point coordinate.
|
|
381
|
+
*/
|
|
382
|
+
const getPointCoordinateFromGeoJsonObject = (geoObject) => {
|
|
383
|
+
if (!geoObject) {
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
else if ("geometry" in geoObject) {
|
|
387
|
+
return getPointCoordinateFromGeoJsonObject(geoObject.geometry);
|
|
388
|
+
}
|
|
389
|
+
else if ("coordinates" in geoObject &&
|
|
390
|
+
Array.isArray(geoObject.coordinates) &&
|
|
391
|
+
typeof geoObject.coordinates[0] === "number" &&
|
|
392
|
+
typeof geoObject.coordinates[1] === "number") {
|
|
393
|
+
const [point] = coordinatesToStandardFormat(geoObject.coordinates);
|
|
394
|
+
if (point) {
|
|
395
|
+
return { latitude: point[1], longitude: point[0] };
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
/**
|
|
406
|
+
* @description Extracts multiple point coordinates from a GeoJSON object.
|
|
407
|
+
* @param geoObject A GeoJSON object.
|
|
408
|
+
* @returns {PointCoordinate[]} An array of point coordinates.
|
|
409
|
+
* @example getMultipleCoordinatesFromGeoJsonObject({ type: "Point", coordinates: [1, 2] }) // [{ longitude: 1, latitude: 2 }]
|
|
410
|
+
*/
|
|
411
|
+
const getMultipleCoordinatesFromGeoJsonObject = (geoObject) => {
|
|
412
|
+
if (!geoObject) {
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
else if ("geometry" in geoObject) {
|
|
416
|
+
return getMultipleCoordinatesFromGeoJsonObject(geoObject.geometry);
|
|
417
|
+
}
|
|
418
|
+
else if ("coordinates" in geoObject) {
|
|
419
|
+
return coordinatesToStandardFormat(geoObject.coordinates).map(([longitude, latitude]) => ({ longitude, latitude }));
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
//* -------- Trackunit-invented schemas and types to extend the GeoJson spec -------- *//
|
|
427
|
+
/**
|
|
428
|
+
* Polygon geometry object that explicitly disallows holes.
|
|
429
|
+
*
|
|
430
|
+
* Same as geoJsonPolygonSchema but type disallows holes by
|
|
431
|
+
* using tuple of one single linear ring instead of an array.
|
|
432
|
+
*/
|
|
433
|
+
const tuGeoJsonPolygonNoHolesSchema = zod.z.strictObject({
|
|
434
|
+
//The type is still "Polygon" (not PolygonNoHoles or similar) since it's always
|
|
435
|
+
//compliant with Polygon, just not the other way around
|
|
436
|
+
type: zod.z.literal("Polygon"),
|
|
437
|
+
//uses tuple instead of array to enforce only 1 linear ring aka the polygon itself
|
|
438
|
+
coordinates: zod.z.tuple([geoJsonLinearRingSchema]),
|
|
439
|
+
});
|
|
440
|
+
/**
|
|
441
|
+
* Point radius object.
|
|
442
|
+
* For when you wish to define an area by a point and a radius.
|
|
443
|
+
*
|
|
444
|
+
* radius is in meters
|
|
445
|
+
*/
|
|
446
|
+
const tuGeoJsonPointRadiusSchema = zod.z.strictObject({
|
|
447
|
+
type: zod.z.literal("PointRadius"),
|
|
448
|
+
coordinates: geoJsonPositionSchema,
|
|
449
|
+
radius: zod.z.number().positive(), // in meters
|
|
450
|
+
});
|
|
451
|
+
/**
|
|
452
|
+
* A Polygon with exactly 5 points and 4 horizontal/vertical sides that form a normal rectangular box.
|
|
453
|
+
*/
|
|
454
|
+
const tuGeoJsonRectangularBoxPolygonSchema = zod.z
|
|
455
|
+
.strictObject({
|
|
456
|
+
type: zod.z.literal("Polygon"),
|
|
457
|
+
coordinates: zod.z.array(geoJsonLinearRingSchema),
|
|
458
|
+
})
|
|
459
|
+
.superRefine((data, ctx) => {
|
|
460
|
+
const coordinates = data.coordinates[0];
|
|
461
|
+
// Validate polygon has exactly 5 points
|
|
462
|
+
if ((coordinates === null || coordinates === void 0 ? void 0 : coordinates.length) !== 5) {
|
|
463
|
+
ctx.addIssue({
|
|
464
|
+
code: zod.z.ZodIssueCode.custom,
|
|
465
|
+
message: "Polygon must have exactly 5 coordinates to form a closed box.",
|
|
466
|
+
});
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
// Check each side is either horizontal or vertical
|
|
470
|
+
for (let i = 0; i < 4; i++) {
|
|
471
|
+
const point1 = coordinates[i];
|
|
472
|
+
const point2 = coordinates[i + 1];
|
|
473
|
+
if (point1 === undefined || point2 === undefined) {
|
|
474
|
+
ctx.addIssue({
|
|
475
|
+
code: zod.z.ZodIssueCode.custom,
|
|
476
|
+
message: "Each coordinate must be a defined point.",
|
|
477
|
+
});
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const [x1, y1] = point1;
|
|
481
|
+
const [x2, y2] = point2;
|
|
482
|
+
// Ensure each line segment is either horizontal or vertical
|
|
483
|
+
if (x1 !== x2 && y1 !== y2) {
|
|
484
|
+
ctx.addIssue({
|
|
485
|
+
code: zod.z.ZodIssueCode.custom,
|
|
486
|
+
message: "Polygon sides must be horizontal or vertical to form a box shape.",
|
|
487
|
+
});
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
exports.EARTH_RADIUS = EARTH_RADIUS;
|
|
494
|
+
exports.coordinatesToStandardFormat = coordinatesToStandardFormat;
|
|
495
|
+
exports.geoJsonBboxSchema = geoJsonBboxSchema;
|
|
496
|
+
exports.geoJsonGeometrySchema = geoJsonGeometrySchema;
|
|
497
|
+
exports.geoJsonLineStringSchema = geoJsonLineStringSchema;
|
|
498
|
+
exports.geoJsonLinearRingSchema = geoJsonLinearRingSchema;
|
|
499
|
+
exports.geoJsonMultiLineStringSchema = geoJsonMultiLineStringSchema;
|
|
500
|
+
exports.geoJsonMultiPointSchema = geoJsonMultiPointSchema;
|
|
501
|
+
exports.geoJsonMultiPolygonSchema = geoJsonMultiPolygonSchema;
|
|
502
|
+
exports.geoJsonPointSchema = geoJsonPointSchema;
|
|
503
|
+
exports.geoJsonPolygonSchema = geoJsonPolygonSchema;
|
|
504
|
+
exports.geoJsonPositionSchema = geoJsonPositionSchema;
|
|
505
|
+
exports.getBboxFromGeoJsonPolygon = getBboxFromGeoJsonPolygon;
|
|
506
|
+
exports.getBoundingBoxFromGeoJsonPolygon = getBoundingBoxFromGeoJsonPolygon;
|
|
507
|
+
exports.getExtremeGeoJsonPointFromPolygon = getExtremeGeoJsonPointFromPolygon;
|
|
508
|
+
exports.getGeoJsonPolygonFromBoundingBox = getGeoJsonPolygonFromBoundingBox;
|
|
509
|
+
exports.getGeoJsonPolygonIntersection = getGeoJsonPolygonIntersection;
|
|
510
|
+
exports.getMultipleCoordinatesFromGeoJsonObject = getMultipleCoordinatesFromGeoJsonObject;
|
|
511
|
+
exports.getPointCoordinateFromGeoJsonObject = getPointCoordinateFromGeoJsonObject;
|
|
512
|
+
exports.getPointCoordinateFromGeoJsonPoint = getPointCoordinateFromGeoJsonPoint;
|
|
513
|
+
exports.getPolygonFromBbox = getPolygonFromBbox;
|
|
514
|
+
exports.getPolygonFromPointAndRadius = getPolygonFromPointAndRadius;
|
|
515
|
+
exports.isFullyContainedInGeoJsonPolygon = isFullyContainedInGeoJsonPolygon;
|
|
516
|
+
exports.isGeoJsonPointInPolygon = isGeoJsonPointInPolygon;
|
|
517
|
+
exports.isGeoJsonPositionInLinearRing = isGeoJsonPositionInLinearRing;
|
|
518
|
+
exports.tuGeoJsonPointRadiusSchema = tuGeoJsonPointRadiusSchema;
|
|
519
|
+
exports.tuGeoJsonPolygonNoHolesSchema = tuGeoJsonPolygonNoHolesSchema;
|
|
520
|
+
exports.tuGeoJsonRectangularBoxPolygonSchema = tuGeoJsonRectangularBoxPolygonSchema;
|
package/index.esm.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./src/index";
|