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}