Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interpreting negative space #250

Merged
merged 15 commits into from
Mar 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions osm2streets-js/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ impl JsStreetNetwork {
let clip_pts = if clip_pts_geojson.is_empty() {
None
} else {
let mut list = LonLat::parse_geojson_polygons(clip_pts_geojson.to_string())
.map_err(|err| JsValue::from_str(&err.to_string()))?;
let mut list =
LonLat::parse_geojson_polygons(clip_pts_geojson.to_string()).map_err(err_to_js)?;
if list.len() != 1 {
return Err(JsValue::from_str(&format!(
"{clip_pts_geojson} doesn't contain exactly one polygon"
Expand All @@ -66,7 +66,7 @@ impl JsStreetNetwork {
let mut timer = Timer::throwaway();
let (mut street_network, doc) =
streets_reader::osm_to_street_network(osm_input, clip_pts, cfg, &mut timer)
.map_err(|err| JsValue::from_str(&err.to_string()))?;
.map_err(err_to_js)?;
let mut transformations = Transformation::standard_for_clipped_areas();
if input.dual_carriageway_experiment {
// Collapsing short roads tries to touch "bridges," making debugging harder
Expand Down Expand Up @@ -249,6 +249,20 @@ impl JsStreetNetwork {
out.push_str("</way>");
Ok(out)
}

#[wasm_bindgen(js_name = findBlock)]
pub fn find_block(&self, road: usize, left: bool) -> Result<String, JsValue> {
self.inner
.find_block(RoadID(road), left)
.map_err(err_to_js)?
.render_polygon(&self.inner)
.map_err(err_to_js)
}

#[wasm_bindgen(js_name = findAllBlocks)]
pub fn find_all_blocks(&self) -> String {
self.inner.find_all_blocks().unwrap()
}
}

// Mutations
Expand Down Expand Up @@ -332,3 +346,7 @@ impl JsDebugStreets {
self.inner.to_debug_geojson()
}
}

fn err_to_js<E: std::fmt::Display>(err: E) -> JsValue {
JsValue::from_str(&err.to_string())
}
258 changes: 258 additions & 0 deletions osm2streets/src/block.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
use std::collections::HashSet;

use anyhow::Result;
use geojson::Feature;
use geom::{Polygon, Ring};

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<Step>,
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 a road and cycle lane. It should contain some kind of separation.
RoadAndCycleLane,
/// The space between a cycle lane and sidewalk. It should contain some kind of separation --
/// at least a curb.
CycleLaneAndSidewalk,
/// The space between one-way roads. Probably has some kind of physical barrier, not just
/// markings.
DualCarriageway,
Unknown,
}

impl StreetNetwork {
// Start at road's src_i
pub fn find_block(&self, start: RoadID, left: bool) -> Result<Block> {
// TODO ??
let clockwise = left;
let steps = walk_around(self, start, clockwise)?;
let polygon = trace_polygon(self, &steps, clockwise)?;
let kind = classify(self, &steps);

Ok(Block {
kind,
steps,
polygon,
})
}

pub fn find_all_blocks(&self) -> Result<String> {
// TODO consider a Left/Right enum instead of bool
let mut visited_roads: HashSet<(RoadID, bool)> = HashSet::new();
let mut blocks = Vec::new();

for r in self.roads.keys() {
for left in [true, false] {
if visited_roads.contains(&(*r, left)) {
continue;
}
if let Ok(block) = self.find_block(*r, left) {
// TODO Put more info in Step to avoid duplicating logic with trace_polygon
for pair in block.steps.windows(2) {
match (pair[0], pair[1]) {
(Step::Edge(r), Step::Node(i)) => {
let road = &self.roads[&r];
if road.dst_i == i {
visited_roads.insert((r, left));
} else {
visited_roads.insert((r, !left));
}
}
// Skip... unless for the last case?
(Step::Node(_), Step::Edge(_)) => {}
_ => unreachable!(),
}
}
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");
f.set_property("kind", format!("{:?}", block.kind));
features.push(f);
}
serialize_features(features)
}
}

impl Block {
pub fn render_polygon(&self, streets: &StreetNetwork) -> Result<String> {
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])
}

pub fn render_debug(&self, streets: &StreetNetwork) -> Result<String> {
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);
}
}
}

serialize_features(features)
}
}

fn walk_around(streets: &StreetNetwork, start_road: RoadID, clockwise: bool) -> Result<Vec<Step>> {
let start_i = streets.roads[&start_road].src_i;

let mut current_i = start_i;
let mut current_r = start_road;

let mut steps = vec![Step::Edge(current_r)];

while current_i != start_i || steps.len() < 2 {
// Fail for dead-ends (for now, to avoid tracing around the entire clipped map)
if streets.intersections[&current_i].roads.len() == 1 {
bail!("Found a dead-end at {current_i}");
}

let next_i = &streets.intersections[&streets.roads[&current_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;
}

Ok(steps)
}

fn trace_polygon(streets: &StreetNetwork, steps: &Vec<Step>, clockwise: bool) -> Result<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 = &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<Step>) -> BlockKind {
let mut has_road = false;
let mut has_cycle_lane = 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::Biking
{
has_cycle_lane = 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 && has_cycle_lane {
return BlockKind::RoadAndCycleLane;
}
if has_road {
// TODO Insist on one-ways pointing the opposite direction? What about different types of
// small connector roads?
return BlockKind::DualCarriageway;
}
// TODO This one is usually missed, because of a small piece of road crossing both
if !has_road && has_cycle_lane && has_sidewalk {
return BlockKind::CycleLaneAndSidewalk;
}

BlockKind::Unknown
}

fn serialize_features(features: Vec<Feature>) -> Result<String> {
let gj = geojson::GeoJson::from(geojson::FeatureCollection {
bbox: None,
features,
foreign_members: None,
});
let output = serde_json::to_string_pretty(&gj)?;
Ok(output)
}
1 change: 1 addition & 0 deletions osm2streets/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub use osm2lanes::{
Placement, NORMAL_LANE_THICKNESS, SIDEWALK_THICKNESS,
};

mod block;
mod geometry;
mod ids;
mod intersection;
Expand Down
24 changes: 24 additions & 0 deletions web/src/common/Legend.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<script lang="ts">
// (Label, color) pairs
export let rows: [string, string][];
</script>

<ul>
{#each rows as [label, color]}
<li>
<span style:background={color} />
{label}
</li>
{/each}
</ul>

<style>
span {
display: block;
float: left;
height: 16px;
width: 30px;
margin-right: 5px;
border: 1px solid #999;
}
</style>
1 change: 1 addition & 0 deletions web/src/common/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
31 changes: 31 additions & 0 deletions web/src/common/layers/DebugIDs.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts">
import LayerControls from "../LayerControls.svelte";
import { network } from "../store";
import { layerId, emptyGeojson } from "../utils";
import { SymbolLayer, GeoJSON } from "svelte-maplibre";

let show = false;

$: gj = $network ? JSON.parse($network.toGeojsonPlain()) : emptyGeojson();
</script>

<GeoJSON data={gj} generateId>
<SymbolLayer
{...layerId("debug-ids")}
layout={{
"text-field": ["get", "id"],
visibility: show ? "visible" : "none",
}}
paint={{
"text-halo-color": [
"case",
["==", ["get", "type"], "intersection"],
"red",
"cyan",
],
"text-halo-width": 3,
}}
/>
</GeoJSON>

<LayerControls {gj} name="Debug IDs" bind:show />
3 changes: 3 additions & 0 deletions web/src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,11 @@ const layerZorder = [
"lane-markings",
"intersection-markings",

// Street Explorer only
"movements",
"connected-roads",
"block",
"debug-ids",

// TODO This is specific to lane-editor. Figure out how different apps can
// build on top of the common common ordering.
Expand Down
Loading