Skip to main content

trailgen_core/
raster.rs

1use crate::crs::{UtmCrs, geographic_to_utm, wgs84_to_web_mercator};
2use crate::enrich::{ElevationSample, ElevationSampler};
3use crate::geo::Coord;
4use crate::model::Provenance;
5use crate::{Result, TrailgenError};
6use num_traits::ToPrimitive;
7use serde::{Deserialize, Serialize};
8use std::fs::File;
9use std::iter::Peekable;
10use std::path::{Path, PathBuf};
11use std::str::FromStr;
12use tiff::decoder::{Decoder, DecodingResult};
13use tiff::tags::Tag;
14use tiff::{TiffError, TiffFormatError};
15
16#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
17pub struct ArcAsciiGrid {
18    pub ncols: usize,
19    pub nrows: usize,
20    pub xllcorner: f64,
21    pub yllcorner: f64,
22    pub cellsize: f64,
23    pub nodata_value: f64,
24    pub confidence: f64,
25    pub provenance: Provenance,
26    values: Vec<f64>,
27}
28
29#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
30pub struct GeoTiffDem {
31    pub width: usize,
32    pub height: usize,
33    pub crs: RasterCrs,
34    #[serde(alias = "origin_lon")]
35    pub origin_x: f64,
36    #[serde(alias = "origin_lat")]
37    pub origin_y: f64,
38    #[serde(alias = "pixel_width_deg")]
39    pub pixel_width: f64,
40    #[serde(alias = "pixel_height_deg")]
41    pub pixel_height: f64,
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub transform: Option<RasterTransform>,
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub nodata_value: Option<f64>,
46    pub confidence: f64,
47    pub provenance: Provenance,
48    values: Vec<f64>,
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
52pub struct RasterTransform {
53    pub x0: f64,
54    pub y0: f64,
55    pub dx_col: f64,
56    pub dy_col: f64,
57    pub dx_row: f64,
58    pub dy_row: f64,
59}
60
61impl RasterTransform {
62    fn north_up(origin_x: f64, origin_y: f64, pixel_width: f64, pixel_height: f64) -> Self {
63        Self {
64            x0: origin_x,
65            y0: origin_y,
66            dx_col: pixel_width,
67            dy_col: 0.0,
68            dx_row: 0.0,
69            dy_row: -pixel_height,
70        }
71    }
72
73    fn from_model_transformation(xs: &[f64]) -> Result<Self> {
74        if xs.len() < 16 {
75            return Err(TrailgenError::InvalidData(
76                "GeoTIFF ModelTransformationTag must contain sixteen numbers".to_owned(),
77            ));
78        }
79        let transform = Self {
80            x0: xs[3],
81            y0: xs[7],
82            dx_col: xs[0],
83            dy_col: xs[4],
84            dx_row: xs[1],
85            dy_row: xs[5],
86        };
87        transform.validate("GeoTIFF ModelTransformationTag")?;
88        Ok(transform)
89    }
90
91    fn validate(self, label: &str) -> Result<()> {
92        let finite = [
93            self.x0,
94            self.y0,
95            self.dx_col,
96            self.dy_col,
97            self.dx_row,
98            self.dy_row,
99        ]
100        .into_iter()
101        .all(f64::is_finite);
102        if !finite || self.det().abs() <= f64::EPSILON {
103            return Err(TrailgenError::InvalidData(format!(
104                "{label} must be finite and invertible"
105            )));
106        }
107        Ok(())
108    }
109
110    fn pixel_xy(self, x: f64, y: f64) -> (f64, f64) {
111        let dx = x - self.x0;
112        let dy = y - self.y0;
113        let det = self.det();
114        (
115            (dx.mul_add(self.dy_row, -self.dx_row * dy)) / det,
116            (self.dx_col.mul_add(dy, -dx * self.dy_col)) / det,
117        )
118    }
119
120    const fn det(self) -> f64 {
121        self.dx_col * self.dy_row - self.dx_row * self.dy_col
122    }
123
124    fn pixel_width(self) -> f64 {
125        self.dx_col.hypot(self.dy_col)
126    }
127
128    fn pixel_height(self) -> f64 {
129        self.dx_row.hypot(self.dy_row)
130    }
131}
132
133#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
134#[serde(rename_all = "kebab-case")]
135pub enum RasterCrs {
136    Wgs84Degrees,
137    Nad83Degrees,
138    WebMercatorMeters,
139    #[serde(alias = "wgs84-utm-meters")]
140    UtmMeters(UtmCrs),
141}
142
143impl RasterCrs {
144    fn xy(self, coord: Coord) -> Option<(f64, f64)> {
145        match self {
146            Self::Wgs84Degrees | Self::Nad83Degrees => Some((coord.lon, coord.lat)),
147            Self::WebMercatorMeters => Some(wgs84_to_web_mercator(coord)),
148            Self::UtmMeters(crs) => geographic_to_utm(coord, crs),
149        }
150    }
151}
152
153const fn default_raster_crs() -> RasterCrs {
154    RasterCrs::Wgs84Degrees
155}
156
157#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
158pub struct VrtDem {
159    pub width: usize,
160    pub height: usize,
161    #[serde(default = "default_raster_crs")]
162    pub crs: RasterCrs,
163    #[serde(alias = "origin_lon")]
164    pub origin_x: f64,
165    #[serde(alias = "origin_lat")]
166    pub origin_y: f64,
167    #[serde(alias = "pixel_width_deg")]
168    pub pixel_width: f64,
169    #[serde(alias = "pixel_height_deg")]
170    pub pixel_height: f64,
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    pub transform: Option<RasterTransform>,
173    #[serde(default, skip_serializing_if = "Option::is_none")]
174    pub nodata_value: Option<f64>,
175    pub source_filename: String,
176    pub confidence: f64,
177    pub provenance: Provenance,
178    values: Vec<f64>,
179}
180
181#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
182#[serde(tag = "kind", rename_all = "kebab-case")]
183pub enum RasterDem {
184    ArcAscii(ArcAsciiGrid),
185    GeoTiff(GeoTiffDem),
186    Vrt(VrtDem),
187}
188
189impl ArcAsciiGrid {
190    pub fn parse(raw: &str, provenance: Provenance, confidence: f64) -> Result<Self> {
191        let mut lines = raw
192            .lines()
193            .map(str::trim)
194            .filter(|line| !line.is_empty())
195            .peekable();
196        let ncols: usize = header(&mut lines, "ncols")?;
197        let nrows: usize = header(&mut lines, "nrows")?;
198        let xllcorner: f64 = header(&mut lines, "xllcorner")?;
199        let yllcorner: f64 = header(&mut lines, "yllcorner")?;
200        let cellsize: f64 = header(&mut lines, "cellsize")?;
201        let nodata_value: f64 = optional_header(&mut lines, "nodata_value")?.unwrap_or(-9999.0);
202        if ncols == 0 || nrows == 0 {
203            return Err(TrailgenError::InvalidData(
204                "raster dimensions must be nonzero".to_owned(),
205            ));
206        }
207        if !xllcorner.is_finite()
208            || !yllcorner.is_finite()
209            || !cellsize.is_finite()
210            || cellsize <= 0.0
211            || !nodata_value.is_finite()
212        {
213            return Err(TrailgenError::InvalidData(
214                "raster georeference headers must be finite and cellsize positive".to_owned(),
215            ));
216        }
217        let values = lines
218            .flat_map(str::split_whitespace)
219            .map(|x| {
220                x.parse::<f64>()
221                    .map_err(|e| TrailgenError::InvalidData(format!("invalid raster cell: {e}")))
222            })
223            .collect::<Result<Vec<_>>>()?;
224        let expected = ncols
225            .checked_mul(nrows)
226            .ok_or_else(|| TrailgenError::InvalidData("raster dimensions overflow".to_owned()))?;
227        if values.len() != expected {
228            return Err(TrailgenError::InvalidData(format!(
229                "raster has {} cells, expected {expected}",
230                values.len()
231            )));
232        }
233        Ok(Self {
234            ncols,
235            nrows,
236            xllcorner,
237            yllcorner,
238            cellsize,
239            nodata_value,
240            confidence: confidence.clamp(0.0, 1.0),
241            provenance,
242            values,
243        })
244    }
245
246    #[must_use]
247    pub fn contains(&self, coord: Coord) -> bool {
248        let xmax = self
249            .cellsize
250            .mul_add(usize_to_f64(self.ncols), self.xllcorner);
251        let ymax = self
252            .cellsize
253            .mul_add(usize_to_f64(self.nrows), self.yllcorner);
254        (self.xllcorner..=xmax).contains(&coord.lon) && (self.yllcorner..=ymax).contains(&coord.lat)
255    }
256
257    fn interpolated_elevation_m(&self, coord: Coord) -> Option<f64> {
258        if !self.contains(coord) {
259            return None;
260        }
261        let col = self.x_center_index(coord.lon);
262        let row = self.y_center_index(coord.lat);
263        let col0 = f64_to_index(col.floor(), self.ncols)?;
264        let col1 = f64_to_index(col.ceil(), self.ncols)?;
265        let row0 = f64_to_index(row.floor(), self.nrows)?;
266        let row1 = f64_to_index(row.ceil(), self.nrows)?;
267        let tx = col - usize_to_f64(col0);
268        let ty = row - usize_to_f64(row0);
269        let z00 = self.cell(row0, col0)?;
270        let z01 = self.cell(row0, col1)?;
271        let z10 = self.cell(row1, col0)?;
272        let z11 = self.cell(row1, col1)?;
273        let top = tx.mul_add(z01 - z00, z00);
274        let bottom = tx.mul_add(z11 - z10, z10);
275        Some(ty.mul_add(bottom - top, top))
276    }
277
278    fn x_center_index(&self, lon: f64) -> f64 {
279        ((lon - self.xllcorner) / self.cellsize - 0.5)
280            .clamp(0.0, usize_to_f64(self.ncols.saturating_sub(1)))
281    }
282
283    fn y_center_index(&self, lat: f64) -> f64 {
284        let ymax = self
285            .cellsize
286            .mul_add(usize_to_f64(self.nrows), self.yllcorner);
287        ((ymax - lat) / self.cellsize - 0.5).clamp(0.0, usize_to_f64(self.nrows.saturating_sub(1)))
288    }
289
290    fn cell(&self, row: usize, col: usize) -> Option<f64> {
291        let value = *self
292            .values
293            .get(row.checked_mul(self.ncols)?.checked_add(col)?)?;
294        if (value - self.nodata_value).abs() <= f64::EPSILON {
295            return None;
296        }
297        Some(value)
298    }
299}
300
301impl GeoTiffDem {
302    pub fn from_path(path: &Path, provenance: Provenance, confidence: f64) -> Result<Self> {
303        let file = File::open(path)
304            .map_err(|error| TrailgenError::InvalidData(format!("open GeoTIFF: {error}")))?;
305        let mut decoder = Decoder::new(file)
306            .map_err(|error| TrailgenError::InvalidData(format!("decode GeoTIFF: {error}")))?;
307        let (width, height) = decoder.dimensions().map_err(|error| {
308            TrailgenError::InvalidData(format!("read GeoTIFF dimensions: {error}"))
309        })?;
310        let width = usize::try_from(width)
311            .map_err(|_| TrailgenError::InvalidData("GeoTIFF width overflow".to_owned()))?;
312        let height = usize::try_from(height)
313            .map_err(|_| TrailgenError::InvalidData("GeoTIFF height overflow".to_owned()))?;
314        let expected = width
315            .checked_mul(height)
316            .ok_or_else(|| TrailgenError::InvalidData("GeoTIFF dimensions overflow".to_owned()))?;
317        let georef = GeoTiffGeoref::read(&mut decoder)?;
318        let nodata_value = optional_ascii(&mut decoder, Tag::GdalNodata)?
319            .and_then(|value| value.trim_matches('\0').trim().parse::<f64>().ok());
320        let image = decoder
321            .read_image()
322            .map_err(|error| TrailgenError::InvalidData(format!("read GeoTIFF pixels: {error}")))?;
323        let values = decoding_result_to_f64(image);
324        if values.len() != expected {
325            return Err(TrailgenError::UnsupportedFormat(format!(
326                "GeoTIFF DEM must be single-band; decoded {} sample(s), expected {expected}",
327                values.len()
328            )));
329        }
330        Ok(Self {
331            width,
332            height,
333            crs: georef.crs,
334            origin_x: georef.origin_x,
335            origin_y: georef.origin_y,
336            pixel_width: georef.pixel_width,
337            pixel_height: georef.pixel_height,
338            transform: georef.transform,
339            nodata_value,
340            confidence: confidence.clamp(0.0, 1.0),
341            provenance,
342            values,
343        })
344    }
345
346    #[must_use]
347    pub fn contains(&self, coord: Coord) -> bool {
348        raster_contains(self.width, self.height, self.crs, self.transform(), coord)
349    }
350
351    fn interpolated_elevation_m(&self, coord: Coord) -> Option<f64> {
352        interpolated_raster_value(
353            self.width,
354            self.height,
355            &self.values,
356            self.nodata_value,
357            self.crs,
358            self.transform(),
359            coord,
360        )
361    }
362
363    fn transform(&self) -> RasterTransform {
364        self.transform.unwrap_or_else(|| {
365            RasterTransform::north_up(
366                self.origin_x,
367                self.origin_y,
368                self.pixel_width,
369                self.pixel_height,
370            )
371        })
372    }
373}
374
375impl VrtDem {
376    pub fn from_path(path: &Path, provenance: Provenance, confidence: f64) -> Result<Self> {
377        let raw = std::fs::read_to_string(path)
378            .map_err(|error| TrailgenError::InvalidData(format!("read VRT: {error}")))?;
379        let spec = VrtSpec::parse(path, &raw)?;
380        let source = GeoTiffDem::from_path(
381            &spec.source_path,
382            Provenance {
383                source: "vrt-source-geotiff".to_owned(),
384                layer: Some("vrt-source".to_owned()),
385                source_id: spec
386                    .source_path
387                    .file_name()
388                    .and_then(|name| name.to_str())
389                    .map(str::to_owned),
390                license: None,
391            },
392            confidence,
393        )?;
394        let crs = spec.crs.unwrap_or(source.crs);
395        if spec.crs.is_some_and(|spec_crs| spec_crs != source.crs) {
396            return Err(TrailgenError::UnsupportedFormat(format!(
397                "VRT SRS {:?} does not match source GeoTIFF CRS {:?}; reproject or materialize a consistent VRT",
398                crs, source.crs
399            )));
400        }
401        if source.width != spec.width || source.height != spec.height {
402            return Err(TrailgenError::UnsupportedFormat(format!(
403                "VRT source raster dimensions {}x{} do not match VRT {}x{}",
404                source.width, source.height, spec.width, spec.height
405            )));
406        }
407        Ok(Self {
408            width: spec.width,
409            height: spec.height,
410            crs,
411            origin_x: spec.transform.x0,
412            origin_y: spec.transform.y0,
413            pixel_width: spec.transform.pixel_width(),
414            pixel_height: spec.transform.pixel_height(),
415            transform: Some(spec.transform),
416            nodata_value: spec.nodata_value.or(source.nodata_value),
417            source_filename: spec.source_path.display().to_string(),
418            confidence: confidence.clamp(0.0, 1.0),
419            provenance,
420            values: source.values,
421        })
422    }
423
424    pub fn referenced_sources(path: &Path) -> Result<Vec<PathBuf>> {
425        let raw = std::fs::read_to_string(path)
426            .map_err(|error| TrailgenError::InvalidData(format!("read VRT: {error}")))?;
427        Ok(vec![VrtSpec::parse(path, &raw)?.source_path])
428    }
429
430    #[must_use]
431    pub fn contains(&self, coord: Coord) -> bool {
432        raster_contains(self.width, self.height, self.crs, self.transform(), coord)
433    }
434
435    fn interpolated_elevation_m(&self, coord: Coord) -> Option<f64> {
436        interpolated_raster_value(
437            self.width,
438            self.height,
439            &self.values,
440            self.nodata_value,
441            self.crs,
442            self.transform(),
443            coord,
444        )
445    }
446
447    fn transform(&self) -> RasterTransform {
448        self.transform.unwrap_or_else(|| {
449            RasterTransform::north_up(
450                self.origin_x,
451                self.origin_y,
452                self.pixel_width,
453                self.pixel_height,
454            )
455        })
456    }
457}
458
459impl ElevationSampler for ArcAsciiGrid {
460    fn sample(&self, coord: Coord) -> Option<ElevationSample> {
461        let value = self.interpolated_elevation_m(coord)?;
462        Some(ElevationSample {
463            ele_m: value,
464            confidence: self.confidence,
465            provenance: self.provenance.clone(),
466        })
467    }
468}
469
470impl ElevationSampler for RasterDem {
471    fn sample(&self, coord: Coord) -> Option<ElevationSample> {
472        match self {
473            Self::ArcAscii(raster) => raster.sample(coord),
474            Self::GeoTiff(raster) => raster.sample(coord),
475            Self::Vrt(raster) => raster.sample(coord),
476        }
477    }
478}
479
480impl ElevationSampler for GeoTiffDem {
481    fn sample(&self, coord: Coord) -> Option<ElevationSample> {
482        let value = self.interpolated_elevation_m(coord)?;
483        Some(ElevationSample {
484            ele_m: value,
485            confidence: self.confidence,
486            provenance: self.provenance.clone(),
487        })
488    }
489}
490
491impl ElevationSampler for VrtDem {
492    fn sample(&self, coord: Coord) -> Option<ElevationSample> {
493        let value = self.interpolated_elevation_m(coord)?;
494        Some(ElevationSample {
495            ele_m: value,
496            confidence: self.confidence,
497            provenance: self.provenance.clone(),
498        })
499    }
500}
501
502struct VrtSpec {
503    width: usize,
504    height: usize,
505    crs: Option<RasterCrs>,
506    transform: RasterTransform,
507    nodata_value: Option<f64>,
508    source_path: PathBuf,
509}
510
511impl VrtSpec {
512    fn parse(path: &Path, raw: &str) -> Result<Self> {
513        let doc = roxmltree::Document::parse(raw)
514            .map_err(|error| TrailgenError::InvalidData(format!("parse VRT XML: {error}")))?;
515        let root = doc.root_element();
516        if root.tag_name().name() != "VRTDataset" {
517            return Err(TrailgenError::UnsupportedFormat(
518                "raster VRT must have <VRTDataset> root".to_owned(),
519            ));
520        }
521        let width = required_attr::<usize>(root, "rasterXSize")?;
522        let height = required_attr::<usize>(root, "rasterYSize")?;
523        if width == 0 || height == 0 {
524            return Err(TrailgenError::InvalidData(
525                "VRT raster dimensions must be nonzero".to_owned(),
526            ));
527        }
528        let geotransform = child_text(root, "GeoTransform")
529            .ok_or_else(|| TrailgenError::InvalidData("VRT missing GeoTransform".to_owned()))?
530            .split(',')
531            .map(str::trim)
532            .map(str::parse::<f64>)
533            .collect::<std::result::Result<Vec<_>, _>>()
534            .map_err(|error| {
535                TrailgenError::InvalidData(format!("invalid VRT GeoTransform: {error}"))
536            })?;
537        if geotransform.len() != 6 {
538            return Err(TrailgenError::InvalidData(
539                "VRT GeoTransform must contain six numbers".to_owned(),
540            ));
541        }
542        let transform = RasterTransform {
543            x0: geotransform[0],
544            y0: geotransform[3],
545            dx_col: geotransform[1],
546            dy_col: geotransform[4],
547            dx_row: geotransform[2],
548            dy_row: geotransform[5],
549        };
550        transform.validate("VRT GeoTransform")?;
551        let crs = child_text(root, "SRS").map(parse_vrt_srs).transpose()?;
552        let band = root
553            .children()
554            .find(|node| node.has_tag_name("VRTRasterBand"))
555            .ok_or_else(|| TrailgenError::InvalidData("VRT missing VRTRasterBand".to_owned()))?;
556        let simple = band
557            .children()
558            .find(|node| node.has_tag_name("SimpleSource"))
559            .ok_or_else(|| {
560                TrailgenError::UnsupportedFormat("VRT DEM requires SimpleSource".to_owned())
561            })?;
562        ensure_identity_rect(simple, "SrcRect", width, height)?;
563        ensure_identity_rect(simple, "DstRect", width, height)?;
564        let source_filename = simple
565            .children()
566            .find(|node| node.has_tag_name("SourceFilename"))
567            .ok_or_else(|| {
568                TrailgenError::InvalidData("VRT SimpleSource missing SourceFilename".to_owned())
569            })?;
570        let source_text = source_filename
571            .text()
572            .map(str::trim)
573            .filter(|text| !text.is_empty())
574            .ok_or_else(|| TrailgenError::InvalidData("VRT SourceFilename is empty".to_owned()))?;
575        let source_path = resolve_vrt_source(path, source_filename, source_text);
576        let source_band = child_text(simple, "SourceBand").unwrap_or("1").trim();
577        if source_band != "1" {
578            return Err(TrailgenError::UnsupportedFormat(
579                "VRT DEM only supports SourceBand 1".to_owned(),
580            ));
581        }
582        let nodata_value = child_text(band, "NoDataValue")
583            .or_else(|| child_text(root, "NoDataValue"))
584            .map(str::trim)
585            .map(str::parse::<f64>)
586            .transpose()
587            .map_err(|error| {
588                TrailgenError::InvalidData(format!("invalid VRT NoDataValue: {error}"))
589            })?;
590        Ok(Self {
591            width,
592            height,
593            crs,
594            transform,
595            nodata_value,
596            source_path,
597        })
598    }
599}
600
601fn raster_contains(
602    width: usize,
603    height: usize,
604    crs: RasterCrs,
605    transform: RasterTransform,
606    coord: Coord,
607) -> bool {
608    let Some((x, y)) = crs.xy(coord) else {
609        return false;
610    };
611    let (col, row) = transform.pixel_xy(x, y);
612    (0.0..=usize_to_f64(width)).contains(&col) && (0.0..=usize_to_f64(height)).contains(&row)
613}
614
615fn interpolated_raster_value(
616    width: usize,
617    height: usize,
618    values: &[f64],
619    nodata_value: Option<f64>,
620    crs: RasterCrs,
621    transform: RasterTransform,
622    coord: Coord,
623) -> Option<f64> {
624    if !raster_contains(width, height, crs, transform, coord) {
625        return None;
626    }
627    let (x, y) = crs.xy(coord)?;
628    let (col_px, row_px) = transform.pixel_xy(x, y);
629    let col = (col_px - 0.5).clamp(0.0, usize_to_f64(width.saturating_sub(1)));
630    let row = (row_px - 0.5).clamp(0.0, usize_to_f64(height.saturating_sub(1)));
631    let col0 = f64_to_index(col.floor(), width)?;
632    let col1 = f64_to_index(col.ceil(), width)?;
633    let row0 = f64_to_index(row.floor(), height)?;
634    let row1 = f64_to_index(row.ceil(), height)?;
635    let tx = col - usize_to_f64(col0);
636    let ty = row - usize_to_f64(row0);
637    let z00 = raster_cell(width, values, nodata_value, row0, col0)?;
638    let z01 = raster_cell(width, values, nodata_value, row0, col1)?;
639    let z10 = raster_cell(width, values, nodata_value, row1, col0)?;
640    let z11 = raster_cell(width, values, nodata_value, row1, col1)?;
641    let top = tx.mul_add(z01 - z00, z00);
642    let bottom = tx.mul_add(z11 - z10, z10);
643    Some(ty.mul_add(bottom - top, top))
644}
645
646fn raster_cell(
647    width: usize,
648    values: &[f64],
649    nodata_value: Option<f64>,
650    row: usize,
651    col: usize,
652) -> Option<f64> {
653    let value = *values.get(row.checked_mul(width)?.checked_add(col)?)?;
654    if nodata_value.is_some_and(|nodata| (value - nodata).abs() <= f64::EPSILON) {
655        return None;
656    }
657    value.is_finite().then_some(value)
658}
659
660fn parse_vrt_srs(srs: &str) -> Result<RasterCrs> {
661    let normalized: String = srs
662        .chars()
663        .filter(char::is_ascii_alphanumeric)
664        .flat_map(char::to_uppercase)
665        .collect();
666    if normalized.contains("EPSG3857")
667        || normalized.contains("EPSG900913")
668        || normalized.contains("WEBMERCATOR")
669        || normalized.contains("PSEUDOMERCATOR")
670    {
671        Ok(RasterCrs::WebMercatorMeters)
672    } else if let Some(crs) = UtmCrs::from_normalized_srs(&normalized) {
673        Ok(RasterCrs::UtmMeters(crs))
674    } else if normalized.contains("PROJCS")
675        || normalized.contains("PROJCRS")
676        || normalized.contains("PROJECTION")
677    {
678        Err(TrailgenError::UnsupportedFormat(
679            "projected VRT SRS must be EPSG:3857 Web Mercator or WGS84/NAD83 UTM (EPSG:326xx/327xx/269xx), or reprojected before ingestion"
680                .to_owned(),
681        ))
682    } else if normalized.contains("EPSG4326")
683        || normalized.contains("OGC13CRS84")
684        || normalized.contains("OGC14CRS84")
685        || normalized.contains("CRS84")
686        || normalized.contains("WGS84")
687        || normalized.contains("WGS1984")
688    {
689        Ok(RasterCrs::Wgs84Degrees)
690    } else if normalized.contains("EPSG4269")
691        || normalized.contains("NAD83")
692        || normalized.contains("NORTHAMERICANDATUM1983")
693    {
694        Ok(RasterCrs::Nad83Degrees)
695    } else {
696        Err(TrailgenError::UnsupportedFormat(
697            "VRT SRS must declare WGS84/NAD83/CRS84 geographic, EPSG:3857 Web Mercator, or WGS84/NAD83 UTM (EPSG:326xx/327xx/269xx)"
698                .to_owned(),
699        ))
700    }
701}
702
703fn required_attr<T>(node: roxmltree::Node<'_, '_>, key: &str) -> Result<T>
704where
705    T::Err: std::fmt::Display,
706    T: FromStr,
707{
708    node.attribute(key)
709        .ok_or_else(|| TrailgenError::InvalidData(format!("VRT missing {key} attribute")))?
710        .parse()
711        .map_err(|error| TrailgenError::InvalidData(format!("invalid VRT {key}: {error}")))
712}
713
714fn child_text<'a>(node: roxmltree::Node<'a, 'a>, tag: &str) -> Option<&'a str> {
715    node.children()
716        .find(|child| child.has_tag_name(tag))
717        .and_then(|child| child.text())
718}
719
720fn ensure_identity_rect(
721    simple: roxmltree::Node<'_, '_>,
722    tag: &str,
723    width: usize,
724    height: usize,
725) -> Result<()> {
726    let Some(rect) = simple.children().find(|node| node.has_tag_name(tag)) else {
727        return Ok(());
728    };
729    let x_off = required_attr::<usize>(rect, "xOff")?;
730    let y_off = required_attr::<usize>(rect, "yOff")?;
731    let x_size = required_attr::<usize>(rect, "xSize")?;
732    let y_size = required_attr::<usize>(rect, "ySize")?;
733    if x_off == 0 && y_off == 0 && x_size == width && y_size == height {
734        Ok(())
735    } else {
736        Err(TrailgenError::UnsupportedFormat(format!(
737            "VRT DEM only supports full-raster identity {tag}"
738        )))
739    }
740}
741
742fn resolve_vrt_source(
743    vrt_path: &Path,
744    source_filename: roxmltree::Node<'_, '_>,
745    source_text: &str,
746) -> PathBuf {
747    let path = PathBuf::from(source_text);
748    if path.is_absolute() {
749        return path;
750    }
751    let relative = source_filename
752        .attribute("relativeToVRT")
753        .is_some_and(|value| matches!(value, "1" | "true" | "TRUE" | "True"));
754    if relative || vrt_path.parent().is_some() {
755        vrt_path
756            .parent()
757            .unwrap_or_else(|| Path::new("."))
758            .join(path)
759    } else {
760        path
761    }
762}
763
764#[derive(Clone, Copy)]
765struct GeoTiffGeoref {
766    crs: RasterCrs,
767    origin_x: f64,
768    origin_y: f64,
769    pixel_width: f64,
770    pixel_height: f64,
771    transform: Option<RasterTransform>,
772}
773
774impl GeoTiffGeoref {
775    fn read<R: std::io::Read + std::io::Seek>(decoder: &mut Decoder<R>) -> Result<Self> {
776        let crs = validate_geokeys(decoder)?;
777        if let Some(xs) = optional_f64_vec(decoder, Tag::ModelTransformationTag)? {
778            let transform = RasterTransform::from_model_transformation(&xs)?;
779            return Ok(Self {
780                crs,
781                origin_x: transform.x0,
782                origin_y: transform.y0,
783                pixel_width: transform.pixel_width(),
784                pixel_height: transform.pixel_height(),
785                transform: Some(transform),
786            });
787        }
788        let scale = required_f64_vec(decoder, Tag::ModelPixelScaleTag)?;
789        let tiepoint = required_f64_vec(decoder, Tag::ModelTiepointTag)?;
790        if scale.len() < 2 || tiepoint.len() < 6 {
791            return Err(TrailgenError::InvalidData(
792                "GeoTIFF DEM requires ModelPixelScaleTag[0..2] and ModelTiepointTag[0..6]"
793                    .to_owned(),
794            ));
795        }
796        let pixel_width = scale[0].abs();
797        let pixel_height = scale[1].abs();
798        if !pixel_width.is_finite()
799            || !pixel_height.is_finite()
800            || pixel_width <= 0.0
801            || pixel_height <= 0.0
802        {
803            return Err(TrailgenError::InvalidData(
804                "GeoTIFF DEM pixel scale must be finite and positive".to_owned(),
805            ));
806        }
807        let origin_x = tiepoint[0].mul_add(-pixel_width, tiepoint[3]);
808        let origin_y = tiepoint[1].mul_add(pixel_height, tiepoint[4]);
809        if !origin_x.is_finite() || !origin_y.is_finite() {
810            return Err(TrailgenError::InvalidData(
811                "GeoTIFF DEM tiepoint georeference must be finite".to_owned(),
812            ));
813        }
814        Ok(Self {
815            crs,
816            origin_x,
817            origin_y,
818            pixel_width,
819            pixel_height,
820            transform: None,
821        })
822    }
823}
824
825fn validate_geokeys<R: std::io::Read + std::io::Seek>(
826    decoder: &mut Decoder<R>,
827) -> Result<RasterCrs> {
828    let Some(keys) = optional_u16_vec(decoder, Tag::GeoKeyDirectoryTag)? else {
829        return Err(TrailgenError::InvalidData(
830            "GeoTIFF DEM requires GeoKeyDirectoryTag declaring WGS84/NAD83 geographic, EPSG:3857 Web Mercator, or WGS84/NAD83 UTM (EPSG:326xx/327xx/269xx)"
831                .to_owned(),
832        ));
833    };
834    if keys.len() < 4 {
835        return Err(TrailgenError::InvalidData(
836            "GeoTIFF GeoKeyDirectoryTag header is truncated".to_owned(),
837        ));
838    }
839    let count = usize::from(keys[3]);
840    if keys.len() < 4 + count.saturating_mul(4) {
841        return Err(TrailgenError::InvalidData(
842            "GeoTIFF GeoKeyDirectoryTag entries are truncated".to_owned(),
843        ));
844    }
845    let mut model_type = None;
846    let mut angular_units = None;
847    let mut geographic_crs = None;
848    let mut projected_crs = None;
849    let mut linear_units = None;
850    for entry in keys[4..][..count * 4].chunks_exact(4) {
851        match entry[0] {
852            1024 if entry[1] == 0 => model_type = Some(entry[3]),
853            2048 if entry[1] == 0 => geographic_crs = Some(entry[3]),
854            2054 if entry[1] == 0 => angular_units = Some(entry[3]),
855            3072 if entry[1] == 0 => projected_crs = Some(entry[3]),
856            3076 if entry[1] == 0 => linear_units = Some(entry[3]),
857            _ => {}
858        }
859    }
860    if model_type == Some(1) {
861        if let Some(crs) = projected_crs.and_then(projected_raster_crs) {
862            if linear_units.is_some_and(|unit| unit != 9001) {
863                return Err(TrailgenError::UnsupportedFormat(
864                    "projected GeoTIFF DEM linear units must be metres".to_owned(),
865                ));
866            }
867            return Ok(crs);
868        }
869        return Err(TrailgenError::UnsupportedFormat(
870            "projected GeoTIFF DEM must be EPSG:3857 Web Mercator or WGS84/NAD83 UTM (EPSG:326xx/327xx/269xx), or reprojected before ingestion"
871                .to_owned(),
872        ));
873    }
874    if model_type == Some(2) {
875        if angular_units.is_some_and(|unit| unit != 9102) {
876            return Err(TrailgenError::UnsupportedFormat(
877                "GeoTIFF DEM angular units must be degrees".to_owned(),
878            ));
879        }
880        return geographic_crs.map_or(Ok(RasterCrs::Wgs84Degrees), geographic_raster_crs);
881    }
882    Err(TrailgenError::UnsupportedFormat(
883        "GeoTIFF DEM must declare GTModelTypeGeoKey=geographic, EPSG:3857 projected, or WGS84/NAD83 UTM projected".to_owned(),
884    ))
885}
886
887fn geographic_raster_crs(epsg: u16) -> Result<RasterCrs> {
888    match epsg {
889        4326 => Ok(RasterCrs::Wgs84Degrees),
890        4269 => Ok(RasterCrs::Nad83Degrees),
891        _ => Err(TrailgenError::UnsupportedFormat(
892            "GeoTIFF DEM geographic CRS must be WGS84 EPSG:4326 or NAD83 EPSG:4269".to_owned(),
893        )),
894    }
895}
896
897fn projected_raster_crs(epsg: u16) -> Option<RasterCrs> {
898    match epsg {
899        3857 => Some(RasterCrs::WebMercatorMeters),
900        32601..=32760 | 26901..=26923 => Some(RasterCrs::UtmMeters(UtmCrs::from_epsg(epsg)?)),
901        _ => None,
902    }
903}
904
905fn decoding_result_to_f64(image: DecodingResult) -> Vec<f64> {
906    match image {
907        DecodingResult::U8(xs) => xs.into_iter().map(f64::from).collect(),
908        DecodingResult::U16(xs) => xs.into_iter().map(f64::from).collect(),
909        DecodingResult::U32(xs) => xs.into_iter().map(f64::from).collect(),
910        DecodingResult::U64(xs) => xs.into_iter().map(|x| numeric_to_f64(&x)).collect(),
911        DecodingResult::F16(xs) => xs.into_iter().map(|x| f64::from(x.to_f32())).collect(),
912        DecodingResult::F32(xs) => xs.into_iter().map(f64::from).collect(),
913        DecodingResult::F64(xs) => xs,
914        DecodingResult::I8(xs) => xs.into_iter().map(f64::from).collect(),
915        DecodingResult::I16(xs) => xs.into_iter().map(f64::from).collect(),
916        DecodingResult::I32(xs) => xs.into_iter().map(f64::from).collect(),
917        DecodingResult::I64(xs) => xs.into_iter().map(|x| numeric_to_f64(&x)).collect(),
918    }
919}
920
921fn required_f64_vec<R: std::io::Read + std::io::Seek>(
922    decoder: &mut Decoder<R>,
923    tag: Tag,
924) -> Result<Vec<f64>> {
925    optional_f64_vec(decoder, tag)?
926        .ok_or_else(|| TrailgenError::InvalidData(format!("GeoTIFF DEM missing {tag:?}")))
927}
928
929fn optional_f64_vec<R: std::io::Read + std::io::Seek>(
930    decoder: &mut Decoder<R>,
931    tag: Tag,
932) -> Result<Option<Vec<f64>>> {
933    match decoder.get_tag(tag) {
934        Ok(value) => value
935            .into_f64_vec()
936            .map(Some)
937            .map_err(|error| TrailgenError::InvalidData(format!("read GeoTIFF {tag:?}: {error}"))),
938        Err(error) if missing_tag(&error, tag) => Ok(None),
939        Err(error) => Err(TrailgenError::InvalidData(format!(
940            "read GeoTIFF {tag:?}: {error}"
941        ))),
942    }
943}
944
945fn optional_u16_vec<R: std::io::Read + std::io::Seek>(
946    decoder: &mut Decoder<R>,
947    tag: Tag,
948) -> Result<Option<Vec<u16>>> {
949    match decoder.get_tag(tag) {
950        Ok(value) => value
951            .into_u16_vec()
952            .map(Some)
953            .map_err(|error| TrailgenError::InvalidData(format!("read GeoTIFF {tag:?}: {error}"))),
954        Err(error) if missing_tag(&error, tag) => Ok(None),
955        Err(error) => Err(TrailgenError::InvalidData(format!(
956            "read GeoTIFF {tag:?}: {error}"
957        ))),
958    }
959}
960
961fn optional_ascii<R: std::io::Read + std::io::Seek>(
962    decoder: &mut Decoder<R>,
963    tag: Tag,
964) -> Result<Option<String>> {
965    match decoder.get_tag_ascii_string(tag) {
966        Ok(value) => Ok(Some(value)),
967        Err(error) if missing_tag(&error, tag) => Ok(None),
968        Err(error) => Err(TrailgenError::InvalidData(format!(
969            "read GeoTIFF {tag:?}: {error}"
970        ))),
971    }
972}
973
974fn missing_tag(error: &TiffError, tag: Tag) -> bool {
975    matches!(
976        error,
977        TiffError::FormatError(TiffFormatError::RequiredTagNotFound(missing)) if *missing == tag
978    )
979}
980
981fn header<'a, I, T>(lines: &mut I, key: &str) -> Result<T>
982where
983    I: Iterator<Item = &'a str>,
984    T::Err: std::fmt::Display,
985    T: FromStr,
986{
987    let line = lines
988        .next()
989        .ok_or_else(|| TrailgenError::InvalidData(format!("raster missing {key} header")))?;
990    let (actual, value) = split_header(line)?;
991    if !actual.eq_ignore_ascii_case(key) {
992        return Err(TrailgenError::InvalidData(format!(
993            "raster expected {key} header, found {actual}"
994        )));
995    }
996    value
997        .parse()
998        .map_err(|e| TrailgenError::InvalidData(format!("invalid raster {key}: {e}")))
999}
1000
1001fn optional_header<'a, I, T>(lines: &mut Peekable<I>, key: &str) -> Result<Option<T>>
1002where
1003    I: Iterator<Item = &'a str>,
1004    T::Err: std::fmt::Display,
1005    T: FromStr,
1006{
1007    let Some(line) = lines.peek() else {
1008        return Ok(None);
1009    };
1010    let (actual, _) = split_header(line)?;
1011    if !actual.eq_ignore_ascii_case(key) {
1012        return Ok(None);
1013    }
1014    let Some(line) = lines.next() else {
1015        return Ok(None);
1016    };
1017    let (_, value) = split_header(line)?;
1018    value
1019        .parse()
1020        .map(Some)
1021        .map_err(|e| TrailgenError::InvalidData(format!("invalid raster {key}: {e}")))
1022}
1023
1024fn split_header(line: &str) -> Result<(&str, &str)> {
1025    let mut parts = line.split_whitespace();
1026    let key = parts
1027        .next()
1028        .ok_or_else(|| TrailgenError::InvalidData("empty raster header".to_owned()))?;
1029    let value = parts
1030        .next()
1031        .ok_or_else(|| TrailgenError::InvalidData(format!("raster header {key} has no value")))?;
1032    Ok((key, value))
1033}
1034
1035fn f64_to_index(value: f64, upper: usize) -> Option<usize> {
1036    if value.is_nan() || value < 0.0 || upper == 0 {
1037        return None;
1038    }
1039    let floored = value.floor().min(usize_to_f64(upper.saturating_sub(1)));
1040    floored.to_usize()
1041}
1042
1043fn usize_to_f64(value: usize) -> f64 {
1044    value.to_f64().unwrap_or(f64::INFINITY)
1045}
1046
1047fn numeric_to_f64<T: ToPrimitive + ?Sized>(value: &T) -> f64 {
1048    value.to_f64().unwrap_or(f64::NAN)
1049}