ltn/save/
perma.rs

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
//! TODO All of this is in flux. Ultimately we should "just" use MapEdits, and squeeze partitioning
//! into that somehow.
//!
//! In the meantime, use the existing PermanentMapEdits structure. For partitioning, use "runtime
//! reflection" magic to transform RoadIDs to OriginalRoads. Defining parallel structures manually
//! would be too tedious.
//!
//! 1) Serialize the Partitioning 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, 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.
//!
//! 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::{Map, OriginalRoad, PermanentMapEdits, RoadID};

use super::Proposal;
use crate::save::Partitioning;

pub fn to_permanent(map: &Map, proposal: &Proposal) -> Result<Value> {
    let mut proposal_value = serde_json::to_value(proposal.edits.to_permanent(map))?;

    // Now handle partitioning
    let mut partitioning_value = serde_json::to_value(&proposal.partitioning)?;
    walk("", &mut partitioning_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)?;
        }
        Ok(())
    })?;

    proposal_value
        .as_object_mut()
        .unwrap()
        .insert("partitioning".to_string(), partitioning_value);
    Ok(proposal_value)
}

pub fn from_permanent(map: &Map, mut proposal_value: Value) -> Result<Proposal> {
    // Handle partitioning first
    let mut partitioning_value = proposal_value
        .as_object_mut()
        .unwrap()
        .remove("partitioning")
        .unwrap();
    walk("", &mut partitioning_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)?;
        }
        Ok(())
    })?;
    let partitioning: Partitioning = serde_json::from_value(partitioning_value)?;

    // TODO This repeats a bit of MapEdits code, because we're starting from a Value
    // TODO And it skips the compat code
    let perma_edits: PermanentMapEdits = serde_json::from_value(proposal_value)?;
    let edits = perma_edits.into_edits_permissive(map);

    Ok(Proposal {
        edits,
        partitioning,
    })
}

fn is_road_id(path: &str) -> bool {
    lazy_static! {
        static ref PATTERNS: Vec<Regex> = vec![
            // 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/neighbourhoods/\d+/0/perimeter/interior/\d+$").unwrap(),
            Regex::new(r"^/partitioning/neighbourhoods/\d+/0/perimeter/roads/\d+/road$").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(())
}