@trackunit/shared-utils 0.0.81 → 0.0.82

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/index.cjs.js CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict';
2
2
 
3
+ var zod = require('zod');
3
4
  var uuid = require('uuid');
4
5
 
5
6
  /**
@@ -413,6 +414,181 @@ searchTerm) => {
413
414
  .every(term => filterableProps(asset).some(value => value.toLowerCase().includes(term.toLowerCase()))));
414
415
  };
415
416
 
417
+ // * NOTE: For simplicity these tools are built for 2D coordinate space only!
418
+ /**
419
+ * A Position is an array of coordinates. [x, y]
420
+ * https://tools.ietf.org/html/rfc7946#section-3.1.1
421
+ */
422
+ const geoJsonPositionSchema = zod.z.tuple([zod.z.number(), zod.z.number()]);
423
+ /**
424
+ * Point geometry object.
425
+ * https://tools.ietf.org/html/rfc7946#section-3.1.2
426
+ */
427
+ const geoJsonPointSchema = zod.z.strictObject({
428
+ type: zod.z.literal("Point"),
429
+ coordinates: geoJsonPositionSchema,
430
+ });
431
+ /**
432
+ * MultiPoint geometry object.
433
+ * https://tools.ietf.org/html/rfc7946#section-3.1.3
434
+ */
435
+ const geoJsonMultiPointSchema = zod.z.strictObject({
436
+ type: zod.z.literal("MultiPoint"),
437
+ coordinates: zod.z.array(geoJsonPositionSchema),
438
+ });
439
+ /**
440
+ * LineString geometry object.
441
+ * Minimum length of 2 positions.
442
+ * https://tools.ietf.org/html/rfc7946#section-3.1.4
443
+ */
444
+ const geoJsonLineStringSchema = zod.z.strictObject({
445
+ type: zod.z.literal("LineString"),
446
+ coordinates: zod.z.array(geoJsonPositionSchema).min(2),
447
+ });
448
+ /**
449
+ * MultiLineString geometry object.
450
+ * https://tools.ietf.org/html/rfc7946#section-3.1.5
451
+ */
452
+ const geoJsonMultiLineStringSchema = zod.z.strictObject({
453
+ type: zod.z.literal("MultiLineString"),
454
+ coordinates: zod.z.array(zod.z.array(geoJsonPositionSchema)),
455
+ });
456
+ /**
457
+ * Helper type for reuse across polygon schemas.
458
+ *
459
+ * - A linear ring is a closed LineString with four or more positions.
460
+ * - The first and last positions are equivalent, and they MUST contain
461
+ identical values; their representation SHOULD also be identical
462
+ * - A linear ring is the boundary of a surface or the boundary of a
463
+ hole in a surface
464
+ * - A linear ring MUST follow the right-hand rule with respect to the
465
+ area it bounds, i.e., exterior rings are counterclockwise, and
466
+ holes are clockwise
467
+ */
468
+ const geoJsonLinearRingSchema = zod.z
469
+ .array(geoJsonPositionSchema)
470
+ .min(4, {
471
+ 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)",
472
+ })
473
+ .superRefine((coords, ctx) => {
474
+ const first = coords[0];
475
+ const last = coords[coords.length - 1];
476
+ // Check if first and last coordinates match
477
+ if (JSON.stringify(first) !== JSON.stringify(last)) {
478
+ ctx.addIssue({
479
+ code: zod.z.ZodIssueCode.custom,
480
+ message: "First and last coordinate positions must be identical (to close the linear ring aka polygon).",
481
+ });
482
+ }
483
+ // Check if any coordinate is (0, 0)
484
+ coords.forEach((coord, index) => {
485
+ if (coord[0] === 0 && coord[1] === 0) {
486
+ ctx.addIssue({
487
+ code: zod.z.ZodIssueCode.custom,
488
+ message: `Coordinate at index ${index} should not be (0, 0). This is likely not a valid coordinate.`,
489
+ });
490
+ }
491
+ });
492
+ // Check if consecutive points are identical (excluding first and last)
493
+ for (let i = 1; i < coords.length - 1; i++) {
494
+ if (JSON.stringify(coords[i]) === JSON.stringify(coords[i - 1])) {
495
+ ctx.addIssue({
496
+ code: zod.z.ZodIssueCode.custom,
497
+ message: `Consecutive coordinates at index ${i - 1} and ${i} should not be identical.`,
498
+ });
499
+ }
500
+ }
501
+ });
502
+ /**
503
+ * Polygon geometry object.
504
+ * https://tools.ietf.org/html/rfc7946#section-3.1.6
505
+ */
506
+ const geoJsonPolygonSchema = zod.z.strictObject({
507
+ type: zod.z.literal("Polygon"),
508
+ coordinates: zod.z.array(geoJsonLinearRingSchema),
509
+ });
510
+ /**
511
+ * MultiPolygon geometry object.
512
+ * https://tools.ietf.org/html/rfc7946#section-3.1.7
513
+ */
514
+ const geoJsonMultiPolygonSchema = zod.z.strictObject({
515
+ type: zod.z.literal("MultiPolygon"),
516
+ coordinates: zod.z.array(zod.z.array(geoJsonLinearRingSchema)),
517
+ });
518
+ // The same for Geometry, GeometryCollection, GeoJsonProperties, Feature, FeatureCollection, etc.
519
+ const geoJsonGeometrySchema = zod.z.union([
520
+ geoJsonPointSchema,
521
+ geoJsonMultiPointSchema,
522
+ geoJsonLineStringSchema,
523
+ geoJsonMultiLineStringSchema,
524
+ geoJsonPolygonSchema,
525
+ geoJsonMultiPolygonSchema,
526
+ ]);
527
+ //* -------- Bbox -------- *//
528
+ /**
529
+ * 2D bounding box of the GeoJSON object.
530
+ * The value of the Bbox member is an array of length 4.
531
+ *
532
+ * [min_lon, min_lat, max_lon, max_lat]
533
+ */
534
+ const geoJsonBboxSchema = zod.z
535
+ .tuple([zod.z.number(), zod.z.number(), zod.z.number(), zod.z.number()])
536
+ .refine(([minLng, minLat, maxLng, maxLat]) => maxLng > minLng && maxLat > minLat, {
537
+ message: "Invalid bounding box: maxLng should be greater than minLng, and maxLat should be greater than minLat.",
538
+ });
539
+ //* -------- Extras -------- *//
540
+ /**
541
+ * Polygon geometry object that explicitly disallows holes.
542
+ * https://tools.ietf.org/html/rfc7946#section-3.1.6
543
+ */
544
+ const geoJsonPolygonNoHolesSchema = zod.z.strictObject({
545
+ type: zod.z.literal("Polygon"),
546
+ //uses tuple instead of array to enforce only 1 linear ring aka the polygon itself
547
+ coordinates: zod.z.tuple([geoJsonLinearRingSchema]),
548
+ });
549
+ /**
550
+ * A Polygon with exactly 5 points and 4 horizontal/vertical sides that form a normal rectangular box.
551
+ */
552
+ const geoJsonRectangularBoxPolygonSchema = zod.z
553
+ .strictObject({
554
+ type: zod.z.literal("Polygon"),
555
+ coordinates: zod.z.array(geoJsonLinearRingSchema),
556
+ })
557
+ .superRefine((data, ctx) => {
558
+ const coordinates = data.coordinates[0];
559
+ // Validate polygon has exactly 5 points
560
+ if ((coordinates === null || coordinates === void 0 ? void 0 : coordinates.length) !== 5) {
561
+ ctx.addIssue({
562
+ code: zod.z.ZodIssueCode.custom,
563
+ message: "Polygon must have exactly 5 coordinates to form a closed box.",
564
+ });
565
+ return;
566
+ }
567
+ // Check each side is either horizontal or vertical
568
+ for (let i = 0; i < 4; i++) {
569
+ const point1 = coordinates[i];
570
+ const point2 = coordinates[i + 1];
571
+ if (!point1 || !point2) {
572
+ ctx.addIssue({
573
+ code: zod.z.ZodIssueCode.custom,
574
+ message: "Each coordinate must be a defined point.",
575
+ });
576
+ return;
577
+ }
578
+ const [x1, y1] = point1;
579
+ const [x2, y2] = point2;
580
+ // Ensure each line segment is either horizontal or vertical
581
+ if (x1 !== x2 && y1 !== y2) {
582
+ ctx.addIssue({
583
+ code: zod.z.ZodIssueCode.custom,
584
+ message: "Polygon sides must be horizontal or vertical to form a box shape.",
585
+ });
586
+ return;
587
+ }
588
+ }
589
+ });
590
+
591
+ const EARTH_RADIUS = 6378137; // Earth’s mean radius in meters
416
592
  /**
417
593
  * @description Extracts a point coordinate from a GeoJSON object.
418
594
  * @param geoObject A GeoJSON object.
@@ -459,6 +635,87 @@ const getMultipleCoordinatesFromGeoJsonObject = (geoObject) => {
459
635
  throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
460
636
  }
461
637
  };
638
+ /**
639
+ * @description Creates a polygon from a bounding box.
640
+ */
641
+ const getPolygonFromBbox = (bbox) => {
642
+ const [minLon, minLat, maxLon, maxLat] = bbox;
643
+ return {
644
+ type: "Polygon",
645
+ coordinates: [
646
+ [
647
+ [minLon, minLat],
648
+ [maxLon, minLat],
649
+ [maxLon, maxLat],
650
+ [minLon, maxLat],
651
+ [minLon, minLat],
652
+ ],
653
+ ],
654
+ };
655
+ };
656
+ /**
657
+ * @description Creates a round polygon from a point and a radius.
658
+ */
659
+ const getPolygonFromPointAndRadius = (point, radius) => {
660
+ const [lon, lat] = point.coordinates;
661
+ // Adjust the number of points based on radius (resolution)
662
+ const pointsCount = Math.max(32, Math.floor(radius / 100)); // More points for larger radius
663
+ const angleStep = (2 * Math.PI) / pointsCount;
664
+ const coordinates = [];
665
+ for (let i = 0; i <= pointsCount; i++) {
666
+ const angle = i * angleStep;
667
+ // Calculate offset in latitude and longitude
668
+ const deltaLat = (radius / EARTH_RADIUS) * (180 / Math.PI);
669
+ const deltaLon = deltaLat / Math.cos((lat * Math.PI) / 180);
670
+ // Calculate new coordinates based on angle
671
+ const newLat = lat + deltaLat * Math.sin(angle);
672
+ const newLon = lon + deltaLon * Math.cos(angle);
673
+ coordinates.push([newLon, newLat]);
674
+ }
675
+ return {
676
+ type: "Polygon",
677
+ coordinates: [coordinates],
678
+ };
679
+ };
680
+ /**
681
+ * @description Creates a TU bounding box from a GeoJson Polygon.
682
+ */
683
+ const getBoundingBoxFromGeoJsonPolygon = (polygon) => {
684
+ const points = polygon.coordinates[0];
685
+ const latitudes = points === null || points === void 0 ? void 0 : points.map(point => point[1]);
686
+ const longitudes = points === null || points === void 0 ? void 0 : points.map(point => point[0]);
687
+ if (!latitudes || !longitudes) {
688
+ return undefined;
689
+ }
690
+ return {
691
+ nw: {
692
+ latitude: Math.max(...latitudes),
693
+ longitude: Math.min(...longitudes),
694
+ },
695
+ se: {
696
+ latitude: Math.min(...latitudes),
697
+ longitude: Math.max(...longitudes),
698
+ },
699
+ };
700
+ };
701
+ /**
702
+ * @description Creates a GeoJSON Polygon from a TU bounding box.
703
+ */
704
+ const getGeoJsonPolygonFromBoundingBox = (boundingBox) => {
705
+ const { nw, se } = boundingBox;
706
+ return {
707
+ type: "Polygon",
708
+ coordinates: [
709
+ [
710
+ [nw.longitude, nw.latitude], // Northwest corner
711
+ [se.longitude, nw.latitude], // Northeast corner
712
+ [se.longitude, se.latitude], // Southeast corner
713
+ [nw.longitude, se.latitude], // Southwest corner
714
+ [nw.longitude, nw.latitude], // Close the loop back to Northwest corner
715
+ ],
716
+ ],
717
+ };
718
+ };
462
719
 
