@trackunit/shared-utils 0.0.84 → 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,59 +528,239 @@ 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 -------- *//
531
+
532
+ const EARTH_RADIUS = 6378137; // Earth’s mean radius in meters
540
533
  /**
541
- * Polygon geometry object that explicitly disallows holes.
542
- * https://tools.ietf.org/html/rfc7946#section-3.1.6
534
+ * @description Creates a polygon (with no holes) from a bounding box.
543
535
  */
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
- });
536
+ const getPolygonFromBbox = (bbox) => {
537
+ const [minLon, minLat, maxLon, maxLat] = bbox;
538
+ return {
539
+ type: "Polygon",
540
+ coordinates: [
541
+ [
542
+ [minLon, minLat],
543
+ [maxLon, minLat],
544
+ [maxLon, maxLat],
545
+ [minLon, maxLat],
546
+ [minLon, minLat],
547
+ ],
548
+ ],
549
+ };
550
+ };
549
551
  /**
550
- * A Polygon with exactly 5 points and 4 horizontal/vertical sides that form a normal rectangular box.
552
+ * @description Creates a bounding box from a GeoJSON Polygon.
551
553
  */
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;
554
+ const getBboxFromGeoJsonPolygon = (polygon) => {
555
+ const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
556
+ if (!polygonParsed.success) {
557
+ return null;
566
558
  }
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];
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
+ };
568
+ /**
569
+ * @description Creates a round polygon from a point and a radius.
570
+ */
571
+ const getPolygonFromPointAndRadius = (point, radius) => {
572
+ const [lon, lat] = point.coordinates;
573
+ // Adjust the number of points based on radius (resolution)
574
+ const pointsCount = Math.max(32, Math.floor(radius / 100)); // More points for larger radius
575
+ const angleStep = (2 * Math.PI) / pointsCount;
576
+ const coordinates = [];
577
+ for (let i = 0; i <= pointsCount; i++) {
578
+ const angle = i * angleStep;
579
+ // Calculate offset in latitude and longitude
580
+ const deltaLat = (radius / EARTH_RADIUS) * (180 / Math.PI);
581
+ const deltaLon = deltaLat / Math.cos((lat * Math.PI) / 180);
582
+ // Calculate new coordinates based on angle
583
+ const newLat = lat + deltaLat * Math.sin(angle);
584
+ const newLon = lon + deltaLon * Math.cos(angle);
585
+ coordinates.push([newLon, newLat]);
586
+ }
587
+ return {
588
+ type: "Polygon",
589
+ coordinates: [coordinates],
590
+ };
591
+ };
592
+ /**
593
+ * @description Creates a TU bounding box from a GeoJson Polygon.
594
+ */
595
+ const getBoundingBoxFromGeoJsonPolygon = (polygon) => {
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;
604
+ }
605
+ const latitudes = points.map(point => point[1]);
606
+ const longitudes = points.map(point => point[0]);
607
+ return {
608
+ nw: {
609
+ latitude: Math.max(...latitudes),
610
+ longitude: Math.min(...longitudes),
611
+ },
612
+ se: {
613
+ latitude: Math.min(...latitudes),
614
+ longitude: Math.max(...longitudes),
615
+ },
616
+ };
617
+ };
618
+ /**
619
+ * @description Creates a GeoJSON Polygon from a TU bounding box.
620
+ */
621
+ const getGeoJsonPolygonFromBoundingBox = (boundingBox) => {
622
+ const { nw, se } = boundingBox;
623
+ return {
624
+ type: "Polygon",
625
+ coordinates: [
626
+ [
627
+ [nw.longitude, nw.latitude], // Northwest corner
628
+ [se.longitude, nw.latitude], // Northeast corner
629
+ [se.longitude, se.latitude], // Southeast corner
630
+ [nw.longitude, se.latitude], // Southwest corner
631
+ [nw.longitude, nw.latitude], // Close the loop back to Northwest corner
632
+ ],
633
+ ],
634
+ };
635
+ };
636
+ /**
637
+ * @description Creates TU point coordinate from a GeoJSON Point.
638
+ */
639
+ const getPointCoordinateFromGeoJsonPoint = (point) => {
640
+ return { latitude: point.coordinates[1], longitude: point.coordinates[0] };
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];
571
696
  if (!point1 || !point2) {
572
- ctx.addIssue({
573
- code: zod.z.ZodIssueCode.custom,
574
- message: "Each coordinate must be a defined point.",
575
- });
576
- return;
697
+ continue;
577
698
  }
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;
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;
587
704
  }
