Make the LTN tool handle roads that already ban cars.

- Don't allow filtering them
- Don't cross them when calculating cell connectivity
- Create special green cells for them, and don't count towards the
  disconnected warning
This commit is contained in:
Dustin Carlino 2021-12-31 16:16:35 +00:00
parent 332e88f235
commit 7d6f133f1c
6 changed files with 80 additions and 13 deletions

View File

@ -60,7 +60,7 @@ impl Viewer {
.neighborhood
.cells
.iter()
.filter(|c| c.borders.is_empty())
.filter(|c| c.borders.is_empty() && !c.car_free)
.count();
// TODO Also add a red outline to them or something
let warning = if disconnected_cells == 0 {

View File

@ -10,11 +10,12 @@ use super::Neighborhood;
const COLORS: [Color; 6] = [
Color::BLUE,
Color::YELLOW,
Color::GREEN,
Color::RED,
Color::PURPLE,
Color::PINK,
Color::ORANGE,
];
const CAR_FREE_COLOR: Color = Color::GREEN;
/// Partition a neighborhood's boundary polygon based on the cells. Currently this discretizes
/// space into a grid, so the results don't look perfect, but it's fast. Also returns the color for
@ -74,7 +75,14 @@ pub fn draw_cells(map: &Map, neighborhood: &Neighborhood) -> (GeomBatch, Vec<Col
}
let adjacencies = diffusion(&mut grid, boundary_marker);
let cell_colors = color_cells(neighborhood.cells.len(), adjacencies);
let mut cell_colors = color_cells(neighborhood.cells.len(), adjacencies);
// Color car-free cells in a special way
for (idx, cell) in neighborhood.cells.iter().enumerate() {
if cell.car_free {
cell_colors[idx] = CAR_FREE_COLOR;
}
}
// Just draw rectangles based on the grid
// TODO We should be able to generate actual polygons per cell using the contours crate

View File

@ -4,7 +4,7 @@ use maplit::btreeset;
use geom::{Circle, Distance, Line, Polygon};
use map_gui::tools::DrawRoadLabels;
use map_model::{IntersectionID, Map, Perimeter, RoadID, RoutingParams, TurnID};
use map_model::{IntersectionID, Map, PathConstraints, Perimeter, RoadID, RoutingParams, TurnID};
use widgetry::mapspace::ToggleZoomed;
use widgetry::{Color, Drawable, EventCtx, GeomBatch};
@ -88,6 +88,8 @@ pub struct Cell {
pub roads: BTreeMap<RoadID, DistanceInterval>,
/// Intersections where this cell touches the boundary of the neighborhood.
pub borders: BTreeSet<IntersectionID>,
/// This cell only contains roads that ban cars.
pub car_free: bool,
}
/// An interval along a road's length, with start < end.
@ -245,11 +247,17 @@ fn find_cells(
let mut cells = Vec::new();
let mut visited = BTreeSet::new();
let mut no_car_roads = Vec::new();
for start in &perimeter.interior {
if visited.contains(start) || modal_filters.roads.contains_key(start) {
continue;
}
let cell = floodfill(map, *start, perimeter, borders, &modal_filters);
let start = *start;
if !PathConstraints::Car.can_use_road(map.get_r(start), map) {
no_car_roads.push(start);
continue;
}
let cell = floodfill(map, start, perimeter, borders, &modal_filters);
visited.extend(cell.roads.keys().cloned());
cells.push(cell);
}
@ -261,6 +269,7 @@ fn find_cells(
let mut cell = Cell {
roads: BTreeMap::new(),
borders: btreeset! { road.src_i },
car_free: false,
};
cell.roads.insert(
road.id,
@ -275,6 +284,7 @@ fn find_cells(
let mut cell = Cell {
roads: BTreeMap::new(),
borders: btreeset! { road.dst_i },
car_free: false,
};
cell.roads.insert(
road.id,
@ -287,6 +297,34 @@ fn find_cells(
}
}
// Roads already banning cars should still contribute a cell, so the cell coloring can still
// account for them
//
// TODO Should we attempt to merge adjacent cells like this? If we have lots of tiny pieces of
// bike-only roads, they'll each get their own cell
for r in no_car_roads {
let mut cell = Cell {
roads: BTreeMap::new(),
borders: BTreeSet::new(),
car_free: true,
};
let road = map.get_r(r);
if borders.contains(&road.src_i) {
cell.borders.insert(road.src_i);
}
if borders.contains(&road.dst_i) {
cell.borders.insert(road.dst_i);
}
cell.roads.insert(
road.id,
DistanceInterval {
start: Distance::ZERO,
end: road.length(),
},
);
cells.push(cell);
}
cells
}
@ -304,6 +342,7 @@ fn floodfill(
// The caller should handle this case
assert!(!modal_filters.roads.contains_key(&start));
assert!(PathConstraints::Car.can_use_road(map.get_r(start), map));
while !queue.is_empty() {
let current = map.get_r(queue.pop().unwrap());
@ -319,6 +358,7 @@ fn floodfill(
);
for i in [current.src_i, current.dst_i] {
for next in &map.get_i(i).roads {
let next_road = map.get_r(*next);
if !perimeter.interior.contains(next) {
if neighborhood_borders.contains(&i) {
cell_borders.insert(i);
@ -331,7 +371,6 @@ fn floodfill(
}
}
if let Some(filter_dist) = modal_filters.roads.get(next) {
let next_road = map.get_r(*next);
// Which ends of the filtered road have we reached?
let mut visited_start = next_road.src_i == i;
let mut visited_end = next_road.dst_i == i;
@ -359,9 +398,15 @@ fn floodfill(
},
},
);
} else {
queue.push(*next);
continue;
}
if !PathConstraints::Car.can_use_road(next_road, map) {
// The road is only for bikes/pedestrians to start with
continue;
}
queue.push(*next);
}
}
}
@ -369,6 +414,7 @@ fn floodfill(
Cell {
roads: visited_roads,
borders: cell_borders,
car_free: false,
}
}

View File

@ -1,6 +1,6 @@
use geom::Distance;
use map_gui::tools::CityPicker;
use map_model::{IntersectionID, RoadID};
use map_model::{IntersectionID, PathConstraints, RoadID};
use widgetry::mapspace::{ObjectID, World, WorldOutcome};
use widgetry::{
Color, EventCtx, HorizontalAlignment, Key, Panel, PanelBuilder, State, TextExt,
@ -177,11 +177,17 @@ pub fn handle_world_outcome(
app: &mut App,
outcome: WorldOutcome<FilterableObj>,
) -> bool {
let map = &app.primary.map;
match outcome {
WorldOutcome::ClickedObject(FilterableObj::InteriorRoad(r)) => {
let road = map.get_r(r);
// Filtering on a road that's already marked bike-only doesn't make sense
if !PathConstraints::Car.can_use_road(road, map) {
return true;
}
if app.session.modal_filters.roads.remove(&r).is_none() {
// Place the filter on the part of the road that was clicked
let road = app.primary.map.get_r(r);
// These calls shouldn't fail -- since we clicked a road, the cursor must be in
// map-space. And project_pt returns a point that's guaranteed to be on the
// polyline.
@ -194,7 +200,7 @@ pub fn handle_world_outcome(
true
}
WorldOutcome::ClickedObject(FilterableObj::InteriorIntersection(i)) => {
if app.primary.map.get_i(i).roads.len() != 4 {
if map.get_i(i).roads.len() != 4 {
// Misleading. Nothing changes, but we'll "fall through" to other cases without
// this
return true;

View File

@ -13,7 +13,7 @@ pub use self::v1::{Path, PathRequest, PathStep};
pub use self::v2::{PathStepV2, PathV2};
pub use self::vehicles::vehicle_cost;
pub use self::walking::WalkingNode;
use crate::{osm, Lane, LaneID, LaneType, Map, MovementID, RoadID, TurnType};
use crate::{osm, Lane, LaneID, LaneType, Map, MovementID, Road, RoadID, TurnType};
mod engine;
mod node_map;
@ -115,6 +115,12 @@ impl PathConstraints {
false
}
/// Can an agent use a road in either direction? There are some subtle exceptions with using
/// bus-only lanes for turns.
pub fn can_use_road(self, road: &Road, map: &Map) -> bool {
road.lanes.iter().any(|lane| self.can_use(lane, map))
}
/// Strict for bikes. If there are bike lanes, not allowed to use other lanes.
pub(crate) fn filter_lanes(self, mut choices: Vec<LaneID>, map: &Map) -> Vec<LaneID> {
choices.retain(|l| self.can_use(map.get_l(*l), map));

View File

@ -68,7 +68,7 @@ impl Texture {
}
impl Color {
// TODO Won't this confuse the shader? :P
// TODO Won't this confuse the shader?
pub const CLEAR: Color = Color::rgba_f(1.0, 0.0, 0.0, 0.0);
pub const BLACK: Color = Color::rgb_f(0.0, 0.0, 0.0);
pub const WHITE: Color = Color::rgb_f(1.0, 1.0, 1.0);
@ -110,6 +110,7 @@ impl Color {
Color::rgb_f(f, f, f)
}
/// Note this is incorrect for `Color::CLEAR`. Can't fix in a const fn.
pub const fn alpha(&self, a: f32) -> Color {
Color::rgba_f(self.r, self.g, self.b, a)
}