@trackunit/geo-json-utils 1.15.41 → 1.15.42

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
@@ -1079,6 +1079,76 @@ const distributeInteriorRingsForAntimeridianSplit = (holeRings, eastHoles, westH
1079
1079
  }
1080
1080
  return true;
1081
1081
  };
1082
+ const isSeamJumpDelta = (delta) => {
1083
+ const absDelta = Math.abs(delta);
1084
+ return absDelta > 180 && absDelta < 360;
1085
+ };
1086
+ const ringCrossesAntimeridian = (ring) => ring.some((position, index) => {
1087
+ const next = ring[index + 1];
1088
+ return next !== undefined && isSeamJumpDelta(next[0] - position[0]);
1089
+ });
1090
+ const shiftPositionLongitude = (position, offset) => {
1091
+ const longitude = position[0] + offset;
1092
+ if (position.length === 3) {
1093
+ return [longitude, position[1], position[2]];
1094
+ }
1095
+ return [longitude, position[1]];
1096
+ };
1097
+ /**
1098
+ * Rewrite a ring's longitudes to be continuous across the antimeridian.
1099
+ *
1100
+ * Backend polygons store every longitude in `[-180, 180]`, so a shape spanning
1101
+ * the dateline appears as a single ring with a ±360° jump between two adjacent
1102
+ * vertices (e.g. `170 → -170`). Walking the ring and accumulating a ±360° offset
1103
+ * whenever a jump is detected unwraps those vertices into continuous coordinates
1104
+ * (`170 → 190`), which is the form the unwrapped split path expects.
1105
+ *
1106
+ * `initialOffset` anchors the ring's first vertex to a specific longitude world.
1107
+ * Interior rings pass the offset computed from the exterior so both rings end up
1108
+ * in the same world.
1109
+ */
1110
+ const unwrapRingLongitudes = (ring, initialOffset = 0) => {
1111
+ let offset = initialOffset;
1112
+ let prevLng;
1113
+ return ring.map(position => {
1114
+ const lng = position[0];
1115
+ if (prevLng !== undefined) {
1116
+ const delta = lng - prevLng;
1117
+ if (delta > 180 && delta < 360) {
1118
+ offset -= 360;
1119
+ }
1120
+ else if (delta < -180 && delta > -360) {
1121
+ offset += 360;
1122
+ }
1123
+ }
1124
+ prevLng = lng;
1125
+ return shiftPositionLongitude(position, offset);
1126
+ });
1127
+ };
1128
+ const HOLE_UNWRAP_OFFSETS = [0, 360, -360];
1129
+ const unwrapSeamJumpingPolygon = (polygon) => {
1130
+ const rings = polygon.coordinates;
1131
+ const exterior = rings[0];
1132
+ if (exterior === undefined) {
1133
+ return polygon;
1134
+ }
1135
+ const unwrappedExterior = unwrapRingLongitudes(exterior);
1136
+ const refLng = unwrappedExterior[0]?.[0] ?? 0;
1137
+ const unwrappedHoles = rings.slice(1).map(hole => {
1138
+ const firstLng = hole[0]?.[0];
1139
+ if (firstLng === undefined) {
1140
+ return unwrapRingLongitudes(hole, 0);
1141
+ }
1142
+ const useOffset = HOLE_UNWRAP_OFFSETS.reduce((best, offset) => {
1143
+ return Math.abs(firstLng + offset - refLng) < Math.abs(firstLng + best - refLng) ? offset : best;
1144
+ }, 0);
1145
+ return unwrapRingLongitudes(hole, useOffset);
1146
+ });
1147
+ return {
1148
+ type: "Polygon",
1149
+ coordinates: [unwrappedExterior, ...unwrappedHoles],
1150
+ };
1151
+ };
1082
1152
  /**
1083
1153
  * @description Splits a polygon (exterior + holes) at the antimeridian (±180°)
1084
1154
  * into a two-member MultiPolygon per RFC 7946 Section 3.1.9, preserving
@@ -1086,6 +1156,9 @@ const distributeInteriorRingsForAntimeridianSplit = (holeRings, eastHoles, westH
1086
1156
  * If the exterior does not cross the antimeridian, the input is returned unchanged
1087
1157
  * (including any hole coordinates).
1088
1158
  *
1159
+ * Exterior rings that stay in `[-180, 180]` but contain a longitude jump wider
1160
+ * than 180° (the form site geofence APIs return) are unwrapped first, then split.
1161
+ *
1089
1162
  * Interior rings that lie entirely in [-180, 180] are assigned to exactly one
1090
1163
  * shell using longitude midpoint (see `oneSidedHoleGoesToFirstMultiPolygonMember`).
1091
1164
  * If splitting the exterior or any crossing hole would produce a fragment with
@@ -1100,7 +1173,12 @@ const splitPolygonWithHolesAtAntimeridian = (polygon) => {
1100
1173
  const holeRings = polygon.coordinates.slice(1);
1101
1174
  const { maxLng, minLng } = ringLongitudeBounds(outerRing);
1102
1175
  if (maxLng <= 180 && minLng >= -180) {
1103
- return polygon;
1176
+ if (!ringCrossesAntimeridian(outerRing)) {
1177
+ return polygon;
1178
+ }
1179
+ const unwrapped = unwrapSeamJumpingPolygon(polygon);
1180
+ const result = splitPolygonWithHolesAtAntimeridian(unwrapped);
1181
+ return result === unwrapped ? polygon : result;
1104
1182
  }
1105
1183
  const eastHoles = [];
1106
1184
  const westHoles = [];
@@ -1162,17 +1240,89 @@ const splitPolygonWithHolesAtAntimeridian = (polygon) => {
1162
1240
  * MultiPolygon per RFC 7946 Section 3.1.9. If the polygon does not cross the
1163
1241
  * antimeridian, it is returned unchanged.
1164
1242
  *
1165
- * Accepts unwrapped longitudes (outside [-180, 180]) as input for example,
1166
- * coordinates from a map SDK where the user panned past the antimeridian.
1167
- * The output is always RFC 7946 compliant (all longitudes in [-180, 180]).
1168
- *
1169
- * For use in a future polygon draw mode: the user draws a polygon on the map
1170
- * with coordinates that may wrap past ±180, and this function produces the
1171
- * RFC 7946-compliant split representation.
1243
+ * Accepts both unwrapped longitudes (outside [-180, 180], e.g. from a map SDK
1244
+ * after panning past the dateline) and seam-jumping rings that stay in
1245
+ * [-180, 180] (e.g. `170 -170` from a site geofence API). The output is
1246
+ * always RFC 7946 compliant (all longitudes in [-180, 180]).
1172
1247
  *
1173
1248
  * Delegates to {@link splitPolygonWithHolesAtAntimeridian} (holes are preserved).
1174
1249
  */
