Skip to main content

geospatial_types/
vector.rs

1//! Vector tile imagery records (option-2 map source).
2//!
3//! A vector tile travels as its wire bytes — the Mapbox Vector Tile (MVT)
4//! protobuf, optionally wrapped in gzip by the serving CDN — and the
5//! decoder (M9's later slice) turns them into layers and geometries. The
6//! record deliberately carries no parsed state so the cached payload stays
7//! byte-identical to what the provider delivered.
8
9use serde::{Deserialize, Serialize};
10
11use crate::error::SDKError;
12
13/// First byte of the two-byte gzip magic number (`1f 8b`).
14const GZIP_MAGIC_FIRST_BYTE: u8 = 0x1f;
15/// Second byte of the two-byte gzip magic number (`1f 8b`).
16const GZIP_MAGIC_SECOND_BYTE: u8 = 0x8b;
17
18/// Wire encoding of a vector tile's byte payload.
19///
20/// Tile services commonly serve MVT protobuf with `Content-Encoding: gzip`.
21/// The record preserves what arrived on the wire; a decoder applies gzip
22/// decompression only for [`VectorTileEncoding::GzippedMapboxVectorTile`].
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum VectorTileEncoding {
26    /// Plain MVT protobuf bytes.
27    MapboxVectorTile,
28    /// MVT protobuf bytes wrapped in gzip (the CDN's `Content-Encoding`).
29    GzippedMapboxVectorTile,
30}
31
32/// One fetched web-mercator XYZ vector tile.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct VectorTileData {
35    /// Tile column coordinate at the given zoom level.
36    pub tile_x: u32,
37    /// Tile row coordinate at the given zoom level.
38    pub tile_y: u32,
39    /// Zoom level this tile belongs to.
40    pub zoom_level: u8,
41    /// Wire encoding of the byte payload.
42    pub tile_encoding: VectorTileEncoding,
43    /// Encoded tile bytes (MVT protobuf, optionally gzip-wrapped).
44    pub tile_bytes: Vec<u8>,
45}
46
47impl VectorTileData {
48    /// Builds the record from wire bytes, sniffing the gzip wrapper off the
49    /// two-byte magic number so the record names the encoding it actually
50    /// carries.
51    pub fn from_wire_bytes(
52        tile_x: u32,
53        tile_y: u32,
54        zoom_level: u8,
55        wire_bytes: Vec<u8>,
56    ) -> Result<Self, SDKError> {
57        let tile_encoding = Self::sniff_tile_encoding(&wire_bytes)?;
58        Ok(Self {
59            tile_x,
60            tile_y,
61            zoom_level,
62            tile_encoding,
63            tile_bytes: wire_bytes,
64        })
65    }
66
67    /// Classifies the wire payload by its leading gzip magic number; an
68    /// empty payload is a parsing failure, not a silent empty tile.
69    fn sniff_tile_encoding(wire_bytes: &[u8]) -> Result<VectorTileEncoding, SDKError> {
70        if wire_bytes.is_empty() {
71            return Err(SDKError::ResponseParsingFailure(
72                "vector tile payload is empty".to_string(),
73            ));
74        }
75        let carries_gzip_magic = wire_bytes[0] == GZIP_MAGIC_FIRST_BYTE
76            && wire_bytes.len() > 1
77            && wire_bytes[1] == GZIP_MAGIC_SECOND_BYTE;
78        if carries_gzip_magic {
79            Ok(VectorTileEncoding::GzippedMapboxVectorTile)
80        } else {
81            Ok(VectorTileEncoding::MapboxVectorTile)
82        }
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::{VectorTileData, VectorTileEncoding};
89    use crate::SDKError;
90
91    /// 0x1f 0x8b — the two-byte gzip magic the encoding sniff keys on.
92    const GZIP_MAGIC_BYTES: [u8; 2] = [0x1f, 0x8b];
93    /// Protobuf key for MVT's repeated `layers` field (field 3, wire type 2).
94    const LAYER_FIELD_PROTOBUF_KEY: u8 = 0x1a;
95
96    #[test]
97    fn plain_protobuf_bytes_classify_as_mapbox_vector_tiles() {
98        let tile =
99            VectorTileData::from_wire_bytes(1, 2, 3, vec![LAYER_FIELD_PROTOBUF_KEY, 0]).unwrap();
100        assert_eq!(tile.tile_encoding, VectorTileEncoding::MapboxVectorTile);
101        assert_eq!(tile.tile_bytes, vec![LAYER_FIELD_PROTOBUF_KEY, 0]);
102    }
103
104    #[test]
105    fn gzip_magic_bytes_classify_as_gzipped_vector_tiles() {
106        let wire_bytes = [GZIP_MAGIC_BYTES.as_slice(), &[0x08]].concat();
107        let tile = VectorTileData::from_wire_bytes(4, 5, 6, wire_bytes).unwrap();
108        assert_eq!(
109            tile.tile_encoding,
110            VectorTileEncoding::GzippedMapboxVectorTile
111        );
112    }
113
114    #[test]
115    fn empty_wire_payload_is_a_parsing_failure() {
116        assert!(matches!(
117            VectorTileData::from_wire_bytes(0, 0, 0, Vec::new()),
118            Err(SDKError::ResponseParsingFailure(_))
119        ));
120    }
121}