Skip to main content

mlt_core/convert/mvt/
decode.rs

1//! Decode MVT bytes into [`FeatureCollection`] or row-oriented [`TileLayer`]s.
2
3use std::collections::{BTreeMap, HashMap};
4
5use fast_mvt::{MvtLayer, MvtReaderRef, MvtValue};
6use serde_json::Value;
7
8use crate::decoder::{PropValue, TileFeature, TileLayer};
9use crate::geojson::{Feature, FeatureCollection};
10use crate::{MltError, MltResult};
11
12/// Parse MVT bytes into a list of layers, each holding its raw features.
13///
14/// This is the single place where the `fast-mvt` API is called; both
15/// [`mvt_to_feature_collection`] and [`mvt_to_tile_layers`] build on top of it.
16fn read_mvt_layers(data: &[u8]) -> MltResult<Vec<MvtLayer>> {
17    let layers = MvtReaderRef::new(data)?.to_tile()?.layers;
18    if layers.iter().any(|layer| layer.name.is_empty()) {
19        return Err(MltError::MissingLayerName);
20    }
21    Ok(layers)
22}
23
24/// Parse MVT binary data and convert to a [`FeatureCollection`].
25pub fn mvt_to_feature_collection(data: impl AsRef<[u8]>) -> MltResult<FeatureCollection> {
26    let mut features = Vec::new();
27
28    for layer in read_mvt_layers(data.as_ref())? {
29        for feat in layer.features {
30            let mut properties = feat
31                .properties
32                .into_iter()
33                .map(|(k, v)| Ok((k, Value::try_from(v)?)))
34                .collect::<MltResult<BTreeMap<_, _>>>()?;
35            properties.insert("_layer".into(), Value::String(layer.name.clone()));
36            properties.insert("_extent".into(), Value::Number(layer.extent.get().into()));
37            features.push(Feature {
38                geometry: feat.geometry,
39                id: feat.id,
40                properties,
41                ty: "Feature".into(),
42            });
43        }
44    }
45
46    Ok(FeatureCollection {
47        features,
48        ty: "FeatureCollection".into(),
49    })
50}
51
52/// Parse MVT binary data and convert each layer to a row-oriented [`TileLayer`].
53///
54/// Each MVT layer becomes one [`TileLayer`].  Property column types are inferred
55/// from all features in the layer: the first non-null value seen for each column
56/// determines its type, with `I64`+`U64` widened to `I64` and `F32`+`F64` widened
57/// to `F64`; all other type conflicts fall back to `Str`.
58pub fn mvt_to_tile_layers(data: impl AsRef<[u8]>) -> MltResult<Vec<TileLayer>> {
59    read_mvt_layers(data.as_ref())?
60        .into_iter()
61        .map(TileLayer::try_from)
62        .collect::<Result<Vec<_>, _>>()
63}
64
65impl TryFrom<MvtLayer> for TileLayer {
66    type Error = MltError;
67
68    fn try_from(layer: MvtLayer) -> Result<Self, Self::Error> {
69        if layer.name.is_empty() {
70            return Err(MltError::MissingLayerName);
71        }
72
73        // First pass: collect property names (insertion-ordered) and infer column types.
74        let mut col_names: Vec<String> = Vec::new();
75        let mut col_index: HashMap<String, usize> = HashMap::new();
76        let mut col_types: Vec<InferredType> = Vec::new();
77
78        for feat in &layer.features {
79            for (key, val) in &feat.properties {
80                let idx = *col_index.entry(key.clone()).or_insert_with(|| {
81                    let i = col_names.len();
82                    col_names.push(key.clone());
83                    col_types.push(InferredType::Unknown);
84                    i
85                });
86                col_types[idx] = col_types[idx].merge(InferredType::from_mvt(val));
87            }
88        }
89
90        // Columns that were only ever null fall back to Str.
91        for t in &mut col_types {
92            if *t == InferredType::Unknown {
93                *t = InferredType::Str;
94            }
95        }
96
97        // Second pass: build TileFeature objects.
98        let mut tile_features = Vec::with_capacity(layer.features.len());
99        for feat in layer.features {
100            // Start every slot with a typed null; fill in present values below.
101            let mut properties: Vec<PropValue> = col_types.iter().map(|t| t.typed_null()).collect();
102            for (key, val) in feat.properties {
103                if let Some(&idx) = col_index.get(&key)
104                    && !matches!(val, MvtValue::Null)
105                {
106                    properties[idx] = col_types[idx].convert(val);
107                }
108            }
109            tile_features.push(TileFeature {
110                id: feat.id,
111                geometry: feat.geometry,
112                properties,
113            });
114        }
115
116        Self::from_parts(layer.name, layer.extent.get(), col_names, tile_features)
117    }
118}
119
120/// Column type inferred from MVT property values across all features in a layer.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122enum InferredType {
123    Unknown,
124    Bool,
125    I64,
126    U64,
127    F32,
128    F64,
129    Str,
130}
131
132impl InferredType {
133    fn from_mvt(val: &MvtValue) -> Self {
134        match val {
135            MvtValue::Bool(_) => Self::Bool,
136            MvtValue::Int(_) | MvtValue::SInt(_) => Self::I64,
137            MvtValue::UInt(_) => Self::U64,
138            MvtValue::Float(_) => Self::F32,
139            MvtValue::Double(_) => Self::F64,
140            MvtValue::String(_) => Self::Str,
141            MvtValue::Null => Self::Unknown,
142        }
143    }
144
145    /// Merge with another type, widening when necessary.
146    fn merge(self, other: Self) -> Self {
147        if self == Self::Unknown {
148            return other;
149        }
150        if other == Self::Unknown || self == other {
151            return self;
152        }
153        if matches!(
154            (self, other),
155            (Self::I64, Self::U64) | (Self::U64, Self::I64)
156        ) {
157            return Self::I64;
158        }
159        if matches!(
160            (self, other),
161            (Self::F32, Self::F64) | (Self::F64, Self::F32)
162        ) {
163            return Self::F64;
164        }
165        Self::Str
166    }
167
168    fn typed_null(self) -> PropValue {
169        match self {
170            Self::Unknown | Self::Str => PropValue::Str(None),
171            Self::Bool => PropValue::Bool(None),
172            Self::I64 => PropValue::I64(None),
173            Self::U64 => PropValue::U64(None),
174            Self::F32 => PropValue::F32(None),
175            Self::F64 => PropValue::F64(None),
176        }
177    }
178
179    /// Convert an owned [`MvtValue`] into a [`PropValue`] matching this column type.
180    fn convert(self, val: MvtValue) -> PropValue {
181        match (self, val) {
182            (_, MvtValue::Null) => self.typed_null(),
183            (Self::Bool, MvtValue::Bool(b)) => PropValue::Bool(Some(b)),
184            (Self::I64, MvtValue::Int(i) | MvtValue::SInt(i)) => PropValue::I64(Some(i)),
185            (Self::I64, MvtValue::UInt(u)) if i64::try_from(u).is_ok() => {
186                // Value must be within 0..i64::MAX
187                #[expect(clippy::cast_possible_wrap, reason = "checked above")]
188                PropValue::I64(Some(u as i64))
189            }
190            (Self::U64, MvtValue::UInt(u)) => PropValue::U64(Some(u)),
191            (Self::F32, MvtValue::Float(f)) => PropValue::F32(Some(f)),
192            (Self::F64, MvtValue::Double(f)) => PropValue::F64(Some(f)),
193            (Self::F64, MvtValue::Float(f)) => PropValue::F64(Some(f64::from(f))),
194            (_, MvtValue::String(s)) => PropValue::Str(Some(s)),
195            // Type conflict at runtime: fall back to a debug string.
196            (_, v) => PropValue::Str(Some(format!("{v:?}"))),
197        }
198    }
199}