use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::bounds::Bounds;
use crate::center::Center;
use crate::vector_layer::VectorLayer;
#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub struct TileJSON {
pub tilejson: String,
pub tiles: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub vector_layers: Option<Vec<VectorLayer>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attribution: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bounds: Option<Bounds>,
#[serde(skip_serializing_if = "Option::is_none")]
pub center: Option<Center>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fillzoom: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub grids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub legend: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub maxzoom: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub minzoom: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scheme: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub template: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(flatten)]
pub other: BTreeMap<String, Value>,
}
impl TileJSON {
pub fn set_missing_defaults(&mut self) {
self.version.get_or_insert_with(|| "1.0.0".to_string());
self.scheme.get_or_insert_with(|| "xyz".to_string());
self.minzoom.get_or_insert(0);
self.maxzoom.get_or_insert(30);
self.bounds.get_or_insert_with(Bounds::default);
}
}
#[macro_export]
macro_rules! tilejson {
( tilejson: $ver:expr, tiles: $sources:expr $(, $tag:tt : $val:expr)* $(,)? ) => {
$crate::TileJSON {
$( $tag: Some($val), )*
..$crate::TileJSON {
tilejson: $ver,
tiles: $sources,
vector_layers: None,
attribution: None,
bounds: None,
center: None,
data: None,
description: None,
fillzoom: None,
grids: None,
legend: None,
maxzoom: None,
minzoom: None,
name: None,
scheme: None,
template: None,
version: None,
other: Default::default(),
}
}
};
( tiles: $sources:expr $(, $tag:tt : $val:expr)* $(,)? ) => {
$crate::tilejson! {
tilejson: "3.0.0".to_string(),
tiles: $sources,
$( $tag: $val , )* }
};
( $tile_source:expr $(, $tag:tt : $val:expr)* $(,)? ) => {
$crate::tilejson! {
tiles: vec! [ $tile_source ],
$( $tag: $val , )* }
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_reading() {
let tilejson_str = r#"{
"tilejson": "3.0.0",
"attribution": "",
"name": "compositing",
"scheme": "tms",
"tiles": [
"http://localhost:8888/foo/{z}/{x}/{y}.png"
]
}"#;
let mut tilejson: TileJSON = serde_json::from_str(tilejson_str).unwrap();
assert_eq!(
tilejson,
tilejson! {
tilejson: "3.0.0".to_string(),
tiles: vec!["http://localhost:8888/foo/{z}/{x}/{y}.png".to_string()],
attribution: String::new(),
name: "compositing".to_string(),
scheme: "tms".to_string(),
}
);
tilejson.set_missing_defaults();
assert_eq!(
tilejson,
tilejson! {
tilejson: "3.0.0".to_string(),
tiles: vec!["http://localhost:8888/foo/{z}/{x}/{y}.png".to_string()],
attribution: String::new(),
name: "compositing".to_string(),
scheme: "tms".to_string(),
bounds: Bounds::new(
-180.0,
-85.051_128_779_806_59,
180.0,
85.051_128_779_806_6,
),
maxzoom: 30,
minzoom: 0,
version: "1.0.0".to_string(),
}
);
}
#[test]
fn test_other() {
let tilejson_str = r#"{
"tilejson": "3.0.0",
"attribution": "",
"name": "compositing",
"scheme": "tms",
"tiles": [
"http://localhost:8888/foo/{z}/{x}/{y}.png"
],
"foo": "foo value",
"bar": "bar value"
}"#;
let tilejson: TileJSON = serde_json::from_str(tilejson_str).unwrap();
let mut expected = tilejson! {
tilejson: "3.0.0".to_string(),
tiles: vec!["http://localhost:8888/foo/{z}/{x}/{y}.png".to_string()],
attribution: String::new(),
name: "compositing".to_string(),
scheme: "tms".to_string(),
};
expected.other.insert("foo".to_string(), "foo value".into());
expected.other.insert("bar".to_string(), "bar value".into());
assert_eq!(tilejson, expected);
}
#[test]
fn test_writing() {
let source = "http://localhost:8888/foo/{z}/{x}/{y}.png";
let tj = tilejson! {
source.to_string(),
name: "compositing".to_string(),
scheme: "tms".to_string(),
bounds: Bounds::new(-1.0, -2.0, 3.0, 4.0),
center: Center::new(-5.0, -6.0, 3),
};
assert_eq!(
serde_json::to_string(&tj).unwrap(),
r#"{"tilejson":"3.0.0","tiles":["http://localhost:8888/foo/{z}/{x}/{y}.png"],"bounds":[-1.0,-2.0,3.0,4.0],"center":[-5.0,-6.0,3],"name":"compositing","scheme":"tms"}"#,
);
let vl = VectorLayer::new(
"a".to_string(),
BTreeMap::from([("b".to_string(), "c".to_string())]),
);
let tj = tilejson! {
source.to_string(),
vector_layers: vec![vl]
};
assert_eq!(
serde_json::to_string(&tj).unwrap(),
r#"{"tilejson":"3.0.0","tiles":["http://localhost:8888/foo/{z}/{x}/{y}.png"],"vector_layers":[{"id":"a","fields":{"b":"c"}}]}"#,
);
}
fn parse(json_str: &str) -> serde_json::Result<TileJSON> {
serde_json::from_str(json_str)
}
#[test]
fn test_bad_json() {
parse(r#"{"tilejson":"3.0.0", "tiles":["x"], "center":[]}"#).unwrap_err();
parse(r#"{"tilejson":"3.0.0", "tiles":["x"], "center":[1,2]}"#).unwrap_err();
parse(r#"{"tilejson":"3.0.0", "tiles":["x"], "center":[1,2,3,4]}"#).unwrap_err();
parse(r#"{"tilejson":"3.0.0", "tiles":["x"], "bounds":[]}"#).unwrap_err();
parse(r#"{"tilejson":"3.0.0", "tiles":["x"], "bounds":[1,2,3]}"#).unwrap_err();
parse(r#"{"tilejson":"3.0.0", "tiles":["x"], "bounds":[1,2,3,4,5]}"#).unwrap_err();
}
}