1use crate::error::{Result, TemporalError};
7use crate::timeseries::TimeSeriesRaster;
8use scirs2_core::ndarray::Array3;
9use serde::{Deserialize, Serialize};
10use tracing::info;
11
12#[cfg(feature = "parallel")]
13#[allow(unused_imports)]
14use rayon::prelude::*;
15
16pub mod max_ndvi;
17pub mod mean;
18pub mod median;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
22pub enum CompositingMethod {
23 Median,
25 Mean,
27 Maximum,
29 Minimum,
31 MaxNDVI,
33 QualityWeighted,
35 FirstValid,
37 LastValid,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct CompositingConfig {
44 pub method: CompositingMethod,
46 pub max_cloud_cover: Option<f32>,
48 pub min_quality: Option<f32>,
50 pub nodata: Option<f64>,
52 pub red_band: Option<usize>,
54 pub nir_band: Option<usize>,
56}
57
58impl Default for CompositingConfig {
59 fn default() -> Self {
60 Self {
61 method: CompositingMethod::Median,
62 max_cloud_cover: None,
63 min_quality: None,
64 nodata: Some(f64::NAN),
65 red_band: Some(0),
66 nir_band: Some(1),
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct CompositeResult {
74 pub data: Array3<f64>,
76 pub count: Array3<usize>,
78 pub quality: Option<Array3<f64>>,
80}
81
82impl CompositeResult {
83 #[must_use]
85 pub fn new(data: Array3<f64>, count: Array3<usize>) -> Self {
86 Self {
87 data,
88 count,
89 quality: None,
90 }
91 }
92
93 #[must_use]
95 pub fn with_quality(mut self, quality: Array3<f64>) -> Self {
96 self.quality = Some(quality);
97 self
98 }
99}
100
101pub struct TemporalCompositor;
103
104impl TemporalCompositor {
105 pub fn composite(ts: &TimeSeriesRaster, config: &CompositingConfig) -> Result<CompositeResult> {
110 match config.method {
111 CompositingMethod::Median => Self::median_composite(ts, config),
112 CompositingMethod::Mean => Self::mean_composite(ts, config),
113 CompositingMethod::Maximum => Self::max_composite(ts, config),
114 CompositingMethod::Minimum => Self::min_composite(ts, config),
115 CompositingMethod::MaxNDVI => Self::max_ndvi_composite(ts, config),
116 CompositingMethod::QualityWeighted => Self::quality_weighted_composite(ts, config),
117 CompositingMethod::FirstValid => Self::first_valid_composite(ts, config),
118 CompositingMethod::LastValid => Self::last_valid_composite(ts, config),
119 }
120 }
121
122 fn median_composite(
124 ts: &TimeSeriesRaster,
125 config: &CompositingConfig,
126 ) -> Result<CompositeResult> {
127 if ts.is_empty() {
128 return Err(TemporalError::insufficient_data("Empty time series"));
129 }
130
131 let (height, width, n_bands) = ts
132 .expected_shape()
133 .ok_or_else(|| TemporalError::insufficient_data("No shape information"))?;
134
135 let mut composite = Array3::zeros((height, width, n_bands));
136 let mut count = Array3::zeros((height, width, n_bands));
137
138 for i in 0..height {
139 for j in 0..width {
140 for k in 0..n_bands {
141 let mut values = Vec::new();
142
143 for entry in ts.entries().values() {
144 if let Some(max_cc) = config.max_cloud_cover {
146 if let Some(cc) = entry.metadata.cloud_cover {
147 if cc > max_cc {
148 continue;
149 }
150 }
151 }
152
153 if let Some(data) = &entry.data {
154 let value = data[[i, j, k]];
155 if let Some(nodata) = config.nodata {
156 if !value.is_nan() && value != nodata {
157 values.push(value);
158 }
159 } else if !value.is_nan() {
160 values.push(value);
161 }
162 }
163 }
164
165 if !values.is_empty() {
166 values
167 .sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
168 let median = if values.len() % 2 == 0 {
169 (values[values.len() / 2 - 1] + values[values.len() / 2]) / 2.0
170 } else {
171 values[values.len() / 2]
172 };
173 composite[[i, j, k]] = median;
174 count[[i, j, k]] = values.len();
175 }
176 }
177 }
178 }
179
180 info!("Created median composite");
181 Ok(CompositeResult::new(composite, count))
182 }
183
184 fn mean_composite(
186 ts: &TimeSeriesRaster,
187 config: &CompositingConfig,
188 ) -> Result<CompositeResult> {
189 if ts.is_empty() {
190 return Err(TemporalError::insufficient_data("Empty time series"));
191 }
192
193 let (height, width, n_bands) = ts
194 .expected_shape()
195 .ok_or_else(|| TemporalError::insufficient_data("No shape information"))?;
196
197 let mut composite = Array3::zeros((height, width, n_bands));
198 let mut count = Array3::zeros((height, width, n_bands));
199
200 for i in 0..height {
201 for j in 0..width {
202 for k in 0..n_bands {
203 let mut sum = 0.0;
204 let mut n = 0;
205
206 for entry in ts.entries().values() {
207 if let Some(max_cc) = config.max_cloud_cover {
208 if let Some(cc) = entry.metadata.cloud_cover {
209 if cc > max_cc {
210 continue;
211 }
212 }
213 }
214
215 if let Some(data) = &entry.data {
216 let value = data[[i, j, k]];
217 if let Some(nodata) = config.nodata {
218 if !value.is_nan() && value != nodata {
219 sum += value;
220 n += 1;
221 }
222 } else if !value.is_nan() {
223 sum += value;
224 n += 1;
225 }
226 }
227 }
228
229 if n > 0 {
230 composite[[i, j, k]] = sum / n as f64;
231 count[[i, j, k]] = n;
232 }
233 }
234 }
235 }
236
237 info!("Created mean composite");
238 Ok(CompositeResult::new(composite, count))
239 }
240
241 fn max_composite(
243 ts: &TimeSeriesRaster,
244 _config: &CompositingConfig,
245 ) -> Result<CompositeResult> {
246 if ts.is_empty() {
247 return Err(TemporalError::insufficient_data("Empty time series"));
248 }
249
250 let (height, width, n_bands) = ts
251 .expected_shape()
252 .ok_or_else(|| TemporalError::insufficient_data("No shape information"))?;
253
254 let mut composite = Array3::from_elem((height, width, n_bands), f64::NEG_INFINITY);
255 let mut count = Array3::zeros((height, width, n_bands));
256
257 for i in 0..height {
258 for j in 0..width {
259 for k in 0..n_bands {
260 for entry in ts.entries().values() {
261 if let Some(data) = &entry.data {
262 let value = data[[i, j, k]];
263 if !value.is_nan() {
264 if value > composite[[i, j, k]] {
265 composite[[i, j, k]] = value;
266 }
267 count[[i, j, k]] += 1;
268 }
269 }
270 }
271 }
272 }
273 }
274
275 info!("Created maximum value composite");
276 Ok(CompositeResult::new(composite, count))
277 }
278
279 fn min_composite(
281 ts: &TimeSeriesRaster,
282 _config: &CompositingConfig,
283 ) -> Result<CompositeResult> {
284 if ts.is_empty() {
285 return Err(TemporalError::insufficient_data("Empty time series"));
286 }
287
288 let (height, width, n_bands) = ts
289 .expected_shape()
290 .ok_or_else(|| TemporalError::insufficient_data("No shape information"))?;
291
292 let mut composite = Array3::from_elem((height, width, n_bands), f64::INFINITY);
293 let mut count = Array3::zeros((height, width, n_bands));
294
295 for i in 0..height {
296 for j in 0..width {
297 for k in 0..n_bands {
298 for entry in ts.entries().values() {
299 if let Some(data) = &entry.data {
300 let value = data[[i, j, k]];
301 if !value.is_nan() {
302 if value < composite[[i, j, k]] {
303 composite[[i, j, k]] = value;
304 }
305 count[[i, j, k]] += 1;
306 }
307 }
308 }
309 }
310 }
311 }
312
313 info!("Created minimum value composite");
314 Ok(CompositeResult::new(composite, count))
315 }
316
317 fn max_ndvi_composite(
319 ts: &TimeSeriesRaster,
320 config: &CompositingConfig,
321 ) -> Result<CompositeResult> {
322 let red_band = config.red_band.ok_or_else(|| {
323 TemporalError::invalid_parameter("red_band", "required for MaxNDVI composite")
324 })?;
325
326 let nir_band = config.nir_band.ok_or_else(|| {
327 TemporalError::invalid_parameter("nir_band", "required for MaxNDVI composite")
328 })?;
329
330 if ts.is_empty() {
331 return Err(TemporalError::insufficient_data("Empty time series"));
332 }
333
334 let (height, width, n_bands) = ts
335 .expected_shape()
336 .ok_or_else(|| TemporalError::insufficient_data("No shape information"))?;
337
338 if red_band >= n_bands || nir_band >= n_bands {
339 return Err(TemporalError::invalid_parameter(
340 "band_indices",
341 "band indices out of range",
342 ));
343 }
344
345 let mut composite = Array3::zeros((height, width, n_bands));
346 let mut count = Array3::zeros((height, width, n_bands));
347 let mut max_ndvi = Array3::from_elem((height, width, 1), f64::NEG_INFINITY);
348
349 for entry in ts.entries().values() {
350 if let Some(data) = &entry.data {
351 for i in 0..height {
352 for j in 0..width {
353 let red = data[[i, j, red_band]];
354 let nir = data[[i, j, nir_band]];
355
356 if !red.is_nan() && !nir.is_nan() && (red + nir) != 0.0 {
357 let ndvi = (nir - red) / (nir + red);
358
359 if ndvi > max_ndvi[[i, j, 0]] {
360 max_ndvi[[i, j, 0]] = ndvi;
361 for k in 0..n_bands {
363 composite[[i, j, k]] = data[[i, j, k]];
364 }
365 count[[i, j, 0]] += 1;
366 }
367 }
368 }
369 }
370 }
371 }
372
373 info!("Created maximum NDVI composite");
374 Ok(CompositeResult::new(composite, count))
375 }
376
377 fn quality_weighted_composite(
379 ts: &TimeSeriesRaster,
380 _config: &CompositingConfig,
381 ) -> Result<CompositeResult> {
382 if ts.is_empty() {
383 return Err(TemporalError::insufficient_data("Empty time series"));
384 }
385
386 let (height, width, n_bands) = ts
387 .expected_shape()
388 .ok_or_else(|| TemporalError::insufficient_data("No shape information"))?;
389
390 let mut composite: Array3<f64> = Array3::zeros((height, width, n_bands));
391 let mut count: Array3<usize> = Array3::zeros((height, width, n_bands));
392 let mut weight_sum: Array3<f64> = Array3::zeros((height, width, n_bands));
393
394 for entry in ts.entries().values() {
395 let weight = entry.metadata.quality_score.unwrap_or(1.0) as f64;
396
397 if let Some(data) = &entry.data {
398 for i in 0..height {
399 for j in 0..width {
400 for k in 0..n_bands {
401 let value = data[[i, j, k]];
402 if !value.is_nan() {
403 composite[[i, j, k]] += value * weight;
404 weight_sum[[i, j, k]] += weight;
405 count[[i, j, k]] += 1;
406 }
407 }
408 }
409 }
410 }
411 }
412
413 for i in 0..height {
415 for j in 0..width {
416 for k in 0..n_bands {
417 if weight_sum[[i, j, k]] > 0.0 {
418 composite[[i, j, k]] /= weight_sum[[i, j, k]];
419 }
420 }
421 }
422 }
423
424 info!("Created quality-weighted composite");
425 Ok(CompositeResult::new(composite, count))
426 }
427
428 fn first_valid_composite(
430 ts: &TimeSeriesRaster,
431 _config: &CompositingConfig,
432 ) -> Result<CompositeResult> {
433 if ts.is_empty() {
434 return Err(TemporalError::insufficient_data("Empty time series"));
435 }
436
437 let (height, width, n_bands) = ts
438 .expected_shape()
439 .ok_or_else(|| TemporalError::insufficient_data("No shape information"))?;
440
441 let mut composite = Array3::zeros((height, width, n_bands));
442 let mut count = Array3::zeros((height, width, n_bands));
443 let mut filled = Array3::from_elem((height, width, n_bands), false);
444
445 for entry in ts.entries().values() {
446 if let Some(data) = &entry.data {
447 for i in 0..height {
448 for j in 0..width {
449 for k in 0..n_bands {
450 if !filled[[i, j, k]] {
451 let value = data[[i, j, k]];
452 if !value.is_nan() {
453 composite[[i, j, k]] = value;
454 count[[i, j, k]] = 1;
455 filled[[i, j, k]] = true;
456 }
457 }
458 }
459 }
460 }
461 }
462 }
463
464 info!("Created first valid value composite");
465 Ok(CompositeResult::new(composite, count))
466 }
467
468 fn last_valid_composite(
470 ts: &TimeSeriesRaster,
471 _config: &CompositingConfig,
472 ) -> Result<CompositeResult> {
473 if ts.is_empty() {
474 return Err(TemporalError::insufficient_data("Empty time series"));
475 }
476
477 let (height, width, n_bands) = ts
478 .expected_shape()
479 .ok_or_else(|| TemporalError::insufficient_data("No shape information"))?;
480
481 let mut composite = Array3::zeros((height, width, n_bands));
482 let mut count = Array3::zeros((height, width, n_bands));
483
484 for entry in ts.entries().values() {
485 if let Some(data) = &entry.data {
486 for i in 0..height {
487 for j in 0..width {
488 for k in 0..n_bands {
489 let value = data[[i, j, k]];
490 if !value.is_nan() {
491 composite[[i, j, k]] = value;
492 count[[i, j, k]] = 1;
493 }
494 }
495 }
496 }
497 }
498 }
499
500 info!("Created last valid value composite");
501 Ok(CompositeResult::new(composite, count))
502 }
503}