Skip to main content

oxigdal_services/wcs/
mod.rs

1//! WCS (Web Coverage Service) 2.0 implementation
2//!
3//! Provides OGC-compliant Web Coverage Service supporting:
4//! - GetCapabilities: Service metadata
5//! - DescribeCoverage: Coverage schema and structure
6//! - GetCoverage: Raster data retrieval with subsetting and format conversion
7//!
8//! # Standards
9//!
10//! - OGC WCS 2.0.1 Core
11//! - OGC WCS 2.0 GeoTIFF Coverage Encoding
12//! - OGC WCS 2.0 Range Subsetting Extension
13//!
14//! # Example
15//!
16//! ```no_run
17//! use oxigdal_services::wcs::{ServiceInfo, WcsState};
18//!
19//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
20//! let info = ServiceInfo {
21//!     title: "My WCS Service".to_string(),
22//!     abstract_text: None,
23//!     provider: "Provider".to_string(),
24//!     service_url: "http://localhost:8080/wcs".to_string(),
25//!     versions: vec!["2.0.1".to_string()],
26//! };
27//! let service = WcsState::new(info);
28//! // Add coverages and handle requests
29//! # Ok(())
30//! # }
31//! ```
32
33pub mod capabilities;
34pub mod coverage;
35
36use crate::error::{ServiceError, ServiceResult};
37use axum::{
38    extract::{Query, State},
39    response::Response,
40};
41use serde::Deserialize;
42use std::sync::Arc;
43
44/// WCS service state
45#[derive(Clone)]
46pub struct WcsState {
47    /// Service metadata
48    pub service_info: Arc<ServiceInfo>,
49    /// Coverage registry
50    pub coverages: Arc<dashmap::DashMap<String, CoverageInfo>>,
51}
52
53/// Service metadata
54#[derive(Debug, Clone)]
55pub struct ServiceInfo {
56    /// Service title
57    pub title: String,
58    /// Service abstract/description
59    pub abstract_text: Option<String>,
60    /// Service provider
61    pub provider: String,
62    /// Service URL
63    pub service_url: String,
64    /// Supported versions
65    pub versions: Vec<String>,
66}
67
68/// Coverage information
69#[derive(Debug, Clone)]
70pub struct CoverageInfo {
71    /// Coverage identifier
72    pub coverage_id: String,
73    /// Title
74    pub title: String,
75    /// Abstract
76    pub abstract_text: Option<String>,
77    /// Native CRS
78    pub native_crs: String,
79    /// Bounding box in native CRS (minx, miny, maxx, maxy)
80    pub bbox: (f64, f64, f64, f64),
81    /// Grid dimensions (width, height)
82    pub grid_size: (usize, usize),
83    /// Grid origin (x, y)
84    pub grid_origin: (f64, f64),
85    /// Grid resolution (x, y)
86    pub grid_resolution: (f64, f64),
87    /// Number of bands
88    pub band_count: usize,
89    /// Band names
90    pub band_names: Vec<String>,
91    /// Data type
92    pub data_type: String,
93    /// Coverage source
94    pub source: CoverageSource,
95    /// Supported formats
96    pub formats: Vec<String>,
97}
98
99/// Coverage data source
100#[derive(Debug, Clone)]
101pub enum CoverageSource {
102    /// File-based source (GeoTIFF, NetCDF, etc.)
103    File(std::path::PathBuf),
104    /// Remote URL
105    Url(String),
106    /// In-memory coverage
107    Memory,
108}
109
110/// WCS request parameters
111#[derive(Debug, Deserialize)]
112#[serde(rename_all = "UPPERCASE")]
113pub struct WcsRequest {
114    /// Service name (must be "WCS")
115    pub service: Option<String>,
116    /// WCS version
117    pub version: Option<String>,
118    /// Request operation
119    pub request: String,
120    /// Additional parameters
121    #[serde(flatten)]
122    pub params: serde_json::Value,
123}
124
125impl WcsState {
126    /// Create new WCS service state
127    pub fn new(service_info: ServiceInfo) -> Self {
128        Self {
129            service_info: Arc::new(service_info),
130            coverages: Arc::new(dashmap::DashMap::new()),
131        }
132    }
133
134    /// Add a coverage
135    pub fn add_coverage(&self, info: CoverageInfo) -> ServiceResult<()> {
136        self.coverages.insert(info.coverage_id.clone(), info);
137        Ok(())
138    }
139
140    /// Get coverage by ID
141    pub fn get_coverage(&self, coverage_id: &str) -> Option<CoverageInfo> {
142        self.coverages
143            .get(coverage_id)
144            .map(|entry| entry.value().clone())
145    }
146}
147
148/// Main WCS request handler
149pub async fn handle_wcs_request(
150    State(state): State<WcsState>,
151    Query(params): Query<WcsRequest>,
152) -> Result<Response, ServiceError> {
153    // Validate service parameter
154    if let Some(ref service) = params.service {
155        if service.to_uppercase() != "WCS" {
156            return Err(ServiceError::InvalidParameter(
157                "SERVICE".to_string(),
158                format!("Expected 'WCS', got '{}'", service),
159            ));
160        }
161    }
162
163    // Route to appropriate handler based on request type
164    match params.request.to_uppercase().as_str() {
165        "GETCAPABILITIES" => {
166            let version = params.version.as_deref().unwrap_or("2.0.1");
167            capabilities::handle_get_capabilities(&state, version).await
168        }
169        "DESCRIBECOVERAGE" => {
170            let version = params.version.as_deref().unwrap_or("2.0.1");
171            coverage::handle_describe_coverage(&state, version, &params.params).await
172        }
173        "GETCOVERAGE" => {
174            let version = params.version.as_deref().unwrap_or("2.0.1");
175            coverage::handle_get_coverage(&state, version, &params.params).await
176        }
177        _ => Err(ServiceError::UnsupportedOperation(params.request.clone())),
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn test_wcs_state_creation() {
187        let info = ServiceInfo {
188            title: "Test WCS".to_string(),
189            abstract_text: Some("Test service".to_string()),
190            provider: "COOLJAPAN OU".to_string(),
191            service_url: "http://localhost/wcs".to_string(),
192            versions: vec!["2.0.1".to_string()],
193        };
194
195        let state = WcsState::new(info);
196        assert_eq!(state.service_info.title, "Test WCS");
197    }
198
199    #[test]
200    fn test_add_coverage() {
201        let info = ServiceInfo {
202            title: "Test WCS".to_string(),
203            abstract_text: None,
204            provider: "COOLJAPAN OU".to_string(),
205            service_url: "http://localhost/wcs".to_string(),
206            versions: vec!["2.0.1".to_string()],
207        };
208
209        let state = WcsState::new(info);
210
211        let coverage = CoverageInfo {
212            coverage_id: "test_coverage".to_string(),
213            title: "Test Coverage".to_string(),
214            abstract_text: None,
215            native_crs: "EPSG:4326".to_string(),
216            bbox: (-180.0, -90.0, 180.0, 90.0),
217            grid_size: (1024, 512),
218            grid_origin: (-180.0, 90.0),
219            grid_resolution: (0.35, -0.35),
220            band_count: 3,
221            band_names: vec!["Red".to_string(), "Green".to_string(), "Blue".to_string()],
222            data_type: "Byte".to_string(),
223            source: CoverageSource::Memory,
224            formats: vec!["image/tiff".to_string(), "image/png".to_string()],
225        };
226
227        assert!(
228            state.add_coverage(coverage).is_ok(),
229            "Failed to add coverage"
230        );
231
232        let retrieved = state.get_coverage("test_coverage");
233        assert!(retrieved.is_some());
234        assert_eq!(
235            retrieved.as_ref().map(|c| &c.coverage_id),
236            Some(&"test_coverage".to_string())
237        );
238    }
239}