1use crate::model::{Catalogue, Item, Note, ObjectId, Section, SectionKey, TraceItem, TraceRole};
29
30pub const CODE: &str = "erosion";
32
33const Z_TOLERANCE: f64 = 1e-6;
36const MIN_LENGTH: f64 = 1e-6;
38
39#[derive(Clone, Debug, PartialEq)]
41pub struct Portion {
42 pub section: SectionKey,
44 pub older: ObjectId,
46 pub older_age: [f64; 2],
48 pub younger: ObjectId,
50 pub younger_age: [f64; 2],
52 pub s0: f64,
54 pub s1: f64,
56}
57
58impl Portion {
59 pub fn proposal(&self) -> String {
61 format!(
62 "`{older}` should stop against `{younger}` between s = {s0} and s = {s1} (an erosion: the younger surface \
63 truncates the older one), or one of the two ages is inverted; nothing was changed",
64 older = self.older.title,
65 younger = self.younger.title,
66 s0 = round1(self.s0),
67 s1 = round1(self.s1),
68 )
69 }
70
71 pub fn said(&self) -> String {
73 format!(
74 "erosion suspected on {section}: `{younger}` ({ya}) passes below `{older}` ({oa}) between s = {s0} and s = {s1}, \
75 though it is the younger - proposal: {proposal}",
76 section = section_name(self.section),
77 younger = self.younger.title,
78 ya = age_text(self.younger_age),
79 older = self.older.title,
80 oa = age_text(self.older_age),
81 s0 = round1(self.s0),
82 s1 = round1(self.s1),
83 proposal = self.proposal(),
84 )
85 }
86}
87
88pub fn detect(section: SectionKey, traces: &[&TraceItem]) -> Vec<Portion> {
93 let dated: Vec<(&TraceItem, [f64; 2])> = traces
94 .iter()
95 .filter(|t| t.role == TraceRole::Horizon)
96 .filter_map(|t| t.age.map(|age| (*t, age)))
97 .collect();
98 let mut out = Vec::new();
99 for (i, (a, a_age)) in dated.iter().enumerate() {
100 for (b, b_age) in dated.iter().skip(i + 1) {
101 let (older, o_age, younger, y_age) = if a_age[1] > b_age[0] {
103 (*a, *a_age, *b, *b_age)
104 } else if b_age[1] > a_age[0] {
105 (*b, *b_age, *a, *a_age)
106 } else {
107 continue;
108 };
109 for (s0, s1) in below(&older.spans, &younger.spans) {
110 out.push(Portion {
111 section,
112 older: older.object.clone(),
113 older_age: o_age,
114 younger: younger.object.clone(),
115 younger_age: y_age,
116 s0,
117 s1,
118 });
119 }
120 }
121 }
122 out
123}
124
125pub fn of_section(section: &Section) -> Vec<Portion> {
127 let traces: Vec<&TraceItem> = section
128 .items
129 .iter()
130 .filter_map(|item| match item {
131 Item::Trace(t) => Some(t),
132 _ => None,
133 })
134 .collect();
135 detect(section.key, &traces)
136}
137
138pub fn notes_of<'a>(sections: impl IntoIterator<Item = &'a Section>) -> Vec<Note> {
140 sections
141 .into_iter()
142 .flat_map(of_section)
143 .map(|portion| Note {
144 code: CODE.to_string(),
145 sections: vec![portion.section],
146 objects: vec![portion.older.clone(), portion.younger.clone()],
147 said: portion.said(),
148 })
149 .collect()
150}
151
152pub fn notes(catalogue: &Catalogue) -> Vec<Note> {
154 notes_of(std::iter::once(&catalogue.long).chain(&catalogue.crosses))
155}
156
157fn z_at(spans: &[Vec<[f64; 2]>], s: f64) -> Option<f64> {
159 spans.iter().find_map(|span| {
160 let (first, last) = (span.first()?, span.last()?);
161 if s < first[0] || s > last[0] {
162 return None;
163 }
164 span.windows(2).find(|w| s >= w[0][0] && s <= w[1][0]).map(|w| {
165 let run = w[1][0] - w[0][0];
166 if run <= 0.0 {
167 w[0][1]
168 } else {
169 w[0][1] + (w[1][1] - w[0][1]) * (s - w[0][0]) / run
170 }
171 })
172 })
173}
174
175fn below(upper: &[Vec<[f64; 2]>], lower: &[Vec<[f64; 2]>]) -> Vec<(f64, f64)> {
179 let mut at: Vec<f64> = upper
180 .iter()
181 .chain(lower)
182 .flatten()
183 .map(|p| p[0])
184 .filter(|s| s.is_finite())
185 .collect();
186 at.sort_by(f64::total_cmp);
187 at.dedup();
188 let mut found: Vec<(f64, f64)> = Vec::new();
189 for pair in at.windows(2) {
190 let (a, b) = (pair[0], pair[1]);
191 let mid = 0.5 * (a + b);
192 let (Some(ua), Some(ub), Some(la), Some(lb), Some(_), Some(_)) = (
195 z_at(upper, a),
196 z_at(upper, b),
197 z_at(lower, a),
198 z_at(lower, b),
199 z_at(upper, mid),
200 z_at(lower, mid),
201 ) else {
202 continue;
203 };
204 let (fa, fb) = (la - ua, lb - ub);
205 let neg = |f: f64| f < -Z_TOLERANCE;
206 let part = match (neg(fa), neg(fb)) {
207 (true, true) => Some((a, b)),
208 (true, false) => Some((a, a + (b - a) * fa / (fa - fb))),
209 (false, true) => Some((a + (b - a) * fa / (fa - fb), b)),
210 (false, false) => None,
211 };
212 if let Some((s0, s1)) = part.filter(|(s0, s1)| s1 - s0 > MIN_LENGTH) {
213 match found.last_mut() {
214 Some(last) if (s0 - last.1).abs() <= MIN_LENGTH => last.1 = s1,
215 _ => found.push((s0, s1)),
216 }
217 }
218 }
219 found
220}
221
222fn section_name(key: SectionKey) -> String {
223 match key {
224 SectionKey::Long => "the long section".to_string(),
225 SectionKey::Cross { index } => format!("cross section {index}"),
226 }
227}
228
229fn age_text(age: [f64; 2]) -> String {
231 if (age[0] - age[1]).abs() < 1e-9 {
232 format!("{} Ma", trim(age[0]))
233 } else {
234 format!("{}-{} Ma", trim(age[0]), trim(age[1]))
235 }
236}
237
238fn trim(v: f64) -> String {
239 let text = format!("{v:.3}");
240 text.trim_end_matches('0').trim_end_matches('.').to_string()
241}
242
243fn round1(v: f64) -> String {
244 format!("{v:.1}")
245}