1mod area_spawner;
2#[cfg(not(target_arch = "wasm32"))]
3mod importers;
4mod spawner;
5
6use rand::seq::SliceRandom;
7use rand::Rng;
8
9use crate::ID;
10use abstutil::Timer;
11use geom::{Distance, Duration};
12use map_gui::tools::{grey_out_map, CityPicker};
13use map_model::{IntersectionID, Position};
14use sim::rand_dist;
15use synthpop::{IndividTrip, PersonSpec, Scenario, TripEndpoint, TripMode, TripPurpose};
16use widgetry::tools::{open_browser, PopupMsg, PromptInput};
17use widgetry::{
18 lctrl, EventCtx, GfxCtx, HorizontalAlignment, Key, Line, Outcome, Panel, SimpleState, State,
19 Text, VerticalAlignment, Widget,
20};
21
22use crate::app::{App, Transition};
23use crate::common::jump_to_time_upon_startup;
24use crate::edit::EditMode;
25use crate::sandbox::gameplay::{GameplayMode, GameplayState};
26use crate::sandbox::{Actions, SandboxControls, SandboxMode};
27
28pub struct Freeform {
29 top_right: Panel,
30}
31
32impl Freeform {
33 pub fn new_state(ctx: &mut EventCtx, app: &App) -> Box<dyn GameplayState> {
34 map_gui::tools::update_url_map_name(app);
35
36 Box::new(Freeform {
37 top_right: Panel::empty(ctx),
38 })
39 }
40}
41
42impl GameplayState for Freeform {
43 fn event(
44 &mut self,
45 ctx: &mut EventCtx,
46 app: &mut App,
47 _: &mut SandboxControls,
48 _: &mut Actions,
49 ) -> Option<Transition> {
50 match self.top_right.event(ctx) {
51 Outcome::Clicked(x) => match x.as_ref() {
52 "change map" => Some(Transition::Push(CityPicker::new_state(
53 ctx,
54 app,
55 Box::new(|_, app| {
56 let sandbox = if app.opts.dev {
57 SandboxMode::async_new(
58 app,
59 GameplayMode::Freeform(app.primary.map.get_name().clone()),
60 jump_to_time_upon_startup(Duration::hours(6)),
61 )
62 } else {
63 SandboxMode::simple_new(
64 app,
65 GameplayMode::Freeform(app.primary.map.get_name().clone()),
66 )
67 };
68 Transition::Multi(vec![Transition::Pop, Transition::Replace(sandbox)])
69 }),
70 ))),
71 "change scenario" => Some(Transition::Push(ChangeScenario::new_state(
72 ctx, app, "none",
73 ))),
74 "edit map" => Some(Transition::Push(EditMode::new_state(
75 ctx,
76 app,
77 GameplayMode::Freeform(app.primary.map.get_name().clone()),
78 ))),
79 "Start a new trip" => Some(Transition::Push(spawner::AgentSpawner::new_state(
80 ctx, app, None,
81 ))),
82 "Spawn area traffic" => {
83 Some(Transition::Push(area_spawner::AreaSpawner::new_state(ctx)))
84 }
85 "Record trips as a scenario" => Some(Transition::Push(PromptInput::new_state(
86 ctx,
87 "Name this scenario",
88 String::new(),
89 Box::new(|name, ctx, app| {
90 if abstio::file_exists(abstio::path_scenario(
91 app.primary.map.get_name(),
92 &name,
93 )) {
94 Transition::Push(PopupMsg::new_state(
95 ctx,
96 "Error",
97 vec![format!(
98 "A scenario called \"{}\" already exists, please pick another \
99 name",
100 name
101 )],
102 ))
103 } else {
104 app.primary
105 .sim
106 .generate_scenario(&app.primary.map, name)
107 .save();
108 Transition::Pop
109 }
110 }),
111 ))),
112 _ => unreachable!(),
113 },
114 _ => None,
115 }
116 }
117
118 fn draw(&self, g: &mut GfxCtx, _: &App) {
119 self.top_right.draw(g);
120 }
121
122 fn recreate_panels(&mut self, ctx: &mut EventCtx, app: &App) {
123 let rows = vec![
124 Widget::custom_row(vec![
125 Line("Sandbox")
126 .small_heading()
127 .into_widget(ctx)
128 .margin_right(18),
129 map_gui::tools::change_map_btn(ctx, app).margin_right(8),
130 ctx.style()
131 .btn_popup_icon_text("system/assets/tools/calendar.svg", "none")
132 .hotkey(Key::S)
133 .build_widget(ctx, "change scenario")
134 .margin_right(8),
135 ctx.style()
136 .btn_outline
137 .icon_text("system/assets/tools/pencil.svg", "Edit map")
138 .hotkey(lctrl(Key::E))
139 .build_widget(ctx, "edit map")
140 .margin_right(8),
141 ])
142 .centered(),
143 Widget::row(vec![
144 ctx.style()
145 .btn_outline
146 .text("Start a new trip")
147 .build_def(ctx),
148 ctx.style()
154 .btn_outline
155 .text("Record trips as a scenario")
156 .build_def(ctx),
157 ])
158 .centered(),
159 Text::from_all(vec![
160 Line("Select an intersection and press "),
161 Key::Z.txt(ctx),
162 Line(" to start traffic nearby"),
163 ])
164 .into_widget(ctx),
165 ];
166
167 self.top_right = Panel::new_builder(Widget::col(rows))
168 .aligned(HorizontalAlignment::Right, VerticalAlignment::Top)
169 .build(ctx);
170 }
171}
172
173pub struct ChangeScenario;
174
175impl ChangeScenario {
176 pub fn new_state(ctx: &mut EventCtx, app: &App, current_scenario: &str) -> Box<dyn State<App>> {
177 let mut choices = Vec::new();
179 for name in abstio::list_all_objects(abstio::path_all_scenarios(app.primary.map.get_name()))
180 {
181 if name == "weekday" {
182 choices.push((
183 name,
184 "typical weekday traffic".to_string(),
185 "Trips will begin throughout the entire day. Midnight is usually quiet, so \
186 you may need to fast-forward to morning rush hour. Data comes from Puget \
187 Sound Regional Council's Soundcast model from 2014.",
188 ));
189 } else if name == "background" || name == "base_with_bg" {
190 choices.push((
191 name,
192 "typical weekday traffic".to_string(),
193 "Home-to-work trips from 2011 UK census data are simulated. Traffic usually \
194 starts around 7am.",
195 ));
196 } else {
197 choices.push((
198 name.clone(),
199 name,
200 "This is custom scenario data for this map",
201 ));
202 }
203 }
204 choices.push((
205 "home_to_work".to_string(),
206 "trips between home and work".to_string(),
207 "Randomized people will leave homes in the morning, go to work, then return in the \
208 afternoon. It'll be very quiet before 7am and between 10am to 5pm. The population \
209 size and location of homes and workplaces is all guessed just from OpenStreetMap \
210 tags.",
211 ));
212 choices.push((
213 "random".to_string(),
214 "random unrealistic trips".to_string(),
215 "A fixed number of trips will start at midnight, but not constantly appear through \
216 the day.",
217 ));
218 let country = &app.primary.map.get_name().city.country;
219 if country == "us" || country == "zz" {
222 choices.push((
223 "census".to_string(),
224 "generate from US census data".to_string(),
225 "A population from 2010 US census data will travel between home and workplaces. \
226 Generating it will take a few moments as some data is downloaded for this map.",
227 ));
228 }
229 choices.push((
230 "none".to_string(),
231 "none, except for buses".to_string(),
232 "You can manually spawn traffic around a single intersection or by using the tool in \
233 the top panel to start individual trips.",
234 ));
235 if cfg!(not(target_arch = "wasm32")) {
236 choices.push((
237 "import grid2demand".to_string(),
238 "import Grid2Demand data".to_string(),
239 "Select an input_agents.csv file from https://github.com/asu-trans-ai-lab/grid2demand"));
240 choices.push((
241 "import json".to_string(),
242 "import JSON scenario".to_string(),
243 "Select a JSON file specified by https://a-b-street.github.io/docs/tech/dev/formats/scenarios.html"));
244 }
245
246 let mut col = vec![
247 Widget::row(vec![
248 Line("Pick your scenario").small_heading().into_widget(ctx),
249 ctx.style().btn_close_widget(ctx),
250 ]),
251 Line("Each scenario determines what people live and travel around this map")
252 .into_widget(ctx),
253 ];
254 for (name, label, description) in choices {
255 let btn = if name == current_scenario {
256 ctx.style().btn_tab.text(label).disabled(true)
257 } else {
258 ctx.style().btn_outline.text(label)
259 };
260 col.push(
261 Widget::row(vec![
262 btn.build_widget(ctx, name),
263 Text::from(Line(description).secondary())
264 .wrap_to_pct(ctx, 40)
265 .into_widget(ctx)
266 .align_right(),
267 ])
268 .margin_above(30),
269 );
270 }
271 col.push(
272 ctx.style()
273 .btn_plain
274 .btn()
275 .label_underlined_text("Learn how to import your own data.")
276 .build_def(ctx),
277 );
278
279 <dyn SimpleState<_>>::new_state(
280 Panel::new_builder(Widget::col(col)).build(ctx),
281 Box::new(ChangeScenario),
282 )
283 }
284}
285
286impl SimpleState<App> for ChangeScenario {
287 fn on_click(
288 &mut self,
289 ctx: &mut EventCtx,
290 app: &mut App,
291 x: &str,
292 _: &mut Panel,
293 ) -> Transition {
294 if x == "close" {
295 Transition::Pop
296 } else if x == "Learn how to import your own data." {
297 open_browser(
298 "https://a-b-street.github.io/docs/tech/trafficsim/travel_demand.html#custom-import",
299 );
300 Transition::Keep
301 } else if x == "import grid2demand" {
302 #[cfg(not(target_arch = "wasm32"))]
303 {
304 importers::import_grid2demand(ctx)
305 }
306 #[cfg(target_arch = "wasm32")]
307 {
308 let _ = ctx;
310 unreachable!()
311 }
312 } else if x == "import json" {
313 #[cfg(not(target_arch = "wasm32"))]
314 {
315 importers::import_json(ctx)
316 }
317 #[cfg(target_arch = "wasm32")]
318 {
319 let _ = ctx;
321 unreachable!()
322 }
323 } else {
324 Transition::Multi(vec![
325 Transition::Pop,
326 Transition::Replace(SandboxMode::simple_new(
327 app,
328 if x == "none" {
329 GameplayMode::Freeform(app.primary.map.get_name().clone())
330 } else {
331 GameplayMode::PlayScenario(
332 app.primary.map.get_name().clone(),
333 x.to_string(),
334 Vec::new(),
335 )
336 },
337 )),
338 ])
339 }
340 }
341
342 fn draw(&self, g: &mut GfxCtx, app: &App) {
343 grey_out_map(g, app);
344 }
345}
346
347pub fn spawn_agents_around(i: IntersectionID, app: &mut App) {
348 let map = &app.primary.map;
349 let mut rng = app.primary.current_flags.sim_flags.make_rng();
350 let mut scenario = Scenario::empty(map, "one-shot");
351
352 if map.all_buildings().is_empty() {
353 println!("No buildings, can't pick destinations");
354 return;
355 }
356
357 let mut timer = Timer::new(format!(
358 "spawning agents around {} (rng seed {:?})",
359 i, app.primary.current_flags.sim_flags.rng_seed
360 ));
361
362 for l in &map.get_i(i).incoming_lanes {
363 let lane = map.get_l(*l);
364 if lane.is_driving() || lane.is_biking() {
365 for _ in 0..10 {
366 let mode = if rng.gen_bool(0.7) && lane.is_driving() {
367 TripMode::Drive
368 } else {
369 TripMode::Bike
370 };
371 scenario.people.push(PersonSpec {
372 orig_id: None,
373 trips: vec![IndividTrip::new(
374 app.primary.sim.time(),
375 TripPurpose::Shopping,
376 TripEndpoint::SuddenlyAppear(Position::new(
377 lane.id,
378 rand_dist(&mut rng, Distance::ZERO, lane.length()),
379 )),
380 TripEndpoint::Building(map.all_buildings().choose(&mut rng).unwrap().id),
381 mode,
382 )],
383 });
384 }
385 } else if lane.is_walkable() {
386 for _ in 0..5 {
387 scenario.people.push(PersonSpec {
388 orig_id: None,
389 trips: vec![IndividTrip::new(
390 app.primary.sim.time(),
391 TripPurpose::Shopping,
392 TripEndpoint::SuddenlyAppear(Position::new(
393 lane.id,
394 rand_dist(&mut rng, 0.1 * lane.length(), 0.9 * lane.length()),
395 )),
396 TripEndpoint::Building(map.all_buildings().choose(&mut rng).unwrap().id),
397 TripMode::Walk,
398 )],
399 });
400 }
401 }
402 }
403
404 let retry_if_no_room = false;
405 app.primary.sim.instantiate_without_retries(
406 &scenario,
407 map,
408 &mut rng,
409 retry_if_no_room,
410 &mut timer,
411 );
412 app.primary.sim.tiny_step(map, &mut app.primary.sim_cb);
413}
414
415pub fn actions(_: &App, id: ID) -> Vec<(Key, String)> {
416 match id {
417 ID::Building(_) => vec![(Key::Z, "start a trip here".to_string())],
418 ID::Intersection(_) => vec![(Key::Z, "spawn agents here".to_string())],
419 _ => Vec::new(),
420 }
421}
422
423pub fn execute(ctx: &mut EventCtx, app: &mut App, id: ID, action: &str) -> Transition {
424 match (id, action) {
425 (ID::Building(b), "start a trip here") => {
426 Transition::Push(spawner::AgentSpawner::new_state(ctx, app, Some(b)))
427 }
428 (ID::Intersection(id), "spawn agents here") => {
429 spawn_agents_around(id, app);
430 Transition::Keep
431 }
432 _ => unreachable!(),
433 }
434}