1use crate::{Error, Result, WindField};
12use nalgebra::Vector3;
13
14const EARTH_RADIUS_M: f64 = 6_371_000.0;
16
17fn horizontal(speed_mps: f64, direction_from_deg: f64) -> Vector3<f64> {
20 let toward = (direction_from_deg + 180.0).to_radians();
21 Vector3::new(speed_mps * toward.cos(), 0.0, speed_mps * toward.sin())
22}
23
24#[derive(Debug, Clone, Copy, PartialEq)]
26pub struct Monitor {
27 pub x_m: f64,
29 pub z_m: f64,
31 pub speed_mps: f64,
33 pub direction_from_deg: f64,
36}
37
38impl Monitor {
39 pub fn at(x_m: f64, z_m: f64, speed_mps: f64, direction_from_deg: f64) -> Self {
41 Self {
42 x_m,
43 z_m,
44 speed_mps,
45 direction_from_deg,
46 }
47 }
48
49 pub fn from_geo(
54 lat_deg: f64,
55 lon_deg: f64,
56 speed_mps: f64,
57 dir_compass_from_deg: f64,
58 ref_lat_deg: f64,
59 ref_lon_deg: f64,
60 north_azimuth_deg: f64,
61 ) -> Self {
62 let (x_m, z_m) = local_en_m(lat_deg, lon_deg, ref_lat_deg, ref_lon_deg);
63 Self {
64 x_m,
65 z_m,
66 speed_mps,
67 direction_from_deg: (dir_compass_from_deg - north_azimuth_deg).rem_euclid(360.0),
68 }
69 }
70}
71
72pub fn local_en_m(lat_deg: f64, lon_deg: f64, ref_lat_deg: f64, ref_lon_deg: f64) -> (f64, f64) {
76 let dlat = (lat_deg - ref_lat_deg).to_radians();
77 let dlon = (lon_deg - ref_lon_deg).to_radians();
78 let east = EARTH_RADIUS_M * dlon * ref_lat_deg.to_radians().cos();
79 let north = EARTH_RADIUS_M * dlat;
80 (east, north)
81}
82
83pub fn monitors_from_geo_csv(
88 text: &str,
89 ref_lat_deg: f64,
90 ref_lon_deg: f64,
91 north_azimuth_deg: f64,
92) -> Result<Vec<Monitor>> {
93 let mut monitors = Vec::new();
94 for (number, line) in text.lines().enumerate() {
95 let line = line.trim();
96 if line.is_empty() || line.starts_with('#') {
97 continue;
98 }
99 let fields: Vec<&str> = line.split(',').collect();
100 if fields.len() < 4 {
101 return Err(Error::invalid(
102 "monitor csv",
103 format!(
104 "line {}: expected lat,lon,speed_ms,dir_compass_deg",
105 number + 1
106 ),
107 ));
108 }
109 let num = |token: &str, what: &str| -> Result<f64> {
110 token.trim().parse().map_err(|_| {
111 Error::invalid(
112 "monitor csv",
113 format!("line {}: {what} '{token}' is not numeric", number + 1),
114 )
115 })
116 };
117 let (lat, lon) = (num(fields[0], "lat")?, num(fields[1], "lon")?);
118 let speed = num(fields[2], "speed")?;
119 let dir = num(fields[3], "direction")?;
120 if !lat.is_finite()
121 || lat.abs() > 90.0
122 || !lon.is_finite()
123 || !speed.is_finite()
124 || speed < 0.0
125 || !dir.is_finite()
126 {
127 return Err(Error::invalid(
128 "monitor csv",
129 format!(
130 "line {}: readings must be finite with nonnegative speed",
131 number + 1
132 ),
133 ));
134 }
135 monitors.push(Monitor::from_geo(
136 lat,
137 lon,
138 speed,
139 dir,
140 ref_lat_deg,
141 ref_lon_deg,
142 north_azimuth_deg,
143 ));
144 }
145 Ok(monitors)
146}
147
148#[derive(Debug, Clone)]
152pub struct HeightGrid {
153 pub origin_x: f64,
155 pub origin_y: f64,
157 pub dx: f64,
159 pub dy: f64,
161 pub ncols: usize,
163 pub nrows: usize,
165 pub heights: Vec<f64>,
167}
168
169impl HeightGrid {
170 pub fn new(
172 origin_x: f64,
173 origin_y: f64,
174 dx: f64,
175 dy: f64,
176 ncols: usize,
177 nrows: usize,
178 heights: Vec<f64>,
179 ) -> Result<Self> {
180 if dx <= 0.0 || dy <= 0.0 || !dx.is_finite() || !dy.is_finite() {
181 return Err(Error::invalid("height grid", "spacing must be positive"));
182 }
183 if ncols < 2 || nrows < 2 {
184 return Err(Error::invalid("height grid", "needs at least 2x2 cells"));
185 }
186 if heights.len() != ncols * nrows {
187 return Err(Error::invalid(
188 "height grid",
189 "height count does not match the grid shape",
190 ));
191 }
192 if !heights.iter().all(|h| h.is_finite()) {
193 return Err(Error::invalid("height grid", "heights must be finite"));
194 }
195 Ok(Self {
196 origin_x,
197 origin_y,
198 dx,
199 dy,
200 ncols,
201 nrows,
202 heights,
203 })
204 }
205
206 pub fn height_at(&self, x: f64, z: f64) -> f64 {
209 let fx = ((x - self.origin_x) / self.dx).clamp(0.0, self.ncols as f64 - 1.0);
210 let fy = ((z - self.origin_y) / self.dy).clamp(0.0, self.nrows as f64 - 1.0);
211 let (j, i) = (fx.floor() as usize, fy.floor() as usize);
212 let (rx, ry) = (fx - j as f64, fy - i as f64);
213 let j1 = (j + 1).min(self.ncols - 1);
214 let i1 = (i + 1).min(self.nrows - 1);
215 let f = |i: usize, j: usize| self.heights[i * self.ncols + j];
216 (1.0 - ry) * ((1.0 - rx) * f(i, j) + rx * f(i, j1))
217 + ry * ((1.0 - rx) * f(i1, j) + rx * f(i1, j1))
218 }
219
220 pub fn from_ascii(text: &str) -> Result<Self> {
224 let mut ncols = None;
225 let mut nrows = None;
226 let mut xll = None;
227 let mut yll = None;
228 let mut cellsize = None;
229 let mut heights = Vec::new();
230 for line in text.lines() {
231 let line = line.trim();
232 if line.is_empty() {
233 continue;
234 }
235 let mut parts = line.split_whitespace();
236 match parts.next().unwrap_or("").to_ascii_lowercase().as_str() {
237 "ncols" => ncols = parse_asc_num(parts.next()),
238 "nrows" => nrows = parse_asc_num(parts.next()),
239 "xllcorner" | "xllcenter" => xll = parse_asc_num(parts.next()),
240 "yllcorner" | "yllcenter" => yll = parse_asc_num(parts.next()),
241 "cellsize" => cellsize = parse_asc_num(parts.next()),
242 "nodata_value" => continue,
243 _ => {
244 for token in line.split_whitespace() {
245 if let Ok(v) = token.parse::<f64>() {
246 heights.push(v);
247 }
248 }
249 }
250 }
251 }
252 let ncols = ncols.ok_or_else(|| Error::invalid("ascii dem", "missing ncols"))? as usize;
253 let nrows = nrows.ok_or_else(|| Error::invalid("ascii dem", "missing nrows"))? as usize;
254 let cellsize = cellsize.ok_or_else(|| Error::invalid("ascii dem", "missing cellsize"))?;
255 let (xll, yll) = (
256 xll.ok_or_else(|| Error::invalid("ascii dem", "missing xllcorner"))?,
257 yll.ok_or_else(|| Error::invalid("ascii dem", "missing yllcorner"))?,
258 );
259 if heights.len() != ncols * nrows {
260 return Err(Error::invalid(
261 "ascii dem",
262 "height count does not match header",
263 ));
264 }
265 let north_y = yll + cellsize * nrows as f64;
273 Self::new(xll, north_y, cellsize, cellsize, ncols, nrows, heights)
274 }
275
276 pub fn from_north_up(
279 origin_x: f64,
280 origin_north_y: f64,
281 dx: f64,
282 dy: f64,
283 ncols: usize,
284 nrows: usize,
285 heights: Vec<f64>,
286 ) -> Result<Self> {
287 Self::new(origin_x, origin_north_y, dx, dy, ncols, nrows, heights)
288 }
289}
290
291fn parse_asc_num(token: Option<&str>) -> Option<f64> {
292 token.and_then(|s| s.parse().ok())
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq)]
300pub enum SrtmResolution {
301 OneArcSecond,
303 ThreeArcSeconds,
305}
306
307impl SrtmResolution {
308 pub fn samples(self) -> usize {
310 match self {
311 Self::OneArcSecond => 3601,
312 Self::ThreeArcSeconds => 1201,
313 }
314 }
315}
316
317pub fn hgt_grid(
324 data: &[u8],
325 resolution: SrtmResolution,
326 north_lat: f64,
327 west_lon: f64,
328 ref_lat: f64,
329 ref_lon: f64,
330) -> Result<HeightGrid> {
331 let samples = resolution.samples();
332 let expected = samples * samples * 2;
333 if data.len() != expected {
334 return Err(Error::invalid(
335 "hgt",
336 format!("expected {expected} bytes, got {}", data.len()),
337 ));
338 }
339 let mut heights = Vec::with_capacity(samples * samples);
340 for chunk in data.as_chunks::<2>().0 {
341 heights.push(f64::from(i16::from_be_bytes(*chunk)));
342 }
343 let nw = local_en_m(north_lat, west_lon, north_lat, west_lon); let se = local_en_m(north_lat - 1.0, west_lon + 1.0, north_lat, west_lon);
350 let dx = (se.0 - nw.0) / (samples - 1) as f64;
351 let north_spacing = (nw.1 - se.1) / (samples - 1) as f64;
352 let (ref_x, ref_z) = local_en_m(ref_lat, ref_lon, north_lat, west_lon);
353 HeightGrid::new(
354 nw.0 - ref_x,
355 nw.1 - ref_z,
356 dx,
357 north_spacing,
358 samples,
359 samples,
360 heights,
361 )
362}
363
364#[derive(Debug, Clone, Copy, PartialEq)]
366pub struct GeoTransform {
367 pub origin_x: f64,
369 pub origin_y: f64,
370 pub pixel_scale_x: f64,
372 pub pixel_scale_y: f64,
374}
375
376#[cfg(feature = "tiff")]
382pub fn geotransform<R: std::io::Read + std::io::Seek>(
383 decoder: &mut tiff::decoder::Decoder<R>,
384) -> Result<GeoTransform> {
385 if let Ok(transform) = decoder.get_tag_f64_vec(tiff::tags::Tag::ModelTransformationTag) {
387 if transform.len() != 16 {
388 return Err(Error::invalid(
389 "geotiff",
390 "model transform must be 16 values",
391 ));
392 }
393 let is_axis_aligned = [
394 transform[1],
395 transform[2],
396 transform[4],
397 transform[6],
398 transform[8],
399 transform[9],
400 ]
401 .iter()
402 .all(|v| v.abs() < 1e-12);
403 if !is_axis_aligned {
404 return Err(Error::invalid(
405 "geotiff",
406 "rotated model transform is not supported by a HeightGrid",
407 ));
408 }
409 let (sx, sy) = (transform[0], transform[5]);
410 return Ok(GeoTransform {
417 origin_x: transform[3],
418 origin_y: transform[7],
419 pixel_scale_x: sx,
420 pixel_scale_y: -sy,
421 });
422 }
423
424 let tiepoint = decoder
425 .get_tag_f64_vec(tiff::tags::Tag::ModelTiepointTag)
426 .map_err(|_| Error::invalid("geotiff", "missing ModelTiepointTag"))?;
427 let scale = decoder
428 .get_tag_f64_vec(tiff::tags::Tag::ModelPixelScaleTag)
429 .map_err(|_| Error::invalid("geotiff", "missing ModelPixelScaleTag"))?;
430 if scale.len() != 3 {
431 return Err(Error::invalid("geotiff", "pixel scale must be 3 values"));
432 }
433 let (sx, sy) = (scale[0], scale[1]);
434 if sx <= 0.0 {
435 return Err(Error::invalid("geotiff", "pixel scale x must be positive"));
436 }
437 if sy.abs() <= 0.0 {
438 return Err(Error::invalid("geotiff", "pixel scale y must be nonzero"));
439 }
440 let tie_raster_x = tiepoint.first().copied().unwrap_or(0.0);
446 let tie_raster_y = tiepoint.get(1).copied().unwrap_or(0.0);
447 let tie_model_x = tiepoint
448 .get(3)
449 .copied()
450 .ok_or_else(|| Error::invalid("geotiff", "tiepoint must have 6 values (I J K X Y Z)"))?;
451 let tie_model_y = *tiepoint
452 .get(4)
453 .ok_or_else(|| Error::invalid("geotiff", "tiepoint must have 6 values (I J K X Y Z)"))?;
454 let origin_x = tie_model_x - tie_raster_x * sx;
455 let origin_y = tie_model_y + tie_raster_y * sy; Ok(GeoTransform {
457 origin_x,
458 origin_y,
459 pixel_scale_x: sx,
460 pixel_scale_y: -sy, })
462}
463
464#[cfg(feature = "tiff")]
471pub fn geotiff_grid(data: &[u8]) -> Result<HeightGrid> {
472 let mut decoder = tiff::decoder::Decoder::new(std::io::Cursor::new(data))
473 .map_err(|e| Error::invalid("geotiff", format!("cannot open: {e}")))?;
474 let (width, height) = decoder
475 .dimensions()
476 .map_err(|e| Error::invalid("geotiff", format!("cannot read dimensions: {e}")))?;
477 let (width, height) = (width as usize, height as usize);
478 let values = match decoder
479 .read_image()
480 .map_err(|e| Error::invalid("geotiff", format!("cannot decode: {e}")))?
481 {
482 tiff::decoder::DecodingResult::F32(data) => data.into_iter().map(f64::from).collect(),
483 tiff::decoder::DecodingResult::F64(data) => data,
484 tiff::decoder::DecodingResult::U8(data) => data.into_iter().map(f64::from).collect(),
485 tiff::decoder::DecodingResult::U16(data) => data.into_iter().map(f64::from).collect(),
486 tiff::decoder::DecodingResult::U32(data) => data.into_iter().map(f64::from).collect(),
487 tiff::decoder::DecodingResult::I8(data) => data.into_iter().map(f64::from).collect(),
488 tiff::decoder::DecodingResult::I16(data) => data.into_iter().map(f64::from).collect(),
489 tiff::decoder::DecodingResult::I32(data) => data.into_iter().map(f64::from).collect(),
490 tiff::decoder::DecodingResult::U64(data) => data.into_iter().map(|v| v as f64).collect(),
491 tiff::decoder::DecodingResult::I64(data) => data.into_iter().map(|v| v as f64).collect(),
492 _ => {
493 return Err(Error::invalid(
494 "geotiff",
495 "unsupported sample format (expected integer or float samples)",
496 ))
497 }
498 };
499 if values.len() != width * height {
500 return Err(Error::invalid(
501 "geotiff",
502 "decoded size does not match header",
503 ));
504 }
505 let mut north_up = Vec::with_capacity(values.len());
507 for row in values.chunks_exact(width).rev() {
508 north_up.extend_from_slice(row);
509 }
510 let geot = geotransform(&mut decoder).unwrap_or(GeoTransform {
513 origin_x: 0.0,
514 origin_y: 0.0,
515 pixel_scale_x: 1.0,
516 pixel_scale_y: 1.0,
517 });
518 HeightGrid::new(
519 geot.origin_x,
520 geot.origin_y,
521 geot.pixel_scale_x,
522 geot.pixel_scale_y,
523 width,
524 height,
525 north_up,
526 )
527}
528
529#[derive(Debug, Clone)]
531pub struct TerrainFlowField {
532 grid: HeightGrid,
533 levels_agl: Vec<f64>,
535 u: Vec<f64>,
538 v: Vec<f64>,
539}
540
541impl TerrainFlowField {
542 pub fn adjust(
550 grid: HeightGrid,
551 base: &dyn WindField,
552 monitors: &[Monitor],
553 levels_agl: &[f64],
554 obs_length_m: f64,
555 surface_decay_m: f64,
556 terrain_scale_m: f64,
557 ) -> Result<Self> {
558 if levels_agl.is_empty() || levels_agl.iter().any(|h| !h.is_finite() || *h < 0.0) {
559 return Err(Error::invalid(
560 "levels_agl",
561 "must be finite and nonnegative",
562 ));
563 }
564 if obs_length_m <= 0.0 || surface_decay_m <= 0.0 || terrain_scale_m <= 0.0 {
565 return Err(Error::invalid("terrain", "length scales must be positive"));
566 }
567 if levels_agl.is_empty() || levels_agl.iter().any(|h| !h.is_finite() || *h < 0.0) {
568 return Err(Error::invalid(
569 "levels_agl",
570 "must be finite and nonnegative",
571 ));
572 }
573 if obs_length_m <= 0.0 || surface_decay_m <= 0.0 {
574 return Err(Error::invalid("terrain", "length scales must be positive"));
575 }
576 for m in monitors {
577 if !m.x_m.is_finite() || !m.z_m.is_finite() || !m.direction_from_deg.is_finite() {
578 return Err(Error::invalid("monitor", "must be finite"));
579 }
580 if !m.speed_mps.is_finite() || m.speed_mps < 0.0 {
581 return Err(Error::invalid("monitor", "speed must be nonnegative"));
582 }
583 }
584 let (ncols, nrows) = (grid.ncols, grid.nrows);
585 let n = ncols * nrows;
586 let inv_dx2 = 1.0 / (grid.dx * grid.dx);
587 let inv_dy2 = 1.0 / (grid.dy * grid.dy);
588 let diag = -2.0 * (inv_dx2 + inv_dy2);
589 let omega = 1.6;
590 let mut u = Vec::with_capacity(levels_agl.len() * n);
591 let mut v = Vec::with_capacity(levels_agl.len() * n);
592 let mut phi = vec![0.0; n];
593 for &level in levels_agl {
594 let mut u0 = vec![0.0; n];
596 let mut v0 = vec![0.0; n];
597 for i in 0..nrows {
598 for j in 0..ncols {
599 let x = grid.origin_x + j as f64 * grid.dx;
600 let z = grid.origin_y + i as f64 * grid.dy;
601 let height = grid.heights[i * ncols + j] + level;
602 let bg = base.wind(0.0, Vector3::new(x, height, z));
603 let (mut cx, mut cz, mut wsum) = (0.0, 0.0, 0.0);
604 for m in monitors {
605 let mw = horizontal(m.speed_mps, m.direction_from_deg);
606 let bg_m = base.wind(
607 0.0,
608 Vector3::new(m.x_m, grid.height_at(m.x_m, m.z_m), m.z_m),
609 );
610 let dx = x - m.x_m;
611 let dz = z - m.z_m;
612 let weight = (-(dx * dx + dz * dz) / (obs_length_m * obs_length_m)).exp()
613 * (-level / surface_decay_m).exp();
614 cx += weight * (mw.x - bg_m.x);
615 cz += weight * (mw.z - bg_m.z);
616 wsum += weight;
617 }
618 if wsum > 0.0 {
619 let w_floor = 0.1;
622 cx /= wsum + w_floor;
623 cz /= wsum + w_floor;
624 }
625 u0[i * ncols + j] = bg.x + cx;
626 v0[i * ncols + j] = bg.z + cz;
627 }
628 }
629 let level_decay = (-level / terrain_scale_m).exp() / terrain_scale_m;
634 let slopes = |u0x: f64, u0z: f64, i: usize, j: usize| -> f64 {
635 let dhdx = (grid.heights[i * ncols + (j + 1).min(ncols - 1)]
636 - grid.heights[i * ncols + j.saturating_sub(1)])
637 / (2.0 * grid.dx);
638 let dhdz = (grid.heights[(i + 1).min(nrows - 1) * ncols + j]
639 - grid.heights[i.saturating_sub(1) * ncols + j])
640 / (2.0 * grid.dy);
641 u0x * dhdx + u0z * dhdz
642 };
643 phi.fill(0.0);
644 for _ in 0..4000 {
645 let mut max_step = 0.0_f64;
646 for i in 1..nrows - 1 {
647 for j in 1..ncols - 1 {
648 let ip = (i + 1).min(nrows - 1);
649 let im = i.saturating_sub(1);
650 let jp = (j + 1).min(ncols - 1);
651 let jm = j.saturating_sub(1);
652 let lap = (phi[i * ncols + jp] + phi[i * ncols + jm]) * inv_dx2
653 + (phi[ip * ncols + j] + phi[im * ncols + j]) * inv_dy2
654 + diag * phi[i * ncols + j];
655 let div = (u0[i * ncols + jp] - u0[i * ncols + jm]) / (2.0 * grid.dx)
656 + (v0[ip * ncols + j] - v0[im * ncols + j]) / (2.0 * grid.dy);
657 let rhs =
658 div - slopes(u0[i * ncols + j], v0[i * ncols + j], i, j) * level_decay;
659 let updated = phi[i * ncols + j] + omega * (rhs - lap) / diag;
660 max_step = max_step.max((updated - phi[i * ncols + j]).abs());
661 phi[i * ncols + j] = updated;
662 }
663 }
664 if max_step < 1e-6 {
665 break;
666 }
667 }
668 for i in 0..nrows {
669 for j in 0..ncols {
670 let edge = i == 0 || i == nrows - 1 || j == 0 || j == ncols - 1;
671 if edge {
672 u.push(u0[i * ncols + j]);
675 v.push(v0[i * ncols + j]);
676 continue;
677 }
678 let jp = (j + 1).min(ncols - 1);
679 let jm = j.saturating_sub(1);
680 let ip = (i + 1).min(nrows - 1);
681 let im = i.saturating_sub(1);
682 let gx = (phi[i * ncols + jp] - phi[i * ncols + jm]) / (2.0 * grid.dx);
683 let gz = (phi[ip * ncols + j] - phi[im * ncols + j]) / (2.0 * grid.dy);
684 u.push(u0[i * ncols + j] - gx);
685 v.push(v0[i * ncols + j] - gz);
686 }
687 }
688 }
689 Ok(Self {
690 grid,
691 levels_agl: levels_agl.to_vec(),
692 u,
693 v,
694 })
695 }
696
697 fn sample_level(&self, level: usize, x: f64, z: f64) -> (f64, f64) {
698 let (ncols, nrows) = (self.grid.ncols, self.grid.nrows);
699 let fx = ((x - self.grid.origin_x) / self.grid.dx).clamp(0.0, ncols as f64 - 1.0);
700 let fy = ((z - self.grid.origin_y) / self.grid.dy).clamp(0.0, nrows as f64 - 1.0);
701 let (j, i) = (fx.floor() as usize, fy.floor() as usize);
702 let (rx, ry) = (fx - j as f64, fy - i as f64);
703 let j1 = (j + 1).min(ncols - 1);
704 let i1 = (i + 1).min(nrows - 1);
705 let base = level * ncols * nrows;
706 let g = |arr: &[f64], i: usize, j: usize| arr[base + i * ncols + j];
707 let sample = |arr: &[f64]| {
708 (1.0 - ry) * ((1.0 - rx) * g(arr, i, j) + rx * g(arr, i, j1))
709 + ry * ((1.0 - rx) * g(arr, i1, j) + rx * g(arr, i1, j1))
710 };
711 (sample(&self.u), sample(&self.v))
712 }
713
714 pub fn horizontal_at(&self, x: f64, y: f64, z: f64) -> (f64, f64) {
716 let agl = y - self.grid.height_at(x, z);
717 let mut level = 0;
718 for (k, &h) in self.levels_agl.iter().enumerate() {
719 if agl <= h {
720 level = k;
721 break;
722 }
723 level = k;
724 }
725 self.sample_level(level, x, z)
726 }
727}
728
729impl WindField for TerrainFlowField {
730 fn wind(&self, _time_s: f64, position_m: Vector3<f64>) -> Vector3<f64> {
731 let (u, v) = self.horizontal_at(position_m.x, position_m.y, position_m.z);
732 Vector3::new(u, 0.0, v)
733 }
734}