map_model/edits/
mod.rs

1//! Once a Map exists, the player can edit it in the UI (producing `MapEdits` in-memory), then save
2//! the changes to a file (as `PermanentMapEdits`). See
3//! <https://a-b-street.github.io/docs/tech/map/edits.html>.
4
5use std::collections::{BTreeMap, BTreeSet};
6
7use anyhow::Result;
8use serde::{Deserialize, Serialize};
9
10use abstutil::Timer;
11use geom::{Speed, Time};
12use osm2streets::{get_lane_specs_ltr, RestrictionType};
13
14pub use self::perma::PermanentMapEdits;
15use crate::{
16    AccessRestrictions, ControlStopSign, ControlTrafficSignal, Crossing, DiagonalFilter,
17    IntersectionControl, IntersectionID, LaneID, LaneSpec, Map, MapConfig, ParkingLotID, Road,
18    RoadFilter, RoadID, TransitRouteID, TurnID, TurnType,
19};
20
21mod apply;
22mod compat;
23mod perma;
24pub mod perma_traffic_signal;
25
26/// Represents changes to a map. Note this isn't serializable -- that's what `PermanentMapEdits`
27/// does.
28#[derive(Debug, Clone, PartialEq)]
29pub struct MapEdits {
30    pub edits_name: String,
31    /// A stack, oldest edit is first. The same object may be edited multiple times in this stack,
32    /// until compress() happens.
33    pub commands: Vec<EditCmd>,
34
35    /// Derived from commands, kept up to date by update_derived
36    pub original_roads: BTreeMap<RoadID, EditRoad>,
37    pub original_intersections: BTreeMap<IntersectionID, EditIntersection>,
38    pub changed_routes: BTreeSet<TransitRouteID>,
39
40    /// Some edits are included in the game by default, in data/system/proposals, as "community
41    /// proposals." They require a description and may have a link to a write-up.
42    pub proposal_description: Vec<String>,
43    pub proposal_link: Option<String>,
44}
45
46#[derive(Debug, Clone, PartialEq)]
47pub enum EditCmd {
48    ChangeRoad {
49        r: RoadID,
50        old: EditRoad,
51        new: EditRoad,
52    },
53    ChangeIntersection {
54        i: IntersectionID,
55        new: EditIntersection,
56        old: EditIntersection,
57    },
58    ChangeRouteSchedule {
59        id: TransitRouteID,
60        old: Vec<Time>,
61        new: Vec<Time>,
62    },
63}
64
65pub struct EditEffects {
66    pub changed_roads: BTreeSet<RoadID>,
67    pub deleted_lanes: BTreeSet<LaneID>,
68    pub changed_intersections: BTreeSet<IntersectionID>,
69    // TODO Will we need modified turns?
70    pub added_turns: BTreeSet<TurnID>,
71    pub deleted_turns: BTreeSet<TurnID>,
72    pub changed_parking_lots: BTreeSet<ParkingLotID>,
73    modified_lanes: BTreeSet<LaneID>,
74}
75
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77pub struct EditRoad {
78    pub lanes_ltr: Vec<LaneSpec>,
79    pub speed_limit: Speed,
80    pub access_restrictions: AccessRestrictions,
81    pub modal_filter: Option<RoadFilter>,
82    pub crossings: Vec<Crossing>,
83    pub turn_restrictions: Vec<(RestrictionType, RoadID)>,
84    pub complicated_turn_restrictions: Vec<(RoadID, RoadID)>,
85}
86
87#[derive(Debug, Clone, PartialEq)]
88pub struct EditIntersection {
89    pub control: EditIntersectionControl,
90    pub modal_filter: Option<DiagonalFilter>,
91    /// This must contain all crossing turns at one intersection, each mapped either to Crosswalk
92    /// or UnmarkedCrossing
93    pub crosswalks: BTreeMap<TurnID, TurnType>,
94}
95
96#[derive(Debug, Clone, PartialEq)]
97pub enum EditIntersectionControl {
98    StopSign(ControlStopSign),
99    // Don't keep ControlTrafficSignal here, because it contains movements that should be
100    // generated after all lane edits are applied.
101    TrafficSignal(perma_traffic_signal::TrafficSignal),
102    Closed,
103}
104
105impl EditRoad {
106    pub fn get_orig_from_osm(r: &Road, cfg: &MapConfig) -> EditRoad {
107        EditRoad {
108            lanes_ltr: get_lane_specs_ltr(&r.osm_tags, cfg),
109            speed_limit: r.speed_limit_from_osm(),
110            access_restrictions: r.access_restrictions_from_osm(),
111            // TODO Port logic/existing_filters.rs here?
112            modal_filter: None,
113            // TODO From crossing_nodes?
114            crossings: Vec::new(),
115            // TODO - review this. When editing turn restrictions, within the LTN tool we do not
116            // use `get_orig_from_osm()`. The `EditRoad` is populated `map.get_r_edit()`.
117            // Therefore we just create empty vecs here for now.
118            // See https://github.com/a-b-street/abstreet/pull/1091#discussion_r1311717165
119            turn_restrictions: Vec::new(),
120            complicated_turn_restrictions: Vec::new(),
121        }
122    }
123
124    fn diff(&self, other: &EditRoad) -> Vec<String> {
125        #![allow(clippy::comparison_chain)]
126        let mut lt = 0;
127        let mut dir = 0;
128        let mut width = 0;
129        for (spec1, spec2) in self.lanes_ltr.iter().zip(other.lanes_ltr.iter()) {
130            if spec1.lt != spec2.lt {
131                lt += 1;
132            }
133            if spec1.dir != spec2.dir {
134                dir += 1;
135            }
136            if spec1.width != spec2.width {
137                width += 1;
138            }
139        }
140
141        let mut changes = Vec::new();
142        if lt == 1 {
143            changes.push("1 lane type".to_string());
144        } else if lt > 1 {
145            changes.push(format!("{} lane types", lt));
146        }
147        if dir == 1 {
148            changes.push("1 lane reversal".to_string());
149        } else if dir > 1 {
150            changes.push(format!("{} lane reversals", dir));
151        }
152        if width == 1 {
153            changes.push("1 lane width".to_string());
154        } else {
155            changes.push(format!("{} lane widths", width));
156        }
157        if self.speed_limit != other.speed_limit {
158            changes.push("speed limit".to_string());
159        }
160        if self.access_restrictions != other.access_restrictions {
161            changes.push("access restrictions".to_string());
162        }
163        if self.modal_filter != other.modal_filter {
164            changes.push("modal filter".to_string());
165        }
166        if self.crossings != other.crossings {
167            changes.push("crossings".to_string());
168        }
169        changes
170    }
171}
172
173impl EditIntersection {
174    fn diff(&self, other: &EditIntersection) -> Vec<String> {
175        let mut changes = Vec::new();
176        // TODO Could get more specific about changes to stop signs, traffic signals, etc
177        if self.control != other.control {
178            changes.push("control type".to_string());
179        }
180        if self.crosswalks != other.crosswalks {
181            changes.push("crosswalks".to_string());
182        }
183        if self.modal_filter != other.modal_filter {
184            changes.push("modal filter".to_string());
185        }
186        changes
187    }
188}
189
190impl MapEdits {
191    pub(crate) fn new() -> MapEdits {
192        MapEdits {
193            edits_name: "TODO temporary".to_string(),
194            proposal_description: Vec::new(),
195            proposal_link: None,
196            commands: Vec::new(),
197
198            original_roads: BTreeMap::new(),
199            original_intersections: BTreeMap::new(),
200            changed_routes: BTreeSet::new(),
201        }
202    }
203
204    /// Load map edits from a JSON file. Strip out any commands that're broken because they don't
205    /// match the current map. If the resulting edits are totally empty, consider that a failure --
206    /// the edits likely don't cover this map at all.
207    pub fn load_from_file(map: &Map, path: String, timer: &mut Timer) -> Result<MapEdits> {
208        let perma = match abstio::maybe_read_json::<PermanentMapEdits>(path.clone(), timer) {
209            Ok(perma) => perma,
210            Err(_) => {
211                // The JSON format may have changed, so attempt backwards compatibility.
212                let bytes = abstio::slurp_file(path)?;
213                let value = serde_json::from_slice(&bytes)?;
214                compat::upgrade(value, map)?
215            }
216        };
217
218        // Don't compare the full MapName; edits in one part of a city could apply to another. But
219        // make sure at least the city matches. Otherwise, we spend time trying to match up edits,
220        // and produce noisy logs along the way.
221        if map.get_name().city != perma.map_name.city {
222            bail!(
223                "Edits are for {:?}, but this map is {:?}",
224                perma.map_name.city,
225                map.get_name().city
226            );
227        }
228
229        let edits = perma.into_edits_permissive(map);
230        if edits.commands.is_empty() {
231            bail!("None of the edits apply to this map");
232        }
233        Ok(edits)
234    }
235
236    /// Load map edits from the given JSON bytes. Strip out any commands that're broken because
237    /// they don't match the current map. If the resulting edits are totally empty, consider that a
238    /// failure -- the edits likely don't cover this map at all.
239    pub fn load_from_bytes(map: &Map, bytes: Vec<u8>) -> Result<MapEdits> {
240        let perma = match abstutil::from_json::<PermanentMapEdits>(&bytes) {
241            Ok(perma) => perma,
242            Err(_) => {
243                // The JSON format may have changed, so attempt backwards compatibility.
244                let contents = std::str::from_utf8(&bytes)?;
245                let value = serde_json::from_str(contents)?;
246                compat::upgrade(value, map)?
247            }
248        };
249        let edits = perma.into_edits_permissive(map);
250        if edits.commands.is_empty() {
251            bail!("None of the edits apply to this map");
252        }
253        Ok(edits)
254    }
255
256    fn save(&self, map: &Map) {
257        // If untitled and empty, don't actually save anything.
258        if self.edits_name.starts_with("Untitled Proposal") && self.commands.is_empty() {
259            return;
260        }
261
262        abstio::write_json(
263            abstio::path_edits(map.get_name(), &self.edits_name),
264            &self.to_permanent(map),
265        );
266    }
267
268    fn update_derived(&mut self, map: &Map) {
269        self.original_roads.clear();
270        self.original_intersections.clear();
271        self.changed_routes.clear();
272
273        for cmd in &self.commands {
274            match cmd {
275                EditCmd::ChangeRoad { r, ref old, .. } => {
276                    if !self.original_roads.contains_key(r) {
277                        self.original_roads.insert(*r, old.clone());
278                    }
279                }
280                EditCmd::ChangeIntersection { i, ref old, .. } => {
281                    if !self.original_intersections.contains_key(i) {
282                        self.original_intersections.insert(*i, old.clone());
283                    }
284                }
285                EditCmd::ChangeRouteSchedule { id, .. } => {
286                    self.changed_routes.insert(*id);
287                }
288            }
289        }
290
291        self.original_roads
292            .retain(|r, orig| map.get_r_edit(*r) != orig.clone());
293        self.original_intersections
294            .retain(|i, orig| map.get_i_edit(*i) != orig.clone());
295        self.changed_routes.retain(|br| {
296            let r = map.get_tr(*br);
297            r.spawn_times != r.orig_spawn_times
298        });
299    }
300
301    /// Assumes update_derived has been called.
302    pub fn compress(&mut self, map: &Map) {
303        for (r, old) in &self.original_roads {
304            self.commands.push(EditCmd::ChangeRoad {
305                r: *r,
306                old: old.clone(),
307                new: map.get_r_edit(*r),
308            });
309        }
310        for (i, old) in &self.original_intersections {
311            self.commands.push(EditCmd::ChangeIntersection {
312                i: *i,
313                old: old.clone(),
314                new: map.get_i_edit(*i),
315            });
316        }
317        for r in &self.changed_routes {
318            let r = map.get_tr(*r);
319            self.commands.push(EditCmd::ChangeRouteSchedule {
320                id: r.id,
321                new: r.spawn_times.clone(),
322                old: r.orig_spawn_times.clone(),
323            });
324        }
325    }
326
327    /// Pick apart changed_roads and figure out if an entire road was edited, or just a few lanes.
328    /// Doesn't return deleted lanes.
329    pub fn changed_lanes(&self, map: &Map) -> (BTreeSet<LaneID>, BTreeSet<RoadID>) {
330        let mut lanes = BTreeSet::new();
331        let mut roads = BTreeSet::new();
332        for (r, orig) in &self.original_roads {
333            let r = map.get_r(*r);
334            // What exactly changed?
335            if r.speed_limit != orig.speed_limit
336                || r.access_restrictions != orig.access_restrictions
337                || r.modal_filter != orig.modal_filter
338                || r.crossings != orig.crossings
339                // If a lane was added or deleted, figuring out if any were modified is kind of
340                // unclear -- just mark the entire road.
341                || r.lanes.len() != orig.lanes_ltr.len()
342            {
343                roads.insert(r.id);
344            } else {
345                for (l, spec) in r.lanes.iter().zip(orig.lanes_ltr.iter()) {
346                    if l.dir != spec.dir || l.lane_type != spec.lt || l.width != spec.width {
347                        lanes.insert(l.id);
348                    }
349                }
350            }
351        }
352        (lanes, roads)
353    }
354
355    /// Produces an md5sum of the contents of the edits.
356    pub fn get_checksum(&self, map: &Map) -> String {
357        let bytes = abstutil::to_json(&self.to_permanent(map));
358        let mut context = md5::Context::new();
359        context.consume(&bytes);
360        format!("{:x}", context.compute())
361    }
362
363    /// Get the human-friendly of these edits. If they have a description, the first line is the
364    /// title. Otherwise we use the filename.
365    pub fn get_title(&self) -> &str {
366        if self.proposal_description.is_empty() {
367            &self.edits_name
368        } else {
369            &self.proposal_description[0]
370        }
371    }
372
373    pub fn is_crossing_modified(&self, r: RoadID, crossing: &Crossing) -> bool {
374        if let Some(orig) = self.original_roads.get(&r) {
375            !orig.crossings.contains(crossing)
376        } else {
377            // If this road isn't in original_roads at all, then nothing there has been modified
378            false
379        }
380    }
381    pub fn is_filter_modified(&self, r: RoadID, filter: &RoadFilter) -> bool {
382        if let Some(orig) = self.original_roads.get(&r) {
383            orig.modal_filter != Some(filter.clone())
384        } else {
385            false
386        }
387    }
388    pub fn is_diagonal_filter_modified(&self, i: IntersectionID, filter: &DiagonalFilter) -> bool {
389        if let Some(orig) = self.original_intersections.get(&i) {
390            orig.modal_filter != Some(filter.clone())
391        } else {
392            false
393        }
394    }
395}
396
397impl Default for MapEdits {
398    fn default() -> MapEdits {
399        MapEdits::new()
400    }
401}
402
403impl EditCmd {
404    /// (summary, details)
405    pub fn describe(&self, map: &Map) -> (String, Vec<String>) {
406        let mut details = Vec::new();
407        let summary = match self {
408            EditCmd::ChangeRoad { r, old, new } => {
409                details = new.diff(old);
410                format!("road #{}", r.0)
411            }
412            EditCmd::ChangeIntersection { i, old, new } => {
413                details = new.diff(old);
414                format!("intersection #{}", i.0)
415            }
416            EditCmd::ChangeRouteSchedule { id, .. } => {
417                format!("reschedule route {}", map.get_tr(*id).short_name)
418            }
419        };
420        (summary, details)
421    }
422}
423
424impl Map {
425    pub fn new_edits(&self) -> MapEdits {
426        let mut edits = MapEdits::new();
427
428        // Automatically find a new filename
429        let mut i = 1;
430        loop {
431            let name = format!("Untitled Proposal {}", i);
432            if !abstio::file_exists(abstio::path_edits(&self.name, &name)) {
433                edits.edits_name = name;
434                return edits;
435            }
436            i += 1;
437        }
438    }
439
440    pub fn get_edits(&self) -> &MapEdits {
441        &self.edits
442    }
443
444    pub fn unsaved_edits(&self) -> bool {
445        self.edits.edits_name.starts_with("Untitled Proposal") && !self.edits.commands.is_empty()
446    }
447
448    pub fn get_r_edit(&self, r: RoadID) -> EditRoad {
449        let r = self.get_r(r);
450        EditRoad {
451            lanes_ltr: r.lane_specs(),
452            speed_limit: r.speed_limit,
453            access_restrictions: r.access_restrictions.clone(),
454            modal_filter: r.modal_filter.clone(),
455            crossings: r.crossings.clone(),
456            turn_restrictions: r.turn_restrictions.clone(),
457            complicated_turn_restrictions: r.complicated_turn_restrictions.clone(),
458        }
459    }
460
461    pub fn edit_road_cmd<F: FnOnce(&mut EditRoad)>(&self, r: RoadID, f: F) -> EditCmd {
462        let old = self.get_r_edit(r);
463        let mut new = old.clone();
464        f(&mut new);
465        EditCmd::ChangeRoad { r, old, new }
466    }
467
468    pub fn get_i_edit(&self, i: IntersectionID) -> EditIntersection {
469        let i = self.get_i(i);
470        let control = match i.control {
471            IntersectionControl::Signed | IntersectionControl::Uncontrolled => {
472                EditIntersectionControl::StopSign(self.get_stop_sign(i.id).clone())
473            }
474            IntersectionControl::Signalled => {
475                EditIntersectionControl::TrafficSignal(self.get_traffic_signal(i.id).export(self))
476            }
477            IntersectionControl::Construction => EditIntersectionControl::Closed,
478        };
479        let mut crosswalks = BTreeMap::new();
480        for turn in &i.turns {
481            if turn.turn_type.pedestrian_crossing() {
482                crosswalks.insert(turn.id, turn.turn_type);
483            }
484        }
485        EditIntersection {
486            control,
487            modal_filter: i.modal_filter.clone(),
488            crosswalks,
489        }
490    }
491
492    pub fn edit_intersection_cmd<F: FnOnce(&mut EditIntersection)>(
493        &self,
494        i: IntersectionID,
495        f: F,
496    ) -> EditCmd {
497        let old = self.get_i_edit(i);
498        let mut new = old.clone();
499        f(&mut new);
500        EditCmd::ChangeIntersection { i, old, new }
501    }
502
503    pub fn save_edits(&self) {
504        // Don't overwrite the current edits with the compressed first. Otherwise, undo/redo order
505        // in the UI gets messed up.
506        let mut edits = self.edits.clone();
507        edits.commands.clear();
508        edits.compress(self);
509        edits.save(self);
510    }
511
512    /// Since the player is in the middle of editing, the signal may not be valid. Don't go through
513    /// the entire apply_edits flow.
514    pub fn incremental_edit_traffic_signal(&mut self, signal: ControlTrafficSignal) {
515        assert_eq!(
516            self.get_i(signal.id).control,
517            IntersectionControl::Signalled
518        );
519        self.traffic_signals.insert(signal.id, signal);
520    }
521
522    /// If you need to regenerate anything when the map is edited, use this key to detect edits.
523    pub fn get_edits_change_key(&self) -> usize {
524        self.edits_generation
525    }
526}