use std::collections::{HashMap, HashSet};
use abstutil::MultiMap;
use connectivity::Spot;
use geom::{Duration, Polygon};
use map_gui::tools::Grid;
use map_model::{
connectivity, AmenityType, BuildingID, BuildingType, IntersectionID, LaneType, Map, Path,
PathConstraints, PathRequest,
};
use widgetry::{Color, Drawable, EventCtx, GeomBatch};
use crate::App;
pub struct Isochrone {
pub start: Vec<BuildingID>,
pub options: Options,
pub draw: Drawable,
pub thresholds: Vec<f64>,
pub colors: Vec<Color>,
pub time_to_reach_building: HashMap<BuildingID, Duration>,
pub amenities_reachable: MultiMap<AmenityType, BuildingID>,
pub population: usize,
pub onstreet_parking_spots: usize,
}
#[derive(Clone)]
pub enum Options {
Walking(connectivity::WalkingOptions),
Biking,
}
impl Options {
pub fn times_from(self, map: &Map, starts: Vec<Spot>) -> HashMap<BuildingID, Duration> {
match self {
Options::Walking(opts) => {
connectivity::all_walking_costs_from(map, starts, Duration::minutes(15), opts)
}
Options::Biking => connectivity::all_vehicle_costs_from(
map,
starts,
Duration::minutes(15),
PathConstraints::Bike,
),
}
}
}
impl Isochrone {
pub fn new(
ctx: &mut EventCtx,
app: &App,
start: Vec<BuildingID>,
options: Options,
) -> Isochrone {
let spot_starts = start.iter().map(|b_id| Spot::Building(*b_id)).collect();
let time_to_reach_building = options.clone().times_from(&app.map, spot_starts);
let mut amenities_reachable = MultiMap::new();
let mut population = 0;
let mut all_roads = HashSet::new();
for b in time_to_reach_building.keys() {
let bldg = app.map.get_b(*b);
for amenity in &bldg.amenities {
if let Some(category) = AmenityType::categorize(&amenity.amenity_type) {
amenities_reachable.insert(category, bldg.id);
}
}
match bldg.bldg_type {
BuildingType::Residential { num_residents, .. }
| BuildingType::ResidentialCommercial(num_residents, _) => {
population += num_residents;
}
_ => {}
}
all_roads.insert(app.map.get_l(bldg.sidewalk_pos.lane()).parent);
}
let mut onstreet_parking_spots = 0;
for r in all_roads {
let r = app.map.get_r(r);
for (l, _, lt) in r.lanes_ltr() {
if lt == LaneType::Parking {
onstreet_parking_spots +=
app.map.get_l(l).number_parking_spots(app.map.get_config());
}
}
}
let thresholds = vec![
0.1,
Duration::minutes(5).inner_seconds(),
Duration::minutes(10).inner_seconds(),
Duration::minutes(15).inner_seconds(),
];
let colors = vec![
Color::GREEN.alpha(0.5),
Color::ORANGE.alpha(0.5),
Color::RED.alpha(0.5),
];
let mut i = Isochrone {
start,
options,
draw: Drawable::empty(ctx),
thresholds,
colors,
time_to_reach_building,
amenities_reachable,
population,
onstreet_parking_spots,
};
i.draw =
draw_isochrone(app, &i.time_to_reach_building, &i.thresholds, &i.colors).upload(ctx);
i
}
pub fn path_to(&self, map: &Map, to: BuildingID) -> Option<Path> {
if !self.time_to_reach_building.contains_key(&to) {
return None;
}
let constraints = match self.options {
Options::Walking(_) => PathConstraints::Pedestrian,
Options::Biking => PathConstraints::Bike,
};
let all_paths = self.start.iter().map(|b_id| {
map.pathfind(PathRequest::between_buildings(map, *b_id, to, constraints).unwrap())
.ok()
.unwrap()
});
all_paths.min_by_key(|path| path.total_length())
}
}
pub fn draw_isochrone(
app: &App,
time_to_reach_building: &HashMap<BuildingID, Duration>,
thresholds: &[f64],
colors: &[Color],
) -> GeomBatch {
let bounds = app.map.get_bounds();
let resolution_m = 100.0;
let mut grid: Grid<f64> = Grid::new(
(bounds.width() / resolution_m).ceil() as usize,
(bounds.height() / resolution_m).ceil() as usize,
0.0,
);
for (b, cost) in time_to_reach_building {
let pt = app.map.get_b(*b).polygon.center();
let idx = grid.idx(
((pt.x() - bounds.min_x) / resolution_m) as usize,
((pt.y() - bounds.min_y) / resolution_m) as usize,
);
grid.data[idx] = cost.inner_seconds();
}
let smooth = false;
let c = contour::ContourBuilder::new(grid.width as u32, grid.height as u32, smooth);
let mut batch = GeomBatch::new();
for (feature, color) in c
.contours(&grid.data, thresholds)
.unwrap()
.into_iter()
.zip(colors)
{
match feature.geometry.unwrap().value {
geojson::Value::MultiPolygon(polygons) => {
for p in polygons {
if let Ok(poly) = Polygon::from_geojson(&p) {
batch.push(*color, poly.scale(resolution_m));
}
}
}
_ => unreachable!(),
}
}
batch
}
pub struct BorderIsochrone {
pub start: Vec<IntersectionID>,
pub options: Options,
pub draw: Drawable,
pub thresholds: Vec<f64>,
pub colors: Vec<Color>,
pub time_to_reach_building: HashMap<BuildingID, Duration>,
}
impl BorderIsochrone {
pub fn new(
ctx: &mut EventCtx,
app: &App,
start: Vec<IntersectionID>,
options: Options,
) -> BorderIsochrone {
let spot_starts = start.iter().map(|i_id| Spot::Border(*i_id)).collect();
let time_to_reach_building = options.clone().times_from(&app.map, spot_starts);
let thresholds = vec![0.1, Duration::minutes(15).inner_seconds()];
let colors = vec![Color::rgb(0, 0, 0).alpha(0.3)];
let mut i = BorderIsochrone {
start,
options,
draw: Drawable::empty(ctx),
thresholds,
colors,
time_to_reach_building,
};
i.draw =
draw_isochrone(app, &i.time_to_reach_building, &i.thresholds, &i.colors).upload(ctx);
i
}
}