1use serde::{Deserialize, Serialize};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase")]
27pub struct TileMatrixSet {
28 pub id: String,
30 pub title: String,
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub uri: Option<String>,
35 pub crs: String,
37 pub tile_matrices: Vec<TileMatrix>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct TileMatrix {
48 pub id: String,
50 pub scale_denominator: f64,
52 pub cell_size: f64,
54 pub corner_of_origin: CornerOfOrigin,
56 pub point_of_origin: [f64; 2],
58 pub tile_width: u32,
60 pub tile_height: u32,
62 pub matrix_width: u32,
64 pub matrix_height: u32,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "camelCase")]
71pub enum CornerOfOrigin {
72 TopLeft,
74 BottomLeft,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
81#[serde(rename_all = "camelCase")]
82pub struct TileSetMetadata {
83 pub tile_matrix_set_id: String,
85 pub data_type: TileDataType,
87 pub links: Vec<TileLink>,
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub title: Option<String>,
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub description: Option<String>,
95 #[serde(skip_serializing_if = "Option::is_none")]
97 pub attribution: Option<String>,
98 #[serde(skip_serializing_if = "Option::is_none")]
100 pub extent: Option<GeographicBoundingBox>,
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub min_tile_matrix: Option<String>,
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub max_tile_matrix: Option<String>,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
111#[serde(rename_all = "lowercase")]
112pub enum TileDataType {
113 Map,
115 Vector,
117 Coverage,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct TileLink {
124 pub href: String,
126 pub rel: String,
128 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
130 pub media_type: Option<String>,
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub title: Option<String>,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct GeographicBoundingBox {
139 pub lower_left: [f64; 2],
141 pub upper_right: [f64; 2],
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147#[serde(rename_all = "camelCase")]
148pub struct ConformanceDeclaration {
149 pub conforms_to: Vec<String>,
151}
152
153impl ConformanceDeclaration {
154 pub fn ogc_tiles() -> Self {
156 Self {
157 conforms_to: vec![
158 "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/core".into(),
159 "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/tilematrixset".into(),
160 "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/geodata-tilesets".into(),
161 "http://www.opengis.net/spec/ogcapi-tiles-1/1.0/conf/collections-selection".into(),
162 "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/core".into(),
163 "http://www.opengis.net/spec/ogcapi-common-1/1.0/conf/json".into(),
164 ],
165 }
166 }
167}
168
169impl TileMatrixSet {
174 pub fn web_mercator_quad() -> Self {
179 const ORIGIN_X: f64 = -20_037_508.342_789_244;
183 const ORIGIN_Y: f64 = 20_037_508.342_789_244;
184 const SCALE_0: f64 = 559_082_264.028_717_8;
185 const CELL_0: f64 = 156_543.033_928_041;
186
187 let matrices = (0u8..=24)
188 .map(|z| {
189 let n = 1u32 << z;
190 let factor = n as f64;
191 TileMatrix {
192 id: z.to_string(),
193 scale_denominator: SCALE_0 / factor,
194 cell_size: CELL_0 / factor,
195 corner_of_origin: CornerOfOrigin::TopLeft,
196 point_of_origin: [ORIGIN_X, ORIGIN_Y],
197 tile_width: 256,
198 tile_height: 256,
199 matrix_width: n,
200 matrix_height: n,
201 }
202 })
203 .collect();
204
205 Self {
206 id: "WebMercatorQuad".into(),
207 title: "Google Maps Compatible for the World".into(),
208 uri: Some("http://www.opengis.net/def/tilematrixset/OGC/1.0/WebMercatorQuad".into()),
209 crs: "http://www.opengis.net/def/crs/EPSG/0/3857".into(),
210 tile_matrices: matrices,
211 }
212 }
213
214 pub fn world_crs84_quad() -> Self {
219 const SCALE_0: f64 = 279_541_132.014_358_76;
221 const PIXEL_SIZE_DEG: f64 = 0.000_000_277_777_8; let matrices = (0u8..=17)
224 .map(|z| {
225 let n_y = 1u32 << z;
227 let n_x = 2u32 << z;
228 let factor = n_y as f64;
229 TileMatrix {
230 id: z.to_string(),
231 scale_denominator: SCALE_0 / factor,
232 cell_size: PIXEL_SIZE_DEG / factor,
233 corner_of_origin: CornerOfOrigin::TopLeft,
234 point_of_origin: [-180.0, 90.0],
235 tile_width: 256,
236 tile_height: 256,
237 matrix_width: n_x,
238 matrix_height: n_y,
239 }
240 })
241 .collect();
242
243 Self {
244 id: "WorldCRS84Quad".into(),
245 title: "CRS84 for the World".into(),
246 uri: Some("http://www.opengis.net/def/tilematrixset/OGC/1.0/WorldCRS84Quad".into()),
247 crs: "http://www.opengis.net/def/crs/OGC/1.3/CRS84".into(),
248 tile_matrices: matrices,
249 }
250 }
251
252 pub fn tile_matrix(&self, zoom: u8) -> Option<&TileMatrix> {
254 self.tile_matrices.iter().find(|m| m.id == zoom.to_string())
255 }
256
257 pub fn max_zoom(&self) -> u8 {
259 self.tile_matrices
260 .iter()
261 .filter_map(|m| m.id.parse::<u8>().ok())
262 .max()
263 .unwrap_or(0)
264 }
265
266 pub fn min_zoom(&self) -> u8 {
268 self.tile_matrices
269 .iter()
270 .filter_map(|m| m.id.parse::<u8>().ok())
271 .min()
272 .unwrap_or(0)
273 }
274
275 pub fn zoom_level_count(&self) -> usize {
277 self.tile_matrices.len()
278 }
279}
280
281pub fn tile_to_bbox(z: u8, x: u32, y: u32) -> [f64; 4] {
296 let n = 1u32 << z;
297 let nf = n as f64;
298
299 let west = (x as f64 / nf) * 360.0 - 180.0;
300 let east = ((x + 1) as f64 / nf) * 360.0 - 180.0;
301
302 let to_lat = |row: u32| -> f64 {
304 let sinh_arg = (1.0 - 2.0 * row as f64 / nf) * std::f64::consts::PI;
305 sinh_arg.sinh().atan().to_degrees()
306 };
307
308 let north = to_lat(y);
309 let south = to_lat(y + 1);
310
311 [west, south, east, north]
312}
313
314pub fn lonlat_to_tile(lon: f64, lat: f64, zoom: u8) -> (u32, u32) {
326 let n = 1u32 << zoom;
327 let nf = n as f64;
328
329 let x_raw = (lon + 180.0) / 360.0 * nf;
330 let lat_rad = lat.to_radians();
331 let y_raw =
332 (1.0 - (lat_rad.tan() + (1.0 / lat_rad.cos())).ln() / std::f64::consts::PI) / 2.0 * nf;
333
334 let x = (x_raw as u32).min(n.saturating_sub(1));
335 let y = (y_raw as u32).min(n.saturating_sub(1));
336 (x, y)
337}
338
339pub fn tile_to_pixel_bounds(_z: u8, x: u32, y: u32) -> (u64, u64, u64, u64) {
344 let tile_size: u64 = 256;
345 let x0 = x as u64 * tile_size;
346 let y0 = y as u64 * tile_size;
347 (x0, y0, x0 + tile_size, y0 + tile_size)
348}
349
350pub fn validate_tile_coords(z: u8, x: u32, y: u32) -> bool {
354 if z > 30 {
355 return false;
356 }
357 let max = (1u32 << z).saturating_sub(1);
358 x <= max && y <= max
359}
360
361pub fn tile_children(z: u8, x: u32, y: u32) -> Option<[(u8, u32, u32); 4]> {
365 let next_z = z.checked_add(1)?;
366 Some([
367 (next_z, 2 * x, 2 * y),
368 (next_z, 2 * x + 1, 2 * y),
369 (next_z, 2 * x, 2 * y + 1),
370 (next_z, 2 * x + 1, 2 * y + 1),
371 ])
372}
373
374pub fn tile_parent(z: u8, x: u32, y: u32) -> Option<(u8, u32, u32)> {
378 let parent_z = z.checked_sub(1)?;
379 Some((parent_z, x / 2, y / 2))
380}
381
382pub fn tiles_in_bbox(bbox: [f64; 4], zoom: u8) -> impl Iterator<Item = (u32, u32)> {
387 let [west, south, east, north] = bbox;
388 let (x_min, y_max) = lonlat_to_tile(west, south, zoom);
389 let (x_max, y_min) = lonlat_to_tile(east, north, zoom);
390
391 let n = (1u32 << zoom).saturating_sub(1);
392 let x_min = x_min.min(n);
393 let x_max = x_max.min(n);
394 let y_min = y_min.min(n);
395 let y_max = y_max.min(n);
396
397 (y_min..=y_max).flat_map(move |y| (x_min..=x_max).map(move |x| (x, y)))
398}
399
400impl TileSetMetadata {
405 pub fn vector_web_mercator(tile_url_template: impl Into<String>) -> Self {
407 Self {
408 tile_matrix_set_id: "WebMercatorQuad".into(),
409 data_type: TileDataType::Vector,
410 links: vec![TileLink {
411 href: tile_url_template.into(),
412 rel: "item".into(),
413 media_type: Some("application/vnd.mapbox-vector-tile".into()),
414 title: Some("Vector tiles".into()),
415 }],
416 title: None,
417 description: None,
418 attribution: None,
419 extent: None,
420 min_tile_matrix: Some("0".into()),
421 max_tile_matrix: Some("24".into()),
422 }
423 }
424
425 pub fn map_web_mercator(tile_url_template: impl Into<String>) -> Self {
427 Self {
428 tile_matrix_set_id: "WebMercatorQuad".into(),
429 data_type: TileDataType::Map,
430 links: vec![TileLink {
431 href: tile_url_template.into(),
432 rel: "item".into(),
433 media_type: Some("image/png".into()),
434 title: Some("Map tiles".into()),
435 }],
436 title: None,
437 description: None,
438 attribution: None,
439 extent: None,
440 min_tile_matrix: Some("0".into()),
441 max_tile_matrix: Some("24".into()),
442 }
443 }
444
445 pub fn with_extent(mut self, west: f64, south: f64, east: f64, north: f64) -> Self {
447 self.extent = Some(GeographicBoundingBox {
448 lower_left: [west, south],
449 upper_right: [east, north],
450 });
451 self
452 }
453
454 pub fn with_zoom_range(mut self, min_zoom: u8, max_zoom: u8) -> Self {
456 self.min_tile_matrix = Some(min_zoom.to_string());
457 self.max_tile_matrix = Some(max_zoom.to_string());
458 self
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465
466 #[test]
469 fn test_web_mercator_quad_zoom_count() {
470 let tms = TileMatrixSet::web_mercator_quad();
471 assert_eq!(
472 tms.zoom_level_count(),
473 25,
474 "WebMercatorQuad should have zoom 0–24 (25 levels)"
475 );
476 }
477
478 #[test]
479 fn test_web_mercator_quad_max_zoom() {
480 let tms = TileMatrixSet::web_mercator_quad();
481 assert_eq!(tms.max_zoom(), 24);
482 }
483
484 #[test]
485 fn test_web_mercator_quad_min_zoom() {
486 let tms = TileMatrixSet::web_mercator_quad();
487 assert_eq!(tms.min_zoom(), 0);
488 }
489
490 #[test]
491 fn test_web_mercator_quad_zoom0_matrix_size() {
492 let tms = TileMatrixSet::web_mercator_quad();
493 let m = tms.tile_matrix(0).expect("zoom 0 must exist");
494 assert_eq!(m.matrix_width, 1);
495 assert_eq!(m.matrix_height, 1);
496 }
497
498 #[test]
499 fn test_web_mercator_quad_zoom1_matrix_size() {
500 let tms = TileMatrixSet::web_mercator_quad();
501 let m = tms.tile_matrix(1).expect("zoom 1 must exist");
502 assert_eq!(m.matrix_width, 2);
503 assert_eq!(m.matrix_height, 2);
504 }
505
506 #[test]
507 fn test_web_mercator_quad_zoom24_matrix_size() {
508 let tms = TileMatrixSet::web_mercator_quad();
509 let m = tms.tile_matrix(24).expect("zoom 24 must exist");
510 assert_eq!(m.matrix_width, 1 << 24);
511 assert_eq!(m.matrix_height, 1 << 24);
512 }
513
514 #[test]
515 fn test_web_mercator_quad_tile_matrix_none_for_25() {
516 let tms = TileMatrixSet::web_mercator_quad();
517 assert!(tms.tile_matrix(25).is_none());
518 }
519
520 #[test]
521 fn test_world_crs84_quad_zoom_count() {
522 let tms = TileMatrixSet::world_crs84_quad();
523 assert_eq!(
524 tms.zoom_level_count(),
525 18,
526 "WorldCRS84Quad should have zoom 0–17 (18 levels)"
527 );
528 }
529
530 #[test]
531 fn test_world_crs84_quad_max_zoom() {
532 let tms = TileMatrixSet::world_crs84_quad();
533 assert_eq!(tms.max_zoom(), 17);
534 }
535
536 #[test]
537 fn test_world_crs84_quad_zoom0_aspect_ratio() {
538 let tms = TileMatrixSet::world_crs84_quad();
540 let m = tms.tile_matrix(0).expect("zoom 0 must exist");
541 assert_eq!(m.matrix_width, 2);
542 assert_eq!(m.matrix_height, 1);
543 }
544
545 #[test]
546 fn test_world_crs84_quad_zoom1_size() {
547 let tms = TileMatrixSet::world_crs84_quad();
548 let m = tms.tile_matrix(1).expect("zoom 1 must exist");
549 assert_eq!(m.matrix_width, 4);
550 assert_eq!(m.matrix_height, 2);
551 }
552
553 #[test]
554 fn test_world_crs84_quad_tile_matrix_none_for_18() {
555 let tms = TileMatrixSet::world_crs84_quad();
556 assert!(tms.tile_matrix(18).is_none());
557 }
558
559 #[test]
560 fn test_tile_matrix_corner_of_origin() {
561 let tms = TileMatrixSet::web_mercator_quad();
562 let m = tms.tile_matrix(0).expect("zoom 0 must exist");
563 assert_eq!(m.corner_of_origin, CornerOfOrigin::TopLeft);
564 }
565
566 #[test]
567 fn test_tile_matrix_tile_size() {
568 let tms = TileMatrixSet::web_mercator_quad();
569 let m = tms.tile_matrix(10).expect("zoom 10 must exist");
570 assert_eq!(m.tile_width, 256);
571 assert_eq!(m.tile_height, 256);
572 }
573
574 #[test]
575 fn test_tile_matrix_scale_decreases_with_zoom() {
576 let tms = TileMatrixSet::web_mercator_quad();
577 let m0 = tms.tile_matrix(0).expect("zoom 0");
578 let m1 = tms.tile_matrix(1).expect("zoom 1");
579 assert!(
580 m0.scale_denominator > m1.scale_denominator,
581 "Scale denominator should decrease as zoom increases"
582 );
583 }
584
585 #[test]
588 fn test_tile_to_bbox_zoom0_full_world() {
589 let [west, south, east, north] = tile_to_bbox(0, 0, 0);
590 assert!((west - (-180.0)).abs() < 1e-6, "west={}", west);
591 assert!((east - 180.0).abs() < 1e-6, "east={}", east);
592 assert!(south < -85.0, "south={}", south);
594 assert!(north > 85.0, "north={}", north);
595 }
596
597 #[test]
598 fn test_tile_to_bbox_zoom1_nw_quadrant() {
599 let [west, south, east, north] = tile_to_bbox(1, 0, 0);
600 assert!((west - (-180.0)).abs() < 1e-6);
601 assert!((east - 0.0).abs() < 1e-6);
602 assert!(north > 0.0);
603 assert!(south > 0.0 || south.abs() < 1e-6);
604 }
605
606 #[test]
607 fn test_tile_to_bbox_zoom1_se_quadrant() {
608 let [west, south, east, north] = tile_to_bbox(1, 1, 1);
609 assert!((west - 0.0).abs() < 1e-6);
610 assert!((east - 180.0).abs() < 1e-6);
611 assert!(south < 0.0);
612 assert!(north.abs() < 1e-6 || north > -1.0);
613 }
614
615 #[test]
616 fn test_tile_to_bbox_ordering() {
617 for z in 0u8..=5 {
619 let n = 1u32 << z;
620 for x in 0..n {
621 for y in 0..n {
622 let [west, south, east, north] = tile_to_bbox(z, x, y);
623 assert!(west < east, "z={} x={} y={}: west >= east", z, x, y);
624 assert!(south < north, "z={} x={} y={}: south >= north", z, x, y);
625 }
626 }
627 }
628 }
629
630 #[test]
633 fn test_lonlat_to_tile_zoom0_any_point() {
634 assert_eq!(lonlat_to_tile(0.0, 0.0, 0), (0, 0));
636 assert_eq!(lonlat_to_tile(-90.0, 45.0, 0), (0, 0));
637 assert_eq!(lonlat_to_tile(90.0, -45.0, 0), (0, 0));
638 }
639
640 #[test]
641 fn test_lonlat_to_tile_top_left_zoom1() {
642 let (x, y) = lonlat_to_tile(-179.999, 84.999, 1);
644 assert_eq!((x, y), (0, 0), "top-left should be (0,0) got ({},{})", x, y);
645 }
646
647 #[test]
648 fn test_lonlat_to_tile_bottom_right_zoom1() {
649 let (x, y) = lonlat_to_tile(179.999, -84.999, 1);
650 assert_eq!(
651 (x, y),
652 (1, 1),
653 "bottom-right at zoom 1 should be (1,1) got ({},{})",
654 x,
655 y
656 );
657 }
658
659 #[test]
660 fn test_lonlat_to_tile_prime_meridian_equator_zoom8() {
661 let (x, y) = lonlat_to_tile(0.0, 0.0, 8);
662 assert_eq!(x, 128);
664 assert_eq!(y, 128);
665 }
666
667 #[test]
668 fn test_lonlat_to_tile_roundtrip_consistency() {
669 for z in 0u8..=6 {
671 let n = 1u32 << z;
672 for x in 0..n {
673 for y in 0..n {
674 let [west, _south, _east, north] = tile_to_bbox(z, x, y);
675 let center_lon = (west + _east) / 2.0;
677 let (tx, ty) = lonlat_to_tile(center_lon, north - 0.0001, z);
678 assert_eq!(
679 (tx, ty),
680 (x, y),
681 "z={} x={} y={}: center mapped to ({},{})",
682 z,
683 x,
684 y,
685 tx,
686 ty
687 );
688 }
689 }
690 }
691 }
692
693 #[test]
696 fn test_validate_tile_coords_valid() {
697 assert!(validate_tile_coords(0, 0, 0));
698 assert!(validate_tile_coords(10, 0, 0));
699 assert!(validate_tile_coords(10, 1023, 1023));
700 }
701
702 #[test]
703 fn test_validate_tile_coords_out_of_range() {
704 assert!(!validate_tile_coords(0, 1, 0));
705 assert!(!validate_tile_coords(0, 0, 1));
706 assert!(!validate_tile_coords(10, 1024, 0));
707 }
708
709 #[test]
712 fn test_tile_children_count() {
713 let children = tile_children(5, 10, 7).expect("should have children");
714 assert_eq!(children.len(), 4);
715 }
716
717 #[test]
718 fn test_tile_children_zoom_incremented() {
719 let children = tile_children(3, 2, 2).expect("should have children");
720 for (cz, _, _) in &children {
721 assert_eq!(*cz, 4);
722 }
723 }
724
725 #[test]
726 fn test_tile_parent_basic() {
727 let (pz, px, py) = tile_parent(5, 10, 7).expect("should have parent");
728 assert_eq!(pz, 4);
729 assert_eq!(px, 5);
730 assert_eq!(py, 3);
731 }
732
733 #[test]
734 fn test_tile_parent_none_at_zoom0() {
735 assert!(tile_parent(0, 0, 0).is_none());
736 }
737
738 #[test]
741 fn test_tiles_in_bbox_zoom0_world() {
742 let tiles: Vec<_> = tiles_in_bbox([-180.0, -85.0, 180.0, 85.0], 0).collect();
743 assert_eq!(tiles.len(), 1, "zoom 0 whole world = 1 tile");
744 }
745
746 #[test]
747 fn test_tiles_in_bbox_zoom1_world() {
748 let tiles: Vec<_> = tiles_in_bbox([-180.0, -85.0, 180.0, 85.0], 1).collect();
749 assert_eq!(tiles.len(), 4, "zoom 1 whole world = 4 tiles");
750 }
751
752 #[test]
755 fn test_tileset_metadata_vector_web_mercator() {
756 let meta = TileSetMetadata::vector_web_mercator("https://tiles/{z}/{x}/{y}.mvt");
757 assert_eq!(meta.tile_matrix_set_id, "WebMercatorQuad");
758 assert_eq!(meta.data_type, TileDataType::Vector);
759 assert!(!meta.links.is_empty());
760 }
761
762 #[test]
763 fn test_tileset_metadata_map_web_mercator() {
764 let meta = TileSetMetadata::map_web_mercator("https://tiles/{z}/{x}/{y}.png");
765 assert_eq!(meta.data_type, TileDataType::Map);
766 assert!(meta.links[0].href.contains("{z}"));
767 }
768
769 #[test]
770 fn test_tileset_metadata_with_extent() {
771 let meta = TileSetMetadata::vector_web_mercator("https://tiles/{z}/{x}/{y}.mvt")
772 .with_extent(-10.0, 35.0, 40.0, 70.0);
773 let ext = meta.extent.expect("extent should be set");
774 assert_eq!(ext.lower_left, [-10.0, 35.0]);
775 assert_eq!(ext.upper_right, [40.0, 70.0]);
776 }
777
778 #[test]
779 fn test_tileset_metadata_serialization_roundtrip() {
780 let meta =
781 TileSetMetadata::vector_web_mercator("https://example.com/tiles/{z}/{x}/{y}.mvt")
782 .with_extent(-180.0, -90.0, 180.0, 90.0)
783 .with_zoom_range(0, 14);
784
785 let json = serde_json::to_string(&meta).expect("serialization should succeed");
786 let decoded: TileSetMetadata =
787 serde_json::from_str(&json).expect("deserialization should succeed");
788 assert_eq!(decoded.tile_matrix_set_id, "WebMercatorQuad");
789 assert_eq!(decoded.min_tile_matrix.as_deref(), Some("0"));
790 assert_eq!(decoded.max_tile_matrix.as_deref(), Some("14"));
791 }
792
793 #[test]
794 fn test_tile_link_serialization() {
795 let link = TileLink {
796 href: "https://example.com/tiles/0/0/0.mvt".into(),
797 rel: "item".into(),
798 media_type: Some("application/vnd.mapbox-vector-tile".into()),
799 title: Some("Vector tile".into()),
800 };
801 let json = serde_json::to_string(&link).expect("serialization should succeed");
802 assert!(json.contains("application/vnd.mapbox-vector-tile"));
803 assert!(json.contains("item"));
804 }
805
806 #[test]
807 fn test_tile_data_type_variants() {
808 assert_ne!(TileDataType::Map, TileDataType::Vector);
809 assert_ne!(TileDataType::Vector, TileDataType::Coverage);
810 assert_ne!(TileDataType::Map, TileDataType::Coverage);
811 }
812
813 #[test]
814 fn test_corner_of_origin_variants() {
815 assert_ne!(CornerOfOrigin::TopLeft, CornerOfOrigin::BottomLeft);
816 }
817
818 #[test]
819 fn test_geographic_bounding_box() {
820 let bbox = GeographicBoundingBox {
821 lower_left: [-10.0, 35.0],
822 upper_right: [40.0, 70.0],
823 };
824 assert_eq!(bbox.lower_left[0], -10.0);
825 assert_eq!(bbox.upper_right[1], 70.0);
826 }
827
828 #[test]
829 fn test_conformance_declaration_ogc_tiles() {
830 let conf = ConformanceDeclaration::ogc_tiles();
831 assert!(!conf.conforms_to.is_empty());
832 let has_core = conf
833 .conforms_to
834 .iter()
835 .any(|c| c.contains("ogcapi-tiles-1") && c.contains("conf/core"));
836 assert!(has_core, "should include OGC Tiles core conformance class");
837 }
838
839 #[test]
840 fn test_conformance_declaration_serialization() {
841 let conf = ConformanceDeclaration::ogc_tiles();
842 let json = serde_json::to_string(&conf).expect("serialization should succeed");
843 let decoded: ConformanceDeclaration =
844 serde_json::from_str(&json).expect("deserialization should succeed");
845 assert_eq!(decoded.conforms_to.len(), conf.conforms_to.len());
846 }
847
848 #[test]
849 fn test_tile_pixel_bounds() {
850 let (x0, y0, x1, y1) = tile_to_pixel_bounds(0, 0, 0);
851 assert_eq!(x0, 0);
852 assert_eq!(y0, 0);
853 assert_eq!(x1, 256);
854 assert_eq!(y1, 256);
855
856 let (x0, y0, x1, y1) = tile_to_pixel_bounds(1, 1, 1);
857 assert_eq!(x0, 256);
858 assert_eq!(y0, 256);
859 assert_eq!(x1, 512);
860 assert_eq!(y1, 512);
861 }
862
863 #[test]
864 fn test_tile_matrix_set_id_and_crs() {
865 let tms = TileMatrixSet::web_mercator_quad();
866 assert_eq!(tms.id, "WebMercatorQuad");
867 assert!(tms.crs.contains("3857"));
868
869 let tms2 = TileMatrixSet::world_crs84_quad();
870 assert_eq!(tms2.id, "WorldCRS84Quad");
871 assert!(tms2.crs.contains("CRS84") || tms2.crs.contains("4326"));
872 }
873}