Skip to main content

oxigdal_server/handlers/
rendering.rs

1//! Raster rendering utilities for WMS and WMTS handlers
2//!
3//! This module provides colormap application, min/max scaling,
4//! and image generation for serving raster data.
5
6use crate::config::{ImageFormat, StyleConfig};
7use bytes::Bytes;
8use oxigdal_algorithms::resampling::{Resampler, ResamplingMethod};
9use oxigdal_core::buffer::RasterBuffer;
10#[cfg(test)]
11use oxigdal_core::types::RasterDataType;
12use oxigdal_core::types::{BoundingBox, GeoTransform};
13use thiserror::Error;
14
15/// Rendering errors
16#[derive(Debug, Error)]
17pub enum RenderError {
18    /// Invalid parameters
19    #[error("Invalid parameter: {0}")]
20    InvalidParameter(String),
21
22    /// Data read error
23    #[error("Failed to read data: {0}")]
24    ReadError(String),
25
26    /// Resampling error
27    #[error("Resampling failed: {0}")]
28    ResamplingError(String),
29
30    /// Image encoding error
31    #[error("Image encoding failed: {0}")]
32    EncodingError(String),
33
34    /// Unsupported operation
35    #[error("Unsupported: {0}")]
36    Unsupported(String),
37}
38
39impl From<oxigdal_core::OxiGdalError> for RenderError {
40    fn from(e: oxigdal_core::OxiGdalError) -> Self {
41        RenderError::ReadError(e.to_string())
42    }
43}
44
45impl From<oxigdal_algorithms::AlgorithmError> for RenderError {
46    fn from(e: oxigdal_algorithms::AlgorithmError) -> Self {
47        RenderError::ResamplingError(e.to_string())
48    }
49}
50
51/// Colormap types for raster visualization
52#[derive(Debug, Clone, Copy, PartialEq)]
53pub enum Colormap {
54    /// Grayscale (black to white)
55    Grayscale,
56    /// Viridis (perceptually uniform, blue to yellow)
57    Viridis,
58    /// Terrain (green/brown, for elevation)
59    Terrain,
60    /// Jet (rainbow, blue to red)
61    Jet,
62    /// Hot (black to red to yellow to white)
63    Hot,
64    /// Cool (cyan to magenta)
65    Cool,
66    /// Spectral (diverging, red-yellow-blue)
67    Spectral,
68    /// NDVI (brown to green, for vegetation)
69    Ndvi,
70}
71
72impl Colormap {
73    /// Parse colormap name from string
74    pub fn from_name(name: &str) -> Option<Self> {
75        match name.to_lowercase().as_str() {
76            "grayscale" | "gray" | "grey" => Some(Self::Grayscale),
77            "viridis" => Some(Self::Viridis),
78            "terrain" => Some(Self::Terrain),
79            "jet" | "rainbow" => Some(Self::Jet),
80            "hot" => Some(Self::Hot),
81            "cool" => Some(Self::Cool),
82            "spectral" => Some(Self::Spectral),
83            "ndvi" | "vegetation" => Some(Self::Ndvi),
84            _ => None,
85        }
86    }
87
88    /// Apply colormap to a normalized value (0.0 to 1.0)
89    /// Returns (R, G, B) values in range 0-255
90    #[must_use]
91    pub fn apply(&self, value: f64) -> (u8, u8, u8) {
92        let v = value.clamp(0.0, 1.0);
93
94        match self {
95            Self::Grayscale => {
96                let gray = (v * 255.0) as u8;
97                (gray, gray, gray)
98            }
99            Self::Viridis => Self::viridis(v),
100            Self::Terrain => Self::terrain(v),
101            Self::Jet => Self::jet(v),
102            Self::Hot => Self::hot(v),
103            Self::Cool => Self::cool(v),
104            Self::Spectral => Self::spectral(v),
105            Self::Ndvi => Self::ndvi(v),
106        }
107    }
108
109    fn viridis(t: f64) -> (u8, u8, u8) {
110        // Viridis colormap approximation
111        let r = ((0.267004 + t * (0.993248 - 0.267004)) * 255.0) as u8;
112        let g = if t < 0.5 {
113            (t * 2.0 * 0.7).clamp(0.0, 1.0) * 255.0
114        } else {
115            (0.7 + (t - 0.5) * 2.0 * 0.3).clamp(0.0, 1.0) * 255.0
116        } as u8;
117        let b = if t < 0.3 {
118            ((0.33 + t / 0.3 * 0.37) * 255.0) as u8
119        } else if t < 0.7 {
120            ((0.7 - (t - 0.3) / 0.4 * 0.5) * 255.0) as u8
121        } else {
122            ((0.2 * (1.0 - (t - 0.7) / 0.3)) * 255.0) as u8
123        };
124        (r, g, b)
125    }
126
127    fn terrain(t: f64) -> (u8, u8, u8) {
128        // Terrain colormap: blue -> green -> brown -> white
129        if t < 0.1 {
130            // Deep water (dark blue)
131            (0, 0, (t / 0.1 * 128.0 + 64.0) as u8)
132        } else if t < 0.25 {
133            // Shallow water (light blue)
134            let v = (t - 0.1) / 0.15;
135            (0, (v * 128.0) as u8, (192.0 - v * 64.0) as u8)
136        } else if t < 0.5 {
137            // Lowland (green)
138            let v = (t - 0.25) / 0.25;
139            (
140                (v * 100.0) as u8,
141                (128.0 + v * 64.0) as u8,
142                (128.0 - v * 64.0) as u8,
143            )
144        } else if t < 0.75 {
145            // Highland (brown/tan)
146            let v = (t - 0.5) / 0.25;
147            (
148                (100.0 + v * 100.0) as u8,
149                (192.0 - v * 64.0) as u8,
150                (64.0 + v * 32.0) as u8,
151            )
152        } else {
153            // Mountain/snow (gray to white)
154            let v = (t - 0.75) / 0.25;
155            let c = (200.0 + v * 55.0) as u8;
156            (c, c, c)
157        }
158    }
159
160    fn jet(t: f64) -> (u8, u8, u8) {
161        // Classic jet/rainbow colormap
162        let r = if t < 0.35 {
163            0.0
164        } else if t < 0.65 {
165            (t - 0.35) / 0.3
166        } else {
167            1.0
168        };
169
170        let g = if t < 0.125 {
171            0.0
172        } else if t < 0.375 {
173            (t - 0.125) / 0.25
174        } else if t < 0.625 {
175            1.0
176        } else if t < 0.875 {
177            1.0 - (t - 0.625) / 0.25
178        } else {
179            0.0
180        };
181
182        let b = if t < 0.35 {
183            0.5 + t / 0.35 * 0.5
184        } else if t < 0.65 {
185            1.0 - (t - 0.35) / 0.3
186        } else {
187            0.0
188        };
189
190        ((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
191    }
192
193    fn hot(t: f64) -> (u8, u8, u8) {
194        // Hot colormap: black -> red -> yellow -> white
195        let r = if t < 0.4 { t / 0.4 } else { 1.0 };
196        let g = if t < 0.4 {
197            0.0
198        } else if t < 0.8 {
199            (t - 0.4) / 0.4
200        } else {
201            1.0
202        };
203        let b = if t < 0.8 { 0.0 } else { (t - 0.8) / 0.2 };
204
205        ((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
206    }
207
208    fn cool(t: f64) -> (u8, u8, u8) {
209        // Cool colormap: cyan to magenta
210        ((t * 255.0) as u8, ((1.0 - t) * 255.0) as u8, 255)
211    }
212
213    fn spectral(t: f64) -> (u8, u8, u8) {
214        // Spectral diverging colormap
215        if t < 0.2 {
216            let v = t / 0.2;
217            (
218                (158.0 + v * 60.0) as u8,
219                (1.0 + v * 102.0) as u8,
220                (66.0) as u8,
221            )
222        } else if t < 0.4 {
223            let v = (t - 0.2) / 0.2;
224            (
225                (213.0 + v * 40.0) as u8,
226                (103.0 + v * 96.0) as u8,
227                ((66.0 + v * 8.0) as u8),
228            )
229        } else if t < 0.6 {
230            let v = (t - 0.4) / 0.2;
231            (
232                (253.0 - v * 82.0) as u8,
233                (199.0 + v * 32.0) as u8,
234                (74.0 + v * 92.0) as u8,
235            )
236        } else if t < 0.8 {
237            let v = (t - 0.6) / 0.2;
238            (
239                (171.0 - v * 69.0) as u8,
240                (231.0 - v * 42.0) as u8,
241                (166.0 - v * 22.0) as u8,
242            )
243        } else {
244            let v = (t - 0.8) / 0.2;
245            (
246                (102.0 - v * 49.0) as u8,
247                (189.0 - v * 60.0) as u8,
248                ((144.0 - v * 45.0) as u8),
249            )
250        }
251    }
252
253    fn ndvi(t: f64) -> (u8, u8, u8) {
254        // NDVI colormap: brown -> yellow -> green
255        if t < 0.2 {
256            // Brown (sparse/no vegetation)
257            let v = t / 0.2;
258            (
259                (139.0 - v * 30.0) as u8,
260                (69.0 + v * 40.0) as u8,
261                (19.0 + v * 30.0) as u8,
262            )
263        } else if t < 0.4 {
264            // Yellow-brown
265            let v = (t - 0.2) / 0.2;
266            (
267                (109.0 + v * 100.0) as u8,
268                (109.0 + v * 90.0) as u8,
269                (49.0 - v * 20.0) as u8,
270            )
271        } else if t < 0.6 {
272            // Yellow-green
273            let v = (t - 0.4) / 0.2;
274            (
275                (209.0 - v * 77.0) as u8,
276                (199.0 - v * 30.0) as u8,
277                (29.0 + v * 20.0) as u8,
278            )
279        } else if t < 0.8 {
280            // Light green
281            let v = (t - 0.6) / 0.2;
282            (
283                (132.0 - v * 66.0) as u8,
284                (169.0 - v * 24.0) as u8,
285                (49.0) as u8,
286            )
287        } else {
288            // Dark green (dense vegetation)
289            let v = (t - 0.8) / 0.2;
290            (
291                (66.0 - v * 32.0) as u8,
292                ((145.0 - v * 45.0) as u8),
293                ((49.0 - v * 20.0) as u8),
294            )
295        }
296    }
297}
298
299/// Style parameters for rendering
300#[derive(Debug, Clone)]
301pub struct RenderStyle {
302    /// Colormap for single-band data
303    pub colormap: Option<Colormap>,
304    /// Value range for normalization (min, max)
305    pub value_range: Option<(f64, f64)>,
306    /// Alpha/transparency (0.0 = transparent, 1.0 = opaque)
307    pub alpha: f32,
308    /// Gamma correction (1.0 = no correction)
309    pub gamma: f32,
310    /// Brightness adjustment (-1.0 to 1.0)
311    pub brightness: f32,
312    /// Contrast adjustment (0.0 to 2.0, 1.0 = normal)
313    pub contrast: f32,
314    /// Resampling method
315    pub resampling: ResamplingMethod,
316}
317
318impl Default for RenderStyle {
319    fn default() -> Self {
320        Self {
321            colormap: Some(Colormap::Grayscale),
322            value_range: None,
323            alpha: 1.0,
324            gamma: 1.0,
325            brightness: 0.0,
326            contrast: 1.0,
327            resampling: ResamplingMethod::Bilinear,
328        }
329    }
330}
331
332impl RenderStyle {
333    /// Create from StyleConfig
334    pub fn from_config(config: &StyleConfig) -> Self {
335        let colormap = config
336            .colormap
337            .as_ref()
338            .and_then(|name| Colormap::from_name(name))
339            .or(Some(Colormap::Grayscale));
340
341        Self {
342            colormap,
343            value_range: config.value_range,
344            alpha: config.alpha,
345            gamma: config.gamma,
346            brightness: config.brightness,
347            contrast: config.contrast,
348            resampling: ResamplingMethod::Bilinear,
349        }
350    }
351}
352
353/// Raster renderer for generating images from raster data
354pub struct RasterRenderer;
355
356impl RasterRenderer {
357    /// Render a RasterBuffer to an RGBA image buffer
358    ///
359    /// # Arguments
360    /// * `buffer` - Source raster data
361    /// * `style` - Rendering style parameters
362    ///
363    /// # Returns
364    /// RGBA image data as `Vec<u8>` (4 bytes per pixel)
365    pub fn render_to_rgba(
366        buffer: &RasterBuffer,
367        style: &RenderStyle,
368    ) -> Result<Vec<u8>, RenderError> {
369        let width = buffer.width() as usize;
370        let height = buffer.height() as usize;
371        let pixel_count = width * height;
372
373        // First, compute min/max if not provided
374        let (min_val, max_val) = if let Some((min, max)) = style.value_range {
375            (min, max)
376        } else {
377            // Compute statistics from buffer
378            let stats = buffer.compute_statistics().map_err(|e| {
379                RenderError::ReadError(format!("Failed to compute statistics: {}", e))
380            })?;
381            (stats.min, stats.max)
382        };
383
384        let value_range = max_val - min_val;
385        if value_range.abs() < f64::EPSILON {
386            // All values are the same - return solid color
387            let alpha = (style.alpha * 255.0) as u8;
388            return Ok([128, 128, 128, alpha].repeat(pixel_count));
389        }
390
391        // Allocate output buffer
392        let mut rgba = vec![0u8; pixel_count * 4];
393
394        // Apply colormap based on data type
395        let colormap = style.colormap.unwrap_or(Colormap::Grayscale);
396        let gamma = style.gamma;
397        let brightness = style.brightness;
398        let contrast = style.contrast;
399        let alpha = (style.alpha * 255.0) as u8;
400
401        for y in 0..height {
402            for x in 0..width {
403                let pixel_idx = y * width + x;
404                let rgba_idx = pixel_idx * 4;
405
406                // Get pixel value
407                let value = buffer.get_pixel(x as u64, y as u64).unwrap_or(f64::NAN);
408
409                // Handle nodata
410                if value.is_nan() || buffer.is_nodata(value) {
411                    // Transparent
412                    rgba[rgba_idx] = 0;
413                    rgba[rgba_idx + 1] = 0;
414                    rgba[rgba_idx + 2] = 0;
415                    rgba[rgba_idx + 3] = 0;
416                    continue;
417                }
418
419                // Normalize to 0-1
420                let mut normalized = (value - min_val) / value_range;
421
422                // Apply gamma correction
423                if (gamma - 1.0).abs() > f32::EPSILON {
424                    normalized = normalized.powf(gamma as f64);
425                }
426
427                // Apply brightness and contrast
428                if contrast.abs() > f32::EPSILON || brightness.abs() > f32::EPSILON {
429                    normalized = ((normalized - 0.5) * contrast as f64 + 0.5 + brightness as f64)
430                        .clamp(0.0, 1.0);
431                }
432
433                // Apply colormap
434                let (r, g, b) = colormap.apply(normalized);
435
436                rgba[rgba_idx] = r;
437                rgba[rgba_idx + 1] = g;
438                rgba[rgba_idx + 2] = b;
439                rgba[rgba_idx + 3] = alpha;
440            }
441        }
442
443        Ok(rgba)
444    }
445
446    /// Render RGB bands to RGBA image
447    ///
448    /// # Arguments
449    /// * `red` - Red band buffer
450    /// * `green` - Green band buffer
451    /// * `blue` - Blue band buffer
452    /// * `style` - Rendering style parameters
453    pub fn render_rgb_to_rgba(
454        red: &RasterBuffer,
455        green: &RasterBuffer,
456        blue: &RasterBuffer,
457        style: &RenderStyle,
458    ) -> Result<Vec<u8>, RenderError> {
459        let width = red.width() as usize;
460        let height = red.height() as usize;
461
462        if green.width() as usize != width
463            || green.height() as usize != height
464            || blue.width() as usize != width
465            || blue.height() as usize != height
466        {
467            return Err(RenderError::InvalidParameter(
468                "RGB bands must have same dimensions".to_string(),
469            ));
470        }
471
472        let pixel_count = width * height;
473        let mut rgba = vec![0u8; pixel_count * 4];
474
475        let alpha = (style.alpha * 255.0) as u8;
476        let gamma = style.gamma;
477        let brightness = style.brightness;
478        let contrast = style.contrast;
479
480        // Get value ranges for each band
481        let (r_min, r_max) = if let Some((min, max)) = style.value_range {
482            (min, max)
483        } else {
484            let stats = red.compute_statistics().map_err(|e| {
485                RenderError::ReadError(format!("Failed to compute red stats: {}", e))
486            })?;
487            (stats.min, stats.max)
488        };
489        let r_range = (r_max - r_min).max(1.0);
490
491        let g_stats = green
492            .compute_statistics()
493            .map_err(|e| RenderError::ReadError(format!("Failed to compute green stats: {}", e)))?;
494        let g_range = (g_stats.max - g_stats.min).max(1.0);
495        let g_min = g_stats.min;
496
497        let b_stats = blue
498            .compute_statistics()
499            .map_err(|e| RenderError::ReadError(format!("Failed to compute blue stats: {}", e)))?;
500        let b_range = (b_stats.max - b_stats.min).max(1.0);
501        let b_min = b_stats.min;
502
503        for y in 0..height {
504            for x in 0..width {
505                let pixel_idx = y * width + x;
506                let rgba_idx = pixel_idx * 4;
507
508                let r_val = red.get_pixel(x as u64, y as u64).unwrap_or(0.0);
509                let g_val = green.get_pixel(x as u64, y as u64).unwrap_or(0.0);
510                let b_val = blue.get_pixel(x as u64, y as u64).unwrap_or(0.0);
511
512                // Normalize
513                let mut r_norm = (r_val - r_min) / r_range;
514                let mut g_norm = (g_val - g_min) / g_range;
515                let mut b_norm = (b_val - b_min) / b_range;
516
517                // Apply gamma
518                if (gamma - 1.0).abs() > f32::EPSILON {
519                    let g = gamma as f64;
520                    r_norm = r_norm.powf(g);
521                    g_norm = g_norm.powf(g);
522                    b_norm = b_norm.powf(g);
523                }
524
525                // Apply brightness/contrast
526                if contrast.abs() > f32::EPSILON || brightness.abs() > f32::EPSILON {
527                    let c = contrast as f64;
528                    let b_adj = brightness as f64;
529                    r_norm = ((r_norm - 0.5) * c + 0.5 + b_adj).clamp(0.0, 1.0);
530                    g_norm = ((g_norm - 0.5) * c + 0.5 + b_adj).clamp(0.0, 1.0);
531                    b_norm = ((b_norm - 0.5) * c + 0.5 + b_adj).clamp(0.0, 1.0);
532                }
533
534                rgba[rgba_idx] = (r_norm * 255.0).clamp(0.0, 255.0) as u8;
535                rgba[rgba_idx + 1] = (g_norm * 255.0).clamp(0.0, 255.0) as u8;
536                rgba[rgba_idx + 2] = (b_norm * 255.0).clamp(0.0, 255.0) as u8;
537                rgba[rgba_idx + 3] = alpha;
538            }
539        }
540
541        Ok(rgba)
542    }
543
544    /// Resample buffer to target dimensions
545    pub fn resample(
546        buffer: &RasterBuffer,
547        target_width: u64,
548        target_height: u64,
549        method: ResamplingMethod,
550    ) -> Result<RasterBuffer, RenderError> {
551        let resampler = Resampler::new(method);
552        resampler
553            .resample(buffer, target_width, target_height)
554            .map_err(RenderError::from)
555    }
556
557    /// Read a window from a buffer (subset extraction)
558    pub fn read_window(
559        buffer: &RasterBuffer,
560        src_x: u64,
561        src_y: u64,
562        src_width: u64,
563        src_height: u64,
564    ) -> Result<RasterBuffer, RenderError> {
565        let width = buffer.width();
566        let height = buffer.height();
567
568        // Validate bounds
569        if src_x >= width || src_y >= height {
570            return Err(RenderError::InvalidParameter(format!(
571                "Window start ({}, {}) is outside buffer bounds ({}x{})",
572                src_x, src_y, width, height
573            )));
574        }
575
576        // Clamp window to buffer bounds
577        let actual_width = (src_width).min(width - src_x);
578        let actual_height = (src_height).min(height - src_y);
579
580        // Create output buffer
581        let data_type = buffer.data_type();
582        let mut output = RasterBuffer::zeros(actual_width, actual_height, data_type);
583
584        // Copy pixels
585        for dy in 0..actual_height {
586            for dx in 0..actual_width {
587                let value = buffer
588                    .get_pixel(src_x + dx, src_y + dy)
589                    .map_err(|e| RenderError::ReadError(e.to_string()))?;
590                output
591                    .set_pixel(dx, dy, value)
592                    .map_err(|e| RenderError::ReadError(e.to_string()))?;
593            }
594        }
595
596        Ok(output)
597    }
598}
599
600/// Encode RGBA data to PNG format
601pub fn encode_png(data: &[u8], width: u32, height: u32) -> Result<Vec<u8>, RenderError> {
602    let mut output = Vec::new();
603    {
604        let mut encoder = png::Encoder::new(&mut output, width, height);
605        encoder.set_color(png::ColorType::Rgba);
606        encoder.set_depth(png::BitDepth::Eight);
607
608        let mut writer = encoder
609            .write_header()
610            .map_err(|e| RenderError::EncodingError(e.to_string()))?;
611
612        writer
613            .write_image_data(data)
614            .map_err(|e| RenderError::EncodingError(e.to_string()))?;
615    }
616
617    Ok(output)
618}
619
620/// Encode RGBA data to JPEG format
621pub fn encode_jpeg(data: &[u8], width: u32, height: u32) -> Result<Vec<u8>, RenderError> {
622    // Convert RGBA to RGB (JPEG doesn't support alpha)
623    let rgb_data: Vec<u8> = data
624        .chunks(4)
625        .flat_map(|rgba| &rgba[0..3])
626        .copied()
627        .collect();
628
629    let mut jpeg_buffer = Vec::new();
630    let mut encoder = jpeg_encoder::Encoder::new(&mut jpeg_buffer, 90);
631    encoder.set_progressive(true);
632    encoder
633        .encode(
634            &rgb_data,
635            width as u16,
636            height as u16,
637            jpeg_encoder::ColorType::Rgb,
638        )
639        .map_err(|e| RenderError::EncodingError(e.to_string()))?;
640
641    Ok(jpeg_buffer)
642}
643
644/// Encode RGBA data to the specified format
645pub fn encode_image(
646    data: &[u8],
647    width: u32,
648    height: u32,
649    format: ImageFormat,
650) -> Result<Bytes, RenderError> {
651    let encoded = match format {
652        ImageFormat::Png => encode_png(data, width, height)?,
653        ImageFormat::Jpeg => encode_jpeg(data, width, height)?,
654        ImageFormat::Webp => {
655            return Err(RenderError::Unsupported(
656                "WebP encoding not yet implemented".to_string(),
657            ));
658        }
659        ImageFormat::Geotiff => {
660            return Err(RenderError::Unsupported(
661                "GeoTIFF encoding not yet implemented".to_string(),
662            ));
663        }
664    };
665
666    Ok(Bytes::from(encoded))
667}
668
669/// Calculate pixel coordinates from world coordinates
670pub fn world_to_pixel(
671    geo_transform: &GeoTransform,
672    world_x: f64,
673    world_y: f64,
674) -> Result<(u64, u64), RenderError> {
675    let (px, py) = geo_transform
676        .world_to_pixel(world_x, world_y)
677        .map_err(|e| RenderError::InvalidParameter(format!("Transform error: {}", e)))?;
678
679    if px < 0.0 || py < 0.0 {
680        return Err(RenderError::InvalidParameter(format!(
681            "Negative pixel coordinates: ({}, {})",
682            px, py
683        )));
684    }
685
686    Ok((px as u64, py as u64))
687}
688
689/// Calculate bounding box for a tile
690pub fn tile_to_bbox(
691    tile_matrix_set: &str,
692    z: u32,
693    x: u32,
694    y: u32,
695) -> Result<BoundingBox, RenderError> {
696    match tile_matrix_set {
697        "WebMercatorQuad" => {
698            let world_extent = 20_037_508.342_789_244;
699            let tile_count = 1u64 << z;
700            let tile_size = (2.0 * world_extent) / tile_count as f64;
701
702            let min_x = -world_extent + (x as f64) * tile_size;
703            let max_x = min_x + tile_size;
704            let max_y = world_extent - (y as f64) * tile_size;
705            let min_y = max_y - tile_size;
706
707            BoundingBox::new(min_x, min_y, max_x, max_y)
708                .map_err(|e| RenderError::InvalidParameter(format!("Invalid bbox: {}", e)))
709        }
710        "WorldCRS84Quad" => {
711            let tiles_x = 2u64 << z;
712            let tiles_y = 1u64 << z;
713            let tile_width = 360.0 / tiles_x as f64;
714            let tile_height = 180.0 / tiles_y as f64;
715
716            let min_x = -180.0 + (x as f64) * tile_width;
717            let max_x = min_x + tile_width;
718            let max_y = 90.0 - (y as f64) * tile_height;
719            let min_y = max_y - tile_height;
720
721            BoundingBox::new(min_x, min_y, max_x, max_y)
722                .map_err(|e| RenderError::InvalidParameter(format!("Invalid bbox: {}", e)))
723        }
724        _ => Err(RenderError::InvalidParameter(format!(
725            "Unknown tile matrix set: {}",
726            tile_matrix_set
727        ))),
728    }
729}
730
731/// Create a synthetic test buffer for testing
732#[cfg(test)]
733pub fn create_test_buffer(width: u64, height: u64) -> RasterBuffer {
734    let mut buffer = RasterBuffer::zeros(width, height, RasterDataType::Float32);
735
736    for y in 0..height {
737        for x in 0..width {
738            let value = (x as f64 + y as f64 * width as f64) / (width * height) as f64 * 100.0;
739            let _ = buffer.set_pixel(x, y, value);
740        }
741    }
742
743    buffer
744}
745
746#[cfg(test)]
747mod tests {
748    use super::*;
749
750    #[test]
751    fn test_colormap_grayscale() {
752        let cm = Colormap::Grayscale;
753        assert_eq!(cm.apply(0.0), (0, 0, 0));
754        assert_eq!(cm.apply(1.0), (255, 255, 255));
755        assert_eq!(cm.apply(0.5), (127, 127, 127));
756    }
757
758    #[test]
759    fn test_colormap_from_name() {
760        assert!(Colormap::from_name("viridis").is_some());
761        assert!(Colormap::from_name("VIRIDIS").is_some());
762        assert!(Colormap::from_name("terrain").is_some());
763        assert!(Colormap::from_name("unknown").is_none());
764    }
765
766    #[test]
767    fn test_render_to_rgba() {
768        let buffer = create_test_buffer(10, 10);
769        let style = RenderStyle::default();
770
771        let result = RasterRenderer::render_to_rgba(&buffer, &style);
772        assert!(result.is_ok());
773
774        let rgba = result.expect("render should succeed");
775        assert_eq!(rgba.len(), 10 * 10 * 4);
776    }
777
778    #[test]
779    fn test_encode_png() {
780        let rgba = vec![128u8; 4 * 4 * 4]; // 4x4 image
781        let result = encode_png(&rgba, 4, 4);
782        assert!(result.is_ok());
783    }
784
785    #[test]
786    fn test_tile_to_bbox_web_mercator() {
787        let bbox = tile_to_bbox("WebMercatorQuad", 0, 0, 0);
788        assert!(bbox.is_ok());
789
790        let bbox = bbox.expect("bbox should be valid");
791        assert!(bbox.min_x < bbox.max_x);
792        assert!(bbox.min_y < bbox.max_y);
793    }
794
795    #[test]
796    fn test_tile_to_bbox_wgs84() {
797        let bbox = tile_to_bbox("WorldCRS84Quad", 0, 0, 0);
798        assert!(bbox.is_ok());
799
800        let bbox = bbox.expect("bbox should be valid");
801        assert_eq!(bbox.min_x, -180.0);
802        assert_eq!(bbox.max_y, 90.0);
803    }
804
805    #[test]
806    fn test_read_window() {
807        let buffer = create_test_buffer(100, 100);
808        let window = RasterRenderer::read_window(&buffer, 10, 10, 20, 20);
809
810        assert!(window.is_ok());
811        let window = window.expect("window should succeed");
812        assert_eq!(window.width(), 20);
813        assert_eq!(window.height(), 20);
814    }
815
816    #[test]
817    fn test_resample() {
818        let buffer = create_test_buffer(100, 100);
819        let resampled = RasterRenderer::resample(&buffer, 50, 50, ResamplingMethod::Bilinear);
820
821        assert!(resampled.is_ok());
822        let resampled = resampled.expect("resample should succeed");
823        assert_eq!(resampled.width(), 50);
824        assert_eq!(resampled.height(), 50);
825    }
826}