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