588
705
  }
589
- });
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
+ };
590
761
 
591
- const EARTH_RADIUS = 6378137; // Earth’s mean radius in meters
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
592
764
  const isDoubleNestedCoords = (coords) => Array.isArray(coords) &&
593
765
  Array.isArray(coords[0]) &&
594
766
  Array.isArray(coords[0][0]) &&
@@ -597,7 +769,7 @@ const isSingleCoords = (coords) => typeof coords[0] === "number";
597
769
  /**
598
770
  * @description Returns coordinates in consistent format
599
771
  * @param inconsistentCoordinates Single point, array of points or nested array of points
600
- * @returns GeoPoint[]
772
+ * @returns {GeoJsonPosition[]} Array of standardized coordinates
601
773
  */
602
774
  const coordinatesToStandardFormat = (inconsistentCoordinates) => {
603
775
  if (!inconsistentCoordinates) {
@@ -662,93 +834,73 @@ const getMultipleCoordinatesFromGeoJsonObject = (geoObject) => {
662
834
  throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
663
835
  }
664
836
  };
837
+
838
+ //* -------- Trackunit-invented schemas and types to extend the GeoJson spec -------- *//
665
839
  /**
666
- * @description Creates a polygon from a bounding box.
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.
667
844
  */
668
- const getPolygonFromBbox = (bbox) => {
669
- const [minLon, minLat, maxLon, maxLat] = bbox;
670
- return {
671
- type: "Polygon",
672
- coordinates: [
673
- [
674
- [minLon, minLat],
675
- [maxLon, minLat],
676
- [maxLon, maxLat],
677
- [minLon, maxLat],
678
- [minLon, minLat],
679
- ],
680
- ],
681
- };
682
- };
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
+ });
683
852
  /**
684
- * @description Creates a round polygon from a point and a radius.
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
685
857
  */
686
- const getPolygonFromPointAndRadius = (point, radius) => {
687
- const [lon, lat] = point.coordinates;
688
- // Adjust the number of points based on radius (resolution)
689
- const pointsCount = Math.max(32, Math.floor(radius / 100)); // More points for larger radius
690
- const angleStep = (2 * Math.PI) / pointsCount;
691
- const coordinates = [];
692
- for (let i = 0; i <= pointsCount; i++) {
693
- const angle = i * angleStep;
694
- // Calculate offset in latitude and longitude
695
- const deltaLat = (radius / EARTH_RADIUS) * (180 / Math.PI);
696
- const deltaLon = deltaLat / Math.cos((lat * Math.PI) / 180);
697
- // Calculate new coordinates based on angle
698
- const newLat = lat + deltaLat * Math.sin(angle);
699
- const newLon = lon + deltaLon * Math.cos(angle);
700
- coordinates.push([newLon, newLat]);
701
- }
702
- return {
703
- type: "Polygon",
704
- coordinates: [coordinates],
705
- };
706
- };
858
+ const tuGeoJsonPointRadiusSchema = zod.z.strictObject({
859
+ type: zod.z.literal("PointRadius"),
860
+ coordinates: geoJsonPositionSchema,
861
+ radius: zod.z.number().positive(), // in meters
862
+ });
707
863
  /**
708
- * @description Creates a TU bounding box from a GeoJson Polygon.
864
+ * A Polygon with exactly 5 points and 4 horizontal/vertical sides that form a normal rectangular box.
709
865
  */
710
- const getBoundingBoxFromGeoJsonPolygon = (polygon) => {
711
- const points = polygon.coordinates[0];
712
- const latitudes = points === null || points === void 0 ? void 0 : points.map(point => point[1]);
713
- const longitudes = points === null || points === void 0 ? void 0 : points.map(point => point[0]);
714
- if (!latitudes || !longitudes) {
715
- return undefined;
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;
716
880
  }
717
- return {
718
- nw: {
719
- latitude: Math.max(...latitudes),
720
- longitude: Math.min(...longitudes),
721
- },
722
- se: {
723
- latitude: Math.min(...latitudes),
724
- longitude: Math.max(...longitudes),
725
- },
726
- };
727
- };
728
- /**
729
- * @description Creates a GeoJSON Polygon from a TU bounding box.
730
- */
731
- const getGeoJsonPolygonFromBoundingBox = (boundingBox) => {
732
- const { nw, se } = boundingBox;
733
- return {
734
- type: "Polygon",
735
- coordinates: [
736
- [
737
- [nw.longitude, nw.latitude], // Northwest corner
738
- [se.longitude, nw.latitude], // Northeast corner
739
- [se.longitude, se.latitude], // Southeast corner
740
- [nw.longitude, se.latitude], // Southwest corner
741
- [nw.longitude, nw.latitude], // Close the loop back to Northwest corner
742
- ],
743
- ],
744
- };
745
- };
746
- /**
747
- * @description Creates TU point coordinate from a GeoJSON Point.
748
- */
749
- const getPointCoordinateFromGeoJsonPoint = (point) => {
750
- return { latitude: point.coordinates[1], longitude: point.coordinates[0] };
751
- };
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
+ });
752
904
 
