@@ -135,6 +135,16 @@ Get image tiles from a web map tiles provider for given longitude, latitude coor
135135 - "Bing" (the default), "Google", "OSM", "Esri" or a custom provider.
136136 - A `Provider` type from the ``TileProviders.jl`` package. You must consult the documentation of that package
137137 for more details on how to choose a *provider*.
138+ - "ortosat23" / "Ortosat23": Portuguese DGT orthophotos (2023) served via WMS. Always returned in
139+ WGS84 geographical coordinates. The spelling controls the output pipeline:
140+ * `provider="ortosat23"` (lowercase): single-warp **approximate** path. Cheap (one warp). Output
141+ size differs slightly from the same-zoom Bing/Google result because the WMS source SRS
142+ (EPSG:3763) and Bing's source SRS (EPSG:3857) have different aspect after warping to geog.
143+ * `provider="Ortosat23"` (capital `O`): double-warp **exact** path. Two `gdalwarp` calls
144+ (3763 -> 3857 -> latlong) replicate Bing's pipeline so the output bbox and pixel count match
145+ Bing exactly at the same `zoom` / `neighbors`. More expensive (two warps over the full image).
146+ For native EPSG:3763 metric coordinates or custom pixel resolution / `zoom` / `refine` factors,
147+ call `wmsread` directly (see `? wmsread`).
138148- `zoom`: Zoom level (0 for automatic). A number between 0 and ~19. The maximum is provider and area dependent.
139149 If `zoom=0`, the zoom level is computed automatically based on the `mapwidth` and `dpi` options.
140150- `cache`: Full name of the the cache directory where to save the downloaded tiles. If empty, a cache
@@ -482,6 +492,12 @@ function mosaic(lon::Vector{<:Float64}, lat::Vector{<:Float64}; pt_radius=637813
482492 # Return here if user wants a GMTdataset with the coordinates of the tiles
483493 quadTiles && return quadbounds (quad_; quadkey= quadkey)[1 ]
484494
495+ # -------- WMS-backed providers (ortosat23): fetch one image covering the tile-matrix bbox ---------
496+ if (provider_code == " ortosat23" || provider_code == " Ortosat23" )
497+ return _mosaic_ortosat23 (provider_url, variant, lon_mm, lat_mm, mMo, nMo, zoom, pt_radius;
498+ exact = (provider_code == " Ortosat23" ))
499+ end
500+
485501 # ------------------ Get the tiles and build up the image --------------------
486502 cache, cache_supp = completeCacheName (cache, zoom, provider_code; variant= variant)
487503 if ! isa (tile_url, Matrix)
@@ -513,6 +529,63 @@ function mosaic(lon::Vector{<:Float64}, lat::Vector{<:Float64}; pt_radius=637813
513529 return I
514530end
515531
532+ # ---------------------------------------------------------------------------------------------------
533+ # Helper for the 'ortosat23' / 'Ortosat23' providers: instead of fetching individual XYZ tiles, query
534+ # the DGT WMS service once with the bounding box covering the tile-matrix area. Returns a GMTimage in
535+ # geographical coordinates (WGS84). The `variant` selects the WMS layer ("CorVerdadeira" or "FalsaCor");
536+ # when no exact/partial match is found, layer 2 is used.
537+ #
538+ # Two output pipelines, selected by the `exact` flag:
539+ # - `exact=false` (provider="ortosat23"): single-warp approximate path. Passes the geog bbox to
540+ # `wmsread` with a `pixelsize` derived from the Web Mercator ground resolution at the bbox
541+ # mid-latitude. `wms_helper` reprojects bbox to EPSG:3763, gdaltranslate fetches the image and the
542+ # internal gdalwarp warps to latlong with auto-sizing. Cheap (one warp), but output size differs
543+ # slightly from the same-zoom Bing/Google result because the source SRS (3763) and Bing's source
544+ # SRS (3857) have different aspect after warping to geog.
545+ # - `exact=true` (provider="Ortosat23"): double-warp exact path. Projects the bbox to EPSG:3763
546+ # ourselves and passes those *projected* lims plus `size=(mMo*256, nMo*256)` to `wmsread`. Since
547+ # the lims are no longer in geog range, `wms_helper` skips its internal warp and the image stays
548+ # in 3763. We then warp 3763 -> 3857 (with `-ts` and `-te` matching Bing's intermediate mosaic in
549+ # spherical Mercator) and finally 3857 -> latlong (auto-size, identical to Bing's last step). The
550+ # output bbox/size then matches Bing exactly. Two warps over the full image.
551+ function _mosaic_ortosat23 (url:: String , variant:: String , lon_mm:: Vector{Float64} ,
552+ lat_mm:: Vector{Float64} , mMo:: Int , nMo:: Int , zoom:: Int ,
553+ pt_radius:: Float64 ; exact:: Bool = false ):: GMTimage
554+ wms = wmsinfo (url)
555+ layer_n = 2
556+ if (variant != " " )
557+ v = lowercase (variant)
558+ idx = findfirst (n -> occursin (v, lowercase (n)), wms. layernames)
559+ (idx != = nothing ) && (layer_n = idx)
560+ end
561+
562+ if (! exact) # Single-warp approximate path
563+ bbox = (minimum (lon_mm), maximum (lon_mm), minimum (lat_mm), maximum (lat_mm))
564+ lat_mid = (bbox[3 ] + bbox[4 ]) / 2
565+ res_m = 156543.034 * cosd (lat_mid) / 2 ^ zoom # Web Mercator ground resolution at zoom & lat_mid
566+ return wmsread (wms; layer= layer_n, region= bbox, pixelsize= res_m)
567+ end
568+
569+ # --- Double-warp exact path ---
570+ # 1) Project geog bbox -> layer SRS (EPSG:3763) ourselves so wmsread keeps the image in 3763.
571+ srs_str:: String = wms. layer[layer_n]. srs
572+ t_srs_3763:: String = epsg2proj (parse (Int, srs_str[6 : end ]))
573+ corners_3763:: Matrix{Float64} = lonlat2xy ([lon_mm[1 ] lat_mm[1 ]; lon_mm[2 ] lat_mm[2 ]], t_srs_3763)
574+ lims_3763 = (minimum (corners_3763[:, 1 ]), maximum (corners_3763[:, 1 ]),
575+ minimum (corners_3763[:, 2 ]), maximum (corners_3763[:, 2 ]))
576+ I_native = wmsread (wms; layer= layer_n, region= lims_3763, size= (mMo* 256 , nMo* 256 ))
577+
578+ # 2) Warp 3763 -> 3857 (Bing's intermediate mosaic) with the same `-te` / `-ts` as the Bing path.
579+ xm, ym = geog2merc (lon_mm, lat_mm, pt_radius)
580+ merc_srs:: String = " +proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +a=$pt_radius +b=$pt_radius +units=m +no_defs"
581+ I_3857 = gdalwarp (I_native, [" -t_srs" , merc_srs, " -r" , " cubic" , " -ts" , string (nMo* 256 ), string (mMo* 256 ),
582+ " -te" , string (xm[1 ]), string (ym[1 ]), string (xm[2 ]), string (ym[2 ])]):: GMTimage{UInt8, 3}
583+
584+ # 3) Final warp 3857 -> latlong (auto-size). Identical to Bing's last gdalwarp call.
585+ I_geog = gdalwarp (I_3857, [" -t_srs" , " +proj=latlong +datum=WGS84" , " -r" , " cubic" ]):: GMTimage{UInt8, 3}
586+ return I_geog
587+ end
588+
516589# ---------------------------------------------------------------------------------------------------
517590"""
518591 quadtree = XY2quadtree(x, y, zoom; quadkey::Matrix{Char}=['0' '1'; '2' '3']) -> String
@@ -587,7 +660,7 @@ Get information about a tile provider given its name and zoom level. The returne
587660is only relevant for internal use and is an implementation detail not documented here.
588661
589662### Arguments
590- - `name`: Name of the tile provider. Currently available are "Bing" (the default), "Google", "OSM", "Esri", "Nimbo".
663+ - `name`: Name of the tile provider. Currently available are "Bing" (the default), "Google", "OSM", "Esri", "Nimbo", "ortosat23" .
591664 Optionally, the `name` can be a tuple of two strings, where the first string is the provider name
592665 and the second string is the variant name (see the `variant` bellow).
593666
@@ -605,6 +678,17 @@ is only relevant for internal use and is an implementation detail not documented
605678 - `Google`: variants => "Satellite", "Road", "Terrain", or "Hybrid".
606679 - `Esri`: variants => "World\\ _Street\\ _Map" (default), "Elevation/World\\ _Hillshade", or "World\\ _Imagery".
607680 - `Nimbo`: variants => "RGB" (default), "NIR", "NDVI", or "RADAR".
681+ - `ortosat23` / `Ortosat23`: variants => "CorVerdadeira" (default) or "FalsaCor". Fetched via the DGT
682+ WMS service (https://ortos.dgterritorio.gov.pt/wms/ortosat2023) — one WMS request covers the full
683+ tile-matrix bbox. Always returned in WGS84 geographical coordinates. Spelling controls the pipeline:
684+ * `"ortosat23"` (lowercase): single-warp approximate path. Output size slightly different from
685+ same-zoom Bing/Google result (3763 vs 3857 aspect after warping to geog).
686+ * `"Ortosat23"` (capital `O`): double-warp exact path. Two warps (3763 -> 3857 -> latlong)
687+ replicate Bing's pipeline so output bbox/pixel count match Bing exactly.
688+ For native EPSG:3763 metric coordinates or custom pixel resolution / `zoom` / `refine` factors,
689+ call `wmsread` directly: first obtain the WMS handle with
690+ `wms = wmsinfo("https://ortos.dgterritorio.gov.pt/wms/ortosat2023?service=wms&request=getcapabilities")`
691+ and then `wmsread(wms; layer=..., region=..., pixelsize=...)`.
608692"""
609693getprovider (name:: Tuple{String,String} , zoom:: Int ; date:: String = " " , key= " " ) = getprovider (name[1 ], zoom; variant= name[2 ], date= date, key= key)
610694function getprovider (name:: StrSymb , zoom:: Int ; variant= " " , format= " " , ZYX:: Bool = false , dir_code= " " , date:: String = " " , key:: String = " " )
@@ -649,11 +733,18 @@ function getprovider(name::StrSymb, zoom::Int; variant="", format="", ZYX::Bool=
649733 variant = d * " _" * v * filesep * variant # Need to carry also the dates (this will be used in cache name)
650734 url = " https://prod-data.nimbo.earth/mapcache/tms/1.0.0/" * d * " _" * v * " @kermap/"
651735 max_zoom = 13 ; isZXY = true ; code = " nimbo" ; ext = " png" ; sitekey = " ?kermap_token=" * key
652- # elseif (_name == "ortosat") #
653- # variants: CorVerdadeira, falsacor
654- # layer = (variant == "falsacor") ? 1 : 2
655- # wms = wmsinfo("https://ortos.dgterritorio.gov.pt/wms/ortosat2023?service=wms&request=getcapabilities")
656- # url, isZXY, max_zoom, code = wms, true, 19, "ortosat"
736+ elseif (_name == " ortosat23" ) # Portuguese DGT WMS orthophotos 2023
737+ # Tile XYZ is computed normally, but the actual image is fetched via WMS (wmsinfo/wmsread).
738+ # variants: "CorVerdadeira" (default) or "FalsaCor".
739+ # Provider name spelling controls the output pipeline:
740+ # "ortosat23" (lowercase) -> single-warp approximate path (output ~matches XYZ tile providers).
741+ # "Ortosat23" (capital O) -> double-warp exact path (output bbox/size exactly matches Bing).
742+ (variant == " " ) && (variant = " CorVerdadeira" )
743+ vv = lowercase (variant)
744+ (vv != " corverdadeira" && vv != " falsacor" ) &&
745+ error (" Ortosat23 only supports 'CorVerdadeira' (default) or 'FalsaCor' variants" )
746+ url = " https://ortos.dgterritorio.gov.pt/wms/ortosat2023?service=wms&request=getcapabilities"
747+ max_zoom = 19 ; isZXY = true ; code = (string (name)[1 ] == ' O' ) ? " Ortosat23" : " ortosat23"
657748 elseif (_name == " mesh" ) # For mesh we don't care the provider and max zoom is ilimitted
658749 url, isZXY, max_zoom, code = string (name), true , 50 , " unknown"
659750 else
0 commit comments