1use serde::{Deserialize, Serialize};
14
15use crate::tile_coords::{geodetic_tms, web_mercator, web_mercator_tms};
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum TilingScheme {
20 Tms,
22 Xyz,
24}
25
26impl TilingScheme {
27 pub const fn as_str(self) -> &'static str {
29 match self {
30 Self::Tms => "tms",
31 Self::Xyz => "xyz",
32 }
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum TerrainFormat {
39 Heightmap1,
41 QuantizedMesh1,
43}
44
45impl TerrainFormat {
46 pub const fn as_str(self) -> &'static str {
48 match self {
49 Self::Heightmap1 => "heightmap-1.0",
50 Self::QuantizedMesh1 => "quantized-mesh-1.0",
51 }
52 }
53}
54
55#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
59#[serde(rename_all = "camelCase")]
60pub struct TileAvailability {
61 pub start_x: u32,
63 pub start_y: u32,
65 pub end_x: u32,
67 pub end_y: u32,
69}
70
71impl TileAvailability {
72 pub const fn new(start_x: u32, start_y: u32, end_x: u32, end_y: u32) -> Self {
74 Self {
75 start_x,
76 start_y,
77 end_x,
78 end_y,
79 }
80 }
81
82 pub fn full_level_geodetic_tms(zoom: u8) -> Self {
85 Self::new(
86 0,
87 0,
88 geodetic_tms::tile_count_x(zoom) - 1,
89 geodetic_tms::tile_count_y(zoom) - 1,
90 )
91 }
92
93 pub fn full_level_xyz(zoom: u8) -> Self {
95 let n = web_mercator::tile_count(zoom);
96 Self::new(0, 0, n - 1, n - 1)
97 }
98
99 pub fn from_bounds_geodetic_tms(
102 zoom: u8,
103 west: f64,
104 south: f64,
105 east: f64,
106 north: f64,
107 ) -> Self {
108 let (start_x, start_y) = geodetic_tms::lonlat_to_tile(west, south, zoom);
109 let (end_x, end_y) = geodetic_tms::lonlat_to_tile(east, north, zoom);
110 Self::new(start_x, start_y, end_x, end_y)
111 }
112
113 pub fn from_bounds_xyz(zoom: u8, west: f64, south: f64, east: f64, north: f64) -> Self {
115 let (start_x, end_y) = web_mercator::lonlat_to_tile(west, south, zoom);
117 let (end_x, start_y) = web_mercator::lonlat_to_tile(east, north, zoom);
118 Self::new(start_x, start_y, end_x, end_y)
119 }
120
121 pub fn from_bounds_web_mercator_tms(
124 zoom: u8,
125 west: f64,
126 south: f64,
127 east: f64,
128 north: f64,
129 ) -> Self {
130 let (start_x, start_y) = web_mercator_tms::lonlat_to_tile(west, south, zoom);
131 let (end_x, end_y) = web_mercator_tms::lonlat_to_tile(east, north, zoom);
132 Self::new(start_x, start_y, end_x, end_y)
133 }
134}
135
136#[derive(Debug, Clone)]
138pub struct LayerJsonConfig {
139 pub tiles_template: String,
141 pub version: String,
143 pub attribution: Option<String>,
145 pub available: Vec<Vec<TileAvailability>>,
147 pub min_zoom: Option<u8>,
149 pub max_zoom: Option<u8>,
151 pub scheme: TilingScheme,
153 pub bounds: Option<[f64; 4]>,
155 pub extensions: Vec<String>,
158 pub format: TerrainFormat,
160 pub metadata_availability: Option<u8>,
164}
165
166impl Default for LayerJsonConfig {
167 fn default() -> Self {
168 Self {
169 tiles_template: "{z}/{x}/{y}.terrain".to_string(),
170 version: "1.0.0".to_string(),
171 attribution: None,
172 available: Vec::new(),
173 min_zoom: None,
174 max_zoom: None,
175 scheme: TilingScheme::Tms,
176 bounds: None,
177 extensions: Vec::new(),
178 format: TerrainFormat::QuantizedMesh1,
179 metadata_availability: None,
180 }
181 }
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
189#[serde(rename_all = "camelCase")]
190pub struct LayerJson {
191 pub tilejson: String,
193 pub format: String,
195 pub version: String,
197 pub scheme: String,
199 pub tiles: Vec<String>,
201 #[serde(skip_serializing_if = "Vec::is_empty", default)]
203 pub available: Vec<Vec<TileAvailability>>,
204 #[serde(skip_serializing_if = "Option::is_none", default)]
206 pub attribution: Option<String>,
207 #[serde(skip_serializing_if = "Option::is_none", rename = "minzoom", default)]
209 pub min_zoom: Option<u8>,
210 #[serde(skip_serializing_if = "Option::is_none", rename = "maxzoom", default)]
212 pub max_zoom: Option<u8>,
213 #[serde(skip_serializing_if = "Option::is_none", default)]
215 pub bounds: Option<[f64; 4]>,
216 #[serde(skip_serializing_if = "Vec::is_empty", default)]
218 pub extensions: Vec<String>,
219 #[serde(skip_serializing_if = "Option::is_none", default)]
221 pub metadata_availability: Option<u8>,
222}
223
224impl LayerJson {
225 pub fn from_config(config: &LayerJsonConfig) -> Self {
227 Self {
228 tilejson: "2.1.0".to_string(),
229 format: config.format.as_str().to_string(),
230 version: config.version.clone(),
231 scheme: config.scheme.as_str().to_string(),
232 tiles: vec![config.tiles_template.clone()],
233 available: config.available.clone(),
234 attribution: config.attribution.clone(),
235 min_zoom: config.min_zoom,
236 max_zoom: config.max_zoom,
237 bounds: config.bounds,
238 extensions: config.extensions.clone(),
239 metadata_availability: config.metadata_availability,
240 }
241 }
242
243 pub fn to_json_pretty(&self) -> serde_json::Result<String> {
245 serde_json::to_string_pretty(self)
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn full_level_geodetic_tms_z0_covers_two_x_one_y() {
255 let r = TileAvailability::full_level_geodetic_tms(0);
256 assert_eq!(r, TileAvailability::new(0, 0, 1, 0));
257 }
258
259 #[test]
260 fn from_bounds_geodetic_tms_is_ordered() {
261 let r = TileAvailability::from_bounds_geodetic_tms(4, 122.0, 20.0, 154.0, 46.0);
262 assert!(r.start_x <= r.end_x);
263 assert!(r.start_y <= r.end_y);
264 }
265
266 #[test]
267 fn layer_json_round_trips_through_serde() {
268 let cfg = LayerJsonConfig {
269 attribution: Some("Made with terrain-codec".into()),
270 available: vec![vec![TileAvailability::full_level_geodetic_tms(0)]],
271 min_zoom: Some(0),
272 max_zoom: Some(10),
273 bounds: Some([-180.0, -90.0, 180.0, 90.0]),
274 extensions: vec!["octvertexnormals".into(), "watermask".into()],
275 format: TerrainFormat::QuantizedMesh1,
276 metadata_availability: Some(10),
277 ..Default::default()
278 };
279 let lj = LayerJson::from_config(&cfg);
280 let json = serde_json::to_string(&lj).unwrap();
281 let parsed: LayerJson = serde_json::from_str(&json).unwrap();
282 assert_eq!(parsed, lj);
283 assert!(json.contains("quantized-mesh-1.0"));
284 assert!(json.contains("octvertexnormals"));
285 assert!(json.contains("metadataAvailability"));
287 }
288
289 #[test]
290 fn empty_optionals_are_omitted() {
291 let lj = LayerJson::from_config(&LayerJsonConfig::default());
292 let json = serde_json::to_string(&lj).unwrap();
293 assert!(!json.contains("available"));
294 assert!(!json.contains("bounds"));
295 assert!(!json.contains("attribution"));
296 assert!(!json.contains("extensions"));
297 assert!(!json.contains("minzoom"));
298 assert!(!json.contains("maxzoom"));
299 }
300}