fifteen_min/
isochrone.rs

1use std::collections::{HashMap, HashSet};
2
3use abstutil::MultiMap;
4use connectivity::Spot;
5use geom::Duration;
6use map_gui::tools::draw_isochrone;
7use map_model::{
8    connectivity, AmenityType, BuildingID, BuildingType, IntersectionID, LaneType, Map, Path,
9    PathConstraints, PathRequest,
10};
11use widgetry::mapspace::{ToggleZoomed, ToggleZoomedBuilder};
12use widgetry::{Color, EventCtx};
13
14use crate::App;
15
16/// Represents the area reachable from a single building.
17pub struct Isochrone {
18    /// The center of the isochrone (can be multiple points)
19    pub start: Vec<BuildingID>,
20    /// The options used to generate this isochrone
21    pub options: Options,
22    /// Colored polygon contours, uploaded to the GPU and ready for drawing
23    pub draw: ToggleZoomed,
24    /// Thresholds used to draw the isochrone
25    pub thresholds: Vec<f64>,
26    /// Colors used to draw the isochrone
27    pub colors: Vec<Color>,
28    /// How far away is each building from the start?
29    pub time_to_reach_building: HashMap<BuildingID, Duration>,
30    /// Per category of amenity, what buildings have that?
31    pub amenities_reachable: MultiMap<AmenityType, BuildingID>,
32    /// How many people live in the returned area, according to estimates included in the map (from
33    /// city-specific parcel data, guesses from census, or a guess based on OSM tags)
34    pub population: usize,
35    /// How many sreet parking spots are on the same road as any buildings returned.
36    pub onstreet_parking_spots: usize,
37}
38
39#[derive(Clone)]
40pub struct Options {
41    pub movement: MovementOptions,
42    pub thresholds: Vec<(Duration, Color)>,
43}
44
45impl Options {
46    pub fn default_thresholds() -> Vec<(Duration, Color)> {
47        vec![
48            (Duration::minutes(5), Color::GREEN.alpha(0.5)),
49            (Duration::minutes(10), Color::ORANGE.alpha(0.5)),
50            (Duration::minutes(15), Color::RED.alpha(0.5)),
51        ]
52    }
53}
54
55/// The constraints on how we're moving.
56#[derive(Clone)]
57pub enum MovementOptions {
58    Walking(connectivity::WalkingOptions),
59    Biking,
60}
61
62impl MovementOptions {
63    /// Calculate the quickest time to reach buildings across the map from any of the starting
64    /// points, subject to the walking/biking settings configured in these Options.
65    pub fn times_from(self, map: &Map, starts: Vec<Spot>) -> HashMap<BuildingID, Duration> {
66        match self {
67            MovementOptions::Walking(opts) => {
68                connectivity::all_walking_costs_from(map, starts, Duration::minutes(15), opts)
69            }
70            MovementOptions::Biking => connectivity::all_vehicle_costs_from(
71                map,
72                starts,
73                Duration::minutes(15),
74                PathConstraints::Bike,
75            ),
76        }
77    }
78}
79
80impl Isochrone {
81    pub fn new(
82        ctx: &mut EventCtx,
83        app: &App,
84        start: Vec<BuildingID>,
85        options: Options,
86    ) -> Isochrone {
87        let spot_starts = start.iter().map(|b_id| Spot::Building(*b_id)).collect();
88        let time_to_reach_building = options.movement.clone().times_from(&app.map, spot_starts);
89
90        let mut amenities_reachable = MultiMap::new();
91        let mut population = 0;
92        let mut all_roads = HashSet::new();
93        for b in time_to_reach_building.keys() {
94            let bldg = app.map.get_b(*b);
95            for amenity in &bldg.amenities {
96                if let Some(category) = AmenityType::categorize(&amenity.amenity_type) {
97                    amenities_reachable.insert(category, bldg.id);
98                }
99            }
100            match bldg.bldg_type {
101                BuildingType::Residential { num_residents, .. }
102                | BuildingType::ResidentialCommercial(num_residents, _) => {
103                    population += num_residents;
104                }
105                _ => {}
106            }
107            all_roads.insert(bldg.sidewalk_pos.lane().road);
108        }
109
110        let mut onstreet_parking_spots = 0;
111        for r in all_roads {
112            let r = app.map.get_r(r);
113            for l in &r.lanes {
114                if l.lane_type == LaneType::Parking {
115                    onstreet_parking_spots += l.number_parking_spots(app.map.get_config());
116                }
117            }
118        }
119
120        // Generate polygons covering the contour line where the cost in the grid crosses these
121        // threshold values.
122        let mut thresholds = vec![0.1];
123        let mut colors = Vec::new();
124        for (threshold, color) in &options.thresholds {
125            thresholds.push(threshold.inner_seconds());
126            colors.push(*color);
127        }
128
129        let mut i = Isochrone {
130            start,
131            options,
132            draw: ToggleZoomed::empty(ctx),
133            thresholds,
134            colors,
135            time_to_reach_building,
136            amenities_reachable,
137            population,
138            onstreet_parking_spots,
139        };
140
141        i.draw = ToggleZoomedBuilder::from(draw_isochrone(
142            &app.map,
143            &i.time_to_reach_building,
144            &i.thresholds,
145            &i.colors,
146        ))
147        .build(ctx);
148        i
149    }
150
151    pub fn path_to(&self, map: &Map, to: BuildingID) -> Option<Path> {
152        // Don't draw paths to places far away
153        if !self.time_to_reach_building.contains_key(&to) {
154            return None;
155        }
156
157        let constraints = match self.options.movement {
158            MovementOptions::Walking(_) => PathConstraints::Pedestrian,
159            MovementOptions::Biking => PathConstraints::Bike,
160        };
161
162        let all_paths = self.start.iter().filter_map(|b_id| {
163            PathRequest::between_buildings(map, *b_id, to, constraints)
164                .and_then(|req| map.pathfind(req).ok())
165        });
166
167        all_paths.min_by_key(|path| path.total_length())
168    }
169}
170
171/// Represents the area reachable from all intersections on the map border
172pub struct BorderIsochrone {
173    /// The center of the isochrone (can be multiple points)
174    pub start: Vec<IntersectionID>,
175    /// The options used to generate this isochrone
176    pub options: Options,
177    /// Colored polygon contours, uploaded to the GPU and ready for drawing
178    pub draw: ToggleZoomed,
179    /// Thresholds used to draw the isochrone
180    pub thresholds: Vec<f64>,
181    /// Colors used to draw the isochrone
182    pub colors: Vec<Color>,
183    /// How far away is each building from the start?
184    pub time_to_reach_building: HashMap<BuildingID, Duration>,
185}
186
187impl BorderIsochrone {
188    pub fn new(
189        ctx: &mut EventCtx,
190        app: &App,
191        start: Vec<IntersectionID>,
192        options: Options,
193    ) -> BorderIsochrone {
194        let spot_starts = start.iter().map(|i_id| Spot::Border(*i_id)).collect();
195        let time_to_reach_building = options.movement.clone().times_from(&app.map, spot_starts);
196
197        // Generate a single polygon showing 15 minutes from the border
198        let thresholds = vec![0.1, Duration::minutes(15).inner_seconds()];
199
200        // Use one color for the entire polygon
201        let colors = vec![Color::rgb(0, 0, 0).alpha(0.3)];
202
203        let mut i = BorderIsochrone {
204            start,
205            options,
206            draw: ToggleZoomed::empty(ctx),
207            thresholds,
208            colors,
209            time_to_reach_building,
210        };
211
212        i.draw = ToggleZoomedBuilder::from(draw_isochrone(
213            &app.map,
214            &i.time_to_reach_building,
215            &i.thresholds,
216            &i.colors,
217        ))
218        .build(ctx);
219        i
220    }
221}