Skip to main content

oxigdal_streaming/tile/
pyramid.rs

1//! Tile pyramid and matrix implementations.
2
3use super::protocol::TileCoordinate;
4use crate::error::{Result, StreamingError};
5use oxigdal_core::types::BoundingBox;
6use serde::{Deserialize, Serialize};
7
8/// Zoom level information.
9#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
10pub struct ZoomLevel {
11    /// Zoom level index
12    pub level: u8,
13
14    /// Number of tiles in X direction
15    pub matrix_width: u32,
16
17    /// Number of tiles in Y direction
18    pub matrix_height: u32,
19
20    /// Resolution in units per pixel
21    pub resolution: f64,
22
23    /// Scale denominator
24    pub scale_denominator: f64,
25}
26
27impl ZoomLevel {
28    /// Create a new zoom level.
29    pub fn new(level: u8, resolution: f64) -> Self {
30        let tiles = 1u32 << level;
31        Self {
32            level,
33            matrix_width: tiles,
34            matrix_height: tiles,
35            resolution,
36            scale_denominator: resolution * 111_319.49079327358, // meters per degree
37        }
38    }
39
40    /// Get the number of tiles at this zoom level.
41    pub fn num_tiles(&self) -> u64 {
42        (self.matrix_width as u64) * (self.matrix_height as u64)
43    }
44}
45
46/// Tile matrix definition.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct TileMatrix {
49    /// Matrix identifier
50    pub identifier: String,
51
52    /// Bounding box
53    pub bbox: BoundingBox,
54
55    /// Tile width in pixels
56    pub tile_width: u32,
57
58    /// Tile height in pixels
59    pub tile_height: u32,
60
61    /// Zoom levels
62    pub zoom_levels: Vec<ZoomLevel>,
63}
64
65impl TileMatrix {
66    /// Create a new tile matrix.
67    pub fn new(identifier: String, bbox: BoundingBox, tile_width: u32, tile_height: u32) -> Self {
68        Self {
69            identifier,
70            bbox,
71            tile_width,
72            tile_height,
73            zoom_levels: Vec::new(),
74        }
75    }
76
77    /// Add a zoom level.
78    pub fn add_zoom_level(&mut self, level: ZoomLevel) {
79        self.zoom_levels.push(level);
80    }
81
82    /// Get a zoom level by index.
83    pub fn get_zoom_level(&self, level: u8) -> Option<&ZoomLevel> {
84        self.zoom_levels.iter().find(|z| z.level == level)
85    }
86
87    /// Get the bounding box for a specific tile.
88    pub fn tile_bbox(&self, coord: &TileCoordinate) -> Result<BoundingBox> {
89        let zoom = self.get_zoom_level(coord.z).ok_or_else(|| {
90            StreamingError::InvalidOperation(format!("Zoom level {} not found", coord.z))
91        })?;
92
93        let width = self.bbox.width() / (zoom.matrix_width as f64);
94        let height = self.bbox.height() / (zoom.matrix_height as f64);
95
96        let min_x = self.bbox.min_x + (coord.x as f64) * width;
97        let max_y = self.bbox.max_y - (coord.y as f64) * height;
98        let max_x = min_x + width;
99        let min_y = max_y - height;
100
101        BoundingBox::new(min_x, min_y, max_x, max_y).map_err(StreamingError::Core)
102    }
103}
104
105/// Tile pyramid for multi-resolution data.
106pub struct TilePyramid {
107    /// Tile matrices
108    matrices: Vec<TileMatrix>,
109
110    /// Minimum zoom level
111    min_zoom: u8,
112
113    /// Maximum zoom level
114    max_zoom: u8,
115}
116
117impl TilePyramid {
118    /// Create a new tile pyramid.
119    pub fn new(min_zoom: u8, max_zoom: u8) -> Self {
120        Self {
121            matrices: Vec::new(),
122            min_zoom,
123            max_zoom,
124        }
125    }
126
127    /// Create a standard Web Mercator pyramid.
128    pub fn web_mercator(max_zoom: u8) -> Result<Self> {
129        let bbox =
130            BoundingBox::new(-180.0, -85.0511, 180.0, 85.0511).map_err(StreamingError::Core)?;
131
132        let mut pyramid = Self::new(0, max_zoom);
133
134        for z in 0..=max_zoom {
135            let mut matrix = TileMatrix::new(format!("WebMercator:{}", z), bbox, 256, 256);
136
137            let resolution = 360.0 / (256.0 * (1u32 << z) as f64);
138            matrix.add_zoom_level(ZoomLevel::new(z, resolution));
139
140            pyramid.add_matrix(matrix);
141        }
142
143        Ok(pyramid)
144    }
145
146    /// Add a tile matrix.
147    pub fn add_matrix(&mut self, matrix: TileMatrix) {
148        self.matrices.push(matrix);
149    }
150
151    /// Get a tile matrix by identifier.
152    pub fn get_matrix(&self, identifier: &str) -> Option<&TileMatrix> {
153        self.matrices.iter().find(|m| m.identifier == identifier)
154    }
155
156    /// Get all tile coordinates for a zoom level.
157    pub fn tiles_for_zoom(&self, zoom: u8) -> Vec<TileCoordinate> {
158        if zoom < self.min_zoom || zoom > self.max_zoom {
159            return Vec::new();
160        }
161
162        let num_tiles = 1u32 << zoom;
163        let mut tiles = Vec::with_capacity((num_tiles * num_tiles) as usize);
164
165        for y in 0..num_tiles {
166            for x in 0..num_tiles {
167                tiles.push(TileCoordinate::new(zoom, x, y));
168            }
169        }
170
171        tiles
172    }
173
174    /// Get all tile coordinates that intersect a bounding box.
175    pub fn tiles_for_bbox(&self, bbox: &BoundingBox, zoom: u8) -> Result<Vec<TileCoordinate>> {
176        if zoom < self.min_zoom || zoom > self.max_zoom {
177            return Ok(Vec::new());
178        }
179
180        let matrix = self.matrices.get(zoom as usize).ok_or_else(|| {
181            StreamingError::InvalidOperation(format!("Matrix for zoom {} not found", zoom))
182        })?;
183
184        let zoom_level = matrix.get_zoom_level(zoom).ok_or_else(|| {
185            StreamingError::InvalidOperation(format!("Zoom level {} not found", zoom))
186        })?;
187
188        let width = matrix.bbox.width() / (zoom_level.matrix_width as f64);
189        let height = matrix.bbox.height() / (zoom_level.matrix_height as f64);
190
191        let min_x = ((bbox.min_x - matrix.bbox.min_x) / width).floor().max(0.0) as u32;
192        let max_x = ((bbox.max_x - matrix.bbox.min_x) / width)
193            .ceil()
194            .min(zoom_level.matrix_width as f64) as u32;
195        let min_y = ((matrix.bbox.max_y - bbox.max_y) / height).floor().max(0.0) as u32;
196        let max_y = ((matrix.bbox.max_y - bbox.min_y) / height)
197            .ceil()
198            .min(zoom_level.matrix_height as f64) as u32;
199
200        let mut tiles = Vec::new();
201        for y in min_y..max_y {
202            for x in min_x..max_x {
203                tiles.push(TileCoordinate::new(zoom, x, y));
204            }
205        }
206
207        Ok(tiles)
208    }
209
210    /// Get the zoom levels.
211    pub fn zoom_levels(&self) -> (u8, u8) {
212        (self.min_zoom, self.max_zoom)
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn test_zoom_level() {
222        let zoom = ZoomLevel::new(10, 0.0001);
223        assert_eq!(zoom.level, 10);
224        assert_eq!(zoom.matrix_width, 1024);
225        assert_eq!(zoom.matrix_height, 1024);
226        assert_eq!(zoom.num_tiles(), 1024 * 1024);
227    }
228
229    #[test]
230    fn test_web_mercator_pyramid() {
231        let pyramid = TilePyramid::web_mercator(18);
232        assert!(pyramid.is_ok());
233
234        if let Ok(pyramid) = pyramid {
235            assert_eq!(pyramid.zoom_levels(), (0, 18));
236            assert_eq!(pyramid.matrices.len(), 19);
237        }
238    }
239
240    #[test]
241    fn test_tiles_for_zoom() {
242        let pyramid = TilePyramid::new(0, 10);
243        let tiles = pyramid.tiles_for_zoom(2);
244        assert_eq!(tiles.len(), 16); // 4x4 = 16 tiles at zoom 2
245    }
246}