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#[gen_stub_pyclass]
29#[pyclass]
30struct MltLayer {
31 #[pyo3(get)]
32 name: String,
33 #[pyo3(get)]
34 extent: u32,
35 #[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
55struct 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 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 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 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#[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#[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#[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}