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
class Waypoint:
 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
Waypoint( lat: float, lon: float, alt: float, head: float = 180, curve: float = 0, rot: litchi_wp.enums.RotationDirection = <RotationDirection.CW: 0>, speed: float = 0)
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()
def set_coordinates(self, lat: float, lon: float):
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
def set_altitude( self, value: float, mode: litchi_wp.enums.AltitudeMode = <AltitudeMode.AGL: 1>):
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
def set_heading(self, value: float):
 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
def set_curvesize(self, value: 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
def set_rotation_direction( self, value: litchi_wp.enums.RotationDirection = <RotationDirection.CW: 0>):
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
def set_gimbal(self, mode: litchi_wp.enums.GimbalMode, pitchangle: float = 0):
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)
def set_speed_ms(self, value: float):
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
def set_speed_kmh(self, value: 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
def set_poi( self, lat: float, lon: float, alt: float, alt_mode: litchi_wp.enums.AltitudeMode = <AltitudeMode.MSL: 0>):
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
def set_photo_interval_time(self, seconds: float):
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
def set_photo_interval_distance(self, meters: 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
def replace_action( self, index: int, action_type: litchi_wp.enums.ActionType, param: Union[int, float] = 0):
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
def set_action( self, action_type: litchi_wp.enums.ActionType, param: Union[int, float] = 0) -> int:
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
@staticmethod
def get_header(line_break: str = '\n') -> str:
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

def to_line(self, line_break: Union[str, bool, NoneType] = '\n') -> str:
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

@staticmethod
def from_line(line: str) -> litchi_wp.waypoint.Waypoint:
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
@staticmethod
def from_file(filename: str) -> list[litchi_wp.waypoint.Waypoint]:
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