1use std::collections::HashMap;
2use std::fmt::Debug;
3use std::fs::File;
4use std::path::{Path, PathBuf};
5use std::vec;
6
7use async_trait::async_trait;
8use log::warn;
9use martin_tile_utils::{Format, TileCoord, TileData, TileInfo};
10use tiff::decoder::{ChunkType, Decoder};
11use tiff::tags::Tag::{self, GdalNodata};
12use tilejson::{TileJSON, tilejson};
13
14use crate::tiles::cog::CogError;
15use crate::tiles::cog::image::Image;
16use crate::tiles::cog::model::ModelInfo;
17use crate::tiles::{MartinCoreResult, Source, UrlQuery};
18
19#[derive(Clone, Debug)]
21pub struct CogSource {
22 id: String,
23 path: PathBuf,
24 min_zoom: u8,
25 max_zoom: u8,
26 _model: ModelInfo,
27 _origin: [f64; 3],
29 _extent: [f64; 4],
31 images: HashMap<u8, Image>,
32 nodata: Option<f64>,
33 tilejson: TileJSON,
34 tileinfo: TileInfo,
35}
36
37impl CogSource {
38 #[expect(clippy::cast_possible_truncation)]
39 pub fn new(id: String, path: PathBuf) -> Result<Self, CogError> {
41 let tileinfo = TileInfo::new(Format::Png, martin_tile_utils::Encoding::Uncompressed);
42 let tif_file =
43 File::open(&path).map_err(|e: std::io::Error| CogError::IoError(e, path.clone()))?;
44 let mut decoder = Decoder::new(tif_file)
45 .map_err(|e| CogError::InvalidTiffFile(e, path.clone()))?
46 .with_limits(tiff::decoder::Limits::unlimited());
47 let model = ModelInfo::decode(&mut decoder, &path);
48 verify_requirements(&mut decoder, &model, &path.clone())?;
49 let nodata: Option<f64> = if let Ok(no_data) = decoder.get_tag_ascii_string(GdalNodata) {
50 no_data.parse().ok()
51 } else {
52 None
53 };
54 let origin = get_origin(
55 model.tie_points.as_deref(),
56 model.transformation.as_deref(),
57 &path,
58 )?;
59 let (full_width_pixel, full_length_pixel) = dimensions_in_pixel(&mut decoder, &path, 0)?;
60 let (full_width, full_length) = dimensions_in_model(
61 &mut decoder,
62 &path,
63 0,
64 model.pixel_scale.as_deref(),
65 model.transformation.as_deref(),
66 )?;
67 let extent = get_extent(
68 &origin,
69 model.transformation.as_deref(),
70 (full_width_pixel, full_length_pixel),
71 (full_width, full_length),
72 );
73
74 let mut images = vec![];
75 let mut ifd_index = 0;
76
77 loop {
78 let is_image = decoder
79 .get_tag_u32(Tag::NewSubfileType)
80 .map_or_else(|_| true, |v| v & 4 != 4); if is_image {
82 images.push(get_image(&mut decoder, &path, ifd_index)?);
84 } else {
85 warn!(
86 "A subfile of {} is ignored in the tiff file as Martin currently does not support mask subfile in tiff. IFD={ifd_index}",
87 path.display(),
88 );
89 }
90
91 ifd_index += 1;
92
93 let next_res = decoder.seek_to_image(ifd_index);
94 if next_res.is_err() {
95 break;
97 }
98 }
99 let min_zoom = 0;
100 let max_zoom = (images.len() - 1) as u8;
101 let images: HashMap<u8, Image> = images
102 .into_iter()
103 .enumerate()
104 .map(|(idx, image)| {
105 let zoom = max_zoom.saturating_sub(idx as u8);
106 (zoom, image)
107 })
108 .collect();
109 let tilejson = tilejson! {
110 tiles: vec![],
111 minzoom: min_zoom,
112 maxzoom: max_zoom
113 };
114 Ok(CogSource {
115 id,
116 path,
117 min_zoom,
118 max_zoom,
119 _model: model,
121 _origin: origin,
122 _extent: extent,
123 images,
124 nodata,
125 tilejson,
126 tileinfo,
127 })
128 }
129}
130
131#[async_trait]
132impl Source for CogSource {
133 fn get_id(&self) -> &str {
134 &self.id
135 }
136
137 fn get_tilejson(&self) -> &TileJSON {
138 &self.tilejson
139 }
140
141 fn get_tile_info(&self) -> TileInfo {
142 self.tileinfo
143 }
144
145 fn clone_source(&self) -> Box<dyn Source> {
146 Box::new(self.clone())
147 }
148
149 fn benefits_from_concurrent_scraping(&self) -> bool {
153 false
156 }
157
158 async fn get_tile(
159 &self,
160 xyz: TileCoord,
161 _url_query: Option<&UrlQuery>,
162 ) -> MartinCoreResult<TileData> {
163 if xyz.z < self.min_zoom || xyz.z > self.max_zoom {
164 return Ok(Vec::new());
165 }
166 let tif_file =
167 File::open(&self.path).map_err(|e| CogError::IoError(e, self.path.clone()))?;
168 let mut decoder =
169 Decoder::new(tif_file).map_err(|e| CogError::InvalidTiffFile(e, self.path.clone()))?;
170 decoder = decoder.with_limits(tiff::decoder::Limits::unlimited());
171
172 let image = self.images.get(&(xyz.z)).ok_or_else(|| {
173 CogError::ZoomOutOfRange(xyz.z, self.path.clone(), self.min_zoom, self.max_zoom)
174 })?;
175
176 let bytes = image.get_tile(&mut decoder, xyz, self.nodata, &self.path)?;
177 Ok(bytes)
178 }
179}
180
181fn verify_requirements(
182 decoder: &mut Decoder<File>,
183 model: &ModelInfo,
184 path: &Path,
185) -> Result<(), CogError> {
186 let chunk_type = decoder.get_chunk_type();
187 if chunk_type != ChunkType::Tile {
189 Err(CogError::NotSupportedChunkType(path.to_path_buf()))?;
190 }
191
192 decoder
195 .get_tag_unsigned(Tag::PlanarConfiguration)
196 .map_err(|e| {
197 CogError::TagsNotFound(
198 e,
199 vec![Tag::PlanarConfiguration.to_u16()],
200 0,
201 path.to_path_buf(),
202 )
203 })
204 .and_then(|config| {
205 if config == 1 {
206 Ok(())
207 } else {
208 Err(CogError::PlanarConfigurationNotSupported(
209 path.to_path_buf(),
210 0,
211 config,
212 ))
213 }
214 })?;
215
216 let color_type = decoder
217 .colortype()
218 .map_err(|e| CogError::InvalidTiffFile(e, path.to_path_buf()))?;
219
220 if !matches!(
221 color_type,
222 tiff::ColorType::RGB(8) | tiff::ColorType::RGBA(8)
223 ) {
224 Err(CogError::NotSupportedColorTypeAndBitDepth(
225 color_type,
226 path.to_path_buf(),
227 ))?;
228 }
229
230 match (&model.pixel_scale, &model.tie_points, &model.transformation) {
231 (Some(pixel_scale), Some(tie_points), _)
232 =>
233 {
234 if pixel_scale.len() != 3 {
235 Err(CogError::InvalidGeoInformation(path.to_path_buf(), "The count of pixel scale should be 3".to_string()))
236 }
237 else if (pixel_scale[0].abs() - pixel_scale[1].abs()).abs() > 0.01{
238 Err(CogError::NonSquaredImage(path.to_path_buf(), pixel_scale[0], pixel_scale[1]))
239 }
240 else if tie_points.len() % 6 != 0 {
241 Err(CogError::InvalidGeoInformation(path.to_path_buf(), "The count of tie points should be a multiple of 6".to_string()))
242 }else{
243 Ok(())
244 }
245 }
246 (_, _, Some(matrix))
247 => {
248 if matrix.len() == 16 {
249 Ok(())
250 } else {
251 Err(CogError::InvalidGeoInformation(path.to_path_buf(), "The length of matrix should be 16".to_string()))
252 }
253 },
254 _ => Err(CogError::InvalidGeoInformation(path.to_path_buf(), "Either a valid transformation (tag 34264) or both pixel scale (tag 33550) and tie points (tag 33922) must be provided".to_string())),
255 }?;
256
257 Ok(())
258}
259
260fn get_image(
261 decoder: &mut Decoder<File>,
262 path: &Path,
263 ifd_index: usize,
264) -> Result<Image, CogError> {
265 let (tile_width, tile_height) = (decoder.chunk_dimensions().0, decoder.chunk_dimensions().1);
266 let (image_width, image_length) = dimensions_in_pixel(decoder, path, ifd_index)?;
267 let tiles_across = image_width.div_ceil(tile_width);
268 let tiles_down = image_length.div_ceil(tile_height);
269
270 Ok(Image::new(ifd_index, tiles_across, tiles_down))
271}
272
273fn dimensions_in_pixel(
275 decoder: &mut Decoder<File>,
276 path: &Path,
277 ifd_index: usize,
278) -> Result<(u32, u32), CogError> {
279 let (image_width, image_length) = decoder.dimensions().map_err(|e| {
280 CogError::TagsNotFound(
281 e,
282 vec![Tag::ImageWidth.to_u16(), Tag::ImageLength.to_u16()],
283 ifd_index,
284 path.to_path_buf(),
285 )
286 })?;
287
288 Ok((image_width, image_length))
289}
290
291fn dimensions_in_model(
293 decoder: &mut Decoder<File>,
294 path: &Path,
295 ifd_index: usize,
296 pixel_scale: Option<&[f64]>,
297 transformation: Option<&[f64]>,
298) -> Result<(f64, f64), CogError> {
299 let (image_width_pixel, image_length_pixel) = dimensions_in_pixel(decoder, path, ifd_index)?;
300
301 let full_resolution = get_full_resolution(pixel_scale, transformation, path)?;
302
303 let width_in_model = f64::from(image_width_pixel) * full_resolution[0].abs();
304 let length_in_model = f64::from(image_length_pixel) * full_resolution[1].abs();
305
306 Ok((width_in_model, length_in_model))
307}
308
309fn get_origin(
310 tie_points: Option<&[f64]>,
311 transformation: Option<&[f64]>,
312 path: &Path,
313) -> Result<[f64; 3], CogError> {
314 match (tie_points, transformation) {
317 (Some(points), _) if points.len() >= 6 => Ok([points[3], points[4], points[5]]),
319
320 (_, Some(matrix)) if matrix.len() >= 12 => Ok([matrix[3], matrix[7], matrix[11]]),
337 _ => Err(CogError::GetOriginFailed(path.to_path_buf())),
338 }
339}
340
341fn get_full_resolution(
342 pixel_scale: Option<&[f64]>,
343 transformation: Option<&[f64]>,
344 path: &Path,
345) -> Result<[f64; 2], CogError> {
346 match (pixel_scale, transformation) {
347 (Some(scale), _) => Ok([scale[0], scale[1]]),
349 (_, Some(matrix)) => {
363 let mut x_res = (matrix[0] * matrix[0] + matrix[4] * matrix[4]).sqrt();
364 x_res = x_res.copysign(matrix[0]);
365 let mut y_res = (matrix[1] * matrix[1] + matrix[5] * matrix[5]).sqrt();
366 y_res = y_res.copysign(-matrix[5]);
368 Ok([x_res, y_res]) }
370 (None, None) => Err(CogError::GetFullResolutionFailed(path.to_path_buf())),
371 }
372}
373
374fn raster2model(i: u32, j: u32, matrix: &[f64]) -> (f64, f64) {
375 let i = f64::from(i);
376 let j = f64::from(j);
377 let x = matrix[3] + (matrix[0] * i) + (matrix[1] * j);
378 let y = matrix[7] + (matrix[4] * i) + (matrix[5] * j);
379 (x, y)
380}
381
382fn get_extent(
384 origin: &[f64; 3],
385 transformation: Option<&[f64]>,
386 (full_width_pixel, full_height_pixel): (u32, u32),
387 (full_width, full_height): (f64, f64),
388) -> [f64; 4] {
389 if let Some(matrix) = transformation {
390 let corner_pixels = [
391 (0, 0), (0, full_height_pixel), (full_width_pixel, 0), (full_width_pixel, full_height_pixel), ];
396
397 let (mut min_x, mut min_y) = raster2model(corner_pixels[0].0, corner_pixels[0].1, matrix);
399 let mut max_x = min_x;
400 let mut max_y = min_y;
401
402 for &(i, j) in corner_pixels.iter().skip(1) {
404 let (x, y) = raster2model(i, j, matrix);
405 min_x = min_x.min(x);
406 min_y = min_y.min(y);
407 max_x = max_x.max(x);
408 max_y = max_y.max(y);
409 }
410 return [min_x, min_y, max_x, max_y];
411 }
412 let [x1, y1, _] = origin;
413 let x2 = x1 + full_width;
414 let y2 = y1 - full_height;
415
416 [x1.min(x2), y1.min(y2), x1.max(x2), y1.max(y2)]
417}
418
419#[cfg(test)]
420mod tests {
421 use std::fs::File;
422 use std::path::Path;
423
424 use insta::assert_yaml_snapshot;
425 use rstest::rstest;
426 use tiff::decoder::Decoder;
427
428 use crate::tiles::cog::model::ModelInfo;
429
430 #[test]
431 fn can_get_model_info() {
432 let path = Path::new("../tests/fixtures/cog/rgb_u8.tif");
433 let tif_file = File::open(path).unwrap();
434 let mut decoder = Decoder::new(tif_file).unwrap();
435 let model = ModelInfo::decode(&mut decoder, path);
436
437 assert_yaml_snapshot!(model.pixel_scale, @r"
438 - 10
439 - 10
440 - 0
441 ");
442 assert_yaml_snapshot!(model.tie_points, @r"
443 - 0
444 - 0
445 - 0
446 - 1620750.2508
447 - 4277012.7153
448 - 0
449 ");
450 assert_yaml_snapshot!(model.transformation, @"~");
451 }
452
453 #[rstest]
454 #[case(
455 Some(vec![0.0, 0.0, 0.0, 1_620_750.250_8, 4_277_012.715_3, 0.0]),None,
456 Some([1_620_750.250_8, 4_277_012.715_3, 0.0])
457 )]
458 #[case(
459 None,Some(vec![
460 0.0, 100.0, 0.0, 400_000.0, 100.0, 0.0, 0.0, 500_000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
461 0.0, 1.0,
462 ]),
463 Some([400_000.0, 500_000.0, 0.0])
464 )]
465 #[case(None, None, None)]
466 fn can_get_origin(
467 #[case] tie_point: Option<Vec<f64>>,
468 #[case] matrix: Option<Vec<f64>>,
469 #[case] expected: Option<[f64; 3]>,
470 ) {
471 use approx::assert_abs_diff_eq;
472
473 let origin = super::get_origin(
474 tie_point.as_deref(),
475 matrix.as_deref(),
476 Path::new("not_exist.tif"),
477 )
478 .ok();
479 match (origin, expected) {
480 (Some(o), Some(e)) => {
481 assert_abs_diff_eq!(o[0], e[0]);
482 assert_abs_diff_eq!(o[1], e[1]);
483 assert_abs_diff_eq!(o[2], e[2]);
484 }
485 (None, None) => {
486 }
488 _ => {
489 panic!("Origin {origin:?} does not match expected {expected:?}");
490 }
491 }
492 }
493
494 #[rstest]
495 #[case(
496 None,Some(vec![10.0, 10.0,0.0]),Some(vec![0.0, 0.0, 0.0, 1_620_750.250_8, 4_277_012.715_3, 0.0]),(512,512),
497 [1_620_750.250_8, 4_271_892.715_3, 1_625_870.250_8, 4_277_012.715_3]
498 )]
499 #[case(
500 Some(vec![
501 10.0,0.0,0.0,1_620_750.250_8,
502 0.0,-10.0,0.0,4_277_012.715_3,
503 0.0,0.0,0.0,0.0,
504 0.0,0.0,0.0,1.0
505 ]),None,None,(512,512),
506 [1_620_750.250_8, 4_271_892.715_3, 1_625_870.250_8, 4_277_012.715_3]
507 )]
508 #[case(
509 Some(vec![
510 0.010_005_529_647_693, 0.0, 0.0, -7.583_906_932_854_38,
511 0.0, -0.009_986_188_755_447_6, 0.0, 38.750_354_738_325_9,
512 0.0, 0.0, 0.0, 0.0,
513 0.0, 0.0, 0.0, 1.0
514 ]), None, None, (598, 279),
515 [-7.583_906_9, 35.964_208_1, -1.600_600_2, 38.750_354_7]
516 )]
517 fn can_get_extent(
518 #[case] matrix: Option<Vec<f64>>,
519 #[case] pixel_scale: Option<Vec<f64>>,
520 #[case] tie_point: Option<Vec<f64>>,
521 #[case] (full_width_pixel, full_length_pixel): (u32, u32),
522 #[case] expected_extent: [f64; 4],
523 ) {
524 use approx::assert_abs_diff_eq;
525
526 use crate::tiles::cog::source::{get_extent, get_full_resolution, get_origin};
527
528 let origin = get_origin(
529 tie_point.as_deref(),
530 matrix.as_deref(),
531 Path::new("not_exist.tif"),
532 )
533 .unwrap();
534 let full_resolution = get_full_resolution(
535 pixel_scale.as_deref(),
536 matrix.as_deref(),
537 Path::new("not_exist.tif"),
538 )
539 .unwrap();
540
541 let full_width = full_resolution[0] * f64::from(full_width_pixel);
542 let full_length = full_resolution[1] * f64::from(full_length_pixel);
543
544 let extent = get_extent(
545 &origin,
546 matrix.as_deref(),
547 (full_width_pixel, full_length_pixel),
548 (full_width, full_length),
549 );
550
551 assert_abs_diff_eq!(extent[0], expected_extent[0], epsilon = 0.00001);
552 assert_abs_diff_eq!(extent[1], expected_extent[1], epsilon = 0.00001);
553 assert_abs_diff_eq!(extent[2], expected_extent[2], epsilon = 0.00001);
554 assert_abs_diff_eq!(extent[3], expected_extent[3], epsilon = 0.00001);
555 }
556
557 #[rstest]
558 #[case(
559 None,Some(vec![118.450_587_6, 118.450_587_6, 0.0]), [118.450_587_6, 118.450_587_6]
560 )]
561 #[case(
562 None,Some(vec![100.00, -100.0]), [100.0, -100.0]
563 )]
564 #[
565 case(
566 Some(vec![
567 0.010_005_529_647_693_3, 0.0, 0.0, -7.583_906_932_854_38, 0.0, -0.009_986_188_755_447_63, 0.0, 38.750_354_738_325_9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0]),
568 None, [0.010_005_529_647_693, 0.009_986_188_755_448])
569 ]
570 fn can_get_full_resolution(
571 #[case] matrix: Option<Vec<f64>>,
572 #[case] pixel_scale: Option<Vec<f64>>,
573 #[case] expected: [f64; 2],
574 ) {
575 use approx::assert_abs_diff_eq;
576
577 use crate::tiles::cog::source::get_full_resolution;
578
579 let full_resolution = get_full_resolution(
580 pixel_scale.as_deref(),
581 matrix.as_deref(),
582 Path::new("not_exist.tif"),
583 )
584 .unwrap();
585 assert_abs_diff_eq!(full_resolution[0], expected[0], epsilon = 0.00001);
586 assert_abs_diff_eq!(full_resolution[1], expected[1], epsilon = 0.00001);
587 }
588}