Skip to main content

mlt_py/
lib.rs

1mod encode;
2mod feature;
3mod tile_transform;
4
5use std::iter::once;
6
7use mlt_core::geo_types::{Coord, Geometry, LineString, Polygon};
8use mlt_core::geojson::FeatureCollection;
9use mlt_core::{
10    Decoder, GeometryType, Layer, LendingIterator, MltError, MltResult, ParsedLayer01, Parser,
11    PropValueRef, ZStep,
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
22#[expect(clippy::needless_pass_by_value, reason = "helper util")]
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    /// The power of ten of the z grid's step in metres, or `None` when the vertices carry no z.
36    #[pyo3(get)]
37    z_step: Option<i8>,
38    #[pyo3(get)]
39    features: Vec<Py<MltFeature>>,
40}
41
42#[gen_stub_pymethods]
43#[pymethods]
44impl MltLayer {
45    fn __repr__(&self) -> String {
46        format!(
47            "MltLayer(name={:?}, extent={}, features=<{} features>)",
48            self.name,
49            self.extent,
50            self.features.len()
51        )
52    }
53}
54
55/// Writes WKB, taking the next z for each stored vertex when the layer has z.
56///
57/// Z follows xy: the grid value in tile coordinates, the elevation in metres once `xf` projects.
58struct WkbWriter<'a> {
59    buf: Vec<u8>,
60    xf: Option<TileTransform>,
61    z: Option<(ZStep, &'a [i32])>,
62    vertex: usize,
63}
64
65impl<'a> WkbWriter<'a> {
66    fn new(xf: Option<TileTransform>, z: Option<(ZStep, &'a [i32])>) -> Self {
67        Self {
68            buf: Vec::with_capacity(128),
69            xf,
70            z,
71            vertex: 0,
72        }
73    }
74
75    fn f64(&mut self, v: f64) {
76        self.buf.extend_from_slice(&v.to_le_bytes());
77    }
78
79    fn u32(&mut self, v: u32) {
80        self.buf.extend_from_slice(&v.to_le_bytes());
81    }
82
83    fn len(&mut self, len: usize) -> MltResult<()> {
84        self.u32(u32::try_from(len).map_err(|_| MltError::IntegerOverflow)?);
85        Ok(())
86    }
87
88    /// The byte order and type, in ISO WKB's Z variant when the layer has z.
89    fn header(&mut self, ty: u32) {
90        self.buf.push(0x01);
91        self.u32(if self.z.is_some() { ty + 1000 } else { ty });
92    }
93
94    /// Write the next stored vertex, returning its z.
95    fn vertex(&mut self, coord: Coord<i32>) -> Option<i32> {
96        let z = self
97            .z
98            .map(|(_, z)| z.get(self.vertex).copied().unwrap_or_default());
99        self.vertex += 1;
100        self.coord(coord, z);
101        z
102    }
103
104    fn coord(&mut self, coord: Coord<i32>, z: Option<i32>) {
105        let [x, y] = match self.xf {
106            Some(xf) => xf.apply(coord.into()),
107            None => [coord.x, coord.y].map(f64::from),
108        };
109        self.f64(x);
110        self.f64(y);
111        if let (Some((step, _)), Some(z)) = (self.z, z) {
112            let z = if self.xf.is_some() {
113                step.elevation(z)
114            } else {
115                f64::from(z)
116            };
117            self.f64(z);
118        }
119    }
120
121    fn point(&mut self, coord: Coord<i32>) {
122        self.header(1);
123        self.vertex(coord);
124    }
125
126    fn line(&mut self, line: &LineString<i32>) -> MltResult<()> {
127        self.header(2);
128        self.len(line.0.len())?;
129        for &c in &line.0 {
130            self.vertex(c);
131        }
132        Ok(())
133    }
134
135    /// A ring's closing position repeats its first, z included, because MLT does not store it.
136    fn ring(&mut self, ring: &LineString<i32>) -> MltResult<()> {
137        let coords = &ring.0;
138        self.len(coords.len())?;
139        let Some((&first, rest)) = coords.split_first() else {
140            return Ok(());
141        };
142        let first_z = self.vertex(first);
143        match rest.split_last() {
144            Some((&last, middle)) if last == first => {
145                for &c in middle {
146                    self.vertex(c);
147                }
148                self.coord(first, first_z);
149            }
150            _ => {
151                for &c in rest {
152                    self.vertex(c);
153                }
154            }
155        }
156        Ok(())
157    }
158
159    fn polygon(&mut self, poly: &Polygon<i32>) -> MltResult<()> {
160        self.header(3);
161        self.len(poly.interiors().len() + 1)?;
162        for ring in once(poly.exterior()).chain(poly.interiors()) {
163            self.ring(ring)?;
164        }
165        Ok(())
166    }
167
168    fn geometry(&mut self, geom: &Geometry<i32>) -> MltResult<()> {
169        match geom {
170            Geometry::Point(p) => self.point(p.0),
171            Geometry::LineString(line) => self.line(line)?,
172            Geometry::Polygon(poly) => self.polygon(poly)?,
173            Geometry::MultiPoint(points) => {
174                self.header(4);
175                self.len(points.0.len())?;
176                for p in &points.0 {
177                    self.point(p.0);
178                }
179            }
180            Geometry::MultiLineString(lines) => {
181                self.header(5);
182                self.len(lines.0.len())?;
183                for line in &lines.0 {
184                    self.line(line)?;
185                }
186            }
187            Geometry::MultiPolygon(polygons) => {
188                self.header(6);
189                self.len(polygons.0.len())?;
190                for polygon in &polygons.0 {
191                    self.polygon(polygon)?;
192                }
193            }
194            Geometry::Line(_)
195            | Geometry::GeometryCollection(_)
196            | Geometry::Rect(_)
197            | Geometry::Triangle(_) => {
198                return Err(MltError::NotImplemented("unsupported geometry type"));
199            }
200        }
201        Ok(())
202    }
203
204    fn finish(self) -> MltResult<Vec<u8>> {
205        match self.z {
206            Some((_, z)) if z.len() != self.vertex => Err(MltError::ZVertexCountMismatch {
207                expected: self.vertex,
208                actual: z.len(),
209            }),
210            _ => Ok(self.buf),
211        }
212    }
213}
214
215fn geom32_to_wkb(
216    geom: &Geometry<i32>,
217    xf: Option<TileTransform>,
218    z: Option<(ZStep, &[i32])>,
219) -> MltResult<Vec<u8>> {
220    let mut writer = WkbWriter::new(xf, z);
221    writer.geometry(geom)?;
222    writer.finish()
223}
224
225fn prop_value_to_py(py: Python<'_>, v: PropValueRef<'_>) -> Py<PyAny> {
226    match v {
227        PropValueRef::Bool(b) => b.into_pyobject(py).unwrap().to_owned().into_any().unbind(),
228        PropValueRef::I8(n) => n.into_pyobject(py).unwrap().into_any().unbind(),
229        PropValueRef::U8(n) => n.into_pyobject(py).unwrap().into_any().unbind(),
230        PropValueRef::I32(n) => n.into_pyobject(py).unwrap().into_any().unbind(),
231        PropValueRef::U32(n) => n.into_pyobject(py).unwrap().into_any().unbind(),
232        PropValueRef::I64(n) => n.into_pyobject(py).unwrap().into_any().unbind(),
233        PropValueRef::U64(n) => n.into_pyobject(py).unwrap().into_any().unbind(),
234        PropValueRef::F32(n) => n.into_pyobject(py).unwrap().into_any().unbind(),
235        PropValueRef::F64(n) => n.into_pyobject(py).unwrap().into_any().unbind(),
236        PropValueRef::Str(s) => s.into_pyobject(py).unwrap().into_any().unbind(),
237    }
238}
239
240fn build_features(
241    py: Python<'_>,
242    layer: &ParsedLayer01<'_>,
243    xf: Option<TileTransform>,
244) -> PyResult<Vec<Py<MltFeature>>> {
245    let geometry = layer.geometry_values();
246    let z_step = geometry.z_step();
247    let mut features = Vec::new();
248    let mut feat_iter = layer.iter_features();
249    let mut index = 0;
250    while let Some(feat_result) = feat_iter.next() {
251        let feat = feat_result.map_err(mlt_err)?;
252        let geometry_type = GeometryType::try_from(feat.geometry())
253            .map_or_else(|()| "Unknown".to_string(), |gt| gt.to_string());
254        let z = z_step
255            .map(|step| geometry.z(index).map(|z| (step, z)))
256            .transpose()
257            .map_err(mlt_err)?;
258        index += 1;
259        let z = z.as_ref().map(|(step, z)| (*step, z.as_slice()));
260        let wkb_bytes = geom32_to_wkb(feat.geometry(), xf, z).map_err(mlt_err)?;
261        let wkb = PyBytes::new(py, &wkb_bytes).unbind();
262        let prop_dict = PyDict::new(py);
263        for p in feat.iter_properties() {
264            prop_dict.set_item(p.name().to_string(), prop_value_to_py(py, p.value()))?;
265        }
266        let feature = MltFeature::new(feat.id(), geometry_type, wkb, prop_dict.unbind());
267        features.push(Py::new(py, feature)?);
268    }
269    Ok(features)
270}
271
272/// Decode an MLT binary blob into a list of `MltLayer` objects.
273///
274/// If `z`, `x`, `y` are provided, tile-local coordinates are transformed
275/// to EPSG:3857 (Web Mercator) meters. Without them, raw tile coordinates
276/// are preserved.
277///
278/// A layer whose vertices carry z writes them into the WKB as its third ordinate.
279/// They are grid values in raw tile coordinates, and elevations in metres once transformed.
280///
281/// `tms`: when True (the default), treat `y` as TMS convention (y=0 at south,
282/// used by `OpenMapTiles` / `MBTiles`). Set to False for XYZ / slippy-map tiles
283/// (y=0 at north, e.g. OSM raster tiles).
284#[gen_stub_pyfunction]
285#[pyfunction]
286#[pyo3(signature = (data, z=None, x=None, y=None, tms=true))]
287fn decode_mlt(
288    py: Python<'_>,
289    #[gen_stub(override_type(type_repr = "bytes"))] data: &[u8],
290    z: Option<u32>,
291    x: Option<u32>,
292    y: Option<u32>,
293    tms: bool,
294) -> PyResult<Vec<MltLayer>> {
295    let mut dec = Decoder::default();
296    let mut result = Vec::new();
297    for lazy_layer in Parser::default().parse_layers(data).map_err(mlt_err)? {
298        let decoded = match lazy_layer {
299            Layer::Tag01(layer) => layer.decode_all(&mut dec).map_err(mlt_err)?,
300            Layer::Tag02(layer) => layer.decode_all(&mut dec).map_err(mlt_err)?.into_layer(),
301            Layer::Unknown(_) | _ => {
302                return Err(PyValueError::new_err(
303                    "unsupported layer tag (expected 0x01 or 0x02)",
304                ));
305            }
306        };
307        let extent = decoded.extent().get();
308        let xf = match (z, x, y) {
309            (Some(z), Some(x), Some(y)) => Some(TileTransform::from_zxy(z, x, y, extent, tms)?),
310            _ => None,
311        };
312        result.push(MltLayer {
313            name: decoded.name().to_string(),
314            extent,
315            z_step: decoded.geometry_values().z_step().map(ZStep::exponent),
316            features: build_features(py, &decoded, xf)?,
317        });
318    }
319
320    Ok(result)
321}
322
323/// Decode an MLT binary blob and return `GeoJSON` as a string.
324#[gen_stub_pyfunction]
325#[pyfunction]
326fn decode_mlt_to_geojson(
327    #[gen_stub(override_type(type_repr = "bytes"))] data: &[u8],
328) -> PyResult<String> {
329    let mut dec = Decoder::default();
330    let layers = dec
331        .decode_all(Parser::default().parse_layers(data).map_err(mlt_err)?)
332        .map_err(mlt_err)?;
333    let fc = FeatureCollection::from_layers(layers).map_err(mlt_err)?;
334    serde_json::to_string(&fc).map_err(|e| PyValueError::new_err(format!("JSON error: {e}")))
335}
336
337/// Return a list of layer names without fully decoding.
338#[gen_stub_pyfunction]
339#[pyfunction]
340fn list_layers(
341    #[gen_stub(override_type(type_repr = "bytes"))] data: &[u8],
342) -> PyResult<Vec<String>> {
343    let layers = Parser::default().parse_layers(data).map_err(mlt_err)?;
344    Ok(layers
345        .iter()
346        .filter_map(|l| l.name().map(str::to_string))
347        .collect())
348}
349
350#[pymodule]
351fn maplibre_tiles(m: &Bound<'_, PyModule>) -> PyResult<()> {
352    m.add_function(wrap_pyfunction!(decode_mlt, m)?)?;
353    m.add_function(wrap_pyfunction!(decode_mlt_to_geojson, m)?)?;
354    m.add_function(wrap_pyfunction!(list_layers, m)?)?;
355    m.add_function(wrap_pyfunction!(encode::geojson::encode_geojson, m)?)?;
356    m.add_function(wrap_pyfunction!(encode::mvt::encode_mvt, m)?)?;
357    m.add_class::<MltLayer>()?;
358    m.add_class::<MltFeature>()?;
359    Ok(())
360}
361
362define_stub_info_gatherer!(stub_info);
363
364#[cfg(test)]
365mod tests {
366    use std::f64::consts::PI;
367    use std::fs;
368
369    use mlt_core::{Decoder, GeometryValues, ParsedLayer};
370
371    use super::*;
372
373    fn first_layer01<'a, 'b>(layers: &'b [ParsedLayer<'a>]) -> &'b ParsedLayer01<'a> {
374        let ParsedLayer::Tag01(l) = &layers[0] else {
375            panic!("first layer should be 0x01")
376        };
377        l
378    }
379
380    fn geom_to_wkb(
381        geom: &GeometryValues,
382        index: usize,
383        xf: Option<TileTransform>,
384    ) -> MltResult<Vec<u8>> {
385        geom32_to_wkb(&geom.to_geojson(index)?, xf, None)
386    }
387
388    #[test]
389    fn tile_transform_rejects_zoom_above_30() {
390        let result = TileTransform::from_zxy(31, 0, 0, 4096, false);
391        assert!(result.is_err(), "z=31 should be rejected");
392
393        let result = TileTransform::from_zxy(30, 0, 0, 4096, false);
394        assert!(result.is_ok(), "z=30 should be accepted");
395
396        let result = TileTransform::from_zxy(0, 0, 0, 4096, false);
397        assert!(result.is_ok(), "z=0 should be accepted");
398    }
399
400    #[test]
401    fn tile_transform_zoom_zero_covers_world() {
402        let xf = TileTransform::from_zxy(0, 0, 0, 4096, false).unwrap();
403
404        let circumference = 2.0 * PI * 6_378_137.0;
405        let half = circumference / 2.0;
406
407        assert!(
408            (xf.x_origin + half).abs() < 1.0,
409            "x_origin at z=0 should be -half_circumference"
410        );
411        assert!(
412            (xf.y_origin - half).abs() < 1.0,
413            "y_origin at z=0 should be +half_circumference"
414        );
415
416        let tile_scale = circumference / 4096.0;
417        assert!(
418            (xf.x_scale - tile_scale).abs() < 1e-6,
419            "x_scale should equal circumference / extent"
420        );
421        assert!(
422            (xf.y_scale + tile_scale).abs() < 1e-6,
423            "y_scale should be negative (flipped)"
424        );
425    }
426
427    #[test]
428    fn tile_transform_apply_maps_origin_and_extent() {
429        let xf = TileTransform::from_zxy(0, 0, 0, 4096, false).unwrap();
430
431        let origin = xf.apply([0, 0]);
432        assert!(
433            (origin[0] - xf.x_origin).abs() < 1e-6,
434            "apply([0,0]).x should equal x_origin"
435        );
436        assert!(
437            (origin[1] - xf.y_origin).abs() < 1e-6,
438            "apply([0,0]).y should equal y_origin"
439        );
440
441        let far_corner = xf.apply([4096, 4096]);
442        let circumference = 2.0 * PI * 6_378_137.0;
443        let half = circumference / 2.0;
444        assert!(
445            (far_corner[0] - half).abs() < 1.0,
446            "apply([4096,4096]).x should reach +half"
447        );
448        assert!(
449            (far_corner[1] + half).abs() < 1.0,
450            "apply([4096,4096]).y should reach -half"
451        );
452    }
453
454    #[test]
455    fn tile_transform_tms_vs_xyz() {
456        let xyz = TileTransform::from_zxy(1, 0, 0, 4096, false).unwrap();
457        let tms = TileTransform::from_zxy(1, 0, 1, 4096, true).unwrap();
458
459        assert!(
460            (xyz.x_origin - tms.x_origin).abs() < 1e-6,
461            "same tile via TMS and XYZ should produce same x_origin"
462        );
463        assert!(
464            (xyz.y_origin - tms.y_origin).abs() < 1e-6,
465            "same tile via TMS and XYZ should produce same y_origin"
466        );
467    }
468
469    #[test]
470    fn fixture_parse_and_feature_collection() {
471        let fixture_path = "../../test/synthetic/0x01/point.mlt";
472        let data = fs::read(fixture_path)
473            .unwrap_or_else(|e| panic!("failed to read fixture {fixture_path}: {e}"));
474
475        let layers = Parser::default()
476            .parse_layers(&data)
477            .expect("parse_layers should succeed");
478        let mut dec = Decoder::default();
479        let decoded = dec.decode_all(layers).expect("decode_all should succeed");
480
481        assert!(!decoded.is_empty(), "should parse at least one layer");
482        let l = first_layer01(&decoded);
483        assert!(!l.name().is_empty(), "layer name should be non-empty");
484
485        let fc = FeatureCollection::from_layers(decoded).expect("FeatureCollection should succeed");
486        assert!(
487            !fc.features.is_empty(),
488            "feature collection should have features"
489        );
490    }
491
492    #[test]
493    fn fixture_geom_to_wkb_produces_valid_output() {
494        let fixture_path = "../../test/synthetic/0x01/poly.mlt";
495        let data = fs::read(fixture_path)
496            .unwrap_or_else(|e| panic!("failed to read fixture {fixture_path}: {e}"));
497
498        let layers = Parser::default()
499            .parse_layers(&data)
500            .expect("parse_layers should succeed");
501        let mut dec = Decoder::default();
502        let decoded = dec.decode_all(layers).expect("decode_all should succeed");
503
504        let l = first_layer01(&decoded);
505        let geom = l.geometry_values();
506
507        let wkb = geom_to_wkb(geom, 0, None).expect("geom_to_wkb should succeed");
508        assert!(
509            wkb.len() >= 5,
510            "WKB must be at least 5 bytes (byte order + type)"
511        );
512        assert_eq!(wkb[0], 0x01, "WKB byte order should be little-endian");
513        let wkb_type = u32::from_le_bytes([wkb[1], wkb[2], wkb[3], wkb[4]]);
514        assert_eq!(
515            wkb_type, 3,
516            "polygon fixture should produce WKB type 3 (Polygon)"
517        );
518    }
519
520    #[test]
521    fn fixture_geom_to_wkb_with_transform() {
522        let fixture_path = "../../test/synthetic/0x01/point.mlt";
523        let data = fs::read(fixture_path)
524            .unwrap_or_else(|e| panic!("failed to read fixture {fixture_path}: {e}"));
525
526        let layers = Parser::default()
527            .parse_layers(&data)
528            .expect("parse_layers should succeed");
529        let mut dec = Decoder::default();
530        let decoded = dec.decode_all(layers).expect("decode_all should succeed");
531
532        let l = first_layer01(&decoded);
533        let geom = l.geometry_values();
534
535        let xf = TileTransform::from_zxy(0, 0, 0, l.extent().get(), false).unwrap();
536
537        let wkb_raw = geom_to_wkb(geom, 0, None).expect("raw wkb should succeed");
538        let wkb_xf = geom_to_wkb(geom, 0, Some(xf)).expect("transformed wkb should succeed");
539
540        assert_eq!(
541            wkb_raw.len(),
542            wkb_xf.len(),
543            "raw and transformed WKB should have the same length"
544        );
545        assert_ne!(
546            wkb_raw, wkb_xf,
547            "transformed WKB should differ from raw (unless coordinates are trivially 0)"
548        );
549    }
550
551    #[test]
552    fn fixture_line_produces_wkb_linestring() {
553        let fixture_path = "../../test/synthetic/0x01/line.mlt";
554        let data = fs::read(fixture_path)
555            .unwrap_or_else(|e| panic!("failed to read fixture {fixture_path}: {e}"));
556
557        let layers = Parser::default()
558            .parse_layers(&data)
559            .expect("parse_layers should succeed");
560        let mut dec = Decoder::default();
561        let decoded = dec.decode_all(layers).expect("decode_all should succeed");
562
563        let l = first_layer01(&decoded);
564        let geom = l.geometry_values();
565
566        let wkb = geom_to_wkb(geom, 0, None).expect("geom_to_wkb should succeed");
567        assert!(wkb.len() >= 5);
568        let wkb_type = u32::from_le_bytes([wkb[1], wkb[2], wkb[3], wkb[4]]);
569        assert_eq!(
570            wkb_type, 2,
571            "line fixture should produce WKB type 2 (LineString)"
572        );
573    }
574}