ltn/pages/design_ltn/
filters.rs

1use map_model::{DiagonalFilter, FilterType, RoadFilter};
2use widgetry::mapspace::{World, WorldOutcome};
3use widgetry::tools::open_browser;
4use widgetry::{lctrl, EventCtx, Key, Text, Transition};
5
6use super::{modals, road_name, EditOutcome, Obj};
7use crate::render::colors;
8use crate::{redraw_all_icons, App, Neighbourhood};
9
10/// Creates clickable objects for managing filters on roads and intersections. Everything is
11/// invisible; the caller is responsible for drawing things.
12pub fn make_world(ctx: &mut EventCtx, app: &App, neighbourhood: &Neighbourhood) -> World<Obj> {
13    let map = &app.per_map.map;
14    let mut world = World::new();
15
16    for r in &neighbourhood.interior_roads {
17        let road = map.get_r(*r);
18        world
19            .add(Obj::Road(*r))
20            .hitbox(road.get_thick_polygon())
21            .drawn_in_master_batch()
22            .hover_color(colors::HOVER)
23            .tooltip(Text::from(format!(
24                "{} possible shortcuts cross {}",
25                neighbourhood.shortcuts.count_per_road.get(*r),
26                road_name(app, road)
27            )))
28            .hotkey(lctrl(Key::D), "debug")
29            .clickable()
30            .build(ctx);
31    }
32
33    for i in &neighbourhood.interior_intersections {
34        world
35            .add(Obj::Intersection(*i))
36            .hitbox(map.get_i(*i).polygon.clone())
37            .drawn_in_master_batch()
38            .hover_color(colors::HOVER)
39            .tooltip(Text::from(format!(
40                "{} possible shortcuts cross this intersection",
41                neighbourhood.shortcuts.count_per_intersection.get(*i)
42            )))
43            .clickable()
44            .hotkey(lctrl(Key::D), "debug")
45            .build(ctx);
46    }
47
48    world.initialize_hover(ctx);
49    world
50}
51
52pub fn handle_world_outcome(
53    ctx: &mut EventCtx,
54    app: &mut App,
55    outcome: WorldOutcome<Obj>,
56) -> EditOutcome {
57    let map = &app.per_map.map;
58    match outcome {
59        WorldOutcome::ClickedObject(Obj::Road(r)) => {
60            let road = map.get_r(r);
61            // The world doesn't contain non-driveable roads, so no need to check for that error
62            if road.is_deadend_for_driving(map) {
63                return EditOutcome::error(ctx, "You can't filter a dead-end");
64            }
65
66            // Place the filter on the part of the road that was clicked
67            // These calls shouldn't fail -- since we clicked a road, the cursor must be in
68            // map-space. And project_pt returns a point that's guaranteed to be on the polyline.
69            let cursor_pt = ctx.canvas.get_cursor_in_map_space().unwrap();
70            let pt_on_line = road.center_pts.project_pt(cursor_pt);
71            let (distance, _) = road.center_pts.dist_along_of_point(pt_on_line).unwrap();
72
73            if road.oneway_for_driving().is_some() {
74                if app.session.layers.autofix_one_ways {
75                    modals::fix_oneway_and_add_filter(ctx, app, &[(r, distance)]);
76                    return EditOutcome::UpdateAll;
77                }
78
79                return EditOutcome::Transition(Transition::Push(
80                    modals::ResolveOneWayAndFilter::new_state(ctx, vec![(r, distance)]),
81                ));
82            }
83
84            let mut edits = map.get_edits().clone();
85            if map.get_r(r).modal_filter.is_some() {
86                edits.commands.push(map.edit_road_cmd(r, |new| {
87                    new.modal_filter = None;
88                }));
89            } else {
90                let mut filter_type = app.session.filter_type;
91
92                if filter_type != FilterType::BusGate
93                    && !app.per_map.map.get_bus_routes_on_road(r).is_empty()
94                {
95                    if app.session.layers.autofix_bus_gates {
96                        filter_type = FilterType::BusGate;
97                    } else {
98                        // If we have a one-way bus route, the one-way resolver will win and we
99                        // won't warn about bus gates. Oh well.
100                        return EditOutcome::Transition(Transition::Push(
101                            modals::ResolveBusGate::new_state(ctx, app, vec![(r, distance)]),
102                        ));
103                    }
104                }
105
106                edits.commands.push(map.edit_road_cmd(r, |new| {
107                    new.modal_filter = Some(RoadFilter::new(distance, filter_type));
108                }));
109            }
110            app.apply_edits(edits);
111            redraw_all_icons(ctx, app);
112            EditOutcome::UpdateAll
113        }
114        WorldOutcome::ClickedObject(Obj::Intersection(i)) => {
115            let mut edits = map.get_edits().clone();
116            edits
117                .commands
118                .extend(DiagonalFilter::cycle_through_alternatives(
119                    &app.per_map.map,
120                    i,
121                    app.session.filter_type,
122                ));
123            app.apply_edits(edits);
124            redraw_all_icons(ctx, app);
125            EditOutcome::UpdateAll
126        }
127        WorldOutcome::Keypress("debug", Obj::Intersection(i)) => {
128            open_browser(app.per_map.map.get_i(i).orig_id.to_string());
129            EditOutcome::Nothing
130        }
131        WorldOutcome::Keypress("debug", Obj::Road(r)) => {
132            open_browser(app.per_map.map.get_r(r).orig_id.osm_way_id.to_string());
133            EditOutcome::Nothing
134        }
135        _ => EditOutcome::Nothing,
136    }
137}