463
720
  /**
464
721
  * Group an array of items by a key.
@@ -1107,6 +1364,7 @@ const uuidv4 = () => {
1107
1364
  const uuidv5 = (name, namespace) => uuid.v5(name, namespace);
1108
1365
 
1109
1366
  exports.DateTimeFormat = DateTimeFormat;
1367
+ exports.EARTH_RADIUS = EARTH_RADIUS;
1110
1368
  exports.align = align;
1111
1369
  exports.alphabeticallySort = alphabeticallySort;
1112
1370
  exports.arrayLengthCompare = arrayLengthCompare;
@@ -1128,12 +1386,28 @@ exports.filterByMultiple = filterByMultiple;
1128
1386
  exports.formatAddress = formatAddress;
1129
1387
  exports.formatCoordinates = formatCoordinates;
1130
1388
  exports.fuzzySearch = fuzzySearch;
1389
+ exports.geoJsonBboxSchema = geoJsonBboxSchema;
1390
+ exports.geoJsonGeometrySchema = geoJsonGeometrySchema;
1391
+ exports.geoJsonLineStringSchema = geoJsonLineStringSchema;
1392
+ exports.geoJsonLinearRingSchema = geoJsonLinearRingSchema;
1393
+ exports.geoJsonMultiLineStringSchema = geoJsonMultiLineStringSchema;
1394
+ exports.geoJsonMultiPointSchema = geoJsonMultiPointSchema;
1395
+ exports.geoJsonMultiPolygonSchema = geoJsonMultiPolygonSchema;
1396
+ exports.geoJsonPointSchema = geoJsonPointSchema;
1397
+ exports.geoJsonPolygonNoHolesSchema = geoJsonPolygonNoHolesSchema;
1398
+ exports.geoJsonPolygonSchema = geoJsonPolygonSchema;
1399
+ exports.geoJsonPositionSchema = geoJsonPositionSchema;
1400
+ exports.geoJsonRectangularBoxPolygonSchema = geoJsonRectangularBoxPolygonSchema;
1401
+ exports.getBoundingBoxFromGeoJsonPolygon = getBoundingBoxFromGeoJsonPolygon;
1131
1402
  exports.getDifferenceBetweenDates = getDifferenceBetweenDates;
1132
1403
  exports.getEndOfDay = getEndOfDay;
1133
1404
  exports.getFirstLevelObjectPropertyDifferences = getFirstLevelObjectPropertyDifferences;
1405
+ exports.getGeoJsonPolygonFromBoundingBox = getGeoJsonPolygonFromBoundingBox;
1134
1406
  exports.getISOStringFromDate = getISOStringFromDate;
1135
1407
  exports.getMultipleCoordinatesFromGeoJsonObject = getMultipleCoordinatesFromGeoJsonObject;
1136
1408
  exports.getPointCoordinateFromGeoJsonObject = getPointCoordinateFromGeoJsonObject;
1409
+ exports.getPolygonFromBbox = getPolygonFromBbox;
1410
+ exports.getPolygonFromPointAndRadius = getPolygonFromPointAndRadius;
1137
1411
  exports.getResizedDimensions = getResizedDimensions;
1138
1412
  exports.getStartOfDay = getStartOfDay;
1139
1413
  exports.groupBy = groupBy;
package/index.esm.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { z } from 'zod';
1
2
  import { v3, v4, v5 } from 'uuid';
2
3
 
3
4
  /**
@@ -411,6 +412,181 @@ searchTerm) => {
411
412
  .every(term => filterableProps(asset).some(value => value.toLowerCase().includes(term.toLowerCase()))));
412
413
  };
413
414
 
415
+ // * NOTE: For simplicity these tools are built for 2D coordinate space only!
416
+ /**
417
+ * A Position is an array of coordinates. [x, y]
418
+ * https://tools.ietf.org/html/rfc7946#section-3.1.1
419
+ */
420
+ const geoJsonPositionSchema = z.tuple([z.number(), z.number()]);
421
+ /**
422
+ * Point geometry object.
423
+ * https://tools.ietf.org/html/rfc7946#section-3.1.2
424
+ */
425
+ const geoJsonPointSchema = z.strictObject({
426
+ type: z.literal("Point"),
427
+ coordinates: geoJsonPositionSchema,
428
+ });
429
+ /**
430
+ * MultiPoint geometry object.
431
+ * https://tools.ietf.org/html/rfc7946#section-3.1.3
432
+ */
433
+ const geoJsonMultiPointSchema = z.strictObject({
434
+ type: z.literal("MultiPoint"),
435
+ coordinates: z.array(geoJsonPositionSchema),
436
+ });
437
+ /**
438
+ * LineString geometry object.
439
+ * Minimum length of 2 positions.
440
+ * https://tools.ietf.org/html/rfc7946#section-3.1.4
441
+ */
442
+ const geoJsonLineStringSchema = z.strictObject({
443
+ type: z.literal("LineString"),
444
+ coordinates: z.array(geoJsonPositionSchema).min(2),
445
+ });
446
+ /**
447
+ * MultiLineString geometry object.
448
+ * https://tools.ietf.org/html/rfc7946#section-3.1.5
449
+ */
450
+ const geoJsonMultiLineStringSchema = z.strictObject({
451
+ type: z.literal("MultiLineString"),
452
+ coordinates: z.array(z.array(geoJsonPositionSchema)),
453
+ });
454
+ /**
455
+ * Helper type for reuse across polygon schemas.
456
+ *
457
+ * - A linear ring is a closed LineString with four or more positions.
458
+ * - The first and last positions are equivalent, and they MUST contain
459
+ identical values; their representation SHOULD also be identical
460
+ * - A linear ring is the boundary of a surface or the boundary of a
461
+ hole in a surface
462
+ * - A linear ring MUST follow the right-hand rule with respect to the
463
+ area it bounds, i.e., exterior rings are counterclockwise, and
464
+ holes are clockwise
465
+ */
466
+ const geoJsonLinearRingSchema = z
467
+ .array(geoJsonPositionSchema)
468
+ .min(4, {
469
+ 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)",
470
+ })
471
+ .superRefine((coords, ctx) => {
472
+ const first = coords[0];
473
+ const last = coords[coords.length - 1];
474
+ // Check if first and last coordinates match
475
+ if (JSON.stringify(first) !== JSON.stringify(last)) {
476
+ ctx.addIssue({
477
+ code: z.ZodIssueCode.custom,
478
+ message: "First and last coordinate positions must be identical (to close the linear ring aka polygon).",
479
+ });
480
+ }
481
+ // Check if any coordinate is (0, 0)
482
+ coords.forEach((coord, index) => {
483
+ if (coord[0] === 0 && coord[1] === 0) {
484
+ ctx.addIssue({
485
+ code: z.ZodIssueCode.custom,
486
+ message: `Coordinate at index ${index} should not be (0, 0). This is likely not a valid coordinate.`,
487
+ });
488
+ }
489
+ });
490
+ // Check if consecutive points are identical (excluding first and last)
491
+ for (let i = 1; i < coords.length - 1; i++) {
492
+ if (JSON.stringify(coords[i]) === JSON.stringify(coords[i - 1])) {
493
+ ctx.addIssue({
494
+ code: z.ZodIssueCode.custom,
495
+ message: `Consecutive coordinates at index ${i - 1} and ${i} should not be identical.`,
496
+ });
497
+ }
498
+ }
499
+ });
500
+ /**
501
+ * Polygon geometry object.
502
+ * https://tools.ietf.org/html/rfc7946#section-3.1.6
503
+ */
504
+ const geoJsonPolygonSchema = z.strictObject({
505
+ type: z.literal("Polygon"),
506
+ coordinates: z.array(geoJsonLinearRingSchema),
507
+ });
508
+ /**
509
+ * MultiPolygon geometry object.
510
+ * https://tools.ietf.org/html/rfc7946#section-3.1.7
511
+ */
512
+ const geoJsonMultiPolygonSchema = z.strictObject({
513
+ type: z.literal("MultiPolygon"),
514
+ coordinates: z.array(z.array(geoJsonLinearRingSchema)),
515
+ });
516
+ // The same for Geometry, GeometryCollection, GeoJsonProperties, Feature, FeatureCollection, etc.
517
+ const geoJsonGeometrySchema = z.union([
518
+ geoJsonPointSchema,
519
+ geoJsonMultiPointSchema,
520
+ geoJsonLineStringSchema,
521
+ geoJsonMultiLineStringSchema,
522
+ geoJsonPolygonSchema,
523
+ geoJsonMultiPolygonSchema,
524
+ ]);
525
+ //* -------- Bbox -------- *//
526
+ /**
527
+ * 2D bounding box of the GeoJSON object.
528
+ * The value of the Bbox member is an array of length 4.
529
+ *
530
+ * [min_lon, min_lat, max_lon, max_lat]
531
+ */
532
+ const geoJsonBboxSchema = z
533
+ .tuple([z.number(), z.number(), z.number(), z.number()])
534
+ .refine(([minLng, minLat, maxLng, maxLat]) => maxLng > minLng && maxLat > minLat, {
535
+ message: "Invalid bounding box: maxLng should be greater than minLng, and maxLat should be greater than minLat.",
536
+ });
537
+ //* -------- Extras -------- *//
538
+ /**
539
+ * Polygon geometry object that explicitly disallows holes.
540
+ * https://tools.ietf.org/html/rfc7946#section-3.1.6
541
+ */
542
+ const geoJsonPolygonNoHolesSchema = z.strictObject({
543
+ type: z.literal("Polygon"),
544
+ //uses tuple instead of array to enforce only 1 linear ring aka the polygon itself
545
+ coordinates: z.tuple([geoJsonLinearRingSchema]),
546
+ });
547
+ /**
548
+ * A Polygon with exactly 5 points and 4 horizontal/vertical sides that form a normal rectangular box.
549
+ */
550
+ const geoJsonRectangularBoxPolygonSchema = z
551
+ .strictObject({
552
+ type: z.literal("Polygon"),
553
+ coordinates: z.array(geoJsonLinearRingSchema),
554
+ })
555
+ .superRefine((data, ctx) => {
556
+ const coordinates = data.coordinates[0];
557
+ // Validate polygon has exactly 5 points
558
+ if ((coordinates === null || coordinates === void 0 ? void 0 : coordinates.length) !== 5) {
559
+ ctx.addIssue({
560
+ code: z.ZodIssueCode.custom,
561
+ message: "Polygon must have exactly 5 coordinates to form a closed box.",
562
+ });
563
+ return;
564
+ }
565
+ // Check each side is either horizontal or vertical
566
+ for (let i = 0; i < 4; i++) {
567
+ const point1 = coordinates[i];
568
+ const point2 = coordinates[i + 1];
569
+ if (!point1 || !point2) {
570
+ ctx.addIssue({
571
+ code: z.ZodIssueCode.custom,
572
+ message: "Each coordinate must be a defined point.",
573
+ });
574
+ return;
575
+ }
576
+ const [x1, y1] = point1;
577
+ const [x2, y2] = point2;
578
+ // Ensure each line segment is either horizontal or vertical
579
+ if (x1 !== x2 && y1 !== y2) {
580
+ ctx.addIssue({
581
+ code: z.ZodIssueCode.custom,
582
+ message: "Polygon sides must be horizontal or vertical to form a box shape.",
583
+ });
584
+ return;
585
+ }
586
+ }
587
+ });
588
+
589
+ const EARTH_RADIUS = 6378137; // Earth’s mean radius in meters
414
590
  /**
415
591
  * @description Extracts a point coordinate from a GeoJSON object.
416
592
  * @param geoObject A GeoJSON object.
@@ -457,6 +633,87 @@ const getMultipleCoordinatesFromGeoJsonObject = (geoObject) => {
457
633
  throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
458
634
  }
459
635
  };
636
+ /**
637
+ * @description Creates a polygon from a bounding box.
638
+ */
639
+ const getPolygonFromBbox = (bbox) => {
640
+ const [minLon, minLat, maxLon, maxLat] = bbox;
641
+ return {
642
+ type: "Polygon",
643
+ coordinates: [
644
+ [
645
+ [minLon, minLat],
646
+ [maxLon, minLat],
647
+ [maxLon, maxLat],
648
+ [minLon, maxLat],
649
+ [minLon, minLat],
650
+ ],
651
+ ],
652
+ };
653
+ };
654
+ /**
655
+ * @description Creates a round polygon from a point and a radius.
656
+ */
657
+ const getPolygonFromPointAndRadius = (point, radius) => {
658
+ const [lon, lat] = point.coordinates;
659
+ // Adjust the number of points based on radius (resolution)
660
+ const pointsCount = Math.max(32, Math.floor(radius / 100)); // More points for larger radius
661
+ const angleStep = (2 * Math.PI) / pointsCount;
662
+ const coordinates = [];
663
+ for (let i = 0; i <= pointsCount; i++) {
664
+ const angle = i * angleStep;
665
+ // Calculate offset in latitude and longitude
666
+ const deltaLat = (radius / EARTH_RADIUS) * (180 / Math.PI);
667
+ const deltaLon = deltaLat / Math.cos((lat * Math.PI) / 180);
668
+ // Calculate new coordinates based on angle
669
+ const newLat = lat + deltaLat * Math.sin(angle);
670
+ const newLon = lon + deltaLon * Math.cos(angle);
671
+ coordinates.push([newLon, newLat]);
672
+ }
673
+ return {
674
+ type: "Polygon",
675
+ coordinates: [coordinates],
676
+ };
677
+ };
678
+ /**
679
+ * @description Creates a TU bounding box from a GeoJson Polygon.
680
+ */
681
+ const getBoundingBoxFromGeoJsonPolygon = (polygon) => {
682
+ const points = polygon.coordinates[0];
683
+ const latitudes = points === null || points === void 0 ? void 0 : points.map(point => point[1]);
684
+ const longitudes = points === null || points === void 0 ? void 0 : points.map(point => point[0]);
685
+ if (!latitudes || !longitudes) {
686
+ return undefined;
687
+ }
688
+ return {
689
+ nw: {
690
+ latitude: Math.max(...latitudes),
691
+ longitude: Math.min(...longitudes),
692
+ },
693
+ se: {
694
+ latitude: Math.min(...latitudes),
695
+ longitude: Math.max(...longitudes),
696
+ },
697
+ };
698
+ };
699
+ /**
700
+ * @description Creates a GeoJSON Polygon from a TU bounding box.
701
+ */
702
+ const getGeoJsonPolygonFromBoundingBox = (boundingBox) => {
703
+ const { nw, se } = boundingBox;
704
+ return {
705
+ type: "Polygon",
706
+ coordinates: [
707
+ [
708
+ [nw.longitude, nw.latitude], // Northwest corner
709
+ [se.longitude, nw.latitude], // Northeast corner
710
+ [se.longitude, se.latitude], // Southeast corner
711
+ [nw.longitude, se.latitude], // Southwest corner
712
+ [nw.longitude, nw.latitude], // Close the loop back to Northwest corner
713
+ ],
714
+ ],
715
+ };
716
+ };
460
717
 
