From 47dbf24b0504ff9f152224de3dba1fc1f6301408 Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Sat, 23 Mar 2024 12:52:50 +0000 Subject: [PATCH 01/15] Find and display cycles starting around a road or junction WIP, got things glued together, but the algorithm itself is very wrong --- osm2streets-js/src/lib.rs | 5 + osm2streets/src/operations/find_cycle.rs | 93 +++++++++++++++++++ osm2streets/src/operations/mod.rs | 1 + web/src/common/utils.ts | 2 + web/src/street-explorer/App.svelte | 2 + .../street-explorer/IntersectionPopup.svelte | 11 ++- web/src/street-explorer/RenderCycle.svelte | 27 ++++++ web/src/street-explorer/stores.ts | 5 + 8 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 osm2streets/src/operations/find_cycle.rs create mode 100644 web/src/street-explorer/RenderCycle.svelte create mode 100644 web/src/street-explorer/stores.ts diff --git a/osm2streets-js/src/lib.rs b/osm2streets-js/src/lib.rs index 1fc0819c..c45893f8 100644 --- a/osm2streets-js/src/lib.rs +++ b/osm2streets-js/src/lib.rs @@ -249,6 +249,11 @@ impl JsStreetNetwork { out.push_str(""); Ok(out) } + + #[wasm_bindgen(js_name = findCycle)] + pub fn find_cycle(&self, intersection: usize) -> String { + self.inner.find_cycle(IntersectionID(intersection)).unwrap() + } } // Mutations diff --git a/osm2streets/src/operations/find_cycle.rs b/osm2streets/src/operations/find_cycle.rs new file mode 100644 index 00000000..857d1c5f --- /dev/null +++ b/osm2streets/src/operations/find_cycle.rs @@ -0,0 +1,93 @@ +use std::collections::{hash_map::Entry, BinaryHeap, HashMap}; + +use anyhow::Result; +use geojson::Feature; +use geom::Distance; + +use crate::{IntersectionID, RoadID, StreetNetwork}; + +enum Step { + Node(IntersectionID), + Edge(RoadID), +} + +impl StreetNetwork { + pub fn find_cycle(&self, start: IntersectionID) -> Result { + let mut backrefs: HashMap = HashMap::new(); + // This is a max-heap, so negate all distances. Tie breaker is intersection ID, arbitrary + // but deterministic. + let mut queue: BinaryHeap<(Distance, IntersectionID)> = BinaryHeap::new(); + queue.push((Distance::ZERO, start)); + + while !queue.is_empty() { + let (dist_so_far, current) = queue.pop().unwrap(); + + if current == start && dist_so_far != Distance::ZERO { + let mut steps = vec![Step::Node(current)]; + let mut current = current; + loop { + if current == start && steps.len() > 1 { + /*steps.pop(); + steps.reverse();*/ + return render(self, steps); + } + let road = backrefs[¤t]; + current = self.roads[&road].other_side(current); + steps.push(Step::Edge(road)); + steps.push(Step::Node(current)); + } + } + + for road in &self.intersections[¤t].roads { + let next_i = self.roads[road].other_side(current); + if let Entry::Vacant(e) = backrefs.entry(next_i) { + e.insert(*road); + // Remember to keep things negative + let dist = dist_so_far - self.roads[road].center_line.length(); + queue.push((dist, next_i)); + } + } + } + + bail!("Something broke"); + } +} + +fn render(streets: &StreetNetwork, steps: Vec) -> Result { + let mut features = Vec::new(); + + info!("Cycle!"); + for step in steps { + match step { + Step::Node(i) => { + info!("- {i}"); + let mut f = Feature::from( + streets.intersections[&i] + .polygon + .to_geojson(Some(&streets.gps_bounds)), + ); + f.set_property("type", "intersection"); + features.push(f); + } + Step::Edge(r) => { + info!("- {r}"); + let road = &streets.roads[&r]; + let mut f = Feature::from( + road.center_line + .make_polygons(road.total_width()) + .to_geojson(Some(&streets.gps_bounds)), + ); + f.set_property("type", "road"); + features.push(f); + } + } + } + + let gj = geojson::GeoJson::from(geojson::FeatureCollection { + bbox: None, + features, + foreign_members: None, + }); + let output = serde_json::to_string_pretty(&gj)?; + Ok(output) +} diff --git a/osm2streets/src/operations/mod.rs b/osm2streets/src/operations/mod.rs index 6c9dba2b..60434c80 100644 --- a/osm2streets/src/operations/mod.rs +++ b/osm2streets/src/operations/mod.rs @@ -2,5 +2,6 @@ mod collapse_intersection; mod collapse_short_road; +mod find_cycle; mod update_geometry; pub mod zip_sidepath; diff --git a/web/src/common/utils.ts b/web/src/common/utils.ts index 9f5530f4..7b4a8da4 100644 --- a/web/src/common/utils.ts +++ b/web/src/common/utils.ts @@ -115,8 +115,10 @@ const layerZorder = [ "lane-markings", "intersection-markings", + // Street Explorer only "movements", "connected-roads", + "cycle", // TODO This is specific to lane-editor. Figure out how different apps can // build on top of the common common ordering. diff --git a/web/src/street-explorer/App.svelte b/web/src/street-explorer/App.svelte index b063a320..8707cde5 100644 --- a/web/src/street-explorer/App.svelte +++ b/web/src/street-explorer/App.svelte @@ -3,6 +3,7 @@ import init from "osm2streets-js"; import { onMount } from "svelte"; import AppSwitcher from "../AppSwitcher.svelte"; + import RenderCycle from "./RenderCycle.svelte"; import { StreetView, ThemePicker, @@ -92,6 +93,7 @@ +
diff --git a/web/src/street-explorer/IntersectionPopup.svelte b/web/src/street-explorer/IntersectionPopup.svelte index 6d2a571a..2771ce94 100644 --- a/web/src/street-explorer/IntersectionPopup.svelte +++ b/web/src/street-explorer/IntersectionPopup.svelte @@ -2,6 +2,7 @@ import type { Polygon } from "geojson"; import type { FeatureWithProps } from "../common/utils"; import { network } from "../common"; + import { cycleGj } from "./stores"; // Note the input is maplibre's GeoJSONFeature, which stringifies nested properties export let data: FeatureWithProps | undefined; @@ -14,6 +15,11 @@ $network = $network; close(); } + + function findCycle() { + cycleGj.set(JSON.parse($network!.findCycle(props.id))); + close(); + }

Intersection #{props.id}

@@ -36,4 +42,7 @@ {/each}

