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
20pub struct ExtractCommand<'a> {
22 input_file: String,
24 output_file: String,
26 bbox_str: Option<String>,
28 coordinate_str: Option<String>,
30 radius: Option<f64>,
32 shape: String,
34 crs_code: Option<u32>,
36 proj_code: Option<u32>,
38 colormap_output: Option<String>,
40 colormap_input: Option<String>,
42 array_mode: bool,
44 array_format: String,
46 filter_range: Option<String>,
48 filter_transparency: bool,
50 logger: &'a Logger,
52}
53
54impl<'a> ExtractCommand<'a> {
55 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 let bbox_str = args.get_one::<String>("bbox").cloned();
78 info!("Bounding box: {:?}", bbox_str);
79
80 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 let shape = args.get_one::<String>("shape")
101 .cloned()
102 .unwrap_or_else(|| "square".to_string());
103 info!("Shape: {}", shape);
104
105 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 let crs_code = if let Some(crs_str) = args.get_one::<String>("crs") {
113 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 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 if coordinate_str.is_some() || bbox_str.is_some() {
137 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 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 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 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 let filter_range = args.get_one::<String>("filter").cloned();
183 info!("Filter range: {:?}", filter_range);
184
185 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 fn determine_effective_bbox(&self) -> TiffResult<Option<String>> {
219 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 )?;
228 info!("Calculated bounding box from coordinate: {}", bbox_str);
229 Ok(Some(bbox_str))
230 }
231 else if let Some(bbox) = &self.bbox_str {
233 info!("Using provided bounding box: {}", bbox);
234 Ok(Some(bbox.clone()))
235 }
236 else {
238 info!("No bounding box or coordinate specified");
239 Ok(None)
240 }
241 }
242
243 fn determine_region(&self) -> TiffResult<Option<Region>> {
252 info!("Determining extraction region");
253
254 let effective_bbox = self.determine_effective_bbox()?;
256
257 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 info!("Parsing bounding box");
267 let mut bbox = image_extraction_utils::parse_bbox(&bbox_str)?;
268
269 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 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 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 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 Ok(())
319 }
320 }
321 }
322
323 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 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 if let Some(filter_str) = &self.filter_range {
345 info!("Applying filter: {}", filter_str);
346
347 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 image = filter_utils::filter_image_values(
354 &image,
355 min_value,
356 max_value,
357 0, 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 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 info!("Converting image to grayscale");
388 let grayscale = image.to_luma8();
389
390 info!("Applying colormap to transform image");
392 let rgb_image = colormap_utils::apply_colormap_to_image(&grayscale, &colormap);
393
394 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) )
403 }
404
405 fn save_colorized_image(&self, rgb_image: image::RgbImage, region: Option<Region>) -> TiffResult<()> {
416 info!("Saving colorized image to {}", self.output_file);
417
418 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 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 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 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 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 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 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 match &result {
501 Ok(_) => info!("Array extraction completed successfully"),
502 Err(e) => error!("Array extraction failed: {}", e),
503 }
504
505 result
506 }
507
508 fn determine_region_with_radius(&self, radius_meters: Option<f64>) -> TiffResult<Option<Region>> {
510 info!("Determining extraction region with radius information");
511
512 let effective_bbox = self.determine_effective_bbox()?;
514
515 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 info!("Parsing bounding box");
525 let mut bbox = image_extraction_utils::parse_bbox(&bbox_str)?;
526
527 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 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 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 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 fn execute(&self) -> TiffResult<()> {
568 info!("Executing extract command with array_mode={}", self.array_mode);
569
570 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 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 info!("Using array extraction mode");
593 self.extract_array_data(region)
594 } else {
595 info!("Using image extraction mode");
597 info!("Extracting image data from {} to {}", self.input_file, self.output_file);
598
599 let mut extractor = ImageExtractor::new(self.logger);
601
602 if let Some(proj_code) = self.proj_code {
604 info!("Reprojection requested to EPSG:{}", proj_code);
605
606 if let Some(colormap_path) = &self.colormap_input {
608 let mut image = extractor.extract_image(&self.input_file, region)?;
610
611 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 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 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 let mut image = extractor.extract_image(&self.input_file, region)?;
643
644 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 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 info!("No reprojection requested, using standard extraction");
672
673 if let Some(colormap_path) = &self.colormap_input {
675 self.extract_with_colormap(&mut extractor, region, colormap_path)
677 } else {
678 if let Some(filter_str) = &self.filter_range {
680 info!("Extracting and filtering image");
682 let image = extractor.extract_image(&self.input_file, region)?;
683
684 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, self.filter_transparency
694 )
695 },
696 Err(err) => {
697 warn!("Failed to parse filter range: {}", err);
698 image
699 }
700 };
701
702 crate::utils::mask_utils::save_shaped_image(&filtered_image, &self.output_file, &self.shape)
704 } else {
705 extractor.extract_to_file(&self.input_file, &self.output_file, region, Some(&self.shape))
707 }
708 }
709 }
710 }
711 }
712}