461
718
  /**
462
719
  * Group an array of items by a key.
@@ -1104,4 +1361,4 @@ const uuidv4 = () => {
1104
1361
  */
1105
1362
  const uuidv5 = (name, namespace) => v5(name, namespace);
1106
1363
 
1107
- export { DateTimeFormat, HoursAndMinutesFormat, align, alphabeticallySort, arrayLengthCompare, arrayNotEmpty, booleanCompare, capitalize, convertBlobToBase64, convertMetersToYards, convertYardsToMeters, dateCompare, deleteUndefinedKeys, difference, doNothing, enumFromValue, enumFromValueTypesafe, enumOrUndefinedFromValue, exhaustiveCheck, filterByMultiple, formatAddress, formatCoordinates, fuzzySearch, getDifferenceBetweenDates, getEndOfDay, getFirstLevelObjectPropertyDifferences, getISOStringFromDate, getMultipleCoordinatesFromGeoJsonObject, getPointCoordinateFromGeoJsonObject, getResizedDimensions, getStartOfDay, groupBy, groupTinyDataToOthers, hourIntervals, intersection, isArrayEqual, isSorted, isUUID, isValidImage, nonNullable, numberCompare, numberCompareUnknownAfterHighest, objNotEmpty, objectEntries, objectFromEntries, objectKeys, objectValues, pick, removeLeftPadding, resizeBlob, resizeImage, size, stringCompare, stringCompareFromKey, stringNaturalCompare, stripHiddenCharacters, titleCase, toID, toIDs, toUUID, trimIds, trimPath, truthy, unionArraysByKey, uuidv3, uuidv4, uuidv5 };
1364
+ export { DateTimeFormat, EARTH_RADIUS, HoursAndMinutesFormat, align, alphabeticallySort, arrayLengthCompare, arrayNotEmpty, booleanCompare, capitalize, convertBlobToBase64, convertMetersToYards, convertYardsToMeters, dateCompare, deleteUndefinedKeys, difference, doNothing, enumFromValue, enumFromValueTypesafe, enumOrUndefinedFromValue, exhaustiveCheck, filterByMultiple, formatAddress, formatCoordinates, fuzzySearch, geoJsonBboxSchema, geoJsonGeometrySchema, geoJsonLineStringSchema, geoJsonLinearRingSchema, geoJsonMultiLineStringSchema, geoJsonMultiPointSchema, geoJsonMultiPolygonSchema, geoJsonPointSchema, geoJsonPolygonNoHolesSchema, geoJsonPolygonSchema, geoJsonPositionSchema, geoJsonRectangularBoxPolygonSchema, getBoundingBoxFromGeoJsonPolygon, getDifferenceBetweenDates, getEndOfDay, getFirstLevelObjectPropertyDifferences, getGeoJsonPolygonFromBoundingBox, getISOStringFromDate, getMultipleCoordinatesFromGeoJsonObject, getPointCoordinateFromGeoJsonObject, getPolygonFromBbox, getPolygonFromPointAndRadius, getResizedDimensions, getStartOfDay, groupBy, groupTinyDataToOthers, hourIntervals, intersection, isArrayEqual, isSorted, isUUID, isValidImage, nonNullable, numberCompare, numberCompareUnknownAfterHighest, objNotEmpty, objectEntries, objectFromEntries, objectKeys, objectValues, pick, removeLeftPadding, resizeBlob, resizeImage, size, stringCompare, stringCompareFromKey, stringNaturalCompare, stripHiddenCharacters, titleCase, toID, toIDs, toUUID, trimIds, trimPath, truthy, unionArraysByKey, uuidv3, uuidv4, uuidv5 };
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@trackunit/shared-utils",
3
- "version": "0.0.81",
3
+ "version": "0.0.82",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
7
7
  "node": ">=20.x"
