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