1use crate::config::{ConfigError, LayerConfig};
24use oxigeo_core::buffer::RasterBuffer;
25use oxigeo_core::io::FileDataSource;
26use oxigeo_core::types::{GeoTransform, NoDataValue, RasterDataType};
27use oxigeo_geotiff::GeoTiffReader;
28use oxigeo_proj::{BoundingBox, Coordinate, Crs, Transformer};
29use std::collections::HashMap;
30use std::path::{Path, PathBuf};
31use std::sync::{Arc, RwLock};
32use thiserror::Error;
33use tracing::{debug, info, warn};
34
35pub struct Dataset {
37 path: PathBuf,
39 reader: RwLock<Option<GeoTiffReader<FileDataSource>>>,
41 width: u64,
43 height: u64,
44 band_count: u32,
45 data_type: RasterDataType,
46 geo_transform: Option<GeoTransform>,
47 nodata: NoDataValue,
48 tile_size: Option<(u32, u32)>,
49 overview_count: usize,
50}
51
52impl Dataset {
53 pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, oxigeo_core::OxiGeoError> {
55 let path_buf = path.as_ref().to_path_buf();
56
57 let source = FileDataSource::open(&path_buf)?;
59
60 let reader = GeoTiffReader::open(source)?;
62
63 let width = reader.width();
65 let height = reader.height();
66 let band_count = reader.band_count();
67 let data_type = reader.data_type().unwrap_or(RasterDataType::UInt8);
68 let geo_transform = reader.geo_transform().cloned();
69 let nodata = reader.nodata();
70 let tile_size = reader.tile_size();
71 let overview_count = reader.overview_count();
72
73 Ok(Self {
74 path: path_buf,
75 reader: RwLock::new(Some(reader)),
76 width,
77 height,
78 band_count,
79 data_type,
80 geo_transform,
81 nodata,
82 tile_size,
83 overview_count,
84 })
85 }
86
87 #[must_use]
89 pub fn path(&self) -> &Path {
90 &self.path
91 }
92
93 #[must_use]
95 pub fn raster_size(&self) -> (usize, usize) {
96 (self.width as usize, self.height as usize)
97 }
98
99 #[must_use]
101 pub fn width(&self) -> u64 {
102 self.width
103 }
104
105 #[must_use]
107 pub fn height(&self) -> u64 {
108 self.height
109 }
110
111 #[must_use]
113 pub fn raster_count(&self) -> usize {
114 self.band_count as usize
115 }
116
117 #[must_use]
119 pub fn data_type(&self) -> RasterDataType {
120 self.data_type
121 }
122
123 pub fn projection(&self) -> Result<String, oxigeo_core::OxiGeoError> {
125 if let Ok(guard) = self.reader.read()
127 && let Some(ref reader) = *guard
128 && let Some(epsg) = reader.epsg_code()
129 {
130 return Ok(format!("EPSG:{}", epsg));
131 }
132 Ok("EPSG:4326".to_string())
133 }
134
135 pub fn geotransform(&self) -> Result<[f64; 6], oxigeo_core::OxiGeoError> {
137 if let Some(ref gt) = self.geo_transform {
138 Ok(gt.to_gdal_array())
139 } else {
140 Ok([0.0, 1.0, 0.0, 0.0, 0.0, -1.0])
142 }
143 }
144
145 #[must_use]
147 pub fn geo_transform_obj(&self) -> Option<&GeoTransform> {
148 self.geo_transform.as_ref()
149 }
150
151 #[must_use]
153 pub fn nodata(&self) -> NoDataValue {
154 self.nodata
155 }
156
157 #[must_use]
159 pub fn tile_size(&self) -> Option<(u32, u32)> {
160 self.tile_size
161 }
162
163 #[must_use]
165 pub fn overview_count(&self) -> usize {
166 self.overview_count
167 }
168
169 #[must_use]
171 pub fn bounds(&self) -> Option<oxigeo_core::types::BoundingBox> {
172 self.geo_transform
173 .as_ref()
174 .map(|gt| gt.compute_bounds(self.width, self.height))
175 }
176
177 pub fn read_tile(
184 &self,
185 level: usize,
186 tile_x: u32,
187 tile_y: u32,
188 ) -> Result<Vec<u8>, oxigeo_core::OxiGeoError> {
189 let guard = self
190 .reader
191 .read()
192 .map_err(|_| oxigeo_core::OxiGeoError::Internal {
193 message: "Failed to acquire read lock".to_string(),
194 })?;
195
196 if let Some(ref reader) = *guard {
197 reader.read_tile(level, tile_x, tile_y)
198 } else {
199 Err(oxigeo_core::OxiGeoError::Internal {
200 message: "Reader not initialized".to_string(),
201 })
202 }
203 }
204
205 pub fn read_tile_buffer(
207 &self,
208 level: usize,
209 tile_x: u32,
210 tile_y: u32,
211 ) -> Result<RasterBuffer, oxigeo_core::OxiGeoError> {
212 let guard = self
213 .reader
214 .read()
215 .map_err(|_| oxigeo_core::OxiGeoError::Internal {
216 message: "Failed to acquire read lock".to_string(),
217 })?;
218
219 if let Some(ref reader) = *guard {
220 reader.read_tile_buffer(level, tile_x, tile_y)
221 } else {
222 Err(oxigeo_core::OxiGeoError::Internal {
223 message: "Reader not initialized".to_string(),
224 })
225 }
226 }
227
228 pub fn read_band(
230 &self,
231 level: usize,
232 band: usize,
233 ) -> Result<Vec<u8>, oxigeo_core::OxiGeoError> {
234 let guard = self
235 .reader
236 .read()
237 .map_err(|_| oxigeo_core::OxiGeoError::Internal {
238 message: "Failed to acquire read lock".to_string(),
239 })?;
240
241 if let Some(ref reader) = *guard {
242 reader.read_band(level, band)
243 } else {
244 Err(oxigeo_core::OxiGeoError::Internal {
245 message: "Reader not initialized".to_string(),
246 })
247 }
248 }
249
250 pub fn read_window(
258 &self,
259 x_offset: u64,
260 y_offset: u64,
261 x_size: u64,
262 y_size: u64,
263 ) -> Result<RasterBuffer, oxigeo_core::OxiGeoError> {
264 if x_offset >= self.width || y_offset >= self.height {
266 return Err(oxigeo_core::OxiGeoError::OutOfBounds {
267 message: format!(
268 "Window offset ({}, {}) out of bounds ({}x{})",
269 x_offset, y_offset, self.width, self.height
270 ),
271 });
272 }
273
274 let actual_x_size = x_size.min(self.width - x_offset);
276 let actual_y_size = y_size.min(self.height - y_offset);
277
278 let mut window_buffer = RasterBuffer::zeros(x_size, y_size, self.data_type);
280
281 let (tile_w, tile_h) = self.tile_size.unwrap_or((256, 256));
283 let tile_w = tile_w as u64;
284 let tile_h = tile_h as u64;
285
286 let start_tile_x = x_offset / tile_w;
288 let start_tile_y = y_offset / tile_h;
289 let end_tile_x = (x_offset + actual_x_size).div_ceil(tile_w);
290 let end_tile_y = (y_offset + actual_y_size).div_ceil(tile_h);
291
292 for tile_y in start_tile_y..end_tile_y {
294 for tile_x in start_tile_x..end_tile_x {
295 let tile_pixel_x = tile_x * tile_w;
297 let tile_pixel_y = tile_y * tile_h;
298
299 let tile_buffer = match self.read_tile_buffer(0, tile_x as u32, tile_y as u32) {
301 Ok(buf) => buf,
302 Err(e) => {
303 debug!("Failed to read tile ({}, {}): {}", tile_x, tile_y, e);
305 continue;
306 }
307 };
308
309 let tile_width = tile_buffer.width();
310 let tile_height = tile_buffer.height();
311
312 let win_min_x = x_offset;
314 let win_min_y = y_offset;
315 let win_max_x = x_offset + actual_x_size;
316 let win_max_y = y_offset + actual_y_size;
317
318 let tile_max_x = (tile_pixel_x + tile_width).min(self.width);
319 let tile_max_y = (tile_pixel_y + tile_height).min(self.height);
320
321 let intersect_min_x = win_min_x.max(tile_pixel_x);
322 let intersect_min_y = win_min_y.max(tile_pixel_y);
323 let intersect_max_x = win_max_x.min(tile_max_x);
324 let intersect_max_y = win_max_y.min(tile_max_y);
325
326 for src_y in intersect_min_y..intersect_max_y {
328 for src_x in intersect_min_x..intersect_max_x {
329 let tile_local_x = src_x - tile_pixel_x;
331 let tile_local_y = src_y - tile_pixel_y;
332
333 let win_local_x = src_x - x_offset;
335 let win_local_y = src_y - y_offset;
336
337 if let Ok(value) = tile_buffer.get_pixel(tile_local_x, tile_local_y) {
339 let _ = window_buffer.set_pixel(win_local_x, win_local_y, value);
340 }
341 }
342 }
343 }
344 }
345
346 Ok(window_buffer)
347 }
348
349 pub fn get_pixel(&self, x: u64, y: u64) -> Result<f64, oxigeo_core::OxiGeoError> {
351 if x >= self.width || y >= self.height {
352 return Err(oxigeo_core::OxiGeoError::OutOfBounds {
353 message: format!(
354 "Pixel ({}, {}) out of bounds ({}x{})",
355 x, y, self.width, self.height
356 ),
357 });
358 }
359
360 let (tile_w, tile_h) = self.tile_size.unwrap_or((256, 256));
362 let tile_x = x / tile_w as u64;
363 let tile_y = y / tile_h as u64;
364 let local_x = x % tile_w as u64;
365 let local_y = y % tile_h as u64;
366
367 let tile_buffer = self.read_tile_buffer(0, tile_x as u32, tile_y as u32)?;
369
370 tile_buffer.get_pixel(local_x, local_y)
372 }
373
374 pub fn rasterband(&self, _band: usize) -> Result<RasterBandInfo, oxigeo_core::OxiGeoError> {
376 Ok(RasterBandInfo {
377 nodata: self.nodata,
378 data_type: self.data_type,
379 })
380 }
381}
382
383pub struct RasterBandInfo {
385 nodata: NoDataValue,
386 data_type: RasterDataType,
387}
388
389impl RasterBandInfo {
390 pub fn nodata(&self) -> Option<f64> {
392 self.nodata.as_f64()
393 }
394
395 pub fn datatype(&self) -> &str {
397 match self.data_type {
398 RasterDataType::UInt8 => "UInt8",
399 RasterDataType::Int8 => "Int8",
400 RasterDataType::UInt16 => "UInt16",
401 RasterDataType::Int16 => "Int16",
402 RasterDataType::UInt32 => "UInt32",
403 RasterDataType::Int32 => "Int32",
404 RasterDataType::Float32 => "Float32",
405 RasterDataType::Float64 => "Float64",
406 RasterDataType::UInt64 => "UInt64",
407 RasterDataType::Int64 => "Int64",
408 RasterDataType::CFloat32 => "CFloat32",
409 RasterDataType::CFloat64 => "CFloat64",
410 }
411 }
412}
413
414#[derive(Debug, Error)]
416pub enum RegistryError {
417 #[error("Layer not found: {0}")]
419 LayerNotFound(String),
420
421 #[error("Failed to open dataset: {0}")]
423 DatasetOpen(#[from] oxigeo_core::OxiGeoError),
424
425 #[error("Configuration error: {0}")]
427 Config(#[from] ConfigError),
428
429 #[error("CRS transformation failed: {0}")]
431 CrsTransformation(String),
432
433 #[error("Invalid CRS: {0}")]
435 InvalidCrs(String),
436
437 #[error("Lock poisoned")]
439 LockPoisoned,
440}
441
442impl From<oxigeo_proj::Error> for RegistryError {
443 fn from(err: oxigeo_proj::Error) -> Self {
444 RegistryError::CrsTransformation(err.to_string())
445 }
446}
447
448pub type RegistryResult<T> = Result<T, RegistryError>;
450
451#[derive(Debug, Clone)]
453pub struct LayerInfo {
454 pub name: String,
456
457 pub title: String,
459
460 pub abstract_: String,
462
463 pub config: LayerConfig,
465
466 pub metadata: DatasetMetadata,
468}
469
470#[derive(Debug, Clone)]
472pub struct DatasetMetadata {
473 pub width: usize,
475
476 pub height: usize,
478
479 pub band_count: usize,
481
482 pub data_type: String,
484
485 pub srs: Option<String>,
487
488 pub bbox: Option<(f64, f64, f64, f64)>,
490
491 pub geotransform: Option<[f64; 6]>,
493
494 pub nodata: Option<f64>,
496}
497
498pub struct DatasetRegistry {
500 layers: Arc<RwLock<HashMap<String, LayerInfo>>>,
502
503 datasets: Arc<RwLock<HashMap<String, Arc<Dataset>>>>,
505}
506
507impl DatasetRegistry {
508 pub fn new() -> Self {
510 Self {
511 layers: Arc::new(RwLock::new(HashMap::new())),
512 datasets: Arc::new(RwLock::new(HashMap::new())),
513 }
514 }
515
516 pub fn register_layer(&self, config: LayerConfig) -> RegistryResult<()> {
518 info!("Registering layer: {}", config.name);
519
520 let layer_name = config.name.clone();
522
523 let dataset = Self::open_dataset(&config.path)?;
525 let metadata = Self::extract_metadata(&dataset)?;
526
527 debug!(
528 "Layer {} metadata: {}x{}, {} bands",
529 layer_name, metadata.width, metadata.height, metadata.band_count
530 );
531
532 let layer_info = LayerInfo {
533 name: config.name.clone(),
534 title: config.title.clone().unwrap_or_else(|| config.name.clone()),
535 abstract_: config
536 .abstract_
537 .clone()
538 .unwrap_or_else(|| format!("Layer {}", config.name)),
539 config,
540 metadata,
541 };
542
543 let mut layers = self
545 .layers
546 .write()
547 .map_err(|_| RegistryError::LockPoisoned)?;
548 layers.insert(layer_info.name.clone(), layer_info);
549
550 let mut datasets = self
552 .datasets
553 .write()
554 .map_err(|_| RegistryError::LockPoisoned)?;
555 datasets.insert(layer_name, Arc::new(dataset));
556
557 Ok(())
558 }
559
560 pub fn register_layers(&self, configs: Vec<LayerConfig>) -> RegistryResult<()> {
562 for config in configs {
563 if !config.enabled {
564 debug!("Skipping disabled layer: {}", config.name);
565 continue;
566 }
567
568 if let Err(e) = self.register_layer(config) {
569 warn!("Failed to register layer: {}", e);
570 }
572 }
573 Ok(())
574 }
575
576 pub fn get_layer(&self, name: &str) -> RegistryResult<LayerInfo> {
578 let layers = self
579 .layers
580 .read()
581 .map_err(|_| RegistryError::LockPoisoned)?;
582
583 layers
584 .get(name)
585 .cloned()
586 .ok_or_else(|| RegistryError::LayerNotFound(name.to_string()))
587 }
588
589 pub fn get_dataset(&self, name: &str) -> RegistryResult<Arc<Dataset>> {
591 let datasets = self
592 .datasets
593 .read()
594 .map_err(|_| RegistryError::LockPoisoned)?;
595
596 datasets
597 .get(name)
598 .cloned()
599 .ok_or_else(|| RegistryError::LayerNotFound(name.to_string()))
600 }
601
602 pub fn list_layers(&self) -> RegistryResult<Vec<LayerInfo>> {
604 let layers = self
605 .layers
606 .read()
607 .map_err(|_| RegistryError::LockPoisoned)?;
608
609 Ok(layers.values().cloned().collect())
610 }
611
612 pub fn has_layer(&self, name: &str) -> bool {
614 self.layers
615 .read()
616 .map(|layers| layers.contains_key(name))
617 .unwrap_or(false)
618 }
619
620 pub fn unregister_layer(&self, name: &str) -> RegistryResult<()> {
622 let mut layers = self
623 .layers
624 .write()
625 .map_err(|_| RegistryError::LockPoisoned)?;
626
627 let mut datasets = self
628 .datasets
629 .write()
630 .map_err(|_| RegistryError::LockPoisoned)?;
631
632 layers.remove(name);
633 datasets.remove(name);
634
635 info!("Unregistered layer: {}", name);
636 Ok(())
637 }
638
639 pub fn layer_count(&self) -> usize {
641 self.layers.read().map(|l| l.len()).unwrap_or(0)
642 }
643
644 fn open_dataset<P: AsRef<Path>>(path: P) -> RegistryResult<Dataset> {
646 let dataset = Dataset::open(path.as_ref())?;
647 Ok(dataset)
648 }
649
650 fn extract_metadata(dataset: &Dataset) -> RegistryResult<DatasetMetadata> {
652 let raster_size = dataset.raster_size();
653 let band_count = dataset.raster_count();
654
655 let srs = dataset.projection().ok();
657
658 let geotransform = dataset.geotransform().ok();
660 let bbox = geotransform.map(|gt| {
661 let width = raster_size.0 as f64;
662 let height = raster_size.1 as f64;
663
664 let min_x = gt[0];
665 let max_x = gt[0] + gt[1] * width + gt[2] * height;
666 let max_y = gt[3];
667 let min_y = gt[3] + gt[4] * width + gt[5] * height;
668
669 let (min_x, max_x) = if min_x < max_x {
671 (min_x, max_x)
672 } else {
673 (max_x, min_x)
674 };
675 let (min_y, max_y) = if min_y < max_y {
676 (min_y, max_y)
677 } else {
678 (max_y, min_y)
679 };
680
681 (min_x, min_y, max_x, max_y)
682 });
683
684 let nodata = if band_count > 0 {
686 dataset.rasterband(1).ok().and_then(|band| band.nodata())
687 } else {
688 None
689 };
690
691 let data_type = if band_count > 0 {
693 dataset
694 .rasterband(1)
695 .ok()
696 .map(|band| format!("{:?}", band.datatype()))
697 .unwrap_or_else(|| "Unknown".to_string())
698 } else {
699 "Unknown".to_string()
700 };
701
702 Ok(DatasetMetadata {
703 width: raster_size.0,
704 height: raster_size.1,
705 band_count,
706 data_type,
707 srs,
708 bbox,
709 geotransform,
710 nodata,
711 })
712 }
713
714 pub fn reload_layer(&self, name: &str) -> RegistryResult<()> {
716 let config = {
717 let layers = self
718 .layers
719 .read()
720 .map_err(|_| RegistryError::LockPoisoned)?;
721
722 layers
723 .get(name)
724 .ok_or_else(|| RegistryError::LayerNotFound(name.to_string()))?
725 .config
726 .clone()
727 };
728
729 self.unregister_layer(name)?;
730 self.register_layer(config)?;
731
732 info!("Reloaded layer: {}", name);
733 Ok(())
734 }
735
736 pub fn get_layer_bbox(
767 &self,
768 name: &str,
769 target_crs: Option<&str>,
770 ) -> RegistryResult<Option<(f64, f64, f64, f64)>> {
771 let layer = self.get_layer(name)?;
772
773 let target_crs_str = match target_crs {
775 Some(crs) => crs,
776 None => return Ok(layer.metadata.bbox),
777 };
778
779 let native_bbox = match layer.metadata.bbox {
781 Some(bbox) => bbox,
782 None => return Ok(None),
783 };
784
785 let source_crs_str = layer.metadata.srs.as_deref().unwrap_or("EPSG:4326");
787
788 if Self::crs_strings_equivalent(source_crs_str, target_crs_str) {
790 return Ok(Some(native_bbox));
791 }
792
793 let source_crs = Self::parse_crs(source_crs_str)?;
795 let target_crs = Self::parse_crs(target_crs_str)?;
796
797 let transformed =
799 Self::transform_bbox_with_densification(native_bbox, &source_crs, &target_crs)?;
800
801 Ok(Some(transformed))
802 }
803
804 fn parse_crs(crs_str: &str) -> RegistryResult<Crs> {
811 let trimmed = crs_str.trim();
812
813 if let Some(code_str) = trimmed.to_uppercase().strip_prefix("EPSG:") {
815 let code: u32 = code_str.parse().map_err(|_| {
816 RegistryError::InvalidCrs(format!("Invalid EPSG code: {}", code_str))
817 })?;
818 return Ok(Crs::from_epsg(code)?);
819 }
820
821 if trimmed.starts_with("+proj=") || trimmed.contains("+proj=") {
823 return Ok(Crs::from_proj(trimmed)?);
824 }
825
826 if trimmed.starts_with("GEOGCS[")
828 || trimmed.starts_with("PROJCS[")
829 || trimmed.starts_with("GEOCCS[")
830 {
831 return Ok(Crs::from_wkt(trimmed)?);
832 }
833
834 Err(RegistryError::InvalidCrs(format!(
835 "Unrecognized CRS format: {}",
836 crs_str
837 )))
838 }
839
840 fn crs_strings_equivalent(crs1: &str, crs2: &str) -> bool {
842 let norm1 = crs1.trim().to_uppercase();
843 let norm2 = crs2.trim().to_uppercase();
844 norm1 == norm2
845 }
846
847 fn transform_bbox_with_densification(
864 bbox: (f64, f64, f64, f64),
865 source_crs: &Crs,
866 target_crs: &Crs,
867 ) -> RegistryResult<(f64, f64, f64, f64)> {
868 let (min_x, min_y, max_x, max_y) = bbox;
869
870 let transformer = Transformer::new(source_crs.clone(), target_crs.clone())?;
872
873 const DENSIFY_POINTS: usize = 21;
877
878 let edge_points = Self::densify_bbox_edges(min_x, min_y, max_x, max_y, DENSIFY_POINTS);
880
881 let transformed_points: Vec<Coordinate> = edge_points
883 .iter()
884 .filter_map(|coord| transformer.transform(coord).ok())
885 .collect();
886
887 if transformed_points.is_empty() {
889 return Err(RegistryError::CrsTransformation(
890 "All points failed to transform".to_string(),
891 ));
892 }
893
894 let mut result_min_x = f64::INFINITY;
896 let mut result_min_y = f64::INFINITY;
897 let mut result_max_x = f64::NEG_INFINITY;
898 let mut result_max_y = f64::NEG_INFINITY;
899
900 for point in &transformed_points {
901 if point.x.is_finite() && point.y.is_finite() {
902 result_min_x = result_min_x.min(point.x);
903 result_min_y = result_min_y.min(point.y);
904 result_max_x = result_max_x.max(point.x);
905 result_max_y = result_max_y.max(point.y);
906 }
907 }
908
909 if !result_min_x.is_finite()
911 || !result_min_y.is_finite()
912 || !result_max_x.is_finite()
913 || !result_max_y.is_finite()
914 {
915 return Err(RegistryError::CrsTransformation(
916 "Transformation resulted in non-finite values".to_string(),
917 ));
918 }
919
920 Ok((result_min_x, result_min_y, result_max_x, result_max_y))
921 }
922
923 fn densify_bbox_edges(
928 min_x: f64,
929 min_y: f64,
930 max_x: f64,
931 max_y: f64,
932 points_per_edge: usize,
933 ) -> Vec<Coordinate> {
934 let mut points = Vec::with_capacity(points_per_edge * 4);
935
936 let n = points_per_edge.max(2);
938
939 for i in 0..n {
941 let t = i as f64 / (n - 1) as f64;
942 let x = min_x + t * (max_x - min_x);
943 points.push(Coordinate::new(x, min_y));
944 }
945
946 for i in 1..n {
949 let t = i as f64 / (n - 1) as f64;
950 let y = min_y + t * (max_y - min_y);
951 points.push(Coordinate::new(max_x, y));
952 }
953
954 for i in 1..n {
957 let t = i as f64 / (n - 1) as f64;
958 let x = max_x - t * (max_x - min_x);
959 points.push(Coordinate::new(x, max_y));
960 }
961
962 for i in 1..(n - 1) {
965 let t = i as f64 / (n - 1) as f64;
966 let y = max_y - t * (max_y - min_y);
967 points.push(Coordinate::new(min_x, y));
968 }
969
970 points
971 }
972
973 #[allow(dead_code)]
977 fn transform_point(
978 x: f64,
979 y: f64,
980 source_crs: &Crs,
981 target_crs: &Crs,
982 ) -> RegistryResult<(f64, f64)> {
983 let transformer = Transformer::new(source_crs.clone(), target_crs.clone())?;
984 let coord = Coordinate::new(x, y);
985 let transformed = transformer.transform(&coord)?;
986 Ok((transformed.x, transformed.y))
987 }
988
989 pub fn get_layer_bbox_web_mercator(
993 &self,
994 name: &str,
995 ) -> RegistryResult<Option<(f64, f64, f64, f64)>> {
996 self.get_layer_bbox(name, Some("EPSG:3857"))
997 }
998
999 pub fn get_layer_bbox_wgs84(&self, name: &str) -> RegistryResult<Option<(f64, f64, f64, f64)>> {
1003 self.get_layer_bbox(name, Some("EPSG:4326"))
1004 }
1005
1006 #[allow(dead_code)]
1011 fn transform_bbox_simple(
1012 bbox: (f64, f64, f64, f64),
1013 source_crs: &Crs,
1014 target_crs: &Crs,
1015 ) -> RegistryResult<(f64, f64, f64, f64)> {
1016 let (min_x, min_y, max_x, max_y) = bbox;
1017
1018 let source_bbox = BoundingBox::new(min_x, min_y, max_x, max_y)
1020 .map_err(|e| RegistryError::CrsTransformation(e.to_string()))?;
1021
1022 let transformer = Transformer::new(source_crs.clone(), target_crs.clone())?;
1024 let transformed = transformer.transform_bbox(&source_bbox)?;
1025
1026 Ok((
1027 transformed.min_x,
1028 transformed.min_y,
1029 transformed.max_x,
1030 transformed.max_y,
1031 ))
1032 }
1033}
1034
1035impl Default for DatasetRegistry {
1036 fn default() -> Self {
1037 Self::new()
1038 }
1039}
1040
1041impl Clone for DatasetRegistry {
1042 fn clone(&self) -> Self {
1043 Self {
1044 layers: Arc::clone(&self.layers),
1045 datasets: Arc::clone(&self.datasets),
1046 }
1047 }
1048}
1049
1050#[cfg(test)]
1051mod tests {
1052 use super::*;
1053
1054 #[test]
1055 fn test_registry_creation() {
1056 let registry = DatasetRegistry::new();
1057 assert_eq!(registry.layer_count(), 0);
1058 assert!(!registry.has_layer("test"));
1059 }
1060
1061 #[test]
1062 fn test_layer_not_found() {
1063 let registry = DatasetRegistry::new();
1064 let result = registry.get_layer("nonexistent");
1065 assert!(matches!(result, Err(RegistryError::LayerNotFound(_))));
1066 }
1067
1068 #[test]
1069 fn test_registry_clone() {
1070 let registry1 = DatasetRegistry::new();
1071 let registry2 = registry1.clone();
1072
1073 assert_eq!(registry1.layer_count(), registry2.layer_count());
1074 }
1075
1076 #[test]
1078 fn test_parse_crs_epsg_uppercase() {
1079 let crs = DatasetRegistry::parse_crs("EPSG:4326");
1080 assert!(crs.is_ok());
1081 let crs = crs.expect("should parse");
1082 assert_eq!(crs.epsg_code(), Some(4326));
1083 }
1084
1085 #[test]
1086 fn test_parse_crs_epsg_lowercase() {
1087 let crs = DatasetRegistry::parse_crs("epsg:3857");
1088 assert!(crs.is_ok());
1089 let crs = crs.expect("should parse");
1090 assert_eq!(crs.epsg_code(), Some(3857));
1091 }
1092
1093 #[test]
1094 fn test_parse_crs_proj_string() {
1095 let crs = DatasetRegistry::parse_crs("+proj=longlat +datum=WGS84 +no_defs");
1096 assert!(crs.is_ok());
1097 }
1098
1099 #[test]
1100 fn test_parse_crs_wkt() {
1101 let wkt = r#"GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]]]"#;
1102 let crs = DatasetRegistry::parse_crs(wkt);
1103 assert!(crs.is_ok());
1104 }
1105
1106 #[test]
1107 fn test_parse_crs_invalid() {
1108 let crs = DatasetRegistry::parse_crs("invalid crs");
1109 assert!(matches!(crs, Err(RegistryError::InvalidCrs(_))));
1110 }
1111
1112 #[test]
1113 fn test_parse_crs_invalid_epsg() {
1114 let crs = DatasetRegistry::parse_crs("EPSG:abc");
1115 assert!(matches!(crs, Err(RegistryError::InvalidCrs(_))));
1116 }
1117
1118 #[test]
1120 fn test_crs_strings_equivalent_same() {
1121 assert!(DatasetRegistry::crs_strings_equivalent(
1122 "EPSG:4326",
1123 "EPSG:4326"
1124 ));
1125 }
1126
1127 #[test]
1128 fn test_crs_strings_equivalent_case_insensitive() {
1129 assert!(DatasetRegistry::crs_strings_equivalent(
1130 "EPSG:4326",
1131 "epsg:4326"
1132 ));
1133 assert!(DatasetRegistry::crs_strings_equivalent(
1134 "epsg:3857",
1135 "EPSG:3857"
1136 ));
1137 }
1138
1139 #[test]
1140 fn test_crs_strings_equivalent_with_whitespace() {
1141 assert!(DatasetRegistry::crs_strings_equivalent(
1142 " EPSG:4326 ",
1143 "EPSG:4326"
1144 ));
1145 }
1146
1147 #[test]
1148 fn test_crs_strings_not_equivalent() {
1149 assert!(!DatasetRegistry::crs_strings_equivalent(
1150 "EPSG:4326",
1151 "EPSG:3857"
1152 ));
1153 }
1154
1155 #[test]
1157 fn test_densify_bbox_edges_minimum() {
1158 let points = DatasetRegistry::densify_bbox_edges(0.0, 0.0, 10.0, 10.0, 2);
1159 assert_eq!(points.len(), 4);
1162
1163 assert!(
1165 points
1166 .iter()
1167 .any(|p| (p.x - 0.0).abs() < 1e-10 && (p.y - 0.0).abs() < 1e-10)
1168 );
1169 assert!(
1170 points
1171 .iter()
1172 .any(|p| (p.x - 10.0).abs() < 1e-10 && (p.y - 0.0).abs() < 1e-10)
1173 );
1174 assert!(
1175 points
1176 .iter()
1177 .any(|p| (p.x - 10.0).abs() < 1e-10 && (p.y - 10.0).abs() < 1e-10)
1178 );
1179 assert!(
1180 points
1181 .iter()
1182 .any(|p| (p.x - 0.0).abs() < 1e-10 && (p.y - 10.0).abs() < 1e-10)
1183 );
1184 }
1185
1186 #[test]
1187 fn test_densify_bbox_edges_5_points() {
1188 let points = DatasetRegistry::densify_bbox_edges(0.0, 0.0, 10.0, 10.0, 5);
1189 assert_eq!(points.len(), 16);
1191 }
1192
1193 #[test]
1194 fn test_densify_bbox_edges_21_points() {
1195 let points = DatasetRegistry::densify_bbox_edges(-10.0, -10.0, 10.0, 10.0, 21);
1196 assert_eq!(points.len(), 80);
1198
1199 let has_bottom_left = points
1201 .iter()
1202 .any(|p| (p.x - (-10.0)).abs() < 1e-10 && (p.y - (-10.0)).abs() < 1e-10);
1203 let has_bottom_right = points
1204 .iter()
1205 .any(|p| (p.x - 10.0).abs() < 1e-10 && (p.y - (-10.0)).abs() < 1e-10);
1206 let has_top_right = points
1207 .iter()
1208 .any(|p| (p.x - 10.0).abs() < 1e-10 && (p.y - 10.0).abs() < 1e-10);
1209 let has_top_left = points
1210 .iter()
1211 .any(|p| (p.x - (-10.0)).abs() < 1e-10 && (p.y - 10.0).abs() < 1e-10);
1212
1213 assert!(has_bottom_left, "Should have bottom-left corner");
1214 assert!(has_bottom_right, "Should have bottom-right corner");
1215 assert!(has_top_right, "Should have top-right corner");
1216 assert!(has_top_left, "Should have top-left corner");
1217 }
1218
1219 #[test]
1221 fn test_transform_bbox_same_crs() {
1222 let source_crs = Crs::wgs84();
1223 let target_crs = Crs::wgs84();
1224 let bbox = (0.0, 0.0, 10.0, 10.0);
1225
1226 let result =
1227 DatasetRegistry::transform_bbox_with_densification(bbox, &source_crs, &target_crs);
1228 assert!(result.is_ok());
1229
1230 let (min_x, min_y, max_x, max_y) = result.expect("should transform");
1231 assert!((min_x - 0.0).abs() < 1e-6);
1232 assert!((min_y - 0.0).abs() < 1e-6);
1233 assert!((max_x - 10.0).abs() < 1e-6);
1234 assert!((max_y - 10.0).abs() < 1e-6);
1235 }
1236
1237 #[test]
1238 fn test_transform_bbox_wgs84_to_web_mercator() {
1239 let source_crs = Crs::wgs84();
1240 let target_crs = Crs::web_mercator();
1241
1242 let bbox = (-1.0, -1.0, 1.0, 1.0);
1244
1245 let result =
1246 DatasetRegistry::transform_bbox_with_densification(bbox, &source_crs, &target_crs);
1247 assert!(result.is_ok());
1248
1249 let (min_x, min_y, max_x, max_y) = result.expect("should transform");
1250
1251 assert!(min_x < 0.0, "min_x should be negative");
1254 assert!(min_y < 0.0, "min_y should be negative");
1255 assert!(max_x > 0.0, "max_x should be positive");
1256 assert!(max_y > 0.0, "max_y should be positive");
1257
1258 assert!(min_x > -200_000.0, "min_x should be > -200000");
1260 assert!(max_x < 200_000.0, "max_x should be < 200000");
1261 assert!(min_y > -200_000.0, "min_y should be > -200000");
1262 assert!(max_y < 200_000.0, "max_y should be < 200000");
1263 }
1264
1265 #[test]
1266 fn test_transform_bbox_web_mercator_to_wgs84() {
1267 let source_crs = Crs::web_mercator();
1268 let target_crs = Crs::wgs84();
1269
1270 let bbox = (-1_000_000.0, -1_000_000.0, 1_000_000.0, 1_000_000.0);
1272
1273 let result =
1274 DatasetRegistry::transform_bbox_with_densification(bbox, &source_crs, &target_crs);
1275 assert!(result.is_ok());
1276
1277 let (min_x, min_y, max_x, max_y) = result.expect("should transform");
1278
1279 assert!(
1281 min_x > -15.0 && min_x < -5.0,
1282 "min_x should be around -9 degrees"
1283 );
1284 assert!(
1285 max_x > 5.0 && max_x < 15.0,
1286 "max_x should be around 9 degrees"
1287 );
1288 assert!(
1289 min_y > -15.0 && min_y < -5.0,
1290 "min_y should be around -9 degrees"
1291 );
1292 assert!(
1293 max_y > 5.0 && max_y < 15.0,
1294 "max_y should be around 9 degrees"
1295 );
1296 }
1297
1298 #[test]
1299 fn test_transform_bbox_high_latitude() {
1300 let source_crs = Crs::wgs84();
1301 let target_crs = Crs::web_mercator();
1302
1303 let bbox = (0.0, 50.0, 10.0, 60.0);
1305
1306 let result =
1307 DatasetRegistry::transform_bbox_with_densification(bbox, &source_crs, &target_crs);
1308 assert!(result.is_ok());
1309
1310 let (_min_x, min_y, _max_x, max_y) = result.expect("should transform");
1311
1312 assert!(min_y > 6_000_000.0, "min_y should be > 6M for 50 degrees N");
1314 assert!(max_y > 8_000_000.0, "max_y should be > 8M for 60 degrees N");
1315 }
1316
1317 #[test]
1318 fn test_transform_bbox_simple_vs_densified() {
1319 let source_crs = Crs::wgs84();
1320 let target_crs = Crs::web_mercator();
1321
1322 let bbox = (-20.0, 40.0, 20.0, 70.0);
1324
1325 let simple = DatasetRegistry::transform_bbox_simple(bbox, &source_crs, &target_crs);
1326 let densified =
1327 DatasetRegistry::transform_bbox_with_densification(bbox, &source_crs, &target_crs);
1328
1329 assert!(simple.is_ok());
1330 assert!(densified.is_ok());
1331
1332 let simple = simple.expect("simple should work");
1334 let densified = densified.expect("densified should work");
1335
1336 assert!(
1339 (simple.0 - densified.0).abs() < 100.0,
1340 "min_x should be similar"
1341 );
1342 assert!(
1343 (simple.2 - densified.2).abs() < 100.0,
1344 "max_x should be similar"
1345 );
1346 }
1347
1348 #[test]
1350 fn test_transform_point() {
1351 let source_crs = Crs::wgs84();
1352 let target_crs = Crs::web_mercator();
1353
1354 let result = DatasetRegistry::transform_point(0.0, 0.0, &source_crs, &target_crs);
1355 assert!(result.is_ok());
1356
1357 let (x, y) = result.expect("should transform");
1358 assert!((x - 0.0).abs() < 1.0, "x should be close to 0");
1359 assert!((y - 0.0).abs() < 1.0, "y should be close to 0");
1360 }
1361
1362 #[test]
1363 fn test_transform_point_london() {
1364 let source_crs = Crs::wgs84();
1365 let target_crs = Crs::web_mercator();
1366
1367 let result = DatasetRegistry::transform_point(-0.1276, 51.5074, &source_crs, &target_crs);
1369 assert!(result.is_ok());
1370
1371 let (x, y) = result.expect("should transform");
1372 assert!(x > -20_000.0 && x < 0.0, "x should be slightly negative");
1374 assert!(
1375 y > 6_500_000.0 && y < 7_000_000.0,
1376 "y should be around 6.7M"
1377 );
1378 }
1379
1380 #[test]
1382 fn test_transform_bbox_to_utm() {
1383 let source_crs = Crs::wgs84();
1384 let target_crs = Crs::from_epsg(32632).expect("UTM 32N should exist");
1386
1387 let bbox = (8.0, 48.0, 10.0, 50.0);
1389
1390 let result =
1391 DatasetRegistry::transform_bbox_with_densification(bbox, &source_crs, &target_crs);
1392 assert!(result.is_ok());
1393
1394 let (min_x, min_y, _max_x, _max_y) = result.expect("should transform");
1395
1396 assert!(
1400 min_x > 300_000.0 && min_x < 700_000.0,
1401 "easting should be in valid range"
1402 );
1403 assert!(
1404 min_y > 5_000_000.0 && min_y < 6_000_000.0,
1405 "northing should be in valid range"
1406 );
1407 }
1408
1409 #[test]
1411 fn test_transform_bbox_antimeridian() {
1412 let source_crs = Crs::wgs84();
1413 let target_crs = Crs::web_mercator();
1414
1415 let bbox = (170.0, -10.0, 179.0, 10.0);
1417
1418 let result =
1419 DatasetRegistry::transform_bbox_with_densification(bbox, &source_crs, &target_crs);
1420 assert!(result.is_ok());
1421
1422 let (min_x, _min_y, max_x, _max_y) = result.expect("should transform");
1423 assert!(min_x > 0.0 && max_x > min_x, "should have valid x range");
1424 }
1425
1426 #[test]
1427 fn test_transform_bbox_polar_region() {
1428 let source_crs = Crs::wgs84();
1429 let target_crs = Crs::from_epsg(3413).expect("NSIDC Polar Stereographic should exist");
1431
1432 let bbox = (-10.0, 70.0, 10.0, 80.0);
1434
1435 let result =
1436 DatasetRegistry::transform_bbox_with_densification(bbox, &source_crs, &target_crs);
1437 assert!(result.is_ok());
1438 }
1439
1440 #[test]
1442 fn test_transform_bbox_invalid_crs() {
1443 let crs = DatasetRegistry::parse_crs("EPSG:99999");
1445 assert!(crs.is_err());
1446 }
1447}