litchi_wp.waypoint
Module for working with the litchi csv waypoints
1""" 2Module for working with the litchi csv waypoints 3""" 4# pylint: disable=import-error,too-many-arguments,too-many-instance-attributes 5import re 6from litchi_wp.action import Action, ActionType 7from litchi_wp.altitude import Altitude, AltitudeMode 8from litchi_wp.enums import RotationDirection, RegEx 9from litchi_wp.gimbal import Gimbal, GimbalMode 10from litchi_wp.photo import Photo 11from litchi_wp.poi import Poi 12 13 14class Waypoint: 15 """ 16 Class representing a litchi waypoint 17 18 Attributes: 19 lat (float): Latitude coordinate for the waypoint in WGS84 format 20 lon (float): Longitude coordinate for the waypoint in WGS84 format 21 altitude (Altitude): Altitude object 22 heading (float): Heading in degrees 23 curvesize (float): Curvesize (radius?) in meter 24 rotationdir (RotationDirection): 25 Direction of rotation ??(0=cw, 1=ccw)?? (always 0 from mission hub) 26 gimbal (Gimbal): Gimbal settings 27 actions (list[Action]): List of Action objects 28 speed (float): Speed in meters per second 29 poi (Poi): Poi object 30 photo (Photo): Photo object 31 next_action_index (int): The index of the next free action slot (max 14) 32 33 Raises: 34 ValueError: If [lat, lon, alt, head, curve, speed] cannot be float 35 or rot is no valid RotationDirection 36 37 """ 38 next_action_index = 0 39 40 def __init__( 41 self, 42 lat: float, 43 lon: float, 44 alt: float, 45 head: float = 180, 46 curve: float = 0, 47 rot: RotationDirection = RotationDirection.CW, 48 speed: float = 0, 49 ): 50 self.lat = float(lat) 51 self.lon = float(lon) 52 self.altitude = Altitude(value=alt) 53 self.heading = float(head) 54 self.curvesize = float(curve) 55 self.rotationdir = RotationDirection(rot) 56 self.gimbal = Gimbal() 57 self.actions = [Action() for i in range(0, 15)] 58 self.speed = float(speed) 59 self.poi = Poi() 60 self.photo = Photo() 61 62 def set_coordinates(self, lat: float, lon: float): 63 """ 64 Setter for coordinates 65 66 Args: 67 lat (float): Latitude coordinate for the waypoint in WGS84 format 68 lon (float): Longitude coordinate for the waypoint in WGS84 format 69 70 Raises: 71 ValueError: If lat cannot be float or lon cannot be float 72 73 """ 74 self.lat = float(lat) 75 self.lon = float(lon) 76 77 def set_altitude(self, value: float, mode: AltitudeMode = AltitudeMode.AGL): 78 """ 79 Setter for altitude 80 81 Args: 82 value (float): The height in meters 83 mode (AltitudeMode): The altitude mode (MSL or AGL) 84 85 Raises: 86 ValueError: If value cannot be float or mode is no valid AltitudeMode 87 88 """ 89 self.altitude.set_value(float(value)) 90 self.altitude.set_mode(AltitudeMode(mode)) 91 92 def set_heading(self, value: float): 93 """ 94 Setter for heading 95 96 Args: 97 value (float): The heading in degrees (0 = north, 180 = south) 98 99 Raises: 100 ValueError: If value cannot be float 101 102 """ 103 self.heading = float(value) 104 105 def set_curvesize(self, value: float): 106 """ 107 Setter for curve size 108 109 Args: 110 value (float): The curve radius in meters 111 112 Raises: 113 ValueError: If value cannot be float 114 115 """ 116 self.curvesize = abs(float(value)) 117 118 def set_rotation_direction(self, value: RotationDirection = RotationDirection.CW): 119 """ 120 Setter for the rotation direction 121 122 Args: 123 value (RotationDirection): Clockwise or Counterclockwise rotation 124 125 Raises: 126 ValueError: If value is not a valid RotationDirection 127 128 """ 129 self.rotationdir = RotationDirection(value) 130 131 def set_gimbal(self, mode: GimbalMode, pitchangle: float = 0): 132 """ 133 Setter for gimbal mode 134 135 Args: 136 mode (GimbalMode): The mode setting for the gimbal 137 pitchangle (float): The angle for the gimbal (only for interpolate mode) 138 139 Raises: 140 ValueError: If mode is no valid GimbalMode. Or mode is GimbalMode.Interpolate and 141 (pitchangle cannot be float or pitchangle < -90 or pitchangle > 30) 142 143 """ 144 match GimbalMode(mode): 145 case GimbalMode.DISABLED: 146 self.gimbal.set_mode(GimbalMode.DISABLED) 147 case GimbalMode.FOCUS_POI: 148 self.gimbal.set_focus_poi() 149 case GimbalMode.INTERPOLATE: 150 self.gimbal.set_interpolate(pitchangle) 151 152 def set_speed_ms(self, value: float): 153 """ 154 Setter for speed in meters per second 155 156 Args: 157 value (float): The speed in meters per second 158 159 Raises: 160 ValueError: If value cannot be float 161 162 """ 163 self.speed = float(value) 164 165 def set_speed_kmh(self, value: float): 166 """ 167 Setter for speed in kilometers per hour 168 169 Args: 170 value (float): The speed in kilometers per hour 171 172 Raises: 173 ValueError: If value cannot be float 174 175 """ 176 self.speed = float(value) / 3.6 177 178 def set_poi( 179 self, 180 lat: float, 181 lon: float, 182 alt: float, 183 alt_mode: AltitudeMode = AltitudeMode.MSL 184 ): 185 """ 186 Setter for point of interest 187 Args: 188 lat (float): Latitude coordinate for the waypoint in WGS84 format 189 lon (float): Longitude coordinate for the waypoint in WGS84 format 190 alt (float): The altitude in meters 191 alt_mode (AltitudeMode): The altitudemode, MSL or AGL 192 193 Raises: 194 ValueError: If [lat, lon, alt] cannot be float or alt_mode is no valid AltitudeMode 195 196 """ 197 self.poi.set_coordinates(float(lat), float(lon)) 198 self.poi.set_altitude(float(alt)) 199 self.poi.set_altitude_mode(AltitudeMode(alt_mode)) 200 201 def set_photo_interval_time(self, seconds: float): 202 """ 203 Setter for photo time interval 204 205 Args: 206 seconds (float): The time in seconds between each photo 207 208 Raises: 209 ValueError: If seconds cannot be float 210 211 """ 212 self.photo.set_time_interval(float(seconds)) 213 214 def set_photo_interval_distance(self, meters: float): 215 """ 216 Setter for photo distance interval 217 218 Args: 219 meters (float): The distance in meters between each photo 220 221 Raises: 222 ValueError: If meters cannot be float 223 224 """ 225 self.photo.set_distance_interval(float(meters)) 226 227 def replace_action(self, index: int, action_type: ActionType, param: int | float = 0): 228 """ 229 Replaces actions at index 230 231 Args: 232 index (int): Action slot (0, ... ,14) 233 action_type (ActionType): The type of the action 234 param (int | float): The parameter of the action. Depends on the actiontype 235 236 - Stay For (time in milliseconds), 237 - Rotate Aircraft (angle in degrees), 238 - Tilt Camera (angle in degrees) 239 - Take Photo (set to 0) 240 - Start Recording (set to 0) 241 - Stop Recording (set to 0) 242 243 Raises: 244 IndexError: If index < 0 or index > 14 245 ValueError: If index cannot be int or param cannot be float 246 or action_type is not valid ActionType 247 248 """ 249 if int(index) > 14 or int(index) < 0: 250 raise IndexError(f"Index {index} is out of bounds (0 ... 14)") 251 match ActionType(action_type): 252 case ActionType.NO_ACTION: 253 self.actions[index].delete() 254 case ActionType.ROTATE_AIRCRAFT: 255 self.actions[index].set_rotate(param) 256 case ActionType.STAY_FOR: 257 self.actions[index].set_stay_for(int(param)) 258 case ActionType.TILT_CAMERA: 259 self.actions[index].set_tilt_cam(param) 260 case ActionType.TAKE_PHOTO: 261 self.actions[index].set_take_photo() 262 case ActionType.START_RECORDING: 263 self.actions[index].set_start_rec() 264 case ActionType.STOP_RECORDING: 265 self.actions[index].set_stop_rec() 266 267 def set_action(self, action_type: ActionType, param: int | float = 0) -> int: 268 """ 269 Setter for actions 270 271 Args: 272 action_type (ActionType): The type of the action 273 param (int | float): The parameter of the action. Depends on the actiontype 274 275 - Stay For (time in milliseconds), 276 - Rotate Aircraft (angle in degrees), 277 - Tilt Camera (angle in degrees) 278 - Take Photo (set to 0) 279 - Start Recording (set to 0) 280 - Stop Recording (set to 0) 281 282 Returns: 283 The index of the action or -1 if no action slots are available 284 285 Raises: 286 ValueError: If action_type is no valid ActionType or param cannot be float 287 288 """ 289 ret = -1 290 if self.next_action_index <= 14: 291 self.replace_action( 292 index=self.next_action_index, 293 action_type=ActionType(action_type), 294 param=float(param) 295 ) 296 ret = self.next_action_index 297 self.next_action_index += 1 298 return ret 299 300 @staticmethod 301 def get_header(line_break='\n') -> str: 302 """ 303 Getter for the waypoint file header 304 305 Args: 306 line_break (str | bool | None): Linebreak character, disable with None or False 307 308 Returns: 309 The header as a string 310 311 """ 312 ret = 'latitude,longitude,altitude(m),heading(deg),' \ 313 'curvesize(m),rotationdir,gimbalmode,gimbalpitchangle,' \ 314 'actiontype1,actionparam1,actiontype2,actionparam2,' \ 315 'actiontype3,actionparam3,actiontype4,actionparam4,' \ 316 'actiontype5,actionparam5,actiontype6,actionparam6,' \ 317 'actiontype7,actionparam7,actiontype8,actionparam8,' \ 318 'actiontype9,actionparam9,actiontype10,actionparam10,' \ 319 'actiontype11,actionparam11,actiontype12,' \ 320 'actionparam12,actiontype13,actionparam13,actiontype14,' \ 321 'actionparam14,actiontype15,actionparam15,' \ 322 'altitudemode,speed(m/s),poi_latitude,' \ 323 'poi_longitude,poi_altitude(m),poi_altitudemode,' \ 324 'photo_timeinterval,photo_distinterval' 325 if line_break: 326 ret += line_break 327 return ret 328 329 def to_line(self, line_break: str | bool | None = '\n') -> str: 330 """ 331 Transforms the waypoint to a line in litchi csv format 332 333 Args: 334 line_break (str | bool | None): Linebreak character, disable with None or False 335 336 Returns: 337 The serialized waypoint in litchi csv format 338 339 """ 340 line = '' 341 line += str(self.lat) + ',' 342 line += str(self.lon) + ',' 343 line += str(self.altitude.value) + ',' 344 line += str(self.heading) + ',' 345 line += str(self.curvesize) + ',' 346 line += str(self.rotationdir.value) + ',' 347 line += str(self.gimbal.mode.value) + ',' 348 line += str(self.gimbal.pitchangle) + ',' 349 for i in range(0, 15): 350 line += str(self.actions[i].type.value) + ',' 351 line += str(self.actions[i].param) + ',' 352 line += str(self.altitude.mode.value) + ',' 353 line += str(self.speed) + ',' 354 line += str(self.poi.lat) + ',' 355 line += str(self.poi.lon) + ',' 356 line += str(self.poi.altitude.value) + ',' 357 line += str(self.poi.altitude.mode.value) + ',' 358 line += str(self.photo.time_interval) + ',' 359 line += str(self.photo.distance_interval) 360 if line_break: 361 if line_break is not True: 362 line += line_break 363 return line 364 365 @staticmethod 366 def from_line(line: str) -> 'Waypoint': 367 """ 368 Parses a line from a litchi waypoint csv file and returns an instance of the Waypoint 369 370 Args: 371 line (str): The litchi waypoints csv line 372 373 Returns: 374 The Waypoint as an instance 375 376 Raises: 377 ValueError: If line does not match regex filter 378 379 """ 380 match = re.search(RegEx.VALID_LITCHI_WP_LINE.value, line) 381 if match: 382 rows = match.group().split(',') 383 384 def get_float(index: int): 385 return float(rows[index]) 386 387 def get_int(index: int): 388 return int(get_float(index)) 389 390 waypoint = Waypoint( 391 lat=get_float(0), 392 lon=get_float(1), 393 alt=get_float(2) 394 ) 395 waypoint.set_heading(get_float(3)) 396 waypoint.set_curvesize(get_float(4)) 397 waypoint.set_rotation_direction(RotationDirection(get_int(5))) 398 waypoint.set_gimbal( 399 mode=GimbalMode(get_int(6)), 400 pitchangle=get_float(7) 401 ) 402 action_index = 0 403 for i in range(8, 8 + 30, 2): 404 waypoint.replace_action( 405 index=action_index, 406 action_type=ActionType(get_int(i)), 407 param=get_float(i + 1) 408 ) 409 action_index += 1 410 waypoint.set_altitude( 411 get_float(2), 412 mode=AltitudeMode(get_int(38)) 413 ) 414 waypoint.set_speed_ms(get_float(39)) 415 waypoint.set_poi( 416 lat=get_float(40), 417 lon=get_float(41), 418 alt=get_float(42), 419 alt_mode=AltitudeMode(get_int(43)) 420 ) 421 waypoint.photo.time_interval = get_float(44) 422 waypoint.photo.distance_interval = get_float(45) 423 return waypoint 424 raise ValueError('invalid_input') 425 426 @staticmethod 427 def from_file(filename: str) -> list['Waypoint']: 428 """ 429 Creates a list of Waypoints from a litchi waypoint csv file. 430 Prints out any lines that did not match the regular expression. 431 Format: {line_number}: {line} 432 433 Args: 434 filename (str): The path + filename of the file to be parsed 435 436 Returns: 437 The list of parsed Waypoints 438 439 """ 440 with open(filename, encoding='utf-8', mode='r') as file: 441 file_content = file.read() 442 rows = file_content.split('\n') 443 wp_list = [] 444 no_ignored_lines = True 445 for row in rows: 446 try: 447 wp_list.append( 448 Waypoint.from_line(row) 449 ) 450 except ValueError: 451 if no_ignored_lines: 452 no_ignored_lines = False 453 print('Ignored lines:') 454 print(f"{rows.index(row)}: {row}") 455 return wp_list
15class Waypoint: 16 """ 17 Class representing a litchi waypoint 18 19 Attributes: 20 lat (float): Latitude coordinate for the waypoint in WGS84 format 21 lon (float): Longitude coordinate for the waypoint in WGS84 format 22 altitude (Altitude): Altitude object 23 heading (float): Heading in degrees 24 curvesize (float): Curvesize (radius?) in meter 25 rotationdir (RotationDirection): 26 Direction of rotation ??(0=cw, 1=ccw)?? (always 0 from mission hub) 27 gimbal (Gimbal): Gimbal settings 28 actions (list[Action]): List of Action objects 29 speed (float): Speed in meters per second 30 poi (Poi): Poi object 31 photo (Photo): Photo object 32 next_action_index (int): The index of the next free action slot (max 14) 33 34 Raises: 35 ValueError: If [lat, lon, alt, head, curve, speed] cannot be float 36 or rot is no valid RotationDirection 37 38 """ 39 next_action_index = 0 40 41 def __init__( 42 self, 43 lat: float, 44 lon: float, 45 alt: float, 46 head: float = 180, 47 curve: float = 0, 48 rot: RotationDirection = RotationDirection.CW, 49 speed: float = 0, 50 ): 51 self.lat = float(lat) 52 self.lon = float(lon) 53 self.altitude = Altitude(value=alt) 54 self.heading = float(head) 55 self.curvesize = float(curve) 56 self.rotationdir = RotationDirection(rot) 57 self.gimbal = Gimbal() 58 self.actions = [Action() for i in range(0, 15)] 59 self.speed = float(speed) 60 self.poi = Poi() 61 self.photo = Photo() 62 63 def set_coordinates(self, lat: float, lon: float): 64 """ 65 Setter for coordinates 66 67 Args: 68 lat (float): Latitude coordinate for the waypoint in WGS84 format 69 lon (float): Longitude coordinate for the waypoint in WGS84 format 70 71 Raises: 72 ValueError: If lat cannot be float or lon cannot be float 73 74 """ 75 self.lat = float(lat) 76 self.lon = float(lon) 77 78 def set_altitude(self, value: float, mode: AltitudeMode = AltitudeMode.AGL): 79 """ 80 Setter for altitude 81 82 Args: 83 value (float): The height in meters 84 mode (AltitudeMode): The altitude mode (MSL or AGL) 85 86 Raises: 87 ValueError: If value cannot be float or mode is no valid AltitudeMode 88 89 """ 90 self.altitude.set_value(float(value)) 91 self.altitude.set_mode(AltitudeMode(mode)) 92 93 def set_heading(self, value: float): 94 """ 95 Setter for heading 96 97 Args: 98 value (float): The heading in degrees (0 = north, 180 = south) 99 100 Raises: 101 ValueError: If value cannot be float 102 103 """ 104 self.heading = float(value) 105 106 def set_curvesize(self, value: float): 107 """ 108 Setter for curve size 109 110 Args: 111 value (float): The curve radius in meters 112 113 Raises: 114 ValueError: If value cannot be float 115 116 """ 117 self.curvesize = abs(float(value)) 118 119 def set_rotation_direction(self, value: RotationDirection = RotationDirection.CW): 120 """ 121 Setter for the rotation direction 122 123 Args: 124 value (RotationDirection): Clockwise or Counterclockwise rotation 125 126 Raises: 127 ValueError: If value is not a valid RotationDirection 128 129 """ 130 self.rotationdir = RotationDirection(value) 131 132 def set_gimbal(self, mode: GimbalMode, pitchangle: float = 0): 133 """ 134 Setter for gimbal mode 135 136 Args: 137 mode (GimbalMode): The mode setting for the gimbal 138 pitchangle (float): The angle for the gimbal (only for interpolate mode) 139 140 Raises: 141 ValueError: If mode is no valid GimbalMode. Or mode is GimbalMode.Interpolate and 142 (pitchangle cannot be float or pitchangle < -90 or pitchangle > 30) 143 144 """ 145 match GimbalMode(mode): 146 case GimbalMode.DISABLED: 147 self.gimbal.set_mode(GimbalMode.DISABLED) 148 case GimbalMode.FOCUS_POI: 149 self.gimbal.set_focus_poi() 150 case GimbalMode.INTERPOLATE: 151 self.gimbal.set_interpolate(pitchangle) 152 153 def set_speed_ms(self, value: float): 154 """ 155 Setter for speed in meters per second 156 157 Args: 158 value (float): The speed in meters per second 159 160 Raises: 161 ValueError: If value cannot be float 162 163 """ 164 self.speed = float(value) 165 166 def set_speed_kmh(self, value: float): 167 """ 168 Setter for speed in kilometers per hour 169 170 Args: 171 value (float): The speed in kilometers per hour 172 173 Raises: 174 ValueError: If value cannot be float 175 176 """ 177 self.speed = float(value) / 3.6 178 179 def set_poi( 180 self, 181 lat: float, 182 lon: float, 183 alt: float, 184 alt_mode: AltitudeMode = AltitudeMode.MSL 185 ): 186 """ 187 Setter for point of interest 188 Args: 189 lat (float): Latitude coordinate for the waypoint in WGS84 format 190 lon (float): Longitude coordinate for the waypoint in WGS84 format 191 alt (float): The altitude in meters 192 alt_mode (AltitudeMode): The altitudemode, MSL or AGL 193 194 Raises: 195 ValueError: If [lat, lon, alt] cannot be float or alt_mode is no valid AltitudeMode 196 197 """ 198 self.poi.set_coordinates(float(lat), float(lon)) 199 self.poi.set_altitude(float(alt)) 200 self.poi.set_altitude_mode(AltitudeMode(alt_mode)) 201 202 def set_photo_interval_time(self, seconds: float): 203 """ 204 Setter for photo time interval 205 206 Args: 207 seconds (float): The time in seconds between each photo 208 209 Raises: 210 ValueError: If seconds cannot be float 211 212 """ 213 self.photo.set_time_interval(float(seconds)) 214 215 def set_photo_interval_distance(self, meters: float): 216 """ 217 Setter for photo distance interval 218 219 Args: 220 meters (float): The distance in meters between each photo 221 222 Raises: 223 ValueError: If meters cannot be float 224 225 """ 226 self.photo.set_distance_interval(float(meters)) 227 228 def replace_action(self, index: int, action_type: ActionType, param: int | float = 0): 229 """ 230 Replaces actions at index 231 232 Args: 233 index (int): Action slot (0, ... ,14) 234 action_type (ActionType): The type of the action 235 param (int | float): The parameter of the action. Depends on the actiontype 236 237 - Stay For (time in milliseconds), 238 - Rotate Aircraft (angle in degrees), 239 - Tilt Camera (angle in degrees) 240 - Take Photo (set to 0) 241 - Start Recording (set to 0) 242 - Stop Recording (set to 0) 243 244 Raises: 245 IndexError: If index < 0 or index > 14 246 ValueError: If index cannot be int or param cannot be float 247 or action_type is not valid ActionType 248 249 """ 250 if int(index) > 14 or int(index) < 0: 251 raise IndexError(f"Index {index} is out of bounds (0 ... 14)") 252 match ActionType(action_type): 253 case ActionType.NO_ACTION: 254 self.actions[index].delete() 255 case ActionType.ROTATE_AIRCRAFT: 256 self.actions[index].set_rotate(param) 257 case ActionType.STAY_FOR: 258 self.actions[index].set_stay_for(int(param)) 259 case ActionType.TILT_CAMERA: 260 self.actions[index].set_tilt_cam(param) 261 case ActionType.TAKE_PHOTO: 262 self.actions[index].set_take_photo() 263 case ActionType.START_RECORDING: 264 self.actions[index].set_start_rec() 265 case ActionType.STOP_RECORDING: 266 self.actions[index].set_stop_rec() 267 268 def set_action(self, action_type: ActionType, param: int | float = 0) -> int: 269 """ 270 Setter for actions 271 272 Args: 273 action_type (ActionType): The type of the action 274 param (int | float): The parameter of the action. Depends on the actiontype 275 276 - Stay For (time in milliseconds), 277 - Rotate Aircraft (angle in degrees), 278 - Tilt Camera (angle in degrees) 279 - Take Photo (set to 0) 280 - Start Recording (set to 0) 281 - Stop Recording (set to 0) 282 283 Returns: 284 The index of the action or -1 if no action slots are available 285 286 Raises: 287 ValueError: If action_type is no valid ActionType or param cannot be float 288 289 """ 290 ret = -1 291 if self.next_action_index <= 14: 292 self.replace_action( 293 index=self.next_action_index, 294 action_type=ActionType(action_type), 295 param=float(param) 296 ) 297 ret = self.next_action_index 298 self.next_action_index += 1 299 return ret 300 301 @staticmethod 302 def get_header(line_break='\n') -> str: 303 """ 304 Getter for the waypoint file header 305 306 Args: 307 line_break (str | bool | None): Linebreak character, disable with None or False 308 309 Returns: 310 The header as a string 311 312 """ 313 ret = 'latitude,longitude,altitude(m),heading(deg),' \ 314 'curvesize(m),rotationdir,gimbalmode,gimbalpitchangle,' \ 315 'actiontype1,actionparam1,actiontype2,actionparam2,' \ 316 'actiontype3,actionparam3,actiontype4,actionparam4,' \ 317 'actiontype5,actionparam5,actiontype6,actionparam6,' \ 318 'actiontype7,actionparam7,actiontype8,actionparam8,' \ 319 'actiontype9,actionparam9,actiontype10,actionparam10,' \ 320 'actiontype11,actionparam11,actiontype12,' \ 321 'actionparam12,actiontype13,actionparam13,actiontype14,' \ 322 'actionparam14,actiontype15,actionparam15,' \ 323 'altitudemode,speed(m/s),poi_latitude,' \ 324 'poi_longitude,poi_altitude(m),poi_altitudemode,' \ 325 'photo_timeinterval,photo_distinterval' 326 if line_break: 327 ret += line_break 328 return ret 329 330 def to_line(self, line_break: str | bool | None = '\n') -> str: 331 """ 332 Transforms the waypoint to a line in litchi csv format 333 334 Args: 335 line_break (str | bool | None): Linebreak character, disable with None or False 336 337 Returns: 338 The serialized waypoint in litchi csv format 339 340 """ 341 line = '' 342 line += str(self.lat) + ',' 343 line += str(self.lon) + ',' 344 line += str(self.altitude.value) + ',' 345 line += str(self.heading) + ',' 346 line += str(self.curvesize) + ',' 347 line += str(self.rotationdir.value) + ',' 348 line += str(self.gimbal.mode.value) + ',' 349 line += str(self.gimbal.pitchangle) + ',' 350 for i in range(0, 15): 351 line += str(self.actions[i].type.value) + ',' 352 line += str(self.actions[i].param) + ',' 353 line += str(self.altitude.mode.value) + ',' 354 line += str(self.speed) + ',' 355 line += str(self.poi.lat) + ',' 356 line += str(self.poi.lon) + ',' 357 line += str(self.poi.altitude.value) + ',' 358 line += str(self.poi.altitude.mode.value) + ',' 359 line += str(self.photo.time_interval) + ',' 360 line += str(self.photo.distance_interval) 361 if line_break: 362 if line_break is not True: 363 line += line_break 364 return line 365 366 @staticmethod 367 def from_line(line: str) -> 'Waypoint': 368 """ 369 Parses a line from a litchi waypoint csv file and returns an instance of the Waypoint 370 371 Args: 372 line (str): The litchi waypoints csv line 373 374 Returns: 375 The Waypoint as an instance 376 377 Raises: 378 ValueError: If line does not match regex filter 379 380 """ 381 match = re.search(RegEx.VALID_LITCHI_WP_LINE.value, line) 382 if match: 383 rows = match.group().split(',') 384 385 def get_float(index: int): 386 return float(rows[index]) 387 388 def get_int(index: int): 389 return int(get_float(index)) 390 391 waypoint = Waypoint( 392 lat=get_float(0), 393 lon=get_float(1), 394 alt=get_float(2) 395 ) 396 waypoint.set_heading(get_float(3)) 397 waypoint.set_curvesize(get_float(4)) 398 waypoint.set_rotation_direction(RotationDirection(get_int(5))) 399 waypoint.set_gimbal( 400 mode=GimbalMode(get_int(6)), 401 pitchangle=get_float(7) 402 ) 403 action_index = 0 404 for i in range(8, 8 + 30, 2): 405 waypoint.replace_action( 406 index=action_index, 407 action_type=ActionType(get_int(i)), 408 param=get_float(i + 1) 409 ) 410 action_index += 1 411 waypoint.set_altitude( 412 get_float(2), 413 mode=AltitudeMode(get_int(38)) 414 ) 415 waypoint.set_speed_ms(get_float(39)) 416 waypoint.set_poi( 417 lat=get_float(40), 418 lon=get_float(41), 419 alt=get_float(42), 420 alt_mode=AltitudeMode(get_int(43)) 421 ) 422 waypoint.photo.time_interval = get_float(44) 423 waypoint.photo.distance_interval = get_float(45) 424 return waypoint 425 raise ValueError('invalid_input') 426 427 @staticmethod 428 def from_file(filename: str) -> list['Waypoint']: 429 """ 430 Creates a list of Waypoints from a litchi waypoint csv file. 431 Prints out any lines that did not match the regular expression. 432 Format: {line_number}: {line} 433 434 Args: 435 filename (str): The path + filename of the file to be parsed 436 437 Returns: 438 The list of parsed Waypoints 439 440 """ 441 with open(filename, encoding='utf-8', mode='r') as file: 442 file_content = file.read() 443 rows = file_content.split('\n') 444 wp_list = [] 445 no_ignored_lines = True 446 for row in rows: 447 try: 448 wp_list.append( 449 Waypoint.from_line(row) 450 ) 451 except ValueError: 452 if no_ignored_lines: 453 no_ignored_lines = False 454 print('Ignored lines:') 455 print(f"{rows.index(row)}: {row}") 456 return wp_list
Class representing a litchi waypoint
Attributes:
- lat (float): Latitude coordinate for the waypoint in WGS84 format
- lon (float): Longitude coordinate for the waypoint in WGS84 format
- altitude (Altitude): Altitude object
- heading (float): Heading in degrees
- curvesize (float): Curvesize (radius?) in meter
- rotationdir (RotationDirection): Direction of rotation ??(0=cw, 1=ccw)?? (always 0 from mission hub)
- gimbal (Gimbal): Gimbal settings
- actions (list[Action]): List of Action objects
- speed (float): Speed in meters per second
- poi (Poi): Poi object
- photo (Photo): Photo object
- next_action_index (int): The index of the next free action slot (max 14)
- Raises: ValueError: If [lat, lon, alt, head, curve, speed] cannot be float or rot is no valid RotationDirection
41 def __init__( 42 self, 43 lat: float, 44 lon: float, 45 alt: float, 46 head: float = 180, 47 curve: float = 0, 48 rot: RotationDirection = RotationDirection.CW, 49 speed: float = 0, 50 ): 51 self.lat = float(lat) 52 self.lon = float(lon) 53 self.altitude = Altitude(value=alt) 54 self.heading = float(head) 55 self.curvesize = float(curve) 56 self.rotationdir = RotationDirection(rot) 57 self.gimbal = Gimbal() 58 self.actions = [Action() for i in range(0, 15)] 59 self.speed = float(speed) 60 self.poi = Poi() 61 self.photo = Photo()
63 def set_coordinates(self, lat: float, lon: float): 64 """ 65 Setter for coordinates 66 67 Args: 68 lat (float): Latitude coordinate for the waypoint in WGS84 format 69 lon (float): Longitude coordinate for the waypoint in WGS84 format 70 71 Raises: 72 ValueError: If lat cannot be float or lon cannot be float 73 74 """ 75 self.lat = float(lat) 76 self.lon = float(lon)
Setter for coordinates
Arguments:
- lat (float): Latitude coordinate for the waypoint in WGS84 format
- lon (float): Longitude coordinate for the waypoint in WGS84 format
Raises:
- ValueError: If lat cannot be float or lon cannot be float
78 def set_altitude(self, value: float, mode: AltitudeMode = AltitudeMode.AGL): 79 """ 80 Setter for altitude 81 82 Args: 83 value (float): The height in meters 84 mode (AltitudeMode): The altitude mode (MSL or AGL) 85 86 Raises: 87 ValueError: If value cannot be float or mode is no valid AltitudeMode 88 89 """ 90 self.altitude.set_value(float(value)) 91 self.altitude.set_mode(AltitudeMode(mode))
Setter for altitude
Arguments:
- value (float): The height in meters
- mode (AltitudeMode): The altitude mode (MSL or AGL)
Raises:
- ValueError: If value cannot be float or mode is no valid AltitudeMode
93 def set_heading(self, value: float): 94 """ 95 Setter for heading 96 97 Args: 98 value (float): The heading in degrees (0 = north, 180 = south) 99 100 Raises: 101 ValueError: If value cannot be float 102 103 """ 104 self.heading = float(value)
Setter for heading
Arguments:
- value (float): The heading in degrees (0 = north, 180 = south)
Raises:
- ValueError: If value cannot be float
106 def set_curvesize(self, value: float): 107 """ 108 Setter for curve size 109 110 Args: 111 value (float): The curve radius in meters 112 113 Raises: 114 ValueError: If value cannot be float 115 116 """ 117 self.curvesize = abs(float(value))
Setter for curve size
Arguments:
- value (float): The curve radius in meters
Raises:
- ValueError: If value cannot be float
119 def set_rotation_direction(self, value: RotationDirection = RotationDirection.CW): 120 """ 121 Setter for the rotation direction 122 123 Args: 124 value (RotationDirection): Clockwise or Counterclockwise rotation 125 126 Raises: 127 ValueError: If value is not a valid RotationDirection 128 129 """ 130 self.rotationdir = RotationDirection(value)
Setter for the rotation direction
Arguments:
- value (RotationDirection): Clockwise or Counterclockwise rotation
Raises:
- ValueError: If value is not a valid RotationDirection
132 def set_gimbal(self, mode: GimbalMode, pitchangle: float = 0): 133 """ 134 Setter for gimbal mode 135 136 Args: 137 mode (GimbalMode): The mode setting for the gimbal 138 pitchangle (float): The angle for the gimbal (only for interpolate mode) 139 140 Raises: 141 ValueError: If mode is no valid GimbalMode. Or mode is GimbalMode.Interpolate and 142 (pitchangle cannot be float or pitchangle < -90 or pitchangle > 30) 143 144 """ 145 match GimbalMode(mode): 146 case GimbalMode.DISABLED: 147 self.gimbal.set_mode(GimbalMode.DISABLED) 148 case GimbalMode.FOCUS_POI: 149 self.gimbal.set_focus_poi() 150 case GimbalMode.INTERPOLATE: 151 self.gimbal.set_interpolate(pitchangle)
Setter for gimbal mode
Arguments:
- mode (GimbalMode): The mode setting for the gimbal
- pitchangle (float): The angle for the gimbal (only for interpolate mode)
Raises:
- ValueError: If mode is no valid GimbalMode. Or mode is GimbalMode.Interpolate and (pitchangle cannot be float or pitchangle < -90 or pitchangle > 30)
153 def set_speed_ms(self, value: float): 154 """ 155 Setter for speed in meters per second 156 157 Args: 158 value (float): The speed in meters per second 159 160 Raises: 161 ValueError: If value cannot be float 162 163 """ 164 self.speed = float(value)
Setter for speed in meters per second
Arguments:
- value (float): The speed in meters per second
Raises:
- ValueError: If value cannot be float
166 def set_speed_kmh(self, value: float): 167 """ 168 Setter for speed in kilometers per hour 169 170 Args: 171 value (float): The speed in kilometers per hour 172 173 Raises: 174 ValueError: If value cannot be float 175 176 """ 177 self.speed = float(value) / 3.6
Setter for speed in kilometers per hour
Arguments:
- value (float): The speed in kilometers per hour
Raises:
- ValueError: If value cannot be float
179 def set_poi( 180 self, 181 lat: float, 182 lon: float, 183 alt: float, 184 alt_mode: AltitudeMode = AltitudeMode.MSL 185 ): 186 """ 187 Setter for point of interest 188 Args: 189 lat (float): Latitude coordinate for the waypoint in WGS84 format 190 lon (float): Longitude coordinate for the waypoint in WGS84 format 191 alt (float): The altitude in meters 192 alt_mode (AltitudeMode): The altitudemode, MSL or AGL 193 194 Raises: 195 ValueError: If [lat, lon, alt] cannot be float or alt_mode is no valid AltitudeMode 196 197 """ 198 self.poi.set_coordinates(float(lat), float(lon)) 199 self.poi.set_altitude(float(alt)) 200 self.poi.set_altitude_mode(AltitudeMode(alt_mode))
Setter for point of interest
Arguments:
- lat (float): Latitude coordinate for the waypoint in WGS84 format
- lon (float): Longitude coordinate for the waypoint in WGS84 format
- alt (float): The altitude in meters
- alt_mode (AltitudeMode): The altitudemode, MSL or AGL
Raises:
- ValueError: If [lat, lon, alt] cannot be float or alt_mode is no valid AltitudeMode
202 def set_photo_interval_time(self, seconds: float): 203 """ 204 Setter for photo time interval 205 206 Args: 207 seconds (float): The time in seconds between each photo 208 209 Raises: 210 ValueError: If seconds cannot be float 211 212 """ 213 self.photo.set_time_interval(float(seconds))
Setter for photo time interval
Arguments:
- seconds (float): The time in seconds between each photo
Raises:
- ValueError: If seconds cannot be float
215 def set_photo_interval_distance(self, meters: float): 216 """ 217 Setter for photo distance interval 218 219 Args: 220 meters (float): The distance in meters between each photo 221 222 Raises: 223 ValueError: If meters cannot be float 224 225 """ 226 self.photo.set_distance_interval(float(meters))
Setter for photo distance interval
Arguments:
- meters (float): The distance in meters between each photo
Raises:
- ValueError: If meters cannot be float
228 def replace_action(self, index: int, action_type: ActionType, param: int | float = 0): 229 """ 230 Replaces actions at index 231 232 Args: 233 index (int): Action slot (0, ... ,14) 234 action_type (ActionType): The type of the action 235 param (int | float): The parameter of the action. Depends on the actiontype 236 237 - Stay For (time in milliseconds), 238 - Rotate Aircraft (angle in degrees), 239 - Tilt Camera (angle in degrees) 240 - Take Photo (set to 0) 241 - Start Recording (set to 0) 242 - Stop Recording (set to 0) 243 244 Raises: 245 IndexError: If index < 0 or index > 14 246 ValueError: If index cannot be int or param cannot be float 247 or action_type is not valid ActionType 248 249 """ 250 if int(index) > 14 or int(index) < 0: 251 raise IndexError(f"Index {index} is out of bounds (0 ... 14)") 252 match ActionType(action_type): 253 case ActionType.NO_ACTION: 254 self.actions[index].delete() 255 case ActionType.ROTATE_AIRCRAFT: 256 self.actions[index].set_rotate(param) 257 case ActionType.STAY_FOR: 258 self.actions[index].set_stay_for(int(param)) 259 case ActionType.TILT_CAMERA: 260 self.actions[index].set_tilt_cam(param) 261 case ActionType.TAKE_PHOTO: 262 self.actions[index].set_take_photo() 263 case ActionType.START_RECORDING: 264 self.actions[index].set_start_rec() 265 case ActionType.STOP_RECORDING: 266 self.actions[index].set_stop_rec()
Replaces actions at index
Arguments:
- index (int): Action slot (0, ... ,14)
- action_type (ActionType): The type of the action
param (int | float): The parameter of the action. Depends on the actiontype
- Stay For (time in milliseconds),
- Rotate Aircraft (angle in degrees),
- Tilt Camera (angle in degrees)
- Take Photo (set to 0)
- Start Recording (set to 0)
- Stop Recording (set to 0)
Raises:
- IndexError: If index < 0 or index > 14
- ValueError: If index cannot be int or param cannot be float or action_type is not valid ActionType
268 def set_action(self, action_type: ActionType, param: int | float = 0) -> int: 269 """ 270 Setter for actions 271 272 Args: 273 action_type (ActionType): The type of the action 274 param (int | float): The parameter of the action. Depends on the actiontype 275 276 - Stay For (time in milliseconds), 277 - Rotate Aircraft (angle in degrees), 278 - Tilt Camera (angle in degrees) 279 - Take Photo (set to 0) 280 - Start Recording (set to 0) 281 - Stop Recording (set to 0) 282 283 Returns: 284 The index of the action or -1 if no action slots are available 285 286 Raises: 287 ValueError: If action_type is no valid ActionType or param cannot be float 288 289 """ 290 ret = -1 291 if self.next_action_index <= 14: 292 self.replace_action( 293 index=self.next_action_index, 294 action_type=ActionType(action_type), 295 param=float(param) 296 ) 297 ret = self.next_action_index 298 self.next_action_index += 1 299 return ret
Setter for actions
Arguments:
- action_type (ActionType): The type of the action
param (int | float): The parameter of the action. Depends on the actiontype
- Stay For (time in milliseconds),
- Rotate Aircraft (angle in degrees),
- Tilt Camera (angle in degrees)
- Take Photo (set to 0)
- Start Recording (set to 0)
- Stop Recording (set to 0)
Returns:
The index of the action or -1 if no action slots are available
Raises:
- ValueError: If action_type is no valid ActionType or param cannot be float
301 @staticmethod 302 def get_header(line_break='\n') -> str: 303 """ 304 Getter for the waypoint file header 305 306 Args: 307 line_break (str | bool | None): Linebreak character, disable with None or False 308 309 Returns: 310 The header as a string 311 312 """ 313 ret = 'latitude,longitude,altitude(m),heading(deg),' \ 314 'curvesize(m),rotationdir,gimbalmode,gimbalpitchangle,' \ 315 'actiontype1,actionparam1,actiontype2,actionparam2,' \ 316 'actiontype3,actionparam3,actiontype4,actionparam4,' \ 317 'actiontype5,actionparam5,actiontype6,actionparam6,' \ 318 'actiontype7,actionparam7,actiontype8,actionparam8,' \ 319 'actiontype9,actionparam9,actiontype10,actionparam10,' \ 320 'actiontype11,actionparam11,actiontype12,' \ 321 'actionparam12,actiontype13,actionparam13,actiontype14,' \ 322 'actionparam14,actiontype15,actionparam15,' \ 323 'altitudemode,speed(m/s),poi_latitude,' \ 324 'poi_longitude,poi_altitude(m),poi_altitudemode,' \ 325 'photo_timeinterval,photo_distinterval' 326 if line_break: 327 ret += line_break 328 return ret
Getter for the waypoint file header
Arguments:
- line_break (str | bool | None): Linebreak character, disable with None or False
Returns:
The header as a string
330 def to_line(self, line_break: str | bool | None = '\n') -> str: 331 """ 332 Transforms the waypoint to a line in litchi csv format 333 334 Args: 335 line_break (str | bool | None): Linebreak character, disable with None or False 336 337 Returns: 338 The serialized waypoint in litchi csv format 339 340 """ 341 line = '' 342 line += str(self.lat) + ',' 343 line += str(self.lon) + ',' 344 line += str(self.altitude.value) + ',' 345 line += str(self.heading) + ',' 346 line += str(self.curvesize) + ',' 347 line += str(self.rotationdir.value) + ',' 348 line += str(self.gimbal.mode.value) + ',' 349 line += str(self.gimbal.pitchangle) + ',' 350 for i in range(0, 15): 351 line += str(self.actions[i].type.value) + ',' 352 line += str(self.actions[i].param) + ',' 353 line += str(self.altitude.mode.value) + ',' 354 line += str(self.speed) + ',' 355 line += str(self.poi.lat) + ',' 356 line += str(self.poi.lon) + ',' 357 line += str(self.poi.altitude.value) + ',' 358 line += str(self.poi.altitude.mode.value) + ',' 359 line += str(self.photo.time_interval) + ',' 360 line += str(self.photo.distance_interval) 361 if line_break: 362 if line_break is not True: 363 line += line_break 364 return line
Transforms the waypoint to a line in litchi csv format
Arguments:
- line_break (str | bool | None): Linebreak character, disable with None or False
Returns:
The serialized waypoint in litchi csv format
366 @staticmethod 367 def from_line(line: str) -> 'Waypoint': 368 """ 369 Parses a line from a litchi waypoint csv file and returns an instance of the Waypoint 370 371 Args: 372 line (str): The litchi waypoints csv line 373 374 Returns: 375 The Waypoint as an instance 376 377 Raises: 378 ValueError: If line does not match regex filter 379 380 """ 381 match = re.search(RegEx.VALID_LITCHI_WP_LINE.value, line) 382 if match: 383 rows = match.group().split(',') 384 385 def get_float(index: int): 386 return float(rows[index]) 387 388 def get_int(index: int): 389 return int(get_float(index)) 390 391 waypoint = Waypoint( 392 lat=get_float(0), 393 lon=get_float(1), 394 alt=get_float(2) 395 ) 396 waypoint.set_heading(get_float(3)) 397 waypoint.set_curvesize(get_float(4)) 398 waypoint.set_rotation_direction(RotationDirection(get_int(5))) 399 waypoint.set_gimbal( 400 mode=GimbalMode(get_int(6)), 401 pitchangle=get_float(7) 402 ) 403 action_index = 0 404 for i in range(8, 8 + 30, 2): 405 waypoint.replace_action( 406 index=action_index, 407 action_type=ActionType(get_int(i)), 408 param=get_float(i + 1) 409 ) 410 action_index += 1 411 waypoint.set_altitude( 412 get_float(2), 413 mode=AltitudeMode(get_int(38)) 414 ) 415 waypoint.set_speed_ms(get_float(39)) 416 waypoint.set_poi( 417 lat=get_float(40), 418 lon=get_float(41), 419 alt=get_float(42), 420 alt_mode=AltitudeMode(get_int(43)) 421 ) 422 waypoint.photo.time_interval = get_float(44) 423 waypoint.photo.distance_interval = get_float(45) 424 return waypoint 425 raise ValueError('invalid_input')
Parses a line from a litchi waypoint csv file and returns an instance of the Waypoint
Arguments:
- line (str): The litchi waypoints csv line
Returns:
The Waypoint as an instance
Raises:
- ValueError: If line does not match regex filter
427 @staticmethod 428 def from_file(filename: str) -> list['Waypoint']: 429 """ 430 Creates a list of Waypoints from a litchi waypoint csv file. 431 Prints out any lines that did not match the regular expression. 432 Format: {line_number}: {line} 433 434 Args: 435 filename (str): The path + filename of the file to be parsed 436 437 Returns: 438 The list of parsed Waypoints 439 440 """ 441 with open(filename, encoding='utf-8', mode='r') as file: 442 file_content = file.read() 443 rows = file_content.split('\n') 444 wp_list = [] 445 no_ignored_lines = True 446 for row in rows: 447 try: 448 wp_list.append( 449 Waypoint.from_line(row) 450 ) 451 except ValueError: 452 if no_ignored_lines: 453 no_ignored_lines = False 454 print('Ignored lines:') 455 print(f"{rows.index(row)}: {row}") 456 return wp_list
Creates a list of Waypoints from a litchi waypoint csv file. Prints out any lines that did not match the regular expression. Format: {line_number}: {line}
Arguments:
- filename (str): The path + filename of the file to be parsed
Returns:
The list of parsed Waypoints