Coverage for piemap/geometry.py: 98.62%

142 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-02-04 21:26:31 +00:00

1import math 

2from typing import no_type_check 

3 

4 

5@no_type_check 

6def exact_top_of_center(angle): 

7 """ 

8 True, if angle leads to point exactly top of center 

9 """ 

10 return True if angle == 270 else False 

11 

12 

13@no_type_check 

14def exact_bottom_of_center(angle): 

15 """ 

16 True, if angle leads to point exactly bottom of center 

17 """ 

18 return True if angle == 90 else False 

19 

20 

21@no_type_check 

22def right_bottom_of_center(angle): 

23 """ 

24 True, if angle leads to point right bottom of center 

25 """ 

26 return True if angle < 90 else False 

27 

28 

29@no_type_check 

30def right_top_of_center(angle): 

31 """ 

32 True, if angle leads to point right top of center 

33 """ 

34 return True if angle > 270 else False 

35 

36 

37@no_type_check 

38def left_bottom_of_center(angle): 

39 """ 

40 True, if angle leads to point left bottom of center 

41 """ 

42 return True if 90 < angle < 180 else False 

43 

44 

45@no_type_check 

46def left_top_of_center(angle): 

47 """ 

48 True, if angle leads to point left top of center 

49 """ 

50 return True if 180 <= angle < 270 else False 

51 

52 

53@no_type_check 

54def octant_of_angle(angle): 

55 """ 

56 derive octant in (N,NW,W,SW,S,SE,E,NE) counterclockwise from angle 

57 """ 

58 try: 

59 angle = int(angle) 

60 except TypeError: 

61 return 'X' 

62 if exact_top_of_center(angle): 

63 return 'N' 

64 elif exact_bottom_of_center(angle): 

65 return 'S' 

66 elif right_bottom_of_center(angle): 

67 return 'E' if angle == 0 else 'SE' 

68 elif left_top_of_center(angle): 

69 return 'W' if angle == 180 else 'NW' 

70 elif left_bottom_of_center(angle): 

71 return 'SW' 

72 elif right_top_of_center(angle): 72 ↛ exitline 72 didn't return from function 'octant_of_angle', because the condition on line 72 was never false

73 return 'NE' 

74 

75 

76@no_type_check 

77def axis_name_circle_adjust(angle, font_size_pts, text_angle, font_name, axis_name, axis_name_space_sep=None): 

78 """ 

79 calculate axis name placements from bounding box and angle 

80 """ 

81 

82 def bounding_box_from_font(fnt_size_pts, txt_angle, fnt_name, axs_name): 

83 """ 

84 TODO(sthagen): PHP::imagettfbbox(font_size_pts, text_angle, font_name, axis_name) 

85 6,7 upper left corners X,Y-Pos and 4,5 upper right corners X,Y-Pos 

86 0,1 lower left corners X,Y-Pos and 2,3 lower right corners X,Y-Pos 

87 

88 Returns tuple with: 

89 0, 1, 2, 3, 4, 5, 6, 7 

90 lo_left_x, lo_left_y, lo_right_x, lo_right_y, hi_right_x, hi_right_y, hi_left_x, hi_left_y 

91 """ 

92 return 100, 200, 300, None, None, None, None, 400 # TODO(sthagen) implement real functionality 

93 

94 axis_name_space_sep = True if axis_name_space_sep is None else axis_name_space_sep 

95 

96 bbox = bounding_box_from_font(font_size_pts, text_angle, font_name, axis_name) 

97 text_width = abs(bbox[2] - bbox[0]) 

98 text_height = abs(bbox[7] - bbox[1]) 

99 

100 sep_x = 0 

101 sep_y = 0 

102 if axis_name_space_sep: 102 ↛ 106line 102 didn't jump to line 106, because the condition on line 102 was never false

103 sep_x = 5 

104 sep_y = 5 

105 

106 dx_pixel = 0 

107 dy_pixel = 0 

108 octant = octant_of_angle(angle) 

109 if octant == 'SE': 

110 dx_pixel = 0 + sep_x 

111 dy_pixel = +text_height + sep_y 

112 elif octant == 'NE': 

113 dx_pixel = 0 + sep_x 

114 dy_pixel = 0 - sep_y 

115 elif octant == 'SW': 

116 dx_pixel = -text_width - sep_x 

117 dy_pixel = +text_height + sep_y 

118 elif octant == 'NW': 

119 dx_pixel = -text_width - sep_x 

120 dy_pixel = 0 - sep_y 

121 elif octant == 'S': 

122 dx_pixel = -text_width / 2 

123 dy_pixel = +text_height + 2 * sep_y 

124 elif octant == 'N': 

125 # exactly on top of center 

126 dx_pixel = -text_width / 2 

127 dy_pixel = 0 - 2 * sep_y 

128 elif octant == 'W': 

129 dx_pixel = -text_width - sep_x 

130 dy_pixel = +text_height / 2 + 0 

131 elif octant == 'E': 131 ↛ 136line 131 didn't jump to line 136, because the condition on line 131 was never false

132 dx_pixel = +text_width + sep_x 

133 dy_pixel = +text_height / 2 + 0 

134 

135 # Note: Unmatched octant returns (0,0) 

136 return dx_pixel, dy_pixel 

137 

138 

139@no_type_check 

140def xy_point_from_radius_angle(radius, angle, c_x=0, c_y=0): 

141 """ 

142 return point as tuple in (x,y) = (radius,angle/[deg]) possibly shifted (c_x, c_y) 

143 """ 

