diff --git a/martin/src/args/root.rs b/martin/src/args/root.rs index 00f3075c6..57ba2cb16 100644 --- a/martin/src/args/root.rs +++ b/martin/src/args/root.rs @@ -297,8 +297,10 @@ mod tests { let err = args.merge_into_config(&mut config, &env); assert!(err.is_ok()); assert_yaml_snapshot!(config, @r#" + pmtiles: "../tests/fixtures/cog" + mbtiles: "../tests/fixtures/cog" cog: - - "../tests/fixtures/cog/rgb_u8.tif" + - "../tests/fixtures/cog" - "../tests/fixtures/cog/rgba_u8_nodata.tiff" - "../tests/fixtures/cog/rgba_u8.tif" "#); diff --git a/martin/src/cog/errors.rs b/martin/src/cog/errors.rs index c78529b78..ebbed487d 100644 --- a/martin/src/cog/errors.rs +++ b/martin/src/cog/errors.rs @@ -48,4 +48,7 @@ pub enum CogError { #[error("Striped tiff file is not supported, the tiff file is {0}")] NotSupportedChunkType(PathBuf), + + #[error("Failed to parse GDAL metadata: {0}")] + GdalMetadataError(String), } diff --git a/martin/src/cog/mod.rs b/martin/src/cog/mod.rs index b3794303b..8366800d1 100644 --- a/martin/src/cog/mod.rs +++ b/martin/src/cog/mod.rs @@ -25,6 +25,8 @@ use crate::{ MartinResult, Source, TileData, UrlQuery, }; +use regex::Regex; + #[derive(Clone, Debug)] pub struct CogSource { id: String, @@ -41,6 +43,8 @@ struct Meta { zoom_and_ifd: HashMap, zoom_and_tile_across_down: HashMap, nodata: Option, + google_compatible_max_zoom: Option, + google_compatible_min_zoom: Option, } #[async_trait] @@ -75,9 +79,14 @@ impl Source for CogSource { Decoder::new(tif_file).map_err(|e| CogError::InvalidTiffFile(e, self.path.clone()))?; decoder = decoder.with_limits(tiff::decoder::Limits::unlimited()); - let ifd = self.meta.zoom_and_ifd.get(&(xyz.z)).ok_or_else(|| { + let actual_zoom = if let Some(google_zoom) = self.meta.google_compatible_max_zoom { + self.meta.max_zoom - google_zoom + xyz.z + } else { + xyz.z + }; + let ifd = self.meta.zoom_and_ifd.get(&(actual_zoom)).ok_or_else(|| { CogError::ZoomOutOfRange( - xyz.z, + actual_zoom, self.path.clone(), self.meta.min_zoom, self.meta.max_zoom, @@ -216,8 +225,8 @@ impl SourceConfigExtras for CogConfig { let meta = get_meta(&path)?; let tilejson = tilejson! { tiles: vec![], - minzoom: meta.min_zoom, - maxzoom: meta.max_zoom + minzoom: meta.google_compatible_min_zoom.unwrap_or(meta.min_zoom), + maxzoom: meta.google_compatible_max_zoom.unwrap_or(meta.max_zoom), }; Ok(Box::new(CogSource { id, @@ -287,6 +296,9 @@ fn get_meta(path: &PathBuf) -> Result { } else { None }; + + let (tiling_schema_name, zoom_level) = get_tilling_schema(&mut decoder).unwrap_or((None, None)); + let images_ifd = get_images_ifd(&mut decoder); let mut zoom_and_ifd: HashMap = HashMap::new(); @@ -315,9 +327,21 @@ fn get_meta(path: &PathBuf) -> Result { .keys() .max() .ok_or_else(|| CogError::NoImagesFound(path.clone()))?; + + let google_compatible_max_zoom = + if tiling_schema_name == Some("GoogleMapsCompatible".to_string()) { + zoom_level + } else { + None + }; + let google_compatible_min_zoom = + google_compatible_max_zoom.map(|google_max_zoom| google_max_zoom - max_zoom + min_zoom); + Ok(Meta { min_zoom: *min_zoom, max_zoom: *max_zoom, + google_compatible_max_zoom, + google_compatible_min_zoom, zoom_and_ifd, zoom_and_tile_across_down, nodata, @@ -375,3 +399,64 @@ fn get_images_ifd(decoder: &mut Decoder) -> Vec { } res } + +fn get_tilling_schema( + decoder: &mut Decoder, +) -> Result<(Option, Option), CogError> { + let gdal_metadata = decoder + .get_tag_ascii_string(Tag::Unknown(42112)) + .map_err(|e| CogError::TagsNotFound(e, vec![42112], 0, PathBuf::new()))?; + + let mut tiling_schema_name = None; + let mut zoom_level = None; + + let re_name = Regex::new(r#"([^<]+)"#).unwrap(); + let re_zoom = + Regex::new(r#"([^<]+)"#).unwrap(); + + if let Some(caps) = re_name.captures(&gdal_metadata) { + tiling_schema_name = Some(caps[1].to_string()); + } + + if let Some(caps) = re_zoom.captures(&gdal_metadata) { + zoom_level = caps[1].parse().ok(); + } + + Ok((tiling_schema_name, zoom_level)) +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + #[rstest] + #[case("../tests/fixtures/cog/google_compatible.tif", Some("GoogleMapsCompatible".to_string()), Some(14))] + #[case("../tests/fixtures/cog/rgba_u8_nodata.tiff", None, None)] + fn test_get_gdal_metadata( + #[case] path: &str, + #[case] expected_tiling_schema_name: Option, + #[case] expected_zoom_level: Option, + ) { + use std::{fs::File, path::PathBuf}; + + use tiff::decoder::Decoder; + + use crate::cog::get_tilling_schema; + + let path = PathBuf::from(path); + let tif_file = File::open(&path).expect("Failed to open test fixture file"); + let mut decoder = Decoder::new(tif_file).expect("Failed to create decoder"); + + let (tiling_schema_name, zoom_level) = + get_tilling_schema(&mut decoder).expect("Failed to get GDAL metadata"); + + assert_eq!( + tiling_schema_name, expected_tiling_schema_name, + "TILING_SCHEME_NAME value should match the expected value" + ); + assert_eq!( + zoom_level, expected_zoom_level, + "ZOOM_LEVEL value should match the expected value" + ); + } +} diff --git a/tests/expected/auto/catalog_auto.json b/tests/expected/auto/catalog_auto.json index a4efa942b..09eb7e603 100644 --- a/tests/expected/auto/catalog_auto.json +++ b/tests/expected/auto/catalog_auto.json @@ -117,6 +117,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/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/save_config.yaml b/tests/expected/auto/save_config.yaml index dfa59a7f2..1cc8b17f4 100644 --- a/tests/expected/auto/save_config.yaml +++ b/tests/expected/auto/save_config.yaml @@ -258,6 +258,7 @@ 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 diff --git a/tests/fixtures/cog/google_compatible.tif b/tests/fixtures/cog/google_compatible.tif new file mode 100644 index 000000000..d5dc97744 Binary files /dev/null and b/tests/fixtures/cog/google_compatible.tif differ diff --git a/tests/test.sh b/tests/test.sh index 8cf70f35d..e85fc5f03 100755 --- a/tests/test.sh +++ b/tests/test.sh @@ -296,6 +296,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_13_0_0 google_compatible/13/0/0 +# test_png google_compatible_14_0_0 google_compatible/14/0/0 + >&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