use serde::{Deserialize, Serialize};
use crate::tile_coords::{geodetic_tms, web_mercator, web_mercator_tms};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TilingScheme {
Tms,
Xyz,
}
impl TilingScheme {
pub const fn as_str(self) -> &'static str {
match self {
Self::Tms => "tms",
Self::Xyz => "xyz",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TerrainFormat {
Heightmap1,
QuantizedMesh1,
}
impl TerrainFormat {
pub const fn as_str(self) -> &'static str {
match self {
Self::Heightmap1 => "heightmap-1.0",
Self::QuantizedMesh1 => "quantized-mesh-1.0",
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct TileAvailability {
pub start_x: u32,
pub start_y: u32,
pub end_x: u32,
pub end_y: u32,
}
impl TileAvailability {
pub const fn new(start_x: u32, start_y: u32, end_x: u32, end_y: u32) -> Self {
Self {
start_x,
start_y,
end_x,
end_y,
}
}
pub fn full_level_geodetic_tms(zoom: u8) -> Self {
Self::new(
0,
0,
geodetic_tms::tile_count_x(zoom) - 1,
geodetic_tms::tile_count_y(zoom) - 1,
)
}
pub fn full_level_xyz(zoom: u8) -> Self {
let n = web_mercator::tile_count(zoom);
Self::new(0, 0, n - 1, n - 1)
}
pub fn from_bounds_geodetic_tms(
zoom: u8,
west: f64,
south: f64,
east: f64,
north: f64,
) -> Self {
let (start_x, start_y) = geodetic_tms::lonlat_to_tile(west, south, zoom);
let (end_x, end_y) = geodetic_tms::lonlat_to_tile(east, north, zoom);
Self::new(start_x, start_y, end_x, end_y)
}
pub fn from_bounds_xyz(zoom: u8, west: f64, south: f64, east: f64, north: f64) -> Self {
let (start_x, end_y) = web_mercator::lonlat_to_tile(west, south, zoom);
let (end_x, start_y) = web_mercator::lonlat_to_tile(east, north, zoom);
Self::new(start_x, start_y, end_x, end_y)
}
pub fn from_bounds_web_mercator_tms(
zoom: u8,
west: f64,
south: f64,
east: f64,
north: f64,
) -> Self {
let (start_x, start_y) = web_mercator_tms::lonlat_to_tile(west, south, zoom);
let (end_x, end_y) = web_mercator_tms::lonlat_to_tile(east, north, zoom);
Self::new(start_x, start_y, end_x, end_y)
}
}
#[derive(Debug, Clone)]
pub struct LayerJsonConfig {
pub tiles_template: String,
pub version: String,
pub attribution: Option<String>,
pub available: Vec<Vec<TileAvailability>>,
pub min_zoom: Option<u8>,
pub max_zoom: Option<u8>,
pub scheme: TilingScheme,
pub bounds: Option<[f64; 4]>,
pub extensions: Vec<String>,
pub format: TerrainFormat,
pub metadata_availability: Option<u8>,
}
impl Default for LayerJsonConfig {
fn default() -> Self {
Self {
tiles_template: "{z}/{x}/{y}.terrain".to_string(),
version: "1.0.0".to_string(),
attribution: None,
available: Vec::new(),
min_zoom: None,
max_zoom: None,
scheme: TilingScheme::Tms,
bounds: None,
extensions: Vec::new(),
format: TerrainFormat::QuantizedMesh1,
metadata_availability: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct LayerJson {
pub tilejson: String,
pub format: String,
pub version: String,
pub scheme: String,
pub tiles: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub available: Vec<Vec<TileAvailability>>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub attribution: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "minzoom", default)]
pub min_zoom: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none", rename = "maxzoom", default)]
pub max_zoom: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub bounds: Option<[f64; 4]>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub extensions: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub metadata_availability: Option<u8>,
}
impl LayerJson {
pub fn from_config(config: &LayerJsonConfig) -> Self {
Self {
tilejson: "2.1.0".to_string(),
format: config.format.as_str().to_string(),
version: config.version.clone(),
scheme: config.scheme.as_str().to_string(),
tiles: vec![config.tiles_template.clone()],
available: config.available.clone(),
attribution: config.attribution.clone(),
min_zoom: config.min_zoom,
max_zoom: config.max_zoom,
bounds: config.bounds,
extensions: config.extensions.clone(),
metadata_availability: config.metadata_availability,
}
}
pub fn to_json_pretty(&self) -> serde_json::Result<String> {
serde_json::to_string_pretty(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn full_level_geodetic_tms_z0_covers_two_x_one_y() {
let r = TileAvailability::full_level_geodetic_tms(0);
assert_eq!(r, TileAvailability::new(0, 0, 1, 0));
}
#[test]
fn from_bounds_geodetic_tms_is_ordered() {
let r = TileAvailability::from_bounds_geodetic_tms(4, 122.0, 20.0, 154.0, 46.0);
assert!(r.start_x <= r.end_x);
assert!(r.start_y <= r.end_y);
}
#[test]
fn layer_json_round_trips_through_serde() {
let cfg = LayerJsonConfig {
attribution: Some("Made with terrain-codec".into()),
available: vec![vec![TileAvailability::full_level_geodetic_tms(0)]],
min_zoom: Some(0),
max_zoom: Some(10),
bounds: Some([-180.0, -90.0, 180.0, 90.0]),
extensions: vec!["octvertexnormals".into(), "watermask".into()],
format: TerrainFormat::QuantizedMesh1,
metadata_availability: Some(10),
..Default::default()
};
let lj = LayerJson::from_config(&cfg);
let json = serde_json::to_string(&lj).unwrap();
let parsed: LayerJson = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, lj);
assert!(json.contains("quantized-mesh-1.0"));
assert!(json.contains("octvertexnormals"));
assert!(json.contains("metadataAvailability"));
}
#[test]
fn empty_optionals_are_omitted() {
let lj = LayerJson::from_config(&LayerJsonConfig::default());
let json = serde_json::to_string(&lj).unwrap();
assert!(!json.contains("available"));
assert!(!json.contains("bounds"));
assert!(!json.contains("attribution"));
assert!(!json.contains("extensions"));
assert!(!json.contains("minzoom"));
assert!(!json.contains("maxzoom"));
}
}