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(|| {
185 TileError::Decode(format!(
186 "mip source dimensions overflow byte length computation: {}x{}",
187 prev.width, prev.height
188 ))
189 })?;
190
191 if prev.data.len() != expected_len {
192 return Err(TileError::Decode(format!(
193 "invalid mip source length: got {}, expected {} for {}x{}",
194 prev.data.len(),
195 expected_len,
196 prev.width,
197 prev.height
198 )));
199 }
200
201 let mut out = vec![0u8; dst_width as usize * dst_height as usize * RGBA8_BYTES_PER_PIXEL];
202
203 for y in 0..dst_height as usize {
204 for x in 0..dst_width as usize {
205 let sx0 = (x * 2).min(src_width - 1);
206 let sy0 = (y * 2).min(src_height - 1);
207 let sx1 = (sx0 + 1).min(src_width - 1);
208 let sy1 = (sy0 + 1).min(src_height - 1);
209
210 let taps = [(sx0, sy0), (sx1, sy0), (sx0, sy1), (sx1, sy1)];
211 let mut premul_r = 0.0f32;
212 let mut premul_g = 0.0f32;
213 let mut premul_b = 0.0f32;
214 let mut alpha = 0.0f32;
215
216 for (sx, sy) in taps {
217 let idx = (sy * src_width + sx) * RGBA8_BYTES_PER_PIXEL;
218 let a = prev.data[idx + 3] as f32 / 255.0;
219 premul_r += srgb8_to_linear(prev.data[idx]) * a;
220 premul_g += srgb8_to_linear(prev.data[idx + 1]) * a;
221 premul_b += srgb8_to_linear(prev.data[idx + 2]) * a;
222 alpha += a;
223 }
224
225 let sample_count = taps.len() as f32;
226 let out_idx = (y * dst_width as usize + x) * RGBA8_BYTES_PER_PIXEL;
227 let avg_alpha = alpha / sample_count;
228
229 if avg_alpha > 0.0 {
230 let inv_alpha = 1.0 / alpha.max(1e-6);
231 out[out_idx] = linear_to_srgb8((premul_r * inv_alpha).clamp(0.0, 1.0));
232 out[out_idx + 1] = linear_to_srgb8((premul_g * inv_alpha).clamp(0.0, 1.0));
233 out[out_idx + 2] = linear_to_srgb8((premul_b * inv_alpha).clamp(0.0, 1.0));
234 } else {
235 out[out_idx] = 0;
236 out[out_idx + 1] = 0;
237 out[out_idx + 2] = 0;
238 }
239 out[out_idx + 3] = ((avg_alpha.clamp(0.0, 1.0) * 255.0) + 0.5) as u8;
240 }
241 }
242
243 Ok(RasterMipLevel {
244 width: dst_width,
245 height: dst_height,
246 data: out,
247 })
248}
249
250use std::sync::LazyLock;
260
261static SRGB_TO_LINEAR_LUT: LazyLock<[f32; 256]> = LazyLock::new(|| {
263 let mut lut = [0.0f32; 256];
264 for i in 0u32..256 {
265 let s = i as f64 / 255.0;
266 lut[i as usize] = if s <= 0.04045 {
267 (s / 12.92) as f32
268 } else {
269 ((s + 0.055) / 1.055).powf(2.4) as f32
270 };
271 }
272 lut
273});
274
275static LINEAR_TO_SRGB_LUT: LazyLock<[u8; 4096]> = LazyLock::new(|| {
279 let mut lut = [0u8; 4096];
280 for i in 0u32..4096 {
281 let lin = i as f64 / 4095.0;
282 let s = if lin <= 0.0031308 {
283 lin * 12.92
284 } else {
285 1.055 * lin.powf(1.0 / 2.4) - 0.055
286 };
287 lut[i as usize] = ((s.clamp(0.0, 1.0) * 255.0) + 0.5) as u8;
288 }
289 lut
290});
291
292#[inline]
293fn srgb8_to_linear(v: u8) -> f32 {
294 SRGB_TO_LINEAR_LUT[v as usize]
295}
296
297#[inline]
298fn linear_to_srgb8(v: f32) -> u8 {
299 let idx = ((v * 4095.0) + 0.5) as usize;
300 LINEAR_TO_SRGB_LUT[if idx > 4095 { 4095 } else { idx }]
301}
302
303#[derive(Debug, Clone, Default, PartialEq, Eq)]
305pub struct TileFreshness {
306 pub expires_at: Option<SystemTime>,
308 pub etag: Option<String>,
310 pub last_modified: Option<String>,
312}
313
314impl TileFreshness {
315 #[inline]
317 pub fn is_expired_at(&self, now: SystemTime) -> bool {
318 self.expires_at.is_some_and(|expires_at| now >= expires_at)
319 }
320
321 #[inline]
323 pub fn is_expired(&self) -> bool {
324 self.is_expired_at(SystemTime::now())
325 }
326}
327
328#[derive(Debug, Clone, Default)]
338pub struct RevalidationHint {
339 pub etag: Option<String>,
341 pub last_modified: Option<String>,
343}
344
345impl RevalidationHint {
346 #[inline]
348 pub fn has_validators(&self) -> bool {
349 self.etag.is_some() || self.last_modified.is_some()
350 }
351}
352
353#[derive(Debug, Clone)]
355pub struct TileResponse {
356 pub data: TileData,
358 pub freshness: TileFreshness,
360 pub not_modified: bool,
365}
366
367impl TileResponse {
368 #[inline]
370 pub fn from_data(data: TileData) -> Self {
371 Self {
372 data,
373 freshness: TileFreshness::default(),
374 not_modified: false,
375 }
376 }
377
378 #[inline]
380 pub fn with_freshness(mut self, freshness: TileFreshness) -> Self {
381 self.freshness = freshness;
382 self
383 }
384
385 #[inline]
392 pub fn not_modified(freshness: TileFreshness) -> Self {
393 Self {
394 data: TileData::Raster(DecodedImage {
395 width: 0,
396 height: 0,
397 data: std::sync::Arc::new(Vec::new()),
398 }),
399 freshness,
400 not_modified: true,
401 }
402 }
403}
404
405impl From<TileData> for TileResponse {
406 #[inline]
407 fn from(value: TileData) -> Self {
408 Self::from_data(value)
409 }
410}
411
412#[derive(Debug, Clone)]
418pub struct VectorTileData {
419 pub layers: HashMap<String, FeatureCollection>,
421}
422
423impl VectorTileData {
424 pub fn feature_count(&self) -> usize {
426 self.layers.values().map(|fc| fc.len()).sum()
427 }
428
429 #[inline]
431 pub fn layer_count(&self) -> usize {
432 self.layers.len()
433 }
434
435 pub fn is_empty(&self) -> bool {
437 self.layers.values().all(|fc| fc.is_empty())
438 }
439
440 pub fn layer(&self, name: &str) -> Option<&FeatureCollection> {
442 self.layers.get(name)
443 }
444
445 pub fn layer_names(&self) -> Vec<&str> {
447 self.layers.keys().map(String::as_str).collect()
448 }
449
450 pub fn approx_byte_len(&self) -> usize {
454 self.layers.values().map(|fc| fc.total_coords() * 16).sum()
455 }
456}
457
458#[derive(Debug, Clone)]
464pub struct RawVectorPayload {
465 pub tile_id: TileId,
467 pub bytes: Arc<Vec<u8>>,
469 pub decode_options: crate::mvt::MvtDecodeOptions,
471}
472
473#[derive(Debug, Clone)]
475pub enum TileData {
476 Raster(DecodedImage),
478 Vector(VectorTileData),
480 RawVector(RawVectorPayload),
486}
487
488impl TileData {
489 #[inline]
491 pub fn as_raster(&self) -> Option<&DecodedImage> {
492 match self {
493 Self::Raster(image) => Some(image),
494 Self::Vector(_) | Self::RawVector(_) => None,
495 }
496 }
497
498 #[inline]
500 pub fn as_vector(&self) -> Option<&VectorTileData> {
501 match self {
502 Self::Vector(data) => Some(data),
503 Self::Raster(_) | Self::RawVector(_) => None,
504 }
505 }
506
507 #[inline]
509 pub fn is_raster(&self) -> bool {
510 matches!(self, Self::Raster(_))
511 }
512
513 #[inline]
515 pub fn is_vector(&self) -> bool {
516 matches!(self, Self::Vector(_))
517 }
518
519 #[inline]
521 pub fn is_raw_vector(&self) -> bool {
522 matches!(self, Self::RawVector(_))
523 }
524
525 #[inline]
527 pub fn as_raw_vector(&self) -> Option<&RawVectorPayload> {
528 match self {
529 Self::RawVector(raw) => Some(raw),
530 _ => None,
531 }
532 }
533
534 #[inline]
538 pub fn dimensions(&self) -> (u32, u32) {
539 match self {
540 Self::Raster(image) => (image.width, image.height),
541 Self::Vector(_) | Self::RawVector(_) => (0, 0),
542 }
543 }
544
545 #[inline]
547 pub fn byte_len(&self) -> usize {
548 match self {
549 Self::Raster(image) => image.byte_len(),
550 Self::Vector(data) => data.approx_byte_len(),
551 Self::RawVector(raw) => raw.bytes.len(),
552 }
553 }
554
555 #[inline]
557 pub fn is_empty(&self) -> bool {
558 match self {
559 Self::Raster(image) => image.is_empty(),
560 Self::Vector(data) => data.is_empty(),
561 Self::RawVector(raw) => raw.bytes.is_empty(),
562 }
563 }
564
565 #[inline]
567 pub fn validate(&self) -> Result<(), TileError> {
568 match self {
569 Self::Raster(image) => image.validate_rgba8(),
570 Self::Vector(_) | Self::RawVector(_) => Ok(()),
571 }
572 }
573}
574
575pub trait TileDecoder: Send + Sync {
581 fn decode(&self, bytes: &[u8]) -> Result<DecodedImage, TileError>;
583}
584
585#[derive(Debug, Clone, Default, PartialEq, Eq)]
587pub struct TileSourceFailureDiagnostics {
588 pub transport_failures: u64,
590 pub http_status_failures: u64,
592 pub not_found_failures: u64,
594 pub decode_failures: u64,
596 pub timeout_failures: u64,
598 pub forced_cancellations: u64,
600 pub ignored_completed_responses: u64,
602}
603
604#[derive(Debug, Clone, Default, PartialEq, Eq)]
606pub struct TileSourceDiagnostics {
607 pub queued_requests: usize,
609 pub in_flight_requests: usize,
611 pub known_requests: usize,
613 pub cancelled_in_flight_requests: usize,
615 pub max_concurrent_requests: usize,
617 pub pending_decode_tasks: usize,
619 pub failure_diagnostics: TileSourceFailureDiagnostics,
621}
622
623pub trait TileSource: Send + Sync {
628 fn request(&self, id: TileId);
633
634 fn request_many(&self, ids: &[TileId]) {
640 for &id in ids {
641 self.request(id);
642 }
643 }
644
645 fn request_revalidate(&self, id: TileId, _hint: RevalidationHint) {
655 self.request(id);
656 }
657
658 fn request_revalidate_many(&self, ids: &[(TileId, RevalidationHint)]) {
663 for (id, hint) in ids {
664 self.request_revalidate(*id, hint.clone());
665 }
666 }
667
668 fn poll(&self) -> Vec<(TileId, Result<TileResponse, TileError>)>;
673
674 fn cancel(&self, _id: TileId) {}
679
680 fn cancel_many(&self, ids: &[TileId]) {
685 for &id in ids {
686 self.cancel(id);
687 }
688 }
689
690 fn diagnostics(&self) -> Option<TileSourceDiagnostics> {
692 None
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699 use std::sync::Mutex;
700
701 #[derive(Default)]
702 struct RecordingSource {
703 requested: Mutex<Vec<TileId>>,
704 cancelled: Mutex<Vec<TileId>>,
705 }
706
707 impl TileSource for RecordingSource {
708 fn request(&self, id: TileId) {
709 self.requested.lock().unwrap().push(id);
710 }
711
712 fn poll(&self) -> Vec<(TileId, Result<TileResponse, TileError>)> {
713 Vec::new()
714 }
715
716 fn cancel(&self, id: TileId) {
717 self.cancelled.lock().unwrap().push(id);
718 }
719 }
720
721 #[test]
722 fn decoded_image_validation_accepts_valid_rgba8() {
723 let image = DecodedImage {
724 width: 2,
725 height: 2,
726 data: vec![255u8; 16].into(),
727 };
728
729 assert_eq!(image.expected_len(), Some(16));
730 assert_eq!(image.byte_len(), 16);
731 assert!(!image.is_empty());
732 assert!(image.validate_rgba8().is_ok());
733 }
734
735 #[test]
736 fn decoded_image_validation_rejects_invalid_length() {
737 let image = DecodedImage {
738 width: 2,
739 height: 2,
740 data: vec![255u8; 15].into(),
741 };
742
743 let err = image.validate_rgba8().expect_err("image should be invalid");
744 assert!(matches!(err, TileError::Decode(_)));
745 }
746
747 #[test]
748 fn tile_data_helpers_delegate_to_raster_payload() {
749 let tile = TileData::Raster(DecodedImage {
750 width: 1,
751 height: 2,
752 data: vec![1u8; 8].into(),
753 });
754
755 assert_eq!(tile.dimensions(), (1, 2));
756 assert_eq!(tile.byte_len(), 8);
757 assert!(tile.as_raster().is_some());
758 assert!(tile.as_vector().is_none());
759 assert!(tile.is_raster());
760 assert!(!tile.is_vector());
761 assert!(!tile.is_empty());
762 assert!(tile.validate().is_ok());
763 }
764
765 #[test]
766 fn tile_data_vector_variant() {
767 use crate::geometry::FeatureCollection;
768 let mut layers = HashMap::new();
769 layers.insert("water".to_string(), FeatureCollection::default());
770 let tile = TileData::Vector(VectorTileData { layers });
771
772 assert!(tile.as_vector().is_some());
773 assert!(tile.as_raster().is_none());
774 assert!(tile.is_vector());
775 assert!(!tile.is_raster());
776 assert_eq!(tile.dimensions(), (0, 0));
777 assert!(tile.is_empty());
778 assert!(tile.validate().is_ok());
779 }
780
781 #[test]
782 fn vector_tile_data_helpers() {
783 use crate::geometry::{Feature, FeatureCollection, Geometry, Point};
784 use rustial_math::GeoCoord;
785
786 let mut layers = HashMap::new();
787 layers.insert(
788 "places".to_string(),
789 FeatureCollection {
790 features: vec![Feature {
791 geometry: Geometry::Point(Point {
792 coord: GeoCoord::from_lat_lon(0.0, 0.0),
793 }),
794 properties: HashMap::new(),
795 }],
796 },
797 );
798
799 let vt = VectorTileData { layers };
800 assert_eq!(vt.feature_count(), 1);
801 assert_eq!(vt.layer_count(), 1);
802 assert!(!vt.is_empty());
803 assert!(vt.layer("places").is_some());
804 assert!(vt.layer("missing").is_none());
805 assert_eq!(vt.layer_names(), vec!["places"]);
806 assert!(vt.approx_byte_len() > 0);
807 }
808
809 #[test]
810 fn tile_source_batch_defaults_forward_in_order() {
811 let source = RecordingSource::default();
812 let ids = [
813 TileId::new(1, 0, 0),
814 TileId::new(1, 1, 0),
815 TileId::new(1, 0, 1),
816 ];
817
818 source.request_many(&ids);
819 source.cancel_many(&ids[1..]);
820
821 assert_eq!(*source.requested.lock().unwrap(), ids);
822 assert_eq!(*source.cancelled.lock().unwrap(), ids[1..]);
823 }
824
825 #[test]
826 fn decoded_image_builds_full_mip_chain() {
827 let image = DecodedImage {
828 width: 4,
829 height: 2,
830 data: vec![255u8; 4 * 2 * 4].into(),
831 };
832
833 let mip_chain = image
834 .build_mip_chain_rgba8()
835 .expect("valid image should mipmap");
836 let dims: Vec<(u32, u32)> = mip_chain
837 .levels()
838 .iter()
839 .map(|level| (level.width, level.height))
840 .collect();
841
842 assert_eq!(dims, vec![(4, 2), (2, 1), (1, 1)]);
843 assert_eq!(mip_chain.level_count(), 3);
844 assert_eq!(mip_chain.byte_len(), 32 + 8 + 4);
845 }
846
847 #[test]
848 fn decoded_image_mip_chain_preserves_constant_opaque_color() {
849 let mut data = vec![0u8; 4 * 4 * 4];
850 for pixel in data.chunks_exact_mut(4) {
851 pixel.copy_from_slice(&[32, 96, 224, 255]);
852 }
853
854 let image = DecodedImage {
855 width: 4,
856 height: 4,
857 data: data.into(),
858 };
859
860 let mip_chain = image
861 .build_mip_chain_rgba8()
862 .expect("valid image should mipmap");
863 for level in mip_chain.levels() {
864 for pixel in level.data.chunks_exact(4) {
865 assert_eq!(pixel, [32, 96, 224, 255]);
866 }
867 }
868 }
869
870 #[test]
871 fn srgb_lut_roundtrip_is_within_one_lsb() {
872 for i in 0u16..=255 {
875 let lut_val = SRGB_TO_LINEAR_LUT[i as usize];
876 let s = i as f32 / 255.0;
877 let ref_val = if s <= 0.04045 {
878 s / 12.92
879 } else {
880 ((s + 0.055) / 1.055).powf(2.4)
881 };
882 let err = (lut_val - ref_val).abs();
883 assert!(
884 err < 1e-6,
885 "srgb_to_linear LUT[{i}]: lut={lut_val}, ref={ref_val}, err={err}"
886 );
887 }
888
889 for i in 0u16..=255 {
891 let linear = SRGB_TO_LINEAR_LUT[i as usize];
892 let back = linear_to_srgb8(linear);
893 let diff = (back as i16 - i as i16).unsigned_abs();
894 assert!(
895 diff <= 1,
896 "roundtrip failed for {i}: got {back}, diff={diff}"
897 );
898 }
899 }
900}