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}