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 #[structopt(long)]
38 proposal: Option<String>,
39 #[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 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 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 app.session
88 .layers
89 .event(ctx, &app.cs, components::Mode::PickArea, None);
90
91 if let Some(ref name) = args.proposal {
92 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
111fn 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 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 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 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 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 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#[macro_export]
244macro_rules! mut_partitioning {
245 ($app:ident) => {
246 $app.per_map.proposals.list[$app.per_map.proposals.current].partitioning
247 };
248}