game/sandbox/gameplay/freeform/
mod.rs

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()
149                .btn_outline
150                .text("Spawn area traffic")
151                .hotkey(Key::A)
152                .build_def(ctx),*/
153                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        // (Button action, label, full description)
178        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        // Until we add in census data for other countries, offering the option doesn't make sense.
220        // Include "zz", used for one-shot imports, since we have no idea where those are located.
221        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                // Silence compiler warnings
309                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                // Silence compiler warnings
320                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}