1use std::collections::HashMap;
20
21use ndarray::Array2;
22
23use crate::error::{Error, Result};
24
25#[derive(Clone, Debug)]
27pub struct ContourLine {
28 pub elevation: f64,
30 pub coordinates: Vec<(f64, f64)>,
32}
33
34#[derive(Clone, Debug)]
36pub struct ContourConfig {
37 pub interval: f64,
39 pub base: f64,
42 pub min_value: Option<f64>,
44 pub max_value: Option<f64>,
46}
47
48impl ContourConfig {
49 pub fn new(interval: f64) -> Self {
51 Self {
52 interval,
53 base: 0.0,
54 min_value: None,
55 max_value: None,
56 }
57 }
58}
59
60pub fn contour(dem: &Array2<f64>, config: &ContourConfig) -> Result<Vec<ContourLine>> {
78 if !config.interval.is_finite() || config.interval <= 0.0 {
79 return Err(Error::InvalidContourInterval(config.interval));
80 }
81
82 let (h, w) = dem.dim();
83 if h < 2 || w < 2 {
84 return Ok(Vec::new());
85 }
86
87 let mut min_elev = f64::INFINITY;
89 let mut max_elev = f64::NEG_INFINITY;
90 for &v in dem.iter() {
91 if v.is_finite() {
92 if v < min_elev {
93 min_elev = v;
94 }
95 if v > max_elev {
96 max_elev = v;
97 }
98 }
99 }
100
101 if !min_elev.is_finite() || !max_elev.is_finite() {
102 return Ok(Vec::new());
103 }
104
105 let first_level =
107 ((min_elev - config.base) / config.interval).ceil() * config.interval + config.base;
108
109 let mut contours = Vec::new();
110 let mut level = first_level;
111
112 while level <= max_elev {
113 if let Some(min) = config.min_value {
115 if level < min {
116 level += config.interval;
117 continue;
118 }
119 }
120 if let Some(max) = config.max_value {
121 if level > max {
122 break;
123 }
124 }
125
126 let segments = march_squares(dem, level);
127 let lines = connect_segments(segments);
128
129 for coords in lines {
130 if coords.len() >= 2 {
131 contours.push(ContourLine {
132 elevation: level,
133 coordinates: coords,
134 });
135 }
136 }
137
138 level += config.interval;
139 }
140
141 Ok(contours)
142}
143
144type Segment = ((f64, f64), (f64, f64));
150
151fn march_squares(grid: &Array2<f64>, level: f64) -> Vec<Segment> {
153 let (h, w) = grid.dim();
154 let mut segments = Vec::new();
155
156 for row in 0..h - 1 {
157 for col in 0..w - 1 {
158 let tl = grid[[row, col]];
159 let tr = grid[[row, col + 1]];
160 let br = grid[[row + 1, col + 1]];
161 let bl = grid[[row + 1, col]];
162
163 if tl.is_nan() || tr.is_nan() || br.is_nan() || bl.is_nan() {
164 continue;
165 }
166
167 let mut case = 0u8;
169 if tl >= level {
170 case |= 1;
171 }
172 if tr >= level {
173 case |= 2;
174 }
175 if br >= level {
176 case |= 4;
177 }
178 if bl >= level {
179 case |= 8;
180 }
181
182 if case == 0 || case == 15 {
183 continue;
184 }
185
186 let top = lerp_h(col as f64, tl, (col + 1) as f64, tr, level, row as f64);
187 let right = lerp_v(
188 row as f64,
189 tr,
190 (row + 1) as f64,
191 br,
192 level,
193 (col + 1) as f64,
194 );
195 let bottom = lerp_h(
196 col as f64,
197 bl,
198 (col + 1) as f64,
199 br,
200 level,
201 (row + 1) as f64,
202 );
203 let left = lerp_v(row as f64, tl, (row + 1) as f64, bl, level, col as f64);
204
205 match case {
206 1 | 14 => segments.push((top, left)),
207 2 | 13 => segments.push((top, right)),
208 3 | 12 => segments.push((left, right)),
209 4 | 11 => segments.push((right, bottom)),
210 5 => {
211 segments.push((top, right));
212 segments.push((bottom, left));
213 }
214 6 | 9 => segments.push((top, bottom)),
215 7 | 8 => segments.push((left, bottom)),
216 10 => {
217 segments.push((top, left));
218 segments.push((right, bottom));
219 }
220 _ => {}
221 }
222 }
223 }
224
225 segments
226}
227
228#[inline]
230fn lerp_h(col0: f64, val0: f64, col1: f64, val1: f64, level: f64, fixed_row: f64) -> (f64, f64) {
231 let t = interp_t(val0, val1, level);
232 (col0 + t * (col1 - col0), fixed_row)
233}
234
235#[inline]
237fn lerp_v(row0: f64, val0: f64, row1: f64, val1: f64, level: f64, fixed_col: f64) -> (f64, f64) {
238 let t = interp_t(val0, val1, level);
239 (fixed_col, row0 + t * (row1 - row0))
240}
241
242#[inline]
243fn interp_t(val0: f64, val1: f64, level: f64) -> f64 {
244 let denom = val1 - val0;
245 if denom.abs() < f64::EPSILON {
246 0.5
247 } else {
248 ((level - val0) / denom).clamp(0.0, 1.0)
249 }
250}
251
252fn point_key(p: (f64, f64)) -> (i64, i64) {
259 ((p.0 * 1e6).round() as i64, (p.1 * 1e6).round() as i64)
260}
261
262fn connect_segments(segments: Vec<Segment>) -> Vec<Vec<(f64, f64)>> {
266 if segments.is_empty() {
267 return Vec::new();
268 }
269
270 let mut endpoint_map: HashMap<(i64, i64), Vec<usize>> =
272 HashMap::with_capacity(segments.len() * 2);
273
274 for (i, seg) in segments.iter().enumerate() {
275 endpoint_map.entry(point_key(seg.0)).or_default().push(i);
276 endpoint_map.entry(point_key(seg.1)).or_default().push(i);
277 }
278
279 let mut used = vec![false; segments.len()];
280 let mut lines: Vec<Vec<(f64, f64)>> = Vec::new();
281
282 for start_idx in 0..segments.len() {
283 if used[start_idx] {
284 continue;
285 }
286 used[start_idx] = true;
287
288 let mut line = vec![segments[start_idx].0, segments[start_idx].1];
289
290 loop {
292 let end = *line.last().unwrap();
293 let key = point_key(end);
294 let mut found = false;
295
296 if let Some(indices) = endpoint_map.get(&key) {
297 for &j in indices {
298 if used[j] {
299 continue;
300 }
301 let seg = &segments[j];
302 if point_key(seg.0) == key {
303 line.push(seg.1);
304 used[j] = true;
305 found = true;
306 break;
307 } else if point_key(seg.1) == key {
308 line.push(seg.0);
309 used[j] = true;
310 found = true;
311 break;
312 }
313 }
314 }
315
316 if !found {
317 break;
318 }
319 }
320
321 lines.push(line);
322 }
323
324 lines
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn gradient_produces_contours() {
333 let dem = Array2::from_shape_fn((10, 10), |(r, _)| r as f64 * 10.0);
334 let lines = contour(&dem, &ContourConfig::new(20.0)).unwrap();
335 assert!(!lines.is_empty(), "gradient should produce contour lines");
336 let levels: Vec<f64> = lines.iter().map(|l| l.elevation).collect();
337 assert!(
338 levels.contains(&20.0),
339 "should have contour at 20, got {levels:?}"
340 );
341 assert!(
342 levels.contains(&40.0),
343 "should have contour at 40, got {levels:?}"
344 );
345 }
346
347 #[test]
348 fn flat_dem_no_contours() {
349 let dem = Array2::from_elem((10, 10), 100.0);
350 let lines = contour(&dem, &ContourConfig::new(10.0)).unwrap();
351 assert!(
352 lines.len() <= 1,
353 "flat DEM should produce 0 or 1 contour lines"
354 );
355 }
356
357 #[test]
358 fn invalid_interval_errors() {
359 let dem = Array2::from_elem((5, 5), 10.0);
360 assert!(contour(&dem, &ContourConfig::new(0.0)).is_err());
361 assert!(contour(&dem, &ContourConfig::new(-5.0)).is_err());
362 assert!(contour(&dem, &ContourConfig::new(f64::NAN)).is_err());
363 }
364
365 #[test]
366 fn small_grid_empty() {
367 let dem = Array2::from_elem((1, 1), 100.0);
368 let lines = contour(&dem, &ContourConfig::new(10.0)).unwrap();
369 assert!(lines.is_empty());
370 }
371
372 #[test]
373 fn min_max_filter() {
374 let dem = Array2::from_shape_fn((10, 10), |(r, _)| r as f64 * 10.0);
375 let config = ContourConfig {
376 interval: 10.0,
377 base: 0.0,
378 min_value: Some(25.0),
379 max_value: Some(55.0),
380 };
381 let lines = contour(&dem, &config).unwrap();
382 for line in &lines {
383 assert!(line.elevation >= 25.0);
384 assert!(line.elevation <= 55.0);
385 }
386 }
387
388 #[test]
389 fn contours_have_coordinates() {
390 let dem = Array2::from_shape_fn((5, 5), |(r, _)| r as f64 * 25.0);
391 let lines = contour(&dem, &ContourConfig::new(25.0)).unwrap();
392 for l in &lines {
393 assert!(
394 l.coordinates.len() >= 2,
395 "contour should have >= 2 points, got {}",
396 l.coordinates.len()
397 );
398 }
399 }
400
401 #[test]
402 fn base_alignment() {
403 let dem = Array2::from_shape_fn((10, 10), |(r, _)| r as f64 * 10.0);
405 let config = ContourConfig {
406 interval: 10.0,
407 base: 5.0,
408 min_value: None,
409 max_value: None,
410 };
411 let lines = contour(&dem, &config).unwrap();
412 for l in &lines {
413 let offset = (l.elevation - 5.0) % 10.0;
414 assert!(
415 offset.abs() < 1e-6 || (offset - 10.0).abs() < 1e-6,
416 "contour at {} not aligned to base 5 + interval 10",
417 l.elevation
418 );
419 }
420 }
421}