Skip to content

Commit 0bd9ee4

Browse files
committed
Use an rtree to pick the roads for existing modal filters. The results
slightly change in Strasbourg, but seem reasonable. The time drops from 4.1s to 0.9s.
1 parent ea47738 commit 0bd9ee4

File tree

7 files changed

+47
-17
lines changed

7 files changed

+47
-17
lines changed

backend/src/lib.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ impl LTN {
160160
x: pos.lng,
161161
y: pos.lat,
162162
}),
163-
&self.neighbourhood.as_ref().unwrap().interior_roads,
163+
Some(&self.neighbourhood.as_ref().unwrap().interior_roads),
164164
FilterKind::from_string(&kind).unwrap(),
165165
);
166166
self.after_edit();

backend/src/map_model.rs

+26-10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use geo::{
77
LineString, Point, Polygon,
88
};
99
use geojson::{Feature, FeatureCollection, GeoJson};
10+
use rstar::{primitives::GeomWithData, RTree, AABB};
1011
use serde::Serialize;
1112
use utils::{Mercator, Tags};
1213

@@ -20,6 +21,7 @@ pub struct MapModel {
2021
pub mercator: Mercator,
2122
pub study_area_name: Option<String>,
2223
pub boundary_polygon: Polygon,
24+
pub closest_road: RTree<GeomWithData<LineString, RoadID>>,
2325

2426
// TODO Wasteful, can share some
2527
// This is guaranteed to exist, only Option during MapModel::new internals
@@ -107,7 +109,7 @@ impl MapModel {
107109
pub fn add_modal_filter(
108110
&mut self,
109111
pt: Coord,
110-
candidate_roads: &BTreeSet<RoadID>,
112+
candidate_roads: Option<&BTreeSet<RoadID>>,
111113
kind: FilterKind,
112114
) {
113115
let cmd = self.do_edit(self.add_modal_filter_cmd(pt, candidate_roads, kind));
@@ -119,7 +121,7 @@ impl MapModel {
119121
fn add_modal_filter_cmd(
120122
&self,
121123
pt: Coord,
122-
candidate_roads: &BTreeSet<RoadID>,
124+
candidate_roads: Option<&BTreeSet<RoadID>>,
123125
kind: FilterKind,
124126
) -> Command {
125127
let (r, percent_along) = self.closest_point_on_road(pt, candidate_roads).unwrap();
@@ -135,13 +137,28 @@ impl MapModel {
135137
fn closest_point_on_road(
136138
&self,
137139
click_pt: Coord,
138-
candidate_roads: &BTreeSet<RoadID>,
140+
candidate_roads: Option<&BTreeSet<RoadID>>,
139141
) -> Option<(RoadID, f64)> {
140-
// TODO prune with rtree?
141-
candidate_roads
142-
.iter()
142+
// If candidate_roads is not specified, search around the point with a generous buffer
143+
let roads: Vec<RoadID> = if let Some(set) = candidate_roads {
144+
set.iter().cloned().collect()
145+
} else {
146+
// 50m each direction should be enough
147+
let buffer = 50.0;
148+
let bbox = AABB::from_corners(
149+
Point::new(click_pt.x - buffer, click_pt.y - buffer),
150+
Point::new(click_pt.x + buffer, click_pt.y + buffer),
151+
);
152+
self.closest_road
153+
.locate_in_envelope_intersecting(&bbox)
154+
.map(|r| r.data)
155+
.collect()
156+
};
157+
158+
roads
159+
.into_iter()
143160
.filter_map(|r| {
144-
let road = self.get_r(*r);
161+
let road = self.get_r(r);
145162
if let Some(hit_pt) = match road.linestring.closest_point(&click_pt.into()) {
146163
Closest::Intersection(pt) => Some(pt),
147164
Closest::SinglePoint(pt) => Some(pt),
@@ -362,7 +379,6 @@ impl MapModel {
362379

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

368384
for f in gj.features {
@@ -372,15 +388,15 @@ impl MapModel {
372388
let gj_pt: Point = f.geometry.unwrap().try_into()?;
373389
cmds.push(self.add_modal_filter_cmd(
374390
self.mercator.pt_to_mercator(gj_pt.into()),
375-
&all_roads,
391+
None,
376392
kind,
377393
));
378394
}
379395
"deleted_existing_modal_filter" => {
380396
let gj_pt: Point = f.geometry.unwrap().try_into()?;
381397
let pt = self.mercator.pt_to_mercator(gj_pt.into());
382398
// TODO Better error handling if we don't match
383-
let (r, _) = self.closest_point_on_road(pt, &all_roads).unwrap();
399+
let (r, _) = self.closest_point_on_road(pt, None).unwrap();
384400
cmds.push(Command::SetModalFilter(r, None));
385401
}
386402
"direction" => {

backend/src/scrape.rs

+12-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
33
use anyhow::Result;
44
use geo::Coord;
55
use osm_reader::{Element, NodeID};
6+
use rstar::{primitives::GeomWithData, RTree};
67
use utils::Tags;
78

89
use crate::{Direction, FilterKind, Intersection, IntersectionID, MapModel, Road, RoadID, Router};
@@ -95,6 +96,15 @@ pub fn scrape_osm(input_bytes: &[u8], study_area_name: Option<String>) -> Result
9596
for coord in &mut barrier_pts {
9697
*coord = graph.mercator.pt_to_mercator(*coord);
9798
}
99+
100+
info!("Building RTree");
101+
let closest_road = RTree::bulk_load(
102+
roads
103+
.iter()
104+
.map(|r| GeomWithData::new(r.linestring.clone(), r.id))
105+
.collect(),
106+
);
107+
98108
info!("Finalizing the map model");
99109

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

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

126137
// Apply barriers (only those that're exactly on one of the roads)
127-
let all_roads: BTreeSet<RoadID> = map.roads.iter().map(|r| r.id).collect();
128138
for pt in barrier_pts {
129139
// TODO What kind?
130-
map.add_modal_filter(pt, &all_roads, FilterKind::NoEntry);
140+
map.add_modal_filter(pt, None, FilterKind::NoEntry);
131141
}
132142
// The commands above populate the existing modal filters and edit history. Undo that.
133143
map.original_modal_filters = map.modal_filters.clone();

tests/README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ The `output` directory has GeoJSON output capturing:
99
- Shortcuts per interior road
1010
- Existing modal filters
1111

12-
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.
12+
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:
13+
14+
1. Load the test input file in the [current version of the tool](https://a-b-street.github.io/ltn) and locally.
15+
2. Load the old and new test output files in [GeoDiffr](https://dabreegster.github.io/geodiffr).
16+
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.
1317

1418
## Notes per test case
1519

tests/output/bristol_east.geojson

+1-1
Large diffs are not rendered by default.

tests/output/bristol_west.geojson

+1-1
Large diffs are not rendered by default.

tests/output/strasbourg.geojson

+1-1
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)