Skip to main content

oxigdal_algorithms/parallel/
tiles.rs

1//! Parallel tile processing
2//!
3//! This module provides parallel implementations for tile processing,
4//! COG pyramid generation, and overview computation.
5
6use 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/// Configuration for tile processing
15#[derive(Debug, Clone)]
16pub struct TileConfig {
17    /// Tile width in pixels
18    pub tile_width: u32,
19    /// Tile height in pixels
20    pub tile_height: u32,
21    /// Number of threads for parallel processing
22    pub num_threads: Option<usize>,
23    /// Enable progress tracking
24    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    /// Creates a new tile configuration
40    #[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    /// Sets the tile dimensions
51    #[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    /// Sets the number of threads
59    #[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    /// Enables progress tracking
66    #[must_use]
67    pub const fn with_progress(mut self, progress: bool) -> Self {
68        self.progress = progress;
69        self
70    }
71}
72
73/// A tile extracted from a raster
74#[derive(Debug, Clone)]
75pub struct Tile {
76    /// Tile X index
77    pub x: u32,
78    /// Tile Y index
79    pub y: u32,
80    /// X offset in pixels in the source raster
81    pub x_offset: u64,
82    /// Y offset in pixels in the source raster
83    pub y_offset: u64,
84    /// Actual width of the tile (may be smaller at edges)
85    pub width: u32,
86    /// Actual height of the tile (may be smaller at edges)
87    pub height: u32,
88    /// Tile data
89    pub data: RasterBuffer,
90}
91
92/// Trait for processing tiles
93pub trait TileProcessor: Sync + Send {
94    /// Process a single tile
95    ///
96    /// # Errors
97    /// Returns an error if processing fails
98    fn process_tile(&self, tile: &Tile) -> Result<RasterBuffer>;
99}
100
101/// Progress tracker for tile processing
102pub struct ProgressTracker {
103    total: usize,
104    processed: AtomicUsize,
105}
106
107impl ProgressTracker {
108    /// Creates a new progress tracker
109    #[must_use]
110    pub const fn new(total: usize) -> Self {
111        Self {
112            total,
113            processed: AtomicUsize::new(0),
114        }
115    }
116
117    /// Increments the progress counter
118    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    /// Returns the current progress count
132    #[must_use]
133    pub fn current(&self) -> usize {
134        self.processed.load(Ordering::Relaxed)
135    }
136
137    /// Returns the total count
138    #[must_use]
139    pub const fn total(&self) -> usize {
140        self.total
141    }
142}
143
144/// Extract tiles from a raster buffer
145///
146/// # Arguments
147///
148/// * `input` - Input raster buffer
149/// * `config` - Tile configuration
150///
151/// # Returns
152///
153/// Vector of extracted tiles
154///
155/// # Errors
156///
157/// Returns an error if tile extraction fails
158pub 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            // Extract tile data
178            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
208/// Process tiles in parallel
209///
210/// # Arguments
211///
212/// * `tiles` - Input tiles to process
213/// * `processor` - Tile processor implementation
214/// * `config` - Tile configuration
215///
216/// # Returns
217///
218/// Vector of processed tiles
219///
220/// # Errors
221///
222/// Returns an error if processing fails
223pub 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/// Overview level for pyramid generation
254#[derive(Debug, Clone)]
255pub struct OverviewLevel {
256    /// Scale factor (2 = half size, 4 = quarter size, etc.)
257    pub factor: u32,
258    /// Overview raster data
259    pub data: RasterBuffer,
260}
261
262/// Generate overviews (image pyramids) in parallel
263///
264/// This function generates multiple overview levels simultaneously,
265/// which is much faster than sequential generation.
266///
267/// # Arguments
268///
269/// * `input` - Input raster buffer
270/// * `levels` - Scale factors for each level (e.g., [2, 4, 8, 16])
271/// * `method` - Resampling method to use
272///
273/// # Returns
274///
275/// Vector of overview levels
276///
277/// # Errors
278///
279/// Returns an error if overview generation fails
280///
281/// # Example
282///
283/// ```ignore
284/// # #[cfg(feature = "parallel")]
285/// # {
286/// use oxigdal_algorithms::parallel::parallel_generate_overviews;
287/// use oxigdal_algorithms::resampling::ResamplingMethod;
288/// use oxigdal_core::buffer::RasterBuffer;
289/// use oxigdal_core::types::RasterDataType;
290/// # use oxigdal_algorithms::error::Result;
291///
292/// # fn main() -> Result<()> {
293/// let input = RasterBuffer::zeros(4096, 4096, RasterDataType::UInt8);
294/// let overviews = parallel_generate_overviews(
295///     &input,
296///     &[2, 4, 8, 16],
297///     ResamplingMethod::Average
298/// )?;
299/// # Ok(())
300/// # }
301/// # }
302/// ```
303pub 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    // Validate levels
313    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    // Generate overviews in parallel
325    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
347/// Generate COG (Cloud Optimized GeoTIFF) pyramid in parallel
348///
349/// This generates a complete pyramid with all levels up to a maximum size.
350///
351/// # Arguments
352///
353/// * `input` - Input raster buffer
354/// * `min_size` - Minimum dimension size for the smallest overview
355/// * `method` - Resampling method to use
356///
357/// # Returns
358///
359/// Vector of overview levels in order (2x, 4x, 8x, etc.)
360///
361/// # Errors
362///
363/// Returns an error if pyramid generation fails
364pub 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    // Calculate required levels
372    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
387/// Merge tiles back into a single raster
388///
389/// # Arguments
390///
391/// * `tiles` - Vector of tiles with their processed data
392/// * `width` - Total width of the output raster
393/// * `height` - Total height of the output raster
394/// * `data_type` - Data type for the output
395///
396/// # Returns
397///
398/// Merged raster buffer
399///
400/// # Errors
401///
402/// Returns an error if merging fails
403pub 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
428/// Simple tile processor that applies a function to each tile
429pub 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    /// Creates a new function tile processor
441    #[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        // Should have 4x4 = 16 tiles
491        assert_eq!(tiles.len(), 16);
492
493        // Check first tile
494        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        // Check edge tile (last column)
500        let edge_tile = &tiles[3];
501        assert_eq!(edge_tile.x, 3);
502        assert_eq!(edge_tile.width, 1000 - 3 * 256); // 232 pixels
503    }
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        // Process tiles: multiply all values by 2
513        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); // 2x2 tiles
527    }
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        // Check sizes
539        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        // Should generate levels: 2, 4, 8 (stopping at 256x256)
560        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        // Fill with test pattern
571        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        // Convert to (Tile, RasterBuffer) pairs
581        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        // Verify data matches
587        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        // Level < 2 should fail
616        let result = parallel_generate_overviews(&input, &[1], ResamplingMethod::Nearest);
617        assert!(result.is_err());
618
619        // Level too large should fail
620        let result = parallel_generate_overviews(&input, &[1000], ResamplingMethod::Nearest);
621        assert!(result.is_err());
622    }
623}