753
905
  /**
754
906
  * Group an array of items by a key.
@@ -1428,15 +1580,16 @@ exports.geoJsonMultiLineStringSchema = geoJsonMultiLineStringSchema;
1428
1580
  exports.geoJsonMultiPointSchema = geoJsonMultiPointSchema;
1429
1581
  exports.geoJsonMultiPolygonSchema = geoJsonMultiPolygonSchema;
1430
1582
  exports.geoJsonPointSchema = geoJsonPointSchema;
1431
- exports.geoJsonPolygonNoHolesSchema = geoJsonPolygonNoHolesSchema;
1432
1583
  exports.geoJsonPolygonSchema = geoJsonPolygonSchema;
1433
1584
  exports.geoJsonPositionSchema = geoJsonPositionSchema;
1434
- exports.geoJsonRectangularBoxPolygonSchema = geoJsonRectangularBoxPolygonSchema;
1585
+ exports.getBboxFromGeoJsonPolygon = getBboxFromGeoJsonPolygon;
1435
1586
  exports.getBoundingBoxFromGeoJsonPolygon = getBoundingBoxFromGeoJsonPolygon;
1436
1587
  exports.getDifferenceBetweenDates = getDifferenceBetweenDates;
1437
1588
  exports.getEndOfDay = getEndOfDay;
1589
+ exports.getExtremeGeoJsonPointFromPolygon = getExtremeGeoJsonPointFromPolygon;
1438
1590
  exports.getFirstLevelObjectPropertyDifferences = getFirstLevelObjectPropertyDifferences;
1439
1591
  exports.getGeoJsonPolygonFromBoundingBox = getGeoJsonPolygonFromBoundingBox;
1592
+ exports.getGeoJsonPolygonIntersection = getGeoJsonPolygonIntersection;
1440
1593
  exports.getISOStringFromDate = getISOStringFromDate;
1441
1594
  exports.getMultipleCoordinatesFromGeoJsonObject = getMultipleCoordinatesFromGeoJsonObject;
1442
1595
  exports.getPointCoordinateFromGeoJsonObject = getPointCoordinateFromGeoJsonObject;
@@ -1450,6 +1603,9 @@ exports.groupTinyDataToOthers = groupTinyDataToOthers;
1450
1603
  exports.hourIntervals = hourIntervals;
1451
1604
  exports.intersection = intersection;
1452
1605
  exports.isArrayEqual = isArrayEqual;
1606
+ exports.isFullyContainedInGeoJsonPolygon = isFullyContainedInGeoJsonPolygon;
1607
+ exports.isGeoJsonPointInPolygon = isGeoJsonPointInPolygon;
1608
+ exports.isGeoJsonPositionInLinearRing = isGeoJsonPositionInLinearRing;
1453
1609
  exports.isSorted = isSorted;
1454
1610
  exports.isUUID = isUUID;
1455
1611
  exports.isValidImage = isValidImage;
@@ -1477,6 +1633,9 @@ exports.toUUID = toUUID;
1477
1633
  exports.trimIds = trimIds;
1478
1634
  exports.trimPath = trimPath;
1479
1635
  exports.truthy = truthy;
1636
+ exports.tuGeoJsonPointRadiusSchema = tuGeoJsonPointRadiusSchema;
1637
+ exports.tuGeoJsonPolygonNoHolesSchema = tuGeoJsonPolygonNoHolesSchema;
1638
+ exports.tuGeoJsonRectangularBoxPolygonSchema = tuGeoJsonRectangularBoxPolygonSchema;
1480
1639
  exports.unionArraysByKey = unionArraysByKey;
1481
1640
  exports.uuidv3 = uuidv3;
1482
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,59 +526,239 @@ 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 -------- *//
529
+
530
+ const EARTH_RADIUS = 6378137; // Earth’s mean radius in meters
538
531
  /**
539
- * Polygon geometry object that explicitly disallows holes.
540
- * https://tools.ietf.org/html/rfc7946#section-3.1.6
532
+ * @description Creates a polygon (with no holes) from a bounding box.
541
533
  */
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
- });
534
+ const getPolygonFromBbox = (bbox) => {
535
+ const [minLon, minLat, maxLon, maxLat] = bbox;
536
+ return {
537
+ type: "Polygon",
538
+ coordinates: [
539
+ [
540
+ [minLon, minLat],
541
+ [maxLon, minLat],
542
+ [maxLon, maxLat],
543
+ [minLon, maxLat],
544
+ [minLon, minLat],
545
+ ],
546
+ ],
547
+ };
548
+ };
547
549
  /**
548
- * A Polygon with exactly 5 points and 4 horizontal/vertical sides that form a normal rectangular box.
550
+ * @description Creates a bounding box from a GeoJSON Polygon.
549
551
  */
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;
552
+ const getBboxFromGeoJsonPolygon = (polygon) => {
553
+ const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
554
+ if (!polygonParsed.success) {
555
+ return null;
564
556
  }
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];
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
+ };
566
+ /**
567
+ * @description Creates a round polygon from a point and a radius.
568
+ */
569
+ const getPolygonFromPointAndRadius = (point, radius) => {
570
+ const [lon, lat] = point.coordinates;
571
+ // Adjust the number of points based on radius (resolution)
572
+ const pointsCount = Math.max(32, Math.floor(radius / 100)); // More points for larger radius
573
+ const angleStep = (2 * Math.PI) / pointsCount;
574
+ const coordinates = [];
575
+ for (let i = 0; i <= pointsCount; i++) {
576
+ const angle = i * angleStep;
577
+ // Calculate offset in latitude and longitude
578
+ const deltaLat = (radius / EARTH_RADIUS) * (180 / Math.PI);
579
+ const deltaLon = deltaLat / Math.cos((lat * Math.PI) / 180);
580
+ // Calculate new coordinates based on angle
581
+ const newLat = lat + deltaLat * Math.sin(angle);
582
+ const newLon = lon + deltaLon * Math.cos(angle);
583
+ coordinates.push([newLon, newLat]);
584
+ }
585
+ return {
586
+ type: "Polygon",
587
+ coordinates: [coordinates],
588
+ };
589
+ };
590
+ /**
591
+ * @description Creates a TU bounding box from a GeoJson Polygon.
592
+ */
593
+ const getBoundingBoxFromGeoJsonPolygon = (polygon) => {
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;
602
+ }
603
+ const latitudes = points.map(point => point[1]);
604
+ const longitudes = points.map(point => point[0]);
605
+ return {
606
+ nw: {
607
+ latitude: Math.max(...latitudes),
608
+ longitude: Math.min(...longitudes),
609
+ },
610
+ se: {
611
+ latitude: Math.min(...latitudes),
612
+ longitude: Math.max(...longitudes),
613
+ },
614
+ };
615
+ };
616
+ /**
617
+ * @description Creates a GeoJSON Polygon from a TU bounding box.
618
+ */
619
+ const getGeoJsonPolygonFromBoundingBox = (boundingBox) => {
620
+ const { nw, se } = boundingBox;
621
+ return {
622
+ type: "Polygon",
623
+ coordinates: [
624
+ [
625
+ [nw.longitude, nw.latitude], // Northwest corner
626
+ [se.longitude, nw.latitude], // Northeast corner
627
+ [se.longitude, se.latitude], // Southeast corner
628
+ [nw.longitude, se.latitude], // Southwest corner
629
+ [nw.longitude, nw.latitude], // Close the loop back to Northwest corner
630
+ ],
631
+ ],
632
+ };
633
+ };
634
+ /**
635
+ * @description Creates TU point coordinate from a GeoJSON Point.
636
+ */
637
+ const getPointCoordinateFromGeoJsonPoint = (point) => {
638
+ return { latitude: point.coordinates[1], longitude: point.coordinates[0] };
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];
569
694
  if (!point1 || !point2) {
570
- ctx.addIssue({
571
- code: z.ZodIssueCode.custom,
572
- message: "Each coordinate must be a defined point.",
573
- });
574
- return;
695
+ continue;
575
696
  }
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;
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;
585
702
  }
