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#[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#[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 extent = decoded.extent().get();
214 let xf = match (z, x, y) {
215 (Some(z), Some(x), Some(y)) => Some(TileTransform::from_zxy(z, x, y, extent, tms)?),
216 _ => None,
217 };
218 result.push(MltLayer {
219 name: decoded.name().to_string(),
220 extent,
221 features: build_features(py, &decoded, xf)?,
222 });
223 }
224
225 Ok(result)
226}
227
228#[gen_stub_pyfunction]
230#[pyfunction]
231fn decode_mlt_to_geojson(
232 #[gen_stub(override_type(type_repr = "bytes"))] data: &[u8],
233) -> PyResult<String> {
234 let mut dec = Decoder::default();
235 let layers = dec
236 .decode_all(Parser::default().parse_layers(data).map_err(mlt_err)?)
237 .map_err(mlt_err)?;
238 let fc = FeatureCollection::from_layers(layers).map_err(mlt_err)?;
239 serde_json::to_string(&fc).map_err(|e| PyValueError::new_err(format!("JSON error: {e}")))
240}
241
242#[gen_stub_pyfunction]
244#[pyfunction]
245fn list_layers(
246 #[gen_stub(override_type(type_repr = "bytes"))] data: &[u8],
247) -> PyResult<Vec<String>> {
248 let layers = Parser::default().parse_layers(data).map_err(mlt_err)?;
249 Ok(layers
250 .iter()
251 .filter_map(|l| l.as_layer01().map(|l| l.name().to_string()))
252 .collect())
253}
254
255#[pymodule]
256fn maplibre_tiles(m: &Bound<'_, PyModule>) -> PyResult<()> {
257 m.add_function(wrap_pyfunction!(decode_mlt, m)?)?;
258 m.add_function(wrap_pyfunction!(decode_mlt_to_geojson, m)?)?;
259 m.add_function(wrap_pyfunction!(list_layers, m)?)?;
260 m.add_function(wrap_pyfunction!(encode::geojson::encode_geojson, m)?)?;
261 m.add_function(wrap_pyfunction!(encode::mvt::encode_mvt, m)?)?;
262 m.add_class::<MltLayer>()?;
263 m.add_class::<MltFeature>()?;
264 Ok(())
265}
266
267define_stub_info_gatherer!(stub_info);
268
269#[cfg(test)]
270mod tests {
271 use std::f64::consts::PI;
272 use std::fs;
273
274 use mlt_core::{Decoder, GeometryValues};
275
276 use super::*;
277
278 fn geom_to_wkb(
279 geom: &GeometryValues,
280 index: usize,
281 xf: Option<TileTransform>,
282 ) -> MltResult<Vec<u8>> {
283 geom32_to_wkb(&geom.to_geojson(index)?, xf)
284 }
285
286 #[test]
287 fn tile_transform_rejects_zoom_above_30() {
288 let result = TileTransform::from_zxy(31, 0, 0, 4096, false);
289 assert!(result.is_err(), "z=31 should be rejected");
290
291 let result = TileTransform::from_zxy(30, 0, 0, 4096, false);
292 assert!(result.is_ok(), "z=30 should be accepted");
293
294 let result = TileTransform::from_zxy(0, 0, 0, 4096, false);
295 assert!(result.is_ok(), "z=0 should be accepted");
296 }
297
298 #[test]
299 fn tile_transform_zoom_zero_covers_world() {
300 let xf = TileTransform::from_zxy(0, 0, 0, 4096, false).unwrap();
301
302 let circumference = 2.0 * PI * 6_378_137.0;
303 let half = circumference / 2.0;
304
305 assert!(
306 (xf.x_origin + half).abs() < 1.0,
307 "x_origin at z=0 should be -half_circumference"
308 );
309 assert!(
310 (xf.y_origin - half).abs() < 1.0,
311 "y_origin at z=0 should be +half_circumference"
312 );
313
314 let tile_scale = circumference / 4096.0;
315 assert!(
316 (xf.x_scale - tile_scale).abs() < 1e-6,
317 "x_scale should equal circumference / extent"
318 );
319 assert!(
320 (xf.y_scale + tile_scale).abs() < 1e-6,
321 "y_scale should be negative (flipped)"
322 );
323 }
324
325 #[test]
326 fn tile_transform_apply_maps_origin_and_extent() {
327 let xf = TileTransform::from_zxy(0, 0, 0, 4096, false).unwrap();
328
329 let origin = xf.apply([0, 0]);
330 assert!(
331 (origin[0] - xf.x_origin).abs() < 1e-6,
332 "apply([0,0]).x should equal x_origin"
333 );
334 assert!(
335 (origin[1] - xf.y_origin).abs() < 1e-6,
336 "apply([0,0]).y should equal y_origin"
337 );
338
339 let far_corner = xf.apply([4096, 4096]);
340 let circumference = 2.0 * PI * 6_378_137.0;
341 let half = circumference / 2.0;
342 assert!(
343 (far_corner[0] - half).abs() < 1.0,
344 "apply([4096,4096]).x should reach +half"
345 );
346 assert!(
347 (far_corner[1] + half).abs() < 1.0,
348 "apply([4096,4096]).y should reach -half"
349 );
350 }
351
352 #[test]
353 fn tile_transform_tms_vs_xyz() {
354 let xyz = TileTransform::from_zxy(1, 0, 0, 4096, false).unwrap();
355 let tms = TileTransform::from_zxy(1, 0, 1, 4096, true).unwrap();
356
357 assert!(
358 (xyz.x_origin - tms.x_origin).abs() < 1e-6,
359 "same tile via TMS and XYZ should produce same x_origin"
360 );
361 assert!(
362 (xyz.y_origin - tms.y_origin).abs() < 1e-6,
363 "same tile via TMS and XYZ should produce same y_origin"
364 );
365 }
366
367 #[test]
368 fn fixture_parse_and_feature_collection() {
369 let fixture_path = "../../test/synthetic/0x01/point.mlt";
370 let data = fs::read(fixture_path)
371 .unwrap_or_else(|e| panic!("failed to read fixture {fixture_path}: {e}"));
372
373 let layers = Parser::default()
374 .parse_layers(&data)
375 .expect("parse_layers should succeed");
376 let mut dec = Decoder::default();
377 let decoded = dec.decode_all(layers).expect("decode_all should succeed");
378
379 assert!(!decoded.is_empty(), "should parse at least one layer");
380 let l = decoded[0].as_layer01().expect("first layer should be v0.1");
381 assert!(!l.name().is_empty(), "layer name should be non-empty");
382
383 let fc = FeatureCollection::from_layers(decoded).expect("FeatureCollection should succeed");
384 assert!(
385 !fc.features.is_empty(),
386 "feature collection should have features"
387 );
388 }
389
390 #[test]
391 fn fixture_geom_to_wkb_produces_valid_output() {
392 let fixture_path = "../../test/synthetic/0x01/poly.mlt";
393 let data = fs::read(fixture_path)
394 .unwrap_or_else(|e| panic!("failed to read fixture {fixture_path}: {e}"));
395
396 let layers = Parser::default()
397 .parse_layers(&data)
398 .expect("parse_layers should succeed");
399 let mut dec = Decoder::default();
400 let decoded = dec.decode_all(layers).expect("decode_all should succeed");
401
402 let l = decoded[0].as_layer01().expect("first layer should be v0.1");
403 let geom = l.geometry_values();
404
405 let wkb = geom_to_wkb(geom, 0, None).expect("geom_to_wkb should succeed");
406 assert!(
407 wkb.len() >= 5,
408 "WKB must be at least 5 bytes (byte order + type)"
409 );
410 assert_eq!(wkb[0], 0x01, "WKB byte order should be little-endian");
411 let wkb_type = u32::from_le_bytes([wkb[1], wkb[2], wkb[3], wkb[4]]);
412 assert_eq!(
413 wkb_type, 3,
414 "polygon fixture should produce WKB type 3 (Polygon)"
415 );
416 }
417
418 #[test]
419 fn fixture_geom_to_wkb_with_transform() {
420 let fixture_path = "../../test/synthetic/0x01/point.mlt";
421 let data = fs::read(fixture_path)
422 .unwrap_or_else(|e| panic!("failed to read fixture {fixture_path}: {e}"));
423
424 let layers = Parser::default()
425 .parse_layers(&data)
426 .expect("parse_layers should succeed");
427 let mut dec = Decoder::default();
428 let decoded = dec.decode_all(layers).expect("decode_all should succeed");
429
430 let l = decoded[0].as_layer01().expect("first layer should be v0.1");
431 let geom = l.geometry_values();
432
433 let xf = TileTransform::from_zxy(0, 0, 0, l.extent().get(), false).unwrap();
434
435 let wkb_raw = geom_to_wkb(geom, 0, None).expect("raw wkb should succeed");
436 let wkb_xf = geom_to_wkb(geom, 0, Some(xf)).expect("transformed wkb should succeed");
437
438 assert_eq!(
439 wkb_raw.len(),
440 wkb_xf.len(),
441 "raw and transformed WKB should have the same length"
442 );
443 assert_ne!(
444 wkb_raw, wkb_xf,
445 "transformed WKB should differ from raw (unless coordinates are trivially 0)"
446 );
447 }
448
449 #[test]
450 fn fixture_line_produces_wkb_linestring() {
451 let fixture_path = "../../test/synthetic/0x01/line.mlt";
452 let data = fs::read(fixture_path)
453 .unwrap_or_else(|e| panic!("failed to read fixture {fixture_path}: {e}"));
454
455 let layers = Parser::default()
456 .parse_layers(&data)
457 .expect("parse_layers should succeed");
458 let mut dec = Decoder::default();
459 let decoded = dec.decode_all(layers).expect("decode_all should succeed");
460
461 let l = decoded[0].as_layer01().expect("first layer should be v0.1");
462 let geom = l.geometry_values();
463
464 let wkb = geom_to_wkb(geom, 0, None).expect("geom_to_wkb should succeed");
465 assert!(wkb.len() >= 5);
466 let wkb_type = u32::from_le_bytes([wkb[1], wkb[2], wkb[3], wkb[4]]);
467 assert_eq!(
468 wkb_type, 2,
469 "line fixture should produce WKB type 2 (LineString)"
470 );
471 }
472}