ltn/pages/design_ltn/
turn_restrictions.rs

1use geom::Distance;
2use map_model::{EditRoad, Map, RoadID};
3use osm2streets::RestrictionType;
4use widgetry::mapspace::{World, WorldOutcome};
5use widgetry::Color;
6use widgetry::{EventCtx, GeomBatch, Text, TextExt, Widget};
7
8use super::{road_name, EditMode, EditOutcome, Obj};
9use crate::logic::turn_restrictions::{
10    possible_destination_roads, restricted_destination_roads, FocusedTurns,
11};
12use crate::render::colors;
13use crate::{App, Neighbourhood};
14
15pub fn widget(ctx: &mut EventCtx, app: &App, focus: Option<&FocusedTurns>) -> Widget {
16    match focus {
17        Some(focus) => {
18            let road = app.per_map.map.get_r(focus.from_r);
19            let restricted = focus.restricted_t.len();
20            let permitted = focus.possible_t.len() - restricted;
21            Widget::col(vec![
22                format!("{} permitted and {} restricted", permitted, restricted,).text_widget(ctx),
23                format!("turns from {} ", road.get_name(app.opts.language.as_ref()))
24                    .text_widget(ctx),
25                format!("at selected intersection").text_widget(ctx),
26            ])
27        }
28        None => Widget::nothing(),
29    }
30}
31
32pub fn make_world(
33    ctx: &mut EventCtx,
34    app: &App,
35    neighbourhood: &Neighbourhood,
36    focus: &Option<FocusedTurns>,
37) -> World<Obj> {
38    let mut world = World::new();
39
40    if focus.is_none() {
41        // Draw all roads as normal, with hoverover showing extant restrictions
42        for r in [
43            &neighbourhood.perimeter_roads,
44            &neighbourhood.interior_roads,
45            &neighbourhood.connected_exterior_roads,
46        ]
47        .into_iter()
48        .flatten()
49        {
50            build_turn_restriction_hover_geom(*r, ctx, &mut world, app);
51        }
52    } else {
53        let focused_t = focus.as_ref().unwrap();
54        // Draw FocusTurns
55        build_focused_turns_geom(focused_t, ctx, &mut world, app);
56        // Create hover geoms for each road connected to the FocusTurns
57        build_turn_options_geom(focused_t, ctx, &mut world, app);
58    }
59
60    world.initialize_hover(ctx);
61    world
62}
63
64/// Builds the hover geom for showing turn restrictions when no FocusTurns are selected
65fn build_turn_restriction_hover_geom(
66    r: RoadID,
67    ctx: &mut EventCtx,
68    world: &mut World<Obj>,
69    app: &App,
70) {
71    let map = &app.per_map.map;
72    let road = map.get_r(r);
73
74    // Because we have "possible" destinations (rather than just "permitted") we must draw possible_destinations
75    // first so that the distinct rendering of restricted_destinations shows on top of possible_destinations.
76    let restricted_destinations = restricted_destination_roads(map, r, None);
77    let possible_destinations = possible_destination_roads(map, road.id, None);
78
79    let mut hover_batch = GeomBatch::new();
80    // Create a single compound geometry which represents a Road *and its connected roads* and draw
81    // that geom as the mouseover geom for the Road. This avoids needing to update the representation of
82    // any Roads other then FocusedRoad.
83    // Add focus road segment itself
84    hover_batch.push(colors::HOVER, road.get_thick_polygon());
85
86    // Add possible destinations
87    for possible_r in possible_destinations.clone() {
88        let possible_road = map.get_r(possible_r);
89        hover_batch.push(
90            colors::TURN_PERMITTED_DESTINATION,
91            possible_road.get_thick_polygon(),
92        );
93    }
94
95    // Add restricted_destinations
96    for restricted_r in restricted_destinations.clone() {
97        let restricted_road = map.get_r(restricted_r);
98        hover_batch.push(
99            colors::TURN_RESTRICTED_DESTINATION,
100            restricted_road.get_thick_polygon(),
101        );
102    }
103
104    world
105        .add(Obj::Road(r))
106        .hitbox(road.get_thick_polygon())
107        .drawn_in_master_batch()
108        .draw_hovered(hover_batch)
109        .tooltip(Text::from(format!(
110            "Click to edit turn restrictions from {}",
111            road_name(app, road)
112        )))
113        .clickable()
114        .build(ctx);
115}
116
117/// Builds the geom representing the FocusTurns hull and intersection
118fn build_focused_turns_geom(
119    focused_t: &FocusedTurns,
120    ctx: &mut EventCtx,
121    world: &mut World<Obj>,
122    app: &App,
123) {
124    let map = &app.per_map.map;
125    let mut batch = GeomBatch::new();
126    let from_road = map.get_r(focused_t.from_r);
127
128    // Highlight the convex hull
129    batch.push(Color::grey(0.4).alpha(0.8), focused_t.hull.clone());
130
131    batch.push(
132        Color::grey(0.2),
133        focused_t.hull.to_outline(Distance::meters(2.0)),
134    );
135
136    // Highlight the selected intersection (the same color as the selected road)
137    batch.push(
138        colors::HOVER.alpha(1.0),
139        map.get_i(focused_t.i).polygon.clone(),
140    );
141    batch.push(
142        colors::HOVER.alpha(1.0),
143        map.get_i(focused_t.i)
144            .polygon
145            .to_outline(Distance::meters(3.0)),
146    );
147
148    // add the convex hull using the IntersectionID
149    world
150        .add(Obj::Intersection(focused_t.i))
151        .hitbox(focused_t.hull.clone())
152        .draw(batch.clone())
153        .draw_hovered(batch)
154        .zorder(1)
155        .tooltip(Text::from(format!(
156            "Edit restricted turn from {}",
157            road_name(app, from_road)
158        )))
159        .build(ctx);
160}
161
162/// Builds the geom representing each of the individual turn options (permitted and restricted)
163/// within the FocusTurns.
164fn build_turn_options_geom(
165    focused_t: &FocusedTurns,
166    ctx: &mut EventCtx,
167    world: &mut World<Obj>,
168    app: &App,
169) {
170    let map = &app.per_map.map;
171    let from_road = map.get_r(focused_t.from_r);
172    let from_road_name = road_name(app, from_road);
173
174    let mut batch = GeomBatch::new();
175    // Highlight the selected road
176    batch.push(
177        colors::HOVER.alpha(1.0),
178        from_road
179            .get_thick_polygon()
180            .to_outline(Distance::meters(3.0)),
181    );
182
183    world
184        .add(Obj::Road(focused_t.from_r))
185        .hitbox(from_road.get_thick_polygon())
186        .draw(batch.clone())
187        .draw_hovered(batch)
188        .zorder(2)
189        .tooltip(Text::from(format!(
190            "Edit restricted turn from {}",
191            road_name(app, from_road)
192        )))
193        .clickable()
194        .build(ctx);
195
196    // Highlight permitted destinations (Because we have "possible" but only what to show "permitted"
197    // we need to draw these first, and then "restricted" on top - with a higher z-order.)
198    for target_r in &focused_t.possible_t {
199        if !&focused_t.restricted_t.contains(target_r) && target_r != &focused_t.from_r {
200            build_individual_target_road_geom(
201                *target_r,
202                colors::TURN_PERMITTED_DESTINATION.alpha(1.0),
203                colors::TURN_RESTRICTED_DESTINATION.alpha(1.0),
204                3,
205                format!(
206                    "Add new turn restriction from '{}' to '{}'",
207                    from_road_name,
208                    road_name(app, map.get_r(*target_r))
209                ),
210                focused_t,
211                ctx,
212                world,
213                map,
214            );
215        }
216    }
217
218    // Highlight restricted destinations
219    for target_r in &focused_t.restricted_t {
220        build_individual_target_road_geom(
221            *target_r,
222            colors::TURN_RESTRICTED_DESTINATION.alpha(1.0),
223            colors::TURN_PERMITTED_DESTINATION.alpha(1.0),
224            4,
225            format!(
226                "Remove turn restriction from '{}' to '{}'",
227                from_road_name,
228                road_name(app, map.get_r(*target_r))
229            ),
230            focused_t,
231            ctx,
232            world,
233            map,
234        );
235    }
236}
237
238fn build_individual_target_road_geom(
239    target_r: RoadID,
240    before_edit_color: Color,
241    post_edit_color: Color,
242    z_order: usize,
243    tooltip: String,
244    focused_t: &FocusedTurns,
245    ctx: &mut EventCtx,
246    world: &mut World<Obj>,
247    map: &Map,
248) {
249    // Highlight restricted destinations
250    // Don't show U-Turns
251    if target_r == focused_t.from_r {
252        return;
253    }
254
255    let mut norm_batch = GeomBatch::new();
256    let mut hover_batch = GeomBatch::new();
257    let target_road = map.get_r(target_r);
258
259    norm_batch.push(
260        before_edit_color,
261        target_road
262            .get_thick_polygon()
263            .to_outline(Distance::meters(3.0)),
264    );
265    hover_batch.push(post_edit_color, target_road.get_thick_polygon());
266    world
267        .add(Obj::Road(target_r))
268        .hitbox(target_road.get_thick_polygon())
269        .draw(norm_batch)
270        .draw_hovered(hover_batch)
271        .zorder(z_order)
272        .tooltip(Text::from(tooltip))
273        .clickable()
274        .build(ctx);
275}
276
277pub fn handle_world_outcome(
278    ctx: &mut EventCtx,
279    app: &mut App,
280    outcome: WorldOutcome<Obj>,
281) -> EditOutcome {
282    match outcome {
283        WorldOutcome::ClickedObject(Obj::Road(r)) => {
284            // Check if the ClickedObject is already highlighted (ie there is a pre-existing FocusTurns)
285            // If so, then we recreate the FocusTurns with the relevant clicked_point (as this
286            //      is the easiest way to ensure the correct intersection is selected)
287            // If not and is one of the current restricted destination roads,
288            //      then we should remove that restricted turn
289            // If not and is one of the permitted destination roads,
290            //      then we should add that restricted turn
291            let cursor_pt = ctx.canvas.get_cursor_in_map_space().unwrap();
292            debug!("click point {:?}", cursor_pt);
293
294            if let EditMode::TurnRestrictions(ref prev_selection) = app.session.edit_mode {
295                if prev_selection.is_some() {
296                    let prev = prev_selection.as_ref().unwrap();
297                    if r == prev.from_r {
298                        debug!("The same road has been clicked on twice {:?}", r);
299                    } else if prev.restricted_t.contains(&r) || prev.possible_t.contains(&r) {
300                        let mut edits = app.per_map.map.get_edits().clone();
301                        // We are editing the previous road, not the most recently clicked road
302                        let erc = app.per_map.map.edit_road_cmd(prev.from_r, |new| {
303                            handle_edited_turn_restrictions(new, prev, r)
304                        });
305                        debug!("erc={:?}", erc);
306                        edits.commands.push(erc);
307                        app.apply_edits(edits);
308
309                        // Redraw the turn restriction symbols
310                        crate::redraw_all_icons(ctx, app);
311
312                        // Now clear the highlighted intersection/turns
313                        app.session.edit_mode = EditMode::TurnRestrictions(None);
314                        return EditOutcome::UpdateAll;
315                    } else {
316                        // Unreachable if the FocusTurns is is the only clickable objects in the world
317                        debug!(
318                            "Two difference roads have been clicked on prev={:?}, new {:?}",
319                            prev.from_r, r
320                        );
321                    }
322                } else {
323                    debug!("No previous road selected. New selection {:?}", r);
324                }
325            }
326
327            app.session.edit_mode =
328                EditMode::TurnRestrictions(Some(FocusedTurns::new(r, cursor_pt, &app.per_map.map)));
329            debug!(
330                "TURN RESTRICTIONS: handle_world_outcome - Clicked on Road {:?}",
331                r
332            );
333            EditOutcome::UpdatePanelAndWorld
334        }
335        WorldOutcome::ClickedFreeSpace(_) => {
336            app.session.edit_mode = EditMode::TurnRestrictions(None);
337            debug!("TURN RESTRICTIONS: handle_world_outcome - Clicked on FreeSpace");
338            EditOutcome::UpdatePanelAndWorld
339        }
340        _ => EditOutcome::Nothing,
341    }
342}
343
344pub fn handle_edited_turn_restrictions(new: &mut EditRoad, ft: &FocusedTurns, target_r: RoadID) {
345    if ft.restricted_t.contains(&target_r) {
346        debug!(
347            "Remove existing banned turn from src={:?}, to dst {:?}",
348            ft.from_r, target_r
349        );
350        new.turn_restrictions.retain(|(_, r)| *r != target_r);
351        new.complicated_turn_restrictions
352            .retain(|(_, r)| *r != target_r);
353    } else if ft.possible_t.contains(&target_r) {
354        debug!(
355            "Create new banned turn from src={:?}, to dst {:?}",
356            ft.from_r, target_r
357        );
358        new.turn_restrictions
359            .push((RestrictionType::BanTurns, target_r));
360    } else {
361        debug!(
362            "Nothing to change src={:?}, to dst {:?}",
363            ft.from_r, target_r
364        );
365    }
366}