use super::cache::{CacheStats, CachedTile, TileCache, TileFormat, TileKey, TilePrefetcher};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PushRel {
Preload,
Prefetch,
Preconnect,
}
impl PushRel {
fn as_str(&self) -> &'static str {
match self {
PushRel::Preload => "preload",
PushRel::Prefetch => "prefetch",
PushRel::Preconnect => "preconnect",
}
}
}
#[derive(Debug, Clone)]
pub struct PushHint {
pub url: String,
pub rel: PushRel,
pub type_: Option<String>,
pub as_: Option<String>,
pub crossorigin: bool,
pub nopush: bool,
}
impl PushHint {
pub fn new(url: impl Into<String>, rel: PushRel) -> Self {
Self {
url: url.into(),
rel,
type_: None,
as_: None,
crossorigin: false,
nopush: false,
}
}
pub fn preload_tile(url: impl Into<String>, format: &TileFormat) -> Self {
let as_ = match format {
TileFormat::Png | TileFormat::Jpeg | TileFormat::Webp => "image",
TileFormat::Mvt | TileFormat::Json => "fetch",
};
let type_ = format.content_type().to_owned();
Self {
url: url.into(),
rel: PushRel::Preload,
type_: Some(type_),
as_: Some(as_.to_owned()),
crossorigin: false,
nopush: false,
}
}
#[must_use]
pub fn to_link_header(&self) -> String {
let mut s = format!("<{}>; rel={}", self.url, self.rel.as_str());
if let Some(ref as_) = self.as_ {
s.push_str(&format!("; as={as_}"));
}
if let Some(ref type_) = self.type_ {
s.push_str(&format!("; type=\"{type_}\""));
}
if self.crossorigin {
s.push_str("; crossorigin");
}
if self.nopush {
s.push_str("; nopush");
}
s
}
}
pub struct PushPolicy {
pub max_push_count: u8,
pub min_zoom: u8,
pub max_zoom: u8,
pub formats: Vec<TileFormat>,
pub base_url: String,
}
impl PushPolicy {
pub fn new(base_url: impl Into<String>) -> Self {
Self {
max_push_count: 8,
min_zoom: 0,
max_zoom: 22,
formats: vec![TileFormat::Mvt],
base_url: base_url.into(),
}
}
pub fn generate_hints(&self, requested: &TileKey) -> Vec<PushHint> {
let prefetcher = TilePrefetcher::new(1);
let neighbours = prefetcher.neighbors(requested);
let mut hints = Vec::new();
for neighbour in neighbours {
if hints.len() >= self.max_push_count as usize {
break;
}
if neighbour.z < self.min_zoom || neighbour.z > self.max_zoom {
continue;
}
if !self.formats.contains(&neighbour.format) {
continue;
}
let url = format!(
"{}/{}",
self.base_url.trim_end_matches('/'),
neighbour.path_string()
);
hints.push(PushHint::preload_tile(url, &neighbour.format));
}
hints
}
#[must_use]
pub fn to_link_header_value(hints: &[PushHint]) -> String {
hints
.iter()
.map(PushHint::to_link_header)
.collect::<Vec<_>>()
.join(", ")
}
#[must_use]
pub fn parse_tile_url(url: &str, base_url: &str) -> Option<TileKey> {
let base = base_url.trim_end_matches('/');
let path = url.strip_prefix(base)?.trim_start_matches('/');
let parts: Vec<&str> = path.splitn(4, '/').collect();
if parts.len() != 4 {
return None;
}
let layer = parts[0];
let z: u8 = parts[1].parse().ok()?;
let x: u32 = parts[2].parse().ok()?;
let (y_str, ext) = parts[3].rsplit_once('.')?;
let y: u32 = y_str.parse().ok()?;
let format = match ext {
"mvt" => TileFormat::Mvt,
"png" => TileFormat::Png,
"jpg" => TileFormat::Jpeg,
"webp" => TileFormat::Webp,
"json" => TileFormat::Json,
_ => return None,
};
Some(TileKey::new(z, x, y, layer, format))
}
}
pub struct ETagValidator;
impl ETagValidator {
#[must_use]
pub fn check_none_match(if_none_match: &str, tile_etag: &str) -> bool {
let trimmed = if_none_match.trim();
if trimmed == "*" {
return false; }
let list = Self::parse_etag_list(trimmed);
let normalized = Self::normalize_etag(tile_etag);
!list.iter().any(|e| Self::normalize_etag(e) == normalized)
}
#[must_use]
pub fn check_match(if_match: &str, tile_etag: &str) -> bool {
let trimmed = if_match.trim();
if trimmed == "*" {
return true;
}
let list = Self::parse_etag_list(trimmed);
let normalized = Self::normalize_etag(tile_etag);
list.iter().any(|e| Self::normalize_etag(e) == normalized)
}
#[must_use]
pub fn parse_etag_list(header: &str) -> Vec<String> {
header
.split(',')
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
.collect()
}
#[must_use]
pub fn is_weak(etag: &str) -> bool {
etag.starts_with("W/")
}
fn normalize_etag(etag: &str) -> &str {
etag.strip_prefix("W/").unwrap_or(etag)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TileResponseStatus {
Ok,
NotModified,
NotFound,
}
#[derive(Debug)]
pub struct TileResponse {
pub status: TileResponseStatus,
pub data: Option<Vec<u8>>,
pub headers: Vec<(String, String)>,
pub push_hints: Vec<PushHint>,
}
pub struct TileServer {
pub cache: TileCache,
pub push_policy: PushPolicy,
pub prefetcher: TilePrefetcher,
}
impl TileServer {
pub fn new(base_url: impl Into<String>) -> Self {
let base_url = base_url.into();
Self {
cache: TileCache::new(1024, 256 * 1024 * 1024),
push_policy: PushPolicy::new(base_url),
prefetcher: TilePrefetcher::new(1),
}
}
pub fn serve(&mut self, key: &TileKey, if_none_match: Option<&str>, now: u64) -> TileResponse {
let cached: Option<(Vec<u8>, String)> = self
.cache
.get(key, now)
.map(|t: &CachedTile| (t.data.clone(), t.etag.clone()));
match cached {
None => TileResponse {
status: TileResponseStatus::NotFound,
data: None,
headers: vec![],
push_hints: vec![],
},
Some((data, etag)) => {
if let Some(inm) = if_none_match {
if !ETagValidator::check_none_match(inm, &etag) {
let headers = vec![
("ETag".to_owned(), etag),
(
"Cache-Control".to_owned(),
"public, max-age=3600".to_owned(),
),
];
return TileResponse {
status: TileResponseStatus::NotModified,
data: None,
headers,
push_hints: vec![],
};
}
}
let content_type = key.content_type().to_owned();
let headers = vec![
(
"Cache-Control".to_owned(),
"public, max-age=3600".to_owned(),
),
("ETag".to_owned(), etag),
("Content-Type".to_owned(), content_type),
("Vary".to_owned(), "Accept-Encoding".to_owned()),
];
let push_hints = self.push_policy.generate_hints(key);
TileResponse {
status: TileResponseStatus::Ok,
data: Some(data),
headers,
push_hints,
}
}
}
}
pub fn cache_tile(&mut self, key: TileKey, data: Vec<u8>, now: u64) {
let tile = CachedTile::new(key, data, now);
self.cache.insert(tile);
}
#[must_use]
pub fn cache_stats(&self) -> CacheStats {
self.cache.stats()
}
}