Skip to content

Commit

Permalink
Use an rtree to pick the roads for existing modal filters. The results
Browse files Browse the repository at this point in the history
slightly change in Strasbourg, but seem reasonable. The time drops from
4.1s to 0.9s.
  • Loading branch information
dabreegster committed Nov 27, 2024
1 parent ea47738 commit 0bd9ee4
Show file tree
Hide file tree
Showing 7 changed files with 47 additions and 17 deletions.
2 changes: 1 addition & 1 deletion backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ impl LTN {
x: pos.lng,
y: pos.lat,
}),
&self.neighbourhood.as_ref().unwrap().interior_roads,
Some(&self.neighbourhood.as_ref().unwrap().interior_roads),
FilterKind::from_string(&kind).unwrap(),
);
self.after_edit();
Expand Down
36 changes: 26 additions & 10 deletions backend/src/map_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use geo::{
LineString, Point, Polygon,
};
use geojson::{Feature, FeatureCollection, GeoJson};
use rstar::{primitives::GeomWithData, RTree, AABB};
use serde::Serialize;
use utils::{Mercator, Tags};

Expand All @@ -20,6 +21,7 @@ pub struct MapModel {
pub mercator: Mercator,
pub study_area_name: Option<String>,
pub boundary_polygon: Polygon,
pub closest_road: RTree<GeomWithData<LineString, RoadID>>,

// TODO Wasteful, can share some
// This is guaranteed to exist, only Option during MapModel::new internals
Expand Down Expand Up @@ -107,7 +109,7 @@ impl MapModel {
pub fn add_modal_filter(
&mut self,
pt: Coord,
candidate_roads: &BTreeSet<RoadID>,
candidate_roads: Option<&BTreeSet<RoadID>>,
kind: FilterKind,
) {
let cmd = self.do_edit(self.add_modal_filter_cmd(pt, candidate_roads, kind));
Expand All @@ -119,7 +121,7 @@ impl MapModel {
fn add_modal_filter_cmd(
&self,
pt: Coord,
candidate_roads: &BTreeSet<RoadID>,
candidate_roads: Option<&BTreeSet<RoadID>>,
kind: FilterKind,
) -> Command {
let (r, percent_along) = self.closest_point_on_road(pt, candidate_roads).unwrap();
Expand All @@ -135,13 +137,28 @@ impl MapModel {
fn closest_point_on_road(
&self,
click_pt: Coord,
candidate_roads: &BTreeSet<RoadID>,
candidate_roads: Option<&BTreeSet<RoadID>>,
) -> Option<(RoadID, f64)> {
// TODO prune with rtree?
candidate_roads
.iter()
// If candidate_roads is not specified, search around the point with a generous buffer
let roads: Vec<RoadID> = if let Some(set) = candidate_roads {
set.iter().cloned().collect()
} else {
// 50m each direction should be enough
let buffer = 50.0;
let bbox = AABB::from_corners(
Point::new(click_pt.x - buffer, click_pt.y - buffer),
Point::new(click_pt.x + buffer, click_pt.y + buffer),
);
self.closest_road
.locate_in_envelope_intersecting(&bbox)
.map(|r| r.data)
.collect()
};

roads
.into_iter()
.filter_map(|r| {
let road = self.get_r(*r);
let road = self.get_r(r);
if let Some(hit_pt) = match road.linestring.closest_point(&click_pt.into()) {
Closest::Intersection(pt) => Some(pt),
Closest::SinglePoint(pt) => Some(pt),
Expand Down Expand Up @@ -362,7 +379,6 @@ impl MapModel {

// Filters could be defined for multiple neighbourhoods, not just the one
// in the savefile
let all_roads: BTreeSet<RoadID> = self.roads.iter().map(|r| r.id).collect();
let mut cmds = Vec::new();

for f in gj.features {
Expand All @@ -372,15 +388,15 @@ impl MapModel {
let gj_pt: Point = f.geometry.unwrap().try_into()?;
cmds.push(self.add_modal_filter_cmd(
self.mercator.pt_to_mercator(gj_pt.into()),
&all_roads,
None,
kind,
));
}
"deleted_existing_modal_filter" => {
let gj_pt: Point = f.geometry.unwrap().try_into()?;
let pt = self.mercator.pt_to_mercator(gj_pt.into());
// TODO Better error handling if we don't match
let (r, _) = self.closest_point_on_road(pt, &all_roads).unwrap();
let (r, _) = self.closest_point_on_road(pt, None).unwrap();
cmds.push(Command::SetModalFilter(r, None));
}
"direction" => {
Expand Down
14 changes: 12 additions & 2 deletions backend/src/scrape.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
use anyhow::Result;
use geo::Coord;
use osm_reader::{Element, NodeID};
use rstar::{primitives::GeomWithData, RTree};
use utils::Tags;

use crate::{Direction, FilterKind, Intersection, IntersectionID, MapModel, Road, RoadID, Router};
Expand Down Expand Up @@ -95,6 +96,15 @@ pub fn scrape_osm(input_bytes: &[u8], study_area_name: Option<String>) -> Result
for coord in &mut barrier_pts {
*coord = graph.mercator.pt_to_mercator(*coord);
}

info!("Building RTree");
let closest_road = RTree::bulk_load(
roads
.iter()
.map(|r| GeomWithData::new(r.linestring.clone(), r.id))
.collect(),
);

info!("Finalizing the map model");

let mut directions = BTreeMap::new();
Expand All @@ -108,6 +118,7 @@ pub fn scrape_osm(input_bytes: &[u8], study_area_name: Option<String>) -> Result
mercator: graph.mercator,
boundary_polygon: graph.boundary_polygon,
study_area_name,
closest_road,

router_original: None,
router_current: None,
Expand All @@ -124,10 +135,9 @@ pub fn scrape_osm(input_bytes: &[u8], study_area_name: Option<String>) -> Result
};

// Apply barriers (only those that're exactly on one of the roads)
let all_roads: BTreeSet<RoadID> = map.roads.iter().map(|r| r.id).collect();
for pt in barrier_pts {
// TODO What kind?
map.add_modal_filter(pt, &all_roads, FilterKind::NoEntry);
map.add_modal_filter(pt, None, FilterKind::NoEntry);
}
// The commands above populate the existing modal filters and edit history. Undo that.
map.original_modal_filters = map.modal_filters.clone();
Expand Down
6 changes: 5 additions & 1 deletion tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ The `output` directory has GeoJSON output capturing:
- Shortcuts per interior road
- Existing modal filters

The "unit" test in `backend/src/tests.rs` verifies this output doesn't change. When it does, we can manually load the savefile in the old and new web UI, check any differences, and manually approve them.
The "unit" test in `backend/src/tests.rs` verifies this output doesn't change. When it does, we can manually check the diffs and commit the updated file to approve it. There are a few methods for manually diffing:

1. Load the test input file in the [current version of the tool](https://a-b-street.github.io/ltn) and locally.
2. Load the old and new test output files in [GeoDiffr](https://dabreegster.github.io/geodiffr).
3. Use a JSON diff tool like [meld](https://en.wikipedia.org/wiki/Meld_(software)). It's difficult to understand reordered features or slightly changed coordinates, though.

## Notes per test case

Expand Down
2 changes: 1 addition & 1 deletion tests/output/bristol_east.geojson

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion tests/output/bristol_west.geojson

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion tests/output/strasbourg.geojson

Large diffs are not rendered by default.

0 comments on commit 0bd9ee4

Please sign in to comment.