use super::mvt::{self, Kind, Property, Shape};
use crate::data::{Ids, Name, Patch, Table, Values};
use crate::tiles::TileKey;
#[derive(Clone, PartialEq, Debug)]
pub(crate) struct Template {
pub(crate) url: String,
pub(crate) min_zoom: u8,
pub(crate) max_zoom: u8,
pub(crate) bounds: [f64; 4],
pub(crate) tms: bool,
}
pub(crate) const WORLD: [f64; 4] = [-180.0, -85.051_128_78, 180.0, 85.051_128_78];
impl Template {
pub(crate) fn of(url: &str) -> Template {
Template { url: url.to_owned(), min_zoom: 0, max_zoom: 14, bounds: WORLD, tms: false }
}
pub(crate) fn tilejson(bytes: &[u8], base: &str) -> Result<Template, String> {
let json: serde_json::Value =
serde_json::from_slice(bytes).map_err(|e| format!("{base} is not TileJSON: {e}"))?;
let template = json["tiles"][0].as_str().ok_or_else(|| format!("the TileJSON at {base} names no tiles"))?;
if let Some(format) = json["format"].as_str()
&& ["png", "jpg", "jpeg", "webp", "avif"].contains(&format)
{
return Err(format!("the TileJSON at {base} names image tiles; only vector tiles draw"));
}
let zoom = |key: &str, or: u8| json[key].as_u64().map_or(or, |z| z.min(24) as u8);
let bounds = json["bounds"]
.as_array()
.filter(|b| b.len() == 4)
.and_then(|b| Some([b[0].as_f64()?, b[1].as_f64()?, b[2].as_f64()?, b[3].as_f64()?]))
.unwrap_or(WORLD);
Ok(Template {
url: crate::url::resolve(base, template),
min_zoom: zoom("minzoom", 0),
max_zoom: zoom("maxzoom", 14),
bounds,
tms: json["scheme"].as_str() == Some("tms"),
})
}
pub(crate) fn url_of(&self, key: TileKey) -> String {
let y = if self.tms { (1_u32 << key.z) - 1 - key.y } else { key.y };
self.url.replace("{z}", &key.z.to_string()).replace("{x}", &key.x.to_string()).replace("{y}", &y.to_string())
}
}
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub(crate) struct Decoded {
pub(crate) layer: String,
pub(crate) kind: Kind,
pub(crate) table: Name,
}
pub(crate) const FEATURES: u64 = 1 << 20;
pub(crate) fn decode(bytes: &[u8], key: TileKey, tables: &[Decoded]) -> Option<Patch> {
let bytes = super::gunzipped(bytes).ok()?;
let layers = mvt::decode(&bytes, key)?;
let first = super::pmtiles::tile_id(key) * FEATURES;
let mut patch = Patch::new();
for decoded in tables {
let Some(layer) = layers.iter().find(|l| l.name == decoded.layer) else { continue };
let mut shapes: Vec<Shape> = Vec::new();
let mut properties: Vec<&[(u32, Property)]> = Vec::new();
for feature in layer.features.iter().filter(|f| f.shape.kind() == decoded.kind) {
match &feature.shape {
Shape::Points(points) => {
for &p in points {
shapes.push(Shape::Points(vec![p]));
properties.push(&feature.properties);
}
}
shape => {
shapes.push(shape.clone());
properties.push(&feature.properties);
}
}
}
let count = shapes.len().min(FEATURES as usize);
if count == 0 {
continue;
}
let ids: Vec<u64> = (0..count as u64).map(|k| first + k).collect();
let geometry: Values = match decoded.kind {
Kind::Points => shapes[..count]
.iter()
.map(|s| if let Shape::Points(p) = s { p[0] } else { [f64::NAN; 2] })
.collect::<Vec<_>>()
.into(),
Kind::Lines => shapes
.into_iter()
.take(count)
.map(|s| if let Shape::Lines(parts) = s { parts } else { Vec::new() })
.collect::<Vec<_>>()
.into(),
Kind::Polygons => shapes
.into_iter()
.take(count)
.map(|s| if let Shape::Polygons(polygons) = s { polygons } else { Vec::new() })
.collect::<Vec<_>>()
.into(),
};
let mut table = Table::new(decoded.table.clone(), Ids::from(ids)).field("geometry", geometry);
for (k, key) in layer.keys.iter().enumerate() {
if key == "geometry" {
continue;
}
let values: Vec<Option<&Property>> = properties[..count]
.iter()
.map(|props| props.iter().find(|(i, _)| *i as usize == k).map(|(_, v)| v))
.collect();
if values.iter().any(Option::is_some) {
table = table.field(key.as_str(), field(&values));
}
}
patch = patch.set(table);
}
Some(patch)
}
fn field(values: &[Option<&Property>]) -> Values {
let present = || values.iter().flatten();
if present().all(|v| matches!(v, Property::Number(_))) {
return values
.iter()
.map(|v| v.and_then(|v| if let Property::Number(n) = v { Some(*n) } else { None }))
.collect::<Vec<_>>()
.into();
}
if present().all(|v| matches!(v, Property::Bool(_))) {
return values
.iter()
.map(|v| v.and_then(|v| if let Property::Bool(b) = v { Some(*b) } else { None }))
.collect::<Vec<_>>()
.into();
}
values.iter().map(|v| v.map(text)).collect::<Vec<Option<String>>>().into()
}
pub(crate) fn text(property: &Property) -> String {
match property {
Property::Text(t) => t.clone(),
Property::Number(n) => n.to_string(),
Property::Bool(b) => b.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geo::mvt::tests::{square, tile};
use crate::{Data, Value};
#[test]
fn a_tile_decodes_into_its_tables_rows() {
let key = TileKey { z: 3, x: 2, y: 3 };
let bytes = tile("water", 3, &square(100, 100, 1000), "bay");
let tables = [Decoded { layer: "water".into(), kind: Kind::Polygons, table: "water".into() }];
let patch = decode(&bytes, key, &tables).expect("a tile");
let mut data = Data::new("base");
data.apply(patch).expect("valid");
let water = data.table("water").expect("the table");
assert_eq!(water.len(), 1);
let id = crate::geo::pmtiles::tile_id(key) * FEATURES;
assert_eq!(water.get(id, "name"), Some(Value::Text("bay".into())), "stable ids");
assert!(matches!(water.get(id, "geometry"), Some(Value::Polygons(_))));
let other = [Decoded { layer: "water".into(), kind: Kind::Lines, table: "water".into() }];
assert!(decode(&bytes, key, &other).expect("a tile").is_empty(), "no lines in it");
}
#[test]
fn templates_fill_and_resolve() {
let json = br#"{"tiles": ["/planet/{z}/{x}/{y}.pbf"], "minzoom": 0, "maxzoom": 14, "scheme": "xyz"}"#;
let template = Template::tilejson(json, "https://tiles.example/planet").expect("TileJSON");
assert_eq!(template.url_of(TileKey { z: 2, x: 1, y: 3 }), "https://tiles.example/planet/2/1/3.pbf");
let tms = Template { tms: true, ..Template::of("{z}/{x}/{y}") };
assert_eq!(tms.url_of(TileKey { z: 1, x: 0, y: 0 }), "1/0/1");
}
#[test]
fn tile_bounds_meet_their_parents() {
let key = TileKey { z: 1, x: 1, y: 0 };
let [w, s, e, n] = key.bounds();
assert_eq!([w, s, e], [0.0, 0.0, 180.0]);
assert!((n - 85.051_128_78).abs() < 1e-6);
assert_eq!(key.children()[3].ancestor(1), key);
}
}