#[cfg(feature = "tiles")]
mod archive;
#[cfg(feature = "tiles")]
pub(crate) mod clip;
#[cfg(feature = "tiles")]
pub(crate) mod mbtiles;
#[cfg(feature = "tiles")]
pub(crate) mod mvt;
#[cfg(feature = "tiles")]
pub(crate) mod pmtiles;
#[cfg(feature = "tiles")]
pub(crate) mod raster;
#[cfg(feature = "tiles")]
pub(crate) mod source;
#[cfg(feature = "tiles")]
pub(crate) mod valid;
#[cfg(feature = "tiles")]
pub(crate) mod vector;
#[cfg(feature = "tiles")]
pub use archive::*;
use crate::{Error, Result};
pub(crate) const MAX_GM: f64 = 6378137.0 * std::f64::consts::PI;
pub(crate) fn lonlat_to_mercator(lon: f64, lat: f64) -> (f64, f64) {
let r = 6378137.0;
(r * lon / 180.0 * std::f64::consts::PI, r * (std::f64::consts::FRAC_PI_4 + 0.5 * lat / 180.0 * std::f64::consts::PI).tan().ln())
}
pub(crate) fn mercator_to_lonlat(x: f64, y: f64) -> (f64, f64) {
let r = 6378137.0;
(x / r * 180.0 / std::f64::consts::PI, (2.0 * (y / r).exp().atan() - std::f64::consts::FRAC_PI_2) * 180.0 / std::f64::consts::PI)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TileFormat {
Mvt,
Png,
Jpeg,
Webp,
Avif,
Unknown,
}
impl TileFormat {
pub fn name(self) -> &'static str {
match self {
TileFormat::Mvt => "pbf",
TileFormat::Png => "png",
TileFormat::Jpeg => "jpg",
TileFormat::Webp => "webp",
TileFormat::Avif => "avif",
TileFormat::Unknown => "unknown",
}
}
fn from_mbtiles(s: &str) -> Self {
match s.trim().to_ascii_lowercase().as_str() {
"pbf" | "mvt" => TileFormat::Mvt,
"png" => TileFormat::Png,
"jpg" | "jpeg" => TileFormat::Jpeg,
"webp" => TileFormat::Webp,
"avif" => TileFormat::Avif,
_ => TileFormat::Unknown,
}
}
fn sniff(b: &[u8]) -> Self {
if b.starts_with(b"\x89PNG") {
TileFormat::Png
} else if b.starts_with(&[0xff, 0xd8]) {
TileFormat::Jpeg
} else if b.len() > 12 && &b[0..4] == b"RIFF" && &b[8..12] == b"WEBP" {
TileFormat::Webp
} else if is_avif(b) {
TileFormat::Avif
} else if b.starts_with(&[0x1f, 0x8b]) || b.first() == Some(&0x1a) {
TileFormat::Mvt
} else {
TileFormat::Unknown
}
}
}
pub(crate) fn is_avif(b: &[u8]) -> bool {
if b.len() < 16 || &b[4..8] != b"ftyp" {
return false;
}
let size = (u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as usize).min(b.len());
std::iter::once(&b[8..12])
.chain(b.get(16..size).unwrap_or_default().as_chunks::<4>().0.iter().map(|c| &c[..]))
.any(|brand| brand == b"avif" || brand == b"avis")
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TileOpenOptions {
pub zoom_level: Option<u8>,
pub clip: Option<bool>,
}
impl TileOpenOptions {
pub fn set(&mut self, key: &str, value: &str) -> Result<bool> {
match key.to_ascii_uppercase().as_str() {
"ZOOM_LEVEL" => {
self.zoom_level = Some(value.trim().parse().map_err(|_| Error::InvalidArgument(format!("invalid ZOOM_LEVEL '{value}'")))?)
}
"CLIP" => self.clip = Some(parse_bool(value)?),
_ => return Ok(false),
}
Ok(true)
}
}
pub(crate) fn parse_bool(v: &str) -> Result<bool> {
match v.trim().to_ascii_uppercase().as_str() {
"YES" | "TRUE" | "ON" | "1" => Ok(true),
"NO" | "FALSE" | "OFF" | "0" => Ok(false),
_ => Err(Error::InvalidArgument(format!("invalid boolean '{v}'"))),
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TileWriteOptions {
pub min_zoom: Option<u8>,
pub max_zoom: Option<u8>,
pub zoom_level_strategy: String,
pub resampling: Option<String>,
pub tile_format: Option<String>,
pub quality: u8,
pub name: Option<String>,
pub description: Option<String>,
pub tileset_type: String,
pub extent: u32,
pub buffer: u32,
pub simplification: f64,
pub simplification_max_zoom: Option<f64>,
pub max_size: usize,
pub max_features: usize,
}
impl Default for TileWriteOptions {
fn default() -> Self {
Self {
min_zoom: None,
max_zoom: None,
zoom_level_strategy: "AUTO".into(),
resampling: None,
tile_format: None,
quality: 75,
name: None,
description: None,
tileset_type: "overlay".into(),
extent: 4096,
buffer: 80,
simplification: 0.0,
simplification_max_zoom: None,
max_size: 500_000,
max_features: 200_000,
}
}
}
impl TileWriteOptions {
pub fn set(&mut self, key: &str, value: &str) -> Result<bool> {
let num = |v: &str| v.trim().parse::<f64>().map_err(|_| Error::InvalidArgument(format!("invalid value '{v}' for {key}")));
let zoom = |v: &str| {
v.trim().parse::<u8>().ok().filter(|z| *z <= 30).ok_or_else(|| Error::InvalidArgument(format!("invalid zoom level '{v}'")))
};
match key.to_ascii_uppercase().as_str() {
"MINZOOM" => self.min_zoom = Some(zoom(value)?),
"MAXZOOM" => self.max_zoom = Some(zoom(value)?),
"ZOOM_LEVEL_STRATEGY" => self.zoom_level_strategy = value.to_ascii_uppercase(),
"RESAMPLING" => self.resampling = Some(value.to_string()),
"TILE_FORMAT" => self.tile_format = Some(value.to_ascii_uppercase()),
"QUALITY" => self.quality = num(value)?.clamp(1.0, 100.0) as u8,
"NAME" => self.name = Some(value.to_string()),
"DESCRIPTION" => self.description = Some(value.to_string()),
"TYPE" => self.tileset_type = value.to_string(),
"EXTENT" => self.extent = num(value)?.max(1.0) as u32,
"BUFFER" => self.buffer = num(value)?.max(0.0) as u32,
"SIMPLIFICATION" => self.simplification = num(value)?,
"SIMPLIFICATION_MAX_ZOOM" => self.simplification_max_zoom = Some(num(value)?),
"MAX_SIZE" => self.max_size = num(value)?.max(1.0) as usize,
"MAX_FEATURES" => self.max_features = num(value)?.max(1.0) as usize,
"FORMAT" | "COMPRESS" | "BLOCKSIZE" | "ZLEVEL" | "BOUNDS" | "WRITE_BOUNDS" => {}
_ => return Ok(false),
}
Ok(true)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArchiveKind {
PMTiles,
MBTiles,
}
pub(crate) fn archive_kind(path: &str) -> Option<ArchiveKind> {
let lower = path.split(['?', '#']).next().unwrap_or(path).to_ascii_lowercase();
if lower.ends_with(".pmtiles") {
Some(ArchiveKind::PMTiles)
} else if lower.ends_with(".mbtiles") {
Some(ArchiveKind::MBTiles)
} else {
None
}
}