Skip to content

Commit 7e6e430

Browse files
committed
[EJP] add sources
1 parent cfd088c commit 7e6e430

File tree

1 file changed

+145
-49
lines changed

1 file changed

+145
-49
lines changed

bluemath_tk/core/plotting/WMS_USGSTopo.py

Lines changed: 145 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -163,58 +163,57 @@ def get_cartopy_scale(zoom: int) -> str:
163163
return "110m"
164164

165165

166-
def webmercator_to_lonlat(x: float, y: float) -> Tuple[float, float]:
167-
"""
168-
Converts Web Mercator projection coordinates (meters) to lon/lat.
169-
170-
Parameters
171-
----------
172-
x : float
173-
X coordinate in meters (Web Mercator).
174-
y : float
175-
Y coordinate in meters (Web Mercator).
176-
177-
Returns
178-
-------
179-
lon : float
180-
Longitude in degrees.
181-
lat : float
182-
Latitude in degrees.
183-
"""
184-
lon = math.degrees(x / EARTH_RADIUS_M)
185-
lat = math.degrees(2 * math.atan(math.exp(y / EARTH_RADIUS_M)) - math.pi / 2)
186-
return lon, lat
187-
188-
189166
def plot_usgs_raster_map(
190167
lat_min: float,
191168
lat_max: float,
192169
lon_min: float,
193170
lon_max: float,
194171
zoom: int = None,
172+
source: str = "arcgis",
195173
verbose: bool = True,
196174
mask_ocean: bool = False,
197175
add_features: bool = True,
198176
display_width_px: int = 1024,
199-
figsize: Tuple[int, int] = (12, 8),
200-
fig: plt.Figure = None,
201-
ax: plt.Axes = None,
202177
) -> Tuple[plt.Figure, plt.Axes]:
203178
"""
204179
Downloads and displays a USGS raster map for the given bounding box.
205180
206181
Parameters
207182
----------
208183
lat_min : float
209-
Minimum latitude of the region.
184+
Minimum latitude of the bounding box.
210185
lat_max : float
211-
Maximum latitude of the region.
186+
Maximum latitude of the bounding box.
212187
lon_min : float
213-
Minimum longitude of the region.
188+
Minimum longitude of the bounding box.
214189
lon_max : float
215-
Maximum longitude of the region.
190+
Maximum longitude of the bounding box.
191+
zoom : int, optional
192+
Zoom level (default is None, which auto-calculates based on bounding box).
193+
source : str, optional
194+
Source of the map tiles (default is "arcgis"). Options include:
195+
- "arcgis" (Esri World Imagery)
196+
- "google" (Google Maps Satellite)
197+
- "eox" (EOX Sentinel-2 Cloudless)
198+
- "osm" (OpenStreetMap Standard Tiles)
199+
- "amazon" (Amazon Terrarium Elevation Tiles)
200+
- "esri_world" (Esri World Imagery)
201+
- "geoportail_fr" (Geoportail France Orthophotos)
202+
- "ign_spain_pnoa" (IGN Spain PNOA Orthophotos)
203+
verbose : bool, optional
204+
If True, prints additional information about the source and zoom level.
205+
mask_ocean : bool, optional
206+
If True, masks ocean areas in the map.
207+
add_features : bool, optional
208+
If True, adds borders, coastlines, and states to the map.
216209
display_width_px : int, optional
217-
Approximate pixel width for display (default is 1024).
210+
Width of the display in pixels (default is 1024).
211+
Returns
212+
-------
213+
fig : plt.Figure
214+
The matplotlib figure containing the map.
215+
ax : plt.Axes
216+
The matplotlib axes with the map projection.
218217
"""
219218

