route lengths, edit saved routs, ui visibility toggle

This commit is contained in:
Evan Scamehorn
2025-12-02 13:34:39 -06:00
parent c5437de834
commit 852016088b
7 changed files with 146 additions and 65 deletions

View File

@@ -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),
} }
) )

View File

@@ -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

View File

@@ -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) {

View File

@@ -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);
}); });
} }

View File

@@ -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; }