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
16const IGNORE_OLD_LANES: bool = false;
18
19#[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 pub proposal_description: Vec<String>,
31 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 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 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(>fs_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 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 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 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 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 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 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 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 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 modal_filter: self.modal_filter.clone(),
316 crosswalks,
317 })
318 }
319}