Skip to content

Commit 02848bb

Browse files
Initial GSSI reading functionality (#112)
1 parent 5bea4f5 commit 02848bb

8 files changed

Lines changed: 524 additions & 40 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "ridal"
3-
version = "0.5.1"
3+
version = "0.5.2"
44
edition = "2021"
55
readme = "README.md"
66
description="Speeding up Ground Penetrating Radar (GPR) processing"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ A near-term goal of Ridal is to enable easy conversion between formats, such as
1515

1616

1717
Much of the functionality has been inspired from the projects [RGPR](https://github.com/emanuelhuber/RGPR) and [ImpDAR](https://github.com/dlilien/ImpDAR); both of which are more mature projects.
18-
For example, Ridal currently only works on Malå (.rd3) and pulseEKKO (.dt1) radar formats.
18+
For example, Ridal currently only works on Malå (.rd3), GSSI (.dzt) and pulseEKKO (.dt1) radar formats.
1919
For many uses, these will more likely be the tools for you!
2020

2121
![Image of a glacier radargram](https://raw.githubusercontent.com/erikmannerfelt/ridal/v0.5.0/images/kroppbreen_rgm.webp)

default.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ rustPlatform.buildRustPackage {
2929
cargoLock.lockFile = ./Cargo.lock;
3030

3131
buildNoDefaultFeatures = true;
32-
buildFeatures = ["cli"];
32+
buildFeatures = [ "cli" ];
3333

3434
nativeBuildInputs = with pkgs; [
3535
pkg-config

src/formats.rs

Lines changed: 145 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::path::{Path, PathBuf};
44
pub enum FormatKind {
55
Ramac,
66
PulseEkko,
7+
Gssi,
78
}
89

910
#[derive(Debug, Clone, serde::Serialize)]
@@ -64,6 +65,19 @@ pub fn all_formats() -> Vec<FormatInfo> {
6465
coordinates: ".gp2",
6566
},
6667
},
68+
FormatInfo {
69+
name: "gssi",
70+
description: "GSSI DZT/DZG format",
71+
capabilities: FormatCapabilities {
72+
read: true,
73+
write: false,
74+
},
75+
files: FormatFiles {
76+
header: ".DZT",
77+
data: ".DZT",
78+
coordinates: ".DZG",
79+
},
80+
},
6781
]
6882
}
6983

@@ -77,9 +91,37 @@ pub fn format_info(kind: FormatKind) -> FormatInfo {
7791
.into_iter()
7892
.find(|fmt| fmt.name == "pulseekko")
7993
.unwrap(),
94+
FormatKind::Gssi => all_formats()
95+
.into_iter()
96+
.find(|fmt| fmt.name == "gssi")
97+
.unwrap(),
8098
}
8199
}
82100

101+
pub fn find_neighbor_case_insensitive(base: &Path, target_extension: &str) -> Option<PathBuf> {
102+
let parent = base.parent().unwrap_or_else(|| Path::new("."));
103+
let stem = base.file_stem().or_else(|| base.file_name())?.to_str()?;
104+
let target_extension = target_extension.trim_start_matches('.');
105+
106+
std::fs::read_dir(parent)
107+
.ok()?
108+
.filter_map(|entry| entry.ok().map(|entry| entry.path()))
109+
.find(|path| {
110+
path.file_stem()
111+
.and_then(|s| s.to_str())
112+
.is_some_and(|s| s.eq_ignore_ascii_case(stem))
113+
&& path
114+
.extension()
115+
.and_then(|s| s.to_str())
116+
.is_some_and(|ext| ext.eq_ignore_ascii_case(target_extension))
117+
})
118+
}
119+
120+
fn resolve_sidecar(base: &Path, target_extension: &str) -> PathBuf {
121+
find_neighbor_case_insensitive(base, target_extension)
122+
.unwrap_or_else(|| base.with_extension(target_extension))
123+
}
124+
83125
pub fn resolve_input(input: &Path) -> Result<ResolvedInput, String> {
84126
let ext = input
85127
.extension()
@@ -90,60 +132,90 @@ pub fn resolve_input(input: &Path) -> Result<ResolvedInput, String> {
90132
Some("rad") => Ok(ResolvedInput {
91133
input: input.to_path_buf(),
92134
kind: FormatKind::Ramac,
93-
header: input.to_path_buf(),
94-
data: input.with_extension("rd3"),
95-
coordinates: input.with_extension("cor"),
135+
header: resolve_sidecar(input, "rad"),
136+
data: resolve_sidecar(input, "rd3"),
137+
coordinates: resolve_sidecar(input, "cor"),
96138
}),
97139
Some("rd3") | Some("cor") => Ok(ResolvedInput {
98140
input: input.to_path_buf(),
99141
kind: FormatKind::Ramac,
100-
header: input.with_extension("rad"),
101-
data: input.with_extension("rd3"),
102-
coordinates: input.with_extension("cor"),
142+
header: resolve_sidecar(input, "rad"),
143+
data: resolve_sidecar(input, "rd3"),
144+
coordinates: resolve_sidecar(input, "cor"),
103145
}),
104146
Some("hd") => Ok(ResolvedInput {
105147
input: input.to_path_buf(),
106148
kind: FormatKind::PulseEkko,
107-
header: input.to_path_buf(),
108-
data: input.with_extension("dt1"),
109-
coordinates: input.with_extension("gp2"),
149+
header: resolve_sidecar(input, "hd"),
150+
data: resolve_sidecar(input, "dt1"),
151+
coordinates: resolve_sidecar(input, "gp2"),
110152
}),
111153
Some("dt1") | Some("gp2") => Ok(ResolvedInput {
112154
input: input.to_path_buf(),
113155
kind: FormatKind::PulseEkko,
114-
header: input.with_extension("hd"),
115-
data: input.with_extension("dt1"),
116-
coordinates: input.with_extension("gp2"),
156+
header: resolve_sidecar(input, "hd"),
157+
data: resolve_sidecar(input, "dt1"),
158+
coordinates: resolve_sidecar(input, "gp2"),
159+
}),
160+
Some("dzt") | Some("dzg") | Some("dzx") => Ok(ResolvedInput {
161+
input: input.to_path_buf(),
162+
kind: FormatKind::Gssi,
163+
header: resolve_sidecar(input, "dzt"),
164+
data: resolve_sidecar(input, "dzt"),
165+
coordinates: resolve_sidecar(input, "dzg"),
117166
}),
118167
Some(other) => Err(format!(
119-
"Unsupported input extension '.{other}' for {:?}. Supported formats are RAMAC (.rad/.rd3/.cor) and pulseEKKO (.hd/.dt1/.gp2).",
168+
"Unsupported input extension '.{other}' for {:?}. Supported formats are RAMAC (.rad/.rd3/.cor), pulseEKKO (.hd/.dt1/.gp2), and GSSI (.dzt/.dzg/.dzx).",
120169
input
121170
)),
122171
None => {
123-
let ramac = input.with_extension("rad");
124-
let pulseekko = input.with_extension("hd");
125-
match (ramac.is_file(), pulseekko.is_file()) {
126-
(true, false) => Ok(ResolvedInput {
172+
let ramac = find_neighbor_case_insensitive(input, "rad");
173+
let pulseekko = find_neighbor_case_insensitive(input, "hd");
174+
let gssi = find_neighbor_case_insensitive(input, "dzt");
175+
match (ramac, pulseekko, gssi) {
176+
(Some(ramac), None, None) => Ok(ResolvedInput {
127177
input: input.to_path_buf(),
128178
kind: FormatKind::Ramac,
129179
header: ramac.clone(),
130-
data: ramac.with_extension("rd3"),
131-
coordinates: ramac.with_extension("cor"),
180+
data: resolve_sidecar(&ramac, "rd3"),
181+
coordinates: resolve_sidecar(&ramac, "cor"),
132182
}),
133-
(false, true) => Ok(ResolvedInput {
183+
(None, Some(pulseekko), None) => Ok(ResolvedInput {
134184
input: input.to_path_buf(),
135185
kind: FormatKind::PulseEkko,
136186
header: pulseekko.clone(),
137-
data: pulseekko.with_extension("dt1"),
138-
coordinates: pulseekko.with_extension("gp2"),
187+
data: resolve_sidecar(&pulseekko, "dt1"),
188+
coordinates: resolve_sidecar(&pulseekko, "gp2"),
189+
}),
190+
(None, None, Some(gssi)) => Ok(ResolvedInput {
191+
input: input.to_path_buf(),
192+
kind: FormatKind::Gssi,
193+
header: gssi.clone(),
194+
data: gssi.clone(),
195+
coordinates: resolve_sidecar(&gssi, "dzg"),
139196
}),
140-
(true, true) => Err(format!(
197+
(Some(ramac), Some(pulseekko), None) => Err(format!(
141198
"Ambiguous extension-less input {:?}: both {:?} and {:?} exist.",
142199
input, ramac, pulseekko
143200
)),
144-
(false, false) => Err(format!(
145-
"Could not infer format for extension-less input {:?}. Tried {:?} and {:?}.",
146-
input, ramac, pulseekko
201+
(Some(ramac), None, Some(gssi)) => Err(format!(
202+
"Ambiguous extension-less input {:?}: both {:?} and {:?} exist.",
203+
input, ramac, gssi
204+
)),
205+
(None, Some(pulseekko), Some(gssi)) => Err(format!(
206+
"Ambiguous extension-less input {:?}: both {:?} and {:?} exist.",
207+
input, pulseekko, gssi
208+
)),
209+
(Some(ramac), Some(pulseekko), Some(gssi)) => Err(format!(
210+
"Ambiguous extension-less input {:?}: {:?}, {:?}, and {:?} exist.",
211+
input, ramac, pulseekko, gssi
212+
)),
213+
(None, None, None) => Err(format!(
214+
"Could not infer format for extension-less input {:?}. Tried {:?}, {:?}, and {:?}.",
215+
input,
216+
input.with_extension("rad"),
217+
input.with_extension("hd"),
218+
input.with_extension("dzt")
147219
)),
148220
}
149221
}
@@ -160,7 +232,14 @@ mod tests {
160232
.into_iter()
161233
.map(|fmt| fmt.name.to_string())
162234
.collect::<Vec<String>>();
163-
assert_eq!(names, vec!["ramac".to_string(), "pulseekko".to_string()]);
235+
assert_eq!(
236+
names,
237+
vec![
238+
"ramac".to_string(),
239+
"pulseekko".to_string(),
240+
"gssi".to_string()
241+
]
242+
);
164243
}
165244

166245
#[test]
@@ -186,4 +265,43 @@ mod tests {
186265
assert_eq!(resolved.kind, FormatKind::PulseEkko);
187266
assert_eq!(resolved.header, PathBuf::from("line01.hd"));
188267
}
268+
269+
#[test]
270+
fn test_resolve_gssi_extensions() {
271+
let resolved = resolve_input(Path::new("line01.dzt")).unwrap();
272+
assert_eq!(resolved.kind, FormatKind::Gssi);
273+
assert_eq!(resolved.data, PathBuf::from("line01.dzt"));
274+
assert_eq!(resolved.coordinates, PathBuf::from("line01.dzg"));
275+
276+
let resolved = resolve_input(Path::new("line01.dzg")).unwrap();
277+
assert_eq!(resolved.kind, FormatKind::Gssi);
278+
assert_eq!(resolved.header, PathBuf::from("line01.dzt"));
279+
}
280+
281+
#[test]
282+
fn test_resolve_case_insensitive_sidecars() {
283+
let temp_dir = tempfile::tempdir().unwrap();
284+
let rad = temp_dir.path().join("LINE01.RAD");
285+
let rd3 = temp_dir.path().join("LINE01.RD3");
286+
let cor = temp_dir.path().join("LINE01.COR");
287+
std::fs::write(&rad, "").unwrap();
288+
std::fs::write(&rd3, "").unwrap();
289+
std::fs::write(&cor, "").unwrap();
290+
291+
let resolved = resolve_input(&temp_dir.path().join("LINE01")).unwrap();
292+
assert_eq!(resolved.kind, FormatKind::Ramac);
293+
assert_eq!(resolved.header, rad);
294+
assert_eq!(resolved.data, rd3);
295+
assert_eq!(resolved.coordinates, cor);
296+
297+
let dzt = temp_dir.path().join("TRACK.DZT");
298+
let dzg = temp_dir.path().join("TRACK.DZG");
299+
std::fs::write(&dzt, "").unwrap();
300+
std::fs::write(&dzg, "").unwrap();
301+
302+
let resolved = resolve_input(&temp_dir.path().join("TRACK")).unwrap();
303+
assert_eq!(resolved.kind, FormatKind::Gssi);
304+
assert_eq!(resolved.header, dzt);
305+
assert_eq!(resolved.coordinates, dzg);
306+
}
189307
}

src/gpr.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ impl GPRMeta {
5555
/// # Arguments
5656
/// - `projected_crs`: The CRS to project coordinates into
5757
pub fn find_cor(&self, projected_crs: Option<&String>) -> Result<GPRLocation, Box<dyn Error>> {
58-
io::load_cor(&self.data_filepath.with_extension("cor"), projected_crs)
58+
let cor = crate::formats::find_neighbor_case_insensitive(&self.data_filepath, "cor")
59+
.unwrap_or_else(|| self.data_filepath.with_extension("cor"));
60+
io::load_cor(&cor, projected_crs)
5961
}
6062
}
6163

@@ -740,15 +742,19 @@ impl GPR {
740742
metadata: GPRMeta,
741743
) -> Result<GPR, Box<dyn Error>> {
742744
let data = match metadata.data_filepath.extension().and_then(|s| s.to_str()) {
743-
Some("rd3") => Ok(io::load_rd3(
745+
Some(ext) if ext.eq_ignore_ascii_case("rd3") => Ok(io::load_rd3(
744746
&metadata.data_filepath,
745747
metadata.samples as usize,
746748
)?),
747-
Some("dt1") => Ok(io::load_pe_dt1(
749+
Some(ext) if ext.eq_ignore_ascii_case("dt1") => Ok(io::load_pe_dt1(
748750
&metadata.data_filepath,
749751
metadata.samples as usize,
750752
metadata.last_trace as usize,
751753
)?),
754+
Some(ext) if ext.eq_ignore_ascii_case("dzt") => Ok(io::load_dzt(
755+
&metadata.data_filepath,
756+
metadata.samples as usize,
757+
)?),
752758
_ => Err(format!("Unknown filetype: {:?}", metadata.data_filepath)),
753759
}?;
754760

@@ -1817,6 +1823,17 @@ fn load_meta_and_location(
18171823
let location = io::load_pe_gp2(&resolved.coordinates, crs)?;
18181824
(meta, location)
18191825
}
1826+
FormatKind::Gssi => {
1827+
if cor_path.is_some() {
1828+
return Err("The --cor option is only supported for RAMAC inputs.".into());
1829+
}
1830+
if !resolved.header.is_file() {
1831+
return Err(format!("File not found: {:?}", resolved.header).into());
1832+
}
1833+
let meta = io::load_gssi_dzt(&resolved.header, medium_velocity, override_antenna_mhz)?;
1834+
let location = io::load_gssi_dzg(&resolved.coordinates, crs)?;
1835+
(meta, location)
1836+
}
18201837
};
18211838

18221839
if let Some(dem_path) = dem_path {
@@ -2201,6 +2218,16 @@ fn load_single_gpr_for_grouping(
22012218
},
22022219
)?
22032220
}
2221+
FormatKind::Gssi => {
2222+
io::load_gssi_dzt(&resolved.header, medium_velocity, override_antenna_mhz).map_err(
2223+
|e| {
2224+
format!(
2225+
"Failed to load GSSI header '{}': {e}",
2226+
resolved.header.display()
2227+
)
2228+
},
2229+
)?
2230+
}
22042231
};
22052232

22062233
let mut location = if let Some(cor_override) = &cor_path {
@@ -2223,6 +2250,11 @@ fn load_single_gpr_for_grouping(
22232250
io::load_pe_gp2(gp2, crs.as_ref())
22242251
.map_err(|e| format!("Failed to load .gp2 '{}': {e}", gp2.display()))?
22252252
}
2253+
FormatKind::Gssi => {
2254+
let dzg = &resolved.coordinates;
2255+
io::load_gssi_dzg(dzg, crs.as_ref())
2256+
.map_err(|e| format!("Failed to load .dzg '{}': {e}", dzg.display()))?
2257+
}
22262258
}
22272259
};
22282260

0 commit comments

Comments
 (0)