@@ -169,6 +169,7 @@ def plot_usgs_raster_map(
169169 lon_min : float ,
170170 lon_max : float ,
171171 zoom : int = None ,
172+ source : str = "arcgis" ,
172173 verbose : bool = True ,
173174 mask_ocean : bool = False ,
174175 add_features : bool = True ,
@@ -180,15 +181,39 @@ def plot_usgs_raster_map(
180181 Parameters
181182 ----------
182183 lat_min : float
183- Minimum latitude of the region .
184+ Minimum latitude of the bounding box .
184185 lat_max : float
185- Maximum latitude of the region .
186+ Maximum latitude of the bounding box .
186187 lon_min : float
187- Minimum longitude of the region .
188+ Minimum longitude of the bounding box .
188189 lon_max : float
189- 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.
190209 display_width_px : int, optional
191- 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.
192217 """
193218
194219 tile_size = 256
@@ -203,11 +228,117 @@ def plot_usgs_raster_map(
203228 height = y_end - y_start + 1
204229
205230 map_img = Image .new ("RGB" , (width * tile_size , height * tile_size ))
206- 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
207334
208335 for x in range (x_start , x_end + 1 ):
209336 for y in range (y_start , y_end + 1 ):
210- 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 )
211342 try :
212343 response = requests .get (url , timeout = 10 )
213344 tile = Image .open (BytesIO (response .content ))
@@ -219,13 +350,12 @@ def plot_usgs_raster_map(
219350
220351 xmin , ymin , xmax , ymax = tile_bounds_meters (x_start , y_start , x_end , y_end , zoom )
221352
222- crs_tiles = ccrs .Mercator . GOOGLE
353+ crs_tiles = ccrs .PlateCarree ()
223354 fig = plt .figure (figsize = (12 , 8 ))
224355 ax = plt .axes (projection = crs_tiles )
225- ax .set_extent ([xmin , xmax , ymin , ymax ], crs = crs_tiles )
226-
356+ ax .set_extent ([lon_min , lon_max , lat_min , lat_max ], crs = crs_tiles )
227357 ax .imshow (
228- map_img , origin = "upper" , extent = [xmin , xmax , ymin , ymax ], transform = crs_tiles
358+ map_img , origin = "upper" , extent = [xmin , xmax , ymin , ymax ], transform = ccrs . Mercator . GOOGLE
229359 )
230360 scale = get_cartopy_scale (zoom )
231361 if verbose :
@@ -237,7 +367,7 @@ def plot_usgs_raster_map(
237367 ax .add_feature (cfeature .STATES .with_scale (scale ), linewidth = 0.5 )
238368
239369 if mask_ocean :
240- ax .add_feature (cfeature .OCEAN .with_scale (scale ), facecolor = "w" , zorder = 3 )
370+ ax .add_feature (cfeature .OCEAN .with_scale (scale ), facecolor = "w" , zorder = 1 )
241371
242372 return fig , ax
243373
@@ -256,3 +386,4 @@ def plot_usgs_raster_map(
256386 mask_ocean = True ,
257387 )
258388 fig .show ()
389+
0 commit comments