use crate::geometry::{
Feature, FeatureCollection, Geometry, LineString, MultiLineString, MultiPoint, MultiPolygon,
Point, Polygon, PropertyValue,
};
use rustial_math::TileId;
use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, Default)]
pub struct MvtDecodeOptions {
pub layer_filter: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MvtError {
TruncatedPayload,
UnsupportedWireType(u8),
InvalidGeometryCommand(u32),
DecodeError(String),
}
impl fmt::Display for MvtError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MvtError::TruncatedPayload => write!(f, "truncated MVT payload"),
MvtError::UnsupportedWireType(wt) => {
write!(f, "unsupported protobuf wire type: {wt}")
}
MvtError::InvalidGeometryCommand(cmd) => {
write!(f, "invalid MVT geometry command: {cmd}")
}
MvtError::DecodeError(msg) => write!(f, "MVT decode error: {msg}"),
}
}
}
impl std::error::Error for MvtError {}
pub type DecodedVectorTile = HashMap<String, FeatureCollection>;
pub fn decode_mvt(
bytes: &[u8],
tile_id: &TileId,
options: &MvtDecodeOptions,
) -> Result<DecodedVectorTile, MvtError> {
let tile_bounds = tile_geo_bounds(tile_id);
let mut result = DecodedVectorTile::new();
let mut reader = PbReader::new(bytes);
while reader.remaining() > 0 {
let (field_number, wire_type) = reader.read_tag()?;
match (field_number, wire_type) {
(3, WIRE_LEN) => {
let layer_bytes = reader.read_bytes()?;
match decode_layer(layer_bytes, &tile_bounds, options) {
Ok(Some((name, features))) => {
result
.entry(name)
.or_insert_with(|| FeatureCollection {
features: Vec::new(),
})
.features
.extend(features.features);
}
Ok(None) => {} Err(e) => {
log::warn!("skipping malformed MVT layer: {e}");
}
}
}
_ => {
reader.skip_field(wire_type)?;
}
}
}
Ok(result)
}
struct TileGeoBounds {
west: f64,
south: f64,
east: f64,
north: f64,
}
fn tile_geo_bounds(tile: &TileId) -> TileGeoBounds {
let n = (1u64 << tile.zoom) as f64;
let west = tile.x as f64 / n * 360.0 - 180.0;
let east = (tile.x as f64 + 1.0) / n * 360.0 - 180.0;
let north_rad = std::f64::consts::PI * (1.0 - 2.0 * tile.y as f64 / n);
let south_rad = std::f64::consts::PI * (1.0 - 2.0 * (tile.y as f64 + 1.0) / n);
let north = north_rad.sinh().atan().to_degrees();
let south = south_rad.sinh().atan().to_degrees();
TileGeoBounds {
west,
south,
east,
north,
}
}
#[inline]
fn tile_coord_to_geo(
x: i32,
y: i32,
extent: u32,
bounds: &TileGeoBounds,
) -> rustial_math::GeoCoord {
let extent_f = extent as f64;
let lon = bounds.west + (x as f64 / extent_f) * (bounds.east - bounds.west);
let lat = bounds.north + (y as f64 / extent_f) * (bounds.south - bounds.north);
rustial_math::GeoCoord::from_lat_lon(lat, lon)
}
fn decode_layer(
bytes: &[u8],
bounds: &TileGeoBounds,
options: &MvtDecodeOptions,
) -> Result<Option<(String, FeatureCollection)>, MvtError> {
let mut reader = PbReader::new(bytes);
let mut name = String::new();
let mut keys: Vec<String> = Vec::new();
let mut values: Vec<PropertyValue> = Vec::new();
let mut features_bytes: Vec<&[u8]> = Vec::new();
let mut extent: u32 = 4096;
while reader.remaining() > 0 {
let (field_number, wire_type) = reader.read_tag()?;
match (field_number, wire_type) {
(1, WIRE_LEN) => {
name = reader.read_string()?;
}
(2, WIRE_LEN) => {
features_bytes.push(reader.read_bytes()?);
}
(3, WIRE_LEN) => {
keys.push(reader.read_string()?);
}
(4, WIRE_LEN) => {
let val_bytes = reader.read_bytes()?;
values.push(decode_value(val_bytes)?);
}
(5, WIRE_VARINT) => {
extent = reader.read_varint()? as u32;
if extent == 0 {
extent = 4096;
}
}
(15, WIRE_VARINT) => {
let _ = reader.read_varint()?;
}
_ => {
reader.skip_field(wire_type)?;
}
}
}
if !options.layer_filter.is_empty() && !options.layer_filter.iter().any(|f| f == &name) {
return Ok(None);
}
let mut features = Vec::with_capacity(features_bytes.len());
for feat_bytes in features_bytes {
match decode_feature(feat_bytes, &keys, &values, extent, bounds) {
Ok(feature) => features.push(feature),
Err(e) => {
log::debug!("skipping malformed MVT feature in layer '{name}': {e}");
}
}
}
Ok(Some((name, FeatureCollection { features })))
}
fn decode_value(bytes: &[u8]) -> Result<PropertyValue, MvtError> {
let mut reader = PbReader::new(bytes);
let mut result = PropertyValue::Null;
while reader.remaining() > 0 {
let (field_number, wire_type) = reader.read_tag()?;
match (field_number, wire_type) {
(1, WIRE_LEN) => {
result = PropertyValue::String(reader.read_string()?);
}
(2, WIRE_32) => {
result = PropertyValue::Number(reader.read_fixed32_f32()? as f64);
}
(3, WIRE_64) => {
result = PropertyValue::Number(reader.read_fixed64_f64()?);
}
(4, WIRE_VARINT) => {
result = PropertyValue::Number(reader.read_varint()? as f64);
}
(5, WIRE_VARINT) => {
result = PropertyValue::Number(reader.read_varint()? as f64);
}
(6, WIRE_VARINT) => {
let raw = reader.read_varint()?;
let decoded = zigzag_decode(raw);
result = PropertyValue::Number(decoded as f64);
}
(7, WIRE_VARINT) => {
result = PropertyValue::Bool(reader.read_varint()? != 0);
}
_ => {
reader.skip_field(wire_type)?;
}
}
}
Ok(result)
}
const GEOM_UNKNOWN: u32 = 0;
const GEOM_POINT: u32 = 1;
const GEOM_LINESTRING: u32 = 2;
const GEOM_POLYGON: u32 = 3;
fn decode_feature(
bytes: &[u8],
keys: &[String],
values: &[PropertyValue],
extent: u32,
bounds: &TileGeoBounds,
) -> Result<Feature, MvtError> {
let mut reader = PbReader::new(bytes);
let mut geom_type: u32 = GEOM_UNKNOWN;
let mut geometry_bytes: &[u8] = &[];
let mut tags_bytes: &[u8] = &[];
let mut feature_id: Option<u64> = None;
while reader.remaining() > 0 {
let (field_number, wire_type) = reader.read_tag()?;
match (field_number, wire_type) {
(1, WIRE_VARINT) => {
feature_id = Some(reader.read_varint()?);
}
(2, WIRE_LEN) => {
tags_bytes = reader.read_bytes()?;
}
(3, WIRE_VARINT) => {
geom_type = reader.read_varint()? as u32;
}
(4, WIRE_LEN) => {
geometry_bytes = reader.read_bytes()?;
}
_ => {
reader.skip_field(wire_type)?;
}
}
}
let geometry = decode_geometry(geom_type, geometry_bytes, extent, bounds)?;
let properties = decode_tags(tags_bytes, keys, values)?;
let mut props = properties;
if let Some(id) = feature_id {
props.insert("$id".to_owned(), PropertyValue::Number(id as f64));
}
Ok(Feature {
geometry,
properties: props,
})
}
fn decode_tags(
bytes: &[u8],
keys: &[String],
values: &[PropertyValue],
) -> Result<HashMap<String, PropertyValue>, MvtError> {
let mut props = HashMap::new();
if bytes.is_empty() {
return Ok(props);
}
let mut reader = PbReader::new(bytes);
while reader.remaining() > 0 {
let key_idx = reader.read_varint()? as usize;
if reader.remaining() == 0 {
break;
}
let val_idx = reader.read_varint()? as usize;
if let (Some(key), Some(val)) = (keys.get(key_idx), values.get(val_idx)) {
props.insert(key.clone(), val.clone());
}
}
Ok(props)
}
const CMD_MOVE_TO: u32 = 1;
const CMD_LINE_TO: u32 = 2;
const CMD_CLOSE_PATH: u32 = 7;
fn decode_geometry(
geom_type: u32,
bytes: &[u8],
extent: u32,
bounds: &TileGeoBounds,
) -> Result<Geometry, MvtError> {
let commands = decode_commands(bytes)?;
let rings = commands_to_rings(&commands, extent, bounds);
match geom_type {
GEOM_POINT => geometry_from_points(rings),
GEOM_LINESTRING => geometry_from_linestrings(rings),
GEOM_POLYGON => geometry_from_polygons(rings),
_ => Err(MvtError::DecodeError(format!(
"unknown geometry type: {geom_type}"
))),
}
}
struct GeomCommand {
id: u32,
params: Vec<(i32, i32)>,
}
fn decode_commands(bytes: &[u8]) -> Result<Vec<GeomCommand>, MvtError> {
let mut reader = PbReader::new(bytes);
let mut commands = Vec::new();
let mut cursor_x: i32 = 0;
let mut cursor_y: i32 = 0;
while reader.remaining() > 0 {
let cmd_int = reader.read_varint()? as u32;
let cmd_id = cmd_int & 0x7;
let cmd_count = cmd_int >> 3;
if cmd_id == CMD_CLOSE_PATH {
commands.push(GeomCommand {
id: CMD_CLOSE_PATH,
params: Vec::new(),
});
continue;
}
if cmd_id != CMD_MOVE_TO && cmd_id != CMD_LINE_TO {
return Err(MvtError::InvalidGeometryCommand(cmd_int));
}
let mut params = Vec::with_capacity(cmd_count as usize);
for _ in 0..cmd_count {
if reader.remaining() < 2 {
break;
}
let dx = zigzag_decode(reader.read_varint()?) as i32;
let dy = zigzag_decode(reader.read_varint()?) as i32;
cursor_x += dx;
cursor_y += dy;
params.push((cursor_x, cursor_y));
}
commands.push(GeomCommand { id: cmd_id, params });
}
Ok(commands)
}
fn commands_to_rings(
commands: &[GeomCommand],
extent: u32,
bounds: &TileGeoBounds,
) -> Vec<Vec<rustial_math::GeoCoord>> {
let mut rings: Vec<Vec<rustial_math::GeoCoord>> = Vec::new();
let mut current_ring: Vec<rustial_math::GeoCoord> = Vec::new();
for cmd in commands {
match cmd.id {
CMD_MOVE_TO => {
if !current_ring.is_empty() {
rings.push(std::mem::take(&mut current_ring));
}
for &(x, y) in &cmd.params {
current_ring.push(tile_coord_to_geo(x, y, extent, bounds));
}
}
CMD_LINE_TO => {
for &(x, y) in &cmd.params {
current_ring.push(tile_coord_to_geo(x, y, extent, bounds));
}
}
CMD_CLOSE_PATH => {
if let Some(&first) = current_ring.first() {
current_ring.push(first);
}
rings.push(std::mem::take(&mut current_ring));
}
_ => {}
}
}
if !current_ring.is_empty() {
rings.push(current_ring);
}
rings
}
fn geometry_from_points(rings: Vec<Vec<rustial_math::GeoCoord>>) -> Result<Geometry, MvtError> {
let points: Vec<Point> = rings
.into_iter()
.flat_map(|ring| ring.into_iter().map(|coord| Point { coord }))
.collect();
match points.len() {
0 => Err(MvtError::DecodeError("empty point geometry".into())),
1 => Ok(Geometry::Point(points.into_iter().next().expect("len==1"))),
_ => Ok(Geometry::MultiPoint(MultiPoint { points })),
}
}
fn geometry_from_linestrings(
rings: Vec<Vec<rustial_math::GeoCoord>>,
) -> Result<Geometry, MvtError> {
let lines: Vec<LineString> = rings
.into_iter()
.filter(|r| r.len() >= 2)
.map(|coords| LineString { coords })
.collect();
match lines.len() {
0 => Err(MvtError::DecodeError("empty linestring geometry".into())),
1 => Ok(Geometry::LineString(
lines.into_iter().next().expect("len==1"),
)),
_ => Ok(Geometry::MultiLineString(MultiLineString { lines })),
}
}
fn geometry_from_polygons(rings: Vec<Vec<rustial_math::GeoCoord>>) -> Result<Geometry, MvtError> {
if rings.is_empty() {
return Err(MvtError::DecodeError("empty polygon geometry".into()));
}
let mut polygons: Vec<Polygon> = Vec::new();
for ring in rings {
if ring.len() < 4 {
continue;
}
let area = signed_ring_area(&ring);
if area > 0.0 {
polygons.push(Polygon {
exterior: ring,
interiors: Vec::new(),
});
} else if area < 0.0 {
if let Some(last) = polygons.last_mut() {
last.interiors.push(ring);
} else {
let mut reversed = ring;
reversed.reverse();
polygons.push(Polygon {
exterior: reversed,
interiors: Vec::new(),
});
}
}
}
match polygons.len() {
0 => Err(MvtError::DecodeError("no valid polygon rings found".into())),
1 => Ok(Geometry::Polygon(
polygons.into_iter().next().expect("len==1"),
)),
_ => Ok(Geometry::MultiPolygon(MultiPolygon { polygons })),
}
}
fn signed_ring_area(ring: &[rustial_math::GeoCoord]) -> f64 {
let mut area = 0.0f64;
let n = ring.len();
if n < 3 {
return 0.0;
}
for i in 0..n {
let j = (i + 1) % n;
area += ring[i].lon * ring[j].lat;
area -= ring[j].lon * ring[i].lat;
}
area / 2.0
}
const WIRE_VARINT: u8 = 0;
const WIRE_64: u8 = 1;
const WIRE_LEN: u8 = 2;
const WIRE_32: u8 = 5;
struct PbReader<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> PbReader<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 }
}
#[inline]
fn remaining(&self) -> usize {
self.data.len().saturating_sub(self.pos)
}
fn read_byte(&mut self) -> Result<u8, MvtError> {
if self.pos >= self.data.len() {
return Err(MvtError::TruncatedPayload);
}
let b = self.data[self.pos];
self.pos += 1;
Ok(b)
}
fn read_varint(&mut self) -> Result<u64, MvtError> {
let mut result: u64 = 0;
let mut shift: u32 = 0;
loop {
let b = self.read_byte()?;
result |= ((b & 0x7F) as u64) << shift;
if b & 0x80 == 0 {
return Ok(result);
}
shift += 7;
if shift >= 64 {
return Err(MvtError::DecodeError("varint too long".into()));
}
}
}
fn read_tag(&mut self) -> Result<(u32, u8), MvtError> {
let varint = self.read_varint()? as u32;
let field_number = varint >> 3;
let wire_type = (varint & 0x7) as u8;
Ok((field_number, wire_type))
}
fn read_bytes(&mut self) -> Result<&'a [u8], MvtError> {
let len = self.read_varint()? as usize;
if self.pos + len > self.data.len() {
return Err(MvtError::TruncatedPayload);
}
let slice = &self.data[self.pos..self.pos + len];
self.pos += len;
Ok(slice)
}
fn read_string(&mut self) -> Result<String, MvtError> {
let bytes = self.read_bytes()?;
String::from_utf8(bytes.to_vec())
.map_err(|e| MvtError::DecodeError(format!("invalid UTF-8: {e}")))
}
fn read_fixed32_f32(&mut self) -> Result<f32, MvtError> {
if self.remaining() < 4 {
return Err(MvtError::TruncatedPayload);
}
let bytes = [
self.data[self.pos],
self.data[self.pos + 1],
self.data[self.pos + 2],
self.data[self.pos + 3],
];
self.pos += 4;
Ok(f32::from_le_bytes(bytes))
}
fn read_fixed64_f64(&mut self) -> Result<f64, MvtError> {
if self.remaining() < 8 {
return Err(MvtError::TruncatedPayload);
}
let mut bytes = [0u8; 8];
bytes.copy_from_slice(&self.data[self.pos..self.pos + 8]);
self.pos += 8;
Ok(f64::from_le_bytes(bytes))
}
fn skip_field(&mut self, wire_type: u8) -> Result<(), MvtError> {
match wire_type {
WIRE_VARINT => {
let _ = self.read_varint()?;
}
WIRE_64 => {
if self.remaining() < 8 {
return Err(MvtError::TruncatedPayload);
}
self.pos += 8;
}
WIRE_LEN => {
let _ = self.read_bytes()?;
}
WIRE_32 => {
if self.remaining() < 4 {
return Err(MvtError::TruncatedPayload);
}
self.pos += 4;
}
other => return Err(MvtError::UnsupportedWireType(other)),
}
Ok(())
}
}
#[inline]
fn zigzag_decode(n: u64) -> i64 {
((n >> 1) as i64) ^ -((n & 1) as i64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zigzag_decode_positive() {
assert_eq!(zigzag_decode(0), 0);
assert_eq!(zigzag_decode(2), 1);
assert_eq!(zigzag_decode(4), 2);
assert_eq!(zigzag_decode(100), 50);
}
#[test]
fn zigzag_decode_negative() {
assert_eq!(zigzag_decode(1), -1);
assert_eq!(zigzag_decode(3), -2);
assert_eq!(zigzag_decode(5), -3);
assert_eq!(zigzag_decode(99), -50);
}
#[test]
fn pb_reader_read_varint_single_byte() {
let data = [0x08]; let mut reader = PbReader::new(&data);
assert_eq!(reader.read_varint().unwrap(), 8);
}
#[test]
fn pb_reader_read_varint_multi_byte() {
let data = [0xAC, 0x02]; let mut reader = PbReader::new(&data);
assert_eq!(reader.read_varint().unwrap(), 300);
}
#[test]
fn pb_reader_read_tag() {
let data = [26];
let mut reader = PbReader::new(&data);
let (field, wire) = reader.read_tag().unwrap();
assert_eq!(field, 3);
assert_eq!(wire, WIRE_LEN);
}
#[test]
fn pb_reader_read_string() {
let data = [5, b'h', b'e', b'l', b'l', b'o'];
let mut reader = PbReader::new(&data);
assert_eq!(reader.read_string().unwrap(), "hello");
}
#[test]
fn pb_reader_truncated_payload() {
let data = [0xFF]; let mut reader = PbReader::new(&data);
assert!(reader.read_varint().is_err());
}
#[test]
fn tile_geo_bounds_zoom_0() {
let bounds = tile_geo_bounds(&TileId::new(0, 0, 0));
assert!((bounds.west - (-180.0)).abs() < 1e-6);
assert!((bounds.east - 180.0).abs() < 1e-6);
assert!(bounds.north > 85.0);
assert!(bounds.south < -85.0);
}
#[test]
fn tile_geo_bounds_higher_zoom() {
let bounds = tile_geo_bounds(&TileId::new(1, 0, 0));
assert!((bounds.west - (-180.0)).abs() < 1e-6);
assert!((bounds.east - 0.0).abs() < 1e-6);
assert!(bounds.north > 85.0);
assert!(bounds.south > -1.0); }
#[test]
fn signed_ring_area_ccw_positive() {
use rustial_math::GeoCoord;
let ring = vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(0.0, 1.0),
GeoCoord::from_lat_lon(1.0, 1.0),
GeoCoord::from_lat_lon(1.0, 0.0),
GeoCoord::from_lat_lon(0.0, 0.0),
];
let area = signed_ring_area(&ring);
assert!(area > 0.0, "CCW ring should have positive area, got {area}");
}
#[test]
fn signed_ring_area_cw_negative() {
use rustial_math::GeoCoord;
let ring = vec![
GeoCoord::from_lat_lon(0.0, 0.0),
GeoCoord::from_lat_lon(1.0, 0.0),
GeoCoord::from_lat_lon(1.0, 1.0),
GeoCoord::from_lat_lon(0.0, 1.0),
GeoCoord::from_lat_lon(0.0, 0.0),
];
let area = signed_ring_area(&ring);
assert!(area < 0.0, "CW ring should have negative area, got {area}");
}
#[test]
fn tile_coord_to_geo_corners() {
let bounds = tile_geo_bounds(&TileId::new(0, 0, 0));
let nw = tile_coord_to_geo(0, 0, 4096, &bounds);
assert!((nw.lon - (-180.0)).abs() < 0.01);
assert!(nw.lat > 85.0);
let se = tile_coord_to_geo(4096, 4096, 4096, &bounds);
assert!((se.lon - 180.0).abs() < 0.01);
assert!(se.lat < -85.0);
}
fn encode_varint(mut val: u64) -> Vec<u8> {
let mut buf = Vec::new();
loop {
let mut byte = (val & 0x7F) as u8;
val >>= 7;
if val != 0 {
byte |= 0x80;
}
buf.push(byte);
if val == 0 {
break;
}
}
buf
}
fn encode_tag(field_number: u32, wire_type: u8) -> Vec<u8> {
encode_varint(((field_number as u64) << 3) | wire_type as u64)
}
fn encode_len_delimited(field_number: u32, data: &[u8]) -> Vec<u8> {
let mut buf = encode_tag(field_number, WIRE_LEN);
buf.extend(encode_varint(data.len() as u64));
buf.extend_from_slice(data);
buf
}
fn encode_string_field(field_number: u32, s: &str) -> Vec<u8> {
encode_len_delimited(field_number, s.as_bytes())
}
fn encode_varint_field(field_number: u32, val: u64) -> Vec<u8> {
let mut buf = encode_tag(field_number, WIRE_VARINT);
buf.extend(encode_varint(val));
buf
}
fn zigzag_encode(n: i32) -> u32 {
((n << 1) ^ (n >> 31)) as u32
}
fn encode_geometry_commands(commands: &[(u32, &[(i32, i32)])]) -> Vec<u8> {
let mut buf = Vec::new();
for &(cmd_id, params) in commands {
let count = if cmd_id == CMD_CLOSE_PATH {
1u32
} else {
params.len() as u32
};
buf.extend(encode_varint(((count as u64) << 3) | cmd_id as u64));
for &(dx, dy) in params {
buf.extend(encode_varint(zigzag_encode(dx) as u64));
buf.extend(encode_varint(zigzag_encode(dy) as u64));
}
}
buf
}
fn build_mvt_value_string(s: &str) -> Vec<u8> {
encode_string_field(1, s)
}
fn build_mvt_value_double(v: f64) -> Vec<u8> {
let mut buf = encode_tag(3, WIRE_64);
buf.extend(v.to_le_bytes());
buf
}
fn build_mvt_feature(
id: Option<u64>,
geom_type: u32,
tags: &[u32],
geometry: &[u8],
) -> Vec<u8> {
let mut buf = Vec::new();
if let Some(id) = id {
buf.extend(encode_varint_field(1, id));
}
if !tags.is_empty() {
let mut tags_buf = Vec::new();
for &t in tags {
tags_buf.extend(encode_varint(t as u64));
}
buf.extend(encode_len_delimited(2, &tags_buf));
}
buf.extend(encode_varint_field(3, geom_type as u64));
buf.extend(encode_len_delimited(4, geometry));
buf
}
fn build_mvt_layer(
name: &str,
features: &[Vec<u8>],
keys: &[&str],
values: &[Vec<u8>],
extent: u32,
) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend(encode_string_field(1, name));
for feat in features {
buf.extend(encode_len_delimited(2, feat));
}
for key in keys {
buf.extend(encode_string_field(3, key));
}
for val in values {
buf.extend(encode_len_delimited(4, val));
}
buf.extend(encode_varint_field(5, extent as u64));
buf.extend(encode_varint_field(15, 2)); buf
}
fn build_mvt_tile(layers: &[Vec<u8>]) -> Vec<u8> {
let mut buf = Vec::new();
for layer in layers {
buf.extend(encode_len_delimited(3, layer));
}
buf
}
#[test]
fn decode_empty_tile() {
let bytes = build_mvt_tile(&[]);
let result = decode_mvt(&bytes, &TileId::new(0, 0, 0), &MvtDecodeOptions::default());
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
#[test]
fn decode_point_feature() {
let geom = encode_geometry_commands(&[(CMD_MOVE_TO, &[(2048, 2048)])]);
let feature = build_mvt_feature(Some(42), GEOM_POINT, &[], &geom);
let layer = build_mvt_layer("points", &[feature], &[], &[], 4096);
let tile = build_mvt_tile(&[layer]);
let tile_id = TileId::new(0, 0, 0);
let result = decode_mvt(&tile, &tile_id, &MvtDecodeOptions::default()).unwrap();
assert_eq!(result.len(), 1);
let features = &result["points"];
assert_eq!(features.len(), 1);
match &features.features[0].geometry {
Geometry::Point(pt) => {
assert!(pt.coord.lon.abs() < 1.0);
assert!(pt.coord.lat.abs() < 1.0);
}
other => panic!("expected Point, got {}", other.type_name()),
}
let id_prop = features.features[0].property("$id");
assert_eq!(id_prop.and_then(|v| v.as_f64()), Some(42.0));
}
#[test]
fn decode_linestring_feature() {
let geom = encode_geometry_commands(&[
(CMD_MOVE_TO, &[(0, 0)]),
(CMD_LINE_TO, &[(4096, 0), (0, 4096)]),
]);
let feature = build_mvt_feature(None, GEOM_LINESTRING, &[], &geom);
let layer = build_mvt_layer("roads", &[feature], &[], &[], 4096);
let tile = build_mvt_tile(&[layer]);
let tile_id = TileId::new(0, 0, 0);
let result = decode_mvt(&tile, &tile_id, &MvtDecodeOptions::default()).unwrap();
let features = &result["roads"];
assert_eq!(features.len(), 1);
match &features.features[0].geometry {
Geometry::LineString(ls) => {
assert_eq!(ls.coords.len(), 3);
}
other => panic!("expected LineString, got {}", other.type_name()),
}
}
#[test]
fn decode_polygon_feature() {
let geom = encode_geometry_commands(&[
(CMD_MOVE_TO, &[(0, 0)]),
(CMD_LINE_TO, &[(4096, 0), (0, 4096), (-4096, 0)]),
(CMD_CLOSE_PATH, &[]),
]);
let feature = build_mvt_feature(None, GEOM_POLYGON, &[], &geom);
let layer = build_mvt_layer("water", &[feature], &[], &[], 4096);
let tile = build_mvt_tile(&[layer]);
let tile_id = TileId::new(0, 0, 0);
let result = decode_mvt(&tile, &tile_id, &MvtDecodeOptions::default()).unwrap();
let features = &result["water"];
assert_eq!(features.len(), 1);
match &features.features[0].geometry {
Geometry::Polygon(poly) => {
assert!(poly.exterior.len() >= 4, "polygon should have 4+ vertices");
assert!(poly.interiors.is_empty());
}
other => panic!("expected Polygon, got {}", other.type_name()),
}
}
#[test]
fn decode_feature_with_properties() {
let geom = encode_geometry_commands(&[(CMD_MOVE_TO, &[(100, 100)])]);
let keys = &["name", "population"];
let values = &[
build_mvt_value_string("Springfield"),
build_mvt_value_double(12345.0),
];
let feature = build_mvt_feature(None, GEOM_POINT, &[0, 0, 1, 1], &geom);
let layer = build_mvt_layer("places", &[feature], keys, values, 4096);
let tile = build_mvt_tile(&[layer]);
let tile_id = TileId::new(1, 0, 0);
let result = decode_mvt(&tile, &tile_id, &MvtDecodeOptions::default()).unwrap();
let features = &result["places"];
assert_eq!(features.len(), 1);
let props = &features.features[0].properties;
assert_eq!(
props.get("name").and_then(|v| v.as_str()),
Some("Springfield")
);
assert_eq!(
props.get("population").and_then(|v| v.as_f64()),
Some(12345.0)
);
}
#[test]
fn decode_with_layer_filter() {
let geom = encode_geometry_commands(&[(CMD_MOVE_TO, &[(100, 100)])]);
let feat = build_mvt_feature(None, GEOM_POINT, &[], &geom);
let layer_a = build_mvt_layer("water", std::slice::from_ref(&feat), &[], &[], 4096);
let layer_b = build_mvt_layer("roads", &[feat], &[], &[], 4096);
let tile = build_mvt_tile(&[layer_a, layer_b]);
let options = MvtDecodeOptions {
layer_filter: vec!["water".into()],
};
let tile_id = TileId::new(0, 0, 0);
let result = decode_mvt(&tile, &tile_id, &options).unwrap();
assert_eq!(result.len(), 1);
assert!(result.contains_key("water"));
assert!(!result.contains_key("roads"));
}
#[test]
fn decode_multi_point_feature() {
let geom = encode_geometry_commands(&[(CMD_MOVE_TO, &[(100, 100), (200, 200)])]);
let feature = build_mvt_feature(None, GEOM_POINT, &[], &geom);
let layer = build_mvt_layer("multi", &[feature], &[], &[], 4096);
let tile = build_mvt_tile(&[layer]);
let tile_id = TileId::new(0, 0, 0);
let result = decode_mvt(&tile, &tile_id, &MvtDecodeOptions::default()).unwrap();
match &result["multi"].features[0].geometry {
Geometry::MultiPoint(mp) => {
assert_eq!(mp.points.len(), 2);
}
other => panic!("expected MultiPoint, got {}", other.type_name()),
}
}
#[test]
fn mvt_error_display() {
assert!(MvtError::TruncatedPayload.to_string().contains("truncated"));
assert!(MvtError::UnsupportedWireType(6).to_string().contains("6"));
assert!(MvtError::InvalidGeometryCommand(99)
.to_string()
.contains("99"));
}
}