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)]
24pub struct PMTiles<R> {
26 pub tile_type: TileType,
28
29 pub tile_compression: Compression,
31
32 pub internal_compression: Compression,
34
35 pub min_zoom: u8,
37
38 pub max_zoom: u8,
40
41 pub center_zoom: u8,
45
46 pub min_longitude: f64,
48
49 pub min_latitude: f64,
51
52 pub max_longitude: f64,
54
55 pub max_latitude: f64,
57
58 pub center_longitude: f64,
62
63 pub center_latitude: f64,
67
68 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 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 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 pub fn tile_ids(&self) -> Vec<&u64> {
131 self.tile_manager.get_tile_ids()
132 }
133
134 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 pub fn remove_tile(&mut self, tile_id: u64) {
149 self.tile_manager.remove_tile(tile_id);
150 }
151
152 pub fn num_tiles(&self) -> usize {
154 self.tile_manager.num_addressed_tiles()
155 }
156}
157
158impl<R: Read + Seek> PMTiles<R> {
159 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 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 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 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 let header = add_await([Header::from_reader(&mut input)])?;
272
273 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 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 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 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 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 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 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 ))])?; 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 ))])?; Ok(())
415 }
416}
417
418impl<R: Read + Seek> PMTiles<R> {
419 pub fn from_reader(input: R) -> Result<Self> {
440 Self::from_reader_impl(input, ..)
441 }
442
443 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 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 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 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 pub async fn from_async_reader(input: R) -> Result<Self> {
586 Self::from_async_reader_impl(input, ..).await
587 }
588
589 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 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}