gistools/writers/s2tiles/
mod.rs

1use crate::{
2    geometry::S2CellId,
3    parsers::{Buffer, Writer},
4    util::{CompressionFormat, compress_data},
5    writers::TileWriter,
6};
7use alloc::{vec, vec::Vec};
8use s2_tilejson::Metadata;
9use s2json::Face;
10
11type Node = Directory;
12
13/// A directory consists of an offset and a length pointing to a node or a leaf.
14/// The maximum value for a 6-byte offset is `281,474,976,710,655`
15/// This is large enough to address 281 TB of byte-indexed data.
16/// - Offset: 6 bytes
17/// - Length: 4 bytes
18#[derive(Debug, Clone, Copy, PartialEq)]
19struct Directory {
20    pub offset: u64,
21    pub length: u32,
22}
23
24const NODE_SIZE: usize = 10; // [offset, length] => [6 bytes, 4 bytes]
25const 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
26const 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
27const ROOT_DIR_SIZE: usize = DIR_SIZE * 6; // 27_300 * 6 = 163_800
28const ROOT_SIZE: usize = METADATA_SIZE + ROOT_DIR_SIZE;
29// assuming all tiles exist for every face from 0->30 the max leafs to reach depth of 30 is 5
30// root: 6sides * 27_300bytes/dir = (163_800 bytes)
31// all leafs at 6: 1024 * 6sides * 27_300bytes/dir (0.167731 GB)
32// al leafs at 12: 524_288 * 6sides * 27_300bytes/dir (85.8783744 GB) - obviously most of this is water
33
34/// # S2 Tiles Writer
35///
36/// ## Description
37///
38/// An S2 Tile Writer to store tile and metadata in a cloud optimized format. Similar to PMTiles
39/// but simplified to have as few features as possible.
40///
41/// Writes either a Web Mercator tile or an S2 tile to the folder location given its (zoom, x, y) or (face, zoom, x, y) coordinates.
42///
43/// ## Usage
44///
45/// This struct uses the [`TileWriter`] trait.
46///
47/// Takes a [`Writer`] input to write data to.
48///
49/// The methods you have access to:
50/// - [`S2TilesWriter::new`]: Create a new S2TilesWriter
51/// - [`S2TilesWriter::write_tile_wm`]: Write a Web Mercator tile to the folder location given its (zoom, x, y) coordinates.
52/// - [`S2TilesWriter::write_tile_s2`]: Write a S2 tile to the folder location given its (face, zoom, x, y) coordinates.
53/// - [`S2TilesWriter::commit`]: Write the metadata to the folder location.
54/// - [`S2TilesWriter::writer`]: Borrow the writer mutably if needed
55/// - [`S2TilesWriter::put_tile_fzxy`]: Write a tile to the S2Tiles file given its (face, zoom, x, y) coordinates.
56/// - [`S2TilesWriter::put_tile`]: Inserts a tile into the S2Tiles store.
57///
58/// ```rust
59/// use gistools::{
60///     parsers::{BufferWriter, Writer},
61///     util::CompressionFormat,
62///     writers::{S2TilesWriter, TileWriter},
63/// };
64/// use s2_tilejson::Metadata;
65///
66/// let local_writer = BufferWriter::default();
67/// let mut s2tiles_writer = S2TilesWriter::new(local_writer, 9, CompressionFormat::None);
68///
69/// let s = String::from("hello world");
70/// let buf = s.as_bytes().to_vec();
71///
72/// // write data in an wm tile
73/// s2tiles_writer.write_tile_wm(0, 0, 0, buf.clone());
74/// // or write data in an s2 tile
75/// s2tiles_writer.write_tile_s2(0.into(), 0, 0, 0, buf.clone());
76///
77/// // finish
78/// s2tiles_writer.commit(Metadata::default(), None);
79/// ```
80///
81/// ## Links
82/// - https://github.com/Open-S2/s2tiles/blob/master/s2tiles-spec/1.0.0/README.md
83#[derive(Debug)]
84pub struct S2TilesWriter<W: Writer> {
85    offset: u64,
86    version: u16,
87    maxzoom: u8,
88    writer: W,
89    compression: CompressionFormat,
90}
91impl<W: Writer> TileWriter for S2TilesWriter<W> {
92    fn write_tile_wm(&mut self, zoom: u8, x: u32, y: u32, data: Vec<u8>) {
93        self.put_tile_fzxy(0.into(), zoom, x, y, data);
94    }
95    fn write_tile_s2(&mut self, face: Face, zoom: u8, x: u32, y: u32, data: Vec<u8>) {
96        self.put_tile_fzxy(face, zoom, x, y, data);
97    }
98    /// Finish writing by building the header with root and leaf directories
99    fn commit(&mut self, metadata: Metadata, tile_compression: Option<CompressionFormat>) {
100        let compression = tile_compression.unwrap_or(self.compression);
101        // set the ID, version, and compression type
102        let mut data = Buffer::new(vec![10]);
103        // Store format metadata
104        data.set_u8_at(0, 83); // S
105        data.set_u8_at(1, 50); // 2
106        data.set_u16_at(2, self.version);
107        data.set_u8_at(4, self.maxzoom);
108        data.set_u8_at(5, compression as u8);
109        // store the metadata's length then actual data
110        let mut meta_buffer = serde_json::to_vec(&metadata).unwrap();
111        meta_buffer = compress_data(meta_buffer, compression).unwrap();
112        if meta_buffer.len() > METADATA_SIZE - 10 {
113            panic!("Metadata too large for S2Tiles");
114        }
115        data.set_u32_at(6, meta_buffer.len() as u32);
116        // store the format metadata and lengthen the writer to fill METADATA_SIZE. Then store the map metadata
117        self.writer.write(&data.take(), 0);
118        self.writer.write(&meta_buffer, 10);
119    }
120}
121impl<W: Writer> S2TilesWriter<W> {
122    /// given a compression scheme, maxzoom, and a data writer, create an instance to
123    /// start storing tiles and metadata.
124    /// Compression describes how both the tiles and the metadata are compressed
125    pub fn new(writer: W, maxzoom: u8, compression: CompressionFormat) -> Self {
126        let mut writer =
127            S2TilesWriter { offset: ROOT_SIZE as u64, version: 1, maxzoom, compression, writer };
128        writer.writer.append(&vec![0u8; ROOT_SIZE]);
129        writer
130    }
131
132    /// Borrow the writer
133    pub fn writer(&mut self) -> &mut W {
134        &mut self.writer
135    }
136
137    /// Write a tile to the S2Tiles file given its (face, zoom, x, y) coordinates.
138    ///
139    /// ## Parameters
140    /// - `face`: the Open S2 projection face
141    /// - `zoom`: the zoom level
142    /// - `x`: the tile X coordinate
143    /// - `y`: the tile Y coordinate
144    /// - `data`: the tile data to store
145    pub fn put_tile_fzxy(&mut self, face: Face, zoom: u8, x: u32, y: u32, data: Vec<u8>) {
146        let id = S2CellId::from_face_ij(face.into(), x, y, Some(zoom));
147        self.put_tile(id, data);
148    }
149
150    /// Inserts a tile into the S2Tiles store.
151    ///
152    /// ## Parameters
153    /// - `id`: the tile ID
154    /// - `data`: the tile data
155    pub fn put_tile(&mut self, id: S2CellId, data: Vec<u8>) {
156        // const length = data.byteLength;
157        let length = data.len() as u32;
158        // first create node, setting offset
159        let node = Node { offset: self.offset, length };
160        self.writer.append(&data);
161        self.offset += length as u64;
162        // store node in the correct directory
163        self.put_node_in_dir(id, node);
164    }
165
166    /// Work our way towards the correct parent directory.
167    /// If parent directory does not exists, we create it.
168    ///
169    /// ## Parameters
170    /// - `id`: the s2cellID
171    /// - `node`: the node
172    fn put_node_in_dir(&mut self, id: S2CellId, node: Node) {
173        // use the s2cellID and move the cursor
174        let cursor = self.walk(id);
175        // finally store
176        self.write_node(cursor, node);
177    }
178
179    /// given position and level, explain where to adust the cursor to file
180    ///
181    /// ## Parameters
182    /// - `id`: the s2cellID
183    ///
184    /// ## Returns
185    /// The new cursor position
186    fn walk(&mut self, id: S2CellId) -> u64 {
187        // grab properties
188        let (face, level, i, j) = id.to_face_ij();
189
190        let mut cursor = (METADATA_SIZE + DIR_SIZE * face as usize) as u64;
191        let mut leaf;
192        let mut depth = 0;
193        let mut path = get_path(level, i, j);
194
195        while !path.is_empty() {
196            // grab movement
197            let shift = path.remove(0);
198            depth += 1;
199            // update cursor position
200            cursor += shift * NODE_SIZE as u64;
201
202            if !path.is_empty() {
203                // if we hit a leaf, adjust nodePos position and move cursor to new directory
204                // if we are at the max zoom, we are already in the correct position (the "leaf" is actually a node instead)
205                if self.maxzoom.is_multiple_of(5)
206                    && path.len() == 1
207                    && level == self.maxzoom
208                    && path[0] == 0
209                {
210                    return cursor;
211                }
212                // grab the leaf from the file
213                let mut leaf_node =
214                    Buffer::new(self.writer.slice(cursor, cursor + NODE_SIZE as u64));
215                leaf = read_uint_48le(&mut leaf_node, None);
216                // if the leaf doesn't exist we create a new directory to host it
217                if leaf == 0 {
218                    cursor = self.create_leaf(cursor, depth * 5);
219                } else {
220                    cursor = leaf;
221                } // move to where leaf is pointing
222            }
223        }
224
225        cursor
226    }
227
228    /// Create a new leaf directory
229    ///
230    /// ## Parameters
231    /// - `cursor`: the cursor
232    /// - `depth`: the depth
233    ///
234    /// ## Returns
235    /// The offset of the new leaf
236    fn create_leaf(&mut self, cursor: u64, depth: u64) -> u64 {
237        // build directory size according to maxzoom
238        let dir_size = build_dir_size(depth, self.maxzoom as u64);
239        // create offset & node
240        let offset = self.offset;
241        let node = Node { offset, length: dir_size };
242        // create a dir of said size and update to new offset
243        self.writer.write(&vec![0u8; dir_size as usize], offset);
244        self.offset += dir_size as u64;
245        // store our newly created directory as a leaf directory in our current directory
246        self.write_node(cursor, node);
247
248        // return the offset of the leaf directory
249        offset
250    }
251
252    /// Writes a node to the file
253    ///
254    /// ## Parameters
255    /// - `cursor`: the cursor
256    /// - `node`: the node
257    fn write_node(&mut self, cursor: u64, node: Node) {
258        let Node { offset, length } = node;
259        // write offset and length to buffer
260        let mut node_buf = Buffer::new(vec![0; NODE_SIZE]);
261        write_uint48_le(&mut node_buf, offset, 0);
262        node_buf.set_u32_at(6, length);
263        // write buffer to file at directory offset
264        self.writer.write(&node_buf.take(), cursor);
265    }
266}
267
268/// write a 32 bit and a 16 bit
269///
270/// ## Parameters
271/// - `data`: the data to write to
272/// - `num`: the number
273/// - `offset`: the offset to write at
274fn write_uint48_le(data: &mut Buffer, num: u64, offset: usize) {
275    let lower = (num & 0xffff) as u16;
276    let upper = (num >> 16) as u32;
277    data.set_u16_at(offset, lower);
278    data.set_u32_at(offset + 2, upper);
279}
280
281/// read a 48 bit number
282///
283/// ## Parameters
284/// - `buffer`: the buffer
285/// - `offset`: the offset
286///
287/// ## Returns
288/// The number
289fn read_uint_48le(buffer: &mut Buffer, offset: Option<usize>) -> u64 {
290    let offset = offset.unwrap_or(0);
291    buffer.get_u32_at(offset + 2) as u64 * (1 << 16) + buffer.get_u16_at(offset) as u64
292}
293
294/// Build a directory size relative to maxzoom
295///
296/// ## Parameters
297/// - `depth`: the depth
298/// - `maxzoom`: the maxzoom
299///
300/// ## Returns
301/// The directory size
302fn build_dir_size(depth: u64, maxzoom: u64) -> u32 {
303    let mut dir_size = 0;
304    // grab the remainder
305    let mut remainder = u64::min(maxzoom - depth, 5); // must be increments of 5, so if level 4 then inc is 0 but if 5, inc is 5
306    // for each remainder (including 0), we add a quadrant
307    loop {
308        dir_size += (1 << remainder) * (1 << remainder);
309        remainder -= 1;
310        if remainder == 0 {
311            break;
312        }
313    }
314
315    dir_size * NODE_SIZE as u32
316}
317
318/// Get the path to a tile
319///
320/// ## Parameters
321/// - `zoom`: the zoom
322/// - `x`: the x
323/// - `y`: the y
324///
325/// ## Returns
326/// The path
327fn get_path(mut zoom: u8, mut x: u32, mut y: u32) -> Vec<u64> {
328    let mut path = vec![];
329
330    while zoom >= 5 {
331        path.push((5, x & 31, y & 31));
332        x >>= 5;
333        y >>= 5;
334        zoom = zoom.saturating_sub(5);
335    }
336    path.push((zoom, x, y));
337
338    path.into_iter()
339        .map(|(zoom, x, y)| {
340            let val = (y as u64) * ((1 << zoom) as u64) + (x as u64);
341            let sum: u64 = (0..zoom).map(|z| (1 << z) * (1 << z)).sum();
342            val + sum
343        })
344        .collect()
345}