Skip to main content

oxigdal_server/handlers/
wmts.rs

1//! WMTS (Web Map Tile Service) handlers
2//!
3//! Implements OGC Web Map Tile Service 1.0.0 protocol:
4//! - GetCapabilities: Returns XML metadata about tile sets
5//! - GetTile: Returns a specific tile from the tile matrix set
6
7use crate::cache::{CacheKey, TileCache};
8use crate::config::ImageFormat;
9use crate::dataset_registry::{DatasetRegistry, LayerInfo};
10use crate::handlers::rendering::{RasterRenderer, RenderStyle, encode_image, tile_to_bbox};
11use axum::{
12    extract::{Path as AxumPath, Query, State},
13    http::{StatusCode, header},
14    response::{IntoResponse, Response},
15};
16use bytes::Bytes;
17use oxigdal_algorithms::resampling::ResamplingMethod;
18use oxigdal_core::buffer::RasterBuffer;
19use quick_xml::Writer;
20use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
21use serde::Deserialize;
22use std::io::Cursor;
23use std::sync::Arc;
24use thiserror::Error;
25use tracing::{debug, trace};
26
27/// Configuration for source region to read from a dataset
28#[derive(Debug, Clone, Copy)]
29struct SourceRegion {
30    x: u64,
31    y: u64,
32    width: u64,
33    height: u64,
34}
35
36impl SourceRegion {
37    fn new(x: u64, y: u64, width: u64, height: u64) -> Self {
38        Self {
39            x,
40            y,
41            width,
42            height,
43        }
44    }
45}
46
47/// Configuration for tile dimensions
48#[derive(Debug, Clone, Copy)]
49struct TileDimensions {
50    width: u64,
51    height: u64,
52}
53
54impl TileDimensions {
55    fn new(width: u64, height: u64) -> Self {
56        Self { width, height }
57    }
58}
59
60/// WMTS errors
61#[derive(Debug, Error)]
62pub enum WmtsError {
63    /// Invalid request parameter
64    #[error("Invalid parameter: {0}")]
65    InvalidParameter(String),
66
67    /// Missing required parameter
68    #[error("Missing required parameter: {0}")]
69    MissingParameter(String),
70
71    /// Layer not found
72    #[error("Layer not found: {0}")]
73    LayerNotFound(String),
74
75    /// Tile matrix set not found
76    #[error("TileMatrixSet not found: {0}")]
77    TileMatrixSetNotFound(String),
78
79    /// Tile out of bounds
80    #[error("Tile coordinates out of bounds")]
81    TileOutOfBounds,
82
83    /// Rendering error
84    #[error("Rendering error: {0}")]
85    Rendering(String),
86
87    /// Registry error
88    #[error("Registry error: {0}")]
89    Registry(#[from] crate::dataset_registry::RegistryError),
90
91    /// Unsupported format
92    #[error("Unsupported format: {0}")]
93    UnsupportedFormat(String),
94}
95
96impl IntoResponse for WmtsError {
97    fn into_response(self) -> Response {
98        let (status, message) = match self {
99            WmtsError::InvalidParameter(_) | WmtsError::MissingParameter(_) => {
100                (StatusCode::BAD_REQUEST, self.to_string())
101            }
102            WmtsError::LayerNotFound(_) | WmtsError::TileOutOfBounds => {
103                (StatusCode::NOT_FOUND, self.to_string())
104            }
105            _ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
106        };
107
108        let xml = format!(
109            r#"<?xml version="1.0" encoding="UTF-8"?>
110<ExceptionReport version="1.0.0" xmlns="http://www.opengis.net/ows/1.1">
111  <Exception exceptionCode="{}">{}</Exception>
112</ExceptionReport>"#,
113            status.as_u16(),
114            message
115        );
116
117        (status, [(header::CONTENT_TYPE, "application/xml")], xml).into_response()
118    }
119}
120
121/// Shared WMTS state
122#[derive(Clone)]
123pub struct WmtsState {
124    /// Dataset registry
125    pub registry: DatasetRegistry,
126
127    /// Tile cache
128    pub cache: TileCache,
129
130    /// Service URL
131    pub service_url: String,
132
133    /// Service title
134    pub service_title: String,
135    /// Service description/abstract
136    pub service_abstract: String,
137}
138
139/// Tile matrix set definition
140#[derive(Debug, Clone)]
141pub struct TileMatrixSet {
142    /// Identifier (e.g., "WebMercatorQuad")
143    pub identifier: String,
144
145    /// Supported CRS
146    pub crs: String,
147
148    /// Tile matrices (one per zoom level)
149    pub matrices: Vec<TileMatrix>,
150}
151
152/// Individual tile matrix (zoom level)
153#[derive(Debug, Clone)]
154pub struct TileMatrix {
155    /// Identifier (zoom level as string)
156    pub identifier: String,
157
158    /// Scale denominator
159    pub scale_denominator: f64,
160
161    /// Top-left corner
162    pub top_left_corner: (f64, f64),
163
164    /// Tile width in pixels
165    pub tile_width: u32,
166
167    /// Tile height in pixels
168    pub tile_height: u32,
169
170    /// Matrix width in tiles
171    pub matrix_width: u32,
172
173    /// Matrix height in tiles
174    pub matrix_height: u32,
175}
176
177impl TileMatrixSet {
178    /// Create Web Mercator tile matrix set
179    pub fn web_mercator_quad() -> Self {
180        let mut matrices = Vec::new();
181
182        for z in 0..=18 {
183            let tiles_at_zoom = 1u32 << z;
184            let scale_denominator = 559082264.0287178 / (1u64 << z) as f64;
185
186            matrices.push(TileMatrix {
187                identifier: z.to_string(),
188                scale_denominator,
189                top_left_corner: (-20037508.34278925, 20037508.34278925),
190                tile_width: 256,
191                tile_height: 256,
192                matrix_width: tiles_at_zoom,
193                matrix_height: tiles_at_zoom,
194            });
195        }
196
197        Self {
198            identifier: "WebMercatorQuad".to_string(),
199            crs: "urn:ogc:def:crs:EPSG::3857".to_string(),
200            matrices,
201        }
202    }
203
204    /// Create World CRS84 tile matrix set
205    pub fn world_crs84_quad() -> Self {
206        let mut matrices = Vec::new();
207
208        for z in 0..=18 {
209            let tiles_x = 2u32 << z;
210            let tiles_y = 1u32 << z;
211            let scale_denominator = 279541132.0143589 / (1u64 << z) as f64;
212
213            matrices.push(TileMatrix {
214                identifier: z.to_string(),
215                scale_denominator,
216                top_left_corner: (-180.0, 90.0),
217                tile_width: 256,
218                tile_height: 256,
219                matrix_width: tiles_x,
220                matrix_height: tiles_y,
221            });
222        }
223
224        Self {
225            identifier: "WorldCRS84Quad".to_string(),
226            crs: "urn:ogc:def:crs:OGC:1.3:CRS84".to_string(),
227            matrices,
228        }
229    }
230
231    /// Get tile matrix by identifier
232    pub fn get_matrix(&self, identifier: &str) -> Option<&TileMatrix> {
233        self.matrices.iter().find(|m| m.identifier == identifier)
234    }
235}
236
237/// GetCapabilities request parameters
238#[derive(Debug, Deserialize)]
239#[allow(dead_code)]
240pub struct GetCapabilitiesParams {
241    #[serde(rename = "SERVICE")]
242    service: Option<String>,
243
244    #[serde(rename = "REQUEST")]
245    request: Option<String>,
246
247    #[serde(rename = "VERSION")]
248    version: Option<String>,
249}
250
251/// GetTile request parameters (KVP encoding)
252#[derive(Debug, Deserialize)]
253#[allow(dead_code)]
254pub struct GetTileParams {
255    #[serde(rename = "SERVICE")]
256    service: Option<String>,
257
258    #[serde(rename = "REQUEST")]
259    request: Option<String>,
260
261    #[serde(rename = "VERSION")]
262    version: Option<String>,
263
264    #[serde(rename = "LAYER")]
265    layer: String,
266
267    #[serde(rename = "STYLE")]
268    style: Option<String>,
269
270    #[serde(rename = "FORMAT")]
271    format: String,
272
273    #[serde(rename = "TILEMATRIXSET")]
274    tile_matrix_set: String,
275
276    #[serde(rename = "TILEMATRIX")]
277    tile_matrix: String,
278
279    #[serde(rename = "TILEROW")]
280    tile_row: u32,
281
282    #[serde(rename = "TILECOL")]
283    tile_col: u32,
284}
285
286/// RESTful GetTile path parameters
287#[derive(Debug, Deserialize)]
288pub struct GetTilePath {
289    layer: String,
290    tile_matrix_set: String,
291    tile_matrix: String,
292    tile_row: u32,
293    tile_col: u32,
294}
295
296/// Handle GetCapabilities request
297pub async fn get_capabilities(
298    State(state): State<Arc<WmtsState>>,
299    Query(params): Query<GetCapabilitiesParams>,
300) -> Result<Response, WmtsError> {
301    debug!("WMTS GetCapabilities request");
302
303    // Validate service parameter
304    if let Some(ref service) = params.service {
305        if service.to_uppercase() != "WMTS" {
306            return Err(WmtsError::InvalidParameter(format!(
307                "Invalid SERVICE: {}",
308                service
309            )));
310        }
311    }
312
313    // Get all layers
314    let layers = state.registry.list_layers()?;
315
316    // Get tile matrix sets
317    let matrix_sets = vec![
318        TileMatrixSet::web_mercator_quad(),
319        TileMatrixSet::world_crs84_quad(),
320    ];
321
322    // Generate capabilities XML
323    let xml = generate_capabilities_xml(&state, &layers, &matrix_sets)?;
324
325    Ok((
326        StatusCode::OK,
327        [(header::CONTENT_TYPE, "application/xml")],
328        xml,
329    )
330        .into_response())
331}
332
333/// Generate WMTS capabilities XML
334fn generate_capabilities_xml(
335    state: &WmtsState,
336    layers: &[LayerInfo],
337    matrix_sets: &[TileMatrixSet],
338) -> Result<String, WmtsError> {
339    let mut writer = Writer::new(Cursor::new(Vec::new()));
340
341    // XML declaration
342    writer
343        .write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))
344        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
345
346    // Root element
347    let mut root = BytesStart::new("Capabilities");
348    root.push_attribute(("version", "1.0.0"));
349    root.push_attribute(("xmlns", "http://www.opengis.net/wmts/1.0"));
350    root.push_attribute(("xmlns:ows", "http://www.opengis.net/ows/1.1"));
351    root.push_attribute(("xmlns:xlink", "http://www.w3.org/1999/xlink"));
352    writer
353        .write_event(Event::Start(root))
354        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
355
356    // ServiceIdentification
357    write_service_identification(&mut writer, state)?;
358
359    // Contents
360    write_contents(&mut writer, state, layers, matrix_sets)?;
361
362    // Close root
363    writer
364        .write_event(Event::End(BytesEnd::new("Capabilities")))
365        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
366
367    let result = writer.into_inner().into_inner();
368    String::from_utf8(result).map_err(|e| WmtsError::Rendering(e.to_string()))
369}
370
371/// Write ServiceIdentification section
372fn write_service_identification(
373    writer: &mut Writer<Cursor<Vec<u8>>>,
374    state: &WmtsState,
375) -> Result<(), WmtsError> {
376    writer
377        .write_event(Event::Start(BytesStart::new("ows:ServiceIdentification")))
378        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
379
380    // Title
381    writer
382        .write_event(Event::Start(BytesStart::new("ows:Title")))
383        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
384    writer
385        .write_event(Event::Text(BytesText::new(&state.service_title)))
386        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
387    writer
388        .write_event(Event::End(BytesEnd::new("ows:Title")))
389        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
390
391    // Abstract
392    writer
393        .write_event(Event::Start(BytesStart::new("ows:Abstract")))
394        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
395    writer
396        .write_event(Event::Text(BytesText::new(&state.service_abstract)))
397        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
398    writer
399        .write_event(Event::End(BytesEnd::new("ows:Abstract")))
400        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
401
402    // ServiceType
403    writer
404        .write_event(Event::Start(BytesStart::new("ows:ServiceType")))
405        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
406    writer
407        .write_event(Event::Text(BytesText::new("OGC WMTS")))
408        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
409    writer
410        .write_event(Event::End(BytesEnd::new("ows:ServiceType")))
411        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
412
413    // ServiceTypeVersion
414    writer
415        .write_event(Event::Start(BytesStart::new("ows:ServiceTypeVersion")))
416        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
417    writer
418        .write_event(Event::Text(BytesText::new("1.0.0")))
419        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
420    writer
421        .write_event(Event::End(BytesEnd::new("ows:ServiceTypeVersion")))
422        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
423
424    writer
425        .write_event(Event::End(BytesEnd::new("ows:ServiceIdentification")))
426        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
427
428    Ok(())
429}
430
431/// Write Contents section
432fn write_contents(
433    writer: &mut Writer<Cursor<Vec<u8>>>,
434    state: &WmtsState,
435    layers: &[LayerInfo],
436    matrix_sets: &[TileMatrixSet],
437) -> Result<(), WmtsError> {
438    writer
439        .write_event(Event::Start(BytesStart::new("Contents")))
440        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
441
442    // Write layers
443    for layer in layers {
444        write_layer(writer, state, layer, matrix_sets)?;
445    }
446
447    // Write tile matrix sets
448    for matrix_set in matrix_sets {
449        write_tile_matrix_set(writer, matrix_set)?;
450    }
451
452    writer
453        .write_event(Event::End(BytesEnd::new("Contents")))
454        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
455
456    Ok(())
457}
458
459/// Write Layer element
460fn write_layer(
461    writer: &mut Writer<Cursor<Vec<u8>>>,
462    state: &WmtsState,
463    layer: &LayerInfo,
464    matrix_sets: &[TileMatrixSet],
465) -> Result<(), WmtsError> {
466    writer
467        .write_event(Event::Start(BytesStart::new("Layer")))
468        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
469
470    // Identifier
471    writer
472        .write_event(Event::Start(BytesStart::new("ows:Identifier")))
473        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
474    writer
475        .write_event(Event::Text(BytesText::new(&layer.name)))
476        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
477    writer
478        .write_event(Event::End(BytesEnd::new("ows:Identifier")))
479        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
480
481    // Title
482    writer
483        .write_event(Event::Start(BytesStart::new("ows:Title")))
484        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
485    writer
486        .write_event(Event::Text(BytesText::new(&layer.title)))
487        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
488    writer
489        .write_event(Event::End(BytesEnd::new("ows:Title")))
490        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
491
492    // Formats
493    for format in &layer.config.formats {
494        writer
495            .write_event(Event::Start(BytesStart::new("Format")))
496            .map_err(|e| WmtsError::Rendering(e.to_string()))?;
497        writer
498            .write_event(Event::Text(BytesText::new(format.mime_type())))
499            .map_err(|e| WmtsError::Rendering(e.to_string()))?;
500        writer
501            .write_event(Event::End(BytesEnd::new("Format")))
502            .map_err(|e| WmtsError::Rendering(e.to_string()))?;
503    }
504
505    // TileMatrixSetLinks
506    for matrix_set in matrix_sets {
507        if layer
508            .config
509            .tile_matrix_sets
510            .contains(&matrix_set.identifier)
511        {
512            writer
513                .write_event(Event::Start(BytesStart::new("TileMatrixSetLink")))
514                .map_err(|e| WmtsError::Rendering(e.to_string()))?;
515
516            writer
517                .write_event(Event::Start(BytesStart::new("TileMatrixSet")))
518                .map_err(|e| WmtsError::Rendering(e.to_string()))?;
519            writer
520                .write_event(Event::Text(BytesText::new(&matrix_set.identifier)))
521                .map_err(|e| WmtsError::Rendering(e.to_string()))?;
522            writer
523                .write_event(Event::End(BytesEnd::new("TileMatrixSet")))
524                .map_err(|e| WmtsError::Rendering(e.to_string()))?;
525
526            writer
527                .write_event(Event::End(BytesEnd::new("TileMatrixSetLink")))
528                .map_err(|e| WmtsError::Rendering(e.to_string()))?;
529        }
530    }
531
532    // ResourceURL (RESTful template)
533    let mut resource_url = BytesStart::new("ResourceURL");
534    resource_url.push_attribute(("format", "image/png"));
535    resource_url.push_attribute(("resourceType", "tile"));
536    let template = format!(
537        "{}/wmts/1.0.0/{}/{{TileMatrixSet}}/{{TileMatrix}}/{{TileRow}}/{{TileCol}}.png",
538        state.service_url, layer.name
539    );
540    resource_url.push_attribute(("template", template.as_str()));
541    writer
542        .write_event(Event::Empty(resource_url))
543        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
544
545    writer
546        .write_event(Event::End(BytesEnd::new("Layer")))
547        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
548
549    Ok(())
550}
551
552/// Write TileMatrixSet element
553fn write_tile_matrix_set(
554    writer: &mut Writer<Cursor<Vec<u8>>>,
555    matrix_set: &TileMatrixSet,
556) -> Result<(), WmtsError> {
557    writer
558        .write_event(Event::Start(BytesStart::new("TileMatrixSet")))
559        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
560
561    // Identifier
562    writer
563        .write_event(Event::Start(BytesStart::new("ows:Identifier")))
564        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
565    writer
566        .write_event(Event::Text(BytesText::new(&matrix_set.identifier)))
567        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
568    writer
569        .write_event(Event::End(BytesEnd::new("ows:Identifier")))
570        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
571
572    // SupportedCRS
573    writer
574        .write_event(Event::Start(BytesStart::new("ows:SupportedCRS")))
575        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
576    writer
577        .write_event(Event::Text(BytesText::new(&matrix_set.crs)))
578        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
579    writer
580        .write_event(Event::End(BytesEnd::new("ows:SupportedCRS")))
581        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
582
583    // TileMatrix elements
584    for matrix in &matrix_set.matrices {
585        write_tile_matrix(writer, matrix)?;
586    }
587
588    writer
589        .write_event(Event::End(BytesEnd::new("TileMatrixSet")))
590        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
591
592    Ok(())
593}
594
595/// Write TileMatrix element
596fn write_tile_matrix(
597    writer: &mut Writer<Cursor<Vec<u8>>>,
598    matrix: &TileMatrix,
599) -> Result<(), WmtsError> {
600    writer
601        .write_event(Event::Start(BytesStart::new("TileMatrix")))
602        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
603
604    // Identifier
605    writer
606        .write_event(Event::Start(BytesStart::new("ows:Identifier")))
607        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
608    writer
609        .write_event(Event::Text(BytesText::new(&matrix.identifier)))
610        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
611    writer
612        .write_event(Event::End(BytesEnd::new("ows:Identifier")))
613        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
614
615    // ScaleDenominator
616    writer
617        .write_event(Event::Start(BytesStart::new("ScaleDenominator")))
618        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
619    writer
620        .write_event(Event::Text(BytesText::new(
621            &matrix.scale_denominator.to_string(),
622        )))
623        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
624    writer
625        .write_event(Event::End(BytesEnd::new("ScaleDenominator")))
626        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
627
628    // TopLeftCorner
629    writer
630        .write_event(Event::Start(BytesStart::new("TopLeftCorner")))
631        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
632    let corner = format!("{} {}", matrix.top_left_corner.0, matrix.top_left_corner.1);
633    writer
634        .write_event(Event::Text(BytesText::new(&corner)))
635        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
636    writer
637        .write_event(Event::End(BytesEnd::new("TopLeftCorner")))
638        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
639
640    // TileWidth
641    writer
642        .write_event(Event::Start(BytesStart::new("TileWidth")))
643        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
644    writer
645        .write_event(Event::Text(BytesText::new(&matrix.tile_width.to_string())))
646        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
647    writer
648        .write_event(Event::End(BytesEnd::new("TileWidth")))
649        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
650
651    // TileHeight
652    writer
653        .write_event(Event::Start(BytesStart::new("TileHeight")))
654        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
655    writer
656        .write_event(Event::Text(BytesText::new(&matrix.tile_height.to_string())))
657        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
658    writer
659        .write_event(Event::End(BytesEnd::new("TileHeight")))
660        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
661
662    // MatrixWidth
663    writer
664        .write_event(Event::Start(BytesStart::new("MatrixWidth")))
665        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
666    writer
667        .write_event(Event::Text(BytesText::new(
668            &matrix.matrix_width.to_string(),
669        )))
670        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
671    writer
672        .write_event(Event::End(BytesEnd::new("MatrixWidth")))
673        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
674
675    // MatrixHeight
676    writer
677        .write_event(Event::Start(BytesStart::new("MatrixHeight")))
678        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
679    writer
680        .write_event(Event::Text(BytesText::new(
681            &matrix.matrix_height.to_string(),
682        )))
683        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
684    writer
685        .write_event(Event::End(BytesEnd::new("MatrixHeight")))
686        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
687
688    writer
689        .write_event(Event::End(BytesEnd::new("TileMatrix")))
690        .map_err(|e| WmtsError::Rendering(e.to_string()))?;
691
692    Ok(())
693}
694
695/// Handle GetTile request (RESTful)
696pub async fn get_tile_rest(
697    State(state): State<Arc<WmtsState>>,
698    AxumPath(path): AxumPath<GetTilePath>,
699) -> Result<Response, WmtsError> {
700    debug!(
701        "WMTS GetTile (REST): layer={}, z={}, x={}, y={}",
702        path.layer, path.tile_matrix, path.tile_col, path.tile_row
703    );
704
705    get_tile_impl(
706        &state,
707        &path.layer,
708        &path.tile_matrix_set,
709        &path.tile_matrix,
710        path.tile_row,
711        path.tile_col,
712        ImageFormat::Png,
713    )
714    .await
715}
716
717/// Handle GetTile request (KVP)
718pub async fn get_tile_kvp(
719    State(state): State<Arc<WmtsState>>,
720    Query(params): Query<GetTileParams>,
721) -> Result<Response, WmtsError> {
722    debug!("WMTS GetTile (KVP): layer={}", params.layer);
723
724    let format = parse_format(&params.format)?;
725
726    get_tile_impl(
727        &state,
728        &params.layer,
729        &params.tile_matrix_set,
730        &params.tile_matrix,
731        params.tile_row,
732        params.tile_col,
733        format,
734    )
735    .await
736}
737
738/// Shared GetTile implementation
739async fn get_tile_impl(
740    state: &WmtsState,
741    layer_name: &str,
742    tile_matrix_set: &str,
743    tile_matrix: &str,
744    tile_row: u32,
745    tile_col: u32,
746    format: ImageFormat,
747) -> Result<Response, WmtsError> {
748    // Check cache first
749    let zoom_level = tile_matrix.parse::<u8>().ok().unwrap_or(0);
750    let cache_key = CacheKey::new(
751        layer_name.to_string(),
752        zoom_level,
753        tile_col,
754        tile_row,
755        format.extension().to_string(),
756    );
757
758    if let Some(cached_tile) = state.cache.get(&cache_key) {
759        trace!("Cache hit for tile: {}", cache_key.to_string());
760        return Ok((
761            StatusCode::OK,
762            [(header::CONTENT_TYPE, format.mime_type())],
763            cached_tile,
764        )
765            .into_response());
766    }
767
768    // Get layer (for validation and config)
769    let _ = state.registry.get_layer(layer_name)?;
770
771    // Validate tile matrix set
772    let matrix_set_obj = get_tile_matrix_set(tile_matrix_set)?;
773    let matrix = matrix_set_obj.get_matrix(tile_matrix).ok_or_else(|| {
774        WmtsError::InvalidParameter(format!("Invalid tile matrix: {}", tile_matrix))
775    })?;
776
777    // Validate tile coordinates
778    if tile_col >= matrix.matrix_width || tile_row >= matrix.matrix_height {
779        return Err(WmtsError::TileOutOfBounds);
780    }
781
782    // Render tile
783    let tile_data = render_tile(
784        &state.registry,
785        layer_name,
786        &matrix_set_obj,
787        matrix,
788        tile_row,
789        tile_col,
790        format,
791    )
792    .await?;
793
794    // Cache the tile
795    let _ = state.cache.put(cache_key, tile_data.clone());
796
797    Ok((
798        StatusCode::OK,
799        [(header::CONTENT_TYPE, format.mime_type())],
800        tile_data,
801    )
802        .into_response())
803}
804
805/// Get tile matrix set by identifier
806fn get_tile_matrix_set(identifier: &str) -> Result<TileMatrixSet, WmtsError> {
807    match identifier {
808        "WebMercatorQuad" => Ok(TileMatrixSet::web_mercator_quad()),
809        "WorldCRS84Quad" => Ok(TileMatrixSet::world_crs84_quad()),
810        _ => Err(WmtsError::TileMatrixSetNotFound(identifier.to_string())),
811    }
812}
813
814/// Parse image format from MIME type
815fn parse_format(format_str: &str) -> Result<ImageFormat, WmtsError> {
816    match format_str.to_lowercase().as_str() {
817        "image/png" => Ok(ImageFormat::Png),
818        "image/jpeg" | "image/jpg" => Ok(ImageFormat::Jpeg),
819        "image/webp" => Ok(ImageFormat::Webp),
820        _ => Err(WmtsError::UnsupportedFormat(format_str.to_string())),
821    }
822}
823
824/// Render a tile from the actual dataset
825///
826/// Performs the full WMTS tile rendering pipeline:
827/// 1. Calculates the geographic bounds of the tile from the tile matrix parameters
828/// 2. Converts tile bounds to pixel coordinates in the source dataset
829/// 3. Reads the corresponding raster data, using overviews when available
830/// 4. Resamples to the tile pixel dimensions (typically 256x256)
831/// 5. Applies colormap/styling
832/// 6. Encodes to the requested image format
833async fn render_tile(
834    registry: &DatasetRegistry,
835    layer_name: &str,
836    matrix_set: &TileMatrixSet,
837    matrix: &TileMatrix,
838    tile_row: u32,
839    tile_col: u32,
840    format: ImageFormat,
841) -> Result<Bytes, WmtsError> {
842    let zoom: u32 = matrix.identifier.parse().map_err(|_| {
843        WmtsError::InvalidParameter(format!(
844            "Cannot parse zoom level from matrix identifier: {}",
845            matrix.identifier
846        ))
847    })?;
848
849    debug!(
850        "Rendering tile: layer={}, z={}, col={}, row={}, matrix_set={}",
851        layer_name, zoom, tile_col, tile_row, matrix_set.identifier
852    );
853
854    // Get the dataset for this layer
855    let dataset = registry.get_dataset(layer_name).map_err(|e| {
856        WmtsError::Rendering(format!(
857            "Failed to get dataset for layer {}: {}",
858            layer_name, e
859        ))
860    })?;
861
862    // Get layer config for styling
863    let layer_info = registry
864        .get_layer(layer_name)
865        .map_err(|e| WmtsError::Rendering(format!("Failed to get layer info: {}", e)))?;
866
867    let tile_width = matrix.tile_width as u64;
868    let tile_height = matrix.tile_height as u64;
869
870    // Calculate the geographic bounds of this tile
871    let tile_bbox = tile_to_bbox(&matrix_set.identifier, zoom, tile_col, tile_row)
872        .map_err(|e| WmtsError::Rendering(format!("Failed to calculate tile bbox: {}", e)))?;
873
874    // Get the dataset's GeoTransform
875    let geo_transform = dataset
876        .geo_transform_obj()
877        .ok_or_else(|| WmtsError::Rendering("Dataset has no geotransform".to_string()))?;
878
879    let ds_width = dataset.width();
880    let ds_height = dataset.height();
881
882    // Convert tile bounds to pixel coordinates in the source dataset
883    // tile_bbox is in the CRS of the tile matrix set (e.g., EPSG:3857 or CRS84)
884    // The dataset may be in a different CRS, but for now we assume matching CRS
885    // or that the dataset is already in the tile CRS
886    let (px_min_x, px_min_y) = geo_transform
887        .world_to_pixel(tile_bbox.min_x, tile_bbox.max_y)
888        .map_err(|e| WmtsError::Rendering(format!("Coordinate transform error: {}", e)))?;
889    let (px_max_x, px_max_y) = geo_transform
890        .world_to_pixel(tile_bbox.max_x, tile_bbox.min_y)
891        .map_err(|e| WmtsError::Rendering(format!("Coordinate transform error: {}", e)))?;
892
893    // Determine the pixel window, clamping to dataset bounds
894    let src_x = (px_min_x.min(px_max_x).floor().max(0.0)) as u64;
895    let src_y = (px_min_y.min(px_max_y).floor().max(0.0)) as u64;
896    let src_end_x = (px_min_x.max(px_max_x).ceil().max(0.0) as u64).min(ds_width);
897    let src_end_y = (px_min_y.max(px_max_y).ceil().max(0.0) as u64).min(ds_height);
898
899    let src_width = src_end_x.saturating_sub(src_x);
900    let src_height = src_end_y.saturating_sub(src_y);
901
902    // Check if the tile has any overlap with the dataset
903    if src_width == 0 || src_height == 0 {
904        // No overlap - return a transparent/empty tile
905        return render_empty_tile(tile_width, tile_height, format);
906    }
907
908    // Determine the best overview level based on the resolution ratio
909    let overview_level =
910        select_tile_overview_level(&dataset, src_width, src_height, tile_width, tile_height);
911
912    // Build the rendering style from layer configuration
913    let render_style = if let Some(ref style_cfg) = layer_info.config.style {
914        RenderStyle::from_config(style_cfg)
915    } else {
916        RenderStyle::default()
917    };
918
919    let band_count = dataset.raster_count();
920
921    // Render based on band count
922    let rgba_data = if band_count >= 3 {
923        render_tile_rgb(
924            &dataset,
925            overview_level,
926            SourceRegion::new(src_x, src_y, src_width, src_height),
927            TileDimensions::new(tile_width, tile_height),
928            &render_style,
929        )?
930    } else {
931        render_tile_single_band(
932            &dataset,
933            overview_level,
934            SourceRegion::new(src_x, src_y, src_width, src_height),
935            TileDimensions::new(tile_width, tile_height),
936            &render_style,
937        )?
938    };
939
940    // Handle partial coverage: if the tile only partially overlaps the dataset,
941    // the edges will be nodata (transparent). This is already handled by the
942    // rendering functions returning 0-alpha for nodata pixels.
943
944    // Encode to the requested format
945    encode_image(&rgba_data, tile_width as u32, tile_height as u32, format)
946        .map_err(|e| WmtsError::Rendering(format!("Image encoding failed: {}", e)))
947}
948
949/// Render an empty (transparent) tile for areas outside the dataset
950fn render_empty_tile(width: u64, height: u64, format: ImageFormat) -> Result<Bytes, WmtsError> {
951    // Create a fully transparent RGBA buffer
952    let rgba = vec![0u8; (width * height * 4) as usize];
953    encode_image(&rgba, width as u32, height as u32, format)
954        .map_err(|e| WmtsError::Rendering(format!("Empty tile encoding failed: {}", e)))
955}
956
957/// Select the best overview level for tile rendering
958///
959/// Chooses the overview level that provides just enough resolution for the
960/// requested tile, avoiding reading unnecessarily high-resolution data
961/// at low zoom levels.
962fn select_tile_overview_level(
963    dataset: &crate::dataset_registry::Dataset,
964    src_width: u64,
965    src_height: u64,
966    tile_width: u64,
967    tile_height: u64,
968) -> usize {
969    let overview_count = dataset.overview_count();
970    if overview_count == 0 {
971        return 0;
972    }
973
974    // Calculate the downsample ratio
975    let ratio_x = if tile_width > 0 {
976        src_width as f64 / tile_width as f64
977    } else {
978        1.0
979    };
980    let ratio_y = if tile_height > 0 {
981        src_height as f64 / tile_height as f64
982    } else {
983        1.0
984    };
985    let request_ratio = ratio_x.max(ratio_y);
986
987    if request_ratio <= 1.0 {
988        return 0; // Full resolution or upsampling needed
989    }
990
991    // Find the best overview level (each level typically halves resolution)
992    let mut best_level = 0;
993    for level in 1..=overview_count {
994        let overview_factor = (1u64 << level) as f64;
995        if overview_factor <= request_ratio * 1.5 {
996            best_level = level;
997        } else {
998            break;
999        }
1000    }
1001
1002    best_level
1003}
1004
1005/// Render a single-band tile with colormap
1006fn render_tile_single_band(
1007    dataset: &crate::dataset_registry::Dataset,
1008    _overview_level: usize,
1009    source: SourceRegion,
1010    tile: TileDimensions,
1011    style: &RenderStyle,
1012) -> Result<Vec<u8>, WmtsError> {
1013    // Read the source window from the dataset
1014    let src_buffer = dataset
1015        .read_window(source.x, source.y, source.width, source.height)
1016        .map_err(|e| WmtsError::Rendering(format!("Failed to read window: {}", e)))?;
1017
1018    // Resample to tile dimensions
1019    let resampled = if src_buffer.width() != tile.width || src_buffer.height() != tile.height {
1020        RasterRenderer::resample(&src_buffer, tile.width, tile.height, style.resampling)
1021            .map_err(|e| WmtsError::Rendering(format!("Resampling failed: {}", e)))?
1022    } else {
1023        src_buffer
1024    };
1025
1026    // Render with colormap to RGBA
1027    RasterRenderer::render_to_rgba(&resampled, style)
1028        .map_err(|e| WmtsError::Rendering(format!("Rendering failed: {}", e)))
1029}
1030
1031/// Render an RGB tile from three bands
1032fn render_tile_rgb(
1033    dataset: &crate::dataset_registry::Dataset,
1034    _overview_level: usize,
1035    source: SourceRegion,
1036    tile: TileDimensions,
1037    style: &RenderStyle,
1038) -> Result<Vec<u8>, WmtsError> {
1039    // Read band 0 (red) using read_window
1040    let red_buffer = dataset
1041        .read_window(source.x, source.y, source.width, source.height)
1042        .map_err(|e| WmtsError::Rendering(format!("Failed to read red band: {}", e)))?;
1043
1044    // Read bands 1 (green) and 2 (blue) from full band data and extract windows
1045    let green_buffer =
1046        build_band_window(dataset, 1, source.x, source.y, source.width, source.height);
1047    let blue_buffer =
1048        build_band_window(dataset, 2, source.x, source.y, source.width, source.height);
1049
1050    let (green_buf, blue_buf) = match (green_buffer, blue_buffer) {
1051        (Ok(g), Ok(b)) => (g, b),
1052        _ => {
1053            // Fallback: use red band for all channels (grayscale as RGB)
1054            let gray = red_buffer.clone();
1055            (gray.clone(), gray)
1056        }
1057    };
1058
1059    // Resample each band
1060    let resample_method = style.resampling;
1061    let r_resampled = resample_if_needed(&red_buffer, tile.width, tile.height, resample_method)?;
1062    let g_resampled = resample_if_needed(&green_buf, tile.width, tile.height, resample_method)?;
1063    let b_resampled = resample_if_needed(&blue_buf, tile.width, tile.height, resample_method)?;
1064
1065    // Compose RGB to RGBA
1066    RasterRenderer::render_rgb_to_rgba(&r_resampled, &g_resampled, &b_resampled, style)
1067        .map_err(|e| WmtsError::Rendering(format!("RGB rendering failed: {}", e)))
1068}
1069
1070/// Resample a buffer to target dimensions if needed
1071fn resample_if_needed(
1072    buffer: &RasterBuffer,
1073    target_width: u64,
1074    target_height: u64,
1075    method: ResamplingMethod,
1076) -> Result<RasterBuffer, WmtsError> {
1077    if buffer.width() != target_width || buffer.height() != target_height {
1078        RasterRenderer::resample(buffer, target_width, target_height, method)
1079            .map_err(|e| WmtsError::Rendering(format!("Resampling failed: {}", e)))
1080    } else {
1081        Ok(buffer.clone())
1082    }
1083}
1084
1085/// Build a RasterBuffer window for a specific band from full band data
1086fn build_band_window(
1087    dataset: &crate::dataset_registry::Dataset,
1088    band: usize,
1089    src_x: u64,
1090    src_y: u64,
1091    src_width: u64,
1092    src_height: u64,
1093) -> Result<RasterBuffer, WmtsError> {
1094    let band_data = dataset
1095        .read_band(0, band)
1096        .map_err(|e| WmtsError::Rendering(format!("Failed to read band {}: {}", band, e)))?;
1097
1098    let ds_width = dataset.width();
1099    let ds_height = dataset.height();
1100    let data_type = dataset.data_type();
1101    let nodata = dataset.nodata();
1102
1103    let full_buffer = RasterBuffer::new(band_data, ds_width, ds_height, data_type, nodata)
1104        .map_err(|e| WmtsError::Rendering(format!("Buffer creation error: {}", e)))?;
1105
1106    // Extract the window
1107    let mut window = RasterBuffer::zeros(src_width, src_height, data_type);
1108    for dy in 0..src_height {
1109        for dx in 0..src_width {
1110            let gx = src_x + dx;
1111            let gy = src_y + dy;
1112            if gx < ds_width && gy < ds_height {
1113                if let Ok(val) = full_buffer.get_pixel(gx, gy) {
1114                    let _ = window.set_pixel(dx, dy, val);
1115                }
1116            }
1117        }
1118    }
1119
1120    Ok(window)
1121}