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}