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

1#!/usr/bin/env python3 

2 

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 

31 

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. 

35 

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 

88 

89 Returns 

90 ------- 

91 tilesIm : PIL.Image 

92 the merged tile 

93 

94 Notes 

95 ----- 

96 Copyright 2017 Thomas Guymer [1]_ 

97 

98 References 

99 ---------- 

100 .. [1] PyGuymer3, https://github.com/Guymer/PyGuymer3 

101 """ 

102 

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 

111 

112 # Import sub-functions ... 

113 from .deg2num import deg2num 

114 from .num2deg import num2deg 

115 from .tile import tile 

116 

117 # Check inputs ... 

118 if not 0 <= zoom <= 19: 

119 raise Exception(f"\"zoom\" is not in the required range ({zoom:d})") from None 

120 

121 # Populate default values ... 

122 if cookies is None: 

123 cookies = {} 

124 if headers is None: 

125 headers = {} 

126 

127 # ************************************************************************** 

128 

129 # Create short-hands ... 

130 n = pow(2, zoom) 

131 tileSize = scale * 256 # [px] 

132 

133 # Find which tile contains the location ... 

134 xtileC, ytileC = deg2num(lonC_deg, latC_deg, zoom) # [#], [#] 

135 

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] 

143 

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 # [#] 

149 

150 # ************************************************************************** 

151 

152 # Make blank map ... 

153 tilesIm = PIL.Image.new( 

154 "RGB", 

155 (width, height), 

156 background, 

157 ) 

158 

159 # Make drawing object, if the user wants to draw circle ... 

160 if radius is not None: 

161 draw = PIL.ImageDraw.Draw(tilesIm, "RGBA") 

162 

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] 

167 

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] 

172 

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 ) 

193 

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 

199 

200 # Paste the tile onto the map ... 

201 tilesIm.paste(tileIm, (x, y)) 

202 

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 ) 

209 

210 # Return answer ... 

211 return tilesIm