ltn/save/
perma.rs

1//! TODO All of this is in flux. Ultimately we should "just" use MapEdits, and squeeze partitioning
2//! into that somehow.
3//!
4//! In the meantime, use the existing PermanentMapEdits structure. For partitioning, use "runtime
5//! reflection" magic to transform RoadIDs to OriginalRoads. Defining parallel structures manually
6//! would be too tedious.
7//!
8//! 1) Serialize the Partitioning with RoadIDs to JSON
9//! 2) Dynamically walk the JSON
10//! 3) When the path of a value matches the hardcoded list of patterns in is_road_id, transform
11//!    to a permanent ID
12//! 4) Save the proposal as JSON with that ID instead
13//! 5) Do the inverse to later load
14//!
15//! In practice, this attempt to keep proposals compatible with future basemap updates might be
16//! futile. We're embedding loads of details about the partitioning, but not checking that they
17//! remain valid after loading. Even splitting one road in two anywhere in the map would likely
18//! break things kind of silently.
19//!
20//! Also, the JSON blobs are massive because of the partitioning, so compress everything.
21
22use anyhow::Result;
23use lazy_static::lazy_static;
24use regex::Regex;
25use serde_json::Value;
26
27use map_model::{Map, OriginalRoad, PermanentMapEdits, RoadID};
28
29use super::Proposal;
30use crate::save::Partitioning;
31
32pub fn to_permanent(map: &Map, proposal: &Proposal) -> Result<Value> {
33    let mut proposal_value = serde_json::to_value(proposal.edits.to_permanent(map))?;
34
35    // Now handle partitioning
36    let mut partitioning_value = serde_json::to_value(&proposal.partitioning)?;
37    walk("", &mut partitioning_value, &|path, value| {
38        if is_road_id(path) {
39            let replace_with = map.get_r(RoadID(value.as_u64().unwrap() as usize)).orig_id;
40            *value = serde_json::to_value(&replace_with)?;
41        }
42        Ok(())
43    })?;
44
45    proposal_value
46        .as_object_mut()
47        .unwrap()
48        .insert("partitioning".to_string(), partitioning_value);
49    Ok(proposal_value)
50}
51
52pub fn from_permanent(map: &Map, mut proposal_value: Value) -> Result<Proposal> {
53    // Handle partitioning first
54    let mut partitioning_value = proposal_value
55        .as_object_mut()
56        .unwrap()
57        .remove("partitioning")
58        .unwrap();
59    walk("", &mut partitioning_value, &|path, value| {
60        if is_road_id(path) {
61            let orig_id: OriginalRoad = serde_json::from_value(value.clone())?;
62            let replace_with = map.find_r_by_osm_id(orig_id)?;
63            *value = serde_json::to_value(&replace_with)?;
64        }
65        Ok(())
66    })?;
67    let partitioning: Partitioning = serde_json::from_value(partitioning_value)?;
68
69    // TODO This repeats a bit of MapEdits code, because we're starting from a Value
70    // TODO And it skips the compat code
71    let perma_edits: PermanentMapEdits = serde_json::from_value(proposal_value)?;
72    let edits = perma_edits.into_edits_permissive(map);
73
74    Ok(Proposal {
75        edits,
76        partitioning,
77    })
78}
79
80fn is_road_id(path: &str) -> bool {
81    lazy_static! {
82        static ref PATTERNS: Vec<Regex> = vec![
83            // First place a Block is stored
84            Regex::new(r"^/partitioning/single_blocks/\d+/perimeter/interior/\d+$").unwrap(),
85            Regex::new(r"^/partitioning/single_blocks/\d+/perimeter/roads/\d+/road$").unwrap(),
86            // The other
87            Regex::new(r"^/partitioning/neighbourhoods/\d+/0/perimeter/interior/\d+$").unwrap(),
88            Regex::new(r"^/partitioning/neighbourhoods/\d+/0/perimeter/roads/\d+/road$").unwrap(),
89        ];
90    }
91
92    PATTERNS.iter().any(|re| re.is_match(path))
93}
94
95// Note there's no chance to transform keys in a map. So use serialize_btreemap elsewhere to force
96// into a list of pairs
97fn walk<F: Fn(&str, &mut Value) -> Result<()>>(
98    path: &str,
99    value: &mut Value,
100    transform: &F,
101) -> Result<()> {
102    match value {
103        Value::Array(list) => {
104            for (idx, x) in list.into_iter().enumerate() {
105                walk(&format!("{}/{}", path, idx), x, transform)?;
106            }
107            transform(path, value)?;
108        }
109        Value::Object(map) => {
110            for (key, val) in map {
111                walk(&format!("{}/{}", path, key), val, transform)?;
112            }
113            // After recursing, possibly transform this. We turn a number into an object, so to
114            // reverse that...
115            transform(path, value)?;
116        }
117        _ => {
118            transform(path, value)?;
119            // The value may have been transformed into an array or object, but don't walk it.
120        }
121    }
122    Ok(())
123}