map_gui/tools/
mod.rs

1//! Assorted tools and UI states that're useful for applications built to display maps.
2
3use std::collections::BTreeSet;
4
5use abstio::MapName;
6use geom::Polygon;
7use map_model::{IntersectionID, Map, RoadID};
8use widgetry::{lctrl, EventCtx, GfxCtx, Key, Line, Text, Widget};
9
10pub use self::camera::{CameraState, DefaultMap};
11pub use self::city_picker::CityPicker;
12pub use self::colors::{ColorDiscrete, ColorNetwork};
13pub use self::draw_overlapping_paths::draw_overlapping_paths;
14pub use self::heatmap::{draw_isochrone, make_heatmap, Grid, HeatmapOptions};
15pub use self::icons::{goal_marker, start_marker};
16pub use self::labels::{DrawRoadLabels, DrawSimpleRoadLabels};
17pub use self::minimap::{Minimap, MinimapControls};
18pub use self::navigate::Navigator;
19pub use self::polygon::EditPolygon;
20pub use self::title_screen::{Executable, TitleScreen};
21pub use self::trip_files::{TripManagement, TripManagementState};
22pub use self::ui::{
23    checkbox_per_mode, cmp_count, cmp_dist, cmp_duration, color_for_mode, percentage_bar,
24    FilePicker, FileSaver, FileSaverContents,
25};
26pub use self::waypoints::{InputWaypoints, WaypointID};
27use crate::AppLike;
28
29#[cfg(not(target_arch = "wasm32"))]
30pub use self::command::RunCommand;
31#[cfg(not(target_arch = "wasm32"))]
32pub use self::updater::prompt_to_download_missing_data;
33
34mod camera;
35mod city_picker;
36mod colors;
37#[cfg(not(target_arch = "wasm32"))]
38mod command;
39pub mod compare_counts;
40mod draw_overlapping_paths;
41mod heatmap;
42mod icons;
43#[cfg(not(target_arch = "wasm32"))]
44mod importer;
45mod labels;
46mod minimap;
47mod navigate;
48mod polygon;
49mod title_screen;
50mod trip_files;
51mod ui;
52#[cfg(not(target_arch = "wasm32"))]
53mod updater;
54mod waypoints;
55
56// Update this ___before___ pushing the commit with "[rebuild] [release]".
57const NEXT_RELEASE: &str = "0.3.50";
58
59/// Returns the version of A/B Street to link to. When building for a release, this points to that
60/// new release. Otherwise it points to the current dev version.
61pub fn version() -> &'static str {
62    if cfg!(feature = "release_s3") {
63        NEXT_RELEASE
64    } else {
65        "dev"
66    }
67}
68
69// TODO This is A/B Street specific
70pub fn loading_tips() -> Text {
71    Text::from_multiline(vec![
72        Line("Have you tried..."),
73        Line(""),
74        Line("- simulating cities in Britain, Taiwan, Poland, and more?"),
75        Line("- the 15-minute neighborhood tool?"),
76        Line("- exploring all of the map layers?"),
77        Line("- playing 15-minute Santa, our arcade game spin-off?"),
78    ])
79}
80
81/// Make it clear the map can't be interacted with right now.
82pub fn grey_out_map(g: &mut GfxCtx, app: &dyn AppLike) {
83    g.fork_screenspace();
84    // TODO - OSD height
85    g.draw_polygon(
86        app.cs().fade_map_dark,
87        Polygon::rectangle(g.canvas.window_width, g.canvas.window_height),
88    );
89    g.unfork();
90}
91
92// TODO Associate this with maps, but somehow avoid reading the entire file when listing them.
93pub fn nice_map_name(name: &MapName) -> &str {
94    match name.city.country.as_ref() {
95        "au" => match (name.city.city.as_ref(), name.map.as_ref()) {
96            ("melbourne", "brunswick") => "Melbourne (Brunswick)",
97            ("melbourne", "dandenong") => "Melbourne (Dandenong)",
98            ("melbourne", "maribyrnong") => "Melbourne (Maribyrnong)",
99            _ => &name.map,
100        },
101        "at" => match (name.city.city.as_ref(), name.map.as_ref()) {
102            ("salzburg", "north") => "Salzburg (north)",
103            ("salzburg", "south") => "Salzburg (south)",
104            ("salzburg", "east") => "Salzburg (east)",
105            ("salzburg", "west") => "Salzburg (west)",
106            _ => &name.map,
107        },
108        "br" => match (name.city.city.as_ref(), name.map.as_ref()) {
109            ("sao_paulo", "aricanduva") => "São Paulo (Avenue Aricanduva)",
110            ("sao_paulo", "center") => "São Paulo (city center)",
111            ("sao_paulo", "sao_miguel_paulista") => "São Miguel Paulista",
112            _ => &name.map,
113        },
114        "ca" => match (name.city.city.as_ref(), name.map.as_ref()) {
115            ("montreal", "plateau") => "Montréal (Plateau)",
116            ("toronto", "dufferin") => "Toronto (Dufferin)",
117            ("toronto", "sw") => "Toronto (southwest)",
118            _ => &name.map,
119        },
120        "ch" => match (name.city.city.as_ref(), name.map.as_ref()) {
121            ("geneva", "center") => "Geneva",
122            ("zurich", "center") => "Zürich (city center)",
123            ("zurich", "north") => "Zürich (north)",
124            ("zurich", "south") => "Zürich (south)",
125            ("zurich", "east") => "Zürich (east)",
126            ("zurich", "west") => "Zürich (west)",
127            _ => &name.map,
128        },
129        "cl" => match (name.city.city.as_ref(), name.map.as_ref()) {
130            ("santiago", "bellavista") => "Bellavista (Santiago)",
131            _ => &name.map,
132        },
133        "cz" => match (name.city.city.as_ref(), name.map.as_ref()) {
134            ("frytek_mistek", "huge") => "Frýdek-Místek (entire area)",
135            _ => &name.map,
136        },
137        "de" => match (name.city.city.as_ref(), name.map.as_ref()) {
138            ("berlin", "center") => "Berlin (city center)",
139            ("berlin", "neukolln") => "Berlin-Neukölln",
140            ("bonn", "center") => "Bonn (city center)",
141            ("bonn", "nordstadt") => "Bonn (Nordstadt)",
142            ("bonn", "venusberg") => "Bonn (Venusberg)",
143            ("rostock", "center") => "Rostock",
144            _ => &name.map,
145        },
146        "fr" => match (name.city.city.as_ref(), name.map.as_ref()) {
147            ("brest", "city") => "Brest",
148            ("charleville_mezieres", "secteur1") => "Charleville-Mézières (secteur 1)",
149            ("charleville_mezieres", "secteur2") => "Charleville-Mézières (secteur 2)",
150            ("charleville_mezieres", "secteur3") => "Charleville-Mézières (secteur 3)",
151            ("charleville_mezieres", "secteur4") => "Charleville-Mézières (secteur 4)",
152            ("charleville_mezieres", "secteur5") => "Charleville-Mézières (secteur 5)",
153            ("lyon", "center") => "Lyon",
154            ("paris", "center") => "Paris (city center)",
155            ("paris", "north") => "Paris (north)",
156            ("paris", "south") => "Paris (south)",
157            ("paris", "east") => "Paris (east)",
158            ("paris", "west") => "Paris (west)",
159            ("strasbourg", "center") => "Strasbourg (center)",
160            ("strasbourg", "north") => "Strasbourg (north)",
161            ("strasbourg", "south") => "Strasbourg (south)",
162            _ => &name.map,
163        },
164        "gb" => match (name.city.city.as_ref(), name.map.as_ref()) {
165            ("allerton_bywater", "center") => "Allerton Bywater",
166            ("ashton_park", "center") => "Ashton Park",
167            ("aylesbury", "center") => "Aylesbury",
168            ("aylesham", "center") => "Aylesham",
169            ("bailrigg", "center") => "Bailrigg (Lancaster)",
170            ("bath_riverside", "center") => "Bath Riverside",
171            ("bicester", "center") => "Bicester",
172            ("birmingham", "center") => "Birmingham",
173            ("bournemouth", "center") => "Bournemouth",
174            ("bradford", "center") => "Bradford",
175            ("brighton", "center") => "Brighton",
176            ("brighton", "shoreham_by_sea") => "Shoreham-by-Sea",
177            ("bristol", "east") => "East Bristol",
178            ("bristol", "south") => "South Bristol",
179            ("burnley", "center") => "Burnley",
180            ("cambridge", "north") => "North Cambridge",
181            ("cardiff", "north") => "Cardiff",
182            ("castlemead", "center") => "Castlemead",
183            ("chapelford", "center") => "Chapelford (Cheshire)",
184            ("chapeltown_cohousing", "center") => "Chapeltown Cohousing",
185            ("chichester", "center") => "Chichester",
186            ("chorlton", "center") => "Chorlton",
187            ("clackers_brook", "center") => "Clackers Brook",
188            ("cricklewood", "center") => "Cricklewood",
189            ("culm", "center") => "Culm",
190            ("derby", "center") => "Derby",
191            ("dickens_heath", "center") => "Dickens Heath",
192            ("didcot", "center") => "Didcot (Harwell)",
193            ("dunton_hills", "center") => "Dunton Hills",
194            ("ebbsfleet", "center") => "Ebbsfleet (Dartford)",
195            ("edinburgh", "center") => "Edinburgh",
196            ("exeter_red_cow_village", "center") => "Exeter Red Cow Village",
197            ("glenrothes", "center") => "Glenrothes (Scotland)",
198            ("great_kneighton", "center") => "Great Kneighton (Cambridge)",
199            ("halsnhead", "center") => "Halsnead",
200            ("hampton", "center") => "Hampton",
201            ("inverness", "center") => "Inverness",
202            ("kergilliack", "center") => "Kergilliack",
203            ("keighley", "center") => "Keighley",
204            ("kidbrooke_village", "center") => "Kidbrooke Village",
205            ("lcid", "center") => "Leeds Climate Innovation District",
206            ("leeds", "central") => "Leeds (city center)",
207            ("leeds", "huge") => "Leeds (entire area inside motorways)",
208            ("leeds", "north") => "North Leeds",
209            ("leeds", "west") => "West Leeds",
210            ("lockleaze", "center") => "Lockleaze",
211            ("london", "camden") => "Camden",
212            ("london", "central") => "Central London",
213            ("london", "hackney") => "Hackney",
214            ("london", "kennington") => "Kennington (London)",
215            ("london", "kingston_upon_thames") => "Kingston upon Thames",
216            ("london", "southwark") => "Southwark",
217            ("long_marston", "center") => "Long Marston (Stratford)",
218            ("manchester", "levenshulme") => "Levenshulme (Manchester)",
219            ("manchester", "stockport") => "Stockport",
220            ("marsh_barton", "center") => "Marsh Barton",
221            ("micklefield", "center") => "Micklefield",
222            ("newborough_road", "center") => "Newborough Road",
223            ("newcastle_great_park", "center") => "Newcastle Great Park",
224            ("newcastle_upon_tyne", "center") => "Newcastle upon Tyne",
225            ("nottingham", "center") => "Nottingham (city center)",
226            ("nottingham", "huge") => "Nottingham (entire area)",
227            ("nottingham", "stapleford") => "Stapleford",
228            ("northwick_park", "center") => "Northwick Park",
229            ("oxford", "center") => "Oxford",
230            ("poundbury", "center") => "Poundbury",
231            ("priors_hall", "center") => "Priors Hall",
232            ("sheffield", "center") => "Sheffield",
233            ("sheffield", "darnall") => "Darnall",
234            ("st_albans", "center") => "St Albans",
235            ("taunton_firepool", "center") => "Taunton Firepool",
236            ("taunton_garden", "center") => "Taunton Garden",
237            ("tresham", "center") => "Tresham",
238            ("trumpington_meadows", "center") => "Trumpington Meadows",
239            ("tyersal_lane", "center") => "Tyersal Lane",
240            ("upton", "center") => "Upton",
241            ("water_lane", "center") => "Water Lane",
242            ("wichelstowe", "center") => "Wichelstowe",
243            ("wixams", "center") => "Wixams",
244            ("wokingham", "center") => "Wokingham",
245            ("wynyard", "center") => "Wynyard",
246            _ => &name.map,
247        },
248        "hk" => match (name.city.city.as_ref(), name.map.as_ref()) {
249            ("kowloon", "tsim_sha_tsui") => "Tsim Sha Tsui",
250            _ => &name.map,
251        },
252        "il" => match (name.city.city.as_ref(), name.map.as_ref()) {
253            ("tel_aviv", "center") => "Tel Aviv (city center)",
254            _ => &name.map,
255        },
256        "in" => match (name.city.city.as_ref(), name.map.as_ref()) {
257            ("pune", "center") => "Pune",
258            _ => &name.map,
259        },
260        "ir" => match (name.city.city.as_ref(), name.map.as_ref()) {
261            ("tehran", "parliament") => "Tehran (near Parliament)",
262            _ => &name.map,
263        },
264        "jp" => match (name.city.city.as_ref(), name.map.as_ref()) {
265            ("hiroshima", "uni") => "Hiroshima University",
266            ("tokyo", "shibuya") => "Shibuya",
267            _ => &name.map,
268        },
269        "kr" => match (name.city.city.as_ref(), name.map.as_ref()) {
270            ("seoul", "itaewon_dong") => "Itaewon Dong",
271            _ => &name.map,
272        },
273        "ly" => match (name.city.city.as_ref(), name.map.as_ref()) {
274            ("tripoli", "center") => "Tripoli",
275            _ => &name.map,
276        },
277        "nl" => match (name.city.city.as_ref(), name.map.as_ref()) {
278            ("groningen", "center") => "Groningen (city center)",
279            ("groningen", "huge") => "Groningen (entire area)",
280            _ => &name.map,
281        },
282        "nz" => match (name.city.city.as_ref(), name.map.as_ref()) {
283            ("auckland", "mangere") => "Māngere (Auckland)",
284            _ => &name.map,
285        },
286        "pl" => match (name.city.city.as_ref(), name.map.as_ref()) {
287            ("krakow", "center") => "Kraków (city center)",
288            ("warsaw", "center") => "Warsaw (city center)",
289            _ => &name.map,
290        },
291        "pt" => match (name.city.city.as_ref(), name.map.as_ref()) {
292            ("lisbon", "center") => "Lisbon (city center)",
293            _ => &name.map,
294        },
295        "sg" => match (name.city.city.as_ref(), name.map.as_ref()) {
296            ("jurong", "center") => "Jurong",
297            _ => &name.map,
298        },
299        "tw" => match (name.city.city.as_ref(), name.map.as_ref()) {
300            ("keelung", "center") => "Keelung",
301            ("taipei", "center") => "Taipei (city center)",
302            _ => &name.map,
303        },
304        "us" => match (name.city.city.as_ref(), name.map.as_ref()) {
305            ("anchorage", "downtown") => "Anchorage",
306            ("bellevue", "huge") => "Bellevue",
307            ("beltsville", "i495") => "I-495 in Beltsville, MD",
308            ("detroit", "downtown") => "Detroit",
309            ("lynnwood", "hazelwood") => "Lynnwood, WA",
310            ("milwaukee", "downtown") => "Downtown Milwaukee",
311            ("milwaukee", "oak_creek") => "Oak Creek",
312            ("missoula", "center") => "Missoula",
313            ("mt_vernon", "burlington") => "Burlington",
314            ("mt_vernon", "downtown") => "Mt. Vernon",
315            ("new_haven", "center") => "New Haven",
316            ("nyc", "fordham") => "Fordham",
317            ("nyc", "lower_manhattan") => "Lower Manhattan",
318            ("nyc", "midtown_manhattan") => "Midtown Manhattan",
319            ("nyc", "downtown_brooklyn") => "Downtown Brooklyn",
320            ("phoenix", "gilbert") => "Gilbert",
321            ("phoenix", "tempe") => "Tempe",
322            ("providence", "downtown") => "Providence",
323            ("san_francisco", "downtown") => "San Francisco",
324            ("seattle", "arboretum") => "Arboretum",
325            ("seattle", "central_seattle") => "Central Seattle",
326            ("seattle", "downtown") => "Downtown Seattle",
327            ("seattle", "huge_seattle") => "Seattle (entire area)",
328            ("seattle", "lakeslice") => "Lake Washington corridor",
329            ("seattle", "montlake") => "Montlake and Eastlake",
330            ("seattle", "north_seattle") => "North Seattle",
331            ("seattle", "phinney") => "Phinney Ridge",
332            ("seattle", "qa") => "Queen Anne",
333            ("seattle", "slu") => "South Lake Union",
334            ("seattle", "south_seattle") => "South Seattle",
335            ("seattle", "udistrict_ravenna") => "University District",
336            ("seattle", "wallingford") => "Wallingford",
337            ("seattle", "west_seattle") => "West Seattle",
338            ("tucson", "center") => "Tucson",
339            _ => &name.map,
340        },
341        _ => &name.map,
342    }
343}
344
345pub fn nice_country_name(code: &str) -> &str {
346    // If you add something here, please also add the flag to data/system/assets/flags.
347    // https://github.com/hampusborgos/country-flags/tree/main/svg
348    match code {
349        "au" => "Australia",
350        "at" => "Austria",
351        "br" => "Brazil",
352        "ca" => "Canada",
353        "ch" => "Switzerland",
354        "cl" => "Chile",
355        "cz" => "Czech Republic",
356        "de" => "Germany",
357        "fr" => "France",
358        "gb" => "Great Britain",
359        "il" => "Israel",
360        "in" => "India",
361        "ir" => "Iran",
362        "hk" => "Hong Kong",
363        "jp" => "Japan",
364        "kr" => "South Korea",
365        "ly" => "Libya",
366        "nl" => "Netherlands",
367        "nz" => "New Zealand",
368        "pl" => "Poland",
369        "pt" => "Portugal",
370        "sg" => "Singapore",
371        "tw" => "Taiwan",
372        "us" => "United States of America",
373        _ => code,
374    }
375}
376
377/// Returns the path to an executable. Native-only.
378pub fn find_exe(cmd: &str) -> String {
379    let mut directories = Vec::new();
380    // Some cargo configurations explicitly use a platform-specific directory
381    for arch in ["x86_64-unknown-linux-gnu", ""] {
382        // When running from source, prefer release builds, but fallback to debug. This might be
383        // confusing when developing and not recompiling in release mode.
384        for mode in ["release", "debug"] {
385            for relative_dir in [".", "..", "../.."] {
386                directories.push(
387                    std::path::Path::new(relative_dir)
388                        .join("target")
389                        .join(arch)
390                        .join(mode)
391                        .display()
392                        .to_string(),
393                );
394            }
395        }
396    }
397    // When running from the .zip release
398    directories.push("./binaries".to_string());
399
400    for dir in directories {
401        // Apparently std::path on Windows doesn't do any of this correction. We could build up a
402        // PathBuf properly, I guess
403        let path = if cfg!(windows) {
404            format!("{}/{}.exe", dir, cmd).replace("/", "\\")
405        } else {
406            format!("{}/{}", dir, cmd)
407        };
408        if let Ok(metadata) = fs_err::metadata(&path) {
409            if metadata.is_file() {
410                return path;
411            } else {
412                debug!(
413                    "found matching path: {}/{} but it's not a file.",
414                    &path, cmd
415                );
416            }
417        }
418    }
419    panic!("Couldn't find the {} executable. Is it built?", cmd);
420}
421
422/// A button to change maps, with default keybindings
423pub fn change_map_btn(ctx: &EventCtx, app: &dyn AppLike) -> Widget {
424    ctx.style()
425        .btn_popup_icon_text(
426            "system/assets/tools/map.svg",
427            nice_map_name(app.map().get_name()),
428        )
429        .hotkey(lctrl(Key::L))
430        .build_widget(ctx, "change map")
431}
432
433/// A button to return to the title screen
434pub fn home_btn(ctx: &EventCtx) -> Widget {
435    ctx.style()
436        .btn_plain
437        .btn()
438        .image_path("system/assets/pregame/logo.svg")
439        .image_dims(50.0)
440        .build_widget(ctx, "Home")
441}
442
443/// A standard way to group a home button back to the title screen, the title of the current app,
444/// and a button to change maps. Callers must handle the `change map` and `home` click events.
445pub fn app_header(ctx: &EventCtx, app: &dyn AppLike, title: &str) -> Widget {
446    Widget::col(vec![
447        Widget::row(vec![
448            home_btn(ctx),
449            Line(title).small_heading().into_widget(ctx).centered_vert(),
450        ]),
451        change_map_btn(ctx, app),
452    ])
453}
454
455pub fn intersections_from_roads(roads: &BTreeSet<RoadID>, map: &Map) -> BTreeSet<IntersectionID> {
456    let mut results = BTreeSet::new();
457    for r in roads {
458        let r = map.get_r(*r);
459        for i in [r.src_i, r.dst_i] {
460            if results.contains(&i) {
461                continue;
462            }
463            if map.get_i(i).roads.iter().all(|r| roads.contains(r)) {
464                results.insert(i);
465            }
466        }
467    }
468    results
469}
470
471/// Modify the current URL to set the first free parameter to the current map name.
472pub fn update_url_map_name(app: &dyn AppLike) {
473    widgetry::tools::URLManager::update_url_free_param(
474        app.map()
475            .get_name()
476            .path()
477            .strip_prefix(&abstio::path(""))
478            .unwrap()
479            .to_string(),
480    );
481}