- +
+ + +
diff --git a/web/src/street-explorer/RenderCycle.svelte b/web/src/street-explorer/RenderCycle.svelte new file mode 100644 index 00000000..4462abae --- /dev/null +++ b/web/src/street-explorer/RenderCycle.svelte @@ -0,0 +1,27 @@ + + + + + +
{JSON.stringify(data.properties, null, "  ")}
+
+
+
+ +
+ Cycle +
diff --git a/web/src/street-explorer/stores.ts b/web/src/street-explorer/stores.ts new file mode 100644 index 00000000..22eddc31 --- /dev/null +++ b/web/src/street-explorer/stores.ts @@ -0,0 +1,5 @@ +import type { FeatureCollection } from "geojson"; +import { writable, type Writable } from "svelte/store"; +import { emptyGeojson } from "../common/utils"; + +export const cycleGj: Writable = writable(emptyGeojson()); From 419d75e0fe46f932e1f3a0b8065d4a3a27556dff Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Sat, 23 Mar 2024 13:22:06 +0000 Subject: [PATCH 02/15] Get the cycle finding algorithm to work? i think we're inserting backrefs too early, before we commit --- osm2streets/src/operations/find_cycle.rs | 26 ++++++++++++++----- .../layers/RenderIntersectionPolygons.svelte | 8 +++++- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/osm2streets/src/operations/find_cycle.rs b/osm2streets/src/operations/find_cycle.rs index 857d1c5f..7d3c26c1 100644 --- a/osm2streets/src/operations/find_cycle.rs +++ b/osm2streets/src/operations/find_cycle.rs @@ -1,4 +1,4 @@ -use std::collections::{hash_map::Entry, BinaryHeap, HashMap}; +use std::collections::{hash_map::Entry, BinaryHeap, HashMap, HashSet}; use anyhow::Result; use geojson::Feature; @@ -14,15 +14,25 @@ enum Step { impl StreetNetwork { pub fn find_cycle(&self, start: IntersectionID) -> Result { let mut backrefs: HashMap = HashMap::new(); - // This is a max-heap, so negate all distances. Tie breaker is intersection ID, arbitrary - // but deterministic. - let mut queue: BinaryHeap<(Distance, IntersectionID)> = BinaryHeap::new(); - queue.push((Distance::ZERO, start)); + // Never cross the same road twice + let mut visited: HashSet = HashSet::new(); + // This is a max-heap, so negate all distances. Tie breaker is arbitrary but deterministic. + // Track where we're going and how we got there. + let mut queue: BinaryHeap<(Distance, IntersectionID, Option)> = BinaryHeap::new(); + queue.push((Distance::ZERO, start, None)); while !queue.is_empty() { - let (dist_so_far, current) = queue.pop().unwrap(); + let (dist_so_far, current, via_road) = queue.pop().unwrap(); + if let Some(via) = via_road { + if visited.contains(&via) { + continue; + } + visited.insert(via); + } + info!("Current step: {current} with dist {dist_so_far} via {:?}", via_road); if current == start && dist_so_far != Distance::ZERO { + info!(" found cycle"); let mut steps = vec![Step::Node(current)]; let mut current = current; loop { @@ -38,13 +48,15 @@ impl StreetNetwork { } } + // when on i12, we skip over going to i17. when does it wind up in backrefs? for road in &self.intersections[¤t].roads { let next_i = self.roads[road].other_side(current); if let Entry::Vacant(e) = backrefs.entry(next_i) { e.insert(*road); // Remember to keep things negative let dist = dist_so_far - self.roads[road].center_line.length(); - queue.push((dist, next_i)); + queue.push((dist, next_i, Some(*road))); + info!(" Havent been to {next_i} yet, so go there via {road}. dist will be {dist}"); } } } diff --git a/web/src/common/layers/RenderIntersectionPolygons.svelte b/web/src/common/layers/RenderIntersectionPolygons.svelte index 2d25c745..fe8b5129 100644 --- a/web/src/common/layers/RenderIntersectionPolygons.svelte +++ b/web/src/common/layers/RenderIntersectionPolygons.svelte @@ -2,7 +2,7 @@ import LayerControls from "../LayerControls.svelte"; import { theme, hoveredIntersection, network } from "../store"; import { caseHelper, layerId, emptyGeojson } from "../utils"; - import { hoverStateFilter, FillLayer, GeoJSON } from "svelte-maplibre"; + import { SymbolLayer, hoverStateFilter, FillLayer, GeoJSON } from "svelte-maplibre"; export let hoverCursor: string | undefined = undefined; @@ -41,6 +41,12 @@ > + From 481087c8fdcbf4e1b964819e6944716c36be34da Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Sat, 23 Mar 2024 13:27:32 +0000 Subject: [PATCH 03/15] or maybe that doesnt help --- osm2streets/src/operations/find_cycle.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osm2streets/src/operations/find_cycle.rs b/osm2streets/src/operations/find_cycle.rs index 7d3c26c1..5f742bff 100644 --- a/osm2streets/src/operations/find_cycle.rs +++ b/osm2streets/src/operations/find_cycle.rs @@ -28,6 +28,7 @@ impl StreetNetwork { continue; } visited.insert(via); + backrefs.insert(current, via); } info!("Current step: {current} with dist {dist_so_far} via {:?}", via_road); @@ -52,7 +53,8 @@ impl StreetNetwork { for road in &self.intersections[¤t].roads { let next_i = self.roads[road].other_side(current); if let Entry::Vacant(e) = backrefs.entry(next_i) { - e.insert(*road); + // DONT do this yet + //e.insert(*road); // Remember to keep things negative let dist = dist_so_far - self.roads[road].center_line.length(); queue.push((dist, next_i, Some(*road))); From 6f411a6395fb60166d2e4c3716f883b9a31967a1 Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Sat, 23 Mar 2024 13:35:12 +0000 Subject: [PATCH 04/15] try DFS. we just need any cycle, really eh, it finds a cycle, but has to backtrack and so includes too much stuff --- osm2streets/src/operations/find_cycle.rs | 54 +++++-------------- .../layers/RenderIntersectionPolygons.svelte | 15 ++++-- 2 files changed, 24 insertions(+), 45 deletions(-) diff --git a/osm2streets/src/operations/find_cycle.rs b/osm2streets/src/operations/find_cycle.rs index 5f742bff..9eab01f3 100644 --- a/osm2streets/src/operations/find_cycle.rs +++ b/osm2streets/src/operations/find_cycle.rs @@ -13,53 +13,27 @@ enum Step { impl StreetNetwork { pub fn find_cycle(&self, start: IntersectionID) -> Result { - let mut backrefs: HashMap = HashMap::new(); - // Never cross the same road twice - let mut visited: HashSet = HashSet::new(); - // This is a max-heap, so negate all distances. Tie breaker is arbitrary but deterministic. - // Track where we're going and how we got there. - let mut queue: BinaryHeap<(Distance, IntersectionID, Option)> = BinaryHeap::new(); - queue.push((Distance::ZERO, start, None)); + // TODO Or even simpler... always try to go CW or CCW consistently. we dont want to + // backtrack ever. - while !queue.is_empty() { - let (dist_so_far, current, via_road) = queue.pop().unwrap(); - if let Some(via) = via_road { - if visited.contains(&via) { - continue; - } - visited.insert(via); - backrefs.insert(current, via); - } - info!("Current step: {current} with dist {dist_so_far} via {:?}", via_road); + let mut stack = vec![start]; + let mut steps = Vec::new(); + let mut visited = HashSet::new(); - if current == start && dist_so_far != Distance::ZERO { - info!(" found cycle"); - let mut steps = vec![Step::Node(current)]; - let mut current = current; - loop { - if current == start && steps.len() > 1 { - /*steps.pop(); - steps.reverse();*/ - return render(self, steps); - } - let road = backrefs[¤t]; - current = self.roads[&road].other_side(current); - steps.push(Step::Edge(road)); - steps.push(Step::Node(current)); + while let Some(current) = stack.pop() { + if visited.contains(¤t) { + if current == start && steps.len() > 1 { + return render(self, steps); } + + continue; } + visited.insert(current); + steps.push(Step::Node(current)); - // when on i12, we skip over going to i17. when does it wind up in backrefs? for road in &self.intersections[¤t].roads { let next_i = self.roads[road].other_side(current); - if let Entry::Vacant(e) = backrefs.entry(next_i) { - // DONT do this yet - //e.insert(*road); - // Remember to keep things negative - let dist = dist_so_far - self.roads[road].center_line.length(); - queue.push((dist, next_i, Some(*road))); - info!(" Havent been to {next_i} yet, so go there via {road}. dist will be {dist}"); - } + stack.push(next_i); } } diff --git a/web/src/common/layers/RenderIntersectionPolygons.svelte b/web/src/common/layers/RenderIntersectionPolygons.svelte index fe8b5129..6ea7cbbf 100644 --- a/web/src/common/layers/RenderIntersectionPolygons.svelte +++ b/web/src/common/layers/RenderIntersectionPolygons.svelte @@ -2,7 +2,12 @@ import LayerControls from "../LayerControls.svelte"; import { theme, hoveredIntersection, network } from "../store"; import { caseHelper, layerId, emptyGeojson } from "../utils"; - import { SymbolLayer, hoverStateFilter, FillLayer, GeoJSON } from "svelte-maplibre"; + import { + SymbolLayer, + hoverStateFilter, + FillLayer, + GeoJSON, + } from "svelte-maplibre"; export let hoverCursor: string | undefined = undefined; @@ -42,10 +47,10 @@ From 82200fa027c14bd5b6df95beac740f1cae3ddb82 Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Sat, 23 Mar 2024 13:54:16 +0000 Subject: [PATCH 05/15] just walk CW or CCW --- osm2streets/src/operations/find_cycle.rs | 59 +++++++++++-------- .../layers/RenderIntersectionPolygons.svelte | 4 +- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/osm2streets/src/operations/find_cycle.rs b/osm2streets/src/operations/find_cycle.rs index 9eab01f3..fcae9e32 100644 --- a/osm2streets/src/operations/find_cycle.rs +++ b/osm2streets/src/operations/find_cycle.rs @@ -1,8 +1,5 @@ -use std::collections::{hash_map::Entry, BinaryHeap, HashMap, HashSet}; - use anyhow::Result; use geojson::Feature; -use geom::Distance; use crate::{IntersectionID, RoadID, StreetNetwork}; @@ -13,31 +10,47 @@ enum Step { impl StreetNetwork { pub fn find_cycle(&self, start: IntersectionID) -> Result { - // TODO Or even simpler... always try to go CW or CCW consistently. we dont want to - // backtrack ever. + let steps_cw = self.walk_around(start, true); + let steps_ccw = self.walk_around(start, false); + // Use the shorter + if steps_cw.len() < steps_ccw.len() { + render(self, steps_cw) + } else { + render(self, steps_ccw) + } + } - let mut stack = vec![start]; - let mut steps = Vec::new(); - let mut visited = HashSet::new(); + fn walk_around(&self, start: IntersectionID, clockwise: bool) -> Vec { + let mut current_i = start; + // Start arbitrarily + let mut current_r = self.intersections[&start].roads[0]; - while let Some(current) = stack.pop() { - if visited.contains(¤t) { - if current == start && steps.len() > 1 { - return render(self, steps); - } + let mut steps = vec![Step::Edge(current_r)]; - continue; - } - visited.insert(current); - steps.push(Step::Node(current)); - - for road in &self.intersections[¤t].roads { - let next_i = self.roads[road].other_side(current); - stack.push(next_i); - } + while current_i != start || steps.len() < 2 { + let next_i = &self.intersections[&self.roads[¤t_r].other_side(current_i)]; + let idx = next_i.roads.iter().position(|x| *x == current_r).unwrap(); + let next_idx = if clockwise { + if idx == next_i.roads.len() - 1 { + 0 + } else { + idx + 1 + } + } else { + if idx == 0 { + next_i.roads.len() - 1 + } else { + idx - 1 + } + }; + let next_r = next_i.roads[next_idx]; + steps.push(Step::Node(next_i.id)); + steps.push(Step::Edge(next_r)); + current_i = next_i.id; + current_r = next_r; } - bail!("Something broke"); + steps } } diff --git a/web/src/common/layers/RenderIntersectionPolygons.svelte b/web/src/common/layers/RenderIntersectionPolygons.svelte index 6ea7cbbf..45a852b9 100644 --- a/web/src/common/layers/RenderIntersectionPolygons.svelte +++ b/web/src/common/layers/RenderIntersectionPolygons.svelte @@ -46,12 +46,12 @@ > - + />--> From 4ef6d5b79eeeb2e1d0d67eee4a2606dd77a7b05d Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Sun, 24 Mar 2024 13:10:02 +0000 Subject: [PATCH 06/15] Trace around the block, the imprecise simple way --- osm2streets/src/operations/find_cycle.rs | 42 ++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/osm2streets/src/operations/find_cycle.rs b/osm2streets/src/operations/find_cycle.rs index fcae9e32..16c251ac 100644 --- a/osm2streets/src/operations/find_cycle.rs +++ b/osm2streets/src/operations/find_cycle.rs @@ -1,8 +1,10 @@ use anyhow::Result; use geojson::Feature; +use geom::Ring; use crate::{IntersectionID, RoadID, StreetNetwork}; +#[derive(Clone, Copy)] enum Step { Node(IntersectionID), Edge(RoadID), @@ -14,9 +16,9 @@ impl StreetNetwork { let steps_ccw = self.walk_around(start, false); // Use the shorter if steps_cw.len() < steps_ccw.len() { - render(self, steps_cw) + trace_polygon(self, steps_cw) } else { - render(self, steps_ccw) + trace_polygon(self, steps_ccw) } } @@ -54,7 +56,41 @@ impl StreetNetwork { } } -fn render(streets: &StreetNetwork, steps: Vec) -> Result { +fn trace_polygon(streets: &StreetNetwork, steps: Vec) -> Result { + let mut pts = Vec::new(); + + // steps will begin and end with an edge + for pair in steps.windows(2) { + match (pair[0], pair[1]) { + (Step::Edge(r), Step::Node(i)) => { + let road = &streets.roads[&r]; + if road.dst_i == i { + pts.extend(road.reference_line.clone().into_points()); + } else { + pts.extend(road.reference_line.reversed().into_points()); + } + } + // Skip... unless for the last case? + (Step::Node(_), Step::Edge(_)) => {} + _ => unreachable!(), + } + } + + pts.push(pts[0]); + let polygon = Ring::deduping_new(pts)?.into_polygon(); + + let mut f = Feature::from(polygon.to_geojson(Some(&streets.gps_bounds))); + f.set_property("type", "block"); + let gj = geojson::GeoJson::from(geojson::FeatureCollection { + bbox: None, + features: vec![f], + foreign_members: None, + }); + let output = serde_json::to_string_pretty(&gj)?; + Ok(output) +} + +fn _debug_steps(streets: &StreetNetwork, steps: Vec) -> Result { let mut features = Vec::new(); info!("Cycle!"); From d1f14736d113ab78d78f26829f75e4a0cf656edd Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Sun, 24 Mar 2024 13:16:46 +0000 Subject: [PATCH 07/15] Trace the polygon and use road edges --- osm2streets/src/operations/find_cycle.rs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/osm2streets/src/operations/find_cycle.rs b/osm2streets/src/operations/find_cycle.rs index 16c251ac..9ecba139 100644 --- a/osm2streets/src/operations/find_cycle.rs +++ b/osm2streets/src/operations/find_cycle.rs @@ -16,9 +16,9 @@ impl StreetNetwork { let steps_ccw = self.walk_around(start, false); // Use the shorter if steps_cw.len() < steps_ccw.len() { - trace_polygon(self, steps_cw) + trace_polygon(self, steps_cw, true) } else { - trace_polygon(self, steps_ccw) + trace_polygon(self, steps_ccw, false) } } @@ -56,7 +56,8 @@ impl StreetNetwork { } } -fn trace_polygon(streets: &StreetNetwork, steps: Vec) -> Result { +fn trace_polygon(streets: &StreetNetwork, steps: Vec, clockwise: bool) -> Result { + let shift_dir = if clockwise { -1.0 } else { 1.0 }; let mut pts = Vec::new(); // steps will begin and end with an edge @@ -65,9 +66,18 @@ fn trace_polygon(streets: &StreetNetwork, steps: Vec) -> Result { (Step::Edge(r), Step::Node(i)) => { let road = &streets.roads[&r]; if road.dst_i == i { - pts.extend(road.reference_line.clone().into_points()); + pts.extend( + road.center_line + .shift_either_direction(shift_dir * road.half_width())? + .into_points(), + ); } else { - pts.extend(road.reference_line.reversed().into_points()); + pts.extend( + road.center_line + .reversed() + .shift_either_direction(shift_dir * road.half_width())? + .into_points(), + ); } } // Skip... unless for the last case? From e5432942559c02100e98b12f944de3afad44c5bf Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Sun, 24 Mar 2024 13:31:15 +0000 Subject: [PATCH 08/15] Refactor and rename stuff --- osm2streets-js/src/lib.rs | 10 +- osm2streets/src/block.rs | 147 ++++++++++++++++++ osm2streets/src/lib.rs | 1 + osm2streets/src/operations/find_cycle.rs | 140 ----------------- osm2streets/src/operations/mod.rs | 1 - web/src/common/utils.ts | 2 +- web/src/street-explorer/App.svelte | 4 +- .../street-explorer/IntersectionPopup.svelte | 8 +- ...{RenderCycle.svelte => RenderBlock.svelte} | 10 +- web/src/street-explorer/stores.ts | 2 +- 10 files changed, 168 insertions(+), 157 deletions(-) create mode 100644 osm2streets/src/block.rs delete mode 100644 osm2streets/src/operations/find_cycle.rs rename web/src/street-explorer/{RenderCycle.svelte => RenderBlock.svelte} (71%) diff --git a/osm2streets-js/src/lib.rs b/osm2streets-js/src/lib.rs index c45893f8..b1ddfde8 100644 --- a/osm2streets-js/src/lib.rs +++ b/osm2streets-js/src/lib.rs @@ -250,9 +250,13 @@ impl JsStreetNetwork { Ok(out) } - #[wasm_bindgen(js_name = findCycle)] - pub fn find_cycle(&self, intersection: usize) -> String { - self.inner.find_cycle(IntersectionID(intersection)).unwrap() + #[wasm_bindgen(js_name = findBlock)] + pub fn find_block(&self, intersection: usize) -> String { + self.inner + .find_block(IntersectionID(intersection)) + .unwrap() + .render_polygon(&self.inner) + .unwrap() } } diff --git a/osm2streets/src/block.rs b/osm2streets/src/block.rs new file mode 100644 index 00000000..d3594582 --- /dev/null +++ b/osm2streets/src/block.rs @@ -0,0 +1,147 @@ +use anyhow::Result; +use geojson::Feature; +use geom::{Polygon, Ring}; + +use crate::{IntersectionID, RoadID, StreetNetwork}; + +/// A "tight" cycle of roads and intersections, with a polygon capturing the negative space inside. +pub struct Block { + pub steps: Vec, + pub polygon: Polygon, +} +#[derive(Clone, Copy)] +pub enum Step { + Node(IntersectionID), + Edge(RoadID), +} + +impl StreetNetwork { + pub fn find_block(&self, start: IntersectionID) -> Result { + let steps_cw = self.walk_around(start, true); + let steps_ccw = self.walk_around(start, false); + // Use the shorter one + let (steps, clockwise) = if steps_cw.len() < steps_ccw.len() { + (steps_cw, true) + } else { + (steps_ccw, false) + }; + + // Trace the polygon + let shift_dir = if clockwise { -1.0 } else { 1.0 }; + let mut pts = Vec::new(); + + // steps will begin and end with an edge + for pair in steps.windows(2) { + match (pair[0], pair[1]) { + (Step::Edge(r), Step::Node(i)) => { + let road = &self.roads[&r]; + if road.dst_i == i { + pts.extend( + road.center_line + .shift_either_direction(shift_dir * road.half_width())? + .into_points(), + ); + } else { + pts.extend( + road.center_line + .reversed() + .shift_either_direction(shift_dir * road.half_width())? + .into_points(), + ); + } + } + // Skip... unless for the last case? + (Step::Node(_), Step::Edge(_)) => {} + _ => unreachable!(), + } + } + + pts.push(pts[0]); + let polygon = Ring::deduping_new(pts)?.into_polygon(); + + Ok(Block { steps, polygon }) + } + + fn walk_around(&self, start: IntersectionID, clockwise: bool) -> Vec { + let mut current_i = start; + // Start arbitrarily + let mut current_r = self.intersections[&start].roads[0]; + + let mut steps = vec![Step::Edge(current_r)]; + + while current_i != start || steps.len() < 2 { + let next_i = &self.intersections[&self.roads[¤t_r].other_side(current_i)]; + let idx = next_i.roads.iter().position(|x| *x == current_r).unwrap(); + let next_idx = if clockwise { + if idx == next_i.roads.len() - 1 { + 0 + } else { + idx + 1 + } + } else { + if idx == 0 { + next_i.roads.len() - 1 + } else { + idx - 1 + } + }; + let next_r = next_i.roads[next_idx]; + steps.push(Step::Node(next_i.id)); + steps.push(Step::Edge(next_r)); + current_i = next_i.id; + current_r = next_r; + } + + steps + } +} + +impl Block { + pub fn render_polygon(&self, streets: &StreetNetwork) -> Result { + let mut f = Feature::from(self.polygon.to_geojson(Some(&streets.gps_bounds))); + f.set_property("type", "block"); + let gj = geojson::GeoJson::from(geojson::FeatureCollection { + bbox: None, + features: vec![f], + foreign_members: None, + }); + let output = serde_json::to_string_pretty(&gj)?; + Ok(output) + } + + pub fn render_debug(&self, streets: &StreetNetwork) -> Result { + let mut features = Vec::new(); + + for step in &self.steps { + match step { + Step::Node(i) => { + let mut f = Feature::from( + streets.intersections[&i] + .polygon + .to_geojson(Some(&streets.gps_bounds)), + ); + f.set_property("type", "intersection"); + features.push(f); + } + Step::Edge(r) => { + let road = &streets.roads[&r]; + let mut f = Feature::from( + road.center_line + .make_polygons(road.total_width()) + .to_geojson(Some(&streets.gps_bounds)), + ); + f.set_property("type", "road"); + features.push(f); + } + } + } + + let gj = geojson::GeoJson::from(geojson::FeatureCollection { + bbox: None, + features, + foreign_members: None, + }); + let output = serde_json::to_string_pretty(&gj)?; + Ok(output) + } +} diff --git a/osm2streets/src/lib.rs b/osm2streets/src/lib.rs index 361e4980..345c728d 100644 --- a/osm2streets/src/lib.rs +++ b/osm2streets/src/lib.rs @@ -30,6 +30,7 @@ pub use osm2lanes::{ Placement, NORMAL_LANE_THICKNESS, SIDEWALK_THICKNESS, }; +mod block; mod geometry; mod ids; mod intersection; diff --git a/osm2streets/src/operations/find_cycle.rs b/osm2streets/src/operations/find_cycle.rs deleted file mode 100644 index 9ecba139..00000000 --- a/osm2streets/src/operations/find_cycle.rs +++ /dev/null @@ -1,140 +0,0 @@ -use anyhow::Result; -use geojson::Feature; -use geom::Ring; - -use crate::{IntersectionID, RoadID, StreetNetwork}; - -#[derive(Clone, Copy)] -enum Step { - Node(IntersectionID), - Edge(RoadID), -} - -impl StreetNetwork { - pub fn find_cycle(&self, start: IntersectionID) -> Result { - let steps_cw = self.walk_around(start, true); - let steps_ccw = self.walk_around(start, false); - // Use the shorter - if steps_cw.len() < steps_ccw.len() { - trace_polygon(self, steps_cw, true) - } else { - trace_polygon(self, steps_ccw, false) - } - } - - fn walk_around(&self, start: IntersectionID, clockwise: bool) -> Vec { - let mut current_i = start; - // Start arbitrarily - let mut current_r = self.intersections[&start].roads[0]; - - let mut steps = vec![Step::Edge(current_r)]; - - while current_i != start || steps.len() < 2 { - let next_i = &self.intersections[&self.roads[¤t_r].other_side(current_i)]; - let idx = next_i.roads.iter().position(|x| *x == current_r).unwrap(); - let next_idx = if clockwise { - if idx == next_i.roads.len() - 1 { - 0 - } else { - idx + 1 - } - } else { - if idx == 0 { - next_i.roads.len() - 1 - } else { - idx - 1 - } - }; - let next_r = next_i.roads[next_idx]; - steps.push(Step::Node(next_i.id)); - steps.push(Step::Edge(next_r)); - current_i = next_i.id; - current_r = next_r; - } - - steps - } -} - -fn trace_polygon(streets: &StreetNetwork, steps: Vec, clockwise: bool) -> Result { - let shift_dir = if clockwise { -1.0 } else { 1.0 }; - let mut pts = Vec::new(); - - // steps will begin and end with an edge - for pair in steps.windows(2) { - match (pair[0], pair[1]) { - (Step::Edge(r), Step::Node(i)) => { - let road = &streets.roads[&r]; - if road.dst_i == i { - pts.extend( - road.center_line - .shift_either_direction(shift_dir * road.half_width())? - .into_points(), - ); - } else { - pts.extend( - road.center_line - .reversed() - .shift_either_direction(shift_dir * road.half_width())? - .into_points(), - ); - } - } - // Skip... unless for the last case? - (Step::Node(_), Step::Edge(_)) => {} - _ => unreachable!(), - } - } - - pts.push(pts[0]); - let polygon = Ring::deduping_new(pts)?.into_polygon(); - - let mut f = Feature::from(polygon.to_geojson(Some(&streets.gps_bounds))); - f.set_property("type", "block"); - let gj = geojson::GeoJson::from(geojson::FeatureCollection { - bbox: None, - features: vec![f], - foreign_members: None, - }); - let output = serde_json::to_string_pretty(&gj)?; - Ok(output) -} - -fn _debug_steps(streets: &StreetNetwork, steps: Vec) -> Result { - let mut features = Vec::new(); - - info!("Cycle!"); - for step in steps { - match step { - Step::Node(i) => { - info!("- {i}"); - let mut f = Feature::from( - streets.intersections[&i] - .polygon - .to_geojson(Some(&streets.gps_bounds)), - ); - f.set_property("type", "intersection"); - features.push(f); - } - Step::Edge(r) => { - info!("- {r}"); - let road = &streets.roads[&r]; - let mut f = Feature::from( - road.center_line - .make_polygons(road.total_width()) - .to_geojson(Some(&streets.gps_bounds)), - ); - f.set_property("type", "road"); - features.push(f); - } - } - } - - let gj = geojson::GeoJson::from(geojson::FeatureCollection { - bbox: None, - features, - foreign_members: None, - }); - let output = serde_json::to_string_pretty(&gj)?; - Ok(output) -} diff --git a/osm2streets/src/operations/mod.rs b/osm2streets/src/operations/mod.rs index 60434c80..6c9dba2b 100644 --- a/osm2streets/src/operations/mod.rs +++ b/osm2streets/src/operations/mod.rs @@ -2,6 +2,5 @@ mod collapse_intersection; mod collapse_short_road; -mod find_cycle; mod update_geometry; pub mod zip_sidepath; diff --git a/web/src/common/utils.ts b/web/src/common/utils.ts index 7b4a8da4..4afff6c0 100644 --- a/web/src/common/utils.ts +++ b/web/src/common/utils.ts @@ -118,7 +118,7 @@ const layerZorder = [ // Street Explorer only "movements", "connected-roads", - "cycle", + "block", // TODO This is specific to lane-editor. Figure out how different apps can // build on top of the common common ordering. diff --git a/web/src/street-explorer/App.svelte b/web/src/street-explorer/App.svelte index 8707cde5..07f2dce7 100644 --- a/web/src/street-explorer/App.svelte +++ b/web/src/street-explorer/App.svelte @@ -3,7 +3,7 @@ import init from "osm2streets-js"; import { onMount } from "svelte"; import AppSwitcher from "../AppSwitcher.svelte"; - import RenderCycle from "./RenderCycle.svelte"; + import RenderBlock from "./RenderBlock.svelte"; import { StreetView, ThemePicker, @@ -93,7 +93,7 @@ - +
diff --git a/web/src/street-explorer/IntersectionPopup.svelte b/web/src/street-explorer/IntersectionPopup.svelte index 2771ce94..eb20106e 100644 --- a/web/src/street-explorer/IntersectionPopup.svelte +++ b/web/src/street-explorer/IntersectionPopup.svelte @@ -2,7 +2,7 @@ import type { Polygon } from "geojson"; import type { FeatureWithProps } from "../common/utils"; import { network } from "../common"; - import { cycleGj } from "./stores"; + import { blockGj } from "./stores"; // Note the input is maplibre's GeoJSONFeature, which stringifies nested properties export let data: FeatureWithProps | undefined; @@ -16,8 +16,8 @@ close(); } - function findCycle() { - cycleGj.set(JSON.parse($network!.findCycle(props.id))); + function findBlock() { + blockGj.set(JSON.parse($network!.findBlock(props.id))); close(); } @@ -44,5 +44,5 @@
- +
diff --git a/web/src/street-explorer/RenderCycle.svelte b/web/src/street-explorer/RenderBlock.svelte similarity index 71% rename from web/src/street-explorer/RenderCycle.svelte rename to web/src/street-explorer/RenderBlock.svelte index 4462abae..893bcd64 100644 --- a/web/src/street-explorer/RenderCycle.svelte +++ b/web/src/street-explorer/RenderBlock.svelte @@ -1,16 +1,16 @@ - +
- Cycle + Block
diff --git a/web/src/street-explorer/stores.ts b/web/src/street-explorer/stores.ts index 22eddc31..7f7a2320 100644 --- a/web/src/street-explorer/stores.ts +++ b/web/src/street-explorer/stores.ts @@ -2,4 +2,4 @@ import type { FeatureCollection } from "geojson"; import { writable, type Writable } from "svelte/store"; import { emptyGeojson } from "../common/utils"; -export const cycleGj: Writable = writable(emptyGeojson()); +export const blockGj: Writable = writable(emptyGeojson()); From 39e7741dfc2f8aa1760a212d47d8b8161a170162 Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Sun, 24 Mar 2024 13:44:52 +0000 Subject: [PATCH 09/15] Find all blocks... well, many of them --- osm2streets-js/src/lib.rs | 5 ++ osm2streets/src/block.rs | 67 ++++++++++++++----- .../street-explorer/IntersectionPopup.svelte | 6 +- web/src/street-explorer/RenderBlock.svelte | 6 ++ 4 files changed, 68 insertions(+), 16 deletions(-) diff --git a/osm2streets-js/src/lib.rs b/osm2streets-js/src/lib.rs index b1ddfde8..328dfbd8 100644 --- a/osm2streets-js/src/lib.rs +++ b/osm2streets-js/src/lib.rs @@ -258,6 +258,11 @@ impl JsStreetNetwork { .render_polygon(&self.inner) .unwrap() } + + #[wasm_bindgen(js_name = findAllBlocks)] + pub fn find_all_blocks(&self) -> String { + self.inner.find_all_blocks().unwrap() + } } // Mutations diff --git a/osm2streets/src/block.rs b/osm2streets/src/block.rs index d3594582..5ff23468 100644 --- a/osm2streets/src/block.rs +++ b/osm2streets/src/block.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use anyhow::Result; use geojson::Feature; use geom::{Polygon, Ring}; @@ -20,11 +22,14 @@ impl StreetNetwork { let steps_cw = self.walk_around(start, true); let steps_ccw = self.walk_around(start, false); // Use the shorter one - let (steps, clockwise) = if steps_cw.len() < steps_ccw.len() { + let (steps, clockwise) = if steps_cw.len() < steps_ccw.len() && !steps_cw.is_empty() { (steps_cw, true) } else { (steps_ccw, false) }; + if steps.is_empty() { + bail!("Found a dead-end"); + } // Trace the polygon let shift_dir = if clockwise { -1.0 } else { 1.0 }; @@ -62,6 +67,35 @@ impl StreetNetwork { Ok(Block { steps, polygon }) } + pub fn find_all_blocks(&self) -> Result { + // TODO We should track by side of the road (but then need a way to start there) + let mut visited_intersections = HashSet::new(); + let mut blocks = Vec::new(); + + for i in self.intersections.keys() { + if visited_intersections.contains(i) { + continue; + } + if let Ok(block) = self.find_block(*i) { + for step in &block.steps { + if let Step::Node(i) = step { + visited_intersections.insert(*i); + } + } + blocks.push(block); + } + } + + let mut features = Vec::new(); + for block in blocks { + let mut f = Feature::from(block.polygon.to_geojson(Some(&self.gps_bounds))); + f.set_property("type", "block"); + features.push(f); + } + serialize_features(features) + } + + // Returns an empty Vec for failures (hitting a dead-end) fn walk_around(&self, start: IntersectionID, clockwise: bool) -> Vec { let mut current_i = start; // Start arbitrarily @@ -70,6 +104,11 @@ impl StreetNetwork { let mut steps = vec![Step::Edge(current_r)]; while current_i != start || steps.len() < 2 { + // Fail for dead-ends (for now, to avoid tracing around the entire clipped map) + if self.intersections[¤t_i].roads.len() == 1 { + return Vec::new(); + } + let next_i = &self.intersections[&self.roads[¤t_r].other_side(current_i)]; let idx = next_i.roads.iter().position(|x| *x == current_r).unwrap(); let next_idx = if clockwise { @@ -100,13 +139,7 @@ impl Block { pub fn render_polygon(&self, streets: &StreetNetwork) -> Result { let mut f = Feature::from(self.polygon.to_geojson(Some(&streets.gps_bounds))); f.set_property("type", "block"); - let gj = geojson::GeoJson::from(geojson::FeatureCollection { - bbox: None, - features: vec![f], - foreign_members: None, - }); - let output = serde_json::to_string_pretty(&gj)?; - Ok(output) + serialize_features(vec![f]) } pub fn render_debug(&self, streets: &StreetNetwork) -> Result { @@ -136,12 +169,16 @@ impl Block { } } - let gj = geojson::GeoJson::from(geojson::FeatureCollection { - bbox: None, - features, - foreign_members: None, - }); - let output = serde_json::to_string_pretty(&gj)?; - Ok(output) + serialize_features(features) } } + +fn serialize_features(features: Vec) -> Result { + let gj = geojson::GeoJson::from(geojson::FeatureCollection { + bbox: None, + features, + foreign_members: None, + }); + let output = serde_json::to_string_pretty(&gj)?; + Ok(output) +} diff --git a/web/src/street-explorer/IntersectionPopup.svelte b/web/src/street-explorer/IntersectionPopup.svelte index eb20106e..b44cbcaf 100644 --- a/web/src/street-explorer/IntersectionPopup.svelte +++ b/web/src/street-explorer/IntersectionPopup.svelte @@ -17,7 +17,11 @@ } function findBlock() { - blockGj.set(JSON.parse($network!.findBlock(props.id))); + try { + blockGj.set(JSON.parse($network!.findBlock(props.id))); + } catch (err) { + window.alert(err); + } close(); } diff --git a/web/src/street-explorer/RenderBlock.svelte b/web/src/street-explorer/RenderBlock.svelte index 893bcd64..3170c518 100644 --- a/web/src/street-explorer/RenderBlock.svelte +++ b/web/src/street-explorer/RenderBlock.svelte @@ -2,10 +2,15 @@ import { layerId, emptyGeojson } from "../common/utils"; import { Popup, FillLayer, GeoJSON } from "svelte-maplibre"; import { blockGj } from "./stores"; + import { network } from "../common"; function clear() { blockGj.set(emptyGeojson()); } + + function findAll() { + blockGj.set(JSON.parse($network!.findAllBlocks())); + } @@ -24,4 +29,5 @@
Block +
From 7b07d144943853910ee22ebba191d052217ada2e Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Sun, 24 Mar 2024 14:01:49 +0000 Subject: [PATCH 10/15] Start to classify blocks --- osm2streets/src/block.rs | 202 +++++++++++++-------- web/src/street-explorer/RenderBlock.svelte | 12 +- 2 files changed, 138 insertions(+), 76 deletions(-) diff --git a/osm2streets/src/block.rs b/osm2streets/src/block.rs index 5ff23468..d8d51f2f 100644 --- a/osm2streets/src/block.rs +++ b/osm2streets/src/block.rs @@ -4,23 +4,36 @@ use anyhow::Result; use geojson::Feature; use geom::{Polygon, Ring}; -use crate::{IntersectionID, RoadID, StreetNetwork}; +use crate::{IntersectionID, LaneType, RoadID, StreetNetwork}; /// A "tight" cycle of roads and intersections, with a polygon capturing the negative space inside. pub struct Block { + pub kind: BlockKind, pub steps: Vec, pub polygon: Polygon, } + #[derive(Clone, Copy)] pub enum Step { Node(IntersectionID), Edge(RoadID), } +#[derive(Debug)] +pub enum BlockKind { + /// The space between a road and sidewalk. It might be a wide sidewalk or contain grass. + RoadAndSidewalk, + /// The space between one-way roads. Probably has some kind of physical barrier, not just + /// markings. + DualCarriageway, + Unknown, +} + impl StreetNetwork { pub fn find_block(&self, start: IntersectionID) -> Result { - let steps_cw = self.walk_around(start, true); - let steps_ccw = self.walk_around(start, false); + let steps_cw = walk_around(self, start, true); + let steps_ccw = walk_around(self, start, false); + // Use the shorter one let (steps, clockwise) = if steps_cw.len() < steps_ccw.len() && !steps_cw.is_empty() { (steps_cw, true) @@ -31,40 +44,15 @@ impl StreetNetwork { bail!("Found a dead-end"); } - // Trace the polygon - let shift_dir = if clockwise { -1.0 } else { 1.0 }; - let mut pts = Vec::new(); - - // steps will begin and end with an edge - for pair in steps.windows(2) { - match (pair[0], pair[1]) { - (Step::Edge(r), Step::Node(i)) => { - let road = &self.roads[&r]; - if road.dst_i == i { - pts.extend( - road.center_line - .shift_either_direction(shift_dir * road.half_width())? - .into_points(), - ); - } else { - pts.extend( - road.center_line - .reversed() - .shift_either_direction(shift_dir * road.half_width())? - .into_points(), - ); - } - } - // Skip... unless for the last case? - (Step::Node(_), Step::Edge(_)) => {} - _ => unreachable!(), - } - } + let polygon = trace_polygon(self, &steps, clockwise)?; - pts.push(pts[0]); - let polygon = Ring::deduping_new(pts)?.into_polygon(); + let kind = classify(self, &steps); - Ok(Block { steps, polygon }) + Ok(Block { + kind, + steps, + polygon, + }) } pub fn find_all_blocks(&self) -> Result { @@ -90,55 +78,18 @@ impl StreetNetwork { for block in blocks { let mut f = Feature::from(block.polygon.to_geojson(Some(&self.gps_bounds))); f.set_property("type", "block"); + f.set_property("kind", format!("{:?}", block.kind)); features.push(f); } serialize_features(features) } - - // Returns an empty Vec for failures (hitting a dead-end) - fn walk_around(&self, start: IntersectionID, clockwise: bool) -> Vec { - let mut current_i = start; - // Start arbitrarily - let mut current_r = self.intersections[&start].roads[0]; - - let mut steps = vec![Step::Edge(current_r)]; - - while current_i != start || steps.len() < 2 { - // Fail for dead-ends (for now, to avoid tracing around the entire clipped map) - if self.intersections[¤t_i].roads.len() == 1 { - return Vec::new(); - } - - let next_i = &self.intersections[&self.roads[¤t_r].other_side(current_i)]; - let idx = next_i.roads.iter().position(|x| *x == current_r).unwrap(); - let next_idx = if clockwise { - if idx == next_i.roads.len() - 1 { - 0 - } else { - idx + 1 - } - } else { - if idx == 0 { - next_i.roads.len() - 1 - } else { - idx - 1 - } - }; - let next_r = next_i.roads[next_idx]; - steps.push(Step::Node(next_i.id)); - steps.push(Step::Edge(next_r)); - current_i = next_i.id; - current_r = next_r; - } - - steps - } } impl Block { pub fn render_polygon(&self, streets: &StreetNetwork) -> Result { let mut f = Feature::from(self.polygon.to_geojson(Some(&streets.gps_bounds))); f.set_property("type", "block"); + f.set_property("kind", format!("{:?}", self.kind)); serialize_features(vec![f]) } @@ -173,6 +124,109 @@ impl Block { } } +// Returns an empty Vec for failures (hitting a dead-end) +fn walk_around(streets: &StreetNetwork, start: IntersectionID, clockwise: bool) -> Vec { + let mut current_i = start; + // Start arbitrarily + let mut current_r = streets.intersections[&start].roads[0]; + + let mut steps = vec![Step::Edge(current_r)]; + + while current_i != start || steps.len() < 2 { + // Fail for dead-ends (for now, to avoid tracing around the entire clipped map) + if streets.intersections[¤t_i].roads.len() == 1 { + return Vec::new(); + } + + let next_i = &streets.intersections[&streets.roads[¤t_r].other_side(current_i)]; + let idx = next_i.roads.iter().position(|x| *x == current_r).unwrap(); + let next_idx = if clockwise { + if idx == next_i.roads.len() - 1 { + 0 + } else { + idx + 1 + } + } else { + if idx == 0 { + next_i.roads.len() - 1 + } else { + idx - 1 + } + }; + let next_r = next_i.roads[next_idx]; + steps.push(Step::Node(next_i.id)); + steps.push(Step::Edge(next_r)); + current_i = next_i.id; + current_r = next_r; + } + + steps +} + +fn trace_polygon(streets: &StreetNetwork, steps: &Vec, clockwise: bool) -> Result { + let shift_dir = if clockwise { -1.0 } else { 1.0 }; + let mut pts = Vec::new(); + + // steps will begin and end with an edge + for pair in steps.windows(2) { + match (pair[0], pair[1]) { + (Step::Edge(r), Step::Node(i)) => { + let road = &streets.roads[&r]; + if road.dst_i == i { + pts.extend( + road.center_line + .shift_either_direction(shift_dir * road.half_width())? + .into_points(), + ); + } else { + pts.extend( + road.center_line + .reversed() + .shift_either_direction(shift_dir * road.half_width())? + .into_points(), + ); + } + } + // Skip... unless for the last case? + (Step::Node(_), Step::Edge(_)) => {} + _ => unreachable!(), + } + } + + pts.push(pts[0]); + Ok(Ring::deduping_new(pts)?.into_polygon()) +} + +fn classify(streets: &StreetNetwork, steps: &Vec) -> BlockKind { + let mut has_road = false; + let mut has_sidewalk = false; + + for step in steps { + if let Step::Edge(r) = step { + let road = &streets.roads[r]; + if road.is_driveable() { + // TODO Or bus lanes? + has_road = true; + } else if road.lane_specs_ltr.len() == 1 + && road.lane_specs_ltr[0].lt == LaneType::Sidewalk + { + has_sidewalk = true; + } + } + } + + if has_road && has_sidewalk { + return BlockKind::RoadAndSidewalk; + } + if has_road { + // TODO Insist on one-ways pointing the opposite direction? What about different types of + // small connector roads? + return BlockKind::DualCarriageway; + } + + BlockKind::Unknown +} + fn serialize_features(features: Vec) -> Result { let gj = geojson::GeoJson::from(geojson::FeatureCollection { bbox: None, diff --git a/web/src/street-explorer/RenderBlock.svelte b/web/src/street-explorer/RenderBlock.svelte index 3170c518..0321349f 100644 --- a/web/src/street-explorer/RenderBlock.svelte +++ b/web/src/street-explorer/RenderBlock.svelte @@ -1,5 +1,5 @@

Intersection #{props.id}

@@ -48,5 +38,4 @@
-
diff --git a/web/src/street-explorer/LanePopup.svelte b/web/src/street-explorer/LanePopup.svelte index a1e1774a..a13437c9 100644 --- a/web/src/street-explorer/LanePopup.svelte +++ b/web/src/street-explorer/LanePopup.svelte @@ -2,6 +2,7 @@ import type { Polygon } from "geojson"; import type { FeatureWithProps } from "../common/utils"; import { network } from "../common"; + import { blockGj } from "./stores"; // Note the input is maplibre's GeoJSONFeature, which stringifies nested properties export let data: FeatureWithProps | undefined; @@ -22,6 +23,15 @@ $network = $network; close(); } + + function findBlock(left: boolean) { + try { + blockGj.set(JSON.parse($network!.findBlock(props.road, left))); + } catch (err) { + window.alert(err); + } + close(); + }

Lane {props.index} of Road {props.road}

@@ -62,6 +72,14 @@ +
+ + +
diff --git a/web/src/common/index.ts b/web/src/common/index.ts index 7b40d02d..5ecf453c 100644 --- a/web/src/common/index.ts +++ b/web/src/common/index.ts @@ -1,6 +1,7 @@ export { default as BasemapPicker } from "./BasemapPicker.svelte"; export { default as Geocoder } from "./Geocoder.svelte"; export { default as Layout } from "./Layout.svelte"; +export { default as Legend } from "./Legend.svelte"; export { default as Map } from "./Map.svelte"; export { default as StreetView } from "./StreetView.svelte"; export { default as ThemePicker } from "./ThemePicker.svelte"; diff --git a/web/src/street-explorer/App.svelte b/web/src/street-explorer/App.svelte index 99886561..d8083e07 100644 --- a/web/src/street-explorer/App.svelte +++ b/web/src/street-explorer/App.svelte @@ -91,11 +91,14 @@
+ + +
+ -
diff --git a/web/src/street-explorer/RenderBlock.svelte b/web/src/street-explorer/RenderBlock.svelte index 0321349f..2853bc12 100644 --- a/web/src/street-explorer/RenderBlock.svelte +++ b/web/src/street-explorer/RenderBlock.svelte @@ -2,7 +2,9 @@ import { caseHelper, layerId, emptyGeojson } from "../common/utils"; import { Popup, FillLayer, GeoJSON } from "svelte-maplibre"; import { blockGj } from "./stores"; - import { network } from "../common"; + import { network, Legend } from "../common"; + + $: active = $blockGj.features.length > 0; function clear() { blockGj.set(emptyGeojson()); @@ -11,31 +13,35 @@ function findAll() { blockGj.set(JSON.parse($network!.findAllBlocks())); } + + let colors = { + RoadAndSidewalk: "green", + RoadAndCycleLane: "orange", + CycleLaneAndSidewalk: "yellow", + DualCarriageway: "purple", + Unknown: "blue", + }; -
{JSON.stringify(data.properties, null, "  ")}
+

{data.properties.kind}

- Block + Blocks +
+{#if active} + +{/if}