Skip to main content

rustial_engine/
mvt.rs

1//! Mapbox Vector Tile (MVT / PBF) binary decoder.
2//!
3//! This module decodes [Mapbox Vector Tile v2][spec] protocol-buffer
4//! payloads into the engine's [`FeatureCollection`] geometry model.
5//!
6//! [spec]: https://github.com/mapbox/vector-tile-spec/tree/master/2.1
7//!
8//! # Wire format summary
9//!
10//! An MVT tile is a protobuf message containing one or more **layers**,
11//! each of which contains:
12//!
13//! - a `name` (source layer id)
14//! - shared `keys` and `values` string tables
15//! - one or more **features** with:
16//!   - an optional `id`
17//!   - a geometry type (`POINT`, `LINESTRING`, `POLYGON`)
18//!   - a command-encoded geometry stream
19//!   - interleaved `tags` indexing into the key/value tables
20//!
21//! Coordinates in MVT are integer tile-local pixel positions in a grid
22//! of `extent` units (typically 4096).  This decoder converts them to
23//! WGS-84 [`GeoCoord`] using the tile's geographic bounds.
24//!
25//! # Usage
26//!
27//! ```rust,ignore
28//! use rustial_engine::mvt::{decode_mvt, MvtDecodeOptions};
29//! use rustial_math::TileId;
30//!
31//! let tile_id = TileId::new(14, 8192, 5461);
32//! let layers = decode_mvt(&pbf_bytes, &tile_id, &MvtDecodeOptions::default())?;
33//! for (layer_name, features) in &layers {
34//!     println!("{}: {} features", layer_name, features.len());
35//! }
36//! ```
37//!
38//! # Error handling
39//!
40//! Decoding never panics.  Malformed protobuf fields are skipped, and
41//! individual feature decode failures are logged and skipped rather
42//! than aborting the entire tile.
43//!
44//! # No external protobuf dependency
45//!
46//! This decoder hand-rolls the protobuf wire format reading rather
47//! than depending on `prost` or `protobuf`.  The MVT spec uses a tiny
48//! subset of protobuf (varint, length-delimited, fixed32) and the
49//! hand-rolled approach avoids a heavy code-generation dependency for
50//! ~200 lines of wire decoding.
51
52use crate::geometry::{
53    Feature, FeatureCollection, Geometry, LineString, MultiLineString, MultiPoint, MultiPolygon,
54    Point, Polygon, PropertyValue,
55};
56use rustial_math::TileId;
57use std::collections::HashMap;
58use std::fmt;
59
60// ---------------------------------------------------------------------------
61// Public API
62// ---------------------------------------------------------------------------
63
64/// Options controlling MVT decoding behaviour.
65#[derive(Debug, Clone, Default)]
66pub struct MvtDecodeOptions {
67    /// Filter layers to decode.  When empty, all layers are decoded.
68    pub layer_filter: Vec<String>,
69}
70
71/// Errors that can occur during MVT decoding.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub enum MvtError {
74    /// The payload was too short or truncated.
75    TruncatedPayload,
76    /// An unsupported protobuf wire type was encountered.
77    UnsupportedWireType(u8),
78    /// A feature contained an invalid geometry command.
79    InvalidGeometryCommand(u32),
80    /// A generic decode error.
81    DecodeError(String),
82}
83
84impl fmt::Display for MvtError {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        match self {
87            MvtError::TruncatedPayload => write!(f, "truncated MVT payload"),
88            MvtError::UnsupportedWireType(wt) => {
89                write!(f, "unsupported protobuf wire type: {wt}")
90            }
91            MvtError::InvalidGeometryCommand(cmd) => {
92                write!(f, "invalid MVT geometry command: {cmd}")
93            }
94            MvtError::DecodeError(msg) => write!(f, "MVT decode error: {msg}"),
95        }
96    }
97}
98
99impl std::error::Error for MvtError {}
100
101/// Decoded vector tile: a map from source-layer name to feature collection.
102pub type DecodedVectorTile = HashMap<String, FeatureCollection>;
103
104/// Decode an MVT (PBF) binary payload into per-layer feature collections.
105///
106/// Coordinates are transformed from tile-local integer positions to
107/// WGS-84 geographic coordinates using `tile_id` to derive the
108/// tile's geographic bounds.
109pub fn decode_mvt(
110    bytes: &[u8],
111    tile_id: &TileId,
112    options: &MvtDecodeOptions,
113) -> Result<DecodedVectorTile, MvtError> {
114    let tile_bounds = tile_geo_bounds(tile_id);
115    let mut result = DecodedVectorTile::new();
116    let mut reader = PbReader::new(bytes);
117
118    while reader.remaining() > 0 {
119        let (field_number, wire_type) = reader.read_tag()?;
120        match (field_number, wire_type) {
121            // Tile.layers (field 3, length-delimited)
122            (3, WIRE_LEN) => {
123                let layer_bytes = reader.read_bytes()?;
124                match decode_layer(layer_bytes, &tile_bounds, options) {
125                    Ok(Some((name, features))) => {
126                        result
127                            .entry(name)
128                            .or_insert_with(|| FeatureCollection {
129                                features: Vec::new(),
130                            })
131                            .features
132                            .extend(features.features);
133                    }
134                    Ok(None) => {} // filtered out
135                    Err(e) => {
136                        log::warn!("skipping malformed MVT layer: {e}");
137                    }
138                }
139            }
140            _ => {
141                reader.skip_field(wire_type)?;
142            }
143        }
144    }
145
146    Ok(result)
147}
148
149// ---------------------------------------------------------------------------
150// Tile geographic bounds
151// ---------------------------------------------------------------------------
152
153/// Geographic bounds of a tile in WGS-84 degrees.
154struct TileGeoBounds {
155    west: f64,
156    south: f64,
157    east: f64,
158    north: f64,
159}
160
161fn tile_geo_bounds(tile: &TileId) -> TileGeoBounds {
162    let n = (1u64 << tile.zoom) as f64;
163    let west = tile.x as f64 / n * 360.0 - 180.0;
164    let east = (tile.x as f64 + 1.0) / n * 360.0 - 180.0;
165
166    let north_rad = std::f64::consts::PI * (1.0 - 2.0 * tile.y as f64 / n);
167    let south_rad = std::f64::consts::PI * (1.0 - 2.0 * (tile.y as f64 + 1.0) / n);
168
169    let north = north_rad.sinh().atan().to_degrees();
170    let south = south_rad.sinh().atan().to_degrees();
171
172    TileGeoBounds {
173        west,
174        south,
175        east,
176        north,
177    }
178}
179
180/// Convert tile-local integer coordinates to WGS-84.
181#[inline]
182fn tile_coord_to_geo(
183    x: i32,
184    y: i32,
185    extent: u32,
186    bounds: &TileGeoBounds,
187) -> rustial_math::GeoCoord {
188    let extent_f = extent as f64;
189    let lon = bounds.west + (x as f64 / extent_f) * (bounds.east - bounds.west);
190    let lat = bounds.north + (y as f64 / extent_f) * (bounds.south - bounds.north);
191    rustial_math::GeoCoord::from_lat_lon(lat, lon)
192}
193
194// ---------------------------------------------------------------------------
195// Layer decoding
196// ---------------------------------------------------------------------------
197
198fn decode_layer(
199    bytes: &[u8],
200    bounds: &TileGeoBounds,
201    options: &MvtDecodeOptions,
202) -> Result<Option<(String, FeatureCollection)>, MvtError> {
203    let mut reader = PbReader::new(bytes);
204    let mut name = String::new();
205    let mut keys: Vec<String> = Vec::new();
206    let mut values: Vec<PropertyValue> = Vec::new();
207    let mut features_bytes: Vec<&[u8]> = Vec::new();
208    let mut extent: u32 = 4096;
209
210    while reader.remaining() > 0 {
211        let (field_number, wire_type) = reader.read_tag()?;
212        match (field_number, wire_type) {
213            // Layer.name (field 1)
214            (1, WIRE_LEN) => {
215                name = reader.read_string()?;
216            }
217            // Layer.features (field 2)
218            (2, WIRE_LEN) => {
219                features_bytes.push(reader.read_bytes()?);
220            }
221            // Layer.keys (field 3)
222            (3, WIRE_LEN) => {
223                keys.push(reader.read_string()?);
224            }
225            // Layer.values (field 4)
226            (4, WIRE_LEN) => {
227                let val_bytes = reader.read_bytes()?;
228                values.push(decode_value(val_bytes)?);
229            }
230            // Layer.extent (field 5)
231            (5, WIRE_VARINT) => {
232                extent = reader.read_varint()? as u32;
233                if extent == 0 {
234                    extent = 4096;
235                }
236            }
237            // Layer.version (field 15) -- ignored
238            (15, WIRE_VARINT) => {
239                let _ = reader.read_varint()?;
240            }
241            _ => {
242                reader.skip_field(wire_type)?;
243            }
244        }
245    }
246
247    // Apply layer filter.
248    if !options.layer_filter.is_empty() && !options.layer_filter.iter().any(|f| f == &name) {
249        return Ok(None);
250    }
251
252    let mut features = Vec::with_capacity(features_bytes.len());
253    for feat_bytes in features_bytes {
254        match decode_feature(feat_bytes, &keys, &values, extent, bounds) {
255            Ok(feature) => features.push(feature),
256            Err(e) => {
257                log::debug!("skipping malformed MVT feature in layer '{name}': {e}");
258            }
259        }
260    }
261
262    Ok(Some((name, FeatureCollection { features })))
263}
264
265// ---------------------------------------------------------------------------
266// Value decoding
267// ---------------------------------------------------------------------------
268
269fn decode_value(bytes: &[u8]) -> Result<PropertyValue, MvtError> {
270    let mut reader = PbReader::new(bytes);
271    let mut result = PropertyValue::Null;
272
273    while reader.remaining() > 0 {
274        let (field_number, wire_type) = reader.read_tag()?;
275        match (field_number, wire_type) {
276            // string_value (field 1)
277            (1, WIRE_LEN) => {
278                result = PropertyValue::String(reader.read_string()?);
279            }
280            // float_value (field 2)
281            (2, WIRE_32) => {
282                result = PropertyValue::Number(reader.read_fixed32_f32()? as f64);
283            }
284            // double_value (field 3)
285            (3, WIRE_64) => {
286                result = PropertyValue::Number(reader.read_fixed64_f64()?);
287            }
288            // int_value (field 4)
289            (4, WIRE_VARINT) => {
290                result = PropertyValue::Number(reader.read_varint()? as f64);
291            }
292            // uint_value (field 5)
293            (5, WIRE_VARINT) => {
294                result = PropertyValue::Number(reader.read_varint()? as f64);
295            }
296            // sint_value (field 6)
297            (6, WIRE_VARINT) => {
298                let raw = reader.read_varint()?;
299                let decoded = zigzag_decode(raw);
300                result = PropertyValue::Number(decoded as f64);
301            }
302            // bool_value (field 7)
303            (7, WIRE_VARINT) => {
304                result = PropertyValue::Bool(reader.read_varint()? != 0);
305            }
306            _ => {
307                reader.skip_field(wire_type)?;
308            }
309        }
310    }
311
312    Ok(result)
313}
314
315// ---------------------------------------------------------------------------
316// Feature decoding
317// ---------------------------------------------------------------------------
318
319/// MVT geometry types.
320const GEOM_UNKNOWN: u32 = 0;
321const GEOM_POINT: u32 = 1;
322const GEOM_LINESTRING: u32 = 2;
323const GEOM_POLYGON: u32 = 3;
324
325fn decode_feature(
326    bytes: &[u8],
327    keys: &[String],
328    values: &[PropertyValue],
329    extent: u32,
330    bounds: &TileGeoBounds,
331) -> Result<Feature, MvtError> {
332    let mut reader = PbReader::new(bytes);
333    let mut geom_type: u32 = GEOM_UNKNOWN;
334    let mut geometry_bytes: &[u8] = &[];
335    let mut tags_bytes: &[u8] = &[];
336    let mut feature_id: Option<u64> = None;
337
338    while reader.remaining() > 0 {
339        let (field_number, wire_type) = reader.read_tag()?;
340        match (field_number, wire_type) {
341            // Feature.id (field 1)
342            (1, WIRE_VARINT) => {
343                feature_id = Some(reader.read_varint()?);
344            }
345            // Feature.tags (field 2, packed varint)
346            (2, WIRE_LEN) => {
347                tags_bytes = reader.read_bytes()?;
348            }
349            // Feature.type (field 3)
350            (3, WIRE_VARINT) => {
351                geom_type = reader.read_varint()? as u32;
352            }
353            // Feature.geometry (field 4, packed uint32)
354            (4, WIRE_LEN) => {
355                geometry_bytes = reader.read_bytes()?;
356            }
357            _ => {
358                reader.skip_field(wire_type)?;
359            }
360        }
361    }
362
363    let geometry = decode_geometry(geom_type, geometry_bytes, extent, bounds)?;
364    let properties = decode_tags(tags_bytes, keys, values)?;
365
366    let mut props = properties;
367    if let Some(id) = feature_id {
368        props.insert("$id".to_owned(), PropertyValue::Number(id as f64));
369    }
370
371    Ok(Feature {
372        geometry,
373        properties: props,
374    })
375}
376
377fn decode_tags(
378    bytes: &[u8],
379    keys: &[String],
380    values: &[PropertyValue],
381) -> Result<HashMap<String, PropertyValue>, MvtError> {
382    let mut props = HashMap::new();
383    if bytes.is_empty() {
384        return Ok(props);
385    }
386
387    let mut reader = PbReader::new(bytes);
388    while reader.remaining() > 0 {
389        let key_idx = reader.read_varint()? as usize;
390        if reader.remaining() == 0 {
391            break;
392        }
393        let val_idx = reader.read_varint()? as usize;
394
395        if let (Some(key), Some(val)) = (keys.get(key_idx), values.get(val_idx)) {
396            props.insert(key.clone(), val.clone());
397        }
398    }
399
400    Ok(props)
401}
402
403// ---------------------------------------------------------------------------
404// Geometry command decoding
405// ---------------------------------------------------------------------------
406
407/// MVT geometry command IDs.
408const CMD_MOVE_TO: u32 = 1;
409const CMD_LINE_TO: u32 = 2;
410const CMD_CLOSE_PATH: u32 = 7;
411
412fn decode_geometry(
413    geom_type: u32,
414    bytes: &[u8],
415    extent: u32,
416    bounds: &TileGeoBounds,
417) -> Result<Geometry, MvtError> {
418    let commands = decode_commands(bytes)?;
419    let rings = commands_to_rings(&commands, extent, bounds);
420
421    match geom_type {
422        GEOM_POINT => geometry_from_points(rings),
423        GEOM_LINESTRING => geometry_from_linestrings(rings),
424        GEOM_POLYGON => geometry_from_polygons(rings),
425        _ => Err(MvtError::DecodeError(format!(
426            "unknown geometry type: {geom_type}"
427        ))),
428    }
429}
430
431struct GeomCommand {
432    id: u32,
433    params: Vec<(i32, i32)>,
434}
435
436fn decode_commands(bytes: &[u8]) -> Result<Vec<GeomCommand>, MvtError> {
437    let mut reader = PbReader::new(bytes);
438    let mut commands = Vec::new();
439    let mut cursor_x: i32 = 0;
440    let mut cursor_y: i32 = 0;
441
442    while reader.remaining() > 0 {
443        let cmd_int = reader.read_varint()? as u32;
444        let cmd_id = cmd_int & 0x7;
445        let cmd_count = cmd_int >> 3;
446
447        if cmd_id == CMD_CLOSE_PATH {
448            commands.push(GeomCommand {
449                id: CMD_CLOSE_PATH,
450                params: Vec::new(),
451            });
452            continue;
453        }
454
455        if cmd_id != CMD_MOVE_TO && cmd_id != CMD_LINE_TO {
456            return Err(MvtError::InvalidGeometryCommand(cmd_int));
457        }
458
459        let mut params = Vec::with_capacity(cmd_count as usize);
460        for _ in 0..cmd_count {
461            if reader.remaining() < 2 {
462                break;
463            }
464            let dx = zigzag_decode(reader.read_varint()?) as i32;
465            let dy = zigzag_decode(reader.read_varint()?) as i32;
466            cursor_x += dx;
467            cursor_y += dy;
468            params.push((cursor_x, cursor_y));
469        }
470
471        commands.push(GeomCommand { id: cmd_id, params });
472    }
473
474    Ok(commands)
475}
476
477fn commands_to_rings(
478    commands: &[GeomCommand],
479    extent: u32,
480    bounds: &TileGeoBounds,
481) -> Vec<Vec<rustial_math::GeoCoord>> {
482    let mut rings: Vec<Vec<rustial_math::GeoCoord>> = Vec::new();
483    let mut current_ring: Vec<rustial_math::GeoCoord> = Vec::new();
484
485    for cmd in commands {
486        match cmd.id {
487            CMD_MOVE_TO => {
488                if !current_ring.is_empty() {
489                    rings.push(std::mem::take(&mut current_ring));
490                }
491                for &(x, y) in &cmd.params {
492                    current_ring.push(tile_coord_to_geo(x, y, extent, bounds));
493                }
494            }
495            CMD_LINE_TO => {
496                for &(x, y) in &cmd.params {
497                    current_ring.push(tile_coord_to_geo(x, y, extent, bounds));
498                }
499            }
500            CMD_CLOSE_PATH => {
501                if let Some(&first) = current_ring.first() {
502                    current_ring.push(first);
503                }
504                rings.push(std::mem::take(&mut current_ring));
505            }
506            _ => {}
507        }
508    }
509
510    if !current_ring.is_empty() {
511        rings.push(current_ring);
512    }
513
514    rings
515}
516
517fn geometry_from_points(rings: Vec<Vec<rustial_math::GeoCoord>>) -> Result<Geometry, MvtError> {
518    let points: Vec<Point> = rings
519        .into_iter()
520        .flat_map(|ring| ring.into_iter().map(|coord| Point { coord }))
521        .collect();
522
523    match points.len() {
524        0 => Err(MvtError::DecodeError("empty point geometry".into())),
525        1 => Ok(Geometry::Point(points.into_iter().next().expect("len==1"))),
526        _ => Ok(Geometry::MultiPoint(MultiPoint { points })),
527    }
528}
529
530fn geometry_from_linestrings(
531    rings: Vec<Vec<rustial_math::GeoCoord>>,
532) -> Result<Geometry, MvtError> {
533    let lines: Vec<LineString> = rings
534        .into_iter()
535        .filter(|r| r.len() >= 2)
536        .map(|coords| LineString { coords })
537        .collect();
538
539    match lines.len() {
540        0 => Err(MvtError::DecodeError("empty linestring geometry".into())),
541        1 => Ok(Geometry::LineString(
542            lines.into_iter().next().expect("len==1"),
543        )),
544        _ => Ok(Geometry::MultiLineString(MultiLineString { lines })),
545    }
546}
547
548fn geometry_from_polygons(rings: Vec<Vec<rustial_math::GeoCoord>>) -> Result<Geometry, MvtError> {
549    if rings.is_empty() {
550        return Err(MvtError::DecodeError("empty polygon geometry".into()));
551    }
552
553    let mut polygons: Vec<Polygon> = Vec::new();
554
555    for ring in rings {
556        if ring.len() < 4 {
557            continue;
558        }
559
560        let area = signed_ring_area(&ring);
561
562        if area > 0.0 {
563            // Positive area = exterior ring (counter-clockwise in screen space
564            // = clockwise in geographic = outer ring in MVT spec).
565            // Start a new polygon.
566            polygons.push(Polygon {
567                exterior: ring,
568                interiors: Vec::new(),
569            });
570        } else if area < 0.0 {
571            // Negative area = interior ring (hole).
572            if let Some(last) = polygons.last_mut() {
573                last.interiors.push(ring);
574            } else {
575                // Orphan hole -- promote to exterior.
576                let mut reversed = ring;
577                reversed.reverse();
578                polygons.push(Polygon {
579                    exterior: reversed,
580                    interiors: Vec::new(),
581                });
582            }
583        }
584        // area == 0 -> degenerate, skip
585    }
586
587    match polygons.len() {
588        0 => Err(MvtError::DecodeError("no valid polygon rings found".into())),
589        1 => Ok(Geometry::Polygon(
590            polygons.into_iter().next().expect("len==1"),
591        )),
592        _ => Ok(Geometry::MultiPolygon(MultiPolygon { polygons })),
593    }
594}
595
596/// Compute the signed area of a ring using the shoelace formula.
597///
598/// Positive = counter-clockwise in screen space (MVT exterior ring).
599fn signed_ring_area(ring: &[rustial_math::GeoCoord]) -> f64 {
600    let mut area = 0.0f64;
601    let n = ring.len();
602    if n < 3 {
603        return 0.0;
604    }
605    for i in 0..n {
606        let j = (i + 1) % n;
607        // Using lon as x, lat as y for area computation.
608        area += ring[i].lon * ring[j].lat;
609        area -= ring[j].lon * ring[i].lat;
610    }
611    area / 2.0
612}
613
614// ---------------------------------------------------------------------------
615// Protobuf wire format reader
616// ---------------------------------------------------------------------------
617
618/// Protobuf wire types used by MVT.
619const WIRE_VARINT: u8 = 0;
620const WIRE_64: u8 = 1;
621const WIRE_LEN: u8 = 2;
622const WIRE_32: u8 = 5;
623
624/// Minimal protobuf reader for MVT decoding.
625struct PbReader<'a> {
626    data: &'a [u8],
627    pos: usize,
628}
629
630impl<'a> PbReader<'a> {
631    fn new(data: &'a [u8]) -> Self {
632        Self { data, pos: 0 }
633    }
634
635    #[inline]
636    fn remaining(&self) -> usize {
637        self.data.len().saturating_sub(self.pos)
638    }
639
640    fn read_byte(&mut self) -> Result<u8, MvtError> {
641        if self.pos >= self.data.len() {
642            return Err(MvtError::TruncatedPayload);
643        }
644        let b = self.data[self.pos];
645        self.pos += 1;
646        Ok(b)
647    }
648
649    fn read_varint(&mut self) -> Result<u64, MvtError> {
650        let mut result: u64 = 0;
651        let mut shift: u32 = 0;
652        loop {
653            let b = self.read_byte()?;
654            result |= ((b & 0x7F) as u64) << shift;
655            if b & 0x80 == 0 {
656                return Ok(result);
657            }
658            shift += 7;
659            if shift >= 64 {
660                return Err(MvtError::DecodeError("varint too long".into()));
661            }
662        }
663    }
664
665    fn read_tag(&mut self) -> Result<(u32, u8), MvtError> {
666        let varint = self.read_varint()? as u32;
667        let field_number = varint >> 3;
668        let wire_type = (varint & 0x7) as u8;
669        Ok((field_number, wire_type))
670    }
671
672    fn read_bytes(&mut self) -> Result<&'a [u8], MvtError> {
673        let len = self.read_varint()? as usize;
674        if self.pos + len > self.data.len() {
675            return Err(MvtError::TruncatedPayload);
676        }
677        let slice = &self.data[self.pos..self.pos + len];
678        self.pos += len;
679        Ok(slice)
680    }
681
682    fn read_string(&mut self) -> Result<String, MvtError> {
683        let bytes = self.read_bytes()?;
684        String::from_utf8(bytes.to_vec())
685            .map_err(|e| MvtError::DecodeError(format!("invalid UTF-8: {e}")))
686    }
687
688    fn read_fixed32_f32(&mut self) -> Result<f32, MvtError> {
689        if self.remaining() < 4 {
690            return Err(MvtError::TruncatedPayload);
691        }
692        let bytes = [
693            self.data[self.pos],
694            self.data[self.pos + 1],
695            self.data[self.pos + 2],
696            self.data[self.pos + 3],
697        ];
698        self.pos += 4;
699        Ok(f32::from_le_bytes(bytes))
700    }
701
702    fn read_fixed64_f64(&mut self) -> Result<f64, MvtError> {
703        if self.remaining() < 8 {
704            return Err(MvtError::TruncatedPayload);
705        }
706        let mut bytes = [0u8; 8];
707        bytes.copy_from_slice(&self.data[self.pos..self.pos + 8]);
708        self.pos += 8;
709        Ok(f64::from_le_bytes(bytes))
710    }
711
712    fn skip_field(&mut self, wire_type: u8) -> Result<(), MvtError> {
713        match wire_type {
714            WIRE_VARINT => {
715                let _ = self.read_varint()?;
716            }
717            WIRE_64 => {
718                if self.remaining() < 8 {
719                    return Err(MvtError::TruncatedPayload);
720                }
721                self.pos += 8;
722            }
723            WIRE_LEN => {
724                let _ = self.read_bytes()?;
725            }
726            WIRE_32 => {
727                if self.remaining() < 4 {
728                    return Err(MvtError::TruncatedPayload);
729                }
730                self.pos += 4;
731            }
732            other => return Err(MvtError::UnsupportedWireType(other)),
733        }
734        Ok(())
735    }
736}
737
738/// Protobuf zigzag decoding: maps unsigned varint back to signed.
739#[inline]
740fn zigzag_decode(n: u64) -> i64 {
741    ((n >> 1) as i64) ^ -((n & 1) as i64)
742}
743
744// ---------------------------------------------------------------------------
745// Tests
746// ---------------------------------------------------------------------------
747
748#[cfg(test)]
749mod tests {
750    use super::*;
751
752    // -- Zigzag decoding --------------------------------------------------
753
754    #[test]
755    fn zigzag_decode_positive() {
756        assert_eq!(zigzag_decode(0), 0);
757        assert_eq!(zigzag_decode(2), 1);
758        assert_eq!(zigzag_decode(4), 2);
759        assert_eq!(zigzag_decode(100), 50);
760    }
761
762    #[test]
763    fn zigzag_decode_negative() {
764        assert_eq!(zigzag_decode(1), -1);
765        assert_eq!(zigzag_decode(3), -2);
766        assert_eq!(zigzag_decode(5), -3);
767        assert_eq!(zigzag_decode(99), -50);
768    }
769
770    // -- PbReader basic operations ----------------------------------------
771
772    #[test]
773    fn pb_reader_read_varint_single_byte() {
774        let data = [0x08]; // varint 8
775        let mut reader = PbReader::new(&data);
776        assert_eq!(reader.read_varint().unwrap(), 8);
777    }
778
779    #[test]
780    fn pb_reader_read_varint_multi_byte() {
781        let data = [0xAC, 0x02]; // 300
782        let mut reader = PbReader::new(&data);
783        assert_eq!(reader.read_varint().unwrap(), 300);
784    }
785
786    #[test]
787    fn pb_reader_read_tag() {
788        // field_number=3, wire_type=2 (length-delimited): (3 << 3) | 2 = 26
789        let data = [26];
790        let mut reader = PbReader::new(&data);
791        let (field, wire) = reader.read_tag().unwrap();
792        assert_eq!(field, 3);
793        assert_eq!(wire, WIRE_LEN);
794    }
795
796    #[test]
797    fn pb_reader_read_string() {
798        // length=5, then "hello"
799        let data = [5, b'h', b'e', b'l', b'l', b'o'];
800        let mut reader = PbReader::new(&data);
801        assert_eq!(reader.read_string().unwrap(), "hello");
802    }
803
804    #[test]
805    fn pb_reader_truncated_payload() {
806        let data = [0xFF]; // varint continuation but no next byte
807        let mut reader = PbReader::new(&data);
808        assert!(reader.read_varint().is_err());
809    }
810
811    // -- Tile geo bounds --------------------------------------------------
812
813    #[test]
814    fn tile_geo_bounds_zoom_0() {
815        let bounds = tile_geo_bounds(&TileId::new(0, 0, 0));
816        assert!((bounds.west - (-180.0)).abs() < 1e-6);
817        assert!((bounds.east - 180.0).abs() < 1e-6);
818        assert!(bounds.north > 85.0);
819        assert!(bounds.south < -85.0);
820    }
821
822    #[test]
823    fn tile_geo_bounds_higher_zoom() {
824        let bounds = tile_geo_bounds(&TileId::new(1, 0, 0));
825        assert!((bounds.west - (-180.0)).abs() < 1e-6);
826        assert!((bounds.east - 0.0).abs() < 1e-6);
827        assert!(bounds.north > 85.0);
828        assert!(bounds.south > -1.0); // north half
829    }
830
831    // -- Signed ring area -------------------------------------------------
832
833    #[test]
834    fn signed_ring_area_ccw_positive() {
835        use rustial_math::GeoCoord;
836        // CCW square in lon/lat space
837        let ring = vec![
838            GeoCoord::from_lat_lon(0.0, 0.0),
839            GeoCoord::from_lat_lon(0.0, 1.0),
840            GeoCoord::from_lat_lon(1.0, 1.0),
841            GeoCoord::from_lat_lon(1.0, 0.0),
842            GeoCoord::from_lat_lon(0.0, 0.0),
843        ];
844        let area = signed_ring_area(&ring);
845        assert!(area > 0.0, "CCW ring should have positive area, got {area}");
846    }
847
848    #[test]
849    fn signed_ring_area_cw_negative() {
850        use rustial_math::GeoCoord;
851        // CW square
852        let ring = vec![
853            GeoCoord::from_lat_lon(0.0, 0.0),
854            GeoCoord::from_lat_lon(1.0, 0.0),
855            GeoCoord::from_lat_lon(1.0, 1.0),
856            GeoCoord::from_lat_lon(0.0, 1.0),
857            GeoCoord::from_lat_lon(0.0, 0.0),
858        ];
859        let area = signed_ring_area(&ring);
860        assert!(area < 0.0, "CW ring should have negative area, got {area}");
861    }
862
863    // -- Coordinate conversion --------------------------------------------
864
865    #[test]
866    fn tile_coord_to_geo_corners() {
867        let bounds = tile_geo_bounds(&TileId::new(0, 0, 0));
868        let nw = tile_coord_to_geo(0, 0, 4096, &bounds);
869        assert!((nw.lon - (-180.0)).abs() < 0.01);
870        assert!(nw.lat > 85.0);
871
872        let se = tile_coord_to_geo(4096, 4096, 4096, &bounds);
873        assert!((se.lon - 180.0).abs() < 0.01);
874        assert!(se.lat < -85.0);
875    }
876
877    // -- MVT encoding helper for tests ------------------------------------
878
879    fn encode_varint(mut val: u64) -> Vec<u8> {
880        let mut buf = Vec::new();
881        loop {
882            let mut byte = (val & 0x7F) as u8;
883            val >>= 7;
884            if val != 0 {
885                byte |= 0x80;
886            }
887            buf.push(byte);
888            if val == 0 {
889                break;
890            }
891        }
892        buf
893    }
894
895    fn encode_tag(field_number: u32, wire_type: u8) -> Vec<u8> {
896        encode_varint(((field_number as u64) << 3) | wire_type as u64)
897    }
898
899    fn encode_len_delimited(field_number: u32, data: &[u8]) -> Vec<u8> {
900        let mut buf = encode_tag(field_number, WIRE_LEN);
901        buf.extend(encode_varint(data.len() as u64));
902        buf.extend_from_slice(data);
903        buf
904    }
905
906    fn encode_string_field(field_number: u32, s: &str) -> Vec<u8> {
907        encode_len_delimited(field_number, s.as_bytes())
908    }
909
910    fn encode_varint_field(field_number: u32, val: u64) -> Vec<u8> {
911        let mut buf = encode_tag(field_number, WIRE_VARINT);
912        buf.extend(encode_varint(val));
913        buf
914    }
915
916    fn zigzag_encode(n: i32) -> u32 {
917        ((n << 1) ^ (n >> 31)) as u32
918    }
919
920    fn encode_geometry_commands(commands: &[(u32, &[(i32, i32)])]) -> Vec<u8> {
921        let mut buf = Vec::new();
922        for &(cmd_id, params) in commands {
923            let count = if cmd_id == CMD_CLOSE_PATH {
924                1u32
925            } else {
926                params.len() as u32
927            };
928            buf.extend(encode_varint(((count as u64) << 3) | cmd_id as u64));
929            for &(dx, dy) in params {
930                buf.extend(encode_varint(zigzag_encode(dx) as u64));
931                buf.extend(encode_varint(zigzag_encode(dy) as u64));
932            }
933        }
934        buf
935    }
936
937    fn build_mvt_value_string(s: &str) -> Vec<u8> {
938        encode_string_field(1, s)
939    }
940
941    fn build_mvt_value_double(v: f64) -> Vec<u8> {
942        let mut buf = encode_tag(3, WIRE_64);
943        buf.extend(v.to_le_bytes());
944        buf
945    }
946
947    fn build_mvt_feature(
948        id: Option<u64>,
949        geom_type: u32,
950        tags: &[u32],
951        geometry: &[u8],
952    ) -> Vec<u8> {
953        let mut buf = Vec::new();
954        if let Some(id) = id {
955            buf.extend(encode_varint_field(1, id));
956        }
957        if !tags.is_empty() {
958            let mut tags_buf = Vec::new();
959            for &t in tags {
960                tags_buf.extend(encode_varint(t as u64));
961            }
962            buf.extend(encode_len_delimited(2, &tags_buf));
963        }
964        buf.extend(encode_varint_field(3, geom_type as u64));
965        buf.extend(encode_len_delimited(4, geometry));
966        buf
967    }
968
969    fn build_mvt_layer(
970        name: &str,
971        features: &[Vec<u8>],
972        keys: &[&str],
973        values: &[Vec<u8>],
974        extent: u32,
975    ) -> Vec<u8> {
976        let mut buf = Vec::new();
977        buf.extend(encode_string_field(1, name));
978        for feat in features {
979            buf.extend(encode_len_delimited(2, feat));
980        }
981        for key in keys {
982            buf.extend(encode_string_field(3, key));
983        }
984        for val in values {
985            buf.extend(encode_len_delimited(4, val));
986        }
987        buf.extend(encode_varint_field(5, extent as u64));
988        buf.extend(encode_varint_field(15, 2)); // version = 2
989        buf
990    }
991
992    fn build_mvt_tile(layers: &[Vec<u8>]) -> Vec<u8> {
993        let mut buf = Vec::new();
994        for layer in layers {
995            buf.extend(encode_len_delimited(3, layer));
996        }
997        buf
998    }
999
1000    // -- Full MVT decode --------------------------------------------------
1001
1002    #[test]
1003    fn decode_empty_tile() {
1004        let bytes = build_mvt_tile(&[]);
1005        let result = decode_mvt(&bytes, &TileId::new(0, 0, 0), &MvtDecodeOptions::default());
1006        assert!(result.is_ok());
1007        assert!(result.unwrap().is_empty());
1008    }
1009
1010    #[test]
1011    fn decode_point_feature() {
1012        // A point at tile-local coords (2048, 2048) = center of tile
1013        let geom = encode_geometry_commands(&[(CMD_MOVE_TO, &[(2048, 2048)])]);
1014        let feature = build_mvt_feature(Some(42), GEOM_POINT, &[], &geom);
1015        let layer = build_mvt_layer("points", &[feature], &[], &[], 4096);
1016        let tile = build_mvt_tile(&[layer]);
1017
1018        let tile_id = TileId::new(0, 0, 0);
1019        let result = decode_mvt(&tile, &tile_id, &MvtDecodeOptions::default()).unwrap();
1020
1021        assert_eq!(result.len(), 1);
1022        let features = &result["points"];
1023        assert_eq!(features.len(), 1);
1024
1025        match &features.features[0].geometry {
1026            Geometry::Point(pt) => {
1027                // Center of zoom-0 tile should be roughly (0, 0)
1028                assert!(pt.coord.lon.abs() < 1.0);
1029                assert!(pt.coord.lat.abs() < 1.0);
1030            }
1031            other => panic!("expected Point, got {}", other.type_name()),
1032        }
1033
1034        // Check feature id
1035        let id_prop = features.features[0].property("$id");
1036        assert_eq!(id_prop.and_then(|v| v.as_f64()), Some(42.0));
1037    }
1038
1039    #[test]
1040    fn decode_linestring_feature() {
1041        let geom = encode_geometry_commands(&[
1042            (CMD_MOVE_TO, &[(0, 0)]),
1043            (CMD_LINE_TO, &[(4096, 0), (0, 4096)]),
1044        ]);
1045        let feature = build_mvt_feature(None, GEOM_LINESTRING, &[], &geom);
1046        let layer = build_mvt_layer("roads", &[feature], &[], &[], 4096);
1047        let tile = build_mvt_tile(&[layer]);
1048
1049        let tile_id = TileId::new(0, 0, 0);
1050        let result = decode_mvt(&tile, &tile_id, &MvtDecodeOptions::default()).unwrap();
1051
1052        let features = &result["roads"];
1053        assert_eq!(features.len(), 1);
1054        match &features.features[0].geometry {
1055            Geometry::LineString(ls) => {
1056                assert_eq!(ls.coords.len(), 3);
1057            }
1058            other => panic!("expected LineString, got {}", other.type_name()),
1059        }
1060    }
1061
1062    #[test]
1063    fn decode_polygon_feature() {
1064        // Exterior ring (CW in screen space = positive area in MVT convention)
1065        let geom = encode_geometry_commands(&[
1066            (CMD_MOVE_TO, &[(0, 0)]),
1067            (CMD_LINE_TO, &[(4096, 0), (0, 4096), (-4096, 0)]),
1068            (CMD_CLOSE_PATH, &[]),
1069        ]);
1070        let feature = build_mvt_feature(None, GEOM_POLYGON, &[], &geom);
1071        let layer = build_mvt_layer("water", &[feature], &[], &[], 4096);
1072        let tile = build_mvt_tile(&[layer]);
1073
1074        let tile_id = TileId::new(0, 0, 0);
1075        let result = decode_mvt(&tile, &tile_id, &MvtDecodeOptions::default()).unwrap();
1076
1077        let features = &result["water"];
1078        assert_eq!(features.len(), 1);
1079        match &features.features[0].geometry {
1080            Geometry::Polygon(poly) => {
1081                assert!(poly.exterior.len() >= 4, "polygon should have 4+ vertices");
1082                assert!(poly.interiors.is_empty());
1083            }
1084            other => panic!("expected Polygon, got {}", other.type_name()),
1085        }
1086    }
1087
1088    #[test]
1089    fn decode_feature_with_properties() {
1090        let geom = encode_geometry_commands(&[(CMD_MOVE_TO, &[(100, 100)])]);
1091        let keys = &["name", "population"];
1092        let values = &[
1093            build_mvt_value_string("Springfield"),
1094            build_mvt_value_double(12345.0),
1095        ];
1096        // tags: key_idx=0, val_idx=0, key_idx=1, val_idx=1
1097        let feature = build_mvt_feature(None, GEOM_POINT, &[0, 0, 1, 1], &geom);
1098        let layer = build_mvt_layer("places", &[feature], keys, values, 4096);
1099        let tile = build_mvt_tile(&[layer]);
1100
1101        let tile_id = TileId::new(1, 0, 0);
1102        let result = decode_mvt(&tile, &tile_id, &MvtDecodeOptions::default()).unwrap();
1103
1104        let features = &result["places"];
1105        assert_eq!(features.len(), 1);
1106        let props = &features.features[0].properties;
1107        assert_eq!(
1108            props.get("name").and_then(|v| v.as_str()),
1109            Some("Springfield")
1110        );
1111        assert_eq!(
1112            props.get("population").and_then(|v| v.as_f64()),
1113            Some(12345.0)
1114        );
1115    }
1116
1117    #[test]
1118    fn decode_with_layer_filter() {
1119        let geom = encode_geometry_commands(&[(CMD_MOVE_TO, &[(100, 100)])]);
1120        let feat = build_mvt_feature(None, GEOM_POINT, &[], &geom);
1121        let layer_a = build_mvt_layer("water", std::slice::from_ref(&feat), &[], &[], 4096);
1122        let layer_b = build_mvt_layer("roads", &[feat], &[], &[], 4096);
1123        let tile = build_mvt_tile(&[layer_a, layer_b]);
1124
1125        let options = MvtDecodeOptions {
1126            layer_filter: vec!["water".into()],
1127        };
1128        let tile_id = TileId::new(0, 0, 0);
1129        let result = decode_mvt(&tile, &tile_id, &options).unwrap();
1130
1131        assert_eq!(result.len(), 1);
1132        assert!(result.contains_key("water"));
1133        assert!(!result.contains_key("roads"));
1134    }
1135
1136    #[test]
1137    fn decode_multi_point_feature() {
1138        let geom = encode_geometry_commands(&[(CMD_MOVE_TO, &[(100, 100), (200, 200)])]);
1139        let feature = build_mvt_feature(None, GEOM_POINT, &[], &geom);
1140        let layer = build_mvt_layer("multi", &[feature], &[], &[], 4096);
1141        let tile = build_mvt_tile(&[layer]);
1142
1143        let tile_id = TileId::new(0, 0, 0);
1144        let result = decode_mvt(&tile, &tile_id, &MvtDecodeOptions::default()).unwrap();
1145        match &result["multi"].features[0].geometry {
1146            Geometry::MultiPoint(mp) => {
1147                assert_eq!(mp.points.len(), 2);
1148            }
1149            other => panic!("expected MultiPoint, got {}", other.type_name()),
1150        }
1151    }
1152
1153    #[test]
1154    fn mvt_error_display() {
1155        assert!(MvtError::TruncatedPayload.to_string().contains("truncated"));
1156        assert!(MvtError::UnsupportedWireType(6).to_string().contains("6"));
1157        assert!(MvtError::InvalidGeometryCommand(99)
1158            .to_string()
1159            .contains("99"));
1160    }
1161}