game/edit/
validate.rs

1use std::collections::BTreeSet;
2
3use abstutil::Timer;
4use map_model::{connectivity, Direction, DrivingSide, EditCmd, Map, PathConstraints};
5use widgetry::tools::PopupMsg;
6use widgetry::{EventCtx, State};
7
8use crate::app::App;
9
10// Some of these take a candidate EditCmd to do, then see if it's valid. If they return None, it's
11// fine. They always leave the map in the original state without the new EditCmd.
12
13// Could be caused by closing intersections
14pub fn check_sidewalk_connectivity(
15    ctx: &mut EventCtx,
16    app: &mut App,
17    cmd: EditCmd,
18) -> Option<Box<dyn State<App>>> {
19    let orig_edits = app.primary.map.get_edits().clone();
20    let (_, disconnected_before) =
21        connectivity::find_scc(&app.primary.map, PathConstraints::Pedestrian);
22
23    let mut edits = orig_edits.clone();
24    edits.commands.push(cmd);
25    app.primary
26        .map
27        .try_apply_edits(edits, &mut Timer::throwaway());
28
29    let (_, disconnected_after) =
30        connectivity::find_scc(&app.primary.map, PathConstraints::Pedestrian);
31    app.primary
32        .map
33        .must_apply_edits(orig_edits, &mut Timer::throwaway());
34
35    let newly_disconnected = disconnected_after
36        .difference(&disconnected_before)
37        .collect::<Vec<_>>();
38    if newly_disconnected.is_empty() {
39        return None;
40    }
41
42    // TODO Think through a proper UI for showing editing errors to the user and letting them
43    // understand the problem. We used to just draw problems in red and mostly cover it up with the
44    // popup.
45    Some(PopupMsg::new_state(
46        ctx,
47        "Error",
48        vec![format!(
49            "Can't close this intersection; {} sidewalks disconnected",
50            newly_disconnected.len()
51        )],
52    ))
53}
54
55#[allow(unused)]
56// Could be caused by closing intersections, changing lane types, or reversing lanes
57pub fn check_blackholes(
58    ctx: &mut EventCtx,
59    app: &mut App,
60    cmd: EditCmd,
61) -> Option<Box<dyn State<App>>> {
62    let orig_edits = app.primary.map.get_edits().clone();
63    let mut driving_ok_originally = BTreeSet::new();
64    let mut biking_ok_originally = BTreeSet::new();
65    for l in app.primary.map.all_lanes() {
66        if !l.driving_blackhole {
67            driving_ok_originally.insert(l.id);
68        }
69        if !l.biking_blackhole {
70            biking_ok_originally.insert(l.id);
71        }
72    }
73
74    let mut edits = orig_edits.clone();
75    edits.commands.push(cmd);
76    app.primary
77        .map
78        .try_apply_edits(edits, &mut Timer::throwaway());
79
80    let mut newly_disconnected = BTreeSet::new();
81    for l in connectivity::find_scc(&app.primary.map, PathConstraints::Car).1 {
82        if driving_ok_originally.contains(&l) {
83            newly_disconnected.insert(l);
84        }
85    }
86    for l in connectivity::find_scc(&app.primary.map, PathConstraints::Bike).1 {
87        if biking_ok_originally.contains(&l) {
88            newly_disconnected.insert(l);
89        }
90    }
91    app.primary
92        .map
93        .must_apply_edits(orig_edits, &mut Timer::throwaway());
94
95    if newly_disconnected.is_empty() {
96        return None;
97    }
98
99    Some(PopupMsg::new_state(
100        ctx,
101        "Error",
102        vec![format!(
103            "{} lanes have been disconnected",
104            newly_disconnected.len()
105        )],
106    ))
107}
108
109/// Looks at all changed roads and makes sure sidewalk directions are correct -- this is easy for
110/// the user to mix up. Returns a list of new fixes to apply on top of the original edits.
111pub fn fix_sidewalk_direction(map: &Map) -> Vec<EditCmd> {
112    let mut fixes = Vec::new();
113    for cmd in &map.get_edits().commands {
114        if let EditCmd::ChangeRoad { r, new, .. } = cmd {
115            let mut fixed = new.clone();
116            if fixed.lanes_ltr[0].lt.is_walkable() {
117                fixed.lanes_ltr[0].dir = if map.get_config().driving_side == DrivingSide::Right {
118                    Direction::Back
119                } else {
120                    Direction::Fwd
121                };
122            }
123            if fixed.lanes_ltr.len() > 1 {
124                let last = fixed.lanes_ltr.last_mut().unwrap();
125                if last.lt.is_walkable() {
126                    last.dir = if map.get_config().driving_side == DrivingSide::Right {
127                        Direction::Fwd
128                    } else {
129                        Direction::Back
130                    };
131                }
132            }
133            if &fixed != new {
134                fixes.push(EditCmd::ChangeRoad {
135                    r: *r,
136                    old: new.clone(),
137                    new: fixed,
138                });
139            }
140        }
141    }
142    fixes
143}