New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@trackunit/shared-utils

Package Overview
Dependencies
Maintainers
0
Versions
180
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@trackunit/shared-utils - npm Package Compare versions

Comparing version 0.0.85 to 0.0.86

517

index.cjs.js
'use strict';
var zod = require('zod');
var polygonClipping = require('polygon-clipping');
var uuid = require('uuid');

@@ -418,490 +416,3 @@

// * NOTE: For simplicity these tools are built for 2D coordinate space only!
/**
* A Position is an array of coordinates. [x, y]
* https://tools.ietf.org/html/rfc7946#section-3.1.1
*/
const geoJsonPositionSchema = zod.z.tuple([zod.z.number(), zod.z.number()]);
/**
* Point geometry object.
* https://tools.ietf.org/html/rfc7946#section-3.1.2
*/
const geoJsonPointSchema = zod.z.strictObject({
type: zod.z.literal("Point"),
coordinates: geoJsonPositionSchema,
});
/**
* MultiPoint geometry object.
* https://tools.ietf.org/html/rfc7946#section-3.1.3
*/
const geoJsonMultiPointSchema = zod.z.strictObject({
type: zod.z.literal("MultiPoint"),
coordinates: zod.z.array(geoJsonPositionSchema),
});
/**
* LineString geometry object.
* Minimum length of 2 positions.
* https://tools.ietf.org/html/rfc7946#section-3.1.4
*/
const geoJsonLineStringSchema = zod.z.strictObject({
type: zod.z.literal("LineString"),
coordinates: zod.z.array(geoJsonPositionSchema).min(2),
});
/**
* MultiLineString geometry object.
* https://tools.ietf.org/html/rfc7946#section-3.1.5
*/
const geoJsonMultiLineStringSchema = zod.z.strictObject({
type: zod.z.literal("MultiLineString"),
coordinates: zod.z.array(zod.z.array(geoJsonPositionSchema)),
});
/**
* Helper type for reuse across polygon schemas.
*
* - A linear ring is a closed LineString with four or more positions.
* - The first and last positions are equivalent, and they MUST contain
identical values; their representation SHOULD also be identical
* - A linear ring is the boundary of a surface or the boundary of a
hole in a surface
* - A linear ring MUST follow the right-hand rule with respect to the
area it bounds, i.e., exterior rings are counterclockwise, and
holes are clockwise
*/
const geoJsonLinearRingSchema = zod.z
.array(geoJsonPositionSchema)
.min(4, {
message: "Coordinates array must contain at least 4 positions. 3 to make a non-line shape and 1 to close the shape (duplicate of first)",
})
.superRefine((coords, ctx) => {
const first = coords[0];
const last = coords[coords.length - 1];
// Check if first and last coordinates match
if (JSON.stringify(first) !== JSON.stringify(last)) {
ctx.addIssue({
code: zod.z.ZodIssueCode.custom,
message: "First and last coordinate positions must be identical (to close the linear ring aka polygon).",
});
}
// Check if consecutive points are identical (excluding first and last)
for (let i = 1; i < coords.length - 1; i++) {
if (JSON.stringify(coords[i]) === JSON.stringify(coords[i - 1])) {
ctx.addIssue({
code: zod.z.ZodIssueCode.custom,
message: `Consecutive coordinates at index ${i - 1} and ${i} should not be identical.`,
});
}
}
});
/**
* Polygon geometry object.
* https://tools.ietf.org/html/rfc7946#section-3.1.6
*/
const geoJsonPolygonSchema = zod.z.strictObject({
type: zod.z.literal("Polygon"),
coordinates: zod.z.array(geoJsonLinearRingSchema),
});
/**
* MultiPolygon geometry object.
* https://tools.ietf.org/html/rfc7946#section-3.1.7
*/
const geoJsonMultiPolygonSchema = zod.z.strictObject({
type: zod.z.literal("MultiPolygon"),
coordinates: zod.z.array(zod.z.array(geoJsonLinearRingSchema)),
});
// The same for Geometry, GeometryCollection, GeoJsonProperties, Feature, FeatureCollection, etc.
const geoJsonGeometrySchema = zod.z.union([
geoJsonPointSchema,
geoJsonMultiPointSchema,
geoJsonLineStringSchema,
geoJsonMultiLineStringSchema,
geoJsonPolygonSchema,
geoJsonMultiPolygonSchema,
]);
//* -------- Bbox -------- *//
/**
* 2D bounding box of the GeoJSON object.
* The value of the Bbox member is an array of length 4.
*
* [min_lon, min_lat, max_lon, max_lat]
*/
const geoJsonBboxSchema = zod.z
.tuple([zod.z.number(), zod.z.number(), zod.z.number(), zod.z.number()])
.refine(([minLng, minLat, maxLng, maxLat]) => maxLng > minLng && maxLat > minLat, {
message: "Invalid bounding box: maxLng should be greater than minLng, and maxLat should be greater than minLat.",
});
const EARTH_RADIUS = 6378137; // Earth’s mean radius in meters
/**
* @description Creates a polygon (with no holes) from a bounding box.
*/
const getPolygonFromBbox = (bbox) => {
const [minLon, minLat, maxLon, maxLat] = bbox;
return {
type: "Polygon",
coordinates: [
[
[minLon, minLat],
[maxLon, minLat],
[maxLon, maxLat],
[minLon, maxLat],
[minLon, minLat],
],
],
};
};
/**
* @description Creates a bounding box from a GeoJSON Polygon.
*/
const getBboxFromGeoJsonPolygon = (polygon) => {
const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
if (!polygonParsed.success) {
return null;
}
const points = polygonParsed.data.coordinates[0];
if (!points) {
// Should never happen since the schema checks for it
return null;
}
const latitudes = points.map(point => point[1]);
const longitudes = points.map(point => point[0]);
return [Math.min(...longitudes), Math.min(...latitudes), Math.max(...longitudes), Math.max(...latitudes)];
};
/**
* @description Creates a round polygon from a point and a radius.
*/
const getPolygonFromPointAndRadius = (point, radius) => {
const [lon, lat] = point.coordinates;
// Adjust the number of points based on radius (resolution)
const pointsCount = Math.max(32, Math.floor(radius / 100)); // More points for larger radius
const angleStep = (2 * Math.PI) / pointsCount;
const coordinates = [];
for (let i = 0; i <= pointsCount; i++) {
const angle = i * angleStep;
// Calculate offset in latitude and longitude
const deltaLat = (radius / EARTH_RADIUS) * (180 / Math.PI);
const deltaLon = deltaLat / Math.cos((lat * Math.PI) / 180);
// Calculate new coordinates based on angle
const newLat = lat + deltaLat * Math.sin(angle);
const newLon = lon + deltaLon * Math.cos(angle);
coordinates.push([newLon, newLat]);
}
return {
type: "Polygon",
coordinates: [coordinates],
};
};
/**
* @description Creates a TU bounding box from a GeoJson Polygon.
*/
const getBoundingBoxFromGeoJsonPolygon = (polygon) => {
const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
if (!polygonParsed.success) {
return null;
}
const points = polygonParsed.data.coordinates[0];
if (!points) {
// Should never happen since the schema checks for it
return null;
}
const latitudes = points.map(point => point[1]);
const longitudes = points.map(point => point[0]);
return {
nw: {
latitude: Math.max(...latitudes),
longitude: Math.min(...longitudes),
},
se: {
latitude: Math.min(...latitudes),
longitude: Math.max(...longitudes),
},
};
};
/**
* @description Creates a GeoJSON Polygon from a TU bounding box.
*/
const getGeoJsonPolygonFromBoundingBox = (boundingBox) => {
const { nw, se } = boundingBox;
return {
type: "Polygon",
coordinates: [
[
[nw.longitude, nw.latitude], // Northwest corner
[se.longitude, nw.latitude], // Northeast corner
[se.longitude, se.latitude], // Southeast corner
[nw.longitude, se.latitude], // Southwest corner
[nw.longitude, nw.latitude], // Close the loop back to Northwest corner
],
],
};
};
/**
* @description Creates TU point coordinate from a GeoJSON Point.
*/
const getPointCoordinateFromGeoJsonPoint = (point) => {
return { latitude: point.coordinates[1], longitude: point.coordinates[0] };
};
/**
* @description Gets the extreme point of a polygon in a given direction.
* @param {object} params - The parameters object
* @param {GeoJsonPolygon} params.polygon - The polygon to get the extreme point from
* @param {("top" | "right" | "bottom" | "left")} params.direction - The direction to get the extreme point in
* @returns {GeoJsonPoint} The extreme point in the given direction
*/
const getExtremeGeoJsonPointFromPolygon = ({ polygon, direction, }) => {
var _a, _b, _c;
const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
if (!polygonParsed.success) {
return null;
}
const firstPoint = (_a = polygonParsed.data.coordinates[0]) === null || _a === void 0 ? void 0 : _a[0];
if (!firstPoint) {
// Should never happen since the schema checks for it
return null;
}
const extremePosition = (_b = polygonParsed.data.coordinates[0]) === null || _b === void 0 ? void 0 : _b.reduce((extremePoint, currentPoint) => {
var _a, _b, _c, _d;
switch (direction) {
case "top":
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;
case "right":
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;
case "bottom":
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;
case "left":
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;
default: {
throw new Error(`${direction} is not known`);
}
}
}, (_c = polygonParsed.data.coordinates[0]) === null || _c === void 0 ? void 0 : _c[0]);
return extremePosition
? {
type: "Point",
coordinates: extremePosition,
}
: null; // Should never happen since the schema checks for it
};
/**
* Checks if a position is inside a linear ring. On edge is considered inside.
*/
const isGeoJsonPositionInLinearRing = ({ position, linearRing, }) => {
const linearRingParsed = geoJsonLinearRingSchema.safeParse(linearRing);
if (!linearRingParsed.success) {
return null;
}
let inside = false;
const [x, y] = position;
for (let i = 0, j = linearRingParsed.data.length - 1; i < linearRingParsed.data.length; j = i++) {
const point1 = linearRingParsed.data[i];
const point2 = linearRingParsed.data[j];
if (!point1 || !point2) {
continue;
}
const [xi, yi] = point1;
const [xj, yj] = point2;
const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
if (intersect) {
inside = !inside;
}
}
return inside;
};
/**
* @description Checks if a point is inside a polygon.
*/
const isGeoJsonPointInPolygon = ({ point, polygon, }) => {
const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
if (!polygonParsed.success) {
return null;
}
return polygonParsed.data.coordinates.some(linearRing => isGeoJsonPositionInLinearRing({ position: point.coordinates, linearRing }));
};
/**
* Checks if polygon1 is fully contained within polygon2
*/
const isFullyContainedInGeoJsonPolygon = (polygon1, polygon2) => {
const polygon1Parsed = geoJsonPolygonSchema.safeParse(polygon1);
const polygon2Parsed = geoJsonPolygonSchema.safeParse(polygon2);
// The schema checks more than a TypeScript type can represent
if (!polygon1Parsed.success || !polygon2Parsed.success) {
return null;
}
return polygon1Parsed.data.coordinates.every(linearRing => polygon2Parsed.data.coordinates.some(lr => linearRing.every(position => isGeoJsonPositionInLinearRing({ position, linearRing: lr }))));
};
/**
* @description Gets the intersection between two GeoJSON polygons. If one polygon is fully contained within the other,
* returns the contained polygon. Otherwise returns a MultiPolygon representing the intersection.
* @param polygon1 The first polygon to check intersection
* @param polygon2 The second polygon to check intersection
* @returns {(GeoJsonMultiPolygon | GeoJsonPolygon)} The intersection as either a Polygon (if one contains the other) or MultiPolygon
*/
const getGeoJsonPolygonIntersection = (polygon1, polygon2) => {
const polygon1Parsed = geoJsonPolygonSchema.safeParse(polygon1);
const polygon2Parsed = geoJsonPolygonSchema.safeParse(polygon2);
if (!polygon1Parsed.success || !polygon2Parsed.success) {
return null;
}
if (isFullyContainedInGeoJsonPolygon(polygon1, polygon2)) {
return polygon1;
}
if (isFullyContainedInGeoJsonPolygon(polygon2, polygon1)) {
return polygon2;
}
const intersectionResult = polygonClipping.intersection(polygon1.coordinates, polygon2.coordinates);
if (intersectionResult.length === 1 && intersectionResult[0]) {
return {
type: "Polygon",
coordinates: intersectionResult[0],
};
}
return {
type: "MultiPolygon",
coordinates: polygonClipping.intersection(polygon1.coordinates, polygon2.coordinates),
};
};
//! These tools are used to bridge the gap with out poorly typed graphql types
// Should be ideally be avoided but are needed until we fix the graphql types
const isDoubleNestedCoords = (coords) => Array.isArray(coords) &&
Array.isArray(coords[0]) &&
Array.isArray(coords[0][0]) &&
typeof coords[0][0][0] === "number";
const isSingleCoords = (coords) => typeof coords[0] === "number";
/**
* @description Returns coordinates in consistent format
* @param inconsistentCoordinates Single point, array of points or nested array of points
* @returns {GeoJsonPosition[]} Array of standardized coordinates
*/
const coordinatesToStandardFormat = (inconsistentCoordinates) => {
if (!inconsistentCoordinates) {
return [];
}
if (isSingleCoords(inconsistentCoordinates)) {
return [inconsistentCoordinates];
}
if (isDoubleNestedCoords(inconsistentCoordinates)) {
return inconsistentCoordinates[0] || [];
}
if (inconsistentCoordinates[0] && typeof inconsistentCoordinates[0][0] === "number") {
return inconsistentCoordinates;
}
return [];
};
/**
* @description Extracts a point coordinate from a GeoJSON object.
* @param geoObject A GeoJSON object.
* @returns {PointCoordinate} A point coordinate.
*/
const getPointCoordinateFromGeoJsonObject = (geoObject) => {
if (!geoObject) {
return undefined;
}
else if ("geometry" in geoObject) {
return getPointCoordinateFromGeoJsonObject(geoObject.geometry);
}
else if ("coordinates" in geoObject &&
Array.isArray(geoObject.coordinates) &&
typeof geoObject.coordinates[0] === "number" &&
typeof geoObject.coordinates[1] === "number") {
const [point] = coordinatesToStandardFormat(geoObject.coordinates);
if (point) {
return { latitude: point[1], longitude: point[0] };
}
else {
throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
}
}
else {
throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
}
};
/**
* @description Extracts multiple point coordinates from a GeoJSON object.
* @param geoObject A GeoJSON object.
* @returns {PointCoordinate[]} An array of point coordinates.
* @example getMultipleCoordinatesFromGeoJsonObject({ type: "Point", coordinates: [1, 2] }) // [{ longitude: 1, latitude: 2 }]
*/
const getMultipleCoordinatesFromGeoJsonObject = (geoObject) => {
if (!geoObject) {
return undefined;
}
else if ("geometry" in geoObject) {
return getMultipleCoordinatesFromGeoJsonObject(geoObject.geometry);
}
else if ("coordinates" in geoObject) {
return coordinatesToStandardFormat(geoObject.coordinates).map(([longitude, latitude]) => ({ longitude, latitude }));
}
else {
throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
}
};
//* -------- Trackunit-invented schemas and types to extend the GeoJson spec -------- *//
/**
* Polygon geometry object that explicitly disallows holes.
*
* Same as geoJsonPolygonSchema but type disallows holes by
* using tuple of one single linear ring instead of an array.
*/
const tuGeoJsonPolygonNoHolesSchema = zod.z.strictObject({
//The type is still "Polygon" (not PolygonNoHoles or similar) since it's always
//compliant with Polygon, just not the other way around
type: zod.z.literal("Polygon"),
//uses tuple instead of array to enforce only 1 linear ring aka the polygon itself
coordinates: zod.z.tuple([geoJsonLinearRingSchema]),
});
/**
* Point radius object.
* For when you wish to define an area by a point and a radius.
*
* radius is in meters
*/
const tuGeoJsonPointRadiusSchema = zod.z.strictObject({
type: zod.z.literal("PointRadius"),
coordinates: geoJsonPositionSchema,
radius: zod.z.number().positive(), // in meters
});
/**
* A Polygon with exactly 5 points and 4 horizontal/vertical sides that form a normal rectangular box.
*/
const tuGeoJsonRectangularBoxPolygonSchema = zod.z
.strictObject({
type: zod.z.literal("Polygon"),
coordinates: zod.z.array(geoJsonLinearRingSchema),
})
.superRefine((data, ctx) => {
const coordinates = data.coordinates[0];
// Validate polygon has exactly 5 points
if ((coordinates === null || coordinates === void 0 ? void 0 : coordinates.length) !== 5) {
ctx.addIssue({
code: zod.z.ZodIssueCode.custom,
message: "Polygon must have exactly 5 coordinates to form a closed box.",
});
return;
}
// Check each side is either horizontal or vertical
for (let i = 0; i < 4; i++) {
const point1 = coordinates[i];
const point2 = coordinates[i + 1];
if (point1 === undefined || point2 === undefined) {
ctx.addIssue({
code: zod.z.ZodIssueCode.custom,
message: "Each coordinate must be a defined point.",
});
return;
}
const [x1, y1] = point1;
const [x2, y2] = point2;
// Ensure each line segment is either horizontal or vertical
if (x1 !== x2 && y1 !== y2) {
ctx.addIssue({
code: zod.z.ZodIssueCode.custom,
message: "Polygon sides must be horizontal or vertical to form a box shape.",
});
return;
}
}
});
/**
* Group an array of items by a key.

@@ -1553,3 +1064,2 @@ *

exports.DateTimeFormat = DateTimeFormat;
exports.EARTH_RADIUS = EARTH_RADIUS;
exports.align = align;

@@ -1564,3 +1074,2 @@ exports.alphabeticallySort = alphabeticallySort;

exports.convertYardsToMeters = convertYardsToMeters;
exports.coordinatesToStandardFormat = coordinatesToStandardFormat;
exports.dateCompare = dateCompare;

@@ -1578,26 +1087,6 @@ exports.deleteUndefinedKeys = deleteUndefinedKeys;

exports.fuzzySearch = fuzzySearch;
exports.geoJsonBboxSchema = geoJsonBboxSchema;
exports.geoJsonGeometrySchema = geoJsonGeometrySchema;
exports.geoJsonLineStringSchema = geoJsonLineStringSchema;
exports.geoJsonLinearRingSchema = geoJsonLinearRingSchema;
exports.geoJsonMultiLineStringSchema = geoJsonMultiLineStringSchema;
exports.geoJsonMultiPointSchema = geoJsonMultiPointSchema;
exports.geoJsonMultiPolygonSchema = geoJsonMultiPolygonSchema;
exports.geoJsonPointSchema = geoJsonPointSchema;
exports.geoJsonPolygonSchema = geoJsonPolygonSchema;
exports.geoJsonPositionSchema = geoJsonPositionSchema;
exports.getBboxFromGeoJsonPolygon = getBboxFromGeoJsonPolygon;
exports.getBoundingBoxFromGeoJsonPolygon = getBoundingBoxFromGeoJsonPolygon;
exports.getDifferenceBetweenDates = getDifferenceBetweenDates;
exports.getEndOfDay = getEndOfDay;
exports.getExtremeGeoJsonPointFromPolygon = getExtremeGeoJsonPointFromPolygon;
exports.getFirstLevelObjectPropertyDifferences = getFirstLevelObjectPropertyDifferences;
exports.getGeoJsonPolygonFromBoundingBox = getGeoJsonPolygonFromBoundingBox;
exports.getGeoJsonPolygonIntersection = getGeoJsonPolygonIntersection;
exports.getISOStringFromDate = getISOStringFromDate;
exports.getMultipleCoordinatesFromGeoJsonObject = getMultipleCoordinatesFromGeoJsonObject;
exports.getPointCoordinateFromGeoJsonObject = getPointCoordinateFromGeoJsonObject;
exports.getPointCoordinateFromGeoJsonPoint = getPointCoordinateFromGeoJsonPoint;
exports.getPolygonFromBbox = getPolygonFromBbox;
exports.getPolygonFromPointAndRadius = getPolygonFromPointAndRadius;
exports.getResizedDimensions = getResizedDimensions;

@@ -1610,5 +1099,2 @@ exports.getStartOfDay = getStartOfDay;

exports.isArrayEqual = isArrayEqual;
exports.isFullyContainedInGeoJsonPolygon = isFullyContainedInGeoJsonPolygon;
exports.isGeoJsonPointInPolygon = isGeoJsonPointInPolygon;
exports.isGeoJsonPositionInLinearRing = isGeoJsonPositionInLinearRing;
exports.isSorted = isSorted;

@@ -1641,5 +1127,2 @@ exports.isUUID = isUUID;

exports.truthy = truthy;
exports.tuGeoJsonPointRadiusSchema = tuGeoJsonPointRadiusSchema;
exports.tuGeoJsonPolygonNoHolesSchema = tuGeoJsonPolygonNoHolesSchema;
exports.tuGeoJsonRectangularBoxPolygonSchema = tuGeoJsonRectangularBoxPolygonSchema;
exports.unionArraysByKey = unionArraysByKey;

@@ -1646,0 +1129,0 @@ exports.uuidv3 = uuidv3;

491

index.esm.js

@@ -1,3 +0,1 @@

import { z } from 'zod';
import { intersection as intersection$1 } from 'polygon-clipping';
import { v3, v4, v5 } from 'uuid';

@@ -416,490 +414,3 @@

// * NOTE: For simplicity these tools are built for 2D coordinate space only!
/**
* A Position is an array of coordinates. [x, y]
* https://tools.ietf.org/html/rfc7946#section-3.1.1
*/
const geoJsonPositionSchema = z.tuple([z.number(), z.number()]);
/**
* Point geometry object.
* https://tools.ietf.org/html/rfc7946#section-3.1.2
*/
const geoJsonPointSchema = z.strictObject({
type: z.literal("Point"),
coordinates: geoJsonPositionSchema,
});
/**
* MultiPoint geometry object.
* https://tools.ietf.org/html/rfc7946#section-3.1.3
*/
const geoJsonMultiPointSchema = z.strictObject({
type: z.literal("MultiPoint"),
coordinates: z.array(geoJsonPositionSchema),
});
/**
* LineString geometry object.
* Minimum length of 2 positions.
* https://tools.ietf.org/html/rfc7946#section-3.1.4
*/
const geoJsonLineStringSchema = z.strictObject({
type: z.literal("LineString"),
coordinates: z.array(geoJsonPositionSchema).min(2),
});
/**
* MultiLineString geometry object.
* https://tools.ietf.org/html/rfc7946#section-3.1.5
*/
const geoJsonMultiLineStringSchema = z.strictObject({
type: z.literal("MultiLineString"),
coordinates: z.array(z.array(geoJsonPositionSchema)),
});
/**
* Helper type for reuse across polygon schemas.
*
* - A linear ring is a closed LineString with four or more positions.
* - The first and last positions are equivalent, and they MUST contain
identical values; their representation SHOULD also be identical
* - A linear ring is the boundary of a surface or the boundary of a
hole in a surface
* - A linear ring MUST follow the right-hand rule with respect to the
area it bounds, i.e., exterior rings are counterclockwise, and
holes are clockwise
*/
const geoJsonLinearRingSchema = z
.array(geoJsonPositionSchema)
.min(4, {
message: "Coordinates array must contain at least 4 positions. 3 to make a non-line shape and 1 to close the shape (duplicate of first)",
})
.superRefine((coords, ctx) => {
const first = coords[0];
const last = coords[coords.length - 1];
// Check if first and last coordinates match
if (JSON.stringify(first) !== JSON.stringify(last)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "First and last coordinate positions must be identical (to close the linear ring aka polygon).",
});
}
// Check if consecutive points are identical (excluding first and last)
for (let i = 1; i < coords.length - 1; i++) {
if (JSON.stringify(coords[i]) === JSON.stringify(coords[i - 1])) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Consecutive coordinates at index ${i - 1} and ${i} should not be identical.`,
});
}
}
});
/**
* Polygon geometry object.
* https://tools.ietf.org/html/rfc7946#section-3.1.6
*/
const geoJsonPolygonSchema = z.strictObject({
type: z.literal("Polygon"),
coordinates: z.array(geoJsonLinearRingSchema),
});
/**
* MultiPolygon geometry object.
* https://tools.ietf.org/html/rfc7946#section-3.1.7
*/
const geoJsonMultiPolygonSchema = z.strictObject({
type: z.literal("MultiPolygon"),
coordinates: z.array(z.array(geoJsonLinearRingSchema)),
});
// The same for Geometry, GeometryCollection, GeoJsonProperties, Feature, FeatureCollection, etc.
const geoJsonGeometrySchema = z.union([
geoJsonPointSchema,
geoJsonMultiPointSchema,
geoJsonLineStringSchema,
geoJsonMultiLineStringSchema,
geoJsonPolygonSchema,
geoJsonMultiPolygonSchema,
]);
//* -------- Bbox -------- *//
/**
* 2D bounding box of the GeoJSON object.
* The value of the Bbox member is an array of length 4.
*
* [min_lon, min_lat, max_lon, max_lat]
*/
const geoJsonBboxSchema = z
.tuple([z.number(), z.number(), z.number(), z.number()])
.refine(([minLng, minLat, maxLng, maxLat]) => maxLng > minLng && maxLat > minLat, {
message: "Invalid bounding box: maxLng should be greater than minLng, and maxLat should be greater than minLat.",
});
const EARTH_RADIUS = 6378137; // Earth’s mean radius in meters
/**
* @description Creates a polygon (with no holes) from a bounding box.
*/
const getPolygonFromBbox = (bbox) => {
const [minLon, minLat, maxLon, maxLat] = bbox;
return {
type: "Polygon",
coordinates: [
[
[minLon, minLat],
[maxLon, minLat],
[maxLon, maxLat],
[minLon, maxLat],
[minLon, minLat],
],
],
};
};
/**
* @description Creates a bounding box from a GeoJSON Polygon.
*/
const getBboxFromGeoJsonPolygon = (polygon) => {
const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
if (!polygonParsed.success) {
return null;
}
const points = polygonParsed.data.coordinates[0];
if (!points) {
// Should never happen since the schema checks for it
return null;
}
const latitudes = points.map(point => point[1]);
const longitudes = points.map(point => point[0]);
return [Math.min(...longitudes), Math.min(...latitudes), Math.max(...longitudes), Math.max(...latitudes)];
};
/**
* @description Creates a round polygon from a point and a radius.
*/
const getPolygonFromPointAndRadius = (point, radius) => {
const [lon, lat] = point.coordinates;
// Adjust the number of points based on radius (resolution)
const pointsCount = Math.max(32, Math.floor(radius / 100)); // More points for larger radius
const angleStep = (2 * Math.PI) / pointsCount;
const coordinates = [];
for (let i = 0; i <= pointsCount; i++) {
const angle = i * angleStep;
// Calculate offset in latitude and longitude
const deltaLat = (radius / EARTH_RADIUS) * (180 / Math.PI);
const deltaLon = deltaLat / Math.cos((lat * Math.PI) / 180);
// Calculate new coordinates based on angle
const newLat = lat + deltaLat * Math.sin(angle);
const newLon = lon + deltaLon * Math.cos(angle);
coordinates.push([newLon, newLat]);
}
return {
type: "Polygon",
coordinates: [coordinates],
};
};
/**
* @description Creates a TU bounding box from a GeoJson Polygon.
*/
const getBoundingBoxFromGeoJsonPolygon = (polygon) => {
const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
if (!polygonParsed.success) {
return null;
}
const points = polygonParsed.data.coordinates[0];
if (!points) {
// Should never happen since the schema checks for it
return null;
}
const latitudes = points.map(point => point[1]);
const longitudes = points.map(point => point[0]);
return {
nw: {
latitude: Math.max(...latitudes),
longitude: Math.min(...longitudes),
},
se: {
latitude: Math.min(...latitudes),
longitude: Math.max(...longitudes),
},
};
};
/**
* @description Creates a GeoJSON Polygon from a TU bounding box.
*/
const getGeoJsonPolygonFromBoundingBox = (boundingBox) => {
const { nw, se } = boundingBox;
return {
type: "Polygon",
coordinates: [
[
[nw.longitude, nw.latitude], // Northwest corner
[se.longitude, nw.latitude], // Northeast corner
[se.longitude, se.latitude], // Southeast corner
[nw.longitude, se.latitude], // Southwest corner
[nw.longitude, nw.latitude], // Close the loop back to Northwest corner
],
],
};
};
/**
* @description Creates TU point coordinate from a GeoJSON Point.
*/
const getPointCoordinateFromGeoJsonPoint = (point) => {
return { latitude: point.coordinates[1], longitude: point.coordinates[0] };
};
/**
* @description Gets the extreme point of a polygon in a given direction.
* @param {object} params - The parameters object
* @param {GeoJsonPolygon} params.polygon - The polygon to get the extreme point from
* @param {("top" | "right" | "bottom" | "left")} params.direction - The direction to get the extreme point in
* @returns {GeoJsonPoint} The extreme point in the given direction
*/
const getExtremeGeoJsonPointFromPolygon = ({ polygon, direction, }) => {
var _a, _b, _c;
const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
if (!polygonParsed.success) {
return null;
}
const firstPoint = (_a = polygonParsed.data.coordinates[0]) === null || _a === void 0 ? void 0 : _a[0];
if (!firstPoint) {
// Should never happen since the schema checks for it
return null;
}
const extremePosition = (_b = polygonParsed.data.coordinates[0]) === null || _b === void 0 ? void 0 : _b.reduce((extremePoint, currentPoint) => {
var _a, _b, _c, _d;
switch (direction) {
case "top":
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;
case "right":
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;
case "bottom":
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;
case "left":
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;
default: {
throw new Error(`${direction} is not known`);
}
}
}, (_c = polygonParsed.data.coordinates[0]) === null || _c === void 0 ? void 0 : _c[0]);
return extremePosition
? {
type: "Point",
coordinates: extremePosition,
}
: null; // Should never happen since the schema checks for it
};
/**
* Checks if a position is inside a linear ring. On edge is considered inside.
*/
const isGeoJsonPositionInLinearRing = ({ position, linearRing, }) => {
const linearRingParsed = geoJsonLinearRingSchema.safeParse(linearRing);
if (!linearRingParsed.success) {
return null;
}
let inside = false;
const [x, y] = position;
for (let i = 0, j = linearRingParsed.data.length - 1; i < linearRingParsed.data.length; j = i++) {
const point1 = linearRingParsed.data[i];
const point2 = linearRingParsed.data[j];
if (!point1 || !point2) {
continue;
}
const [xi, yi] = point1;
const [xj, yj] = point2;
const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
if (intersect) {
inside = !inside;
}
}
return inside;
};
/**
* @description Checks if a point is inside a polygon.
*/
const isGeoJsonPointInPolygon = ({ point, polygon, }) => {
const polygonParsed = geoJsonPolygonSchema.safeParse(polygon);
if (!polygonParsed.success) {
return null;
}
return polygonParsed.data.coordinates.some(linearRing => isGeoJsonPositionInLinearRing({ position: point.coordinates, linearRing }));
};
/**
* Checks if polygon1 is fully contained within polygon2
*/
const isFullyContainedInGeoJsonPolygon = (polygon1, polygon2) => {
const polygon1Parsed = geoJsonPolygonSchema.safeParse(polygon1);
const polygon2Parsed = geoJsonPolygonSchema.safeParse(polygon2);
// The schema checks more than a TypeScript type can represent
if (!polygon1Parsed.success || !polygon2Parsed.success) {
return null;
}
return polygon1Parsed.data.coordinates.every(linearRing => polygon2Parsed.data.coordinates.some(lr => linearRing.every(position => isGeoJsonPositionInLinearRing({ position, linearRing: lr }))));
};
/**
* @description Gets the intersection between two GeoJSON polygons. If one polygon is fully contained within the other,
* returns the contained polygon. Otherwise returns a MultiPolygon representing the intersection.
* @param polygon1 The first polygon to check intersection
* @param polygon2 The second polygon to check intersection
* @returns {(GeoJsonMultiPolygon | GeoJsonPolygon)} The intersection as either a Polygon (if one contains the other) or MultiPolygon
*/
const getGeoJsonPolygonIntersection = (polygon1, polygon2) => {
const polygon1Parsed = geoJsonPolygonSchema.safeParse(polygon1);
const polygon2Parsed = geoJsonPolygonSchema.safeParse(polygon2);
if (!polygon1Parsed.success || !polygon2Parsed.success) {
return null;
}
if (isFullyContainedInGeoJsonPolygon(polygon1, polygon2)) {
return polygon1;
}
if (isFullyContainedInGeoJsonPolygon(polygon2, polygon1)) {
return polygon2;
}
const intersectionResult = intersection$1(polygon1.coordinates, polygon2.coordinates);
if (intersectionResult.length === 1 && intersectionResult[0]) {
return {
type: "Polygon",
coordinates: intersectionResult[0],
};
}
return {
type: "MultiPolygon",
coordinates: intersection$1(polygon1.coordinates, polygon2.coordinates),
};
};
//! These tools are used to bridge the gap with out poorly typed graphql types
// Should be ideally be avoided but are needed until we fix the graphql types
const isDoubleNestedCoords = (coords) => Array.isArray(coords) &&
Array.isArray(coords[0]) &&
Array.isArray(coords[0][0]) &&
typeof coords[0][0][0] === "number";
const isSingleCoords = (coords) => typeof coords[0] === "number";
/**
* @description Returns coordinates in consistent format
* @param inconsistentCoordinates Single point, array of points or nested array of points
* @returns {GeoJsonPosition[]} Array of standardized coordinates
*/
const coordinatesToStandardFormat = (inconsistentCoordinates) => {
if (!inconsistentCoordinates) {
return [];
}
if (isSingleCoords(inconsistentCoordinates)) {
return [inconsistentCoordinates];
}
if (isDoubleNestedCoords(inconsistentCoordinates)) {
return inconsistentCoordinates[0] || [];
}
if (inconsistentCoordinates[0] && typeof inconsistentCoordinates[0][0] === "number") {
return inconsistentCoordinates;
}
return [];
};
/**
* @description Extracts a point coordinate from a GeoJSON object.
* @param geoObject A GeoJSON object.
* @returns {PointCoordinate} A point coordinate.
*/
const getPointCoordinateFromGeoJsonObject = (geoObject) => {
if (!geoObject) {
return undefined;
}
else if ("geometry" in geoObject) {
return getPointCoordinateFromGeoJsonObject(geoObject.geometry);
}
else if ("coordinates" in geoObject &&
Array.isArray(geoObject.coordinates) &&
typeof geoObject.coordinates[0] === "number" &&
typeof geoObject.coordinates[1] === "number") {
const [point] = coordinatesToStandardFormat(geoObject.coordinates);
if (point) {
return { latitude: point[1], longitude: point[0] };
}
else {
throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
}
}
else {
throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
}
};
/**
* @description Extracts multiple point coordinates from a GeoJSON object.
* @param geoObject A GeoJSON object.
* @returns {PointCoordinate[]} An array of point coordinates.
* @example getMultipleCoordinatesFromGeoJsonObject({ type: "Point", coordinates: [1, 2] }) // [{ longitude: 1, latitude: 2 }]
*/
const getMultipleCoordinatesFromGeoJsonObject = (geoObject) => {
if (!geoObject) {
return undefined;
}
else if ("geometry" in geoObject) {
return getMultipleCoordinatesFromGeoJsonObject(geoObject.geometry);
}
else if ("coordinates" in geoObject) {
return coordinatesToStandardFormat(geoObject.coordinates).map(([longitude, latitude]) => ({ longitude, latitude }));
}
else {
throw new Error(`Unable to extract point coordinate from ${JSON.stringify(geoObject)}`);
}
};
//* -------- Trackunit-invented schemas and types to extend the GeoJson spec -------- *//
/**
* Polygon geometry object that explicitly disallows holes.
*
* Same as geoJsonPolygonSchema but type disallows holes by
* using tuple of one single linear ring instead of an array.
*/
const tuGeoJsonPolygonNoHolesSchema = z.strictObject({
//The type is still "Polygon" (not PolygonNoHoles or similar) since it's always
//compliant with Polygon, just not the other way around
type: z.literal("Polygon"),
//uses tuple instead of array to enforce only 1 linear ring aka the polygon itself
coordinates: z.tuple([geoJsonLinearRingSchema]),
});
/**
* Point radius object.
* For when you wish to define an area by a point and a radius.
*
* radius is in meters
*/
const tuGeoJsonPointRadiusSchema = z.strictObject({
type: z.literal("PointRadius"),
coordinates: geoJsonPositionSchema,
radius: z.number().positive(), // in meters
});
/**
* A Polygon with exactly 5 points and 4 horizontal/vertical sides that form a normal rectangular box.
*/
const tuGeoJsonRectangularBoxPolygonSchema = z
.strictObject({
type: z.literal("Polygon"),
coordinates: z.array(geoJsonLinearRingSchema),
})
.superRefine((data, ctx) => {
const coordinates = data.coordinates[0];
// Validate polygon has exactly 5 points
if ((coordinates === null || coordinates === void 0 ? void 0 : coordinates.length) !== 5) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Polygon must have exactly 5 coordinates to form a closed box.",
});
return;
}
// Check each side is either horizontal or vertical
for (let i = 0; i < 4; i++) {
const point1 = coordinates[i];
const point2 = coordinates[i + 1];
if (point1 === undefined || point2 === undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Each coordinate must be a defined point.",
});
return;
}
const [x1, y1] = point1;
const [x2, y2] = point2;
// Ensure each line segment is either horizontal or vertical
if (x1 !== x2 && y1 !== y2) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Polygon sides must be horizontal or vertical to form a box shape.",
});
return;
}
}
});
/**
* Group an array of items by a key.

@@ -1550,2 +1061,2 @@ *

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 };
export { DateTimeFormat, HoursAndMinutesFormat, align, alphabeticallySort, arrayLengthCompare, arrayNotEmpty, booleanCompare, capitalize, convertBlobToBase64, convertMetersToYards, convertYardsToMeters, dateCompare, deleteUndefinedKeys, difference, doNothing, enumFromValue, enumFromValueTypesafe, enumOrUndefinedFromValue, exhaustiveCheck, filterByMultiple, formatAddress, formatCoordinates, fuzzySearch, getDifferenceBetweenDates, getEndOfDay, getFirstLevelObjectPropertyDifferences, getISOStringFromDate, 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 };

6

package.json
{
"name": "@trackunit/shared-utils",
"version": "0.0.85",
"version": "0.0.86",
"repository": "https://github.com/Trackunit/manager",

@@ -10,5 +10,3 @@ "license": "SEE LICENSE IN LICENSE.txt",

"dependencies": {
"uuid": "^10.0.0",
"zod": "3.22.4",
"polygon-clipping": "^0.15.7"
"uuid": "^10.0.0"
},

@@ -15,0 +13,0 @@ "module": "./index.esm.js",

@@ -12,6 +12,2 @@ export * from "./addressUtils";

export * from "./filter";
export * from "./GeoJson/GeoJsonSchemas";
export * from "./GeoJson/GeoJsonUtils";
export * from "./GeoJson/TUGeoJsonObjectBridgeUtils";
export * from "./GeoJson/TuGeoJsonSchemas";
export * from "./groupBy/groupBy";

@@ -18,0 +14,0 @@ export * from "./GroupingUtility";

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc