Skip to main content

mlt_py/
lib.rs

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