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

Start modelling movements at intersections #93

Merged
merged 5 commits into from
Jan 22, 2025
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
2 changes: 1 addition & 1 deletion backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

120 changes: 110 additions & 10 deletions backend/src/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ struct Osm {
railways: Vec<LineString>,
waterways: Vec<LineString>,
barrier_nodes: BTreeSet<NodeID>,
// Only represent one case of restricted turns (from, to) on a particular node
turn_restrictions: HashMap<NodeID, Vec<(WayID, WayID)>>,
}

impl OsmReader for Osm {
Expand Down Expand Up @@ -74,6 +76,55 @@ impl OsmReader for Osm {
}
}
}

// https://wiki.openstreetmap.org/wiki/Relation:restriction describes many cases. Only handle
// the simplest: a banned turn involving exactly 2 ways and 1 node.
if tags.is("type", "restriction")
&& tags.is_any(
"restriction",
vec![
"no_right_turn",
"no_left_turn",
"no_u_turn",
"no_straight_on",
],
)
{
let mut from = None;
let mut via = None;
let mut to = None;
for (role, member) in members {
match member {
OsmID::Way(w) => {
if role == "from" && from.is_none() {
from = Some(*w);
} else if role == "to" && to.is_none() {
to = Some(*w);
} else {
// Some other case, bail out
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not very familiar with this logic yet, but should these be continue?

}
}
OsmID::Node(n) => {
if role == "via" && via.is_none() {
via = Some(*n);
} else {
return;
}
}
OsmID::Relation(_) => {
return;
}
}
}

if let (Some(from), Some(via), Some(to)) = (from, via, to) {
self.turn_restrictions
.entry(via)
.or_insert_with(Vec::new)
.push((from, to));
}
}
}
}

Expand All @@ -96,6 +147,7 @@ pub fn create_from_osm(
point: i.point,
node: i.osm_node,
roads: i.edges.into_iter().map(|e| RoadID(e.0)).collect(),
turn_restrictions: Vec::new(),
})
.collect();

Expand Down Expand Up @@ -172,10 +224,40 @@ pub fn create_from_osm(
};
map.impact = Some(Impact::new(&map));

let graph = GraphSubset {
node_to_edge: graph.node_to_edge,
node_to_pt: graph.node_to_pt,
};

apply_existing_filters(&mut map, osm.barrier_nodes, &graph);
apply_turn_restrictions(&mut map, osm.turn_restrictions);

let main_road_penalty = 1.0;
map.router_before = Some(Router::new(
&map.roads,
&map.modal_filters,
&map.directions,
main_road_penalty,
));

Ok(map)
}

// Handles a partial borrow of Graph
struct GraphSubset {
node_to_edge: HashMap<NodeID, EdgeID>,
node_to_pt: HashMap<NodeID, Coord>,
}

