Skip to main content

oxigdal_server/handlers/
wms.rs

1//! WMS (Web Map Service) handlers
2//!
3//! Implements OGC Web Map Service 1.3.0 protocol:
4//! - GetCapabilities: Returns XML metadata about available layers
5//! - GetMap: Renders and returns a map image
6//! - GetFeatureInfo: Queries pixel values at a point
7
8use crate::cache::TileCache;
9use crate::config::ImageFormat;
10use crate::dataset_registry::Dataset;
11use crate::dataset_registry::{DatasetRegistry, LayerInfo};
12use crate::handlers::rendering::{RasterRenderer, RenderStyle, encode_image};
13use axum::{
14    extract::{Query, State},
15    http::{StatusCode, header},
16    response::{IntoResponse, Response},
17};
18use bytes::Bytes;
19use oxigdal_core::buffer::RasterBuffer;
20use quick_xml::Writer;
21use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
22use serde::Deserialize;
23use std::io::Cursor;
24use std::sync::Arc;
25use thiserror::Error;
26use tracing::debug;
27
28/// Configuration for source region to read from a dataset
29#[derive(Debug, Clone, Copy)]
30struct SourceRegion {
31    x: u64,
32    y: u64,
33    width: u64,
34    height: u64,
35}
36
37impl SourceRegion {
38    fn new(x: u64, y: u64, width: u64, height: u64) -> Self {
39        Self {
40            x,
41            y,
42            width,
43            height,
44        }
45    }
46}
47
48/// Configuration for target rendering dimensions
49#[derive(Debug, Clone, Copy)]
50struct TargetDimensions {
51    width: u64,
52    height: u64,
53}
54
55impl TargetDimensions {
56    fn new(width: u64, height: u64) -> Self {
57        Self { width, height }
58    }
59}
60
61/// WMS errors
62#[derive(Debug, Error)]
63pub enum WmsError {
64    /// Invalid request parameter
65    #[error("Invalid parameter: {0}")]
66    InvalidParameter(String),
67
68    /// Missing required parameter
69    #[error("Missing required parameter: {0}")]
70    MissingParameter(String),
71
72    /// Layer not found
73    #[error("Layer not found: {0}")]
74    LayerNotFound(String),
75
76    /// Invalid CRS
77    #[error("Invalid CRS: {0}")]
78    InvalidCrs(String),
79
80    /// Invalid bounding box
81    #[error("Invalid bounding box: {0}")]
82    InvalidBbox(String),
83
84    /// Rendering error
85    #[error("Rendering error: {0}")]
86    Rendering(String),
87
88    /// OxiGDAL error
89    #[error("GDAL error: {0}")]
90    Gdal(#[from] oxigdal_core::OxiGdalError),
91
92    /// Registry error
93    #[error("Registry error: {0}")]
94    Registry(#[from] crate::dataset_registry::RegistryError),
95
96    /// Unsupported format
97    #[error("Unsupported format: {0}")]
98    UnsupportedFormat(String),
99}
100
101impl IntoResponse for WmsError {
102    fn into_response(self) -> Response {
103        let (status, message) = match self {
104            WmsError::InvalidParameter(_) | WmsError::MissingParameter(_) => {
105                (StatusCode::BAD_REQUEST, self.to_string())
106            }
107            WmsError::LayerNotFound(_) => (StatusCode::NOT_FOUND, self.to_string()),
108            _ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
109        };
110
111        // Return OGC ServiceException format
112        let xml = format!(
113            r#"<?xml version="1.0" encoding="UTF-8"?>
114<ServiceExceptionReport version="1.3.0" xmlns="http://www.opengis.net/ogc">
115  <ServiceException>{}</ServiceException>
116</ServiceExceptionReport>"#,
117            message
118        );
119
120        (
121            status,
122            [(header::CONTENT_TYPE, "application/vnd.ogc.se_xml")],
123            xml,
124        )
125            .into_response()
126    }
127}
128
129/// Shared server state
130#[derive(Clone)]
131pub struct WmsState {
132    /// Dataset registry
133    pub registry: DatasetRegistry,
134
135    /// Tile cache
136    pub cache: TileCache,
137
138    /// Service URL
139    pub service_url: String,
140
141    /// Service title
142    pub service_title: String,
143    /// Service description/abstract
144    pub service_abstract: String,
145}
146
147/// GetCapabilities request parameters
148#[derive(Debug, Deserialize)]
149#[allow(dead_code)]
150pub struct GetCapabilitiesParams {
151    #[serde(rename = "SERVICE")]
152    service: Option<String>,
153
154    #[serde(rename = "REQUEST")]
155    request: Option<String>,
156
157    #[serde(rename = "VERSION")]
158    version: Option<String>,
159}
160
161/// GetMap request parameters
162#[derive(Debug, Deserialize)]
163#[allow(dead_code)]
164pub struct GetMapParams {
165    #[serde(rename = "SERVICE")]
166    service: Option<String>,
167
168    #[serde(rename = "REQUEST")]
169    request: Option<String>,
170
171    #[serde(rename = "VERSION")]
172    version: Option<String>,
173
174    #[serde(rename = "LAYERS")]
175    layers: String,
176
177    #[serde(rename = "STYLES")]
178    styles: Option<String>,
179
180    #[serde(rename = "CRS")]
181    crs: Option<String>,
182
183    #[serde(rename = "SRS")]
184    srs: Option<String>,
185
186    #[serde(rename = "BBOX")]
187    bbox: String,
188
189    #[serde(rename = "WIDTH")]
190    width: u32,
191
192    #[serde(rename = "HEIGHT")]
193    height: u32,
194
195    #[serde(rename = "FORMAT")]
196    format: String,
197
198    #[serde(rename = "TRANSPARENT")]
199    transparent: Option<bool>,
200
201    #[serde(rename = "BGCOLOR")]
202    bgcolor: Option<String>,
203}
204
205/// GetFeatureInfo request parameters
206#[derive(Debug, Deserialize)]
207#[allow(dead_code)]
208pub struct GetFeatureInfoParams {
209    #[serde(rename = "SERVICE")]
210    service: Option<String>,
211
212    #[serde(rename = "REQUEST")]
213    request: Option<String>,
214
215    #[serde(rename = "VERSION")]
216    version: Option<String>,
217
218    #[serde(rename = "LAYERS")]
219    layers: String,
220
221    #[serde(rename = "QUERY_LAYERS")]
222    query_layers: String,
223
224    #[serde(rename = "CRS")]
225    crs: Option<String>,
226
227    #[serde(rename = "SRS")]
228    srs: Option<String>,
229
230    #[serde(rename = "BBOX")]
231    bbox: String,
232
233    #[serde(rename = "WIDTH")]
234    width: u32,
235
236    #[serde(rename = "HEIGHT")]
237    height: u32,
238
239    #[serde(rename = "I")]
240    i: Option<u32>,
241
242    #[serde(rename = "X")]
243    x: Option<u32>,
244
245    #[serde(rename = "J")]
246    j: Option<u32>,
247
248    #[serde(rename = "Y")]
249    y: Option<u32>,
250
251    #[serde(rename = "INFO_FORMAT")]
252    info_format: Option<String>,
253}
254
255/// Handle GetCapabilities request
256pub async fn get_capabilities(
257    State(state): State<Arc<WmsState>>,
258    Query(params): Query<GetCapabilitiesParams>,
259) -> Result<Response, WmsError> {
260    debug!("WMS GetCapabilities request");
261
262    // Validate service parameter
263    if let Some(ref service) = params.service {
264        if service.to_uppercase() != "WMS" {
265            return Err(WmsError::InvalidParameter(format!(
266                "Invalid SERVICE: {}",
267                service
268            )));
269        }
270    }
271
272    // Get all layers
273    let layers = state.registry.list_layers()?;
274
275    // Generate capabilities XML
276    let xml = generate_capabilities_xml(&state, &layers)?;
277
278    Ok((
279        StatusCode::OK,
280        [(header::CONTENT_TYPE, "application/vnd.ogc.wms_xml")],
281        xml,
282    )
283        .into_response())
284}
285
286/// Generate WMS capabilities XML document
287fn generate_capabilities_xml(state: &WmsState, layers: &[LayerInfo]) -> Result<String, WmsError> {
288    let mut writer = Writer::new(Cursor::new(Vec::new()));
289
290    // XML declaration
291    writer
292        .write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))
293        .map_err(|e| WmsError::Rendering(e.to_string()))?;
294
295    // Root element
296    let mut root = BytesStart::new("WMS_Capabilities");
297    root.push_attribute(("version", "1.3.0"));
298    root.push_attribute(("xmlns", "http://www.opengis.net/wms"));
299    root.push_attribute(("xmlns:xlink", "http://www.w3.org/1999/xlink"));
300    writer
301        .write_event(Event::Start(root))
302        .map_err(|e| WmsError::Rendering(e.to_string()))?;
303
304    // Service section
305    write_service_section(&mut writer, state)?;
306
307    // Capability section
308    write_capability_section(&mut writer, state, layers)?;
309
310    // Close root
311    writer
312        .write_event(Event::End(BytesEnd::new("WMS_Capabilities")))
313        .map_err(|e| WmsError::Rendering(e.to_string()))?;
314
315    let result = writer.into_inner().into_inner();
316    String::from_utf8(result).map_err(|e| WmsError::Rendering(e.to_string()))
317}
318
319/// Write Service section of capabilities
320fn write_service_section(
321    writer: &mut Writer<Cursor<Vec<u8>>>,
322    state: &WmsState,
323) -> Result<(), WmsError> {
324    writer
325        .write_event(Event::Start(BytesStart::new("Service")))
326        .map_err(|e| WmsError::Rendering(e.to_string()))?;
327
328    // Name
329    writer
330        .write_event(Event::Start(BytesStart::new("Name")))
331        .map_err(|e| WmsError::Rendering(e.to_string()))?;
332    writer
333        .write_event(Event::Text(BytesText::new("WMS")))
334        .map_err(|e| WmsError::Rendering(e.to_string()))?;
335    writer
336        .write_event(Event::End(BytesEnd::new("Name")))
337        .map_err(|e| WmsError::Rendering(e.to_string()))?;
338
339    // Title
340    writer
341        .write_event(Event::Start(BytesStart::new("Title")))
342        .map_err(|e| WmsError::Rendering(e.to_string()))?;
343    writer
344        .write_event(Event::Text(BytesText::new(&state.service_title)))
345        .map_err(|e| WmsError::Rendering(e.to_string()))?;
346    writer
347        .write_event(Event::End(BytesEnd::new("Title")))
348        .map_err(|e| WmsError::Rendering(e.to_string()))?;
349
350    // Abstract
351    writer
352        .write_event(Event::Start(BytesStart::new("Abstract")))
353        .map_err(|e| WmsError::Rendering(e.to_string()))?;
354    writer
355        .write_event(Event::Text(BytesText::new(&state.service_abstract)))
356        .map_err(|e| WmsError::Rendering(e.to_string()))?;
357    writer
358        .write_event(Event::End(BytesEnd::new("Abstract")))
359        .map_err(|e| WmsError::Rendering(e.to_string()))?;
360
361    // OnlineResource
362    let mut online_resource = BytesStart::new("OnlineResource");
363    online_resource.push_attribute(("xmlns:xlink", "http://www.w3.org/1999/xlink"));
364    online_resource.push_attribute(("xlink:type", "simple"));
365    online_resource.push_attribute(("xlink:href", state.service_url.as_str()));
366    writer
367        .write_event(Event::Empty(online_resource))
368        .map_err(|e| WmsError::Rendering(e.to_string()))?;
369
370    writer
371        .write_event(Event::End(BytesEnd::new("Service")))
372        .map_err(|e| WmsError::Rendering(e.to_string()))?;
373
374    Ok(())
375}
376
377/// Write Capability section of capabilities
378fn write_capability_section(
379    writer: &mut Writer<Cursor<Vec<u8>>>,
380    state: &WmsState,
381    layers: &[LayerInfo],
382) -> Result<(), WmsError> {
383    writer
384        .write_event(Event::Start(BytesStart::new("Capability")))
385        .map_err(|e| WmsError::Rendering(e.to_string()))?;
386
387    // Request
388    write_request_section(writer, state)?;
389
390    // Exception
391    writer
392        .write_event(Event::Start(BytesStart::new("Exception")))
393        .map_err(|e| WmsError::Rendering(e.to_string()))?;
394    writer
395        .write_event(Event::Empty(BytesStart::new("Format")))
396        .map_err(|e| WmsError::Rendering(e.to_string()))?;
397    writer
398        .write_event(Event::End(BytesEnd::new("Exception")))
399        .map_err(|e| WmsError::Rendering(e.to_string()))?;
400
401    // Layers
402    write_layers_section(writer, layers)?;
403
404    writer
405        .write_event(Event::End(BytesEnd::new("Capability")))
406        .map_err(|e| WmsError::Rendering(e.to_string()))?;
407
408    Ok(())
409}
410
411/// Write Request section
412fn write_request_section(
413    writer: &mut Writer<Cursor<Vec<u8>>>,
414    state: &WmsState,
415) -> Result<(), WmsError> {
416    writer
417        .write_event(Event::Start(BytesStart::new("Request")))
418        .map_err(|e| WmsError::Rendering(e.to_string()))?;
419
420    // GetCapabilities
421    write_operation(writer, "GetCapabilities", &state.service_url)?;
422
423    // GetMap
424    write_operation(writer, "GetMap", &state.service_url)?;
425
426    // GetFeatureInfo
427    write_operation(writer, "GetFeatureInfo", &state.service_url)?;
428
429    writer
430        .write_event(Event::End(BytesEnd::new("Request")))
431        .map_err(|e| WmsError::Rendering(e.to_string()))?;
432
433    Ok(())
434}
435
436/// Write operation definition
437fn write_operation(
438    writer: &mut Writer<Cursor<Vec<u8>>>,
439    operation: &str,
440    url: &str,
441) -> Result<(), WmsError> {
442    writer
443        .write_event(Event::Start(BytesStart::new(operation)))
444        .map_err(|e| WmsError::Rendering(e.to_string()))?;
445
446    // Format
447    for format in &["image/png", "image/jpeg", "image/webp"] {
448        writer
449            .write_event(Event::Start(BytesStart::new("Format")))
450            .map_err(|e| WmsError::Rendering(e.to_string()))?;
451        writer
452            .write_event(Event::Text(BytesText::new(format)))
453            .map_err(|e| WmsError::Rendering(e.to_string()))?;
454        writer
455            .write_event(Event::End(BytesEnd::new("Format")))
456            .map_err(|e| WmsError::Rendering(e.to_string()))?;
457    }
458
459    // DCPType
460    writer
461        .write_event(Event::Start(BytesStart::new("DCPType")))
462        .map_err(|e| WmsError::Rendering(e.to_string()))?;
463    writer
464        .write_event(Event::Start(BytesStart::new("HTTP")))
465        .map_err(|e| WmsError::Rendering(e.to_string()))?;
466    writer
467        .write_event(Event::Start(BytesStart::new("Get")))
468        .map_err(|e| WmsError::Rendering(e.to_string()))?;
469
470    let mut online_resource = BytesStart::new("OnlineResource");
471    online_resource.push_attribute(("xmlns:xlink", "http://www.w3.org/1999/xlink"));
472    online_resource.push_attribute(("xlink:type", "simple"));
473    online_resource.push_attribute(("xlink:href", url));
474    writer
475        .write_event(Event::Empty(online_resource))
476        .map_err(|e| WmsError::Rendering(e.to_string()))?;
477
478    writer
479        .write_event(Event::End(BytesEnd::new("Get")))
480        .map_err(|e| WmsError::Rendering(e.to_string()))?;
481    writer
482        .write_event(Event::End(BytesEnd::new("HTTP")))
483        .map_err(|e| WmsError::Rendering(e.to_string()))?;
484    writer
485        .write_event(Event::End(BytesEnd::new("DCPType")))
486        .map_err(|e| WmsError::Rendering(e.to_string()))?;
487
488    writer
489        .write_event(Event::End(BytesEnd::new(operation)))
490        .map_err(|e| WmsError::Rendering(e.to_string()))?;
491
492    Ok(())
493}
494
495/// Write Layers section
496fn write_layers_section(
497    writer: &mut Writer<Cursor<Vec<u8>>>,
498    layers: &[LayerInfo],
499) -> Result<(), WmsError> {
500    for layer in layers {
501        write_layer(writer, layer)?;
502    }
503
504    Ok(())
505}
506
507/// Write individual Layer
508fn write_layer(writer: &mut Writer<Cursor<Vec<u8>>>, layer: &LayerInfo) -> Result<(), WmsError> {
509    writer
510        .write_event(Event::Start(BytesStart::new("Layer")))
511        .map_err(|e| WmsError::Rendering(e.to_string()))?;
512
513    // Name
514    writer
515        .write_event(Event::Start(BytesStart::new("Name")))
516        .map_err(|e| WmsError::Rendering(e.to_string()))?;
517    writer
518        .write_event(Event::Text(BytesText::new(&layer.name)))
519        .map_err(|e| WmsError::Rendering(e.to_string()))?;
520    writer
521        .write_event(Event::End(BytesEnd::new("Name")))
522        .map_err(|e| WmsError::Rendering(e.to_string()))?;
523
524    // Title
525    writer
526        .write_event(Event::Start(BytesStart::new("Title")))
527        .map_err(|e| WmsError::Rendering(e.to_string()))?;
528    writer
529        .write_event(Event::Text(BytesText::new(&layer.title)))
530        .map_err(|e| WmsError::Rendering(e.to_string()))?;
531    writer
532        .write_event(Event::End(BytesEnd::new("Title")))
533        .map_err(|e| WmsError::Rendering(e.to_string()))?;
534
535    // Abstract
536    writer
537        .write_event(Event::Start(BytesStart::new("Abstract")))
538        .map_err(|e| WmsError::Rendering(e.to_string()))?;
539    writer
540        .write_event(Event::Text(BytesText::new(&layer.abstract_)))
541        .map_err(|e| WmsError::Rendering(e.to_string()))?;
542    writer
543        .write_event(Event::End(BytesEnd::new("Abstract")))
544        .map_err(|e| WmsError::Rendering(e.to_string()))?;
545
546    // BoundingBox
547    if let Some((min_x, min_y, max_x, max_y)) = layer.metadata.bbox {
548        let mut bbox = BytesStart::new("BoundingBox");
549        bbox.push_attribute(("CRS", "EPSG:4326"));
550        bbox.push_attribute(("minx", min_x.to_string().as_str()));
551        bbox.push_attribute(("miny", min_y.to_string().as_str()));
552        bbox.push_attribute(("maxx", max_x.to_string().as_str()));
553        bbox.push_attribute(("maxy", max_y.to_string().as_str()));
554        writer
555            .write_event(Event::Empty(bbox))
556            .map_err(|e| WmsError::Rendering(e.to_string()))?;
557    }
558
559    writer
560        .write_event(Event::End(BytesEnd::new("Layer")))
561        .map_err(|e| WmsError::Rendering(e.to_string()))?;
562
563    Ok(())
564}
565
566/// Handle GetMap request
567pub async fn get_map(
568    State(state): State<Arc<WmsState>>,
569    Query(params): Query<GetMapParams>,
570) -> Result<Response, WmsError> {
571    debug!("WMS GetMap request: layers={}", params.layers);
572
573    // Parse parameters
574    let layer_name = params
575        .layers
576        .split(',')
577        .next()
578        .ok_or_else(|| WmsError::InvalidParameter("LAYERS parameter is empty".to_string()))?;
579
580    let format = parse_format(&params.format)?;
581    let bbox = parse_bbox(&params.bbox)?;
582
583    // Validate dimensions
584    if params.width == 0 || params.height == 0 {
585        return Err(WmsError::InvalidParameter(
586            "WIDTH and HEIGHT must be greater than 0".to_string(),
587        ));
588    }
589
590    if params.width > 4096 || params.height > 4096 {
591        return Err(WmsError::InvalidParameter(
592            "WIDTH and HEIGHT must be <= 4096".to_string(),
593        ));
594    }
595
596    // Get layer and dataset
597    let layer = state.registry.get_layer(layer_name)?;
598    let dataset = state.registry.get_dataset(layer_name)?;
599
600    // Render map with actual dataset data
601    let image_data = render_map(
602        &dataset,
603        bbox,
604        params.width,
605        params.height,
606        format,
607        params.transparent.unwrap_or(false),
608        layer.config.style.as_ref(),
609    )?;
610
611    Ok((
612        StatusCode::OK,
613        [(header::CONTENT_TYPE, format.mime_type())],
614        image_data,
615    )
616        .into_response())
617}
618
619/// Parse image format from string
620fn parse_format(format_str: &str) -> Result<ImageFormat, WmsError> {
621    match format_str.to_lowercase().as_str() {
622        "image/png" => Ok(ImageFormat::Png),
623        "image/jpeg" | "image/jpg" => Ok(ImageFormat::Jpeg),
624        "image/webp" => Ok(ImageFormat::Webp),
625        _ => Err(WmsError::UnsupportedFormat(format_str.to_string())),
626    }
627}
628
629/// Parse bounding box from string
630fn parse_bbox(bbox_str: &str) -> Result<(f64, f64, f64, f64), WmsError> {
631    let parts: Vec<&str> = bbox_str.split(',').collect();
632    if parts.len() != 4 {
633        return Err(WmsError::InvalidBbox("BBOX must have 4 values".to_string()));
634    }
635
636    let min_x = parts[0]
637        .parse::<f64>()
638        .map_err(|_| WmsError::InvalidBbox(format!("Invalid minx: {}", parts[0])))?;
639    let min_y = parts[1]
640        .parse::<f64>()
641        .map_err(|_| WmsError::InvalidBbox(format!("Invalid miny: {}", parts[1])))?;
642    let max_x = parts[2]
643        .parse::<f64>()
644        .map_err(|_| WmsError::InvalidBbox(format!("Invalid maxx: {}", parts[2])))?;
645    let max_y = parts[3]
646        .parse::<f64>()
647        .map_err(|_| WmsError::InvalidBbox(format!("Invalid maxy: {}", parts[3])))?;
648
649    if min_x >= max_x || min_y >= max_y {
650        return Err(WmsError::InvalidBbox(
651            "Invalid bbox: min must be < max".to_string(),
652        ));
653    }
654
655    Ok((min_x, min_y, max_x, max_y))
656}
657
658/// Render a map image from dataset
659///
660/// Performs the full WMS GetMap rendering pipeline:
661/// 1. Determines the pixel window in the source dataset corresponding to the requested bbox
662/// 2. Reads source raster data from the dataset
663/// 3. Resamples to the requested output dimensions
664/// 4. Applies colormap/styling for single-band or composes RGB for multi-band
665/// 5. Encodes to the requested image format (PNG, JPEG, etc.)
666fn render_map(
667    dataset: &Dataset,
668    bbox: (f64, f64, f64, f64),
669    width: u32,
670    height: u32,
671    format: ImageFormat,
672    transparent: bool,
673    style: Option<&crate::config::StyleConfig>,
674) -> Result<Bytes, WmsError> {
675    let (req_min_x, req_min_y, req_max_x, req_max_y) = bbox;
676
677    debug!(
678        "Rendering map: bbox=({},{},{},{}), size={}x{}, format={:?}",
679        req_min_x, req_min_y, req_max_x, req_max_y, width, height, format
680    );
681
682    // Get the dataset's GeoTransform for coordinate conversions
683    let geo_transform = dataset.geo_transform_obj().ok_or_else(|| {
684        WmsError::Rendering("Dataset has no geotransform - cannot map coordinates".to_string())
685    })?;
686
687    let ds_width = dataset.width();
688    let ds_height = dataset.height();
689    let band_count = dataset.raster_count();
690
691    // Convert the requested bbox corners to pixel coordinates in the source dataset
692    let (px_min_x, px_min_y) = geo_transform
693        .world_to_pixel(req_min_x, req_max_y)
694        .map_err(|e| WmsError::Rendering(format!("Coordinate transform error: {}", e)))?;
695    let (px_max_x, px_max_y) = geo_transform
696        .world_to_pixel(req_max_x, req_min_y)
697        .map_err(|e| WmsError::Rendering(format!("Coordinate transform error: {}", e)))?;
698
699    // Determine the pixel window, clamping to dataset bounds
700    let src_x = (px_min_x.min(px_max_x).floor().max(0.0)) as u64;
701    let src_y = (px_min_y.min(px_max_y).floor().max(0.0)) as u64;
702    let src_end_x = (px_min_x.max(px_max_x).ceil().max(0.0) as u64).min(ds_width);
703    let src_end_y = (px_min_y.max(px_max_y).ceil().max(0.0) as u64).min(ds_height);
704
705    let src_width = if src_end_x > src_x {
706        src_end_x - src_x
707    } else {
708        1
709    };
710    let src_height = if src_end_y > src_y {
711        src_end_y - src_y
712    } else {
713        1
714    };
715
716    debug!(
717        "Source window: offset=({}, {}), size={}x{}, dataset={}x{}, bands={}",
718        src_x, src_y, src_width, src_height, ds_width, ds_height, band_count
719    );
720
721    // Determine the best overview level for this request
722    let overview_level =
723        select_overview_level(dataset, src_width, src_height, width as u64, height as u64);
724
725    // Build the rendering style
726    let render_style = if let Some(style_cfg) = style {
727        RenderStyle::from_config(style_cfg)
728    } else {
729        let mut s = RenderStyle::default();
730        if !transparent {
731            s.alpha = 1.0;
732        } else {
733            s.alpha = 1.0; // alpha for data pixels; nodata will be transparent
734        }
735        s
736    };
737
738    // Render based on the number of bands
739    let rgba_data = if band_count >= 3 {
740        // RGB(A) dataset: read three separate bands and compose
741        render_rgb_bands(
742            dataset,
743            overview_level,
744            SourceRegion::new(src_x, src_y, src_width, src_height),
745            TargetDimensions::new(width as u64, height as u64),
746            &render_style,
747        )?
748    } else {
749        // Single-band dataset: read one band and apply colormap
750        render_single_band(
751            dataset,
752            overview_level,
753            SourceRegion::new(src_x, src_y, src_width, src_height),
754            TargetDimensions::new(width as u64, height as u64),
755            &render_style,
756        )?
757    };
758
759    // If transparent and format supports it, nodata pixels are already 0-alpha.
760    // If not transparent, ensure all alpha values are 255 for non-nodata pixels.
761    let final_rgba = if !transparent {
762        let mut data = rgba_data;
763        // Set all alpha values to 255 for opaque mode
764        for chunk in data.chunks_exact_mut(4) {
765            if chunk[3] > 0 {
766                chunk[3] = 255;
767            }
768        }
769        data
770    } else {
771        rgba_data
772    };
773
774    // Encode to the requested image format
775    encode_image(&final_rgba, width, height, format).map_err(|e| WmsError::Rendering(e.to_string()))
776}
777
778/// Select the best overview level for the requested resolution
779///
780/// Returns 0 for full resolution, or a higher number for coarser overviews.
781/// Picks the overview where the overview resolution is just finer than what
782/// the client requested, ensuring quality while avoiding reading unnecessary data.
783fn select_overview_level(
784    dataset: &Dataset,
785    src_width: u64,
786    src_height: u64,
787    target_width: u64,
788    target_height: u64,
789) -> usize {
790    let overview_count = dataset.overview_count();
791    if overview_count == 0 {
792        return 0;
793    }
794
795    // Calculate the downsample ratio the client is requesting
796    let ratio_x = if target_width > 0 {
797        src_width as f64 / target_width as f64
798    } else {
799        1.0
800    };
801    let ratio_y = if target_height > 0 {
802        src_height as f64 / target_height as f64
803    } else {
804        1.0
805    };
806    let request_ratio = ratio_x.max(ratio_y);
807
808    if request_ratio <= 1.0 {
809        // Client wants full resolution or upsampling
810        return 0;
811    }
812
813    // Each overview level typically halves the resolution (factor of 2)
814    // Find the highest overview level where the overview factor <= request ratio
815    let mut best_level = 0;
816    for level in 1..=overview_count {
817        let overview_factor = (1u64 << level) as f64;
818        if overview_factor <= request_ratio * 1.5 {
819            // Allow slight overshoot (1.5x) to avoid reading unnecessarily large data
820            best_level = level;
821        } else {
822            break;
823        }
824    }
825
826    best_level
827}
828
829/// Render a single-band dataset with colormap
830fn render_single_band(
831    dataset: &Dataset,
832    _overview_level: usize,
833    source: SourceRegion,
834    target: TargetDimensions,
835    style: &RenderStyle,
836) -> Result<Vec<u8>, WmsError> {
837    // Read the source window
838    let src_buffer = dataset
839        .read_window(source.x, source.y, source.width, source.height)
840        .map_err(|e| WmsError::Rendering(format!("Failed to read window: {}", e)))?;
841
842    // Resample to target dimensions if needed
843    let resampled = if src_buffer.width() != target.width || src_buffer.height() != target.height {
844        RasterRenderer::resample(&src_buffer, target.width, target.height, style.resampling)
845            .map_err(|e| WmsError::Rendering(format!("Resampling failed: {}", e)))?
846    } else {
847        src_buffer
848    };
849
850    // Render with colormap to RGBA
851    RasterRenderer::render_to_rgba(&resampled, style)
852        .map_err(|e| WmsError::Rendering(format!("Rendering failed: {}", e)))
853}
854
855/// Render an RGB dataset by reading three separate bands
856fn render_rgb_bands(
857    dataset: &Dataset,
858    _overview_level: usize,
859    source: SourceRegion,
860    target: TargetDimensions,
861    style: &RenderStyle,
862) -> Result<Vec<u8>, WmsError> {
863    // For RGB datasets we read three bands and compose them.
864    // Read the full window (band 0) first.
865    let band_0 = dataset
866        .read_window(source.x, source.y, source.width, source.height)
867        .map_err(|e| WmsError::Rendering(format!("Failed to read red band: {}", e)))?;
868
869    // For datasets with interleaved bands, we need to synthesize per-band buffers
870    // from the single read_window result. The dataset's read_window returns
871    // band 0 data. We generate approximate G and B by shifting pixel reads.
872    // However, if the dataset truly has separate bands accessible, we use
873    // band data from read_band calls.
874
875    // Attempt to read bands 1 and 2 as separate band data and build windows
876    let green_buffer =
877        build_band_window_from_full(dataset, 1, source.x, source.y, source.width, source.height);
878    let blue_buffer =
879        build_band_window_from_full(dataset, 2, source.x, source.y, source.width, source.height);
880
881    let (green_buf, blue_buf) = match (green_buffer, blue_buffer) {
882        (Ok(g), Ok(b)) => (g, b),
883        _ => {
884            // Fallback: use band 0 for all channels (grayscale rendered as RGB)
885            let gray = band_0.clone();
886            (gray.clone(), gray)
887        }
888    };
889
890    // Resample each band to target dimensions
891    let resample_method = style.resampling;
892    let r_resampled = if band_0.width() != target.width || band_0.height() != target.height {
893        RasterRenderer::resample(&band_0, target.width, target.height, resample_method)
894            .map_err(|e| WmsError::Rendering(format!("Red resample failed: {}", e)))?
895    } else {
896        band_0
897    };
898    let g_resampled = if green_buf.width() != target.width || green_buf.height() != target.height {
899        RasterRenderer::resample(&green_buf, target.width, target.height, resample_method)
900            .map_err(|e| WmsError::Rendering(format!("Green resample failed: {}", e)))?
901    } else {
902        green_buf
903    };
904    let b_resampled = if blue_buf.width() != target.width || blue_buf.height() != target.height {
905        RasterRenderer::resample(&blue_buf, target.width, target.height, resample_method)
906            .map_err(|e| WmsError::Rendering(format!("Blue resample failed: {}", e)))?
907    } else {
908        blue_buf
909    };
910
911    // Compose RGB to RGBA
912    RasterRenderer::render_rgb_to_rgba(&r_resampled, &g_resampled, &b_resampled, style)
913        .map_err(|e| WmsError::Rendering(format!("RGB rendering failed: {}", e)))
914}
915
916/// Build a RasterBuffer for a specific band from band-level data
917///
918/// Reads the full band and extracts the window region.
919fn build_band_window_from_full(
920    dataset: &Dataset,
921    band: usize,
922    src_x: u64,
923    src_y: u64,
924    src_width: u64,
925    src_height: u64,
926) -> Result<RasterBuffer, WmsError> {
927    let band_data = dataset
928        .read_band(0, band)
929        .map_err(|e| WmsError::Rendering(format!("Failed to read band {}: {}", band, e)))?;
930
931    let ds_width = dataset.width();
932    let ds_height = dataset.height();
933    let data_type = dataset.data_type();
934    let nodata = dataset.nodata();
935
936    let full_buffer = RasterBuffer::new(band_data, ds_width, ds_height, data_type, nodata)
937        .map_err(|e| WmsError::Rendering(format!("Buffer creation error: {}", e)))?;
938
939    // Extract the window from the full buffer
940    let mut window = RasterBuffer::zeros(src_width, src_height, data_type);
941    for dy in 0..src_height {
942        for dx in 0..src_width {
943            let gx = src_x + dx;
944            let gy = src_y + dy;
945            if gx < ds_width && gy < ds_height {
946                if let Ok(val) = full_buffer.get_pixel(gx, gy) {
947                    let _ = window.set_pixel(dx, dy, val);
948                }
949            }
950        }
951    }
952
953    Ok(window)
954}
955
956/// Handle GetFeatureInfo request
957///
958/// Queries pixel values from a dataset at the screen coordinates specified
959/// by the I/J (or X/Y) parameters. The screen coordinates are converted to
960/// world coordinates using the BBOX and WIDTH/HEIGHT, then to pixel coordinates
961/// in the source dataset using the GeoTransform.
962pub async fn get_feature_info(
963    State(state): State<Arc<WmsState>>,
964    Query(params): Query<GetFeatureInfoParams>,
965) -> Result<Response, WmsError> {
966    debug!("WMS GetFeatureInfo request");
967
968    // Parse parameters
969    let layer_name =
970        params.query_layers.split(',').next().ok_or_else(|| {
971            WmsError::InvalidParameter("QUERY_LAYERS parameter is empty".to_string())
972        })?;
973
974    // I/J are WMS 1.3.0 parameters, X/Y are WMS 1.1.1 compatibility
975    let screen_x = params
976        .i
977        .or(params.x)
978        .ok_or_else(|| WmsError::MissingParameter("I or X parameter required".to_string()))?;
979
980    let screen_y = params
981        .j
982        .or(params.y)
983        .ok_or_else(|| WmsError::MissingParameter("J or Y parameter required".to_string()))?;
984
985    // Validate screen coordinates are within the requested image dimensions
986    if screen_x >= params.width || screen_y >= params.height {
987        return Err(WmsError::InvalidParameter(format!(
988            "Query point ({}, {}) is outside image dimensions ({}x{})",
989            screen_x, screen_y, params.width, params.height
990        )));
991    }
992
993    // Parse the bounding box
994    let bbox = parse_bbox(&params.bbox)?;
995
996    // Get layer and dataset
997    let layer = state.registry.get_layer(layer_name)?;
998    let dataset = state.registry.get_dataset(layer_name)?;
999
1000    // Query pixel values using the actual dataset
1001    let info = query_pixel_info(
1002        &dataset,
1003        &layer,
1004        bbox,
1005        params.width,
1006        params.height,
1007        screen_x,
1008        screen_y,
1009    )?;
1010
1011    // Determine response format
1012    let info_format = params.info_format.as_deref().unwrap_or("text/plain");
1013
1014    let response_text = match info_format {
1015        "application/json" | "text/json" => {
1016            format_feature_info_json(layer_name, screen_x, screen_y, &info)
1017        }
1018        "text/xml" | "application/xml" | "application/vnd.ogc.gml" => {
1019            format_feature_info_xml(layer_name, screen_x, screen_y, &info)
1020        }
1021        "text/html" => format_feature_info_html(layer_name, screen_x, screen_y, &info),
1022        _ => {
1023            // Default: text/plain
1024            format_feature_info_text(layer_name, screen_x, screen_y, &info)
1025        }
1026    };
1027
1028    let content_type = match info_format {
1029        "application/json" | "text/json" => "application/json",
1030        "text/xml" | "application/xml" | "application/vnd.ogc.gml" => "application/xml",
1031        "text/html" => "text/html",
1032        _ => "text/plain",
1033    };
1034
1035    Ok((
1036        StatusCode::OK,
1037        [(header::CONTENT_TYPE, content_type)],
1038        response_text,
1039    )
1040        .into_response())
1041}
1042
1043/// Pixel query result containing values for each band
1044struct PixelQueryResult {
1045    /// World coordinate X
1046    world_x: f64,
1047    /// World coordinate Y
1048    world_y: f64,
1049    /// Pixel coordinate X in the dataset
1050    pixel_x: u64,
1051    /// Pixel coordinate Y in the dataset
1052    pixel_y: u64,
1053    /// Band values (band_index, value, is_nodata)
1054    band_values: Vec<(usize, f64, bool)>,
1055    /// Data type name
1056    data_type: String,
1057}
1058
1059/// Query pixel information from the actual dataset
1060///
1061/// Converts screen coordinates to world coordinates using the BBOX/dimensions,
1062/// then to pixel coordinates using the dataset's GeoTransform, and finally
1063/// reads the pixel value(s) from all bands.
1064fn query_pixel_info(
1065    dataset: &Dataset,
1066    _layer: &LayerInfo,
1067    bbox: (f64, f64, f64, f64),
1068    screen_width: u32,
1069    screen_height: u32,
1070    screen_x: u32,
1071    screen_y: u32,
1072) -> Result<PixelQueryResult, WmsError> {
1073    let (req_min_x, req_min_y, req_max_x, req_max_y) = bbox;
1074
1075    // Convert screen coordinates to world coordinates
1076    // Screen origin is top-left, Y increases downward
1077    let world_x = req_min_x + (screen_x as f64 / screen_width as f64) * (req_max_x - req_min_x);
1078    let world_y = req_max_y - (screen_y as f64 / screen_height as f64) * (req_max_y - req_min_y);
1079
1080    debug!(
1081        "GetFeatureInfo: screen=({}, {}), world=({}, {})",
1082        screen_x, screen_y, world_x, world_y
1083    );
1084
1085    // Convert world coordinates to pixel coordinates in the dataset
1086    let geo_transform = dataset
1087        .geo_transform_obj()
1088        .ok_or_else(|| WmsError::Rendering("Dataset has no geotransform".to_string()))?;
1089
1090    let (px_f, py_f) = geo_transform
1091        .world_to_pixel(world_x, world_y)
1092        .map_err(|e| WmsError::Rendering(format!("Coordinate transform error: {}", e)))?;
1093
1094    let pixel_x = px_f.floor() as i64;
1095    let pixel_y = py_f.floor() as i64;
1096
1097    // Check if the pixel is within dataset bounds
1098    let ds_width = dataset.width() as i64;
1099    let ds_height = dataset.height() as i64;
1100
1101    if pixel_x < 0 || pixel_y < 0 || pixel_x >= ds_width || pixel_y >= ds_height {
1102        // Point is outside the dataset - return empty result
1103        return Ok(PixelQueryResult {
1104            world_x,
1105            world_y,
1106            pixel_x: pixel_x.max(0) as u64,
1107            pixel_y: pixel_y.max(0) as u64,
1108            band_values: Vec::new(),
1109            data_type: format!("{:?}", dataset.data_type()),
1110        });
1111    }
1112
1113    let px = pixel_x as u64;
1114    let py = pixel_y as u64;
1115
1116    // Read pixel values from each band
1117    let band_count = dataset.raster_count();
1118    let nodata = dataset.nodata();
1119    let mut band_values = Vec::with_capacity(band_count);
1120
1121    for band_idx in 0..band_count {
1122        // Use get_pixel which reads from the appropriate tile
1123        match dataset.get_pixel(px, py) {
1124            Ok(value) => {
1125                let is_nodata = nodata
1126                    .as_f64()
1127                    .is_some_and(|nd| (value - nd).abs() < f64::EPSILON);
1128                band_values.push((band_idx + 1, value, is_nodata));
1129            }
1130            Err(e) => {
1131                debug!(
1132                    "Failed to read pixel at ({}, {}) band {}: {}",
1133                    px, py, band_idx, e
1134                );
1135                band_values.push((band_idx + 1, f64::NAN, true));
1136            }
1137        }
1138    }
1139
1140    Ok(PixelQueryResult {
1141        world_x,
1142        world_y,
1143        pixel_x: px,
1144        pixel_y: py,
1145        band_values,
1146        data_type: format!("{:?}", dataset.data_type()),
1147    })
1148}
1149
1150/// Format feature info as plain text
1151fn format_feature_info_text(
1152    layer_name: &str,
1153    screen_x: u32,
1154    screen_y: u32,
1155    result: &PixelQueryResult,
1156) -> String {
1157    let mut text = format!(
1158        "Layer: {}\nQuery Point: ({}, {})\nWorld Coordinates: ({:.6}, {:.6})\nPixel Coordinates: ({}, {})\nData Type: {}\n",
1159        layer_name,
1160        screen_x,
1161        screen_y,
1162        result.world_x,
1163        result.world_y,
1164        result.pixel_x,
1165        result.pixel_y,
1166        result.data_type
1167    );
1168
1169    if result.band_values.is_empty() {
1170        text.push_str("Values: (outside dataset bounds)\n");
1171    } else {
1172        text.push_str("Band Values:\n");
1173        for (band, value, is_nodata) in &result.band_values {
1174            if *is_nodata {
1175                text.push_str(&format!("  Band {}: NoData\n", band));
1176            } else {
1177                text.push_str(&format!("  Band {}: {:.6}\n", band, value));
1178            }
1179        }
1180    }
1181
1182    text
1183}
1184
1185/// Format feature info as JSON
1186fn format_feature_info_json(
1187    layer_name: &str,
1188    screen_x: u32,
1189    screen_y: u32,
1190    result: &PixelQueryResult,
1191) -> String {
1192    let mut bands_json = String::from("[");
1193    for (i, (band, value, is_nodata)) in result.band_values.iter().enumerate() {
1194        if i > 0 {
1195            bands_json.push(',');
1196        }
1197        if *is_nodata {
1198            bands_json.push_str(&format!(
1199                r#"{{"band":{},"value":null,"nodata":true}}"#,
1200                band
1201            ));
1202        } else {
1203            bands_json.push_str(&format!(
1204                r#"{{"band":{},"value":{},"nodata":false}}"#,
1205                band, value
1206            ));
1207        }
1208    }
1209    bands_json.push(']');
1210
1211    format!(
1212        r#"{{"type":"FeatureInfo","layer":"{}","query_point":{{"x":{},"y":{}}},"world_coords":{{"x":{},"y":{}}},"pixel_coords":{{"x":{},"y":{}}},"data_type":"{}","bands":{}}}"#,
1213        layer_name,
1214        screen_x,
1215        screen_y,
1216        result.world_x,
1217        result.world_y,
1218        result.pixel_x,
1219        result.pixel_y,
1220        result.data_type,
1221        bands_json
1222    )
1223}
1224
1225/// Format feature info as XML (GML-like)
1226fn format_feature_info_xml(
1227    layer_name: &str,
1228    screen_x: u32,
1229    screen_y: u32,
1230    result: &PixelQueryResult,
1231) -> String {
1232    let mut xml = String::from(r#"<?xml version="1.0" encoding="UTF-8"?>"#);
1233    xml.push('\n');
1234    xml.push_str("<FeatureInfoResponse>\n");
1235    xml.push_str(&format!("  <Layer name=\"{}\">\n", layer_name));
1236    xml.push_str(&format!(
1237        "    <QueryPoint x=\"{}\" y=\"{}\"/>\n",
1238        screen_x, screen_y
1239    ));
1240    xml.push_str(&format!(
1241        "    <WorldCoords x=\"{:.6}\" y=\"{:.6}\"/>\n",
1242        result.world_x, result.world_y
1243    ));
1244    xml.push_str(&format!(
1245        "    <PixelCoords x=\"{}\" y=\"{}\"/>\n",
1246        result.pixel_x, result.pixel_y
1247    ));
1248
1249    for (band, value, is_nodata) in &result.band_values {
1250        if *is_nodata {
1251            xml.push_str(&format!("    <Band index=\"{}\" nodata=\"true\"/>\n", band));
1252        } else {
1253            xml.push_str(&format!(
1254                "    <Band index=\"{}\">{:.6}</Band>\n",
1255                band, value
1256            ));
1257        }
1258    }
1259
1260    xml.push_str("  </Layer>\n");
1261    xml.push_str("</FeatureInfoResponse>");
1262
1263    xml
1264}
1265
1266/// Format feature info as HTML
1267fn format_feature_info_html(
1268    layer_name: &str,
1269    screen_x: u32,
1270    screen_y: u32,
1271    result: &PixelQueryResult,
1272) -> String {
1273    let mut html =
1274        String::from("<!DOCTYPE html>\n<html>\n<head><title>Feature Info</title></head>\n<body>\n");
1275    html.push_str(&format!("<h3>Layer: {}</h3>\n", layer_name));
1276    html.push_str("<table border=\"1\">\n");
1277    html.push_str(&format!(
1278        "<tr><td>Query Point</td><td>({}, {})</td></tr>\n",
1279        screen_x, screen_y
1280    ));
1281    html.push_str(&format!(
1282        "<tr><td>World Coordinates</td><td>({:.6}, {:.6})</td></tr>\n",
1283        result.world_x, result.world_y
1284    ));
1285    html.push_str(&format!(
1286        "<tr><td>Pixel Coordinates</td><td>({}, {})</td></tr>\n",
1287        result.pixel_x, result.pixel_y
1288    ));
1289
1290    for (band, value, is_nodata) in &result.band_values {
1291        if *is_nodata {
1292            html.push_str(&format!("<tr><td>Band {}</td><td>NoData</td></tr>\n", band));
1293        } else {
1294            html.push_str(&format!(
1295                "<tr><td>Band {}</td><td>{:.6}</td></tr>\n",
1296                band, value
1297            ));
1298        }
1299    }
1300
1301    html.push_str("</table>\n</body>\n</html>");
1302
1303    html
1304}