@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 +160 -8
- package/index.esm.js +159 -9
- package/package.json +1 -1
- package/src/GeoJsonUtils.d.ts +23 -8
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
|
-
|
|
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]
|
|
1166
|
-
*
|
|
1167
|
-
*
|
|
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
|
-
|
|
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]
|
|
1164
|
-
*
|
|
1165
|
-
*
|
|
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
package/src/GeoJsonUtils.d.ts
CHANGED
|
@@ -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]
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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
|