Skip to main content

rasterkit/commands/
extract_command.rs

1use clap::ArgMatches;
2use log::{debug, info, warn, error};
3use std::path::Path;
4use image::DynamicImage;
5use crate::commands::command_traits::Command;
6use crate::tiff::errors::{TiffResult, TiffError};
7use crate::utils::logger::Logger;
8use crate::extractor::{ImageExtractor, Region};
9use crate::coordinate::BoundingBox;
10use crate::tiff::TiffReader;
11use crate::tiff::constants::epsg;
12use crate::tiff::types::TIFF;
13use crate::utils::colormap_utils;
14use crate::utils::reference_utils;
15use crate::utils::image_extraction_utils;
16use crate::utils::coordinate_utils;
17use crate::utils::reprojection_utils;
18use crate::utils::filter_utils;
19
20/// Command for extracting image data from TIFF files
21pub struct ExtractCommand<'a> {
22    /// Path to the input file
23    input_file: String,
24    /// Path to the output file
25    output_file: String,
26    /// Bounding box string for region extraction
27    bbox_str: Option<String>,
28    /// Coordinate string for point-based extraction
29    coordinate_str: Option<String>,
30    /// Radius in meters for point-based extraction
31    radius: Option<f64>,
32    /// Shape for coordinate-based extraction (circle or square)
33    shape: String,
34    /// CRS code for the bounding box/coordinate
35    crs_code: Option<u32>,
36    /// Target projection EPSG code for reprojection
37    proj_code: Option<u32>,
38    /// Path to save the colormap as SLD (optional)
39    colormap_output: Option<String>,
40    /// Path to a colormap file to apply (optional)
41    colormap_input: Option<String>,
42    /// Whether to extract array data instead of image
43    array_mode: bool,
44    /// Format for array output
45    array_format: String,
46    /// Filter range to extract only specific pixel values (e.g., "15,160")
47    filter_range: Option<String>,
48    /// Whether to make filtered pixels transparent
49    filter_transparency: bool,
50    /// Logger for recording operations
51    logger: &'a Logger,
52}
53
54impl<'a> ExtractCommand<'a> {
55    /// Create a new extract command
56    ///
57    /// # Arguments
58    /// * `args` - CLI argument matches from clap
59    /// * `logger` - Logger for recording operations
60    ///
61    /// # Returns
62    /// A new ExtractCommand instance or an error
63    pub fn new(args: &ArgMatches, logger: &'a Logger) -> TiffResult<Self> {
64        info!("Creating new extract command from arguments");
65
66        let input_file = args.get_one::<String>("input")
67            .ok_or_else(|| TiffError::GenericError("Missing input file".to_string()))?
68            .clone();
69        info!("Input file: {}", input_file);
70
71        let output_file = args.get_one::<String>("output")
72            .ok_or_else(|| TiffError::GenericError("Missing output file path for extraction".to_string()))?
73            .clone();
74        info!("Output file: {}", output_file);
75
76        // Get bounding box string if provided
77        let bbox_str = args.get_one::<String>("bbox").cloned();
78        info!("Bounding box: {:?}", bbox_str);
79
80        // Get coordinate and radius if provided
81        let coordinate_str = args.get_one::<String>("coordinate").cloned();
82        info!("Coordinate: {:?}", coordinate_str);
83
84        let radius = if let Some(radius_str) = args.get_one::<String>("radius") {
85            match radius_str.parse::<f64>() {
86                Ok(r) => {
87                    info!("Radius: {} meters", r);
88                    Some(r)
89                },
90                Err(e) => {
91                    return Err(TiffError::GenericError(
92                        format!("Invalid radius value: {}", e)));
93                }
94            }
95        } else {
96            None
97        };
98
99        // Get shape for coordinate-based extraction
100        let shape = args.get_one::<String>("shape")
101            .cloned()
102            .unwrap_or_else(|| "square".to_string());
103        info!("Shape: {}", shape);
104
105        // Validate that if radius is specified, coordinate is also specified
106        if radius.is_some() && coordinate_str.is_none() {
107            return Err(TiffError::GenericError(
108                "Radius specified but no coordinate provided".to_string()));
109        }
110
111        // Get CRS code if provided
112        let crs_code = if let Some(crs_str) = args.get_one::<String>("crs") {
113            // If a CRS was provided, parse it
114            info!("Parsing CRS code: {}", crs_str);
115            match crs_str.parse::<u32>() {
116                Ok(code) => {
117                    info!("Using CRS code: {}", code);
118                    Some(code)
119                },
120                Err(_) => {
121                    return Err(TiffError::GenericError(format!("Invalid CRS code: {}", crs_str)));
122                }
123            }
124        } else if let Some(epsg_str) = args.get_one::<String>("epsg") {
125            // For backward compatibility with --epsg
126            info!("Using EPSG code from --epsg parameter: {}", epsg_str);
127            match epsg_str.parse::<u32>() {
128                Ok(code) => {
129                    info!("Using coordinate system EPSG:{}", code);
130                    Some(code)
131                },
132                Err(_) => return Err(TiffError::GenericError(format!("Invalid EPSG code: {}", epsg_str)))
133            }
134        } else {
135            // Only default to WGS84 if no CRS/EPSG was explicitly specified
136            if coordinate_str.is_some() || bbox_str.is_some() {
137                // If we have coordinates but no CRS, default to WGS84
138                info!("No CRS specified with coordinates, defaulting to EPSG:4326 (WGS84)");
139                Some(4326)
140            } else {
141                None
142            }
143        };
144
145        info!("CRS code: {:?}", crs_code);
146
147        // Get target projection code if provided
148        let proj_code = if let Some(proj_str) = args.get_one::<String>("proj") {
149            info!("Parsing target projection code: {}", proj_str);
150            match proj_str.parse::<u32>() {
151                Ok(code) => {
152                    info!("Using target projection EPSG:{}", code);
153                    Some(code)
154                },
155                Err(_) => {
156                    return Err(TiffError::GenericError(format!("Invalid projection code: {}", proj_str)));
157                }
158            }
159        } else {
160            None
161        };
162
163        info!("Target projection code: {:?}", proj_code);
164
165        // Get colormap options
166        let colormap_output = args.get_one::<String>("colormap-output").cloned();
167        info!("Colormap output: {:?}", colormap_output);
168
169        let colormap_input = args.get_one::<String>("colormap-input").cloned();
170        info!("Colormap input: {:?}", colormap_input);
171
172        // Get array extraction options
173        let array_mode = args.get_flag("extract-array");
174        info!("Array extraction mode: {}", array_mode);
175
176        let array_format = args.get_one::<String>("array-format")
177            .cloned()
178            .unwrap_or_else(|| "csv".to_string());
179        info!("Array format: {}", array_format);
180
181        // Get filter range if provided
182        let filter_range = args.get_one::<String>("filter").cloned();
183        info!("Filter range: {:?}", filter_range);
184
185        // Get filter transparency option
186        let filter_transparency = args.get_flag("filter-transparency");
187        info!("Filter transparency: {}", filter_transparency);
188
189        Ok(ExtractCommand {
190            input_file,
191            output_file,
192            bbox_str,
193            coordinate_str,
194            radius,
195            shape,
196            crs_code,
197            proj_code,
198            colormap_output,
199            colormap_input,
200            array_mode,
201            array_format,
202            filter_range,
203            filter_transparency,
204            logger,
205        })
206    }
207
208    /// Determine the effective bounding box based on input parameters
209    ///
210    /// This method analyzes the command parameters to determine the appropriate
211    /// bounding box to use. It handles the following cases:
212    /// - Coordinate + radius: Converts to bounding box using coordinate_utils
213    /// - Direct bounding box: Uses the provided bbox_str
214    /// - No spatial filter: Returns None to extract the entire image
215    ///
216    /// # Returns
217    /// An optional string containing the bounding box coordinates, or None if no spatial filter specified
218    fn determine_effective_bbox(&self) -> TiffResult<Option<String>> {
219        // If coordinate and radius are specified, convert to bbox
220        if let (Some(coord_str), Some(rad)) = (&self.coordinate_str, self.radius) {
221            info!("Converting coordinate and radius to bounding box");
222            let bbox_str = coordinate_utils::coord_to_bbox(
223                coord_str,
224                rad,
225                &self.shape,
226                self.crs_code  // This was using epsg_code - now using crs_code
227            )?;
228            info!("Calculated bounding box from coordinate: {}", bbox_str);
229            Ok(Some(bbox_str))
230        }
231        // Otherwise use the provided bbox if any
232        else if let Some(bbox) = &self.bbox_str {
233            info!("Using provided bounding box: {}", bbox);
234            Ok(Some(bbox.clone()))
235        }
236        // No spatial filter specified
237        else {
238            info!("No bounding box or coordinate specified");
239            Ok(None)
240        }
241    }
242
243    /// Determine extraction region from input parameters
244    ///
245    /// Converts geographic coordinates (bounding box or coordinate+radius)
246    /// to pixel coordinates for extraction. Handles different spatial
247    /// filter methods and coordinate reference systems.
248    ///
249    /// # Returns
250    /// An optional Region for extraction, or None to extract the entire image
251    fn determine_region(&self) -> TiffResult<Option<Region>> {
252        info!("Determining extraction region");
253
254        // Get the effective bounding box (either from bbox_str or calculated from coordinate+radius)
255        let effective_bbox = self.determine_effective_bbox()?;
256
257        // If no spatial filter specified, use full image
258        let Some(bbox_str) = effective_bbox else {
259            info!("No spatial filter specified, will use full image");
260            return Ok(None);
261        };
262
263        info!("Using bounding box: {}", bbox_str);
264
265        // Parse the bounding box
266        info!("Parsing bounding box");
267        let mut bbox = image_extraction_utils::parse_bbox(&bbox_str)?;
268
269        // Set the CRS code if we have one
270        if let Some(code) = self.crs_code {
271            bbox.epsg = Some(code);
272        }
273
274        info!("Parsed bounding box: min_x={}, min_y={}, max_x={}, max_y={}",
275              bbox.min_x, bbox.min_y, bbox.max_x, bbox.max_y);
276
277        // Load the TIFF file
278        info!("Loading TIFF file to determine region");
279        let mut reader = TiffReader::new(self.logger);
280        let tiff = reader.load(&self.input_file)?;
281
282        // Determine extraction region based on the bounding box
283        info!("Converting bounding box to pixel region");
284        let region = image_extraction_utils::determine_extraction_region(
285            bbox, &tiff, &reader, &self.input_file, self.logger)?;
286
287        info!("Determined extraction region: x={}, y={}, width={}, height={}",
288              region.x, region.y, region.width, region.height);
289
290        Ok(Some(region))
291    }
292
293    /// Extract colormap from input file if requested
294    ///
295    /// If a colormap output path is specified, extracts the colormap
296    /// from the input file and saves it.
297    ///
298    /// # Returns
299    /// Result indicating success or an error
300    fn handle_colormap_extraction(&self) -> TiffResult<()> {
301        info!("Checking if colormap extraction is requested");
302
303        let Some(colormap_path) = &self.colormap_output else {
304            info!("No colormap extraction requested");
305            return Ok(());
306        };
307
308        info!("Extracting colormap from {} to {}", self.input_file, colormap_path);
309
310        match colormap_utils::extract_colormap(&self.input_file, colormap_path, self.logger) {
311            Ok(_) => {
312                info!("Colormap extraction successful");
313                Ok(())
314            },
315            Err(e) => {
316                warn!("Failed to extract colormap: {}", e);
317                // Continue with extraction even if colormap extraction fails
318                Ok(())
319            }
320        }
321    }
322
323    /// Extract image with colormap application
324    ///
325    /// Extracts an image and applies a colormap to it, transforming
326    /// grayscale values to RGB colors based on the colormap.
327    ///
328    /// # Arguments
329    /// * `extractor` - Image extractor to use
330    /// * `region` - Region to extract
331    /// * `colormap_path` - Path to the colormap file
332    ///
333    /// # Returns
334    /// Result indicating success or an error
335    fn extract_with_colormap(&self, extractor: &mut ImageExtractor, region: Option<Region>, colormap_path: &str) -> TiffResult<()> {
336        info!("Will apply colormap from {} when extracting", colormap_path);
337
338        // First extract the image to memory for colormap application
339        info!("Extracting image to memory for colormap application");
340        let mut image = extractor.extract_image(&self.input_file, region)?;
341        info!("Image extracted: {}x{}", image.width(), image.height());
342
343        // Apply filtering if specified
344        if let Some(filter_str) = &self.filter_range {
345            info!("Applying filter: {}", filter_str);
346
347            // Parse the filter range
348            match filter_utils::parse_filter_range(filter_str) {
349                Ok((min_value, max_value)) => {
350                    info!("Filtering values from {} to {}", min_value, max_value);
351
352                    // Apply the filter
353                    image = filter_utils::filter_image_values(
354                        &image,
355                        min_value,
356                        max_value,
357                        0, // Background value (black)
358                        self.filter_transparency
359                    );
360
361                    info!("Filtering applied");
362                },
363                Err(err) => {
364                    warn!("Failed to parse filter range: {}", err);
365                    warn!("Continuing without filtering");
366                }
367            }
368        }
369
370        // Load the colormap
371        info!("Loading colormap from {}", colormap_path);
372        let colormap = match colormap_utils::load_colormap(colormap_path, self.logger) {
373            Ok(cm) => {
374                info!("Colormap loaded with {} entries", cm.len());
375                cm
376            },
377            Err(e) => {
378                warn!("Failed to read colormap file: {:?}", e);
379                warn!("Continuing with extraction without applying colormap");
380                return extractor.extract_to_file(&self.input_file, &self.output_file, region, Some(&self.shape));
381            }
382        };
383
384        info!("Applying colormap with {} entries", colormap.len());
385
386        // Convert to grayscale if not already
387        info!("Converting image to grayscale");
388        let grayscale = image.to_luma8();
389
390        // Apply colormap to transform image
391        info!("Applying colormap to transform image");
392        let rgb_image = colormap_utils::apply_colormap_to_image(&grayscale, &colormap);
393
394        // Save the image, passing shape for proper masking
395        colormap_utils::save_colorized_tiff(
396            rgb_image,
397            &self.output_file,
398            &self.input_file,
399            region,
400            self.logger,
401            Some(&self.shape)  // Pass the shape
402        )
403    }
404
405    /// Save colorized image in appropriate format
406    ///
407    /// Saves an RGB image to a file, preserving georeferencing if it's a TIFF.
408    ///
409    /// # Arguments
410    /// * `rgb_image` - The RGB image to save
411    /// * `region` - Region that was extracted (for georeferencing)
412    ///
413    /// # Returns
414    /// Result indicating success or an error
415    fn save_colorized_image(&self, rgb_image: image::RgbImage, region: Option<Region>) -> TiffResult<()> {
416        info!("Saving colorized image to {}", self.output_file);
417
418        // Check output format
419        let is_tiff = Path::new(&self.output_file)
420            .extension()
421            .map(|ext| ext.to_string_lossy().to_lowercase())
422            .map(|ext| ext == "tif" || ext == "tiff")
423            .unwrap_or(false);
424
425        if is_tiff {
426            // Save as georeferenced TIFF
427            info!("Saving as georeferenced TIFF");
428            colormap_utils::save_colorized_tiff(
429                rgb_image,
430                &self.output_file,
431                &self.input_file,
432                region,
433                self.logger,
434                Some(&self.shape)
435            )
436        } else {
437            // For other formats, just save the RGB image
438            info!("Saving as standard image format");
439            match rgb_image.save(&self.output_file) {
440                Ok(_) => {
441                    info!("Image saved successfully");
442                    Ok(())
443                },
444                Err(e) => {
445                    error!("Failed to save colorized image: {}", e);
446                    Err(TiffError::GenericError(format!("Failed to save colorized image: {}", e)))
447                }
448            }
449        }
450    }
451
452    /// Extract array data from input file
453    ///
454    /// Extracts numeric array data from a TIFF file and saves it in the
455    /// specified format (CSV, JSON, or NPY).
456    ///
457    /// # Arguments
458    /// * `region` - Region to extract
459    ///
460    /// # Returns
461    /// Result indicating success or an error
462    fn extract_array_data(&self, region: Option<Region>) -> TiffResult<()> {
463        info!("Starting array data extraction from {} to {} in {} format",
464              self.input_file, self.output_file, self.array_format);
465
466        // Test if output file is writable
467        info!("Testing if output file is writable");
468        let test_file = std::fs::File::create(&self.output_file);
469        match test_file {
470            Ok(_) => info!("Output path is writable"),
471            Err(e) => {
472                error!("Cannot write to output path: {}", e);
473                return Err(TiffError::GenericError(format!("Cannot write to output file: {}", e)));
474            }
475        }
476
477        // Create API instance
478        info!("Creating RasterKit API instance");
479        let api = match crate::api::RasterKit::new(Some("rasterkit.log")) {
480            Ok(api) => {
481                info!("API instance created successfully");
482                api
483            },
484            Err(e) => {
485                error!("Failed to create API instance: {}", e);
486                return Err(e);
487            }
488        };
489
490        // Extract the array data to file
491        info!("Calling extract_to_array API method");
492        let result = api.extract_to_array(
493            &self.input_file,
494            &self.output_file,
495            &self.array_format,
496            region.map(|r| (r.x, r.y, r.width, r.height))
497        );
498
499        // Check result
500        match &result {
501            Ok(_) => info!("Array extraction completed successfully"),
502            Err(e) => error!("Array extraction failed: {}", e),
503        }
504
505        result
506    }
507
508    /// Determine region with radius information
509    fn determine_region_with_radius(&self, radius_meters: Option<f64>) -> TiffResult<Option<Region>> {
510        info!("Determining extraction region with radius information");
511
512        // Get the effective bounding box (either from bbox_str or calculated from coordinate+radius)
513        let effective_bbox = self.determine_effective_bbox()?;
514
515        // If no spatial filter specified, use full image
516        let Some(bbox_str) = effective_bbox else {
517            info!("No spatial filter specified, will use full image");
518            return Ok(None);
519        };
520
521        info!("Using bounding box: {}", bbox_str);
522
523        // Parse the bounding box
524        info!("Parsing bounding box");
525        let mut bbox = image_extraction_utils::parse_bbox(&bbox_str)?;
526
527        // Add radius information if available
528        if let Some(radius) = radius_meters {
529            info!("Using radius of {} meters for fallback handling", radius);
530            bbox.radius_meters = Some(radius);
531        }
532
533        // Set the CRS code if we have one
534        if let Some(code) = self.crs_code {
535            bbox.epsg = Some(code);
536        }
537
538        info!("Parsed bounding box: min_x={}, min_y={}, max_x={}, max_y={}",
539             bbox.min_x, bbox.min_y, bbox.max_x, bbox.max_y);
540
541        // Load the TIFF file
542        info!("Loading TIFF file to determine region");
543        let mut reader = TiffReader::new(self.logger);
544        let tiff = reader.load(&self.input_file)?;
545
546        // Determine extraction region based on the bounding box
547        info!("Converting bounding box to pixel region");
548        let region = image_extraction_utils::determine_extraction_region(
549            bbox, &tiff, &reader, &self.input_file, self.logger)?;
550
551        info!("Determined extraction region: x={}, y={}, width={}, height={}",
552             region.x, region.y, region.width, region.height);
553
554        Ok(Some(region))
555    }
556}
557
558impl<'a> Command for ExtractCommand<'a> {
559    /// Execute the extract command
560    ///
561    /// This is the main entry point for the extract command. It determines
562    /// the extraction region, handles colormap extraction if requested, and
563    /// then performs either image or array extraction.
564    ///
565    /// # Returns
566    /// Result indicating success or an error
567    fn execute(&self) -> TiffResult<()> {
568        info!("Executing extract command with array_mode={}", self.array_mode);
569
570        // Determine region to extract
571        info!("Determining extraction region");
572        let region = match self.determine_region() {
573            Ok(r) => {
574                info!("Region determination successful: {:?}", r);
575                r
576            },
577            Err(e) => {
578                error!("Failed to determine region: {}", e);
579                return Err(e);
580            }
581        };
582
583        // Handle colormap extraction if requested (for both image and array modes)
584        info!("Handling colormap extraction");
585        if let Err(e) = self.handle_colormap_extraction() {
586            error!("Colormap extraction failed: {}", e);
587            return Err(e);
588        }
589
590        if self.array_mode {
591            // Array extraction mode
592            info!("Using array extraction mode");
593            self.extract_array_data(region)
594        } else {
595            // Image extraction mode
596            info!("Using image extraction mode");
597            info!("Extracting image data from {} to {}", self.input_file, self.output_file);
598
599            // Create an extractor instance
600            let mut extractor = ImageExtractor::new(self.logger);
601
602            // Check for reprojection requirement
603            if let Some(proj_code) = self.proj_code {
604                info!("Reprojection requested to EPSG:{}", proj_code);
605
606                // Handle extraction with or without colormap
607                if let Some(colormap_path) = &self.colormap_input {
608                    // Extract image data to memory first
609                    let mut image = extractor.extract_image(&self.input_file, region)?;
610
611                    // Apply filtering if specified
612                    if let Some(filter_str) = &self.filter_range {
613                        if let Ok((min_value, max_value)) = filter_utils::parse_filter_range(filter_str) {
614                            info!("Filtering values from {} to {}", min_value, max_value);
615                            image = filter_utils::filter_image_values(
616                                &image,
617                                min_value,
618                                max_value,
619                                0,
620                                self.filter_transparency
621                            );
622                        }
623                    }
624
625                    // Apply colormap to the extracted image
626                    let grayscale = image.to_luma8();
627                    let colormap = colormap_utils::load_colormap(colormap_path, self.logger)?;
628                    let rgb_image = colormap_utils::apply_colormap_to_image(&grayscale, &colormap);
629
630                    // Reproject and save image
631                    reprojection_utils::reproject_and_save(
632                        &DynamicImage::ImageRgb8(rgb_image),
633                        &self.input_file,
634                        &self.output_file,
635                        region,
636                        proj_code,
637                        self.logger,
638                        Some(&self.shape)
639                    )
640                } else {
641                    // Extract image first
642                    let mut image = extractor.extract_image(&self.input_file, region)?;
643
644                    // Apply filtering if specified
645                    if let Some(filter_str) = &self.filter_range {
646                        if let Ok((min_value, max_value)) = filter_utils::parse_filter_range(filter_str) {
647                            info!("Filtering values from {} to {}", min_value, max_value);
648                            image = filter_utils::filter_image_values(
649                                &image,
650                                min_value,
651                                max_value,
652                                0,
653                                self.filter_transparency
654                            );
655                        }
656                    }
657
658                    // Reproject and save without colormap
659                    reprojection_utils::reproject_and_save(
660                        &image,
661                        &self.input_file,
662                        &self.output_file,
663                        region,
664                        proj_code,
665                        self.logger,
666                        Some(&self.shape)
667                    )
668                }
669            } else {
670                // No reprojection requested - use standard extraction
671                info!("No reprojection requested, using standard extraction");
672
673                // Handle extraction with or without colormap
674                if let Some(colormap_path) = &self.colormap_input {
675                    // Extract with colormap
676                    self.extract_with_colormap(&mut extractor, region, colormap_path)
677                } else {
678                    // Check if we need to filter
679                    if let Some(filter_str) = &self.filter_range {
680                        // Extract the image first
681                        info!("Extracting and filtering image");
682                        let image = extractor.extract_image(&self.input_file, region)?;
683
684                        // Apply filtering
685                        let filtered_image = match filter_utils::parse_filter_range(filter_str) {
686                            Ok((min_value, max_value)) => {
687                                info!("Filtering values from {} to {}", min_value, max_value);
688                                filter_utils::filter_image_values(
689                                    &image,
690                                    min_value,
691                                    max_value,
692                                    0, // Background value
693                                    self.filter_transparency
694                                )
695                            },
696                            Err(err) => {
697                                warn!("Failed to parse filter range: {}", err);
698                                image
699                            }
700                        };
701
702                        // Save the filtered image
703                        crate::utils::mask_utils::save_shaped_image(&filtered_image, &self.output_file, &self.shape)
704                    } else {
705                        // Simple extraction with shape masking
706                        extractor.extract_to_file(&self.input_file, &self.output_file, region, Some(&self.shape))
707                    }
708                }
709            }
710        }
711    }
712}