1use std::collections::{BTreeMap, BTreeSet, BinaryHeap};
2
3use abstutil::PriorityQueueItem;
4use geom::{Circle, Duration};
5use map_model::{osm, Crossing, CrossingType, Road, RoadID};
6use widgetry::mapspace::{DrawCustomUnzoomedShapes, ObjectID, PerZoom, World, WorldOutcome};
7use widgetry::{
8 lctrl, Color, ControlState, Drawable, EventCtx, GeomBatch, GfxCtx, Key, Line, Outcome, Panel,
9 RewriteColor, State, Text, TextExt, Widget,
10};
11
12use crate::components::{AppwidePanel, BottomPanel, Mode};
13use crate::render::{colors, Toggle3Zoomed};
14use crate::{App, Transition};
15
16pub struct Crossings {
17 appwide_panel: AppwidePanel,
18 bottom_panel: Panel,
19 world: World<Obj>,
20 draw_porosity: Drawable,
21 draw_crossings: Toggle3Zoomed,
22 draw_nearest_crossing: Option<Drawable>,
23 time_to_nearest_crossing: BTreeMap<RoadID, Duration>,
24}
25
26impl Crossings {
27 pub fn new_state(ctx: &mut EventCtx, app: &mut App) -> Box<dyn State<App>> {
28 let appwide_panel = AppwidePanel::new(ctx, app, Mode::Crossings);
29 let contents = make_bottom_panel(ctx, app);
30 let bottom_panel = BottomPanel::new(ctx, &appwide_panel, contents);
31
32 app.session
34 .layers
35 .event(ctx, &app.cs, Mode::Crossings, Some(&bottom_panel));
36
37 let mut state = Self {
38 appwide_panel,
39 bottom_panel,
40 world: World::new(),
41 draw_porosity: Drawable::empty(ctx),
42 draw_crossings: Toggle3Zoomed::empty(ctx),
43 draw_nearest_crossing: None,
44 time_to_nearest_crossing: BTreeMap::new(),
45 };
46 state.update(ctx, app);
47 Box::new(state)
48 }
49
50 pub fn svg_path(ct: CrossingType) -> &'static str {
51 match ct {
52 CrossingType::Signalized => "system/assets/tools/signalized_crossing.svg",
53 CrossingType::Unsignalized => "system/assets/tools/unsignalized_crossing.svg",
54 }
55 }
56
57 fn update(&mut self, ctx: &mut EventCtx, app: &App) {
58 self.draw_porosity = draw_porosity(ctx, app);
59 self.draw_crossings = draw_crossings(ctx, app);
60 let contents = make_bottom_panel(ctx, app);
61 self.bottom_panel = BottomPanel::new(ctx, &self.appwide_panel, contents);
62 self.draw_nearest_crossing = None;
63 self.time_to_nearest_crossing.clear();
64
65 if app.session.layers.show_crossing_time {
66 let (draw, time) = draw_nearest_crossing(ctx, app);
67 self.draw_nearest_crossing = Some(draw);
68 self.time_to_nearest_crossing = time;
69 }
70
71 self.world = make_world(ctx, app, &self.time_to_nearest_crossing);
72 }
73}
74
75impl State<App> for Crossings {
76 fn event(&mut self, ctx: &mut EventCtx, app: &mut App) -> Transition {
77 if let Some(t) =
78 self.appwide_panel
79 .event(ctx, app, &crate::save::PreserveState::Crossings, help)
80 {
81 return t;
82 }
83 if let Some(t) =
84 app.session
85 .layers
86 .event(ctx, &app.cs, Mode::Crossings, Some(&self.bottom_panel))
87 {
88 if app.session.layers.show_crossing_time != self.draw_nearest_crossing.is_some() {
89 if app.session.layers.show_crossing_time {
90 let (draw, time) = draw_nearest_crossing(ctx, app);
91 self.draw_nearest_crossing = Some(draw);
92 self.time_to_nearest_crossing = time;
93 } else {
94 self.draw_nearest_crossing = None;
95 self.time_to_nearest_crossing.clear();
96 }
97 self.world = make_world(ctx, app, &self.time_to_nearest_crossing);
98 }
99
100 return t;
101 }
102 if let Outcome::Clicked(x) = self.bottom_panel.event(ctx) {
103 match x.as_ref() {
104 "signalized crossing" => {
105 app.session.crossing_type = CrossingType::Signalized;
106 let contents = make_bottom_panel(ctx, app);
107 self.bottom_panel = BottomPanel::new(ctx, &self.appwide_panel, contents);
108 }
109 "unsignalized crossing" => {
110 app.session.crossing_type = CrossingType::Unsignalized;
111 let contents = make_bottom_panel(ctx, app);
112 self.bottom_panel = BottomPanel::new(ctx, &self.appwide_panel, contents);
113 }
114 "undo" => {
115 let mut edits = app.per_map.map.get_edits().clone();
116 edits.commands.pop().unwrap();
117 app.apply_edits(edits);
118 crate::redraw_all_icons(ctx, app);
119 self.update(ctx, app);
120 }
121 _ => unreachable!(),
122 }
123 }
124
125 let map = &mut app.per_map.map;
126 match self.world.event(ctx) {
127 WorldOutcome::ClickedObject(Obj::Road(r)) => {
128 let cursor_pt = ctx.canvas.get_cursor_in_map_space().unwrap();
129 let road = map.get_r(r);
130 let pt_on_line = road.center_pts.project_pt(cursor_pt);
131 let (dist, _) = road.center_pts.dist_along_of_point(pt_on_line).unwrap();
132
133 let mut edits = map.get_edits().clone();
134 edits.commands.push(map.edit_road_cmd(r, |new| {
135 new.crossings.push(Crossing {
136 kind: app.session.crossing_type,
137 dist,
138 });
139 new.crossings.sort_by_key(|c| c.dist);
140 }));
141 app.apply_edits(edits);
142 self.update(ctx, app);
143 }
144 WorldOutcome::ClickedObject(Obj::Crossing(r, idx)) => {
145 let mut edits = map.get_edits().clone();
147 edits.commands.push(map.edit_road_cmd(r, |new| {
148 new.crossings.remove(idx);
149 }));
151 app.apply_edits(edits);
152 self.update(ctx, app);
153 }
154 _ => {}
155 }
156
157 Transition::Keep
158 }
159
160 fn draw(&self, g: &mut GfxCtx, app: &App) {
161 self.appwide_panel.draw(g);
162 self.bottom_panel.draw(g);
163 g.redraw(&self.draw_porosity);
164 app.per_map.draw_major_road_labels.draw(g);
165 app.session.layers.draw(g, app);
166 app.per_map.draw_poi_icons.draw(g);
167 if let Some(ref draw) = self.draw_nearest_crossing {
168 g.redraw(draw);
169 }
170 self.draw_crossings.draw(g);
171 self.world.draw(g);
173 }
174
175 fn recreate(&mut self, ctx: &mut EventCtx, app: &mut App) -> Box<dyn State<App>> {
176 Self::new_state(ctx, app)
177 }
178}
179
180fn help() -> Vec<&'static str> {
181 vec![
182 "This shows crossings over main roads.",
183 "The number of crossings determines the \"porosity\" of areas",
184 ]
185}
186
187fn main_roads(app: &App) -> Vec<&Road> {
188 let mut result = Vec::new();
189 for r in app.per_map.map.all_roads() {
190 if r.get_rank() != osm::RoadRank::Local && !r.is_light_rail() {
191 result.push(r);
192 }
193 }
194 result
195}
196
197fn draw_crossings(ctx: &EventCtx, app: &App) -> Toggle3Zoomed {
198 let mut batch = GeomBatch::new();
199 let mut low_zoom = DrawCustomUnzoomedShapes::builder();
200
201 let mut icons = BTreeMap::new();
202 for ct in [CrossingType::Signalized, CrossingType::Unsignalized] {
203 icons.insert(ct, GeomBatch::load_svg(ctx, Crossings::svg_path(ct)));
204 }
205
206 let edits = app.per_map.map.get_edits();
207
208 for road in main_roads(app) {
209 for crossing in &road.crossings {
210 let rewrite_color = if edits.is_crossing_modified(road.id, crossing) {
211 RewriteColor::NoOp
212 } else {
213 RewriteColor::ChangeAlpha(0.7)
214 };
215
216 let icon = &icons[&crossing.kind];
217 if let Ok((pt, angle)) = road.center_pts.dist_along(crossing.dist) {
218 let angle = angle.rotate_degs(90.0);
219 batch.append(
220 icon.clone()
221 .scale_to_fit_width(road.get_width().inner_meters())
222 .centered_on(pt)
223 .rotate_around_batch_center(angle)
224 .color(rewrite_color),
225 );
226
227 let icon = icon.clone();
229 low_zoom.add_custom(Box::new(move |batch, thickness| {
231 batch.append(
232 icon.clone()
233 .scale_to_fit_width(30.0 * thickness)
234 .centered_on(pt)
235 .rotate_around_batch_center(angle)
236 .color(rewrite_color),
237 );
238 }));
239 }
240 }
241 }
242
243 let min_zoom_for_detail = 5.0;
244 let step_size = 0.1;
245 Toggle3Zoomed::new(
248 batch.build(ctx),
249 low_zoom.build(PerZoom::new(min_zoom_for_detail, step_size)),
250 )
251}
252
253#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
254enum Obj {
255 Road(RoadID),
256 Crossing(RoadID, usize),
259}
260
261impl ObjectID for Obj {}
262
263fn make_world(
264 ctx: &EventCtx,
265 app: &App,
266 time_to_nearest_crossing: &BTreeMap<RoadID, Duration>,
267) -> World<Obj> {
268 let mut world = World::new();
269
270 for road in main_roads(app) {
271 for (idx, crossing) in road.crossings.iter().enumerate() {
272 world
273 .add(Obj::Crossing(road.id, idx))
274 .hitbox(
277 Circle::new(
278 road.center_pts.must_dist_along(crossing.dist).0,
279 3.0 * road.get_width() / 2.0,
280 )
281 .to_polygon(),
282 )
283 .drawn_in_master_batch()
284 .hover_color(colors::HOVER)
285 .zorder(1)
286 .clickable()
287 .build(ctx);
288 }
289
290 world
291 .add(Obj::Road(road.id))
292 .hitbox(road.get_thick_polygon())
293 .drawn_in_master_batch()
294 .hover_color(colors::HOVER)
295 .zorder(0)
296 .clickable()
297 .maybe_tooltip(if let Some(time) = time_to_nearest_crossing.get(&road.id) {
298 Some(Text::from(Line(format!(
299 "{time} walking to the nearest crossing"
300 ))))
301 } else {
302 None
303 })
304 .build(ctx);
305 }
306
307 world.initialize_hover(ctx);
308 world
309}
310
311fn draw_porosity(ctx: &EventCtx, app: &App) -> Drawable {
312 let mut batch = GeomBatch::new();
313 for info in app.partitioning().all_neighbourhoods().values() {
314 let num_crossings = info
317 .block
318 .perimeter
319 .roads
320 .iter()
321 .filter(|id| !app.per_map.map.get_r(id.road).crossings.is_empty())
322 .count();
323 let color = if num_crossings == 0 {
324 *colors::IMPERMEABLE
325 } else if num_crossings == 1 {
326 *colors::SEMI_PERMEABLE
327 } else {
328 *colors::POROUS
329 };
330
331 batch.push(color.alpha(0.5), info.block.polygon.clone());
332 }
333 ctx.upload(batch)
334}
335
336fn make_bottom_panel(ctx: &mut EventCtx, app: &App) -> Widget {
337 let icon = |ct: CrossingType, key: Key, name: &str| {
338 let hide_color = Color::hex("#FDDA06");
339
340 ctx.style()
341 .btn_solid_primary
342 .icon(Crossings::svg_path(ct))
343 .image_color(
344 RewriteColor::Change(hide_color, Color::CLEAR),
345 ControlState::Default,
346 )
347 .image_color(
348 RewriteColor::Change(hide_color, Color::CLEAR),
349 ControlState::Disabled,
350 )
351 .hotkey(key)
352 .disabled(app.session.crossing_type == ct)
353 .tooltip_and_disabled({
354 let mut txt = Text::new();
355 txt.append(Line(name));
356 txt.add_line(Line("Click").fg(ctx.style().text_hotkey_color));
357 txt.append(Line(" a main road to add or remove a crossing"));
358 txt
359 })
360 .build_widget(ctx, name)
361 };
362
363 let mut total_crossings = 0;
364 for r in main_roads(app) {
365 total_crossings += r.crossings.len();
366 }
367
368 Widget::row(vec![
369 icon(CrossingType::Unsignalized, Key::F1, "unsignalized crossing"),
370 icon(CrossingType::Signalized, Key::F2, "signalized crossing"),
371 Widget::vertical_separator(ctx),
372 Widget::row(vec![
373 ctx.style()
374 .btn_plain
375 .icon("system/assets/tools/undo.svg")
376 .disabled(app.per_map.map.get_edits().commands.is_empty())
377 .hotkey(lctrl(Key::Z))
378 .build_widget(ctx, "undo"),
379 format!("{total_crossings} crossings",)
381 .text_widget(ctx)
382 .centered_vert(),
383 ]),
384 ])
385}
386
387fn draw_nearest_crossing(ctx: &EventCtx, app: &App) -> (Drawable, BTreeMap<RoadID, Duration>) {
388 let mut queue: BinaryHeap<PriorityQueueItem<Duration, RoadID>> = BinaryHeap::new();
394
395 let mut main_road_ids = BTreeSet::new();
396 for r in main_roads(app) {
397 main_road_ids.insert(r.id);
398 if !app.per_map.map.get_r(r.id).crossings.is_empty() {
399 queue.push(PriorityQueueItem {
400 cost: Duration::ZERO,
401 value: r.id,
402 });
403 }
404 }
405
406 let mut cost_per_node: BTreeMap<RoadID, Duration> = BTreeMap::new();
407 while let Some(current) = queue.pop() {
408 if cost_per_node.contains_key(¤t.value) {
409 continue;
410 }
411 cost_per_node.insert(current.value, current.cost);
412
413 for next in app.per_map.map.get_next_roads(current.value) {
415 if main_road_ids.contains(&next) {
416 let cost = app.per_map.map.get_r(next).length() / map_model::MAX_WALKING_SPEED;
417 queue.push(PriorityQueueItem {
418 cost: current.cost + cost,
419 value: next,
420 });
421 }
422 }
423 }
424
425 let mut drawn_intersections = BTreeSet::new();
426 let mut batch = GeomBatch::new();
427 for (r, time) in &cost_per_node {
428 let scale = if *time < Duration::minutes(1) {
429 continue;
430 } else if *time < Duration::minutes(2) {
431 0.2
432 } else if *time < Duration::minutes(3) {
433 0.4
434 } else if *time < Duration::minutes(4) {
435 0.6
436 } else if *time < Duration::minutes(5) {
437 0.8
438 } else {
439 1.0
440 };
441 let color = app.cs.good_to_bad_red.eval(scale);
442 let road = app.per_map.map.get_r(*r);
443 batch.push(color, road.get_thick_polygon());
444
445 for i in [road.src_i, road.dst_i] {
448 if drawn_intersections.contains(&i) {
449 continue;
450 }
451 drawn_intersections.insert(i);
452 batch.push(color, app.per_map.map.get_i(i).polygon.clone());
453 }
454 }
455 (ctx.upload(batch), cost_per_node)
456}