586
703
  }
587
- });
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
+ };
588
759
 
589
- const EARTH_RADIUS = 6378137; // Earth’s mean radius in meters
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
590
762
  const isDoubleNestedCoords = (coords) => Array.isArray(coords) &&
591
763
  Array.isArray(coords[0]) &&
592
764
  Array.isArray(coords[0][0]) &&
@@ -595,7 +767,7 @@ const isSingleCoords = (coords) => typeof coords[0] === "number";
595
767
  /**
596
768
  * @description Returns coordinates in consistent format
597
769
  * @param inconsistentCoordinates Single point, array of points or nested array of points
598
- * @returns GeoPoint[]
770
+ * @returns {GeoJsonPosition[]} Array of standardized coordinates
599
771
  */
600
772
  const coordinatesToStandardFormat = (inconsistentCoordinates) => {
601
773
  if (!inconsistentCoordinates) {
@@ -660,93 +832,73 @@ const getMultipleCoordinatesFromGeoJsonObject = (geoObject) => {
660
832
  throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
661
833
  }
662
834
  };
835
+
836
+ //* -------- Trackunit-invented schemas and types to extend the GeoJson spec -------- *//
663
837
  /**
664
- * @description Creates a polygon from a bounding box.
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.
665
842
  */
666
- const getPolygonFromBbox = (bbox) => {
667
- const [minLon, minLat, maxLon, maxLat] = bbox;
668
- return {
669
- type: "Polygon",
670
- coordinates: [
671
- [
672
- [minLon, minLat],
673
- [maxLon, minLat],
674
- [maxLon, maxLat],
675
- [minLon, maxLat],
676
- [minLon, minLat],
677
- ],
678
- ],
679
- };
680
- };
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
+ });
681
850
  /**
682
- * @description Creates a round polygon from a point and a radius.
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
683
855
  */
