1use std::collections::BTreeSet;
2
3use maplit::btreeset;
4use rand::seq::SliceRandom;
5use rand::SeedableRng;
6use rand_xorshift::XorShiftRng;
7
8use geom::Duration;
9use map_gui::tools::{grey_out_map, nice_map_name};
10use map_model::AreaType;
11use sim::{AgentType, PersonID, TripID};
12use synthpop::TripEndpoint;
13use widgetry::tools::{open_browser, PopupMsg};
14use widgetry::{
15 lctrl, ControlState, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Panel,
16 SimpleState, Text, TextExt, Toggle, VerticalAlignment, Widget,
17};
18
19use crate::app::{App, Transition};
20use crate::common::jump_to_time_upon_startup;
21use crate::edit::EditMode;
22use crate::info::{OpenTrip, Tab};
23use crate::sandbox::gameplay::{GameplayMode, GameplayState};
24use crate::sandbox::{Actions, SandboxControls, SandboxMode, SpeedSetting};
25
26pub struct Actdev {
29 top_right: Panel,
30 scenario_name: String,
31 bg_traffic: bool,
32 once: bool,
33}
34
35impl Actdev {
36 pub fn new_state(
37 ctx: &mut EventCtx,
38 scenario_name: String,
39 bg_traffic: bool,
40 ) -> Box<dyn GameplayState> {
41 Box::new(Actdev {
42 top_right: Panel::empty(ctx),
43 scenario_name,
44 bg_traffic,
45 once: true,
46 })
47 }
48}
49
50impl GameplayState for Actdev {
51 fn event(
52 &mut self,
53 ctx: &mut EventCtx,
54 app: &mut App,
55 controls: &mut SandboxControls,
56 actions: &mut Actions,
57 ) -> Option<Transition> {
58 if self.once {
59 self.once = false;
60
61 if self.bg_traffic {
62 let mut highlight = BTreeSet::new();
63 let study_area = &app
64 .primary
65 .map
66 .all_areas()
67 .iter()
68 .find(|a| a.area_type == AreaType::StudyArea)
69 .unwrap()
70 .polygon;
71
72 for person in app.primary.sim.get_all_people() {
73 if let TripEndpoint::Building(b) =
74 app.primary.sim.trip_info(person.trips[0]).start
75 {
76 if study_area.contains_pt(app.primary.map.get_b(b).polygon.center()) {
77 highlight.insert(person.id);
78 }
79 }
80 }
81 app.primary.sim.set_highlighted_people(highlight);
82 }
83
84 controls.time_panel.as_mut().unwrap().override_height =
86 Some(self.top_right.panel_dims().height);
87
88 controls
89 .time_panel
90 .as_mut()
91 .unwrap()
92 .resume(ctx, app, SpeedSetting::Faster);
93 }
94
95 match self.top_right.event(ctx) {
96 Outcome::Clicked(x) => match x.as_ref() {
97 "change scenario" => {
98 let scenario = if self.scenario_name == "base" {
99 "go_active"
100 } else {
101 "base"
102 };
103 return Some(Transition::Replace(SandboxMode::async_new(
104 app,
105 GameplayMode::Actdev(
106 app.primary.map.get_name().clone(),
107 scenario.to_string(),
108 self.bg_traffic,
109 ),
110 jump_to_time_upon_startup(Duration::hours(8)),
111 )));
112 }
113 "Edit map" => Some(Transition::Push(EditMode::new_state(
114 ctx,
115 app,
116 GameplayMode::Actdev(
117 app.primary.map.get_name().clone(),
118 self.scenario_name.clone(),
119 self.bg_traffic,
120 ),
121 ))),
122 "about A/B Street" => {
123 let panel = Panel::new_builder(Widget::col(vec![
124 Widget::row(vec![
125 Line("About A/B Street").small_heading().into_widget(ctx),
126 ctx.style().btn_close_widget(ctx),
127 ]),
128 Line("Created by Dustin Carlino, Yuwen Li, & Michael Kirk")
129 .small()
130 .into_widget(ctx),
131 Text::from(
132 "A/B Street is a traffic simulation game based on OpenStreetMap. You \
133 can modify roads and intersections, measure the effects on different \
134 groups, and advocate for your proposal.",
135 )
136 .wrap_to_pct(ctx, 50)
137 .into_widget(ctx),
138 "This is a simplified version. Check out the full version below."
139 .text_widget(ctx),
140 ctx.style().btn_outline.text("abstreet.org").build_def(ctx),
141 ]))
142 .build(ctx);
143 Some(Transition::Push(<dyn SimpleState<_>>::new_state(
144 panel,
145 Box::new(About),
146 )))
147 }
148 "Follow someone" => {
149 if let Some((person, trip)) = find_active_trip(app) {
150 app.primary.layer = None;
152 ctx.canvas.cam_zoom = 40.0;
153 controls.common.as_mut().unwrap().launch_info_panel(
154 ctx,
155 app,
156 Tab::PersonTrips(person, OpenTrip::single(trip)),
157 actions,
158 );
159 None
160 } else {
161 return Some(Transition::Push(PopupMsg::new_state(
162 ctx,
163 "Nobody's around...",
164 vec!["There are no active trips right now"],
165 )));
166 }
167 }
168 "Cycling" => {
169 app.primary.layer =
170 Some(Box::new(crate::layer::map::BikeActivity::new(ctx, app)));
171 None
172 }
173 "Walking" => {
174 app.primary.layer = Some(Box::new(crate::layer::traffic::Throughput::new(
175 ctx,
176 app,
177 btreeset! { AgentType::Pedestrian },
178 )));
179 None
180 }
181 _ => unreachable!(),
182 },
183 Outcome::Changed(_) => {
184 return Some(Transition::Replace(SandboxMode::async_new(
186 app,
187 GameplayMode::Actdev(
188 app.primary.map.get_name().clone(),
189 self.scenario_name.clone(),
190 !self.bg_traffic,
191 ),
192 jump_to_time_upon_startup(Duration::hours(8)),
193 )));
194 }
195 _ => None,
196 }
197 }
198
199 fn draw(&self, g: &mut GfxCtx, _: &App) {
200 self.top_right.draw(g);
201 }
202
203 fn recreate_panels(&mut self, ctx: &mut EventCtx, app: &App) {
204 let col = Widget::col(vec![
205 Widget::row(vec![
206 ctx.style()
207 .btn_plain
208 .btn()
209 .image_path("system/assets/pregame/logo.svg")
210 .image_dims(50.0)
211 .build_widget(ctx, "about A/B Street"),
212 Line(nice_map_name(app.primary.map.get_name()))
213 .small_heading()
214 .into_widget(ctx),
215 ctx.style()
216 .btn_outline
217 .icon_text("system/assets/tools/pencil.svg", "Edit map")
218 .hotkey(lctrl(Key::E))
219 .build_def(ctx),
220 ])
221 .centered(),
222 Widget::row(vec![
223 ctx.style()
224 .btn_popup_icon_text("system/assets/tools/calendar.svg", "scenario")
225 .label_styled_text(
226 match self.scenario_name.as_ref() {
227 "base" => Text::from_all(vec![
228 Line("Baseline / "),
229 Line("Go Active").secondary(),
230 ]),
231 "go_active" => Text::from_all(vec![
232 Line("Baseline").secondary(),
233 Line(" / Go Active"),
234 ]),
235 _ => unreachable!(),
236 },
237 ControlState::Default,
238 )
239 .build_widget(ctx, "change scenario"),
240 Toggle::checkbox(ctx, "background traffic", None, self.bg_traffic),
241 ]),
242 Widget::row(vec![
243 ctx.style()
244 .btn_plain
245 .icon_text("system/assets/tools/location.svg", "Follow someone")
246 .build_def(ctx),
247 ctx.style()
248 .btn_plain
249 .icon_text("system/assets/meters/pedestrian.svg", "Walking")
250 .build_def(ctx),
251 ctx.style()
252 .btn_plain
253 .icon_text("system/assets/meters/bike.svg", "Cycling")
254 .build_def(ctx),
255 ]),
256 ]);
257
258 self.top_right = Panel::new_builder(col)
259 .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
260 .build(ctx);
261 }
262
263 fn has_tool_panel(&self) -> bool {
264 false
266 }
267}
268
269struct About;
270
271impl SimpleState<App> for About {
272 fn on_click(&mut self, _: &mut EventCtx, _: &mut App, x: &str, _: &mut Panel) -> Transition {
273 if x == "close" {
274 return Transition::Pop;
275 } else if x == "abstreet.org" {
276 open_browser("https://abstreet.org");
277 }
278 Transition::Keep
279 }
280
281 fn draw(&self, g: &mut GfxCtx, app: &App) {
282 grey_out_map(g, app);
283 }
284}
285
286fn find_active_trip(app: &App) -> Option<(PersonID, TripID)> {
287 let mut all = Vec::new();
288 for agent in app.primary.sim.active_agents() {
289 if let Some(trip) = app.primary.sim.agent_to_trip(agent) {
290 if let Some(person) = app.primary.sim.trip_to_person(trip) {
291 all.push((person, trip));
292 }
293 }
294 }
295 all.choose(&mut XorShiftRng::from_entropy()).cloned()
296}