use crate::model::{Catalogue, Item, Note, ObjectId, Section, SectionKey, TraceItem, TraceRole};
pub const CODE: &str = "erosion";
const Z_TOLERANCE: f64 = 1e-6;
const MIN_LENGTH: f64 = 1e-6;
#[derive(Clone, Debug, PartialEq)]
pub struct Portion {
pub section: SectionKey,
pub older: ObjectId,
pub older_age: [f64; 2],
pub younger: ObjectId,
pub younger_age: [f64; 2],
pub s0: f64,
pub s1: f64,
}
impl Portion {
pub fn proposal(&self) -> String {
format!(
"`{older}` should stop against `{younger}` between s = {s0} and s = {s1} (an erosion: the younger surface \
truncates the older one), or one of the two ages is inverted; nothing was changed",
older = self.older.title,
younger = self.younger.title,
s0 = round1(self.s0),
s1 = round1(self.s1),
)
}
pub fn said(&self) -> String {
format!(
"erosion suspected on {section}: `{younger}` ({ya}) passes below `{older}` ({oa}) between s = {s0} and s = {s1}, \
though it is the younger - proposal: {proposal}",
section = section_name(self.section),
younger = self.younger.title,
ya = age_text(self.younger_age),
older = self.older.title,
oa = age_text(self.older_age),
s0 = round1(self.s0),
s1 = round1(self.s1),
proposal = self.proposal(),
)
}
}
pub fn detect(section: SectionKey, traces: &[&TraceItem]) -> Vec<Portion> {
let dated: Vec<(&TraceItem, [f64; 2])> = traces
.iter()
.filter(|t| t.role == TraceRole::Horizon)
.filter_map(|t| t.age.map(|age| (*t, age)))
.collect();
let mut out = Vec::new();
for (i, (a, a_age)) in dated.iter().enumerate() {
for (b, b_age) in dated.iter().skip(i + 1) {
let (older, o_age, younger, y_age) = if a_age[1] > b_age[0] {
(*a, *a_age, *b, *b_age)
} else if b_age[1] > a_age[0] {
(*b, *b_age, *a, *a_age)
} else {
continue;
};
for (s0, s1) in below(&older.spans, &younger.spans) {
out.push(Portion {
section,
older: older.object.clone(),
older_age: o_age,
younger: younger.object.clone(),
younger_age: y_age,
s0,
s1,
});
}
}
}
out
}
pub fn of_section(section: &Section) -> Vec<Portion> {
let traces: Vec<&TraceItem> = section
.items
.iter()
.filter_map(|item| match item {
Item::Trace(t) => Some(t),
_ => None,
})
.collect();
detect(section.key, &traces)
}
pub fn notes_of<'a>(sections: impl IntoIterator<Item = &'a Section>) -> Vec<Note> {
sections
.into_iter()
.flat_map(of_section)
.map(|portion| Note {
code: CODE.to_string(),
sections: vec![portion.section],
objects: vec![portion.older.clone(), portion.younger.clone()],
said: portion.said(),
})
.collect()
}
pub fn notes(catalogue: &Catalogue) -> Vec<Note> {
notes_of(std::iter::once(&catalogue.long).chain(&catalogue.crosses))
}
fn z_at(spans: &[Vec<[f64; 2]>], s: f64) -> Option<f64> {
spans.iter().find_map(|span| {
let (first, last) = (span.first()?, span.last()?);
if s < first[0] || s > last[0] {
return None;
}
span.windows(2).find(|w| s >= w[0][0] && s <= w[1][0]).map(|w| {
let run = w[1][0] - w[0][0];
if run <= 0.0 {
w[0][1]
} else {
w[0][1] + (w[1][1] - w[0][1]) * (s - w[0][0]) / run
}
})
})
}
fn below(upper: &[Vec<[f64; 2]>], lower: &[Vec<[f64; 2]>]) -> Vec<(f64, f64)> {
let mut at: Vec<f64> = upper
.iter()
.chain(lower)
.flatten()
.map(|p| p[0])
.filter(|s| s.is_finite())
.collect();
at.sort_by(f64::total_cmp);
at.dedup();
let mut found: Vec<(f64, f64)> = Vec::new();
for pair in at.windows(2) {
let (a, b) = (pair[0], pair[1]);
let mid = 0.5 * (a + b);
let (Some(ua), Some(ub), Some(la), Some(lb), Some(_), Some(_)) = (
z_at(upper, a),
z_at(upper, b),
z_at(lower, a),
z_at(lower, b),
z_at(upper, mid),
z_at(lower, mid),
) else {
continue;
};
let (fa, fb) = (la - ua, lb - ub);
let neg = |f: f64| f < -Z_TOLERANCE;
let part = match (neg(fa), neg(fb)) {
(true, true) => Some((a, b)),
(true, false) => Some((a, a + (b - a) * fa / (fa - fb))),
(false, true) => Some((a + (b - a) * fa / (fa - fb), b)),
(false, false) => None,
};
if let Some((s0, s1)) = part.filter(|(s0, s1)| s1 - s0 > MIN_LENGTH) {
match found.last_mut() {
Some(last) if (s0 - last.1).abs() <= MIN_LENGTH => last.1 = s1,
_ => found.push((s0, s1)),
}
}
}
found
}
fn section_name(key: SectionKey) -> String {
match key {
SectionKey::Long => "the long section".to_string(),
SectionKey::Cross { index } => format!("cross section {index}"),
}
}
fn age_text(age: [f64; 2]) -> String {
if (age[0] - age[1]).abs() < 1e-9 {
format!("{} Ma", trim(age[0]))
} else {
format!("{}-{} Ma", trim(age[0]), trim(age[1]))
}
}
fn trim(v: f64) -> String {
let text = format!("{v:.3}");
text.trim_end_matches('0').trim_end_matches('.').to_string()
}
fn round1(v: f64) -> String {
format!("{v:.1}")
}