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.as_layer01().map(|l| l.name().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};
292
293 use super::*;
294
295 fn geom_to_wkb(
296 geom: &GeometryValues,
297 index: usize,
298 xf: Option<TileTransform>,
299 ) -> MltResult<Vec<u8>> {
300 geom32_to_wkb(&geom.to_geojson(index)?, xf)
301 }
302
303 #[test]
304 fn tile_transform_rejects_zoom_above_30() {
305 let result = TileTransform::from_zxy(31, 0, 0, 4096, false);
306 assert!(result.is_err(), "z=31 should be rejected");
307
308 let result = TileTransform::from_zxy(30, 0, 0, 4096, false);
309 assert!(result.is_ok(), "z=30 should be accepted");
310
311 let result = TileTransform::from_zxy(0, 0, 0, 4096, false);
312 assert!(result.is_ok(), "z=0 should be accepted");
313 }
314
315 #[test]
316 fn tile_transform_zoom_zero_covers_world() {
317 let xf = TileTransform::from_zxy(0, 0, 0, 4096, false).unwrap();
318
319 let circumference = 2.0 * PI * 6_378_137.0;
320 let half = circumference / 2.0;
321
322 assert!(
323 (xf.x_origin + half).abs() < 1.0,
324 "x_origin at z=0 should be -half_circumference"
325 );
326 assert!(
327 (xf.y_origin - half).abs() < 1.0,
328 "y_origin at z=0 should be +half_circumference"
329 );
330
331 let tile_scale = circumference / 4096.0;
332 assert!(
333 (xf.x_scale - tile_scale).abs() < 1e-6,
334 "x_scale should equal circumference / extent"
335 );
336 assert!(
337 (xf.y_scale + tile_scale).abs() < 1e-6,
338 "y_scale should be negative (flipped)"
339 );
340 }
341
342 #[test]
343 fn tile_transform_apply_maps_origin_and_extent() {
344 let xf = TileTransform::from_zxy(0, 0, 0, 4096, false).unwrap();
345
346 let origin = xf.apply([0, 0]);
347 assert!(
348 (origin[0] - xf.x_origin).abs() < 1e-6,
349 "apply([0,0]).x should equal x_origin"
350 );
351 assert!(
352 (origin[1] - xf.y_origin).abs() < 1e-6,
353 "apply([0,0]).y should equal y_origin"
354 );
355
356 let far_corner = xf.apply([4096, 4096]);
357 let circumference = 2.0 * PI * 6_378_137.0;
358 let half = circumference / 2.0;
359 assert!(
360 (far_corner[0] - half).abs() < 1.0,
361 "apply([4096,4096]).x should reach +half"
362 );
363 assert!(
364 (far_corner[1] + half).abs() < 1.0,
365 "apply([4096,4096]).y should reach -half"
366 );
367 }
368
369 #[test]
370 fn tile_transform_tms_vs_xyz() {
371 let xyz = TileTransform::from_zxy(1, 0, 0, 4096, false).unwrap();
372 let tms = TileTransform::from_zxy(1, 0, 1, 4096, true).unwrap();
373
374 assert!(
375 (xyz.x_origin - tms.x_origin).abs() < 1e-6,
376 "same tile via TMS and XYZ should produce same x_origin"
377 );
378 assert!(
379 (xyz.y_origin - tms.y_origin).abs() < 1e-6,
380 "same tile via TMS and XYZ should produce same y_origin"
381 );
382 }
383
384 #[test]
385 fn fixture_parse_and_feature_collection() {
386 let fixture_path = "../../test/synthetic/0x01/point.mlt";
387 let data = fs::read(fixture_path)
388 .unwrap_or_else(|e| panic!("failed to read fixture {fixture_path}: {e}"));
389
390 let layers = Parser::default()
391 .parse_layers(&data)
392 .expect("parse_layers should succeed");
393 let mut dec = Decoder::default();
394 let decoded = dec.decode_all(layers).expect("decode_all should succeed");
395
396 assert!(!decoded.is_empty(), "should parse at least one layer");
397 let l = decoded[0].as_layer01().expect("first layer should be v0.1");
398 assert!(!l.name().is_empty(), "layer name should be non-empty");
399
400 let fc = FeatureCollection::from_layers(decoded).expect("FeatureCollection should succeed");
401 assert!(
402 !fc.features.is_empty(),
403 "feature collection should have features"
404 );
405 }
406
407 #[test]
408 fn fixture_geom_to_wkb_produces_valid_output() {
409 let fixture_path = "../../test/synthetic/0x01/poly.mlt";
410 let data = fs::read(fixture_path)
411 .unwrap_or_else(|e| panic!("failed to read fixture {fixture_path}: {e}"));
412
413 let layers = Parser::default()
414 .parse_layers(&data)
415 .expect("parse_layers should succeed");
416 let mut dec = Decoder::default();
417 let decoded = dec.decode_all(layers).expect("decode_all should succeed");
418
419 let l = decoded[0].as_layer01().expect("first layer should be v0.1");
420 let geom = l.geometry_values();
421
422 let wkb = geom_to_wkb(geom, 0, None).expect("geom_to_wkb should succeed");
423 assert!(
424 wkb.len() >= 5,
425 "WKB must be at least 5 bytes (byte order + type)"
426 );
427 assert_eq!(wkb[0], 0x01, "WKB byte order should be little-endian");
428 let wkb_type = u32::from_le_bytes([wkb[1], wkb[2], wkb[3], wkb[4]]);
429 assert_eq!(
430 wkb_type, 3,
431 "polygon fixture should produce WKB type 3 (Polygon)"
432 );
433 }
434
435 #[test]
436 fn fixture_geom_to_wkb_with_transform() {
437 let fixture_path = "../../test/synthetic/0x01/point.mlt";
438 let data = fs::read(fixture_path)
439 .unwrap_or_else(|e| panic!("failed to read fixture {fixture_path}: {e}"));
440
441 let layers = Parser::default()
442 .parse_layers(&data)
443 .expect("parse_layers should succeed");
444 let mut dec = Decoder::default();
445 let decoded = dec.decode_all(layers).expect("decode_all should succeed");
446
447 let l = decoded[0].as_layer01().expect("first layer should be v0.1");
448 let geom = l.geometry_values();
449
450 let xf = TileTransform::from_zxy(0, 0, 0, l.extent().get(), false).unwrap();
451
452 let wkb_raw = geom_to_wkb(geom, 0, None).expect("raw wkb should succeed");
453 let wkb_xf = geom_to_wkb(geom, 0, Some(xf)).expect("transformed wkb should succeed");
454
455 assert_eq!(
456 wkb_raw.len(),
457 wkb_xf.len(),
458 "raw and transformed WKB should have the same length"
459 );
460 assert_ne!(
461 wkb_raw, wkb_xf,
462 "transformed WKB should differ from raw (unless coordinates are trivially 0)"
463 );
464 }
465
466 #[test]
467 fn fixture_line_produces_wkb_linestring() {
468 let fixture_path = "../../test/synthetic/0x01/line.mlt";
469 let data = fs::read(fixture_path)
470 .unwrap_or_else(|e| panic!("failed to read fixture {fixture_path}: {e}"));
471
472 let layers = Parser::default()
473 .parse_layers(&data)
474 .expect("parse_layers should succeed");
475 let mut dec = Decoder::default();
476 let decoded = dec.decode_all(layers).expect("decode_all should succeed");
477
478 let l = decoded[0].as_layer01().expect("first layer should be v0.1");
479 let geom = l.geometry_values();
480
481 let wkb = geom_to_wkb(geom, 0, None).expect("geom_to_wkb should succeed");
482 assert!(wkb.len() >= 5);
483 let wkb_type = u32::from_le_bytes([wkb[1], wkb[2], wkb[3], wkb[4]]);
484 assert_eq!(
485 wkb_type, 2,
486 "line fixture should produce WKB type 2 (LineString)"
487 );
488 }
489}