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

More steps towards blocks and bundles #257

Merged
merged 4 commits into from
Mar 30, 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
4 changes: 2 additions & 2 deletions osm2streets-js/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,8 @@ impl JsStreetNetwork {
}

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

Expand Down
147 changes: 113 additions & 34 deletions osm2streets/src/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ pub struct Block {
pub kind: BlockKind,
pub steps: Vec<Step>,
pub polygon: Polygon,
/// Not counting the boundary (described by steps)
pub member_roads: HashSet<RoadID>,
pub member_intersections: HashSet<IntersectionID>,
}

#[derive(Clone, Copy)]
Expand All @@ -21,6 +24,9 @@ pub enum Step {

#[derive(Debug)]
pub enum BlockKind {
/// A "city" block; the space in between sidewals, probably just containing buildings and not
/// roads
LandUseBlock,
/// 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.
Expand All @@ -31,6 +37,10 @@ pub enum BlockKind {
/// The space between one-way roads. Probably has some kind of physical barrier, not just
/// markings.
DualCarriageway,
/// A segment of road and all sidepaths and internal connections
RoadBundle,
/// A possibly complex junction; everything in between all the crossings
IntersectionBundle,
Unknown,
}

Expand All @@ -41,26 +51,55 @@ impl StreetNetwork {
let clockwise = left;
let steps = walk_around(self, start, clockwise, sidewalks)?;
let polygon = trace_polygon(self, &steps, clockwise)?;
let kind = classify(self, &steps);

let mut member_roads = HashSet::new();
let mut member_intersections = HashSet::new();
if sidewalks {
// Look for roads inside the polygon geometrically
// TODO Slow; could cache an rtree
// TODO Incorrect near bridges/tunnels
for road in self.roads.values() {
if polygon.contains_pt(road.center_line.middle()) {
member_roads.insert(road.id);
}
}
for intersection in self.intersections.values() {
if polygon.contains_pt(intersection.polygon.center()) {
member_intersections.insert(intersection.id);
}
}
}
let kind = if sidewalks {
classify_bundle(self, &polygon, &member_roads, &member_intersections)
} else {
classify_block(self, &steps)
};

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

pub fn find_all_blocks(&self) -> Result<String> {
// TODO Messy API again
pub fn find_all_blocks(&self, sidewalks: bool) -> 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() {
if sidewalks && !self.roads[r].is_footway() {
continue;
}

for left in [true, false] {
if visited_roads.contains(&(*r, left)) {
continue;
}
if let Ok(block) = self.find_block(*r, left, false) {
if let Ok(block) = self.find_block(*r, left, sidewalks) {
// 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]) {
Expand Down Expand Up @@ -95,36 +134,33 @@ impl StreetNetwork {

impl Block {
pub fn render_polygon(&self, streets: &StreetNetwork) -> Result<String> {
let mut features = Vec::new();

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();
features.push(f);

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);
}
// Debugging
if false {
for r in &self.member_roads {
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", "member-road");
features.push(f);
}
for i in &self.member_intersections {
let mut f = Feature::from(
streets.intersections[i]
.polygon
.to_geojson(Some(&streets.gps_bounds)),
);
f.set_property("type", "member-intersection");
features.push(f);
}
}

Expand Down Expand Up @@ -191,10 +227,7 @@ fn filter_roads(
if !sidewalks {
return roads;
}
roads.retain(|r| {
let road = &streets.roads[r];
road.lane_specs_ltr.len() == 1 && road.lane_specs_ltr[0].lt.is_walkable()
});
roads.retain(|r| streets.roads[r].is_footway());
roads
}

Expand Down Expand Up @@ -232,7 +265,7 @@ fn trace_polygon(streets: &StreetNetwork, steps: &Vec<Step>, clockwise: bool) ->
Ok(Ring::deduping_new(pts)?.into_polygon())
}

fn classify(streets: &StreetNetwork, steps: &Vec<Step>) -> BlockKind {
fn classify_block(streets: &StreetNetwork, steps: &Vec<Step>) -> BlockKind {
let mut has_road = false;
let mut has_cycle_lane = false;
let mut has_sidewalk = false;
Expand All @@ -256,6 +289,7 @@ fn classify(streets: &StreetNetwork, steps: &Vec<Step>) -> BlockKind {
}

if has_road && has_sidewalk {
// TODO But ignore driveways and service roads?
return BlockKind::RoadAndSidewalk;
}
if has_road && has_cycle_lane {
Expand All @@ -270,10 +304,55 @@ fn classify(streets: &StreetNetwork, steps: &Vec<Step>) -> BlockKind {
if !has_road && has_cycle_lane && has_sidewalk {
return BlockKind::CycleLaneAndSidewalk;
}
if !has_road && !has_cycle_lane && has_sidewalk {
return BlockKind::LandUseBlock;
}

BlockKind::Unknown
}

fn classify_bundle(
streets: &StreetNetwork,
polygon: &Polygon,
member_roads: &HashSet<RoadID>,
member_intersections: &HashSet<IntersectionID>,
) -> BlockKind {
if member_intersections.is_empty() && member_roads.is_empty() {
return BlockKind::LandUseBlock;
}

// A bad heuristic: sum the intersection and road polygon area, and see which is greater
if false {
let mut road_area = 0.0;
for r in member_roads {
let road = &streets.roads[r];
road_area += road.center_line.make_polygons(road.total_width()).area();
}

let mut intersection_area = 0.0;
for i in member_intersections {
intersection_area += streets.intersections[i].polygon.area();
}

if road_area > intersection_area {
return BlockKind::RoadBundle;
} else {
return BlockKind::IntersectionBundle;
}
}

// TODO Check member road names and ignore service roads?

// See how "square" the block polygon is. Even if it's not axis-aligned, this sometimes works
let bounds = polygon.get_bounds();
let ratio = bounds.width() / bounds.height();
if ratio > 0.5 && ratio < 2.0 {
return BlockKind::IntersectionBundle;
} else {
return BlockKind::RoadBundle;
}
}

fn serialize_features(features: Vec<Feature>) -> Result<String> {
let gj = geojson::GeoJson::from(geojson::FeatureCollection {
bbox: None,
Expand Down
2 changes: 1 addition & 1 deletion osm2streets/src/render/intersection_markings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ fn get_crossing_line_and_min_width(
let mut roads = Vec::new();
for r in &intersection.roads {
let road = &streets.roads[r];
if road.lane_specs_ltr.len() == 1 && road.lane_specs_ltr[0].lt.is_walkable() {
if road.is_footway() {
let endpt = center_line_pointed_at(road, intersection).last_pt();
roads.push((road, endpt));
}
Expand Down
5 changes: 5 additions & 0 deletions osm2streets/src/road.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ impl Road {
.any(|spec| spec.lt == LaneType::Driving)
}

/// Is it only for walking?
pub fn is_footway(&self) -> bool {
self.lane_specs_ltr.len() == 1 && self.lane_specs_ltr[0].lt.is_walkable()
}

pub fn oneway_for_driving(&self) -> Option<Direction> {
LaneSpec::oneway_for_driving(&self.lane_specs_ltr)
}
Expand Down
1 change: 1 addition & 0 deletions web/src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ const layerZorder = [
"movements",
"connected-roads",
"block",
"block-debug",
"debug-ids",

// TODO This is specific to lane-editor. Figure out how different apps can
Expand Down
5 changes: 3 additions & 2 deletions web/src/street-explorer/LanePopup.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type { Polygon } from "geojson";
import type { FeatureWithProps } from "../common/utils";
import { network } from "../common";
import { blockGj } from "./stores";
import { blockGj, showingBundles } from "./stores";

// Note the input is maplibre's GeoJSONFeature, which stringifies nested properties
export let data: FeatureWithProps<Polygon> | undefined;
Expand All @@ -27,10 +27,11 @@
function findBlock(left: boolean, sidewalks: boolean) {
try {
blockGj.set(JSON.parse($network!.findBlock(props.road, left, sidewalks)));
showingBundles.set(sidewalks);
close();
} catch (err) {
window.alert(err);
}
close();
}
</script>

Expand Down
52 changes: 44 additions & 8 deletions web/src/street-explorer/RenderBlock.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
<script lang="ts">
import { caseHelper, layerId, emptyGeojson } from "../common/utils";
import { Popup, FillLayer, GeoJSON } from "svelte-maplibre";
import { blockGj } from "./stores";
import {
hoverStateFilter,
Popup,
LineLayer,
FillLayer,
GeoJSON,
} from "svelte-maplibre";
import { showingBundles, blockGj } from "./stores";
import { network, Legend } from "../common";

$: active = $blockGj.features.length > 0;
Expand All @@ -10,37 +16,67 @@
blockGj.set(emptyGeojson());
}

function findAll() {
blockGj.set(JSON.parse($network!.findAllBlocks()));
function findAll(sidewalks: boolean) {
blockGj.set(JSON.parse($network!.findAllBlocks(sidewalks)));
showingBundles.set(sidewalks);
}

let colors = {
let blockColors = {
LandUseBlock: "grey",
RoadAndSidewalk: "green",
RoadAndCycleLane: "orange",
CycleLaneAndSidewalk: "yellow",
DualCarriageway: "purple",
Unknown: "blue",
};
let bundleColors = {
LandUseBlock: "grey",
RoadBundle: "green",
IntersectionBundle: "orange",
};

$: colors = $showingBundles ? bundleColors : blockColors;
</script>

<GeoJSON data={$blockGj}>
<GeoJSON data={$blockGj} generateId>
<FillLayer
{...layerId("block")}
filter={["==", ["get", "type"], "block"]}
manageHoverState
paint={{
"fill-color": caseHelper("kind", colors, "red"),
"fill-opacity": 0.8,
"fill-opacity": hoverStateFilter(0.8, 0.4),
}}
>
<Popup openOn="hover" let:data>
<p>{data.properties.kind}</p>
</Popup>
</FillLayer>

<LineLayer
{...layerId("block-debug")}
filter={["!=", ["get", "type"], "block"]}
paint={{
"line-color": [
"case",
["==", ["get", "type"], "member-road"],
"red",
"black",
],
"line-width": 5,
}}
>
<Popup openOn="hover" let:data>
<pre>{JSON.stringify(data.properties, null, " ")}</pre>
</Popup>
</LineLayer>
</GeoJSON>

<div>
Blocks
<button on:click={clear} disabled={!active}>Clear</button>
<button on:click={findAll}>Find all</button>
<button on:click={() => findAll(false)}>Find all blocks</button>
<button on:click={() => findAll(true)}>Find all sidewalk bundles</button>
</div>
{#if active}
<Legend rows={Object.entries(colors)} />
Expand Down
2 changes: 2 additions & 0 deletions web/src/street-explorer/stores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { emptyGeojson } from "../common/utils";
import { network } from "../common";

export const blockGj: Writable<FeatureCollection> = writable(emptyGeojson());
// Does blockGj currently represent blocks or bundles?
export const showingBundles = writable(false);

// TODO Need to unsubscribe
// Unset when the network changes
Expand Down