1175
1250
  const splitPolygonAtAntimeridian = (polygon) => splitPolygonWithHolesAtAntimeridian(polygon);
1251
+ const splitAntimeridianCrossingPolygonMembers = (members) => {
1252
+ let modified = false;
1253
+ const splitMembers = [];
1254
+ for (const coordinates of members) {
1255
+ const polygon = { type: "Polygon", coordinates };
1256
+ const result = splitPolygonAtAntimeridian(polygon);
1257
+ if (result !== polygon) {
1258
+ modified = true;
1259
+ }
1260
+ if (result.type === "Polygon") {
1261
+ splitMembers.push(result.coordinates);
1262
+ }
1263
+ else {
1264
+ splitMembers.push(...result.coordinates);
1265
+ }
1266
+ }
1267
+ return modified ? splitMembers : null;
1268
+ };
1269
+ /**
1270
+ * Split Polygon / MultiPolygon geometry that crosses the antimeridian into RFC
1271
+ * 7946 MultiPolygon halves. GeometryCollection children are split recursively.
1272
+ * Other geometry types are returned unchanged.
1273
+ *
1274
+ * Seam-jumping rings in `[-180, 180]` and already-unwrapped rings are both
1275
+ * handled. Already-split halves that do not themselves jump the seam are
1276
+ * returned by reference.
1277
+ */
1278
+ const splitAntimeridianCrossingGeometry = (geometry) => {
1279
+ if (geometry.type === "Polygon") {
1280
+ return splitPolygonAtAntimeridian(geometry);
1281
+ }
1282
+ if (geometry.type === "MultiPolygon") {
1283
+ const splitMembers = splitAntimeridianCrossingPolygonMembers(geometry.coordinates);
1284
+ if (splitMembers === null) {
1285
+ return geometry;
1286
+ }
1287
+ return { type: "MultiPolygon", coordinates: splitMembers };
1288
+ }
1289
+ if (geometry.type === "GeometryCollection") {
1290
+ let modified = false;
1291
+ const geometries = [];
1292
+ for (const child of geometry.geometries) {
1293
+ const split = splitAntimeridianCrossingGeometry(child);
1294
+ if (split !== child) {
1295
+ modified = true;
1296
+ }
1297
+ geometries.push(split);
1298
+ }
1299
+ return modified ? { ...geometry, geometries } : geometry;
1300
+ }
1301
+ return geometry;
1302
+ };
1303
+ /**
1304
+ * RFC-canonicalize every polygonal feature that crosses the antimeridian.
1305
+ * Returns the input collection when nothing changed.
1306
+ */
1307
+ const splitAntimeridianCrossingFeatures = (collection) => {
1308
+ let modified = false;
1309
+ const features = [];
1310
+ for (const feature of collection.features) {
1311
+ const sourceGeometry = feature.geometry;
1312
+ if (sourceGeometry === null) {
1313
+ features.push(feature);
1314
+ continue;
1315
+ }
1316
+ const geometry = splitAntimeridianCrossingGeometry(sourceGeometry);
1317
+ if (geometry === sourceGeometry) {
1318
+ features.push(feature);
1319
+ continue;
1320
+ }
1321
+ modified = true;
1322
+ features.push({ ...feature, geometry });
1323
+ }
1324
+ return modified ? { ...collection, features } : collection;
1325
+ };
1176
1326
  /**
1177
1327
  * @description Gets the extreme point of a polygon in a given direction.
1178
1328
  * @param {object} params - The parameters object
@@ -1964,6 +2114,8 @@ exports.pixelsToLatDegrees = pixelsToLatDegrees;
1964
2114
  exports.projectLngLatToWebMercator = projectLngLatToWebMercator;
1965
2115
  exports.projectPolygonalToWebMercator = projectPolygonalToWebMercator;
1966
2116
  exports.scaleGeoJsonBbox = scaleGeoJsonBbox;
2117
+ exports.splitAntimeridianCrossingFeatures = splitAntimeridianCrossingFeatures;
2118
+ exports.splitAntimeridianCrossingGeometry = splitAntimeridianCrossingGeometry;
1967
2119
  exports.splitPolygonAtAntimeridian = splitPolygonAtAntimeridian;
1968
2120
  exports.splitPolygonWithHolesAtAntimeridian = splitPolygonWithHolesAtAntimeridian;
1969
2121
  exports.toFeatureCollection = toFeatureCollection;
package/index.esm.js CHANGED
@@ -1077,6 +1077,76 @@ const distributeInteriorRingsForAntimeridianSplit = (holeRings, eastHoles, westH
1077
1077
  }
1078
1078
  return true;
1079
1079
  };
1080
+ const isSeamJumpDelta = (delta) => {
1081
+ const absDelta = Math.abs(delta);
1082
+ return absDelta > 180 && absDelta < 360;
1083
+ };
1084
+ const ringCrossesAntimeridian = (ring) => ring.some((position, index) => {
1085
+ const next = ring[index + 1];
1086
+ return next !== undefined && isSeamJumpDelta(next[0] - position[0]);
1087
+ });
1088
+ const shiftPositionLongitude = (position, offset) => {
1089
+ const longitude = position[0] + offset;
1090
+ if (position.length === 3) {
1091
+ return [longitude, position[1], position[2]];
1092
+ }
1093
+ return [longitude, position[1]];
1094
+ };
1095
+ /**
1096
+ * Rewrite a ring's longitudes to be continuous across the antimeridian.
1097
+ *
1098
+ * Backend polygons store every longitude in `[-180, 180]`, so a shape spanning
1099
+ * the dateline appears as a single ring with a ±360° jump between two adjacent
1100
+ * vertices (e.g. `170 → -170`). Walking the ring and accumulating a ±360° offset
1101
+ * whenever a jump is detected unwraps those vertices into continuous coordinates
1102
+ * (`170 → 190`), which is the form the unwrapped split path expects.
1103
+ *
1104
+ * `initialOffset` anchors the ring's first vertex to a specific longitude world.
1105
+ * Interior rings pass the offset computed from the exterior so both rings end up
1106
+ * in the same world.
1107
+ */
1108
+ const unwrapRingLongitudes = (ring, initialOffset = 0) => {
1109
+ let offset = initialOffset;
1110
+ let prevLng;
1111
+ return ring.map(position => {
1112
+ const lng = position[0];
1113
+ if (prevLng !== undefined) {
1114
+ const delta = lng - prevLng;
1115
+ if (delta > 180 && delta < 360) {
1116
+ offset -= 360;
1117
+ }
1118
+ else if (delta < -180 && delta > -360) {
1119
+ offset += 360;
1120
+ }
1121
+ }
1122
+ prevLng = lng;
1123
+ return shiftPositionLongitude(position, offset);
1124
+ });
1125
+ };
1126
+ const HOLE_UNWRAP_OFFSETS = [0, 360, -360];
1127
+ const unwrapSeamJumpingPolygon = (polygon) => {
1128
+ const rings = polygon.coordinates;
1129
+ const exterior = rings[0];
1130
+ if (exterior === undefined) {
1131
+ return polygon;
1132
+ }
1133
+ const unwrappedExterior = unwrapRingLongitudes(exterior);
1134
+ const refLng = unwrappedExterior[0]?.[0] ?? 0;
1135
+ const unwrappedHoles = rings.slice(1).map(hole => {
1136
+ const firstLng = hole[0]?.[0];
1137
+ if (firstLng === undefined) {
1138
+ return unwrapRingLongitudes(hole, 0);
1139
+ }
1140
+ const useOffset = HOLE_UNWRAP_OFFSETS.reduce((best, offset) => {
1141
+ return Math.abs(firstLng + offset - refLng) < Math.abs(firstLng + best - refLng) ? offset : best;
1142
+ }, 0);
1143
+ return unwrapRingLongitudes(hole, useOffset);
1144
+ });
1145
+ return {
1146
+ type: "Polygon",
1147
+ coordinates: [unwrappedExterior, ...unwrappedHoles],
1148
+ };
1149
+ };
1080
1150
  /**
1081
1151
  * @description Splits a polygon (exterior + holes) at the antimeridian (±180°)
1082
1152
  * into a two-member MultiPolygon per RFC 7946 Section 3.1.9, preserving
@@ -1084,6 +1154,9 @@ const distributeInteriorRingsForAntimeridianSplit = (holeRings, eastHoles, westH
1084
1154
  * If the exterior does not cross the antimeridian, the input is returned unchanged
1085
1155
  * (including any hole coordinates).
1086
1156
  *
1157
+ * Exterior rings that stay in `[-180, 180]` but contain a longitude jump wider
1158
+ * than 180° (the form site geofence APIs return) are unwrapped first, then split.
1159
+ *
1087
1160
  * Interior rings that lie entirely in [-180, 180] are assigned to exactly one
1088
1161
  * shell using longitude midpoint (see `oneSidedHoleGoesToFirstMultiPolygonMember`).
1089
1162
  * If splitting the exterior or any crossing hole would produce a fragment with
@@ -1098,7 +1171,12 @@ const splitPolygonWithHolesAtAntimeridian = (polygon) => {
1098
1171
  const holeRings = polygon.coordinates.slice(1);
1099
1172
  const { maxLng, minLng } = ringLongitudeBounds(outerRing);
1100
1173
  if (maxLng <= 180 && minLng >= -180) {
1101
- return polygon;
1174
+ if (!ringCrossesAntimeridian(outerRing)) {
1175
+ return polygon;
1176
+ }
1177
+ const unwrapped = unwrapSeamJumpingPolygon(polygon);
1178
+ const result = splitPolygonWithHolesAtAntimeridian(unwrapped);
1179
+ return result === unwrapped ? polygon : result;
1102
1180
  }
1103
1181
  const eastHoles = [];
1104
1182
  const westHoles = [];
@@ -1160,17 +1238,89 @@ const splitPolygonWithHolesAtAntimeridian = (polygon) => {
1160
1238
  * MultiPolygon per RFC 7946 Section 3.1.9. If the polygon does not cross the
1161
1239
  * antimeridian, it is returned unchanged.
1162
1240
  *
1163
- * Accepts unwrapped longitudes (outside [-180, 180]) as input for example,
1164
- * coordinates from a map SDK where the user panned past the antimeridian.
1165
- * The output is always RFC 7946 compliant (all longitudes in [-180, 180]).
1166
- *
1167
- * For use in a future polygon draw mode: the user draws a polygon on the map
1168
- * with coordinates that may wrap past ±180, and this function produces the
1169
- * RFC 7946-compliant split representation.
1241
+ * Accepts both unwrapped longitudes (outside [-180, 180], e.g. from a map SDK
1242
+ * after panning past the dateline) and seam-jumping rings that stay in
1243
+ * [-180, 180] (e.g. `170 -170` from a site geofence API). The output is
1244
+ * always RFC 7946 compliant (all longitudes in [-180, 180]).
1170
1245
  *
1171
1246
  * Delegates to {@link splitPolygonWithHolesAtAntimeridian} (holes are preserved).
1172
1247
  */
