Spaces:
Runtime error
Runtime error
Commit
·
2f322ce
1
Parent(s):
1d6d0dd
Fix area using geographiclib-geodesic package
Browse files- geodesiclib package is deprecated (2025). so i used geographiclib-geodesic package
- did plenty of trials on how to accurately calculated geodesic polygon's area.
- got error whenever I selected points in china or anywhere near that longitude.
- found out that i was actually inverting the (lat, lon) input to (lon, lat) and lat cannon > 90, while lon can be.
- frontend/package-lock.json +7 -0
- frontend/package.json +1 -0
- frontend/src/components/Map.js +1 -1
- frontend/src/utils/mapUtils.js +19 -55
frontend/package-lock.json
CHANGED
|
@@ -13,6 +13,7 @@
|
|
| 13 |
"@testing-library/react": "^16.3.0",
|
| 14 |
"@testing-library/user-event": "^13.5.0",
|
| 15 |
"geodesy": "^2.4.0",
|
|
|
|
| 16 |
"leaflet": "^1.9.4",
|
| 17 |
"leaflet.geodesic": "^2.7.1",
|
| 18 |
"react": "^19.0.0-rc.1",
|
|
@@ -8509,6 +8510,12 @@
|
|
| 8509 |
"node": ">=8.0.0"
|
| 8510 |
}
|
| 8511 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8512 |
"node_modules/get-caller-file": {
|
| 8513 |
"version": "2.0.5",
|
| 8514 |
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
|
|
|
| 13 |
"@testing-library/react": "^16.3.0",
|
| 14 |
"@testing-library/user-event": "^13.5.0",
|
| 15 |
"geodesy": "^2.4.0",
|
| 16 |
+
"geographiclib-geodesic": "^2.1.1",
|
| 17 |
"leaflet": "^1.9.4",
|
| 18 |
"leaflet.geodesic": "^2.7.1",
|
| 19 |
"react": "^19.0.0-rc.1",
|
|
|
|
| 8510 |
"node": ">=8.0.0"
|
| 8511 |
}
|
| 8512 |
},
|
| 8513 |
+
"node_modules/geographiclib-geodesic": {
|
| 8514 |
+
"version": "2.1.1",
|
| 8515 |
+
"resolved": "https://registry.npmjs.org/geographiclib-geodesic/-/geographiclib-geodesic-2.1.1.tgz",
|
| 8516 |
+
"integrity": "sha512-lkd8EUkPSByobWu9BPMHTdYA5AUZxOa8McmUNtBE9KrvUJEvSADnN6gTDmhXbi6NzdA16LtWLpSxLE/lIIRhyA==",
|
| 8517 |
+
"license": "MIT"
|
| 8518 |
+
},
|
| 8519 |
"node_modules/get-caller-file": {
|
| 8520 |
"version": "2.0.5",
|
| 8521 |
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
frontend/package.json
CHANGED
|
@@ -8,6 +8,7 @@
|
|
| 8 |
"@testing-library/react": "^16.3.0",
|
| 9 |
"@testing-library/user-event": "^13.5.0",
|
| 10 |
"geodesy": "^2.4.0",
|
|
|
|
| 11 |
"leaflet": "^1.9.4",
|
| 12 |
"leaflet.geodesic": "^2.7.1",
|
| 13 |
"react": "^19.0.0-rc.1",
|
|
|
|
| 8 |
"@testing-library/react": "^16.3.0",
|
| 9 |
"@testing-library/user-event": "^13.5.0",
|
| 10 |
"geodesy": "^2.4.0",
|
| 11 |
+
"geographiclib-geodesic": "^2.1.1",
|
| 12 |
"leaflet": "^1.9.4",
|
| 13 |
"leaflet.geodesic": "^2.7.1",
|
| 14 |
"react": "^19.0.0-rc.1",
|
frontend/src/components/Map.js
CHANGED
|
@@ -276,7 +276,7 @@ const Map = ( { onMapClick, searchQuery, contentType } ) => {
|
|
| 276 |
if (geoToolMode === "area" && areaPoints.length >= 3) {
|
| 277 |
// Just ensuring that the polygon is closed (first == last)
|
| 278 |
const closed = [...areaPoints, areaPoints[0]];
|
| 279 |
-
const area = calculatePolygonArea(closed.map(([lat, lon]) => [
|
| 280 |
setPolygonArea(area);
|
| 281 |
} else {
|
| 282 |
setPolygonArea(null);
|
|
|
|
| 276 |
if (geoToolMode === "area" && areaPoints.length >= 3) {
|
| 277 |
// Just ensuring that the polygon is closed (first == last)
|
| 278 |
const closed = [...areaPoints, areaPoints[0]];
|
| 279 |
+
const area = calculatePolygonArea(closed.map(([lat, lon]) => [lat, lon])); // This took me a while to figure out, it should be just (lat, lon), not (lon, lat)
|
| 280 |
setPolygonArea(area);
|
| 281 |
} else {
|
| 282 |
setPolygonArea(null);
|
frontend/src/utils/mapUtils.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 2 |
// Haversine-based geodesic interpolator
|
| 3 |
function generateGeodesicPoints(lat1, lon1, lat2, lon2, numPoints = 512) {
|
| 4 |
/**
|
|
@@ -47,61 +47,24 @@ function generateGeodesicPoints(lat1, lon1, lat2, lon2, numPoints = 512) {
|
|
| 47 |
|
| 48 |
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
const f = 1 / 298.257223563; // Flattening
|
| 63 |
-
const e2 = f * (2 - f); // First eccentricity squared
|
| 64 |
-
|
| 65 |
-
// Ensure polygon is closed
|
| 66 |
-
const coords = [...coordinates];
|
| 67 |
-
if (coords[0][0] !== coords[coords.length - 1][0] ||
|
| 68 |
-
coords[0][1] !== coords[coords.length - 1][1]) {
|
| 69 |
-
coords.push(coords[0]);
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
let area = 0;
|
| 73 |
-
const n = coords.length - 1;
|
| 74 |
-
|
| 75 |
-
// Calculate area using simplified geodesic excess method
|
| 76 |
-
for (let i = 0; i < n; i++) {
|
| 77 |
-
const [lat1, lon1] = coords[i];
|
| 78 |
-
const [lat2, lon2] = coords[i + 1];
|
| 79 |
-
|
| 80 |
-
// Convert to radians
|
| 81 |
-
const phi1 = lat1 * Math.PI / 180;
|
| 82 |
-
const phi2 = lat2 * Math.PI / 180;
|
| 83 |
-
let dL = (lon2 - lon1) * Math.PI / 180;
|
| 84 |
-
|
| 85 |
-
// Normalize longitude difference
|
| 86 |
-
while (dL > Math.PI) dL -= 2 * Math.PI;
|
| 87 |
-
while (dL < -Math.PI) dL += 2 * Math.PI;
|
| 88 |
-
|
| 89 |
-
// Geodesic excess contribution
|
| 90 |
-
const E = 2 * Math.atan2(
|
| 91 |
-
Math.tan(dL / 2) * (Math.sin(phi1) + Math.sin(phi2)),
|
| 92 |
-
2 + Math.sin(phi1) * Math.sin(phi2) + Math.cos(phi1) * Math.cos(phi2) * Math.cos(dL)
|
| 93 |
-
);
|
| 94 |
-
|
| 95 |
-
area += E;
|
| 96 |
-
}
|
| 97 |
-
|
| 98 |
-
// Convert to actual area using ellipsoid parameters
|
| 99 |
-
const ellipsoidArea = Math.abs(area) * (a * a / 2) * (1 - e2);
|
| 100 |
-
|
| 101 |
-
return ellipsoidArea; // Return area in square meters
|
| 102 |
-
|
| 103 |
-
}
|
| 104 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
function getPolygonCentroid(points) {
|
| 107 |
// Simple centroid calculation for small polygons
|
|
@@ -113,6 +76,7 @@ function getPolygonCentroid(points) {
|
|
| 113 |
function formatArea(area, unit = 'sqm', format = "normal") {
|
| 114 |
|
| 115 |
if (typeof area !== 'number' || isNaN(area)) {
|
|
|
|
| 116 |
return 'Invalid area';
|
| 117 |
}
|
| 118 |
let value;
|
|
|
|
| 1 |
+
import geodesic from 'geographiclib-geodesic';
|
| 2 |
// Haversine-based geodesic interpolator
|
| 3 |
function generateGeodesicPoints(lat1, lon1, lat2, lon2, numPoints = 512) {
|
| 4 |
/**
|
|
|
|
| 47 |
|
| 48 |
|
| 49 |
|
| 50 |
+
function calculatePolygonArea(coords) {
|
| 51 |
+
/*** Calculate the geodesic area of a polygon on the WGS84 ellipsoid.
|
| 52 |
+
* @param {Array<Array<number>>} coords - Array of [lat, lon] pairs.
|
| 53 |
+
* @returns {number} Area in square meters.
|
| 54 |
+
*/
|
| 55 |
+
// console.log(coords); // Lifesaver
|
| 56 |
+
const geod = geodesic.Geodesic.WGS84;
|
| 57 |
+
const poly = geod.Polygon(false); // false = polygon, not polyline
|
| 58 |
+
|
| 59 |
+
for (const [lat, lon] of coords) {
|
| 60 |
+
poly.AddPoint(lat, lon);
|
| 61 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
+
// Closing the polygon is not required as I am already doing it in the frontend.
|
| 64 |
+
|
| 65 |
+
const result = poly.Compute();
|
| 66 |
+
return result.area; // area in square meters, important.
|
| 67 |
+
}
|
| 68 |
|
| 69 |
function getPolygonCentroid(points) {
|
| 70 |
// Simple centroid calculation for small polygons
|
|
|
|
| 76 |
function formatArea(area, unit = 'sqm', format = "normal") {
|
| 77 |
|
| 78 |
if (typeof area !== 'number' || isNaN(area)) {
|
| 79 |
+
console.log('Invalid area input:', area);
|
| 80 |
return 'Invalid area';
|
| 81 |
}
|
| 82 |
let value;
|