use crate::error::{Result, StreamingError};
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct TileCoordinate {
pub z: u8,
pub x: u32,
pub y: u32,
}
impl TileCoordinate {
pub fn new(z: u8, x: u32, y: u32) -> Self {
Self { z, x, y }
}
pub fn to_xyz_string(&self) -> String {
format!("{}/{}/{}", self.z, self.x, self.y)
}
pub fn to_tms(&self) -> Self {
let max_y = (1u32 << self.z) - 1;
Self {
z: self.z,
x: self.x,
y: max_y - self.y,
}
}
pub fn parent(&self) -> Option<Self> {
if self.z == 0 {
return None;
}
Some(Self {
z: self.z - 1,
x: self.x / 2,
y: self.y / 2,
})
}
pub fn children(&self) -> Vec<Self> {
if self.z >= 31 {
return vec![];
}
let z = self.z + 1;
let x = self.x * 2;
let y = self.y * 2;
vec![
Self::new(z, x, y),
Self::new(z, x + 1, y),
Self::new(z, x, y + 1),
Self::new(z, x + 1, y + 1),
]
}
pub fn is_valid(&self) -> bool {
if self.z > 31 {
return false;
}
let max_coord = 1u32 << self.z;
self.x < max_coord && self.y < max_coord
}
}
impl fmt::Display for TileCoordinate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}/{}", self.z, self.x, self.y)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TileRequest {
pub coord: TileCoordinate,
pub format: TileFormat,
pub layer: Option<String>,
pub style: Option<String>,
pub params: std::collections::HashMap<String, String>,
}
impl TileRequest {
pub fn new(coord: TileCoordinate, format: TileFormat) -> Self {
Self {
coord,
format,
layer: None,
style: None,
params: std::collections::HashMap::new(),
}
}
pub fn with_layer(mut self, layer: String) -> Self {
self.layer = Some(layer);
self
}
pub fn with_style(mut self, style: String) -> Self {
self.style = Some(style);
self
}
pub fn with_param(mut self, key: String, value: String) -> Self {
self.params.insert(key, value);
self
}
}
#[derive(Debug, Clone)]
pub struct TileResponse {
pub coord: TileCoordinate,
pub data: Bytes,
pub content_type: String,
pub cache_control: Option<String>,
pub etag: Option<String>,
pub last_modified: Option<chrono::DateTime<chrono::Utc>>,
}
impl TileResponse {
pub fn new(coord: TileCoordinate, data: Bytes, content_type: String) -> Self {
Self {
coord,
data,
content_type,
cache_control: None,
etag: None,
last_modified: None,
}
}
pub fn with_cache_control(mut self, cache_control: String) -> Self {
self.cache_control = Some(cache_control);
self
}
pub fn with_etag(mut self, etag: String) -> Self {
self.etag = Some(etag);
self
}
pub fn size_bytes(&self) -> usize {
self.data.len()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TileFormat {
Png,
Jpeg,
WebP,
Pbf,
GeoJson,
Json,
}
impl TileFormat {
pub fn mime_type(&self) -> &'static str {
match self {
TileFormat::Png => "image/png",
TileFormat::Jpeg => "image/jpeg",
TileFormat::WebP => "image/webp",
TileFormat::Pbf => "application/x-protobuf",
TileFormat::GeoJson => "application/geo+json",
TileFormat::Json => "application/json",
}
}
pub fn extension(&self) -> &'static str {
match self {
TileFormat::Png => "png",
TileFormat::Jpeg => "jpg",
TileFormat::WebP => "webp",
TileFormat::Pbf => "pbf",
TileFormat::GeoJson => "geojson",
TileFormat::Json => "json",
}
}
}
impl fmt::Display for TileFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.extension())
}
}
#[async_trait::async_trait]
pub trait TileProtocol: Send + Sync {
async fn get_tile(&self, request: &TileRequest) -> Result<TileResponse>;
async fn has_tile(&self, coord: &TileCoordinate) -> Result<bool>;
async fn get_tile_metadata(&self, coord: &TileCoordinate) -> Result<TileMetadata>;
fn zoom_levels(&self) -> (u8, u8);
fn tile_size(&self) -> (u32, u32);
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TileMetadata {
pub coord: TileCoordinate,
pub size_bytes: usize,
pub format: TileFormat,
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
pub modified_at: Option<chrono::DateTime<chrono::Utc>>,
pub bbox: Option<oxigdal_core::types::BoundingBox>,
pub metadata: std::collections::HashMap<String, String>,
}
pub struct XyzProtocol {
url_template: String,
min_zoom: u8,
max_zoom: u8,
tile_size: (u32, u32),
}
impl XyzProtocol {
pub fn new(url_template: String, min_zoom: u8, max_zoom: u8) -> Self {
Self {
url_template,
min_zoom,
max_zoom,
tile_size: (256, 256),
}
}
pub fn with_tile_size(mut self, width: u32, height: u32) -> Self {
self.tile_size = (width, height);
self
}
pub fn build_url(&self, coord: &TileCoordinate) -> String {
self.url_template
.replace("{z}", &coord.z.to_string())
.replace("{x}", &coord.x.to_string())
.replace("{y}", &coord.y.to_string())
}
}
#[async_trait::async_trait]
impl TileProtocol for XyzProtocol {
async fn get_tile(&self, request: &TileRequest) -> Result<TileResponse> {
if request.coord.z < self.min_zoom || request.coord.z > self.max_zoom {
return Err(StreamingError::InvalidOperation(
format!("Zoom level {} out of range", request.coord.z)
));
}
let data = Bytes::new();
Ok(TileResponse::new(
request.coord,
data,
request.format.mime_type().to_string(),
))
}
async fn has_tile(&self, coord: &TileCoordinate) -> Result<bool> {
Ok(coord.z >= self.min_zoom && coord.z <= self.max_zoom && coord.is_valid())
}
async fn get_tile_metadata(&self, coord: &TileCoordinate) -> Result<TileMetadata> {
Ok(TileMetadata {
coord: *coord,
size_bytes: 0,
format: TileFormat::Png,
created_at: None,
modified_at: None,
bbox: None,
metadata: std::collections::HashMap::new(),
})
}
fn zoom_levels(&self) -> (u8, u8) {
(self.min_zoom, self.max_zoom)
}
fn tile_size(&self) -> (u32, u32) {
self.tile_size
}
}
pub struct TmsProtocol {
inner: XyzProtocol,
}
impl TmsProtocol {
pub fn new(url_template: String, min_zoom: u8, max_zoom: u8) -> Self {
Self {
inner: XyzProtocol::new(url_template, min_zoom, max_zoom),
}
}
}
#[async_trait::async_trait]
impl TileProtocol for TmsProtocol {
async fn get_tile(&self, request: &TileRequest) -> Result<TileResponse> {
let tms_coord = request.coord.to_tms();
let tms_request = TileRequest {
coord: tms_coord,
..request.clone()
};
self.inner.get_tile(&tms_request).await
}
async fn has_tile(&self, coord: &TileCoordinate) -> Result<bool> {
let tms_coord = coord.to_tms();
self.inner.has_tile(&tms_coord).await
}
async fn get_tile_metadata(&self, coord: &TileCoordinate) -> Result<TileMetadata> {
let tms_coord = coord.to_tms();
self.inner.get_tile_metadata(&tms_coord).await
}
fn zoom_levels(&self) -> (u8, u8) {
self.inner.zoom_levels()
}
fn tile_size(&self) -> (u32, u32) {
self.inner.tile_size()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tile_coordinate() {
let coord = TileCoordinate::new(10, 512, 384);
assert_eq!(coord.z, 10);
assert_eq!(coord.x, 512);
assert_eq!(coord.y, 384);
assert!(coord.is_valid());
}
#[test]
fn test_tile_parent() {
let coord = TileCoordinate::new(10, 512, 384);
let parent = coord.parent();
assert!(parent.is_some());
let parent = parent.expect("parent tile should exist for non-zero zoom level");
assert_eq!(parent.z, 9);
assert_eq!(parent.x, 256);
assert_eq!(parent.y, 192);
}
#[test]
fn test_tile_children() {
let coord = TileCoordinate::new(10, 512, 384);
let children = coord.children();
assert_eq!(children.len(), 4);
assert_eq!(children[0], TileCoordinate::new(11, 1024, 768));
assert_eq!(children[1], TileCoordinate::new(11, 1025, 768));
assert_eq!(children[2], TileCoordinate::new(11, 1024, 769));
assert_eq!(children[3], TileCoordinate::new(11, 1025, 769));
}
#[test]
fn test_tms_conversion() {
let coord = TileCoordinate::new(10, 512, 384);
let tms = coord.to_tms();
assert_eq!(tms.z, 10);
assert_eq!(tms.x, 512);
assert_eq!(tms.y, 639); }
#[test]
fn test_tile_format() {
assert_eq!(TileFormat::Png.mime_type(), "image/png");
assert_eq!(TileFormat::Jpeg.extension(), "jpg");
assert_eq!(TileFormat::WebP.to_string(), "webp");
}
}