Skip to content

Commit

Permalink
Undo support
Browse files Browse the repository at this point in the history
  • Loading branch information
dabreegster committed Nov 13, 2023
1 parent 67fb67d commit ebd1859
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 6 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ changes.

## Unreleased

- Undo support

## 0.2.4

- Include road labels for waypoints in interactive output
Expand Down
21 changes: 21 additions & 0 deletions route-snapper/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ export class RouteSnapper {
e.preventDefault();
this.inner.toggleSnapMode();
this.#redraw();
} else if (e.key == "z" && e.ctrlKey) {
e.preventDefault();
this.inner.undo();
this.#redraw();
}
});

Expand Down Expand Up @@ -250,6 +254,7 @@ export class RouteSnapper {
this.controlDiv.innerHTML = `
<div style="display: flex; justify-content: space-evenly;">
<button type="button" id="finish-route-button">Finish route</button>
<button type="button" id="undo-button" disabled>Undo</button>
<button type="button" id="cancel-button">Cancel</button>
</div>
Expand Down Expand Up @@ -284,6 +289,7 @@ export class RouteSnapper {
<li>Press <b>s</b> to toggle snapping / freehand mode</li>
<li><b>Click and drag</b> any point to move it</li>
<li><b>Click</b> a red waypoint to delete it</li>
<li>Press <b>Control+Z</b> to undo</li>
<li>Press <b>Enter</b> or <b>double click</b> to finish route</li>
<li>Press <b>Escape</b> to cancel and discard route</li>
</ul>
Expand All @@ -297,6 +303,10 @@ export class RouteSnapper {
document.getElementById("finish-route-button").onclick = () => {
this.#finishSnapping();
};
document.getElementById("undo-button").onclick = () => {
this.inner.undo();
this.#redraw();
};
document.getElementById("cancel-button").onclick = () => {
this.controlDiv.dispatchEvent(new CustomEvent("no-new-route"));
this.stop();
Expand Down Expand Up @@ -377,6 +387,17 @@ export class RouteSnapper {
this.map.getSource("route-snapper").setData(gj);
this.map.getCanvas().style.cursor = gj.cursor;

let undoButton = document.getElementById("undo-button");
if (undoButton) {
if (gj.undo_length > 0) {
undoButton.disabled = false;
undoButton.textContent = `Undo (${gj.undo_length})`;
} else {
undoButton.textContent = "Undo";
undoButton.disabled = true;
}
}

// TODO Detect changes, don't do this constantly?
let snapDiv = document.getElementById("snap_mode");
if (snapDiv) {
Expand Down
36 changes: 36 additions & 0 deletions route-snapper/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ use route_snapper_graph::{EdgeID, NodeID, RouteSnapperMap};

static START: Once = Once::new();

const MAX_PREVIOUS_STATES: usize = 100;

type Graph = DiGraphMap<NodeID, DirectedEdge>;

#[wasm_bindgen]
Expand All @@ -29,6 +31,8 @@ pub struct JsRouteSnapper {
route: Route,
mode: Mode,
snap_mode: bool,
// Copies of route.waypoints are sufficient to represent state
previous_states: Vec<Vec<Waypoint>>,
}

#[derive(Default, Serialize, Deserialize)]
Expand Down Expand Up @@ -168,6 +172,7 @@ impl JsRouteSnapper {
route: Route::new(),
mode: Mode::Neutral,
snap_mode: true,
previous_states: Vec::new(),
})
}

Expand Down Expand Up @@ -417,6 +422,7 @@ impl JsRouteSnapper {
let mut props = serde_json::Map::new();
props.insert("cursor".to_string(), cursor.into());
props.insert("snap_mode".to_string(), self.snap_mode.into());
props.insert("undo_length".to_string(), self.previous_states.len().into());
fc.foreign_members = Some(props);
}
serde_json::to_string_pretty(&gj).unwrap()
Expand Down Expand Up @@ -454,6 +460,7 @@ impl JsRouteSnapper {
}
}
};
// Don't keep every single update during a drag
let new_idx = self.route.move_waypoint(&self.router, idx, new_waypt);
self.mode = Mode::Dragging {
idx: new_idx,
Expand Down Expand Up @@ -510,6 +517,7 @@ impl JsRouteSnapper {
};
if let Some(new_waypt) = new_waypt {
if new_waypt != at {
// Don't keep every single update during a drag
let new_idx = self.route.move_waypoint(&self.router, idx, new_waypt);
self.mode = Mode::Dragging {
idx: new_idx,
Expand Down Expand Up @@ -551,6 +559,7 @@ impl JsRouteSnapper {
// TODO Allow freehand points for areas, once we can convert existing waypoints
if !self.router.config.area_mode {
if let Mode::Freehand(pt) = self.mode {
self.before_update();
self.route.add_waypoint(&self.router, Waypoint::Free(pt));
}
}
Expand All @@ -565,12 +574,14 @@ impl JsRouteSnapper {
&& idx != 0
&& idx != self.route.waypoints.len() - 1
{
self.before_update();
self.route.waypoints.remove(idx);
self.route.recalculate_full_path(&self.router);
}
} else {
// Don't delete the only waypoint
if self.route.waypoints.len() > 1 {
self.before_update();
self.route.waypoints.remove(idx);
self.route.recalculate_full_path(&self.router);
// We're still in a hovering state on the point we just deleted. We may
Expand All @@ -585,6 +596,7 @@ impl JsRouteSnapper {
return;
}

self.before_update();
self.route.add_waypoint(&self.router, hover);
if self.router.config.area_mode
&& !self.route.is_closed_area()
Expand All @@ -608,6 +620,8 @@ impl JsRouteSnapper {
.iter()
.position(|x| *x == at.to_path_entry())
{
// TODO Only do this for the first actual bit of drag?
self.before_update();
self.mode = Mode::Dragging { idx, at };
self.snap_mode = matches!(at, Waypoint::Snapped(_));
return true;
Expand All @@ -631,6 +645,7 @@ impl JsRouteSnapper {
self.route = Route::new();
self.mode = Mode::Neutral;
self.snap_mode = true;
self.previous_states.clear();
}

#[wasm_bindgen(js_name = editExisting)]
Expand Down Expand Up @@ -691,11 +706,24 @@ impl JsRouteSnapper {
}
let pt = LonLat::new(lon, lat).to_pt(&self.router.map.gps_bounds);
if let Some(node) = self.mouseover_node(pt) {
self.before_update();
self.route
.add_waypoint(&self.router, Waypoint::Snapped(node));
}
}

#[wasm_bindgen()]
pub fn undo(&mut self) {
if let Mode::Dragging { .. } = self.mode {
// Too confusing
return;
}
if let Some(state) = self.previous_states.pop() {
self.route.waypoints = state;
self.route.recalculate_full_path(&self.router);
}
}

fn name_for_waypoint(&self, waypoint: &RouteWaypoint) -> Result<String, JsValue> {
if !waypoint.snapped {
return Ok("???".to_string());
Expand All @@ -708,6 +736,14 @@ impl JsRouteSnapper {
return Err(JsValue::from_str("A waypoint didn't snap"));
}
}

fn before_update(&mut self) {
self.previous_states.push(self.route.waypoints.clone());
// TODO Different data structure to make this more efficient
if self.previous_states.len() > MAX_PREVIOUS_STATES {
self.previous_states.remove(0);
}
}
}

impl JsRouteSnapper {
Expand Down
14 changes: 8 additions & 6 deletions user_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,14 @@ If you're using the WASM API directly, the best reference is currently [the code
- It'll include LineStrings showing the confirmed route and also any speculative addition, based on the current state. The LineStrings will have a boolean `snapped` property, which is false if either end touches a freehand point.
- In area mode, it'll have a Polygon once there are at least 3 points.
- It'll include a Point for every graph node involved in the current route. These will have a `type` property that's either `snapped-waypoint`, `free-waypoint`, or just `node` to indicate a draggable node that hasn't been touched yet. One Point may also have a `"hovered": true` property to indicate the mouse is currently on that Point. Points may also have a `name` property with the road names for that intersection.
- The GeoJSON object will also contain a foreign member called `cursor` to indicate the current mode of the tool. The values can be set to `map.getCanvas().style.cursor` as desired.
- `inherit`: The user is just idling on the map, not interacting with the map
- `pointer`: The user is hovering on some node
- `grabbing`: The user is actively dragging a node
- `crosshair`: The user is choosing a location for a new freehand point. If they click, the point will be added.
- The GeoJSON object will also have a boolean foreign member called `snap_mode`.
- The GeoJSON object will have some additional foreign members:
- `cursor`, indicating the current mode of the tool. The values can be set to `map.getCanvas().style.cursor` as desired.
- `inherit`: The user is just idling on the map, not interacting with the map
- `pointer`: The user is hovering on some node
- `grabbing`: The user is actively dragging a node
- `crosshair`: The user is choosing a location for a new freehand point. If they click, the point will be added.
- A boolean `snap_mode`
- A numeric `undo_length`
- `toggleSnapMode` attempts to switch between snapping and freehand drawing. It may not succeed.
- `addSnappedWaypoint` adds a new waypoint to the end of the route, snapping to the nearest node. It's useful for clients to hook up a geocoder and add a point by address. Unsupported in area mode.

Expand Down

0 comments on commit ebd1859

Please sign in to comment.