1173
1248
  const splitPolygonAtAntimeridian = (polygon) => splitPolygonWithHolesAtAntimeridian(polygon);
1249
+ const splitAntimeridianCrossingPolygonMembers = (members) => {
1250
+ let modified = false;
1251
+ const splitMembers = [];
1252
+ for (const coordinates of members) {
1253
+ const polygon = { type: "Polygon", coordinates };
1254
+ const result = splitPolygonAtAntimeridian(polygon);
1255
+ if (result !== polygon) {
1256
+ modified = true;
1257
+ }
1258
+ if (result.type === "Polygon") {
1259
+ splitMembers.push(result.coordinates);
1260
+ }
1261
+ else {
1262
+ splitMembers.push(...result.coordinates);
1263
+ }
1264
+ }
1265
+ return modified ? splitMembers : null;
1266
+ };
1267
+ /**
1268
+ * Split Polygon / MultiPolygon geometry that crosses the antimeridian into RFC
1269
+ * 7946 MultiPolygon halves. GeometryCollection children are split recursively.
1270
+ * Other geometry types are returned unchanged.
1271
+ *
1272
+ * Seam-jumping rings in `[-180, 180]` and already-unwrapped rings are both
1273
+ * handled. Already-split halves that do not themselves jump the seam are
1274
+ * returned by reference.
1275
+ */
1276
+ const splitAntimeridianCrossingGeometry = (geometry) => {
1277
+ if (geometry.type === "Polygon") {
1278
+ return splitPolygonAtAntimeridian(geometry);
1279
+ }
1280
+ if (geometry.type === "MultiPolygon") {
1281
+ const splitMembers = splitAntimeridianCrossingPolygonMembers(geometry.coordinates);
1282
+ if (splitMembers === null) {
1283
+ return geometry;
1284
+ }
1285
+ return { type: "MultiPolygon", coordinates: splitMembers };
1286
+ }
1287
+ if (geometry.type === "GeometryCollection") {
1288
+ let modified = false;
1289
+ const geometries = [];
1290
+ for (const child of geometry.geometries) {
1291
+ const split = splitAntimeridianCrossingGeometry(child);
1292
+ if (split !== child) {
1293
+ modified = true;
1294
+ }
1295
+ geometries.push(split);
1296
+ }
1297
+ return modified ? { ...geometry, geometries } : geometry;
1298
+ }
1299
+ return geometry;
1300
+ };
1301
+ /**
1302
+ * RFC-canonicalize every polygonal feature that crosses the antimeridian.
1303
+ * Returns the input collection when nothing changed.
1304
+ */
1305
+ const splitAntimeridianCrossingFeatures = (collection) => {
1306
+ let modified = false;
1307
+ const features = [];
1308
+ for (const feature of collection.features) {
1309
+ const sourceGeometry = feature.geometry;
1310
+ if (sourceGeometry === null) {
1311
+ features.push(feature);
1312
+ continue;
1313
+ }
1314
+ const geometry = splitAntimeridianCrossingGeometry(sourceGeometry);
1315
+ if (geometry === sourceGeometry) {
1316
+ features.push(feature);
1317
+ continue;
1318
+ }
1319
+ modified = true;
1320
+ features.push({ ...feature, geometry });
1321
+ }
1322
+ return modified ? { ...collection, features } : collection;
1323
+ };
1174
1324
  /**
1175
1325
  * @description Gets the extreme point of a polygon in a given direction.
1176
1326
  * @param {object} params - The parameters object
@@ -1906,4 +2056,4 @@ const edgePixelLength = (x0, y0, x1, y1, zoom, midLatDeg, tileSize = 256) => {
1906
2056
  return Math.sqrt(dxPx * dxPx + dyPx * dyPx);
1907
2057
  };
1908
2058
 
1909
- export { EARTH_RADIUS, EMPTY_FEATURE_COLLECTION, boundingBoxCrossesMeridian, checkCrossesMeridian, computeGeometryCentroid, coordinatesToStandardFormat, denormalizeLongitude, distanceToGeoJsonPolygonBoundary, edgePixelLength, extractEdges, extractFirstPointCoordinate, extractPositionsFromGeometry, geoJsonBboxSchema, geoJsonFeatureCollectionSchema, geoJsonFeatureSchema, geoJsonGeometryCollectionSchema, geoJsonGeometrySchema, geoJsonLineStringSchema, geoJsonLinearRingSchema, geoJsonMultiLineStringSchema, geoJsonMultiPointSchema, geoJsonMultiPolygonSchema, geoJsonPointSchema, geoJsonPolygonDifference, geoJsonPolygonSchema, geoJsonPosition2dSchema, geoJsonPositionSchema, getBboxFromGeoJsonPolygon, getBoundingBoxFromGeoJsonBbox, getBoundingBoxFromGeoJsonPolygon, getExtremeGeoJsonPointFromPolygon, getGeoJsonPolygonFromBoundingBox, getGeoJsonPolygonIntersection, getMinMaxLongitudes, getMultipleCoordinatesFromGeoJsonObject, getPointCoordinateFromGeoJsonObject, getPointCoordinateFromGeoJsonPoint, getPolygonFromBbox, getPolygonFromPointAndRadius, isBboxInsideFeatureCollection, isFullyContainedInGeoJsonGeometry, isFullyContainedInGeoJsonPolygon, isFullyContainedInGeometry, isGeoJsonPointInPolygon, isGeoJsonPositionInLinearRing, isPointInPolygon, isPositionInsideRing, lngLatToMercatorPxWS, lngLatToWebMercatorPx, mercatorPxToLngLatWS, normalizeLongitudes, padGeoJsonBbox, pixelsToLatDegrees, projectLngLatToWebMercator, projectPolygonalToWebMercator, scaleGeoJsonBbox, splitPolygonAtAntimeridian, splitPolygonWithHolesAtAntimeridian, toFeatureCollection, toPosition2d, tuGeoJsonPointRadiusSchema, tuGeoJsonPolygonNoHolesSchema, tuGeoJsonRectangularBoxPolygonSchema, unprojectPolygonalFromWebMercator, unprojectWebMercatorToLngLat, validateBbox, validateBboxWithFallback, validateFeatureCollection, validatePosition, webMercatorPxToLngLat };
2059
+ export { EARTH_RADIUS, EMPTY_FEATURE_COLLECTION, boundingBoxCrossesMeridian, checkCrossesMeridian, computeGeometryCentroid, coordinatesToStandardFormat, denormalizeLongitude, distanceToGeoJsonPolygonBoundary, edgePixelLength, extractEdges, extractFirstPointCoordinate, extractPositionsFromGeometry, geoJsonBboxSchema, geoJsonFeatureCollectionSchema, geoJsonFeatureSchema, geoJsonGeometryCollectionSchema, geoJsonGeometrySchema, geoJsonLineStringSchema, geoJsonLinearRingSchema, geoJsonMultiLineStringSchema, geoJsonMultiPointSchema, geoJsonMultiPolygonSchema, geoJsonPointSchema, geoJsonPolygonDifference, geoJsonPolygonSchema, geoJsonPosition2dSchema, geoJsonPositionSchema, getBboxFromGeoJsonPolygon, getBoundingBoxFromGeoJsonBbox, getBoundingBoxFromGeoJsonPolygon, getExtremeGeoJsonPointFromPolygon, getGeoJsonPolygonFromBoundingBox, getGeoJsonPolygonIntersection, getMinMaxLongitudes, getMultipleCoordinatesFromGeoJsonObject, getPointCoordinateFromGeoJsonObject, getPointCoordinateFromGeoJsonPoint, getPolygonFromBbox, getPolygonFromPointAndRadius, isBboxInsideFeatureCollection, isFullyContainedInGeoJsonGeometry, isFullyContainedInGeoJsonPolygon, isFullyContainedInGeometry, isGeoJsonPointInPolygon, isGeoJsonPositionInLinearRing, isPointInPolygon, isPositionInsideRing, lngLatToMercatorPxWS, lngLatToWebMercatorPx, mercatorPxToLngLatWS, normalizeLongitudes, padGeoJsonBbox, pixelsToLatDegrees, projectLngLatToWebMercator, projectPolygonalToWebMercator, scaleGeoJsonBbox, splitAntimeridianCrossingFeatures, splitAntimeridianCrossingGeometry, splitPolygonAtAntimeridian, splitPolygonWithHolesAtAntimeridian, toFeatureCollection, toPosition2d, tuGeoJsonPointRadiusSchema, tuGeoJsonPolygonNoHolesSchema, tuGeoJsonRectangularBoxPolygonSchema, unprojectPolygonalFromWebMercator, unprojectWebMercatorToLngLat, validateBbox, validateBboxWithFallback, validateFeatureCollection, validatePosition, webMercatorPxToLngLat };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trackunit/geo-json-utils",
3
- "version": "1.15.41",
3
+ "version": "1.15.42",
4
4
  "repository": "https://github.com/Trackunit/manager",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "engines": {
@@ -1,4 +1,4 @@
1
- import { GeoJsonBbox, GeoJsonLinearRing, GeoJsonMultiPolygon, GeoJsonPoint, GeoJsonPolygon, GeoJsonPosition } from "./GeoJsonSchemas";
1
+ import { GeoJsonBbox, GeoJsonFeatureCollection, GeoJsonGeometry, GeoJsonLinearRing, GeoJsonMultiPolygon, GeoJsonPoint, GeoJsonPolygon, GeoJsonPosition } from "./GeoJsonSchemas";
2
2
  export declare const EARTH_RADIUS = 6378137;
3
3
  /**
4
4
  * @description Creates a polygon from a bounding box.
@@ -39,6 +39,9 @@ export declare const getPolygonFromPointAndRadius: (point: GeoJsonPoint, radius:
39
39
  * If the exterior does not cross the antimeridian, the input is returned unchanged
40
40
  * (including any hole coordinates).
41
41
  *
42
+ * Exterior rings that stay in `[-180, 180]` but contain a longitude jump wider
43
+ * than 180° (the form site geofence APIs return) are unwrapped first, then split.
44
+ *
42
45
  * Interior rings that lie entirely in [-180, 180] are assigned to exactly one
43
46
  * shell using longitude midpoint (see `oneSidedHoleGoesToFirstMultiPolygonMember`).
44
47
  * If splitting the exterior or any crossing hole would produce a fragment with
@@ -51,17 +54,29 @@ export declare const splitPolygonWithHolesAtAntimeridian: (polygon: GeoJsonPolyg
51
54
  * MultiPolygon per RFC 7946 Section 3.1.9. If the polygon does not cross the
52
55
  * antimeridian, it is returned unchanged.
53
56
  *
54
- * Accepts unwrapped longitudes (outside [-180, 180]) as input for example,
55
- * coordinates from a map SDK where the user panned past the antimeridian.
56
- * The output is always RFC 7946 compliant (all longitudes in [-180, 180]).
57
- *
58
- * For use in a future polygon draw mode: the user draws a polygon on the map
59
- * with coordinates that may wrap past ±180, and this function produces the
60
- * RFC 7946-compliant split representation.
57
+ * Accepts both unwrapped longitudes (outside [-180, 180], e.g. from a map SDK
58
+ * after panning past the dateline) and seam-jumping rings that stay in
59
+ * [-180, 180] (e.g. `170 -170` from a site geofence API). The output is
60
+ * always RFC 7946 compliant (all longitudes in [-180, 180]).
61
61
  *
62
62
  * Delegates to {@link splitPolygonWithHolesAtAntimeridian} (holes are preserved).
63
63
  */
64
64
  export declare const splitPolygonAtAntimeridian: (polygon: GeoJsonPolygon) => GeoJsonPolygon | GeoJsonMultiPolygon;
65
+ /**
66
+ * Split Polygon / MultiPolygon geometry that crosses the antimeridian into RFC
67
+ * 7946 MultiPolygon halves. GeometryCollection children are split recursively.
68
+ * Other geometry types are returned unchanged.
69
+ *
70
+ * Seam-jumping rings in `[-180, 180]` and already-unwrapped rings are both
71
+ * handled. Already-split halves that do not themselves jump the seam are
72
+ * returned by reference.
73
+ */
74
+ export declare const splitAntimeridianCrossingGeometry: (geometry: GeoJsonGeometry) => GeoJsonGeometry;
75
+ /**
76
+ * RFC-canonicalize every polygonal feature that crosses the antimeridian.
77
+ * Returns the input collection when nothing changed.
78
+ */
79
+ export declare const splitAntimeridianCrossingFeatures: (collection: GeoJsonFeatureCollection) => GeoJsonFeatureCollection;
65
80
  /**
66
81
  * @description Gets the extreme point of a polygon in a given direction.
67
82
  * @param {object} params - The parameters object