Coverage for pyguymer3/geo/great_circle.py: 63%

86 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 great_circle( 

5 lon1, 

6 lat1, 

7 lon2, 

8 lat2, 

9 /, 

10 *, 

11 debug = __debug__, 

12 eps = 1.0e-12, 

13 maxdist = None, 

14 nIter = 100, 

15 npoint = None, 

16 prefix = ".", 

17 ramLimit = 1073741824, 

18): 

19 """Calculate the great circle that connects two coordinates. 

20 

21 This function reads in two coordinates (in degrees) on the surface of the 

22 Earth and calculates the great circle that connects them, correctly handling 

23 crossing over the anti-meridian (should it occur). 

24 

25 Parameters 

26 ---------- 

27 lon1 : float 

28 the longitude of the first coordinate (in degrees) 

29 lat1 : float 

30 the latitude of the first coordinate (in degrees) 

31 lon2 : float 

32 the longitude of the second coordinate (in degrees) 

33 lat2 : float 

34 the latitude of the second coordinate (in degrees) 

35 debug : bool, optional 

36 print debug messages (in metres) 

37 eps : float, optional 

38 the tolerance of the Vincenty formula iterations 

39 maxdist : float, optional 

40 the maximum distance between points along the great circle 

41 nIter : int, optional 

42 the maximum number of iterations (particularly the Vincenty formula) 

43 npoint : int, optional 

44 the number of points along the great circle 

45 prefix : str, optional 

46 change the name of the output debugging CSVs 

47 ramLimit : int, optional 

48 the maximum RAM usage of each "large" array (in bytes) 

49 

50 Returns 

51 ------- 

52 line : shapely.geometry.linestring.LineString, shapely.geometry.multilinestring.MultiLineString 

53 the great circle 

54 

55 Notes 

56 ----- 

57 If neither maxdist nor npoint are provided then npoint will default to 5. 

58 For large great circles, prettier results are obtained by using maxdist 

59 rather than npoint. 

60 

61 Copyright 2017 Thomas Guymer [1]_ 

62 

63 References 

64 ---------- 

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

66 """ 

67 

68 # Import special modules ... 

69 try: 

70 import numpy 

71 except: 

72 raise Exception("\"numpy\" is not installed; run \"pip install --user numpy\"") from None 

73 try: 

74 import shapely 

75 import shapely.geometry 

76 except: 

77 raise Exception("\"shapely\" is not installed; run \"pip install --user Shapely\"") from None 

78 

79 # Import sub-functions ... 

80 from .calc_dist_between_two_locs import calc_dist_between_two_locs 

81 from .check import check 

82 from .find_middle_of_great_circle import find_middle_of_great_circle 

83 from .find_point_on_great_circle import find_point_on_great_circle 

84 from ..interpolate import interpolate 

85 from ..consts import CIRCUMFERENCE_OF_EARTH 

86 

87 # ************************************************************************** 

88 

89 # Set default value ... 

90 if maxdist is None and npoint is None: 

91 npoint = 5 # [#] 

92 

93 # Check inputs ... 

94 if isinstance(maxdist, float): 

95 assert maxdist >= 10.0, f"the maximum distance is too small ({maxdist:,.1f}m < {10.0:,.1f}m)" 

96 assert maxdist <= 0.5 * CIRCUMFERENCE_OF_EARTH, f"the maximum distance is too large ({maxdist:,.1f}m > {0.5 * CIRCUMFERENCE_OF_EARTH:,.1f}m)" 

97 elif isinstance(npoint, int): 

98 assert npoint >= 3, f"the number of points is too small ({npoint:,d} < {3:,d})" 

99 else: 

100 raise Exception("\"maxdist\" is not a float and \"npoint\" is not an integer") from None 

101 

102 # ************************************************************************** 

103 

104 # Skip if the start- and end-points are the same ... 

105 if lon1 == lon2 and lat1 == lat2: 

106 return None 

107 

108 # Check if the user wants to make the great circle using either "maxdist" or 

109 # "npoints" ... 

110 if isinstance(maxdist, float): 

111 # Create a poorly resolved great circle ... 

112 points = [ 

113 (lon1, lat1), 

114 find_point_on_great_circle(0.2, lon1, lat1, lon2, lat2), 

115 find_point_on_great_circle(0.4, lon1, lat1, lon2, lat2), 

116 find_point_on_great_circle(0.6, lon1, lat1, lon2, lat2), 

117 (lon2, lat2), 

118 ] # [°], [°] 

119 

120 # Initialize flag ... 

121 addedPoint = True 

122 

123 # Commence infinite loop ... 

124 while addedPoint: 

125 # Reset the flag .. 

126 addedPoint = False 

127 

128 # Loop over points ... 

129 for ipoint in range(1, len(points)): 

130 # Calculate the distance between this point and the previous one ... 

131 dist, _, _ = calc_dist_between_two_locs( 

132 points[ipoint - 1][0], 

133 points[ipoint - 1][1], 

134 points[ipoint][0], 

135 points[ipoint][1], 

136 eps = eps, 

137 nIter = nIter, 

138 ) # [m] 

