@trackunit/shared-utils 0.0.83 → 0.0.85

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,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var zod = require('zod');
4
+ var polygonClipping = require('polygon-clipping');
4
5
  var uuid = require('uuid');
5
6
 
6
7
  /**
@@ -480,15 +481,6 @@ const geoJsonLinearRingSchema = zod.z
480
481
  message: "First and last coordinate positions must be identical (to close the linear ring aka polygon).",
481
482
  });
482
483
  }
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
484
  // Check if consecutive points are identical (excluding first and last)
493
485
  for (let i = 1; i < coords.length - 1; i++) {
494
486
  if (JSON.stringify(coords[i]) === JSON.stringify(coords[i - 1])) {
@@ -536,107 +528,10 @@ const geoJsonBboxSchema = zod.z
536
528
  .refine(([minLng, minLat, maxLng, maxLat]) => maxLng > minLng && maxLat > minLat, {
537
529
  message: "Invalid bounding box: maxLng should be greater than minLng, and maxLat should be greater than minLat.",
538
530
  });
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
531
 
591
532
  const EARTH_RADIUS = 6378137; // Earth’s mean radius in meters
592
533
  /**
593
- * @description Extracts a point coordinate from a GeoJSON object.
594
- * @param geoObject A GeoJSON object.
595
- * @returns {PointCoordinate} A point coordinate.
596
- */
597
- const getPointCoordinateFromGeoJsonObject = (geoObject) => {
598
- if (!geoObject) {
599
- return undefined;
600
- }
601
- else if ("geometry" in geoObject) {
602
- return getPointCoordinateFromGeoJsonObject(geoObject.geometry);
603
- }
604
- else if ("coordinates" in geoObject &&
605
- Array.isArray(geoObject.coordinates) &&
606
- typeof geoObject.coordinates[0] === "number" &&
607
- typeof geoObject.coordinates[1] === "number") {
608
- return { longitude: geoObject.coordinates[0], latitude: geoObject.coordinates[1] };
609
- }
610
- else {
611
- throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
612
- }
613
- };
614
- /**
615
- * @description Extracts multiple point coordinates from a GeoJSON object.
616
- * @param geoObject A GeoJSON object.
617
- * @returns {PointCoordinate[]} An array of point coordinates.
618
- * @example getMultipleCoordinatesFromGeoJsonObject({ type: "Point", coordinates: [1, 2] }) // [{ longitude: 1, latitude: 2 }]
619
- */
620
- const getMultipleCoordinatesFromGeoJsonObject = (geoObject) => {
621
- if (!geoObject) {
622
- return undefined;
623
- }
624
- else if ("geometry" in geoObject) {
625
- return getMultipleCoordinatesFromGeoJsonObject(geoObject.geometry);
626
- }
627
- else if ("coordinates" in geoObject &&
628
- Array.isArray(geoObject.coordinates) &&
629
- Array.isArray(geoObject.coordinates[0])) {
630
- // @ts-ignore - suppressImplicitAnyIndexErrors
631
- // We would not be here if element was not an array.
632
- return geoObject.coordinates.map(element => ({ longitude: element[0], latitude: element[1] }));
633
- }
634
- else {
635
- throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
636
- }
637
- };
638
- /**
639
- * @description Creates a polygon from a bounding box.
534
+ * @description Creates a polygon (with no holes) from a bounding box.
640
535
  */
641
536
  const getPolygonFromBbox = (bbox) => {
642
537
  const [minLon, minLat, maxLon, maxLat] = bbox;
@@ -653,6 +548,23 @@ const getPolygonFromBbox = (bbox) => {
653
548
  ],
654
549
  };
655
550
  };
551
+ /**
552
+ * @description Creates a bounding box from a GeoJSON Polygon.
553
+ */
554
+ const getBboxFromGeoJsonPolygon = (polygon) => {
555
+ const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
556
+ if (!polygonParsed.success) {
557
+ return null;
558
+ }
559
+ const points = polygonParsed.data.coordinates[0];
560
+ if (!points) {
561
+ // Should never happen since the schema checks for it
562
+ return null;
563
+ }
564
+ const latitudes = points.map(point => point[1]);
565
+ const longitudes = points.map(point => point[0]);
566
+ return [Math.min(...longitudes), Math.min(...latitudes), Math.max(...longitudes), Math.max(...latitudes)];
567
+ };
656
568
  /**
657
569
  * @description Creates a round polygon from a point and a radius.
658
570
  */
