use crate::{SharedTileSource, SourceType, Tile, TileSource, TileSourceMetadata, TilesRuntime};
use anyhow::Result;
use async_trait::async_trait;
use std::{path::Path, sync::Arc};
use versatiles_core::{GeoBBox, TileBBox, TileBBoxPyramid, TileCompression, TileCoord, TileJSON, TileStream};
use versatiles_derive::context;
#[derive(Debug)]
pub struct TilesConverterParameters {
pub bbox_pyramid: Option<TileBBoxPyramid>,
pub geo_bbox: Option<GeoBBox>,
pub tile_compression: Option<TileCompression>,
pub flip_y: bool,
pub swap_xy: bool,
}
impl Default for TilesConverterParameters {
fn default() -> Self {
TilesConverterParameters {
bbox_pyramid: None,
geo_bbox: None,
tile_compression: None,
flip_y: false,
swap_xy: false,
}
}
}
#[context("Converting tiles from reader to file")]
pub async fn convert_tiles_container(
reader: SharedTileSource,
cp: TilesConverterParameters,
path: &Path,
runtime: TilesRuntime,
) -> Result<()> {
runtime.events().step("Starting conversion".to_string());
let converter = TilesConvertReader::new_from_reader(reader, cp)?;
runtime.write_to_path(converter.into_shared(), path).await?;
runtime.events().step("Conversion complete".to_string());
Ok(())
}
#[derive(Debug)]
pub struct TilesConvertReader {
reader: SharedTileSource,
converter_parameters: TilesConverterParameters,
reader_metadata: TileSourceMetadata,
tilejson: TileJSON,
}
impl TilesConvertReader {
#[context("Creating converter reader from existing reader")]
pub fn new_from_reader(reader: SharedTileSource, cp: TilesConverterParameters) -> Result<TilesConvertReader> {
let rp: TileSourceMetadata = reader.metadata().to_owned();
let mut new_rp: TileSourceMetadata = rp.clone();
if cp.flip_y {
new_rp.bbox_pyramid.flip_y();
}
if cp.swap_xy {
new_rp.bbox_pyramid.swap_xy();
}
if let Some(bbox_pyramid) = &cp.bbox_pyramid {
new_rp.bbox_pyramid.intersect(bbox_pyramid);
}
if let Some(tile_compression) = cp.tile_compression {
new_rp.tile_compression = tile_compression;
}
let mut tilejson = reader.tilejson().clone();
if let Some(ref geo_bbox) = cp.geo_bbox {
if let Some(ref mut bounds) = tilejson.bounds {
bounds.intersect(geo_bbox);
} else {
tilejson.bounds = Some(*geo_bbox);
}
tilejson.center = None;
}
new_rp.update_tilejson(&mut tilejson);
Ok(TilesConvertReader {
reader,
converter_parameters: cp,
reader_metadata: new_rp,
tilejson,
})
}
}
#[async_trait]
impl TileSource for TilesConvertReader {
fn source_type(&self) -> Arc<SourceType> {
SourceType::new_processor("TilesConvertReader", self.reader.source_type())
}
fn metadata(&self) -> &TileSourceMetadata {
&self.reader_metadata
}
fn tilejson(&self) -> &TileJSON {
&self.tilejson
}
async fn get_tile(&self, coord: &TileCoord) -> Result<Option<Tile>> {
let mut coord = *coord;
if self.converter_parameters.flip_y {
coord.flip_y();
}
if self.converter_parameters.swap_xy {
coord.swap_xy();
}
let tile = self.reader.get_tile(&coord).await?;
let Some(mut tile) = tile else { return Ok(None) };
if let Some(compression) = self.converter_parameters.tile_compression {
tile.change_compression(compression)?;
}
Ok(Some(tile))
}
async fn get_tile_stream(&self, mut bbox: TileBBox) -> Result<TileStream<'static, Tile>> {
if self.converter_parameters.swap_xy {
bbox.swap_xy();
}
if self.converter_parameters.flip_y {
bbox.flip_y();
}
let mut stream = self.reader.get_tile_stream(bbox).await?;
let flip_y = self.converter_parameters.flip_y;
let swap_xy = self.converter_parameters.swap_xy;
if flip_y || swap_xy {
stream = stream.map_coord(move |mut coord| {
if flip_y {
coord.flip_y();
}
if swap_xy {
coord.swap_xy();
}
coord
});
}
if let Some(tile_compression) = self.converter_parameters.tile_compression {
stream = stream
.map_parallel_try(move |_coord, mut tile| {
tile.change_compression(tile_compression)?;
Ok(tile)
})
.unwrap_results();
}
Ok(stream)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{MockReader, Traversal, VersaTilesReader};
use assert_fs::NamedTempFile;
use rstest::rstest;
use versatiles_core::{
TileCompression::*,
TileFormat::{self, *},
};
fn new_bbox(b: [u32; 4]) -> TileBBoxPyramid {
let mut pyramid = TileBBoxPyramid::new_empty();
pyramid.include_bbox(&TileBBox::from_min_and_max(3, b[0], b[1], b[2], b[3]).unwrap());
pyramid
}
fn get_mock_reader(tf: TileFormat, tc: TileCompression) -> SharedTileSource {
let bbox_pyramid = TileBBoxPyramid::new_full_up_to(4);
let reader_metadata = TileSourceMetadata::new(tf, tc, bbox_pyramid, Traversal::ANY);
MockReader::new_mock(reader_metadata).unwrap().into_shared()
}
#[rstest]
#[case(false, false, [2, 3, 4, 5], "23 33 43 24 34 25 35 44 45")]
#[case(false, true, [2, 3, 5, 4], "32 33 34 35 42 43 44 45")]
#[case(true, false, [2, 3, 4, 6], "24 34 44 23 33 22 32 21 31 43 42 41")]
#[case(true, true, [2, 3, 6, 4], "35 34 33 32 31 45 44 43 42 41")]
#[tokio::test]
async fn bbox_and_tile_order(
#[case] flip_y: bool,
#[case] swap_xy: bool,
#[case] bbox_out: [u32; 4],
#[case] tile_list: &str,
) -> Result<()> {
let pyramid_in = new_bbox([0, 1, 4, 5]);
let pyramid_convert = new_bbox([2, 3, 7, 7]);
let pyramid_out = new_bbox(bbox_out);
let reader_metadata = TileSourceMetadata::new(JSON, Uncompressed, pyramid_in, Traversal::ANY);
let reader = MockReader::new_mock(reader_metadata)?.into_shared();
let temp_file = NamedTempFile::new("test.versatiles")?;
let runtime = TilesRuntime::default();
let cp = TilesConverterParameters {
bbox_pyramid: Some(pyramid_convert),
geo_bbox: None,
flip_y,
swap_xy,
tile_compression: None,
};
convert_tiles_container(reader, cp, &temp_file, runtime.clone()).await?;
let reader_out = VersaTilesReader::open_path(&temp_file, runtime).await?;
let parameters_out = reader_out.metadata();
let tile_compression_out = parameters_out.tile_compression;
assert_eq!(parameters_out.bbox_pyramid, pyramid_out);
let bbox = pyramid_out.get_level_bbox(3);
let mut tiles: Vec<String> = Vec::new();
for coord in bbox.iter_coords_zorder() {
let mut text = reader_out
.get_tile(&coord)
.await?
.unwrap()
.into_blob(tile_compression_out)?
.to_string();
text = text
.replace("{\"z\":3,\"x\":", "")
.replace(",\"y\":", "")
.replace('}', "");
tiles.push(text);
}
let tiles = tiles.join(" ");
assert_eq!(tiles, tile_list);
Ok(())
}
#[test]
fn test_tiles_converter_parameters_new() {
let cp = TilesConverterParameters {
bbox_pyramid: Some(TileBBoxPyramid::new_full_up_to(1)),
geo_bbox: None,
flip_y: true,
swap_xy: true,
tile_compression: None,
};
assert!(cp.bbox_pyramid.is_some());
assert!(cp.flip_y);
assert!(cp.swap_xy);
}
#[test]
fn test_tiles_converter_parameters_default() {
let cp = TilesConverterParameters::default();
assert_eq!(cp.bbox_pyramid, None);
assert!(!cp.flip_y);
assert!(!cp.swap_xy);
}
#[test]
fn test_tiles_convert_reader_new_from_reader() {
let reader = get_mock_reader(MVT, Uncompressed);
let cp = TilesConverterParameters::default();
let tcr = TilesConvertReader::new_from_reader(reader, cp).unwrap();
assert_eq!(tcr.reader.source_type().to_string(), "container 'dummy' ('dummy')");
assert_eq!(tcr.source_type().to_string(), "processor 'TilesConvertReader'");
}
#[tokio::test]
async fn test_get_tile() -> Result<()> {
let reader = get_mock_reader(MVT, Uncompressed);
let cp = TilesConverterParameters::default();
let tcr = TilesConvertReader::new_from_reader(reader, cp)?;
let coord = TileCoord::new(0, 0, 0)?;
let data = tcr.get_tile(&coord).await?;
assert!(data.is_some());
Ok(())
}
#[tokio::test]
async fn test_flip_y_and_swap_xy() -> Result<()> {
let reader = get_mock_reader(MVT, Uncompressed);
let cp = TilesConverterParameters {
flip_y: true,
swap_xy: true,
..Default::default()
};
let tcr = TilesConvertReader::new_from_reader(reader, cp)?;
let mut coord = TileCoord::new(4, 5, 6)?;
let data = tcr.get_tile(&coord).await?;
assert!(data.is_some());
coord.flip_y();
coord.swap_xy();
let data_flipped = tcr.get_tile(&coord).await?;
assert_eq!(data, data_flipped);
Ok(())
}
#[test]
fn test_geo_bbox_intersects_existing_tilejson_bounds() {
let source_bounds = GeoBBox::new(10.0, 50.0, 15.0, 55.0).unwrap();
let bbox_pyramid = TileBBoxPyramid::new_full_up_to(4);
let reader_metadata = TileSourceMetadata::new(MVT, Uncompressed, bbox_pyramid, Traversal::ANY);
let mut reader = MockReader::new_mock(reader_metadata).unwrap();
reader.tilejson_mut().bounds = Some(source_bounds);
let reader = reader.into_shared();
let filter_bbox = GeoBBox::new(12.0, 52.0, 20.0, 60.0).unwrap();
let mut filter_pyramid = TileBBoxPyramid::new_full();
filter_pyramid.intersect_geo_bbox(&filter_bbox).unwrap();
let cp = TilesConverterParameters {
bbox_pyramid: Some(filter_pyramid),
geo_bbox: Some(filter_bbox),
..Default::default()
};
let tcr = TilesConvertReader::new_from_reader(reader, cp).unwrap();
let bounds = tcr.tilejson().bounds.unwrap();
assert_eq!(bounds.as_tuple(), (12.0, 52.0, 15.0, 55.0));
}
#[test]
fn test_geo_bbox_without_existing_bounds_uses_pyramid() {
let bbox_pyramid = TileBBoxPyramid::new_full_up_to(4);
let reader_metadata = TileSourceMetadata::new(MVT, Uncompressed, bbox_pyramid, Traversal::ANY);
let reader = MockReader::new_mock(reader_metadata).unwrap().into_shared();
let filter_bbox = GeoBBox::new(12.0, 52.0, 14.0, 54.0).unwrap();
let mut filter_pyramid = TileBBoxPyramid::new_full();
filter_pyramid.intersect_geo_bbox(&filter_bbox).unwrap();
let cp = TilesConverterParameters {
bbox_pyramid: Some(filter_pyramid),
geo_bbox: Some(filter_bbox),
..Default::default()
};
let tcr = TilesConvertReader::new_from_reader(reader, cp).unwrap();
assert!(tcr.tilejson().bounds.is_some());
}
}