@trackunit/geo-json-utils 1.14.30 → 1.14.31
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 +203 -0
- package/index.esm.js +197 -1
- package/package.json +1 -1
- package/src/GeoJsonUtils.d.ts +43 -0
package/index.cjs.js
CHANGED
|
@@ -1256,6 +1256,202 @@ const getGeoJsonPolygonIntersection = (polygon1, polygon2) => {
|
|
|
1256
1256
|
coordinates: intersectionResult,
|
|
1257
1257
|
};
|
|
1258
1258
|
};
|
|
1259
|
+
/**
|
|
1260
|
+
* Computes the difference of subject minus the union of all clips.
|
|
1261
|
+
* Returns null when the subject is fully covered (zero remaining area).
|
|
1262
|
+
* Returns Polygon for a single-ring result, MultiPolygon otherwise.
|
|
1263
|
+
* When clips is empty the subject is returned unchanged.
|
|
1264
|
+
*/
|
|
1265
|
+
const geoJsonPolygonDifference = (subject, clips) => {
|
|
1266
|
+
if (clips.length === 0) {
|
|
1267
|
+
return subject;
|
|
1268
|
+
}
|
|
1269
|
+
const subjectClip = subject.type === "MultiPolygon" ? toClipMultiPolygon(subject.coordinates) : [toClipPolygon(subject.coordinates)];
|
|
1270
|
+
const clipsConverted = clips.map(clip => clip.type === "MultiPolygon" ? toClipMultiPolygon(clip.coordinates) : [toClipPolygon(clip.coordinates)]);
|
|
1271
|
+
let differenceResult;
|
|
1272
|
+
try {
|
|
1273
|
+
differenceResult = polygonClipping.difference(subjectClip, ...clipsConverted);
|
|
1274
|
+
}
|
|
1275
|
+
catch {
|
|
1276
|
+
// polygon-clipping can throw "Unable to complete output ring" for degenerate or
|
|
1277
|
+
// near-self-intersecting polygons. Fall back to returning the subject unclipped
|
|
1278
|
+
// so the shape still renders rather than crashing the caller.
|
|
1279
|
+
return subject;
|
|
1280
|
+
}
|
|
1281
|
+
if (differenceResult.length === 0) {
|
|
1282
|
+
return null;
|
|
1283
|
+
}
|
|
1284
|
+
if (differenceResult.length === 1 && differenceResult[0]) {
|
|
1285
|
+
return {
|
|
1286
|
+
type: "Polygon",
|
|
1287
|
+
coordinates: differenceResult[0],
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
return {
|
|
1291
|
+
type: "MultiPolygon",
|
|
1292
|
+
coordinates: differenceResult,
|
|
1293
|
+
};
|
|
1294
|
+
};
|
|
1295
|
+
// Web Mercator (EPSG:3857) latitude limit. Beyond this the projection diverges to
|
|
1296
|
+
// infinity; clamping keeps the round-trip finite for shapes near the poles.
|
|
1297
|
+
const WEB_MERCATOR_MAX_LAT = 85.05112878;
|
|
1298
|
+
const DEG_TO_RAD$1 = Math.PI / 180;
|
|
1299
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
1300
|
+
/**
|
|
1301
|
+
* Project a `[lng, lat]` position to Web Mercator metres (EPSG:3857).
|
|
1302
|
+
*
|
|
1303
|
+
* Web map renderers (Google Maps, Mapbox, …) draw straight polygon edges in this
|
|
1304
|
+
* projected space, not in planar lng/lat. Boolean geometry operations whose result
|
|
1305
|
+
* must visually align with rendered edges therefore have to run in Web Mercator —
|
|
1306
|
+
* see `unprojectWebMercatorToLngLat` for the inverse.
|
|
1307
|
+
*/
|
|
1308
|
+
const projectLngLatToWebMercator = (position) => {
|
|
1309
|
+
const [lng, lat] = position;
|
|
1310
|
+
const clampedLat = Math.max(-WEB_MERCATOR_MAX_LAT, Math.min(WEB_MERCATOR_MAX_LAT, lat));
|
|
1311
|
+
const x = lng * DEG_TO_RAD$1 * EARTH_RADIUS;
|
|
1312
|
+
const y = Math.log(Math.tan(Math.PI / 4 + (clampedLat * DEG_TO_RAD$1) / 2)) * EARTH_RADIUS;
|
|
1313
|
+
return [x, y];
|
|
1314
|
+
};
|
|
1315
|
+
/** Inverse of `projectLngLatToWebMercator`: Web Mercator metres back to `[lng, lat]`. */
|
|
1316
|
+
const unprojectWebMercatorToLngLat = (position) => {
|
|
1317
|
+
const [x, y] = position;
|
|
1318
|
+
const lng = (x / EARTH_RADIUS) * RAD_TO_DEG;
|
|
1319
|
+
const lat = (2 * Math.atan(Math.exp(y / EARTH_RADIUS)) - Math.PI / 2) * RAD_TO_DEG;
|
|
1320
|
+
return [lng, lat];
|
|
1321
|
+
};
|
|
1322
|
+
const mapPolygonalCoordinates = (geometry, project) => {
|
|
1323
|
+
const mapRing = (ring) => ring.map(project);
|
|
1324
|
+
if (geometry.type === "Polygon") {
|
|
1325
|
+
return { type: "Polygon", coordinates: geometry.coordinates.map(mapRing) };
|
|
1326
|
+
}
|
|
1327
|
+
return { type: "MultiPolygon", coordinates: geometry.coordinates.map(rings => rings.map(mapRing)) };
|
|
1328
|
+
};
|
|
1329
|
+
/**
|
|
1330
|
+
* Project a polygonal geometry's coordinates to Web Mercator metres, preserving its
|
|
1331
|
+
* ring/part structure. The output is no longer valid lng/lat — feed it to operations
|
|
1332
|
+
* such as `geoJsonPolygonDifference` and unproject the result with
|
|
1333
|
+
* `unprojectPolygonalFromWebMercator`.
|
|
1334
|
+
*/
|
|
1335
|
+
const projectPolygonalToWebMercator = (geometry) => mapPolygonalCoordinates(geometry, projectLngLatToWebMercator);
|
|
1336
|
+
/** Inverse of `projectPolygonalToWebMercator`. */
|
|
1337
|
+
const unprojectPolygonalFromWebMercator = (geometry) => mapPolygonalCoordinates(geometry, unprojectWebMercatorToLngLat);
|
|
1338
|
+
/**
|
|
1339
|
+
* Generalisation of isFullyContainedInGeoJsonPolygon to Polygon|MultiPolygon arguments.
|
|
1340
|
+
* Returns true when every outer-ring vertex of geom1 lies inside geom2 (holes respected).
|
|
1341
|
+
* For a MultiPolygon geom2, "inside" means inside any one of its polygon members.
|
|
1342
|
+
* Returns null if either geometry fails validation.
|
|
1343
|
+
*/
|
|
1344
|
+
const isFullyContainedInGeoJsonGeometry = (geom1, geom2) => {
|
|
1345
|
+
const outerRings1 = [];
|
|
1346
|
+
if (geom1.type === "Polygon") {
|
|
1347
|
+
const parsed = geoJsonPolygonSchema.safeParse(geom1);
|
|
1348
|
+
if (!parsed.success)
|
|
1349
|
+
return null;
|
|
1350
|
+
const outerRing = parsed.data.coordinates[0];
|
|
1351
|
+
if (!outerRing)
|
|
1352
|
+
return null;
|
|
1353
|
+
outerRings1.push(outerRing);
|
|
1354
|
+
}
|
|
1355
|
+
else {
|
|
1356
|
+
const parsed = geoJsonMultiPolygonSchema.safeParse(geom1);
|
|
1357
|
+
if (!parsed.success)
|
|
1358
|
+
return null;
|
|
1359
|
+
for (const poly of parsed.data.coordinates) {
|
|
1360
|
+
const outerRing = poly[0];
|
|
1361
|
+
if (!outerRing)
|
|
1362
|
+
return null;
|
|
1363
|
+
outerRings1.push(outerRing);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
const polygonParts2 = [];
|
|
1367
|
+
if (geom2.type === "Polygon") {
|
|
1368
|
+
const parsed = geoJsonPolygonSchema.safeParse(geom2);
|
|
1369
|
+
if (!parsed.success)
|
|
1370
|
+
return null;
|
|
1371
|
+
polygonParts2.push(parsed.data);
|
|
1372
|
+
}
|
|
1373
|
+
else {
|
|
1374
|
+
const parsed = geoJsonMultiPolygonSchema.safeParse(geom2);
|
|
1375
|
+
if (!parsed.success)
|
|
1376
|
+
return null;
|
|
1377
|
+
for (const poly of parsed.data.coordinates) {
|
|
1378
|
+
polygonParts2.push({ type: "Polygon", coordinates: poly });
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
for (const outerRing of outerRings1) {
|
|
1382
|
+
for (const position of outerRing) {
|
|
1383
|
+
const point = { coordinates: position };
|
|
1384
|
+
let insideAny = false;
|
|
1385
|
+
for (const part of polygonParts2) {
|
|
1386
|
+
const result = isGeoJsonPointInPolygon({ point, polygon: part });
|
|
1387
|
+
if (result === null)
|
|
1388
|
+
return null;
|
|
1389
|
+
if (result) {
|
|
1390
|
+
insideAny = true;
|
|
1391
|
+
break;
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
if (!insideAny)
|
|
1395
|
+
return false;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return true;
|
|
1399
|
+
};
|
|
1400
|
+
/**
|
|
1401
|
+
* Returns the minimum distance (in coordinate units) from the given position to the
|
|
1402
|
+
* nearest edge segment of any ring of the polygon/multipolygon, including hole rings.
|
|
1403
|
+
* Returns null if the geometry fails validation.
|
|
1404
|
+
*
|
|
1405
|
+
* Distance is in geographic-coordinate units (degrees), appropriate for relative
|
|
1406
|
+
* Penetration Depth comparisons where only ordering matters, not absolute metric values.
|
|
1407
|
+
*/
|
|
1408
|
+
const distanceToGeoJsonPolygonBoundary = (position, geometry) => {
|
|
1409
|
+
const rings = [];
|
|
1410
|
+
if (geometry.type === "Polygon") {
|
|
1411
|
+
const parsed = geoJsonPolygonSchema.safeParse(geometry);
|
|
1412
|
+
if (!parsed.success)
|
|
1413
|
+
return null;
|
|
1414
|
+
rings.push(...parsed.data.coordinates);
|
|
1415
|
+
}
|
|
1416
|
+
else {
|
|
1417
|
+
const parsed = geoJsonMultiPolygonSchema.safeParse(geometry);
|
|
1418
|
+
if (!parsed.success)
|
|
1419
|
+
return null;
|
|
1420
|
+
for (const poly of parsed.data.coordinates) {
|
|
1421
|
+
rings.push(...poly);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
if (rings.length === 0)
|
|
1425
|
+
return null;
|
|
1426
|
+
const [px, py] = position;
|
|
1427
|
+
let minDist = Infinity;
|
|
1428
|
+
for (const ring of rings) {
|
|
1429
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
1430
|
+
const a = ring[j];
|
|
1431
|
+
const b = ring[i];
|
|
1432
|
+
if (!a || !b)
|
|
1433
|
+
continue;
|
|
1434
|
+
const [ax, ay] = a;
|
|
1435
|
+
const [bx, by] = b;
|
|
1436
|
+
const dx = bx - ax;
|
|
1437
|
+
const dy = by - ay;
|
|
1438
|
+
let dist;
|
|
1439
|
+
if (dx === 0 && dy === 0) {
|
|
1440
|
+
dist = Math.sqrt((px - ax) ** 2 + (py - ay) ** 2);
|
|
1441
|
+
}
|
|
1442
|
+
else {
|
|
1443
|
+
const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy)));
|
|
1444
|
+
const nearx = ax + t * dx;
|
|
1445
|
+
const neary = ay + t * dy;
|
|
1446
|
+
dist = Math.sqrt((px - nearx) ** 2 + (py - neary) ** 2);
|
|
1447
|
+
}
|
|
1448
|
+
if (dist < minDist) {
|
|
1449
|
+
minDist = dist;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return minDist === Infinity ? null : minDist;
|
|
1454
|
+
};
|
|
1259
1455
|
|
|
1260
1456
|
//! These tools are used to bridge the gap with out poorly typed graphql types
|
|
1261
1457
|
// Should be ideally be avoided but are needed until we fix the graphql types
|
|
@@ -1553,6 +1749,7 @@ exports.checkCrossesMeridian = checkCrossesMeridian;
|
|
|
1553
1749
|
exports.computeGeometryCentroid = computeGeometryCentroid;
|
|
1554
1750
|
exports.coordinatesToStandardFormat = coordinatesToStandardFormat;
|
|
1555
1751
|
exports.denormalizeLongitude = denormalizeLongitude;
|
|
1752
|
+
exports.distanceToGeoJsonPolygonBoundary = distanceToGeoJsonPolygonBoundary;
|
|
1556
1753
|
exports.edgePixelLength = edgePixelLength;
|
|
1557
1754
|
exports.extractEdges = extractEdges;
|
|
1558
1755
|
exports.extractFirstPointCoordinate = extractFirstPointCoordinate;
|
|
@@ -1568,6 +1765,7 @@ exports.geoJsonMultiLineStringSchema = geoJsonMultiLineStringSchema;
|
|
|
1568
1765
|
exports.geoJsonMultiPointSchema = geoJsonMultiPointSchema;
|
|
1569
1766
|
exports.geoJsonMultiPolygonSchema = geoJsonMultiPolygonSchema;
|
|
1570
1767
|
exports.geoJsonPointSchema = geoJsonPointSchema;
|
|
1768
|
+
exports.geoJsonPolygonDifference = geoJsonPolygonDifference;
|
|
1571
1769
|
exports.geoJsonPolygonSchema = geoJsonPolygonSchema;
|
|
1572
1770
|
exports.geoJsonPosition2dSchema = geoJsonPosition2dSchema;
|
|
1573
1771
|
exports.geoJsonPositionSchema = geoJsonPositionSchema;
|
|
@@ -1584,11 +1782,14 @@ exports.getPointCoordinateFromGeoJsonPoint = getPointCoordinateFromGeoJsonPoint;
|
|
|
1584
1782
|
exports.getPolygonFromBbox = getPolygonFromBbox;
|
|
1585
1783
|
exports.getPolygonFromPointAndRadius = getPolygonFromPointAndRadius;
|
|
1586
1784
|
exports.isBboxInsideFeatureCollection = isBboxInsideFeatureCollection;
|
|
1785
|
+
exports.isFullyContainedInGeoJsonGeometry = isFullyContainedInGeoJsonGeometry;
|
|
1587
1786
|
exports.isFullyContainedInGeoJsonPolygon = isFullyContainedInGeoJsonPolygon;
|
|
1588
1787
|
exports.isGeoJsonPointInPolygon = isGeoJsonPointInPolygon;
|
|
1589
1788
|
exports.isGeoJsonPositionInLinearRing = isGeoJsonPositionInLinearRing;
|
|
1590
1789
|
exports.isPositionInsideRing = isPositionInsideRing;
|
|
1591
1790
|
exports.normalizeLongitudes = normalizeLongitudes;
|
|
1791
|
+
exports.projectLngLatToWebMercator = projectLngLatToWebMercator;
|
|
1792
|
+
exports.projectPolygonalToWebMercator = projectPolygonalToWebMercator;
|
|
1592
1793
|
exports.splitPolygonAtAntimeridian = splitPolygonAtAntimeridian;
|
|
1593
1794
|
exports.splitPolygonWithHolesAtAntimeridian = splitPolygonWithHolesAtAntimeridian;
|
|
1594
1795
|
exports.toFeatureCollection = toFeatureCollection;
|
|
@@ -1596,6 +1797,8 @@ exports.toPosition2d = toPosition2d;
|
|
|
1596
1797
|
exports.tuGeoJsonPointRadiusSchema = tuGeoJsonPointRadiusSchema;
|
|
1597
1798
|
exports.tuGeoJsonPolygonNoHolesSchema = tuGeoJsonPolygonNoHolesSchema;
|
|
1598
1799
|
exports.tuGeoJsonRectangularBoxPolygonSchema = tuGeoJsonRectangularBoxPolygonSchema;
|
|
1800
|
+
exports.unprojectPolygonalFromWebMercator = unprojectPolygonalFromWebMercator;
|
|
1801
|
+
exports.unprojectWebMercatorToLngLat = unprojectWebMercatorToLngLat;
|
|
1599
1802
|
exports.validateBbox = validateBbox;
|
|
1600
1803
|
exports.validateBboxWithFallback = validateBboxWithFallback;
|
|
1601
1804
|
exports.validateFeatureCollection = validateFeatureCollection;
|
package/index.esm.js
CHANGED
|
@@ -1254,6 +1254,202 @@ const getGeoJsonPolygonIntersection = (polygon1, polygon2) => {
|
|
|
1254
1254
|
coordinates: intersectionResult,
|
|
1255
1255
|
};
|
|
1256
1256
|
};
|
|
1257
|
+
/**
|
|
1258
|
+
* Computes the difference of subject minus the union of all clips.
|
|
1259
|
+
* Returns null when the subject is fully covered (zero remaining area).
|
|
1260
|
+
* Returns Polygon for a single-ring result, MultiPolygon otherwise.
|
|
1261
|
+
* When clips is empty the subject is returned unchanged.
|
|
1262
|
+
*/
|
|
1263
|
+
const geoJsonPolygonDifference = (subject, clips) => {
|
|
1264
|
+
if (clips.length === 0) {
|
|
1265
|
+
return subject;
|
|
1266
|
+
}
|
|
1267
|
+
const subjectClip = subject.type === "MultiPolygon" ? toClipMultiPolygon(subject.coordinates) : [toClipPolygon(subject.coordinates)];
|
|
1268
|
+
const clipsConverted = clips.map(clip => clip.type === "MultiPolygon" ? toClipMultiPolygon(clip.coordinates) : [toClipPolygon(clip.coordinates)]);
|
|
1269
|
+
let differenceResult;
|
|
1270
|
+
try {
|
|
1271
|
+
differenceResult = polygonClipping.difference(subjectClip, ...clipsConverted);
|
|
1272
|
+
}
|
|
1273
|
+
catch {
|
|
1274
|
+
// polygon-clipping can throw "Unable to complete output ring" for degenerate or
|
|
1275
|
+
// near-self-intersecting polygons. Fall back to returning the subject unclipped
|
|
1276
|
+
// so the shape still renders rather than crashing the caller.
|
|
1277
|
+
return subject;
|
|
1278
|
+
}
|
|
1279
|
+
if (differenceResult.length === 0) {
|
|
1280
|
+
return null;
|
|
1281
|
+
}
|
|
1282
|
+
if (differenceResult.length === 1 && differenceResult[0]) {
|
|
1283
|
+
return {
|
|
1284
|
+
type: "Polygon",
|
|
1285
|
+
coordinates: differenceResult[0],
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
return {
|
|
1289
|
+
type: "MultiPolygon",
|
|
1290
|
+
coordinates: differenceResult,
|
|
1291
|
+
};
|
|
1292
|
+
};
|
|
1293
|
+
// Web Mercator (EPSG:3857) latitude limit. Beyond this the projection diverges to
|
|
1294
|
+
// infinity; clamping keeps the round-trip finite for shapes near the poles.
|
|
1295
|
+
const WEB_MERCATOR_MAX_LAT = 85.05112878;
|
|
1296
|
+
const DEG_TO_RAD$1 = Math.PI / 180;
|
|
1297
|
+
const RAD_TO_DEG = 180 / Math.PI;
|
|
1298
|
+
/**
|
|
1299
|
+
* Project a `[lng, lat]` position to Web Mercator metres (EPSG:3857).
|
|
1300
|
+
*
|
|
1301
|
+
* Web map renderers (Google Maps, Mapbox, …) draw straight polygon edges in this
|
|
1302
|
+
* projected space, not in planar lng/lat. Boolean geometry operations whose result
|
|
1303
|
+
* must visually align with rendered edges therefore have to run in Web Mercator —
|
|
1304
|
+
* see `unprojectWebMercatorToLngLat` for the inverse.
|
|
1305
|
+
*/
|
|
1306
|
+
const projectLngLatToWebMercator = (position) => {
|
|
1307
|
+
const [lng, lat] = position;
|
|
1308
|
+
const clampedLat = Math.max(-WEB_MERCATOR_MAX_LAT, Math.min(WEB_MERCATOR_MAX_LAT, lat));
|
|
1309
|
+
const x = lng * DEG_TO_RAD$1 * EARTH_RADIUS;
|
|
1310
|
+
const y = Math.log(Math.tan(Math.PI / 4 + (clampedLat * DEG_TO_RAD$1) / 2)) * EARTH_RADIUS;
|
|
1311
|
+
return [x, y];
|
|
1312
|
+
};
|
|
1313
|
+
/** Inverse of `projectLngLatToWebMercator`: Web Mercator metres back to `[lng, lat]`. */
|
|
1314
|
+
const unprojectWebMercatorToLngLat = (position) => {
|
|
1315
|
+
const [x, y] = position;
|
|
1316
|
+
const lng = (x / EARTH_RADIUS) * RAD_TO_DEG;
|
|
1317
|
+
const lat = (2 * Math.atan(Math.exp(y / EARTH_RADIUS)) - Math.PI / 2) * RAD_TO_DEG;
|
|
1318
|
+
return [lng, lat];
|
|
1319
|
+
};
|
|
1320
|
+
const mapPolygonalCoordinates = (geometry, project) => {
|
|
1321
|
+
const mapRing = (ring) => ring.map(project);
|
|
1322
|
+
if (geometry.type === "Polygon") {
|
|
1323
|
+
return { type: "Polygon", coordinates: geometry.coordinates.map(mapRing) };
|
|
1324
|
+
}
|
|
1325
|
+
return { type: "MultiPolygon", coordinates: geometry.coordinates.map(rings => rings.map(mapRing)) };
|
|
1326
|
+
};
|
|
1327
|
+
/**
|
|
1328
|
+
* Project a polygonal geometry's coordinates to Web Mercator metres, preserving its
|
|
1329
|
+
* ring/part structure. The output is no longer valid lng/lat — feed it to operations
|
|
1330
|
+
* such as `geoJsonPolygonDifference` and unproject the result with
|
|
1331
|
+
* `unprojectPolygonalFromWebMercator`.
|
|
1332
|
+
*/
|
|
1333
|
+
const projectPolygonalToWebMercator = (geometry) => mapPolygonalCoordinates(geometry, projectLngLatToWebMercator);
|
|
1334
|
+
/** Inverse of `projectPolygonalToWebMercator`. */
|
|
1335
|
+
const unprojectPolygonalFromWebMercator = (geometry) => mapPolygonalCoordinates(geometry, unprojectWebMercatorToLngLat);
|
|
1336
|
+
/**
|
|
1337
|
+
* Generalisation of isFullyContainedInGeoJsonPolygon to Polygon|MultiPolygon arguments.
|
|
1338
|
+
* Returns true when every outer-ring vertex of geom1 lies inside geom2 (holes respected).
|
|
1339
|
+
* For a MultiPolygon geom2, "inside" means inside any one of its polygon members.
|
|
1340
|
+
* Returns null if either geometry fails validation.
|
|
1341
|
+
*/
|
|
1342
|
+
const isFullyContainedInGeoJsonGeometry = (geom1, geom2) => {
|
|
1343
|
+
const outerRings1 = [];
|
|
1344
|
+
if (geom1.type === "Polygon") {
|
|
1345
|
+
const parsed = geoJsonPolygonSchema.safeParse(geom1);
|
|
1346
|
+
if (!parsed.success)
|
|
1347
|
+
return null;
|
|
1348
|
+
const outerRing = parsed.data.coordinates[0];
|
|
1349
|
+
if (!outerRing)
|
|
1350
|
+
return null;
|
|
1351
|
+
outerRings1.push(outerRing);
|
|
1352
|
+
}
|
|
1353
|
+
else {
|
|
1354
|
+
const parsed = geoJsonMultiPolygonSchema.safeParse(geom1);
|
|
1355
|
+
if (!parsed.success)
|
|
1356
|
+
return null;
|
|
1357
|
+
for (const poly of parsed.data.coordinates) {
|
|
1358
|
+
const outerRing = poly[0];
|
|
1359
|
+
if (!outerRing)
|
|
1360
|
+
return null;
|
|
1361
|
+
outerRings1.push(outerRing);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
const polygonParts2 = [];
|
|
1365
|
+
if (geom2.type === "Polygon") {
|
|
1366
|
+
const parsed = geoJsonPolygonSchema.safeParse(geom2);
|
|
1367
|
+
if (!parsed.success)
|
|
1368
|
+
return null;
|
|
1369
|
+
polygonParts2.push(parsed.data);
|
|
1370
|
+
}
|
|
1371
|
+
else {
|
|
1372
|
+
const parsed = geoJsonMultiPolygonSchema.safeParse(geom2);
|
|
1373
|
+
if (!parsed.success)
|
|
1374
|
+
return null;
|
|
1375
|
+
for (const poly of parsed.data.coordinates) {
|
|
1376
|
+
polygonParts2.push({ type: "Polygon", coordinates: poly });
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
for (const outerRing of outerRings1) {
|
|
1380
|
+
for (const position of outerRing) {
|
|
1381
|
+
const point = { coordinates: position };
|
|
1382
|
+
let insideAny = false;
|
|
1383
|
+
for (const part of polygonParts2) {
|
|
1384
|
+
const result = isGeoJsonPointInPolygon({ point, polygon: part });
|
|
1385
|
+
if (result === null)
|
|
1386
|
+
return null;
|
|
1387
|
+
if (result) {
|
|
1388
|
+
insideAny = true;
|
|
1389
|
+
break;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
if (!insideAny)
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
return true;
|
|
1397
|
+
};
|
|
1398
|
+
/**
|
|
1399
|
+
* Returns the minimum distance (in coordinate units) from the given position to the
|
|
1400
|
+
* nearest edge segment of any ring of the polygon/multipolygon, including hole rings.
|
|
1401
|
+
* Returns null if the geometry fails validation.
|
|
1402
|
+
*
|
|
1403
|
+
* Distance is in geographic-coordinate units (degrees), appropriate for relative
|
|
1404
|
+
* Penetration Depth comparisons where only ordering matters, not absolute metric values.
|
|
1405
|
+
*/
|
|
1406
|
+
const distanceToGeoJsonPolygonBoundary = (position, geometry) => {
|
|
1407
|
+
const rings = [];
|
|
1408
|
+
if (geometry.type === "Polygon") {
|
|
1409
|
+
const parsed = geoJsonPolygonSchema.safeParse(geometry);
|
|
1410
|
+
if (!parsed.success)
|
|
1411
|
+
return null;
|
|
1412
|
+
rings.push(...parsed.data.coordinates);
|
|
1413
|
+
}
|
|
1414
|
+
else {
|
|
1415
|
+
const parsed = geoJsonMultiPolygonSchema.safeParse(geometry);
|
|
1416
|
+
if (!parsed.success)
|
|
1417
|
+
return null;
|
|
1418
|
+
for (const poly of parsed.data.coordinates) {
|
|
1419
|
+
rings.push(...poly);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
if (rings.length === 0)
|
|
1423
|
+
return null;
|
|
1424
|
+
const [px, py] = position;
|
|
1425
|
+
let minDist = Infinity;
|
|
1426
|
+
for (const ring of rings) {
|
|
1427
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
1428
|
+
const a = ring[j];
|
|
1429
|
+
const b = ring[i];
|
|
1430
|
+
if (!a || !b)
|
|
1431
|
+
continue;
|
|
1432
|
+
const [ax, ay] = a;
|
|
1433
|
+
const [bx, by] = b;
|
|
1434
|
+
const dx = bx - ax;
|
|
1435
|
+
const dy = by - ay;
|
|
1436
|
+
let dist;
|
|
1437
|
+
if (dx === 0 && dy === 0) {
|
|
1438
|
+
dist = Math.sqrt((px - ax) ** 2 + (py - ay) ** 2);
|
|
1439
|
+
}
|
|
1440
|
+
else {
|
|
1441
|
+
const t = Math.max(0, Math.min(1, ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy)));
|
|
1442
|
+
const nearx = ax + t * dx;
|
|
1443
|
+
const neary = ay + t * dy;
|
|
1444
|
+
dist = Math.sqrt((px - nearx) ** 2 + (py - neary) ** 2);
|
|
1445
|
+
}
|
|
1446
|
+
if (dist < minDist) {
|
|
1447
|
+
minDist = dist;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
return minDist === Infinity ? null : minDist;
|
|
1452
|
+
};
|
|
1257
1453
|
|
|
1258
1454
|
//! These tools are used to bridge the gap with out poorly typed graphql types
|
|
1259
1455
|
// Should be ideally be avoided but are needed until we fix the graphql types
|
|
@@ -1544,4 +1740,4 @@ const edgePixelLength = (x0, y0, x1, y1, zoom, midLatDeg, tileSize = 256) => {
|
|
|
1544
1740
|
return Math.sqrt(dxPx * dxPx + dyPx * dyPx);
|
|
1545
1741
|
};
|
|
1546
1742
|
|
|
1547
|
-
export { EARTH_RADIUS, EMPTY_FEATURE_COLLECTION, boundingBoxCrossesMeridian, checkCrossesMeridian, computeGeometryCentroid, coordinatesToStandardFormat, denormalizeLongitude, edgePixelLength, extractEdges, extractFirstPointCoordinate, extractPositionsFromGeometry, geoJsonBboxSchema, geoJsonFeatureCollectionSchema, geoJsonFeatureSchema, geoJsonGeometryCollectionSchema, geoJsonGeometrySchema, geoJsonLineStringSchema, geoJsonLinearRingSchema, geoJsonMultiLineStringSchema, geoJsonMultiPointSchema, geoJsonMultiPolygonSchema, geoJsonPointSchema, geoJsonPolygonSchema, geoJsonPosition2dSchema, geoJsonPositionSchema, getBboxFromGeoJsonPolygon, getBoundingBoxFromGeoJsonBbox, getBoundingBoxFromGeoJsonPolygon, getExtremeGeoJsonPointFromPolygon, getGeoJsonPolygonFromBoundingBox, getGeoJsonPolygonIntersection, getMinMaxLongitudes, getMultipleCoordinatesFromGeoJsonObject, getPointCoordinateFromGeoJsonObject, getPointCoordinateFromGeoJsonPoint, getPolygonFromBbox, getPolygonFromPointAndRadius, isBboxInsideFeatureCollection, isFullyContainedInGeoJsonPolygon, isGeoJsonPointInPolygon, isGeoJsonPositionInLinearRing, isPositionInsideRing, normalizeLongitudes, splitPolygonAtAntimeridian, splitPolygonWithHolesAtAntimeridian, toFeatureCollection, toPosition2d, tuGeoJsonPointRadiusSchema, tuGeoJsonPolygonNoHolesSchema, tuGeoJsonRectangularBoxPolygonSchema, validateBbox, validateBboxWithFallback, validateFeatureCollection, validatePosition };
|
|
1743
|
+
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, isGeoJsonPointInPolygon, isGeoJsonPositionInLinearRing, isPositionInsideRing, normalizeLongitudes, projectLngLatToWebMercator, projectPolygonalToWebMercator, splitPolygonAtAntimeridian, splitPolygonWithHolesAtAntimeridian, toFeatureCollection, toPosition2d, tuGeoJsonPointRadiusSchema, tuGeoJsonPolygonNoHolesSchema, tuGeoJsonRectangularBoxPolygonSchema, unprojectPolygonalFromWebMercator, unprojectWebMercatorToLngLat, validateBbox, validateBboxWithFallback, validateFeatureCollection, validatePosition };
|
package/package.json
CHANGED
package/src/GeoJsonUtils.d.ts
CHANGED
|
@@ -101,3 +101,46 @@ export declare const isFullyContainedInGeoJsonPolygon: (polygon1: GeoJsonPolygon
|
|
|
101
101
|
* Returns a MultiPolygon representing the intersection, or null if there is no intersection.
|
|
102
102
|
*/
|
|
103
103
|
export declare const getGeoJsonPolygonIntersection: (polygon1: GeoJsonPolygon | GeoJsonMultiPolygon, polygon2: GeoJsonPolygon | GeoJsonMultiPolygon) => GeoJsonMultiPolygon | GeoJsonPolygon | null;
|
|
104
|
+
/**
|
|
105
|
+
* Computes the difference of subject minus the union of all clips.
|
|
106
|
+
* Returns null when the subject is fully covered (zero remaining area).
|
|
107
|
+
* Returns Polygon for a single-ring result, MultiPolygon otherwise.
|
|
108
|
+
* When clips is empty the subject is returned unchanged.
|
|
109
|
+
*/
|
|
110
|
+
export declare const geoJsonPolygonDifference: (subject: GeoJsonPolygon | GeoJsonMultiPolygon, clips: ReadonlyArray<GeoJsonPolygon | GeoJsonMultiPolygon>) => GeoJsonPolygon | GeoJsonMultiPolygon | null;
|
|
111
|
+
/**
|
|
112
|
+
* Project a `[lng, lat]` position to Web Mercator metres (EPSG:3857).
|
|
113
|
+
*
|
|
114
|
+
* Web map renderers (Google Maps, Mapbox, …) draw straight polygon edges in this
|
|
115
|
+
* projected space, not in planar lng/lat. Boolean geometry operations whose result
|
|
116
|
+
* must visually align with rendered edges therefore have to run in Web Mercator —
|
|
117
|
+
* see `unprojectWebMercatorToLngLat` for the inverse.
|
|
118
|
+
*/
|
|
119
|
+
export declare const projectLngLatToWebMercator: (position: GeoJsonPosition) => GeoJsonPosition;
|
|
120
|
+
/** Inverse of `projectLngLatToWebMercator`: Web Mercator metres back to `[lng, lat]`. */
|
|
121
|
+
export declare const unprojectWebMercatorToLngLat: (position: GeoJsonPosition) => GeoJsonPosition;
|
|
122
|
+
/**
|
|
123
|
+
* Project a polygonal geometry's coordinates to Web Mercator metres, preserving its
|
|
124
|
+
* ring/part structure. The output is no longer valid lng/lat — feed it to operations
|
|
125
|
+
* such as `geoJsonPolygonDifference` and unproject the result with
|
|
126
|
+
* `unprojectPolygonalFromWebMercator`.
|
|
127
|
+
*/
|
|
128
|
+
export declare const projectPolygonalToWebMercator: (geometry: GeoJsonPolygon | GeoJsonMultiPolygon) => GeoJsonPolygon | GeoJsonMultiPolygon;
|
|
129
|
+
/** Inverse of `projectPolygonalToWebMercator`. */
|
|
130
|
+
export declare const unprojectPolygonalFromWebMercator: (geometry: GeoJsonPolygon | GeoJsonMultiPolygon) => GeoJsonPolygon | GeoJsonMultiPolygon;
|
|
131
|
+
/**
|
|
132
|
+
* Generalisation of isFullyContainedInGeoJsonPolygon to Polygon|MultiPolygon arguments.
|
|
133
|
+
* Returns true when every outer-ring vertex of geom1 lies inside geom2 (holes respected).
|
|
134
|
+
* For a MultiPolygon geom2, "inside" means inside any one of its polygon members.
|
|
135
|
+
* Returns null if either geometry fails validation.
|
|
136
|
+
*/
|
|
137
|
+
export declare const isFullyContainedInGeoJsonGeometry: (geom1: GeoJsonPolygon | GeoJsonMultiPolygon, geom2: GeoJsonPolygon | GeoJsonMultiPolygon) => boolean | null;
|
|
138
|
+
/**
|
|
139
|
+
* Returns the minimum distance (in coordinate units) from the given position to the
|
|
140
|
+
* nearest edge segment of any ring of the polygon/multipolygon, including hole rings.
|
|
141
|
+
* Returns null if the geometry fails validation.
|
|
142
|
+
*
|
|
143
|
+
* Distance is in geographic-coordinate units (degrees), appropriate for relative
|
|
144
|
+
* Penetration Depth comparisons where only ordering matters, not absolute metric values.
|
|
145
|
+
*/
|
|
146
|
+
export declare const distanceToGeoJsonPolygonBoundary: (position: GeoJsonPosition, geometry: GeoJsonPolygon | GeoJsonMultiPolygon) => number | null;
|