220219
tile_size = 256
@@ -229,11 +228,117 @@ def plot_usgs_raster_map(
229228
height = y_end - y_start + 1
230229

231230
map_img = Image.new("RGB", (width * tile_size, height * tile_size))
232-
tile_url = "https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}"
231+
if source == "arcgis":
232+
tile_url = "https://services.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
233+
z_max = 19
234+
if verbose:
235+
print(
236+
"Using Esri World Imagery (ArcGIS):\n"
237+
"- Free for public and non-commercial use\n"
238+
"- Commercial use allowed under Esri terms of service\n"
239+
"- Attribution required: 'Tiles © Esri — Sources: Esri, Earthstar Geographics, CNES/Airbus DS, "
240+
"USDA, USGS, AeroGRID, IGN, and the GIS User Community'\n"
241+
"- Max zoom ~19"
242+
)
243+
244+
elif source == "google":
245+
tile_url = "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}"
246+
z_max = 21
247+
if verbose:
248+
print(
249+
"Using Google Maps Satellite:\n"
250+
"- NOT license-free\n"
251+
"- Usage outside official Google Maps SDKs/APIs is prohibited\n"
252+
"- Commercial use requires a paid license from Google\n"
253+
"- May be blocked without an API key\n"
254+
"- Max zoom ~21"
255+
)
256+
257+
elif source == "eox":
258+
tile_url = "https://tiles.maps.eox.at/wmts/1.0.0/s2cloudless-2024_3857/default/g/{z}/{y}/{x}.jpg"
259+
z_max = 16
260+
if verbose:
261+
print(
262+
"Using EOX Sentinel-2 Cloudless:\n"
263+
"- Based on Copernicus Sentinel-2 data processed by EOX\n"
264+
"- Licensed under Creative Commons BY-NC-SA 4.0 (non-commercial use only)\n"
265+
"- Attribution required: 'Sentinel-2 cloudless – © EOX, based on modified Copernicus Sentinel data 2016–2024'\n"
266+
"- Versions from 2016–2017 are under CC BY 4.0 (commercial use allowed)\n"
267+
"- Max zoom ~16"
268+
)
269+
270+
elif source == "osm":
271+
tile_url = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
272+
z_max = 19
273+
if verbose:
274+
print(
275+
"Using OpenStreetMap Standard Tiles:\n"
276+
"- Free and open-source (ODbL license)\n"
277+
"- Commercial use allowed with attribution\n"
278+
"- Attribution required: '© OpenStreetMap contributors'\n"
279+
"- Max zoom ~19"
280+
)
281+
282+
elif source == "amazon":
283+
tile_url = "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png"
284+
z_max = 15
285+
if verbose:
286+
print(
287+
"Using Amazon Terrarium Elevation Tiles:\n"
288+
"- Free for public use\n"
289+
"- Attribution recommended: 'Amazon Terrarium'\n"
290+
"- Max zoom ~15"
291+
)
292+
293+
elif source == "esri_world":
294+
tile_url = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
295+
z_max = 19
296+
if verbose:
297+
print(
298+
"Using Esri World Imagery:\n"
299+
"- High-resolution global imagery\n"
300+
"- Free with attribution under Esri terms\n"
301+
"- Max zoom ~19"
302+
)
303+
304+
elif source == "geoportail_fr":
305+
tile_url = ("https://data.geopf.fr/wmts?"
306+
"REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0"
307+
"&STYLE=normal&TILEMATRIXSET=PM&FORMAT=image/jpeg"
308+
"&LAYER=ORTHOIMAGERY.ORTHOPHOTOS&TILEMATRIX={z}"
309+
"&TILEROW={y}&TILECOL={x}")
310+
z_max = 19
311+
if verbose:
312+
print(
313+
"Using Geoportail France (Orthophotos):\n"
314+
"- Aerial orthophotos from the French National Institute (IGN)\n"
315+
"- Free for public use under Etalab license\n"
316+
"- Attribution: Geoportail France / IGN\n"
317+
"- Max zoom ~19"
318+
)
319+
elif source == "ign_spain_pnoa":
320+
tile_url = ("https://tms-pnoa-ma.idee.es/1.0.0/pnoa-ma/{z}/{x}/{y_inv}.jpeg")
321+
z_max = 19
322+
if verbose:
323+
print(
324+
"Using IGN Spain PNOA Orthophotos:\n"
325+
"- High-resolution aerial imagery (PNOA program)\n"
326+
"- Provided by IGN/CNIG (Government of Spain)\n"
327+
"- Free to use under Creative Commons BY 4.0 license\n"
328+
"- Attribution required: 'Ortofotografía PNOA – © IGN / CNIG (Gobierno de España) – CC BY 4.0'\n"
329+
"- Max zoom ~19"
330+
)
331+
332+
if zoom > z_max:
333+
zoom = z_max
233334

234335
for x in range(x_start, x_end + 1):
235336
for y in range(y_start, y_end + 1):
236-
url = tile_url.format(z=zoom, x=x, y=y)
337+
if source == "ign_spain_pnoa":
338+
y_inv = (2**zoom - 1) - y
339+
url = tile_url.format(z=zoom, x=x, y_inv=y_inv)
340+
else:
341+
url = tile_url.format(z=zoom, x=x, y=y)
237342
try:
238343
response = requests.get(url, timeout=10)
239344
tile = Image.open(BytesIO(response.content))
@@ -244,30 +349,21 @@ def plot_usgs_raster_map(
244349
print(f"Error fetching tile {x},{y}: {e}")
245350

246351
xmin, ymin, xmax, ymax = tile_bounds_meters(x_start, y_start, x_end, y_end, zoom)
247-
lon_min, lat_min = webmercator_to_lonlat(xmin, ymin)
248-
lon_max, lat_max = webmercator_to_lonlat(xmax, ymax)
249-
250-
scale = get_cartopy_scale(zoom)
251-
if verbose:
252-
print(f"Using Cartopy scale: {scale}")
253352

254353
crs_tiles = ccrs.PlateCarree()
255-
if fig is None or ax is None:
256-
fig = plt.figure(figsize=figsize)
257-
ax = plt.axes(projection=crs_tiles)
258-
354+
fig = plt.figure(figsize=(12, 8))
355+
ax = plt.axes(projection=crs_tiles)
259356
ax.set_extent([lon_min, lon_max, lat_min, lat_max], crs=crs_tiles)
260-
261357
ax.imshow(
262-
map_img,
263-
origin="upper",
264-
extent=[xmin, xmax, ymin, ymax],
265-
transform=ccrs.Mercator.GOOGLE,
358+
map_img, origin="upper", extent=[xmin, xmax, ymin, ymax], transform=ccrs.Mercator.GOOGLE
266359
)
360+
scale = get_cartopy_scale(zoom)
361+
if verbose:
362+
print(f"Using Cartopy scale: {scale}")
267363

268364
if add_features:
269365
ax.add_feature(cfeature.BORDERS.with_scale(scale), linewidth=0.8)
270-
ax.add_feature(cfeature.COASTLINE.with_scale(scale), zorder=1)
366+
ax.add_feature(cfeature.COASTLINE.with_scale(scale))
271367
ax.add_feature(cfeature.STATES.with_scale(scale), linewidth=0.5)
272368

273369
if mask_ocean:

0 commit comments

Comments
 (0)