1/** 2 * Undo/Redo â Waypoint state snapshots with history navigation. 3 */ 4 5import { eventBus } from '../core/event-bus.js?v=284'; 6import { store } from '../core/state.js?v=284'; 7import { getWaypoints, restoreWaypoints } from './waypoints.js?v=284'; 8import { calculateRoute, clearRoute } from './route-engine.js?v=284'; 9 10const MAX_HISTORY = 50; 11 12let waypointsHistory = [[]]; // Start with empty snapshot 13let historyIndex = 0; 14let isUndoRedo = false; // Prevent recursive saves during restore 15 16// âââ Public API âââ 17 18export function initUndoRedo() { 19 const undoBtn = document.getElementById('undo-btn'); 20 const redoBtn = document.getElementById('redo-btn'); 21 22 if (undoBtn) undoBtn.addEventListener('click', undo); 23 if (redoBtn) redoBtn.addEventListener('click', redo); 24 25 // Keyboard shortcuts 26 document.addEventListener('keydown', (e) => { 27 // Cmd+Z / Ctrl+Z = undo 28 if ((e.metaKey || e.ctrlKey) && e.key === 'z' && !e.shiftKey) { 29 // Don't capture if focused on an input 30 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; 31 e.preventDefault(); 32 undo(); 33 } 34 // Cmd+Shift+Z / Ctrl+Shift+Z or Cmd+Y / Ctrl+Y = redo 35 if ((e.metaKey || e.ctrlKey) && ((e.key === 'z' && e.shiftKey) || e.key === 'y')) { 36 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; 37 e.preventDefault(); 38 redo(); 39 } 40 }); 41 42 // Save snapshot whenever waypoints change 43 eventBus.on('waypoint:changed', () => { 44 if (isUndoRedo) return; // Skip saves during undo/redo restore 45 saveSnapshot(getWaypoints()); 46 }); 47 48 // GPX split: save snapshot without triggering route recalculation 49 eventBus.on('waypoint:gpxSplit', () => { 50 saveSnapshot(getWaypoints()); 51 }); 52 53 eventBus.on('history:commit', ({ before, after } = {}) => { 54 if (before) { 55 // Freeze the pre-edit geometry so undo does not ask the router to redraw it. 56 waypointsHistory[historyIndex] = structuredClone(before); 57 } 58 saveSnapshot(after || getWaypoints()); 59 }); 60 61 // Route loaded (saved route open, GPX import): those paths restore waypoints 62 // without emitting waypoint:changed, so no baseline snapshot exists â the 63 // first undo after loading would step back to empty (or to a previously 64 // built route's stale history). Reset and seed with the loaded state so 65 // undo reverts edits made ON the loaded route. 66 eventBus.on('history:seed', () => { 67 waypointsHistory = [[]]; 68 historyIndex = 0; 69 const current = getWaypoints(); 70 if (current.length > 0) saveSnapshot(current); 71 updateButtons(); 72 }); 73 74 updateButtons(); 75} 76 77// âââ Core âââ 78 79function saveSnapshot(waypoints) { 80 // Deep copy the waypoints array (including gpxCoords for GPX-imported routes) 81 const snapshot = waypoints.map(wp => ({ 82 id: wp.id, 83 lngLat: wp.lngLat ? [...wp.lngLat] : null, 84 address: wp.address || '', 85 profile: wp.profile || null, 86 gapToNext: wp.gapToNext || false, 87 gpxTimes: wp.gpxTimes ? [...wp.gpxTimes] : undefined, 88 gpxCoords: wp.gpxCoords ? wp.gpxCoords.map(c => [...c]) : undefined, 89 gpxRoadClasses: wp.gpxRoadClasses ? [...wp.gpxRoadClasses] : undefined, 90 gpxSurfaces: wp.gpxSurfaces ? [...wp.gpxSurfaces] : undefined, 91 gpxLts: wp.gpxLts ? [...wp.gpxLts] : undefined, 92 gpxBikePathAccess: wp.gpxBikePathAccess ? [...wp.gpxBikePathAccess] : undefined, 93 gpxBikeInfra: wp.gpxBikeInfra ? [...wp.gpxBikeInfra] : undefined, 94 })); 95 96 // Truncate any future states (user made a new action after undoing) 97 waypointsHistory = waypointsHistory.slice(0, historyIndex + 1); 98 99 // Push new snapshot 100 waypointsHistory.push(snapshot); 101 102 // Cap history size 103 if (waypointsHistory.length > MAX_HISTORY) { 104 waypointsHistory.shift(); 105 } 106 107 historyIndex = waypointsHistory.length - 1; 108 updateButtons(); 109} 110 111function undo() { 112 if (historyIndex <= 0) return; 113 114 const previousSnapshot = waypointsHistory[historyIndex - 1]; 115 const hasActiveRoute = !!store.get('routeGeometry'); 116 117 // If there's an active route and undo would leave 0 waypoints, warn before deleting
118 if (hasActiveRoute && previousSnapshot.length === 0) { 119 if (!confirm('This will delete the entire route. Are you sure?')) return; 120 } 121 122 historyIndex--; 123 124 isUndoRedo = true; 125 restoreWaypoints(previousSnapshot); 126 if (previousSnapshot.length < 2) { 127 clearRoute(); 128 } else { 129 calculateRoute(); 130 } 131 isUndoRedo = false; 132 133 updateButtons(); 134} 135 136function redo() { 137 if (historyIndex >= waypointsHistory.length - 1) return; 138 139 historyIndex++; 140 const snapshot = waypointsHistory[historyIndex]; 141 142 isUndoRedo = true; 143 restoreWaypoints(snapshot); 144 calculateRoute(); 145 isUndoRedo = false; 146 147 updateButtons(); 148} 149 150function updateButtons() { 151 const canUndo = historyIndex > 0; 152 const canRedo = historyIndex < waypointsHistory.length - 1; 153 154 const undoBtn = document.getElementById('undo-btn'); 155 const redoBtn = document.getElementById('redo-btn'); 156 157 if (undoBtn) { 158 undoBtn.style.display = canUndo ? 'inline-flex' : 'none'; 159 } 160 if (redoBtn) { 161 redoBtn.style.display = canRedo ? 'inline-flex' : 'none'; 162 } 163}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.