pmtiles2/
pmtiles.rs

1use std::{
2    io::{Cursor, Read, Result, Seek, Write},
3    ops::RangeBounds,
4};
5
6use duplicate::duplicate_item;
7#[cfg(feature = "async")]
8use futures::{AsyncRead, AsyncReadExt, AsyncSeekExt, AsyncWrite, AsyncWriteExt};
9use serde_json::{Map as JSONMap, Value as JSONValue};
10
11use crate::{
12    header::{LatLng, HEADER_BYTES},
13    tile_manager::TileManager,
14    util::{compress, decompress, read_directories, tile_id, write_directories},
15    Compression, Header, TileType,
16};
17
18#[cfg(feature = "async")]
19use crate::util::{
20    compress_async, decompress_async, read_directories_async, write_directories_async,
21};
22
23#[derive(Debug)]
24/// A structure representing a `PMTiles` archive.
25pub struct PMTiles<R> {
26    /// Type of tiles
27    pub tile_type: TileType,
28
29    /// Compression of tiles
30    pub tile_compression: Compression,
31
32    /// Compression of directories and meta data
33    pub internal_compression: Compression,
34
35    /// Minimum zoom of all tiles this archive
36    pub min_zoom: u8,
37
38    /// Maximum zoom of all tiles this archive
39    pub max_zoom: u8,
40
41    /// Center zoom
42    ///
43    /// _Implementations may use this to set the default zoom_
44    pub center_zoom: u8,
45
46    /// Minimum longitude of bounds of available tiles
47    pub min_longitude: f64,
48
49    /// Minimum latitude of bounds of available tiles
50    pub min_latitude: f64,
51
52    /// Maximum longitude of bounds of available tiles
53    pub max_longitude: f64,
54
55    /// Maximum latitude of bounds of available tiles
56    pub max_latitude: f64,
57
58    /// Center longitude
59    ///
60    /// _Implementations may use the center longitude and latitude to set the default location_
61    pub center_longitude: f64,
62
63    /// Center latitude
64    ///
65    /// _Implementations may use the center longitude and latitude to set the default location_
66    pub center_latitude: f64,
67
68    /// JSON meta data of this archive
69    pub meta_data: JSONMap<String, JSONValue>,
70
71    tile_manager: TileManager<R>,
72}
73
74impl<R> Default for PMTiles<R> {
75    fn default() -> Self {
76        Self {
77            tile_type: TileType::Unknown,
78            internal_compression: Compression::GZip,
79            tile_compression: Compression::Unknown,
80            min_zoom: 0,
81            max_zoom: 0,
82            center_zoom: 0,
83            min_longitude: 0.0,
84            min_latitude: 0.0,
85            max_longitude: 0.0,
86            max_latitude: 0.0,
87            center_longitude: 0.0,
88            center_latitude: 0.0,
89            meta_data: JSONMap::new(),
90            tile_manager: TileManager::<R>::new(None),
91        }
92    }
93}
94
95impl PMTiles<Cursor<&[u8]>> {
96    /// Constructs a new, empty `PMTiles` archive, with no meta data, an [`internal_compression`](Self::internal_compression) of GZIP and all numeric fields set to `0`.
97    ///
98    /// # Arguments
99    /// * `tile_type` - Type of tiles in this archive
100    /// * `tile_compression` - Compression of tiles in this archive
101    pub fn new(tile_type: TileType, tile_compression: Compression) -> Self {
102        Self {
103            tile_type,
104            tile_compression,
105            ..Default::default()
106        }
107    }
108}
109
110#[cfg(feature = "async")]
111impl PMTiles<futures::io::Cursor<&[u8]>> {
112    /// Async version of [`new`](Self::new).
113    ///
114    /// Constructs a new, empty `PMTiles` archive, that works with asynchronous readers / writers.
115    ///
116    /// # Arguments
117    /// * `tile_type` - Type of tiles in this archive
118    /// * `tile_compression` - Compression of tiles in this archive
119    pub fn new_async(tile_type: TileType, tile_compression: Compression) -> Self {
120        Self {
121            tile_type,
122            tile_compression,
123            ..Default::default()
124        }
125    }
126}
127
128impl<R> PMTiles<R> {
129    /// Get vector of all tile ids in this `PMTiles` archive.
130    pub fn tile_ids(&self) -> Vec<&u64> {
131        self.tile_manager.get_tile_ids()
132    }
133
134    /// Adds a tile to this `PMTiles` archive.
135    ///
136    /// Note that the data should already be compressed if [`Self::tile_compression`] is set to a value other than [`Compression::None`].
137    /// The data will **NOT** be compressed automatically.  
138    /// The [`util`-module](crate::util) includes utilities to compress data.
139    ///
140    /// # Errors
141    /// Will return [`Err`] if `data` converts into an empty `Vec`.
142    ///
143    pub fn add_tile(&mut self, tile_id: u64, data: impl Into<Vec<u8>>) -> Result<()> {
144        self.tile_manager.add_tile(tile_id, data)
145    }
146
147    /// Removes a tile from this archive.
148    pub fn remove_tile(&mut self, tile_id: u64) {
149        self.tile_manager.remove_tile(tile_id);
150    }
151
152    /// Returns the number of addressed tiles in this archive.
153    pub fn num_tiles(&self) -> usize {
154        self.tile_manager.num_addressed_tiles()
155    }
156}
157
158impl<R: Read + Seek> PMTiles<R> {
159    /// Get data of a tile by its id.
160    ///
161    /// The returned data is the raw data, meaning It is NOT uncompressed automatically,
162    /// if it was compressed in the first place.  
163    /// If you need the uncompressed data, take a look at the [`util`-module](crate::util)
164    ///
165    /// Will return [`Ok`] with an value of [`None`] if no a tile with the specified tile id was found.
166    ///
167    /// # Errors
168    /// Will return [`Err`] if the tile data was not read into memory yet and there was an error while
169    /// attempting to read it.
170    ///
171    pub fn get_tile_by_id(&mut self, tile_id: u64) -> Result<Option<Vec<u8>>> {
172        self.tile_manager.get_tile(tile_id)
173    }
174
175    /// Returns the data of the tile with the specified coordinates.
176    ///
177    /// See [`get_tile_by_id`](Self::get_tile_by_id) for further details on the return type.
178    ///
179    /// # Errors
180    /// See [`get_tile_by_id`](Self::get_tile_by_id) for details on possible errors.
181    pub fn get_tile(&mut self, x: u64, y: u64, z: u8) -> Result<Option<Vec<u8>>> {
182        self.get_tile_by_id(tile_id(z, x, y))
183    }
184}
185
186#[cfg(feature = "async")]
187impl<R: AsyncRead + AsyncReadExt + Send + Unpin + AsyncSeekExt> PMTiles<R> {
188    /// Async version of [`get_tile_by_id`](Self::get_tile_by_id).
189    ///
190    /// Get data of a tile by its id.
191    ///
192    /// The returned data is the raw data, meaning It is NOT uncompressed automatically,
193    /// if it was compressed in the first place.  
194    /// If you need the uncompressed data, take a look at the [`util`-module](crate::util)
195    ///
196    /// Will return [`Ok`] with an value of [`None`] if no a tile with the specified tile id was found.
197    ///
198    /// # Errors
199    /// Will return [`Err`] if the tile data was not read into memory yet and there was an error while
200    /// attempting to read it.
201    ///
202    pub async fn get_tile_by_id_async(&mut self, tile_id: u64) -> Result<Option<Vec<u8>>> {
203        self.tile_manager.get_tile_async(tile_id).await
204    }
205
206    /// Async version of [`get_tile`](Self::get_tile).
207    ///
208    /// Returns the data of the tile with the specified coordinates.
209    ///
210    /// See [`get_tile_by_id_async`](Self::get_tile_by_id_async) for further details on the return type.
211    ///
212    /// # Errors
213    /// See [`get_tile_by_id_async`](Self::get_tile_by_id_async) for details on possible errors.
214    pub async fn get_tile_async(&mut self, x: u64, y: u64, z: u8) -> Result<Option<Vec<u8>>> {
215        self.get_tile_by_id_async(tile_id(z, x, y)).await
216    }
217}
218
219impl<R> PMTiles<R> {
220    fn parse_meta_data(val: JSONValue) -> Result<JSONMap<String, JSONValue>> {
221        let JSONValue::Object(map) = val else {
222            return Err(std::io::Error::new(
223                std::io::ErrorKind::InvalidData,
224                "PMTiles' metadata must be JSON Object",
225            ));
226        };
227
228        Ok(map)
229    }
230}
231
232impl<R: Read + Seek> PMTiles<R> {
233    fn read_meta_data(
234        compression: Compression,
235        reader: &mut impl Read,
236    ) -> Result<JSONMap<String, JSONValue>> {
237        let reader = decompress(compression, reader)?;
238
239        let val: JSONValue = serde_json::from_reader(reader)?;
240
241        Self::parse_meta_data(val)
242    }
243}
244
245#[cfg(feature = "async")]
246impl<R: AsyncRead + AsyncSeekExt + Send + Unpin> PMTiles<R> {
247    async fn read_meta_data_async(
248        compression: Compression,
249        reader: &mut (impl AsyncRead + Unpin + Send),
250    ) -> Result<JSONMap<String, JSONValue>> {
251        let mut reader = decompress_async(compression, reader)?;
252
253        let mut output = Vec::with_capacity(2048);
254        reader.read_to_end(&mut output).await?;
255
256        let val: JSONValue = serde_json::from_slice(&output[..])?;
257
258        Self::parse_meta_data(val)
259    }
260}
261
262#[duplicate_item(
263    fn_name                  cfg_async_filter       async    add_await(code) SeekFrom                FilterRangeTraits                RTraits                                                  read_directories         read_meta_data         from_reader;
264    [from_reader_impl]       [cfg(all())]           []       [code]          [std::io::SeekFrom]     [RangeBounds<u64>]               [Read + Seek]                                            [read_directories]       [read_meta_data]       [from_reader];
265    [from_async_reader_impl] [cfg(feature="async")] [async]  [code.await]    [futures::io::SeekFrom] [RangeBounds<u64> + Sync + Send] [AsyncRead + AsyncReadExt + Send + Unpin + AsyncSeekExt] [read_directories_async] [read_meta_data_async] [from_async_reader];
266)]
267#[cfg_async_filter]
268impl<R: RTraits> PMTiles<R> {
269    async fn fn_name(mut input: R, tiles_filter_range: impl FilterRangeTraits) -> Result<Self> {
270        // HEADER
271        let header = add_await([Header::from_reader(&mut input)])?;
272
273        // META DATA
274        let meta_data = if header.json_metadata_length == 0 {
275            JSONMap::new()
276        } else {
277            add_await([input.seek(SeekFrom::Start(header.json_metadata_offset))])?;
278
279            let mut meta_data_reader = (&mut input).take(header.json_metadata_length);
280            add_await([Self::read_meta_data(
281                header.internal_compression,
282                &mut meta_data_reader,
283            )])?
284        };
285
286        // DIRECTORIES
287        let tiles = add_await([read_directories(
288            &mut input,
289            header.internal_compression,
290            (header.root_directory_offset, header.root_directory_length),
291            header.leaf_directories_offset,
292            tiles_filter_range,
293        )])?;
294
295        let mut tile_manager = TileManager::new(Some(input));
296
297        for (tile_id, info) in tiles {
298            tile_manager.add_offset_tile(
299                tile_id,
300                header.tile_data_offset + info.offset,
301                info.length,
302            )?;
303        }
304
305        Ok(Self {
306            tile_type: header.tile_type,
307            internal_compression: header.internal_compression,
308            tile_compression: header.tile_compression,
309            min_zoom: header.min_zoom,
310            max_zoom: header.max_zoom,
311            center_zoom: header.center_zoom,
312            min_longitude: header.min_pos.longitude,
313            min_latitude: header.min_pos.latitude,
314            max_longitude: header.max_pos.longitude,
315            max_latitude: header.max_pos.latitude,
316            center_longitude: header.center_pos.longitude,
317            center_latitude: header.center_pos.latitude,
318            meta_data,
319            tile_manager,
320        })
321    }
322}
323
324#[duplicate_item(
325    fn_name                cfg_async_filter       async    add_await(code) RTraits                                                  SeekFrom                WTraits                                    finish         compress         flush   write_directories         to_writer;
326    [to_writer_impl]       [cfg(all())]           []       [code]          [Read + Seek]                                            [std::io::SeekFrom]     [Write + Seek]                             [finish]       [compress]       [flush] [write_directories]       [to_writer];
327    [to_async_writer_impl] [cfg(feature="async")] [async]  [code.await]    [AsyncRead + AsyncReadExt + Send + Unpin + AsyncSeekExt] [futures::io::SeekFrom] [AsyncWrite + Send + Unpin + AsyncSeekExt] [finish_async] [compress_async] [close] [write_directories_async] [to_async_writer];
328)]
329#[cfg_async_filter]
330impl<R: RTraits> PMTiles<R> {
331    #[allow(clippy::wrong_self_convention)]
332    async fn fn_name(self, output: &mut (impl WTraits)) -> Result<()> {
333        let result = add_await([self.tile_manager.finish()])?;
334
335        // ROOT DIR
336        add_await([output.seek(SeekFrom::Current(i64::from(HEADER_BYTES)))])?;
337        let root_directory_offset = u64::from(HEADER_BYTES);
338        let leaf_directories_data = add_await([write_directories(
339            output,
340            &result.directory[0..],
341            self.internal_compression,
342            None,
343        )])?;
344        let root_directory_length = add_await([output.stream_position()])? - root_directory_offset;
345
346        // META DATA
347        let json_metadata_offset = root_directory_offset + root_directory_length;
348        {
349            let mut compression_writer = compress(self.internal_compression, output)?;
350            let vec = serde_json::to_vec(&self.meta_data)?;
351            add_await([compression_writer.write_all(&vec)])?;
352
353            add_await([compression_writer.flush()])?;
354        }
355        let json_metadata_length = add_await([output.stream_position()])? - json_metadata_offset;
356
357        // LEAF DIRECTORIES
358        let leaf_directories_offset = json_metadata_offset + json_metadata_length;
359        add_await([output.write_all(&leaf_directories_data[0..])])?;
360        drop(leaf_directories_data);
361        let leaf_directories_length =
362            add_await([output.stream_position()])? - leaf_directories_offset;
363
364        // DATA
365        let tile_data_offset = leaf_directories_offset + leaf_directories_length;
366        add_await([output.write_all(&result.data[0..])])?;
367        let tile_data_length = result.data.len() as u64;
368
369        // HEADER
370        let header = Header {
371            spec_version: 3,
372            root_directory_offset,
373            root_directory_length,
374            json_metadata_offset,
375            json_metadata_length,
376            leaf_directories_offset,
377            leaf_directories_length,
378            tile_data_offset,
379            tile_data_length,
380            num_addressed_tiles: result.num_addressed_tiles,
381            num_tile_entries: result.num_tile_entries,
382            num_tile_content: result.num_tile_content,
383            clustered: true,
384            internal_compression: self.internal_compression,
385            tile_compression: self.tile_compression,
386            tile_type: self.tile_type,
387            min_zoom: self.min_zoom,
388            max_zoom: self.max_zoom,
389            min_pos: LatLng {
390                longitude: self.min_longitude,
391                latitude: self.min_latitude,
392            },
393            max_pos: LatLng {
394                longitude: self.max_longitude,
395                latitude: self.max_latitude,
396            },
397            center_zoom: self.center_zoom,
398            center_pos: LatLng {
399                longitude: self.center_longitude,
400                latitude: self.center_latitude,
401            },
402        };
403
404        add_await([output.seek(SeekFrom::Start(
405            root_directory_offset - u64::from(HEADER_BYTES),
406        ))])?; // jump to start of stream
407
408        add_await([header.to_writer(output)])?;
409
410        add_await([output.seek(SeekFrom::Start(
411            (root_directory_offset - u64::from(HEADER_BYTES)) + tile_data_offset + tile_data_length,
412        ))])?; // jump to end of stream
413
414        Ok(())
415    }
416}
417
418impl<R: Read + Seek> PMTiles<R> {
419    /// Reads a `PMTiles` archive from a reader.
420    ///
421    /// This takes ownership of the reader, because tile data is only read when required.
422    ///
423    /// # Arguments
424    /// * `input` - Reader
425    ///
426    /// # Errors
427    /// Will return [`Err`] if there was any kind of I/O error while reading from `input`, the data
428    /// stream was no valid `PMTiles` archive or the internal compression of the archive is set to "Unknown".
429    ///
430    ///
431    /// # Example
432    /// ```rust
433    /// # use pmtiles2::{PMTiles};
434    /// # let file_path = "./test/stamen_toner(raster)CC-BY+ODbL_z3.pmtiles";
435    /// let mut file = std::fs::File::open(file_path).unwrap();
436    ///
437    /// let pm_tiles = PMTiles::from_reader(file).unwrap();
438    /// ```
439    pub fn from_reader(input: R) -> Result<Self> {
440        Self::from_reader_impl(input, ..)
441    }
442
443    /// Same as [`from_reader`](Self::from_reader), but with an extra parameter.
444    ///
445    /// Reads a `PMTiles` archive from a reader, but only parses tile entries whose tile IDs are included in the filter
446    /// range. Tiles that are not included in the range will appear as missing.
447    ///
448    /// This can improve performance in cases where only a limited range of tiles is needed, as whole leaf directories
449    /// may be skipped during parsing.
450    ///
451    /// # Arguments
452    /// * `input` - Reader
453    /// * `tiles_filter_range` - Range of Tile IDs to load
454    ///
455    /// # Errors
456    /// See [`from_reader`](Self::from_reader) for details on possible errors.
457    ///
458    /// # Example
459    /// ```rust
460    /// # use pmtiles2::{PMTiles};
461    /// # let file_path = "./test/stamen_toner(raster)CC-BY+ODbL_z3.pmtiles";
462    /// let mut file = std::fs::File::open(file_path).unwrap();
463    ///
464    /// let pm_tiles = PMTiles::from_reader_partially(file, ..).unwrap();
465    /// ```
466    pub fn from_reader_partially(
467        input: R,
468        tiles_filter_range: impl RangeBounds<u64>,
469    ) -> Result<Self> {
470        Self::from_reader_impl(input, tiles_filter_range)
471    }
472
473    /// Writes the archive to a writer.
474    ///
475    /// The archive is always deduped and the directory entries clustered to produce the smallest
476    /// possible archive size.
477    ///
478    /// This takes ownership of the object so all data does not need to be copied.
479    /// This prevents large memory consumption when writing large `PMTiles` archives.
480    ///
481    /// # Arguments
482    /// * `output` - Writer to write data to
483    ///
484    /// # Errors
485    /// Will return [`Err`] if [`Self::internal_compression`] was set to [`Compression::Unknown`]
486    /// or an I/O error occurred while writing to `output`.
487    ///
488    /// # Example
489    /// Write the archive to a file.
490    /// ```rust
491    /// # use pmtiles2::{PMTiles, TileType, Compression};
492    /// # let dir = temp_dir::TempDir::new().unwrap();
493    /// # let file_path = dir.path().join("foo.pmtiles");
494    /// let pm_tiles = PMTiles::new(TileType::Png, Compression::None);
495    /// let mut file = std::fs::File::create(file_path).unwrap();
496    /// pm_tiles.to_writer(&mut file).unwrap();
497    /// ```
498    pub fn to_writer(self, output: &mut (impl Write + Seek)) -> Result<()> {
499        self.to_writer_impl(output)
500    }
501}
502
503impl<T: AsRef<[u8]>> PMTiles<Cursor<T>> {
504    /// Reads a `PMTiles` archive from anything that can be turned into a byte slice (e.g. [`Vec<u8>`]).
505    ///
506    /// # Arguments
507    /// * `bytes` - Input bytes
508    ///
509    /// # Errors
510    /// Will return [`Err`] if there was any kind of I/O error while reading from `input`, the data
511    /// stream was no valid `PMTiles` archive or the internal compression of the archive is set to "Unknown".
512    ///
513    /// # Example
514    /// ```rust
515    /// # use pmtiles2::{PMTiles};
516    /// let bytes = include_bytes!("../test/stamen_toner(raster)CC-BY+ODbL_z3.pmtiles");
517    /// let pm_tiles = PMTiles::from_bytes(bytes).unwrap();
518    /// ```
519    ///
520    pub fn from_bytes(bytes: T) -> std::io::Result<Self> {
521        let reader = std::io::Cursor::new(bytes);
522
523        Self::from_reader(reader)
524    }
525
526    /// Same as [`from_bytes`](Self::from_bytes), but with an extra parameter.
527    ///
528    /// Reads a `PMTiles` archive from something that can be turned into a byte slice (e.g. [`Vec<u8>`]),
529    /// but only parses tile entries whose tile IDs are included in the filter range. Tiles that are not
530    /// included in the range will appear as missing.
531    ///
532    /// This can improve performance in cases where only a limited range of tiles is needed, as whole leaf directories
533    /// may be skipped during parsing.
534    ///
535    /// # Arguments
536    /// * `bytes` - Input bytes
537    /// * `tiles_filter_range` - Range of Tile IDs to load
538    ///
539    /// # Errors
540    /// See [`from_bytes`](Self::from_bytes) for details on possible errors.
541    ///
542    /// # Example
543    /// ```rust
544    /// # use pmtiles2::{PMTiles};
545    /// let bytes = include_bytes!("../test/stamen_toner(raster)CC-BY+ODbL_z3.pmtiles");
546    /// let pm_tiles = PMTiles::from_bytes_partially(bytes, ..).unwrap();
547    /// ```
548    pub fn from_bytes_partially(
549        bytes: T,
550        tiles_filter_range: impl RangeBounds<u64>,
551    ) -> Result<Self> {
552        let reader = std::io::Cursor::new(bytes);
553
554        Self::from_reader_partially(reader, tiles_filter_range)
555    }
556}
557
558#[cfg(feature = "async")]
559impl<R: AsyncRead + AsyncSeekExt + Send + Unpin> PMTiles<R> {
560    /// Async version of [`from_reader`](Self::from_reader).
561    ///
562    /// Reads a `PMTiles` archive from a reader.
563    ///
564    /// This takes ownership of the reader, because tile data is only read when required.
565    ///
566    /// # Arguments
567    /// * `input` - Reader
568    ///
569    /// # Errors
570    /// Will return [`Err`] if there was any kind of I/O error while reading from `input`, the data
571    /// stream was no valid `PMTiles` archive or the internal compression of the archive is set to "Unknown".
572    ///
573    ///
574    /// # Example
575    /// ```rust
576    /// # use pmtiles2::PMTiles;
577    /// # use futures::io::{AsyncReadExt, AsyncSeekExt, SeekFrom};
578    /// # tokio_test::block_on(async {
579    /// let bytes = include_bytes!("../test/stamen_toner(raster)CC-BY+ODbL_z3.pmtiles");
580    /// let mut reader = futures::io::Cursor::new(bytes);
581    ///
582    /// let pm_tiles = PMTiles::from_async_reader(reader).await.unwrap();
583    /// # })
584    /// ```
585    pub async fn from_async_reader(input: R) -> Result<Self> {
586        Self::from_async_reader_impl(input, ..).await
587    }
588
589    /// Same as [`from_async_reader`](Self::from_async_reader), but with an extra parameter.
590    ///
591    /// Reads a `PMTiles` archive from a reader, but only parses tile entries whose tile IDs are included in the filter
592    /// range. Tiles that are not included in the range will appear as missing.
593    ///
594    /// This can improve performance in cases where only a limited range of tiles is needed, as whole leaf directories
595    /// may be skipped during parsing.
596    ///
597    /// # Arguments
598    /// * `input` - Reader
599    /// * `tiles_filter_range` - Range of Tile IDs to load
600    ///
601    /// # Errors
602    /// See [`from_async_reader`](Self::from_async_reader) for details on possible errors.
603    ///
604    /// # Example
605    /// ```rust
606    /// # use pmtiles2::PMTiles;
607    /// # use futures::io::{AsyncReadExt, AsyncSeekExt, SeekFrom};
608    /// # tokio_test::block_on(async {
609    /// let bytes = include_bytes!("../test/stamen_toner(raster)CC-BY+ODbL_z3.pmtiles");
610    /// let mut reader = futures::io::Cursor::new(bytes);
611    ///
612    /// let pm_tiles = PMTiles::from_async_reader_partially(reader, ..).await.unwrap();
613    /// # })
614    /// ```
615    pub async fn from_async_reader_partially(
616        input: R,
617        tiles_filter_range: (impl RangeBounds<u64> + Sync + Send),
618    ) -> Result<Self> {
619        Self::from_async_reader_impl(input, tiles_filter_range).await
620    }
621
622    /// Async version of [`to_writer`](Self::to_writer).
623    ///
624    /// Writes the archive to a writer.
625    ///
626    /// The archive is always deduped and the directory entries clustered to produce the smallest
627    /// possible archive size.
628    ///
629    /// This takes ownership of the object so all data does not need to be copied.
630    /// This prevents large memory consumption when writing large `PMTiles` archives.
631    ///
632    /// # Arguments
633    /// * `output` - Writer to write data to
634    ///
635    /// # Errors
636    /// Will return [`Err`] if [`Self::internal_compression`] was set to [`Compression::Unknown`]
637    /// or an I/O error occurred while writing to `output`.
638    ///
639    /// # Example
640    /// Write the archive to a file.
641    /// ```rust
642    /// # use pmtiles2::{PMTiles, TileType, Compression};
643    /// # use futures::io::{AsyncWrite, AsyncWriteExt, AsyncSeekExt};
644    /// # use tokio_util::compat::TokioAsyncReadCompatExt;
645    /// # let dir = temp_dir::TempDir::new().unwrap();
646    /// # let file_path = dir.path().join("foo.pmtiles");
647    /// # tokio_test::block_on(async {
648    /// let pm_tiles = PMTiles::new_async(TileType::Png, Compression::None);
649    /// let mut out_file = tokio::fs::File::create(file_path).await.unwrap().compat();
650    /// pm_tiles.to_async_writer(&mut out_file).await.unwrap();
651    /// # })
652    /// ```
653    pub async fn to_async_writer(
654        self,
655        output: &mut (impl AsyncWrite + AsyncSeekExt + Unpin + Send),
656    ) -> Result<()> {
657        self.to_async_writer_impl(output).await
658    }
659}
660
661#[cfg(test)]
662#[allow(clippy::unwrap_used)]
663mod test {
664    use std::io::Cursor;
665
666    use serde_json::json;
667
668    use super::*;
669
670    const PM_TILES_BYTES: &[u8] =
671        include_bytes!("../test/stamen_toner(raster)CC-BY+ODbL_z3.pmtiles");
672
673    const PM_TILES_BYTES2: &[u8] = include_bytes!("../test/protomaps(vector)ODbL_firenze.pmtiles");
674
675    #[test]
676    fn test_read_meta_data() -> Result<()> {
677        let meta_data = PMTiles::<Cursor<Vec<u8>>>::read_meta_data(
678            Compression::GZip,
679            &mut Cursor::new(&PM_TILES_BYTES[373..373 + 22]),
680        )?;
681        assert_eq!(meta_data, JSONMap::new());
682
683        let meta_data2 = PMTiles::<Cursor<Vec<u8>>>::read_meta_data(
684            Compression::GZip,
685            &mut Cursor::new(&PM_TILES_BYTES2[530..530 + 266]),
686        )?;
687
688        assert_eq!(
689            meta_data2,
690            json!({
691                "attribution":"<a href=\"https://protomaps.com\" target=\"_blank\">Protomaps</a> © <a href=\"https://www.openstreetmap.org\" target=\"_blank\"> OpenStreetMap</a>",
692                "tilestats":{
693                    "layers":[
694                        {"geometry":"Polygon","layer":"earth"},
695                        {"geometry":"Polygon","layer":"natural"},
696                        {"geometry":"Polygon","layer":"land"},
697                        {"geometry":"Polygon","layer":"water"},
698                        {"geometry":"LineString","layer":"physical_line"},
699                        {"geometry":"Polygon","layer":"buildings"},
700                        {"geometry":"Point","layer":"physical_point"},
701                        {"geometry":"Point","layer":"places"},
702                        {"geometry":"LineString","layer":"roads"},
703                        {"geometry":"LineString","layer":"transit"},
704                        {"geometry":"Point","layer":"pois"},
705                        {"geometry":"LineString","layer":"boundaries"},
706                        {"geometry":"Polygon","layer":"mask"}
707                    ]
708                }
709            }).as_object().unwrap().to_owned()
710        );
711
712        Ok(())
713    }
714
715    #[test]
716    fn test_from_reader() -> Result<()> {
717        let mut reader = Cursor::new(PM_TILES_BYTES);
718
719        let pm_tiles = PMTiles::from_reader(&mut reader)?;
720
721        assert_eq!(pm_tiles.tile_type, TileType::Png);
722        assert_eq!(pm_tiles.internal_compression, Compression::GZip);
723        assert_eq!(pm_tiles.tile_compression, Compression::None);
724        assert_eq!(pm_tiles.min_zoom, 0);
725        assert_eq!(pm_tiles.max_zoom, 3);
726        assert_eq!(pm_tiles.center_zoom, 0);
727        assert!((-180.0 - pm_tiles.min_longitude).abs() < f64::EPSILON);
728        assert!((-85.0 - pm_tiles.min_latitude).abs() < f64::EPSILON);
729        assert!((180.0 - pm_tiles.max_longitude).abs() < f64::EPSILON);
730        assert!((85.0 - pm_tiles.max_latitude).abs() < f64::EPSILON);
731        assert!(pm_tiles.center_longitude < f64::EPSILON);
732        assert!(pm_tiles.center_latitude < f64::EPSILON);
733        assert_eq!(pm_tiles.meta_data, JSONMap::default());
734        assert_eq!(pm_tiles.num_tiles(), 85);
735
736        Ok(())
737    }
738
739    #[test]
740    fn test_from_reader2() -> Result<()> {
741        let mut reader = std::fs::File::open("./test/protomaps(vector)ODbL_firenze.pmtiles")?;
742
743        let pm_tiles = PMTiles::from_reader(&mut reader)?;
744
745        assert_eq!(pm_tiles.tile_type, TileType::Mvt);
746        assert_eq!(pm_tiles.internal_compression, Compression::GZip);
747        assert_eq!(pm_tiles.tile_compression, Compression::GZip);
748        assert_eq!(pm_tiles.min_zoom, 0);
749        assert_eq!(pm_tiles.max_zoom, 14);
750        assert_eq!(pm_tiles.center_zoom, 0);
751        assert!((pm_tiles.min_longitude - 11.154_026).abs() < f64::EPSILON);
752        assert!((pm_tiles.min_latitude - 43.727_012_5).abs() < f64::EPSILON);
753        assert!((pm_tiles.max_longitude - 11.328_939_5).abs() < f64::EPSILON);
754        assert!((pm_tiles.max_latitude - 43.832_545_5).abs() < f64::EPSILON);
755        assert!((pm_tiles.center_longitude - 11.241_482_7).abs() < f64::EPSILON);
756        assert!((pm_tiles.center_latitude - 43.779_779).abs() < f64::EPSILON);
757        assert_eq!(
758            pm_tiles.meta_data,
759            json!({
760                "attribution":"<a href=\"https://protomaps.com\" target=\"_blank\">Protomaps</a> © <a href=\"https://www.openstreetmap.org\" target=\"_blank\"> OpenStreetMap</a>",
761                "tilestats":{
762                    "layers":[
763                        {"geometry":"Polygon","layer":"earth"},
764                        {"geometry":"Polygon","layer":"natural"},
765                        {"geometry":"Polygon","layer":"land"},
766                        {"geometry":"Polygon","layer":"water"},
767                        {"geometry":"LineString","layer":"physical_line"},
768                        {"geometry":"Polygon","layer":"buildings"},
769                        {"geometry":"Point","layer":"physical_point"},
770                        {"geometry":"Point","layer":"places"},
771                        {"geometry":"LineString","layer":"roads"},
772                        {"geometry":"LineString","layer":"transit"},
773                        {"geometry":"Point","layer":"pois"},
774                        {"geometry":"LineString","layer":"boundaries"},
775                        {"geometry":"Polygon","layer":"mask"}
776                    ]
777                }
778            }).as_object().unwrap().to_owned()
779        );
780        assert_eq!(pm_tiles.num_tiles(), 108);
781
782        Ok(())
783    }
784
785    #[test]
786    #[allow(clippy::too_many_lines)]
787    fn test_from_reader3() -> Result<()> {
788        let mut reader =
789            std::fs::File::open("./test/protomaps_vector_planet_odbl_z10_without_data.pmtiles")?;
790
791        let pm_tiles = PMTiles::from_reader(&mut reader)?;
792
793        assert_eq!(pm_tiles.tile_type, TileType::Mvt);
794        assert_eq!(pm_tiles.internal_compression, Compression::GZip);
795        assert_eq!(pm_tiles.tile_compression, Compression::GZip);
796        assert_eq!(pm_tiles.min_zoom, 0);
797        assert_eq!(pm_tiles.max_zoom, 10);
798        assert_eq!(pm_tiles.center_zoom, 0);
799        assert!((-180.0 - pm_tiles.min_longitude).abs() < f64::EPSILON);
800        assert!((-90.0 - pm_tiles.min_latitude).abs() < f64::EPSILON);
801        assert!((180.0 - pm_tiles.max_longitude).abs() < f64::EPSILON);
802        assert!((90.0 - pm_tiles.max_latitude).abs() < f64::EPSILON);
803        assert!(pm_tiles.center_longitude < f64::EPSILON);
804        assert!(pm_tiles.center_latitude < f64::EPSILON);
805        assert_eq!(
806            pm_tiles.meta_data,
807            json!({
808                "attribution": "<a href=\"https://protomaps.com\" target=\"_blank\">Protomaps</a> © <a href=\"https://www.openstreetmap.org\" target=\"_blank\"> OpenStreetMap</a>",
809                "name": "protomaps 2022-11-08T03:35:13Z",
810                "tilestats": {
811                    "layers": [
812                        { "geometry": "Polygon", "layer": "earth" },
813                        { "geometry": "Polygon", "layer": "natural" },
814                        { "geometry": "Polygon", "layer": "land" },
815                        { "geometry": "Polygon", "layer": "water" },
816                        { "geometry": "LineString", "layer": "physical_line" },
817                        { "geometry": "Polygon", "layer": "buildings" },
818                        { "geometry": "Point", "layer": "physical_point" },
819                        { "geometry": "Point", "layer": "places" },
820                        { "geometry": "LineString", "layer": "roads" },
821                        { "geometry": "LineString", "layer": "transit" },
822                        { "geometry": "Point", "layer": "pois" },
823                        { "geometry": "LineString", "layer": "boundaries" },
824                        { "geometry": "Polygon", "layer": "mask" }
825                    ]
826                },
827                "vector_layers": [
828                    {
829                        "fields": {},
830                        "id": "earth"
831                    },
832                    {
833                        "fields": {
834                            "boundary": "string",
835                            "landuse": "string",
836                            "leisure": "string",
837                            "name": "string",
838                            "natural": "string"
839                        },
840                        "id": "natural"
841                    },
842                    {
843                        "fields": {
844                            "aeroway": "string",
845                            "amenity": "string",
846                            "area:aeroway": "string",
847                            "highway": "string",
848                            "landuse": "string",
849                            "leisure": "string",
850                            "man_made": "string",
851                            "name": "string",
852                            "place": "string",
853                            "pmap:kind": "string",
854                            "railway": "string",
855                            "sport": "string"
856                        },
857                        "id": "land"
858                    },
859                    {
860                        "fields": {
861                            "landuse": "string",
862                            "leisure": "string",
863                            "name": "string",
864                            "natural": "string",
865                            "water": "string",
866                            "waterway": "string"
867                        },
868                        "id": "water"
869                    },
870                    {
871                        "fields": {
872                            "natural": "string",
873                            "waterway": "string"
874                        },
875                        "id": "physical_line"
876                    },
877                    {
878                        "fields": {
879                            "building:part": "string",
880                            "height": "number",
881                            "layer": "string",
882                            "name": "string"
883                        },
884                        "id": "buildings"
885                    },
886                    {
887                        "fields": {
888                            "ele": "number",
889                            "name": "string",
890                            "natural": "string",
891                            "place": "string"
892                        },
893                        "id": "physical_point"
894                    },
895                    {
896                        "fields": {
897                            "capital": "string",
898                            "country_code_iso3166_1_alpha_2": "string",
899                            "name": "string",
900                            "place": "string",
901                            "pmap:kind": "string",
902                            "pmap:rank": "string",
903                            "population": "string"
904                        },
905                        "id": "places"
906                    },
907                    {
908                        "fields": {
909                            "bridge": "string",
910                            "highway": "string",
911                            "layer": "string",
912                            "oneway": "string",
913                            "pmap:kind": "string",
914                            "ref": "string",
915                            "tunnel": "string"
916                        },
917                        "id": "roads"
918                    },
919                    {
920                        "fields": {
921                            "aerialway": "string",
922                            "aeroway": "string",
923                            "highspeed": "string",
924                            "layer": "string",
925                            "name": "string",
926                            "network": "string",
927                            "pmap:kind": "string",
928                            "railway": "string",
929                            "ref": "string",
930                            "route": "string",
931                            "service": "string"
932                        },
933                        "id": "transit"
934                    },
935                    {
936                        "fields": {
937                            "amenity": "string",
938                            "cuisine": "string",
939                            "name": "string",
940                            "railway": "string",
941                            "religion": "string",
942                            "shop": "string",
943                            "tourism": "string"
944                        },
945                        "id": "pois"
946                    },
947                    {
948                        "fields": {
949                            "pmap:min_admin_level": "number"
950                        },
951                        "id": "boundaries"
952                    },
953                    {
954                        "fields": {},
955                        "id": "mask"
956                    }
957                ]
958            }).as_object().unwrap().to_owned()
959        );
960        assert_eq!(pm_tiles.num_tiles(), 1_398_101);
961
962        Ok(())
963    }
964
965    #[test]
966    #[ignore]
967    fn test_to_writer() -> Result<()> {
968        todo!()
969    }
970
971    #[test]
972    #[ignore]
973    fn test_to_writer_with_leaf_directories() -> Result<()> {
974        todo!()
975    }
976}