map_model/edits/
perma.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5
6use abstio::MapName;
7use abstutil::{deserialize_btreemap, serialize_btreemap};
8use geom::Time;
9
10use super::perma_traffic_signal;
11use crate::edits::{EditCmd, EditIntersection, EditIntersectionControl, EditRoad, MapEdits};
12use crate::{
13    osm, ControlStopSign, DiagonalFilter, IntersectionID, Map, MovementID, OriginalRoad, TurnType,
14};
15
16// Manually change this to attempt to preserve edits after major OSM updates.
17const IGNORE_OLD_LANES: bool = false;
18
19/// MapEdits are converted to this before serializing. Referencing things like LaneID in a Map won't
20/// work if the basemap is rebuilt from new OSM data, so instead we use stabler OSM IDs that're less
21/// likely to change.
22#[derive(Serialize, Deserialize, Clone)]
23pub struct PermanentMapEdits {
24    pub map_name: MapName,
25    pub edits_name: String,
26    pub version: usize,
27    commands: Vec<PermanentEditCmd>,
28
29    /// Edits without these are player generated.
30    pub proposal_description: Vec<String>,
31    /// The link is optional even for proposals
32    pub proposal_link: Option<String>,
33}
34
35#[derive(Serialize, Deserialize, Clone)]
36pub struct PermanentEditIntersection {
37    control: PermanentEditIntersectionControl,
38    modal_filter: Option<DiagonalFilter>,
39    #[serde(
40        serialize_with = "serialize_btreemap",
41        deserialize_with = "deserialize_btreemap"
42    )]
43    crosswalks: BTreeMap<perma_traffic_signal::Turn, TurnType>,
44}
45
46#[derive(Serialize, Deserialize, Clone)]
47pub enum PermanentEditIntersectionControl {
48    StopSign {
49        #[serde(
50            serialize_with = "serialize_btreemap",
51            deserialize_with = "deserialize_btreemap"
52        )]
53        must_stop: BTreeMap<OriginalRoad, bool>,
54    },
55    TrafficSignal(perma_traffic_signal::TrafficSignal),
56    Closed,
57}
58
59#[allow(clippy::enum_variant_names)]
60#[derive(Serialize, Deserialize, Clone)]
61pub enum PermanentEditCmd {
62    ChangeRoad {
63        r: OriginalRoad,
64        new: EditRoad,
65        old: EditRoad,
66    },
67    ChangeIntersection {
68        i: osm::NodeID,
69        new: PermanentEditIntersection,
70        old: PermanentEditIntersection,
71    },
72    ChangeRouteSchedule {
73        gtfs_id: String,
74        old: Vec<Time>,
75        new: Vec<Time>,
76    },
77}
78
79impl EditCmd {
80    pub fn to_perma(&self, map: &Map) -> PermanentEditCmd {
81        match self {
82            EditCmd::ChangeRoad { r, new, old } => PermanentEditCmd::ChangeRoad {
83                r: map.get_r(*r).orig_id,
84                new: new.clone(),
85                old: old.clone(),
86            },
87            EditCmd::ChangeIntersection { i, new, old } => PermanentEditCmd::ChangeIntersection {
88                i: map.get_i(*i).orig_id,
89                new: new.to_permanent(map),
90                old: old.to_permanent(map),
91            },
92            EditCmd::ChangeRouteSchedule { id, old, new } => {
93                PermanentEditCmd::ChangeRouteSchedule {
94                    gtfs_id: map.get_tr(*id).gtfs_id.clone(),
95                    old: old.clone(),
96                    new: new.clone(),
97                }
98            }
99        }
100    }
101}
102
103impl PermanentEditCmd {
104    pub fn into_cmd(self, map: &Map) -> Result<EditCmd> {
105        match self {
106            PermanentEditCmd::ChangeRoad { r, new, old } => {
107                let id = map.find_r_by_osm_id(r)?;
108                let num_current = map.get_r(id).lanes.len();
109                // The basemap changed -- it'd be pretty hard to understand the original
110                // intent of the edit.
111                if num_current != old.lanes_ltr.len() {
112                    if IGNORE_OLD_LANES {
113                        warn!("Lanes in {r} have changed since the edits, but keeping the edits anyway");
114                        return Ok(EditCmd::ChangeRoad {
115                            r: id,
116                            new,
117                            // Note we change 'old' to match the current basemap
118                            old: EditRoad::get_orig_from_osm(map.get_r(id), map.get_config()),
119                        });
120                    } else {
121                        bail!(
122                            "number of lanes in {} is {} now, but {} in the edits",
123                            r,
124                            num_current,
125                            old.lanes_ltr.len()
126                        );
127                    }
128                }
129                Ok(EditCmd::ChangeRoad { r: id, new, old })
130            }
131            PermanentEditCmd::ChangeIntersection { i, new, old } => {
132                let id = map.find_i_by_osm_id(i)?;
133                Ok(EditCmd::ChangeIntersection {
134                    i: id,
135                    new: new
136                        .with_permanent(id, map)
137                        .with_context(|| format!("new ChangeIntersection of {} invalid", i))?,
138                    old: old
139                        .with_permanent(id, map)
140                        .with_context(|| format!("old ChangeIntersection of {} invalid", i))?,
141                })
142            }
143            PermanentEditCmd::ChangeRouteSchedule { gtfs_id, old, new } => {
144                let id = map
145                    .find_tr_by_gtfs(&gtfs_id)
146                    .ok_or_else(|| anyhow!("can't find {}", gtfs_id))?;
147                Ok(EditCmd::ChangeRouteSchedule { id, old, new })
148            }
149        }
150    }
151}
152
153impl MapEdits {
154    /// Encode the edits in a permanent format, referring to more-stable OSM IDs.
155    pub fn to_permanent(&self, map: &Map) -> PermanentMapEdits {
156        PermanentMapEdits {
157            map_name: map.get_name().clone(),
158            edits_name: self.edits_name.clone(),
159            // Increase this every time there's a schema change
160            version: 13,
161            proposal_description: self.proposal_description.clone(),
162            proposal_link: self.proposal_link.clone(),
163            commands: self.commands.iter().map(|cmd| cmd.to_perma(map)).collect(),
164        }
165    }
166}
167
168impl PermanentMapEdits {
169    /// Transform permanent edits to MapEdits, looking up the map IDs by the hopefully stabler OSM
170    /// IDs. Validate that the basemap hasn't changed in important ways.
171    pub fn into_edits(self, map: &Map) -> Result<MapEdits> {
172        let mut edits = MapEdits {
173            edits_name: self.edits_name,
174            proposal_description: self.proposal_description,
175            proposal_link: self.proposal_link,
176            commands: self
177                .commands
178                .into_iter()
179                .map(|cmd| cmd.into_cmd(map))
180                .collect::<Result<Vec<EditCmd>>>()?,
181
182            original_roads: BTreeMap::new(),
183            original_intersections: BTreeMap::new(),
184            changed_routes: BTreeSet::new(),
185        };
186        edits.update_derived(map);
187        Ok(edits)
188    }
189
190    /// Transform permanent edits to MapEdits, looking up the map IDs by the hopefully stabler OSM
191    /// IDs. Strip out commands that're broken, but log warnings.
192    pub fn into_edits_permissive(self, map: &Map) -> MapEdits {
193        let mut edits = MapEdits {
194            edits_name: self.edits_name,
195            proposal_description: self.proposal_description,
196            proposal_link: self.proposal_link,
197            commands: self
198                .commands
199                .into_iter()
200                .filter_map(|cmd| match cmd.into_cmd(map) {
201                    Ok(cmd) => Some(cmd),
202                    Err(err) => {
203                        warn!("Skipping broken command: {}", err);
204                        None
205                    }
206                })
207                .collect(),
208
209            original_roads: BTreeMap::new(),
210            original_intersections: BTreeMap::new(),
211            changed_routes: BTreeSet::new(),
212        };
213        edits.update_derived(map);
214        edits
215    }
216
217    /// Get the human-friendly of these edits. If they have a description, the first line is the
218    /// title. Otherwise we use the filename.
219    pub fn get_title(&self) -> &str {
220        if self.proposal_description.is_empty() {
221            &self.edits_name
222        } else {
223            &self.proposal_description[0]
224        }
225    }
226}
227
228impl EditIntersection {
229    fn to_permanent(&self, map: &Map) -> PermanentEditIntersection {
230        PermanentEditIntersection {
231            control: match self.control {
232                EditIntersectionControl::StopSign(ref ss) => {
233                    PermanentEditIntersectionControl::StopSign {
234                        must_stop: ss
235                            .roads
236                            .iter()
237                            .map(|(r, val)| (map.get_r(*r).orig_id, val.must_stop))
238                            .collect(),
239                    }
240                }
241                EditIntersectionControl::TrafficSignal(ref raw_ts) => {
242                    PermanentEditIntersectionControl::TrafficSignal(raw_ts.clone())
243                }
244                EditIntersectionControl::Closed => PermanentEditIntersectionControl::Closed,
245            },
246            // TODO This uses local map IDs, not even OSM IDs. Inconsistent with PermanentMapEdits,
247            // but this should all get overhauled "soon" to be GeoJSON and reference no IDs at all.
248            modal_filter: self.modal_filter.clone(),
249            crosswalks: self
250                .crosswalks
251                .iter()
252                .map(|(id, turn_type)| (id.to_movement(map).to_permanent(map), *turn_type))
253                .collect(),
254        }
255    }
256}
257
258impl PermanentEditIntersection {
259    fn with_permanent(self, i: IntersectionID, map: &Map) -> Result<EditIntersection> {
260        let control = match self.control {
261            PermanentEditIntersectionControl::StopSign { must_stop } => {
262                let mut translated_must_stop = BTreeMap::new();
263                for (r, stop) in must_stop {
264                    translated_must_stop.insert(map.find_r_by_osm_id(r)?, stop);
265                }
266
267                // Make sure the roads exactly match up
268                let mut ss = ControlStopSign::new(map, i);
269                if translated_must_stop.len() != ss.roads.len() {
270                    bail!(
271                        "Stop sign has {} roads now, but {} from edits",
272                        ss.roads.len(),
273                        translated_must_stop.len()
274                    );
275                }
276                for (r, stop) in translated_must_stop {
277                    if let Some(road) = ss.roads.get_mut(&r) {
278                        road.must_stop = stop;
279                    } else {
280                        bail!("{} doesn't connect to {}", i, r);
281                    }
282                }
283
284                EditIntersectionControl::StopSign(ss)
285            }
286            PermanentEditIntersectionControl::TrafficSignal(ts) => {
287                EditIntersectionControl::TrafficSignal(ts)
288            }
289            PermanentEditIntersectionControl::Closed => EditIntersectionControl::Closed,
290        };
291
292        let mut crosswalks = BTreeMap::new();
293        for (id, turn_type) in self.crosswalks {
294            let movement = MovementID::from_permanent(id, map)?;
295            // Find all TurnIDs that map to this MovementID
296            let mut turn_ids = Vec::new();
297            for turn in &map.get_i(i).turns {
298                if turn.id.to_movement(map) == movement {
299                    turn_ids.push(turn.id);
300                }
301            }
302            if turn_ids.len() != 1 {
303                bail!(
304                    "{:?} didn't map to exactly 1 crossing turn: {:?}",
305                    movement,
306                    turn_ids
307                );
308            }
309            crosswalks.insert(turn_ids.pop().unwrap(), turn_type);
310        }
311
312        Ok(EditIntersection {
313            control,
314            // TODO Express as GeoJSON
315            modal_filter: self.modal_filter.clone(),
316            crosswalks,
317        })
318    }
319}