1use crate::geometry::FeatureCollection;
4use rustial_math::TileId;
5use std::collections::HashMap;
6use std::sync::Arc;
7use std::time::SystemTime;
8use thiserror::Error;
9
10const RGBA8_BYTES_PER_PIXEL: usize = 4;
12
13#[derive(Debug, Clone)]
15pub struct RasterMipLevel {
16 pub width: u32,
18 pub height: u32,
20 pub data: Vec<u8>,
22}
23
24#[derive(Debug, Clone)]
26pub struct RasterMipChain {
27 levels: Vec<RasterMipLevel>,
28}
29
30impl RasterMipChain {
31 #[inline]
33 pub fn levels(&self) -> &[RasterMipLevel] {
34 &self.levels
35 }
36
37 #[inline]
39 pub fn level_count(&self) -> u32 {
40 self.levels.len() as u32
41 }
42
43 #[inline]
45 pub fn byte_len(&self) -> usize {
46 self.levels.iter().map(|level| level.data.len()).sum()
47 }
48
49 pub fn into_bytes(self) -> Vec<u8> {
54 let mut bytes = Vec::with_capacity(self.byte_len());
55 for level in self.levels {
56 bytes.extend_from_slice(&level.data);
57 }
58 bytes
59 }
60}
61
62#[derive(Debug, Clone, Error)]
64pub enum TileError {
65 #[error("network error: {0}")]
67 Network(String),
68 #[error("decode error: {0}")]
70 Decode(String),
71 #[error("not found: tile {0:?}")]
73 NotFound(TileId),
74 #[error("{0}")]
76 Other(String),
77}
78
79#[derive(Debug, Clone)]
85pub struct DecodedImage {
86 pub width: u32,
88 pub height: u32,
90 pub data: Arc<Vec<u8>>,
94}
95
96impl DecodedImage {
97 #[inline]
101 pub fn expected_len(&self) -> Option<usize> {
102 (self.width as usize)
103 .checked_mul(self.height as usize)?
104 .checked_mul(RGBA8_BYTES_PER_PIXEL)
105 }
106
107 #[inline]
109 pub fn byte_len(&self) -> usize {
110 self.data.len()
111 }
112
113 #[inline]
115 pub fn is_empty(&self) -> bool {
116 self.width == 0 || self.height == 0 || self.data.is_empty()
117 }
118
119 pub fn validate_rgba8(&self) -> Result<(), TileError> {
121 if self.width == 0 || self.height == 0 {
122 return Err(TileError::Decode(format!(
123 "invalid raster dimensions: {}x{}",
124 self.width, self.height
125 )));
126 }
127
128 let Some(expected_len) = self.expected_len() else {
129 return Err(TileError::Decode(format!(
130 "image dimensions overflow byte length computation: {}x{}",
131 self.width, self.height
132 )));
133 };
134
135 if self.data.len() != expected_len {
136 return Err(TileError::Decode(format!(
137 "invalid RGBA8 payload length: got {}, expected {} for {}x{}",
138 self.data.len(),
139 expected_len,
140 self.width,
141 self.height
142 )));
143 }
144
145 Ok(())
146 }
147
148 pub fn build_mip_chain_rgba8(&self) -> Result<RasterMipChain, TileError> {
154 self.validate_rgba8()?;
155
156 let mut levels = Vec::new();
157 levels.push(RasterMipLevel {
158 width: self.width,
159 height: self.height,
160 data: self.data.to_vec(),
161 });
162
163 while let Some(prev) = levels.last() {
164 if prev.width == 1 && prev.height == 1 {
165 break;
166 }
167
168 levels.push(downsample_rgba8_level(prev)?);
169 }
170
171 Ok(RasterMipChain { levels })
172 }
173}
174
175fn downsample_rgba8_level(prev: &RasterMipLevel) -> Result<RasterMipLevel, TileError> {
176 let src_width = prev.width as usize;
177 let src_height = prev.height as usize;
178 let dst_width = (prev.width / 2).max(1);
179 let dst_height = (prev.height / 2).max(1);
180
181 let expected_len = src_width
182 .checked_mul(src_height)
183 .and_then(|px| px.checked_mul(RGBA8_BYTES_PER_PIXEL))
184 .ok_or_else(|| TileError::Decode(format!(
185 "mip source dimensions overflow byte length computation: {}x{}",
186 prev.width, prev.height
187 )))?;
188
189 if prev.data.len() != expected_len {
190 return Err(TileError::Decode(format!(
191 "invalid mip source length: got {}, expected {} for {}x{}",
192 prev.data.len(),
193 expected_len,
194 prev.width,
195 prev.height
196 )));
197 }
198
199 let mut out = vec![0u8; dst_width as usize * dst_height as usize * RGBA8_BYTES_PER_PIXEL];
200
201 for y in 0..dst_height as usize {
202 for x in 0..dst_width as usize {
203 let sx0 = (x * 2).min(src_width - 1);
204 let sy0 = (y * 2).min(src_height - 1);
205 let sx1 = (sx0 + 1).min(src_width - 1);
206 let sy1 = (sy0 + 1).min(src_height - 1);
207
208 let taps = [(sx0, sy0), (sx1, sy0), (sx0, sy1), (sx1, sy1)];
209 let mut premul_r = 0.0f32;
210 let mut premul_g = 0.0f32;
211 let mut premul_b = 0.0f32;
212 let mut alpha = 0.0f32;
213
214 for (sx, sy) in taps {
215 let idx = (sy * src_width + sx) * RGBA8_BYTES_PER_PIXEL;
216 let a = prev.data[idx + 3] as f32 / 255.0;
217 premul_r += srgb8_to_linear(prev.data[idx]) * a;
218 premul_g += srgb8_to_linear(prev.data[idx + 1]) * a;
219 premul_b += srgb8_to_linear(prev.data[idx + 2]) * a;
220 alpha += a;
221 }
222
223 let sample_count = taps.len() as f32;
224 let out_idx = (y * dst_width as usize + x) * RGBA8_BYTES_PER_PIXEL;
225 let avg_alpha = alpha / sample_count;
226
227 if avg_alpha > 0.0 {
228 let inv_alpha = 1.0 / alpha.max(1e-6);
229 out[out_idx] = linear_to_srgb8((premul_r * inv_alpha).clamp(0.0, 1.0));
230 out[out_idx + 1] = linear_to_srgb8((premul_g * inv_alpha).clamp(0.0, 1.0));
231 out[out_idx + 2] = linear_to_srgb8((premul_b * inv_alpha).clamp(0.0, 1.0));
232 } else {
233 out[out_idx] = 0;
234 out[out_idx + 1] = 0;
235 out[out_idx + 2] = 0;
236 }
237 out[out_idx + 3] = ((avg_alpha.clamp(0.0, 1.0) * 255.0) + 0.5) as u8;
238 }
239 }
240
241 Ok(RasterMipLevel {
242 width: dst_width,
243 height: dst_height,
244 data: out,
245 })
246}
247
248use std::sync::LazyLock;
258
259static SRGB_TO_LINEAR_LUT: LazyLock<[f32; 256]> = LazyLock::new(|| {
261 let mut lut = [0.0f32; 256];
262 for i in 0u32..256 {
263 let s = i as f64 / 255.0;
264 lut[i as usize] = if s <= 0.04045 {
265 (s / 12.92) as f32
266 } else {
267 ((s + 0.055) / 1.055).powf(2.4) as f32
268 };
269 }
270 lut
271});
272
273static LINEAR_TO_SRGB_LUT: LazyLock<[u8; 4096]> = LazyLock::new(|| {
277 let mut lut = [0u8; 4096];
278 for i in 0u32..4096 {
279 let lin = i as f64 / 4095.0;
280 let s = if lin <= 0.0031308 {
281 lin * 12.92
282 } else {
283 1.055 * lin.powf(1.0 / 2.4) - 0.055
284 };
285 lut[i as usize] = ((s.clamp(0.0, 1.0) * 255.0) + 0.5) as u8;
286 }
287 lut
288});
289
290#[inline]
291fn srgb8_to_linear(v: u8) -> f32 {
292 SRGB_TO_LINEAR_LUT[v as usize]
293}
294
295#[inline]
296fn linear_to_srgb8(v: f32) -> u8 {
297 let idx = ((v * 4095.0) + 0.5) as usize;
298 LINEAR_TO_SRGB_LUT[if idx > 4095 { 4095 } else { idx }]
299}
300
301#[derive(Debug, Clone, Default, PartialEq, Eq)]
303pub struct TileFreshness {
304 pub expires_at: Option<SystemTime>,
306 pub etag: Option<String>,
308 pub last_modified: Option<String>,
310}
311
312impl TileFreshness {
313 #[inline]
315 pub fn is_expired_at(&self, now: SystemTime) -> bool {
316 self.expires_at.is_some_and(|expires_at| now >= expires_at)
317 }
318
319 #[inline]
321 pub fn is_expired(&self) -> bool {
322 self.is_expired_at(SystemTime::now())
323 }
324}
325
326#[derive(Debug, Clone, Default)]
336pub struct RevalidationHint {
337 pub etag: Option<String>,
339 pub last_modified: Option<String>,
341}
342
343impl RevalidationHint {
344 #[inline]
346 pub fn has_validators(&self) -> bool {
347 self.etag.is_some() || self.last_modified.is_some()
348 }
349}
350
351#[derive(Debug, Clone)]
353pub struct TileResponse {
354 pub data: TileData,
356 pub freshness: TileFreshness,
358 pub not_modified: bool,
363}
364
365impl TileResponse {
366 #[inline]
368 pub fn from_data(data: TileData) -> Self {
369 Self {
370 data,
371 freshness: TileFreshness::default(),
372 not_modified: false,
373 }
374 }
375
376 #[inline]
378 pub fn with_freshness(mut self, freshness: TileFreshness) -> Self {
379 self.freshness = freshness;
380 self
381 }
382
383 #[inline]
390 pub fn not_modified(freshness: TileFreshness) -> Self {
391 Self {
392 data: TileData::Raster(DecodedImage {
393 width: 0,
394 height: 0,
395 data: std::sync::Arc::new(Vec::new()),
396 }),
397 freshness,
398 not_modified: true,
399 }
400 }
401}
402
403impl From<TileData> for TileResponse {
404 #[inline]
405 fn from(value: TileData) -> Self {
406 Self::from_data(value)
407 }
408}
409
410#[derive(Debug, Clone)]
416pub struct VectorTileData {
417 pub layers: HashMap<String, FeatureCollection>,
419}
420
421impl VectorTileData {
422 pub fn feature_count(&self) -> usize {
424 self.layers.values().map(|fc| fc.len()).sum()
425 }
426
427 #[inline]
429 pub fn layer_count(&self) -> usize {
430 self.layers.len()
431 }
432
433 pub fn is_empty(&self) -> bool {
435 self.layers.values().all(|fc| fc.is_empty())
436 }
437
438 pub fn layer(&self, name: &str) -> Option<&FeatureCollection> {
440 self.layers.get(name)
441 }
442
443 pub fn layer_names(&self) -> Vec<&str> {
445 self.layers.keys().map(String::as_str).collect()
446 }
447
448 pub fn approx_byte_len(&self) -> usize {
452 self.layers
453 .values()
454 .map(|fc| fc.total_coords() * 16)
455 .sum()
456 }
457}
458
459#[derive(Debug, Clone)]
465pub struct RawVectorPayload {
466 pub tile_id: TileId,
468 pub bytes: Arc<Vec<u8>>,
470 pub decode_options: crate::mvt::MvtDecodeOptions,
472}
473
474#[derive(Debug, Clone)]
476pub enum TileData {
477 Raster(DecodedImage),
479 Vector(VectorTileData),
481 RawVector(RawVectorPayload),
487}
488
489impl TileData {
490 #[inline]
492 pub fn as_raster(&self) -> Option<&DecodedImage> {
493 match self {
494 Self::Raster(image) => Some(image),
495 Self::Vector(_) | Self::RawVector(_) => None,
496 }
497 }
498
499 #[inline]
501 pub fn as_vector(&self) -> Option<&VectorTileData> {
502 match self {
503 Self::Vector(data) => Some(data),
504 Self::Raster(_) | Self::RawVector(_) => None,
505 }
506 }
507
508 #[inline]
510 pub fn is_raster(&self) -> bool {
511 matches!(self, Self::Raster(_))
512 }
513
514 #[inline]
516 pub fn is_vector(&self) -> bool {
517 matches!(self, Self::Vector(_))
518 }
519
520 #[inline]
522 pub fn is_raw_vector(&self) -> bool {
523 matches!(self, Self::RawVector(_))
524 }
525
526 #[inline]
528 pub fn as_raw_vector(&self) -> Option<&RawVectorPayload> {
529 match self {
530 Self::RawVector(raw) => Some(raw),
531 _ => None,
532 }
533 }
534
535 #[inline]
539 pub fn dimensions(&self) -> (u32, u32) {
540 match self {
541 Self::Raster(image) => (image.width, image.height),
542 Self::Vector(_) | Self::RawVector(_) => (0, 0),
543 }
544 }
545
546 #[inline]
548 pub fn byte_len(&self) -> usize {
549 match self {
550 Self::Raster(image) => image.byte_len(),
551 Self::Vector(data) => data.approx_byte_len(),
552 Self::RawVector(raw) => raw.bytes.len(),
553 }
554 }
555
556 #[inline]
558 pub fn is_empty(&self) -> bool {
559 match self {
560 Self::Raster(image) => image.is_empty(),
561 Self::Vector(data) => data.is_empty(),
562 Self::RawVector(raw) => raw.bytes.is_empty(),
563 }
564 }
565
566 #[inline]
568 pub fn validate(&self) -> Result<(), TileError> {
569 match self {
570 Self::Raster(image) => image.validate_rgba8(),
571 Self::Vector(_) | Self::RawVector(_) => Ok(()),
572 }
573 }
574}
575
576pub trait TileDecoder: Send + Sync {
582 fn decode(&self, bytes: &[u8]) -> Result<DecodedImage, TileError>;
584}
585
586#[derive(Debug, Clone, Default, PartialEq, Eq)]
588pub struct TileSourceFailureDiagnostics {
589 pub transport_failures: u64,
591 pub http_status_failures: u64,
593 pub not_found_failures: u64,
595 pub decode_failures: u64,
597 pub timeout_failures: u64,
599 pub forced_cancellations: u64,
601 pub ignored_completed_responses: u64,
603}
604
605#[derive(Debug, Clone, Default, PartialEq, Eq)]
607pub struct TileSourceDiagnostics {
608 pub queued_requests: usize,
610 pub in_flight_requests: usize,
612 pub known_requests: usize,
614 pub cancelled_in_flight_requests: usize,
616 pub max_concurrent_requests: usize,
618 pub pending_decode_tasks: usize,
620 pub failure_diagnostics: TileSourceFailureDiagnostics,
622}
623
624pub trait TileSource: Send + Sync {
629 fn request(&self, id: TileId);
634
635 fn request_many(&self, ids: &[TileId]) {
641 for &id in ids {
642 self.request(id);
643 }
644 }
645
646 fn request_revalidate(&self, id: TileId, _hint: RevalidationHint) {
656 self.request(id);
657 }
658
659 fn request_revalidate_many(&self, ids: &[(TileId, RevalidationHint)]) {
664 for (id, hint) in ids {
665 self.request_revalidate(*id, hint.clone());
666 }
667 }
668
669 fn poll(&self) -> Vec<(TileId, Result<TileResponse, TileError>)>;
674
675 fn cancel(&self, _id: TileId) {}
680
681 fn cancel_many(&self, ids: &[TileId]) {
686 for &id in ids {
687 self.cancel(id);
688 }
689 }
690
691 fn diagnostics(&self) -> Option<TileSourceDiagnostics> {
693 None
694 }
695}
696
697#[cfg(test)]
698mod tests {
699 use super::*;
700 use std::sync::Mutex;
701
702 #[derive(Default)]
703 struct RecordingSource {
704 requested: Mutex<Vec<TileId>>,
705 cancelled: Mutex<Vec<TileId>>,
706 }
707
708 impl TileSource for RecordingSource {
709 fn request(&self, id: TileId) {
710 self.requested.lock().unwrap().push(id);
711 }
712
713 fn poll(&self) -> Vec<(TileId, Result<TileResponse, TileError>)> {
714 Vec::new()
715 }
716
717 fn cancel(&self, id: TileId) {
718 self.cancelled.lock().unwrap().push(id);
719 }
720 }
721
722 #[test]
723 fn decoded_image_validation_accepts_valid_rgba8() {
724 let image = DecodedImage {
725 width: 2,
726 height: 2,
727 data: vec![255u8; 16].into(),
728 };
729
730 assert_eq!(image.expected_len(), Some(16));
731 assert_eq!(image.byte_len(), 16);
732 assert!(!image.is_empty());
733 assert!(image.validate_rgba8().is_ok());
734 }
735
736 #[test]
737 fn decoded_image_validation_rejects_invalid_length() {
738 let image = DecodedImage {
739 width: 2,
740 height: 2,
741 data: vec![255u8; 15].into(),
742 };
743
744 let err = image.validate_rgba8().expect_err("image should be invalid");
745 assert!(matches!(err, TileError::Decode(_)));
746 }
747
748 #[test]
749 fn tile_data_helpers_delegate_to_raster_payload() {
750 let tile = TileData::Raster(DecodedImage {
751 width: 1,
752 height: 2,
753 data: vec![1u8; 8].into(),
754 });
755
756 assert_eq!(tile.dimensions(), (1, 2));
757 assert_eq!(tile.byte_len(), 8);
758 assert!(tile.as_raster().is_some());
759 assert!(tile.as_vector().is_none());
760 assert!(tile.is_raster());
761 assert!(!tile.is_vector());
762 assert!(!tile.is_empty());
763 assert!(tile.validate().is_ok());
764 }
765
766 #[test]
767 fn tile_data_vector_variant() {
768 use crate::geometry::FeatureCollection;
769 let mut layers = HashMap::new();
770 layers.insert("water".to_string(), FeatureCollection::default());
771 let tile = TileData::Vector(VectorTileData { layers });
772
773 assert!(tile.as_vector().is_some());
774 assert!(tile.as_raster().is_none());
775 assert!(tile.is_vector());
776 assert!(!tile.is_raster());
777 assert_eq!(tile.dimensions(), (0, 0));
778 assert!(tile.is_empty());
779 assert!(tile.validate().is_ok());
780 }
781
782 #[test]
783 fn vector_tile_data_helpers() {
784 use crate::geometry::{Feature, FeatureCollection, Geometry, Point};
785 use rustial_math::GeoCoord;
786
787 let mut layers = HashMap::new();
788 layers.insert(
789 "places".to_string(),
790 FeatureCollection {
791 features: vec![Feature {
792 geometry: Geometry::Point(Point {
793 coord: GeoCoord::from_lat_lon(0.0, 0.0),
794 }),
795 properties: HashMap::new(),
796 }],
797 },
798 );
799
800 let vt = VectorTileData { layers };
801 assert_eq!(vt.feature_count(), 1);
802 assert_eq!(vt.layer_count(), 1);
803 assert!(!vt.is_empty());
804 assert!(vt.layer("places").is_some());
805 assert!(vt.layer("missing").is_none());
806 assert_eq!(vt.layer_names(), vec!["places"]);
807 assert!(vt.approx_byte_len() > 0);
808 }
809
810 #[test]
811 fn tile_source_batch_defaults_forward_in_order() {
812 let source = RecordingSource::default();
813 let ids = [TileId::new(1, 0, 0), TileId::new(1, 1, 0), TileId::new(1, 0, 1)];
814
815 source.request_many(&ids);
816 source.cancel_many(&ids[1..]);
817
818 assert_eq!(*source.requested.lock().unwrap(), ids);
819 assert_eq!(*source.cancelled.lock().unwrap(), ids[1..]);
820 }
821
822 #[test]
823 fn decoded_image_builds_full_mip_chain() {
824 let image = DecodedImage {
825 width: 4,
826 height: 2,
827 data: vec![255u8; 4 * 2 * 4].into(),
828 };
829
830 let mip_chain = image.build_mip_chain_rgba8().expect("valid image should mipmap");
831 let dims: Vec<(u32, u32)> = mip_chain
832 .levels()
833 .iter()
834 .map(|level| (level.width, level.height))
835 .collect();
836
837 assert_eq!(dims, vec![(4, 2), (2, 1), (1, 1)]);
838 assert_eq!(mip_chain.level_count(), 3);
839 assert_eq!(mip_chain.byte_len(), 32 + 8 + 4);
840 }
841
842 #[test]
843 fn decoded_image_mip_chain_preserves_constant_opaque_color() {
844 let mut data = vec![0u8; 4 * 4 * 4];
845 for pixel in data.chunks_exact_mut(4) {
846 pixel.copy_from_slice(&[32, 96, 224, 255]);
847 }
848
849 let image = DecodedImage {
850 width: 4,
851 height: 4,
852 data: data.into(),
853 };
854
855 let mip_chain = image.build_mip_chain_rgba8().expect("valid image should mipmap");
856 for level in mip_chain.levels() {
857 for pixel in level.data.chunks_exact(4) {
858 assert_eq!(pixel, [32, 96, 224, 255]);
859 }
860 }
861 }
862
863 #[test]
864 fn srgb_lut_roundtrip_is_within_one_lsb() {
865 for i in 0u16..=255 {
868 let lut_val = SRGB_TO_LINEAR_LUT[i as usize];
869 let s = i as f32 / 255.0;
870 let ref_val = if s <= 0.04045 {
871 s / 12.92
872 } else {
873 ((s + 0.055) / 1.055).powf(2.4)
874 };
875 let err = (lut_val - ref_val).abs();
876 assert!(
877 err < 1e-6,
878 "srgb_to_linear LUT[{i}]: lut={lut_val}, ref={ref_val}, err={err}"
879 );
880 }
881
882 for i in 0u16..=255 {
884 let linear = SRGB_TO_LINEAR_LUT[i as usize];
885 let back = linear_to_srgb8(linear);
886 let diff = (back as i16 - i as i16).unsigned_abs();
887 assert!(
888 diff <= 1,
889 "roundtrip failed for {i}: got {back}, diff={diff}"
890 );
891 }
892 }
893}