use crate::config::PmtilesStoreCfg;
use crate::store::{TileReader, TileStoreError, TileWriter};
use async_trait::async_trait;
use bbox_core::{Compression, Format, TileResponse};
use log::{debug, info};
use martin_mbtiles::Metadata;
use pmtiles::async_reader::AsyncPmTilesReader;
use pmtiles::mmap::MmapBackend;
use pmtiles2::{util::tile_id, Compression as PmCompression, PMTiles, TileType};
use serde_json::json;
use std::ffi::OsStr;
use std::fs::File;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use tile_grid::Xyz;
pub struct PmtilesStoreReader {
pub path: PathBuf,
reader: AsyncPmTilesReader<MmapBackend>,
}
#[derive(Debug)]
pub struct PmtilesStoreWriter {
path: PathBuf,
format: Format,
metadata: Metadata,
archive: Option<PMTiles<Cursor<Vec<u8>>>>,
}
impl Clone for PmtilesStoreReader {
fn clone(&self) -> Self {
futures::executor::block_on(async {
Self::create_reader(self.path.clone()).await.expect("clone")
})
}
}
impl Clone for PmtilesStoreWriter {
fn clone(&self) -> Self {
Self::new(self.path.clone(), self.metadata.clone(), &self.format)
}
}
impl PmtilesStoreReader {
pub async fn create_reader(path: PathBuf) -> Result<Self, TileStoreError> {
let reader = AsyncPmTilesReader::new_with_path(&path).await?;
Ok(Self { path, reader })
}
pub async fn from_config(cfg: &PmtilesStoreCfg) -> Result<Self, TileStoreError> {
Self::create_reader(cfg.path.clone()).await
}
pub fn config_from_cli_arg(file_or_url: &str) -> Option<PmtilesStoreCfg> {
match Path::new(file_or_url).extension().and_then(OsStr::to_str) {
Some("pmtiles") => {
let cfg = PmtilesStoreCfg {
path: file_or_url.into(),
};
Some(cfg)
}
_ => None,
}
}
pub async fn get_metadata(&self) -> Result<String, ::pmtiles::error::Error> {
self.reader.get_metadata().await
}
}
#[async_trait]
impl TileReader for PmtilesStoreReader {
async fn get_tile(&self, xyz: &Xyz) -> Result<Option<TileResponse>, TileStoreError> {
let resp = if let Some(tile) = self.reader.get_tile(xyz.z, xyz.x, xyz.y).await {
let mut response = TileResponse::new();
response.set_content_type(tile.tile_type.content_type());
if let Some(encoding) = tile.tile_compression.content_encoding() {
response.insert_header(("Content-Encoding", encoding.to_lowercase()));
}
Some(response.with_body(Box::new(Cursor::new(tile.data))))
} else {
None
};
Ok(resp)
}
}
#[async_trait]
impl TileWriter for PmtilesStoreWriter {
fn compression(&self) -> Compression {
match self.archive.as_ref().expect("initialized").tile_compression {
PmCompression::GZip => Compression::Gzip,
_ => Compression::None,
}
}
async fn exists(&self, _xyz: &Xyz) -> bool {
false
}
async fn put_tile(&self, _xyz: &Xyz, _data: Vec<u8>) -> Result<(), TileStoreError> {
Err(TileStoreError::ReadOnly)
}
async fn put_tile_mut(&mut self, xyz: &Xyz, data: Vec<u8>) -> Result<(), TileStoreError> {
self.archive
.as_mut()
.expect("initialized")
.add_tile(tile_id(xyz.z, xyz.x, xyz.y), data);
debug!(
"put_tile_mut - num_tiles: {}",
self.archive.as_ref().expect("initialized").num_tiles()
);
Ok(())
}
fn finalize(&mut self) -> Result<(), TileStoreError> {
info!("Writing {}", self.path.display());
let mut file = File::create(&self.path)
.map_err(|e| TileStoreError::FileError(self.path.clone(), e))?;
if let Some(archive) = self.archive.take() {
info!("Number of tiles: {}", archive.num_tiles(),);
archive
.to_writer(&mut file)
.map_err(|e| TileStoreError::FileError(self.path.clone(), e))?;
}
Ok(())
}
}
impl PmtilesStoreWriter {
pub fn new(path: PathBuf, metadata: Metadata, format: &Format) -> Self {
let mut archive = PMTiles::default();
archive.tile_type = match format {
Format::Jpeg => TileType::Jpeg,
Format::Mvt => TileType::Mvt,
Format::Png => TileType::Png,
Format::Webp => TileType::WebP,
_ => TileType::Unknown,
};
archive.tile_compression = if *format == Format::Mvt {
PmCompression::GZip
} else {
PmCompression::None
};
if let Some(minzoom) = metadata.tilejson.minzoom {
archive.min_zoom = minzoom;
}
if let Some(maxzoom) = metadata.tilejson.maxzoom {
archive.max_zoom = maxzoom;
}
if let Some(bounds) = metadata.tilejson.bounds {
archive.min_longitude = bounds.left;
archive.min_latitude = bounds.bottom;
archive.max_longitude = bounds.right;
archive.max_latitude = bounds.top;
}
if let Some(center) = metadata.tilejson.center {
archive.center_longitude = center.longitude;
archive.center_latitude = center.latitude;
archive.center_zoom = center.zoom;
}
let mut meta_data = json!({
"name": &metadata.id, "description": &metadata.tilejson.description, "attribution": &metadata.tilejson.attribution
});
if let Some(vector_layers) = &metadata.tilejson.vector_layers {
meta_data["vector_layers"] = json!(vector_layers);
}
archive.meta_data = Some(meta_data);
Self {
path,
metadata,
format: *format,
archive: Some(archive),
}
}
pub fn from_config(cfg: &PmtilesStoreCfg, metadata: Metadata, format: &Format) -> Self {
Self::new(cfg.path.clone(), metadata, format)
}
}