use crate::geometry::FeatureCollection;
use rustial_math::TileId;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::SystemTime;
use thiserror::Error;
const RGBA8_BYTES_PER_PIXEL: usize = 4;
#[derive(Debug, Clone)]
pub struct RasterMipLevel {
pub width: u32,
pub height: u32,
pub data: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct RasterMipChain {
levels: Vec<RasterMipLevel>,
}
impl RasterMipChain {
#[inline]
pub fn levels(&self) -> &[RasterMipLevel] {
&self.levels
}
#[inline]
pub fn level_count(&self) -> u32 {
self.levels.len() as u32
}
#[inline]
pub fn byte_len(&self) -> usize {
self.levels.iter().map(|level| level.data.len()).sum()
}
pub fn into_bytes(self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(self.byte_len());
for level in self.levels {
bytes.extend_from_slice(&level.data);
}
bytes
}
}
#[derive(Debug, Clone, Error)]
pub enum TileError {
#[error("network error: {0}")]
Network(String),
#[error("decode error: {0}")]
Decode(String),
#[error("not found: tile {0:?}")]
NotFound(TileId),
#[error("{0}")]
Other(String),
}
#[derive(Debug, Clone)]
pub struct DecodedImage {
pub width: u32,
pub height: u32,
pub data: Arc<Vec<u8>>,
}
impl DecodedImage {
#[inline]
pub fn expected_len(&self) -> Option<usize> {
(self.width as usize)
.checked_mul(self.height as usize)?
.checked_mul(RGBA8_BYTES_PER_PIXEL)
}
#[inline]
pub fn byte_len(&self) -> usize {
self.data.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.width == 0 || self.height == 0 || self.data.is_empty()
}
pub fn validate_rgba8(&self) -> Result<(), TileError> {
if self.width == 0 || self.height == 0 {
return Err(TileError::Decode(format!(
"invalid raster dimensions: {}x{}",
self.width, self.height
)));
}
let Some(expected_len) = self.expected_len() else {
return Err(TileError::Decode(format!(
"image dimensions overflow byte length computation: {}x{}",
self.width, self.height
)));
};
if self.data.len() != expected_len {
return Err(TileError::Decode(format!(
"invalid RGBA8 payload length: got {}, expected {} for {}x{}",
self.data.len(),
expected_len,
self.width,
self.height
)));
}
Ok(())
}
pub fn build_mip_chain_rgba8(&self) -> Result<RasterMipChain, TileError> {
self.validate_rgba8()?;
let mut levels = Vec::new();
levels.push(RasterMipLevel {
width: self.width,
height: self.height,
data: self.data.to_vec(),
});
while let Some(prev) = levels.last() {
if prev.width == 1 && prev.height == 1 {
break;
}
levels.push(downsample_rgba8_level(prev)?);
}
Ok(RasterMipChain { levels })
}
}
fn downsample_rgba8_level(prev: &RasterMipLevel) -> Result<RasterMipLevel, TileError> {
let src_width = prev.width as usize;
let src_height = prev.height as usize;
let dst_width = (prev.width / 2).max(1);
let dst_height = (prev.height / 2).max(1);
let expected_len = src_width
.checked_mul(src_height)
.and_then(|px| px.checked_mul(RGBA8_BYTES_PER_PIXEL))
.ok_or_else(|| {
TileError::Decode(format!(
"mip source dimensions overflow byte length computation: {}x{}",
prev.width, prev.height
))
})?;
if prev.data.len() != expected_len {
return Err(TileError::Decode(format!(
"invalid mip source length: got {}, expected {} for {}x{}",
prev.data.len(),
expected_len,
prev.width,
prev.height
)));
}
let mut out = vec![0u8; dst_width as usize * dst_height as usize * RGBA8_BYTES_PER_PIXEL];
for y in 0..dst_height as usize {
for x in 0..dst_width as usize {
let sx0 = (x * 2).min(src_width - 1);
let sy0 = (y * 2).min(src_height - 1);
let sx1 = (sx0 + 1).min(src_width - 1);
let sy1 = (sy0 + 1).min(src_height - 1);
let taps = [(sx0, sy0), (sx1, sy0), (sx0, sy1), (sx1, sy1)];
let mut premul_r = 0.0f32;
let mut premul_g = 0.0f32;
let mut premul_b = 0.0f32;
let mut alpha = 0.0f32;
for (sx, sy) in taps {
let idx = (sy * src_width + sx) * RGBA8_BYTES_PER_PIXEL;
let a = prev.data[idx + 3] as f32 / 255.0;
premul_r += srgb8_to_linear(prev.data[idx]) * a;
premul_g += srgb8_to_linear(prev.data[idx + 1]) * a;
premul_b += srgb8_to_linear(prev.data[idx + 2]) * a;
alpha += a;
}
let sample_count = taps.len() as f32;
let out_idx = (y * dst_width as usize + x) * RGBA8_BYTES_PER_PIXEL;
let avg_alpha = alpha / sample_count;
if avg_alpha > 0.0 {
let inv_alpha = 1.0 / alpha.max(1e-6);
out[out_idx] = linear_to_srgb8((premul_r * inv_alpha).clamp(0.0, 1.0));
out[out_idx + 1] = linear_to_srgb8((premul_g * inv_alpha).clamp(0.0, 1.0));
out[out_idx + 2] = linear_to_srgb8((premul_b * inv_alpha).clamp(0.0, 1.0));
} else {
out[out_idx] = 0;
out[out_idx + 1] = 0;
out[out_idx + 2] = 0;
}
out[out_idx + 3] = ((avg_alpha.clamp(0.0, 1.0) * 255.0) + 0.5) as u8;
}
}
Ok(RasterMipLevel {
width: dst_width,
height: dst_height,
data: out,
})
}
use std::sync::LazyLock;
static SRGB_TO_LINEAR_LUT: LazyLock<[f32; 256]> = LazyLock::new(|| {
let mut lut = [0.0f32; 256];
for i in 0u32..256 {
let s = i as f64 / 255.0;
lut[i as usize] = if s <= 0.04045 {
(s / 12.92) as f32
} else {
((s + 0.055) / 1.055).powf(2.4) as f32
};
}
lut
});
static LINEAR_TO_SRGB_LUT: LazyLock<[u8; 4096]> = LazyLock::new(|| {
let mut lut = [0u8; 4096];
for i in 0u32..4096 {
let lin = i as f64 / 4095.0;
let s = if lin <= 0.0031308 {
lin * 12.92
} else {
1.055 * lin.powf(1.0 / 2.4) - 0.055
};
lut[i as usize] = ((s.clamp(0.0, 1.0) * 255.0) + 0.5) as u8;
}
lut
});
#[inline]
fn srgb8_to_linear(v: u8) -> f32 {
SRGB_TO_LINEAR_LUT[v as usize]
}
#[inline]
fn linear_to_srgb8(v: f32) -> u8 {
let idx = ((v * 4095.0) + 0.5) as usize;
LINEAR_TO_SRGB_LUT[if idx > 4095 { 4095 } else { idx }]
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TileFreshness {
pub expires_at: Option<SystemTime>,
pub etag: Option<String>,
pub last_modified: Option<String>,
}
impl TileFreshness {
#[inline]
pub fn is_expired_at(&self, now: SystemTime) -> bool {
self.expires_at.is_some_and(|expires_at| now >= expires_at)
}
#[inline]
pub fn is_expired(&self) -> bool {
self.is_expired_at(SystemTime::now())
}
}
#[derive(Debug, Clone, Default)]
pub struct RevalidationHint {
pub etag: Option<String>,
pub last_modified: Option<String>,
}
impl RevalidationHint {
#[inline]
pub fn has_validators(&self) -> bool {
self.etag.is_some() || self.last_modified.is_some()
}
}
#[derive(Debug, Clone)]
pub struct TileResponse {
pub data: TileData,
pub freshness: TileFreshness,
pub not_modified: bool,
}
impl TileResponse {
#[inline]
pub fn from_data(data: TileData) -> Self {
Self {
data,
freshness: TileFreshness::default(),
not_modified: false,
}
}
#[inline]
pub fn with_freshness(mut self, freshness: TileFreshness) -> Self {
self.freshness = freshness;
self
}
#[inline]
pub fn not_modified(freshness: TileFreshness) -> Self {
Self {
data: TileData::Raster(DecodedImage {
width: 0,
height: 0,
data: std::sync::Arc::new(Vec::new()),
}),
freshness,
not_modified: true,
}
}
}
impl From<TileData> for TileResponse {
#[inline]
fn from(value: TileData) -> Self {
Self::from_data(value)
}
}
#[derive(Debug, Clone)]
pub struct VectorTileData {
pub layers: HashMap<String, FeatureCollection>,
}
impl VectorTileData {
pub fn feature_count(&self) -> usize {
self.layers.values().map(|fc| fc.len()).sum()
}
#[inline]
pub fn layer_count(&self) -> usize {
self.layers.len()
}
pub fn is_empty(&self) -> bool {
self.layers.values().all(|fc| fc.is_empty())
}
pub fn layer(&self, name: &str) -> Option<&FeatureCollection> {
self.layers.get(name)
}
pub fn layer_names(&self) -> Vec<&str> {
self.layers.keys().map(String::as_str).collect()
}
pub fn approx_byte_len(&self) -> usize {
self.layers.values().map(|fc| fc.total_coords() * 16).sum()
}
}
#[derive(Debug, Clone)]
pub struct RawVectorPayload {
pub tile_id: TileId,
pub bytes: Arc<Vec<u8>>,
pub decode_options: crate::mvt::MvtDecodeOptions,
}
#[derive(Debug, Clone)]
pub enum TileData {
Raster(DecodedImage),
Vector(VectorTileData),
RawVector(RawVectorPayload),
}
impl TileData {
#[inline]
pub fn as_raster(&self) -> Option<&DecodedImage> {
match self {
Self::Raster(image) => Some(image),
Self::Vector(_) | Self::RawVector(_) => None,
}
}
#[inline]
pub fn as_vector(&self) -> Option<&VectorTileData> {
match self {
Self::Vector(data) => Some(data),
Self::Raster(_) | Self::RawVector(_) => None,
}
}
#[inline]
pub fn is_raster(&self) -> bool {
matches!(self, Self::Raster(_))
}
#[inline]
pub fn is_vector(&self) -> bool {
matches!(self, Self::Vector(_))
}
#[inline]
pub fn is_raw_vector(&self) -> bool {
matches!(self, Self::RawVector(_))
}
#[inline]
pub fn as_raw_vector(&self) -> Option<&RawVectorPayload> {
match self {
Self::RawVector(raw) => Some(raw),
_ => None,
}
}
#[inline]
pub fn dimensions(&self) -> (u32, u32) {
match self {
Self::Raster(image) => (image.width, image.height),
Self::Vector(_) | Self::RawVector(_) => (0, 0),
}
}
#[inline]
pub fn byte_len(&self) -> usize {
match self {
Self::Raster(image) => image.byte_len(),
Self::Vector(data) => data.approx_byte_len(),
Self::RawVector(raw) => raw.bytes.len(),
}
}
#[inline]
pub fn is_empty(&self) -> bool {
match self {
Self::Raster(image) => image.is_empty(),
Self::Vector(data) => data.is_empty(),
Self::RawVector(raw) => raw.bytes.is_empty(),
}
}
#[inline]
pub fn validate(&self) -> Result<(), TileError> {
match self {
Self::Raster(image) => image.validate_rgba8(),
Self::Vector(_) | Self::RawVector(_) => Ok(()),
}
}
}
pub trait TileDecoder: Send + Sync {
fn decode(&self, bytes: &[u8]) -> Result<DecodedImage, TileError>;
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TileSourceFailureDiagnostics {
pub transport_failures: u64,
pub http_status_failures: u64,
pub not_found_failures: u64,
pub decode_failures: u64,
pub timeout_failures: u64,
pub forced_cancellations: u64,
pub ignored_completed_responses: u64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TileSourceDiagnostics {
pub queued_requests: usize,
pub in_flight_requests: usize,
pub known_requests: usize,
pub cancelled_in_flight_requests: usize,
pub max_concurrent_requests: usize,
pub pending_decode_tasks: usize,
pub failure_diagnostics: TileSourceFailureDiagnostics,
}
pub trait TileSource: Send + Sync {
fn request(&self, id: TileId);
fn request_many(&self, ids: &[TileId]) {
for &id in ids {
self.request(id);
}
}
fn request_revalidate(&self, id: TileId, _hint: RevalidationHint) {
self.request(id);
}
fn request_revalidate_many(&self, ids: &[(TileId, RevalidationHint)]) {
for (id, hint) in ids {
self.request_revalidate(*id, hint.clone());
}
}
fn poll(&self) -> Vec<(TileId, Result<TileResponse, TileError>)>;
fn cancel(&self, _id: TileId) {}
fn cancel_many(&self, ids: &[TileId]) {
for &id in ids {
self.cancel(id);
}
}
fn diagnostics(&self) -> Option<TileSourceDiagnostics> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
#[derive(Default)]
struct RecordingSource {
requested: Mutex<Vec<TileId>>,
cancelled: Mutex<Vec<TileId>>,
}
impl TileSource for RecordingSource {
fn request(&self, id: TileId) {
self.requested.lock().unwrap().push(id);
}
fn poll(&self) -> Vec<(TileId, Result<TileResponse, TileError>)> {
Vec::new()
}
fn cancel(&self, id: TileId) {
self.cancelled.lock().unwrap().push(id);
}
}
#[test]
fn decoded_image_validation_accepts_valid_rgba8() {
let image = DecodedImage {
width: 2,
height: 2,
data: vec![255u8; 16].into(),
};
assert_eq!(image.expected_len(), Some(16));
assert_eq!(image.byte_len(), 16);
assert!(!image.is_empty());
assert!(image.validate_rgba8().is_ok());
}
#[test]
fn decoded_image_validation_rejects_invalid_length() {
let image = DecodedImage {
width: 2,
height: 2,
data: vec![255u8; 15].into(),
};
let err = image.validate_rgba8().expect_err("image should be invalid");
assert!(matches!(err, TileError::Decode(_)));
}
#[test]
fn tile_data_helpers_delegate_to_raster_payload() {
let tile = TileData::Raster(DecodedImage {
width: 1,
height: 2,
data: vec![1u8; 8].into(),
});
assert_eq!(tile.dimensions(), (1, 2));
assert_eq!(tile.byte_len(), 8);
assert!(tile.as_raster().is_some());
assert!(tile.as_vector().is_none());
assert!(tile.is_raster());
assert!(!tile.is_vector());
assert!(!tile.is_empty());
assert!(tile.validate().is_ok());
}
#[test]
fn tile_data_vector_variant() {
use crate::geometry::FeatureCollection;
let mut layers = HashMap::new();
layers.insert("water".to_string(), FeatureCollection::default());
let tile = TileData::Vector(VectorTileData { layers });
assert!(tile.as_vector().is_some());
assert!(tile.as_raster().is_none());
assert!(tile.is_vector());
assert!(!tile.is_raster());
assert_eq!(tile.dimensions(), (0, 0));
assert!(tile.is_empty());
assert!(tile.validate().is_ok());
}
#[test]
fn vector_tile_data_helpers() {
use crate::geometry::{Feature, FeatureCollection, Geometry, Point};
use rustial_math::GeoCoord;
let mut layers = HashMap::new();
layers.insert(
"places".to_string(),
FeatureCollection {
features: vec![Feature {
geometry: Geometry::Point(Point {
coord: GeoCoord::from_lat_lon(0.0, 0.0),
}),
properties: HashMap::new(),
}],
},
);
let vt = VectorTileData { layers };
assert_eq!(vt.feature_count(), 1);
assert_eq!(vt.layer_count(), 1);
assert!(!vt.is_empty());
assert!(vt.layer("places").is_some());
assert!(vt.layer("missing").is_none());
assert_eq!(vt.layer_names(), vec!["places"]);
assert!(vt.approx_byte_len() > 0);
}
#[test]
fn tile_source_batch_defaults_forward_in_order() {
let source = RecordingSource::default();
let ids = [
TileId::new(1, 0, 0),
TileId::new(1, 1, 0),
TileId::new(1, 0, 1),
];
source.request_many(&ids);
source.cancel_many(&ids[1..]);
assert_eq!(*source.requested.lock().unwrap(), ids);
assert_eq!(*source.cancelled.lock().unwrap(), ids[1..]);
}
#[test]
fn decoded_image_builds_full_mip_chain() {
let image = DecodedImage {
width: 4,
height: 2,
data: vec![255u8; 4 * 2 * 4].into(),
};
let mip_chain = image
.build_mip_chain_rgba8()
.expect("valid image should mipmap");
let dims: Vec<(u32, u32)> = mip_chain
.levels()
.iter()
.map(|level| (level.width, level.height))
.collect();
assert_eq!(dims, vec![(4, 2), (2, 1), (1, 1)]);
assert_eq!(mip_chain.level_count(), 3);
assert_eq!(mip_chain.byte_len(), 32 + 8 + 4);
}
#[test]
fn decoded_image_mip_chain_preserves_constant_opaque_color() {
let mut data = vec![0u8; 4 * 4 * 4];
for pixel in data.chunks_exact_mut(4) {
pixel.copy_from_slice(&[32, 96, 224, 255]);
}
let image = DecodedImage {
width: 4,
height: 4,
data: data.into(),
};
let mip_chain = image
.build_mip_chain_rgba8()
.expect("valid image should mipmap");
for level in mip_chain.levels() {
for pixel in level.data.chunks_exact(4) {
assert_eq!(pixel, [32, 96, 224, 255]);
}
}
}
#[test]
fn srgb_lut_roundtrip_is_within_one_lsb() {
for i in 0u16..=255 {
let lut_val = SRGB_TO_LINEAR_LUT[i as usize];
let s = i as f32 / 255.0;
let ref_val = if s <= 0.04045 {
s / 12.92
} else {
((s + 0.055) / 1.055).powf(2.4)
};
let err = (lut_val - ref_val).abs();
assert!(
err < 1e-6,
"srgb_to_linear LUT[{i}]: lut={lut_val}, ref={ref_val}, err={err}"
);
}
for i in 0u16..=255 {
let linear = SRGB_TO_LINEAR_LUT[i as usize];
let back = linear_to_srgb8(linear);
let diff = (back as i16 - i as i16).unsigned_abs();
assert!(
diff <= 1,
"roundtrip failed for {i}: got {back}, diff={diff}"
);
}
}
}