fn apply_existing_filters(
map: &mut MapModel,
barrier_nodes: BTreeSet<NodeID>,
graph: &GraphSubset,
) {
// TODO Batch some or all of these initial edits?

// Apply barriers on any surviving edges. RoadID and osm2graph::EdgeID are the same.
for node in osm.barrier_nodes {
for node in barrier_nodes {
// If there's no surviving edge, then it was a barrier on something we don't consider a
// road or on a road that was removed
let Some(edge) = graph.node_to_edge.get(&node) else {
Expand All @@ -186,7 +268,7 @@ pub fn create_from_osm(
map.add_modal_filter(pt, Some(vec![RoadID(edge.0)]), FilterKind::NoEntry);
}

// Look for roads tagged with restrictions
// Look for roads tagged with access restrictions
let pedestrian_roads: BTreeSet<RoadID> = map
.roads
.iter()
Expand Down Expand Up @@ -257,16 +339,34 @@ pub fn create_from_osm(
map.original_modal_filters = map.modal_filters.clone();
map.undo_stack.clear();
map.redo_queue.clear();
}

let main_road_penalty = 1.0;
map.router_before = Some(Router::new(
&map.roads,
&map.modal_filters,
&map.directions,
main_road_penalty,
));
fn apply_turn_restrictions(
map: &mut MapModel,
mut turn_restrictions: HashMap<NodeID, Vec<(WayID, WayID)>>,
) {
for intersection in &mut map.intersections {
if let Some(list) = turn_restrictions.remove(&intersection.node) {
for (from_way, to_way) in list {
// One OSM way turns into multiple Roads. The restriction only makes sense on the
// Road connected to this intersection. So search only this intersection's roads.
let mut from = None;
let mut to = None;
for r in &intersection.roads {
let way = map.roads[r.0].way;
if way == from_way {
from = Some(*r);
} else if way == to_way {
to = Some(*r);
}
}

Ok(map)
if let (Some(from), Some(to)) = (from, to) {
intersection.turn_restrictions.push((from, to));
}
}
}
}
}

fn is_road(tags: &Tags) -> bool {
Expand Down
14 changes: 14 additions & 0 deletions backend/src/geo_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,20 @@ pub fn make_arrow(line: Line, thickness: f64, double_ended: bool) -> Option<Poly
Some(Polygon::new(LineString::new(pts), Vec::new()))
}

pub fn thicken_line(line: Line, thickness: f64) -> Polygon {
let angle = angle_of_line(line);
Polygon::new(
LineString::new(vec![
euclidean_destination_coord(line.start, angle - 90.0, thickness * 0.5),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should put this in geo... unfortunately I have a big open LineMeasure change in PR limbo that I'd like to resolve first... I'll go kick that to see if it's still alive. 🤔

euclidean_destination_coord(line.end, angle - 90.0, thickness * 0.5),
euclidean_destination_coord(line.end, angle + 90.0, thickness * 0.5),
euclidean_destination_coord(line.start, angle + 90.0, thickness * 0.5),
euclidean_destination_coord(line.start, angle - 90.0, thickness * 0.5),
]),
Vec::new(),
)
}

/// Create a polygon covering the world, minus a hole for the input polygon. Assumes the input is
/// in WGS84 and has no holes itself.
pub fn invert_polygon(wgs84_polygon: Polygon) -> Polygon {
Expand Down
7 changes: 6 additions & 1 deletion backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ mod create;
mod geo_helpers;
mod impact;
mod map_model;
mod movements;
mod neighbourhood;
mod render_cells;
mod route;
Expand Down Expand Up @@ -326,7 +327,11 @@ impl LTN {
self.map
.intersections
.iter()
.map(|i| self.map.mercator.to_wgs84_gj(&i.point))
.map(|i| {
let mut f = self.map.mercator.to_wgs84_gj(&i.point);
f.set_property("has_turn_restrictions", !i.turn_restrictions.is_empty());
f
})
.collect::<Vec<_>>(),
))
.map_err(err_to_js)?)
Expand Down
14 changes: 2 additions & 12 deletions backend/src/map_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,11 @@ pub struct Road {

pub struct Intersection {
pub id: IntersectionID,
#[allow(unused)]
pub node: osm_reader::NodeID,
pub point: Point,
pub roads: Vec<RoadID>,
/// (from, to) is not allowed. May be redundant with the road directions.
pub turn_restrictions: Vec<(RoadID, RoadID)>,
}

impl MapModel {
Expand Down Expand Up @@ -581,17 +582,6 @@ impl MapModel {
}
directions
}

pub fn get_movements(&self, i: IntersectionID) -> GeoJson {
let mut features = Vec::new();

// TODO Temporary
for r in &self.get_i(i).roads {
features.push(self.mercator.to_wgs84_gj(&self.get_r(*r).linestring));
}

GeoJson::from(features)
}
}

impl Road {
Expand Down
80 changes: 80 additions & 0 deletions backend/src/movements.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use geo::{Euclidean, Length, Line, LineInterpolatePoint, Point, Polygon};
use geojson::GeoJson;

use crate::{
geo_helpers::{make_arrow, thicken_line},
Direction, IntersectionID, MapModel, Road,
};

impl MapModel {
pub fn get_movements(&self, i: IntersectionID) -> GeoJson {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No biggie so take it or leave it: movement_arrows or movement_labels might be a better name, since these seem to be intended only as UI affordances, vs some business-logic struct.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To minimize churn, I'll leave this for now. Right now it's a debug mode, but that will probably turn into some kind of "edit turn restrictions at an intersection" mode, and the movements sent over will have a semantic ID that the backend knows how to edit.

let mut features = Vec::new();

let intersection = self.get_i(i);
for r1 in &intersection.roads {
for r2 in &intersection.roads {
// TODO Handle u-turns at dead-ends
if r1 == r2 {
continue;
}
let road1 = self.get_r(*r1);
let road2 = self.get_r(*r2);

// If road1 is one-way, can we go towards i?
let ok1 = match self.directions[r1] {
Direction::BothWays => true,
Direction::Forwards => road1.dst_i == i,
Direction::Backwards => road1.src_i == i,
};
// If road2 is one-way, can we go away from i?
let ok2 = match self.directions[r2] {
Direction::BothWays => true,
Direction::Forwards => road2.src_i == i,
Direction::Backwards => road2.dst_i == i,
};
if !ok1 || !ok2 {
continue;
}

// Is there a turn restriction between this pair?
if intersection.turn_restrictions.contains(&(*r1, *r2)) {
continue;
}

let polygon = render_arrow(i, road1, road2);
features.push(self.mercator.to_wgs84_gj(&polygon));
}
}

GeoJson::from(features)
}
}

fn render_arrow(i: IntersectionID, road1: &Road, road2: &Road) -> Polygon {
let line = Line::new(
pt_near_intersection(i, road1),
pt_near_intersection(i, road2),
);
let thickness = 2.0;
let double_ended = false;
make_arrow(line, thickness, double_ended).unwrap_or_else(|| thicken_line(line, thickness))
}

fn pt_near_intersection(i: IntersectionID, road: &Road) -> Point {
// If the road is long enough, offset from the intersection this much
let distance_away = 10.0;
let len = road.linestring.length::<Euclidean>();

if len > distance_away {
let pct = if road.src_i == i {
distance_away / len
} else {
1.0 - (distance_away / len)
};
return road.linestring.line_interpolate_point(pct).unwrap();
}

// If not, just take the other endpoint
let pct = if road.src_i == i { 1.0 } else { 0.0 };
road.linestring.line_interpolate_point(pct).unwrap()
}
Loading