8
8
  },
9
9
  "dependencies": {
10
- "uuid": "^10.0.0"
10
+ "uuid": "^10.0.0",
11
+ "zod": "3.22.4"
11
12
  },
12
13
  "module": "./index.esm.js",
13
14
  "main": "./index.cjs.js",
@@ -0,0 +1,210 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * A Position is an array of coordinates. [x, y]
4
+ * https://tools.ietf.org/html/rfc7946#section-3.1.1
5
+ */
6
+ export declare const geoJsonPositionSchema: z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>;
7
+ export type GeoJsonPosition = z.infer<typeof geoJsonPositionSchema>;
8
+ /**
9
+ * Point geometry object.
10
+ * https://tools.ietf.org/html/rfc7946#section-3.1.2
11
+ */
12
+ export declare const geoJsonPointSchema: z.ZodObject<{
13
+ type: z.ZodLiteral<"Point">;
14
+ coordinates: z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>;
15
+ }, "strict", z.ZodTypeAny, {
16
+ type: "Point";
17
+ coordinates: [number, number];
18
+ }, {
19
+ type: "Point";
20
+ coordinates: [number, number];
21
+ }>;
22
+ export type GeoJsonPoint = z.infer<typeof geoJsonPointSchema>;
23
+ /**
24
+ * MultiPoint geometry object.
25
+ * https://tools.ietf.org/html/rfc7946#section-3.1.3
26
+ */
27
+ export declare const geoJsonMultiPointSchema: z.ZodObject<{
28
+ type: z.ZodLiteral<"MultiPoint">;
29
+ coordinates: z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>, "many">;
30
+ }, "strict", z.ZodTypeAny, {
31
+ type: "MultiPoint";
32
+ coordinates: [number, number][];
33
+ }, {
34
+ type: "MultiPoint";
35
+ coordinates: [number, number][];
36
+ }>;
37
+ export type GeoJsonMultiPoint = z.infer<typeof geoJsonMultiPointSchema>;
38
+ /**
39
+ * LineString geometry object.
40
+ * Minimum length of 2 positions.
41
+ * https://tools.ietf.org/html/rfc7946#section-3.1.4
42
+ */
43
+ export declare const geoJsonLineStringSchema: z.ZodObject<{
44
+ type: z.ZodLiteral<"LineString">;
45
+ coordinates: z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>, "many">;
46
+ }, "strict", z.ZodTypeAny, {
47
+ type: "LineString";
48
+ coordinates: [number, number][];
49
+ }, {
50
+ type: "LineString";
51
+ coordinates: [number, number][];
52
+ }>;
53
+ export type GeoJsonLineString = z.infer<typeof geoJsonLineStringSchema>;
54
+ /**
55
+ * MultiLineString geometry object.
56
+ * https://tools.ietf.org/html/rfc7946#section-3.1.5
57
+ */
58
+ export declare const geoJsonMultiLineStringSchema: z.ZodObject<{
59
+ type: z.ZodLiteral<"MultiLineString">;
60
+ coordinates: z.ZodArray<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>, "many">, "many">;
61
+ }, "strict", z.ZodTypeAny, {
62
+ type: "MultiLineString";
63
+ coordinates: [number, number][][];
64
+ }, {
65
+ type: "MultiLineString";
66
+ coordinates: [number, number][][];
67
+ }>;
68
+ export type GeoJsonMultiLineString = z.infer<typeof geoJsonMultiLineStringSchema>;
69
+ /**
70
+ * Helper type for reuse across polygon schemas.
71
+ *
72
+ * - A linear ring is a closed LineString with four or more positions.
73
+ * - The first and last positions are equivalent, and they MUST contain
74
+ identical values; their representation SHOULD also be identical
75
+ * - A linear ring is the boundary of a surface or the boundary of a
76
+ hole in a surface
77
+ * - A linear ring MUST follow the right-hand rule with respect to the
78
+ area it bounds, i.e., exterior rings are counterclockwise, and
79
+ holes are clockwise
80
+ */
81
+ export declare const geoJsonLinearRingSchema: z.ZodEffects<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>, "many">, [number, number][], [number, number][]>;
82
+ export type GeoJsonLinearRing = z.infer<typeof geoJsonLinearRingSchema>;
83
+ /**
84
+ * Polygon geometry object.
85
+ * https://tools.ietf.org/html/rfc7946#section-3.1.6
86
+ */
87
+ export declare const geoJsonPolygonSchema: z.ZodObject<{
88
+ type: z.ZodLiteral<"Polygon">;
89
+ coordinates: z.ZodArray<z.ZodEffects<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>, "many">, [number, number][], [number, number][]>, "many">;
90
+ }, "strict", z.ZodTypeAny, {
91
+ type: "Polygon";
92
+ coordinates: [number, number][][];
93
+ }, {
94
+ type: "Polygon";
95
+ coordinates: [number, number][][];
96
+ }>;
97
+ export type GeoJsonPolygon = z.infer<typeof geoJsonPolygonSchema>;
98
+ /**
99
+ * MultiPolygon geometry object.
100
+ * https://tools.ietf.org/html/rfc7946#section-3.1.7
101
+ */
102
+ export declare const geoJsonMultiPolygonSchema: z.ZodObject<{
103
+ type: z.ZodLiteral<"MultiPolygon">;
104
+ coordinates: z.ZodArray<z.ZodArray<z.ZodEffects<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>, "many">, [number, number][], [number, number][]>, "many">, "many">;
105
+ }, "strict", z.ZodTypeAny, {
106
+ type: "MultiPolygon";
107
+ coordinates: [number, number][][][];
108
+ }, {
109
+ type: "MultiPolygon";
110
+ coordinates: [number, number][][][];
111
+ }>;
112
+ export type GeoJsonMultiPolygon = z.infer<typeof geoJsonMultiPolygonSchema>;
113
+ export declare const geoJsonGeometrySchema: z.ZodUnion<[z.ZodObject<{
114
+ type: z.ZodLiteral<"Point">;
115
+ coordinates: z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>;
116
+ }, "strict", z.ZodTypeAny, {
117
+ type: "Point";
118
+ coordinates: [number, number];
119
+ }, {
120
+ type: "Point";
121
+ coordinates: [number, number];
122
+ }>, z.ZodObject<{
123
+ type: z.ZodLiteral<"MultiPoint">;
124
+ coordinates: z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>, "many">;
125
+ }, "strict", z.ZodTypeAny, {
126
+ type: "MultiPoint";
127
+ coordinates: [number, number][];
128
+ }, {
129
+ type: "MultiPoint";
130
+ coordinates: [number, number][];
131
+ }>, z.ZodObject<{
132
+ type: z.ZodLiteral<"LineString">;
133
+ coordinates: z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>, "many">;
134
+ }, "strict", z.ZodTypeAny, {
135
+ type: "LineString";
136
+ coordinates: [number, number][];
137
+ }, {
138
+ type: "LineString";
139
+ coordinates: [number, number][];
140
+ }>, z.ZodObject<{
141
+ type: z.ZodLiteral<"MultiLineString">;
142
+ coordinates: z.ZodArray<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>, "many">, "many">;
143
+ }, "strict", z.ZodTypeAny, {
144
+ type: "MultiLineString";
145
+ coordinates: [number, number][][];
146
+ }, {
147
+ type: "MultiLineString";
148
+ coordinates: [number, number][][];
149
+ }>, z.ZodObject<{
150
+ type: z.ZodLiteral<"Polygon">;
151
+ coordinates: z.ZodArray<z.ZodEffects<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>, "many">, [number, number][], [number, number][]>, "many">;
152
+ }, "strict", z.ZodTypeAny, {
153
+ type: "Polygon";
154
+ coordinates: [number, number][][];
155
+ }, {
156
+ type: "Polygon";
157
+ coordinates: [number, number][][];
158
+ }>, z.ZodObject<{
159
+ type: z.ZodLiteral<"MultiPolygon">;
160
+ coordinates: z.ZodArray<z.ZodArray<z.ZodEffects<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>, "many">, [number, number][], [number, number][]>, "many">, "many">;
161
+ }, "strict", z.ZodTypeAny, {
162
+ type: "MultiPolygon";
163
+ coordinates: [number, number][][][];
164
+ }, {
165
+ type: "MultiPolygon";
166
+ coordinates: [number, number][][][];
167
+ }>]>;
168
+ export type GeoJsonGeometry = z.infer<typeof geoJsonGeometrySchema>;
169
+ /**
170
+ * 2D bounding box of the GeoJSON object.
171
+ * The value of the Bbox member is an array of length 4.
172
+ *
173
+ * [min_lon, min_lat, max_lon, max_lat]
174
+ */
175
+ export declare const geoJsonBboxSchema: z.ZodEffects<z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber, z.ZodNumber], null>, [number, number, number, number], [number, number, number, number]>;
176
+ export type GeoJsonBbox = z.infer<typeof geoJsonBboxSchema>;
177
+ /**
178
+ * Polygon geometry object that explicitly disallows holes.
179
+ * https://tools.ietf.org/html/rfc7946#section-3.1.6
180
+ */
181
+ export declare const geoJsonPolygonNoHolesSchema: z.ZodObject<{
182
+ type: z.ZodLiteral<"Polygon">;
183
+ coordinates: z.ZodTuple<[z.ZodEffects<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>, "many">, [number, number][], [number, number][]>], null>;
184
+ }, "strict", z.ZodTypeAny, {
185
+ type: "Polygon";
186
+ coordinates: [[number, number][]];
187
+ }, {
188
+ type: "Polygon";
189
+ coordinates: [[number, number][]];
190
+ }>;
191
+ export type GeoJsonPolygonNoHoles = z.infer<typeof geoJsonPolygonNoHolesSchema>;
192
+ /**
193
+ * A Polygon with exactly 5 points and 4 horizontal/vertical sides that form a normal rectangular box.
194
+ */
195
+ export declare const geoJsonRectangularBoxPolygonSchema: z.ZodEffects<z.ZodObject<{
196
+ type: z.ZodLiteral<"Polygon">;
197
+ coordinates: z.ZodArray<z.ZodEffects<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>, "many">, [number, number][], [number, number][]>, "many">;
198
+ }, "strict", z.ZodTypeAny, {
199
+ type: "Polygon";
200
+ coordinates: [number, number][][];
201
+ }, {
202
+ type: "Polygon";
203
+ coordinates: [number, number][][];
204
+ }>, {
205
+ type: "Polygon";
206
+ coordinates: [number, number][][];
207
+ }, {
208
+ type: "Polygon";
209
+ coordinates: [number, number][][];
210
+ }>;
@@ -1,7 +1,13 @@
1
+ import { GeoJsonBbox, GeoJsonPoint, GeoJsonPolygon } from "./GeoJsonSchemas";
2
+ export declare const EARTH_RADIUS = 6378137;
1
3
  interface PointCoordinate {
2
4
  longitude: number;
3
5
  latitude: number;
4
6
  }
