gistools/readers/s2tiles/
mod.rs

1use crate::{
2    data_structures::Cache,
3    parsers::{Buffer, Reader},
4    util::{CompressionFormat, decompress_data},
5};
6use alloc::{collections::BTreeMap, vec, vec::Vec};
7use s2_tilejson::Metadata;
8use s2json::Face;
9
10/// A directory consists of an offset and a length pointing to a node or a leaf.
11/// The maximum value for a 6-byte offset is `281,474,976,710,655`
12/// This is large enough to address 281 TB of byte-indexed data.
13/// - Offset: 6 bytes
14/// - Length: 4 bytes
15#[derive(Debug, Clone, Copy, PartialEq)]
16struct Directory {
17    pub offset: u64,
18    pub length: u32,
19}
20
21const NODE_SIZE: usize = 10; // [offset, length] => [6 bytes, 4 bytes]
22const DIR_SIZE: usize = 1_365 * NODE_SIZE; // (13_650) -> 6 levels, the 6th level has both node and leaf (1+4+16+64+256+1024)*2 => (1365)+1365 => 2_730
23const METADATA_SIZE: usize = 131_072; // 131,072 bytes is 128kB. It is assumed the map metadata AND the S2Tile format metadata is less than 128kB
24const ROOT_DIR_SIZE: usize = DIR_SIZE * 6; // 27_300 * 6 = 163_800
25const ROOT_SIZE: usize = METADATA_SIZE + ROOT_DIR_SIZE;
26// assuming all tiles exist for every face from 0->30 the max leafs to reach depth of 30 is 5
27// root: 6sides * 27_300 bytes/dir = (163_800 bytes)
28// all leafs at 6: 1024 * 6sides * 27_300bytes/dir (0.167731 GB)
29// al leafs at 12: 524_288 * 6sides * 27_300bytes/dir (85.8783744 GB) - obviously most of this is water
30
31/// # S2 Tiles Reader
32///
33/// ## Description
34///
35/// An S2 Tile Reader to store tile and metadata in a cloud optimized format. Similar to PMTiles
36/// but simplified to have as few features as possible.
37///
38/// Reads either a Web Mercator tile or an S2 tile to the folder location given its (zoom, x, y) or (face, zoom, x, y) coordinates.
39///
40/// Reads data via the [S2Tiles specification](https://github.com/Open-S2/s2tiles/blob/master/s2tiles-spec/1.0.0/README.md).
41///
42/// ## Usage
43///
44/// S2TilesReader utilizes any struct that implements the [`Reader`] trait.
45/// Options are [`crate::parsers::BufferReader`], [`crate::parsers::FileReader`], [`crate::parsers::MMapReader`], and [`crate::parsers::FetchReader`].
46///
47/// The methods you have access to:
48/// - [`S2TilesReader::new`]: Create a new S2TilesReader
49/// - [`S2TilesReader::get_metadata`]: Get the metadata of the archive
50/// - [`S2TilesReader::has_tile_wm`]: Check if a WM tile exists in the archive
51/// - [`S2TilesReader::has_tile_s2`]: Check if an S2 tile exists in the archive
52/// - [`S2TilesReader::get_tile_wm`]: Get the bytes of the tile at the given (zoom, x, y) coordinates
53/// - [`S2TilesReader::get_tile_s2`]: Get the bytes of the tile at the given (face, zoom, x, y) coordinates
54///
55/// ```rust
56/// use gistools::{parsers::FileReader, readers::S2TilesReader};
57/// use std::path::PathBuf;
58///
59/// let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
60///     .join("tests/writers/fixtures/example.s2tiles");
61/// let file_reader = FileReader::new(path).unwrap();
62/// let mut reader = S2TilesReader::new(file_reader, None);
63///
64/// smol::block_on(async {
65///
66/// // get the metadata
67/// let metadata = reader.get_metadata().await;
68///
69/// // S2 specific functions
70/// assert!(reader.has_tile_s2(0.into(), 0, 0, 0).await);
71/// let tile = reader.get_tile_s2(0.into(), 0, 0, 0).await;
72///
73/// // WM functions
74/// assert!(reader.has_tile_wm(0, 0, 0).await);
75/// let tile = reader.get_tile_wm(0, 0, 0).await;
76///
77/// });
78/// ```
79///
80/// ## Links
81/// - https://github.com/Open-S2/s2tiles/blob/master/s2tiles-spec/1.0.0/README.md
82#[derive(Debug)]
83pub struct S2TilesReader<R: Reader> {
84    is_setup: bool,
85    version: u16,
86    maxzoom: u8,
87    compression: CompressionFormat,
88    metadata: Option<Metadata>,
89    root_dir: BTreeMap<u8, Buffer>,
90    dir_cache: Cache<u64, Buffer>,
91    reader: R,
92}
93impl<R: Reader> S2TilesReader<R> {
94    /// Create a new S2TilesReader
95    ///
96    /// ## Parameters
97    /// - `reader` - the input reader to parse from
98    /// - `max_size` - the max size of the cache before dumping old data. Defaults to 20.
99    pub fn new(reader: R, max_size: Option<usize>) -> Self {
100        Self {
101            is_setup: false,
102            version: 1,
103            maxzoom: 0,
104            compression: CompressionFormat::Gzip,
105            metadata: None,
106            root_dir: BTreeMap::new(),
107            dir_cache: Cache::new(max_size.unwrap_or(20), None),
108            reader,
109        }
110    }
111
112    /// Get the metadata of the archive
113    ///
114    /// ## Returns
115    /// The metadata of the archive
116    pub async fn get_metadata(&mut self) -> Metadata {
117        if let Some(metadata) = &self.metadata {
118            return metadata.clone();
119        }
120        self.setup().await;
121        self.metadata.clone().unwrap()
122    }
123
124    /// Check if a WM tile exists in the archive
125    ///
126    /// ## Parameters
127    /// - `zoom`: the zoom level of the tile
128    /// - `x`: the x coordinate of the tile
129    /// - `y`: the y coordinate of the tile
130    ///
131    /// ## Returns
132    /// True if the tile exists in the archive
133    pub async fn has_tile_wm(&mut self, zoom: u8, x: u32, y: u32) -> bool {
134        self.setup().await;
135        self.has_tile_s2(0.into(), zoom, x, y).await
136    }
137
138    /// Check if an S2 tile exists in the archive
139    ///
140    /// ## Parameters
141    /// - `face`: the Open S2 projection face
142    /// - `zoom`: the zoom level of the tile
143    /// - `x`: the x coordinate of the tile
144    /// - `y`: the y coordinate of the tile
145    ///
146    /// ## Returns
147    /// True if the tile exists in the archive
148    pub async fn has_tile_s2(&mut self, face: Face, zoom: u8, x: u32, y: u32) -> bool {
149        self.setup().await;
150        // pull in the correct face's directory
151        let dir = self.root_dir.get(&(face as u8)).cloned().unwrap();
152        // now we walk to the next directory as necessary
153        let node = self.walk(dir, zoom, x, y).await; // [offset, length]
154        if let Some(node) = node {
155            let Directory { offset, length } = node;
156            offset != 0 && length != 0
157        } else {
158            false
159        }
160    }
161
162    /// Get the bytes of the tile at the given (zoom, x, y) coordinates
163    ///
164    /// ## Parameters
165    /// - `zoom`: the zoom level of the tile
166    /// - `x`: the x coordinate of the tile
167    /// - `y`: the y coordinate of the tile
168    ///
169    /// ## Returns
170    /// The bytes of the tile at the given (z, x, y) coordinates, or undefined if the tile
171    /// does not exist in the archive.
172    pub async fn get_tile_wm(&mut self, zoom: u8, x: u32, y: u32) -> Option<Vec<u8>> {
173        self.setup().await;
174        self.get_tile_s2(0.into(), zoom, x, y).await
175    }
176
177    /// Get the bytes of the tile at the given (face, zoom, x, y) coordinates
178    ///
179    /// ## Parameters
180    /// - `face`: the Open S2 projection face
181    /// - `zoom`: the zoom level of the tile
182    /// - `x`: the x coordinate of the tile
183    /// - `y`: the y coordinate of the tile
184    ///
185    /// ## Returns
186    /// The bytes of the tile at the given (face, zoom, x, y) coordinates, or undefined if
187    /// the tile does not exist in the archive.
188    pub async fn get_tile_s2(&mut self, face: Face, zoom: u8, x: u32, y: u32) -> Option<Vec<u8>> {
189        self.setup().await;
190
191        // pull in the correct face's directory
192        let dir = self.root_dir.get(&(face as u8)).cloned().unwrap();
193        // now we walk to the next directory as necessary
194        let node = self.walk(dir, zoom, x, y).await; // [offset, length]
195        if let Some(node) = node {
196            let Directory { offset, length } = node;
197
198            // we found the vector file, let's send the details off to the tile worker
199            let data = self.get_range(offset, length as u64).await;
200            Some(decompress_data(&data, self.compression).unwrap())
201        } else {
202            None
203        }
204    }
205
206    /// given position and level, find the tile offset and length
207    ///
208    /// ## Parameters
209    /// - `dir`: the directory to walk
210    /// - `zoom`: the zoom level of the tile
211    /// - `x`: the x coordinate of the tile
212    /// - `y`: the y coordinate of the tile
213    ///
214    /// ## Returns
215    /// The offset and length of the tile if it exists
216    async fn walk(&mut self, mut dir: Buffer, zoom: u8, x: u32, y: u32) -> Option<Directory> {
217        let mut path = get_s2_tile_path(zoom, x, y);
218        let mut offset = 0;
219        let mut length = 0;
220
221        // walk the tree if past zoom 0
222        while !path.is_empty() {
223            // grab position
224            let node_pos = path.remove(0) as usize * NODE_SIZE;
225            // set
226            offset = read_uint_48le(&mut dir, Some(node_pos as usize));
227            length = dir.get_u32_at(node_pos + 6);
228            if length == 0 {
229                return None;
230            }
231            // if we are still walking, grab the new directory
232            if !path.is_empty() {
233                // corner case: if maxzoom matches the zoom and is divisible by 5, the leaf is actually a node
234                if self.maxzoom.is_multiple_of(5)
235                    && zoom == self.maxzoom
236                    && path.len() == 1
237                    && path[0] == 0
238                {
239                    return Some(Directory { offset, length });
240                }
241                // otherwise fetch the directory
242                let next_dir = self.get_dir(offset, length).await;
243                dir = next_dir;
244            }
245        }
246
247        if length == 0 { None } else { Some(Directory { offset, length }) }
248    }
249
250    /// get a directory given an offset and length
251    ///
252    /// ## Parameters
253    /// - `offset`: the offset
254    /// - `length`: the length
255    ///
256    /// ## Returns
257    /// The directory
258    async fn get_dir(&mut self, offset: u64, length: u32) -> Buffer {
259        if let Some(dir) = self.dir_cache.get(&offset) {
260            dir.clone()
261        } else {
262            let data = self.get_range(offset, length as u64).await;
263            let dir = Buffer::new(data);
264            self.dir_cache.set(offset, dir.clone());
265            dir
266        }
267    }
268
269    /// Setup the reader
270    async fn setup(&mut self) {
271        if self.is_setup {
272            return;
273        }
274        self.is_setup = true;
275        // fetch the metadata
276        let data = self.get_range(0, ROOT_SIZE as u64).await;
277        // prep a data view, store in header, build metadata
278        let mut dv = Buffer::new(data.clone());
279        if dv.get_u16_at(0) != 12883 {
280            // the first two bytes are S and 2, we validate
281            panic!("Bad metadata");
282        }
283        // parse the version, maxzoom, and compression
284        self.version = dv.get_u16_at(2);
285        self.maxzoom = dv.get_u8_at(4);
286        self.compression = CompressionFormat::from(dv.get_u8_at(5));
287        // parse the JSON metadata length and offset
288        let m_l = dv.get_u32_at(6);
289        if m_l == 0 {
290            // if the metadata is empty, we failed
291            panic!("Failed to extrapolate metadata");
292        }
293        let meta_data =
294            decompress_data(&data[10..(10 + (m_l as usize))], self.compression).unwrap();
295        self.metadata = Some(serde_json::from_slice(&meta_data).unwrap());
296        // create root directories
297        for face in [0, 1, 2, 3, 4, 5] {
298            let start = METADATA_SIZE + (face as usize) * DIR_SIZE;
299            self.root_dir.insert(face, Buffer::new(data[start..(start + DIR_SIZE)].to_vec()));
300        }
301    }
302
303    /// Get a range of bytes given an offset and length
304    async fn get_range(&mut self, offset: u64, length: u64) -> Vec<u8> {
305        let len = self.reader.len();
306        if len != 0 {
307            // This is not a FetchReader
308            let end = u64::min(len, offset + length);
309            self.reader.slice(Some(offset), Some(end))
310        } else {
311            self.reader.get_slice(offset, Some(length)).await
312        }
313    }
314}
315
316/// read a 48 bit number
317///
318/// ## Parameters
319/// - `buffer`: the buffer
320/// - `offset`: the offset
321///
322/// ## Returns
323/// The number
324fn read_uint_48le(buffer: &mut Buffer, offset: Option<usize>) -> u64 {
325    let offset = offset.unwrap_or(0);
326    buffer.get_u32_at(offset + 2) as u64 * (1 << 16) + buffer.get_u16_at(offset) as u64
327}
328
329/// Get the path to a tile
330///
331/// ## Parameters
332/// - `zoom`: the zoom
333/// - `x`: the x
334/// - `y`: the y
335///
336/// ## Returns
337/// The path as a collection of offsets pointing to the tile Node in the directory
338pub fn get_s2_tile_path(mut zoom: u8, mut x: u32, mut y: u32) -> Vec<u64> {
339    let mut path = vec![];
340
341    while zoom >= 5 {
342        path.push((5, x & 31, y & 31));
343        x >>= 5;
344        y >>= 5;
345        zoom = zoom.saturating_sub(5);
346    }
347    path.push((zoom, x, y));
348
349    path.into_iter()
350        .map(|(zoom, x, y)| {
351            let val = (y as u64) * ((1 << zoom) as u64) + (x as u64);
352            let sum: u64 = (0..zoom).map(|z| (1 << z) * (1 << z)).sum();
353            val + sum
354        })
355        .collect()
356}