route lengths, edit saved routs, ui visibility toggle
This commit is contained in:
@@ -214,6 +214,7 @@ for u, v, k in G.edges(keys=True):
|
|||||||
"v": int(v),
|
"v": int(v),
|
||||||
"oneway": bool(row.get("oneway", False)),
|
"oneway": bool(row.get("oneway", False)),
|
||||||
"points": parse_line_points(row.geometry, center_x, center_y),
|
"points": parse_line_points(row.geometry, center_x, center_y),
|
||||||
|
"length": round(float(row.geometry.length), 2),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<!-- The UI Overlay -->
|
<!-- The UI Overlay -->
|
||||||
|
<button id="ui-toggle" title="Toggle Menu">☰</button>
|
||||||
|
|
||||||
<div id="ui-container">
|
<div id="ui-container">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h2>Route Planner</h2>
|
<h2>Route Planner</h2>
|
||||||
@@ -35,6 +37,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -9,16 +9,15 @@ export class RouteManager {
|
|||||||
|
|
||||||
// -- State --
|
// -- State --
|
||||||
this.currentRouteNodes = [];
|
this.currentRouteNodes = [];
|
||||||
this.savedRoutes = []; // { nodes: [], length: number, mesh: THREE.Mesh }
|
this.savedRoutes = [];
|
||||||
|
|
||||||
// -- Visuals --
|
// -- Visuals --
|
||||||
this.markers = []; // Draggable spheres
|
this.markers = [];
|
||||||
this.currentPathMesh = null; // The tube being edited
|
this.currentPathMesh = null;
|
||||||
|
|
||||||
this.ROAD_OFFSET = 2.5;
|
this.ROAD_OFFSET = 2.5;
|
||||||
|
|
||||||
// -- Callbacks --
|
this.onRouteChanged = null;
|
||||||
this.onRouteChanged = null; // function(lengthInMeters)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initGraph(data) {
|
initGraph(data) {
|
||||||
@@ -36,20 +35,25 @@ export class RouteManager {
|
|||||||
|
|
||||||
if (!this.graphData.adjacency[edge.u]) this.graphData.adjacency[edge.u] = [];
|
if (!this.graphData.adjacency[edge.u]) this.graphData.adjacency[edge.u] = [];
|
||||||
this.graphData.adjacency[edge.u].push({
|
this.graphData.adjacency[edge.u].push({
|
||||||
to: edge.v, cost: edge.length || 1, edgeIndex: index
|
to: edge.v,
|
||||||
|
cost: edge.length || 1, // Fallback if length missing
|
||||||
|
edgeIndex: index
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!edge.oneway) {
|
if (!edge.oneway) {
|
||||||
if (!this.graphData.adjacency[edge.v]) this.graphData.adjacency[edge.v] = [];
|
if (!this.graphData.adjacency[edge.v]) this.graphData.adjacency[edge.v] = [];
|
||||||
this.graphData.adjacency[edge.v].push({
|
this.graphData.adjacency[edge.v].push({
|
||||||
to: edge.u, cost: edge.length || 1, edgeIndex: index, isReverse: true
|
to: edge.u,
|
||||||
|
cost: edge.length || 1,
|
||||||
|
edgeIndex: index,
|
||||||
|
isReverse: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// API Methods (For UI/Input)
|
// API Methods
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
addNodeByWorldPosition(vector3) {
|
addNodeByWorldPosition(vector3) {
|
||||||
@@ -88,58 +92,69 @@ export class RouteManager {
|
|||||||
saveCurrentRoute() {
|
saveCurrentRoute() {
|
||||||
if (this.currentRouteNodes.length < 2 || !this.currentPathMesh) return;
|
if (this.currentRouteNodes.length < 2 || !this.currentPathMesh) return;
|
||||||
|
|
||||||
// 1. Calculate final length
|
|
||||||
const totalLength = this.currentPathMesh.userData.length || 0;
|
const totalLength = this.currentPathMesh.userData.length || 0;
|
||||||
|
|
||||||
// 2. Freeze the mesh (Change color to indicate saved state)
|
// Freeze mesh color
|
||||||
this.currentPathMesh.material.color.setHex(0x10B981); // Emerald Green
|
this.currentPathMesh.material.color.setHex(0x10B981);
|
||||||
|
|
||||||
// 3. Store in saved list
|
|
||||||
this.savedRoutes.push({
|
this.savedRoutes.push({
|
||||||
nodes: [...this.currentRouteNodes],
|
nodes: [...this.currentRouteNodes],
|
||||||
length: totalLength,
|
length: totalLength,
|
||||||
mesh: this.currentPathMesh
|
mesh: this.currentPathMesh
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. Detach mesh from "current" reference so we don't delete it on reset
|
|
||||||
this.currentPathMesh = null;
|
this.currentPathMesh = null;
|
||||||
|
|
||||||
// 5. Clear drafting state (remove markers, clear node list)
|
|
||||||
this.resetDraftingState();
|
this.resetDraftingState();
|
||||||
}
|
}
|
||||||
|
|
||||||
clearCurrentRoute() {
|
editSavedRoute(index) {
|
||||||
// 1. Remove current mesh from scene
|
|
||||||
if (this.currentPathMesh) {
|
|
||||||
this.scene.remove(this.currentPathMesh);
|
|
||||||
this.currentPathMesh.geometry.dispose();
|
|
||||||
this.currentPathMesh = null;
|
|
||||||
}
|
|
||||||
// 2. Reset state
|
|
||||||
this.resetDraftingState();
|
|
||||||
}
|
|
||||||
|
|
||||||
resetDraftingState() {
|
|
||||||
this.currentRouteNodes = [];
|
|
||||||
// Remove all markers
|
|
||||||
this.markers.forEach(m => this.scene.remove(m));
|
|
||||||
this.markers = [];
|
|
||||||
|
|
||||||
// Notify UI
|
|
||||||
if (this.onRouteChanged) this.onRouteChanged(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteSavedRoute(index) {
|
|
||||||
if (index < 0 || index >= this.savedRoutes.length) return;
|
if (index < 0 || index >= this.savedRoutes.length) return;
|
||||||
|
|
||||||
|
// 1. If we are currently drafting, discard it (or save it automatically? let's discard for simplicity)
|
||||||
|
this.clearCurrentRoute();
|
||||||
|
|
||||||
const route = this.savedRoutes[index];
|
const route = this.savedRoutes[index];
|
||||||
|
|
||||||
// Remove mesh from scene
|
// 2. Load nodes
|
||||||
|
this.currentRouteNodes = [...route.nodes];
|
||||||
|
|
||||||
|
// 3. Remove the saved mesh from scene (we will redraw it as active)
|
||||||
if (route.mesh) {
|
if (route.mesh) {
|
||||||
this.scene.remove(route.mesh);
|
this.scene.remove(route.mesh);
|
||||||
route.mesh.geometry.dispose();
|
route.mesh.geometry.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Remove from saved list
|
||||||
|
this.savedRoutes.splice(index, 1);
|
||||||
|
|
||||||
|
// 5. Restore Visuals (Markers & Path)
|
||||||
|
this.currentRouteNodes.forEach(nodeId => this.addMarkerVisual(nodeId));
|
||||||
|
this.updatePathVisuals();
|
||||||
|
}
|
||||||
|
|
||||||
|
clearCurrentRoute() {
|
||||||
|
if (this.currentPathMesh) {
|
||||||
|
this.scene.remove(this.currentPathMesh);
|
||||||
|
this.currentPathMesh.geometry.dispose();
|
||||||
|
this.currentPathMesh = null;
|
||||||
|
}
|
||||||
|
this.resetDraftingState();
|
||||||
|
}
|
||||||
|
|
||||||
|
resetDraftingState() {
|
||||||
|
this.currentRouteNodes = [];
|
||||||
|
this.markers.forEach(m => this.scene.remove(m));
|
||||||
|
this.markers = [];
|
||||||
|
if (this.onRouteChanged) this.onRouteChanged(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSavedRoute(index) {
|
||||||
|
if (index < 0 || index >= this.savedRoutes.length) return;
|
||||||
|
const route = this.savedRoutes[index];
|
||||||
|
if (route.mesh) {
|
||||||
|
this.scene.remove(route.mesh);
|
||||||
|
route.mesh.geometry.dispose();
|
||||||
|
}
|
||||||
this.savedRoutes.splice(index, 1);
|
this.savedRoutes.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +167,7 @@ export class RouteManager {
|
|||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
updatePathVisuals() {
|
updatePathVisuals() {
|
||||||
// Needs 2+ nodes
|
// Need 2+ nodes
|
||||||
if (this.currentRouteNodes.length < 2) {
|
if (this.currentRouteNodes.length < 2) {
|
||||||
if (this.currentPathMesh) {
|
if (this.currentPathMesh) {
|
||||||
this.scene.remove(this.currentPathMesh);
|
this.scene.remove(this.currentPathMesh);
|
||||||
@@ -163,7 +178,7 @@ export class RouteManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let fullPathPoints = [];
|
let fullPathPoints = [];
|
||||||
let totalDist = 0;
|
let totalDist = 0; // Reset Distance
|
||||||
|
|
||||||
for (let i = 0; i < this.currentRouteNodes.length - 1; i++) {
|
for (let i = 0; i < this.currentRouteNodes.length - 1; i++) {
|
||||||
const start = this.currentRouteNodes[i];
|
const start = this.currentRouteNodes[i];
|
||||||
@@ -174,7 +189,16 @@ export class RouteManager {
|
|||||||
if (!segmentEdges) continue;
|
if (!segmentEdges) continue;
|
||||||
|
|
||||||
segmentEdges.forEach(step => {
|
segmentEdges.forEach(step => {
|
||||||
totalDist += step.edgeData.cost || 0; // Accumulate distance
|
// --- FIX: Accumulate Distance ---
|
||||||
|
// If Python didn't send 'length', calculate Euclidean
|
||||||
|
let dist = step.edgeData.length;
|
||||||
|
if (!dist) {
|
||||||
|
const p1 = step.edgeData.points[0];
|
||||||
|
const p2 = step.edgeData.points[step.edgeData.points.length - 1];
|
||||||
|
dist = Math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2);
|
||||||
|
}
|
||||||
|
totalDist += dist;
|
||||||
|
// --------------------------------
|
||||||
|
|
||||||
const rawPoints = step.edgeData.points;
|
const rawPoints = step.edgeData.points;
|
||||||
let segmentPoints = rawPoints.map(p => new THREE.Vector2(p[0], p[1]));
|
let segmentPoints = rawPoints.map(p => new THREE.Vector2(p[0], p[1]));
|
||||||
@@ -185,7 +209,6 @@ export class RouteManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update Mesh
|
|
||||||
if (this.currentPathMesh) {
|
if (this.currentPathMesh) {
|
||||||
this.scene.remove(this.currentPathMesh);
|
this.scene.remove(this.currentPathMesh);
|
||||||
this.currentPathMesh.geometry.dispose();
|
this.currentPathMesh.geometry.dispose();
|
||||||
@@ -198,23 +221,19 @@ export class RouteManager {
|
|||||||
const tubeMat = new THREE.MeshBasicMaterial({ color: this.settings.colors.route });
|
const tubeMat = new THREE.MeshBasicMaterial({ color: this.settings.colors.route });
|
||||||
|
|
||||||
this.currentPathMesh = new THREE.Mesh(tubeGeom, tubeMat);
|
this.currentPathMesh = new THREE.Mesh(tubeGeom, tubeMat);
|
||||||
// Store length on the mesh for easy access later
|
|
||||||
this.currentPathMesh.userData.length = totalDist;
|
this.currentPathMesh.userData.length = totalDist;
|
||||||
|
|
||||||
this.scene.add(this.currentPathMesh);
|
this.scene.add(this.currentPathMesh);
|
||||||
|
|
||||||
// Update markers color (First=Green, Last=Red, Others=Yellow)
|
|
||||||
this.updateMarkerColors();
|
this.updateMarkerColors();
|
||||||
|
|
||||||
// Trigger Callback
|
|
||||||
if (this.onRouteChanged) this.onRouteChanged(totalDist);
|
if (this.onRouteChanged) this.onRouteChanged(totalDist);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMarkerColors() {
|
updateMarkerColors() {
|
||||||
this.markers.forEach((marker, i) => {
|
this.markers.forEach((marker, i) => {
|
||||||
let color = 0xFFFF00; // Default Yellow (Waypoint)
|
let color = 0xFFFF00; // Yellow
|
||||||
if (i === 0) color = this.settings.colors.pathStart; // Green
|
if (i === 0) color = this.settings.colors.pathStart;
|
||||||
else if (i === this.markers.length - 1) color = this.settings.colors.pathEnd; // Red
|
else if (i === this.markers.length - 1) color = this.settings.colors.pathEnd;
|
||||||
marker.material.color.setHex(color);
|
marker.material.color.setHex(color);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -230,12 +249,11 @@ export class RouteManager {
|
|||||||
|
|
||||||
this.scene.add(mesh);
|
this.scene.add(mesh);
|
||||||
this.markers.push(mesh);
|
this.markers.push(mesh);
|
||||||
|
|
||||||
this.updateMarkerColors();
|
this.updateMarkerColors();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================
|
// ============================
|
||||||
// Algorithms (A* & Helpers)
|
// Algorithms
|
||||||
// ============================
|
// ============================
|
||||||
|
|
||||||
findNearestNode(x, z) {
|
findNearestNode(x, z) {
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ export class UIManager {
|
|||||||
// DOM Elements
|
// DOM Elements
|
||||||
this.elCurrentLength = document.getElementById('current-length');
|
this.elCurrentLength = document.getElementById('current-length');
|
||||||
this.elRouteList = document.getElementById('route-list');
|
this.elRouteList = document.getElementById('route-list');
|
||||||
|
this.elContainer = document.getElementById('ui-container');
|
||||||
this.btnSave = document.getElementById('btn-save');
|
this.btnSave = document.getElementById('btn-save');
|
||||||
this.btnDiscard = document.getElementById('btn-discard');
|
this.btnDiscard = document.getElementById('btn-discard');
|
||||||
|
this.btnToggle = document.getElementById('ui-toggle');
|
||||||
|
|
||||||
this.initListeners();
|
this.initListeners();
|
||||||
}
|
}
|
||||||
@@ -22,14 +24,14 @@ export class UIManager {
|
|||||||
this.routeManager.clearCurrentRoute();
|
this.routeManager.clearCurrentRoute();
|
||||||
this.updateStats(0);
|
this.updateStats(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Toggle Logic
|
||||||
|
this.btnToggle.addEventListener('click', () => {
|
||||||
|
this.elContainer.classList.toggle('hidden');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the text display for current route length
|
|
||||||
* @param {number} lengthInMeters
|
|
||||||
*/
|
|
||||||
updateStats(lengthInMeters) {
|
updateStats(lengthInMeters) {
|
||||||
// Format: If > 1000m, show km. Else meters.
|
|
||||||
let text = "";
|
let text = "";
|
||||||
if (lengthInMeters > 1000) {
|
if (lengthInMeters > 1000) {
|
||||||
text = (lengthInMeters / 1000).toFixed(2) + " km";
|
text = (lengthInMeters / 1000).toFixed(2) + " km";
|
||||||
@@ -46,24 +48,35 @@ export class UIManager {
|
|||||||
routes.forEach((route, index) => {
|
routes.forEach((route, index) => {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
|
|
||||||
// Format Length
|
|
||||||
let lenStr = route.length > 1000
|
let lenStr = route.length > 1000
|
||||||
? (route.length / 1000).toFixed(2) + " km"
|
? (route.length / 1000).toFixed(2) + " km"
|
||||||
: Math.round(route.length) + " m";
|
: Math.round(route.length) + " m";
|
||||||
|
|
||||||
li.innerHTML = `
|
// Create Label
|
||||||
<span><strong>Route ${index + 1}</strong> (${lenStr})</span>
|
const span = document.createElement('span');
|
||||||
`;
|
span.innerHTML = `<strong>Route ${index + 1}</strong> (${lenStr})`;
|
||||||
|
li.appendChild(span);
|
||||||
|
|
||||||
|
// Edit Button
|
||||||
|
const btnEdit = document.createElement('button');
|
||||||
|
btnEdit.textContent = "Edit";
|
||||||
|
btnEdit.className = "btn-icon btn-edit";
|
||||||
|
btnEdit.onclick = () => {
|
||||||
|
this.routeManager.editSavedRoute(index);
|
||||||
|
this.renderRouteList(); // Re-render to remove it from list
|
||||||
|
};
|
||||||
|
li.appendChild(btnEdit);
|
||||||
|
|
||||||
// Delete Button
|
// Delete Button
|
||||||
const btnDel = document.createElement('button');
|
const btnDel = document.createElement('button');
|
||||||
btnDel.textContent = "✕";
|
btnDel.textContent = "✕";
|
||||||
|
btnDel.className = "btn-icon btn-del";
|
||||||
btnDel.onclick = () => {
|
btnDel.onclick = () => {
|
||||||
this.routeManager.deleteSavedRoute(index);
|
this.routeManager.deleteSavedRoute(index);
|
||||||
this.renderRouteList();
|
this.renderRouteList();
|
||||||
};
|
};
|
||||||
|
|
||||||
li.appendChild(btnDel);
|
li.appendChild(btnDel);
|
||||||
|
|
||||||
this.elRouteList.appendChild(li);
|
this.elRouteList.appendChild(li);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
50
style.css
50
style.css
@@ -4,10 +4,29 @@ body {
|
|||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ui-container {
|
#ui-toggle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20px;
|
top: 20px;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
|
z-index: 100; /* Above UI container */
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||||
|
font-size: 20px;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ui-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 70px; /* Moved down to make room for toggle */
|
||||||
|
left: 20px;
|
||||||
width: 280px;
|
width: 280px;
|
||||||
background: rgba(255, 255, 255, 0.95);
|
background: rgba(255, 255, 255, 0.95);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
@@ -15,8 +34,19 @@ body {
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
max-height: 90vh;
|
max-height: 80vh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
|
/* Animation for toggling */
|
||||||
|
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||||
|
transform-origin: top left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Class to hide UI */
|
||||||
|
#ui-container.hidden {
|
||||||
|
transform: scale(0.95);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header h2 { margin: 0 0 5px 0; font-size: 18px; color: #333; }
|
.header h2 { margin: 0 0 5px 0; font-size: 18px; color: #333; }
|
||||||
@@ -52,10 +82,17 @@ button:hover { opacity: 0.8; }
|
|||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap; /* Allow wrapping for buttons */
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
#route-list li span {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
#route-list li button {
|
#route-list li button {
|
||||||
flex: 0;
|
flex: 0;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
@@ -64,3 +101,12 @@ button:hover { opacity: 0.8; }
|
|||||||
background: #fee2e2;
|
background: #fee2e2;
|
||||||
color: #991b1b;
|
color: #991b1b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
.btn-edit { background: #dbeafe; color: #1e40af; }
|
||||||
|
.btn-del { background: #fee2e2; color: #991b1b; }
|
||||||
|
|||||||
Reference in New Issue
Block a user