684
- const getPolygonFromPointAndRadius = (point, radius) => {
685
- const [lon, lat] = point.coordinates;
686
- // Adjust the number of points based on radius (resolution)
687
- const pointsCount = Math.max(32, Math.floor(radius / 100)); // More points for larger radius
688
- const angleStep = (2 * Math.PI) / pointsCount;
689
- const coordinates = [];
690
- for (let i = 0; i <= pointsCount; i++) {
691
- const angle = i * angleStep;
692
- // Calculate offset in latitude and longitude
693
- const deltaLat = (radius / EARTH_RADIUS) * (180 / Math.PI);
694
- const deltaLon = deltaLat / Math.cos((lat * Math.PI) / 180);
695
- // Calculate new coordinates based on angle
696
- const newLat = lat + deltaLat * Math.sin(angle);
697
- const newLon = lon + deltaLon * Math.cos(angle);
698
- coordinates.push([newLon, newLat]);
699
- }
700
- return {
701
- type: "Polygon",
702
- coordinates: [coordinates],
703
- };
704
- };
856
+ const tuGeoJsonPointRadiusSchema = z.strictObject({
857
+ type: z.literal("PointRadius"),
858
+ coordinates: geoJsonPositionSchema,
859
+ radius: z.number().positive(), // in meters
860
+ });
705
861
  /**
706
- * @description Creates a TU bounding box from a GeoJson Polygon.
862
+ * A Polygon with exactly 5 points and 4 horizontal/vertical sides that form a normal rectangular box.
707
863
  */
708
- const getBoundingBoxFromGeoJsonPolygon = (polygon) => {
709
- const points = polygon.coordinates[0];
710
- const latitudes = points === null || points === void 0 ? void 0 : points.map(point => point[1]);
711
- const longitudes = points === null || points === void 0 ? void 0 : points.map(point => point[0]);
712
- if (!latitudes || !longitudes) {
713
- return undefined;
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;
714
878
  }
715
- return {
716
- nw: {
717
- latitude: Math.max(...latitudes),
718
- longitude: Math.min(...longitudes),
719
- },
720
- se: {
721
- latitude: Math.min(...latitudes),
722
- longitude: Math.max(...longitudes),
723
- },
724
- };
725
- };
726
- /**
727
- * @description Creates a GeoJSON Polygon from a TU bounding box.
728
- */
729
- const getGeoJsonPolygonFromBoundingBox = (boundingBox) => {
730
- const { nw, se } = boundingBox;
731
- return {
732
- type: "Polygon",
733
- coordinates: [
734
- [
735
- [nw.longitude, nw.latitude], // Northwest corner
736
- [se.longitude, nw.latitude], // Northeast corner
737
- [se.longitude, se.latitude], // Southeast corner
738
- [nw.longitude, se.latitude], // Southwest corner
739
- [nw.longitude, nw.latitude], // Close the loop back to Northwest corner
740
- ],
741
- ],
742
- };
743
- };
744
- /**
745
- * @description Creates TU point coordinate from a GeoJSON Point.
746
- */
747
- const getPointCoordinateFromGeoJsonPoint = (point) => {
748
- return { latitude: point.coordinates[1], longitude: point.coordinates[0] };
749
- };
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
+ });
750
902
 
