Coverage for pyguymer3/openstreetmap/tiles.py: 2%
43 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-08 18:47 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-08 18:47 +0000
1#!/usr/bin/env python3
3# Define function ...
4def tiles(
5 lonC_deg,
6 latC_deg,
7 zoom,
8 width,
9 height,
10 sess,
11 /,
12 *,
13 background = (255, 255, 255),
14 chunksize = 1048576,
15 cookies = None,
16 debug = __debug__,
17 exiftoolPath = None,
18 fill = (255, 0, 0, 127),
19 gifsiclePath = None,
20 headers = None,
21 jpegtranPath = None,
22 optipngPath = None,
23 radius = None,
24 scale = 1,
25 thunderforestKey = None,
26 thunderforestMap = "atlas",
27 timeout = 10.0,
28 verify = True,
29):
30 """Merge some OpenStreetMap tiles around a location into one large tile
32 This function reads in a location, a zoom and an image size. It then fetches
33 all of the OpenStreetMap tiles that are in that field-of-view and returns an
34 image of them all merged together.
36 Parameters
37 ----------
38 lonC_deg : float
39 the central longitude (in degrees)
40 latC_deg : float
41 the central latitude (in degrees)
42 zoom : int
43 the OpenStreetMap zoom level
44 width : int
45 the width of the merged tile (in pixels)
46 height : int
47 the height of the merged tile (in pixels)
48 sess : requests.Session
49 the session for any requests calls
50 background : tuple of int, optional
51 the background colour of the merged tile
52 chunksize : int, optional
53 the size of the chunks of any files which are read in (in bytes)
54 cookies : dict, optional
55 extra cookies for any requests calls
56 debug : bool, optional
57 print debug messages
58 exiftoolPath : str, optional
59 the path to the "exiftool" binary (if not provided then Python will
60 attempt to find the binary itself)
61 fill : tuple of int, optional
62 the fill colour of the circle around the central location, if drawn
63 gifsiclePath : str, optional
64 the path to the "gifsicle" binary (if not provided then Python will
65 attempt to find the binary itself)
66 headers : dict, optional
67 extra headers for any requests calls
68 jpegtranPath : str, optional
69 the path to the "jpegtran" binary (if not provided then Python will
70 attempt to find the binary itself)
71 optipngPath : str, optional
72 the path to the "optipng" binary (if not provided then Python will
73 attempt to find the binary itself)
74 radius : int, optional
75 the radius of the circle around the central location, if None then no
76 circle is drawn (in pixels)
77 scale : int, optional
78 the scale of the tiles
79 thunderforestKey : string, optional
80 your personal API key for the Thunderforest service (if provided then it
81 is assumed that you want to use the Thunderforest service)
82 thunderforestMap : string, optional
83 the Thunderforest map style (see https://www.thunderforest.com/maps/)
84 timeout : float, optional
85 the timeout for any requests/subprocess calls (in seconds)
86 verify : bool, optional
87 verify the server's certificates for any requests calls
89 Returns
90 -------
91 tilesIm : PIL.Image
92 the merged tile
94 Notes
95 -----
96 Copyright 2017 Thomas Guymer [1]_
98 References
99 ----------
100 .. [1] PyGuymer3, https://github.com/Guymer/PyGuymer3
101 """
103 # Import special modules ...
104 try:
105 import PIL
106 import PIL.Image
107 PIL.Image.MAX_IMAGE_PIXELS = 1024 * 1024 * 1024 # [px]
108 import PIL.ImageDraw
109 except:
110 raise Exception("\"PIL\" is not installed; run \"pip install --user Pillow\"") from None
112 # Import sub-functions ...
113 from .deg2num import deg2num
114 from .num2deg import num2deg
115 from .tile import tile
117 # Check inputs ...
118 if not 0 <= zoom <= 19:
119 raise Exception(f"\"zoom\" is not in the required range ({zoom:d})") from None
121 # Populate default values ...
122 if cookies is None:
123 cookies = {}
124 if headers is None:
125 headers = {}
127 # **************************************************************************
129 # Create short-hands ...
130 n = pow(2, zoom)
131 tileSize = scale * 256 # [px]
133 # Find which tile contains the location ...
134 xtileC, ytileC = deg2num(lonC_deg, latC_deg, zoom) # [#], [#]
136 # Find where exactly the location is within the central tile (assuming that
137 # both the Euclidean and Geodesic spaces are both rectilinear and uniform
138 # within a single tile) ...
139 lonCW_deg, latCN_deg = num2deg(xtileC, ytileC, zoom) # [°], [°]
140 lonCE_deg, latCS_deg = num2deg(xtileC + 1, ytileC + 1, zoom) # [°], [°]
141 xoffset = int(float(tileSize) * (lonCW_deg - lonC_deg) / (lonCW_deg - lonCE_deg)) # [px]
142 yoffset = int(float(tileSize) * (latCN_deg - latC_deg) / (latCN_deg - latCS_deg)) # [px]
144 # Find out where to start and finish the loops ...
145 xtileW = xtileC - (width // 2 // tileSize) - 1 # [#]
146 xtileE = xtileC + (width // 2 // tileSize) + 1 # [#]
147 ytileN = ytileC - (height // 2 // tileSize) - 1 # [#]
148 ytileS = ytileC + (height // 2 // tileSize) + 1 # [#]
150 # **************************************************************************
152 # Make blank map ...
153 tilesIm = PIL.Image.new(
154 "RGB",
155 (width, height),
156 background,
157 )
159 # Make drawing object, if the user wants to draw circle ...
160 if radius is not None:
161 draw = PIL.ImageDraw.Draw(tilesIm, "RGBA")
163 # Loop over columns ...
164 for xtile in range(xtileW, xtileE + 1):
165 # Find where to put the top-left corner of this tile ...
166 x = width // 2 - xoffset - (xtileC - xtile) * tileSize # [px]
168 # Loop over rows ...
169 for ytile in range(ytileN, ytileS + 1):
170 # Find where to put the top-left corner of this tile ...
171 y = height // 2 - yoffset - (ytileC - ytile) * tileSize # [px]
173 # Obtain the tile ...
174 tileIm = tile(
175 xtile % n,
176 ytile % n,
177 zoom,
178 sess,
179 chunksize = chunksize,
180 cookies = cookies,
181 debug = debug,
182 exiftoolPath = exiftoolPath,
183 gifsiclePath = gifsiclePath,
184 headers = headers,
185 jpegtranPath = jpegtranPath,
186 optipngPath = optipngPath,
187 scale = scale,
188 thunderforestKey = thunderforestKey,
189 thunderforestMap = thunderforestMap,
190 timeout = timeout,
191 verify = verify,
192 )
194 # Check if the tile doesn't exist ...
195 if tileIm is None:
196 # Skip this tile ...
197 print(f"WARNING: Failed to obtain the tile for x={xtile % n:d}, y={ytile % n:d}, scale={scale:d} and zoom={zoom:d}.")
198 continue
200 # Paste the tile onto the map ...
201 tilesIm.paste(tileIm, (x, y))
203 # Draw circle, if the user wants to ...
204 if radius is not None:
205 draw.ellipse(
206 [width // 2 - radius, height // 2 - radius, width // 2 + radius, height // 2 + radius],
207 fill = fill,
208 )
210 # Return answer ...
211 return tilesIm