Skip to main content

mlt_py/
lib.rs

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