751
903
  /**
752
904
  * Group an array of items by a key.
@@ -1394,4 +1546,4 @@ const uuidv4 = () => {
1394
1546
  */
1395
1547
  const uuidv5 = (name, namespace) => v5(name, namespace);
1396
1548
 
1397
- 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, 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.84",
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,38 +9,14 @@ interface BoundingBox {
8
9
  nw: PointCoordinate;
9
10
  se: PointCoordinate;
10
11
  }
11
- export type GeoPoint = [number, number];
12
- interface GeoJSONGeometry {
13
- type?: unknown;
14
- coordinates?: GeoPoint | GeoPoint[] | GeoPoint[][] | null;
15
- }
16
- interface GeoJsonFeature {
17
- type?: unknown;
18
- geometry?: GeoJSONGeometry | null;
19
- }
20
- /**
21
- * @description Returns coordinates in consistent format
22
- * @param inconsistentCoordinates Single point, array of points or nested array of points
23
- * @returns GeoPoint[]
24
- */
25
- export declare const coordinatesToStandardFormat: (inconsistentCoordinates: GeoPoint | GeoPoint[] | GeoPoint[][] | null | undefined) => GeoPoint[];
26
12
  /**
27
- * @description Extracts a point coordinate from a GeoJSON object.
28
- * @param geoObject A GeoJSON object.
29
- * @returns {PointCoordinate} A point coordinate.
13
+ * @description Creates a polygon (with no holes) from a bounding box.
30
14
  */
31
- export declare const getPointCoordinateFromGeoJsonObject: (geoObject: GeoJsonFeature | GeoJSONGeometry | undefined | null) => PointCoordinate | undefined;
15
+ export declare const getPolygonFromBbox: (bbox: GeoJsonBbox) => TuGeoJsonPolygonNoHoles;
32
16
  /**
33
- * @description Extracts multiple point coordinates from a GeoJSON object.
34
- * @param geoObject A GeoJSON object.
35
- * @returns {PointCoordinate[]} An array of point coordinates.
36
- * @example getMultipleCoordinatesFromGeoJsonObject({ type: "Point", coordinates: [1, 2] }) // [{ longitude: 1, latitude: 2 }]
17
+ * @description Creates a bounding box from a GeoJSON Polygon.
37
18
  */
38
- export declare const getMultipleCoordinatesFromGeoJsonObject: (geoObject: GeoJsonFeature | GeoJSONGeometry | undefined | null) => PointCoordinate[] | undefined;
39
- /**
40
- * @description Creates a polygon from a bounding box.
41
- */
42
- export declare const getPolygonFromBbox: (bbox: GeoJsonBbox) => GeoJsonPolygon;
19
+ export declare const getBboxFromGeoJsonPolygon: (polygon: GeoJsonPolygon) => GeoJsonBbox | null;
43
20
  /**
44
21
  * @description Creates a round polygon from a point and a radius.
45
22
  */
@@ -47,7 +24,7 @@ export declare const getPolygonFromPointAndRadius: (point: GeoJsonPoint, radius:
47
24
  /**
48
25
  * @description Creates a TU bounding box from a GeoJson Polygon.
49
26
  */
50
- export declare const getBoundingBoxFromGeoJsonPolygon: (polygon: GeoJsonPolygon) => BoundingBox | undefined;
27
+ export declare const getBoundingBoxFromGeoJsonPolygon: (polygon: GeoJsonPolygon) => BoundingBox | null;
51
28
  /**
52
29
  * @description Creates a GeoJSON Polygon from a TU bounding box.
53
30
  */
@@ -56,4 +33,41 @@ export declare const getGeoJsonPolygonFromBoundingBox: (boundingBox: BoundingBox
56
33
  * @description Creates TU point coordinate from a GeoJSON Point.
57
34
  */
58
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;
59
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";