1use core::sync::atomic::{AtomicUsize, Ordering};
7use rayon::prelude::*;
8
9use crate::error::{AlgorithmError, Result};
10use crate::resampling::{Resampler, ResamplingMethod};
11use oxigdal_core::buffer::RasterBuffer;
12use oxigdal_core::types::RasterDataType;
13
14#[derive(Debug, Clone)]
16pub struct TileConfig {
17 pub tile_width: u32,
19 pub tile_height: u32,
21 pub num_threads: Option<usize>,
23 pub progress: bool,
25}
26
27impl Default for TileConfig {
28 fn default() -> Self {
29 Self {
30 tile_width: 256,
31 tile_height: 256,
32 num_threads: None,
33 progress: false,
34 }
35 }
36}
37
38impl TileConfig {
39 #[must_use]
41 pub const fn new() -> Self {
42 Self {
43 tile_width: 256,
44 tile_height: 256,
45 num_threads: None,
46 progress: false,
47 }
48 }
49
50 #[must_use]
52 pub const fn with_tile_size(mut self, width: u32, height: u32) -> Self {
53 self.tile_width = width;
54 self.tile_height = height;
55 self
56 }
57
58 #[must_use]
60 pub const fn with_threads(mut self, num_threads: usize) -> Self {
61 self.num_threads = Some(num_threads);
62 self
63 }
64
65 #[must_use]
67 pub const fn with_progress(mut self, progress: bool) -> Self {
68 self.progress = progress;
69 self
70 }
71}
72
73#[derive(Debug, Clone)]
75pub struct Tile {
76 pub x: u32,
78 pub y: u32,
80 pub x_offset: u64,
82 pub y_offset: u64,
84 pub width: u32,
86 pub height: u32,
88 pub data: RasterBuffer,
90}
91
92pub trait TileProcessor: Sync + Send {
94 fn process_tile(&self, tile: &Tile) -> Result<RasterBuffer>;
99}
100
101pub struct ProgressTracker {
103 total: usize,
104 processed: AtomicUsize,
105}
106
107impl ProgressTracker {
108 #[must_use]
110 pub const fn new(total: usize) -> Self {
111 Self {
112 total,
113 processed: AtomicUsize::new(0),
114 }
115 }
116
117 pub fn increment(&self) {
119 let current = self.processed.fetch_add(1, Ordering::Relaxed) + 1;
120 if current.is_multiple_of(10) || current == self.total {
121 let percent = (current * 100) / self.total;
122 tracing::debug!(
123 "Processing tiles: {}/{} ({}%)",
124 current,
125 self.total,
126 percent
127 );
128 }
129 }
130
131 #[must_use]
133 pub fn current(&self) -> usize {
134 self.processed.load(Ordering::Relaxed)
135 }
136
137 #[must_use]
139 pub const fn total(&self) -> usize {
140 self.total
141 }
142}
143
144pub fn extract_tiles(input: &RasterBuffer, config: &TileConfig) -> Result<Vec<Tile>> {
159 let width = input.width();
160 let height = input.height();
161
162 let tiles_across =
163 ((width + u64::from(config.tile_width) - 1) / u64::from(config.tile_width)) as u32;
164 let tiles_down =
165 ((height + u64::from(config.tile_height) - 1) / u64::from(config.tile_height)) as u32;
166
167 let mut tiles = Vec::new();
168
169 for ty in 0..tiles_down {
170 for tx in 0..tiles_across {
171 let x_offset = u64::from(tx * config.tile_width);
172 let y_offset = u64::from(ty * config.tile_height);
173
174 let tile_width = config.tile_width.min((width - x_offset) as u32);
175 let tile_height = config.tile_height.min((height - y_offset) as u32);
176
177 let mut tile_data = RasterBuffer::zeros(
179 u64::from(tile_width),
180 u64::from(tile_height),
181 input.data_type(),
182 );
183
184 for y in 0..tile_height {
185 for x in 0..tile_width {
186 let src_x = x_offset + u64::from(x);
187 let src_y = y_offset + u64::from(y);
188 let value = input.get_pixel(src_x, src_y)?;
189 tile_data.set_pixel(u64::from(x), u64::from(y), value)?;
190 }
191 }
192
193 tiles.push(Tile {
194 x: tx,
195 y: ty,
196 x_offset,
197 y_offset,
198 width: tile_width,
199 height: tile_height,
200 data: tile_data,
201 });
202 }
203 }
204
205 Ok(tiles)
206}
207
208pub fn parallel_process_tiles<P>(
224 tiles: &[Tile],
225 processor: &P,
226 config: &TileConfig,
227) -> Result<Vec<(Tile, RasterBuffer)>>
228where
229 P: TileProcessor,
230{
231 let progress = if config.progress {
232 Some(ProgressTracker::new(tiles.len()))
233 } else {
234 None
235 };
236
237 let results: Result<Vec<_>> = tiles
238 .par_iter()
239 .map(|tile| {
240 let processed = processor.process_tile(tile)?;
241
242 if let Some(ref tracker) = progress {
243 tracker.increment();
244 }
245
246 Ok((tile.clone(), processed))
247 })
248 .collect();
249
250 results
251}
252
253#[derive(Debug, Clone)]
255pub struct OverviewLevel {
256 pub factor: u32,
258 pub data: RasterBuffer,
260}
261
262pub fn parallel_generate_overviews(
304 input: &RasterBuffer,
305 levels: &[u32],
306 method: ResamplingMethod,
307) -> Result<Vec<OverviewLevel>> {
308 if levels.is_empty() {
309 return Ok(Vec::new());
310 }
311
312 for &level in levels {
314 if level < 2 {
315 return Err(AlgorithmError::InvalidParameter {
316 parameter: "level",
317 message: "Overview level must be >= 2".to_string(),
318 });
319 }
320 }
321
322 let resampler = Resampler::new(method);
323
324 let overviews: Result<Vec<_>> = levels
326 .par_iter()
327 .map(|&factor| {
328 let width = input.width() / u64::from(factor);
329 let height = input.height() / u64::from(factor);
330
331 if width == 0 || height == 0 {
332 return Err(AlgorithmError::InvalidParameter {
333 parameter: "level",
334 message: format!("Overview factor {} too large for image size", factor),
335 });
336 }
337
338 let data = resampler.resample(input, width, height)?;
339
340 Ok(OverviewLevel { factor, data })
341 })
342 .collect();
343
344 overviews
345}
346
347pub fn parallel_generate_cog_pyramid(
365 input: &RasterBuffer,
366 min_size: u64,
367 method: ResamplingMethod,
368) -> Result<Vec<OverviewLevel>> {
369 let max_dim = input.width().max(input.height());
370
371 let mut levels = Vec::new();
373 let mut factor = 2u32;
374
375 while max_dim / u64::from(factor) >= min_size {
376 levels.push(factor);
377 factor *= 2;
378 }
379
380 if levels.is_empty() {
381 return Ok(Vec::new());
382 }
383
384 parallel_generate_overviews(input, &levels, method)
385}
386
387pub fn merge_tiles(
404 tiles: &[(Tile, RasterBuffer)],
405 width: u64,
406 height: u64,
407 data_type: RasterDataType,
408) -> Result<RasterBuffer> {
409 let mut output = RasterBuffer::zeros(width, height, data_type);
410
411 for (tile, data) in tiles {
412 for y in 0..tile.height {
413 for x in 0..tile.width {
414 let dst_x = tile.x_offset + u64::from(x);
415 let dst_y = tile.y_offset + u64::from(y);
416
417 if dst_x < width && dst_y < height {
418 let value = data.get_pixel(u64::from(x), u64::from(y))?;
419 output.set_pixel(dst_x, dst_y, value)?;
420 }
421 }
422 }
423 }
424
425 Ok(output)
426}
427
428pub struct FunctionTileProcessor<F>
430where
431 F: Fn(&RasterBuffer) -> Result<RasterBuffer> + Sync + Send,
432{
433 func: F,
434}
435
436impl<F> FunctionTileProcessor<F>
437where
438 F: Fn(&RasterBuffer) -> Result<RasterBuffer> + Sync + Send,
439{
440 #[must_use]
442 pub const fn new(func: F) -> Self {
443 Self { func }
444 }
445}
446
447impl<F> TileProcessor for FunctionTileProcessor<F>
448where
449 F: Fn(&RasterBuffer) -> Result<RasterBuffer> + Sync + Send,
450{
451 fn process_tile(&self, tile: &Tile) -> Result<RasterBuffer> {
452 (self.func)(&tile.data)
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 #![allow(clippy::expect_used)]
459
460 use super::*;
461 use approx::assert_relative_eq;
462
463 #[test]
464 fn test_tile_config() {
465 let config = TileConfig::default();
466 assert_eq!(config.tile_width, 256);
467 assert_eq!(config.tile_height, 256);
468 }
469
470 #[test]
471 fn test_tile_config_builder() {
472 let config = TileConfig::new()
473 .with_tile_size(512, 512)
474 .with_threads(4)
475 .with_progress(true);
476
477 assert_eq!(config.tile_width, 512);
478 assert_eq!(config.tile_height, 512);
479 assert_eq!(config.num_threads, Some(4));
480 assert!(config.progress);
481 }
482
483 #[test]
484 fn test_extract_tiles() {
485 let input = RasterBuffer::zeros(1000, 1000, RasterDataType::UInt8);
486 let config = TileConfig::new().with_tile_size(256, 256);
487
488 let tiles = extract_tiles(&input, &config).expect("should work");
489
490 assert_eq!(tiles.len(), 16);
492
493 assert_eq!(tiles[0].x, 0);
495 assert_eq!(tiles[0].y, 0);
496 assert_eq!(tiles[0].width, 256);
497 assert_eq!(tiles[0].height, 256);
498
499 let edge_tile = &tiles[3];
501 assert_eq!(edge_tile.x, 3);
502 assert_eq!(edge_tile.width, 1000 - 3 * 256); }
504
505 #[test]
506 fn test_parallel_process_tiles() {
507 let input = RasterBuffer::zeros(512, 512, RasterDataType::Float32);
508 let config = TileConfig::new().with_tile_size(256, 256);
509
510 let tiles = extract_tiles(&input, &config).expect("should work");
511
512 let processor = FunctionTileProcessor::new(|tile: &RasterBuffer| {
514 let mut result = RasterBuffer::zeros(tile.width(), tile.height(), tile.data_type());
515 for y in 0..tile.height() {
516 for x in 0..tile.width() {
517 let value = tile.get_pixel(x, y)?;
518 result.set_pixel(x, y, value * 2.0)?;
519 }
520 }
521 Ok(result)
522 });
523
524 let processed = parallel_process_tiles(&tiles, &processor, &config).expect("should work");
525
526 assert_eq!(processed.len(), 4); }
528
529 #[test]
530 fn test_parallel_generate_overviews() {
531 let input = RasterBuffer::zeros(1024, 1024, RasterDataType::UInt8);
532
533 let overviews = parallel_generate_overviews(&input, &[2, 4, 8], ResamplingMethod::Nearest)
534 .expect("should work");
535
536 assert_eq!(overviews.len(), 3);
537
538 assert_eq!(overviews[0].factor, 2);
540 assert_eq!(overviews[0].data.width(), 512);
541 assert_eq!(overviews[0].data.height(), 512);
542
543 assert_eq!(overviews[1].factor, 4);
544 assert_eq!(overviews[1].data.width(), 256);
545 assert_eq!(overviews[1].data.height(), 256);
546
547 assert_eq!(overviews[2].factor, 8);
548 assert_eq!(overviews[2].data.width(), 128);
549 assert_eq!(overviews[2].data.height(), 128);
550 }
551
552 #[test]
553 fn test_parallel_generate_cog_pyramid() {
554 let input = RasterBuffer::zeros(2048, 2048, RasterDataType::UInt8);
555
556 let pyramid = parallel_generate_cog_pyramid(&input, 256, ResamplingMethod::Nearest)
557 .expect("should work");
558
559 assert_eq!(pyramid.len(), 3);
561 assert_eq!(pyramid[0].factor, 2);
562 assert_eq!(pyramid[1].factor, 4);
563 assert_eq!(pyramid[2].factor, 8);
564 }
565
566 #[test]
567 fn test_merge_tiles() {
568 let mut input = RasterBuffer::zeros(512, 512, RasterDataType::Float32);
569
570 for y in 0..512 {
572 for x in 0..512 {
573 input.set_pixel(x, y, (x + y) as f64).expect("should work");
574 }
575 }
576
577 let config = TileConfig::new().with_tile_size(256, 256);
578 let tiles = extract_tiles(&input, &config).expect("should work");
579
580 let tile_pairs: Vec<_> = tiles.iter().map(|t| (t.clone(), t.data.clone())).collect();
582
583 let merged =
584 merge_tiles(&tile_pairs, 512, 512, RasterDataType::Float32).expect("should work");
585
586 for y in 0..512 {
588 for x in 0..512 {
589 let original = input.get_pixel(x, y).expect("should work");
590 let merged_val = merged.get_pixel(x, y).expect("should work");
591 assert_relative_eq!(original, merged_val, epsilon = 1e-6);
592 }
593 }
594 }
595
596 #[test]
597 fn test_progress_tracker() {
598 let tracker = ProgressTracker::new(100);
599 assert_eq!(tracker.total(), 100);
600 assert_eq!(tracker.current(), 0);
601
602 tracker.increment();
603 assert_eq!(tracker.current(), 1);
604
605 for _ in 0..99 {
606 tracker.increment();
607 }
608 assert_eq!(tracker.current(), 100);
609 }
610
611 #[test]
612 fn test_invalid_overview_level() {
613 let input = RasterBuffer::zeros(256, 256, RasterDataType::UInt8);
614
615 let result = parallel_generate_overviews(&input, &[1], ResamplingMethod::Nearest);
617 assert!(result.is_err());
618
619 let result = parallel_generate_overviews(&input, &[1000], ResamplingMethod::Nearest);
621 assert!(result.is_err());
622 }
623}