Spaces:
Running
Running
add polyline
Browse files- client/package-lock.json +140 -1
- client/package.json +4 -1
- client/src/components/CesiumViewer.jsx +78 -46
- client/src/components/GeometryCreatorPanel.jsx +221 -0
- client/src/components/MeasurePanel.jsx +61 -64
- client/src/pages/Home.jsx +169 -36
client/package-lock.json
CHANGED
|
@@ -12,6 +12,9 @@
|
|
| 12 |
"@aws-sdk/credential-provider-cognito-identity": "^3.509.0",
|
| 13 |
"@aws-sdk/s3-request-presigner": "^3.509.0",
|
| 14 |
"@aws-sdk/util-endpoints": "^3.509.0",
|
|
|
|
|
|
|
|
|
|
| 15 |
"@emotion/react": "^11.14.0",
|
| 16 |
"@emotion/styled": "^11.14.1",
|
| 17 |
"@mui/icons-material": "^7.3.4",
|
|
@@ -19,7 +22,9 @@
|
|
| 19 |
"cesium": "^1.134.1",
|
| 20 |
"react": "^19.1.1",
|
| 21 |
"react-dom": "^19.1.1",
|
| 22 |
-
"
|
|
|
|
|
|
|
| 23 |
},
|
| 24 |
"devDependencies": {
|
| 25 |
"@eslint/js": "^9.36.0",
|
|
@@ -1437,6 +1442,59 @@
|
|
| 1437 |
"node": ">=20.19.0"
|
| 1438 |
}
|
| 1439 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1440 |
"node_modules/@emotion/babel-plugin": {
|
| 1441 |
"version": "11.13.5",
|
| 1442 |
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
|
@@ -2636,6 +2694,15 @@
|
|
| 2636 |
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
| 2637 |
"license": "BSD-3-Clause"
|
| 2638 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2639 |
"node_modules/@rolldown/pluginutils": {
|
| 2640 |
"version": "1.0.0-rc.3",
|
| 2641 |
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
|
@@ -4286,6 +4353,15 @@
|
|
| 4286 |
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
| 4287 |
"license": "MIT"
|
| 4288 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4289 |
"node_modules/debug": {
|
| 4290 |
"version": "4.4.3",
|
| 4291 |
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
|
@@ -5189,6 +5265,11 @@
|
|
| 5189 |
"dev": true,
|
| 5190 |
"license": "ISC"
|
| 5191 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5192 |
"node_modules/js-tokens": {
|
| 5193 |
"version": "4.0.0",
|
| 5194 |
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
|
@@ -5282,6 +5363,21 @@
|
|
| 5282 |
"graceful-fs": "^4.1.6"
|
| 5283 |
}
|
| 5284 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5285 |
"node_modules/kdbush": {
|
| 5286 |
"version": "4.0.2",
|
| 5287 |
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
|
|
@@ -5938,6 +6034,38 @@
|
|
| 5938 |
"node": ">=0.10.0"
|
| 5939 |
}
|
| 5940 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5941 |
"node_modules/react-transition-group": {
|
| 5942 |
"version": "4.4.5",
|
| 5943 |
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
|
@@ -6181,6 +6309,17 @@
|
|
| 6181 |
"node": ">=8"
|
| 6182 |
}
|
| 6183 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6184 |
"node_modules/slash": {
|
| 6185 |
"version": "3.0.0",
|
| 6186 |
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
|
|
|
| 12 |
"@aws-sdk/credential-provider-cognito-identity": "^3.509.0",
|
| 13 |
"@aws-sdk/s3-request-presigner": "^3.509.0",
|
| 14 |
"@aws-sdk/util-endpoints": "^3.509.0",
|
| 15 |
+
"@dnd-kit/core": "^6.1.0",
|
| 16 |
+
"@dnd-kit/sortable": "^8.0.0",
|
| 17 |
+
"@dnd-kit/utilities": "^3.2.2",
|
| 18 |
"@emotion/react": "^11.14.0",
|
| 19 |
"@emotion/styled": "^11.14.1",
|
| 20 |
"@mui/icons-material": "^7.3.4",
|
|
|
|
| 22 |
"cesium": "^1.134.1",
|
| 23 |
"react": "^19.1.1",
|
| 24 |
"react-dom": "^19.1.1",
|
| 25 |
+
"react-router-dom": "^6.25.1",
|
| 26 |
+
"resium": "^1.19.0-beta.1",
|
| 27 |
+
"shp-write": "^0.3.2"
|
| 28 |
},
|
| 29 |
"devDependencies": {
|
| 30 |
"@eslint/js": "^9.36.0",
|
|
|
|
| 1442 |
"node": ">=20.19.0"
|
| 1443 |
}
|
| 1444 |
},
|
| 1445 |
+
"node_modules/@dnd-kit/accessibility": {
|
| 1446 |
+
"version": "3.1.1",
|
| 1447 |
+
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
| 1448 |
+
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
| 1449 |
+
"license": "MIT",
|
| 1450 |
+
"dependencies": {
|
| 1451 |
+
"tslib": "^2.0.0"
|
| 1452 |
+
},
|
| 1453 |
+
"peerDependencies": {
|
| 1454 |
+
"react": ">=16.8.0"
|
| 1455 |
+
}
|
| 1456 |
+
},
|
| 1457 |
+
"node_modules/@dnd-kit/core": {
|
| 1458 |
+
"version": "6.3.1",
|
| 1459 |
+
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
| 1460 |
+
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
| 1461 |
+
"license": "MIT",
|
| 1462 |
+
"dependencies": {
|
| 1463 |
+
"@dnd-kit/accessibility": "^3.1.1",
|
| 1464 |
+
"@dnd-kit/utilities": "^3.2.2",
|
| 1465 |
+
"tslib": "^2.0.0"
|
| 1466 |
+
},
|
| 1467 |
+
"peerDependencies": {
|
| 1468 |
+
"react": ">=16.8.0",
|
| 1469 |
+
"react-dom": ">=16.8.0"
|
| 1470 |
+
}
|
| 1471 |
+
},
|
| 1472 |
+
"node_modules/@dnd-kit/sortable": {
|
| 1473 |
+
"version": "8.0.0",
|
| 1474 |
+
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-8.0.0.tgz",
|
| 1475 |
+
"integrity": "sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==",
|
| 1476 |
+
"license": "MIT",
|
| 1477 |
+
"dependencies": {
|
| 1478 |
+
"@dnd-kit/utilities": "^3.2.2",
|
| 1479 |
+
"tslib": "^2.0.0"
|
| 1480 |
+
},
|
| 1481 |
+
"peerDependencies": {
|
| 1482 |
+
"@dnd-kit/core": "^6.1.0",
|
| 1483 |
+
"react": ">=16.8.0"
|
| 1484 |
+
}
|
| 1485 |
+
},
|
| 1486 |
+
"node_modules/@dnd-kit/utilities": {
|
| 1487 |
+
"version": "3.2.2",
|
| 1488 |
+
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
| 1489 |
+
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
| 1490 |
+
"license": "MIT",
|
| 1491 |
+
"dependencies": {
|
| 1492 |
+
"tslib": "^2.0.0"
|
| 1493 |
+
},
|
| 1494 |
+
"peerDependencies": {
|
| 1495 |
+
"react": ">=16.8.0"
|
| 1496 |
+
}
|
| 1497 |
+
},
|
| 1498 |
"node_modules/@emotion/babel-plugin": {
|
| 1499 |
"version": "11.13.5",
|
| 1500 |
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
|
|
|
| 2694 |
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
| 2695 |
"license": "BSD-3-Clause"
|
| 2696 |
},
|
| 2697 |
+
"node_modules/@remix-run/router": {
|
| 2698 |
+
"version": "1.23.2",
|
| 2699 |
+
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
|
| 2700 |
+
"integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
|
| 2701 |
+
"license": "MIT",
|
| 2702 |
+
"engines": {
|
| 2703 |
+
"node": ">=14.0.0"
|
| 2704 |
+
}
|
| 2705 |
+
},
|
| 2706 |
"node_modules/@rolldown/pluginutils": {
|
| 2707 |
"version": "1.0.0-rc.3",
|
| 2708 |
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
|
|
|
| 4353 |
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
| 4354 |
"license": "MIT"
|
| 4355 |
},
|
| 4356 |
+
"node_modules/dbf": {
|
| 4357 |
+
"version": "0.1.4",
|
| 4358 |
+
"resolved": "https://registry.npmjs.org/dbf/-/dbf-0.1.4.tgz",
|
| 4359 |
+
"integrity": "sha512-7tQ8w5NB74PL1f0Z/NQ6Y+URjBFhtEsFxzEQSzot2+VpLwWfrNnxFVhzWm6dJyEtFq0WkYWcGEMDf39fy8JFaw==",
|
| 4360 |
+
"license": "BSD-2-Clause",
|
| 4361 |
+
"dependencies": {
|
| 4362 |
+
"jdataview": "~2.5.0"
|
| 4363 |
+
}
|
| 4364 |
+
},
|
| 4365 |
"node_modules/debug": {
|
| 4366 |
"version": "4.4.3",
|
| 4367 |
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
|
|
|
| 5265 |
"dev": true,
|
| 5266 |
"license": "ISC"
|
| 5267 |
},
|
| 5268 |
+
"node_modules/jdataview": {
|
| 5269 |
+
"version": "2.5.0",
|
| 5270 |
+
"resolved": "https://registry.npmjs.org/jdataview/-/jdataview-2.5.0.tgz",
|
| 5271 |
+
"integrity": "sha512-ZJop3D5nyDcWPBPv4NPnhCvx3HgQNsCXMfw8gpNKY16BobgxmVF+kJ08aHuqk6bJQVeL2mkf6nDCcZPMompalw=="
|
| 5272 |
+
},
|
| 5273 |
"node_modules/js-tokens": {
|
| 5274 |
"version": "4.0.0",
|
| 5275 |
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
|
|
|
| 5363 |
"graceful-fs": "^4.1.6"
|
| 5364 |
}
|
| 5365 |
},
|
| 5366 |
+
"node_modules/jszip": {
|
| 5367 |
+
"version": "2.5.0",
|
| 5368 |
+
"resolved": "https://registry.npmjs.org/jszip/-/jszip-2.5.0.tgz",
|
| 5369 |
+
"integrity": "sha512-IRoyf8JSYY3nx+uyh5xPc0qdy8pUDTp2UkHOWYNF/IO/3D8nx7899UlSAjD8rf8wUgOmm0lACWx/GbW3EaxIXQ==",
|
| 5370 |
+
"license": "MIT or GPLv3",
|
| 5371 |
+
"dependencies": {
|
| 5372 |
+
"pako": "~0.2.5"
|
| 5373 |
+
}
|
| 5374 |
+
},
|
| 5375 |
+
"node_modules/jszip/node_modules/pako": {
|
| 5376 |
+
"version": "0.2.9",
|
| 5377 |
+
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
| 5378 |
+
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
|
| 5379 |
+
"license": "MIT"
|
| 5380 |
+
},
|
| 5381 |
"node_modules/kdbush": {
|
| 5382 |
"version": "4.0.2",
|
| 5383 |
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
|
|
|
|
| 6034 |
"node": ">=0.10.0"
|
| 6035 |
}
|
| 6036 |
},
|
| 6037 |
+
"node_modules/react-router": {
|
| 6038 |
+
"version": "6.30.3",
|
| 6039 |
+
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
|
| 6040 |
+
"integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
|
| 6041 |
+
"license": "MIT",
|
| 6042 |
+
"dependencies": {
|
| 6043 |
+
"@remix-run/router": "1.23.2"
|
| 6044 |
+
},
|
| 6045 |
+
"engines": {
|
| 6046 |
+
"node": ">=14.0.0"
|
| 6047 |
+
},
|
| 6048 |
+
"peerDependencies": {
|
| 6049 |
+
"react": ">=16.8"
|
| 6050 |
+
}
|
| 6051 |
+
},
|
| 6052 |
+
"node_modules/react-router-dom": {
|
| 6053 |
+
"version": "6.30.3",
|
| 6054 |
+
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
|
| 6055 |
+
"integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
|
| 6056 |
+
"license": "MIT",
|
| 6057 |
+
"dependencies": {
|
| 6058 |
+
"@remix-run/router": "1.23.2",
|
| 6059 |
+
"react-router": "6.30.3"
|
| 6060 |
+
},
|
| 6061 |
+
"engines": {
|
| 6062 |
+
"node": ">=14.0.0"
|
| 6063 |
+
},
|
| 6064 |
+
"peerDependencies": {
|
| 6065 |
+
"react": ">=16.8",
|
| 6066 |
+
"react-dom": ">=16.8"
|
| 6067 |
+
}
|
| 6068 |
+
},
|
| 6069 |
"node_modules/react-transition-group": {
|
| 6070 |
"version": "4.4.5",
|
| 6071 |
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
|
|
|
| 6309 |
"node": ">=8"
|
| 6310 |
}
|
| 6311 |
},
|
| 6312 |
+
"node_modules/shp-write": {
|
| 6313 |
+
"version": "0.3.2",
|
| 6314 |
+
"resolved": "https://registry.npmjs.org/shp-write/-/shp-write-0.3.2.tgz",
|
| 6315 |
+
"integrity": "sha512-RNmfm+qzIwgwGMiV21lCxfEAtgP/owAd+sHLr6Qu+aDR1bbrCZ42H89nA9FQWUqfL+WHJy3n8+cTZxJrL/ZKWA==",
|
| 6316 |
+
"deprecated": "This package has moved under the @mapbox organization, and can be found here: https://www.npmjs.com/package/@mapbox/shp-write",
|
| 6317 |
+
"license": "BSD-2-Clause",
|
| 6318 |
+
"dependencies": {
|
| 6319 |
+
"dbf": "0.1.4",
|
| 6320 |
+
"jszip": "2.5.0"
|
| 6321 |
+
}
|
| 6322 |
+
},
|
| 6323 |
"node_modules/slash": {
|
| 6324 |
"version": "3.0.0",
|
| 6325 |
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
client/package.json
CHANGED
|
@@ -22,7 +22,10 @@
|
|
| 22 |
"react": "^19.1.1",
|
| 23 |
"react-dom": "^19.1.1",
|
| 24 |
"resium": "^1.19.0-beta.1",
|
| 25 |
-
"react-router-dom": "^6.25.1"
|
|
|
|
|
|
|
|
|
|
| 26 |
},
|
| 27 |
"devDependencies": {
|
| 28 |
"@eslint/js": "^9.36.0",
|
|
|
|
| 22 |
"react": "^19.1.1",
|
| 23 |
"react-dom": "^19.1.1",
|
| 24 |
"resium": "^1.19.0-beta.1",
|
| 25 |
+
"react-router-dom": "^6.25.1",
|
| 26 |
+
"@dnd-kit/core": "^6.1.0",
|
| 27 |
+
"@dnd-kit/sortable": "^8.0.0",
|
| 28 |
+
"@dnd-kit/utilities": "^3.2.2"
|
| 29 |
},
|
| 30 |
"devDependencies": {
|
| 31 |
"@eslint/js": "^9.36.0",
|
client/src/components/CesiumViewer.jsx
CHANGED
|
@@ -6,9 +6,8 @@ import React, {
|
|
| 6 |
useImperativeHandle,
|
| 7 |
useMemo,
|
| 8 |
} from "react";
|
| 9 |
-
import { Viewer } from "resium";
|
| 10 |
import * as Cesium from "cesium";
|
| 11 |
-
import { Entity } from "resium";
|
| 12 |
import { Cartesian3, Color, Matrix3, Quaternion, PolygonHierarchy } from "cesium";
|
| 13 |
|
| 14 |
// Cesium Ion token is safe to embed in client code (not a secret)
|
|
@@ -205,27 +204,41 @@ const CesiumViewer = forwardRef(
|
|
| 205 |
polygons,
|
| 206 |
onPointSelectForDrawing,
|
| 207 |
selectedGeometry,
|
|
|
|
| 208 |
}, ref) => {
|
| 209 |
const viewerRef = useRef(null);
|
| 210 |
const currentModeRef = useRef("default"); // keep internal mode
|
| 211 |
const [measurements, setMeasurements] = useState([]);
|
|
|
|
| 212 |
const tilesetRef = useRef(null);
|
| 213 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
const addMeausure = (cameraPosition, cartesian) => {
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
return [...prev, newMeasurement];
|
| 228 |
-
});
|
| 229 |
};
|
| 230 |
// Expose a function to parent
|
| 231 |
useImperativeHandle(ref, () => ({
|
|
@@ -267,6 +280,7 @@ const CesiumViewer = forwardRef(
|
|
| 267 |
}
|
| 268 |
currentModeRef.current = "default";
|
| 269 |
setMeasurements([]);
|
|
|
|
| 270 |
},
|
| 271 |
}));
|
| 272 |
|
|
@@ -459,24 +473,32 @@ const CesiumViewer = forwardRef(
|
|
| 459 |
// Set up mouse event handler
|
| 460 |
handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
|
| 461 |
|
|
|
|
|
|
|
| 462 |
handler.setInputAction((click) => {
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
}
|
|
|
|
|
|
|
| 480 |
return;
|
| 481 |
}
|
| 482 |
|
|
@@ -519,14 +541,14 @@ const CesiumViewer = forwardRef(
|
|
| 519 |
tilesetRef.current = null;
|
| 520 |
}
|
| 521 |
};
|
| 522 |
-
}, [tilesetUrl, offsetHeight
|
| 523 |
|
| 524 |
return (
|
| 525 |
<Viewer ref={viewerRef} full infoBox={false} selectionIndicator={false}>
|
| 526 |
{points.map((pt) => {
|
| 527 |
const isSelectedForInspection = selectedPoint === pt.id;
|
| 528 |
-
const
|
| 529 |
-
const isPreviewed =
|
| 530 |
|
| 531 |
let ellipsoidColor = Color.BLUE.withAlpha(0.5);
|
| 532 |
let pointColor = Color.WHITE;
|
|
@@ -538,7 +560,7 @@ const CesiumViewer = forwardRef(
|
|
| 538 |
pointColor = Color.CYAN;
|
| 539 |
pixelSize = 16;
|
| 540 |
scale = 0.3;
|
| 541 |
-
} else if (
|
| 542 |
ellipsoidColor = Color.ORANGE.withAlpha(0.7);
|
| 543 |
pointColor = Color.ORANGE;
|
| 544 |
pixelSize = 12;
|
|
@@ -564,7 +586,7 @@ const CesiumViewer = forwardRef(
|
|
| 564 |
outlineColor: Color.BLACK,
|
| 565 |
outlineWidth: 2,
|
| 566 |
// Disable depth test when highlighted to ensure visibility
|
| 567 |
-
disableDepthTestDistance:
|
| 568 |
}}
|
| 569 |
/>
|
| 570 |
);
|
|
@@ -581,12 +603,12 @@ const CesiumViewer = forwardRef(
|
|
| 581 |
const isSelected = selectedGeometry?.type === 'polyline' && selectedGeometry?.id === line.id;
|
| 582 |
return (
|
| 583 |
<Entity key={line.id} id={line.id}>
|
| 584 |
-
<
|
| 585 |
positions={line.points.map(p => new Cartesian3(p.x, p.y, p.z))}
|
| 586 |
width={isSelected ? 5 : 3}
|
| 587 |
material={isSelected ? Color.CYAN : Color.YELLOW}
|
| 588 |
clampToGround={false}
|
| 589 |
-
|
| 590 |
/>
|
| 591 |
</Entity>
|
| 592 |
);
|
|
@@ -595,14 +617,20 @@ const CesiumViewer = forwardRef(
|
|
| 595 |
{/* Render completed polygons */}
|
| 596 |
{polygons.map(poly => {
|
| 597 |
const isSelected = selectedGeometry?.type === 'polygon' && selectedGeometry?.id === poly.id;
|
|
|
|
| 598 |
return (
|
| 599 |
<Entity key={poly.id} id={poly.id}>
|
| 600 |
-
|
|
|
|
| 601 |
hierarchy={new PolygonHierarchy(poly.points.map(p => new Cartesian3(p.x, p.y, p.z)))}
|
| 602 |
material={isSelected ? Color.CYAN.withAlpha(0.5) : Color.ORANGE.withAlpha(0.5)}
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 606 |
/>
|
| 607 |
</Entity>
|
| 608 |
);
|
|
@@ -611,22 +639,26 @@ const CesiumViewer = forwardRef(
|
|
| 611 |
{/* Render the geometry being currently drawn */}
|
| 612 |
{drawingState.mode === 'polyline' && drawingState.points.length > 0 && (
|
| 613 |
<Entity>
|
| 614 |
-
<
|
| 615 |
positions={drawingState.points.map(p => new Cartesian3(p.x, p.y, p.z))}
|
| 616 |
width={3}
|
| 617 |
material={Color.RED.withAlpha(0.7)}
|
|
|
|
| 618 |
/>
|
| 619 |
</Entity>
|
| 620 |
)}
|
| 621 |
{drawingState.mode === 'polygon' && drawingState.points.length > 0 && (
|
| 622 |
<Entity>
|
| 623 |
-
<
|
| 624 |
positions={[...drawingState.points, drawingState.points[0]].map(p => new Cartesian3(p.x, p.y, p.z))}
|
| 625 |
width={3}
|
| 626 |
material={Color.RED.withAlpha(0.7)}
|
|
|
|
| 627 |
/>
|
| 628 |
{drawingState.points.length > 2 && (
|
| 629 |
-
<
|
|
|
|
|
|
|
| 630 |
)}
|
| 631 |
</Entity>
|
| 632 |
)}
|
|
|
|
| 6 |
useImperativeHandle,
|
| 7 |
useMemo,
|
| 8 |
} from "react";
|
| 9 |
+
import { Viewer, Entity, PolylineGraphics, PolygonGraphics } from "resium";
|
| 10 |
import * as Cesium from "cesium";
|
|
|
|
| 11 |
import { Cartesian3, Color, Matrix3, Quaternion, PolygonHierarchy } from "cesium";
|
| 12 |
|
| 13 |
// Cesium Ion token is safe to embed in client code (not a secret)
|
|
|
|
| 204 |
polygons,
|
| 205 |
onPointSelectForDrawing,
|
| 206 |
selectedGeometry,
|
| 207 |
+
previewPointId,
|
| 208 |
}, ref) => {
|
| 209 |
const viewerRef = useRef(null);
|
| 210 |
const currentModeRef = useRef("default"); // keep internal mode
|
| 211 |
const [measurements, setMeasurements] = useState([]);
|
| 212 |
+
const measurementIdCounter = useRef(0);
|
| 213 |
const tilesetRef = useRef(null);
|
| 214 |
|
| 215 |
+
// Refs to hold the latest props that change frequently.
|
| 216 |
+
// This prevents the main `useEffect` from re-running unnecessarily, which would
|
| 217 |
+
// cause the entire 3D scene to reload when points are added or drawing mode changes.
|
| 218 |
+
const pointsRef = useRef(points);
|
| 219 |
+
useEffect(() => { pointsRef.current = points; }, [points]);
|
| 220 |
+
|
| 221 |
+
const drawingStateRef = useRef(drawingState);
|
| 222 |
+
useEffect(() => { drawingStateRef.current = drawingState; }, [drawingState]);
|
| 223 |
+
|
| 224 |
+
// Use a ref to hold the latest onPointSelectForDrawing callback to avoid re-running the main effect.
|
| 225 |
+
const onPointSelectForDrawingRef = useRef(onPointSelectForDrawing);
|
| 226 |
+
useEffect(() => { onPointSelectForDrawingRef.current = onPointSelectForDrawing; }, [onPointSelectForDrawing]);
|
| 227 |
+
|
| 228 |
+
|
| 229 |
const addMeausure = (cameraPosition, cartesian) => {
|
| 230 |
+
measurementIdCounter.current += 1;
|
| 231 |
+
const newMeasurement = {
|
| 232 |
+
id: measurementIdCounter.current,
|
| 233 |
+
camera: {
|
| 234 |
+
x: cameraPosition.x,
|
| 235 |
+
y: cameraPosition.y,
|
| 236 |
+
z: cameraPosition.z,
|
| 237 |
+
},
|
| 238 |
+
point: { x: cartesian.x, y: cartesian.y, z: cartesian.z },
|
| 239 |
+
};
|
| 240 |
+
console.log("Adding measure:", newMeasurement);
|
| 241 |
+
setMeasurements((prev) => [...prev, newMeasurement]);
|
|
|
|
|
|
|
| 242 |
};
|
| 243 |
// Expose a function to parent
|
| 244 |
useImperativeHandle(ref, () => ({
|
|
|
|
| 280 |
}
|
| 281 |
currentModeRef.current = "default";
|
| 282 |
setMeasurements([]);
|
| 283 |
+
measurementIdCounter.current = 0;
|
| 284 |
},
|
| 285 |
}));
|
| 286 |
|
|
|
|
| 473 |
// Set up mouse event handler
|
| 474 |
handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas);
|
| 475 |
|
| 476 |
+
// Prevent the default double-click behavior which zooms into the point
|
| 477 |
+
// and can cause a "matrix is not invertible" crash.
|
| 478 |
handler.setInputAction((click) => {
|
| 479 |
+
// Explicitly set the tracked entity to undefined to fully disable
|
| 480 |
+
// the default camera flight behavior on double-click, which can
|
| 481 |
+
// cause the rendering to crash.
|
| 482 |
+
viewer.trackedEntity = undefined;
|
| 483 |
+
}, Cesium.ScreenSpaceEventType.LEFT_DOUBLE_CLICK);
|
| 484 |
+
|
| 485 |
+
handler.setInputAction((click) => {
|
| 486 |
+
const { mode: drawingMode } = drawingStateRef.current;
|
| 487 |
+
|
| 488 |
+
if (drawingMode === 'polyline' || drawingMode === 'polygon') {
|
| 489 |
+
const viewer = viewerRef.current.cesiumElement;
|
| 490 |
+
|
| 491 |
+
// 1. Try to pick an existing point entity
|
| 492 |
+
const pickedObject = viewer.scene.pick(click.position);
|
| 493 |
+
if (Cesium.defined(pickedObject) && Cesium.defined(pickedObject.id) && typeof pickedObject.id.id === 'string' && pickedObject.id.id.startsWith('point_')) {
|
| 494 |
+
const pointId = parseInt(pickedObject.id.id.split('_')[1], 10);
|
| 495 |
+
const point = pointsRef.current.find(p => p.id === pointId);
|
| 496 |
+
if (point) {
|
| 497 |
+
onPointSelectForDrawingRef.current(point);
|
| 498 |
+
}
|
| 499 |
}
|
| 500 |
+
|
| 501 |
+
// After attempting to pick a point, stop further click processing for this mode.
|
| 502 |
return;
|
| 503 |
}
|
| 504 |
|
|
|
|
| 541 |
tilesetRef.current = null;
|
| 542 |
}
|
| 543 |
};
|
| 544 |
+
}, [tilesetUrl, offsetHeight]);
|
| 545 |
|
| 546 |
return (
|
| 547 |
<Viewer ref={viewerRef} full infoBox={false} selectionIndicator={false}>
|
| 548 |
{points.map((pt) => {
|
| 549 |
const isSelectedForInspection = selectedPoint === pt.id;
|
| 550 |
+
const isSelectedForGeometry = drawingState.points.some(p => p.id === pt.id);
|
| 551 |
+
const isPreviewed = previewPointId === pt.id;
|
| 552 |
|
| 553 |
let ellipsoidColor = Color.BLUE.withAlpha(0.5);
|
| 554 |
let pointColor = Color.WHITE;
|
|
|
|
| 560 |
pointColor = Color.CYAN;
|
| 561 |
pixelSize = 16;
|
| 562 |
scale = 0.3;
|
| 563 |
+
} else if (isSelectedForGeometry) {
|
| 564 |
ellipsoidColor = Color.ORANGE.withAlpha(0.7);
|
| 565 |
pointColor = Color.ORANGE;
|
| 566 |
pixelSize = 12;
|
|
|
|
| 586 |
outlineColor: Color.BLACK,
|
| 587 |
outlineWidth: 2,
|
| 588 |
// Disable depth test when highlighted to ensure visibility
|
| 589 |
+
disableDepthTestDistance: isSelectedForGeometry || isPreviewed || isSelectedForInspection ? Number.POSITIVE_INFINITY : 0,
|
| 590 |
}}
|
| 591 |
/>
|
| 592 |
);
|
|
|
|
| 603 |
const isSelected = selectedGeometry?.type === 'polyline' && selectedGeometry?.id === line.id;
|
| 604 |
return (
|
| 605 |
<Entity key={line.id} id={line.id}>
|
| 606 |
+
<PolylineGraphics
|
| 607 |
positions={line.points.map(p => new Cartesian3(p.x, p.y, p.z))}
|
| 608 |
width={isSelected ? 5 : 3}
|
| 609 |
material={isSelected ? Color.CYAN : Color.YELLOW}
|
| 610 |
clampToGround={false}
|
| 611 |
+
disableDepthTestDistance={Number.POSITIVE_INFINITY}
|
| 612 |
/>
|
| 613 |
</Entity>
|
| 614 |
);
|
|
|
|
| 617 |
{/* Render completed polygons */}
|
| 618 |
{polygons.map(poly => {
|
| 619 |
const isSelected = selectedGeometry?.type === 'polygon' && selectedGeometry?.id === poly.id;
|
| 620 |
+
const polylinePositions = [...poly.points.map(p => new Cartesian3(p.x, p.y, p.z)), new Cartesian3(poly.points[0].x, poly.points[0].y, poly.points[0].z)];
|
| 621 |
return (
|
| 622 |
<Entity key={poly.id} id={poly.id}>
|
| 623 |
+
{/* The fill, which can be occluded to prevent "floating" */}
|
| 624 |
+
<PolygonGraphics
|
| 625 |
hierarchy={new PolygonHierarchy(poly.points.map(p => new Cartesian3(p.x, p.y, p.z)))}
|
| 626 |
material={isSelected ? Color.CYAN.withAlpha(0.5) : Color.ORANGE.withAlpha(0.5)}
|
| 627 |
+
/>
|
| 628 |
+
{/* The outline, which is always on top for visibility */}
|
| 629 |
+
<PolylineGraphics
|
| 630 |
+
positions={polylinePositions}
|
| 631 |
+
width={isSelected ? 3 : 1.5}
|
| 632 |
+
material={isSelected ? Color.CYAN : Color.BLACK}
|
| 633 |
+
disableDepthTestDistance={Number.POSITIVE_INFINITY}
|
| 634 |
/>
|
| 635 |
</Entity>
|
| 636 |
);
|
|
|
|
| 639 |
{/* Render the geometry being currently drawn */}
|
| 640 |
{drawingState.mode === 'polyline' && drawingState.points.length > 0 && (
|
| 641 |
<Entity>
|
| 642 |
+
<PolylineGraphics
|
| 643 |
positions={drawingState.points.map(p => new Cartesian3(p.x, p.y, p.z))}
|
| 644 |
width={3}
|
| 645 |
material={Color.RED.withAlpha(0.7)}
|
| 646 |
+
disableDepthTestDistance={Number.POSITIVE_INFINITY}
|
| 647 |
/>
|
| 648 |
</Entity>
|
| 649 |
)}
|
| 650 |
{drawingState.mode === 'polygon' && drawingState.points.length > 0 && (
|
| 651 |
<Entity>
|
| 652 |
+
<PolylineGraphics
|
| 653 |
positions={[...drawingState.points, drawingState.points[0]].map(p => new Cartesian3(p.x, p.y, p.z))}
|
| 654 |
width={3}
|
| 655 |
material={Color.RED.withAlpha(0.7)}
|
| 656 |
+
disableDepthTestDistance={Number.POSITIVE_INFINITY}
|
| 657 |
/>
|
| 658 |
{drawingState.points.length > 2 && (
|
| 659 |
+
<PolygonGraphics
|
| 660 |
+
hierarchy={new PolygonHierarchy(drawingState.points.map(p => new Cartesian3(p.x, p.y, p.z)))}
|
| 661 |
+
material={Color.RED.withAlpha(0.3)} />
|
| 662 |
)}
|
| 663 |
</Entity>
|
| 664 |
)}
|
client/src/components/GeometryCreatorPanel.jsx
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useMemo } from 'react';
|
| 2 |
+
import {
|
| 3 |
+
Card, CardContent, CardActions, Button, Typography, List, ListItem,
|
| 4 |
+
ListItemText, ListItemSecondaryAction, IconButton, Divider, Box, ListItemButton,
|
| 5 |
+
ListItemIcon
|
| 6 |
+
} from '@mui/material';
|
| 7 |
+
import DeleteIcon from '@mui/icons-material/Delete';
|
| 8 |
+
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
|
| 9 |
+
import {
|
| 10 |
+
DndContext,
|
| 11 |
+
closestCenter,
|
| 12 |
+
KeyboardSensor,
|
| 13 |
+
PointerSensor,
|
| 14 |
+
useSensor,
|
| 15 |
+
useSensors,
|
| 16 |
+
} from '@dnd-kit/core';
|
| 17 |
+
import {
|
| 18 |
+
SortableContext,
|
| 19 |
+
sortableKeyboardCoordinates,
|
| 20 |
+
verticalListSortingStrategy,
|
| 21 |
+
useSortable,
|
| 22 |
+
} from '@dnd-kit/sortable';
|
| 23 |
+
import { CSS } from '@dnd-kit/utilities';
|
| 24 |
+
|
| 25 |
+
// A new component for the sortable list item, using dnd-kit hooks
|
| 26 |
+
function SortablePointItem({ point, index, onPreviewPoint, onPointSelect }) {
|
| 27 |
+
const {
|
| 28 |
+
attributes,
|
| 29 |
+
listeners,
|
| 30 |
+
setNodeRef,
|
| 31 |
+
transform,
|
| 32 |
+
transition,
|
| 33 |
+
isDragging,
|
| 34 |
+
} = useSortable({ id: String(point.id) });
|
| 35 |
+
|
| 36 |
+
const style = {
|
| 37 |
+
transform: CSS.Transform.toString(transform),
|
| 38 |
+
transition,
|
| 39 |
+
zIndex: isDragging ? 100 : 'auto', // Ensure dragging item is on top
|
| 40 |
+
};
|
| 41 |
+
|
| 42 |
+
return (
|
| 43 |
+
<ListItem
|
| 44 |
+
ref={setNodeRef}
|
| 45 |
+
style={style}
|
| 46 |
+
{...attributes}
|
| 47 |
+
{...listeners}
|
| 48 |
+
onMouseEnter={() => onPreviewPoint(point.id || 0)}
|
| 49 |
+
onMouseLeave={() => onPreviewPoint(0)}
|
| 50 |
+
sx={{
|
| 51 |
+
cursor: 'grab',
|
| 52 |
+
touchAction: 'none',
|
| 53 |
+
bgcolor: isDragging ? 'action.hover' : 'transparent',
|
| 54 |
+
}}
|
| 55 |
+
>
|
| 56 |
+
<ListItemIcon sx={{ minWidth: 'auto', pr: 1 }}>
|
| 57 |
+
<DragIndicatorIcon fontSize="small" />
|
| 58 |
+
</ListItemIcon>
|
| 59 |
+
<ListItemText primary={`Point ${point.id}`} secondary={`(${point.lon.toFixed(4)}, ${point.lat.toFixed(4)}, ${point.alt.toFixed(2)})`} />
|
| 60 |
+
<ListItemSecondaryAction><IconButton edge="end" aria-label="delete" onClick={() => onPointSelect(point)}><DeleteIcon /></IconButton></ListItemSecondaryAction>
|
| 61 |
+
</ListItem>
|
| 62 |
+
);
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
export default function GeometryCreatorPanel({
|
| 66 |
+
isOpen,
|
| 67 |
+
drawingState,
|
| 68 |
+
onCancel,
|
| 69 |
+
onFinish,
|
| 70 |
+
onPointSelect, // To remove a point from the list
|
| 71 |
+
onPreviewPoint,
|
| 72 |
+
// For the available points list
|
| 73 |
+
availablePoints,
|
| 74 |
+
multiSelectPoints,
|
| 75 |
+
onAvailablePointClick,
|
| 76 |
+
onAddMultiSelectedPoints,
|
| 77 |
+
onClearPoints,
|
| 78 |
+
onReorderPoints,
|
| 79 |
+
}) {
|
| 80 |
+
if (!isOpen) {
|
| 81 |
+
return null;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
const { mode, points: selectedPoints } = drawingState;
|
| 85 |
+
const title = mode === 'polyline' ? 'Create Polyline' : 'Create Polygon';
|
| 86 |
+
const finishText = mode === 'polyline' ? 'Finish Polyline' : 'Finish Polygon';
|
| 87 |
+
const minPoints = mode === 'polyline' ? 2 : 3;
|
| 88 |
+
|
| 89 |
+
const availablePointsToShow = useMemo(() => {
|
| 90 |
+
const selectedIds = new Set(selectedPoints.map(p => p.id));
|
| 91 |
+
return availablePoints.filter(p => !selectedIds.has(p.id));
|
| 92 |
+
}, [availablePoints, selectedPoints]);
|
| 93 |
+
|
| 94 |
+
const sensors = useSensors(
|
| 95 |
+
useSensor(PointerSensor, {
|
| 96 |
+
// Require the mouse to move by a few pixels before activating
|
| 97 |
+
activationConstraint: {
|
| 98 |
+
distance: 5,
|
| 99 |
+
},
|
| 100 |
+
}),
|
| 101 |
+
useSensor(KeyboardSensor, {
|
| 102 |
+
coordinateGetter: sortableKeyboardCoordinates,
|
| 103 |
+
})
|
| 104 |
+
);
|
| 105 |
+
|
| 106 |
+
function handleDragEnd(event) {
|
| 107 |
+
const { active, over } = event;
|
| 108 |
+
if (over && active.id !== over.id) {
|
| 109 |
+
const oldIndex = selectedPoints.findIndex(p => String(p.id) === active.id);
|
| 110 |
+
const newIndex = selectedPoints.findIndex(p => String(p.id) === over.id);
|
| 111 |
+
if (oldIndex !== -1 && newIndex !== -1) {
|
| 112 |
+
onReorderPoints(oldIndex, newIndex);
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
return (
|
| 118 |
+
<Card
|
| 119 |
+
sx={{
|
| 120 |
+
position: 'absolute',
|
| 121 |
+
top: 80,
|
| 122 |
+
right: 20,
|
| 123 |
+
width: 400,
|
| 124 |
+
zIndex: 1000,
|
| 125 |
+
boxShadow: 5,
|
| 126 |
+
borderRadius: 2,
|
| 127 |
+
}}
|
| 128 |
+
>
|
| 129 |
+
<CardContent>
|
| 130 |
+
<Typography variant="h6" gutterBottom>{title}</Typography>
|
| 131 |
+
<Typography variant="body2" color="textSecondary" gutterBottom>
|
| 132 |
+
Select existing measured points from the 3D view or the list below.
|
| 133 |
+
</Typography>
|
| 134 |
+
|
| 135 |
+
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 2 }}>
|
| 136 |
+
<Typography variant="h6" gutterBottom component="div" sx={{ mb: 0 }}>
|
| 137 |
+
Selected Points ({selectedPoints.length})
|
| 138 |
+
</Typography>
|
| 139 |
+
{selectedPoints.length > 0 && (
|
| 140 |
+
<Button
|
| 141 |
+
onClick={onClearPoints}
|
| 142 |
+
size="small"
|
| 143 |
+
color="error"
|
| 144 |
+
>
|
| 145 |
+
Clear All
|
| 146 |
+
</Button>
|
| 147 |
+
)}
|
| 148 |
+
</Box>
|
| 149 |
+
|
| 150 |
+
{selectedPoints.length === 0 ? (
|
| 151 |
+
<Typography variant="body2" color="textSecondary" sx={{ mt: 1 }}>No points selected yet.</Typography>
|
| 152 |
+
) : (
|
| 153 |
+
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
| 154 |
+
<SortableContext items={selectedPoints.map(p => String(p.id))} strategy={verticalListSortingStrategy}>
|
| 155 |
+
<List dense sx={{ maxHeight: 150, overflow: "auto" }}>
|
| 156 |
+
{selectedPoints.map((point, index) => (
|
| 157 |
+
<SortablePointItem
|
| 158 |
+
key={point.id}
|
| 159 |
+
point={point}
|
| 160 |
+
index={index}
|
| 161 |
+
onPreviewPoint={onPreviewPoint}
|
| 162 |
+
onPointSelect={onPointSelect}
|
| 163 |
+
/>
|
| 164 |
+
))}
|
| 165 |
+
</List>
|
| 166 |
+
</SortableContext>
|
| 167 |
+
</DndContext>
|
| 168 |
+
)}
|
| 169 |
+
|
| 170 |
+
<Divider style={{ margin: '16px 0' }} />
|
| 171 |
+
|
| 172 |
+
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
| 173 |
+
<Typography variant="h6" gutterBottom component="div">
|
| 174 |
+
Available Measured Points
|
| 175 |
+
</Typography>
|
| 176 |
+
<Button
|
| 177 |
+
onClick={onAddMultiSelectedPoints}
|
| 178 |
+
disabled={multiSelectPoints.length === 0}
|
| 179 |
+
size="small"
|
| 180 |
+
variant="outlined"
|
| 181 |
+
>
|
| 182 |
+
Add Selected ({multiSelectPoints.length})
|
| 183 |
+
</Button>
|
| 184 |
+
</Box>
|
| 185 |
+
<List dense style={{ maxHeight: 200, overflow: 'auto' }}>
|
| 186 |
+
{availablePointsToShow.map((point) => (
|
| 187 |
+
<ListItemButton
|
| 188 |
+
key={point.id}
|
| 189 |
+
onClick={(event) => onAvailablePointClick(event, point.id)}
|
| 190 |
+
selected={multiSelectPoints.includes(point.id)}
|
| 191 |
+
onMouseEnter={() => onPreviewPoint(point.id)}
|
| 192 |
+
onMouseLeave={() => onPreviewPoint(0)}
|
| 193 |
+
sx={{
|
| 194 |
+
borderRadius: 1,
|
| 195 |
+
"&.Mui-selected": {
|
| 196 |
+
bgcolor: "primary.light",
|
| 197 |
+
color: "primary.contrastText",
|
| 198 |
+
"&:hover": { bgcolor: "primary.main" },
|
| 199 |
+
},
|
| 200 |
+
}}
|
| 201 |
+
>
|
| 202 |
+
<ListItemText primary={`Point ${point.id}`} />
|
| 203 |
+
</ListItemButton>
|
| 204 |
+
))}
|
| 205 |
+
{availablePointsToShow.length === 0 && (
|
| 206 |
+
<ListItem>
|
| 207 |
+
<ListItemText primary="No other measured points available." />
|
| 208 |
+
</ListItem>
|
| 209 |
+
)}
|
| 210 |
+
</List>
|
| 211 |
+
|
| 212 |
+
</CardContent>
|
| 213 |
+
<CardActions sx={{ justifyContent: 'flex-end', p: 2 }}>
|
| 214 |
+
<Button onClick={onCancel}>Cancel</Button>
|
| 215 |
+
<Button variant="contained" onClick={onFinish} disabled={selectedPoints.length < minPoints} >
|
| 216 |
+
{finishText}
|
| 217 |
+
</Button>
|
| 218 |
+
</CardActions>
|
| 219 |
+
</Card>
|
| 220 |
+
);
|
| 221 |
+
}
|
client/src/components/MeasurePanel.jsx
CHANGED
|
@@ -2,7 +2,8 @@ import React, { useEffect, useState } from "react";
|
|
| 2 |
import {
|
| 3 |
Card, CardContent, Typography, Button, List, ListItemButton,
|
| 4 |
ListItemText, Divider, Stack, FormControl, InputLabel, Select,
|
| 5 |
-
MenuItem, TextField, Dialog, DialogTitle, DialogContent,
|
|
|
|
| 6 |
DialogActions, Tabs, Tab, Box
|
| 7 |
} from "@mui/material";
|
| 8 |
import DownloadIcon from "@mui/icons-material/Download";
|
|
@@ -46,6 +47,43 @@ function a11yProps(index) {
|
|
| 46 |
};
|
| 47 |
}
|
| 48 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
export default function MeasurePanel({
|
| 50 |
points,
|
| 51 |
onSelectPoint,
|
|
@@ -109,20 +147,6 @@ export default function MeasurePanel({
|
|
| 109 |
setSelectedIndex(nextIndex);
|
| 110 |
if (onSelectPoint) onSelectPoint(points[index]);
|
| 111 |
};
|
| 112 |
-
const handleDeletePoint = () => {
|
| 113 |
-
if (onDeletePoint) onDeletePoint();
|
| 114 |
-
};
|
| 115 |
-
const handleExport = () => {
|
| 116 |
-
const dataStr =
|
| 117 |
-
"data:text/json;charset=utf-8," +
|
| 118 |
-
encodeURIComponent(JSON.stringify(points, null, 2));
|
| 119 |
-
const downloadAnchorNode = document.createElement("a");
|
| 120 |
-
downloadAnchorNode.setAttribute("href", dataStr);
|
| 121 |
-
downloadAnchorNode.setAttribute("download", "measured_points.json");
|
| 122 |
-
document.body.appendChild(downloadAnchorNode); // required for firefox
|
| 123 |
-
downloadAnchorNode.click();
|
| 124 |
-
downloadAnchorNode.remove();
|
| 125 |
-
};
|
| 126 |
|
| 127 |
const openCustomDialog = () => {
|
| 128 |
setDraftCustomModel(customModel || DEFAULT_CUSTOM_MODEL);
|
|
@@ -292,18 +316,22 @@ export default function MeasurePanel({
|
|
| 292 |
))}
|
| 293 |
</List>
|
| 294 |
<Stack spacing={1} sx={{ mt: 1 }}>
|
| 295 |
-
<
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
<Button
|
| 308 |
variant="contained"
|
| 309 |
color={measuring ? "error" : "primary"}
|
|
@@ -313,21 +341,12 @@ export default function MeasurePanel({
|
|
| 313 |
{measuring ? "End Measure" : "Start Measure"}
|
| 314 |
</Button>
|
| 315 |
<Stack direction="row" spacing={1}>
|
| 316 |
-
<
|
| 317 |
-
variant="outlined"
|
| 318 |
-
color="primary"
|
| 319 |
-
startIcon={<DownloadIcon />}
|
| 320 |
-
onClick={() => handleExport()}
|
| 321 |
-
disabled={points.length === 0}
|
| 322 |
-
fullWidth
|
| 323 |
-
>
|
| 324 |
-
Export
|
| 325 |
-
</Button>
|
| 326 |
<Button
|
| 327 |
variant="outlined"
|
| 328 |
color="error"
|
| 329 |
startIcon={<DeleteIcon />}
|
| 330 |
-
onClick={
|
| 331 |
disabled={selectedIndex === null}
|
| 332 |
fullWidth
|
| 333 |
>
|
|
@@ -362,13 +381,7 @@ export default function MeasurePanel({
|
|
| 362 |
</ListItemButton>
|
| 363 |
))}
|
| 364 |
</List>
|
| 365 |
-
{
|
| 366 |
-
<Stack direction="row" spacing={1} sx={{ mt: 2 }}>
|
| 367 |
-
<Button variant="contained" onClick={onFinishDrawing} fullWidth>Finish Drawing</Button>
|
| 368 |
-
<Button variant="outlined" color="error" onClick={onCancelDrawing} fullWidth>Cancel</Button>
|
| 369 |
-
</Stack>
|
| 370 |
-
) : (
|
| 371 |
-
<Stack spacing={1} sx={{ mt: 2 }}>
|
| 372 |
<Button
|
| 373 |
variant="contained"
|
| 374 |
startIcon={<AddIcon />}
|
|
@@ -378,15 +391,7 @@ export default function MeasurePanel({
|
|
| 378 |
Create Polyline
|
| 379 |
</Button>
|
| 380 |
<Stack direction="row" spacing={1}>
|
| 381 |
-
<
|
| 382 |
-
variant="outlined"
|
| 383 |
-
startIcon={<DownloadIcon />}
|
| 384 |
-
onClick={() => onExportGeometries('polyline')}
|
| 385 |
-
disabled={polylines.length === 0}
|
| 386 |
-
fullWidth
|
| 387 |
-
>
|
| 388 |
-
Export
|
| 389 |
-
</Button>
|
| 390 |
<Button
|
| 391 |
variant="outlined"
|
| 392 |
color="error"
|
|
@@ -399,7 +404,6 @@ export default function MeasurePanel({
|
|
| 399 |
</Button>
|
| 400 |
</Stack>
|
| 401 |
</Stack>
|
| 402 |
-
)}
|
| 403 |
</TabPanel>
|
| 404 |
|
| 405 |
<TabPanel value={tabIndex} index={2}>
|
|
@@ -427,13 +431,7 @@ export default function MeasurePanel({
|
|
| 427 |
</ListItemButton>
|
| 428 |
))}
|
| 429 |
</List>
|
| 430 |
-
{
|
| 431 |
-
<Stack direction="row" spacing={1} sx={{ mt: 2 }}>
|
| 432 |
-
<Button variant="contained" onClick={onFinishDrawing} fullWidth>Finish Drawing</Button>
|
| 433 |
-
<Button variant="outlined" color="error" onClick={onCancelDrawing} fullWidth>Cancel</Button>
|
| 434 |
-
</Stack>
|
| 435 |
-
) : (
|
| 436 |
-
<Stack spacing={1} sx={{ mt: 2 }}>
|
| 437 |
<Button
|
| 438 |
variant="contained"
|
| 439 |
startIcon={<AddIcon />}
|
|
@@ -443,7 +441,7 @@ export default function MeasurePanel({
|
|
| 443 |
Create Polygon
|
| 444 |
</Button>
|
| 445 |
<Stack direction="row" spacing={1}>
|
| 446 |
-
<
|
| 447 |
<Button
|
| 448 |
variant="outlined"
|
| 449 |
color="error"
|
|
@@ -456,7 +454,6 @@ export default function MeasurePanel({
|
|
| 456 |
</Button>
|
| 457 |
</Stack>
|
| 458 |
</Stack>
|
| 459 |
-
)}
|
| 460 |
</TabPanel>
|
| 461 |
</CardContent>
|
| 462 |
|
|
|
|
| 2 |
import {
|
| 3 |
Card, CardContent, Typography, Button, List, ListItemButton,
|
| 4 |
ListItemText, Divider, Stack, FormControl, InputLabel, Select,
|
| 5 |
+
MenuItem, Menu, TextField, Dialog, DialogTitle, DialogContent, ToggleButton,
|
| 6 |
+
ToggleButtonGroup,
|
| 7 |
DialogActions, Tabs, Tab, Box
|
| 8 |
} from "@mui/material";
|
| 9 |
import DownloadIcon from "@mui/icons-material/Download";
|
|
|
|
| 47 |
};
|
| 48 |
}
|
| 49 |
|
| 50 |
+
function ExportMenu({ onExport, disabled }) {
|
| 51 |
+
const [anchorEl, setAnchorEl] = useState(null);
|
| 52 |
+
const open = Boolean(anchorEl);
|
| 53 |
+
|
| 54 |
+
const handleClick = (event) => {
|
| 55 |
+
setAnchorEl(event.currentTarget);
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
const handleClose = () => {
|
| 59 |
+
setAnchorEl(null);
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
const handleSelect = (format) => {
|
| 63 |
+
onExport(format);
|
| 64 |
+
handleClose();
|
| 65 |
+
};
|
| 66 |
+
|
| 67 |
+
return (
|
| 68 |
+
<>
|
| 69 |
+
<Button
|
| 70 |
+
variant="outlined"
|
| 71 |
+
startIcon={<DownloadIcon />}
|
| 72 |
+
onClick={handleClick}
|
| 73 |
+
disabled={disabled}
|
| 74 |
+
fullWidth
|
| 75 |
+
>
|
| 76 |
+
Export
|
| 77 |
+
</Button>
|
| 78 |
+
<Menu anchorEl={anchorEl} open={open} onClose={handleClose}>
|
| 79 |
+
<MenuItem onClick={() => handleSelect('geojson')}>As GeoJSON (.json)</MenuItem>
|
| 80 |
+
<MenuItem onClick={() => handleSelect('csv')}>As CSV (.csv)</MenuItem>
|
| 81 |
+
<MenuItem onClick={() => handleSelect('kml')}>As KML (.kml)</MenuItem>
|
| 82 |
+
</Menu>
|
| 83 |
+
</>
|
| 84 |
+
);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
export default function MeasurePanel({
|
| 88 |
points,
|
| 89 |
onSelectPoint,
|
|
|
|
| 147 |
setSelectedIndex(nextIndex);
|
| 148 |
if (onSelectPoint) onSelectPoint(points[index]);
|
| 149 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
const openCustomDialog = () => {
|
| 152 |
setDraftCustomModel(customModel || DEFAULT_CUSTOM_MODEL);
|
|
|
|
| 316 |
))}
|
| 317 |
</List>
|
| 318 |
<Stack spacing={1} sx={{ mt: 1 }}>
|
| 319 |
+
<ToggleButtonGroup
|
| 320 |
+
value={pointAppearance}
|
| 321 |
+
exclusive
|
| 322 |
+
fullWidth
|
| 323 |
+
size="small"
|
| 324 |
+
onChange={(event, newAppearance) => {
|
| 325 |
+
// Prevent unselecting all buttons
|
| 326 |
+
if (newAppearance !== null) {
|
| 327 |
+
onPointAppearanceChange(newAppearance);
|
| 328 |
+
}
|
| 329 |
+
}}
|
| 330 |
+
aria-label="point appearance"
|
| 331 |
+
>
|
| 332 |
+
<ToggleButton value="ellipsoid" sx={{ flex: 1 }}>Error Ellipsoid</ToggleButton>
|
| 333 |
+
<ToggleButton value="point" sx={{ flex: 1 }}>Simple Point</ToggleButton>
|
| 334 |
+
</ToggleButtonGroup>
|
| 335 |
<Button
|
| 336 |
variant="contained"
|
| 337 |
color={measuring ? "error" : "primary"}
|
|
|
|
| 341 |
{measuring ? "End Measure" : "Start Measure"}
|
| 342 |
</Button>
|
| 343 |
<Stack direction="row" spacing={1}>
|
| 344 |
+
<ExportMenu onExport={(format) => onExportGeometries('point', format)} disabled={points.length === 0} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
<Button
|
| 346 |
variant="outlined"
|
| 347 |
color="error"
|
| 348 |
startIcon={<DeleteIcon />}
|
| 349 |
+
onClick={onDeletePoint}
|
| 350 |
disabled={selectedIndex === null}
|
| 351 |
fullWidth
|
| 352 |
>
|
|
|
|
| 381 |
</ListItemButton>
|
| 382 |
))}
|
| 383 |
</List>
|
| 384 |
+
<Stack spacing={1} sx={{ mt: 2 }}>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
<Button
|
| 386 |
variant="contained"
|
| 387 |
startIcon={<AddIcon />}
|
|
|
|
| 391 |
Create Polyline
|
| 392 |
</Button>
|
| 393 |
<Stack direction="row" spacing={1}>
|
| 394 |
+
<ExportMenu onExport={(format) => onExportGeometries('polyline', format)} disabled={polylines.length === 0} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
<Button
|
| 396 |
variant="outlined"
|
| 397 |
color="error"
|
|
|
|
| 404 |
</Button>
|
| 405 |
</Stack>
|
| 406 |
</Stack>
|
|
|
|
| 407 |
</TabPanel>
|
| 408 |
|
| 409 |
<TabPanel value={tabIndex} index={2}>
|
|
|
|
| 431 |
</ListItemButton>
|
| 432 |
))}
|
| 433 |
</List>
|
| 434 |
+
<Stack spacing={1} sx={{ mt: 2 }}>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
<Button
|
| 436 |
variant="contained"
|
| 437 |
startIcon={<AddIcon />}
|
|
|
|
| 441 |
Create Polygon
|
| 442 |
</Button>
|
| 443 |
<Stack direction="row" spacing={1}>
|
| 444 |
+
<ExportMenu onExport={(format) => onExportGeometries('polygon', format)} disabled={polygons.length === 0} />
|
| 445 |
<Button
|
| 446 |
variant="outlined"
|
| 447 |
color="error"
|
|
|
|
| 454 |
</Button>
|
| 455 |
</Stack>
|
| 456 |
</Stack>
|
|
|
|
| 457 |
</TabPanel>
|
| 458 |
</CardContent>
|
| 459 |
|
client/src/pages/Home.jsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
-
import React, { useEffect, useMemo, useRef, useState } from "react";
|
| 2 |
import CesiumViewer from "../components/CesiumViewer";
|
| 3 |
import MeasurePanel from "../components/MeasurePanel";
|
|
|
|
| 4 |
import { getDefaultBucket, validateAwsConfig, AWS_REGION } from "../utils/awsConfig";
|
| 5 |
import { getApiUrl, fetchWithAuth } from "../utils/apiConfig";
|
| 6 |
import { recordVisit } from "../utils/visitorTracking";
|
|
@@ -8,7 +9,6 @@ import {
|
|
| 8 |
Dialog, DialogTitle, DialogContent, DialogActions, Button,
|
| 9 |
TextField, Stack
|
| 10 |
} from "@mui/material";
|
| 11 |
-
import * as shp from 'shp-write';
|
| 12 |
|
| 13 |
|
| 14 |
// Models configured to use AWS Cognito authentication
|
|
@@ -67,6 +67,19 @@ function buildTilesetUrl(model) {
|
|
| 67 |
return s3Key;
|
| 68 |
}
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
export default function Home() {
|
| 71 |
// AWS configuration validation
|
| 72 |
useEffect(() => {
|
|
@@ -106,6 +119,10 @@ export default function Home() {
|
|
| 106 |
const [polygons, setPolygons] = useState([]);
|
| 107 |
const [selectedGeometry, setSelectedGeometry] = useState(null); // { type: 'polyline' | 'polygon', id: string | number }
|
| 108 |
const geometryIdCounter = useRef(0);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
const models = useMemo(
|
| 110 |
() => [
|
| 111 |
...BASE_MODELS,
|
|
@@ -174,11 +191,11 @@ export default function Home() {
|
|
| 174 |
const [selectedPoint, setSelectedPoint] = useState(0);
|
| 175 |
const [pointAppearance, setPointAppearance] = useState("ellipsoid"); // 'ellipsoid' or 'point'
|
| 176 |
|
| 177 |
-
const handleAddPoint = (point) => {
|
| 178 |
maxPointId.current += 1;
|
| 179 |
console.log("New 3D point added:", point);
|
| 180 |
setPoints((prev) => [...prev, { ...point, id: maxPointId.current }]);
|
| 181 |
-
};
|
| 182 |
|
| 183 |
const handleMeasureClick = () => {
|
| 184 |
if (!cesiumRef.current) return;
|
|
@@ -216,16 +233,17 @@ export default function Home() {
|
|
| 216 |
setDrawingState({ mode, points: [] });
|
| 217 |
setSelectedPoint(0); // Deselect any points
|
| 218 |
setSelectedGeometry(null); // Deselect any geometry
|
| 219 |
-
|
| 220 |
};
|
| 221 |
|
| 222 |
const handleCancelDrawing = () => {
|
| 223 |
setDrawingState({ mode: 'none', points: [] });
|
| 224 |
-
|
|
|
|
| 225 |
};
|
| 226 |
|
| 227 |
const handleFinishDrawing = () => {
|
| 228 |
-
const { mode, points } = drawingState;
|
| 229 |
if (mode === 'none') {
|
| 230 |
handleCancelDrawing();
|
| 231 |
return;
|
|
@@ -235,26 +253,78 @@ export default function Home() {
|
|
| 235 |
const newId = `${mode}-${geometryIdCounter.current}`;
|
| 236 |
|
| 237 |
if (mode === 'polyline') {
|
| 238 |
-
if (
|
| 239 |
alert("A polyline requires at least 2 points.");
|
| 240 |
return;
|
| 241 |
}
|
| 242 |
-
const newPolyline = { id: newId, points };
|
| 243 |
setPolylines(prev => [...prev, newPolyline]);
|
| 244 |
} else if (mode === 'polygon') {
|
| 245 |
-
if (
|
| 246 |
alert("A polygon requires at least 3 points.");
|
| 247 |
return;
|
| 248 |
}
|
| 249 |
-
const newPolygon = { id: newId, points };
|
| 250 |
setPolygons(prev => [...prev, newPolygon]);
|
| 251 |
}
|
| 252 |
|
| 253 |
handleCancelDrawing();
|
| 254 |
};
|
| 255 |
|
| 256 |
-
const
|
| 257 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
};
|
| 259 |
|
| 260 |
const handleSelectGeometry = (type, id) => {
|
|
@@ -275,33 +345,79 @@ export default function Home() {
|
|
| 275 |
setSelectedGeometry(null);
|
| 276 |
};
|
| 277 |
|
| 278 |
-
const handleExportGeometries = (type) => {
|
| 279 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
if (geometries.length === 0) {
|
| 281 |
alert(`No ${type}s to export.`);
|
| 282 |
return;
|
| 283 |
}
|
| 284 |
-
|
| 285 |
-
const geojson = {
|
| 286 |
-
type: 'FeatureCollection',
|
| 287 |
-
features: geometries.map(geom => ({
|
| 288 |
-
type: 'Feature',
|
| 289 |
-
geometry: {
|
| 290 |
-
type: type === 'polyline' ? 'LineString' : 'Polygon',
|
| 291 |
-
// For polygons, GeoJSON requires the first and last point to be the same.
|
| 292 |
-
coordinates: type === 'polygon'
|
| 293 |
-
? [[...geom.points.map(p => [p.lon, p.lat, p.alt]), [geom.points[0].lon, geom.points[0].lat, geom.points[0].alt]]]
|
| 294 |
-
: geom.points.map(p => [p.lon, p.lat, p.alt]),
|
| 295 |
-
},
|
| 296 |
-
properties: { id: geom.id }
|
| 297 |
-
}))
|
| 298 |
-
};
|
| 299 |
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 305 |
}
|
| 306 |
};
|
| 307 |
|
|
@@ -437,8 +553,9 @@ export default function Home() {
|
|
| 437 |
drawingState={drawingState}
|
| 438 |
polylines={polylines}
|
| 439 |
polygons={polygons}
|
| 440 |
-
onPointSelectForDrawing={
|
| 441 |
selectedGeometry={selectedGeometry}
|
|
|
|
| 442 |
/>
|
| 443 |
|
| 444 |
{/* MUI Overlay */}
|
|
@@ -517,6 +634,22 @@ export default function Home() {
|
|
| 517 |
</DialogActions>
|
| 518 |
</Dialog>
|
| 519 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
</div>
|
| 521 |
);
|
| 522 |
}
|
|
|
|
| 1 |
+
import React, { useEffect, useMemo, useRef, useState, useCallback } from "react";
|
| 2 |
import CesiumViewer from "../components/CesiumViewer";
|
| 3 |
import MeasurePanel from "../components/MeasurePanel";
|
| 4 |
+
import GeometryCreatorPanel from "../components/GeometryCreatorPanel";
|
| 5 |
import { getDefaultBucket, validateAwsConfig, AWS_REGION } from "../utils/awsConfig";
|
| 6 |
import { getApiUrl, fetchWithAuth } from "../utils/apiConfig";
|
| 7 |
import { recordVisit } from "../utils/visitorTracking";
|
|
|
|
| 9 |
Dialog, DialogTitle, DialogContent, DialogActions, Button,
|
| 10 |
TextField, Stack
|
| 11 |
} from "@mui/material";
|
|
|
|
| 12 |
|
| 13 |
|
| 14 |
// Models configured to use AWS Cognito authentication
|
|
|
|
| 67 |
return s3Key;
|
| 68 |
}
|
| 69 |
|
| 70 |
+
function downloadFile(content, fileName, contentType) {
|
| 71 |
+
const blob = new Blob([content], { type: contentType });
|
| 72 |
+
const url = window.URL.createObjectURL(blob);
|
| 73 |
+
const a = document.createElement('a');
|
| 74 |
+
a.style.display = 'none';
|
| 75 |
+
a.href = url;
|
| 76 |
+
a.download = fileName;
|
| 77 |
+
document.body.appendChild(a);
|
| 78 |
+
a.click();
|
| 79 |
+
window.URL.revokeObjectURL(url);
|
| 80 |
+
a.remove();
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
export default function Home() {
|
| 84 |
// AWS configuration validation
|
| 85 |
useEffect(() => {
|
|
|
|
| 119 |
const [polygons, setPolygons] = useState([]);
|
| 120 |
const [selectedGeometry, setSelectedGeometry] = useState(null); // { type: 'polyline' | 'polygon', id: string | number }
|
| 121 |
const geometryIdCounter = useRef(0);
|
| 122 |
+
|
| 123 |
+
// --- State for the Geometry Creator Panel ---
|
| 124 |
+
const [previewPointId, setPreviewPointId] = useState(0);
|
| 125 |
+
const [multiSelectPoints, setMultiSelectPoints] = useState([]);
|
| 126 |
const models = useMemo(
|
| 127 |
() => [
|
| 128 |
...BASE_MODELS,
|
|
|
|
| 191 |
const [selectedPoint, setSelectedPoint] = useState(0);
|
| 192 |
const [pointAppearance, setPointAppearance] = useState("ellipsoid"); // 'ellipsoid' or 'point'
|
| 193 |
|
| 194 |
+
const handleAddPoint = useCallback((point) => {
|
| 195 |
maxPointId.current += 1;
|
| 196 |
console.log("New 3D point added:", point);
|
| 197 |
setPoints((prev) => [...prev, { ...point, id: maxPointId.current }]);
|
| 198 |
+
}, []);
|
| 199 |
|
| 200 |
const handleMeasureClick = () => {
|
| 201 |
if (!cesiumRef.current) return;
|
|
|
|
| 233 |
setDrawingState({ mode, points: [] });
|
| 234 |
setSelectedPoint(0); // Deselect any points
|
| 235 |
setSelectedGeometry(null); // Deselect any geometry
|
| 236 |
+
setMultiSelectPoints([]); // Reset multi-select
|
| 237 |
};
|
| 238 |
|
| 239 |
const handleCancelDrawing = () => {
|
| 240 |
setDrawingState({ mode: 'none', points: [] });
|
| 241 |
+
setMultiSelectPoints([]);
|
| 242 |
+
setPreviewPointId(0);
|
| 243 |
};
|
| 244 |
|
| 245 |
const handleFinishDrawing = () => {
|
| 246 |
+
const { mode, points: geometryPoints } = drawingState;
|
| 247 |
if (mode === 'none') {
|
| 248 |
handleCancelDrawing();
|
| 249 |
return;
|
|
|
|
| 253 |
const newId = `${mode}-${geometryIdCounter.current}`;
|
| 254 |
|
| 255 |
if (mode === 'polyline') {
|
| 256 |
+
if (geometryPoints.length < 2) {
|
| 257 |
alert("A polyline requires at least 2 points.");
|
| 258 |
return;
|
| 259 |
}
|
| 260 |
+
const newPolyline = { id: newId, points: geometryPoints };
|
| 261 |
setPolylines(prev => [...prev, newPolyline]);
|
| 262 |
} else if (mode === 'polygon') {
|
| 263 |
+
if (geometryPoints.length < 3) {
|
| 264 |
alert("A polygon requires at least 3 points.");
|
| 265 |
return;
|
| 266 |
}
|
| 267 |
+
const newPolygon = { id: newId, points: geometryPoints };
|
| 268 |
setPolygons(prev => [...prev, newPolygon]);
|
| 269 |
}
|
| 270 |
|
| 271 |
handleCancelDrawing();
|
| 272 |
};
|
| 273 |
|
| 274 |
+
const handlePointSelectForGeometry = useCallback((point) => {
|
| 275 |
+
// Since we only select existing points, they will always have an ID.
|
| 276 |
+
setDrawingState(prev => {
|
| 277 |
+
const isAlreadyAdded = prev.points.some(p => p.id === point.id);
|
| 278 |
+
if (isAlreadyAdded) {
|
| 279 |
+
// Remove the point if it's already in the list
|
| 280 |
+
return { ...prev, points: prev.points.filter(p => p.id !== point.id) };
|
| 281 |
+
} else {
|
| 282 |
+
// Add the point
|
| 283 |
+
return { ...prev, points: [...prev.points, point] };
|
| 284 |
+
}
|
| 285 |
+
});
|
| 286 |
+
}, []);
|
| 287 |
+
|
| 288 |
+
const handleAvailablePointClick = (event, pointId) => {
|
| 289 |
+
const isCtrlOrMeta = event.ctrlKey || event.metaKey;
|
| 290 |
+
|
| 291 |
+
setMultiSelectPoints(prev => {
|
| 292 |
+
if (isCtrlOrMeta) {
|
| 293 |
+
if (prev.includes(pointId)) {
|
| 294 |
+
return prev.filter(id => id !== pointId);
|
| 295 |
+
} else {
|
| 296 |
+
return [...prev, pointId];
|
| 297 |
+
}
|
| 298 |
+
} else {
|
| 299 |
+
if (prev.length === 1 && prev[0] === pointId) {
|
| 300 |
+
return [];
|
| 301 |
+
} else {
|
| 302 |
+
return [pointId];
|
| 303 |
+
}
|
| 304 |
+
}
|
| 305 |
+
});
|
| 306 |
+
};
|
| 307 |
+
|
| 308 |
+
const handleAddMultiSelectedPoints = () => {
|
| 309 |
+
const pointsToAdd = points.filter(p => multiSelectPoints.includes(p.id));
|
| 310 |
+
setDrawingState(prev => {
|
| 311 |
+
const newPoints = pointsToAdd.filter(p => !prev.points.some(dp => dp.id === p.id));
|
| 312 |
+
return { ...prev, points: [...prev.points, ...newPoints] };
|
| 313 |
+
});
|
| 314 |
+
setMultiSelectPoints([]); // Clear selection after adding
|
| 315 |
+
};
|
| 316 |
+
|
| 317 |
+
const handleClearGeometryPoints = () => {
|
| 318 |
+
setDrawingState(prev => ({ ...prev, points: [] }));
|
| 319 |
+
};
|
| 320 |
+
|
| 321 |
+
const handleReorderGeometryPoints = (oldIndex, newIndex) => {
|
| 322 |
+
setDrawingState(prev => {
|
| 323 |
+
const items = Array.from(prev.points);
|
| 324 |
+
const [reorderedItem] = items.splice(oldIndex, 1);
|
| 325 |
+
items.splice(newIndex, 0, reorderedItem);
|
| 326 |
+
return { ...prev, points: items };
|
| 327 |
+
});
|
| 328 |
};
|
| 329 |
|
| 330 |
const handleSelectGeometry = (type, id) => {
|
|
|
|
| 345 |
setSelectedGeometry(null);
|
| 346 |
};
|
| 347 |
|
| 348 |
+
const handleExportGeometries = (type, format) => {
|
| 349 |
+
let geometries;
|
| 350 |
+
const fileName = `measurements_${type}`;
|
| 351 |
+
|
| 352 |
+
if (type === 'point') {
|
| 353 |
+
geometries = points;
|
| 354 |
+
} else {
|
| 355 |
+
geometries = type === 'polyline' ? polylines : polygons;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
if (geometries.length === 0) {
|
| 359 |
alert(`No ${type}s to export.`);
|
| 360 |
return;
|
| 361 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
|
| 363 |
+
// --- GeoJSON is the common base for some formats ---
|
| 364 |
+
const geojson = type === 'point'
|
| 365 |
+
? {
|
| 366 |
+
type: 'FeatureCollection',
|
| 367 |
+
features: geometries.map(p => ({
|
| 368 |
+
type: 'Feature',
|
| 369 |
+
geometry: { type: 'Point', coordinates: [p.lon, p.lat, p.alt] },
|
| 370 |
+
properties: { id: p.id, accuracy: p.accuracy }
|
| 371 |
+
}))
|
| 372 |
+
}
|
| 373 |
+
: { // polyline or polygon
|
| 374 |
+
type: 'FeatureCollection',
|
| 375 |
+
features: geometries.map(geom => ({
|
| 376 |
+
type: 'Feature',
|
| 377 |
+
geometry: {
|
| 378 |
+
type: type === 'polyline' ? 'LineString' : 'Polygon',
|
| 379 |
+
coordinates: type === 'polygon'
|
| 380 |
+
? [[...geom.points.map(p => [p.lon, p.lat, p.alt]), [geom.points[0].lon, geom.points[0].lat, geom.points[0].alt]]]
|
| 381 |
+
: geom.points.map(p => [p.lon, p.lat, p.alt]),
|
| 382 |
+
},
|
| 383 |
+
properties: { id: geom.id }
|
| 384 |
+
}))
|
| 385 |
+
};
|
| 386 |
+
|
| 387 |
+
// --- Format-specific logic ---
|
| 388 |
+
if (format === 'geojson') {
|
| 389 |
+
const dataStr = JSON.stringify(geojson, null, 2);
|
| 390 |
+
downloadFile(dataStr, `${fileName}.geojson`, 'application/json');
|
| 391 |
+
} else if (format === 'csv') {
|
| 392 |
+
let csvContent = '';
|
| 393 |
+
if (type === 'point') {
|
| 394 |
+
csvContent = 'id,lon,lat,alt,accuracy\n';
|
| 395 |
+
geometries.forEach(p => {
|
| 396 |
+
csvContent += `${p.id},${p.lon},${p.lat},${p.alt},${p.accuracy}\n`;
|
| 397 |
+
});
|
| 398 |
+
} else { // polyline or polygon
|
| 399 |
+
csvContent = 'geometry_id,point_order,lon,lat,alt\n';
|
| 400 |
+
geometries.forEach(geom => {
|
| 401 |
+
geom.points.forEach((p, index) => {
|
| 402 |
+
csvContent += `${geom.id},${index + 1},${p.lon},${p.lat},${p.alt}\n`;
|
| 403 |
+
});
|
| 404 |
+
});
|
| 405 |
+
}
|
| 406 |
+
downloadFile(csvContent, `${fileName}.csv`, 'text/csv;charset=utf-8;');
|
| 407 |
+
} else if (format === 'kml') {
|
| 408 |
+
const placemarks = geometries.map(geom => {
|
| 409 |
+
const name = type === 'point' ? `Point ${geom.id}` : geom.id;
|
| 410 |
+
const points = type === 'point' ? [geom] : geom.points;
|
| 411 |
+
const coords = points.map(p => `${p.lon},${p.lat},${p.alt}`).join(' ');
|
| 412 |
+
let geometryString;
|
| 413 |
+
if (type === 'point') geometryString = `<Point><coordinates>${coords}</coordinates></Point>`;
|
| 414 |
+
else if (type === 'polyline') geometryString = `<LineString><coordinates>${coords}</coordinates></LineString>`;
|
| 415 |
+
else if (type === 'polygon') geometryString = `<Polygon><outerBoundaryIs><LinearRing><coordinates>${coords} ${points[0].lon},${points[0].lat},${points[0].alt}</coordinates></LinearRing></outerBoundaryIs></Polygon>`;
|
| 416 |
+
return `<Placemark><name>${name}</name>${geometryString || ''}</Placemark>`;
|
| 417 |
+
}).join('\n ');
|
| 418 |
+
|
| 419 |
+
const kmlContent = `<?xml version="1.0" encoding="UTF-8"?>\n<kml xmlns="http://www.opengis.net/kml/2.2">\n <Document>\n <name>${fileName}</name>\n ${placemarks}\n </Document>\n</kml>`;
|
| 420 |
+
downloadFile(kmlContent, `${fileName}.kml`, 'application/vnd.google-earth.kml+xml');
|
| 421 |
}
|
| 422 |
};
|
| 423 |
|
|
|
|
| 553 |
drawingState={drawingState}
|
| 554 |
polylines={polylines}
|
| 555 |
polygons={polygons}
|
| 556 |
+
onPointSelectForDrawing={handlePointSelectForGeometry}
|
| 557 |
selectedGeometry={selectedGeometry}
|
| 558 |
+
previewPointId={previewPointId}
|
| 559 |
/>
|
| 560 |
|
| 561 |
{/* MUI Overlay */}
|
|
|
|
| 634 |
</DialogActions>
|
| 635 |
</Dialog>
|
| 636 |
|
| 637 |
+
<GeometryCreatorPanel
|
| 638 |
+
isOpen={drawingState.mode !== 'none'}
|
| 639 |
+
drawingState={drawingState}
|
| 640 |
+
onCancel={handleCancelDrawing}
|
| 641 |
+
onFinish={handleFinishDrawing}
|
| 642 |
+
onPointSelect={handlePointSelectForGeometry}
|
| 643 |
+
onPreviewPoint={setPreviewPointId}
|
| 644 |
+
// Props for available points list
|
| 645 |
+
availablePoints={points}
|
| 646 |
+
multiSelectPoints={multiSelectPoints}
|
| 647 |
+
onAvailablePointClick={handleAvailablePointClick}
|
| 648 |
+
onAddMultiSelectedPoints={handleAddMultiSelectedPoints}
|
| 649 |
+
onClearPoints={handleClearGeometryPoints}
|
| 650 |
+
onReorderPoints={handleReorderGeometryPoints}
|
| 651 |
+
/>
|
| 652 |
+
|
| 653 |
</div>
|
| 654 |
);
|
| 655 |
}
|