From 010ef9911f961670520a601b3aa17af9bb692370 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Tue, 11 Feb 2025 10:19:18 +0800 Subject: [PATCH 01/58] add get_origin function --- martin/src/cog/errors.rs | 3 +++ martin/src/cog/source.rs | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/martin/src/cog/errors.rs b/martin/src/cog/errors.rs index a20fa880e..f4c1535dd 100644 --- a/martin/src/cog/errors.rs +++ b/martin/src/cog/errors.rs @@ -42,4 +42,7 @@ pub enum CogError { #[error("Striped tiff file is not supported, the tiff file is {0}")] NotSupportedChunkType(PathBuf), + + #[error("Get origin failed for {0}")] + GetOriginFailed(PathBuf), } diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index e6e6496d1..20d0b42ac 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -382,10 +382,23 @@ fn get_images_ifd(decoder: &mut Decoder, path: &Path) -> Vec { res } +fn get_origin( + tie_points: Option<&[f64]>, + transformation: Option<&[f64]>, + path: &Path, +) -> Result<[f64; 3], CogError> { + match (tie_points, transformation) { + (Some(points), _) if points.len() == 6 => Ok([points[3], points[4], points[5]]), + (_, Some(matrix)) if matrix.len() >= 12 => Ok([matrix[3], matrix[7], matrix[11]]), + _ => Err(CogError::GetOriginFailed(path.to_path_buf())), + } +} + #[cfg(test)] mod tests { use martin_tile_utils::TileCoord; use rstest::rstest; + use tiff::tags; use std::path::PathBuf; use crate::cog::source::get_tile_idx; @@ -459,4 +472,14 @@ mod tests { let expected = std::fs::read(expected_file_path).unwrap(); assert_eq!(png_bytes, expected); } + + #[test] + fn can_get_origin() { + let matrix: Option<&[f64]> = None; + let tie_point = Some(vec![0.0, 0.0, 0.0, 1620750.2508, 4277012.7153, 0.0]); + + let origin = super::get_origin(tie_point.as_deref(), matrix.as_deref(), &PathBuf::from("not_exist.tif")).unwrap(); + assert_eq!(origin, [1620750.2508, 4277012.7153, 0.0]); + //todo add a test for matrix either in this PR. + } } From ef04f937acf14ebc6ca8e99d3fdd15c2b1991c8a Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Tue, 11 Feb 2025 10:52:36 +0800 Subject: [PATCH 02/58] add get_model_info function --- martin/src/cog/errors.rs | 2 +- martin/src/cog/source.rs | 53 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/martin/src/cog/errors.rs b/martin/src/cog/errors.rs index f4c1535dd..d85c312a9 100644 --- a/martin/src/cog/errors.rs +++ b/martin/src/cog/errors.rs @@ -42,7 +42,7 @@ pub enum CogError { #[error("Striped tiff file is not supported, the tiff file is {0}")] NotSupportedChunkType(PathBuf), - + #[error("Get origin failed for {0}")] GetOriginFailed(PathBuf), } diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 20d0b42ac..8b38b83f2 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -285,6 +285,8 @@ fn get_meta(path: &PathBuf) -> Result { .map_err(|e| CogError::InvalidTiffFile(e, path.clone()))? .with_limits(tiff::decoder::Limits::unlimited()); + let (pixel_scale, tie_points, transformation) = get_model_infos(&mut decoder, path); + verify_requirments(&mut decoder, path)?; let mut zoom_and_ifd: HashMap = HashMap::new(); let mut zoom_and_tile_across_down: HashMap = HashMap::new(); @@ -324,6 +326,46 @@ fn get_meta(path: &PathBuf) -> Result { }) } +fn get_model_infos( + decoder: &mut Decoder, + path: &PathBuf, +) -> (Option>, Option>, Option>) { + let pixel_scale = decoder + .get_tag_f64_vec(Tag::ModelPixelScaleTag) + .map_err(|e| { + CogError::TagsNotFound( + e, + vec![Tag::ModelPixelScaleTag.to_u16()], + 0, + path.to_path_buf(), + ) + }) + .ok(); + let tie_points = decoder + .get_tag_f64_vec(Tag::ModelTiepointTag) + .map_err(|e| { + CogError::TagsNotFound( + e, + vec![Tag::ModelTiepointTag.to_u16()], + 0, + path.to_path_buf(), + ) + }) + .ok(); + let transformation = decoder + .get_tag_f64_vec(Tag::ModelTransformationTag) + .map_err(|e| { + CogError::TagsNotFound( + e, + vec![Tag::ModelTransformationTag.to_u16()], + 0, + path.to_path_buf(), + ) + }) + .ok(); + (pixel_scale, tie_points, transformation) +} + fn get_grid_dims( decoder: &mut Decoder, path: &Path, @@ -398,8 +440,8 @@ fn get_origin( mod tests { use martin_tile_utils::TileCoord; use rstest::rstest; - use tiff::tags; use std::path::PathBuf; + use tiff::tags; use crate::cog::source::get_tile_idx; @@ -478,8 +520,13 @@ mod tests { let matrix: Option<&[f64]> = None; let tie_point = Some(vec![0.0, 0.0, 0.0, 1620750.2508, 4277012.7153, 0.0]); - let origin = super::get_origin(tie_point.as_deref(), matrix.as_deref(), &PathBuf::from("not_exist.tif")).unwrap(); + let origin = super::get_origin( + tie_point.as_deref(), + matrix.as_deref(), + &PathBuf::from("not_exist.tif"), + ) + .unwrap(); assert_eq!(origin, [1620750.2508, 4277012.7153, 0.0]); - //todo add a test for matrix either in this PR. + //todo add a test for matrix either in this PR. } } From 72c6e64b74a8820992d33b59a03e4882b0203c61 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Tue, 11 Feb 2025 11:30:40 +0800 Subject: [PATCH 03/58] add model info verify --- martin/src/cog/errors.rs | 3 +++ martin/src/cog/source.rs | 45 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/martin/src/cog/errors.rs b/martin/src/cog/errors.rs index d85c312a9..aa4b9db32 100644 --- a/martin/src/cog/errors.rs +++ b/martin/src/cog/errors.rs @@ -45,4 +45,7 @@ pub enum CogError { #[error("Get origin failed for {0}")] GetOriginFailed(PathBuf), + + #[error("Coord transformation in {0} is invalid")] + InvalidGeoInformation(PathBuf, String), } diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 8b38b83f2..ff10e7a3b 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -275,6 +275,21 @@ fn verify_requirments(decoder: &mut Decoder, path: &Path) -> Result<(), Co path.to_path_buf(), ))?; }; + + let model = get_model_infos(decoder, &path.to_path_buf()); + + match model { + (Some(pixel_scale), Some(tie_points), _) + if pixel_scale.len() != 3 || tie_points.len() % 6 != 0 => + { + Err(CogError::InvalidGeoInformation(path.to_path_buf(), "The length of pixel scale should be 3, and the length of tie points should be a multiple of 6".to_string())) + } + (_, _, Some(matrix)) if matrix.len() != 16 => { + Err(CogError::InvalidGeoInformation(path.to_path_buf(), "The length of matrix should be 16".to_string())) + } + _ => Err(CogError::InvalidGeoInformation(path.to_path_buf(), "The model information is not found, either transformation (tag number 34264) or pixel scale(tag number 33550) && tie points(33922) should be inside ".to_string())), + }?; + Ok(()) } @@ -438,10 +453,12 @@ fn get_origin( #[cfg(test)] mod tests { + + use insta::assert_yaml_snapshot; use martin_tile_utils::TileCoord; use rstest::rstest; - use std::path::PathBuf; - use tiff::tags; + use std::{fs::File, path::PathBuf}; + use tiff::decoder::Decoder; use crate::cog::source::get_tile_idx; @@ -529,4 +546,28 @@ mod tests { assert_eq!(origin, [1620750.2508, 4277012.7153, 0.0]); //todo add a test for matrix either in this PR. } + + #[test] + fn can_get_model_infos() { + let path = PathBuf::from("../tests/fixtures/cog/rgb_u8.tif"); + let tif_file = File::open(&path).unwrap(); + let mut decoder = Decoder::new(tif_file).unwrap(); + + let (pixel_scale, tie_points, transformation) = super::get_model_infos(&mut decoder, &path); + + assert_yaml_snapshot!(pixel_scale, @r###" + - 10 + - 10 + - 0 + "###); + assert_yaml_snapshot!(tie_points, @r###" + - 0 + - 0 + - 0 + - 1620750.2508 + - 4277012.7153 + - 0 + "###); + assert_yaml_snapshot!(transformation, @"~"); + } } From 4654b135544730525ba8d4f88881822ef5554509 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Tue, 11 Feb 2025 16:16:54 +0800 Subject: [PATCH 04/58] get resolutions in meta --- martin/src/cog/errors.rs | 3 ++ martin/src/cog/source.rs | 77 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/martin/src/cog/errors.rs b/martin/src/cog/errors.rs index aa4b9db32..a860d0ecd 100644 --- a/martin/src/cog/errors.rs +++ b/martin/src/cog/errors.rs @@ -48,4 +48,7 @@ pub enum CogError { #[error("Coord transformation in {0} is invalid")] InvalidGeoInformation(PathBuf, String), + + #[error("Get full resolution failed from file: {0}")] + GetFullResolutionFailed(PathBuf), } diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index ff10e7a3b..b221e44d3 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -300,8 +300,6 @@ fn get_meta(path: &PathBuf) -> Result { .map_err(|e| CogError::InvalidTiffFile(e, path.clone()))? .with_limits(tiff::decoder::Limits::unlimited()); - let (pixel_scale, tie_points, transformation) = get_model_infos(&mut decoder, path); - verify_requirments(&mut decoder, path)?; let mut zoom_and_ifd: HashMap = HashMap::new(); let mut zoom_and_tile_across_down: HashMap = HashMap::new(); @@ -314,6 +312,21 @@ fn get_meta(path: &PathBuf) -> Result { let images_ifd = get_images_ifd(&mut decoder, path); + let (pixel_scale, _, transformations) = get_model_infos(&mut decoder, path); + + let mut resolutions = HashMap::new(); + let full_resolution = + get_full_resolution(pixel_scale.as_deref(), transformations.as_deref(), path)?; + let (full_width_pixel, full_length_pixel) = decoder.dimensions().map_err(|e| { + CogError::TagsNotFound( + e, + vec![Tag::ImageWidth.to_u16(), Tag::ImageLength.to_u16()], + 0, // we are at ifd 0, the first image, haven't seek to others + path.to_path_buf(), + ) + })?; + let full_width = full_resolution[0] * f64::from(full_width_pixel); + let full_height = full_resolution[1] * f64::from(full_length_pixel); for (idx, image_ifd) in images_ifd.iter().enumerate() { decoder .seek_to_image(*image_ifd) @@ -321,11 +334,27 @@ fn get_meta(path: &PathBuf) -> Result { let zoom = u8::try_from(images_ifd.len() - (idx + 1)) .map_err(|_| CogError::TooManyImages(path.clone()))?; - + let resolution; + if zoom == 0 { + resolution = full_resolution; + } else { + let (image_width, image_length) = decoder.dimensions().map_err(|e| { + CogError::TagsNotFound( + e, + vec![Tag::ImageWidth.to_u16(), Tag::ImageLength.to_u16()], + *image_ifd, + path.to_path_buf(), + ) + })?; + let res_x = f64::from(image_width) / f64::from(full_width); + let res_y = f64::from(image_length) / f64::from(full_height); + resolution = [res_x, res_y, 0.0]; + } let (tiles_across, tiles_down) = get_grid_dims(&mut decoder, path, *image_ifd)?; zoom_and_ifd.insert(zoom, *image_ifd); zoom_and_tile_across_down.insert(zoom, (tiles_across, tiles_down)); + resolutions.insert(zoom, resolution); } if images_ifd.is_empty() { @@ -341,6 +370,27 @@ fn get_meta(path: &PathBuf) -> Result { }) } +fn get_full_resolution( + pixel_scale: Option<&[f64]>, + transformation: Option<&[f64]>, + path: &Path, +) -> Result<[f64; 3], CogError> { + match (pixel_scale, transformation) { + (Some(scale), _) => Ok([scale[0], scale[1], scale[2]]), + (_, Some(matrix)) => { + if matrix[1] == 0.0 && matrix[4] == 0.0 { + Ok([matrix[0], matrix[5], matrix[10]]) + } else { + let x_res = (matrix[0] * matrix[0]) + (matrix[4] * matrix[4]); + let y_res = ((matrix[1] * matrix[1]) + (matrix[5] * matrix[5])).sqrt() * -1.0; + let z_res = matrix[10]; + return Ok([x_res, y_res, z_res]); + } + } + (None, None) => Err(CogError::GetFullResolutionFailed(path.to_path_buf())), + } +} + fn get_model_infos( decoder: &mut Decoder, path: &PathBuf, @@ -462,6 +512,8 @@ mod tests { use crate::cog::source::get_tile_idx; + use super::{get_full_resolution, get_model_infos}; + #[test] fn can_calc_tile_idx() { assert_eq!(Some(0), get_tile_idx(TileCoord { z: 0, x: 0, y: 0 }, 3, 3)); @@ -570,4 +622,23 @@ mod tests { "###); assert_yaml_snapshot!(transformation, @"~"); } + + #[test] + fn can_get_full_resolution() { + let pixel_scale = Some(vec![10.000, -10.000, 0.000]); + let transformation: Option<&[f64]> = None; + + let resolution = get_full_resolution( + pixel_scale.as_deref(), + transformation.as_deref(), + &PathBuf::from("not_exist.tif"), + ) + .ok(); + + assert_yaml_snapshot!(resolution, @r###" + - 10 + - -10 + - 0 + "###); + } } From 77b72aa56c6cb17f904781d41af5d323469ee429 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Tue, 11 Feb 2025 16:19:26 +0800 Subject: [PATCH 05/58] add resolutions to meta --- martin/src/cog/source.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index b221e44d3..b56f89d22 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -22,6 +22,7 @@ use super::CogError; struct Meta { min_zoom: u8, max_zoom: u8, + resolutions: HashMap, zoom_and_ifd: HashMap, zoom_and_tile_across_down: HashMap, nodata: Option, @@ -364,6 +365,7 @@ fn get_meta(path: &PathBuf) -> Result { Ok(Meta { min_zoom: 0, max_zoom: images_ifd.len() as u8 - 1, + resolutions, zoom_and_ifd, zoom_and_tile_across_down, nodata, From 139337e9680177c50f23d3803d0ab37414814a17 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Tue, 11 Feb 2025 16:21:17 +0800 Subject: [PATCH 06/58] add origin to meta --- martin/src/cog/source.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index b56f89d22..19f087132 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -26,6 +26,7 @@ struct Meta { zoom_and_ifd: HashMap, zoom_and_tile_across_down: HashMap, nodata: Option, + origin: [f64; 3], } #[derive(Clone, Debug)] @@ -313,7 +314,9 @@ fn get_meta(path: &PathBuf) -> Result { let images_ifd = get_images_ifd(&mut decoder, path); - let (pixel_scale, _, transformations) = get_model_infos(&mut decoder, path); + let (pixel_scale, tie_points, transformations) = get_model_infos(&mut decoder, path); + + let origin = get_origin(tie_points.as_deref(), transformations.as_deref(), path)?; let mut resolutions = HashMap::new(); let full_resolution = @@ -366,6 +369,7 @@ fn get_meta(path: &PathBuf) -> Result { min_zoom: 0, max_zoom: images_ifd.len() as u8 - 1, resolutions, + origin, zoom_and_ifd, zoom_and_tile_across_down, nodata, From 887f8850ef251a0a3d060efc6d555a872898396d Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Tue, 11 Feb 2025 17:03:14 +0800 Subject: [PATCH 07/58] add get_extent method --- martin/src/cog/errors.rs | 3 ++ martin/src/cog/source.rs | 64 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/martin/src/cog/errors.rs b/martin/src/cog/errors.rs index a860d0ecd..1a62d7205 100644 --- a/martin/src/cog/errors.rs +++ b/martin/src/cog/errors.rs @@ -51,4 +51,7 @@ pub enum CogError { #[error("Get full resolution failed from file: {0}")] GetFullResolutionFailed(PathBuf), + + #[error("Get extent failed from file: {0}")] + GetExtentFailed(PathBuf), } diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 19f087132..9fd39d63a 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -27,6 +27,7 @@ struct Meta { zoom_and_tile_across_down: HashMap, nodata: Option, origin: [f64; 3], + extent: [f64; 4], } #[derive(Clone, Debug)] @@ -329,8 +330,17 @@ fn get_meta(path: &PathBuf) -> Result { path.to_path_buf(), ) })?; + let full_width = full_resolution[0] * f64::from(full_width_pixel); - let full_height = full_resolution[1] * f64::from(full_length_pixel); + let full_length = full_resolution[1] * f64::from(full_length_pixel); + + let extent = get_extent( + transformations.as_deref(), + &origin, + (full_width_pixel, full_length_pixel), + (full_width, full_length), + ); + for (idx, image_ifd) in images_ifd.iter().enumerate() { decoder .seek_to_image(*image_ifd) @@ -351,7 +361,7 @@ fn get_meta(path: &PathBuf) -> Result { ) })?; let res_x = f64::from(image_width) / f64::from(full_width); - let res_y = f64::from(image_length) / f64::from(full_height); + let res_y = f64::from(image_length) / f64::from(full_length); resolution = [res_x, res_y, 0.0]; } let (tiles_across, tiles_down) = get_grid_dims(&mut decoder, path, *image_ifd)?; @@ -369,6 +379,7 @@ fn get_meta(path: &PathBuf) -> Result { min_zoom: 0, max_zoom: images_ifd.len() as u8 - 1, resolutions, + extent, origin, zoom_and_ifd, zoom_and_tile_across_down, @@ -376,6 +387,55 @@ fn get_meta(path: &PathBuf) -> Result { }) } +fn get_extent( + transformation: Option<&[f64]>, + origin: &[f64], + (full_width_pixel, full_height_pixel): (u32, u32), + (full_width, full_height): (f64, f64), +) -> [f64; 4] { + if let Some(matrix) = transformation { + let corners = [ + [0, 0], + [0, full_height_pixel], + [full_width_pixel, 0], + [full_width_pixel, full_height_pixel], + ]; + let transed = corners.map(|pixel| { + let i = f64::from(pixel[0]); + let j = f64::from(pixel[1]); + let x = matrix[3] + (matrix[0] * i) + (matrix[1] * j); + let y = matrix[7] + (matrix[4] * i) + (matrix[5] * j); + (x, y) + }); + let mut min_x = transed[0].0; + let mut min_y = transed[1].1; + let mut max_x = transed[0].0; + let mut max_y = transed[1].1; + for (x, y) in transed { + if x <= min_x { + min_x = x; + } + if y <= min_y { + min_y = y; + } + if x >= max_x { + max_x = x; + } + if y >= max_y { + max_y = y; + } + } + return [min_x, min_y, max_x, max_y]; + } else { + let x1 = origin[0]; + let y1 = origin[1]; + let x2 = x1 + f64::from(full_width); + let y2 = y1 + f64::from(full_height); + + return [x1.min(x2), y1.min(y2), x1.max(x2), y1.max(y2)]; + } +} + fn get_full_resolution( pixel_scale: Option<&[f64]>, transformation: Option<&[f64]>, From 83432bc508ae3ce30b44248220c0f162c1072165 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Tue, 11 Feb 2025 23:15:31 +0800 Subject: [PATCH 08/58] get coord transformation info only once --- martin/src/cog/source.rs | 57 ++++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 9fd39d63a..7b42f0c23 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -5,6 +5,7 @@ use std::vec; use std::{fmt::Debug, path::PathBuf}; use log::warn; +use serde::Serialize; use std::io::BufWriter; use tiff::decoder::{ChunkType, Decoder, DecodingResult}; use tiff::tags::Tag::{self, GdalNodata}; @@ -18,7 +19,7 @@ use crate::{file_config::FileResult, MartinResult, Source, TileData, UrlQuery}; use super::CogError; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Serialize)] struct Meta { min_zoom: u8, max_zoom: u8, @@ -234,7 +235,11 @@ fn rgb_to_png( Ok(result_file_buffer) } -fn verify_requirments(decoder: &mut Decoder, path: &Path) -> Result<(), CogError> { +fn verify_requirments( + decoder: &mut Decoder, + (pixel_scale, tie_points, transformation): (Option<&[f64]>, Option<&[f64]>, Option<&[f64]>), + path: &Path, +) -> Result<(), CogError> { let chunk_type = decoder.get_chunk_type(); // see the requirement 2 in https://docs.ogc.org/is/21-026/21-026.html#_tiles if chunk_type != ChunkType::Tile { @@ -279,18 +284,25 @@ fn verify_requirments(decoder: &mut Decoder, path: &Path) -> Result<(), Co ))?; }; - let model = get_model_infos(decoder, &path.to_path_buf()); - - match model { + match (pixel_scale, tie_points, transformation) { (Some(pixel_scale), Some(tie_points), _) - if pixel_scale.len() != 3 || tie_points.len() % 6 != 0 => + => { - Err(CogError::InvalidGeoInformation(path.to_path_buf(), "The length of pixel scale should be 3, and the length of tie points should be a multiple of 6".to_string())) - } - (_, _, Some(matrix)) if matrix.len() != 16 => { - Err(CogError::InvalidGeoInformation(path.to_path_buf(), "The length of matrix should be 16".to_string())) + if pixel_scale.len() != 3 || tie_points.len() % 6 != 0 { + Err(CogError::InvalidGeoInformation(path.to_path_buf(), "The length of pixel scale should be 3, and the length of tie points should be a multiple of 6".to_string())) + }else{ + Ok(()) + } + } + (_, _, Some(matrix)) + => { + if matrix.len() < 16 { + Err(CogError::InvalidGeoInformation(path.to_path_buf(), "The length of matrix should be 16".to_string())) + }else{ + Ok(()) } - _ => Err(CogError::InvalidGeoInformation(path.to_path_buf(), "The model information is not found, either transformation (tag number 34264) or pixel scale(tag number 33550) && tie points(33922) should be inside ".to_string())), + }, + _ => Err(CogError::InvalidGeoInformation(path.to_path_buf(), "The model information is not found, either transformation (tag number 34264) or pixel scale(tag number 33550) && tie points(33922) should be inside ".to_string())), }?; Ok(()) @@ -303,7 +315,16 @@ fn get_meta(path: &PathBuf) -> Result { .map_err(|e| CogError::InvalidTiffFile(e, path.clone()))? .with_limits(tiff::decoder::Limits::unlimited()); - verify_requirments(&mut decoder, path)?; + let (pixel_scale, tie_points, transformations) = get_model_infos(&mut decoder, path); + verify_requirments( + &mut decoder, + ( + pixel_scale.as_deref(), + tie_points.as_deref(), + transformations.as_deref(), + ), + &path, + )?; let mut zoom_and_ifd: HashMap = HashMap::new(); let mut zoom_and_tile_across_down: HashMap = HashMap::new(); @@ -315,8 +336,6 @@ fn get_meta(path: &PathBuf) -> Result { let images_ifd = get_images_ifd(&mut decoder, path); - let (pixel_scale, tie_points, transformations) = get_model_infos(&mut decoder, path); - let origin = get_origin(tie_points.as_deref(), transformations.as_deref(), path)?; let mut resolutions = HashMap::new(); @@ -570,6 +589,7 @@ fn get_origin( #[cfg(test)] mod tests { + use actix_web::dev::Path; use insta::assert_yaml_snapshot; use martin_tile_utils::TileCoord; use rstest::rstest; @@ -707,4 +727,13 @@ mod tests { - 0 "###); } + + #[test] + fn can_get_meta() { + let path = PathBuf::from("../tests/fixtures/cog/rgb_u8.tif"); + + let meta = super::get_meta(&path).unwrap(); + + assert_yaml_snapshot!(meta, @r###""###) + } } From 6f16f0ae57080110628538eac5463e9e497837f6 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Wed, 12 Feb 2025 15:52:10 +0800 Subject: [PATCH 09/58] use reference image t o calc resolution --- martin/src/cog/source.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 7b42f0c23..2f46eaead 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -379,8 +379,10 @@ fn get_meta(path: &PathBuf) -> Result { path.to_path_buf(), ) })?; - let res_x = f64::from(image_width) / f64::from(full_width); - let res_y = f64::from(image_length) / f64::from(full_length); + + let res_x = f64::from(full_width) / f64::from(image_width); + let res_y = f64::from(full_length) / f64::from(image_length); + resolution = [res_x, res_y, 0.0]; } let (tiles_across, tiles_down) = get_grid_dims(&mut decoder, path, *image_ifd)?; From 78a9a0501f5375e91a4b6c8b01a51e055675f8a4 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Wed, 12 Feb 2025 16:55:43 +0800 Subject: [PATCH 10/58] update test --- martin/src/cog/source.rs | 62 +++++++++++++++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 7 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 2f46eaead..eee82fd98 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -325,20 +325,14 @@ fn get_meta(path: &PathBuf) -> Result { ), &path, )?; - let mut zoom_and_ifd: HashMap = HashMap::new(); - let mut zoom_and_tile_across_down: HashMap = HashMap::new(); - let nodata: Option = if let Ok(no_data) = decoder.get_tag_ascii_string(GdalNodata) { no_data.parse().ok() } else { None }; - let images_ifd = get_images_ifd(&mut decoder, path); - let origin = get_origin(tie_points.as_deref(), transformations.as_deref(), path)?; - let mut resolutions = HashMap::new(); let full_resolution = get_full_resolution(pixel_scale.as_deref(), transformations.as_deref(), path)?; let (full_width_pixel, full_length_pixel) = decoder.dimensions().map_err(|e| { @@ -359,6 +353,12 @@ fn get_meta(path: &PathBuf) -> Result { (full_width_pixel, full_length_pixel), (full_width, full_length), ); + let mut zoom_and_ifd: HashMap = HashMap::new(); + let mut zoom_and_tile_across_down: HashMap = HashMap::new(); + + let mut resolutions = HashMap::new(); + + let images_ifd = get_images_ifd(&mut decoder, path); for (idx, image_ifd) in images_ifd.iter().enumerate() { decoder @@ -736,6 +736,54 @@ mod tests { let meta = super::get_meta(&path).unwrap(); - assert_yaml_snapshot!(meta, @r###""###) + assert_yaml_snapshot!(meta, @r###" + min_zoom: 0 + max_zoom: 3 + resolutions: + 2: + - 20 + - 20 + - 0 + 1: + - 40 + - 40 + - 0 + 0: + - 10 + - 10 + - 0 + 3: + - 10 + - 10 + - 0 + zoom_and_ifd: + 3: 0 + 1: 2 + 0: 3 + 2: 1 + zoom_and_tile_across_down: + 0: + - 1 + - 1 + 2: + - 1 + - 1 + 1: + - 1 + - 1 + 3: + - 2 + - 2 + nodata: ~ + origin: + - 1620750.2508 + - 4277012.7153 + - 0 + extent: + - 1620750.2508 + - 4277012.7153 + - 1625870.2508 + - 4282132.7153 + "###) } } From 05d175b15192b031a2fb7a64904584751e2e6034 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 14 Feb 2025 09:57:49 +0800 Subject: [PATCH 11/58] fmt && clippy --- martin/src/cog/source.rs | 75 ++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 42 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index eee82fd98..845b6421c 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -19,6 +19,8 @@ use crate::{file_config::FileResult, MartinResult, Source, TileData, UrlQuery}; use super::CogError; +type ModelInfo = (Option>, Option>, Option>); + #[derive(Clone, Debug, Serialize)] struct Meta { min_zoom: u8, @@ -237,7 +239,7 @@ fn rgb_to_png( fn verify_requirments( decoder: &mut Decoder, - (pixel_scale, tie_points, transformation): (Option<&[f64]>, Option<&[f64]>, Option<&[f64]>), + model_info: &ModelInfo, path: &Path, ) -> Result<(), CogError> { let chunk_type = decoder.get_chunk_type(); @@ -284,7 +286,7 @@ fn verify_requirments( ))?; }; - match (pixel_scale, tie_points, transformation) { + match model_info { (Some(pixel_scale), Some(tie_points), _) => { @@ -315,22 +317,18 @@ fn get_meta(path: &PathBuf) -> Result { .map_err(|e| CogError::InvalidTiffFile(e, path.clone()))? .with_limits(tiff::decoder::Limits::unlimited()); - let (pixel_scale, tie_points, transformations) = get_model_infos(&mut decoder, path); - verify_requirments( - &mut decoder, - ( - pixel_scale.as_deref(), - tie_points.as_deref(), - transformations.as_deref(), - ), - &path, - )?; + let model_info = get_model_infos(&mut decoder, path); + verify_requirments(&mut decoder, &model_info, path)?; let nodata: Option = if let Ok(no_data) = decoder.get_tag_ascii_string(GdalNodata) { no_data.parse().ok() } else { None }; + let pixel_scale = model_info.0; + let tie_points = model_info.1; + let transformations = model_info.2; + let origin = get_origin(tie_points.as_deref(), transformations.as_deref(), path)?; let full_resolution = @@ -340,7 +338,7 @@ fn get_meta(path: &PathBuf) -> Result { e, vec![Tag::ImageWidth.to_u16(), Tag::ImageLength.to_u16()], 0, // we are at ifd 0, the first image, haven't seek to others - path.to_path_buf(), + path.clone(), ) })?; @@ -367,24 +365,24 @@ fn get_meta(path: &PathBuf) -> Result { let zoom = u8::try_from(images_ifd.len() - (idx + 1)) .map_err(|_| CogError::TooManyImages(path.clone()))?; - let resolution; - if zoom == 0 { - resolution = full_resolution; + + let resolution = if zoom == 0 { + full_resolution } else { let (image_width, image_length) = decoder.dimensions().map_err(|e| { CogError::TagsNotFound( e, vec![Tag::ImageWidth.to_u16(), Tag::ImageLength.to_u16()], *image_ifd, - path.to_path_buf(), + path.clone(), ) })?; - let res_x = f64::from(full_width) / f64::from(image_width); - let res_y = f64::from(full_length) / f64::from(image_length); + let res_x = full_width / f64::from(image_width); + let res_y = full_length / f64::from(image_length); - resolution = [res_x, res_y, 0.0]; - } + [res_x, res_y, 0.0] + }; let (tiles_across, tiles_down) = get_grid_dims(&mut decoder, path, *image_ifd)?; zoom_and_ifd.insert(zoom, *image_ifd); @@ -447,14 +445,13 @@ fn get_extent( } } return [min_x, min_y, max_x, max_y]; - } else { - let x1 = origin[0]; - let y1 = origin[1]; - let x2 = x1 + f64::from(full_width); - let y2 = y1 + f64::from(full_height); - - return [x1.min(x2), y1.min(y2), x1.max(x2), y1.max(y2)]; } + let x1 = origin[0]; + let y1 = origin[1]; + let x2 = x1 + full_width; + let y2 = y1 + full_height; + + [x1.min(x2), y1.min(y2), x1.max(x2), y1.max(y2)] } fn get_full_resolution( @@ -471,17 +468,14 @@ fn get_full_resolution( let x_res = (matrix[0] * matrix[0]) + (matrix[4] * matrix[4]); let y_res = ((matrix[1] * matrix[1]) + (matrix[5] * matrix[5])).sqrt() * -1.0; let z_res = matrix[10]; - return Ok([x_res, y_res, z_res]); + Ok([x_res, y_res, z_res]) } } (None, None) => Err(CogError::GetFullResolutionFailed(path.to_path_buf())), } } -fn get_model_infos( - decoder: &mut Decoder, - path: &PathBuf, -) -> (Option>, Option>, Option>) { +fn get_model_infos(decoder: &mut Decoder, path: &Path) -> ModelInfo { let pixel_scale = decoder .get_tag_f64_vec(Tag::ModelPixelScaleTag) .map_err(|e| { @@ -591,16 +585,13 @@ fn get_origin( #[cfg(test)] mod tests { - use actix_web::dev::Path; use insta::assert_yaml_snapshot; use martin_tile_utils::TileCoord; use rstest::rstest; use std::{fs::File, path::PathBuf}; use tiff::decoder::Decoder; - use crate::cog::source::get_tile_idx; - - use super::{get_full_resolution, get_model_infos}; + use crate::cog::source::{get_full_resolution, get_tile_idx}; #[test] fn can_calc_tile_idx() { @@ -675,15 +666,15 @@ mod tests { #[test] fn can_get_origin() { let matrix: Option<&[f64]> = None; - let tie_point = Some(vec![0.0, 0.0, 0.0, 1620750.2508, 4277012.7153, 0.0]); + let tie_point = Some(vec![0.0, 0.0, 0.0, 1_620_750.250_8, 4_277_012.715_3, 0.0]); let origin = super::get_origin( tie_point.as_deref(), - matrix.as_deref(), + matrix, &PathBuf::from("not_exist.tif"), ) .unwrap(); - assert_eq!(origin, [1620750.2508, 4277012.7153, 0.0]); + assert_eq!(origin, [1_620_750.250_8, 4_277_012.715_3, 0.0]); //todo add a test for matrix either in this PR. } @@ -718,7 +709,7 @@ mod tests { let resolution = get_full_resolution( pixel_scale.as_deref(), - transformation.as_deref(), + transformation, &PathBuf::from("not_exist.tif"), ) .ok(); @@ -784,6 +775,6 @@ mod tests { - 4277012.7153 - 1625870.2508 - 4282132.7153 - "###) + "###); } } From c9b05546737c3e90e3682bcf8835a94054a1de17 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 14 Feb 2025 10:36:31 +0800 Subject: [PATCH 12/58] use approx in test --- Cargo.lock | 1 + martin/Cargo.toml | 1 + martin/src/cog/source.rs | 6 +++++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 1800bb8d9..9196e3181 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2719,6 +2719,7 @@ dependencies = [ "actix-web", "actix-web-static-files", "anyhow", + "approx", "async-trait", "bit-set", "brotli 7.0.0", diff --git a/martin/Cargo.toml b/martin/Cargo.toml index d756837e8..d761f162e 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -122,6 +122,7 @@ static-files = { workspace = true, optional = true } [dev-dependencies] anyhow.workspace = true +approx.workspace = true cargo-husky.workspace = true criterion.workspace = true ctor.workspace = true diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 845b6421c..d51dec789 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -592,6 +592,7 @@ mod tests { use tiff::decoder::Decoder; use crate::cog::source::{get_full_resolution, get_tile_idx}; + use approx::assert_abs_diff_eq; #[test] fn can_calc_tile_idx() { @@ -674,7 +675,10 @@ mod tests { &PathBuf::from("not_exist.tif"), ) .unwrap(); - assert_eq!(origin, [1_620_750.250_8, 4_277_012.715_3, 0.0]); + assert_abs_diff_eq!(origin[0], 1_620_750.250_8); + assert_abs_diff_eq!(origin[1], 14_277_012.715_3); + assert_abs_diff_eq!(origin[2], 0.0); + // assert_eq!(origin, [1_620_750.250_8, 4_277_012.715_3, 0.0]); //todo add a test for matrix either in this PR. } From abdd59c2d82729d0f6dacf1cfd7252657acd0fb3 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 14 Feb 2025 10:54:26 +0800 Subject: [PATCH 13/58] add cusom_tile_grid to tilejson --- martin/src/cog/source.rs | 42 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index d51dec789..1d10287b8 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -46,11 +46,51 @@ impl CogSource { pub fn new(id: String, path: PathBuf) -> FileResult { let tileinfo = TileInfo::new(Format::Png, martin_tile_utils::Encoding::Uncompressed); let meta = get_meta(&path)?; - let tilejson = tilejson! { + let mut tilejson = tilejson! { tiles: vec![], minzoom: meta.min_zoom, maxzoom: meta.max_zoom }; + + let mut cog_info = serde_json::Map::new(); + + cog_info.insert( + "minZoom".to_string(), + serde_json::Value::from(meta.min_zoom), + ); + + cog_info.insert( + "maxZoom".to_string(), + serde_json::Value::from(meta.max_zoom), + ); + + let mut resolutions_map = serde_json::Map::new(); + for (key, value) in &meta.resolutions { + resolutions_map.insert( + key.to_string(), // Convert u8 key to String + serde_json::Value::from(value.to_vec()), // Convert [f64; 3] to Vec and then to serde_json::Value + ); + } + + cog_info.insert( + "resolutions".to_string(), + serde_json::Value::from(resolutions_map), + ); + + cog_info.insert( + "origin".to_string(), + serde_json::Value::from(meta.origin.to_vec()), + ); + + cog_info.insert( + "extent".to_string(), + serde_json::Value::from(meta.extent.to_vec()), + ); + + tilejson + .other + .insert("cog_custom_grid".to_string(), serde_json::json!(cog_info)); + Ok(CogSource { id, path, From 72662e8472c04683cef67c2cb2979646e32ab2b5 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 14 Feb 2025 11:09:10 +0800 Subject: [PATCH 14/58] get the images_ifd sorted --- martin/src/cog/source.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 1d10287b8..7e14b30de 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -607,6 +607,7 @@ fn get_images_ifd(decoder: &mut Decoder, path: &Path) -> Vec { break; } } + res.sort_unstable(); res } From 14be39db10b7300fcd82d03f88faae6774dbd6ea Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 14 Feb 2025 13:39:17 +0800 Subject: [PATCH 15/58] fix snapshot for hashmap --- martin/src/cog/source.rs | 123 +++++++++++++++++++++------------------ 1 file changed, 65 insertions(+), 58 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 7e14b30de..566f7b855 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -394,10 +394,9 @@ fn get_meta(path: &PathBuf) -> Result { let mut zoom_and_ifd: HashMap = HashMap::new(); let mut zoom_and_tile_across_down: HashMap = HashMap::new(); - let mut resolutions = HashMap::new(); + let mut resolutions: HashMap = HashMap::new(); let images_ifd = get_images_ifd(&mut decoder, path); - for (idx, image_ifd) in images_ifd.iter().enumerate() { decoder .seek_to_image(*image_ifd) @@ -406,7 +405,7 @@ fn get_meta(path: &PathBuf) -> Result { let zoom = u8::try_from(images_ifd.len() - (idx + 1)) .map_err(|_| CogError::TooManyImages(path.clone()))?; - let resolution = if zoom == 0 { + let resolution = if *image_ifd == 0 { full_resolution } else { let (image_width, image_length) = decoder.dimensions().map_err(|e| { @@ -516,7 +515,7 @@ fn get_full_resolution( } fn get_model_infos(decoder: &mut Decoder, path: &Path) -> ModelInfo { - let pixel_scale = decoder + let mut pixel_scale = decoder .get_tag_f64_vec(Tag::ModelPixelScaleTag) .map_err(|e| { CogError::TagsNotFound( @@ -527,6 +526,9 @@ fn get_model_infos(decoder: &mut Decoder, path: &Path) -> ModelInfo { ) }) .ok(); + if let Some(pixel) = pixel_scale { + pixel_scale = Some(vec![pixel[0], -pixel[1], pixel[2]]); + } let tie_points = decoder .get_tag_f64_vec(Tag::ModelTiepointTag) .map_err(|e| { @@ -607,7 +609,7 @@ fn get_images_ifd(decoder: &mut Decoder, path: &Path) -> Vec { break; } } - res.sort_unstable(); + // how to get it sorted from big to little number res } @@ -625,8 +627,7 @@ fn get_origin( #[cfg(test)] mod tests { - - use insta::assert_yaml_snapshot; + use insta::{assert_yaml_snapshot, Settings}; use martin_tile_utils::TileCoord; use rstest::rstest; use std::{fs::File, path::PathBuf}; @@ -733,7 +734,7 @@ mod tests { assert_yaml_snapshot!(pixel_scale, @r###" - 10 - - 10 + - -10 - 0 "###); assert_yaml_snapshot!(tie_points, @r###" @@ -767,59 +768,65 @@ mod tests { } #[test] - fn can_get_meta() { + fn can_get_resolutions() { let path = PathBuf::from("../tests/fixtures/cog/rgb_u8.tif"); let meta = super::get_meta(&path).unwrap(); - assert_yaml_snapshot!(meta, @r###" - min_zoom: 0 - max_zoom: 3 - resolutions: - 2: - - 20 - - 20 - - 0 - 1: - - 40 - - 40 - - 0 - 0: - - 10 - - 10 - - 0 - 3: - - 10 - - 10 - - 0 - zoom_and_ifd: - 3: 0 - 1: 2 - 0: 3 - 2: 1 - zoom_and_tile_across_down: - 0: - - 1 - - 1 - 2: - - 1 - - 1 - 1: - - 1 - - 1 - 3: - - 2 - - 2 - nodata: ~ - origin: - - 1620750.2508 - - 4277012.7153 - - 0 - extent: - - 1620750.2508 - - 4277012.7153 - - 1625870.2508 - - 4282132.7153 - "###); + let mut settings = Settings::new(); + settings.set_sort_maps(true); + + // with this settings, the order of hashmap would be fixed to get a stable test + settings.bind(|| { + insta::assert_yaml_snapshot!(meta,@r###" + min_zoom: 0 + max_zoom: 3 + resolutions: + 0: + - 80 + - -80 + - 0 + 1: + - 40 + - -40 + - 0 + 2: + - 20 + - -20 + - 0 + 3: + - 10 + - -10 + - 0 + zoom_and_ifd: + 0: 3 + 1: 2 + 2: 1 + 3: 0 + zoom_and_tile_across_down: + 0: + - 1 + - 1 + 1: + - 1 + - 1 + 2: + - 1 + - 1 + 3: + - 2 + - 2 + nodata: ~ + origin: + - 1620750.2508 + - 4277012.7153 + - 0 + extent: + - 1620750.2508 + - 4271892.7153 + - 1625870.2508 + - 4277012.7153 + "###); + }); } } From 03c15c026c691ffcd685624c661cd3bad89e1581 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 14 Feb 2025 13:50:45 +0800 Subject: [PATCH 16/58] fix test --- martin/src/cog/source.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 566f7b855..27d724038 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -718,7 +718,7 @@ mod tests { ) .unwrap(); assert_abs_diff_eq!(origin[0], 1_620_750.250_8); - assert_abs_diff_eq!(origin[1], 14_277_012.715_3); + assert_abs_diff_eq!(origin[1], 4_277_012.715_3); assert_abs_diff_eq!(origin[2], 0.0); // assert_eq!(origin, [1_620_750.250_8, 4_277_012.715_3, 0.0]); //todo add a test for matrix either in this PR. From 638efe02ba6e25b55d74f9732bc5bf4a1208d9d1 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 14 Feb 2025 13:53:54 +0800 Subject: [PATCH 17/58] bless --- tests/expected/auto/rgb_u8.json | 37 +++++++++++++++++++++++++ tests/expected/auto/rgba_u8.json | 37 +++++++++++++++++++++++++ tests/expected/auto/rgba_u8_nodata.json | 32 +++++++++++++++++++++ 3 files changed, 106 insertions(+) diff --git a/tests/expected/auto/rgb_u8.json b/tests/expected/auto/rgb_u8.json index a159d60c5..af8f7c4f9 100644 --- a/tests/expected/auto/rgb_u8.json +++ b/tests/expected/auto/rgb_u8.json @@ -1,4 +1,41 @@ { + "cog_custom_grid": { + "extent": [ + 1620750.2508, + 4271892.7153, + 1625870.2508, + 4277012.7153 + ], + "maxZoom": 3, + "minZoom": 0, + "origin": [ + 1620750.2508, + 4277012.7153, + 0 + ], + "resolutions": { + "0": [ + 80, + -80, + 0 + ], + "1": [ + 40, + -40, + 0 + ], + "2": [ + 20, + -20, + 0 + ], + "3": [ + 10, + -10, + 0 + ] + } + }, "maxzoom": 3, "minzoom": 0, "tilejson": "3.0.0", diff --git a/tests/expected/auto/rgba_u8.json b/tests/expected/auto/rgba_u8.json index 75815ef3e..ed249bf09 100644 --- a/tests/expected/auto/rgba_u8.json +++ b/tests/expected/auto/rgba_u8.json @@ -1,4 +1,41 @@ { + "cog_custom_grid": { + "extent": [ + 1620750.2508, + 4271892.7153, + 1625870.2508, + 4277012.7153 + ], + "maxZoom": 3, + "minZoom": 0, + "origin": [ + 1620750.2508, + 4277012.7153, + 0 + ], + "resolutions": { + "0": [ + 80, + -80, + 0 + ], + "1": [ + 40, + -40, + 0 + ], + "2": [ + 20, + -20, + 0 + ], + "3": [ + 10, + -10, + 0 + ] + } + }, "maxzoom": 3, "minzoom": 0, "tilejson": "3.0.0", diff --git a/tests/expected/auto/rgba_u8_nodata.json b/tests/expected/auto/rgba_u8_nodata.json index 47d2f9bc1..127a359eb 100644 --- a/tests/expected/auto/rgba_u8_nodata.json +++ b/tests/expected/auto/rgba_u8_nodata.json @@ -1,4 +1,36 @@ { + "cog_custom_grid": { + "extent": [ + 1620750.2508, + 4271892.7153, + 1625870.2508, + 4277012.7153 + ], + "maxZoom": 2, + "minZoom": 0, + "origin": [ + 1620750.2508, + 4277012.7153, + 0 + ], + "resolutions": { + "0": [ + 40, + -40, + 0 + ], + "1": [ + 20, + -20, + 0 + ], + "2": [ + 10, + -10, + 0 + ] + } + }, "maxzoom": 2, "minzoom": 0, "tilejson": "3.0.0", From f1c7449b4f01ca2c0aed2dbd35a7be2d35c40b65 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 14 Feb 2025 14:06:17 +0800 Subject: [PATCH 18/58] bless --- martin/src/cog/source.rs | 2 +- tests/expected/auto/rgb_u8.json | 24 ++++-------------------- tests/expected/auto/rgba_u8.json | 24 ++++-------------------- tests/expected/auto/rgba_u8_nodata.json | 18 +++--------------- 4 files changed, 12 insertions(+), 56 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 27d724038..bb6952d8d 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -68,7 +68,7 @@ impl CogSource { for (key, value) in &meta.resolutions { resolutions_map.insert( key.to_string(), // Convert u8 key to String - serde_json::Value::from(value.to_vec()), // Convert [f64; 3] to Vec and then to serde_json::Value + serde_json::Value::from(value.to_vec()[0]), // Convert [f64; 3] to Vec and then to serde_json::Value ); } diff --git a/tests/expected/auto/rgb_u8.json b/tests/expected/auto/rgb_u8.json index af8f7c4f9..28495a68c 100644 --- a/tests/expected/auto/rgb_u8.json +++ b/tests/expected/auto/rgb_u8.json @@ -14,26 +14,10 @@ 0 ], "resolutions": { - "0": [ - 80, - -80, - 0 - ], - "1": [ - 40, - -40, - 0 - ], - "2": [ - 20, - -20, - 0 - ], - "3": [ - 10, - -10, - 0 - ] + "0": 80, + "1": 40, + "2": 20, + "3": 10 } }, "maxzoom": 3, diff --git a/tests/expected/auto/rgba_u8.json b/tests/expected/auto/rgba_u8.json index ed249bf09..4fa1a4664 100644 --- a/tests/expected/auto/rgba_u8.json +++ b/tests/expected/auto/rgba_u8.json @@ -14,26 +14,10 @@ 0 ], "resolutions": { - "0": [ - 80, - -80, - 0 - ], - "1": [ - 40, - -40, - 0 - ], - "2": [ - 20, - -20, - 0 - ], - "3": [ - 10, - -10, - 0 - ] + "0": 80, + "1": 40, + "2": 20, + "3": 10 } }, "maxzoom": 3, diff --git a/tests/expected/auto/rgba_u8_nodata.json b/tests/expected/auto/rgba_u8_nodata.json index 127a359eb..49d1d85f6 100644 --- a/tests/expected/auto/rgba_u8_nodata.json +++ b/tests/expected/auto/rgba_u8_nodata.json @@ -14,21 +14,9 @@ 0 ], "resolutions": { - "0": [ - 40, - -40, - 0 - ], - "1": [ - 20, - -20, - 0 - ], - "2": [ - 10, - -10, - 0 - ] + "0": 40, + "1": 20, + "2": 10 } }, "maxzoom": 2, From 6490c046ed63f09ea8a1b5665fe8fd39b20aa66b Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 14 Feb 2025 14:06:32 +0800 Subject: [PATCH 19/58] fmt --- martin/src/cog/source.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index bb6952d8d..756adf22d 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -67,7 +67,7 @@ impl CogSource { let mut resolutions_map = serde_json::Map::new(); for (key, value) in &meta.resolutions { resolutions_map.insert( - key.to_string(), // Convert u8 key to String + key.to_string(), // Convert u8 key to String serde_json::Value::from(value.to_vec()[0]), // Convert [f64; 3] to Vec and then to serde_json::Value ); } From 8b7f54986be20d3fa9197c71f21a223d17054db1 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 14 Feb 2025 14:10:56 +0800 Subject: [PATCH 20/58] add meta_to_tilejson --- martin/src/cog/source.rs | 92 +++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 756adf22d..850f2af3f 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -46,50 +46,8 @@ impl CogSource { pub fn new(id: String, path: PathBuf) -> FileResult { let tileinfo = TileInfo::new(Format::Png, martin_tile_utils::Encoding::Uncompressed); let meta = get_meta(&path)?; - let mut tilejson = tilejson! { - tiles: vec![], - minzoom: meta.min_zoom, - maxzoom: meta.max_zoom - }; - - let mut cog_info = serde_json::Map::new(); - cog_info.insert( - "minZoom".to_string(), - serde_json::Value::from(meta.min_zoom), - ); - - cog_info.insert( - "maxZoom".to_string(), - serde_json::Value::from(meta.max_zoom), - ); - - let mut resolutions_map = serde_json::Map::new(); - for (key, value) in &meta.resolutions { - resolutions_map.insert( - key.to_string(), // Convert u8 key to String - serde_json::Value::from(value.to_vec()[0]), // Convert [f64; 3] to Vec and then to serde_json::Value - ); - } - - cog_info.insert( - "resolutions".to_string(), - serde_json::Value::from(resolutions_map), - ); - - cog_info.insert( - "origin".to_string(), - serde_json::Value::from(meta.origin.to_vec()), - ); - - cog_info.insert( - "extent".to_string(), - serde_json::Value::from(meta.extent.to_vec()), - ); - - tilejson - .other - .insert("cog_custom_grid".to_string(), serde_json::json!(cog_info)); + let tilejson: TileJSON = meta_to_tilejson(&meta); Ok(CogSource { id, @@ -181,6 +139,54 @@ impl CogSource { } } +fn meta_to_tilejson(meta: &Meta) -> TileJSON { + let mut tilejson = tilejson! { + tiles: vec![], + minzoom: meta.min_zoom, + maxzoom: meta.max_zoom + }; + + let mut cog_info = serde_json::Map::new(); + + cog_info.insert( + "minZoom".to_string(), + serde_json::Value::from(meta.min_zoom), + ); + + cog_info.insert( + "maxZoom".to_string(), + serde_json::Value::from(meta.max_zoom), + ); + + let mut resolutions_map = serde_json::Map::new(); + for (key, value) in &meta.resolutions { + resolutions_map.insert( + key.to_string(), // Convert u8 key to String + serde_json::Value::from(value.to_vec()[0]), // Convert [f64; 3] to Vec and then to serde_json::Value + ); + } + + cog_info.insert( + "resolutions".to_string(), + serde_json::Value::from(resolutions_map), + ); + + cog_info.insert( + "origin".to_string(), + serde_json::Value::from(meta.origin.to_vec()), + ); + + cog_info.insert( + "extent".to_string(), + serde_json::Value::from(meta.extent.to_vec()), + ); + + tilejson + .other + .insert("cog_custom_grid".to_string(), serde_json::json!(cog_info)); + tilejson +} + #[async_trait] impl Source for CogSource { fn get_id(&self) -> &str { From 14eb8807447fc8f102b92250d7fca42f33043133 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 14 Feb 2025 14:16:57 +0800 Subject: [PATCH 21/58] verify whether it's pixel squared --- martin/src/cog/errors.rs | 3 +++ martin/src/cog/source.rs | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/martin/src/cog/errors.rs b/martin/src/cog/errors.rs index 1a62d7205..89121b6ed 100644 --- a/martin/src/cog/errors.rs +++ b/martin/src/cog/errors.rs @@ -54,4 +54,7 @@ pub enum CogError { #[error("Get extent failed from file: {0}")] GetExtentFailed(PathBuf), + + #[error("The pixel size of the image {0} is not squared")] + NonSquaredImage(PathBuf), } diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 850f2af3f..d1b6f3855 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -336,7 +336,10 @@ fn verify_requirments( (Some(pixel_scale), Some(tie_points), _) => { - if pixel_scale.len() != 3 || tie_points.len() % 6 != 0 { + if (pixel_scale[0] - pixel_scale[1]) > 0.001{ + Err(CogError::NonSquaredImage(path.to_path_buf())) + } + else if pixel_scale.len() != 3 || tie_points.len() % 6 != 0 { Err(CogError::InvalidGeoInformation(path.to_path_buf(), "The length of pixel scale should be 3, and the length of tie points should be a multiple of 6".to_string())) }else{ Ok(()) From d80bd6e489beb2c897fcda19e0c7199635aa2fd2 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 14 Feb 2025 14:46:15 +0800 Subject: [PATCH 22/58] cleanup --- martin/src/cog/errors.rs | 2 +- martin/src/cog/source.rs | 22 ++++++++++++---------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/martin/src/cog/errors.rs b/martin/src/cog/errors.rs index 89121b6ed..752f8282d 100644 --- a/martin/src/cog/errors.rs +++ b/martin/src/cog/errors.rs @@ -54,7 +54,7 @@ pub enum CogError { #[error("Get extent failed from file: {0}")] GetExtentFailed(PathBuf), - + #[error("The pixel size of the image {0} is not squared")] NonSquaredImage(PathBuf), } diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index d1b6f3855..e00959e97 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -19,19 +19,21 @@ use crate::{file_config::FileResult, MartinResult, Source, TileData, UrlQuery}; use super::CogError; +// about the model space of tiff image. +// pixel scale, tie points and transformations type ModelInfo = (Option>, Option>, Option>); #[derive(Clone, Debug, Serialize)] struct Meta { min_zoom: u8, max_zoom: u8, - resolutions: HashMap, + origin: [f64; 3], + extent: [f64; 4], + zoom_and_resolutions: HashMap, zoom_and_ifd: HashMap, zoom_and_tile_across_down: HashMap, nodata: Option, - origin: [f64; 3], - extent: [f64; 4], -} + } #[derive(Clone, Debug)] pub struct CogSource { @@ -159,10 +161,10 @@ fn meta_to_tilejson(meta: &Meta) -> TileJSON { ); let mut resolutions_map = serde_json::Map::new(); - for (key, value) in &meta.resolutions { + for (key, value) in &meta.zoom_and_resolutions { resolutions_map.insert( key.to_string(), // Convert u8 key to String - serde_json::Value::from(value.to_vec()[0]), // Convert [f64; 3] to Vec and then to serde_json::Value + serde_json::Value::from(value.to_vec()[0]), ); } @@ -183,7 +185,7 @@ fn meta_to_tilejson(meta: &Meta) -> TileJSON { tilejson .other - .insert("cog_custom_grid".to_string(), serde_json::json!(cog_info)); + .insert("custom_grid".to_string(), serde_json::json!(cog_info)); tilejson } @@ -336,7 +338,7 @@ fn verify_requirments( (Some(pixel_scale), Some(tie_points), _) => { - if (pixel_scale[0] - pixel_scale[1]) > 0.001{ + if (pixel_scale[0] - pixel_scale[1]) > 0.01{ Err(CogError::NonSquaredImage(path.to_path_buf())) } else if pixel_scale.len() != 3 || tie_points.len() % 6 != 0 { @@ -445,7 +447,7 @@ fn get_meta(path: &PathBuf) -> Result { Ok(Meta { min_zoom: 0, max_zoom: images_ifd.len() as u8 - 1, - resolutions, + zoom_and_resolutions: resolutions, extent, origin, zoom_and_ifd, @@ -777,7 +779,7 @@ mod tests { } #[test] - fn can_get_resolutions() { + fn can_get_meta() { let path = PathBuf::from("../tests/fixtures/cog/rgb_u8.tif"); let meta = super::get_meta(&path).unwrap(); From c8a2cc7a445aac2ce4e1a05b4b45f8091c1ddc2a Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 14 Feb 2025 15:08:14 +0800 Subject: [PATCH 23/58] bless --- Cargo.lock | 327 +++++++++++------------- martin/src/cog/errors.rs | 4 +- martin/src/cog/source.rs | 24 +- tests/expected/auto/rgb_u8.json | 2 +- tests/expected/auto/rgba_u8.json | 2 +- tests/expected/auto/rgba_u8_nodata.json | 2 +- 6 files changed, 162 insertions(+), 199 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9196e3181..872f63cb2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,7 +80,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -198,7 +198,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -421,7 +421,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -432,7 +432,7 @@ checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -463,9 +463,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.25.0" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71b2ddd3ada61a305e1d8bb6c005d1eaa7d14d903681edfc400406d523a9b491" +checksum = "54ac4f13dad353b209b34cbec082338202cbc01c8f00336b55c750c13ac91f8f" dependencies = [ "bindgen", "cc", @@ -533,7 +533,7 @@ dependencies = [ "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.96", + "syn 2.0.98", "which", ] @@ -590,9 +590,9 @@ dependencies = [ [[package]] name = "bollard" -version = "0.17.1" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d41711ad46fda47cd701f6908e59d1bd6b9a2b7464c0d0aeab95c6d37096ff8a" +checksum = "97ccca1260af6a459d75994ad5acc1651bcabcbdbc41467cc9786519ab854c30" dependencies = [ "base64 0.22.1", "bollard-stubs", @@ -603,7 +603,7 @@ dependencies = [ "home", "http 1.2.0", "http-body-util", - "hyper 1.5.2", + "hyper 1.6.0", "hyper-named-pipe", "hyper-rustls", "hyper-util", @@ -611,7 +611,7 @@ dependencies = [ "log", "pin-project-lite", "rustls", - "rustls-native-certs 0.7.3", + "rustls-native-certs", "rustls-pemfile", "rustls-pki-types", "serde", @@ -619,7 +619,7 @@ dependencies = [ "serde_json", "serde_repr", "serde_urlencoded", - "thiserror 1.0.69", + "thiserror 2.0.11", "tokio", "tokio-util", "tower-service", @@ -629,9 +629,9 @@ dependencies = [ [[package]] name = "bollard-stubs" -version = "1.45.0-rc.26.0.1" +version = "1.47.1-rc.27.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7c5415e3a6bc6d3e99eff6268e488fd4ee25e7b28c10f08fa6760bd9de16e4" +checksum = "3f179cfbddb6e77a5472703d4b30436bff32929c0aa8a9008ecf23d1d3cdd0da" dependencies = [ "serde", "serde_repr", @@ -699,9 +699,9 @@ checksum = "f7f2e6c4f2a017f63b5a1fd7cc437f061b53a3e890bcca840ef756d72f6b72f2" [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytemuck" @@ -717,9 +717,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" [[package]] name = "bytestring" @@ -744,9 +744,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.10" +version = "1.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda" dependencies = [ "jobserver", "libc", @@ -837,9 +837,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.28" +version = "4.5.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff" +checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184" dependencies = [ "clap_builder", "clap_derive", @@ -847,9 +847,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.27" +version = "4.5.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" +checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9" dependencies = [ "anstream", "anstyle", @@ -866,7 +866,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -877,9 +877,9 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "cmake" -version = "0.1.52" +version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" dependencies = [ "cc", ] @@ -949,16 +949,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation" version = "0.10.0" @@ -1127,7 +1117,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1151,7 +1141,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1162,7 +1152,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1173,9 +1163,9 @@ checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a" [[package]] name = "deadpool" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6541a3916932fe57768d4be0b1ffb5ec7cbf74ca8c903fdfd5c0fe8aa958f0ed" +checksum = "5ed5957ff93768adf7a65ab167a17835c3d2c3c50d084fe305174c112f468e2f" dependencies = [ "deadpool-runtime", "num_cpus", @@ -1235,7 +1225,7 @@ checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1250,15 +1240,15 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.18" +version = "0.99.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" dependencies = [ "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1287,7 +1277,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1366,7 +1356,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1409,7 +1399,7 @@ checksum = "3bf679796c0322556351f287a51b49e48f7c4986e727b5dd78c972d30e2e16cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -1725,7 +1715,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2027,9 +2017,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" [[package]] name = "httpdate" @@ -2068,9 +2058,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", @@ -2093,7 +2083,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b7d8abf35697b81a825e386fc151e0d503e8cb5fcb93cc8669c376dfd6f278" dependencies = [ "hex", - "hyper 1.5.2", + "hyper 1.6.0", "hyper-util", "pin-project-lite", "tokio", @@ -2109,10 +2099,10 @@ checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", "http 1.2.0", - "hyper 1.5.2", + "hyper 1.6.0", "hyper-util", "rustls", - "rustls-native-certs 0.8.1", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls", @@ -2130,7 +2120,7 @@ dependencies = [ "futures-util", "http 1.2.0", "http-body 1.0.1", - "hyper 1.5.2", + "hyper 1.6.0", "pin-project-lite", "socket2", "tokio", @@ -2146,7 +2136,7 @@ checksum = "986c5ce3b994526b3cd75578e62554abd09f0899d6206de48b3e96ab34ccc8c7" dependencies = [ "hex", "http-body-util", - "hyper 1.5.2", + "hyper 1.6.0", "hyper-util", "pin-project-lite", "tokio", @@ -2291,7 +2281,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -2751,7 +2741,7 @@ dependencies = [ "regex", "rstest", "rustls", - "rustls-native-certs 0.8.1", + "rustls-native-certs", "rustls-pemfile", "semver", "serde", @@ -2896,9 +2886,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +checksum = "b3b1c9bd4fe1f0f8b387f6eb9eb3b4a1aa26185e5750efb9140301703f62cd1b" dependencies = [ "adler2", "simd-adler32", @@ -3105,9 +3095,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "oorandom" @@ -3141,7 +3131,7 @@ dependencies = [ "log", "rayon", "rgb", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "zopfli", ] @@ -3208,7 +3198,7 @@ dependencies = [ "regex", "regex-syntax 0.7.5", "structmeta 0.2.0", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -3222,7 +3212,7 @@ dependencies = [ "regex", "regex-syntax 0.8.5", "structmeta 0.3.0", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -3303,22 +3293,22 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -3439,9 +3429,9 @@ dependencies = [ [[package]] name = "postgres" -version = "0.19.9" +version = "0.19.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95c918733159f4d55d2ceb262950f00b0aebd6af4aa97b5a47bb0655120475ed" +checksum = "363e6dfbdd780d3aa3597b6eb430db76bb315fa9bad7fae595bb8def808b8470" dependencies = [ "bytes", "fallible-iterator 0.2.0", @@ -3471,9 +3461,9 @@ dependencies = [ [[package]] name = "postgres-types" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66ea23a2d0e5734297357705193335e0a957696f34bed2f2faefacb2fec336f" +checksum = "613283563cd90e1dfc3518d548caee47e0e725455ed619881f5cf21f36de4b48" dependencies = [ "bytes", "fallible-iterator 0.2.0", @@ -3539,7 +3529,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac" dependencies = [ "proc-macro2", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -3687,7 +3677,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "rustls", "socket2", "thiserror 2.0.11", @@ -3705,7 +3695,7 @@ dependencies = [ "getrandom 0.2.15", "rand 0.8.5", "ring", - "rustc-hash 2.1.0", + "rustc-hash 2.1.1", "rustls", "rustls-pki-types", "slab", @@ -3763,7 +3753,7 @@ checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.0", - "zerocopy 0.8.14", + "zerocopy 0.8.17", ] [[package]] @@ -3802,7 +3792,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" dependencies = [ "getrandom 0.3.1", - "zerocopy 0.8.14", + "zerocopy 0.8.17", ] [[package]] @@ -3924,7 +3914,7 @@ dependencies = [ "http 1.2.0", "http-body 1.0.1", "http-body-util", - "hyper 1.5.2", + "hyper 1.6.0", "hyper-rustls", "hyper-util", "ipnet", @@ -3936,7 +3926,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls", - "rustls-native-certs 0.8.1", + "rustls-native-certs", "rustls-pemfile", "rustls-pki-types", "serde", @@ -3982,15 +3972,14 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.8" +version = "0.17.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24" dependencies = [ "cc", "cfg-if", "getrandom 0.2.15", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] @@ -4056,7 +4045,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.96", + "syn 2.0.98", "unicode-ident", ] @@ -4088,9 +4077,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustc-hash" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustc_version" @@ -4116,9 +4105,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.22" +version = "0.23.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9263ab4eb695e42321db096e3b8fbd715a59b154d5c88d82db2175b681ba7" +checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" dependencies = [ "aws-lc-rs", "log", @@ -4130,19 +4119,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" -dependencies = [ - "openssl-probe", - "rustls-pemfile", - "rustls-pki-types", - "schannel", - "security-framework 2.11.1", -] - [[package]] name = "rustls-native-certs" version = "0.8.1" @@ -4152,7 +4128,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.2.0", + "security-framework", ] [[package]] @@ -4166,9 +4142,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" dependencies = [ "web-time", ] @@ -4209,9 +4185,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" [[package]] name = "same-file" @@ -4253,19 +4229,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.8.0", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - [[package]] name = "security-framework" version = "3.2.0" @@ -4273,7 +4236,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" dependencies = [ "bitflags 2.8.0", - "core-foundation 0.10.0", + "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", @@ -4312,7 +4275,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -4335,7 +4298,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -4398,7 +4361,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -4606,9 +4569,9 @@ dependencies = [ [[package]] name = "sqlite-hashes" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03cb5a95dca257c912e3fa49258150ed95facccb20edbff2cf6e1a74335beff" +checksum = "96bb62526112c007e85b8c80ccec304ea847f60455d5aab62130d837cb44114d" dependencies = [ "digest", "hex", @@ -4867,7 +4830,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive 0.2.0", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -4879,7 +4842,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive 0.3.0", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -4890,7 +4853,7 @@ checksum = "a60bcaff7397072dca0017d1db428e30d5002e00b6847703e2e42005c95fbe00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -4901,7 +4864,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -4934,9 +4897,9 @@ dependencies = [ [[package]] name = "symbolic-common" -version = "12.13.3" +version = "12.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13a4dfe4bbeef59c1f32fc7524ae7c95b9e1de5e79a43ce1604e181081d71b0c" +checksum = "b6189977df1d6ec30c920647919d76f29fb8d8f25e8952e835b0fcda25e8f792" dependencies = [ "debugid", "memmap2 0.9.5", @@ -4946,9 +4909,9 @@ dependencies = [ [[package]] name = "symbolic-demangle" -version = "12.13.3" +version = "12.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cf6a95abff97de4d7ff3473f33cacd38f1ddccad5c1feab435d6760300e3b6" +checksum = "d234917f7986498e7f62061438cee724bafb483fe84cfbe2486f68dce48240d7" dependencies = [ "cpp_demangle", "rustc-demangle", @@ -4968,9 +4931,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.96" +version = "2.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" dependencies = [ "proc-macro2", "quote", @@ -4994,7 +4957,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -5011,13 +4974,13 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.15.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91" dependencies = [ "cfg-if", "fastrand", - "getrandom 0.2.15", + "getrandom 0.3.1", "once_cell", "rustix", "windows-sys 0.52.0", @@ -5025,9 +4988,9 @@ dependencies = [ [[package]] name = "testcontainers" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f40cc2bd72e17f328faf8ca7687fe337e61bccd8acf9674fa78dd3792b045e1" +checksum = "042009c52a4204476bff461ca8ef17bab6f1a91628504a8a36c6fd2c1cde2d5e" dependencies = [ "async-trait", "bollard", @@ -5044,7 +5007,7 @@ dependencies = [ "serde", "serde_json", "serde_with", - "thiserror 1.0.69", + "thiserror 2.0.11", "tokio", "tokio-stream", "tokio-tar", @@ -5087,7 +5050,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -5098,7 +5061,7 @@ checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -5228,9 +5191,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tls_codec" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e78c9c330f8c85b2bae7c8368f2739157db9991235123aa1b15ef9502bfb6a" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" dependencies = [ "tls_codec_derive", "zeroize", @@ -5238,13 +5201,13 @@ dependencies = [ [[package]] name = "tls_codec_derive" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9ef545650e79f30233c0003bcc2504d7efac6dad25fca40744de773fe2049c" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -5273,14 +5236,14 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "tokio-postgres" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b5d3742945bc7d7f210693b0c58ae542c6fd47b17adbbda0885f3dcb34a6bdb" +checksum = "6c95d533c83082bb6490e0189acaa0bbeef9084e60471b696ca6988cd0541fb0" dependencies = [ "async-trait", "byteorder", @@ -5295,7 +5258,7 @@ dependencies = [ "pin-project-lite", "postgres-protocol", "postgres-types", - "rand 0.8.5", + "rand 0.9.0", "socket2", "tokio", "tokio-util", @@ -5383,9 +5346,9 @@ checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap 2.7.1", "toml_datetime", @@ -5454,7 +5417,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -5540,9 +5503,9 @@ checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1" [[package]] name = "unicode-ident" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243" +checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" [[package]] name = "unicode-normalization" @@ -5700,11 +5663,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.12.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" +checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0" dependencies = [ - "getrandom 0.2.15", + "getrandom 0.3.1", ] [[package]] @@ -5793,7 +5756,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "wasm-bindgen-shared", ] @@ -5828,7 +5791,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5962,7 +5925,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -5973,7 +5936,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -6156,9 +6119,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.24" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" +checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" dependencies = [ "memchr", ] @@ -6260,7 +6223,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "synstructure", ] @@ -6276,11 +6239,11 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.14" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" +checksum = "aa91407dacce3a68c56de03abe2760159582b846c6a4acd2f456618087f12713" dependencies = [ - "zerocopy-derive 0.8.14", + "zerocopy-derive 0.8.17", ] [[package]] @@ -6291,18 +6254,18 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] name = "zerocopy-derive" -version = "0.8.14" +version = "0.8.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" +checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -6322,7 +6285,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", "synstructure", ] @@ -6343,7 +6306,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] @@ -6365,7 +6328,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.98", ] [[package]] diff --git a/martin/src/cog/errors.rs b/martin/src/cog/errors.rs index 752f8282d..de137f6b9 100644 --- a/martin/src/cog/errors.rs +++ b/martin/src/cog/errors.rs @@ -55,6 +55,6 @@ pub enum CogError { #[error("Get extent failed from file: {0}")] GetExtentFailed(PathBuf), - #[error("The pixel size of the image {0} is not squared")] - NonSquaredImage(PathBuf), + #[error("The pixel size of the image {0} is not squared, the x_scale is {1}, the y_scale is {2}")] + NonSquaredImage(PathBuf, f64, f64), } diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index e00959e97..68362628e 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -338,8 +338,8 @@ fn verify_requirments( (Some(pixel_scale), Some(tie_points), _) => { - if (pixel_scale[0] - pixel_scale[1]) > 0.01{ - Err(CogError::NonSquaredImage(path.to_path_buf())) + if (pixel_scale[0] + pixel_scale[1]) > 0.01{ + Err(CogError::NonSquaredImage(path.to_path_buf(), pixel_scale[0], pixel_scale[1])) } else if pixel_scale.len() != 3 || tie_points.len() % 6 != 0 { Err(CogError::InvalidGeoInformation(path.to_path_buf(), "The length of pixel scale should be 3, and the length of tie points should be a multiple of 6".to_string())) @@ -792,7 +792,16 @@ mod tests { insta::assert_yaml_snapshot!(meta,@r###" min_zoom: 0 max_zoom: 3 - resolutions: + origin: + - 1620750.2508 + - 4277012.7153 + - 0 + extent: + - 1620750.2508 + - 4271892.7153 + - 1625870.2508 + - 4277012.7153 + zoom_and_resolutions: 0: - 80 - -80 @@ -828,15 +837,6 @@ mod tests { - 2 - 2 nodata: ~ - origin: - - 1620750.2508 - - 4277012.7153 - - 0 - extent: - - 1620750.2508 - - 4271892.7153 - - 1625870.2508 - - 4277012.7153 "###); }); } diff --git a/tests/expected/auto/rgb_u8.json b/tests/expected/auto/rgb_u8.json index 28495a68c..b3efc92b4 100644 --- a/tests/expected/auto/rgb_u8.json +++ b/tests/expected/auto/rgb_u8.json @@ -1,5 +1,5 @@ { - "cog_custom_grid": { + "custom_grid": { "extent": [ 1620750.2508, 4271892.7153, diff --git a/tests/expected/auto/rgba_u8.json b/tests/expected/auto/rgba_u8.json index 4fa1a4664..6cd28e33f 100644 --- a/tests/expected/auto/rgba_u8.json +++ b/tests/expected/auto/rgba_u8.json @@ -1,5 +1,5 @@ { - "cog_custom_grid": { + "custom_grid": { "extent": [ 1620750.2508, 4271892.7153, diff --git a/tests/expected/auto/rgba_u8_nodata.json b/tests/expected/auto/rgba_u8_nodata.json index 49d1d85f6..2513259f3 100644 --- a/tests/expected/auto/rgba_u8_nodata.json +++ b/tests/expected/auto/rgba_u8_nodata.json @@ -1,5 +1,5 @@ { - "cog_custom_grid": { + "custom_grid": { "extent": [ 1620750.2508, 4271892.7153, From 4ea348ffd79a840f407ad3126d287a7434bf0e3e Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 14 Feb 2025 15:08:27 +0800 Subject: [PATCH 24/58] fmt --- martin/src/cog/errors.rs | 4 +++- martin/src/cog/source.rs | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/martin/src/cog/errors.rs b/martin/src/cog/errors.rs index de137f6b9..2c919bc33 100644 --- a/martin/src/cog/errors.rs +++ b/martin/src/cog/errors.rs @@ -55,6 +55,8 @@ pub enum CogError { #[error("Get extent failed from file: {0}")] GetExtentFailed(PathBuf), - #[error("The pixel size of the image {0} is not squared, the x_scale is {1}, the y_scale is {2}")] + #[error( + "The pixel size of the image {0} is not squared, the x_scale is {1}, the y_scale is {2}" + )] NonSquaredImage(PathBuf, f64, f64), } diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 68362628e..b902bdc85 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -19,7 +19,7 @@ use crate::{file_config::FileResult, MartinResult, Source, TileData, UrlQuery}; use super::CogError; -// about the model space of tiff image. +// about the model space of tiff image. // pixel scale, tie points and transformations type ModelInfo = (Option>, Option>, Option>); @@ -33,7 +33,7 @@ struct Meta { zoom_and_ifd: HashMap, zoom_and_tile_across_down: HashMap, nodata: Option, - } +} #[derive(Clone, Debug)] pub struct CogSource { @@ -163,7 +163,7 @@ fn meta_to_tilejson(meta: &Meta) -> TileJSON { let mut resolutions_map = serde_json::Map::new(); for (key, value) in &meta.zoom_and_resolutions { resolutions_map.insert( - key.to_string(), // Convert u8 key to String + key.to_string(), // Convert u8 key to String serde_json::Value::from(value.to_vec()[0]), ); } From ad9ae50696a488e4dbee14b08872e14fa78941a4 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Sun, 16 Feb 2025 14:33:21 +0800 Subject: [PATCH 25/58] Add tileSize to TileJSON --- martin/src/cog/source.rs | 11 +++++++++++ tests/expected/auto/rgb_u8.json | 6 +++++- tests/expected/auto/rgba_u8.json | 6 +++++- tests/expected/auto/rgba_u8_nodata.json | 6 +++++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index b902bdc85..d88086a0e 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -33,6 +33,7 @@ struct Meta { zoom_and_ifd: HashMap, zoom_and_tile_across_down: HashMap, nodata: Option, + tile_size: (u32, u32), } #[derive(Clone, Debug)] @@ -168,6 +169,11 @@ fn meta_to_tilejson(meta: &Meta) -> TileJSON { ); } + cog_info.insert( + "tileSize".to_string(), + serde_json::Value::from([meta.tile_size.0, meta.tile_size.1]), + ); + cog_info.insert( "resolutions".to_string(), serde_json::Value::from(resolutions_map), @@ -370,6 +376,7 @@ fn get_meta(path: &PathBuf) -> Result { let model_info = get_model_infos(&mut decoder, path); verify_requirments(&mut decoder, &model_info, path)?; + let tile_size = decoder.chunk_dimensions(); let nodata: Option = if let Ok(no_data) = decoder.get_tag_ascii_string(GdalNodata) { no_data.parse().ok() } else { @@ -448,6 +455,7 @@ fn get_meta(path: &PathBuf) -> Result { min_zoom: 0, max_zoom: images_ifd.len() as u8 - 1, zoom_and_resolutions: resolutions, + tile_size, extent, origin, zoom_and_ifd, @@ -837,6 +845,9 @@ mod tests { - 2 - 2 nodata: ~ + tile_size: + - 256 + - 256 "###); }); } diff --git a/tests/expected/auto/rgb_u8.json b/tests/expected/auto/rgb_u8.json index b3efc92b4..1a76e725c 100644 --- a/tests/expected/auto/rgb_u8.json +++ b/tests/expected/auto/rgb_u8.json @@ -18,7 +18,11 @@ "1": 40, "2": 20, "3": 10 - } + }, + "tileSize": [ + 256, + 256 + ] }, "maxzoom": 3, "minzoom": 0, diff --git a/tests/expected/auto/rgba_u8.json b/tests/expected/auto/rgba_u8.json index 6cd28e33f..ea7b14606 100644 --- a/tests/expected/auto/rgba_u8.json +++ b/tests/expected/auto/rgba_u8.json @@ -18,7 +18,11 @@ "1": 40, "2": 20, "3": 10 - } + }, + "tileSize": [ + 256, + 256 + ] }, "maxzoom": 3, "minzoom": 0, diff --git a/tests/expected/auto/rgba_u8_nodata.json b/tests/expected/auto/rgba_u8_nodata.json index 2513259f3..3671d02db 100644 --- a/tests/expected/auto/rgba_u8_nodata.json +++ b/tests/expected/auto/rgba_u8_nodata.json @@ -17,7 +17,11 @@ "0": 40, "1": 20, "2": 10 - } + }, + "tileSize": [ + 512, + 512 + ] }, "maxzoom": 2, "minzoom": 0, From 90b8b5e727e1b07b2a0a3c621da18ed0aee26297 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Mon, 17 Feb 2025 10:53:52 +0800 Subject: [PATCH 26/58] update test for get_origin --- martin/src/cog/source.rs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index d88086a0e..17427a11c 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -632,6 +632,7 @@ fn get_images_ifd(decoder: &mut Decoder, path: &Path) -> Vec { res } +// see https://docs.ogc.org/is/19-008r4/19-008r4.html#_geotiff_tags_for_coordinate_transformations fn get_origin( tie_points: Option<&[f64]>, transformation: Option<&[f64]>, @@ -739,8 +740,22 @@ mod tests { assert_abs_diff_eq!(origin[0], 1_620_750.250_8); assert_abs_diff_eq!(origin[1], 4_277_012.715_3); assert_abs_diff_eq!(origin[2], 0.0); - // assert_eq!(origin, [1_620_750.250_8, 4_277_012.715_3, 0.0]); - //todo add a test for matrix either in this PR. + + let matrix = Some(vec![ + 0.0, 100.0, 0.0, 400000.0, 100.0, 0.0, 0.0, 500000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 1.0, + ]); + let tie_point: Option> = None; + + let origin = super::get_origin( + tie_point.as_deref(), + matrix.as_deref(), + &PathBuf::from("not_exist.tif"), + ) + .unwrap(); + assert_abs_diff_eq!(origin[0], 400000.0); + assert_abs_diff_eq!(origin[1], 500000.0); + assert_abs_diff_eq!(origin[2], 0.0); } #[test] From 71acb2e1e2a4714bd031eb54ed5f60d4720b381d Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Mon, 17 Feb 2025 11:17:52 +0800 Subject: [PATCH 27/58] update test of car_get_origin --- martin/src/cog/source.rs | 41 +++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 17427a11c..bcb5048b1 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -726,36 +726,33 @@ mod tests { assert_eq!(png_bytes, expected); } - #[test] - fn can_get_origin() { - let matrix: Option<&[f64]> = None; - let tie_point = Some(vec![0.0, 0.0, 0.0, 1_620_750.250_8, 4_277_012.715_3, 0.0]); - - let origin = super::get_origin( - tie_point.as_deref(), - matrix, - &PathBuf::from("not_exist.tif"), - ) - .unwrap(); - assert_abs_diff_eq!(origin[0], 1_620_750.250_8); - assert_abs_diff_eq!(origin[1], 4_277_012.715_3); - assert_abs_diff_eq!(origin[2], 0.0); - - let matrix = Some(vec![ + #[rstest] + #[case( + None,Some(vec![0.0, 0.0, 0.0, 1_620_750.250_8, 4_277_012.715_3, 0.0]), + [1_620_750.250_8, 4_277_012.715_3, 0.0] + )] + #[case( + Some(vec![ 0.0, 100.0, 0.0, 400000.0, 100.0, 0.0, 0.0, 500000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, - ]); - let tie_point: Option> = None; - + ]), + None, + [400000.0, 500000.0, 0.0] + )] + fn can_get_origin( + #[case] matrix: Option>, + #[case] tie_point: Option>, + #[case] expected: [f64; 3], + ) { let origin = super::get_origin( tie_point.as_deref(), matrix.as_deref(), &PathBuf::from("not_exist.tif"), ) .unwrap(); - assert_abs_diff_eq!(origin[0], 400000.0); - assert_abs_diff_eq!(origin[1], 500000.0); - assert_abs_diff_eq!(origin[2], 0.0); + assert_abs_diff_eq!(origin[0], expected[0]); + assert_abs_diff_eq!(origin[1], expected[1]); + assert_abs_diff_eq!(origin[2], expected[2]); } #[test] From d1c1269e181d788253d0b62410987b4f86f7e5e5 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Mon, 17 Feb 2025 11:21:23 +0800 Subject: [PATCH 28/58] cleanup --- martin/src/cog/source.rs | 121 ++++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 60 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index bcb5048b1..147a0cd46 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -21,6 +21,7 @@ use super::CogError; // about the model space of tiff image. // pixel scale, tie points and transformations +// todo use struct instead of tuple maybe type ModelInfo = (Option>, Option>, Option>); #[derive(Clone, Debug, Serialize)] @@ -142,59 +143,6 @@ impl CogSource { } } -fn meta_to_tilejson(meta: &Meta) -> TileJSON { - let mut tilejson = tilejson! { - tiles: vec![], - minzoom: meta.min_zoom, - maxzoom: meta.max_zoom - }; - - let mut cog_info = serde_json::Map::new(); - - cog_info.insert( - "minZoom".to_string(), - serde_json::Value::from(meta.min_zoom), - ); - - cog_info.insert( - "maxZoom".to_string(), - serde_json::Value::from(meta.max_zoom), - ); - - let mut resolutions_map = serde_json::Map::new(); - for (key, value) in &meta.zoom_and_resolutions { - resolutions_map.insert( - key.to_string(), // Convert u8 key to String - serde_json::Value::from(value.to_vec()[0]), - ); - } - - cog_info.insert( - "tileSize".to_string(), - serde_json::Value::from([meta.tile_size.0, meta.tile_size.1]), - ); - - cog_info.insert( - "resolutions".to_string(), - serde_json::Value::from(resolutions_map), - ); - - cog_info.insert( - "origin".to_string(), - serde_json::Value::from(meta.origin.to_vec()), - ); - - cog_info.insert( - "extent".to_string(), - serde_json::Value::from(meta.extent.to_vec()), - ); - - tilejson - .other - .insert("custom_grid".to_string(), serde_json::json!(cog_info)); - tilejson -} - #[async_trait] impl Source for CogSource { fn get_id(&self) -> &str { @@ -387,7 +335,7 @@ fn get_meta(path: &PathBuf) -> Result { let tie_points = model_info.1; let transformations = model_info.2; - let origin = get_origin(tie_points.as_deref(), transformations.as_deref(), path)?; + let origin: [f64; 3] = get_origin(tie_points.as_deref(), transformations.as_deref(), path)?; let full_resolution = get_full_resolution(pixel_scale.as_deref(), transformations.as_deref(), path)?; @@ -477,18 +425,18 @@ fn get_extent( [full_width_pixel, 0], [full_width_pixel, full_height_pixel], ]; - let transed = corners.map(|pixel| { + let transformed = corners.map(|pixel| { let i = f64::from(pixel[0]); let j = f64::from(pixel[1]); let x = matrix[3] + (matrix[0] * i) + (matrix[1] * j); let y = matrix[7] + (matrix[4] * i) + (matrix[5] * j); (x, y) }); - let mut min_x = transed[0].0; - let mut min_y = transed[1].1; - let mut max_x = transed[0].0; - let mut max_y = transed[1].1; - for (x, y) in transed { + let mut min_x = transformed[0].0; + let mut min_y = transformed[1].1; + let mut max_x = transformed[0].0; + let mut max_y = transformed[1].1; + for (x, y) in transformed { if x <= min_x { min_x = x; } @@ -645,6 +593,59 @@ fn get_origin( } } +fn meta_to_tilejson(meta: &Meta) -> TileJSON { + let mut tilejson = tilejson! { + tiles: vec![], + minzoom: meta.min_zoom, + maxzoom: meta.max_zoom + }; + + let mut cog_info = serde_json::Map::new(); + + cog_info.insert( + "minZoom".to_string(), + serde_json::Value::from(meta.min_zoom), + ); + + cog_info.insert( + "maxZoom".to_string(), + serde_json::Value::from(meta.max_zoom), + ); + + let mut resolutions_map = serde_json::Map::new(); + for (key, value) in &meta.zoom_and_resolutions { + resolutions_map.insert( + key.to_string(), // Convert u8 key to String + serde_json::Value::from(value.to_vec()[0]), + ); + } + + cog_info.insert( + "tileSize".to_string(), + serde_json::Value::from([meta.tile_size.0, meta.tile_size.1]), + ); + + cog_info.insert( + "resolutions".to_string(), + serde_json::Value::from(resolutions_map), + ); + + cog_info.insert( + "origin".to_string(), + serde_json::Value::from(meta.origin.to_vec()), + ); + + cog_info.insert( + "extent".to_string(), + serde_json::Value::from(meta.extent.to_vec()), + ); + + tilejson + .other + .insert("custom_grid".to_string(), serde_json::json!(cog_info)); + tilejson +} + #[cfg(test)] mod tests { use insta::{assert_yaml_snapshot, Settings}; From 6c7f05188e35fa74aa9788c930113d7cd1d20ae8 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Wed, 19 Feb 2025 14:41:16 +0800 Subject: [PATCH 29/58] add test for get_extent --- martin/src/cog/source.rs | 49 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 147a0cd46..aef623bf5 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -799,6 +799,55 @@ mod tests { "###); } + #[rstest] + #[case( + 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)) + ] + #[case( + Some(vec![ + 10.0,0.0,0.0,1_620_750.250_8, + 0.0,-10.0,0.0,4_277_012.715_3, + 0.0,0.0,0.0,0.0, + 0.0,0.0,0.0,1.0 + ]),None,None,(512,512)) + ] + fn can_get_extent( + #[case] matrix: Option>, + #[case] pixel_scale: Option>, + #[case] tie_point: Option>, + #[case] (full_width_pixel, full_length_pixel): (u32, u32), + ) { + use crate::cog::source::{get_extent, get_origin}; + + let origin = get_origin( + tie_point.as_deref(), + matrix.as_deref(), + &PathBuf::from("not_exist.tif"), + ) + .unwrap(); + let full_resolution = get_full_resolution( + pixel_scale.as_deref(), + matrix.as_deref(), + &PathBuf::from("not_exist.tif"), + ) + .unwrap(); + + let full_width = full_resolution[0] * f64::from(full_width_pixel); + let full_length = full_resolution[1] * f64::from(full_length_pixel); + + let extent = get_extent( + matrix.as_deref(), + &origin, + (full_width_pixel, full_length_pixel), + (full_width, full_length), + ); + + assert_abs_diff_eq!(extent[0], 1620750.2508); + assert_abs_diff_eq!(extent[1], 4271892.7153); + assert_abs_diff_eq!(extent[2], 1625870.2508); + assert_abs_diff_eq!(extent[3], 4277012.7153); + } + #[test] fn can_get_meta() { let path = PathBuf::from("../tests/fixtures/cog/rgb_u8.tif"); From 6803908057e64b5990924b42f1f2f769871d63e8 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Wed, 19 Feb 2025 17:04:21 +0800 Subject: [PATCH 30/58] verify squared pixel --- martin/src/cog/source.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index aef623bf5..8c85a92a8 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -292,7 +292,7 @@ fn verify_requirments( (Some(pixel_scale), Some(tie_points), _) => { - if (pixel_scale[0] + pixel_scale[1]) > 0.01{ + if (pixel_scale[0] + pixel_scale[1]).abs() > 0.01{ Err(CogError::NonSquaredImage(path.to_path_buf(), pixel_scale[0], pixel_scale[1])) } else if pixel_scale.len() != 3 || tie_points.len() % 6 != 0 { From 26aa1f0c5bbd0479159346db0f64ebb6741ed9db Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Thu, 20 Feb 2025 14:45:11 +0800 Subject: [PATCH 31/58] clippy --- martin/src/cog/source.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 8c85a92a8..1bfaa1dd3 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -734,11 +734,11 @@ mod tests { )] #[case( Some(vec![ - 0.0, 100.0, 0.0, 400000.0, 100.0, 0.0, 0.0, 500000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 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, 0.0, 1.0, ]), None, - [400000.0, 500000.0, 0.0] + [400_000.0, 500_000.0, 0.0] )] fn can_get_origin( #[case] matrix: Option>, @@ -842,10 +842,10 @@ mod tests { (full_width, full_length), ); - assert_abs_diff_eq!(extent[0], 1620750.2508); - assert_abs_diff_eq!(extent[1], 4271892.7153); - assert_abs_diff_eq!(extent[2], 1625870.2508); - assert_abs_diff_eq!(extent[3], 4277012.7153); + assert_abs_diff_eq!(extent[0], 1_620_750.250_8); + assert_abs_diff_eq!(extent[1], 4_271_892.715_3); + assert_abs_diff_eq!(extent[2], 1_625_870.250_8); + assert_abs_diff_eq!(extent[3], 4_277_012.715_3); } #[test] From 474ede30e6b44be301adca6d6b8ec94268fe4ed2 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Thu, 20 Feb 2025 16:39:48 +0800 Subject: [PATCH 32/58] update tilejson --- martin/src/cog/source.rs | 13 ++++++------- tests/expected/auto/rgb_u8.json | 15 +++++++-------- tests/expected/auto/rgba_u8.json | 15 +++++++-------- tests/expected/auto/rgba_u8_nodata.json | 13 ++++++------- 4 files changed, 26 insertions(+), 30 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 1bfaa1dd3..45a59c570 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -612,14 +612,13 @@ fn meta_to_tilejson(meta: &Meta) -> TileJSON { serde_json::Value::from(meta.max_zoom), ); - let mut resolutions_map = serde_json::Map::new(); - for (key, value) in &meta.zoom_and_resolutions { - resolutions_map.insert( - key.to_string(), // Convert u8 key to String - serde_json::Value::from(value.to_vec()[0]), - ); + let mut resolutions_map = Vec::new(); + for value in meta.zoom_and_resolutions.values() { + resolutions_map.push(value[0]); } + resolutions_map.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + cog_info.insert( "tileSize".to_string(), serde_json::Value::from([meta.tile_size.0, meta.tile_size.1]), @@ -632,7 +631,7 @@ fn meta_to_tilejson(meta: &Meta) -> TileJSON { cog_info.insert( "origin".to_string(), - serde_json::Value::from(meta.origin.to_vec()), + serde_json::Value::from([meta.origin[0], meta.origin[1]]), ); cog_info.insert( diff --git a/tests/expected/auto/rgb_u8.json b/tests/expected/auto/rgb_u8.json index 1a76e725c..c717f0c82 100644 --- a/tests/expected/auto/rgb_u8.json +++ b/tests/expected/auto/rgb_u8.json @@ -10,15 +10,14 @@ "minZoom": 0, "origin": [ 1620750.2508, - 4277012.7153, - 0 + 4277012.7153 + ], + "resolutions": [ + 10, + 20, + 40, + 80 ], - "resolutions": { - "0": 80, - "1": 40, - "2": 20, - "3": 10 - }, "tileSize": [ 256, 256 diff --git a/tests/expected/auto/rgba_u8.json b/tests/expected/auto/rgba_u8.json index ea7b14606..56f40fac9 100644 --- a/tests/expected/auto/rgba_u8.json +++ b/tests/expected/auto/rgba_u8.json @@ -10,15 +10,14 @@ "minZoom": 0, "origin": [ 1620750.2508, - 4277012.7153, - 0 + 4277012.7153 + ], + "resolutions": [ + 10, + 20, + 40, + 80 ], - "resolutions": { - "0": 80, - "1": 40, - "2": 20, - "3": 10 - }, "tileSize": [ 256, 256 diff --git a/tests/expected/auto/rgba_u8_nodata.json b/tests/expected/auto/rgba_u8_nodata.json index 3671d02db..d9a1e4a90 100644 --- a/tests/expected/auto/rgba_u8_nodata.json +++ b/tests/expected/auto/rgba_u8_nodata.json @@ -10,14 +10,13 @@ "minZoom": 0, "origin": [ 1620750.2508, - 4277012.7153, - 0 + 4277012.7153 + ], + "resolutions": [ + 10, + 20, + 40 ], - "resolutions": { - "0": 40, - "1": 20, - "2": 10 - }, "tileSize": [ 512, 512 From 6cda58233e29796826991535b3f871da987f89cf Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 21 Feb 2025 17:02:21 +0800 Subject: [PATCH 33/58] get resolutions sorted --- martin/src/cog/source.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 45a59c570..1e203dba6 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -617,7 +617,7 @@ fn meta_to_tilejson(meta: &Meta) -> TileJSON { resolutions_map.push(value[0]); } - resolutions_map.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + resolutions_map.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); cog_info.insert( "tileSize".to_string(), From ffa3fa13b294673cef371a15b46f18988025e54b Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 21 Feb 2025 21:03:33 +0800 Subject: [PATCH 34/58] bless --- tests/expected/auto/rgb_u8.json | 4 ++-- tests/expected/auto/rgba_u8.json | 4 ++-- tests/expected/auto/rgba_u8_nodata.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/expected/auto/rgb_u8.json b/tests/expected/auto/rgb_u8.json index c717f0c82..adaf74eae 100644 --- a/tests/expected/auto/rgb_u8.json +++ b/tests/expected/auto/rgb_u8.json @@ -1,10 +1,10 @@ { "custom_grid": { "extent": [ - 1620750.2508, + 4277012.7153, 4271892.7153, 1625870.2508, - 4277012.7153 + 1620750.2508 ], "maxZoom": 3, "minZoom": 0, diff --git a/tests/expected/auto/rgba_u8.json b/tests/expected/auto/rgba_u8.json index 56f40fac9..1df676342 100644 --- a/tests/expected/auto/rgba_u8.json +++ b/tests/expected/auto/rgba_u8.json @@ -1,10 +1,10 @@ { "custom_grid": { "extent": [ - 1620750.2508, + 4277012.7153, 4271892.7153, 1625870.2508, - 4277012.7153 + 1620750.2508 ], "maxZoom": 3, "minZoom": 0, diff --git a/tests/expected/auto/rgba_u8_nodata.json b/tests/expected/auto/rgba_u8_nodata.json index d9a1e4a90..30a6cc2d4 100644 --- a/tests/expected/auto/rgba_u8_nodata.json +++ b/tests/expected/auto/rgba_u8_nodata.json @@ -1,10 +1,10 @@ { "custom_grid": { "extent": [ - 1620750.2508, + 4277012.7153, 4271892.7153, 1625870.2508, - 4277012.7153 + 1620750.2508 ], "maxZoom": 2, "minZoom": 0, From 07a3dd62ef22b48fe44068bcb0323081afa31d3a Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Sat, 22 Feb 2025 21:11:14 +0800 Subject: [PATCH 35/58] bless --- tests/expected/auto/rgb_u8.json | 6 +++--- tests/expected/auto/rgba_u8.json | 6 +++--- tests/expected/auto/rgba_u8_nodata.json | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/expected/auto/rgb_u8.json b/tests/expected/auto/rgb_u8.json index adaf74eae..aecc4ae72 100644 --- a/tests/expected/auto/rgb_u8.json +++ b/tests/expected/auto/rgb_u8.json @@ -13,10 +13,10 @@ 4277012.7153 ], "resolutions": [ - 10, - 20, + 80, 40, - 80 + 20, + 10 ], "tileSize": [ 256, diff --git a/tests/expected/auto/rgba_u8.json b/tests/expected/auto/rgba_u8.json index 1df676342..47e321ed5 100644 --- a/tests/expected/auto/rgba_u8.json +++ b/tests/expected/auto/rgba_u8.json @@ -13,10 +13,10 @@ 4277012.7153 ], "resolutions": [ - 10, - 20, + 80, 40, - 80 + 20, + 10 ], "tileSize": [ 256, diff --git a/tests/expected/auto/rgba_u8_nodata.json b/tests/expected/auto/rgba_u8_nodata.json index 30a6cc2d4..4a5d1c77c 100644 --- a/tests/expected/auto/rgba_u8_nodata.json +++ b/tests/expected/auto/rgba_u8_nodata.json @@ -13,9 +13,9 @@ 4277012.7153 ], "resolutions": [ - 10, + 40, 20, - 40 + 10 ], "tileSize": [ 512, From e524ace5256602e3082fed4e5a00b908886603ee Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Tue, 25 Feb 2025 16:40:55 +0800 Subject: [PATCH 36/58] bless --- tests/expected/auto/rgb_u8.json | 4 ++-- tests/expected/auto/rgba_u8.json | 4 ++-- tests/expected/auto/rgba_u8_nodata.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/expected/auto/rgb_u8.json b/tests/expected/auto/rgb_u8.json index aecc4ae72..2f44a3470 100644 --- a/tests/expected/auto/rgb_u8.json +++ b/tests/expected/auto/rgb_u8.json @@ -1,10 +1,10 @@ { "custom_grid": { "extent": [ - 4277012.7153, + 1620750.2508, 4271892.7153, 1625870.2508, - 1620750.2508 + 4277012.7153 ], "maxZoom": 3, "minZoom": 0, diff --git a/tests/expected/auto/rgba_u8.json b/tests/expected/auto/rgba_u8.json index 47e321ed5..0b55bcae7 100644 --- a/tests/expected/auto/rgba_u8.json +++ b/tests/expected/auto/rgba_u8.json @@ -1,10 +1,10 @@ { "custom_grid": { "extent": [ - 4277012.7153, + 1620750.2508, 4271892.7153, 1625870.2508, - 1620750.2508 + 4277012.7153 ], "maxZoom": 3, "minZoom": 0, diff --git a/tests/expected/auto/rgba_u8_nodata.json b/tests/expected/auto/rgba_u8_nodata.json index 4a5d1c77c..27f5ddc94 100644 --- a/tests/expected/auto/rgba_u8_nodata.json +++ b/tests/expected/auto/rgba_u8_nodata.json @@ -1,10 +1,10 @@ { "custom_grid": { "extent": [ - 4277012.7153, + 1620750.2508, 4271892.7153, 1625870.2508, - 1620750.2508 + 4277012.7153 ], "maxZoom": 2, "minZoom": 0, From 4872cc4d2713245008af6cc3be92687976ceb02b Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 28 Feb 2025 15:04:47 +0800 Subject: [PATCH 37/58] add draft doc --- docs/src/sources-cog-files.md | 95 +++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/docs/src/sources-cog-files.md b/docs/src/sources-cog-files.md index f9966ebe3..59891ba69 100644 --- a/docs/src/sources-cog-files.md +++ b/docs/src/sources-cog-files.md @@ -57,6 +57,101 @@ cog: cog-src2: /path/to/cog2.tif ``` +## Tile Grid + +Generally COG has a custom tile grid which is not aligned to the google 3857 which is default in almost any map client like MapLibre, OpenLayers, etc.. + +To display this COG the clients needs to know the custom tile grid of COG. +To not break the compatiblity of TileJSON spec, martin choose to add a field in tilejson to show the custom tile grid. + +```json +{ + "maxzoom": 3, + "minzoom": 0, + "tilejson": "3.0.0", + "tiles": [ + "http://localhost:3111/rgb_u8/{z}/{x}/{y}" + ], + "custom_grid": { + "extent": [ + 1620750.2508, + 4271892.7153, + 1625870.2508, + 4277012.7153 + ], + "maxZoom": 3, + "minZoom": 0, + "origin": [ + 1620750.2508, + 4277012.7153 + ], + "resolutions": [ + 80, + 40, + 20, + 10 + ], + "tileSize": [ + 256, + 256 + ] + }, +} +``` + +A demo about how to load it with `openlayers`. + +```js +import './style.css'; +import { Map, View } from 'ol'; +import TileLayer from 'ol/layer/Tile'; +import OSM from 'ol/source/OSM'; +import XYZ from 'ol/source/XYZ.js'; + + +import TileGrid from 'ol/tilegrid/TileGrid.js'; + +// the tiling schema could be GoogleMapsCompatible, see https://gdal.org/en/latest/drivers/raster/cog.html#reprojection-related-creation-options +var custom_grid = new TileGrid({ + extent: [ + 1620750.2508, + 4271892.7153, + 1625870.2508, + 4277012.7153], + resolutions: [ + 80, + 40, + 20, + 10], + tileSize: [256, 256], + origin: [1620750.2508, 4277012.7153] +}); + +var source = new XYZ({ + url: "http://10.1.155.35:3000/rgb_u8/{z}/{x}/{y}", tileGrid: custom_grid +}); + +const map = new Map({ + target: 'map', + layers: [ + new TileLayer({ + source: new XYZ({ + url: 'https://api.maptiler.com/maps/basic/{z}/{x}/{y}.png?key=vgdy5zgzM4IIqQCC6SOn', + attributions: '© MapTiler © OpenStreetMap contributors' + }) + }), + new TileLayer({ + source: source + }), + + ], + view: new View({ + center: [(1620750.2508 + 1625870.2508) / 2, (4271892.7153 + 4277012.7153) / 2], + zoom: 14 + }) +}); +``` + ## About COG [COG](https://cogeo.org/) is just Cloud Optimized GeoTIFF file. From a2018f0aede4d30685b1fc192b4a146203860c83 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 28 Feb 2025 16:20:03 +0800 Subject: [PATCH 38/58] update doc --- docs/src/sources-cog-files.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/src/sources-cog-files.md b/docs/src/sources-cog-files.md index 59891ba69..3b021928c 100644 --- a/docs/src/sources-cog-files.md +++ b/docs/src/sources-cog-files.md @@ -59,10 +59,13 @@ cog: ## Tile Grid -Generally COG has a custom tile grid which is not aligned to the google 3857 which is default in almost any map client like MapLibre, OpenLayers, etc.. +Generally `COG` file has a custom tile grid which is not aligned to the google 3857 which is default in almost any map client like MapLibre, OpenLayers, etc.. -To display this COG the clients needs to know the custom tile grid of COG. -To not break the compatiblity of TileJSON spec, martin choose to add a field in tilejson to show the custom tile grid. +To display the `COG` file the clients needs to know the custom `tile grid` of COG. + +To not break the compatiblity of TileJSON spec, martin choose to add a field in `TileJSON` to tell the custom tile grid. + +Lile we have a cog source named `rgb_u8`, we could see the custom filed in our `TileJson` by visit `http://your_host:your_port/rgb_u8`. ```json { @@ -72,7 +75,7 @@ To not break the compatiblity of TileJSON spec, martin choose to add a field in "tiles": [ "http://localhost:3111/rgb_u8/{z}/{x}/{y}" ], - "custom_grid": { + "custom_grid": { // the custom tile grid added here "extent": [ 1620750.2508, 4271892.7153, From 8a0b5a5d24bf60b2a51a77ecfddf2671b35c91e6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 22:27:59 +0000 Subject: [PATCH 39/58] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- martin/src/cog/source.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 13d66cd5e..00be2587a 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -7,8 +7,8 @@ use std::vec; use async_trait::async_trait; use log::warn; -use serde::Serialize; use martin_tile_utils::{Format, TileCoord, TileInfo}; +use serde::Serialize; use tiff::decoder::{ChunkType, Decoder, DecodingResult}; use tiff::tags::Tag::{self, GdalNodata}; use tilejson::{tilejson, TileJSON}; @@ -645,11 +645,11 @@ fn meta_to_tilejson(meta: &Meta) -> TileJSON { #[cfg(test)] mod tests { - use std::{fs::File, path::PathBuf}; use insta::{assert_yaml_snapshot, Settings}; - use tiff::decoder::Decoder; use martin_tile_utils::TileCoord; use rstest::rstest; + use std::{fs::File, path::PathBuf}; + use tiff::decoder::Decoder; use crate::cog::source::{get_full_resolution, get_tile_idx}; use approx::assert_abs_diff_eq; From 535e2f170d98f5f7868de8caadbbc0a80df1b43d Mon Sep 17 00:00:00 2001 From: Lucas Date: Wed, 5 Mar 2025 12:38:29 +0800 Subject: [PATCH 40/58] Update sources-cog-files.md update doc --- docs/src/sources-cog-files.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs/src/sources-cog-files.md b/docs/src/sources-cog-files.md index 3b021928c..e4a2a1572 100644 --- a/docs/src/sources-cog-files.md +++ b/docs/src/sources-cog-files.md @@ -114,7 +114,6 @@ import XYZ from 'ol/source/XYZ.js'; import TileGrid from 'ol/tilegrid/TileGrid.js'; -// the tiling schema could be GoogleMapsCompatible, see https://gdal.org/en/latest/drivers/raster/cog.html#reprojection-related-creation-options var custom_grid = new TileGrid({ extent: [ 1620750.2508, @@ -137,16 +136,9 @@ var source = new XYZ({ const map = new Map({ target: 'map', layers: [ - new TileLayer({ - source: new XYZ({ - url: 'https://api.maptiler.com/maps/basic/{z}/{x}/{y}.png?key=vgdy5zgzM4IIqQCC6SOn', - attributions: '© MapTiler © OpenStreetMap contributors' - }) - }), new TileLayer({ source: source }), - ], view: new View({ center: [(1620750.2508 + 1625870.2508) / 2, (4271892.7153 + 4277012.7153) / 2], From 0df310e6cace6cec34e5491662a2e0d644a8db8f Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 24 Apr 2025 03:38:29 +0000 Subject: [PATCH 41/58] add test fixture --- tests/fixtures/cog/google_compatible.tif | Bin 0 -> 92878 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/fixtures/cog/google_compatible.tif diff --git a/tests/fixtures/cog/google_compatible.tif b/tests/fixtures/cog/google_compatible.tif new file mode 100644 index 0000000000000000000000000000000000000000..d5dc97744724e48e5cae409b2d2abfdefde8261b GIT binary patch literal 92878 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

S|I-=A@zpWJU7dEc6ag+r9xqMW(7PwLL_E*QT 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 }M#Z7Rh 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 literal 0 HcmV?d00001 From 3f3568d8b77ef6c4190fae280c62e264c0aec9d1 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 24 Apr 2025 05:26:12 +0000 Subject: [PATCH 42/58] add dep --- Cargo.lock | 590 ++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 2 + martin/Cargo.toml | 4 +- 3 files changed, 580 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b100bc12f..405935eca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ab_glyph" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + [[package]] name = "actix-codec" version = "0.5.2" @@ -250,6 +266,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + [[package]] name = "aligned-vec" version = "0.6.1" @@ -366,6 +388,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "arrayref" version = "0.3.9" @@ -450,6 +489,29 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational 0.4.2", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98922d6a4cfbcb08820c69d8eeccc05bb1f29bfa06b4f5b1dbfe9a868bd7608e" +dependencies = [ + "arrayvec", +] + [[package]] name = "aws-lc-rs" version = "1.12.4" @@ -552,6 +614,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -567,6 +635,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitstream-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" + [[package]] name = "bitvec" version = "1.0.1" @@ -697,6 +771,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7f2e6c4f2a017f63b5a1fd7cc437f061b53a3e890bcca840ef756d72f6b72f2" +[[package]] +name = "built" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b" + [[package]] name = "bumpalo" version = "3.17.0" @@ -715,6 +795,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.10.0" @@ -756,6 +842,16 @@ dependencies = [ "nom", ] +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -1430,7 +1526,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1471,6 +1567,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -1605,7 +1716,7 @@ dependencies = [ "memmap2 0.8.0", "slotmap", "tinyvec", - "ttf-parser", + "ttf-parser 0.19.2", ] [[package]] @@ -1836,6 +1947,16 @@ dependencies = [ "weezl", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.31.1" @@ -2326,12 +2447,69 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif 0.13.1", + "image-webp", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imageproc" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2393fb7808960751a52e8a154f67e7dd3f8a2ef9bd80d1553078a7b4e8ed3f0d" +dependencies = [ + "ab_glyph", + "approx", + "getrandom 0.2.15", + "image", + "itertools 0.12.1", + "nalgebra", + "num 0.4.3", + "rand 0.8.5", + "rand_distr", + "rayon", +] + [[package]] name = "imagesize" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284" +[[package]] +name = "imgref" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" + [[package]] name = "impl-more" version = "0.1.9" @@ -2397,7 +2575,18 @@ dependencies = [ "pin-project", "serde", "similar", - "toml", + "toml 0.5.11", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", ] [[package]] @@ -2414,7 +2603,7 @@ checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37" dependencies = [ "hermit-abi 0.4.0", "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2582,6 +2771,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libc" version = "0.2.170" @@ -2606,6 +2801,16 @@ dependencies = [ "libdeflate-sys", ] +[[package]] +name = "libfuzzer-sys" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75" +dependencies = [ + "arbitrary", + "cc", +] + [[package]] name = "libloading" version = "0.8.6" @@ -2613,7 +2818,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -2714,6 +2919,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "martin" version = "0.15.0" @@ -2735,6 +2949,8 @@ dependencies = [ "enum-display", "env_logger", "futures", + "image", + "imageproc", "indoc", "insta", "itertools 0.14.0", @@ -2794,6 +3010,26 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matrixmultiply" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9380b911e3e96d10c1f415da0876389aaf1b56759054eeb0de7df940c456ba1a" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + [[package]] name = "mbtiles" version = "0.12.0" @@ -2952,6 +3188,21 @@ dependencies = [ "serde", ] +[[package]] +name = "nalgebra" +version = "0.32.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5c17de023a86f59ed79891b2e5d5a94c705dbe904a5b5c9c952ea6221b03e4" +dependencies = [ + "approx", + "matrixmultiply", + "num-complex 0.4.6", + "num-rational 0.4.2", + "num-traits", + "simba", + "typenum", +] + [[package]] name = "nanorand" version = "0.7.0" @@ -2961,6 +3212,12 @@ dependencies = [ "getrandom 0.2.15", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nix" version = "0.26.4" @@ -2982,6 +3239,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2998,10 +3261,34 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" dependencies = [ - "num-complex", + "num-complex 0.2.4", "num-integer", "num-iter", - "num-rational", + "num-rational 0.2.4", + "num-traits", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex 0.4.6", + "num-integer", + "num-iter", + "num-rational 0.4.2", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", "num-traits", ] @@ -3032,12 +3319,32 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "num-format" version = "0.4.4" @@ -3079,6 +3386,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3132,6 +3450,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owned_ttf_parser" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4" +dependencies = [ + "ttf-parser 0.25.1", +] + [[package]] name = "oxipng" version = "9.1.4" @@ -3501,7 +3828,7 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebbe2f8898beba44815fdc9e5a4ae9c929e21c5dc29b0c774a15555f7f58d6d0" dependencies = [ - "aligned-vec", + "aligned-vec 0.6.1", "backtrace", "cfg-if", "criterion", @@ -3565,6 +3892,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +dependencies = [ + "quote", + "syn 2.0.98", +] + [[package]] name = "protobuf" version = "3.7.1" @@ -3673,6 +4019,21 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dc55d7dec32ecaf61e0bd90b3d2392d721a28b95cfd23c3e176eccefbeab2f2" +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quick-xml" version = "0.26.0" @@ -3731,7 +4092,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3810,6 +4171,72 @@ dependencies = [ "zerocopy 0.8.21", ] +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.12.1", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand 0.8.5", + "rand_chacha 0.3.1", + "simd_helpers", + "system-deps", + "thiserror 1.0.69", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6a5f31fcf7500f9401fea858ea4ab5525c99f2322cfcee732c0e6c74208c0c6" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + [[package]] name = "rayon" version = "1.10.0" @@ -3965,7 +4392,7 @@ version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc7980f653f9a7db31acff916a262c3b78c562919263edea29bf41a056e20497" dependencies = [ - "gif", + "gif 0.12.0", "jpeg-decoder", "log", "pico-args", @@ -4115,7 +4542,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4191,7 +4618,7 @@ dependencies = [ "bitflags 1.3.2", "bytemuck", "smallvec", - "ttf-parser", + "ttf-parser 0.19.2", "unicode-bidi-mirroring", "unicode-ccc", "unicode-properties", @@ -4204,6 +4631,15 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + [[package]] name = "same-file" version = "1.0.6" @@ -4316,6 +4752,15 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + [[package]] name = "serde_tuple" version = "0.5.0" @@ -4448,12 +4893,34 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simba" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061507c94fc6ab4ba1c9a0305018408e312e17c041eb63bef8aa726fa33aceae" +dependencies = [ + "approx", + "num-complex 0.4.6", + "num-traits", + "paste", + "wide", +] + [[package]] name = "simd-adler32" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "similar" version = "2.7.0" @@ -4488,7 +4955,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ed5f6ab2122c6dec69dca18c72fa4590a27e581ad20d44960fe74c032a0b23b" dependencies = [ "generic-array 0.12.4", - "num", + "num 0.2.1", ] [[package]] @@ -4975,6 +5442,19 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.20", + "version-compare", +] + [[package]] name = "tagptr" version = "0.2.0" @@ -4987,6 +5467,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempfile" version = "3.17.1" @@ -4998,7 +5484,7 @@ dependencies = [ "getrandom 0.3.1", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5353,11 +5839,26 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -5366,6 +5867,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ "indexmap 2.7.1", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] @@ -5486,6 +5989,12 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1" +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + [[package]] name = "typenum" version = "1.18.0" @@ -5685,6 +6194,17 @@ dependencies = [ "getrandom 0.3.1", ] +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec 0.5.0", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -5703,6 +6223,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.5" @@ -5869,6 +6395,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wide" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b5576b9a81633f3e8df296ce0063042a73507636cbe956c61133dd7034ab22" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "winapi" version = "0.3.9" @@ -5891,7 +6427,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -6393,3 +6929,27 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index 4248fd466..152d68a1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,8 @@ env_logger = "0.11" flate2 = "1" flume = "0.11" futures = "0.3" +image = "0.25.6" +imageproc = "0.25.0" indoc = "2" insta = "1" itertools = "0.14" diff --git a/martin/Cargo.toml b/martin/Cargo.toml index 3d682d14a..d347ace7a 100644 --- a/martin/Cargo.toml +++ b/martin/Cargo.toml @@ -61,7 +61,7 @@ fonts = ["dep:bit-set", "dep:pbf_font_tools"] lambda = ["dep:lambda-web"] mbtiles = ["dep:mbtiles"] pmtiles = ["dep:pmtiles"] -cog = ["dep:tiff", "dep:png"] +cog = ["dep:tiff", "dep:png", "dep:image", "dep:imageproc"] postgres = ["dep:deadpool-postgres", "dep:json-patch", "dep:postgis", "dep:postgres", "dep:postgres-protocol", "dep:semver", "dep:tokio-postgres-rustls"] sprites = ["dep:spreet", "tokio/fs"] bless-tests = [] @@ -81,6 +81,8 @@ enum-display.workspace = true env_logger.workspace = true futures.workspace = true itertools.workspace = true +image = { workspace = true, optional = true } +imageproc = { workspace = true, optional = true } json-patch = { workspace = true, optional = true } lambda-web = { workspace = true, optional = true } log.workspace = true From 236ade693c5d900d7d7bea078ce68e88f45b05b6 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 24 Apr 2025 08:29:26 +0000 Subject: [PATCH 43/58] wip --- martin/src/cog/source.rs | 120 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 00be2587a..68f11e07a 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -5,6 +5,8 @@ use std::io::BufWriter; use std::path::{Path, PathBuf}; use std::vec; +use image::{ImageBuffer, Rgba}; + use async_trait::async_trait; use log::warn; use martin_tile_utils::{Format, TileCoord, TileInfo}; @@ -59,6 +61,38 @@ impl CogSource { tileinfo, }) } + + pub fn sub_region(&self, zoom: u8, window: [f64; 4], output_size: u32) -> MartinResult { + // 求出window的 pixel size, width height + // 求出覆盖到的tile indexs + // 遍历每一个tile,进行put_pixel + let resolution = self.meta.zoom_and_resolutions.get(&zoom).unwrap(); + let window_width_pixel = ((window[2] - window[0]) / resolution[0]).ceil() as u32; + let window_height_pixel = ((window[3] - window[1]) / resolution[1]).ceil() as u32; + + let cog_extent = self.meta.extent; + let cog_tile_size = self.meta.tile_size; + let across_down = self.meta.zoom_and_tile_across_down.get(&zoom).unwrap(); + + let tile_indexes: Vec<(u32, u32)> = + get_covered_tile_indexes(window, cog_extent, *across_down, cog_tile_size, resolution); + + let mut output_image: ImageBuffer, Vec> = + ImageBuffer::new(window_width_pixel, window_height_pixel); + + // let mut decoder = + // Decoder::new(tif_file).map_err(|e| CogError::InvalidTiffFile(e, self.path.clone()))?; + // decoder = decoder.with_limits(tiff::decoder::Limits::unlimited()); + // for (x_idx, y_idx) in tile_indexes {} + // let rgba_pixels = self.chunk_to_rgba(x_idx.y_idx).unwrap(); + // // Calculate tile geographic bounds (using top-left origin convention) + // let tile_geo_min_x = cog_extent[0] + f64::from(col * cog_tile_size.0) * resolution[0]; + // let tile_geo_max_y = cog_extent[3] - f64::from(row * cog_tile_size.1) * resolution_y_abs; + // let tile_geo_max_x = tile_geo_min_x + f64::from(tile_width) * resolution[0]; + // let tile_geo_min_y = tile_geo_max_y - f64::from(tile_height) * resolution_y_abs; + todo!() + } + #[allow(clippy::cast_sign_loss)] #[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines)] @@ -141,6 +175,82 @@ impl CogSource { } } +fn get_covered_tile_indexes( + window: [f64; 4], + cog_extent: [f64; 4], + across_down: (u32, u32), + cog_tile_size: (u32, u32), + resolution: &[f64; 3], +) -> Vec<(u32, u32)> { + let epsilon = 1e-6; + + let tile_span_x = f64::from(cog_tile_size.0) * resolution[0]; + // resolution[1] is typically negative, use its absolute value for span calculation + let tile_span_y = f64::from(cog_tile_size.1) * resolution[1].abs(); + + let tile_matrix_min_x = cog_extent[0]; + // Use max Y from extent as the top edge for row calculation + let tile_matrix_max_y = cog_extent[3]; + + let matrix_width = across_down.0; + let matrix_height = across_down.1; + + // Calculate tile index ranges based on the provided formula + let tile_min_col_f = ((window[0] - tile_matrix_min_x) / tile_span_x + epsilon).floor(); + let tile_max_col_f = ((window[2] - tile_matrix_min_x) / tile_span_x - epsilon).floor(); + let tile_min_row_f = ((tile_matrix_max_y - window[3]) / tile_span_y + epsilon).floor(); + let tile_max_row_f = ((tile_matrix_max_y - window[1]) / tile_span_y - epsilon).floor(); + + // Convert to integer type for clamping and iteration + let mut tile_min_col = tile_min_col_f as i64; + let mut tile_max_col = tile_max_col_f as i64; + let mut tile_min_row = tile_min_row_f as i64; + let mut tile_max_row = tile_max_row_f as i64; + + // Clamp minimum values to 0 + if tile_min_col < 0 { + tile_min_col = 0; + } + if tile_min_row < 0 { + tile_min_row = 0; + } + + // Clamp maximum values to matrix dimensions - 1 + let matrix_width_i64 = i64::from(matrix_width); + let matrix_height_i64 = i64::from(matrix_height); + + if tile_max_col >= matrix_width_i64 { + tile_max_col = matrix_width_i64 - 1; + } + if tile_max_row >= matrix_height_i64 { + tile_max_row = matrix_height_i64 - 1; + } + + // If the calculated range is invalid (max < min), return empty vector + if tile_max_col < tile_min_col || tile_max_row < tile_min_row { + return Vec::new(); + } + + // Convert to u32 for the final result type + let tile_min_col = tile_min_col as u32; + let tile_max_col = tile_max_col as u32; + let tile_min_row = tile_min_row as u32; + let tile_max_row = tile_max_row as u32; + + let mut covered_tiles = Vec::new(); + // Iterate through the valid tile range and collect the indexes + for row in tile_min_row..=tile_max_row { + for col in tile_min_col..=tile_max_col { + // Double check bounds (should be guaranteed by clamping, but safe) + if col < matrix_width && row < matrix_height { + covered_tiles.push((col, row)); + } + } + } + + covered_tiles +} + #[async_trait] impl Source for CogSource { fn get_id(&self) -> &str { @@ -910,4 +1020,14 @@ mod tests { "###); }); } + + #[test] + fn can_trans_to_google() { + let path = PathBuf::from("../tests/fixtures/cog/google_compatible.tif"); + + let source = super::CogSource::new("test".to_string(), path); + let window = [1620847.0, 4276072.0, 1621379.0, 4276545.0]; + + todo!() + } } From 8a6bd1e7c87f4bfea3e00d008d1def99fde326dd Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Fri, 25 Apr 2025 14:48:55 +0800 Subject: [PATCH 44/58] wip --- martin/src/cog/source.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 68f11e07a..5a5c2cbb2 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -62,13 +62,20 @@ impl CogSource { }) } - pub fn sub_region(&self, zoom: u8, window: [f64; 4], output_size: u32) -> MartinResult { + pub fn sub_region( + &self, + zoom: u8, + window: [f64; 4], + output_size: u32, + ) -> MartinResult { // 求出window的 pixel size, width height // 求出覆盖到的tile indexs // 遍历每一个tile,进行put_pixel let resolution = self.meta.zoom_and_resolutions.get(&zoom).unwrap(); - let window_width_pixel = ((window[2] - window[0]) / resolution[0]).ceil() as u32; - let window_height_pixel = ((window[3] - window[1]) / resolution[1]).ceil() as u32; + let res_x = resolution[0]; + let res_y = resolution[1].abs(); + let window_width_pixel = ((window[2] - window[0]) / res_x).ceil() as u32; + let window_height_pixel = ((window[3] - window[1]) / res_y).ceil() as u32; let cog_extent = self.meta.extent; let cog_tile_size = self.meta.tile_size; @@ -1025,9 +1032,9 @@ mod tests { fn can_trans_to_google() { let path = PathBuf::from("../tests/fixtures/cog/google_compatible.tif"); - let source = super::CogSource::new("test".to_string(), path); + let source = super::CogSource::new("test".to_string(), path).unwrap(); let window = [1620847.0, 4276072.0, 1621379.0, 4276545.0]; - + let result = source.sub_region(2, window, 512); todo!() } } From 571895bef99217a316683245f43c1aba48544ee0 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Sun, 27 Apr 2025 13:50:08 +0800 Subject: [PATCH 45/58] add sub_region method --- martin/src/cog/source.rs | 129 ++++++++++++++++++++++++++++++++++----- 1 file changed, 113 insertions(+), 16 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 5a5c2cbb2..e0e23c6e3 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -64,19 +64,20 @@ impl CogSource { pub fn sub_region( &self, + decoder: &mut Decoder, zoom: u8, window: [f64; 4], output_size: u32, ) -> MartinResult { - // 求出window的 pixel size, width height - // 求出覆盖到的tile indexs - // 遍历每一个tile,进行put_pixel + let ifd = self.meta.zoom_and_ifd.get(&zoom).unwrap(); + let resolution = self.meta.zoom_and_resolutions.get(&zoom).unwrap(); let res_x = resolution[0]; let res_y = resolution[1].abs(); let window_width_pixel = ((window[2] - window[0]) / res_x).ceil() as u32; let window_height_pixel = ((window[3] - window[1]) / res_y).ceil() as u32; + let cog_origin = self.meta.origin; let cog_extent = self.meta.extent; let cog_tile_size = self.meta.tile_size; let across_down = self.meta.zoom_and_tile_across_down.get(&zoom).unwrap(); @@ -87,17 +88,109 @@ impl CogSource { let mut output_image: ImageBuffer, Vec> = ImageBuffer::new(window_width_pixel, window_height_pixel); - // let mut decoder = - // Decoder::new(tif_file).map_err(|e| CogError::InvalidTiffFile(e, self.path.clone()))?; - // decoder = decoder.with_limits(tiff::decoder::Limits::unlimited()); - // for (x_idx, y_idx) in tile_indexes {} - // let rgba_pixels = self.chunk_to_rgba(x_idx.y_idx).unwrap(); - // // Calculate tile geographic bounds (using top-left origin convention) - // let tile_geo_min_x = cog_extent[0] + f64::from(col * cog_tile_size.0) * resolution[0]; - // let tile_geo_max_y = cog_extent[3] - f64::from(row * cog_tile_size.1) * resolution_y_abs; - // let tile_geo_max_x = tile_geo_min_x + f64::from(tile_width) * resolution[0]; - // let tile_geo_min_y = tile_geo_max_y - f64::from(tile_height) * resolution_y_abs; - todo!() + decoder.seek_to_image(*ifd); + for (col, row) in tile_indexes { + let tile_idx = get_tile_idx( + TileCoord { + z: zoom, + x: col, + y: row, + }, + across_down.0, + across_down.1, + ) + .unwrap(); + // 我可以求出这个tile的左上角地理坐标,那么我可以求出这个左上角坐标相当于 output_image里的像素坐标吗? 然后根据这个offset把所有的像素逐一绘制到output_image里? + let origin_x = cog_origin[0]; + let origin_y = cog_origin[1]; + let tile_min_x = origin_x + f64::from(col * cog_tile_size.0) * res_x; + let tile_max_y = origin_y - f64::from(row * cog_tile_size.1) * res_y; + + let offset_x_geo = tile_min_x - window[0]; + let offset_y_geo = window[3] - tile_max_y; // Use window's max Y + + let offset_x_pixel = (offset_x_geo / res_x).round() as i64; + let offset_y_pixel = (offset_y_geo / res_y).round() as i64; + + let (data_width, data_height) = decoder.chunk_data_dimensions(tile_idx); + let decoded_result = decoder.read_chunk(tile_idx).unwrap(); + let color_type = decoder.colortype().unwrap(); + for y_tile in 0..data_height { + for x_tile in 0..data_width { + let target_x = offset_x_pixel + i64::from(x_tile); + let target_y = offset_y_pixel + i64::from(y_tile); + match (color_type, &decoded_result) { + (tiff::ColorType::RGB(_), DecodingResult::U8(data)) => { + let idx = (y_tile * data_width + x_tile) * 3; + let r = data[idx as usize]; + let g = data[idx as usize + 1]; + let b = data[idx as usize + 2]; + if target_x >= 0 + && target_y >= 0 + && (target_x as u32) < window_width_pixel + && (target_y as u32) < window_height_pixel + { + output_image.put_pixel( + target_x as u32, + target_y as u32, + Rgba([r, g, b, 255]), + ); + } + } + (tiff::ColorType::RGBA(_), DecodingResult::U8(data)) => { + let idx = (y_tile * data_width + x_tile) * 4; + let r = data[idx as usize]; + let g = data[idx as usize + 1]; + let b = data[idx as usize + 2]; + let a = data[idx as usize + 3]; + if target_x >= 0 + && target_y >= 0 + && (target_x as u32) < window_width_pixel + && (target_y as u32) < window_height_pixel + { + output_image.put_pixel( + target_x as u32, + target_y as u32, + Rgba([r, g, b, a]), + ); + } + } + // Handle other color types or decoding results if necessary, or log a warning/error + _ => { + // Currently unsupported color type or bit depth for sub_region rendering + // Consider logging a warning or returning an error + } + }; + } + } + } + // Resize the image to the requested output_size + let resized_image = image::imageops::resize( + &output_image, + output_size, + output_size, + image::imageops::FilterType::Nearest, //todo should be a configure option + ); + // Encode the resized image to PNG format + let mut png_buffer = Vec::new(); + { + let mut encoder = png::Encoder::new( + BufWriter::new(&mut png_buffer), + output_size, // Use output_size for the encoder dimensions + output_size, // Use output_size for the encoder dimensions + ); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + let mut writer = encoder.write_header().map_err(|e| { + // Reusing existing CogError variants for PNG writing errors + CogError::WritePngHeaderFailed(self.path.clone(), e) + })?; + writer + .write_image_data(resized_image.as_raw()) // Write the resized image data + .map_err(|e| CogError::WriteToPngFailed(self.path.clone(), e))?; + } + + Ok(png_buffer) // Return the encoded PNG bytes } #[allow(clippy::cast_sign_loss)] @@ -759,7 +852,9 @@ fn meta_to_tilejson(meta: &Meta) -> TileJSON { .insert("custom_grid".to_string(), serde_json::json!(cog_info)); tilejson } - +fn read_tile_to_rgba(decoder: &mut Decoder, tile_idx: u32) -> Vec { + todo!() +} #[cfg(test)] mod tests { use insta::{assert_yaml_snapshot, Settings}; @@ -1034,7 +1129,9 @@ mod tests { let source = super::CogSource::new("test".to_string(), path).unwrap(); let window = [1620847.0, 4276072.0, 1621379.0, 4276545.0]; - let result = source.sub_region(2, window, 512); + let tif_file = File::open("../tests/fixtures/cog/google_compatible.tif").unwrap(); + let mut decoder = Decoder::new(tif_file).unwrap(); + let result = source.sub_region(&mut decoder, 2, window, 512); todo!() } } From 0092acd9674e009bed756116dda5c07e9cd01dfc Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Sun, 27 Apr 2025 16:20:12 +0800 Subject: [PATCH 46/58] add cog config option:force google --- martin/src/cog/config.rs | 4 ++- martin/src/cog/source.rs | 78 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/martin/src/cog/config.rs b/martin/src/cog/config.rs index 07dfc07fc..7cf8809d8 100644 --- a/martin/src/cog/config.rs +++ b/martin/src/cog/config.rs @@ -13,6 +13,8 @@ use crate::Source; pub struct CogConfig { #[serde(flatten)] pub unrecognized: UnrecognizedValues, + + pub force_google: Option, } impl ConfigExtras for CogConfig { @@ -23,7 +25,7 @@ impl ConfigExtras for CogConfig { impl SourceConfigExtras for CogConfig { async fn new_sources(&self, id: String, path: PathBuf) -> FileResult> { - let cog = CogSource::new(id, path)?; + let cog = CogSource::new(id, path, self.force_google.unwrap_or(false))?; Ok(Box::new(cog)) } diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index e0e23c6e3..1981ddc6a 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -10,14 +10,16 @@ use image::{ImageBuffer, Rgba}; use async_trait::async_trait; use log::warn; use martin_tile_utils::{Format, TileCoord, TileInfo}; +use regex::Regex; use serde::Serialize; use tiff::decoder::{ChunkType, Decoder, DecodingResult}; use tiff::tags::Tag::{self, GdalNodata}; +use tiff::TiffResult; use tilejson::{tilejson, TileJSON}; use super::CogError; use crate::file_config::{FileError, FileResult}; -use crate::{MartinResult, Source, TileData, UrlQuery}; +use crate::{utils, MartinResult, Source, TileData, UrlQuery}; // about the model space of tiff image. // pixel scale, tie points and transformations @@ -28,6 +30,7 @@ type ModelInfo = (Option>, Option>, Option>); struct Meta { min_zoom: u8, max_zoom: u8, + google_zoom: Option<(u8, u8)>, origin: [f64; 3], extent: [f64; 4], zoom_and_resolutions: HashMap, @@ -44,10 +47,11 @@ pub struct CogSource { meta: Meta, tilejson: TileJSON, tileinfo: TileInfo, + force_google: bool, } impl CogSource { - pub fn new(id: String, path: PathBuf) -> FileResult { + pub fn new(id: String, path: PathBuf, force_google: bool) -> FileResult { let tileinfo = TileInfo::new(Format::Png, martin_tile_utils::Encoding::Uncompressed); let meta = get_meta(&path)?; @@ -59,6 +63,7 @@ impl CogSource { meta, tilejson, tileinfo, + force_google, }) } @@ -200,6 +205,27 @@ impl CogSource { if xyz.z < self.meta.min_zoom || xyz.z > self.meta.max_zoom { return Ok(Vec::new()); } + + if self.force_google { + if let Some(google_zoom) = self.meta.google_zoom { + let google_min_zoom = google_zoom.0; + let internal_zoom = self.meta.min_zoom + (xyz.z - google_min_zoom) as u8; + if internal_zoom < self.meta.min_zoom || internal_zoom > self.meta.max_zoom { + return Ok(Vec::new()); + } + let bbox = martin_tile_utils::xyz_to_bbox(xyz.z, xyz.x, xyz.y, xyz.x, xyz.y); + let tif_file = + File::open(&self.path).map_err(|e| FileError::IoError(e, self.path.clone()))?; + let mut decoder = Decoder::new(tif_file) + .map_err(|e| CogError::InvalidTiffFile(e, self.path.clone()))?; + decoder = decoder.with_limits(tiff::decoder::Limits::unlimited()); + + let png_bytes = self.sub_region(&mut decoder, internal_zoom, bbox, 512)?; + return Ok(png_bytes); + } else { + return Ok(Vec::new()); + } + } let tif_file = File::open(&self.path).map_err(|e| FileError::IoError(e, self.path.clone()))?; let mut decoder = @@ -530,6 +556,8 @@ fn get_meta(path: &PathBuf) -> Result { .map_err(|e| CogError::InvalidTiffFile(e, path.clone()))? .with_limits(tiff::decoder::Limits::unlimited()); + let gdal_metadata = decoder.get_tag_ascii_string(Tag::Unknown(42112)); + let model_info = get_model_infos(&mut decoder, path); verify_requirments(&mut decoder, &model_info, path)?; let tile_size = decoder.chunk_dimensions(); @@ -606,10 +634,15 @@ fn get_meta(path: &PathBuf) -> Result { if images_ifd.is_empty() { Err(CogError::NoImagesFound(path.clone()))?; } + let min_zoom = 0; + let max_zoom = images_ifd.len() as u8 - 1; + + let google_zoom_range = to_google_zoom_range(min_zoom, max_zoom, gdal_metadata); Ok(Meta { min_zoom: 0, - max_zoom: images_ifd.len() as u8 - 1, + max_zoom, + google_zoom: google_zoom_range, zoom_and_resolutions: resolutions, tile_size, extent, @@ -852,9 +885,42 @@ fn meta_to_tilejson(meta: &Meta) -> TileJSON { .insert("custom_grid".to_string(), serde_json::json!(cog_info)); tilejson } -fn read_tile_to_rgba(decoder: &mut Decoder, tile_idx: u32) -> Vec { - todo!() + +fn to_google_zoom_range( + actual_min: u8, + actual_max: u8, + gdal_metadata: TiffResult, +) -> Option<(u8, u8)> { + let mut result = None; + if let Ok(gdal_metadata) = gdal_metadata { + let re_name = Regex::new(r#"([^<]+)"#); + let re_zoom = + Regex::new(r#"([^<]+)"#); + + let mut tiling_schema = None; + if let Ok(re_name) = re_name { + if let Some(caps) = re_name.captures(&gdal_metadata) { + tiling_schema = Some(caps[1].to_string()); + } + }; + + let mut zoom_level: Option = None; + if let Ok(re_zoom) = re_zoom { + if let Some(caps) = re_zoom.captures(&gdal_metadata) { + zoom_level = caps[1].parse().ok(); + } + }; + + if let Some(zoom) = zoom_level { + if tiling_schema == Some("GoogleMapsCompatible".to_string()) { + let google_min = zoom - actual_max + actual_min; + result = Some((google_min, zoom)); + } + } + } + result } + #[cfg(test)] mod tests { use insta::{assert_yaml_snapshot, Settings}; @@ -1127,7 +1193,7 @@ mod tests { fn can_trans_to_google() { let path = PathBuf::from("../tests/fixtures/cog/google_compatible.tif"); - let source = super::CogSource::new("test".to_string(), path).unwrap(); + let source = super::CogSource::new("test".to_string(), path, true).unwrap(); let window = [1620847.0, 4276072.0, 1621379.0, 4276545.0]; let tif_file = File::open("../tests/fixtures/cog/google_compatible.tif").unwrap(); let mut decoder = Decoder::new(tif_file).unwrap(); From 2d2352ed9a201cd102ab54a024ea24b1c8d34dbf Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Tue, 29 Apr 2025 14:31:29 +0800 Subject: [PATCH 47/58] refactor --- martin/src/cog/source.rs | 105 +++++++++++++++++++++++++++++++-------- 1 file changed, 83 insertions(+), 22 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 29668b47c..f4c29d6b0 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -86,6 +86,7 @@ impl CogSource { let cog_extent = self.meta.extent; let cog_tile_size = self.meta.tile_size; let across_down = self.meta.zoom_and_tile_across_down.get(&zoom).unwrap(); + let no_data = self.meta.nodata; let tile_indexes: Vec<(u32, u32)> = get_covered_tile_indexes(window, cog_extent, *across_down, cog_tile_size, resolution); @@ -105,7 +106,6 @@ impl CogSource { across_down.1, ) .unwrap(); - // 我可以求出这个tile的左上角地理坐标,那么我可以求出这个左上角坐标相当于 output_image里的像素坐标吗? 然后根据这个offset把所有的像素逐一绘制到output_image里? let origin_x = cog_origin[0]; let origin_y = cog_origin[1]; let tile_min_x = origin_x + f64::from(col * cog_tile_size.0) * res_x; @@ -124,23 +124,32 @@ impl CogSource { for x_tile in 0..data_width { let target_x = offset_x_pixel + i64::from(x_tile); let target_y = offset_y_pixel + i64::from(y_tile); + + if target_x < 0 + || target_y < 0 + || target_x >= window_width_pixel as i64 + || target_y >= window_height_pixel as i64 + { + continue; + } + match (color_type, &decoded_result) { (tiff::ColorType::RGB(_), DecodingResult::U8(data)) => { let idx = (y_tile * data_width + x_tile) * 3; let r = data[idx as usize]; let g = data[idx as usize + 1]; let b = data[idx as usize + 2]; - if target_x >= 0 - && target_y >= 0 - && (target_x as u32) < window_width_pixel - && (target_y as u32) < window_height_pixel - { - output_image.put_pixel( - target_x as u32, - target_y as u32, - Rgba([r, g, b, 255]), - ); + if let Some(nodata) = no_data { + if r == nodata as u8 || g == nodata as u8 || b == nodata as u8 { + continue; + } } + + output_image.put_pixel( + target_x as u32, + target_y as u32, + Rgba([r, g, b, 255]), + ); } (tiff::ColorType::RGBA(_), DecodingResult::U8(data)) => { let idx = (y_tile * data_width + x_tile) * 4; @@ -148,17 +157,16 @@ impl CogSource { let g = data[idx as usize + 1]; let b = data[idx as usize + 2]; let a = data[idx as usize + 3]; - if target_x >= 0 - && target_y >= 0 - && (target_x as u32) < window_width_pixel - && (target_y as u32) < window_height_pixel - { - output_image.put_pixel( - target_x as u32, - target_y as u32, - Rgba([r, g, b, a]), - ); + if let Some(nodata) = no_data { + if r == nodata as u8 || g == nodata as u8 || b == nodata as u8 { + continue; + } } + output_image.put_pixel( + target_x as u32, + target_y as u32, + Rgba([r, g, b, a]), + ); } // Handle other color types or decoding results if necessary, or log a warning/error _ => { @@ -924,7 +932,7 @@ fn to_google_zoom_range( #[cfg(test)] mod tests { use insta::{Settings, assert_yaml_snapshot}; - use martin_tile_utils::TileCoord; + use martin_tile_utils::{TileCoord, xyz_to_bbox}; use rstest::rstest; use std::{fs::File, path::PathBuf}; use tiff::decoder::Decoder; @@ -932,6 +940,8 @@ mod tests { use crate::cog::source::{get_full_resolution, get_tile_idx}; use approx::assert_abs_diff_eq; + use super::{Meta, get_covered_tile_indexes, get_meta}; + #[test] fn can_calc_tile_idx() { assert_eq!(Some(0), get_tile_idx(TileCoord { z: 0, x: 0, y: 0 }, 3, 3)); @@ -1200,4 +1210,55 @@ mod tests { let result = source.sub_region(&mut decoder, 2, window, 512); todo!() } + + #[test] + fn can_get_covered_tiles() { + let path = PathBuf::from("../tests/fixtures/cog/google_compatible.tif"); + let meta = get_meta(&path).unwrap(); + + let extent = meta.extent; + let tile_size = meta.tile_size; + for zoom in 0..=meta.max_zoom { + let (tile_across, tile_down) = meta.zoom_and_tile_across_down[&zoom]; + let resolution = meta.zoom_and_resolutions[&zoom]; + + for across in 0..tile_across { + for down in 0..tile_down { + let window = calculate_tile_window(&meta, zoom, across, down); + + let idx = get_covered_tile_indexes( + window, + extent, + (tile_across, tile_down), + tile_size, + &resolution, + ); + assert_eq!(1, idx.len()); + assert_eq!(across, idx[0].0); + assert_eq!(down, idx[0].1); + } + } + } + } + + // Helper function to calculate the geographic window of a tile + fn calculate_tile_window(meta: &Meta, zoom: u8, across: u32, down: u32) -> [f64; 4] { + let resolution = meta.zoom_and_resolutions[&zoom]; + let tile_size = meta.tile_size; + + let res_x = resolution[0]; + // Resolution Y is typically negative + let res_y = resolution[1]; + + let tile_width_geo = f64::from(tile_size.0) * res_x; + let tile_height_geo = f64::from(tile_size.1) * res_y; // This will be negative + + let min_x = meta.origin[0] + f64::from(across) * tile_width_geo; + let max_y = meta.origin[1] + f64::from(down) * tile_height_geo; // Top Y coordinate + let max_x = min_x + tile_width_geo; + let min_y = max_y + tile_height_geo; // Bottom Y coordinate + + // The window represents [min_x, min_y, max_x, max_y] + [min_x, min_y, max_x, max_y] + } } From 531f8fdd453be305a36c889bafa8387a7d436c27 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Wed, 30 Apr 2025 17:19:22 +0800 Subject: [PATCH 48/58] update test --- martin/src/cog/source.rs | 10 ++++++---- tests/fixtures/cog/expected/sub_region.png | Bin 0 -> 69623 bytes 2 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 tests/fixtures/cog/expected/sub_region.png diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index f4c29d6b0..b7a2153cf 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::fmt::Debug; use std::fs::File; -use std::io::BufWriter; +use std::io::{BufWriter, Write}; use std::path::{Path, PathBuf}; use std::vec; @@ -934,7 +934,7 @@ mod tests { use insta::{Settings, assert_yaml_snapshot}; use martin_tile_utils::{TileCoord, xyz_to_bbox}; use rstest::rstest; - use std::{fs::File, path::PathBuf}; + use std::{fs::File, io::Write, path::PathBuf}; use tiff::decoder::Decoder; use crate::cog::source::{get_full_resolution, get_tile_idx}; @@ -1207,8 +1207,10 @@ mod tests { let window = [1620847.0, 4276072.0, 1621379.0, 4276545.0]; let tif_file = File::open("../tests/fixtures/cog/google_compatible.tif").unwrap(); let mut decoder = Decoder::new(tif_file).unwrap(); - let result = source.sub_region(&mut decoder, 2, window, 512); - todo!() + let result = source.sub_region(&mut decoder, 2, window, 512).unwrap(); + let expected_bytes = + std::fs::read("../tests/fixtures/cog/expected/sub_region.png").unwrap(); + assert_eq!(result, expected_bytes); } #[test] diff --git a/tests/fixtures/cog/expected/sub_region.png b/tests/fixtures/cog/expected/sub_region.png new file mode 100644 index 0000000000000000000000000000000000000000..998b06a7887f4a5ab1f5fc8acdaff5bd24ab8c60 GIT binary patch literal 69623 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 literal 0 HcmV?d00001 From 4c24877f08da199802fbc233adb06cbc07a01bc0 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Wed, 30 Apr 2025 18:24:26 +0800 Subject: [PATCH 49/58] make force_google default --- martin/src/cog/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/martin/src/cog/config.rs b/martin/src/cog/config.rs index 9f0d763bb..dab5be202 100644 --- a/martin/src/cog/config.rs +++ b/martin/src/cog/config.rs @@ -25,7 +25,7 @@ impl ConfigExtras for CogConfig { impl SourceConfigExtras for CogConfig { async fn new_sources(&self, id: String, path: PathBuf) -> FileResult> { - let cog = CogSource::new(id, path, self.force_google.unwrap_or(false))?; + let cog = CogSource::new(id, path, self.force_google.unwrap_or(true))?; Ok(Box::new(cog)) } From d55fc6ce50507a8f0dff3eccd4f256b58534897b Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Wed, 30 Apr 2025 18:24:39 +0800 Subject: [PATCH 50/58] update tilejson --- martin/src/cog/source.rs | 69 ++++++++++++---------------------------- 1 file changed, 20 insertions(+), 49 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index b7a2153cf..c844adc0b 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -210,9 +210,7 @@ impl CogSource { #[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines)] pub fn get_tile(&self, xyz: TileCoord) -> MartinResult { - if xyz.z < self.meta.min_zoom || xyz.z > self.meta.max_zoom { - return Ok(Vec::new()); - } + if self.force_google { if let Some(google_zoom) = self.meta.google_zoom { @@ -234,6 +232,11 @@ impl CogSource { return Ok(Vec::new()); } } + + if xyz.z < self.meta.min_zoom || xyz.z > self.meta.max_zoom { + return Ok(Vec::new()); + } + let tif_file = File::open(&self.path).map_err(|e| FileError::IoError(e, self.path.clone()))?; let mut decoder = @@ -843,54 +846,21 @@ fn get_origin( } fn meta_to_tilejson(meta: &Meta) -> TileJSON { - let mut tilejson = tilejson! { + let min_zoom; + let max_zoom; + if let Some(google_zoom) = meta.google_zoom { + min_zoom = google_zoom.0; + max_zoom = google_zoom.1; + } else { + min_zoom = meta.min_zoom; + max_zoom = meta.max_zoom; + } + + let tilejson = tilejson! { tiles: vec![], - minzoom: meta.min_zoom, - maxzoom: meta.max_zoom + minzoom: min_zoom, + maxzoom:max_zoom, }; - - let mut cog_info = serde_json::Map::new(); - - cog_info.insert( - "minZoom".to_string(), - serde_json::Value::from(meta.min_zoom), - ); - - cog_info.insert( - "maxZoom".to_string(), - serde_json::Value::from(meta.max_zoom), - ); - - let mut resolutions_map = Vec::new(); - for value in meta.zoom_and_resolutions.values() { - resolutions_map.push(value[0]); - } - - resolutions_map.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); - - cog_info.insert( - "tileSize".to_string(), - serde_json::Value::from([meta.tile_size.0, meta.tile_size.1]), - ); - - cog_info.insert( - "resolutions".to_string(), - serde_json::Value::from(resolutions_map), - ); - - cog_info.insert( - "origin".to_string(), - serde_json::Value::from([meta.origin[0], meta.origin[1]]), - ); - - cog_info.insert( - "extent".to_string(), - serde_json::Value::from(meta.extent.to_vec()), - ); - - tilejson - .other - .insert("custom_grid".to_string(), serde_json::json!(cog_info)); tilejson } @@ -1147,6 +1117,7 @@ mod tests { insta::assert_yaml_snapshot!(meta,@r###" min_zoom: 0 max_zoom: 3 + google_zoom: ~ origin: - 1620750.2508 - 4277012.7153 From c61bacaa54f152d1c810d57f16113e2a2dc5898b Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Wed, 30 Apr 2025 18:24:47 +0800 Subject: [PATCH 51/58] update test --- tests/expected/auto/catalog_auto.json | 3 +++ tests/expected/auto/save_config.yaml | 2 ++ tests/expected/configured/save_config.yaml | 1 + tests/test.sh | 4 ++++ 4 files changed, 10 insertions(+) diff --git a/tests/expected/auto/catalog_auto.json b/tests/expected/auto/catalog_auto.json index 0a72a7bce..88a7845bd 100644 --- a/tests/expected/auto/catalog_auto.json +++ b/tests/expected/auto/catalog_auto.json @@ -128,6 +128,9 @@ "description": "One of the example maps that comes with TileMill - a bright & colorful world map that blends retro and high-tech with its folded paper texture and interactive flag tooltips. ", "name": "Geography Class" }, + "google_compatible": { + "content_type": "image/png" + }, "json": { "content_type": "application/json", "name": "Dummy json data" diff --git a/tests/expected/auto/save_config.yaml b/tests/expected/auto/save_config.yaml index 3aeae2d22..cc0df7fee 100644 --- a/tests/expected/auto/save_config.yaml +++ b/tests/expected/auto/save_config.yaml @@ -259,9 +259,11 @@ cog: - tests/fixtures/pmtiles - tests/fixtures/cog sources: + google_compatible: tests/fixtures/cog/google_compatible.tif rgb_u8: tests/fixtures/cog/rgb_u8.tif rgba_u8: tests/fixtures/cog/rgba_u8.tif rgba_u8_nodata: tests/fixtures/cog/rgba_u8_nodata.tiff + force_google: null sprites: tests/fixtures/sprites/src1 styles: - tests/fixtures/styles/maplibre_demo.json diff --git a/tests/expected/configured/save_config.yaml b/tests/expected/configured/save_config.yaml index 348874368..568ace1d0 100644 --- a/tests/expected/configured/save_config.yaml +++ b/tests/expected/configured/save_config.yaml @@ -171,6 +171,7 @@ cog: cog-src1: tests/fixtures/cog/rgba_u8.tif cog-src2: tests/fixtures/cog/rgb_u8.tif rgba_u8_nodata: tests/fixtures/cog/rgba_u8_nodata.tiff + force_google: null sprites: paths: tests/fixtures/sprites/src1 sources: diff --git a/tests/test.sh b/tests/test.sh index 8f97d5e32..784b818fc 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -335,6 +335,10 @@ test_jsn rgba_u8_nodata rgba_u8_nodata test_png rgba_u8_nodata_0_0_0 rgba_u8_nodata/0/0/0 test_png rgba_u8_nodata_1_0_0 rgba_u8_nodata/1/0/0 +test_jsn google_compatible google_compatible +test_png google_compatible_14_8854_6443 google_compatible/14/8854/6443 +test_png google_compatible_14_8855_6444 google_compatible/14/8855/6444 + >&2 echo "***** Test server response for table source with empty SRID *****" test_pbf points_empty_srid_0_0_0 points_empty_srid/0/0/0 From d352ec957cecb6896323b7d752de1af15117951c Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Wed, 30 Apr 2025 18:24:56 +0800 Subject: [PATCH 52/58] fmt --- martin/src/cog/source.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index c844adc0b..388ae12d3 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -210,8 +210,6 @@ impl CogSource { #[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines)] pub fn get_tile(&self, xyz: TileCoord) -> MartinResult { - - if self.force_google { if let Some(google_zoom) = self.meta.google_zoom { let google_min_zoom = google_zoom.0; @@ -855,7 +853,7 @@ fn meta_to_tilejson(meta: &Meta) -> TileJSON { min_zoom = meta.min_zoom; max_zoom = meta.max_zoom; } - + let tilejson = tilejson! { tiles: vec![], minzoom: min_zoom, From 3fcfb809fabbe7eda1a26e84c79433eb2ec69568 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Wed, 30 Apr 2025 19:02:48 +0800 Subject: [PATCH 53/58] bug fix: fill the gap between tiles --- martin/src/cog/source.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 388ae12d3..21cc944d9 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -79,8 +79,8 @@ impl CogSource { let resolution = self.meta.zoom_and_resolutions.get(&zoom).unwrap(); let res_x = resolution[0]; let res_y = resolution[1].abs(); - let window_width_pixel = ((window[2] - window[0]) / res_x).ceil() as u32; - let window_height_pixel = ((window[3] - window[1]) / res_y).ceil() as u32; + let window_width_pixel = ((window[2] - window[0]) / res_x).round() as u32; + let window_height_pixel = ((window[3] - window[1]) / res_y).round() as u32; let cog_origin = self.meta.origin; let cog_extent = self.meta.extent; From 7b94dcd316d939c5962ab9a0b9f0e6a77c759e34 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Wed, 30 Apr 2025 19:03:11 +0800 Subject: [PATCH 54/58] fix the bbox method --- martin-tile-utils/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/martin-tile-utils/src/lib.rs b/martin-tile-utils/src/lib.rs index 71878dff6..3637e3576 100644 --- a/martin-tile-utils/src/lib.rs +++ b/martin-tile-utils/src/lib.rs @@ -251,7 +251,7 @@ pub fn xyz_to_bbox(zoom: u8, min_x: u32, min_y: u32, max_x: u32, max_y: u32) -> let (min_lng, min_lat) = webmercator_to_wgs84(left_down_bbox[0], left_down_bbox[1]); let (max_lng, max_lat) = webmercator_to_wgs84(right_top_bbox[2], right_top_bbox[3]); - [min_lng, min_lat, max_lng, max_lat] + [left_down_bbox[0], left_down_bbox[1], right_top_bbox[2], right_top_bbox[3]] } #[allow(clippy::cast_lossless)] From 48b09a6bad76f82637175063b319843db72d3227 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Wed, 30 Apr 2025 19:27:29 +0800 Subject: [PATCH 55/58] fix xyz_to_bbox bug --- martin-tile-utils/src/lib.rs | 20 ++++++++++++++++++++ martin/src/cog/source.rs | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/martin-tile-utils/src/lib.rs b/martin-tile-utils/src/lib.rs index 3637e3576..54dadacea 100644 --- a/martin-tile-utils/src/lib.rs +++ b/martin-tile-utils/src/lib.rs @@ -251,9 +251,29 @@ pub fn xyz_to_bbox(zoom: u8, min_x: u32, min_y: u32, max_x: u32, max_y: u32) -> let (min_lng, min_lat) = webmercator_to_wgs84(left_down_bbox[0], left_down_bbox[1]); let (max_lng, max_lat) = webmercator_to_wgs84(right_top_bbox[2], right_top_bbox[3]); + [min_lng, min_lat, max_lng, max_lat] +} + + +/// Convert min/max XYZ tile coordinates to a bounding box values. +/// +/// The result is `[min_lng, min_lat, max_lng, max_lat]` +/// +/// # Panics +/// Panics if `zoom` is greater than [`MAX_ZOOM`]. +#[must_use] +pub fn xyz_to_bbox_webmercator(zoom: u8, min_x: u32, min_y: u32, max_x: u32, max_y: u32) -> [f64; 4] { + assert!(zoom <= MAX_ZOOM, "zoom {zoom} must be <= {MAX_ZOOM}"); + + let tile_length = EARTH_CIRCUMFERENCE / f64::from(1_u32 << zoom); + + let left_down_bbox = tile_bbox(min_x, max_y, tile_length); + let right_top_bbox = tile_bbox(max_x, min_y, tile_length); + [left_down_bbox[0], left_down_bbox[1], right_top_bbox[2], right_top_bbox[3]] } + #[allow(clippy::cast_lossless)] fn tile_bbox(x: u32, y: u32, tile_length: f64) -> [f64; 4] { let min_x = EARTH_CIRCUMFERENCE * -0.5 + x as f64 * tile_length; diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 21cc944d9..1a54e3d98 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -217,7 +217,7 @@ impl CogSource { if internal_zoom < self.meta.min_zoom || internal_zoom > self.meta.max_zoom { return Ok(Vec::new()); } - let bbox = martin_tile_utils::xyz_to_bbox(xyz.z, xyz.x, xyz.y, xyz.x, xyz.y); + let bbox = martin_tile_utils::xyz_to_bbox_webmercator(xyz.z, xyz.x, xyz.y, xyz.x, xyz.y); let tif_file = File::open(&self.path).map_err(|e| FileError::IoError(e, self.path.clone()))?; let mut decoder = Decoder::new(tif_file) From c9631ebe3366cafd7d3fb3e9f459c94fc7a2d9bf Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Wed, 30 Apr 2025 21:21:05 +0800 Subject: [PATCH 56/58] set the option correctly --- martin/src/cog/source.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 1a54e3d98..6c9d43393 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -56,14 +56,17 @@ impl CogSource { let meta = get_meta(&path)?; let tilejson: TileJSON = meta_to_tilejson(&meta); - + let mut google_compatible = false; + if force_google == true && meta.google_zoom.is_some() { + google_compatible = true; + } Ok(CogSource { id, path, meta, tilejson, tileinfo, - force_google, + force_google: google_compatible, }) } From b7ed868d969db7e014760603889411d19df08a57 Mon Sep 17 00:00:00 2001 From: sharkAndshark Date: Wed, 30 Apr 2025 21:21:12 +0800 Subject: [PATCH 57/58] just bless --- tests/expected/auto/google_compatible.json | 8 ++++++ .../auto/google_compatible_14_8854_6443.png | Bin 0 -> 46383 bytes .../google_compatible_14_8854_6443.png.txt | 1 + .../auto/google_compatible_14_8855_6444.png | Bin 0 -> 26987 bytes .../google_compatible_14_8855_6444.png.txt | 1 + tests/expected/auto/rgb_u8.json | 24 ------------------ tests/expected/auto/rgba_u8.json | 24 ------------------ tests/expected/auto/rgba_u8_nodata.json | 23 ----------------- 8 files changed, 10 insertions(+), 71 deletions(-) create mode 100644 tests/expected/auto/google_compatible.json create mode 100644 tests/expected/auto/google_compatible_14_8854_6443.png create mode 100644 tests/expected/auto/google_compatible_14_8854_6443.png.txt create mode 100644 tests/expected/auto/google_compatible_14_8855_6444.png create mode 100644 tests/expected/auto/google_compatible_14_8855_6444.png.txt diff --git a/tests/expected/auto/google_compatible.json b/tests/expected/auto/google_compatible.json new file mode 100644 index 000000000..542ba9f85 --- /dev/null +++ b/tests/expected/auto/google_compatible.json @@ -0,0 +1,8 @@ +{ + "maxzoom": 14, + "minzoom": 12, + "tilejson": "3.0.0", + "tiles": [ + "http://localhost:3111/google_compatible/{z}/{x}/{y}" + ] +} diff --git a/tests/expected/auto/google_compatible_14_8854_6443.png b/tests/expected/auto/google_compatible_14_8854_6443.png new file mode 100644 index 0000000000000000000000000000000000000000..4ccca428e1bbf1f4e754ae01c80ece63180853b7 GIT binary patch literal 46383 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 literal 0 HcmV?d00001 diff --git a/tests/expected/auto/google_compatible_14_8854_6443.png.txt b/tests/expected/auto/google_compatible_14_8854_6443.png.txt new file mode 100644 index 000000000..2a0719c13 --- /dev/null +++ b/tests/expected/auto/google_compatible_14_8854_6443.png.txt @@ -0,0 +1 @@ +tests/output/auto/google_compatible_14_8854_6443.png: PNG image data, 512 x 512, 8-bit/color RGBA, non-interlaced diff --git a/tests/expected/auto/google_compatible_14_8855_6444.png b/tests/expected/auto/google_compatible_14_8855_6444.png new file mode 100644 index 0000000000000000000000000000000000000000..3f46a7325cf86c85161dfe0e112897b9b1f7e1f3 GIT binary patch literal 26987 zcmchAc~}$o*8iDF2wS4azPLn1M8Odv;$EjJuBfyM7#(d?20>91;%-}~S~uJfln_yj zw&I2qQ?&`z8WpG*tXP$bOB(^fy0%xXx3vwqI! z9M{K<8zFHEbOQj%sFB0o2O#l3NpKP2zn(3*`Vt^jHfq?r6PFN=Oafi7Jg5BErp1pp zov+-VK4xao+?ywC|4ZGNpELT4d|9tfkAGkE)$C;jYX{7^Hh%ES#pC-l3Cfb7-~uZV z#2NM`5SBm)@96`-J#m2yH+bbLi2G^;MEgoOe~D=ulh8m)--p<>OoP^V|EKZMFx%Bs z)gRWJa-$Q7hyhn1_iI(nR>D7-Nif_$bhxPG{$O}{`SkLV-#dhc2qNOCIsV*PpW|-u z11OdrgyIz=AZ{Yu8}HAR3?hq?SWm=_fi=}c#KU;d zyKyyIk?GbZ^JcLrqE3G{K5^>8Yw=A))t1+{uUuRatXeIkuQSz#y#b(JodL=_`eCr` zC}BAQrkP+F$1K_!mdyB$hR;RZQ0QA0-}Gu5Dc$d8>Mo(b)|gAy`g5CwbSi{X6a1mK z>e`#npAKr?!avcaEnx~KvZ2u ze%LbV>3N^E6|AQwzUXBKQ>{L=f_>CUQ#RCC_(8fHzkY^2QkU_GdGsCI*9ZTfe7&MU zUjd&R!uu0({fUTHq{8S28%LaBKZENfOC|IuD%*)xuI^8bQORe3%otwD4q=}E^u<1y zG%XyKT!O13)HM%H@Tr7O5M=hEt|%a*)TTxA?u}@A{By(Ltcz?mF;XR8Kw9kjr91+&PQeLMEqHUkXX-@?7($DC-}LA&O5uLC0OkcpexHJ}GXWr3AK#WND zopDAK^bm8a%=b;K+}Lb1e`@{e-cN&W|LL41gr+E8%3Tnb1owA_4Hs0Mg}#QuR!q)z+X=n0 zDHk|tjw~XzFB9lJ~wrveN#Gjnx)%l+W--hdOo+6BOEs}U%HSrDg` zCkQODFoT7??r>u-WUhh({Rm5UYKmIBj8dm-myVMq^1_El`{%1G5>CQZvTlUOz-(i< zvC}GC1RAbYkXub~8AK?GZnB83)yE1fhlkrU)BFK=pr0R*Y1D_Qjv=b zSVfwaNFhlOaWf4n7YABDd_;AvsUsz#nLY7fv1DkCu}t)4CvMf(-8GNYH3$8s5SiJw z12`>tS#fr20MS#LriG)4G8wX+lVD-GvGiW4=E`y>ix&xgr%hnj{mV$Lw+FqqEx z^g@rD*47=I7spdK8}$=6b80#|DhXra@m$JUFN@zM$c7gpvWVw2-8j`%;L@J4|zIu1?^FCh% zDr#K0y+V4EIejXeXQ9eNb5TO?>H<@vpp(Re_O!`EtsPL5YuKxjk7SaGh>_5iDP&H} zhjD|_By_Z6apzV%7YiQ#G?ln_VmKt!`zv+?CWb=Zd*LLH$Ov-XF#?35iTW2oy-DDV^a%6B~5XmAhyYnUAVoyt+GA)OX2b zXVZtXNX4RMq|{d=%^<2$V4xd}VSEX$)F&Fh*Jc7_w$mIRN%hiPTW{X0()x)^*WllA zOakwj9{pFp)%x|dUgC(&KHSGSuq}&e5SlJBA(Z;A3~fnAh^x~_kD+=czUts1L}PHg z8xc1Pyh+n4Lt!8n=*s0V(F@Pm_V}pkzxk~#e?Gq=j_cDMv?A$#p>&6#P^DdE9#|(^ zZ;rylCZz+^*P7bJ5cCaL5(`(=^7p|X?e{j~9tP-?gkHnzK)aV)Pk+3FxIf%UdIjpY z!7wjPZ%VDy&O&pi6$x~sAZb?7%hR8Q!pI=l;>ss9HBU^L)kH3`5#QG_}j9ITJv+B+*GRn^ZhK z;|0f~VMcv(bky|my%Db)4vOxz1Mw!59qxNqj3N3EiXhlvZi}kjaRZ8JTpP`ir6Fqh zC3dbGw0d#~V#h+-J4;AiwTRwmvl%w;I55*{MSOD21^qM&za}_eQg^LhU(WjN##Eu5 zHxzwT(}sKI!iDgfGN3s;qifyN*WwjaUISD8NCcBzxP)wTpl0faD5eYI!0;P80>1AI zI~=0-Cbfu9eC57FOdo{hAXii^okB|)`ov-TDj8`7BUILiOf^P zfsRlvhJw{_eNr@(cUISQ!eF@BaQ@`{CPctLokmfi+iAAH2TAi`whI?2;8Ht6bdH4k z^*nRJ1qIofAtkE1C4Ia&{Qa}V91b{%oKRTWLK+E*b2ukuwBaRa&6!A=s>QU zAw0`m5TNPi#3hjED`y9z!IqEu{|^=y-;*=W@MWU^6&nwQn@d?Q&Be2D-Id;@mWS2q z`3UfjJsn#RehVqq`_|7pG=(>M%S%#+(_#`61Y4u!1y)OkSo zT(}Smh4Wg&jZ!wR55`?Tz&bIU><)(c{0pt@2lJ0#khX=k<3x&TckbF$CvI!liFxe_ zmq0L5Ff0sGs{i)z|M4sCilW?Wpg0u`8!oViSto_um^M>guRrC=jdJ3yAB2<|LoB}; z`z`6td!kuzVRMGPlp+uiSyh4=6C-28}2ixE1ihk1oW{p zoatji-GFF9C!x&mL5d6xRrokerA?$xNgHH6Jh@a<0|ddzP_1iBqe z8-DZln{=O1bln5$%1D#)8_=Q4ZMPc^oHACDrU|4}%#Bvsw5VCbt%A7DIAbVm^mB)E zzmr_DE4}zbQukYf%pJyC^XBd?*7RzOH^lmpoXS(PD+(6=2nXh11ei1mJ(5$c&44Xx z&WqH zH>ROpKaw{WYwhMj>Ixl{a(98Hj`4Nj`qs-<6X)~II~m^!n_k)?_xMCSL_Ooe`FnEy z+2%tql0>vZ-Wzhj8nfDh;nVHj;1>8Nzfva0o$we+ddy6h`@ksBp~j0$o|tIG${d#7 ziU*;6AlHR$p6sOCCNiybMX#eXTD|i@#6u{xI~B$+%AQzB zW&VB0vmV1fiR-@$3F1bSa`EuKiLqQO`MN{R@4OL>ZN~i zRp{)O9#PhWJDIME=?7x^2ScSV%KY3Adbj5N#~raP>xht?h1_4ZT z6m@uYm^TwbND+h6^fL}&-b3-#$l*G_Yn!vcT?a!aV`03y<~%#~j(!vq{m`A87XlY5 z!5?!jp9-g?1@bcUk)DciP%u}A!Ay-z6di+D(E*Sx{sGNecDJUtqJIc zuu>LNUu#&*JJ)8AE%DXv#8RJ4=JI};pGU&gaZorJioY2F&w>aHfra#JNOFS>)-5wz zF~|JU*olwWI@naT>13y(6ejvRW9$f0w}@0+Be~~;NZn5^&@iAgyf2VWz`zT)U7Jzw zrq*7xTUUq7#jCVI8i_k~N&Nt}y#te?iQ*5JK~;y=5Iw%o>JU>g`-O=Z6ol=7ktlN= z|EjM|OFcD9>-DJ{%on;4Rdds8Jc;6Ev!U294i*G?LB1y{jW=c~Y@{m2jq_z1&a(Qy z8m*>*>&*kxYmSw%ceCJLv@vZxOd8Z1ci$CCHo2l9N6pQE&{}=2p_3EsPO0z6YIr%Y zd5-5RrWGvF-NMXwi#a;lkXECrQEK42Apw$tVeVW=v*Ni$>(+<84@q}re$)gWeRRmb z*}L_&Ad&8&s-|3|=`nx`dh7{7Ke%xlg}NgW?xK$;obh?Hi9+ipMz=hOnRWV)%h}#& zcv4_oPh;8_Zrn~)O}O!_JzN|N%@ND<6+d1#6z*2ZCDhkZf!vib)Xl>r!e`NAiXt>x zn|idSo1?f_yEPb$?}IoYJeW!99=ma>2x#L4D^O*Z@Be(B6YK*Baz!-425#k5rS<#)Rq%qE;21d zQ??UR@VT+1<-6s^v_Ccy_xzaX9>&;OnUhUh9je|l+dShtRM_YcLfLqVIq^|BJaeb= za;$mMO52g!0Cd!C)Zn#*(jOuBn?0Em{gm=lQZWqr@^*igt~s)OeW@3zI|qHmv{3WN zkGk2`P#8r-bb_|LruDM9&ZfqF#zH?$Z+C9hR3hT^YV*sF1c<_<4AOM5t+d0+=Gfrn z;GP%RQ-(r0w0-Et{oaGAeiFp_qgOz9sTnhD^01{4O-t)?;n_{Xv}6)t_~R1E1x?E_ z4RquK=Prcz&CWLLM`AHrWwiA=3t-vwq|WQ`5_lR4%EJvHndtRe8Cv7FbH zOJThPRV%|NmAu9ij$#a_mQORYB^t9{8FR^p7m!o-v+nH&|7u^5|V{5glVM$2oLd-jH#Qb*A6R zH`hiYFg6$N?=hx@5x;CD;_k?JG_gs{3)Me-e5pBsNEzR#Uy@7QTTUHb+n~Q#uOCWn zmw9^#i1aDZtOMwx$vNu+c*^<^c?3$@+K zsI_4$&5RI128?wO+1gLIsO4IfHV@8Qu!Q)j5mBjCX1x~#8e((JJT_ntRvKlALX8pQ zabxU7NQ#7U0*?m0tWMU4$UMWY2N!iroy=Loc7-UVHlpQ1FI~e5Kv*(C7YgYpfdx(N zK4Y3UMEXH~>FEp%bectfMQdA>u%aj#(ZEP|n9&8W(s?NedR)W;&^jah68t*8(yp`1 zc{x&dNxTjQA;wC>{JH*hvN6#1u{(G7V^IECdcN77Cl2zmf4$x<2s9R%JLe*Zo5du% za_2rGif^@s&Aa_=nssQ_5St^q)P?f8bX7=;G}^G_T3HNX`Tlp(R5nY9hU8fm9~39@ z3ef1OSwCwwiLqd%PcM~rFDQThkg(MDvf8O-6?_nWm|yrzqf+Yux6iW^?&wvcp(F%K z1*!Zsl`tLed8(erP~v$tILa7eUATGm`h&`vz6hv#!RALgMJ*gX&E#Tzo^24sCLjhchNcR)sF5%F2R}V;Q-@z87Irh9|d#$V=AAd!v*kT zKN~8`KAd)Vq_=l8shp}YuD#u2rSCocO@*5Ek%VTJUk(?7aXkD z3k1cJU%;(jx3%J&Pn8PkXLBIR=AWlNkLB;u7!E^7X|hPVomr%Sga_@p0r}q(zvwi% zu9=rVQa>VhyHGl%<+l?=R+J)G!+Jra6Rh=xn_mhQcZG@s zPcH3m)*Ub~n>bLxen*;Kz7G)-1;xGQL*ZyLB8*9PZVqJLy3(VvB_V>iZ1YYh?)g4Y z91tmHpwky>^z0S_2{C6JEAA8+c_V zU}&a~wN_#?2{aaHA5%!(cp{>r<*Ks`LztXyu#NG>JX)ma&iIanXWbygmQ=hgf`^zK z%qRvCmi6YBMx>cku+u^2#Pz^@y&1=ezx1gsB<(O}yEzMi%&ss{B_G@eZVxnui|GO= zcH+`n9z!=F9pnaPcM>ZS;aFFyNk-_-ltg64z%X0d_4Yd4h^jTD>0c`W!I2G*go+bd za1lhvA*l^)wM9v9>w*hl8Ew)NtB-!UOQ_51q8aN-XB!IrAkbEhddtFww}JW+DE`sv zlAjN7rTdJb@;tFlAnyrLf3uV*=Pt3?!X=sr3f%Lf$OvK^xyZ+6Qc)~mGUT2^Va|tm~6n9?AMy??a|97M%+spkEj0=Z^_ZNq5*`3mV^E@vpINn7Grc z&E9V+L(T%JkWO@>v;RhfFgjsBTWJo5LlSNm+GYfWjzmPb6CEU^`^|&1u1M3loM#?_w1LK!N0f@ynU7Op%~`35U5W809H{F}3&aPa{6?qsPaXRL;eVN_nUJ4|(l1xavYIH}_m<}nZJjK4Aj zo{bSiq@~MOoM&%%0K$4fVOH|gi(Bsq6sK+y%7!jEPoEsPc0%rO!V*L1Hj{Li zAOii-Xx;?bRcQQ7S8g6+YJC9d6^ED;zMAJ&K;i2b324e8>b9ofx5TO<9Jf<2^&L~3HnKI{_yohv z%`ypnSfrb8Wh=3F^jV~0Gm+cLm{zN=7II&M&YSS?(rAT>`(B*?s=r2TD+1*ZY6!mq z$7aLJa#jzMe5tT*(BRK?%RSmj zb>SX)iFBJJ+)hKKCt~%Ph~v2haAImgoz1*Eq%{~mU8;MJi5>yN?&xn%CE`5bW>+yC z6a@WL+Aa`INLAWWA1f|eP;Lw0Z}SMt?(#D%GD=pP*LZ{4mHXbETfRFGYXp|7Lor(q zga++O#4hKFOzlviH@pXv{zm2h<1g$_FUv6R^@9hapfVU{Q1JNA!xp^@6fLa~(udcZ zGaxP!0wpw>-kmD#5ZK0og=%5X8V@o@YxVb>Ov^l>mn)a+&JAelZU`s2In!iB)$Ce? zh9-(RSYb!1x zf5=vGeYO9+IyaSexeC$f25qGeW~yOX2-m+!>hN>kwk%=A4l%XiIINpX=*ro-J&+WI zoDk#-t?P~C3!C?CdXJBcQjykFO-fH#NutM(!}-fhbRXz0l2#)SX%mRIR#0)4J?aaQ zUhtUYem)5)pAqZoTBvhVT$oO(@)0L?{eMN|Vt2vZ1oGHHQvsxy`^0 z<{6%vO{8wFAFLT*RkCfpTulQ#xd_eimP>DJqZvg?w+KzgxhlgxwoGgH@H0MVtg&+^_}4lF3TDiM(UCA3$zLt6Wm~O2nYtMb zH|jF_B?fEyq3@;Cb$S=xXb*O?rP+fwz0XuvvNuKaawf77%L@D9{8_YjzJGHc?;)4M zjoYKFQSL{&bPyI(6d3%sK?F(m0x3THDyH#yQ`Bryhm34*#H+pGRjQz>0%B!Akyh#W zkw>0O?*HV@Z?N1;^ZY4TQU*icfjuGml0UaJ6*iVJ)hc;AA-#sz%OU=4)JhVe4uU@K@aE# zFK57^H!6(u;zoU2JLGftwH;hb%@We_uyL^n9(@k?HV_A7R@*uRN_U#m?I2Cj%NIP- zvA^3jh*HUKBJ7fF9^DIC2`u%py$dfjW54xzu+@yzz8{&buBM3+4q-X?t?>*7^C9+_^ENbXNhSq#S9CScqR2mem-H|K4~+VEJVp zR+Iv`T~}P`5+Wj65Z9Foi-BX^;Z_ToJ4NRZaT#Vzlrrl3t;c`Qd;on|8Tu8B@R#o8R--ou%%xT9}a49$2Yt7za@S&_hB?)HSgPD#6Z*r(N>1 zm)xLpGnkyaP$;s;U8$ofsCu}PsbSUwDn~=P3i$q?3X0xf&q%0tl zrbZ<{A=HUoX{|~=93me2S-CfHm~3nqKG31Oym#uED}eRLKit~|x=6SN!n7rUW+@U5L<@}IeC6WPIin;8`Wr^sgpcv$h{I76Go7xLJkm_OgBFz+j^)>fx zXV}XaKgqP1Jmh={7UdUYanm+8Q^ndNaL?CHLri?N*jAf8MF%jxo6Nx!a&k-Ae(uof zMR)jP{6X0H;dM7|-d4Vhj2@Z)k*CFm!Wc5n8DT5Tm4c}`U0_msT-dxY*tz6^!M#_}#|se9L1D45#L7g{$wvr=@Hy^7kgYLJrbUSKV}juI=?A9U=`~d+uEN+J+=M zf`vh?H<)E&8WR~GB6GEQU;v)uWH;{l#}M(;DA=3ZsK8c;JO8cMtY?ydq~v8pM7+Sl z3UryU&B+R6w_RzjRLd?^oK*5LzC$2%Vk+zb(5C7xO8z$sBrw&DTtCLvC)?dy*+Oz#)oBVB_#V@pyoom4(JJu0Ai7F)a2XgXA zq?+33btU&#@Kp(S?=x7tJC+Iy{Gkweg|m!>kNvr_ab#5ps#r6|d#I@!uUv7$#3<@Y z0Z}y_iWfn|&b#`Onc#~6K%Sw}F82Fv1=dq^s+B&{cP{*^2rLH>6u|<;;#I`;{##*5 zt;`-b?j~(IPhu@^hq?9`asF$P`^*yxcYnv;mWUJ&hLH%S*b$KxZnkL8{c4u^ekpr& zF5H*~#gm0lcZ#W=8^pzEz+jK|uJB5z%V83_QXA$9s(vH6Yn{OV$zA=tn-E?iH1UAk z%iSV=DKScINKuMN+DOoy4dI4Du?R0VO65?yNXMCIQfo7kA-z3Bt;GJC3kq;^r7+rN^3xu zU$37Oz@69%^(MZ|b@;X8us4N8HR?}d-E9--Znc30`wdt~iS3H?CPOTbk-r-MNi+1c z?32L6l<(N37}Nw2w6QbX8>iClAaciHKB?65fc8uK)eNlkNu?h2FTx(SNhEi|53csp zxT+GjLWdG3#1^tyGxBVn~PQ$AeTQC`=?Kq`fNAf-(x6r z;WjLG=h8cCuFZz%G$@GT-QQquo9AA-KCC+yw^Z^r;P=EA?hdC8UxT#k5Sz-|^%bEu z*?5Xg0lneq2T*kql)pVOrakTA#nlQGVG`4Hr5xF2{5tHnbF!3`OK7ypQ9WVjtL40;Ru|#bnpwZ*B5DsI@vqO~$<>x!F)fDdmgL!nRjJwDmdIozNC|Y}Il9 zc!(EW>7U)<5J7)!VsB$>;BQ{YJ)X2kq*H^`z6ZN1(jj1yR7y?U~J5Tlj2s-HJ)>9P-V|BqG|+6XFF(DfkpJ z?c&~nSO1fxy_~S_3HP?BYpw=yzyB^&4C-a=s0=4)71{Epo_bzF_TySoH~2et!hC3G z^x~dZ!Z0`Ps1V9rIs3ACqj6~rbJ>*}_$~Y81X3}7Bt&oVJ|ueRp7t5lJ$g-kZMTa{cK2UA@6^FfCM!q+R^N=Kny=uHdf`3mb2~Du=%H3 zkUKvXj^)7TNC$zcQg%Y*TRUjJuO`15#uu~8hWdj_pIM{%u>Xu1_CmK+eRG zF3^pr>WD_U2Js8~5v!W*a?`uzNOTHO#iYO4zYD0!44yD+_dCv(`W{}8cT>VGAR|;U z!J6&Kz(_b(yY@9})YBE2k=VLmWtm+@?7t03Gm%9?Q_cQMMBk-m?DtqCknVz5E3CUO z01^cX8`lN<9Givc&kjl0QaJ?MmtuV{b+ncbdO+V2ge3cGVZm`Ca}|+W$__zD9$D&f zZ+Q!wUHs2quM|`;Aw`+i;Hfi&BTozCVl=@>H3>4rP6aF@A~2XixY+%$W8wenS9d32 z8ij4QrO1W!1uZEx4mrb~^5h~tIh92#q<8tjAtx?Tq}xZty=8;>J^zQVe-JECsI}NN zrjjeMb5`n1WOl}&JjuiE`kLMi5SbeEDa?Cis2o-R zsscMRHLGD#T31cH0F#pQ3^qZfY%)g$aI!gYeYL&1&%5T=tuI7Cv?iu5qdRJ7${Zqd z3^ga)5H2*$7+@?6gs~#JLH|%_-A)B;lM77LTVb7jA823+?68aoq>LInUl5E{NDr zj^dgAU z!wk4^0bGcPxz13RZ%F$A7W@zdW9g&Zx#|v%VAg`JSXVGq%@I~?yz|sYw<{ns`5)LED2yRN<1N^`^jMD z9X^ly-#ofL4B4Gn*cZ~C@b8llcUQk`E~y(Nuw3Aw!Vj8zTwb^KOhhhe0Ws1Q>s-Gq zC3F|q=Qo7(a*@f_Y>I70{LLgGX1!WWyAu)EI`d9fm@yqLJg;C!xYBL-U}=TqO|bO) z>GQ}0L}=WXR4_j9;W#M8@Z>?Zd9x?iiI;oFab09bQ`k%~!G-%(r>xlnp?MijjnJ$$apbgJEr)KzB?; zA7%>B@!LTu`yGtz2%UtcOw#hpkAPU`iQjc1I#t3Y7-Jt;^!DPs{a7>0u;;qc3??L7 zx3AOB>r7NR2_k$5i_wray2YLS!>9KPbw1RUNpN7Dd2?5oTQ57}ggvppL~ByF+ryQ} z=5(n^ige~Mrr;=G16;fDaN|5X*&TWlmbuiG?)F4>rY$(=*pQJgL;_X80a!2{ZdAdf zBgVosUr1gB#nujUTV0`v|G0-bW;}^Jvc29RA&acG}=)PCi6i)&FF@&OSm_SNxg2FO(pD&cm zfHmnI9#H z8zIn5gJW7azmQP$9}PD!G6~z;sL#b}9J`e%ILj6>4fgbmHzci-x@tBNT=E&Von~n) z4Ah$4kOij)WjaQ9cVzJhNzFi%2pa?JMW&|S&7!eFI>4AF#=0x@z)(4pv{WFMGj?FM z6PF#9h0h;M$AXdh?27M% zksShv>%%I2&hVtc!>|8auNyVtA20}bx@rHQ=yJAqk+8A4*_H-Z( zgPWV({%l>p!&(f~xxg*8Jg<`NL9Ck%6P>sTU_cZU7m61A(kMsV(A2isZ4S;f48e#A zlLqZ%5@%ynO@v90d~wnX{ncfNNF2#@q_Pn(?1w>&U4r}ZhpbxtJ_hVL4YK1xoOz+c z*)%tLz(~Y?KSKs8e%ML~FI8vS>DGsu)HZFTBSmmXq>C3+ArY`%KhGBm*S6v|jQ<&O z=VE^GV+1IK*k)eA*19=edbfhLSMPcMH8^e~!5tz}uEDWnYu8Gi6Fk@>u+&cI56M#m zkk?0ylzwSZT)NgU7+di;W;;+Gh;Giq#VeOQ~;1aJZ}trS@6 z@mC<`#@JPQ4u1?s8ucG{!RY{sb`5TVs(1>$@!fX`T@o2FB^bk16ti-yp`muvV~f0- z-Tl}ceuo3knd&cu-1|^?j+qW67jSw)@xV~n77NK65aYsv=`F0$Rc^;_IHa4j+`!mg zg19Y)G%T5BSmVx!n|Jil${K~hqZYfr8Z7;^7*gR_9F%l>&}WzP|KwdcQ*ar??KE?- z+}7U7O4+U3V=N2?7I|p4U}@^}o%|vBYEq+M=(lVh)}2NWbfuGNNeUHa1=VBtQextu zt~Pnb`5l@5+yTaSyScn0^zH9L&2cp~3@1Hy5w_kz%m5mj>(;NGGsNI2@YrtNn_kmb z2qT=&pyFJ=H4^sr0>?raej!~1+(LhDwJ|LVM!IuPu?BWD0=goAc03p^^6C+PshMi} zXX}YFn6#Fdlxq%g<&Ht$6|=y@U7)PRwt_Ka6<=p~YoDJ$-z?&~zt}Vk;sy}P$I*mt zCbQ^a5*hcs17>%8-rI%i9KdZl1F=Fl?gfqWyr6$M`|Y?8YxRJyt-bYmndV3mBxC1X zCrHLxLDM%DJ=PMDVlx{XnrznpaK4~9FzYa~LBCyWN|S+HXi94j4}bDw5d8TPnKYYb zdD;YV;Wc$LFxDi%8=^!=wPHvv4l+BVNe;2M0)?#}L9Ekd8pa zIX3vxyDiUd8}>oFYc}m7e({F3kDTG@Tq14>|=nRdZ5Vb3FjY6Q;{n z)ZZ8O1z^7sR_yF{%W-m7Js^lVS}4*@b%Wn7Im4760yxT0c@2g|YI^harNlwbyhsg& zG-a7SkXzIZt`F{j<&{}bVv!{ZJh0U3xW~4slxJ&M{9_vVn9 zM+BH0&vxvUcmopu+1k4|qzn}(a$UJ=yV}7%RZZ=UHrzKMP}3i>!anDTDpQ~HKQQbJ zi{oWJM4r)NAM1dMh0#RZ>~w8+%pqM{uKqoxysba%_J(uVp1o5g-!&Gt?O+PrFzvN< zhhXS)iC;_ok8pJ=wP6CO+lBnScIui470!toL(snPA3wcq3Sw|bM1xS{jGIh!Ffu@@ zvE`6&sJ1SnnLF@j>;L(qVG zYry|EY}>(>Xl5q7EF!9W1etTNj~-DAoVe^ulj8RUn673)+Ax9c1EqX3Q=JChBHGsWh%^bi)@pq&tEoFQYq**a*)ZkBCQh*RUVtI#nt$h3{p{0)XetSfuMv4s%lrYS?p zP%?#(ZyQZN44CY;#l6|KYzfA(8rI&Vkf&`+)onEKePB3N7+S&TJNoMpP*cgS!WKp3 znc69Y*VMMued`;4)F{Old#!KG^h$ij_*%gioW++)J;m8qOULnLMLgza4noua>Lu4o zwdbXnfzH3i*6MG6-5-4SdvayN1#~u{^nwc@jfA>eOfnV5=f5TQ`!%->GzlB}t#9m< zziJ7xdmc}12OB21nr3`PAQFTlKv=7HQq98_`RxwH-q~z9R*0Trs8+i=%kXgX=^w#z zeXJMUoK+2dCG-ipCu=#crQT$P|1iOH- zwB6qiSI^ap^Ue&$Q(>&6IH8h+*R#LBuxDEh6Yh&G8(J zaB#wpEePZN)OaaQajLBS-8y?F)cEk8qLr!qsEGbR1m(vG|IrZlHRy89ld&5VgFRc0 zXSa4;JuV?l4a@hkKG@ZaxUihiEx^@%&AZC>F8}jY&vS za#P2o%e6i*dKMfT4F$)ElwF2GH|V12XT?;WkD#)}oHG&E$CL9V z=prYaeHGaUDtcj=*OFn$;ENeW%f{Kx`4f~4nJVUAI|rs~lVG3=SAye5ki(5Mb83T; z7@41H!DdZsu-eST`u`N*jL!~3+Mq_gJP^JR((@9a)ou!+=OKpg$_>L%R~iggwdOSI zjG^_&&)_jL$FFB$Npy&p=13#9)(R}!;rvB6)150$rt(b^h$J#yUZ=mRnDRuzy;rP&c!7wS6cUrjt15>di#A z|KPoB!;CIa{Sg^A3ho~QW!wPRj3mY>ty7`0(Gf=QU^M>FKYPL1GjSO5LYRUXP=NKc zaKiLBj;Pv-ouNUReRf%WZL4-Q(X|;k4``ElqS$n`3*>cy>+QNje;K@5RHyg0-x?21 zV+bbO2e}cT4&vf3u~T+|0V%LMi>@s>U27w?>9jK%f@vFVmbaZO#5&=sh5s+xwK zIH^HA!R`oO*@|^dpL_}!2o4W~4ZSr-u$uXW@nK2?+`K6=VST#6Veajp_r!7>_~VpM zM2HSy`%Z)Yvolzw41zDjbROTaf;12tAl+f46$H;6NK}nyq7Mla3&$}hF2nW7!EmLN zz04!f$kB3Yb_%V7*PF3NF(2-ArGl;z#r?ZdH&YmuC?=tjMGSMiN?Q)J8FbHEN~W{kSlOWe zUaiH3i%89*F7Qqn8z_Jt0^I2x8~5J{H_JYt}&ELh6z1Z;sn7h%-UqUb6U`IH+o`S=bsA>M;ln zbB)YphxxVN&Odzo{Xm!w<=>DIKTd#k7X4!bap0*?+7l!CjpiA)SuSWa9kQDWpA!Cm zpm`ckNLQLm>Tu?y7k4rRZbVWW97f$PTsTS)=hG0}C7(Ae$6&yzk65|A;j1aL7eEf# z&pgXGM1+x;snHFNRLY7}HG49`V|Z_BmpmY!qUK#B+47jsG0`7?7=a#zZ1cMo;} z^h(MNBSAvayNJw6_8KhdL4}=RwSRMu&e(K1qc&q2EMZ~bcxphs{<@o0K9U@s!+sLy zt%ORPr{@RLuw^#e+{ze!g5)MSLqH259Y{}tU=h~rGsSc)NxN_lh1hq1r2j&ko3%fP zdwf}>dxCU}0FF^2?$_IxW{)Laj&DxL+e^bSAI~gKhED_R+NRyZ5cnBm+P}W?L z&@WO1RWC%+-^4V9ppU6X0vtYPok-aZy)iBnp*TZjHz?RcTAqq@w{l@Y zwC0GIzF8~lkS?D{C|XE3&S{JF!g}3hggDBA@!B!9-bpGj7kOtsT-fAF&r;W1fMY$V zr`YlB1Lckd*oa#KgHkSLLY(2T6Mch;4&*{b^e!C!Bwg#qjlkNC2q$A=yUC9h*#{^I z%yb^TwM!|%7{W!^;0rHE>0ByELLF7Jot73k1&BE0Ol*TJykJ1F?`~K z4qkK+50I__sSM5sXnJF9&y_yUB;ep+mE0Y!A|ZkJ<$Fl^&Sy~KLo3VQPANLfdrN0! zBt!~fD?xu}D6D~$(J;h1(?XBTKG6LLp<+&h)~)ECamE(&tquIp0EdSdNUtG6XM#s7 znEWmKvrsA{GI6Y@4;1v)Ec}Sn^-KJm_{GHTP|3ZiE2FJF|8_=^;|!2f7$D%3P=%9$ zyTVp$xvte8_?)fKRXxKUe zm!{uFq_j&EGYQUw&ImXf#!kF*9L4t{sRwi?=rL+}G?Z*M#}bMcMg8D`fKXoSyv54s z=q9K_wiwRLC22K*JPX@^A&h`|J1M{w7%F2RsZO7TtSCeHXE^w}gq#~t4pj0AM4g$O_mF)50};`~$@FVp0uznxl5lKu zn1~Ad%#aoWJB;CfBbr-9Ivf&a(MM>uBQe$5p66lhIqv=IhlqIkyqR^_EYGigXa=}i zV|(^we{4E(SIb8c7K;KWIcZ>>yjZGXjOcfTKS+j za1m!PYt3suqRiXj02WUAVD{K@t>-NWiIfs72I?uFOjmH{&M2JmC3CpOkc8PC^9Dlf3 z1h&>1I=N|ZI0O!D+-bm&E1`c8PTb|;eEuAy)E|ij^P>kNYfnI zoQM%g*y)exgp;*L^Gg_6KI7-PxkeehP}K<+RGoAfH|9Dn?xt!1C+ zgkwmFlr!vBQkRRJGL!GPxpN~OY}~`IZG$5&eY;a+YLgiIWdOW*^-*hjaS9ZzI7o1( zqOqyHoE_@GKKRA{o_huQ)@Gze3oPH9enQ-9B{4nk*&B*~2!V0m8PZZ5F4!hykZX>r z)9c4kb3}^I1&SBnwCCirVDsPc4z;edJ@7wAya9-F|PXQB}j zU&utab3?=)Wi?ME&{_1pOoBsP*4oqtf!)+>b2*Lz4d4#oYK#H;X`op|H}E8|JuSUXfD9 z!4Mzr%S#8W{AtA>FI~BVfgB~^7?m7bE7v=sC|;j*SuiXC9-lhptoUXmjAQzc5mD+I zZ{)uzx|fwpkv|E;xjNw4}(1&|cJ( zv@TF1=0;FqzKLkl9b#Y`pdk3v$_YDxxO7MC&BlKs5weA-{7;@q@`c%V^pgYe*XW=! zpIZF|>oDm^3{354>R`_|>F-jmlB9bu31bA(JR;>YES)A8ahiXGNV=oNL2+l{;qG06 zs)fvnr5{ST?XYnuNS&=EAWW7Ib^#wpOlxOz-+>TD_^$=J70m7m zn2t{v1KnF%f%n9wkH$}fxWmx*<*LXKx_{pG{$kr%_0(nQ>qP5 za75*7ANY1Ywu66&6Vn7-tsDJ;6Fo;_UD`gLA&nXx{6?b%!y=(^{w@JFFE1);2UUec z#8^_wz`YDO9}AO1pv~W|?e{5-))wDgbDSXxmy-=He%8MRLFUEfV7FGwBP(SeoO8p; zV$Yu<6AmVO!4T`~+lXQb7w)**W>+MUs%l;qlOAW-S0C2O0yu1Z_P|IjoP{)j)B#-B zHt-bo?ZCMKdW>LWvBSaqzP0F$BgAdq1qszQ=S?5PiH%$uHZo;fqdsq>AB4Q^hO8zY zN1U5*3QslQ=*Lty1T6OX!%XYnM-h(G6Eb;fhwboiy81@Amn^}tOSuIWH*DIn%1gHS zEQtiP^$vrD`uJ?^Vt;982z}ay3w7jeD?DHB8GP~_1Q4`#3k)0OIEd`3#ZA9G5~a9- z?@ce5y8Es2W7Aie?8iP(LjBMGzrlZY z{VKq;3)bC$x8PP>;hn+8i||-TUlnn0rPA3|TbghjIeiHke|;Tr0zUui>1At(d&Wwf zM&&qy@p!g=Uj=djm#l16K2vELiw?auK@d@g3{uC1opyco%LfB*!!r?{qaz{5r`I;s zVhY&@vaqgi4z2#AxLZ8iC(X>EtECNOo zQb!u{)2AOle6o8U=WG3&4cylyi%(bP`< V?45t(U!nw~V#W Date: Wed, 30 Apr 2025 13:26:59 +0000 Subject: [PATCH 58/58] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- martin-tile-utils/src/lib.rs | 17 +++++++++++++---- martin/src/cog/source.rs | 3 ++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/martin-tile-utils/src/lib.rs b/martin-tile-utils/src/lib.rs index 54dadacea..2214ed269 100644 --- a/martin-tile-utils/src/lib.rs +++ b/martin-tile-utils/src/lib.rs @@ -254,7 +254,6 @@ pub fn xyz_to_bbox(zoom: u8, min_x: u32, min_y: u32, max_x: u32, max_y: u32) -> [min_lng, min_lat, max_lng, max_lat] } - /// Convert min/max XYZ tile coordinates to a bounding box values. /// /// The result is `[min_lng, min_lat, max_lng, max_lat]` @@ -262,7 +261,13 @@ pub fn xyz_to_bbox(zoom: u8, min_x: u32, min_y: u32, max_x: u32, max_y: u32) -> /// # Panics /// Panics if `zoom` is greater than [`MAX_ZOOM`]. #[must_use] -pub fn xyz_to_bbox_webmercator(zoom: u8, min_x: u32, min_y: u32, max_x: u32, max_y: u32) -> [f64; 4] { +pub fn xyz_to_bbox_webmercator( + zoom: u8, + min_x: u32, + min_y: u32, + max_x: u32, + max_y: u32, +) -> [f64; 4] { assert!(zoom <= MAX_ZOOM, "zoom {zoom} must be <= {MAX_ZOOM}"); let tile_length = EARTH_CIRCUMFERENCE / f64::from(1_u32 << zoom); @@ -270,10 +275,14 @@ pub fn xyz_to_bbox_webmercator(zoom: u8, min_x: u32, min_y: u32, max_x: u32, max let left_down_bbox = tile_bbox(min_x, max_y, tile_length); let right_top_bbox = tile_bbox(max_x, min_y, tile_length); - [left_down_bbox[0], left_down_bbox[1], right_top_bbox[2], right_top_bbox[3]] + [ + left_down_bbox[0], + left_down_bbox[1], + right_top_bbox[2], + right_top_bbox[3], + ] } - #[allow(clippy::cast_lossless)] fn tile_bbox(x: u32, y: u32, tile_length: f64) -> [f64; 4] { let min_x = EARTH_CIRCUMFERENCE * -0.5 + x as f64 * tile_length; diff --git a/martin/src/cog/source.rs b/martin/src/cog/source.rs index 6c9d43393..5d083981a 100644 --- a/martin/src/cog/source.rs +++ b/martin/src/cog/source.rs @@ -220,7 +220,8 @@ impl CogSource { if internal_zoom < self.meta.min_zoom || internal_zoom > self.meta.max_zoom { return Ok(Vec::new()); } - let bbox = martin_tile_utils::xyz_to_bbox_webmercator(xyz.z, xyz.x, xyz.y, xyz.x, xyz.y); + let bbox = + martin_tile_utils::xyz_to_bbox_webmercator(xyz.z, xyz.x, xyz.y, xyz.x, xyz.y); let tif_file = File::open(&self.path).map_err(|e| FileError::IoError(e, self.path.clone()))?; let mut decoder = Decoder::new(tif_file)