Skip to main content

quantized_mesh/
types.rs

1//! Type definitions for quantized-mesh format.
2
3/// Maximum value for quantized coordinates (2^15 - 1).
4pub const QUANTIZED_MAX: u16 = 32767;
5
6/// Quantized vertex data.
7///
8/// Each coordinate is quantized to 0-32767 range:
9/// - `u`: horizontal position (0 = west edge, 32767 = east edge)
10/// - `v`: vertical position (0 = south edge, 32767 = north edge)
11/// - `height`: elevation (0 = min height, 32767 = max height)
12#[derive(Debug, Clone, Default)]
13pub struct QuantizedVertices {
14    /// Horizontal coordinates (0 = west, 32767 = east)
15    pub u: Vec<u16>,
16    /// Vertical coordinates (0 = south, 32767 = north)
17    pub v: Vec<u16>,
18    /// Height values (0 = min, 32767 = max)
19    pub height: Vec<u16>,
20}
21
22impl QuantizedVertices {
23    /// Create new empty vertices.
24    pub fn new() -> Self {
25        Self::default()
26    }
27
28    /// Create vertices with pre-allocated capacity.
29    pub fn with_capacity(capacity: usize) -> Self {
30        Self {
31            u: Vec::with_capacity(capacity),
32            v: Vec::with_capacity(capacity),
33            height: Vec::with_capacity(capacity),
34        }
35    }
36
37    /// Get vertex count.
38    pub fn len(&self) -> usize {
39        self.u.len()
40    }
41
42    /// Check if empty.
43    pub fn is_empty(&self) -> bool {
44        self.u.is_empty()
45    }
46
47    /// Add a vertex.
48    pub fn push(&mut self, u: u16, v: u16, height: u16) {
49        self.u.push(u);
50        self.v.push(v);
51        self.height.push(height);
52    }
53}
54
55/// Edge indices for skirt generation.
56///
57/// Contains indices of vertices on each edge of the tile,
58/// sorted by their position along the edge.
59#[derive(Debug, Clone, Default)]
60pub struct EdgeIndices {
61    /// Indices of vertices on the west edge (sorted by v, ascending)
62    pub west: Vec<u32>,
63    /// Indices of vertices on the south edge (sorted by u, ascending)
64    pub south: Vec<u32>,
65    /// Indices of vertices on the east edge (sorted by v, ascending)
66    pub east: Vec<u32>,
67    /// Indices of vertices on the north edge (sorted by u, ascending)
68    pub north: Vec<u32>,
69}
70
71impl EdgeIndices {
72    /// Create new empty edge indices.
73    pub fn new() -> Self {
74        Self::default()
75    }
76
77    /// Extract edge indices from vertex data.
78    ///
79    /// Identifies vertices on tile boundaries (u=0, u=32767, v=0, v=32767)
80    /// and sorts them appropriately.
81    pub fn from_vertices(vertices: &QuantizedVertices) -> Self {
82        let mut west = Vec::new();
83        let mut south = Vec::new();
84        let mut east = Vec::new();
85        let mut north = Vec::new();
86
87        for (i, (&u, &v)) in vertices.u.iter().zip(vertices.v.iter()).enumerate() {
88            let idx = i as u32;
89            if u == 0 {
90                west.push((idx, v));
91            }
92            if u == QUANTIZED_MAX {
93                east.push((idx, v));
94            }
95            if v == 0 {
96                south.push((idx, u));
97            }
98            if v == QUANTIZED_MAX {
99                north.push((idx, u));
100            }
101        }
102
103        // Sort by position along edge
104        west.sort_by_key(|&(_, v)| v);
105        east.sort_by_key(|&(_, v)| v);
106        south.sort_by_key(|&(_, u)| u);
107        north.sort_by_key(|&(_, u)| u);
108
109        Self {
110            west: west.into_iter().map(|(idx, _)| idx).collect(),
111            south: south.into_iter().map(|(idx, _)| idx).collect(),
112            east: east.into_iter().map(|(idx, _)| idx).collect(),
113            north: north.into_iter().map(|(idx, _)| idx).collect(),
114        }
115    }
116}
117
118/// Extension IDs for quantized-mesh format.
119#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120#[repr(u8)]
121pub enum ExtensionId {
122    /// Oct-encoded per-vertex normals (2 bytes per vertex)
123    OctEncodedVertexNormals = 1,
124    /// Water mask (1 byte or 256x256 bytes)
125    WaterMask = 2,
126    /// JSON metadata
127    Metadata = 4,
128}
129
130/// Water mask data.
131#[derive(Debug, Clone)]
132pub enum WaterMask {
133    /// Uniform mask: single byte (0 = all land, 255 = all water)
134    Uniform(u8),
135    /// Grid mask: 256x256 bytes
136    Grid(Box<[u8; 256 * 256]>),
137}
138
139impl Default for WaterMask {
140    fn default() -> Self {
141        Self::Uniform(0) // All land
142    }
143}
144
145impl WaterMask {
146    /// Create water mask from 256x256 grayscale data.
147    ///
148    /// If all values are the same, returns `Uniform`.
149    /// Otherwise, returns `Grid` with the 256x256 data.
150    pub fn from_data(data: &[u8]) -> Self {
151        if data.len() < 256 * 256 {
152            return Self::Uniform(0); // Fallback to all land
153        }
154
155        // Check if all values are the same
156        if let Some(&first) = data.first()
157            && data[..256 * 256].iter().all(|&v| v == first)
158        {
159            return Self::Uniform(first);
160        }
161
162        // Create grid
163        let mut grid = Box::new([0u8; 256 * 256]);
164        grid.copy_from_slice(&data[..256 * 256]);
165        Self::Grid(grid)
166    }
167}
168
169/// Tile availability range for metadata extension.
170#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct AvailableRange {
173    /// Starting X coordinate (inclusive)
174    pub start_x: u32,
175    /// Ending X coordinate (inclusive)
176    pub end_x: u32,
177    /// Starting Y coordinate (inclusive)
178    pub start_y: u32,
179    /// Ending Y coordinate (inclusive)
180    pub end_y: u32,
181}
182
183impl AvailableRange {
184    /// Create a new availability range.
185    pub fn new(start_x: u32, end_x: u32, start_y: u32, end_y: u32) -> Self {
186        Self {
187            start_x,
188            end_x,
189            start_y,
190            end_y,
191        }
192    }
193
194    /// Create a range covering the full level (global-geodetic).
195    ///
196    /// For Cesium's global-geodetic TMS, at zoom level z:
197    /// - X: 0 to 2^(z+1) - 1
198    /// - Y: 0 to 2^z - 1
199    pub fn full_level_geodetic(zoom: u8) -> Self {
200        let max_x = (1u32 << (zoom + 1)) - 1;
201        let max_y = (1u32 << zoom) - 1;
202        Self {
203            start_x: 0,
204            end_x: max_x,
205            start_y: 0,
206            end_y: max_y,
207        }
208    }
209}
210
211/// Metadata for quantized-mesh extension.
212///
213/// Contains child tile availability information.
214#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
215pub struct TileMetadata {
216    /// Available tile ranges by zoom level offset from current tile.
217    /// Level 0 is one level below current tile (children).
218    /// Level 1 is two levels below (grandchildren), etc.
219    #[serde(default)]
220    pub available: Vec<Vec<AvailableRange>>,
221}
222
223impl TileMetadata {
224    /// Create new empty metadata.
225    pub fn new() -> Self {
226        Self {
227            available: Vec::new(),
228        }
229    }
230
231    /// Create metadata indicating all children are available for `levels` more zoom levels.
232    ///
233    /// This is useful for tiles where we know all descendants exist up to a certain depth.
234    #[deprecated(note = "Use for_tile instead, which computes correct child tile ranges")]
235    pub fn all_available(current_zoom: u8, max_zoom: u8) -> Self {
236        let mut available = Vec::new();
237
238        // For each child level from current+1 to max_zoom
239        for child_zoom in (current_zoom + 1)..=max_zoom {
240            available.push(vec![AvailableRange::full_level_geodetic(child_zoom)]);
241        }
242
243        Self { available }
244    }
245
246    /// Create metadata for a specific tile indicating all descendants are available.
247    ///
248    /// # Arguments
249    /// * `tile_x` - X coordinate of the current tile
250    /// * `tile_y` - Y coordinate of the current tile
251    /// * `current_zoom` - Zoom level of the current tile
252    /// * `max_zoom` - Maximum zoom level to include in metadata
253    ///
254    /// The metadata contains availability ranges for descendant tiles relative to this tile.
255    /// For geodetic TMS, each tile at zoom z has 4 children at zoom z+1.
256    pub fn for_tile(tile_x: u32, tile_y: u32, current_zoom: u8, max_zoom: u8) -> Self {
257        let mut available = Vec::new();
258
259        // For each descendant level from current+1 to max_zoom
260        for child_zoom in (current_zoom + 1)..=max_zoom {
261            // Calculate how many levels deep we are from the current tile
262            let levels_deep = child_zoom - current_zoom;
263            let scale = 1u32 << levels_deep; // 2^levels_deep
264
265            // Calculate the range of descendant tiles
266            // At each level, the tile range doubles
267            let start_x = tile_x * scale;
268            let start_y = tile_y * scale;
269            let end_x = start_x + scale - 1;
270            let end_y = start_y + scale - 1;
271
272            available.push(vec![AvailableRange::new(start_x, end_x, start_y, end_y)]);
273        }
274
275        Self { available }
276    }
277}
278
279impl Default for TileMetadata {
280    fn default() -> Self {
281        Self::new()
282    }
283}
284
285/// Tile bounds in geographic coordinates (degrees).
286#[derive(Debug, Clone, Copy)]
287pub struct TileBounds {
288    /// Western longitude (degrees)
289    pub west: f64,
290    /// Southern latitude (degrees)
291    pub south: f64,
292    /// Eastern longitude (degrees)
293    pub east: f64,
294    /// Northern latitude (degrees)
295    pub north: f64,
296}
297
298impl TileBounds {
299    /// Create new tile bounds.
300    pub fn new(west: f64, south: f64, east: f64, north: f64) -> Self {
301        Self {
302            west,
303            south,
304            east,
305            north,
306        }
307    }
308
309    /// Width in degrees.
310    pub fn width(&self) -> f64 {
311        self.east - self.west
312    }
313
314    /// Height in degrees.
315    pub fn height(&self) -> f64 {
316        self.north - self.south
317    }
318
319    /// Center longitude.
320    pub fn center_lon(&self) -> f64 {
321        (self.west + self.east) / 2.0
322    }
323
324    /// Center latitude.
325    pub fn center_lat(&self) -> f64 {
326        (self.south + self.north) / 2.0
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn test_quantized_vertices() {
336        let mut vertices = QuantizedVertices::new();
337        assert!(vertices.is_empty());
338
339        vertices.push(0, 0, 100);
340        vertices.push(QUANTIZED_MAX, QUANTIZED_MAX, 200);
341
342        assert_eq!(vertices.len(), 2);
343        assert_eq!(vertices.u, vec![0, QUANTIZED_MAX]);
344        assert_eq!(vertices.v, vec![0, QUANTIZED_MAX]);
345        assert_eq!(vertices.height, vec![100, 200]);
346    }
347
348    #[test]
349    fn test_edge_indices_extraction() {
350        // Create a simple 4-vertex grid
351        let vertices = QuantizedVertices {
352            u: vec![0, QUANTIZED_MAX, 0, QUANTIZED_MAX],
353            v: vec![0, 0, QUANTIZED_MAX, QUANTIZED_MAX],
354            height: vec![0, 0, 0, 0],
355        };
356
357        let edges = EdgeIndices::from_vertices(&vertices);
358
359        // West edge: vertices 0, 2 (sorted by v)
360        assert_eq!(edges.west, vec![0, 2]);
361        // East edge: vertices 1, 3 (sorted by v)
362        assert_eq!(edges.east, vec![1, 3]);
363        // South edge: vertices 0, 1 (sorted by u)
364        assert_eq!(edges.south, vec![0, 1]);
365        // North edge: vertices 2, 3 (sorted by u)
366        assert_eq!(edges.north, vec![2, 3]);
367    }
368
369    #[test]
370    fn test_tile_bounds() {
371        let bounds = TileBounds::new(-180.0, -90.0, 180.0, 90.0);
372
373        assert_eq!(bounds.width(), 360.0);
374        assert_eq!(bounds.height(), 180.0);
375        assert_eq!(bounds.center_lon(), 0.0);
376        assert_eq!(bounds.center_lat(), 0.0);
377    }
378
379    #[test]
380    fn tile_metadata_decodes_without_available() {
381        // Cesium ION's metadata extension routinely omits `available` and carries
382        // tileset-specific fields instead. Decoding must not fail on that shape.
383        let json = r#"{"geometricError":1234.5}"#;
384        let m: TileMetadata = serde_json::from_str(json).expect("lenient decode");
385        assert!(m.available.is_empty());
386    }
387}