1use crate::error::{AlgorithmError, Result};
9use std::collections::BTreeMap;
10
11#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct ContourPoint {
14 pub x: f64,
15 pub y: f64,
16}
17
18impl ContourPoint {
19 #[must_use]
20 pub const fn new(x: f64, y: f64) -> Self {
21 Self { x, y }
22 }
23}
24
25#[derive(Debug, Clone)]
27pub struct ContourLine {
28 pub level: f64,
29 pub points: Vec<ContourPoint>,
30 pub is_closed: bool,
31}
32
33#[derive(Debug, Clone)]
35pub struct ContourConfig {
36 pub interval: f64,
38 pub base: f64,
40 pub nodata: Option<f64>,
42}
43
44impl ContourConfig {
45 pub fn new(interval: f64) -> Result<Self> {
50 if interval <= 0.0 || !interval.is_finite() {
51 return Err(AlgorithmError::InvalidParameter {
52 parameter: "interval",
53 message: format!("interval must be positive, got {interval}"),
54 });
55 }
56 Ok(Self {
57 interval,
58 base: 0.0,
59 nodata: None,
60 })
61 }
62
63 #[must_use]
64 pub fn with_base(mut self, base: f64) -> Self {
65 self.base = base;
66 self
67 }
68
69 #[must_use]
70 pub fn with_nodata(mut self, nodata: f64) -> Self {
71 self.nodata = Some(nodata);
72 self
73 }
74
75 fn compute_levels(&self, min_val: f64, max_val: f64) -> Vec<f64> {
77 if min_val > max_val || !min_val.is_finite() || !max_val.is_finite() {
78 return vec![];
79 }
80
81 let start = ((min_val - self.base) / self.interval).ceil();
82 let end = ((max_val - self.base) / self.interval).floor();
83
84 let start_i = start as i64;
85 let end_i = end as i64;
86
87 let mut levels = Vec::new();
88 for i in start_i..=end_i {
89 let level = self.base + (i as f64) * self.interval;
90 if level > min_val && level < max_val {
91 levels.push(level);
92 }
93 }
94 levels
95 }
96}
97
98#[derive(Debug, Clone, Copy)]
100struct Segment {
101 p0: ContourPoint,
102 p1: ContourPoint,
103}
104
105#[inline]
107fn lerp_frac(v0: f64, v1: f64, level: f64) -> f64 {
108 let denom = v1 - v0;
109 if denom.abs() < 1e-15 {
110 0.5
111 } else {
112 (level - v0) / denom
113 }
114}
115
116fn cell_segments(
125 col: usize,
126 row: usize,
127 tl: f64,
128 tr: f64,
129 bl: f64,
130 br: f64,
131 level: f64,
132) -> Vec<Segment> {
133 let case = ((tl >= level) as u8) << 3
134 | ((tr >= level) as u8) << 2
135 | ((bl >= level) as u8) << 1
136 | (br >= level) as u8;
137
138 let top = || {
141 let t = lerp_frac(tl, tr, level);
142 ContourPoint::new(col as f64 + t, row as f64)
143 };
144 let bottom = || {
146 let t = lerp_frac(bl, br, level);
147 ContourPoint::new(col as f64 + t, row as f64 + 1.0)
148 };
149 let left = || {
151 let t = lerp_frac(tl, bl, level);
152 ContourPoint::new(col as f64, row as f64 + t)
153 };
154 let right = || {
156 let t = lerp_frac(tr, br, level);
157 ContourPoint::new(col as f64 + 1.0, row as f64 + t)
158 };
159
160 match case {
161 0 | 15 => vec![], 1 => vec![Segment {
163 p0: bottom(),
164 p1: right(),
165 }],
166 2 => vec![Segment {
167 p0: left(),
168 p1: bottom(),
169 }],
170 3 => vec![Segment {
171 p0: left(),
172 p1: right(),
173 }],
174 4 => vec![Segment {
175 p0: top(),
176 p1: right(),
177 }],
178 5 => {
179 let center = (tl + tr + bl + br) * 0.25;
181 if center >= level {
182 vec![
183 Segment {
184 p0: top(),
185 p1: right(),
186 },
187 Segment {
188 p0: left(),
189 p1: bottom(),
190 },
191 ]
192 } else {
193 vec![
194 Segment {
195 p0: top(),
196 p1: left(),
197 },
198 Segment {
199 p0: bottom(),
200 p1: right(),
201 },
202 ]
203 }
204 }
205 6 => vec![Segment {
206 p0: top(),
207 p1: bottom(),
208 }],
209 7 => vec![Segment {
210 p0: top(),
211 p1: left(),
212 }],
213 8 => vec![Segment {
214 p0: top(),
215 p1: left(),
216 }],
217 9 => vec![Segment {
218 p0: top(),
219 p1: bottom(),
220 }],
221 10 => {
222 let center = (tl + tr + bl + br) * 0.25;
224 if center >= level {
225 vec![
226 Segment {
227 p0: top(),
228 p1: left(),
229 },
230 Segment {
231 p0: bottom(),
232 p1: right(),
233 },
234 ]
235 } else {
236 vec![
237 Segment {
238 p0: top(),
239 p1: right(),
240 },
241 Segment {
242 p0: left(),
243 p1: bottom(),
244 },
245 ]
246 }
247 }
248 11 => vec![Segment {
249 p0: top(),
250 p1: right(),
251 }],
252 12 => vec![Segment {
253 p0: left(),
254 p1: right(),
255 }],
256 13 => vec![Segment {
257 p0: left(),
258 p1: bottom(),
259 }],
260 14 => vec![Segment {
261 p0: bottom(),
262 p1: right(),
263 }],
264 _ => vec![], }
266}
267
268#[inline]
270fn is_nodata(val: f64, nodata: Option<f64>) -> bool {
271 if !val.is_finite() {
272 return true;
273 }
274 if let Some(nd) = nodata {
275 (val - nd).abs() < 1e-10
276 } else {
277 false
278 }
279}
280
281fn chain_segments(segments: &[Segment]) -> Vec<(Vec<ContourPoint>, bool)> {
283 if segments.is_empty() {
284 return vec![];
285 }
286
287 let eps = 1e-9;
290
291 let points_eq = |a: &ContourPoint, b: &ContourPoint| -> bool {
292 (a.x - b.x).abs() < eps && (a.y - b.y).abs() < eps
293 };
294
295 let mut chains: Vec<Vec<ContourPoint>> = Vec::new();
296
297 for seg in segments {
298 let mut attached = false;
299
300 for chain in chains.iter_mut() {
301 let first = chain[0];
302 let last = chain[chain.len() - 1];
303
304 if points_eq(&last, &seg.p0) {
305 chain.push(seg.p1);
306 attached = true;
307 break;
308 } else if points_eq(&last, &seg.p1) {
309 chain.push(seg.p0);
310 attached = true;
311 break;
312 } else if points_eq(&first, &seg.p1) {
313 chain.insert(0, seg.p0);
314 attached = true;
315 break;
316 } else if points_eq(&first, &seg.p0) {
317 chain.insert(0, seg.p1);
318 attached = true;
319 break;
320 }
321 }
322
323 if !attached {
324 chains.push(vec![seg.p0, seg.p1]);
325 }
326 }
327
328 let mut merged = true;
330 while merged {
331 merged = false;
332 let mut i = 0;
333 while i < chains.len() {
334 let mut j = i + 1;
335 while j < chains.len() {
336 let i_first = chains[i][0];
337 let i_last = chains[i][chains[i].len() - 1];
338 let j_first = chains[j][0];
339 let j_last = chains[j][chains[j].len() - 1];
340
341 if points_eq(&i_last, &j_first) {
342 let mut taken = chains.remove(j);
343 taken.remove(0); chains[i].append(&mut taken);
345 merged = true;
346 continue; } else if points_eq(&i_first, &j_last) {
348 let mut taken = chains.remove(j);
349 taken.pop(); taken.append(&mut chains[i]);
351 chains[i] = taken;
352 merged = true;
353 continue;
354 } else if points_eq(&i_last, &j_last) {
355 let mut taken = chains.remove(j);
356 taken.reverse();
357 taken.remove(0);
358 chains[i].append(&mut taken);
359 merged = true;
360 continue;
361 } else if points_eq(&i_first, &j_first) {
362 let mut taken = chains.remove(j);
363 taken.reverse();
364 taken.pop();
365 taken.append(&mut chains[i]);
366 chains[i] = taken;
367 merged = true;
368 continue;
369 }
370 j += 1;
371 }
372 i += 1;
373 }
374 }
375
376 chains
377 .into_iter()
378 .map(|pts: Vec<ContourPoint>| {
379 let closed = pts.len() >= 3 && points_eq(&pts[0], &pts[pts.len() - 1]);
380 (pts, closed)
381 })
382 .collect()
383}
384
385pub fn generate_contours(
399 data: &[f64],
400 width: usize,
401 height: usize,
402 config: &ContourConfig,
403) -> Result<Vec<ContourLine>> {
404 if width == 0 || height == 0 {
406 return Ok(vec![]);
407 }
408
409 if data.len() != width * height {
410 return Err(AlgorithmError::InvalidDimensions {
411 message: "data length must equal width * height",
412 actual: data.len(),
413 expected: width * height,
414 });
415 }
416
417 if width < 2 || height < 2 {
419 return Ok(vec![]);
420 }
421
422 let mut min_val = f64::INFINITY;
424 let mut max_val = f64::NEG_INFINITY;
425 for &v in data {
426 if is_nodata(v, config.nodata) {
427 continue;
428 }
429 if v < min_val {
430 min_val = v;
431 }
432 if v > max_val {
433 max_val = v;
434 }
435 }
436
437 if !min_val.is_finite() || !max_val.is_finite() {
438 return Ok(vec![]);
439 }
440
441 let levels = config.compute_levels(min_val, max_val);
442 if levels.is_empty() {
443 return Ok(vec![]);
444 }
445
446 let mut result: BTreeMap<i64, Vec<Segment>> = BTreeMap::new();
448
449 for &level in &levels {
451 let key = (level * 1e10) as i64;
453 let segments = result.entry(key).or_default();
454
455 for row in 0..height - 1 {
456 for col in 0..width - 1 {
457 let tl = data[row * width + col];
458 let tr = data[row * width + col + 1];
459 let bl = data[(row + 1) * width + col];
460 let br = data[(row + 1) * width + col + 1];
461
462 if is_nodata(tl, config.nodata)
464 || is_nodata(tr, config.nodata)
465 || is_nodata(bl, config.nodata)
466 || is_nodata(br, config.nodata)
467 {
468 continue;
469 }
470
471 let cell_segs = cell_segments(col, row, tl, tr, bl, br, level);
472 segments.extend_from_slice(&cell_segs);
473 }
474 }
475 }
476
477 let mut contour_lines: Vec<ContourLine> = Vec::new();
479
480 for level in levels.iter() {
481 let key = (*level * 1e10) as i64;
482 if let Some(segments) = result.get(&key) {
483 let chains: Vec<(Vec<ContourPoint>, bool)> = chain_segments(segments);
484 for (points, is_closed) in chains {
485 if points.len() >= 2 {
486 contour_lines.push(ContourLine {
487 level: *level,
488 points,
489 is_closed,
490 });
491 }
492 }
493 }
494 }
495
496 Ok(contour_lines)
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502
503 #[test]
504 fn test_contour_config_valid() {
505 let config = ContourConfig::new(10.0);
506 assert!(config.is_ok());
507 let config = config.expect("should be ok");
508 assert!((config.interval - 10.0).abs() < f64::EPSILON);
509 assert!((config.base - 0.0).abs() < f64::EPSILON);
510 assert!(config.nodata.is_none());
511 }
512
513 #[test]
514 fn test_contour_config_zero_interval_error() {
515 let result = ContourConfig::new(0.0);
516 assert!(result.is_err());
517 }
518
519 #[test]
520 fn test_contour_config_negative_interval_error() {
521 let result = ContourConfig::new(-5.0);
522 assert!(result.is_err());
523 }
524
525 #[test]
526 fn test_compute_levels() {
527 let config = ContourConfig::new(10.0).expect("valid config");
528 let levels = config.compute_levels(5.0, 35.0);
529 assert_eq!(levels, vec![10.0, 20.0, 30.0]);
530 }
531
532 #[test]
533 fn test_contour_flat_grid() {
534 let data = vec![100.0; 16];
535 let config = ContourConfig::new(10.0).expect("valid config");
536 let contours = generate_contours(&data, 4, 4, &config).expect("should succeed");
537 assert!(contours.is_empty());
538 }
539
540 #[test]
541 fn test_contour_simple_slope() {
542 let data = vec![
548 0.0, 10.0, 20.0, 30.0, 0.0, 10.0, 20.0, 30.0, 0.0, 10.0, 20.0, 30.0, 0.0, 10.0, 20.0,
549 30.0,
550 ];
551 let config = ContourConfig::new(15.0).expect("valid config");
552 let contours = generate_contours(&data, 4, 4, &config).expect("should succeed");
553
554 assert!(!contours.is_empty());
556 let levels: Vec<f64> = contours.iter().map(|c| c.level).collect();
557 assert!(levels.contains(&15.0));
558
559 for c in &contours {
561 assert!(c.points.len() >= 2);
562 }
563 }
564
565 #[test]
566 fn test_contour_single_level() {
567 let data = vec![0.0, 5.0, 10.0, 0.0, 5.0, 10.0, 0.0, 5.0, 10.0];
572 let config = ContourConfig::new(3.0).expect("valid config");
573 let contours = generate_contours(&data, 3, 3, &config).expect("should succeed");
574
575 let levels: Vec<f64> = contours.iter().map(|c| c.level).collect();
577 assert!(levels.contains(&3.0));
578 assert!(levels.contains(&6.0));
579 assert!(levels.contains(&9.0));
580 }
581
582 #[test]
583 fn test_contour_nodata_handling() {
584 let nodata = -9999.0;
586 let data = vec![0.0, 5.0, 10.0, 0.0, nodata, 10.0, 0.0, 5.0, 10.0];
587 let config = ContourConfig::new(4.0)
588 .expect("valid config")
589 .with_nodata(nodata);
590 let contours = generate_contours(&data, 3, 3, &config).expect("should succeed");
591
592 assert!(contours.is_empty());
603 }
604
605 #[test]
606 fn test_contour_closed_loop() {
607 let data = vec![
610 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 5.0, 5.0, 5.0, 0.0, 0.0, 5.0, 20.0, 5.0, 0.0, 0.0, 5.0,
611 5.0, 5.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
612 ];
613 let config = ContourConfig::new(10.0).expect("valid config");
614 let contours = generate_contours(&data, 5, 5, &config).expect("should succeed");
615
616 assert!(!contours.is_empty());
618 let at_10: Vec<&ContourLine> = contours
619 .iter()
620 .filter(|c| (c.level - 10.0).abs() < 1e-10)
621 .collect();
622 assert!(!at_10.is_empty());
623
624 let has_closed = at_10.iter().any(|c| c.is_closed);
626 assert!(has_closed, "expected a closed contour around the hill");
627 }
628
629 #[test]
630 fn test_contour_saddle_point() {
631 let data = vec![10.0, 0.0, 10.0, 0.0, 10.0, 0.0, 10.0, 0.0, 10.0];
638 let config = ContourConfig::new(4.0).expect("valid config");
639 let contours = generate_contours(&data, 3, 3, &config).expect("should succeed");
640
641 assert!(!contours.is_empty());
643
644 for c in &contours {
646 assert!(c.points.len() >= 2);
647 for p in &c.points {
648 assert!(p.x.is_finite() && p.y.is_finite());
649 }
650 }
651 }
652
653 #[test]
654 fn test_contour_empty_grid() {
655 let config = ContourConfig::new(10.0).expect("valid config");
656
657 let contours = generate_contours(&[], 0, 0, &config).expect("should succeed");
659 assert!(contours.is_empty());
660
661 let contours = generate_contours(&[], 0, 5, &config).expect("should succeed");
663 assert!(contours.is_empty());
664
665 let contours = generate_contours(&[], 5, 0, &config).expect("should succeed");
667 assert!(contours.is_empty());
668 }
669
670 #[test]
671 fn test_contour_wrong_data_size() {
672 let data = vec![1.0, 2.0, 3.0]; let config = ContourConfig::new(10.0).expect("valid config");
674
675 let result = generate_contours(&data, 2, 2, &config);
677 assert!(result.is_err());
678 }
679
680 #[test]
681 fn test_compute_levels_with_base() {
682 let config = ContourConfig::new(10.0)
683 .expect("valid config")
684 .with_base(5.0);
685 let levels = config.compute_levels(0.0, 30.0);
686 assert!(levels.contains(&15.0));
689 assert!(levels.contains(&25.0));
690 }
691
692 #[test]
693 fn test_contour_config_nan_interval() {
694 let result = ContourConfig::new(f64::NAN);
695 assert!(result.is_err());
696 }
697
698 #[test]
699 fn test_contour_config_inf_interval() {
700 let result = ContourConfig::new(f64::INFINITY);
701 assert!(result.is_err());
702 }
703}