144 

145 def deg2rad(deg): 

146 """TODO(sthagen) refactor later.""" 

147 return deg * math.pi / 180.0 

148 

149 if math.isfinite(angle): 

150 angle_in_rad = deg2rad(math.fmod(angle + 360, 360)) 

151 x = c_x + math.cos(angle_in_rad) * radius 

152 y = c_y + math.sin(angle_in_rad) * radius 

153 return x, y 

154 else: 

155 return math.nan, math.nan 

156 

157 

158@no_type_check 

159def segment_angle_map(needed_number_of_axis): 

160 """ 

161 return map of clockwise allocated angles for the needed axis segments 

162 from imagefilledarc: 0 degrees is located at the three-oclock position, 

163 ... and the arc is drawn clockwise. 

164 but we want one $angleMid allways point to 12 oclock, the rest 

165 ... determined by $neededNumberOfAxis 

166 and remember, we want the axis to point at 12 oclock, not start-stop 

167 so n = 1 gives (0,360,360) in theory 180 full circle to 180 

168 n = 2 gives [(270,90,360), (90,270,180)] 

169 n = 3 gives [(300,60,360), (60,180,120), (180,300,240)] 

170 """ 

171 closure_guard, norm = 0, 360 

172 

173 if needed_number_of_axis == 1: 

174 return [(closure_guard, norm, norm)] # early exit for cornercase 

175 

176 angle_per_sect = norm / needed_number_of_axis 

177 signed_correct_axis_shift = -angle_per_sect / 2 

178 triplets = [] 

179 for i in range(0, needed_number_of_axis): 

180 angle_start = math.fmod(i * angle_per_sect + signed_correct_axis_shift + norm, norm) 

181 delta = angle_start + angle_per_sect 

182 

183 angle_stop = derive_stop(angle_start, closure_guard, delta, norm) 

184 angle_mid = derive_mid(angle_start, angle_stop, norm) 

185 

186 triplets.append((angle_start, angle_stop, angle_mid)) 

187 

188 return triplets 

189 

190 

191@no_type_check 

192def transform_angle_map_ncw_icw(segment_angle_map_ncw): 

193 """ 

194 return map of adjusted angles for imagefilledarcClockWise from nativeCW 

195 from imagefilledarc: 0 degrees is located at the three-oclock position, 

196 ... and the arc is drawn clockwise. 

197 but we want one $angleMid allways point to 12 oclock, the rest 

198 ... determined by $neededNumberOfAxis 

199 so transform maps 360 deg and 0 deg to 270 deg and 90 deg to 0 deg, 

200 ... i.e. d=-90 modulo 360 

201 """ 

202 my_eps = 0.0 # 0.0000000000001 - one zero more and 12+03 oclock for n=1 

203 

204 if len(segment_angle_map_ncw) == 1: 

205 return [(270 + my_eps, 270 - my_eps, 270)] # early exit for cornercase 

206 

207 closure_guard, norm = 0, 360 

208 signed_shift_degrees = -90 

209 return rotate(closure_guard, norm, segment_angle_map_ncw, signed_shift_degrees) 

210 

211 

212@no_type_check 

213def transform_angle_map_icw_ncw(segment_angle_map_icw): 

214 """ 

215 return map of canonicalized angles for nativeClockWise from imagefilledarcCW 

216 from imagefilledarc: 0 degrees is located at the three-oclock position, 

217 ... and the arc is drawn clockwise. 

218 but we want one $angleMid allways point to 12 oclock, the rest 

219 ... determined by $neededNumberOfAxis 

220 so transform maps 360 deg and 0 deg to 270 deg and 90 deg to 0 deg, 

221 ... i.e. d=-90 modulo 360 

222 """ 

223 closure_guard, norm = 0, 360 

224 # TODO(sthagen) remove after test: my_eps = 0.0000001 

225 

226 if len(segment_angle_map_icw) == 1: 

227 return [(closure_guard, norm, norm)] # early exit for cornercase 

228 

229 signed_shift_degrees = +90 

230 return rotate(closure_guard, norm, segment_angle_map_icw, signed_shift_degrees) 

231 

232 

233@no_type_check 

234def rotate(closure_guard, norm, segment_angle_map_icw, signed_shift_degrees): 

235 """Derive triplet mappings for segments.""" 

236 triplets = [] 

237 for data in segment_angle_map_icw: 

238 angle_start, angle_stop, angle_mid = data 

239 

240 angle_start = math.fmod(angle_start + signed_shift_degrees + norm, norm) 

241 delta = angle_stop + signed_shift_degrees 

242 

243 angle_stop = derive_stop(angle_start, closure_guard, delta, norm) 

244 angle_mid = derive_mid(angle_start, angle_stop, norm) 

245 

246 triplets.append((angle_start, angle_stop, angle_mid)) 

247 

248 return triplets 

249 

250 

251@no_type_check 

252def derive_stop(angle_start, closure_guard, delta, norm): 

253 """Derive stop angle respecting closure via norm.""" 

254 angle = math.fmod(delta + norm, norm) 

255 if angle < angle_start and angle == closure_guard: 

256 angle = norm 

257 return angle 

258 

259 

260@no_type_check 

261def derive_mid(angle_start, angle_stop, norm): 

262 """Derive mid angle respectng closure via norm.""" 

263 angle = (angle_stop + angle_start) / 2.0 

264 if angle_stop < angle_start: 

265 angle = (angle_stop + norm + angle_start) / 2.0 

266 return angle