fifteen_min/
bus.rs

1use abstutil::prettyprint_usize;
2use geom::Duration;
3use map_gui::tools::{InputWaypoints, WaypointID};
4use map_model::connectivity::WalkingOptions;
5use map_model::PathConstraints;
6use synthpop::{TripEndpoint, TripMode};
7use widgetry::mapspace::{ObjectID, World, WorldOutcome};
8use widgetry::{
9    Color, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Panel, State, Text,
10    Transition, VerticalAlignment, Widget,
11};
12
13use crate::isochrone::{Isochrone, MovementOptions, Options};
14use crate::App;
15
16pub struct BusExperiment {
17    panel: Panel,
18    waypoints: InputWaypoints,
19    world: World<ID>,
20}
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23enum ID {
24    Waypoint(WaypointID),
25    // Starting from this waypoint and going to the next
26    BusRoute(usize),
27}
28impl ObjectID for ID {}
29
30impl BusExperiment {
31    pub fn new_state(ctx: &mut EventCtx, app: &App) -> Box<dyn State<App>> {
32        let mut state = BusExperiment {
33            panel: Panel::empty(ctx),
34            waypoints: InputWaypoints::new(app, vec![PathConstraints::Car, PathConstraints::Bus]),
35            world: World::new(),
36        };
37        state.recalculate_everything(ctx, app);
38        Box::new(state)
39    }
40
41    fn recalculate_everything(&mut self, ctx: &mut EventCtx, app: &App) {
42        let map = &app.map;
43        let mut world = World::new();
44        self.waypoints
45            .rebuild_world(ctx, &mut world, ID::Waypoint, 1);
46
47        for (idx, pair) in self.waypoints.get_waypoints().windows(2).enumerate() {
48            // TODO Pathfind for buses
49            if let Some(path) = TripEndpoint::path_req(pair[0], pair[1], TripMode::Drive, map)
50                .and_then(|req| map.pathfind(req).ok())
51            {
52                let duration = path.estimate_duration(map, None);
53                if let Ok(hitbox) = path.trace_v2(map) {
54                    world
55                        .add(ID::BusRoute(idx))
56                        .hitbox(hitbox)
57                        .zorder(0)
58                        .draw_color(self.waypoints.get_waypoint_color(idx))
59                        .hover_alpha(0.8)
60                        .tooltip(Text::from(Line(format!("Freeflow time is {duration}"))))
61                        .build(ctx);
62                }
63            }
64        }
65
66        let stops = self
67            .waypoints
68            .get_waypoints()
69            .into_iter()
70            .filter_map(|endpt| match endpt {
71                TripEndpoint::Building(b) => Some(b),
72                _ => None,
73            })
74            .collect::<Vec<_>>();
75        let isochrone = Isochrone::new(
76            ctx,
77            app,
78            stops,
79            Options {
80                movement: MovementOptions::Walking(WalkingOptions::default()),
81                thresholds: vec![(Duration::minutes(15), Color::grey(0.3).alpha(0.5))],
82                // TODO The inner colors overlap the outer; this doesn't look right yet
83                /*thresholds: vec![
84                    (Duration::minutes(5), Color::grey(0.3).alpha(0.5)),
85                    (Duration::minutes(10), Color::grey(0.3).alpha(0.3)),
86                    (Duration::minutes(15), Color::grey(0.3).alpha(0.2)),
87                ],*/
88            },
89        );
90        world.draw_master_batch_built(isochrone.draw);
91
92        world.initialize_hover(ctx);
93        world.rebuilt_during_drag(ctx, &self.world);
94        self.world = world;
95
96        self.panel = Panel::new_builder(Widget::col(vec![
97            map_gui::tools::app_header(ctx, app, "Bus planner"),
98            ctx.style()
99                .btn_back("15-minute neighborhoods")
100                .hotkey(Key::Escape)
101                .build_def(ctx),
102            Text::from_multiline(vec![
103                Line("Within a 15 min walk of all stops:"),
104                Line(format!(
105                    "Population: {}",
106                    prettyprint_usize(isochrone.population)
107                )),
108                Line(format!(
109                    "Shops: {}",
110                    prettyprint_usize(
111                        isochrone
112                            .amenities_reachable
113                            .borrow()
114                            .values()
115                            .map(|x| x.len())
116                            .sum()
117                    )
118                )),
119            ])
120            .into_widget(ctx),
121            self.waypoints.get_panel_widget(ctx),
122        ]))
123        .aligned(HorizontalAlignment::Left, VerticalAlignment::Top)
124        .ignore_initial_events()
125        .build(ctx);
126    }
127}
128
129impl State<App> for BusExperiment {
130    fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition<App> {
131        let panel_outcome = self.panel.event(ctx);
132        if let Outcome::Clicked(ref x) = panel_outcome {
133            if x == "15-minute neighborhoods" {
134                return Transition::Pop;
135            }
136        }
137
138        let world_outcome = self.world.event(ctx);
139        let world_outcome_for_waypoints = world_outcome
140            .maybe_map_id(|id| match id {
141                ID::Waypoint(id) => Some(id),
142                _ => None,
143            })
144            .unwrap_or(WorldOutcome::Nothing);
145
146        if self
147            .waypoints
148            .event(app, panel_outcome, world_outcome_for_waypoints)
149        {
150            self.recalculate_everything(ctx, app);
151        }
152
153        Transition::Keep
154    }
155
156    fn draw(&self, g: &mut GfxCtx, _: &App) {
157        self.panel.draw(g);
158        self.world.draw(g);
159    }
160}