mlt_wasm/lib.rs
1//! WebAssembly bindings for the `MapLibre` Tile (MLT) format.
2//!
3//! # Design
4//!
5//! A single `MltTile` struct owns all decoded [`mlt_core::TileLayer`] data for
6//! every layer in the tile. No per-layer or per-feature WASM objects are
7//! created; every accessor takes explicit `(layer_idx, feature_idx)` arguments
8//! so the JavaScript side can keep plain numeric indices rather than
9//! heap-allocated wrapper objects.
10//!
11//! ## Geometry
12//!
13//! `MltTile::layer_geometry` returns a `LayerGeometry`
14//! whose typed-array getters expose the raw offset and vertex buffers.
15//! JS walks these directly - zero WASM boundary crossings per feature.
16//!
17//! ## IDs
18//!
19//! `MltTile::layer_ids` returns a `Float64Array` - one `f64` per
20//! feature. Absent IDs are `NaN` (≡ `undefined` after the JS wrapper checks
21//! `isNaN`). IDs above `Number.MAX_SAFE_INTEGER` lose precision.
22//!
23//! ## Properties
24//!
25//! `MltTile::layer_property_keys` and `MltTile::layer_properties`
26//! expose all property columns as typed arrays built once per layer. JS reads
27//! any feature's property with a single array index - zero WASM calls during
28//! traversal.
29
30mod geometry;
31mod layer;
32mod properties;
33mod tile;
34
35use js_sys::Uint8Array;
36use layer::DecodedLayer;
37use mlt_core::{Decoder, GeometryType, MltError, Parser};
38use tile::MltTile;
39use wasm_bindgen::prelude::*;
40
41/// Decode a raw MLT tile blob and return an `MltTile`.
42///
43/// All geometry, IDs and properties are decoded eagerly into row-oriented
44/// [`mlt_core::TileLayer`] values.
45#[wasm_bindgen]
46pub fn decode_tile(data: &[u8]) -> Result<MltTile, JsError> {
47 let mut parser = Parser::default();
48 let raw_layers = parser.parse_layers(data).map_err(|e| to_js_err(&e))?;
49 let mut dec = Decoder::default();
50 let mut layers = Vec::with_capacity(raw_layers.len());
51
52 for raw_layer in raw_layers {
53 // Skip non-Tag01 layers.
54 let mlt_core::Layer::Tag01(layer01) = raw_layer else {
55 continue;
56 };
57
58 // Decode all columns at once, then extract geometry arrays before consuming into tile.
59 let parsed_layer = layer01.decode_all(&mut dec).map_err(|e| to_js_err(&e))?;
60
61 // Clone geometry values for building WASM typed arrays (zero wire-decode overhead:
62 // geometry is already in columnar form from decode_all).
63 let parsed_geometry = parsed_layer.geometry_values().clone();
64
65 let (types_bytes, mlt_types_bytes): (Vec<u8>, Vec<u8>) = parsed_geometry
66 .vector_types()
67 .iter()
68 .map(|t| {
69 let mvt = match t {
70 GeometryType::Point | GeometryType::MultiPoint => 1,
71 GeometryType::LineString | GeometryType::MultiLineString => 2,
72 GeometryType::Polygon | GeometryType::MultiPolygon => 3,
73 #[allow(unreachable_patterns)]
74 _ => 0,
75 };
76 (mvt, *t as u8)
77 })
78 .unzip();
79 let types_array = Uint8Array::from(types_bytes.as_slice());
80 let mlt_types_array = Uint8Array::from(mlt_types_bytes.as_slice());
81
82 let tile = parsed_layer
83 .into_tile(&mut dec)
84 .map_err(|e| to_js_err(&e))?;
85
86 layers.push(DecodedLayer {
87 tile,
88 types_array,
89 mlt_types_array,
90 geometry: parsed_geometry,
91 });
92 }
93
94 Ok(MltTile { layers })
95}
96
97pub(crate) fn to_js_err(e: &MltError) -> JsError {
98 JsError::new(&e.to_string())
99}