1use std::fmt;
24
25#[derive(Debug, Clone, PartialEq)]
33pub struct VectorLayerMeta {
34 pub id: String,
36 pub description: Option<String>,
38 pub min_zoom: Option<u8>,
40 pub max_zoom: Option<u8>,
42}
43
44impl VectorLayerMeta {
45 pub fn new(id: impl Into<String>) -> Self {
47 Self {
48 id: id.into(),
49 description: None,
50 min_zoom: None,
51 max_zoom: None,
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq)]
66pub struct TileJson {
67 pub tilejson: String,
69 pub name: Option<String>,
71 pub description: Option<String>,
73 pub version: Option<String>,
75 pub attribution: Option<String>,
77 pub tiles: Vec<String>,
79 pub min_zoom: u8,
81 pub max_zoom: u8,
83 pub bounds: Option<[f64; 4]>,
87 pub center: Option<[f64; 3]>,
89 pub scheme: TileScheme,
91 pub vector_layers: Vec<VectorLayerMeta>,
93}
94
95impl Default for TileJson {
96 fn default() -> Self {
97 Self {
98 tilejson: "3.0.0".into(),
99 name: None,
100 description: None,
101 version: None,
102 attribution: None,
103 tiles: Vec::new(),
104 min_zoom: 0,
105 max_zoom: 22,
106 bounds: None,
107 center: None,
108 scheme: TileScheme::Xyz,
109 vector_layers: Vec::new(),
110 }
111 }
112}
113
114impl TileJson {
115 pub fn with_tiles(tiles: Vec<String>) -> Self {
117 Self {
118 tiles,
119 ..Self::default()
120 }
121 }
122
123 #[inline]
125 pub fn first_tile_url(&self) -> Option<&str> {
126 self.tiles.first().map(String::as_str)
127 }
128
129 #[inline]
132 pub fn is_vector(&self) -> bool {
133 !self.vector_layers.is_empty()
134 }
135
136 pub fn source_layer_names(&self) -> Vec<&str> {
138 self.vector_layers.iter().map(|vl| vl.id.as_str()).collect()
139 }
140
141 pub fn contains_point(&self, lon: f64, lat: f64) -> bool {
145 match self.bounds {
146 Some([west, south, east, north]) => {
147 lon >= west && lon <= east && lat >= south && lat <= north
148 }
149 None => true,
150 }
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
156pub enum TileScheme {
157 #[default]
159 Xyz,
160 Tms,
162}
163
164impl fmt::Display for TileScheme {
165 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166 match self {
167 TileScheme::Xyz => write!(f, "xyz"),
168 TileScheme::Tms => write!(f, "tms"),
169 }
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq)]
179pub enum TileJsonError {
180 InvalidJson(String),
182 MissingField(&'static str),
184 InvalidField {
186 field: &'static str,
188 reason: String,
190 },
191}
192
193impl fmt::Display for TileJsonError {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 match self {
196 TileJsonError::InvalidJson(msg) => write!(f, "invalid TileJSON: {msg}"),
197 TileJsonError::MissingField(field) => {
198 write!(f, "missing required TileJSON field: `{field}`")
199 }
200 TileJsonError::InvalidField { field, reason } => {
201 write!(f, "invalid TileJSON field `{field}`: {reason}")
202 }
203 }
204 }
205}
206
207impl std::error::Error for TileJsonError {}
208
209#[cfg(feature = "style-json")]
214mod parsing {
215 use super::*;
216 use serde_json::Value;
217
218 pub fn parse_tilejson(bytes: &[u8]) -> Result<TileJson, TileJsonError> {
220 let value: Value =
221 serde_json::from_slice(bytes).map_err(|e| TileJsonError::InvalidJson(e.to_string()))?;
222 parse_tilejson_value(&value)
223 }
224
225 pub fn parse_tilejson_value(value: &Value) -> Result<TileJson, TileJsonError> {
227 let obj = value
228 .as_object()
229 .ok_or(TileJsonError::InvalidJson("root is not an object".into()))?;
230
231 let tilejson = obj
232 .get("tilejson")
233 .and_then(|v| v.as_str())
234 .unwrap_or("3.0.0")
235 .to_owned();
236
237 let tiles = obj
238 .get("tiles")
239 .and_then(|v| v.as_array())
240 .map(|arr| {
241 arr.iter()
242 .filter_map(|v| v.as_str().map(ToOwned::to_owned))
243 .collect::<Vec<_>>()
244 })
245 .unwrap_or_default();
246
247 if tiles.is_empty() {
248 return Err(TileJsonError::MissingField("tiles"));
249 }
250
251 let min_zoom = obj
252 .get("minzoom")
253 .and_then(|v| v.as_u64())
254 .map(|v| v.min(30) as u8)
255 .unwrap_or(0);
256
257 let max_zoom = obj
258 .get("maxzoom")
259 .and_then(|v| v.as_u64())
260 .map(|v| v.min(30) as u8)
261 .unwrap_or(22);
262
263 let bounds = obj.get("bounds").and_then(|v| {
264 let arr = v.as_array()?;
265 if arr.len() >= 4 {
266 Some([
267 arr[0].as_f64()?,
268 arr[1].as_f64()?,
269 arr[2].as_f64()?,
270 arr[3].as_f64()?,
271 ])
272 } else {
273 None
274 }
275 });
276
277 let center = obj.get("center").and_then(|v| {
278 let arr = v.as_array()?;
279 if arr.len() >= 3 {
280 Some([arr[0].as_f64()?, arr[1].as_f64()?, arr[2].as_f64()?])
281 } else {
282 None
283 }
284 });
285
286 let scheme = obj
287 .get("scheme")
288 .and_then(|v| v.as_str())
289 .map(|s| match s {
290 "tms" => TileScheme::Tms,
291 _ => TileScheme::Xyz,
292 })
293 .unwrap_or(TileScheme::Xyz);
294
295 let name = obj
296 .get("name")
297 .and_then(|v| v.as_str())
298 .map(ToOwned::to_owned);
299 let description = obj
300 .get("description")
301 .and_then(|v| v.as_str())
302 .map(ToOwned::to_owned);
303 let version = obj
304 .get("version")
305 .and_then(|v| v.as_str())
306 .map(ToOwned::to_owned);
307 let attribution = obj
308 .get("attribution")
309 .and_then(|v| v.as_str())
310 .map(ToOwned::to_owned);
311
312 let vector_layers = obj
313 .get("vector_layers")
314 .and_then(|v| v.as_array())
315 .map(|arr| arr.iter().filter_map(parse_vector_layer_meta).collect())
316 .unwrap_or_default();
317
318 Ok(TileJson {
319 tilejson,
320 name,
321 description,
322 version,
323 attribution,
324 tiles,
325 min_zoom,
326 max_zoom,
327 bounds,
328 center,
329 scheme,
330 vector_layers,
331 })
332 }
333
334 fn parse_vector_layer_meta(value: &Value) -> Option<VectorLayerMeta> {
335 let obj = value.as_object()?;
336 let id = obj.get("id")?.as_str()?.to_owned();
337 let description = obj
338 .get("description")
339 .and_then(|v| v.as_str())
340 .map(ToOwned::to_owned);
341 let min_zoom = obj
342 .get("minzoom")
343 .and_then(|v| v.as_u64())
344 .map(|v| v.min(30) as u8);
345 let max_zoom = obj
346 .get("maxzoom")
347 .and_then(|v| v.as_u64())
348 .map(|v| v.min(30) as u8);
349 Some(VectorLayerMeta {
350 id,
351 description,
352 min_zoom,
353 max_zoom,
354 })
355 }
356}
357
358#[cfg(feature = "style-json")]
359pub use parsing::{parse_tilejson, parse_tilejson_value};
360
361#[cfg(test)]
366mod tests {
367 use super::*;
368
369 #[test]
370 fn default_tilejson_has_sensible_values() {
371 let tj = TileJson::default();
372 assert_eq!(tj.tilejson, "3.0.0");
373 assert_eq!(tj.min_zoom, 0);
374 assert_eq!(tj.max_zoom, 22);
375 assert!(tj.tiles.is_empty());
376 assert!(!tj.is_vector());
377 assert!(tj.source_layer_names().is_empty());
378 }
379
380 #[test]
381 fn with_tiles_constructor() {
382 let tj = TileJson::with_tiles(vec!["https://example.com/{z}/{x}/{y}.pbf".into()]);
383 assert_eq!(tj.tiles.len(), 1);
384 assert_eq!(
385 tj.first_tile_url(),
386 Some("https://example.com/{z}/{x}/{y}.pbf")
387 );
388 }
389
390 #[test]
391 fn is_vector_when_layers_present() {
392 let mut tj = TileJson::default();
393 assert!(!tj.is_vector());
394 tj.vector_layers.push(VectorLayerMeta::new("water"));
395 assert!(tj.is_vector());
396 assert_eq!(tj.source_layer_names(), vec!["water"]);
397 }
398
399 #[test]
400 fn contains_point_unbounded() {
401 let tj = TileJson::default();
402 assert!(tj.contains_point(0.0, 0.0));
403 assert!(tj.contains_point(180.0, 90.0));
404 }
405
406 #[test]
407 fn contains_point_bounded() {
408 let tj = TileJson {
409 bounds: Some([-10.0, -20.0, 30.0, 40.0]),
410 ..TileJson::default()
411 };
412 assert!(tj.contains_point(0.0, 0.0));
413 assert!(tj.contains_point(-10.0, -20.0));
414 assert!(tj.contains_point(30.0, 40.0));
415 assert!(!tj.contains_point(-11.0, 0.0));
416 assert!(!tj.contains_point(0.0, 41.0));
417 }
418
419 #[test]
420 fn tile_scheme_display() {
421 assert_eq!(TileScheme::Xyz.to_string(), "xyz");
422 assert_eq!(TileScheme::Tms.to_string(), "tms");
423 }
424
425 #[test]
426 fn tilejson_error_display() {
427 let err = TileJsonError::MissingField("tiles");
428 assert!(err.to_string().contains("tiles"));
429 }
430
431 #[cfg(feature = "style-json")]
432 mod json_parsing {
433 use super::*;
434
435 #[test]
436 fn parse_minimal_vector_tilejson() {
437 let json = br#"{
438 "tilejson": "3.0.0",
439 "tiles": ["https://example.com/{z}/{x}/{y}.pbf"],
440 "minzoom": 0,
441 "maxzoom": 14,
442 "vector_layers": [
443 {"id": "water", "minzoom": 0, "maxzoom": 14},
444 {"id": "roads", "description": "Road network"}
445 ]
446 }"#;
447
448 let tj = parse_tilejson(json).expect("valid tilejson");
449 assert_eq!(tj.tilejson, "3.0.0");
450 assert_eq!(tj.tiles.len(), 1);
451 assert_eq!(tj.min_zoom, 0);
452 assert_eq!(tj.max_zoom, 14);
453 assert!(tj.is_vector());
454 assert_eq!(tj.vector_layers.len(), 2);
455 assert_eq!(tj.vector_layers[0].id, "water");
456 assert_eq!(tj.vector_layers[0].min_zoom, Some(0));
457 assert_eq!(tj.vector_layers[0].max_zoom, Some(14));
458 assert_eq!(tj.vector_layers[1].id, "roads");
459 assert_eq!(
460 tj.vector_layers[1].description.as_deref(),
461 Some("Road network")
462 );
463 }
464
465 #[test]
466 fn parse_raster_tilejson() {
467 let json = br#"{
468 "tilejson": "2.2.0",
469 "tiles": ["https://tile.example.com/{z}/{x}/{y}.png"],
470 "minzoom": 0,
471 "maxzoom": 18,
472 "bounds": [-180, -85.05, 180, 85.05],
473 "center": [0, 0, 2],
474 "name": "OpenStreetMap",
475 "attribution": "© OSM contributors"
476 }"#;
477
478 let tj = parse_tilejson(json).expect("valid tilejson");
479 assert_eq!(tj.tilejson, "2.2.0");
480 assert!(!tj.is_vector());
481 assert_eq!(tj.name.as_deref(), Some("OpenStreetMap"));
482 assert!(tj.attribution.is_some());
483 assert!(tj.bounds.is_some());
484 assert!(tj.center.is_some());
485 let bounds = tj.bounds.expect("bounds");
486 assert!((bounds[0] - (-180.0)).abs() < 1e-9);
487 }
488
489 #[test]
490 fn parse_tilejson_missing_tiles_fails() {
491 let json = br#"{"tilejson": "3.0.0"}"#;
492 let err = parse_tilejson(json).expect_err("should fail");
493 assert!(matches!(err, TileJsonError::MissingField("tiles")));
494 }
495
496 #[test]
497 fn parse_tilejson_invalid_json() {
498 let err = parse_tilejson(b"not json").expect_err("should fail");
499 assert!(matches!(err, TileJsonError::InvalidJson(_)));
500 }
501
502 #[test]
503 fn parse_tilejson_with_scheme() {
504 let json = br#"{
505 "tiles": ["https://example.com/{z}/{x}/{y}.pbf"],
506 "scheme": "tms"
507 }"#;
508 let tj = parse_tilejson(json).expect("valid tilejson");
509 assert_eq!(tj.scheme, TileScheme::Tms);
510 }
511 }
512}