ltn/
lib.rs

1#![allow(clippy::type_complexity)]
2
3use structopt::StructOpt;
4
5use abstio::MapName;
6use map_model::{Map, PathConstraints, Road};
7use widgetry::tools::FutureLoader;
8use widgetry::{EventCtx, Settings, State};
9
10pub use app::{App, PerMap, Session, Transition};
11pub use logic::NeighbourhoodID;
12pub use neighbourhood::{Cell, DistanceInterval, Neighbourhood};
13
14#[macro_use]
15extern crate anyhow;
16#[macro_use]
17extern crate log;
18
19mod app;
20mod components;
21mod export;
22pub mod logic;
23mod neighbourhood;
24pub mod pages;
25mod render;
26pub mod save;
27
28pub fn main() {
29    let settings = Settings::new("Low traffic neighbourhoods");
30    run(settings);
31}
32
33#[derive(StructOpt)]
34struct Args {
35    /// Load a previously saved proposal with this name. Note this takes a name, not a full path.
36    /// Or `remote/<ID>`.
37    #[structopt(long)]
38    proposal: Option<String>,
39    /// Lock the user into one fixed neighbourhood, and remove many controls
40    #[structopt(long)]
41    consultation: Option<String>,
42    #[structopt(flatten)]
43    app_args: map_gui::SimpleAppArgs,
44}
45
46const SPRITE_WIDTH: u32 = 750;
47const SPRITE_HEIGHT: u32 = 458;
48
49fn run(mut settings: Settings) {
50    let mut opts = map_gui::options::Options::load_or_default();
51    opts.color_scheme = map_gui::colors::ColorSchemeChoice::LTN;
52    opts.show_building_driveways = false;
53    opts.show_building_outlines = false;
54    // TODO Ideally we would have a better map model in the first place. The next best thing would
55    // be to change these settings based on the map's country, but that's a bit tricky to do early
56    // enough (before map_switched). So for now, assume primary use of this tool is in the UK,
57    // where these settings are most appropriate.
58    opts.show_stop_signs = false;
59    opts.show_crosswalks = false;
60    opts.show_traffic_signal_icon = true;
61    opts.simplify_basemap = true;
62    opts.canvas_settings.min_zoom_for_detail = std::f64::MAX;
63
64    let args = Args::from_iter(abstutil::cli_args());
65    args.app_args.override_options(&mut opts);
66
67    settings = settings.load_default_textures(false);
68    settings = args
69        .app_args
70        .update_widgetry_settings(settings)
71        .canvas_settings(opts.canvas_settings.clone());
72    widgetry::run(settings, move |ctx| {
73        // This file is small enough to bundle in the build
74        ctx.set_texture(
75            include_bytes!("../spritesheet.gif").to_vec(),
76            (SPRITE_WIDTH, SPRITE_HEIGHT),
77            (SPRITE_WIDTH as f32, SPRITE_HEIGHT as f32),
78        );
79
80        App::new(
81            ctx,
82            opts,
83            args.app_args.map_name(),
84            args.app_args.cam,
85            move |ctx, app| {
86                // We need app to fully initialize this
87                app.session
88                    .layers
89                    .event(ctx, &app.cs, components::Mode::PickArea, None);
90
91                if let Some(ref name) = args.proposal {
92                    // Remote edits require another intermediate state to load
93                    if let Some(id) = name.strip_prefix("remote/") {
94                        vec![load_remote(ctx, id.to_string(), args.consultation.clone())]
95                    } else {
96                        let popup_state = crate::save::Proposal::load_from_path(
97                            ctx,
98                            app,
99                            abstio::path_ltn_proposals(app.per_map.map.get_name(), name),
100                        );
101                        setup_initial_states(ctx, app, args.consultation.as_ref(), popup_state)
102                    }
103                } else {
104                    setup_initial_states(ctx, app, args.consultation.as_ref(), None)
105                }
106            },
107        )
108    });
109}
110
111// A Proposal should already be loaded by now, unless consultation is set
112fn setup_initial_states(
113    ctx: &mut EventCtx,
114    app: &mut App,
115    consultation: Option<&String>,
116    popup_state: Option<Box<dyn State<App>>>,
117) -> Vec<Box<dyn State<App>>> {
118    let mut states = Vec::new();
119    if let Some(ref consultation) = consultation {
120        if app.per_map.map.get_name() != &MapName::new("gb", "bristol", "east") {
121            panic!("Consultation mode not supported on this map");
122        }
123
124        let mut consultation_proposal_path = None;
125
126        let focus_on_street = match consultation.as_ref() {
127            "pt1" => "Gregory Street",
128            "pt2" => {
129                // Start from a baked-in proposal with special boundaries
130                consultation_proposal_path = Some(abstio::path(
131                    "system/ltn_proposals/bristol_beaufort_road.json.gz",
132                ));
133                "Jubilee Road"
134            }
135            _ => panic!("Unknown Bristol consultation mode {consultation}"),
136        };
137
138        // If we already loaded something from a saved proposal, then don't clear anything
139        if let Some(path) = consultation_proposal_path {
140            if crate::save::Proposal::load_from_path(ctx, app, path.clone()).is_some() {
141                panic!("Consultation mode broken; go fix {path} manually");
142            }
143            app.per_map.proposals.force_current_to_basemap();
144        }
145
146        // Look for the neighbourhood containing one small street
147        let r = app
148            .per_map
149            .map
150            .all_roads()
151            .iter()
152            .find(|r| r.get_name(None) == focus_on_street)
153            .expect(&format!("Can't find {focus_on_street}"))
154            .id;
155        let (neighbourhood, _) = app
156            .partitioning()
157            .all_neighbourhoods()
158            .iter()
159            .find(|(_, info)| info.block.perimeter.interior.contains(&r))
160            .expect(&format!(
161                "Can't find neighbourhood containing {focus_on_street}"
162            ));
163        app.per_map.consultation = Some(*neighbourhood);
164        app.per_map.consultation_id = Some(consultation.to_string());
165
166        // TODO Maybe center the camera, ignoring any saved values
167
168        states.push(pages::DesignLTN::new_state(
169            ctx,
170            app,
171            app.per_map.consultation.unwrap(),
172        ));
173    } else {
174        states.push(pages::PickArea::new_state(ctx, app));
175    }
176    if let Some(state) = popup_state {
177        states.push(state);
178    }
179    states
180}
181
182fn load_remote(
183    ctx: &mut EventCtx,
184    id: String,
185    consultation: Option<String>,
186) -> Box<dyn State<App>> {
187    let (_, outer_progress_rx) = futures_channel::mpsc::channel(1);
188    let (_, inner_progress_rx) = futures_channel::mpsc::channel(1);
189    let url = format!("{}/get-ltn?id={}", crate::save::PROPOSAL_HOST_URL, id);
190    FutureLoader::<App, Vec<u8>>::new_state(
191        ctx,
192        Box::pin(async move {
193            let bytes = abstio::http_get(url).await?;
194            let wrapper: Box<dyn Send + FnOnce(&App) -> Vec<u8>> = Box::new(move |_| bytes);
195            Ok(wrapper)
196        }),
197        outer_progress_rx,
198        inner_progress_rx,
199        "Downloading proposal",
200        Box::new(move |ctx, app, result| {
201            let popup_state = crate::save::Proposal::load_from_bytes(ctx, app, &id, result);
202            Transition::Clear(setup_initial_states(
203                ctx,
204                app,
205                consultation.as_ref(),
206                popup_state,
207            ))
208        }),
209    )
210}
211
212#[cfg(target_arch = "wasm32")]
213use wasm_bindgen::prelude::*;
214
215#[cfg(target_arch = "wasm32")]
216#[wasm_bindgen(js_name = "run")]
217pub fn run_wasm(root_dom_id: String, assets_base_url: String, assets_are_gzipped: bool) {
218    let settings = Settings::new("Low traffic neighbourhoods")
219        .root_dom_element_id(root_dom_id)
220        .assets_base_url(assets_base_url)
221        .assets_are_gzipped(assets_are_gzipped);
222
223    run(settings);
224}
225
226pub fn redraw_all_icons(ctx: &EventCtx, app: &mut App) {
227    app.per_map.draw_all_filters = render::render_modal_filters(ctx, &app.per_map.map);
228    app.per_map.draw_turn_restrictions = render::render_turn_restrictions(ctx, &app.per_map.map);
229}
230
231fn is_private(road: &Road) -> bool {
232    // See https://wiki.openstreetmap.org/wiki/Tag:access%3Dprivate#Relation_to_access=no
233    road.osm_tags.is_any("access", vec!["no", "private"])
234}
235
236fn is_driveable(road: &Road, map: &Map) -> bool {
237    PathConstraints::Car.can_use_road(road, map) && !is_private(road)
238}
239
240// The current partitioning is stored deeply nested in App. For read-only access, we can use a
241// regular helper method. For writing, we can't, because we'll get a borrow error -- so instead
242// just use macros to make it less annoying to modify
243#[macro_export]
244macro_rules! mut_partitioning {
245    ($app:ident) => {
246        $app.per_map.proposals.list[$app.per_map.proposals.current].partitioning
247    };
248}