Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix flight time through midnight utc #53

Open
wants to merge 14 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
OpenSoar
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(not related to this line)
If all looks good, do final check with Pysoar on some comps

Copy link
Owner

@GliderGeek GliderGeek Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(not related to this line)
Determine new version number (backwards incompatible maybe?) and write changelog

========

busy with making everything timezone aware
- (done) changed start-time and fixes to timezone aware datetimes
- (done) removed unnecessary helper functionality
- check .seconds on delta. should it be 'total_seconds'?
- check date shift `day_diff`
- run tests on opensoar, does it still work?
- run test with pysoar, same results?
- how to keep backwards compatible? add flag?

.. image:: https://img.shields.io/pypi/v/opensoar.svg
:target: https://pypi.org/project/opensoar/
:alt: pypi version and link
Expand Down
15 changes: 8 additions & 7 deletions opensoar/competition/soaringspot.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@
from opensoar.task.race_task import RaceTask
from opensoar.task.task import Task
from opensoar.task.waypoint import Waypoint
from opensoar.utilities.helper_functions import dm2dd, subtract_times
from opensoar.utilities.helper_functions import dm2dd
from opensoar.competition.daily_results_page import DailyResultsPage
from opensoar.utilities.helper_functions import double_iterator


def get_comment_lines_from_parsed_file(parsed_igc_file: dict) -> List[str]:
Expand Down Expand Up @@ -46,7 +47,7 @@ def get_task_rules(lseeyou_tsk_line: str) -> Tuple[datetime.time, datetime.timed
return start_opening, t_min, multi_start


def get_info_from_comment_lines(parsed_igc_file: dict, start_time_buffer: int=0) -> Tuple[Optional[Task], dict, dict]:
def get_info_from_comment_lines(parsed_igc_file: dict, date: datetime.date, start_time_buffer: int=0) -> Tuple[Optional[Task], dict, dict]:
"""
There is specific contest information stored in the comment lines of the IGC files.
This function extracts this information
Expand Down Expand Up @@ -84,8 +85,8 @@ def get_info_from_comment_lines(parsed_igc_file: dict, start_time_buffer: int=0)
timezone = int(line.split(':')[3])

if start_opening is not None:
# convert start opening to UTC time
start_opening = subtract_times(start_opening, datetime.timedelta(hours=timezone))
# make timezone aware datetime object
start_opening = datetime.datetime.combine(date=date, time=start_opening, tzinfo=datetime.timezone(datetime.timedelta(hours=timezone)))

if len(lcu_lines) == 0 or len(lseeyou_lines) == 0:
# somehow some IGC files do not contain the LCU or LSEEYOU lines with task information
Expand Down Expand Up @@ -345,10 +346,10 @@ def generate_competition_day(self, target_directory: str, download_progress=None
try:
try: # try utf-8
with open(file_path, 'r', encoding='utf-8') as f:
parsed_igc_file = Reader().read(f)
parsed_igc_file = Reader(skip_duplicates=True).read(f)
except UnicodeDecodeError: # if not utf-8 use latin1
with open(file_path, 'r', encoding='latin1') as f:
parsed_igc_file = Reader().read(f)
parsed_igc_file = Reader(skip_duplicates=True).read(f)
except Exception:
print('{} is skipped because the file could not be parsed'.format(competition_id))
continue
Expand All @@ -359,7 +360,7 @@ def generate_competition_day(self, target_directory: str, download_progress=None
continue

# get info from file
task, contest_information, competitor_information = get_info_from_comment_lines(parsed_igc_file, start_time_buffer)
task, _, competitor_information = get_info_from_comment_lines(parsed_igc_file, date, start_time_buffer)
plane_model = competitor_information.get('plane_model', None)
pilot_name = competitor_information.get('pilot_name', None)

Expand Down
12 changes: 6 additions & 6 deletions opensoar/task/aat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
from copy import deepcopy

from opensoar.task.task import Task
from opensoar.utilities.helper_functions import double_iterator, calculate_distance_bearing, calculate_destination, \
seconds_time_difference_fixes, add_times
from opensoar.utilities.helper_functions import double_iterator, calculate_distance_bearing, calculate_destination


class AAT(Task):
Expand Down Expand Up @@ -51,12 +50,12 @@ def apply_rules(self, trace):
return fixes, start_time, outlanding_fix, distances, finish_time, sector_fixes

def _determine_finish_time(self, fixes, outlanding_fix):
total_trip_time = seconds_time_difference_fixes(fixes[0], fixes[-1])
total_trip_time = (fixes[-1]['datetime'] - fixes[0]['datetime']).total_seconds()
minimum_trip_time = self._t_min.total_seconds()
if outlanding_fix is None and total_trip_time < minimum_trip_time:
finish_time = add_times(fixes[0]['time'], self._t_min)
finish_time = fixes[0]['datetime'] + self._t_min
else:
finish_time = fixes[-1]['time']
finish_time = fixes[-1]['datetime']
return finish_time

def _calculate_trip_fixes(self, trace):
Expand Down Expand Up @@ -118,7 +117,7 @@ def _get_sector_fixes(self, trace):
if enl_first_fix is None:
enl_first_fix = fix

enl_time = seconds_time_difference_fixes(enl_first_fix, fix)
enl_time = (fix['datetime'] - enl_first_fix['datetime']).total_seconds()
if self.enl_time_exceeded(enl_time):
enl_registered = True
if current_leg > 0:
Expand Down Expand Up @@ -357,6 +356,7 @@ def _get_waypoint_fixes(self, outlanded, sector_fixes, outside_sector_fixes=None
if outside_sector_fixes is None:
outside_sector_fixes = list()

# this doesnt work due to latest aerofiles version
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this still needs to be addressed

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

waypoint_fixes = deepcopy(sector_fixes)
if outlanded:
waypoint_fixes.append(sector_fixes[-1])
Expand Down
12 changes: 6 additions & 6 deletions opensoar/task/race_task.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from opensoar.task.task import Task
from opensoar.utilities.helper_functions import calculate_distance_bearing, double_iterator, \
seconds_time_difference_fixes, add_seconds
import datetime

from opensoar.task.task import Task
from opensoar.utilities.helper_functions import calculate_distance_bearing, double_iterator

class RaceTask(Task):
"""
Expand Down Expand Up @@ -82,7 +82,7 @@ def apply_rules(self, trace):
fixes, outlanding_fix = self.determine_trip_fixes(trace)
distances = self.determine_trip_distances(fixes, outlanding_fix)
refined_start = self.determine_refined_start(trace, fixes)
finish_time = fixes[-1]['time']
finish_time = fixes[-1]['datetime']
sector_fixes = [] # not applicable for race tasks
return fixes, refined_start, outlanding_fix, distances, finish_time, sector_fixes

Expand All @@ -100,15 +100,15 @@ def determine_trip_fixes(self, trace):
if enl_first_fix is None:
enl_first_fix = fix_minus1

enl_time = seconds_time_difference_fixes(enl_first_fix, fix)
enl_time = (fix['datetime'] - enl_first_fix['datetime']).total_seconds()
enl_registered = enl_registered or self.enl_time_exceeded(enl_time)
elif not enl_registered:
enl_first_fix = None

if self.start_opening is None:
after_start_opening = True
else:
after_start_opening = add_seconds(fix['time'], self.start_time_buffer) > self.start_opening
after_start_opening = self.start_opening + datetime.timedelta(seconds=self.start_time_buffer) < fix['datetime']

if leg == -1 and after_start_opening:
if self.started(fix_minus1, fix):
Expand Down
6 changes: 3 additions & 3 deletions opensoar/task/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ class Task:
ENL_VALUE_THRESHOLD = 500
ENL_TIME_THRESHOLD = 30

def __init__(self, waypoints: List[Waypoint], timezone: int, start_opening: datetime.time, start_time_buffer: int,
multistart: bool):
def __init__(self, waypoints: List[Waypoint], timezone: int, start_opening: datetime.datetime,
start_time_buffer: int, multistart: bool):
"""
:param waypoints:
:param timezone: time difference wrt UTC in hours
Expand Down Expand Up @@ -150,7 +150,7 @@ def determine_refined_start(self, trace, fixes):

for fix, next_fix in double_iterator(interpolated_fixes):
if self.started(fix, next_fix):
return fix['time']
return fix['datetime']

raise ValueError('Start should have been determined')

Expand Down
7 changes: 2 additions & 5 deletions opensoar/task/trip.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
from opensoar.utilities.helper_functions import seconds_time_difference


class Trip:
"""
Realised
Expand Down Expand Up @@ -47,11 +44,11 @@ def fix_on_leg(self, fix, leg):
return larger_than_minimum and smaller_than_maximum

def fix_before_leg(self, fix, leg):
return seconds_time_difference(fix['time'], self.fixes[leg]['time']) >= 0
return (self.fixes[leg]['datetime'] - fix['datetime']).total_seconds() >= 0

def fix_after_leg(self, fix, leg):
if leg + 1 <= self.completed_legs():
return seconds_time_difference(self.fixes[leg + 1]['time'], fix['time']) >= 0
return (fix['datetime'] - self.fixes[leg + 1]['datetime']).total_seconds() >= 0
elif self.outlanded() and leg == self.outlanding_leg():
return False
else: # leg > self.completed_legs() + 1
Expand Down
15 changes: 7 additions & 8 deletions opensoar/thermals/pysoar_thermal_detector.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from opensoar.utilities.helper_functions import triple_iterator, calculate_bearing_change, calculate_distance_bearing, \
seconds_time_difference
from opensoar.utilities.helper_functions import triple_iterator, calculate_bearing_change, calculate_distance_bearing


class PySoarThermalDetector:
Expand Down Expand Up @@ -34,13 +33,13 @@ def analyse(self, trace):

for fix_minus2, fix_minus1, fix in triple_iterator(trace):

time_minus2 = fix_minus2['time']
time_minus1 = fix_minus1['time']
time = fix['time']
time_minus2 = fix_minus2['datetime']
time_minus1 = fix_minus1['datetime']
time = fix['datetime']

bearing_change = calculate_bearing_change(fix_minus2, fix_minus1, fix)
delta_t = (0.5 * seconds_time_difference(time_minus1, time) +
0.5 * seconds_time_difference(time_minus2, time))
delta_t = (0.5 * (time - time_minus1).total_seconds() +
0.5 * (time - time_minus2).total_seconds())
bearing_change_rate = bearing_change / delta_t

if cruise:
Expand Down Expand Up @@ -101,7 +100,7 @@ def analyse(self, trace):
possible_cruise_fixes.append(fix)
total_bearing_change += bearing_change

delta_t = seconds_time_difference(possible_cruise_fixes[0]['time'], time)
delta_t = (time - possible_cruise_fixes[0]['datetime']).total_seconds()
cruise_distance, _ = calculate_distance_bearing(possible_cruise_fixes[0], fix)
temp_bearing_rate_avg = 0 if delta_t == 0 else total_bearing_change / delta_t

Expand Down
69 changes: 4 additions & 65 deletions opensoar/utilities/helper_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,6 @@ def altitude_gain_and_loss(fixes: List[dict], gps_altitude=True):
return gain, loss


def seconds_time_difference_fixes(fix1, fix2):
return seconds_time_difference(fix1['time'], fix2['time'])


def total_distance_travelled(fixes: List[dict]):
"""Calculates the total distance, summing over the inter fix distances"""
distance = 0
Expand All @@ -138,61 +134,6 @@ def total_distance_travelled(fixes: List[dict]):
return distance


def seconds_time_difference(time1: datetime.time, time2: datetime.time):
"""
Determines the time difference between to datetime.time instances, mocking the operation time2 - time1
It is assumed that both take place at the same day.
:param time1:
:param time2:
:return: time difference in seconds
"""

today = datetime.date.today()
time_diff = datetime.datetime.combine(today, time2) - datetime.datetime.combine(today, time1)
return time_diff.total_seconds()


def add_times(start_time: datetime.time, delta_time: datetime.timedelta):
"""
Helper to circumvent problem that normal datetime.time instances can not be added.
:param start_time:
:param delta_time:
:return:
"""
full_datetime_start = datetime.datetime.combine(datetime.date.today(), start_time)

full_datetime_result = full_datetime_start + delta_time
return full_datetime_result.time()


def subtract_times(start_time: datetime.time, delta_time: datetime.timedelta):
full_datetime_start = datetime.datetime.combine(datetime.date.today(), start_time)
full_datetime_result = full_datetime_start - delta_time
return full_datetime_result.time()


def add_seconds(time: datetime.time, seconds: int) -> datetime.time:
"""
Add seconds to datetime.time object and return resulting datetime.time object.

:param time:
:param seconds: not limited to 0-59.
:return:
"""

additional_seconds = seconds

additional_hours = additional_seconds // 3600
additional_seconds -= additional_hours * 3600

additional_minutes = additional_seconds // 60
additional_seconds -= additional_minutes * 60

return add_times(time, datetime.timedelta(hours=additional_hours,
minutes=additional_minutes,
seconds=additional_seconds))


def range_with_bounds(start: int, stop: int, interval: int) -> List[int]:
"""Return list"""
result = [int(val) for val in range(start, stop, interval)]
Expand All @@ -202,8 +143,7 @@ def range_with_bounds(start: int, stop: int, interval: int) -> List[int]:


def calculate_time_differences(time1, time2, interval):
total_difference = int(seconds_time_difference(time1, time2))
differences = range_with_bounds(0, total_difference, interval)
differences = range_with_bounds(0, int((time2 - time1).total_seconds()), interval)
return differences


Expand All @@ -217,17 +157,16 @@ def interpolate_fixes(fix1, fix2, interval=1):
:return: list of fixes between fix1 and fix2 with given interval
"""

time_differences = calculate_time_differences(fix1['time'], fix2['time'], interval)
time_differences = calculate_time_differences(fix1['datetime'], fix2['datetime'], interval)

fixes = list()
for difference in time_differences:
fraction = difference / time_differences[-1]

lat = fix1['lat'] + fraction * (fix2['lat'] - fix1['lat'])
lon = fix1['lon'] + fraction * (fix2['lon'] - fix1['lon'])
time = add_seconds(fix1['time'], difference)

fixes.append(dict(time=time, lat=lat, lon=lon))
time = fix1['datetime'] + datetime.timedelta(seconds=difference)
fixes.append(dict(datetime=time, lat=lat, lon=lon))

return fixes

Expand Down
8 changes: 4 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pyproj
aerofiles
beautifulsoup4
geojson
shapely
aerofiles==1.4.0
beautifulsoup4==4.12.3
geojson==3.1.0
shapely==2.0.6
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decide what to do with this. fixing them completely might be too strict. this is a library that is used by others. they need some freedom to use other versions

2 changes: 1 addition & 1 deletion tests/competition/test_soaringspot.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,5 +194,5 @@ def test_get_info_from_comment_lines_no_lcu_no_lseeyou(self):
with open(igc_path, 'r', encoding='latin1') as f:
parsed_igc_file = Reader().read(f)

task, contest_information, competitor_information = get_info_from_comment_lines(parsed_igc_file)
task, contest_information, competitor_information = get_info_from_comment_lines(parsed_igc_file, date=datetime.date(2023, 7, 4))
self.assertIsNone(task, None)
8 changes: 5 additions & 3 deletions tests/competition/test_strepla.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from opensoar.competition.competitor import Competitor
from opensoar.competition.strepla import get_waypoint_name_lat_long, get_waypoints, get_waypoint, get_task_and_competitor_info, get_info_from_comment_lines
from opensoar.task.aat import AAT
from opensoar.utilities.helper_functions import seconds_time_difference


class TestStrepla(unittest.TestCase):
Expand Down Expand Up @@ -107,8 +106,11 @@ def test_aat_from_file(self):
competitor = Competitor(trace, 'CX', 'Discus2b', 1, 'Karsten Leucker')
competitor.analyse(task, 'pysoar')

time_diff = seconds_time_difference(competitor.trip.refined_start_time, datetime.time(13, 22, 40))
self.assertEqual(competitor.trip.refined_start_time.hour, 13)
self.assertEqual(competitor.trip.refined_start_time.minute, 22)
seconds = competitor.trip.refined_start_time.second

dist_diff = sum(competitor.trip.distances) - 283500
self.assertLessEqual(abs(time_diff), 1)
self.assertLessEqual(abs(seconds-40), 1)
self.assertEqual(len(competitor.trip.fixes), len(expected_waypoints))
self.assertLessEqual(abs(dist_diff), 1000)
6 changes: 3 additions & 3 deletions tests/task/helper_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

def get_trace(igc_path):
with open(igc_path, 'r') as f:
parsed_igc_file = Reader().read(f)
parsed_igc_file = Reader(skip_duplicates=True).read(f)

_, trace = parsed_igc_file['fix_records']

Expand All @@ -14,7 +14,7 @@ def get_trace(igc_path):

def get_task(igc_path):
with open(igc_path, 'r') as f:
parsed_igc_file = Reader().read(f)
parsed_igc_file = Reader(skip_duplicates=True).read(f)

task, contest_information, competitor_information = get_info_from_comment_lines(parsed_igc_file)
task, contest_information, competitor_information = get_info_from_comment_lines(parsed_igc_file, date=parsed_igc_file["header"][1]["utc_date"])
return task
Loading