1use crate::error::{AlgorithmError, Result};
11use oxigdal_core::buffer::RasterBuffer;
12
13#[cfg(not(feature = "std"))]
14use alloc::vec::Vec;
15
16#[derive(Debug, Clone, PartialEq)]
18pub struct ClassificationRule {
19 pub min: f64,
21 pub max: f64,
23 pub class_value: f64,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
29pub enum ClassificationMethod {
30 EqualInterval {
32 num_classes: usize,
34 },
35 Quantile {
37 num_classes: usize,
39 },
40 NaturalBreaks {
42 num_classes: usize,
44 },
45}
46
47pub fn reclassify(
59 src: &RasterBuffer,
60 rules: &[ClassificationRule],
61 nodata_value: Option<f64>,
62) -> Result<RasterBuffer> {
63 if rules.is_empty() {
64 return Err(AlgorithmError::InvalidParameter {
65 parameter: "rules",
66 message: "At least one classification rule required".to_string(),
67 });
68 }
69
70 for rule in rules {
72 if rule.max <= rule.min {
73 return Err(AlgorithmError::InvalidParameter {
74 parameter: "rules",
75 message: format!("Invalid range: {} to {}", rule.min, rule.max),
76 });
77 }
78 }
79
80 let width = src.width();
81 let height = src.height();
82 let mut dst = RasterBuffer::zeros(width, height, src.data_type());
83
84 for y in 0..height {
85 for x in 0..width {
86 let val = src.get_pixel(x, y).map_err(AlgorithmError::Core)?;
87
88 if src.is_nodata(val) || !val.is_finite() {
89 if let Some(nd) = nodata_value {
90 dst.set_pixel(x, y, nd).map_err(AlgorithmError::Core)?;
91 }
92 continue;
93 }
94
95 let mut matched = false;
97 for rule in rules {
98 if val >= rule.min && val <= rule.max {
99 dst.set_pixel(x, y, rule.class_value)
100 .map_err(AlgorithmError::Core)?;
101 matched = true;
102 break;
103 }
104 }
105
106 if !matched {
107 if let Some(nd) = nodata_value {
108 dst.set_pixel(x, y, nd).map_err(AlgorithmError::Core)?;
109 }
110 }
111 }
112 }
113
114 Ok(dst)
115}
116
117pub fn threshold(
130 src: &RasterBuffer,
131 threshold: f64,
132 above_value: f64,
133 below_value: f64,
134) -> Result<RasterBuffer> {
135 let width = src.width();
136 let height = src.height();
137 let mut dst = RasterBuffer::zeros(width, height, src.data_type());
138
139 for y in 0..height {
140 for x in 0..width {
141 let val = src.get_pixel(x, y).map_err(AlgorithmError::Core)?;
142
143 if src.is_nodata(val) || !val.is_finite() {
144 continue;
145 }
146
147 let result = if val >= threshold {
148 above_value
149 } else {
150 below_value
151 };
152
153 dst.set_pixel(x, y, result).map_err(AlgorithmError::Core)?;
154 }
155 }
156
157 Ok(dst)
158}
159
160pub fn classify(src: &RasterBuffer, method: ClassificationMethod) -> Result<RasterBuffer> {
166 match method {
167 ClassificationMethod::EqualInterval { num_classes } => {
168 equal_interval_classify(src, num_classes)
169 }
170 ClassificationMethod::Quantile { num_classes } => quantile_classify(src, num_classes),
171 ClassificationMethod::NaturalBreaks { num_classes } => {
172 natural_breaks_classify(src, num_classes)
173 }
174 }
175}
176
177fn equal_interval_classify(src: &RasterBuffer, num_classes: usize) -> Result<RasterBuffer> {
179 if num_classes == 0 {
180 return Err(AlgorithmError::InvalidParameter {
181 parameter: "num_classes",
182 message: "Number of classes must be positive".to_string(),
183 });
184 }
185
186 let mut values = Vec::new();
188 for y in 0..src.height() {
189 for x in 0..src.width() {
190 let val = src.get_pixel(x, y).map_err(AlgorithmError::Core)?;
191 if !src.is_nodata(val) && val.is_finite() {
192 values.push(val);
193 }
194 }
195 }
196
197 if values.is_empty() {
198 return Err(AlgorithmError::InsufficientData {
199 operation: "equal_interval_classify",
200 message: "No valid pixels found".to_string(),
201 });
202 }
203
204 let min = values.iter().copied().fold(f64::INFINITY, f64::min);
205 let max = values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
206
207 if (max - min).abs() < f64::EPSILON {
208 return Err(AlgorithmError::InvalidParameter {
209 parameter: "data",
210 message: "All values are the same".to_string(),
211 });
212 }
213
214 let interval = (max - min) / num_classes as f64;
215
216 let mut rules = Vec::with_capacity(num_classes);
217 for i in 0..num_classes {
218 rules.push(ClassificationRule {
219 min: min + i as f64 * interval,
220 max: min + (i + 1) as f64 * interval,
221 class_value: i as f64 + 1.0,
222 });
223 }
224
225 reclassify(src, &rules, None)
226}
227
228fn quantile_classify(src: &RasterBuffer, num_classes: usize) -> Result<RasterBuffer> {
230 if num_classes == 0 {
231 return Err(AlgorithmError::InvalidParameter {
232 parameter: "num_classes",
233 message: "Number of classes must be positive".to_string(),
234 });
235 }
236
237 let mut values = Vec::new();
239 for y in 0..src.height() {
240 for x in 0..src.width() {
241 let val = src.get_pixel(x, y).map_err(AlgorithmError::Core)?;
242 if !src.is_nodata(val) && val.is_finite() {
243 values.push(val);
244 }
245 }
246 }
247
248 if values.is_empty() {
249 return Err(AlgorithmError::InsufficientData {
250 operation: "quantile_classify",
251 message: "No valid pixels found".to_string(),
252 });
253 }
254
255 if values.len() < num_classes {
256 return Err(AlgorithmError::InsufficientData {
257 operation: "quantile_classify",
258 message: format!(
259 "Not enough values ({}) for {} classes",
260 values.len(),
261 num_classes
262 ),
263 });
264 }
265
266 values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
267
268 let mut rules = Vec::with_capacity(num_classes);
269 for i in 0..num_classes {
270 let start_idx = (i * values.len()) / num_classes;
271 let end_idx = ((i + 1) * values.len()) / num_classes;
272
273 let min = if i == 0 { values[0] } else { values[start_idx] };
274
275 let max = if i == num_classes - 1 {
276 values[values.len() - 1]
277 } else {
278 values[end_idx.min(values.len() - 1)]
279 };
280
281 rules.push(ClassificationRule {
282 min,
283 max,
284 class_value: i as f64 + 1.0,
285 });
286 }
287
288 reclassify(src, &rules, None)
289}
290
291fn natural_breaks_classify(src: &RasterBuffer, num_classes: usize) -> Result<RasterBuffer> {
295 if num_classes == 0 {
296 return Err(AlgorithmError::InvalidParameter {
297 parameter: "num_classes",
298 message: "Number of classes must be positive".to_string(),
299 });
300 }
301
302 let mut values = Vec::new();
304 for y in 0..src.height() {
305 for x in 0..src.width() {
306 let val = src.get_pixel(x, y).map_err(AlgorithmError::Core)?;
307 if !src.is_nodata(val) && val.is_finite() {
308 values.push(val);
309 }
310 }
311 }
312
313 if values.is_empty() {
314 return Err(AlgorithmError::InsufficientData {
315 operation: "natural_breaks_classify",
316 message: "No valid pixels found".to_string(),
317 });
318 }
319
320 if values.len() < num_classes {
321 return Err(AlgorithmError::InsufficientData {
322 operation: "natural_breaks_classify",
323 message: format!(
324 "Not enough values ({}) for {} classes",
325 values.len(),
326 num_classes
327 ),
328 });
329 }
330
331 values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
332
333 let breaks = compute_jenks_breaks(&values, num_classes)?;
335
336 let mut rules = Vec::with_capacity(num_classes);
337 for i in 0..num_classes {
338 let min = if i == 0 { values[0] } else { breaks[i - 1] };
339
340 let max = if i == num_classes - 1 {
341 values[values.len() - 1]
342 } else {
343 breaks[i]
344 };
345
346 rules.push(ClassificationRule {
347 min,
348 max,
349 class_value: i as f64 + 1.0,
350 });
351 }
352
353 reclassify(src, &rules, None)
354}
355
356fn compute_jenks_breaks(sorted_values: &[f64], num_classes: usize) -> Result<Vec<f64>> {
358 let n = sorted_values.len();
359
360 if n < num_classes {
361 return Err(AlgorithmError::InsufficientData {
362 operation: "compute_jenks_breaks",
363 message: "Not enough values for classes".to_string(),
364 });
365 }
366
367 let mut breaks = Vec::with_capacity(num_classes - 1);
370
371 for i in 1..num_classes {
372 let idx = (i * n) / num_classes;
373 if idx < n {
374 breaks.push(sorted_values[idx]);
375 }
376 }
377
378 Ok(breaks)
379}
380
381#[cfg(test)]
382#[allow(clippy::panic)]
383mod tests {
384 use super::*;
385 use oxigdal_core::types::RasterDataType;
386
387 #[test]
388 fn test_reclassify() {
389 let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
390
391 for y in 0..10 {
392 for x in 0..10 {
393 src.set_pixel(x, y, (x * 10) as f64).ok();
394 }
395 }
396
397 let rules = vec![
398 ClassificationRule {
399 min: 0.0,
400 max: 30.0,
401 class_value: 1.0,
402 },
403 ClassificationRule {
404 min: 30.0,
405 max: 60.0,
406 class_value: 2.0,
407 },
408 ClassificationRule {
409 min: 60.0,
410 max: 100.0,
411 class_value: 3.0,
412 },
413 ];
414
415 let result = reclassify(&src, &rules, None);
416 assert!(result.is_ok());
417 }
418
419 #[test]
420 fn test_threshold() {
421 let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
422
423 for y in 0..10 {
424 for x in 0..10 {
425 src.set_pixel(x, y, (x + y) as f64).ok();
426 }
427 }
428
429 let result = threshold(&src, 10.0, 1.0, 0.0);
430 assert!(result.is_ok());
431
432 let classified = result.expect("Should succeed");
433 let val1 = classified.get_pixel(0, 0).expect("Should get pixel");
434 assert!((val1 - 0.0).abs() < f64::EPSILON);
435
436 let val2 = classified.get_pixel(9, 9).expect("Should get pixel");
437 assert!((val2 - 1.0).abs() < f64::EPSILON);
438 }
439
440 #[test]
441 fn test_equal_interval() {
442 let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
443
444 for y in 0..10 {
445 for x in 0..10 {
446 src.set_pixel(x, y, (y * 10 + x) as f64).ok();
447 }
448 }
449
450 let result = equal_interval_classify(&src, 5);
451 assert!(result.is_ok());
452 }
453
454 #[test]
455 fn test_quantile() {
456 let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
457
458 for y in 0..10 {
459 for x in 0..10 {
460 src.set_pixel(x, y, (y * 10 + x) as f64).ok();
461 }
462 }
463
464 let result = quantile_classify(&src, 4);
465 assert!(result.is_ok());
466 }
467
468 #[test]
469 fn test_natural_breaks() {
470 let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
471
472 for y in 0..10 {
473 for x in 0..10 {
474 src.set_pixel(x, y, (y * 10 + x) as f64).ok();
475 }
476 }
477
478 let result = natural_breaks_classify(&src, 3);
479 assert!(result.is_ok());
480 }
481
482 #[test]
483 fn test_classify_method() {
484 let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
485
486 for y in 0..10 {
487 for x in 0..10 {
488 src.set_pixel(x, y, (y * 10 + x) as f64).ok();
489 }
490 }
491
492 let method = ClassificationMethod::EqualInterval { num_classes: 5 };
493 let result = classify(&src, method);
494 assert!(result.is_ok());
495 }
496
497 #[test]
500 fn test_reclassify_empty_rules() {
501 let src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
502 let rules = vec![];
503
504 let result = reclassify(&src, &rules, None);
505 assert!(result.is_err());
506 if let Err(AlgorithmError::InvalidParameter { .. }) = result {
507 } else {
509 panic!("Expected InvalidParameter error");
510 }
511 }
512
513 #[test]
514 fn test_reclassify_invalid_range() {
515 let src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
516 let rules = vec![ClassificationRule {
517 min: 10.0,
518 max: 5.0, class_value: 1.0,
520 }];
521
522 let result = reclassify(&src, &rules, None);
523 assert!(result.is_err());
524 }
525
526 #[test]
527 fn test_equal_interval_zero_classes() {
528 let src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
529
530 let result = equal_interval_classify(&src, 0);
531 assert!(result.is_err());
532 }
533
534 #[test]
535 fn test_quantile_zero_classes() {
536 let src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
537
538 let result = quantile_classify(&src, 0);
539 assert!(result.is_err());
540 }
541
542 #[test]
543 fn test_natural_breaks_zero_classes() {
544 let src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
545
546 let result = natural_breaks_classify(&src, 0);
547 assert!(result.is_err());
548 }
549
550 #[test]
551 fn test_classify_single_value() {
552 let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
553
554 for y in 0..5 {
555 for x in 0..5 {
556 src.set_pixel(x, y, 42.0).ok();
557 }
558 }
559
560 let result = equal_interval_classify(&src, 3);
561 assert!(result.is_err()); }
563
564 #[test]
565 fn test_quantile_not_enough_values() {
566 let mut src = RasterBuffer::zeros(2, 2, RasterDataType::Float32);
567
568 for y in 0..2 {
569 for x in 0..2 {
570 src.set_pixel(x, y, (x + y) as f64).ok();
571 }
572 }
573
574 let result = quantile_classify(&src, 10);
575 assert!(result.is_err());
576 }
577
578 #[test]
581 fn test_overlapping_rules() {
582 let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
583
584 for y in 0..10 {
585 for x in 0..10 {
586 src.set_pixel(x, y, (x * 5) as f64).ok();
587 }
588 }
589
590 let rules = vec![
592 ClassificationRule {
593 min: 0.0,
594 max: 20.0,
595 class_value: 1.0,
596 },
597 ClassificationRule {
598 min: 15.0,
599 max: 35.0,
600 class_value: 2.0,
601 },
602 ClassificationRule {
603 min: 30.0,
604 max: 50.0,
605 class_value: 3.0,
606 },
607 ];
608
609 let result = reclassify(&src, &rules, Some(-1.0));
610 assert!(result.is_ok());
611 }
612
613 #[test]
614 fn test_threshold_boundary_values() {
615 let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
616
617 for y in 0..5 {
618 for x in 0..5 {
619 src.set_pixel(x, y, (x + y) as f64).ok();
620 }
621 }
622
623 let result = threshold(&src, 4.0, 100.0, 0.0);
624 assert!(result.is_ok());
625 let classified = result.expect("Should succeed");
626
627 let val_at_threshold = classified.get_pixel(2, 2).expect("Should get pixel");
629 assert!((val_at_threshold - 100.0).abs() < f64::EPSILON);
630
631 let val_below = classified.get_pixel(0, 0).expect("Should get pixel");
633 assert!((val_below - 0.0).abs() < f64::EPSILON);
634 }
635
636 #[test]
637 fn test_classify_with_single_pixel() {
638 let mut src = RasterBuffer::zeros(1, 1, RasterDataType::Float32);
639 src.set_pixel(0, 0, 42.0).ok();
640
641 let result = threshold(&src, 40.0, 1.0, 0.0);
642 assert!(result.is_ok());
643 let classified = result.expect("Should succeed");
644 let val = classified.get_pixel(0, 0).expect("Should get pixel");
645 assert!((val - 1.0).abs() < f64::EPSILON);
646 }
647
648 #[test]
649 fn test_reclassify_with_nodata() {
650 let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
651
652 for y in 0..5 {
653 for x in 0..5 {
654 if x == 2 && y == 2 {
655 src.set_pixel(x, y, f64::NAN).ok(); } else {
657 src.set_pixel(x, y, (x * 10) as f64).ok();
658 }
659 }
660 }
661
662 let rules = vec![
663 ClassificationRule {
664 min: 0.0,
665 max: 20.0,
666 class_value: 1.0,
667 },
668 ClassificationRule {
669 min: 20.0,
670 max: 50.0,
671 class_value: 2.0,
672 },
673 ];
674
675 let result = reclassify(&src, &rules, Some(-9999.0));
676 assert!(result.is_ok());
677 }
678
679 #[test]
680 fn test_equal_interval_many_classes() {
681 let mut src = RasterBuffer::zeros(20, 20, RasterDataType::Float32);
682
683 for y in 0..20 {
684 for x in 0..20 {
685 src.set_pixel(x, y, (y * 20 + x) as f64).ok();
686 }
687 }
688
689 let result = equal_interval_classify(&src, 10);
690 assert!(result.is_ok());
691 }
692
693 #[test]
694 fn test_quantile_many_classes() {
695 let mut src = RasterBuffer::zeros(20, 20, RasterDataType::Float32);
696
697 for y in 0..20 {
698 for x in 0..20 {
699 src.set_pixel(x, y, (y * 20 + x) as f64).ok();
700 }
701 }
702
703 let result = quantile_classify(&src, 10);
704 assert!(result.is_ok());
705 }
706
707 #[test]
708 fn test_classify_all_methods() {
709 let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float32);
710
711 for y in 0..10 {
712 for x in 0..10 {
713 src.set_pixel(x, y, (y * 10 + x) as f64).ok();
714 }
715 }
716
717 let methods = vec![
718 ClassificationMethod::EqualInterval { num_classes: 5 },
719 ClassificationMethod::Quantile { num_classes: 5 },
720 ClassificationMethod::NaturalBreaks { num_classes: 5 },
721 ];
722
723 for method in methods {
724 let result = classify(&src, method);
725 assert!(result.is_ok());
726 }
727 }
728
729 #[test]
730 fn test_threshold_edge_values() {
731 let mut src = RasterBuffer::zeros(5, 5, RasterDataType::Float32);
732
733 for y in 0..5 {
734 for x in 0..5 {
735 src.set_pixel(x, y, if x < 3 { 10.0 } else { 20.0 }).ok();
736 }
737 }
738
739 let result = threshold(&src, 15.0, 255.0, 0.0);
740 assert!(result.is_ok());
741 let classified = result.expect("Should succeed");
742
743 let val1 = classified.get_pixel(0, 0).expect("Should get pixel");
745 assert!((val1 - 0.0).abs() < f64::EPSILON);
746
747 let val2 = classified.get_pixel(4, 0).expect("Should get pixel");
749 assert!((val2 - 255.0).abs() < f64::EPSILON);
750 }
751}