use std::{fs, io::ErrorKind, path::Path};
use anyhow::{Result, ensure};
use async_trait::async_trait;
use versatiles_core::{Blob, compression::compress};
use versatiles_derive::context;
use crate::{TileSource, TileSourceTraverseExt, TilesRuntime, TilesWriter, Traversal};
pub struct DirectoryWriter {}
impl DirectoryWriter {
#[context("writing file '{}'", path.display())]
fn write(path: &Path, blob: &Blob) -> Result<()> {
match fs::write(path, blob.as_slice()) {
Ok(()) => Ok(()),
Err(error) if error.kind() == ErrorKind::NotFound => {
let parent = path.parent().expect("tile path has parent directory");
fs::create_dir_all(parent)?;
fs::write(path, blob.as_slice())?;
Ok(())
}
Err(error) => Err(error.into()),
}
}
}
#[async_trait]
impl TilesWriter for DirectoryWriter {
fn supports_data_writer() -> bool {
false
}
#[context("writing tiles to directory '{}'", path.display())]
async fn write_to_path(reader: &mut dyn TileSource, path: &Path, runtime: TilesRuntime) -> Result<()> {
ensure!(path.is_absolute(), "path {path:?} must be absolute");
log::trace!("convert_from");
let parameters = reader.metadata();
let tile_format = *parameters.tile_format();
let extension_format = tile_format.as_extension().to_string();
let tile_compression = *reader.metadata().tile_compression();
let extension_compression = tile_compression.as_extension().to_string();
let tilejson = reader.tilejson();
let meta_data = compress(tilejson.into(), &tile_compression)?;
let filename = format!("tiles.json{extension_compression}");
Self::write(&path.join(filename), &meta_data)?;
reader
.traverse_all_tiles(
&Traversal::ANY,
move |_bbox, mut stream| {
let extension_format = extension_format.clone();
let extension_compression = extension_compression.clone();
let path = path.to_path_buf();
Box::pin(async move {
while let Some(entry) = stream.next().await {
let (coord, tile) = entry;
let filename = format!(
"{}/{}/{}{}{}",
coord.level, coord.x, coord.y, extension_format, extension_compression
);
let blob = tile.into_blob(&tile_compression)?;
Self::write(&path.join(filename), &blob)?;
}
Ok(())
})
},
runtime.clone(),
)
.await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use versatiles_core::{TileCompression, TileFormat, TilePyramid, compression::decompress_gzip};
use super::*;
use crate::{MOCK_BYTES_PBF, MockReader, TileSourceMetadata};
#[tokio::test]
async fn test_convert_from() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let temp_path = temp_dir.path();
let mut mock_reader = MockReader::new_mock(
TilePyramid::new_full_up_to(2),
TileSourceMetadata::new(TileFormat::MVT, TileCompression::Gzip, Traversal::ANY, None),
)?;
DirectoryWriter::write_to_path(&mut mock_reader, temp_path, TilesRuntime::default()).await?;
let load = |filename| {
let path = temp_path.join(filename);
path
.try_exists()
.unwrap_or_else(|_| panic!("filename {filename} should exist"));
decompress_gzip(&Blob::from(
fs::read(path).unwrap_or_else(|_| panic!("filename {filename} should be readable")),
))
.unwrap_or_else(|_| panic!("filename {filename} should be gzip compressed"))
};
assert_eq!(
load("tiles.json.gz").as_str(),
"{\"tilejson\":\"3.0.0\",\"type\":\"dummy\"}"
);
assert_eq!(load("0/0/0.pbf.gz").as_slice(), MOCK_BYTES_PBF);
assert_eq!(load("2/3/3.pbf.gz").as_slice(), MOCK_BYTES_PBF);
Ok(())
}
}