1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
//! The Proposal struct references IntersectionIDs and RoadIDs, which won't survive OSM updates.
//! Similar to the MapEdits <-> PermanentMapEdits strategy, transform those IDs before saving.
//!
//! Unlike PermanentMapEdits, we don't define a PermanentProposal struct, because to do so for
//! everything it includes would be a nightmare. In particular, Partitioning includes Blocks, which
//! nest RoadIDs deep inside. Instead, play a "runtime reflection" trick:
//!
//! 1) Serialize the Proposal with RoadIDs to JSON
//! 2) Dynamically walk the JSON
//! 3) When the path of a value matches the hardcoded list of patterns in is_road_id and
//!    is_intersection_id, transform to a permanent ID
//! 4) Save the proposal as JSON with that ID instead
//! 5) Do the inverse to later load
//!
//! In practice, this attempt to keep proposals compatible with future basemap updates might be
//! futile. We're embedding loads of details about the partitioning, but not checking that they
//! remain valid after loading. Even splitting one road in two anywhere in the map would likely
//! break things kind of silently. Absolute worst case, we also record an abst_version field so we
//! could manually load the proposal in the correct version, and do something to manually recover
//! an old proposal.
//!
//! Also, the JSON blobs are massive because of the partitioning, so compress everything.

use anyhow::Result;
use lazy_static::lazy_static;
use regex::Regex;
use serde_json::Value;

use map_model::{IntersectionID, Map, RoadID};
use raw_map::osm::NodeID;
use raw_map::OriginalRoad;

use super::Proposal;

pub fn to_permanent(map: &Map, proposal: &Proposal) -> Result<Value> {
    let mut proposal_value = serde_json::to_value(proposal)?;
    walk("", &mut proposal_value, &|path, value| {
        if is_road_id(path) {
            let replace_with = map.get_r(RoadID(value.as_u64().unwrap() as usize)).orig_id;
            *value = serde_json::to_value(&replace_with)?;
        } else if is_intersection_id(path) {
            let replace_with = map
                .get_i(IntersectionID(value.as_u64().unwrap() as usize))
                .orig_id;
            *value = serde_json::to_value(&replace_with)?;
        }
        Ok(())
    })?;
    Ok(proposal_value)
}

pub fn from_permanent(map: &Map, mut proposal_value: Value) -> Result<Proposal> {
    walk("", &mut proposal_value, &|path, value| {
        if is_road_id(path) {
            let orig_id: OriginalRoad = serde_json::from_value(value.clone())?;
            let replace_with = map.find_r_by_osm_id(orig_id)?;
            *value = serde_json::to_value(&replace_with)?;
        } else if is_intersection_id(path) {
            let orig_id: NodeID = serde_json::from_value(value.clone())?;
            let replace_with = map.find_i_by_osm_id(orig_id)?;
            *value = serde_json::to_value(&replace_with)?;
        }
        Ok(())
    })?;
    let result = serde_json::from_value(proposal_value)?;
    Ok(result)
}

fn is_road_id(path: &str) -> bool {
    lazy_static! {
        static ref PATTERNS: Vec<Regex> = vec![
            Regex::new(r"^/modal_filters/roads/\d+/0$").unwrap(),
            Regex::new(r"^/modal_filters/intersections/\d+/1/r1$").unwrap(),
            Regex::new(r"^/modal_filters/intersections/\d+/1/r2$").unwrap(),
            Regex::new(r"^/modal_filters/intersections/\d+/1/group1/y$").unwrap(),
            Regex::new(r"^/modal_filters/intersections/\d+/1/group2/y$").unwrap(),
            // First place a Block is stored
            Regex::new(r"^/partitioning/single_blocks/\d+/perimeter/interior/\d+$").unwrap(),
            Regex::new(r"^/partitioning/single_blocks/\d+/perimeter/roads/\d+/road$").unwrap(),
            // The other
            Regex::new(r"^/partitioning/neighborhoods/\d+/0/perimeter/interior/\d+$").unwrap(),
            Regex::new(r"^/partitioning/neighborhoods/\d+/0/perimeter/roads/\d+/road$").unwrap(),
        ];
    }

    PATTERNS.iter().any(|re| re.is_match(path))
}

fn is_intersection_id(path: &str) -> bool {
    lazy_static! {
        static ref PATTERNS: Vec<Regex> = vec![
            Regex::new(r"^/modal_filters/intersections/\d+/0$").unwrap(),
            Regex::new(r"^/modal_filters/intersections/\d+/1/i$").unwrap(),
        ];
    }

    PATTERNS.iter().any(|re| re.is_match(path))
}

// Note there's no chance to transform keys in a map. So use serialize_btreemap elsewhere to force
// into a list of pairs
fn walk<F: Fn(&str, &mut Value) -> Result<()>>(
    path: &str,
    value: &mut Value,
    transform: &F,
) -> Result<()> {
    match value {
        Value::Array(list) => {
            for (idx, x) in list.into_iter().enumerate() {
                walk(&format!("{}/{}", path, idx), x, transform)?;
            }
            transform(path, value)?;
        }
        Value::Object(map) => {
            for (key, val) in map {
                walk(&format!("{}/{}", path, key), val, transform)?;
            }
            // After recursing, possibly transform this. We turn a number into an object, so to
            // reverse that...
            transform(path, value)?;
        }
        _ => {
            transform(path, value)?;
            // The value may have been transformed into an array or object, but don't walk it.
        }
    }
    Ok(())
}