@@ -681,12 +593,17 @@ const getPolygonFromPointAndRadius = (point, radius) => {
681
593
  * @description Creates a TU bounding box from a GeoJson Polygon.
682
594
  */
683
595
  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;
596
+ const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
597
+ if (!polygonParsed.success) {
598
+ return null;
599
+ }
600
+ const points = polygonParsed.data.coordinates[0];
601
+ if (!points) {
602
+ // Should never happen since the schema checks for it
603
+ return null;
689
604
  }
605
+ const latitudes = points.map(point => point[1]);
606
+ const longitudes = points.map(point => point[0]);
690
607
  return {
691
608
  nw: {
692
609
  latitude: Math.max(...latitudes),
@@ -722,6 +639,268 @@ const getGeoJsonPolygonFromBoundingBox = (boundingBox) => {
722
639
  const getPointCoordinateFromGeoJsonPoint = (point) => {
723
640
  return { latitude: point.coordinates[1], longitude: point.coordinates[0] };
724
641
  };
642
+ /**
643
+ * @description Gets the extreme point of a polygon in a given direction.
644
+ * @param {object} params - The parameters object
645
+ * @param {GeoJsonPolygon} params.polygon - The polygon to get the extreme point from
646
+ * @param {("top" | "right" | "bottom" | "left")} params.direction - The direction to get the extreme point in
647
+ * @returns {GeoJsonPoint} The extreme point in the given direction
648
+ */
649
+ const getExtremeGeoJsonPointFromPolygon = ({ polygon, direction, }) => {
650
+ var _a, _b, _c;
651
+ const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
652
+ if (!polygonParsed.success) {
653
+ return null;
654
+ }
655
+ const firstPoint = (_a = polygonParsed.data.coordinates[0]) === null || _a === void 0 ? void 0 : _a[0];
656
+ if (!firstPoint) {
657
+ // Should never happen since the schema checks for it
658
+ return null;
659
+ }
660
+ const extremePosition = (_b = polygonParsed.data.coordinates[0]) === null || _b === void 0 ? void 0 : _b.reduce((extremePoint, currentPoint) => {
661
+ var _a, _b, _c, _d;
662
+ switch (direction) {
663
+ case "top":
664
+ 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;
665
+ case "right":
666
+ 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;
667
+ case "bottom":
668
+ 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;
669
+ case "left":
670
+ 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;
671
+ default: {
672
+ throw new Error(`${direction} is not known`);
673
+ }
674
+ }
675
+ }, (_c = polygonParsed.data.coordinates[0]) === null || _c === void 0 ? void 0 : _c[0]);
676
+ return extremePosition
677
+ ? {
678
+ type: "Point",
679
+ coordinates: extremePosition,
680
+ }
681
+ : null; // Should never happen since the schema checks for it
682
+ };
683
+ /**
684
+ * Checks if a position is inside a linear ring. On edge is considered inside.
685
+ */
686
+ const isGeoJsonPositionInLinearRing = ({ position, linearRing, }) => {
687
+ const linearRingParsed = geoJsonLinearRingSchema.safeParse(linearRing);
688
+ if (!linearRingParsed.success) {
689
+ return null;
690
+ }
691
+ let inside = false;
692
+ const [x, y] = position;
693
+ for (let i = 0, j = linearRingParsed.data.length - 1; i < linearRingParsed.data.length; j = i++) {
694
+ const point1 = linearRingParsed.data[i];
695
+ const point2 = linearRingParsed.data[j];
696
+ if (!point1 || !point2) {
697
+ continue;
698
+ }
699
+ const [xi, yi] = point1;
700
+ const [xj, yj] = point2;
701
+ const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
702
+ if (intersect) {
703
+ inside = !inside;
704
+ }
705
+ }
706
+ return inside;
707
+ };
708
+ /**
709
+ * @description Checks if a point is inside a polygon.
710
+ */
711
+ const isGeoJsonPointInPolygon = ({ point, polygon, }) => {
712
+ const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
713
+ if (!polygonParsed.success) {
714
+ return null;
715
+ }
716
+ return polygonParsed.data.coordinates.some(linearRing => isGeoJsonPositionInLinearRing({ position: point.coordinates, linearRing }));
717
+ };
718
+ /**
719
+ * Checks if polygon1 is fully contained within polygon2
720
+ */
721
+ const isFullyContainedInGeoJsonPolygon = (polygon1, polygon2) => {
722
+ const polygon1Parsed = geoJsonPolygonSchema.safeParse(polygon1);
723
+ const polygon2Parsed = geoJsonPolygonSchema.safeParse(polygon2);
724
+ // The schema checks more than a TypeScript type can represent
725
+ if (!polygon1Parsed.success || !polygon2Parsed.success) {
726
+ return null;
727
+ }
728
+ return polygon1Parsed.data.coordinates.every(linearRing => polygon2Parsed.data.coordinates.some(lr => linearRing.every(position => isGeoJsonPositionInLinearRing({ position, linearRing: lr }))));
729
+ };
730
+ /**
731
+ * @description Gets the intersection between two GeoJSON polygons. If one polygon is fully contained within the other,
732
+ * returns the contained polygon. Otherwise returns a MultiPolygon representing the intersection.
733
+ * @param polygon1 The first polygon to check intersection
734
+ * @param polygon2 The second polygon to check intersection
735
+ * @returns {(GeoJsonMultiPolygon | GeoJsonPolygon)} The intersection as either a Polygon (if one contains the other) or MultiPolygon
736
+ */
737
+ const getGeoJsonPolygonIntersection = (polygon1, polygon2) => {
738
+ const polygon1Parsed = geoJsonPolygonSchema.safeParse(polygon1);
739
+ const polygon2Parsed = geoJsonPolygonSchema.safeParse(polygon2);
740
+ if (!polygon1Parsed.success || !polygon2Parsed.success) {
741
+ return null;
742
+ }
743
+ if (isFullyContainedInGeoJsonPolygon(polygon1, polygon2)) {
744
+ return polygon1;
745
+ }
746
+ if (isFullyContainedInGeoJsonPolygon(polygon2, polygon1)) {
747
+ return polygon2;
748
+ }
749
+ const intersectionResult = polygonClipping.intersection(polygon1.coordinates, polygon2.coordinates);
750
+ if (intersectionResult.length === 1 && intersectionResult[0]) {
751
+ return {
752
+ type: "Polygon",
753
+ coordinates: intersectionResult[0],
754
+ };
755
+ }
756
+ return {
757
+ type: "MultiPolygon",
758
+ coordinates: polygonClipping.intersection(polygon1.coordinates, polygon2.coordinates),
759
+ };
760
+ };
761
+
762
+ //! These tools are used to bridge the gap with out poorly typed graphql types
763
+ // Should be ideally be avoided but are needed until we fix the graphql types
764
+ const isDoubleNestedCoords = (coords) => Array.isArray(coords) &&
765
+ Array.isArray(coords[0]) &&
766
+ Array.isArray(coords[0][0]) &&
767
+ typeof coords[0][0][0] === "number";
768
+ const isSingleCoords = (coords) => typeof coords[0] === "number";
769
+ /**
770
+ * @description Returns coordinates in consistent format
771
+ * @param inconsistentCoordinates Single point, array of points or nested array of points
772
+ * @returns {GeoJsonPosition[]} Array of standardized coordinates
773
+ */
774
+ const coordinatesToStandardFormat = (inconsistentCoordinates) => {
775
+ if (!inconsistentCoordinates) {
776
+ return [];
777
+ }
778
+ if (isSingleCoords(inconsistentCoordinates)) {
779
+ return [inconsistentCoordinates];
780
+ }
781
+ if (isDoubleNestedCoords(inconsistentCoordinates)) {
782
+ return inconsistentCoordinates[0] || [];
783
+ }
784
+ if (inconsistentCoordinates[0] && typeof inconsistentCoordinates[0][0] === "number") {
785
+ return inconsistentCoordinates;
786
+ }
787
+ return [];
788
+ };
789
+ /**
790
+ * @description Extracts a point coordinate from a GeoJSON object.
791
+ * @param geoObject A GeoJSON object.
792
+ * @returns {PointCoordinate} A point coordinate.
793
+ */
794
+ const getPointCoordinateFromGeoJsonObject = (geoObject) => {
795
+ if (!geoObject) {
796
+ return undefined;
797
+ }
798
+ else if ("geometry" in geoObject) {
799
+ return getPointCoordinateFromGeoJsonObject(geoObject.geometry);
800
+ }
801
+ else if ("coordinates" in geoObject &&
802
+ Array.isArray(geoObject.coordinates) &&
803
+ typeof geoObject.coordinates[0] === "number" &&
804
+ typeof geoObject.coordinates[1] === "number") {
805
+ const [point] = coordinatesToStandardFormat(geoObject.coordinates);
806
+ if (point) {
807
+ return { latitude: point[1], longitude: point[0] };
808
+ }
809
+ else {
810
+ throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
811
+ }
812
+ }
813
+ else {
814
+ throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
815
+ }
816
+ };
817
+ /**
818
+ * @description Extracts multiple point coordinates from a GeoJSON object.
819
+ * @param geoObject A GeoJSON object.
820
+ * @returns {PointCoordinate[]} An array of point coordinates.
821
+ * @example getMultipleCoordinatesFromGeoJsonObject({ type: "Point", coordinates: [1, 2] }) // [{ longitude: 1, latitude: 2 }]
822
+ */
823
+ const getMultipleCoordinatesFromGeoJsonObject = (geoObject) => {
824
+ if (!geoObject) {
825
+ return undefined;
826
+ }
827
+ else if ("geometry" in geoObject) {
828
+ return getMultipleCoordinatesFromGeoJsonObject(geoObject.geometry);
829
+ }
830
+ else if ("coordinates" in geoObject) {
831
+ return coordinatesToStandardFormat(geoObject.coordinates).map(([longitude, latitude]) => ({ longitude, latitude }));
832
+ }
833
+ else {
834
+ throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
835
+ }
836
+ };
837
+
838
+ //* -------- Trackunit-invented schemas and types to extend the GeoJson spec -------- *//
839
+ /**
840
+ * Polygon geometry object that explicitly disallows holes.
841
+ *
842
+ * Same as geoJsonPolygonSchema but type disallows holes by
843
+ * using tuple of one single linear ring instead of an array.
844
+ */
845
+ const tuGeoJsonPolygonNoHolesSchema = zod.z.strictObject({
846
+ //The type is still "Polygon" (not PolygonNoHoles or similar) since it's always
847
+ //compliant with Polygon, just not the other way around
848
+ type: zod.z.literal("Polygon"),
849
+ //uses tuple instead of array to enforce only 1 linear ring aka the polygon itself
850
+ coordinates: zod.z.tuple([geoJsonLinearRingSchema]),
851
+ });
852
+ /**
853
+ * Point radius object.
854
+ * For when you wish to define an area by a point and a radius.
855
+ *
856
+ * radius is in meters
857
+ */
858
+ const tuGeoJsonPointRadiusSchema = zod.z.strictObject({
859
+ type: zod.z.literal("PointRadius"),
860
+ coordinates: geoJsonPositionSchema,
861
+ radius: zod.z.number().positive(), // in meters
862
+ });
863
+ /**
864
+ * A Polygon with exactly 5 points and 4 horizontal/vertical sides that form a normal rectangular box.
865
+ */
866
+ const tuGeoJsonRectangularBoxPolygonSchema = zod.z
867
+ .strictObject({
868
+ type: zod.z.literal("Polygon"),
869
+ coordinates: zod.z.array(geoJsonLinearRingSchema),
870
+ })
871
+ .superRefine((data, ctx) => {
872
+ const coordinates = data.coordinates[0];
873
+ // Validate polygon has exactly 5 points
874
+ if ((coordinates === null || coordinates === void 0 ? void 0 : coordinates.length) !== 5) {
875
+ ctx.addIssue({
876
+ code: zod.z.ZodIssueCode.custom,
877
+ message: "Polygon must have exactly 5 coordinates to form a closed box.",
878
+ });
879
+ return;
880
+ }
881
+ // Check each side is either horizontal or vertical
882
+ for (let i = 0; i < 4; i++) {
883
+ const point1 = coordinates[i];
884
+ const point2 = coordinates[i + 1];
885
+ if (point1 === undefined || point2 === undefined) {
886
+ ctx.addIssue({
887
+ code: zod.z.ZodIssueCode.custom,
888
+ message: "Each coordinate must be a defined point.",
889
+ });
890
+ return;
891
+ }
892
+ const [x1, y1] = point1;
893
+ const [x2, y2] = point2;
894
+ // Ensure each line segment is either horizontal or vertical
895
+ if (x1 !== x2 && y1 !== y2) {
896
+ ctx.addIssue({
897
+ code: zod.z.ZodIssueCode.custom,
898
+ message: "Polygon sides must be horizontal or vertical to form a box shape.",
899
+ });
900
+ return;
901
+ }
902
+ }
903
+ });
725
904
 
726
905
  /**
727
906
  * Group an array of items by a key.
@@ -1380,6 +1559,7 @@ exports.capitalize = capitalize;
1380
1559
  exports.convertBlobToBase64 = convertBlobToBase64;
1381
1560
  exports.convertMetersToYards = convertMetersToYards;
1382
1561
  exports.convertYardsToMeters = convertYardsToMeters;
1562
+ exports.coordinatesToStandardFormat = coordinatesToStandardFormat;
1383
1563
  exports.dateCompare = dateCompare;
1384
1564
  exports.deleteUndefinedKeys = deleteUndefinedKeys;
1385
1565
  exports.difference = difference;
@@ -1400,15 +1580,16 @@ exports.geoJsonMultiLineStringSchema = geoJsonMultiLineStringSchema;
1400
1580
  exports.geoJsonMultiPointSchema = geoJsonMultiPointSchema;
1401
1581
  exports.geoJsonMultiPolygonSchema = geoJsonMultiPolygonSchema;
1402
1582
  exports.geoJsonPointSchema = geoJsonPointSchema;
1403
- exports.geoJsonPolygonNoHolesSchema = geoJsonPolygonNoHolesSchema;
1404
1583
  exports.geoJsonPolygonSchema = geoJsonPolygonSchema;
1405
1584
  exports.geoJsonPositionSchema = geoJsonPositionSchema;
1406
- exports.geoJsonRectangularBoxPolygonSchema = geoJsonRectangularBoxPolygonSchema;
1585
+ exports.getBboxFromGeoJsonPolygon = getBboxFromGeoJsonPolygon;
1407
1586
  exports.getBoundingBoxFromGeoJsonPolygon = getBoundingBoxFromGeoJsonPolygon;
1408
1587
  exports.getDifferenceBetweenDates = getDifferenceBetweenDates;
1409
1588
  exports.getEndOfDay = getEndOfDay;
1589
+ exports.getExtremeGeoJsonPointFromPolygon = getExtremeGeoJsonPointFromPolygon;
1410
1590
  exports.getFirstLevelObjectPropertyDifferences = getFirstLevelObjectPropertyDifferences;
1411
1591
  exports.getGeoJsonPolygonFromBoundingBox = getGeoJsonPolygonFromBoundingBox;
1592
+ exports.getGeoJsonPolygonIntersection = getGeoJsonPolygonIntersection;
1412
1593
  exports.getISOStringFromDate = getISOStringFromDate;
1413
1594
  exports.getMultipleCoordinatesFromGeoJsonObject = getMultipleCoordinatesFromGeoJsonObject;
1414
1595
  exports.getPointCoordinateFromGeoJsonObject = getPointCoordinateFromGeoJsonObject;
@@ -1422,6 +1603,9 @@ exports.groupTinyDataToOthers = groupTinyDataToOthers;
1422
1603
  exports.hourIntervals = hourIntervals;
1423
1604
  exports.intersection = intersection;
1424
1605
  exports.isArrayEqual = isArrayEqual;
1606
+ exports.isFullyContainedInGeoJsonPolygon = isFullyContainedInGeoJsonPolygon;
1607
+ exports.isGeoJsonPointInPolygon = isGeoJsonPointInPolygon;
1608
+ exports.isGeoJsonPositionInLinearRing = isGeoJsonPositionInLinearRing;
1425
1609
  exports.isSorted = isSorted;
1426
1610
  exports.isUUID = isUUID;
1427
1611
  exports.isValidImage = isValidImage;
@@ -1449,6 +1633,9 @@ exports.toUUID = toUUID;
1449
1633
  exports.trimIds = trimIds;
1450
1634
  exports.trimPath = trimPath;
1451
1635
  exports.truthy = truthy;
1636
+ exports.tuGeoJsonPointRadiusSchema = tuGeoJsonPointRadiusSchema;
1637
+ exports.tuGeoJsonPolygonNoHolesSchema = tuGeoJsonPolygonNoHolesSchema;
1638
+ exports.tuGeoJsonRectangularBoxPolygonSchema = tuGeoJsonRectangularBoxPolygonSchema;
1452
1639
  exports.unionArraysByKey = unionArraysByKey;
1453
1640
  exports.uuidv3 = uuidv3;
1454
1641
  exports.uuidv4 = uuidv4;
package/index.esm.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { intersection as intersection$1 } from 'polygon-clipping';
2
3
  import { v3, v4, v5 } from 'uuid';
3
4
 
4
5
  /**
@@ -478,15 +479,6 @@ const geoJsonLinearRingSchema = z
478
479
  message: "First and last coordinate positions must be identical (to close the linear ring aka polygon).",
479
480
  });
480
481
  }
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
482
  // Check if consecutive points are identical (excluding first and last)
491
483
  for (let i = 1; i < coords.length - 1; i++) {
492
484
  if (JSON.stringify(coords[i]) === JSON.stringify(coords[i - 1])) {
@@ -534,107 +526,10 @@ const geoJsonBboxSchema = z
534
526
  .refine(([minLng, minLat, maxLng, maxLat]) => maxLng > minLng && maxLat > minLat, {
535
527
  message: "Invalid bounding box: maxLng should be greater than minLng, and maxLat should be greater than minLat.",
536
528
  });
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
529
 
589
530
  const EARTH_RADIUS = 6378137; // Earth’s mean radius in meters
590
531
  /**
591
- * @description Extracts a point coordinate from a GeoJSON object.
592
- * @param geoObject A GeoJSON object.
593
- * @returns {PointCoordinate} A point coordinate.
594
- */
595
- const getPointCoordinateFromGeoJsonObject = (geoObject) => {
596
- if (!geoObject) {
597
- return undefined;
598
- }
599
- else if ("geometry" in geoObject) {
600
- return getPointCoordinateFromGeoJsonObject(geoObject.geometry);
601
- }
602
- else if ("coordinates" in geoObject &&
603
- Array.isArray(geoObject.coordinates) &&
604
- typeof geoObject.coordinates[0] === "number" &&
605
- typeof geoObject.coordinates[1] === "number") {
606
- return { longitude: geoObject.coordinates[0], latitude: geoObject.coordinates[1] };
607
- }
608
- else {
609
- throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
610
- }
611
- };
612
- /**
613
- * @description Extracts multiple point coordinates from a GeoJSON object.
614
- * @param geoObject A GeoJSON object.
615
- * @returns {PointCoordinate[]} An array of point coordinates.
616
- * @example getMultipleCoordinatesFromGeoJsonObject({ type: "Point", coordinates: [1, 2] }) // [{ longitude: 1, latitude: 2 }]
617
- */
618
- const getMultipleCoordinatesFromGeoJsonObject = (geoObject) => {
619
- if (!geoObject) {
620
- return undefined;
621
- }
622
- else if ("geometry" in geoObject) {
623
- return getMultipleCoordinatesFromGeoJsonObject(geoObject.geometry);
624
- }
625
- else if ("coordinates" in geoObject &&
626
- Array.isArray(geoObject.coordinates) &&
627
- Array.isArray(geoObject.coordinates[0])) {
628
- // @ts-ignore - suppressImplicitAnyIndexErrors
629
- // We would not be here if element was not an array.
630
- return geoObject.coordinates.map(element => ({ longitude: element[0], latitude: element[1] }));
631
- }
632
- else {
633
- throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
634
- }
635
- };
636
- /**
637
- * @description Creates a polygon from a bounding box.
532
+ * @description Creates a polygon (with no holes) from a bounding box.
638
533
  */
639
534
  const getPolygonFromBbox = (bbox) => {
640
535
  const [minLon, minLat, maxLon, maxLat] = bbox;
@@ -651,6 +546,23 @@ const getPolygonFromBbox = (bbox) => {
651
546
  ],
652
547
  };
653
548
  };
549
+ /**
550
+ * @description Creates a bounding box from a GeoJSON Polygon.
551
+ */
552
+ const getBboxFromGeoJsonPolygon = (polygon) => {
553
+ const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
554
+ if (!polygonParsed.success) {
555
+ return null;
556
+ }
557
+ const points = polygonParsed.data.coordinates[0];
558
+ if (!points) {
559
+ // Should never happen since the schema checks for it
560
+ return null;
561
+ }
562
+ const latitudes = points.map(point => point[1]);
563
+ const longitudes = points.map(point => point[0]);
564
+ return [Math.min(...longitudes), Math.min(...latitudes), Math.max(...longitudes), Math.max(...latitudes)];
565
+ };
654
566
  /**
655
567
  * @description Creates a round polygon from a point and a radius.
656
568
  */
@@ -679,12 +591,17 @@ const getPolygonFromPointAndRadius = (point, radius) => {
679
591
  * @description Creates a TU bounding box from a GeoJson Polygon.
680
592
  */
681
593
  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;
594
+ const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
595
+ if (!polygonParsed.success) {
596
+ return null;
597
+ }
598
+ const points = polygonParsed.data.coordinates[0];
599
+ if (!points) {
600
+ // Should never happen since the schema checks for it
601
+ return null;
687
602
  }
603
+ const latitudes = points.map(point => point[1]);
604
+ const longitudes = points.map(point => point[0]);
688
605
  return {
689
606
  nw: {
690
607
  latitude: Math.max(...latitudes),
@@ -720,6 +637,268 @@ const getGeoJsonPolygonFromBoundingBox = (boundingBox) => {
720
637
  const getPointCoordinateFromGeoJsonPoint = (point) => {
721
638
  return { latitude: point.coordinates[1], longitude: point.coordinates[0] };
722
639
  };
640
+ /**
641
+ * @description Gets the extreme point of a polygon in a given direction.
642
+ * @param {object} params - The parameters object
643
+ * @param {GeoJsonPolygon} params.polygon - The polygon to get the extreme point from
644
+ * @param {("top" | "right" | "bottom" | "left")} params.direction - The direction to get the extreme point in
645
+ * @returns {GeoJsonPoint} The extreme point in the given direction
646
+ */
647
+ const getExtremeGeoJsonPointFromPolygon = ({ polygon, direction, }) => {
648
+ var _a, _b, _c;
649
+ const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
650
+ if (!polygonParsed.success) {
651
+ return null;
652
+ }
653
+ const firstPoint = (_a = polygonParsed.data.coordinates[0]) === null || _a === void 0 ? void 0 : _a[0];
654
+ if (!firstPoint) {
655
+ // Should never happen since the schema checks for it
656
+ return null;
657
+ }
658
+ const extremePosition = (_b = polygonParsed.data.coordinates[0]) === null || _b === void 0 ? void 0 : _b.reduce((extremePoint, currentPoint) => {
659
+ var _a, _b, _c, _d;
660
+ switch (direction) {
661
+ case "top":
662
+ 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;
663
+ case "right":
664
+ 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;
665
+ case "bottom":
666
+ 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;
667
+ case "left":
668
+ 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;
669
+ default: {
670
+ throw new Error(`${direction} is not known`);
671
+ }
672
+ }
673
+ }, (_c = polygonParsed.data.coordinates[0]) === null || _c === void 0 ? void 0 : _c[0]);
674
+ return extremePosition
675
+ ? {
676
+ type: "Point",
677
+ coordinates: extremePosition,
678
+ }
679
+ : null; // Should never happen since the schema checks for it
680
+ };
681
+ /**
682
+ * Checks if a position is inside a linear ring. On edge is considered inside.
683
+ */
684
+ const isGeoJsonPositionInLinearRing = ({ position, linearRing, }) => {
685
+ const linearRingParsed = geoJsonLinearRingSchema.safeParse(linearRing);
686
+ if (!linearRingParsed.success) {
687
+ return null;
688
+ }
689
+ let inside = false;
690
+ const [x, y] = position;
691
+ for (let i = 0, j = linearRingParsed.data.length - 1; i < linearRingParsed.data.length; j = i++) {
692
+ const point1 = linearRingParsed.data[i];
693
+ const point2 = linearRingParsed.data[j];
694
+ if (!point1 || !point2) {
695
+ continue;
696
+ }
697
+ const [xi, yi] = point1;
698
+ const [xj, yj] = point2;
699
+ const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
700
+ if (intersect) {
701
+ inside = !inside;
702
+ }
703
+ }
704
+ return inside;
705
+ };
706
+ /**
707
+ * @description Checks if a point is inside a polygon.
708
+ */
709
+ const isGeoJsonPointInPolygon = ({ point, polygon, }) => {
710
+ const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
711
+ if (!polygonParsed.success) {
712
+ return null;
713
+ }
714
+ return polygonParsed.data.coordinates.some(linearRing => isGeoJsonPositionInLinearRing({ position: point.coordinates, linearRing }));
715
+ };
716
+ /**
717
+ * Checks if polygon1 is fully contained within polygon2
718
+ */
719
+ const isFullyContainedInGeoJsonPolygon = (polygon1, polygon2) => {
720
+ const polygon1Parsed = geoJsonPolygonSchema.safeParse(polygon1);
721
+ const polygon2Parsed = geoJsonPolygonSchema.safeParse(polygon2);
722
+ // The schema checks more than a TypeScript type can represent
723
+ if (!polygon1Parsed.success || !polygon2Parsed.success) {
724
+ return null;
725
+ }
726
+ return polygon1Parsed.data.coordinates.every(linearRing => polygon2Parsed.data.coordinates.some(lr => linearRing.every(position => isGeoJsonPositionInLinearRing({ position, linearRing: lr }))));
727
+ };
728
+ /**
729
+ * @description Gets the intersection between two GeoJSON polygons. If one polygon is fully contained within the other,
730
+ * returns the contained polygon. Otherwise returns a MultiPolygon representing the intersection.
731
+ * @param polygon1 The first polygon to check intersection
732
+ * @param polygon2 The second polygon to check intersection
733
+ * @returns {(GeoJsonMultiPolygon | GeoJsonPolygon)} The intersection as either a Polygon (if one contains the other) or MultiPolygon
734
+ */
735
+ const getGeoJsonPolygonIntersection = (polygon1, polygon2) => {
736
+ const polygon1Parsed = geoJsonPolygonSchema.safeParse(polygon1);
737
+ const polygon2Parsed = geoJsonPolygonSchema.safeParse(polygon2);
738
+ if (!polygon1Parsed.success || !polygon2Parsed.success) {
739
+ return null;
740
+ }
741
+ if (isFullyContainedInGeoJsonPolygon(polygon1, polygon2)) {
742
+ return polygon1;
743
+ }
744
+ if (isFullyContainedInGeoJsonPolygon(polygon2, polygon1)) {
745
+ return polygon2;
746
+ }
747
+ const intersectionResult = intersection$1(polygon1.coordinates, polygon2.coordinates);
748
+ if (intersectionResult.length === 1 && intersectionResult[0]) {
749
+ return {
750
+ type: "Polygon",
751
+ coordinates: intersectionResult[0],
752
+ };
753
+ }
754
+ return {
755
+ type: "MultiPolygon",
756
+ coordinates: intersection$1(polygon1.coordinates, polygon2.coordinates),
757
+ };
758
+ };
759
+
760
+ //! These tools are used to bridge the gap with out poorly typed graphql types
761
+ // Should be ideally be avoided but are needed until we fix the graphql types
762
+ const isDoubleNestedCoords = (coords) => Array.isArray(coords) &&
763
+ Array.isArray(coords[0]) &&
764
+ Array.isArray(coords[0][0]) &&
765
+ typeof coords[0][0][0] === "number";
766
+ const isSingleCoords = (coords) => typeof coords[0] === "number";
767
+ /**
768
+ * @description Returns coordinates in consistent format
769
+ * @param inconsistentCoordinates Single point, array of points or nested array of points
770
+ * @returns {GeoJsonPosition[]} Array of standardized coordinates
771
+ */
772
+ const coordinatesToStandardFormat = (inconsistentCoordinates) => {
773
+ if (!inconsistentCoordinates) {
774
+ return [];
775
+ }
776
+ if (isSingleCoords(inconsistentCoordinates)) {
777
+ return [inconsistentCoordinates];
778
+ }
779
+ if (isDoubleNestedCoords(inconsistentCoordinates)) {
780
+ return inconsistentCoordinates[0] || [];
781
+ }
782
+ if (inconsistentCoordinates[0] && typeof inconsistentCoordinates[0][0] === "number") {
783
+ return inconsistentCoordinates;
784
+ }
785
+ return [];
786
+ };
787
+ /**
788
+ * @description Extracts a point coordinate from a GeoJSON object.
789
+ * @param geoObject A GeoJSON object.
790
+ * @returns {PointCoordinate} A point coordinate.
791
+ */
792
+ const getPointCoordinateFromGeoJsonObject = (geoObject) => {
793
+ if (!geoObject) {
794
+ return undefined;
795
+ }
796
+ else if ("geometry" in geoObject) {
797
+ return getPointCoordinateFromGeoJsonObject(geoObject.geometry);
798
+ }
799
+ else if ("coordinates" in geoObject &&
800
+ Array.isArray(geoObject.coordinates) &&
801
+ typeof geoObject.coordinates[0] === "number" &&
802
+ typeof geoObject.coordinates[1] === "number") {
803
+ const [point] = coordinatesToStandardFormat(geoObject.coordinates);
804
+ if (point) {
805
+ return { latitude: point[1], longitude: point[0] };
806
+ }
807
+ else {
808
+ throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
809
+ }
810
+ }
811
+ else {
812
+ throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
813
+ }
814
+ };
815
+ /**
816
+ * @description Extracts multiple point coordinates from a GeoJSON object.
817
+ * @param geoObject A GeoJSON object.
818
+ * @returns {PointCoordinate[]} An array of point coordinates.
819
+ * @example getMultipleCoordinatesFromGeoJsonObject({ type: "Point", coordinates: [1, 2] }) // [{ longitude: 1, latitude: 2 }]
820
+ */
821
+ const getMultipleCoordinatesFromGeoJsonObject = (geoObject) => {
822
+ if (!geoObject) {
823
+ return undefined;
824
+ }
825
+ else if ("geometry" in geoObject) {
826
+ return getMultipleCoordinatesFromGeoJsonObject(geoObject.geometry);
827
+ }
828
+ else if ("coordinates" in geoObject) {
829
+ return coordinatesToStandardFormat(geoObject.coordinates).map(([longitude, latitude]) => ({ longitude, latitude }));
830
+ }
831
+ else {
832
+ throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
833
+ }
834
+ };
835
+
836
+ //* -------- Trackunit-invented schemas and types to extend the GeoJson spec -------- *//
837
+ /**
838
+ * Polygon geometry object that explicitly disallows holes.
839
+ *
840
+ * Same as geoJsonPolygonSchema but type disallows holes by
841
+ * using tuple of one single linear ring instead of an array.
842
+ */
843
+ const tuGeoJsonPolygonNoHolesSchema = z.strictObject({
844
+ //The type is still "Polygon" (not PolygonNoHoles or similar) since it's always
845
+ //compliant with Polygon, just not the other way around
846
+ type: z.literal("Polygon"),
847
+ //uses tuple instead of array to enforce only 1 linear ring aka the polygon itself
848
+ coordinates: z.tuple([geoJsonLinearRingSchema]),
849
+ });
850
+ /**
851
+ * Point radius object.
852
+ * For when you wish to define an area by a point and a radius.
853
+ *
854
+ * radius is in meters
855
+ */
856
+ const tuGeoJsonPointRadiusSchema = z.strictObject({
857
+ type: z.literal("PointRadius"),
858
+ coordinates: geoJsonPositionSchema,
859
+ radius: z.number().positive(), // in meters
860
+ });
861
+ /**
862
+ * A Polygon with exactly 5 points and 4 horizontal/vertical sides that form a normal rectangular box.
863
+ */
864
+ const tuGeoJsonRectangularBoxPolygonSchema = z
865
+ .strictObject({
866
+ type: z.literal("Polygon"),
867
+ coordinates: z.array(geoJsonLinearRingSchema),
868
+ })
869
+ .superRefine((data, ctx) => {
870
+ const coordinates = data.coordinates[0];
871
+ // Validate polygon has exactly 5 points
872
+ if ((coordinates === null || coordinates === void 0 ? void 0 : coordinates.length) !== 5) {
873
+ ctx.addIssue({
874
+ code: z.ZodIssueCode.custom,
875
+ message: "Polygon must have exactly 5 coordinates to form a closed box.",
876
+ });
877
+ return;
878
+ }
879
+ // Check each side is either horizontal or vertical
880
+ for (let i = 0; i < 4; i++) {
881
+ const point1 = coordinates[i];
882
+ const point2 = coordinates[i + 1];
883
+ if (point1 === undefined || point2 === undefined) {
884
+ ctx.addIssue({
885
+ code: z.ZodIssueCode.custom,
886
+ message: "Each coordinate must be a defined point.",
887
+ });
888
+ return;
889
+ }
890
+ const [x1, y1] = point1;
891
+ const [x2, y2] = point2;
892
+ // Ensure each line segment is either horizontal or vertical
893
+ if (x1 !== x2 && y1 !== y2) {
894
+ ctx.addIssue({
895
+ code: z.ZodIssueCode.custom,
896
+ message: "Polygon sides must be horizontal or vertical to form a box shape.",
897
+ });
898
+ return;
899
+ }
900
+ }
901
+ });
723
902
 
724
903
  /**
725
904
  * Group an array of items by a key.
@@ -1367,4 +1546,4 @@ const uuidv4 = () => {
1367
1546
  */
1368
1547
  const uuidv5 = (name, namespace) => v5(name, namespace);
1369
1548
 
1370
- 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, getPointCoordinateFromGeoJsonPoint, 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 };
1549
+ export { DateTimeFormat, EARTH_RADIUS, HoursAndMinutesFormat, align, alphabeticallySort, arrayLengthCompare, arrayNotEmpty, booleanCompare, capitalize, convertBlobToBase64, convertMetersToYards, convertYardsToMeters, coordinatesToStandardFormat, dateCompare, deleteUndefinedKeys, difference, doNothing, enumFromValue, enumFromValueTypesafe, enumOrUndefinedFromValue, exhaustiveCheck, filterByMultiple, formatAddress, formatCoordinates, fuzzySearch, geoJsonBboxSchema, geoJsonGeometrySchema, geoJsonLineStringSchema, geoJsonLinearRingSchema, geoJsonMultiLineStringSchema, geoJsonMultiPointSchema, geoJsonMultiPolygonSchema, geoJsonPointSchema, geoJsonPolygonSchema, geoJsonPositionSchema, getBboxFromGeoJsonPolygon, getBoundingBoxFromGeoJsonPolygon, getDifferenceBetweenDates, getEndOfDay, getExtremeGeoJsonPointFromPolygon, getFirstLevelObjectPropertyDifferences, getGeoJsonPolygonFromBoundingBox, getGeoJsonPolygonIntersection, getISOStringFromDate, getMultipleCoordinatesFromGeoJsonObject, getPointCoordinateFromGeoJsonObject, getPointCoordinateFromGeoJsonPoint, getPolygonFromBbox, getPolygonFromPointAndRadius, getResizedDimensions, getStartOfDay, groupBy, groupTinyDataToOthers, hourIntervals, intersection, isArrayEqual, isFullyContainedInGeoJsonPolygon, isGeoJsonPointInPolygon, isGeoJsonPositionInLinearRing, 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, tuGeoJsonPointRadiusSchema, tuGeoJsonPolygonNoHolesSchema, tuGeoJsonRectangularBoxPolygonSchema, unionArraysByKey, uuidv3, uuidv4, uuidv5 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/shared-utils",
3
- "version": "0.0.83",
3
+ "version": "0.0.85",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "uuid": "^10.0.0",
11
- "zod": "3.22.4"
11
+ "zod": "3.22.4",
12
+ "polygon-clipping": "^0.15.7"
12
13
  },
13
14
  "module": "./index.esm.js",
14
15
  "main": "./index.cjs.js",
@@ -174,37 +174,3 @@ export type GeoJsonGeometry = z.infer<typeof geoJsonGeometrySchema>;
174
174
  */
175
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
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,4 +1,5 @@
1
- import { GeoJsonBbox, GeoJsonPoint, GeoJsonPolygon } from "./GeoJsonSchemas";
1
+ import { GeoJsonBbox, GeoJsonLinearRing, GeoJsonMultiPolygon, GeoJsonPoint, GeoJsonPolygon, GeoJsonPosition } from "./GeoJsonSchemas";
2
+ import { TuGeoJsonPolygonNoHoles } from "./TuGeoJsonSchemas";
2
3
  export declare const EARTH_RADIUS = 6378137;
3
4
  interface PointCoordinate {
4
5
  longitude: number;
@@ -8,31 +9,14 @@ interface BoundingBox {
8
9
  nw: PointCoordinate;
9
10
  se: PointCoordinate;
10
11
  }
11
- interface GeoJSONGeometry {
12
- type?: unknown;
13
- coordinates?: number[] | number[][] | number[][][] | null;
14
- }
15
- interface GeoJsonFeature {
16
- type?: unknown;
17
- geometry?: GeoJSONGeometry | null;
18
- }
19
- /**
20
- * @description Extracts a point coordinate from a GeoJSON object.
21
- * @param geoObject A GeoJSON object.
22
- * @returns {PointCoordinate} A point coordinate.
23
- */
24
- export declare const getPointCoordinateFromGeoJsonObject: (geoObject: GeoJsonFeature | GeoJSONGeometry | undefined | null) => PointCoordinate | undefined;
25
12
  /**
26
- * @description Extracts multiple point coordinates from a GeoJSON object.
27
- * @param geoObject A GeoJSON object.
28
- * @returns {PointCoordinate[]} An array of point coordinates.
29
- * @example getMultipleCoordinatesFromGeoJsonObject({ type: "Point", coordinates: [1, 2] }) // [{ longitude: 1, latitude: 2 }]
13
+ * @description Creates a polygon (with no holes) from a bounding box.
30
14
  */
31
- export declare const getMultipleCoordinatesFromGeoJsonObject: (geoObject: GeoJsonFeature | GeoJSONGeometry | undefined | null) => PointCoordinate[] | undefined;
15
+ export declare const getPolygonFromBbox: (bbox: GeoJsonBbox) => TuGeoJsonPolygonNoHoles;
32
16
  /**
33
- * @description Creates a polygon from a bounding box.
17
+ * @description Creates a bounding box from a GeoJSON Polygon.
34
18
  */
35
- export declare const getPolygonFromBbox: (bbox: GeoJsonBbox) => GeoJsonPolygon;
19
+ export declare const getBboxFromGeoJsonPolygon: (polygon: GeoJsonPolygon) => GeoJsonBbox | null;
36
20
  /**
37
21
  * @description Creates a round polygon from a point and a radius.
38
22
  */
@@ -40,7 +24,7 @@ export declare const getPolygonFromPointAndRadius: (point: GeoJsonPoint, radius:
40
24
  /**
41
25
  * @description Creates a TU bounding box from a GeoJson Polygon.
42
26
  */
43
- export declare const getBoundingBoxFromGeoJsonPolygon: (polygon: GeoJsonPolygon) => BoundingBox | undefined;
27
+ export declare const getBoundingBoxFromGeoJsonPolygon: (polygon: GeoJsonPolygon) => BoundingBox | null;
44
28
  /**
45
29
  * @description Creates a GeoJSON Polygon from a TU bounding box.
46
30
  */
@@ -49,4 +33,41 @@ export declare const getGeoJsonPolygonFromBoundingBox: (boundingBox: BoundingBox
49
33
  * @description Creates TU point coordinate from a GeoJSON Point.
50
34
  */
51
35
  export declare const getPointCoordinateFromGeoJsonPoint: (point: GeoJsonPoint) => PointCoordinate;
36
+ /**
37
+ * @description Gets the extreme point of a polygon in a given direction.
38
+ * @param {object} params - The parameters object
39
+ * @param {GeoJsonPolygon} params.polygon - The polygon to get the extreme point from
40
+ * @param {("top" | "right" | "bottom" | "left")} params.direction - The direction to get the extreme point in
41
+ * @returns {GeoJsonPoint} The extreme point in the given direction
42
+ */
43
+ export declare const getExtremeGeoJsonPointFromPolygon: ({ polygon, direction, }: {
44
+ polygon: GeoJsonPolygon;
45
+ direction: "top" | "right" | "bottom" | "left";
46
+ }) => GeoJsonPoint | null;
47
+ /**
48
+ * Checks if a position is inside a linear ring. On edge is considered inside.
49
+ */
50
+ export declare const isGeoJsonPositionInLinearRing: ({ position, linearRing, }: {
51
+ position: GeoJsonPosition;
52
+ linearRing: GeoJsonLinearRing;
53
+ }) => boolean | null;
54
+ /**
55
+ * @description Checks if a point is inside a polygon.
56
+ */
57
+ export declare const isGeoJsonPointInPolygon: ({ point, polygon, }: {
58
+ point: GeoJsonPoint;
59
+ polygon: GeoJsonPolygon;
60
+ }) => boolean | null;
61
+ /**
62
+ * Checks if polygon1 is fully contained within polygon2
63
+ */
64
+ export declare const isFullyContainedInGeoJsonPolygon: (polygon1: GeoJsonPolygon, polygon2: GeoJsonPolygon) => boolean | null;
65
+ /**
66
+ * @description Gets the intersection between two GeoJSON polygons. If one polygon is fully contained within the other,
67
+ * returns the contained polygon. Otherwise returns a MultiPolygon representing the intersection.
68
+ * @param polygon1 The first polygon to check intersection
69
+ * @param polygon2 The second polygon to check intersection
70
+ * @returns {(GeoJsonMultiPolygon | GeoJsonPolygon)} The intersection as either a Polygon (if one contains the other) or MultiPolygon
71
+ */
72
+ export declare const getGeoJsonPolygonIntersection: (polygon1: GeoJsonPolygon, polygon2: GeoJsonPolygon) => GeoJsonMultiPolygon | GeoJsonPolygon | null;
52
73
  export {};
@@ -0,0 +1,33 @@
1
+ import { GeoJsonPosition } from "./GeoJsonSchemas";
2
+ interface PointCoordinate {
3
+ longitude: number;
4
+ latitude: number;
5
+ }
6
+ interface GeoJSONGeometry {
7
+ type?: unknown;
8
+ coordinates?: GeoJsonPosition | GeoJsonPosition[] | GeoJsonPosition[][] | null;
9
+ }
10
+ interface GeoJsonFeature {
11
+ type?: unknown;
12
+ geometry?: GeoJSONGeometry | null;
13
+ }
14
+ /**
15
+ * @description Returns coordinates in consistent format
16
+ * @param inconsistentCoordinates Single point, array of points or nested array of points
17
+ * @returns {GeoJsonPosition[]} Array of standardized coordinates
18
+ */
19
+ export declare const coordinatesToStandardFormat: (inconsistentCoordinates: GeoJsonPosition | GeoJsonPosition[] | GeoJsonPosition[][] | null | undefined) => [number, number][];
20
+ /**
21
+ * @description Extracts a point coordinate from a GeoJSON object.
22
+ * @param geoObject A GeoJSON object.
23
+ * @returns {PointCoordinate} A point coordinate.
24
+ */
25
+ export declare const getPointCoordinateFromGeoJsonObject: (geoObject: GeoJsonFeature | GeoJSONGeometry | undefined | null) => PointCoordinate | undefined;
26
+ /**
27
+ * @description Extracts multiple point coordinates from a GeoJSON object.
28
+ * @param geoObject A GeoJSON object.
29
+ * @returns {PointCoordinate[]} An array of point coordinates.
30
+ * @example getMultipleCoordinatesFromGeoJsonObject({ type: "Point", coordinates: [1, 2] }) // [{ longitude: 1, latitude: 2 }]
31
+ */
32
+ export declare const getMultipleCoordinatesFromGeoJsonObject: (geoObject: GeoJsonFeature | GeoJSONGeometry | undefined | null) => PointCoordinate[] | undefined;
33
+ export {};
@@ -0,0 +1,58 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Polygon geometry object that explicitly disallows holes.
4
+ *
5
+ * Same as geoJsonPolygonSchema but type disallows holes by
6
+ * using tuple of one single linear ring instead of an array.
7
+ */
8
+ export declare const tuGeoJsonPolygonNoHolesSchema: z.ZodObject<{
9
+ type: z.ZodLiteral<"Polygon">;
10
+ coordinates: z.ZodTuple<[z.ZodEffects<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>, "many">, [number, number][], [number, number][]>], null>;
11
+ }, "strict", z.ZodTypeAny, {
12
+ type: "Polygon";
13
+ coordinates: [[number, number][]];
14
+ }, {
15
+ type: "Polygon";
16
+ coordinates: [[number, number][]];
17
+ }>;
18
+ export type TuGeoJsonPolygonNoHoles = z.infer<typeof tuGeoJsonPolygonNoHolesSchema>;
19
+ /**
20
+ * Point radius object.
21
+ * For when you wish to define an area by a point and a radius.
22
+ *
23
+ * radius is in meters
24
+ */
25
+ export declare const tuGeoJsonPointRadiusSchema: z.ZodObject<{
26
+ type: z.ZodLiteral<"PointRadius">;
27
+ coordinates: z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>;
28
+ radius: z.ZodNumber;
29
+ }, "strict", z.ZodTypeAny, {
30
+ type: "PointRadius";
31
+ coordinates: [number, number];
32
+ radius: number;
33
+ }, {
34
+ type: "PointRadius";
35
+ coordinates: [number, number];
36
+ radius: number;
37
+ }>;
38
+ export type TuGeoJsonPointRadius = z.infer<typeof tuGeoJsonPointRadiusSchema>;
39
+ /**
40
+ * A Polygon with exactly 5 points and 4 horizontal/vertical sides that form a normal rectangular box.
41
+ */
42
+ export declare const tuGeoJsonRectangularBoxPolygonSchema: z.ZodEffects<z.ZodObject<{
43
+ type: z.ZodLiteral<"Polygon">;
44
+ coordinates: z.ZodArray<z.ZodEffects<z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber], null>, "many">, [number, number][], [number, number][]>, "many">;
45
+ }, "strict", z.ZodTypeAny, {
46
+ type: "Polygon";
47
+ coordinates: [number, number][][];
48
+ }, {
49
+ type: "Polygon";
50
+ coordinates: [number, number][][];
51
+ }>, {
52
+ type: "Polygon";
53
+ coordinates: [number, number][][];
54
+ }, {
55
+ type: "Polygon";
56
+ coordinates: [number, number][][];
57
+ }>;
58
+ export type TuGeoJsonRectangularBoxPolygon = z.infer<typeof tuGeoJsonRectangularBoxPolygonSchema>;
package/src/index.d.ts CHANGED
@@ -11,6 +11,8 @@ export * from "./fastArrayOperations";
11
11
  export * from "./filter";
12
12
  export * from "./GeoJson/GeoJsonSchemas";
13
13
  export * from "./GeoJson/GeoJsonUtils";
14
+ export * from "./GeoJson/TUGeoJsonObjectBridgeUtils";
15
+ export * from "./GeoJson/TuGeoJsonSchemas";
14
16
  export * from "./groupBy/groupBy";
15
17
  export * from "./GroupingUtility";
16
18
  export * from "./idUtils";