Skip to main content

mlt_py/
lib.rs

1mod feature;
2mod tile_transform;
3
4use std::iter::once;
5use std::ops::Deref;
6
7use geo_types::{LineString, Polygon};
8use mlt_core::geojson::{FeatureCollection, Geom32};
9use mlt_core::{
10    Decoder, GeometryType, Layer, MltError, MltResult, ParsedLayer01, Parser, PropValueRef,
11};
12use pyo3::exceptions::PyValueError;
13use pyo3::prelude::*;
14use pyo3::types::{PyBytes, PyDict};
15use pyo3_stub_gen::define_stub_info_gatherer;
16use pyo3_stub_gen::derive::{gen_stub_pyclass, gen_stub_pyfunction, gen_stub_pymethods};
17use tile_transform::TileTransform;
18
19use crate::feature::MltFeature;
20
21fn mlt_err(e: MltError) -> PyErr {
22    PyValueError::new_err(format!("MLT decode error: {e}"))
23}
24
25/// A decoded MLT layer containing features.
26#[gen_stub_pyclass]
27#[pyclass]
28struct MltLayer {
29    #[pyo3(get)]
30    name: String,
31    #[pyo3(get)]
32    extent: u32,
33    #[pyo3(get)]
34    features: Vec<Py<MltFeature>>,
35}
36
37#[gen_stub_pymethods]
38#[pymethods]
39impl MltLayer {
40    fn __repr__(&self) -> String {
41        format!(
42            "MltLayer(name={:?}, extent={}, features=<{} features>)",
43            self.name,
44            self.extent,
45            self.features.len()
46        )
47    }
48}
49
50fn push_coord_raw(buf: &mut Vec<u8>, coord: [i32; 2]) {
51    buf.extend_from_slice(&f64::from(coord[0]).to_le_bytes());
52    buf.extend_from_slice(&f64::from(coord[1]).to_le_bytes());
53}
54
55fn push_coord_xform(buf: &mut Vec<u8>, coord: [i32; 2], xf: TileTransform) {
56    let [x, y] = xf.apply(coord);
57    buf.extend_from_slice(&x.to_le_bytes());
58    buf.extend_from_slice(&y.to_le_bytes());
59}
60
61fn push_coord(buf: &mut Vec<u8>, coord: [i32; 2], xf: Option<TileTransform>) {
62    match xf {
63        Some(xf) => push_coord_xform(buf, coord, xf),
64        None => push_coord_raw(buf, coord),
65    }
66}
67
68fn push_u32(buf: &mut Vec<u8>, v: u32) {
69    buf.extend_from_slice(&v.to_le_bytes());
70}
71
72fn push_rings(
73    buf: &mut Vec<u8>,
74    rings: impl IntoIterator<Item = impl Deref<Target = LineString<i32>>>,
75    xf: Option<TileTransform>,
76) {
77    for ring in rings {
78        push_u32(buf, ring.0.len() as u32);
79        for c in &ring.0 {
80            push_coord(buf, (*c).into(), xf);
81        }
82    }
83}
84
85fn push_linestring(
86    buf: &mut Vec<u8>,
87    line: impl Deref<Target = LineString<i32>>,
88    xf: Option<TileTransform>,
89) {
90    buf.push(0x01);
91    push_u32(buf, 2);
92    push_rings(buf, once(line), xf);
93}
94
95fn push_polygon(buf: &mut Vec<u8>, poly: &Polygon<i32>, xf: Option<TileTransform>) {
96    buf.push(0x01);
97    push_u32(buf, 3);
98    push_u32(buf, (poly.interiors().len() + 1) as u32);
99    push_rings(buf, once(poly.exterior()).chain(poly.interiors()), xf);
100}
101
102fn geom32_to_wkb(geom: &Geom32, xf: Option<TileTransform>) -> MltResult<Vec<u8>> {
103    let mut buf = Vec::with_capacity(128);
104    match geom {
105        Geom32::Point(c) => {
106            buf.push(0x01);
107            push_u32(&mut buf, 1);
108            push_coord(&mut buf, (*c).into(), xf);
109        }
110        Geom32::LineString(coords) => push_linestring(&mut buf, coords, xf),
111        Geom32::Polygon(poly) => push_polygon(&mut buf, poly, xf),
112        Geom32::MultiPoint(coords) => {
113            buf.push(0x01);
114            push_u32(&mut buf, 4);
115            push_u32(&mut buf, coords.0.len() as u32);
116            for c in &coords.0 {
117                buf.push(0x01);
118                push_u32(&mut buf, 1);
119                push_coord(&mut buf, (*c).into(), xf);
120            }
121        }
122        Geom32::MultiLineString(lines) => {
123            buf.push(0x01);
124            push_u32(&mut buf, 5);
125            push_u32(&mut buf, lines.0.len() as u32);
126            for line in &lines.0 {
127                push_linestring(&mut buf, line, xf);
128            }
129        }
130        Geom32::MultiPolygon(polygons) => {
131            buf.push(0x01);
132            push_u32(&mut buf, 6);
133            push_u32(&mut buf, polygons.0.len() as u32);
134            for polygon in &polygons.0 {
135                push_polygon(&mut buf, polygon, xf);
136            }
137        }
138        _ => return Err(MltError::NotImplemented("unsupported geometry type")),
139    }
140    Ok(buf)
141}
142
143fn prop_value_to_py(py: Python<'_>, v: PropValueRef<'_>) -> Py<PyAny> {
144    match v {
145        PropValueRef::Bool(b) => b.into_pyobject(py).unwrap().to_owned().into_any().unbind(),
146        PropValueRef::I8(n) => n.into_pyobject(py).unwrap().into_any().unbind(),
147        PropValueRef::U8(n) => n.into_pyobject(py).unwrap().into_any().unbind(),
148        PropValueRef::I32(n) => n.into_pyobject(py).unwrap().into_any().unbind(),
149        PropValueRef::U32(n) => n.into_pyobject(py).unwrap().into_any().unbind(),
150        PropValueRef::I64(n) => n.into_pyobject(py).unwrap().into_any().unbind(),
151        PropValueRef::U64(n) => n.into_pyobject(py).unwrap().into_any().unbind(),
152        PropValueRef::F32(n) => n.into_pyobject(py).unwrap().into_any().unbind(),
153        PropValueRef::F64(n) => n.into_pyobject(py).unwrap().into_any().unbind(),
154        PropValueRef::Str(s) => s.into_pyobject(py).unwrap().into_any().unbind(),
155    }
156}
157
158fn build_features(
159    py: Python<'_>,
160    layer: &ParsedLayer01<'_>,
161    xf: Option<TileTransform>,
162) -> PyResult<Vec<Py<MltFeature>>> {
163    let mut features = Vec::new();
164    for feat_result in layer.iter_features() {
165        let feat = feat_result.map_err(mlt_err)?;
166        let geometry_type = GeometryType::try_from(&feat.geometry)
167            .map(|gt| gt.to_string())
168            .unwrap_or_else(|_| "Unknown".to_string());
169        let wkb_bytes = geom32_to_wkb(&feat.geometry, xf).map_err(mlt_err)?;
170        let wkb = PyBytes::new(py, &wkb_bytes).unbind();
171        let prop_dict = PyDict::new(py);
172        for p in feat.iter_properties() {
173            prop_dict.set_item(p.name.to_string(), prop_value_to_py(py, p.value))?;
174        }
175        let feature = MltFeature::new(feat.id, geometry_type, wkb, prop_dict.unbind());
176        features.push(Py::new(py, feature)?);
177    }
178    Ok(features)
179}
180
181/// Decode an MLT binary blob into a list of `MltLayer` objects.
182///
183/// If `z`, `x`, `y` are provided, tile-local coordinates are transformed
184/// to EPSG:3857 (Web Mercator) meters. Without them, raw tile coordinates
185/// are preserved.
186///
187/// `tms`: when True (the default), treat `y` as TMS convention (y=0 at south,
188/// used by OpenMapTiles / MBTiles). Set to False for XYZ / slippy-map tiles
189/// (y=0 at north, e.g. OSM raster tiles).
190#[gen_stub_pyfunction]
191#[pyfunction]
192#[pyo3(signature = (data, z=None, x=None, y=None, tms=true))]
193fn decode_mlt(
194    py: Python<'_>,
195    #[gen_stub(override_type(type_repr = "bytes"))] data: &[u8],
196    z: Option<u32>,
197    x: Option<u32>,
198    y: Option<u32>,
199    tms: bool,
200) -> PyResult<Vec<MltLayer>> {
201    let mut dec = Decoder::default();
202    let mut result = Vec::new();
203    for lazy_layer in Parser::default().parse_layers(data).map_err(mlt_err)? {
204        let Layer::Tag01(layer01) = lazy_layer else {
205            return Err(PyValueError::new_err(
206                "unsupported layer tag (expected 0x01)",
207            ));
208        };
209        let decoded = layer01.decode_all(&mut dec).map_err(mlt_err)?;
210        let xf = match (z, x, y) {
211            (Some(z), Some(x), Some(y)) => {
212                Some(TileTransform::from_zxy(z, x, y, decoded.extent, tms)?)
213            }
214            _ => None,
215        };
216        result.push(MltLayer {
217            name: decoded.name.to_string(),
218            extent: decoded.extent,
219            features: build_features(py, &decoded, xf)?,
220        });
221    }
222
223    Ok(result)
224}
225
226/// Decode an MLT binary blob and return GeoJSON as a string.
227#[gen_stub_pyfunction]
228#[pyfunction]
229fn decode_mlt_to_geojson(
230    #[gen_stub(override_type(type_repr = "bytes"))] data: &[u8],
231) -> PyResult<String> {
232    let mut dec = Decoder::default();
233    let layers = dec
234        .decode_all(Parser::default().parse_layers(data).map_err(mlt_err)?)
235        .map_err(mlt_err)?;
236    let fc = FeatureCollection::from_layers(layers).map_err(mlt_err)?;
237    serde_json::to_string(&fc).map_err(|e| PyValueError::new_err(format!("JSON error: {e}")))
238}
239
240/// Return a list of layer names without fully decoding.
241#[gen_stub_pyfunction]
242#[pyfunction]
243fn list_layers(
244    #[gen_stub(override_type(type_repr = "bytes"))] data: &[u8],
245) -> PyResult<Vec<String>> {
246    let layers = Parser::default().parse_layers(data).map_err(mlt_err)?;
247    Ok(layers
248        .iter()
249        .filter_map(|l| l.as_layer01().map(|l| l.name.to_string()))
250        .collect())
251}
252
253#[pymodule]
254fn maplibre_tiles(m: &Bound<'_, PyModule>) -> PyResult<()> {
255    m.add_function(wrap_pyfunction!(decode_mlt, m)?)?;
256    m.add_function(wrap_pyfunction!(decode_mlt_to_geojson, m)?)?;
257    m.add_function(wrap_pyfunction!(list_layers, m)?)?;
258    m.add_class::<MltLayer>()?;
259    m.add_class::<MltFeature>()?;
260    Ok(())
261}
262
263define_stub_info_gatherer!(stub_info);
264
265#[cfg(test)]
266mod tests {
267    use std::f64::consts::PI;
268    use std::fs;
269
270    use mlt_core::{Decoder, GeometryValues};
271
272    use super::*;
273
274    fn geom_to_wkb(
275        geom: &GeometryValues,
276        index: usize,
277        xf: Option<TileTransform>,
278    ) -> MltResult<Vec<u8>> {
279        geom32_to_wkb(&geom.to_geojson(index)?, xf)
280    }
281
282    #[test]
283    fn tile_transform_rejects_zoom_above_30() {
284        let result = TileTransform::from_zxy(31, 0, 0, 4096, false);
285        assert!(result.is_err(), "z=31 should be rejected");
286
287        let result = TileTransform::from_zxy(30, 0, 0, 4096, false);
288        assert!(result.is_ok(), "z=30 should be accepted");
289
290        let result = TileTransform::from_zxy(0, 0, 0, 4096, false);
291        assert!(result.is_ok(), "z=0 should be accepted");
292    }
293
294    #[test]
295    fn tile_transform_zoom_zero_covers_world() {
296        let xf = TileTransform::from_zxy(0, 0, 0, 4096, false).unwrap();
297
298        let circumference = 2.0 * PI * 6_378_137.0;
299        let half = circumference / 2.0;
300
301        assert!(
302            (xf.x_origin + half).abs() < 1.0,
303            "x_origin at z=0 should be -half_circumference"
304        );
305        assert!(
306            (xf.y_origin - half).abs() < 1.0,
307            "y_origin at z=0 should be +half_circumference"
308        );
309
310        let tile_scale = circumference / 4096.0;
311        assert!(
312            (xf.x_scale - tile_scale).abs() < 1e-6,
313            "x_scale should equal circumference / extent"
314        );
315        assert!(
316            (xf.y_scale + tile_scale).abs() < 1e-6,
317            "y_scale should be negative (flipped)"
318        );
319    }
320
321    #[test]
322    fn tile_transform_apply_maps_origin_and_extent() {
323        let xf = TileTransform::from_zxy(0, 0, 0, 4096, false).unwrap();
324
325        let origin = xf.apply([0, 0]);
326        assert!(
327            (origin[0] - xf.x_origin).abs() < 1e-6,
328            "apply([0,0]).x should equal x_origin"
329        );
330        assert!(
331            (origin[1] - xf.y_origin).abs() < 1e-6,
332            "apply([0,0]).y should equal y_origin"
333        );
334
335        let far_corner = xf.apply([4096, 4096]);
336        let circumference = 2.0 * PI * 6_378_137.0;
337        let half = circumference / 2.0;
338        assert!(
339            (far_corner[0] - half).abs() < 1.0,
340            "apply([4096,4096]).x should reach +half"
341        );
342        assert!(
343            (far_corner[1] + half).abs() < 1.0,
344            "apply([4096,4096]).y should reach -half"
345        );
346    }
347
348    #[test]
349    fn tile_transform_tms_vs_xyz() {
350        let xyz = TileTransform::from_zxy(1, 0, 0, 4096, false).unwrap();
351        let tms = TileTransform::from_zxy(1, 0, 1, 4096, true).unwrap();
352
353        assert!(
354            (xyz.x_origin - tms.x_origin).abs() < 1e-6,
355            "same tile via TMS and XYZ should produce same x_origin"
356        );
357        assert!(
358            (xyz.y_origin - tms.y_origin).abs() < 1e-6,
359            "same tile via TMS and XYZ should produce same y_origin"
360        );
361    }
362
363    #[test]
364    fn fixture_parse_and_feature_collection() {
365        let fixture_path = "../../test/synthetic/0x01/point.mlt";
366        let data = fs::read(fixture_path)
367            .unwrap_or_else(|e| panic!("failed to read fixture {fixture_path}: {e}"));
368
369        let layers = Parser::default()
370            .parse_layers(&data)
371            .expect("parse_layers should succeed");
372        let mut dec = Decoder::default();
373        let decoded = dec.decode_all(layers).expect("decode_all should succeed");
374
375        assert!(!decoded.is_empty(), "should parse at least one layer");
376        let l = decoded[0].as_layer01().expect("first layer should be v0.1");
377        assert!(!l.name.is_empty(), "layer name should be non-empty");
378
379        let fc = FeatureCollection::from_layers(decoded).expect("FeatureCollection should succeed");
380        assert!(
381            !fc.features.is_empty(),
382            "feature collection should have features"
383        );
384    }
385
386    #[test]
387    fn fixture_geom_to_wkb_produces_valid_output() {
388        let fixture_path = "../../test/synthetic/0x01/poly.mlt";
389        let data = fs::read(fixture_path)
390            .unwrap_or_else(|e| panic!("failed to read fixture {fixture_path}: {e}"));
391
392        let layers = Parser::default()
393            .parse_layers(&data)
394            .expect("parse_layers should succeed");
395        let mut dec = Decoder::default();
396        let decoded = dec.decode_all(layers).expect("decode_all should succeed");
397
398        let l = decoded[0].as_layer01().expect("first layer should be v0.1");
399        let geom = &l.geometry;
400
401        let wkb = geom_to_wkb(geom, 0, None).expect("geom_to_wkb should succeed");
402        assert!(
403            wkb.len() >= 5,
404            "WKB must be at least 5 bytes (byte order + type)"
405        );
406        assert_eq!(wkb[0], 0x01, "WKB byte order should be little-endian");
407        let wkb_type = u32::from_le_bytes([wkb[1], wkb[2], wkb[3], wkb[4]]);
408        assert_eq!(
409            wkb_type, 3,
410            "polygon fixture should produce WKB type 3 (Polygon)"
411        );
412    }
413
414    #[test]
415    fn fixture_geom_to_wkb_with_transform() {
416        let fixture_path = "../../test/synthetic/0x01/point.mlt";
417        let data = fs::read(fixture_path)
418            .unwrap_or_else(|e| panic!("failed to read fixture {fixture_path}: {e}"));
419
420        let layers = Parser::default()
421            .parse_layers(&data)
422            .expect("parse_layers should succeed");
423        let mut dec = Decoder::default();
424        let decoded = dec.decode_all(layers).expect("decode_all should succeed");
425
426        let l = decoded[0].as_layer01().expect("first layer should be v0.1");
427        let geom = &l.geometry;
428
429        let xf = TileTransform::from_zxy(0, 0, 0, l.extent, false).unwrap();
430
431        let wkb_raw = geom_to_wkb(geom, 0, None).expect("raw wkb should succeed");
432        let wkb_xf = geom_to_wkb(geom, 0, Some(xf)).expect("transformed wkb should succeed");
433
434        assert_eq!(
435            wkb_raw.len(),
436            wkb_xf.len(),
437            "raw and transformed WKB should have the same length"
438        );
439        assert_ne!(
440            wkb_raw, wkb_xf,
441            "transformed WKB should differ from raw (unless coordinates are trivially 0)"
442        );
443    }
444
445    #[test]
446    fn fixture_line_produces_wkb_linestring() {
447        let fixture_path = "../../test/synthetic/0x01/line.mlt";
448        let data = fs::read(fixture_path)
449            .unwrap_or_else(|e| panic!("failed to read fixture {fixture_path}: {e}"));
450
451        let layers = Parser::default()
452            .parse_layers(&data)
453            .expect("parse_layers should succeed");
454        let mut dec = Decoder::default();
455        let decoded = dec.decode_all(layers).expect("decode_all should succeed");
456
457        let l = decoded[0].as_layer01().expect("first layer should be v0.1");
458        let geom = &l.geometry;
459
460        let wkb = geom_to_wkb(geom, 0, None).expect("geom_to_wkb should succeed");
461        assert!(wkb.len() >= 5);
462        let wkb_type = u32::from_le_bytes([wkb[1], wkb[2], wkb[3], wkb[4]]);
463        assert_eq!(
464            wkb_type, 2,
465            "line fixture should produce WKB type 2 (LineString)"
466        );
467    }
468}