diff --git a/backend/src/lib.rs b/backend/src/lib.rs index 4da855a..a03b4b3 100644 --- a/backend/src/lib.rs +++ b/backend/src/lib.rs @@ -154,14 +154,20 @@ impl LTN { } #[wasm_bindgen(js_name = setCurrentNeighbourhood)] - pub fn set_current_neighbourhood(&mut self, name: String) -> Result<(), JsValue> { + pub fn set_current_neighbourhood( + &mut self, + name: String, + edit_perimeter_roads: bool, + ) -> Result<(), JsValue> { info!("setCurrentNeighbourhood"); let boundary_gj = self.map.boundaries.get(&name).cloned().unwrap(); let mut boundary_geo: Polygon = boundary_gj.try_into().map_err(err_to_js)?; self.map.mercator.to_mercator_in_place(&mut boundary_geo); - self.neighbourhood = - Some(Neighbourhood::new(&self.map, name, boundary_geo).map_err(err_to_js)?); + self.neighbourhood = Some( + Neighbourhood::new(&self.map, name, boundary_geo, edit_perimeter_roads) + .map_err(err_to_js)?, + ); info!("done"); Ok(()) } @@ -175,7 +181,7 @@ impl LTN { x: pos.lng, y: pos.lat, }), - Some(&self.neighbourhood.as_ref().unwrap().interior_roads), + Some(self.neighbourhood.as_ref().unwrap().editable_roads()), FilterKind::from_string(&kind).unwrap(), ); self.after_edit(); diff --git a/backend/src/map_model.rs b/backend/src/map_model.rs index ec6cd22..1949b3c 100644 --- a/backend/src/map_model.rs +++ b/backend/src/map_model.rs @@ -118,7 +118,7 @@ impl MapModel { pub fn add_modal_filter( &mut self, pt: Coord, - candidate_roads: Option<&BTreeSet>, + candidate_roads: Option>, kind: FilterKind, ) { let cmd = self.do_edit(self.add_modal_filter_cmd(pt, candidate_roads, kind)); @@ -130,7 +130,7 @@ impl MapModel { fn add_modal_filter_cmd( &self, pt: Coord, - candidate_roads: Option<&BTreeSet>, + candidate_roads: Option>, kind: FilterKind, ) -> Command { let (r, percent_along) = self.closest_point_on_road(pt, candidate_roads).unwrap(); @@ -146,18 +146,16 @@ impl MapModel { fn closest_point_on_road( &self, click_pt: Coord, - candidate_roads: Option<&BTreeSet>, + candidate_roads: Option>, ) -> Option<(RoadID, f64)> { // If candidate_roads is not specified, search around the point with a generous buffer - let roads: Vec = if let Some(set) = candidate_roads { - set.iter().cloned().collect() - } else { + let roads = candidate_roads.unwrap_or_else(|| { let bbox = buffer_aabb(AABB::from_point(click_pt.into()), 50.0); self.closest_road .locate_in_envelope_intersecting(&bbox) .map(|r| r.data) .collect() - }; + }); roads .into_iter() diff --git a/backend/src/neighbourhood.rs b/backend/src/neighbourhood.rs index 3ec39c0..ae62989 100644 --- a/backend/src/neighbourhood.rs +++ b/backend/src/neighbourhood.rs @@ -22,6 +22,9 @@ pub struct Neighbourhood { // Mercator pub boundary_polygon: Polygon, boundary_area_km2: f64, + /// If true, shortcuts across perimeter roads will be calculated, and the user can edit these + /// roads. + pub edit_perimeter_roads: bool, // Updated after mutations derived: Option, @@ -33,7 +36,12 @@ struct DerivedNeighbourhoodState { } impl Neighbourhood { - pub fn new(map: &MapModel, name: String, boundary_polygon: Polygon) -> Result { + pub fn new( + map: &MapModel, + name: String, + boundary_polygon: Polygon, + edit_perimeter_roads: bool, + ) -> Result { info!("match roads"); let bbox = buffer_aabb(aabb(&boundary_polygon), 50.0); @@ -88,6 +96,7 @@ impl Neighbourhood { name, boundary_polygon, boundary_area_km2, + edit_perimeter_roads, derived: None, }; n.after_edit(map); @@ -111,6 +120,18 @@ impl Neighbourhood { } } + pub fn editable_roads(&self) -> Vec { + if self.edit_perimeter_roads { + self.interior_roads + .iter() + .chain(self.crosses.keys()) + .cloned() + .collect() + } else { + self.interior_roads.iter().cloned().collect() + } + } + pub fn to_gj(&self, map: &MapModel) -> FeatureCollection { let mut features = Vec::new(); @@ -119,8 +140,8 @@ impl Neighbourhood { // Just one boundary features.push(map.boundaries.get(&self.name).cloned().unwrap()); - for r in &self.interior_roads { - let road = map.get_r(*r); + for r in self.editable_roads() { + let road = map.get_r(r); let mut f = road.to_gj(&map.mercator); f.set_property("kind", "interior_road"); f.set_property( @@ -128,18 +149,20 @@ impl Neighbourhood { derived .shortcuts .count_per_road - .get(r) + .get(&r) .cloned() .unwrap_or(0), ); - f.set_property("direction", map.directions[r].to_string()); + f.set_property("direction", map.directions[&r].to_string()); f.set_property( "direction_edited", - map.directions[r] != Direction::from_osm(&road.tags), + map.directions[&r] != Direction::from_osm(&road.tags), ); f.set_property("road", r.0); features.push(f); } + + // Only for debugging for (r, pct) in &self.crosses { let mut f = map.get_r(*r).to_gj(&map.mercator); f.set_property("kind", "crosses"); diff --git a/backend/src/shortcuts.rs b/backend/src/shortcuts.rs index 9ed6ad3..0bd9702 100644 --- a/backend/src/shortcuts.rs +++ b/backend/src/shortcuts.rs @@ -22,11 +22,11 @@ impl Shortcuts { let mut input_graph = InputGraph::new(); let mut node_map = NodeMap::new(); - for r in &neighbourhood.interior_roads { - if map.modal_filters.contains_key(r) { + for r in neighbourhood.editable_roads() { + if map.modal_filters.contains_key(&r) { continue; } - let road = map.get_r(*r); + let road = map.get_r(r); // Loops can't be part of a shortest path if road.src_i == road.dst_i { continue; @@ -35,7 +35,7 @@ impl Shortcuts { let i1 = node_map.get_or_insert(road.src_i); let i2 = node_map.get_or_insert(road.dst_i); let cost = (road.length() * 100.0) as usize; - match map.directions[r] { + match map.directions[&r] { Direction::Forwards => { input_graph.add_edge(i1, i2, cost); } diff --git a/backend/src/tests.rs b/backend/src/tests.rs index 9702e31..c4983ed 100644 --- a/backend/src/tests.rs +++ b/backend/src/tests.rs @@ -50,7 +50,13 @@ fn get_gj(study_area_name: &str, savefile_name: &str, neighbourhood_name: &str) let boundary_gj = map.boundaries.get(neighbourhood_name).cloned().unwrap(); let mut boundary_geo: Polygon = boundary_gj.try_into()?; map.mercator.to_mercator_in_place(&mut boundary_geo); - let neighbourhood = Neighbourhood::new(&map, neighbourhood_name.to_string(), boundary_geo)?; + let edit_perimeter_roads = false; + let neighbourhood = Neighbourhood::new( + &map, + neighbourhood_name.to_string(), + boundary_geo, + edit_perimeter_roads, + )?; let mut gj = prune_features(neighbourhood.to_gj(&map)); @@ -59,6 +65,7 @@ fn get_gj(study_area_name: &str, savefile_name: &str, neighbourhood_name: &str) f.set_property("kind", "existing_modal_filter"); f.remove_property("road"); f.remove_property("edited"); + f.remove_property("angle"); gj.features.push(f); } @@ -85,6 +92,7 @@ fn prune_features(mut gj: FeatureCollection) -> FeatureCollection { "way", ] .contains(&k.as_str()) + && k != "angle" }); } } diff --git a/web/src/AutoBoundariesMode.svelte b/web/src/AutoBoundariesMode.svelte index 1858313..975ae62 100644 --- a/web/src/AutoBoundariesMode.svelte +++ b/web/src/AutoBoundariesMode.svelte @@ -11,7 +11,7 @@ import { Link, Popup, layerId } from "./common"; import { isLine, isPolygon } from "svelte-utils/map"; import { SplitComponent } from "svelte-utils/top_bar_layout"; - import { app, mode, autosave } from "./stores"; + import { app, mode, autosave, editPerimeterRoads } from "./stores"; import { downloadGeneratedFile } from "svelte-utils"; import { pickNeighbourhoodName } from "./common/pick_names"; @@ -38,7 +38,7 @@ }; $app!.setNeighbourhoodBoundary(name, feature); autosave(); - $app!.setCurrentNeighbourhood(name); + $app!.setCurrentNeighbourhood(name, $editPerimeterRoads); $mode = { mode: "neighbourhood", }; diff --git a/web/src/NetworkMode.svelte b/web/src/NetworkMode.svelte index 50b440c..cd029d8 100644 --- a/web/src/NetworkMode.svelte +++ b/web/src/NetworkMode.svelte @@ -7,7 +7,13 @@ import { layerId, Popup, Link, HelpButton } from "./common"; import ModalFilterLayer from "./ModalFilterLayer.svelte"; import { SplitComponent } from "svelte-utils/top_bar_layout"; - import { app, autosave, mode, projectName } from "./stores"; + import { + app, + autosave, + mode, + projectName, + editPerimeterRoads, + } from "./stores"; import { pickNeighbourhoodName } from "./common/pick_names"; // Note we do this to trigger a refresh when loading stuff @@ -17,7 +23,7 @@ .map((f: Feature) => f.properties!.name); function pickNeighbourhood(name: string) { - $app!.setCurrentNeighbourhood(name); + $app!.setCurrentNeighbourhood(name, $editPerimeterRoads); $mode = { mode: "neighbourhood" }; } diff --git a/web/src/SetBoundaryMode.svelte b/web/src/SetBoundaryMode.svelte index 1e82f29..08faea6 100644 --- a/web/src/SetBoundaryMode.svelte +++ b/web/src/SetBoundaryMode.svelte @@ -4,7 +4,7 @@ import { notNull } from "svelte-utils"; import AreaControls from "./common/draw_area/AreaControls.svelte"; import { calculateArea, waypoints } from "./common/draw_area/stores"; - import { autosave, app, mode, map } from "./stores"; + import { autosave, app, mode, map, editPerimeterRoads } from "./stores"; import type { AreaProps } from "route-snapper-ts"; export let name: string; @@ -36,7 +36,7 @@ let feature = calculateArea($waypoints); $app!.setNeighbourhoodBoundary(name, feature); autosave(); - $app!.setCurrentNeighbourhood(name); + $app!.setCurrentNeighbourhood(name, $editPerimeterRoads); $mode = { mode: "neighbourhood", }; diff --git a/web/src/edit/NeighbourhoodMode.svelte b/web/src/edit/NeighbourhoodMode.svelte index 6dfad86..721f993 100644 --- a/web/src/edit/NeighbourhoodMode.svelte +++ b/web/src/edit/NeighbourhoodMode.svelte @@ -16,6 +16,7 @@ filterType, autosave, animateShortcuts, + editPerimeterRoads, } from "../stores"; import ChangeModalFilter from "./ChangeModalFilter.svelte"; import FreehandLine from "./FreehandLine.svelte"; @@ -68,6 +69,14 @@ autosave(); } + function recalculateNeighbourhoodDefinition() { + $app!.setCurrentNeighbourhood( + boundary!.properties.name, + $editPerimeterRoads, + ); + $mutationCounter++; + } + function onClickLine(f: Feature, pt: LngLat) { if (action == "filter") { $app!.addModalFilter(pt, $filterType); @@ -239,6 +248,15 @@ Animate shortcuts + +