139 

140 # Check if the distance is too large ... 

141 if dist > maxdist: 

142 # Replace the list of points with one which has a new 

143 # intermediate point inserted ... 

144 points = points[:ipoint] + [ 

145 find_middle_of_great_circle( 

146 points[ipoint - 1][0], 

147 points[ipoint - 1][1], 

148 points[ipoint][0], 

149 points[ipoint][1], 

150 ) 

151 ] + points[ipoint:] # [°], [°] 

152 

153 # Set the flag ... 

154 addedPoint = True 

155 

156 # Stop looping over points and start the survey again ... 

157 break 

158 

159 # Convert list to array and clean up ... 

160 circle = numpy.array(points, dtype = numpy.float64) # [°] 

161 del points 

162 elif isinstance(npoint, int): 

163 # Check array size ... 

164 if npoint * 2 * 8 > ramLimit: 

165 raise Exception(f"\"circle\" is going to be {npoint * 2 * 8:,d} bytes, which is larger than {ramLimit:,d} bytes") from None 

166 

167 # Initialize array ... 

168 circle = numpy.zeros((npoint, 2), dtype = numpy.float64) # [°] 

169 

170 # Loop over points ... 

171 for ipoint in range(npoint): 

172 # Set point ... 

173 circle[ipoint, :] = find_point_on_great_circle(float(ipoint) / float(npoint - 1), lon1, lat1, lon2, lat2) # [°] 

174 else: 

175 raise Exception("\"maxdist\" is not a float and \"npoint\" is not an integer") from None 

176 

177 # ************************************************************************** 

178 

179 # Check if the great circle crosses the anti-meridian W to E ... 

180 if lon2 > lon1 and (circle[1, 0] < lon1 or circle[-2, 0] > lon2): 

181 if debug: 

182 print("INFO: The great circle crosses the anti-meridian (point #2 is E of point #1 but the great circle goes W).") 

183 

184 # Find where it crosses the anti-meridian ... 

185 i = numpy.diff(circle[:, 0]).argmax() 

186 

187 # Calculate the first intersection and convert to a LineString ... 

188 x = -180.0 # [°] 

189 y = interpolate(circle[i, 0], circle[i + 1, 0] - 360.0, circle[i, 1], circle[i + 1, 1], x) # [°] 

190 line1 = shapely.geometry.linestring.LineString(numpy.append(circle[:i + 1, :], [[x, y]], axis = 0)) 

191 if debug: 

192 check(line1, prefix = prefix) 

193 

194 # Calculate the second intersection and convert to a LineString ... 

195 x = +180.0 # [°] 

196 y = interpolate(circle[i, 0] + 360.0, circle[i + 1, 0], circle[i, 1], circle[i + 1, 1], x) # [°] 

197 line2 = shapely.geometry.linestring.LineString(numpy.append([[x, y]], circle[i + 1:, :], axis = 0)) 

198 if debug: 

199 check(line2, prefix = prefix) 

200 

201 # Convert to a MultiLineString ... 

202 multiline = shapely.geometry.multilinestring.MultiLineString([line1, line2]) 

203 if debug: 

204 check(multiline, prefix = prefix) 

205 

206 # Return answer ... 

207 return multiline 

208 

209 # Check if the great circle crosses the anti-meridian E to W ... 

210 if lon2 < lon1 and (circle[1, 0] > lon1 or circle[-2, 0] < lon2): 

211 if debug: 

212 print("INFO: The great circle crosses the anti-meridian (point #2 is W of point #1 but the great circle goes E).") 

213 

214 # Find where it crosses the anti-meridian ... 

215 i = numpy.diff(circle[:, 0]).argmin() 

216 

217 # Calculate the first intersection and convert to a LineString ... 

218 x = +180.0 # [°] 

219 y = interpolate(circle[i, 0], circle[i + 1, 0] + 360.0, circle[i, 1], circle[i + 1, 1], x) # [°] 

220 line1 = shapely.geometry.linestring.LineString(numpy.append(circle[:i + 1, :], [[x, y]], axis = 0)) 

221 if debug: 

222 check(line1, prefix = prefix) 

223 

224 # Calculate the second intersection and convert to a LineString ... 

225 x = -180.0 # [°] 

226 y = interpolate(circle[i, 0] - 360.0, circle[i + 1, 0], circle[i, 1], circle[i + 1, 1], x) # [°] 

227 line2 = shapely.geometry.linestring.LineString(numpy.append([[x, y]], circle[i + 1:, :], axis = 0)) 

228 if debug: 

229 check(line2, prefix = prefix) 

230 

231 # Convert to a MultiLineString ... 

232 multiline = shapely.geometry.multilinestring.MultiLineString([line1, line2]) 

233 if debug: 

234 check(multiline, prefix = prefix) 

235 

236 # Return answer ... 

237 return multiline 

238 

239 # ************************************************************************** 

240 

241 # Convert to a LineString ... 

242 line = shapely.geometry.linestring.LineString(circle) 

243 if debug: 

244 check(line, prefix = prefix) 

245 

246 # Return answer ... 

247 return line