tileyolo 0.2.1

A simple raster-based XYZ tile server for serving GeoTIFFs
// src/reader/metadata.rs

use crate::{
    reader::{ColourStop, Layer, LayerGeometry},
    utils::style::{is_builtin_palette, parse_style_file},
};
use csv::{ReaderBuilder, WriterBuilder};
use serde::{Deserialize, Serialize};
use std::{
    collections::HashMap,
    path::Path,
    time::{Duration, UNIX_EPOCH},
};

/// The minimal fields we need to cache on disk.
#[derive(Serialize, Deserialize, Clone)]
pub struct LayerMetadata {
    pub layer: String,
    pub size_bytes: u64,
    pub last_modified: u64,
    pub crs_code: i32,
    pub min_value: f32,
    pub max_value: f32,
    pub is_cog: bool,

    // split your extent tuple into four CSV columns
    #[serde(default)]
    pub extent_minx: f64,
    #[serde(default)]
    pub extent_miny: f64,
    #[serde(default)]
    pub extent_maxx: f64,
    #[serde(default)]
    pub extent_maxy: f64,
}

impl LayerMetadata {
    /// Build LayerMetadata from a full Layer
    pub fn from_layer(layer: &Layer) -> Self {
        let last_modified = layer
            .last_modified
            .duration_since(UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        LayerMetadata {
            layer: layer.layer.clone(),
            size_bytes: layer.size_bytes,
            last_modified,
            crs_code: layer.geometry.crs_code,
            min_value: layer.min_value,
            max_value: layer.max_value,
            is_cog: layer.is_cog,
            extent_minx: layer.extent.0,
            extent_miny: layer.extent.1,
            extent_maxx: layer.extent.2,
            extent_maxy: layer.extent.3,
        }
    }

    /// Reconstruct a Layer (including style/colour_stops) from metadata + actual file path
    pub fn to_layer(&self, path: &Path) -> Layer {
        // Style is determined at runtime from the immediate parent folder
        let style_name = path
            .parent()
            .and_then(|p| p.file_name())
            .and_then(|s| s.to_str())
            .unwrap_or("default");

        let colour_stops: Vec<ColourStop> = if is_builtin_palette(style_name) {
            Vec::new()
        } else {
            let style_path = path.parent().unwrap().join("style.txt");
            parse_style_file(&style_path).unwrap_or_default()
        };

        let last_modified = UNIX_EPOCH + Duration::from_secs(self.last_modified);

        // rehydrate your extent tuple
        let extent = (
            self.extent_minx,
            self.extent_miny,
            self.extent_maxx,
            self.extent_maxy,
        );

        Layer {
            layer: self.layer.clone(),
            style: style_name.to_string(),
            path: path.to_path_buf(),
            size_bytes: self.size_bytes,
            geometry: LayerGeometry {
                crs_name: "EPSG".to_string(),
                crs_code: self.crs_code,
            },
            extent,
            colour_stops,
            min_value: self.min_value,
            max_value: self.max_value,
            is_cog: self.is_cog,
            last_modified,
        }
    }
}

pub type MetadataCache = HashMap<String, LayerMetadata>;

/// Load the metadata cache from disk (or return empty on any error)
pub fn load_cache(cache_path: &Path) -> MetadataCache {
    let mut cache = MetadataCache::new();
    if let Ok(mut rdr) = ReaderBuilder::new().has_headers(true).from_path(cache_path) {
        for meta in rdr.deserialize::<LayerMetadata>().flatten() {
            cache.insert(meta.layer.clone(), meta);
        }
    }
    cache
}

/// Save the metadata cache back to disk (ignore errors)
pub fn save_cache(cache_path: &Path, cache: &MetadataCache) {
    if let Ok(mut wtr) = WriterBuilder::new().has_headers(true).from_path(cache_path) {
        for meta in cache.values() {
            let _ = wtr.serialize(meta);
        }
        let _ = wtr.flush();
    }
}

/// Produce the key for a file (filename without extension only)
pub fn key_for(path: &Path, _root: &Path) -> String {
    path.file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or_default()
        .to_string()
}