Skip to main content

oxigdal_server/
dataset_registry.rs

1//! Dataset registry for managing available layers
2//!
3//! This module provides a thread-safe registry for managing GDAL datasets
4//! that can be served via WMS/WMTS protocols.
5//!
6//! # CRS Transformation
7//!
8//! The registry supports bounding box transformation between coordinate reference systems.
9//! When requesting a layer's bounding box in a target CRS different from the native CRS,
10//! the registry performs:
11//!
12//! 1. Edge densification - Adding intermediate points along bbox edges for accurate transformation
13//! 2. Coordinate transformation - Using proj4rs for coordinate conversion
14//! 3. Axis order handling - Correctly handling lat/lon vs lon/lat conventions
15//!
16//! ## Supported CRS
17//!
18//! - EPSG:4326 (WGS84 - Geographic)
19//! - EPSG:3857 (Web Mercator - Projected)
20//! - All WGS84 UTM zones (EPSG:32601-32660 North, EPSG:32701-32760 South)
21//! - Common national datums (NAD83, ETRS89, GDA94, JGD2000, etc.)
22
23use crate::config::{ConfigError, LayerConfig};
24use oxigdal_core::buffer::RasterBuffer;
25use oxigdal_core::io::FileDataSource;
26use oxigdal_core::types::{GeoTransform, NoDataValue, RasterDataType};
27use oxigdal_geotiff::GeoTiffReader;
28use oxigdal_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
35/// Dataset wrapper for GeoTIFF files
36pub struct Dataset {
37    /// Path to the dataset file
38    path: PathBuf,
39    /// GeoTIFF reader (wrapped for thread-safety)
40    reader: RwLock<Option<GeoTiffReader<FileDataSource>>>,
41    /// Cached metadata
42    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    /// Open a dataset from a file path
54    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, oxigdal_core::OxiGdalError> {
55        let path_buf = path.as_ref().to_path_buf();
56
57        // Open the file source
58        let source = FileDataSource::open(&path_buf)?;
59
60        // Create the GeoTIFF reader
61        let reader = GeoTiffReader::open(source)?;
62
63        // Extract metadata
64        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    /// Get the file path
88    #[must_use]
89    pub fn path(&self) -> &Path {
90        &self.path
91    }
92
93    /// Get raster size (width, height)
94    #[must_use]
95    pub fn raster_size(&self) -> (usize, usize) {
96        (self.width as usize, self.height as usize)
97    }
98
99    /// Get raster width
100    #[must_use]
101    pub fn width(&self) -> u64 {
102        self.width
103    }
104
105    /// Get raster height
106    #[must_use]
107    pub fn height(&self) -> u64 {
108        self.height
109    }
110
111    /// Get raster band count
112    #[must_use]
113    pub fn raster_count(&self) -> usize {
114        self.band_count as usize
115    }
116
117    /// Get the data type
118    #[must_use]
119    pub fn data_type(&self) -> RasterDataType {
120        self.data_type
121    }
122
123    /// Get projection (WKT string)
124    pub fn projection(&self) -> Result<String, oxigdal_core::OxiGdalError> {
125        // Return EPSG code if available
126        if let Ok(guard) = self.reader.read() {
127            if let Some(ref reader) = *guard {
128                if let Some(epsg) = reader.epsg_code() {
129                    return Ok(format!("EPSG:{}", epsg));
130                }
131            }
132        }
133        Ok("EPSG:4326".to_string())
134    }
135
136    /// Get geotransform as array
137    pub fn geotransform(&self) -> Result<[f64; 6], oxigdal_core::OxiGdalError> {
138        if let Some(ref gt) = self.geo_transform {
139            Ok(gt.to_gdal_array())
140        } else {
141            // Default identity transform
142            Ok([0.0, 1.0, 0.0, 0.0, 0.0, -1.0])
143        }
144    }
145
146    /// Get the GeoTransform
147    #[must_use]
148    pub fn geo_transform_obj(&self) -> Option<&GeoTransform> {
149        self.geo_transform.as_ref()
150    }
151
152    /// Get NoData value
153    #[must_use]
154    pub fn nodata(&self) -> NoDataValue {
155        self.nodata
156    }
157
158    /// Get tile size if tiled
159    #[must_use]
160    pub fn tile_size(&self) -> Option<(u32, u32)> {
161        self.tile_size
162    }
163
164    /// Get number of overview levels
165    #[must_use]
166    pub fn overview_count(&self) -> usize {
167        self.overview_count
168    }
169
170    /// Get bounding box
171    #[must_use]
172    pub fn bounds(&self) -> Option<oxigdal_core::types::BoundingBox> {
173        self.geo_transform
174            .as_ref()
175            .map(|gt| gt.compute_bounds(self.width, self.height))
176    }
177
178    /// Read a tile from the dataset
179    ///
180    /// # Arguments
181    /// * `level` - Overview level (0 = full resolution)
182    /// * `tile_x` - Tile X coordinate
183    /// * `tile_y` - Tile Y coordinate
184    pub fn read_tile(
185        &self,
186        level: usize,
187        tile_x: u32,
188        tile_y: u32,
189    ) -> Result<Vec<u8>, oxigdal_core::OxiGdalError> {
190        let guard = self
191            .reader
192            .read()
193            .map_err(|_| oxigdal_core::OxiGdalError::Internal {
194                message: "Failed to acquire read lock".to_string(),
195            })?;
196
197        if let Some(ref reader) = *guard {
198            reader.read_tile(level, tile_x, tile_y)
199        } else {
200            Err(oxigdal_core::OxiGdalError::Internal {
201                message: "Reader not initialized".to_string(),
202            })
203        }
204    }
205
206    /// Read a tile as RasterBuffer
207    pub fn read_tile_buffer(
208        &self,
209        level: usize,
210        tile_x: u32,
211        tile_y: u32,
212    ) -> Result<RasterBuffer, oxigdal_core::OxiGdalError> {
213        let guard = self
214            .reader
215            .read()
216            .map_err(|_| oxigdal_core::OxiGdalError::Internal {
217                message: "Failed to acquire read lock".to_string(),
218            })?;
219
220        if let Some(ref reader) = *guard {
221            reader.read_tile_buffer(level, tile_x, tile_y)
222        } else {
223            Err(oxigdal_core::OxiGdalError::Internal {
224                message: "Reader not initialized".to_string(),
225            })
226        }
227    }
228
229    /// Read full band data
230    pub fn read_band(
231        &self,
232        level: usize,
233        band: usize,
234    ) -> Result<Vec<u8>, oxigdal_core::OxiGdalError> {
235        let guard = self
236            .reader
237            .read()
238            .map_err(|_| oxigdal_core::OxiGdalError::Internal {
239                message: "Failed to acquire read lock".to_string(),
240            })?;
241
242        if let Some(ref reader) = *guard {
243            reader.read_band(level, band)
244        } else {
245            Err(oxigdal_core::OxiGdalError::Internal {
246                message: "Reader not initialized".to_string(),
247            })
248        }
249    }
250
251    /// Read a window of data from the dataset as RasterBuffer
252    ///
253    /// # Arguments
254    /// * `x_offset` - X offset in pixels
255    /// * `y_offset` - Y offset in pixels
256    /// * `x_size` - Width to read
257    /// * `y_size` - Height to read
258    pub fn read_window(
259        &self,
260        x_offset: u64,
261        y_offset: u64,
262        x_size: u64,
263        y_size: u64,
264    ) -> Result<RasterBuffer, oxigdal_core::OxiGdalError> {
265        // Validate window bounds
266        if x_offset >= self.width || y_offset >= self.height {
267            return Err(oxigdal_core::OxiGdalError::OutOfBounds {
268                message: format!(
269                    "Window offset ({}, {}) out of bounds ({}x{})",
270                    x_offset, y_offset, self.width, self.height
271                ),
272            });
273        }
274
275        // Clamp window size to dataset bounds
276        let actual_x_size = x_size.min(self.width - x_offset);
277        let actual_y_size = y_size.min(self.height - y_offset);
278
279        // Create output buffer with requested size
280        let mut window_buffer = RasterBuffer::zeros(x_size, y_size, self.data_type);
281
282        // Get tile dimensions (default to 256x256 if not tiled)
283        let (tile_w, tile_h) = self.tile_size.unwrap_or((256, 256));
284        let tile_w = tile_w as u64;
285        let tile_h = tile_h as u64;
286
287        // Calculate tile range that intersects with the requested window
288        let start_tile_x = x_offset / tile_w;
289        let start_tile_y = y_offset / tile_h;
290        let end_tile_x = (x_offset + actual_x_size).div_ceil(tile_w);
291        let end_tile_y = (y_offset + actual_y_size).div_ceil(tile_h);
292
293        // Read only the tiles that intersect with the window
294        for tile_y in start_tile_y..end_tile_y {
295            for tile_x in start_tile_x..end_tile_x {
296                // Calculate tile boundaries in dataset coordinates
297                let tile_pixel_x = tile_x * tile_w;
298                let tile_pixel_y = tile_y * tile_h;
299
300                // Read the tile
301                let tile_buffer = match self.read_tile_buffer(0, tile_x as u32, tile_y as u32) {
302                    Ok(buf) => buf,
303                    Err(e) => {
304                        // If tile read fails, skip it (may be outside bounds or missing)
305                        debug!("Failed to read tile ({}, {}): {}", tile_x, tile_y, e);
306                        continue;
307                    }
308                };
309
310                let tile_width = tile_buffer.width();
311                let tile_height = tile_buffer.height();
312
313                // Calculate the intersection between tile and requested window
314                let win_min_x = x_offset;
315                let win_min_y = y_offset;
316                let win_max_x = x_offset + actual_x_size;
317                let win_max_y = y_offset + actual_y_size;
318
319                let tile_max_x = (tile_pixel_x + tile_width).min(self.width);
320                let tile_max_y = (tile_pixel_y + tile_height).min(self.height);
321
322                let intersect_min_x = win_min_x.max(tile_pixel_x);
323                let intersect_min_y = win_min_y.max(tile_pixel_y);
324                let intersect_max_x = win_max_x.min(tile_max_x);
325                let intersect_max_y = win_max_y.min(tile_max_y);
326
327                // Copy pixels from tile to window buffer
328                for src_y in intersect_min_y..intersect_max_y {
329                    for src_x in intersect_min_x..intersect_max_x {
330                        // Calculate position in tile coordinates
331                        let tile_local_x = src_x - tile_pixel_x;
332                        let tile_local_y = src_y - tile_pixel_y;
333
334                        // Calculate position in window coordinates
335                        let win_local_x = src_x - x_offset;
336                        let win_local_y = src_y - y_offset;
337
338                        // Read pixel from tile and write to window buffer
339                        if let Ok(value) = tile_buffer.get_pixel(tile_local_x, tile_local_y) {
340                            let _ = window_buffer.set_pixel(win_local_x, win_local_y, value);
341                        }
342                    }
343                }
344            }
345        }
346
347        Ok(window_buffer)
348    }
349
350    /// Get pixel value at coordinates
351    pub fn get_pixel(&self, x: u64, y: u64) -> Result<f64, oxigdal_core::OxiGdalError> {
352        if x >= self.width || y >= self.height {
353            return Err(oxigdal_core::OxiGdalError::OutOfBounds {
354                message: format!(
355                    "Pixel ({}, {}) out of bounds ({}x{})",
356                    x, y, self.width, self.height
357                ),
358            });
359        }
360
361        // Determine which tile contains this pixel
362        let (tile_w, tile_h) = self.tile_size.unwrap_or((256, 256));
363        let tile_x = x / tile_w as u64;
364        let tile_y = y / tile_h as u64;
365        let local_x = x % tile_w as u64;
366        let local_y = y % tile_h as u64;
367
368        // Read the tile
369        let tile_buffer = self.read_tile_buffer(0, tile_x as u32, tile_y as u32)?;
370
371        // Get the pixel value
372        tile_buffer.get_pixel(local_x, local_y)
373    }
374
375    /// Get raster band info (for compatibility with old code)
376    pub fn rasterband(&self, _band: usize) -> Result<RasterBandInfo, oxigdal_core::OxiGdalError> {
377        Ok(RasterBandInfo {
378            nodata: self.nodata,
379            data_type: self.data_type,
380        })
381    }
382}
383
384/// Raster band information (compatible with old interface)
385pub struct RasterBandInfo {
386    nodata: NoDataValue,
387    data_type: RasterDataType,
388}
389
390impl RasterBandInfo {
391    /// Get nodata value
392    pub fn nodata(&self) -> Option<f64> {
393        self.nodata.as_f64()
394    }
395
396    /// Get datatype string
397    pub fn datatype(&self) -> &str {
398        match self.data_type {
399            RasterDataType::UInt8 => "UInt8",
400            RasterDataType::Int8 => "Int8",
401            RasterDataType::UInt16 => "UInt16",
402            RasterDataType::Int16 => "Int16",
403            RasterDataType::UInt32 => "UInt32",
404            RasterDataType::Int32 => "Int32",
405            RasterDataType::Float32 => "Float32",
406            RasterDataType::Float64 => "Float64",
407            RasterDataType::UInt64 => "UInt64",
408            RasterDataType::Int64 => "Int64",
409            RasterDataType::CFloat32 => "CFloat32",
410            RasterDataType::CFloat64 => "CFloat64",
411        }
412    }
413}
414
415/// Registry errors
416#[derive(Debug, Error)]
417pub enum RegistryError {
418    /// Layer not found
419    #[error("Layer not found: {0}")]
420    LayerNotFound(String),
421
422    /// Dataset open error
423    #[error("Failed to open dataset: {0}")]
424    DatasetOpen(#[from] oxigdal_core::OxiGdalError),
425
426    /// Configuration error
427    #[error("Configuration error: {0}")]
428    Config(#[from] ConfigError),
429
430    /// CRS transformation error
431    #[error("CRS transformation failed: {0}")]
432    CrsTransformation(String),
433
434    /// Invalid CRS specification
435    #[error("Invalid CRS: {0}")]
436    InvalidCrs(String),
437
438    /// Lock poisoned
439    #[error("Lock poisoned")]
440    LockPoisoned,
441}
442
443impl From<oxigdal_proj::Error> for RegistryError {
444    fn from(err: oxigdal_proj::Error) -> Self {
445        RegistryError::CrsTransformation(err.to_string())
446    }
447}
448
449/// Result type for registry operations
450pub type RegistryResult<T> = Result<T, RegistryError>;
451
452/// Information about a registered layer
453#[derive(Debug, Clone)]
454pub struct LayerInfo {
455    /// Layer name
456    pub name: String,
457
458    /// Display title
459    pub title: String,
460
461    /// Layer description
462    pub abstract_: String,
463
464    /// Configuration
465    pub config: LayerConfig,
466
467    /// Dataset metadata
468    pub metadata: DatasetMetadata,
469}
470
471/// Dataset metadata extracted from GDAL
472#[derive(Debug, Clone)]
473pub struct DatasetMetadata {
474    /// Dataset width in pixels
475    pub width: usize,
476
477    /// Dataset height in pixels
478    pub height: usize,
479
480    /// Number of bands
481    pub band_count: usize,
482
483    /// Data type name
484    pub data_type: String,
485
486    /// Spatial reference system (WKT)
487    pub srs: Option<String>,
488
489    /// Bounding box (min_x, min_y, max_x, max_y)
490    pub bbox: Option<(f64, f64, f64, f64)>,
491
492    /// Geotransform coefficients
493    pub geotransform: Option<[f64; 6]>,
494
495    /// NoData value
496    pub nodata: Option<f64>,
497}
498
499/// Thread-safe dataset registry
500pub struct DatasetRegistry {
501    /// Registered layers
502    layers: Arc<RwLock<HashMap<String, LayerInfo>>>,
503
504    /// Dataset cache (opened datasets)
505    datasets: Arc<RwLock<HashMap<String, Arc<Dataset>>>>,
506}
507
508impl DatasetRegistry {
509    /// Create a new empty registry
510    pub fn new() -> Self {
511        Self {
512            layers: Arc::new(RwLock::new(HashMap::new())),
513            datasets: Arc::new(RwLock::new(HashMap::new())),
514        }
515    }
516
517    /// Register a layer from configuration
518    pub fn register_layer(&self, config: LayerConfig) -> RegistryResult<()> {
519        info!("Registering layer: {}", config.name);
520
521        // Save the name before moving config
522        let layer_name = config.name.clone();
523
524        // Open dataset to extract metadata
525        let dataset = Self::open_dataset(&config.path)?;
526        let metadata = Self::extract_metadata(&dataset)?;
527
528        debug!(
529            "Layer {} metadata: {}x{}, {} bands",
530            layer_name, metadata.width, metadata.height, metadata.band_count
531        );
532
533        let layer_info = LayerInfo {
534            name: config.name.clone(),
535            title: config.title.clone().unwrap_or_else(|| config.name.clone()),
536            abstract_: config
537                .abstract_
538                .clone()
539                .unwrap_or_else(|| format!("Layer {}", config.name)),
540            config,
541            metadata,
542        };
543
544        // Store layer info
545        let mut layers = self
546            .layers
547            .write()
548            .map_err(|_| RegistryError::LockPoisoned)?;
549        layers.insert(layer_info.name.clone(), layer_info);
550
551        // Cache the dataset
552        let mut datasets = self
553            .datasets
554            .write()
555            .map_err(|_| RegistryError::LockPoisoned)?;
556        datasets.insert(layer_name, Arc::new(dataset));
557
558        Ok(())
559    }
560
561    /// Register multiple layers from configurations
562    pub fn register_layers(&self, configs: Vec<LayerConfig>) -> RegistryResult<()> {
563        for config in configs {
564            if !config.enabled {
565                debug!("Skipping disabled layer: {}", config.name);
566                continue;
567            }
568
569            if let Err(e) = self.register_layer(config) {
570                warn!("Failed to register layer: {}", e);
571                // Continue with other layers
572            }
573        }
574        Ok(())
575    }
576
577    /// Get layer information
578    pub fn get_layer(&self, name: &str) -> RegistryResult<LayerInfo> {
579        let layers = self
580            .layers
581            .read()
582            .map_err(|_| RegistryError::LockPoisoned)?;
583
584        layers
585            .get(name)
586            .cloned()
587            .ok_or_else(|| RegistryError::LayerNotFound(name.to_string()))
588    }
589
590    /// Get a dataset for a layer
591    pub fn get_dataset(&self, name: &str) -> RegistryResult<Arc<Dataset>> {
592        let datasets = self
593            .datasets
594            .read()
595            .map_err(|_| RegistryError::LockPoisoned)?;
596
597        datasets
598            .get(name)
599            .cloned()
600            .ok_or_else(|| RegistryError::LayerNotFound(name.to_string()))
601    }
602
603    /// List all registered layers
604    pub fn list_layers(&self) -> RegistryResult<Vec<LayerInfo>> {
605        let layers = self
606            .layers
607            .read()
608            .map_err(|_| RegistryError::LockPoisoned)?;
609
610        Ok(layers.values().cloned().collect())
611    }
612
613    /// Check if a layer exists
614    pub fn has_layer(&self, name: &str) -> bool {
615        self.layers
616            .read()
617            .map(|layers| layers.contains_key(name))
618            .unwrap_or(false)
619    }
620
621    /// Remove a layer from the registry
622    pub fn unregister_layer(&self, name: &str) -> RegistryResult<()> {
623        let mut layers = self
624            .layers
625            .write()
626            .map_err(|_| RegistryError::LockPoisoned)?;
627
628        let mut datasets = self
629            .datasets
630            .write()
631            .map_err(|_| RegistryError::LockPoisoned)?;
632
633        layers.remove(name);
634        datasets.remove(name);
635
636        info!("Unregistered layer: {}", name);
637        Ok(())
638    }
639
640    /// Get the number of registered layers
641    pub fn layer_count(&self) -> usize {
642        self.layers.read().map(|l| l.len()).unwrap_or(0)
643    }
644
645    /// Open a dataset from a path
646    fn open_dataset<P: AsRef<Path>>(path: P) -> RegistryResult<Dataset> {
647        let dataset = Dataset::open(path.as_ref())?;
648        Ok(dataset)
649    }
650
651    /// Extract metadata from a dataset
652    fn extract_metadata(dataset: &Dataset) -> RegistryResult<DatasetMetadata> {
653        let raster_size = dataset.raster_size();
654        let band_count = dataset.raster_count();
655
656        // Get spatial reference
657        let srs = dataset.projection().ok();
658
659        // Get bounding box from geotransform
660        let geotransform = dataset.geotransform().ok();
661        let bbox = geotransform.map(|gt| {
662            let width = raster_size.0 as f64;
663            let height = raster_size.1 as f64;
664
665            let min_x = gt[0];
666            let max_x = gt[0] + gt[1] * width + gt[2] * height;
667            let max_y = gt[3];
668            let min_y = gt[3] + gt[4] * width + gt[5] * height;
669
670            // Ensure proper order
671            let (min_x, max_x) = if min_x < max_x {
672                (min_x, max_x)
673            } else {
674                (max_x, min_x)
675            };
676            let (min_y, max_y) = if min_y < max_y {
677                (min_y, max_y)
678            } else {
679                (max_y, min_y)
680            };
681
682            (min_x, min_y, max_x, max_y)
683        });
684
685        // Get NoData value from first band
686        let nodata = if band_count > 0 {
687            dataset.rasterband(1).ok().and_then(|band| band.nodata())
688        } else {
689            None
690        };
691
692        // Get data type from first band
693        let data_type = if band_count > 0 {
694            dataset
695                .rasterband(1)
696                .ok()
697                .map(|band| format!("{:?}", band.datatype()))
698                .unwrap_or_else(|| "Unknown".to_string())
699        } else {
700            "Unknown".to_string()
701        };
702
703        Ok(DatasetMetadata {
704            width: raster_size.0,
705            height: raster_size.1,
706            band_count,
707            data_type,
708            srs,
709            bbox,
710            geotransform,
711            nodata,
712        })
713    }
714
715    /// Reload a layer (useful after dataset updates)
716    pub fn reload_layer(&self, name: &str) -> RegistryResult<()> {
717        let config = {
718            let layers = self
719                .layers
720                .read()
721                .map_err(|_| RegistryError::LockPoisoned)?;
722
723            layers
724                .get(name)
725                .ok_or_else(|| RegistryError::LayerNotFound(name.to_string()))?
726                .config
727                .clone()
728        };
729
730        self.unregister_layer(name)?;
731        self.register_layer(config)?;
732
733        info!("Reloaded layer: {}", name);
734        Ok(())
735    }
736
737    /// Get layer bounding box in a specific CRS
738    ///
739    /// This method transforms the native bounding box of a layer to a target CRS.
740    /// The transformation uses edge densification for accurate results, especially
741    /// when transforming between geographic and projected coordinate systems.
742    ///
743    /// # Arguments
744    ///
745    /// * `name` - The layer name
746    /// * `target_crs` - Optional target CRS specification (e.g., "EPSG:4326", "EPSG:3857")
747    ///
748    /// # Returns
749    ///
750    /// The bounding box in the target CRS as (min_x, min_y, max_x, max_y), or None
751    /// if the layer has no bounding box defined.
752    ///
753    /// # Errors
754    ///
755    /// Returns an error if:
756    /// - The layer is not found
757    /// - The source or target CRS is invalid
758    /// - The transformation fails
759    ///
760    /// # Example
761    ///
762    /// ```ignore
763    /// let registry = DatasetRegistry::new();
764    /// // Get bounding box in Web Mercator
765    /// let bbox = registry.get_layer_bbox("my_layer", Some("EPSG:3857"))?;
766    /// ```
767    pub fn get_layer_bbox(
768        &self,
769        name: &str,
770        target_crs: Option<&str>,
771    ) -> RegistryResult<Option<(f64, f64, f64, f64)>> {
772        let layer = self.get_layer(name)?;
773
774        // Return native bbox if no target CRS specified
775        let target_crs_str = match target_crs {
776            Some(crs) => crs,
777            None => return Ok(layer.metadata.bbox),
778        };
779
780        // Get the native bbox
781        let native_bbox = match layer.metadata.bbox {
782            Some(bbox) => bbox,
783            None => return Ok(None),
784        };
785
786        // Get the source CRS from layer metadata
787        let source_crs_str = layer.metadata.srs.as_deref().unwrap_or("EPSG:4326");
788
789        // Check if source and target CRS are the same
790        if Self::crs_strings_equivalent(source_crs_str, target_crs_str) {
791            return Ok(Some(native_bbox));
792        }
793
794        // Parse source and target CRS
795        let source_crs = Self::parse_crs(source_crs_str)?;
796        let target_crs = Self::parse_crs(target_crs_str)?;
797
798        // Transform the bounding box
799        let transformed =
800            Self::transform_bbox_with_densification(native_bbox, &source_crs, &target_crs)?;
801
802        Ok(Some(transformed))
803    }
804
805    /// Parse a CRS string into a Crs object
806    ///
807    /// Supports formats:
808    /// - "EPSG:4326" or "epsg:4326"
809    /// - PROJ strings ("+proj=longlat +datum=WGS84 +no_defs")
810    /// - WKT strings
811    fn parse_crs(crs_str: &str) -> RegistryResult<Crs> {
812        let trimmed = crs_str.trim();
813
814        // Try EPSG code format
815        if let Some(code_str) = trimmed.to_uppercase().strip_prefix("EPSG:") {
816            let code: u32 = code_str.parse().map_err(|_| {
817                RegistryError::InvalidCrs(format!("Invalid EPSG code: {}", code_str))
818            })?;
819            return Ok(Crs::from_epsg(code)?);
820        }
821
822        // Try PROJ string
823        if trimmed.starts_with("+proj=") || trimmed.contains("+proj=") {
824            return Ok(Crs::from_proj(trimmed)?);
825        }
826
827        // Try WKT
828        if trimmed.starts_with("GEOGCS[")
829            || trimmed.starts_with("PROJCS[")
830            || trimmed.starts_with("GEOCCS[")
831        {
832            return Ok(Crs::from_wkt(trimmed)?);
833        }
834
835        Err(RegistryError::InvalidCrs(format!(
836            "Unrecognized CRS format: {}",
837            crs_str
838        )))
839    }
840
841    /// Check if two CRS strings refer to the same CRS
842    fn crs_strings_equivalent(crs1: &str, crs2: &str) -> bool {
843        let norm1 = crs1.trim().to_uppercase();
844        let norm2 = crs2.trim().to_uppercase();
845        norm1 == norm2
846    }
847
848    /// Transform a bounding box with edge densification for accurate results
849    ///
850    /// For accurate transformation between coordinate systems (especially between
851    /// geographic and projected CRS), we densify the edges of the bounding box
852    /// by adding intermediate points. This accounts for the curvature introduced
853    /// by the projection.
854    ///
855    /// # Arguments
856    ///
857    /// * `bbox` - The bounding box as (min_x, min_y, max_x, max_y)
858    /// * `source_crs` - Source coordinate reference system
859    /// * `target_crs` - Target coordinate reference system
860    ///
861    /// # Returns
862    ///
863    /// The transformed bounding box as (min_x, min_y, max_x, max_y)
864    fn transform_bbox_with_densification(
865        bbox: (f64, f64, f64, f64),
866        source_crs: &Crs,
867        target_crs: &Crs,
868    ) -> RegistryResult<(f64, f64, f64, f64)> {
869        let (min_x, min_y, max_x, max_y) = bbox;
870
871        // Create transformer
872        let transformer = Transformer::new(source_crs.clone(), target_crs.clone())?;
873
874        // Number of points to sample along each edge for accurate transformation
875        // More points = more accurate but slower
876        // 21 points per edge is a good balance (20 segments)
877        const DENSIFY_POINTS: usize = 21;
878
879        // Generate densified edge points
880        let edge_points = Self::densify_bbox_edges(min_x, min_y, max_x, max_y, DENSIFY_POINTS);
881
882        // Transform all edge points
883        let transformed_points: Vec<Coordinate> = edge_points
884            .iter()
885            .filter_map(|coord| transformer.transform(coord).ok())
886            .collect();
887
888        // Check if we got valid transformed points
889        if transformed_points.is_empty() {
890            return Err(RegistryError::CrsTransformation(
891                "All points failed to transform".to_string(),
892            ));
893        }
894
895        // Find the bounding box of transformed points
896        let mut result_min_x = f64::INFINITY;
897        let mut result_min_y = f64::INFINITY;
898        let mut result_max_x = f64::NEG_INFINITY;
899        let mut result_max_y = f64::NEG_INFINITY;
900
901        for point in &transformed_points {
902            if point.x.is_finite() && point.y.is_finite() {
903                result_min_x = result_min_x.min(point.x);
904                result_min_y = result_min_y.min(point.y);
905                result_max_x = result_max_x.max(point.x);
906                result_max_y = result_max_y.max(point.y);
907            }
908        }
909
910        // Verify we have valid results
911        if !result_min_x.is_finite()
912            || !result_min_y.is_finite()
913            || !result_max_x.is_finite()
914            || !result_max_y.is_finite()
915        {
916            return Err(RegistryError::CrsTransformation(
917                "Transformation resulted in non-finite values".to_string(),
918            ));
919        }
920
921        Ok((result_min_x, result_min_y, result_max_x, result_max_y))
922    }
923
924    /// Generate densified points along the edges of a bounding box
925    ///
926    /// Creates evenly spaced points along all four edges of the bbox.
927    /// The points are ordered: bottom edge, right edge, top edge, left edge.
928    fn densify_bbox_edges(
929        min_x: f64,
930        min_y: f64,
931        max_x: f64,
932        max_y: f64,
933        points_per_edge: usize,
934    ) -> Vec<Coordinate> {
935        let mut points = Vec::with_capacity(points_per_edge * 4);
936
937        // Ensure at least 2 points per edge (corners)
938        let n = points_per_edge.max(2);
939
940        // Bottom edge (min_y, from min_x to max_x)
941        for i in 0..n {
942            let t = i as f64 / (n - 1) as f64;
943            let x = min_x + t * (max_x - min_x);
944            points.push(Coordinate::new(x, min_y));
945        }
946
947        // Right edge (max_x, from min_y to max_y)
948        // Skip first point to avoid duplicate corner
949        for i in 1..n {
950            let t = i as f64 / (n - 1) as f64;
951            let y = min_y + t * (max_y - min_y);
952            points.push(Coordinate::new(max_x, y));
953        }
954
955        // Top edge (max_y, from max_x to min_x)
956        // Skip first point to avoid duplicate corner
957        for i in 1..n {
958            let t = i as f64 / (n - 1) as f64;
959            let x = max_x - t * (max_x - min_x);
960            points.push(Coordinate::new(x, max_y));
961        }
962
963        // Left edge (min_x, from max_y to min_y)
964        // Skip first and last points to avoid duplicate corners
965        for i in 1..(n - 1) {
966            let t = i as f64 / (n - 1) as f64;
967            let y = max_y - t * (max_y - min_y);
968            points.push(Coordinate::new(min_x, y));
969        }
970
971        points
972    }
973
974    /// Transform a single point from source to target CRS
975    ///
976    /// Convenience method for transforming individual coordinates.
977    #[allow(dead_code)]
978    fn transform_point(
979        x: f64,
980        y: f64,
981        source_crs: &Crs,
982        target_crs: &Crs,
983    ) -> RegistryResult<(f64, f64)> {
984        let transformer = Transformer::new(source_crs.clone(), target_crs.clone())?;
985        let coord = Coordinate::new(x, y);
986        let transformed = transformer.transform(&coord)?;
987        Ok((transformed.x, transformed.y))
988    }
989
990    /// Get layer bounding box in Web Mercator (EPSG:3857)
991    ///
992    /// Convenience method for getting bbox in the most common web mapping CRS.
993    pub fn get_layer_bbox_web_mercator(
994        &self,
995        name: &str,
996    ) -> RegistryResult<Option<(f64, f64, f64, f64)>> {
997        self.get_layer_bbox(name, Some("EPSG:3857"))
998    }
999
1000    /// Get layer bounding box in WGS84 (EPSG:4326)
1001    ///
1002    /// Convenience method for getting bbox in geographic coordinates.
1003    pub fn get_layer_bbox_wgs84(&self, name: &str) -> RegistryResult<Option<(f64, f64, f64, f64)>> {
1004        self.get_layer_bbox(name, Some("EPSG:4326"))
1005    }
1006
1007    /// Transform bounding box using the simple 4-corner method
1008    ///
1009    /// This is faster but less accurate than densification for projections
1010    /// with significant curvature. Use for quick approximations.
1011    #[allow(dead_code)]
1012    fn transform_bbox_simple(
1013        bbox: (f64, f64, f64, f64),
1014        source_crs: &Crs,
1015        target_crs: &Crs,
1016    ) -> RegistryResult<(f64, f64, f64, f64)> {
1017        let (min_x, min_y, max_x, max_y) = bbox;
1018
1019        // Create bounding box using oxigdal_proj
1020        let source_bbox = BoundingBox::new(min_x, min_y, max_x, max_y)
1021            .map_err(|e| RegistryError::CrsTransformation(e.to_string()))?;
1022
1023        // Create transformer and transform bbox
1024        let transformer = Transformer::new(source_crs.clone(), target_crs.clone())?;
1025        let transformed = transformer.transform_bbox(&source_bbox)?;
1026
1027        Ok((
1028            transformed.min_x,
1029            transformed.min_y,
1030            transformed.max_x,
1031            transformed.max_y,
1032        ))
1033    }
1034}
1035
1036impl Default for DatasetRegistry {
1037    fn default() -> Self {
1038        Self::new()
1039    }
1040}
1041
1042impl Clone for DatasetRegistry {
1043    fn clone(&self) -> Self {
1044        Self {
1045            layers: Arc::clone(&self.layers),
1046            datasets: Arc::clone(&self.datasets),
1047        }
1048    }
1049}
1050
1051#[cfg(test)]
1052mod tests {
1053    use super::*;
1054
1055    #[test]
1056    fn test_registry_creation() {
1057        let registry = DatasetRegistry::new();
1058        assert_eq!(registry.layer_count(), 0);
1059        assert!(!registry.has_layer("test"));
1060    }
1061
1062    #[test]
1063    fn test_layer_not_found() {
1064        let registry = DatasetRegistry::new();
1065        let result = registry.get_layer("nonexistent");
1066        assert!(matches!(result, Err(RegistryError::LayerNotFound(_))));
1067    }
1068
1069    #[test]
1070    fn test_registry_clone() {
1071        let registry1 = DatasetRegistry::new();
1072        let registry2 = registry1.clone();
1073
1074        assert_eq!(registry1.layer_count(), registry2.layer_count());
1075    }
1076
1077    // CRS parsing tests
1078    #[test]
1079    fn test_parse_crs_epsg_uppercase() {
1080        let crs = DatasetRegistry::parse_crs("EPSG:4326");
1081        assert!(crs.is_ok());
1082        let crs = crs.expect("should parse");
1083        assert_eq!(crs.epsg_code(), Some(4326));
1084    }
1085
1086    #[test]
1087    fn test_parse_crs_epsg_lowercase() {
1088        let crs = DatasetRegistry::parse_crs("epsg:3857");
1089        assert!(crs.is_ok());
1090        let crs = crs.expect("should parse");
1091        assert_eq!(crs.epsg_code(), Some(3857));
1092    }
1093
1094    #[test]
1095    fn test_parse_crs_proj_string() {
1096        let crs = DatasetRegistry::parse_crs("+proj=longlat +datum=WGS84 +no_defs");
1097        assert!(crs.is_ok());
1098    }
1099
1100    #[test]
1101    fn test_parse_crs_wkt() {
1102        let wkt = r#"GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]]]"#;
1103        let crs = DatasetRegistry::parse_crs(wkt);
1104        assert!(crs.is_ok());
1105    }
1106
1107    #[test]
1108    fn test_parse_crs_invalid() {
1109        let crs = DatasetRegistry::parse_crs("invalid crs");
1110        assert!(matches!(crs, Err(RegistryError::InvalidCrs(_))));
1111    }
1112
1113    #[test]
1114    fn test_parse_crs_invalid_epsg() {
1115        let crs = DatasetRegistry::parse_crs("EPSG:abc");
1116        assert!(matches!(crs, Err(RegistryError::InvalidCrs(_))));
1117    }
1118
1119    // CRS equivalence tests
1120    #[test]
1121    fn test_crs_strings_equivalent_same() {
1122        assert!(DatasetRegistry::crs_strings_equivalent(
1123            "EPSG:4326",
1124            "EPSG:4326"
1125        ));
1126    }
1127
1128    #[test]
1129    fn test_crs_strings_equivalent_case_insensitive() {
1130        assert!(DatasetRegistry::crs_strings_equivalent(
1131            "EPSG:4326",
1132            "epsg:4326"
1133        ));
1134        assert!(DatasetRegistry::crs_strings_equivalent(
1135            "epsg:3857",
1136            "EPSG:3857"
1137        ));
1138    }
1139
1140    #[test]
1141    fn test_crs_strings_equivalent_with_whitespace() {
1142        assert!(DatasetRegistry::crs_strings_equivalent(
1143            "  EPSG:4326  ",
1144            "EPSG:4326"
1145        ));
1146    }
1147
1148    #[test]
1149    fn test_crs_strings_not_equivalent() {
1150        assert!(!DatasetRegistry::crs_strings_equivalent(
1151            "EPSG:4326",
1152            "EPSG:3857"
1153        ));
1154    }
1155
1156    // Densification tests
1157    #[test]
1158    fn test_densify_bbox_edges_minimum() {
1159        let points = DatasetRegistry::densify_bbox_edges(0.0, 0.0, 10.0, 10.0, 2);
1160        // With 2 points per edge, we get corners only
1161        // 2 (bottom) + 1 (right, skip corner) + 1 (top, skip corner) + 0 (left, skip both corners)
1162        assert_eq!(points.len(), 4);
1163
1164        // Check corners exist
1165        assert!(
1166            points
1167                .iter()
1168                .any(|p| (p.x - 0.0).abs() < 1e-10 && (p.y - 0.0).abs() < 1e-10)
1169        );
1170        assert!(
1171            points
1172                .iter()
1173                .any(|p| (p.x - 10.0).abs() < 1e-10 && (p.y - 0.0).abs() < 1e-10)
1174        );
1175        assert!(
1176            points
1177                .iter()
1178                .any(|p| (p.x - 10.0).abs() < 1e-10 && (p.y - 10.0).abs() < 1e-10)
1179        );
1180        assert!(
1181            points
1182                .iter()
1183                .any(|p| (p.x - 0.0).abs() < 1e-10 && (p.y - 10.0).abs() < 1e-10)
1184        );
1185    }
1186
1187    #[test]
1188    fn test_densify_bbox_edges_5_points() {
1189        let points = DatasetRegistry::densify_bbox_edges(0.0, 0.0, 10.0, 10.0, 5);
1190        // 5 (bottom) + 4 (right) + 4 (top) + 3 (left) = 16
1191        assert_eq!(points.len(), 16);
1192    }
1193
1194    #[test]
1195    fn test_densify_bbox_edges_21_points() {
1196        let points = DatasetRegistry::densify_bbox_edges(-10.0, -10.0, 10.0, 10.0, 21);
1197        // 21 (bottom) + 20 (right) + 20 (top) + 19 (left) = 80
1198        assert_eq!(points.len(), 80);
1199
1200        // Check that corners are included
1201        let has_bottom_left = points
1202            .iter()
1203            .any(|p| (p.x - (-10.0)).abs() < 1e-10 && (p.y - (-10.0)).abs() < 1e-10);
1204        let has_bottom_right = points
1205            .iter()
1206            .any(|p| (p.x - 10.0).abs() < 1e-10 && (p.y - (-10.0)).abs() < 1e-10);
1207        let has_top_right = points
1208            .iter()
1209            .any(|p| (p.x - 10.0).abs() < 1e-10 && (p.y - 10.0).abs() < 1e-10);
1210        let has_top_left = points
1211            .iter()
1212            .any(|p| (p.x - (-10.0)).abs() < 1e-10 && (p.y - 10.0).abs() < 1e-10);
1213
1214        assert!(has_bottom_left, "Should have bottom-left corner");
1215        assert!(has_bottom_right, "Should have bottom-right corner");
1216        assert!(has_top_right, "Should have top-right corner");
1217        assert!(has_top_left, "Should have top-left corner");
1218    }
1219
1220    // Bbox transformation tests
1221    #[test]
1222    fn test_transform_bbox_same_crs() {
1223        let source_crs = Crs::wgs84();
1224        let target_crs = Crs::wgs84();
1225        let bbox = (0.0, 0.0, 10.0, 10.0);
1226
1227        let result =
1228            DatasetRegistry::transform_bbox_with_densification(bbox, &source_crs, &target_crs);
1229        assert!(result.is_ok());
1230
1231        let (min_x, min_y, max_x, max_y) = result.expect("should transform");
1232        assert!((min_x - 0.0).abs() < 1e-6);
1233        assert!((min_y - 0.0).abs() < 1e-6);
1234        assert!((max_x - 10.0).abs() < 1e-6);
1235        assert!((max_y - 10.0).abs() < 1e-6);
1236    }
1237
1238    #[test]
1239    fn test_transform_bbox_wgs84_to_web_mercator() {
1240        let source_crs = Crs::wgs84();
1241        let target_crs = Crs::web_mercator();
1242
1243        // Small bbox around null island
1244        let bbox = (-1.0, -1.0, 1.0, 1.0);
1245
1246        let result =
1247            DatasetRegistry::transform_bbox_with_densification(bbox, &source_crs, &target_crs);
1248        assert!(result.is_ok());
1249
1250        let (min_x, min_y, max_x, max_y) = result.expect("should transform");
1251
1252        // In Web Mercator, 1 degree at equator is approximately 111,320 meters
1253        // So bbox should be roughly centered at 0,0 and extend about 111km in each direction
1254        assert!(min_x < 0.0, "min_x should be negative");
1255        assert!(min_y < 0.0, "min_y should be negative");
1256        assert!(max_x > 0.0, "max_x should be positive");
1257        assert!(max_y > 0.0, "max_y should be positive");
1258
1259        // Rough check for Web Mercator coordinates
1260        assert!(min_x > -200_000.0, "min_x should be > -200000");
1261        assert!(max_x < 200_000.0, "max_x should be < 200000");
1262        assert!(min_y > -200_000.0, "min_y should be > -200000");
1263        assert!(max_y < 200_000.0, "max_y should be < 200000");
1264    }
1265
1266    #[test]
1267    fn test_transform_bbox_web_mercator_to_wgs84() {
1268        let source_crs = Crs::web_mercator();
1269        let target_crs = Crs::wgs84();
1270
1271        // 1 million meters from origin (roughly 9 degrees)
1272        let bbox = (-1_000_000.0, -1_000_000.0, 1_000_000.0, 1_000_000.0);
1273
1274        let result =
1275            DatasetRegistry::transform_bbox_with_densification(bbox, &source_crs, &target_crs);
1276        assert!(result.is_ok());
1277
1278        let (min_x, min_y, max_x, max_y) = result.expect("should transform");
1279
1280        // Should be roughly +-9 degrees
1281        assert!(
1282            min_x > -15.0 && min_x < -5.0,
1283            "min_x should be around -9 degrees"
1284        );
1285        assert!(
1286            max_x > 5.0 && max_x < 15.0,
1287            "max_x should be around 9 degrees"
1288        );
1289        assert!(
1290            min_y > -15.0 && min_y < -5.0,
1291            "min_y should be around -9 degrees"
1292        );
1293        assert!(
1294            max_y > 5.0 && max_y < 15.0,
1295            "max_y should be around 9 degrees"
1296        );
1297    }
1298
1299    #[test]
1300    fn test_transform_bbox_high_latitude() {
1301        let source_crs = Crs::wgs84();
1302        let target_crs = Crs::web_mercator();
1303
1304        // Northern Europe bbox (demonstrates importance of densification)
1305        let bbox = (0.0, 50.0, 10.0, 60.0);
1306
1307        let result =
1308            DatasetRegistry::transform_bbox_with_densification(bbox, &source_crs, &target_crs);
1309        assert!(result.is_ok());
1310
1311        let (_min_x, min_y, _max_x, max_y) = result.expect("should transform");
1312
1313        // Web Mercator Y values should be large positive numbers for 50-60 degrees N
1314        assert!(min_y > 6_000_000.0, "min_y should be > 6M for 50 degrees N");
1315        assert!(max_y > 8_000_000.0, "max_y should be > 8M for 60 degrees N");
1316    }
1317
1318    #[test]
1319    fn test_transform_bbox_simple_vs_densified() {
1320        let source_crs = Crs::wgs84();
1321        let target_crs = Crs::web_mercator();
1322
1323        // Large bbox where densification matters
1324        let bbox = (-20.0, 40.0, 20.0, 70.0);
1325
1326        let simple = DatasetRegistry::transform_bbox_simple(bbox, &source_crs, &target_crs);
1327        let densified =
1328            DatasetRegistry::transform_bbox_with_densification(bbox, &source_crs, &target_crs);
1329
1330        assert!(simple.is_ok());
1331        assert!(densified.is_ok());
1332
1333        // Both should produce valid results
1334        let simple = simple.expect("simple should work");
1335        let densified = densified.expect("densified should work");
1336
1337        // For Mercator projection, the results should be similar
1338        // The densified version may have slightly larger bounds due to curvature
1339        assert!(
1340            (simple.0 - densified.0).abs() < 100.0,
1341            "min_x should be similar"
1342        );
1343        assert!(
1344            (simple.2 - densified.2).abs() < 100.0,
1345            "max_x should be similar"
1346        );
1347    }
1348
1349    // Transform point test
1350    #[test]
1351    fn test_transform_point() {
1352        let source_crs = Crs::wgs84();
1353        let target_crs = Crs::web_mercator();
1354
1355        let result = DatasetRegistry::transform_point(0.0, 0.0, &source_crs, &target_crs);
1356        assert!(result.is_ok());
1357
1358        let (x, y) = result.expect("should transform");
1359        assert!((x - 0.0).abs() < 1.0, "x should be close to 0");
1360        assert!((y - 0.0).abs() < 1.0, "y should be close to 0");
1361    }
1362
1363    #[test]
1364    fn test_transform_point_london() {
1365        let source_crs = Crs::wgs84();
1366        let target_crs = Crs::web_mercator();
1367
1368        // London: -0.1276, 51.5074
1369        let result = DatasetRegistry::transform_point(-0.1276, 51.5074, &source_crs, &target_crs);
1370        assert!(result.is_ok());
1371
1372        let (x, y) = result.expect("should transform");
1373        // London in Web Mercator is approximately (-14200, 6711000)
1374        assert!(x > -20_000.0 && x < 0.0, "x should be slightly negative");
1375        assert!(
1376            y > 6_500_000.0 && y < 7_000_000.0,
1377            "y should be around 6.7M"
1378        );
1379    }
1380
1381    // UTM zone transformation tests
1382    #[test]
1383    fn test_transform_bbox_to_utm() {
1384        let source_crs = Crs::wgs84();
1385        // UTM Zone 32N (Central Europe, 6-12 degrees E)
1386        let target_crs = Crs::from_epsg(32632).expect("UTM 32N should exist");
1387
1388        // Bbox in Germany (within UTM zone 32)
1389        let bbox = (8.0, 48.0, 10.0, 50.0);
1390
1391        let result =
1392            DatasetRegistry::transform_bbox_with_densification(bbox, &source_crs, &target_crs);
1393        assert!(result.is_ok());
1394
1395        let (min_x, min_y, _max_x, _max_y) = result.expect("should transform");
1396
1397        // UTM coordinates should be in typical range
1398        // X (easting): 166,000 to 833,000 meters from false easting of 500,000
1399        // Y (northing): meters from equator
1400        assert!(
1401            min_x > 300_000.0 && min_x < 700_000.0,
1402            "easting should be in valid range"
1403        );
1404        assert!(
1405            min_y > 5_000_000.0 && min_y < 6_000_000.0,
1406            "northing should be in valid range"
1407        );
1408    }
1409
1410    // Edge case tests
1411    #[test]
1412    fn test_transform_bbox_antimeridian() {
1413        let source_crs = Crs::wgs84();
1414        let target_crs = Crs::web_mercator();
1415
1416        // Bbox near antimeridian (but not crossing)
1417        let bbox = (170.0, -10.0, 179.0, 10.0);
1418
1419        let result =
1420            DatasetRegistry::transform_bbox_with_densification(bbox, &source_crs, &target_crs);
1421        assert!(result.is_ok());
1422
1423        let (min_x, _min_y, max_x, _max_y) = result.expect("should transform");
1424        assert!(min_x > 0.0 && max_x > min_x, "should have valid x range");
1425    }
1426
1427    #[test]
1428    fn test_transform_bbox_polar_region() {
1429        let source_crs = Crs::wgs84();
1430        // Polar Stereographic North
1431        let target_crs = Crs::from_epsg(3413).expect("NSIDC Polar Stereographic should exist");
1432
1433        // Arctic region bbox
1434        let bbox = (-10.0, 70.0, 10.0, 80.0);
1435
1436        let result =
1437            DatasetRegistry::transform_bbox_with_densification(bbox, &source_crs, &target_crs);
1438        assert!(result.is_ok());
1439    }
1440
1441    // Error handling tests
1442    #[test]
1443    fn test_transform_bbox_invalid_crs() {
1444        // Try to parse a non-existent EPSG code
1445        let crs = DatasetRegistry::parse_crs("EPSG:99999");
1446        assert!(crs.is_err());
1447    }
1448}