Skip to main content

ezu_features/
mvt.rs

1//! MVT (Mapbox Vector Tile) decoding.
2//!
3//! Decodes the protobuf to a flat, owned representation built on the
4//! crate-root [`Feature`] / [`Geometry`] / [`Polygon`] / [`Value`]
5//! types.
6
7use std::collections::HashMap;
8
9use geozero::mvt::{tile, Message, Tile};
10
11use crate::{Feature, FeatureLayer, Geometry, Polygon, Value};
12
13#[derive(Debug, thiserror::Error)]
14pub enum MvtError {
15    #[error("mvt decode: {0}")]
16    Decode(String),
17}
18
19/// Decode raw MVT bytes (already gunzipped) into owned layers.
20pub fn decode(bytes: &[u8]) -> Result<DecodedTile, MvtError> {
21    let tile = Tile::decode(bytes).map_err(|e| MvtError::Decode(e.to_string()))?;
22    let layers = tile.layers.into_iter().map(decode_layer).collect();
23    Ok(DecodedTile { layers })
24}
25
26#[derive(Debug)]
27pub struct DecodedTile {
28    pub layers: Vec<FeatureLayer>,
29}
30
31impl DecodedTile {
32    pub fn layer(&self, name: &str) -> Option<&FeatureLayer> {
33        self.layers.iter().find(|l| l.name == name)
34    }
35}
36
37fn decode_layer(layer: tile::Layer) -> FeatureLayer {
38    let extent = layer.extent.unwrap_or(4096);
39    let values: Vec<Value> = layer.values.into_iter().map(value_from_proto).collect();
40    let keys = layer.keys;
41    let features = layer
42        .features
43        .into_iter()
44        .map(|f| feature_from_proto(f, &keys, &values))
45        .collect();
46    FeatureLayer {
47        name: layer.name,
48        extent,
49        features,
50    }
51}
52
53fn value_from_proto(v: tile::Value) -> Value {
54    if let Some(s) = v.string_value {
55        Value::String(s)
56    } else if let Some(f) = v.float_value {
57        Value::Float(f)
58    } else if let Some(d) = v.double_value {
59        Value::Double(d)
60    } else if let Some(i) = v.int_value {
61        Value::Int(i)
62    } else if let Some(u) = v.uint_value {
63        Value::UInt(u)
64    } else if let Some(s) = v.sint_value {
65        Value::SInt(s)
66    } else if let Some(b) = v.bool_value {
67        Value::Bool(b)
68    } else {
69        Value::Null
70    }
71}
72
73fn feature_from_proto(f: tile::Feature, keys: &[String], values: &[Value]) -> Feature {
74    let mut properties = HashMap::with_capacity(f.tags.len() / 2);
75    for chunk in f.tags.chunks_exact(2) {
76        if let (Some(k), Some(v)) = (keys.get(chunk[0] as usize), values.get(chunk[1] as usize)) {
77            properties.insert(k.clone(), v.clone());
78        }
79    }
80    let geom_type = f.r#type();
81    let geometry = decode_geometry(&f.geometry, geom_type);
82    Feature {
83        id: f.id,
84        geometry,
85        properties,
86    }
87}
88
89fn decode_geometry(cmds: &[u32], geom_type: tile::GeomType) -> Geometry {
90    let rings = walk_rings(cmds);
91    let mut g = Geometry::default();
92    match geom_type {
93        tile::GeomType::Point => g.points = rings.into_iter().flatten().collect(),
94        tile::GeomType::Linestring => g.lines = rings,
95        tile::GeomType::Polygon => {
96            for ring in rings {
97                if is_exterior(&ring) {
98                    g.polygons.push(Polygon {
99                        exterior: ring,
100                        holes: Vec::new(),
101                    });
102                } else if let Some(last) = g.polygons.last_mut() {
103                    last.holes.push(ring);
104                }
105                // Holes appearing before any exterior are dropped (malformed).
106            }
107        }
108        _ => {}
109    }
110    g
111}
112
113/// Walk MVT geometry commands into raw rings (without the implicit close vertex).
114fn walk_rings(cmds: &[u32]) -> Vec<Vec<(i32, i32)>> {
115    let mut rings: Vec<Vec<(i32, i32)>> = Vec::new();
116    let mut current: Vec<(i32, i32)> = Vec::new();
117    let mut cx: i32 = 0;
118    let mut cy: i32 = 0;
119    let mut i = 0;
120
121    while i < cmds.len() {
122        let header = cmds[i];
123        i += 1;
124        let id = header & 0x7;
125        let count = (header >> 3) as usize;
126        match id {
127            1 => {
128                // MoveTo
129                if !current.is_empty() {
130                    rings.push(std::mem::take(&mut current));
131                }
132                for _ in 0..count {
133                    if i + 1 >= cmds.len() {
134                        return rings;
135                    }
136                    cx = cx.wrapping_add(zigzag(cmds[i]));
137                    cy = cy.wrapping_add(zigzag(cmds[i + 1]));
138                    i += 2;
139                    current.push((cx, cy));
140                }
141            }
142            2 => {
143                // LineTo
144                for _ in 0..count {
145                    if i + 1 >= cmds.len() {
146                        return rings;
147                    }
148                    cx = cx.wrapping_add(zigzag(cmds[i]));
149                    cy = cy.wrapping_add(zigzag(cmds[i + 1]));
150                    i += 2;
151                    current.push((cx, cy));
152                }
153            }
154            7 => {
155                // ClosePath: ring closes implicitly; no parameters.
156            }
157            _ => break,
158        }
159    }
160
161    if !current.is_empty() {
162        rings.push(current);
163    }
164    rings
165}
166
167#[inline]
168fn zigzag(v: u32) -> i32 {
169    ((v >> 1) as i32) ^ -((v & 1) as i32)
170}
171
172/// MVT spec: exterior rings have positive signed area in tile-local (y-down) space.
173fn is_exterior(ring: &[(i32, i32)]) -> bool {
174    if ring.len() < 3 {
175        return true;
176    }
177    let mut sum: i64 = 0;
178    for i in 0..ring.len() {
179        let (x1, y1) = ring[i];
180        let (x2, y2) = ring[(i + 1) % ring.len()];
181        sum += (x1 as i64) * (y2 as i64) - (x2 as i64) * (y1 as i64);
182    }
183    sum > 0
184}
185
186#[cfg(test)]
187mod tests {
188    use super::*;
189
190    #[test]
191    fn zigzag_roundtrip() {
192        assert_eq!(zigzag(0), 0);
193        assert_eq!(zigzag(1), -1);
194        assert_eq!(zigzag(2), 1);
195        assert_eq!(zigzag(3), -2);
196    }
197
198    #[test]
199    fn exterior_cw_in_y_down() {
200        // (0,0) → (10,0) → (10,10) → (0,10): clockwise visually in y-down → exterior.
201        let cw = vec![(0, 0), (10, 0), (10, 10), (0, 10)];
202        assert!(is_exterior(&cw));
203        let ccw = vec![(0, 0), (0, 10), (10, 10), (10, 0)];
204        assert!(!is_exterior(&ccw));
205    }
206}