11import math
2- import requests
32from io import BytesIO
4- from PIL import Image
5- import matplotlib . pyplot as plt
3+ from typing import Tuple
4+
65import cartopy .crs as ccrs
76import cartopy .feature as cfeature
7+ import matplotlib .pyplot as plt
8+ import requests
9+ from PIL import Image
10+
11+ from ..constants import EARTH_RADIUS
12+
13+ EARTH_RADIUS_M = EARTH_RADIUS * 1000 # Convert km to m
814
915
10- def deg2num (lat : float , lon : float , zoom : int ) -> tuple [int , int ]:
16+ def deg2num (lat : float , lon : float , zoom : int ) -> Tuple [int , int ]:
1117 """
1218 Converts geographic coordinates to tile numbers for a given zoom level.
1319
@@ -27,16 +33,18 @@ def deg2num(lat: float, lon: float, zoom: int) -> tuple[int, int]:
2733 ytile : int
2834 Tile number in y-direction.
2935 """
36+
3037 lat_rad = math .radians (lat )
3138 n = 2.0 ** zoom
3239 xtile = int ((lon + 180.0 ) / 360.0 * n )
3340 ytile = int (
3441 (1.0 - math .log (math .tan (lat_rad ) + 1 / math .cos (lat_rad )) / math .pi ) / 2.0 * n
3542 )
43+
3644 return xtile , ytile
3745
3846
39- def num2deg (xtile : int , ytile : int , zoom : int ) -> tuple [float , float ]:
47+ def num2deg (xtile : int , ytile : int , zoom : int ) -> Tuple [float , float ]:
4048 """
4149 Converts tile numbers back to geographic coordinates.
4250
@@ -56,14 +64,16 @@ def num2deg(xtile: int, ytile: int, zoom: int) -> tuple[float, float]:
5664 lon : float
5765 Longitude in degrees.
5866 """
67+
5968 n = 2.0 ** zoom
6069 lon = xtile / n * 360.0 - 180.0
6170 lat_rad = math .atan (math .sinh (math .pi * (1 - 2 * ytile / n )))
6271 lat = math .degrees (lat_rad )
72+
6373 return lat , lon
6474
6575
66- def lonlat_to_webmercator (lon : float , lat : float ) -> tuple [float , float ]:
76+ def lonlat_to_webmercator (lon : float , lat : float ) -> Tuple [float , float ]:
6777 """
6878 Converts lon/lat to Web Mercator projection coordinates in meters.
6979
@@ -81,15 +91,16 @@ def lonlat_to_webmercator(lon: float, lat: float) -> tuple[float, float]:
8191 y : float
8292 Y coordinate in meters.
8393 """
84- R = 6378137.0
85- x = R * math .radians (lon )
86- y = R * math .log (math .tan ((math .pi / 4 ) + math .radians (lat ) / 2 ))
94+
95+ x = EARTH_RADIUS_M * math .radians (lon )
96+ y = EARTH_RADIUS_M * math .log (math .tan ((math .pi / 4 ) + math .radians (lat ) / 2 ))
97+
8798 return x , y
8899
89100
90101def tile_bounds_meters (
91102 x_start : int , y_start : int , x_end : int , y_end : int , zoom : int
92- ) -> tuple [float , float , float , float ]:
103+ ) -> Tuple [float , float , float , float ]:
93104 """
94105 Computes the bounding box of the tile region in Web Mercator meters.
95106
@@ -98,10 +109,12 @@ def tile_bounds_meters(
98109 xmin, ymin, xmax, ymax : float
99110 Bounding box in meters (Web Mercator projection).
100111 """
112+
101113 lat1 , lon1 = num2deg (x_start , y_start , zoom )
102114 lat2 , lon2 = num2deg (x_end + 1 , y_end + 1 , zoom )
103115 x1 , y1 = lonlat_to_webmercator (lon1 , lat2 )
104116 x2 , y2 = lonlat_to_webmercator (lon2 , lat1 )
117+
105118 return x1 , y1 , x2 , y2
106119
107120
@@ -116,12 +129,14 @@ def calculate_zoom(
116129 zoom : int
117130 Estimated zoom level.
118131 """
119- WORLD_MAP_WIDTH = 2 * math .pi * 6378137
132+
133+ WORLD_MAP_WIDTH = 2 * math .pi * EARTH_RADIUS_M
120134 x1 , _ = lonlat_to_webmercator (lon_min , 0 )
121135 x2 , _ = lonlat_to_webmercator (lon_max , 0 )
122136 region_width_m = abs (x2 - x1 )
123137 meters_per_pixel_desired = region_width_m / display_width_px
124138 zoom = math .log2 (WORLD_MAP_WIDTH / (tile_size * meters_per_pixel_desired ))
139+
125140 return int (round (zoom ))
126141
127142
@@ -139,6 +154,7 @@ def get_cartopy_scale(zoom: int) -> str:
139154 scale : str
140155 One of '110m', '50m', or '10m'.
141156 """
157+
142158 if zoom >= 9 :
143159 return "10m"
144160 elif zoom >= 6 :
@@ -157,7 +173,7 @@ def plot_usgs_raster_map(
157173 mask_ocean : bool = False ,
158174 add_features : bool = True ,
159175 display_width_px : int = 1024 ,
160- ) -> tuple [plt .Figure , plt .Axes ]:
176+ ) -> Tuple [plt .Figure , plt .Axes ]:
161177 """
162178 Downloads and displays a USGS raster map for the given bounding box.
163179
@@ -174,6 +190,7 @@ def plot_usgs_raster_map(
174190 display_width_px : int, optional
175191 Approximate pixel width for display (default is 1024).
176192 """
193+
177194 tile_size = 256
178195 if zoom is None :
179196 zoom = calculate_zoom (lon_min , lon_max , display_width_px , tile_size )
@@ -221,4 +238,21 @@ def plot_usgs_raster_map(
221238
222239 if mask_ocean :
223240 ax .add_feature (cfeature .OCEAN .with_scale (scale ), facecolor = "w" , zorder = 3 )
224- return fig , ax
241+
242+ return fig , ax
243+
244+
245+ if __name__ == "__main__" :
246+ lat_min , lat_max = 35.406 , 54.372
247+ lon_min , lon_max = - 13.148 , 12.325
248+ fig , ax = plot_usgs_raster_map (
249+ lat_min ,
250+ lat_max ,
251+ lon_min ,
252+ lon_max ,
253+ zoom = None ,
254+ verbose = True ,
255+ add_features = False ,
256+ mask_ocean = True ,
257+ )
258+ fig .show ()
0 commit comments