Skip to content

Commit 6d324b5

Browse files
authored
Merge pull request #2000 from GenericMappingTools/mosaic-ortosat23
Let mosaic() also deal with the Ortosat23 Portuguese imagerie via new provider.
2 parents 953f405 + dcd5994 commit 6d324b5

3 files changed

Lines changed: 106 additions & 9 deletions

File tree

src/extras/webmapserver.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ returned by the first form.
4141
"""
4242
function wmsinfo(server::String)::WMS
4343
w = gdalinfo("WMS:" * server)
44-
(w === nothing) && error("unable to open '$(server)'")
44+
(w === nothing || w === "") && error("unable to open '$(server)'")
4545
sds = split(w, "SUBDATASET_")
4646
lastguy = collect(2:2:length(sds))[end]
4747

src/gdal_utils.jl

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,7 +1067,10 @@ function lonlat2xy(xy::Matrix{T}, t_srs_=nothing; t_srs=nothing, s_srs=prj4WGS84
10671067
opts = (size(xy,2) == 2) ? ["-s_srs", s_srs, "-t_srs", t_srs, "-overwrite"] :
10681068
["-s_srs", s_srs, "-t_srs", t_srs, "-overwrite", "-dim", "XYZ"]
10691069
D = ogr2ogr(xy, opts)
1070-
return D.data # Return only the array because that's what was sent in
1070+
# ogr2ogr/gd2gmt returns a single GMTdataset when the input has one point, but a
1071+
# Vector{GMTdataset} when it has several (each row becomes a separate point feature).
1072+
# Aggregate back into a single matrix matching the input shape.
1073+
return isa(D, Vector) ? reduce(vcat, (d.data for d in D)) : D.data
10711074
end
10721075

10731076
lonlat2xy(D::GMTdataset, t_srs_=nothing; t_srs=nothing, s_srs=prj4WGS84) = lonlat2xy([D], t_srs_; t_srs=t_srs, s_srs=s_srs)
@@ -1118,7 +1121,10 @@ function xy2lonlat(xy::Matrix{<:Real}, s_srs_=""; s_srs="", t_srs=prj4WGS84)
11181121
(s_srs == "") && error("Must specify at least the source referencing system.")
11191122
D = ogr2ogr(xy, ["-s_srs", s_srs, "-t_srs", t_srs, "-overwrite"])
11201123
isa(D, Vector) && contains(s_srs, "=lee_os") && length(D) > 1 && return [D[1][1,1] D[1][1,2]; D[2][2,1] D[2][2,2]] # GDAL BUG?
1121-
return D.data # Return only the array because that's what was sent in
1124+
# ogr2ogr/gd2gmt returns a single GMTdataset when the input has one point, but a
1125+
# Vector{GMTdataset} when it has several (each row becomes a separate point feature).
1126+
# Aggregate back into a single matrix matching the input shape.
1127+
return isa(D, Vector) ? reduce(vcat, (d.data for d in D)) : D.data
11221128
end
11231129

11241130
xy2lonlat(D::GMTdataset, s_srs_=""; s_srs="", t_srs=prj4WGS84) = xy2lonlat([D], s_srs_; s_srs=s_srs, t_srs=t_srs)

src/imgtiles.jl

Lines changed: 97 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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
514530
end
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
587660
is 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
"""
609693
getprovider(name::Tuple{String,String}, zoom::Int; date::String="", key="") = getprovider(name[1], zoom; variant=name[2], date=date, key=key)
610694
function 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

Comments
 (0)