1use crate::error::{Error, Result};
4use crate::types::{GeometryType, TimeRange};
5use std::cmp::Ordering;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub struct TileId {
10 pub z: u8,
12 pub x: u32,
14 pub y: u32,
16 pub t: u64,
18}
19
20impl TileId {
21 pub fn new(z: u8, x: u32, y: u32, t: u64) -> Self {
23 Self { z, x, y, t }
24 }
25
26 pub fn validate(&self) -> Result<()> {
28 let max_coord = 1u32 << self.z;
29 if self.x >= max_coord || self.y >= max_coord {
30 return Err(Error::InvalidCoordinates(self.z, self.x, self.y));
31 }
32 Ok(())
33 }
34
35 pub fn hilbert_index(&self) -> u64 {
47 if self.z == 0 {
50 return 0;
51 }
52 let order = self.z as usize;
53 let h = hilbert_2d::xy2h_discrete(
54 self.x as usize,
55 self.y as usize,
56 order,
57 hilbert_2d::Variant::Hilbert,
58 );
59 h as u64
60 }
61
62 pub fn parent(&self) -> Option<TileId> {
64 if self.z == 0 {
65 return None;
66 }
67 Some(TileId {
68 z: self.z - 1,
69 x: self.x / 2,
70 y: self.y / 2,
71 t: self.t,
72 })
73 }
74
75 pub fn children(&self) -> Vec<TileId> {
77 if self.z >= 22 {
78 return vec![];
79 }
80 vec![
81 TileId::new(self.z + 1, self.x * 2, self.y * 2, self.t),
82 TileId::new(self.z + 1, self.x * 2 + 1, self.y * 2, self.t),
83 TileId::new(self.z + 1, self.x * 2, self.y * 2 + 1, self.t),
84 TileId::new(self.z + 1, self.x * 2 + 1, self.y * 2 + 1, self.t),
85 ]
86 }
87
88 pub fn to_string(&self) -> String {
90 format!("{}/{}/{}/{}", self.z, self.x, self.y, self.t)
91 }
92}
93
94impl PartialOrd for TileId {
95 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
96 Some(self.cmp(other))
97 }
98}
99
100impl Ord for TileId {
101 fn cmp(&self, other: &Self) -> Ordering {
102 self.z
104 .cmp(&other.z)
105 .then(self.hilbert_index().cmp(&other.hilbert_index()))
106 .then(self.t.cmp(&other.t))
107 }
108}
109
110#[derive(Debug, Clone)]
112pub struct Tile {
113 pub id: TileId,
114 pub time_range: TimeRange,
115 pub layers: Vec<Layer>,
116}
117
118#[derive(Debug, Clone)]
120pub struct Layer {
121 pub name: String,
122 pub extent: u32,
123 pub features: Vec<Feature>,
124}
125
126#[derive(Debug, Clone)]
128pub struct Feature {
129 pub id: u64,
130 pub geometry_type: GeometryType,
131 pub positions: Vec<Position>,
132 pub properties: std::collections::HashMap<String, Value>,
133 pub time_range: Option<TimeRange>,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq)]
138pub struct Position {
139 pub lon: f64,
140 pub lat: f64,
141}
142
143#[derive(Debug, Clone)]
145pub enum Value {
146 String(String),
147 Double(f64),
148 Float(f32),
149 Int(i64),
150 UInt(u64),
151 Bool(bool),
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_tile_id_validation() {
160 let valid = TileId::new(10, 512, 384, 1609459200000);
161 assert!(valid.validate().is_ok());
162
163 let invalid = TileId::new(10, 2048, 384, 1609459200000);
164 assert!(invalid.validate().is_err());
165 }
166
167 #[test]
168 fn test_tile_id_parent() {
169 let tile = TileId::new(10, 512, 384, 1609459200000);
170 let parent = tile.parent().unwrap();
171 assert_eq!(parent.z, 9);
172 assert_eq!(parent.x, 256);
173 assert_eq!(parent.y, 192);
174 }
175
176 #[test]
177 fn test_tile_id_children() {
178 let tile = TileId::new(10, 512, 384, 1609459200000);
179 let children = tile.children();
180 assert_eq!(children.len(), 4);
181 assert_eq!(children[0].z, 11);
182 }
183
184 #[test]
185 fn test_tile_id_ordering() {
186 let t1 = TileId::new(10, 512, 384, 1609459200000);
187 let t2 = TileId::new(10, 512, 384, 1609545600000);
188 let t3 = TileId::new(11, 1024, 768, 1609459200000);
189
190 assert!(t1 < t2); assert!(t1 < t3); }
193
194 #[test]
199 fn hilbert_index_is_distinct_for_distinct_tiles_at_high_zoom() {
200 for &z in &[14u8, 16, 18, 20, 22] {
201 let n = 1u32 << z;
202 let cx = n / 2;
204 let cy = n / 2;
205 let coords = [(cx, cy), (cx + 1, cy), (cx, cy + 1), (cx + 1, cy + 1)];
206 let indices: std::collections::HashSet<u64> = coords
207 .iter()
208 .map(|&(x, y)| TileId::new(z, x, y, 0).hilbert_index())
209 .collect();
210 assert_eq!(
211 indices.len(),
212 4,
213 "hilbert_index collided at zoom {} for adjacent tiles",
214 z
215 );
216 }
217 }
218
219 #[test]
222 fn hilbert_index_zoom_zero_is_zero() {
223 assert_eq!(TileId::new(0, 0, 0, 0).hilbert_index(), 0);
224 }
225}