use crate::foundation::{GeoError, GridGeometry, Result};
use crate::io::{is_null_sentinel, DEFAULT_NULL_1E30};
use ndarray::Array2;
use std::path::Path;
pub fn load_cps3_grid(path: &Path) -> Result<(GridGeometry, Array2<f64>)> {
let text = crate::io::decode_latin1(&std::fs::read(path)?);
parse_cps3_grid(&text)
}
fn header_nums(line: &str) -> Vec<f64> {
line.split_whitespace()
.skip(1) .filter_map(|t| t.parse::<f64>().ok())
.collect()
}
fn parse_cps3_grid(text: &str) -> Result<(GridGeometry, Array2<f64>)> {
let (mut xmin, mut xmax, mut ymin, mut ymax) = (None, None, None, None);
let (mut nrow, mut ncol): (Option<usize>, Option<usize>) = (None, None);
let (mut xinc, mut yinc): (Option<f64>, Option<f64>) = (None, None);
let mut null = DEFAULT_NULL_1E30;
let mut values: Vec<f64> = Vec::new();
let mut in_data = false;
for line in text.lines() {
let s = line.trim();
if s.is_empty() {
continue;
}
if s.starts_with("->") {
in_data = true;
continue;
}
if !in_data && (s.starts_with("FS") || s.starts_with("FF")) {
let tag = s.split_whitespace().next().unwrap_or("");
let nums = header_nums(s);
match tag {
"FSLIMI" if nums.len() >= 4 => {
xmin = Some(nums[0]);
xmax = Some(nums[1]);
ymin = Some(nums[2]);
ymax = Some(nums[3]);
}
"FSNROW" if nums.len() >= 2 => {
nrow = Some(nums[0] as usize);
ncol = Some(nums[1] as usize);
}
"FSXINC" if nums.len() >= 2 => {
xinc = Some(nums[0]);
yinc = Some(nums[1]);
}
"FSASCI" => {
if let Some(&last) = nums.last() {
null = last;
}
}
_ => {}
}
continue;
}
if in_data {
values.extend(s.split_whitespace().filter_map(|t| t.parse::<f64>().ok()));
}
}
let nrow = nrow.ok_or_else(|| GeoError::Parse("CPS-3 grid: missing FSNROW".into()))?;
let ncol = ncol.ok_or_else(|| GeoError::Parse("CPS-3 grid: missing FSNROW ncol".into()))?;
let xmin = xmin.ok_or_else(|| GeoError::Parse("CPS-3 grid: missing FSLIMI".into()))?;
let xmax = xmax.unwrap();
let ymin = ymin.unwrap();
let ymax = ymax.unwrap();
if nrow == 0 || ncol == 0 {
return Err(GeoError::Parse("CPS-3 grid: zero node count".into()));
}
let xinc = xinc.filter(|v| *v > 0.0).unwrap_or_else(|| {
if ncol > 1 {
(xmax - xmin) / (ncol - 1) as f64
} else {
1.0
}
});
let yinc = yinc.filter(|v| *v > 0.0).unwrap_or_else(|| {
if nrow > 1 {
(ymax - ymin) / (nrow - 1) as f64
} else {
1.0
}
});
let n = nrow
.checked_mul(ncol)
.ok_or_else(|| GeoError::Parse("CPS-3 grid: nrow*ncol overflows".into()))?;
values.resize(n, null);
let mut grid = Array2::from_elem((ncol, nrow), f64::NAN);
for (idx, &v) in values.iter().enumerate() {
let r = idx / ncol; let c = idx % ncol; grid[[c, r]] = if is_null_sentinel(v, null) {
f64::NAN
} else {
v
};
}
let geom = GridGeometry {
xori: xmin,
yori: ymin,
xinc,
yinc,
ncol,
nrow,
rotation_deg: 0.0,
yflip: false,
};
Ok((geom, grid))
}
pub fn load_cps3_lines(path: &Path) -> Result<Vec<Vec<[f64; 3]>>> {
let text = crate::io::decode_latin1(&std::fs::read(path)?);
let mut rings: Vec<Vec<[f64; 3]>> = Vec::new();
let mut cur: Option<Vec<[f64; 3]>> = None;
for line in text.lines() {
let s = line.trim();
if s.is_empty() {
continue;
}
if s.starts_with("->") {
if let Some(r) = cur.take() {
rings.push(r);
}
cur = Some(Vec::new());
continue;
}
if s.starts_with("FF") || s.starts_with("FS") || s.starts_with('#') {
continue;
}
let Some(ring) = cur.as_mut() else { continue };
let nums: Vec<f64> = s
.split_whitespace()
.filter_map(|t| t.parse().ok())
.collect();
if nums.len() >= 3 {
ring.push([nums[0], nums[1], nums[2]]);
}
}
if let Some(r) = cur {
rings.push(r);
}
Ok(rings)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn write(name: &str, body: &str) -> std::path::PathBuf {
let p = std::env::temp_dir().join(name);
std::fs::File::create(&p)
.unwrap()
.write_all(body.as_bytes())
.unwrap();
p
}
#[test]
fn grid_maps_row_major_south_to_north() {
let body = "\
FSASCI 0 1 0 5 1.0E+30
FSLIMI 100 110 200 220 0 6
FSNROW 3 2
FSXINC 10 10
->
1 2
3 1.0E+30
5 6
";
let p = write("petekio_cps3_grid.CPS3grid", body);
let (geom, v) = load_cps3_grid(&p).unwrap();
assert_eq!((geom.ncol, geom.nrow), (2, 3));
assert!(!geom.yflip);
assert_eq!(v[[0, 0]], 1.0); assert_eq!(v[[1, 0]], 2.0); assert_eq!(v[[0, 1]], 3.0);
assert!(v[[1, 1]].is_nan()); assert_eq!(v[[0, 2]], 5.0);
assert_eq!(v[[1, 2]], 6.0);
assert_eq!(geom.node_xy(0, 0), (100.0, 200.0));
assert_eq!(geom.node_xy(1, 0), (110.0, 200.0));
assert_eq!(geom.node_xy(0, 1), (100.0, 210.0));
assert_eq!(geom.node_xy(0, 2), (100.0, 220.0));
}
#[test]
fn lines_split_blocks_on_arrow() {
let body = "\
FFASCI 0 1 2 3
FFATTR ...
-> 1
100.0 200.0 -50.0
110.0 200.0 -51.0
110.0 210.0 -52.0
-> 2
300.0 400.0 -60.0
310.0 400.0 -61.0
310.0 410.0 -62.0
";
let p = write("petekio_cps3_lines.CPS3lines", body);
let rings = load_cps3_lines(&p).unwrap();
assert_eq!(rings.len(), 2);
assert_eq!(rings[0].len(), 3);
assert_eq!(rings[0][0], [100.0, 200.0, -50.0]);
assert_eq!(rings[1][2], [310.0, 410.0, -62.0]);
}
}