7
+ interface BoundingBox {
8
+ nw: PointCoordinate;
9
+ se: PointCoordinate;
10
+ }
5
11
  interface GeoJSONGeometry {
6
12
  type?: unknown;
7
13
  coordinates?: number[] | number[][] | null;
@@ -23,4 +29,20 @@ export declare const getPointCoordinateFromGeoJsonObject: (geoObject: GeoJsonFea
23
29
  * @example getMultipleCoordinatesFromGeoJsonObject({ type: "Point", coordinates: [1, 2] }) // [{ longitude: 1, latitude: 2 }]
24
30
  */
25
31
  export declare const getMultipleCoordinatesFromGeoJsonObject: (geoObject: GeoJsonFeature | GeoJSONGeometry | undefined | null) => PointCoordinate[] | undefined;
32
+ /**
33
+ * @description Creates a polygon from a bounding box.
34
+ */
35
+ export declare const getPolygonFromBbox: (bbox: GeoJsonBbox) => GeoJsonPolygon;
36
+ /**
37
+ * @description Creates a round polygon from a point and a radius.
38
+ */
39
+ export declare const getPolygonFromPointAndRadius: (point: GeoJsonPoint, radius: number) => GeoJsonPolygon;
40
+ /**
41
+ * @description Creates a TU bounding box from a GeoJson Polygon.
42
+ */
43
+ export declare const getBoundingBoxFromGeoJsonPolygon: (polygon: GeoJsonPolygon) => BoundingBox | undefined;
44
+ /**
45
+ * @description Creates a GeoJSON Polygon from a TU bounding box.
46
+ */
47
+ export declare const getGeoJsonPolygonFromBoundingBox: (boundingBox: BoundingBox) => GeoJsonPolygon;
26
48
  export {};
package/src/index.d.ts CHANGED
@@ -9,7 +9,8 @@ export * from "./enumUtils";
9
9
  export * from "./exhaustiveCheck";
10
10
  export * from "./fastArrayOperations";
11
11
  export * from "./filter";
12
- export * from "./GeoJsonUtils";
12
+ export * from "./GeoJson/GeoJsonSchemas";
13
+ export * from "./GeoJson/GeoJsonUtils";
13
14
  export * from "./groupBy/groupBy";
14
15
  export * from "./GroupingUtility";
15
16
  export * from "./idUtils";