otime/otime/otime.py

464 lines
18 KiB
Python

import copy
import datetime
import re
import xml.etree.ElementTree as etree
class Runner:
def __init__(self, id: int, first: str, last: str, club=None, club_id=None,
country=None, card_id=None, o_class_str=None, o_class=None,
fork=0, start_time=None, fee_id=None, fee=None, status_override=''):
self.id = id
self.first = first
self.last = last
self.club = club
self.club_id = club_id
self.country = country
self.card_id = card_id
self.o_class = o_class
self.fork = fork
self.start_time = start_time
self.fee_id = fee_id
self.status_override = status_override
def __repr__(self):
return(f'name({self.fullname()})')
def from_ttime_string(ttime_db_line):
# Reads 1 line of a ttime db
#https://web.archive.org/web/20191229124046/http://wiki.ttime.no/index.php/Developer
runner_data = ttime_db_line.split(';')
eventorid = runner_data[0]
country = ''
name = runner_data[2].split(',')
try:
first = name[1].strip()
except:
first = ''
last = name[0]
try:
club = runner_data[4]
except:
club = ''
try:
card = int(runner_data[6])
except:
card = 0
try:
o_class = runner_data[3]
except:
o_class = ''
options = runner_data[5].split(',')
try:
club_id = options[options.index('A')+3]
except:
club_id = 0
try:
start_time = options[options.index('U')+1]
except:
start_time = None
return Runner(eventorid, first, last, club=club, club_id=club_id,
country=country, card_id=card, o_class=o_class, start_time=start_time)
def fullname(self):
return '{} {}'.format(self.first, self.last)
class CardDump:
def __init__(self, card, controls, splits, read_time, s_time, f_time):
self.card = card
self.controls = controls
self.splits = splits
self.read_time = read_time
self.s_time = s_time
self.f_time = f_time
def __repr__(self):
return f'card({self.card}) controls({self.controls}) splits({self.splits})'
def from_mtr_bytes(datamsg):
card = int.from_bytes(datamsg[20:23], 'little')
#Extract codes and splits:
controls = []
splits = []
splits_offset = 26
code_numbytes = 1
time_numbytes = 2
split_numbytes = code_numbytes + time_numbytes
for split_index in range(50):
code_offset = splits_offset + split_index * split_numbytes
time_offset = code_offset + code_numbytes
code = int.from_bytes(datamsg[code_offset:code_offset+code_numbytes], 'little')
time = int.from_bytes(datamsg[time_offset:time_offset+time_numbytes], 'little')
if code != 0:
controls.append(code)
splits.append(time)
# Extract start time:
year = 2000 + int.from_bytes(datamsg[8:9], 'little')
month = int.from_bytes(datamsg[9:10], 'little')
day = int.from_bytes(datamsg[10:11], 'little')
hours = int.from_bytes(datamsg[11:12], 'little')
minutes = int.from_bytes(datamsg[12:13], 'little')
seconds = int.from_bytes(datamsg[13:14], 'little')
milliseconds = int.from_bytes(datamsg[14:16], 'little')
read_time = datetime.datetime(year, month, day, hours, minutes, seconds, milliseconds)
if len(controls) > 2:
s_time = read_time - datetime.timedelta(seconds=splits[-1])
f_time = read_time - (datetime.timedelta(seconds=splits[-1]) + datetime.timedelta(seconds=splits[-2]))
else:
s_time = read_time
f_time = read_time
return(CardDump(card, controls, splits, read_time, s_time, f_time))
def list_from_mtr_f(mtr_file):
with open(mtr_file) as f:
data = [i.replace('"','').split(',') for i in f.readlines()]
cards = []
# hver rad er brikkenummer med tilhørende info
for row in data:
controls = []
splits = []
# postkodene kommer på oddetall fra og med den 11. De blir hevet inn i controls
for item in row[11::2]:
if item == '250':
controls.append(int(item))
break
elif item == '000':
break
else:
controls.append(int(item))
# strekktidene kommer på partall fra og med den 12. De blir hevet i splits.
for item in row[12::2]:
if item == '00000':
break
else:
splits.append(int(item))
# looper gjonnom løperobjektene og legger til poster og strekktider + start og sluttid
# usikker på om dette er riktig klokeslett
tl = row[5].split(' ')
tl[0] = tl[0].split('.')
tl[0][2] = '20' + tl[0][2]
tl[0] = list(map(int, tl[0]))
tl[1] = tl[1].split(':')
tl[1][2] = float(tl[1][2])
tl[1] = list(map(int, tl[1]))
read_time = datetime.datetime(tl[0][2], tl[0][1], tl[0][0], tl[1][0], tl[1][1], tl[1][2])
if len(controls) > 2 and len(splits) > 2:
s_time = read_time - datetime.timedelta(seconds=splits[-1])
f_time = read_time - (datetime.timedelta(seconds=splits[-1]) + datetime.timedelta(seconds=splits[-2]))
else:
s_time = read_time
f_time = read_time
# Remove mtr from splits:
if controls[-1] == 250:
controls.pop(-1)
splits.pop(-1)
cards.append(CardDump(int(row[6]), controls, splits, read_time, s_time, f_time))
return cards
# Stored in Event.courses
class Course:
def __init__(self, name, codes, forked=False):
self.name = name
# Codes is a list of courses
self.codes = codes
self.forked = forked
def __repr__(self):
return f'name({self.name})'
# Stored in Event.o_classes
class OClass:
def __init__(self, name, course):
self.name = name
self.course = course
def __repr__(self):
return f'name({self.name})'
class RunnerResult:
def __init__(self, runner_id: int, first: str, last: str, status: str, place: int, total_time: int, splits: list[int], end_time,
club=None, club_id=None, country=None, card_id=None, o_class=None, controls=None, fork=0, start_time=datetime.datetime(1973, 1, 1), fee_id=None, missed_controls=None, ran_other_course=None):
self.id = runner_id
self.first = first
self.last = last
self.club = club
self.club_id = club_id
self.country = country
self.card_id = card_id
self.o_class = o_class
self.controls = controls
self.fork = fork
self.start_time = start_time
self.fee_id = fee_id
self.status = status
self.place = place
self.total_time = total_time
self.splits = splits
self.end_time = end_time
# List of controls missed
self.missed_controls = missed_controls
# If the runner ran other course it is named here
self.ran_other_course = ran_other_course
def fullname(self):
return f'{self.first} {self.last}'
class ClassResult:
def __init__(self, name: str, course: Course, runner_results: list[RunnerResult]):
self.name = name
self.course = course
self.runner_results = runner_results
class Event:
def __init__(self, eventid=0, name=None, start_time=None, end_time=None,
organiser=None, courses=[], o_classes=[], runners=[],
card_dumps=[], fees=[]):
self.id = eventid
self.name = name
self.start_time = start_time
self.end_time = end_time
self.organiser = organiser
self.courses = courses
self.o_classes = o_classes
self.runners = runners
self.card_dumps = card_dumps
self.fees = fees
def __repr__(self):
return(f'id({self.id}), name({self.name}), organiser({self.organiser}), courses({self.courses}), o_classes({self.o_classes}), runners({self.runners})')
def add_course(self, *args):
for n in args:
self.courses.append(n)
def add_o_class(self, *args):
for n in args:
self.o_classes.append(n)
def add_runners(self, *args):
for n in args:
self.runners.append(n)
def get_runners_in_o_class(self, o_class: str) -> list[Runner]:
return [i for i in self.runners if i.o_class == o_class]
def add_fees(self, *args):
for n in args:
self.fees.append(n)
def get_course(self, name):
return next((copy.copy(i) for i in self.courses if i.name == name), None)
def get_o_class(self, name):
return next((copy.copy(i) for i in self.o_classes if i.name == name), None)
def get_card_dump(self, id):
return next((copy.copy(i) for i in self.card_dumps if i.card == id), None)
def get_runner(self, id):
return next((copy.copy(i) for i in self.runners if i.id == id), None)
def get_runner_status(self, id):
# https://github.com/international-orienteering-federation/datastandard-v3/blob/24eb108e4c6b5e2904e5f8f0e49142e45e2c5230/IOF.xsd#L2903C3-L2903C3
runner = self.get_runner(id)
if runner.status_override: return runner.status_override
o_class = self.get_o_class(runner.o_class)
if not o_class:
return 'Inactive'
course = self.get_course(o_class.course)
if not self.get_card_dump(runner.card_id):
return 'Active'
if contains(course.codes[runner.fork], self.get_card_dump(runner.card_id).controls):
return 'OK'
else:
return 'MissingPunch'
def get_runner_result(self, runner_id):
runner = self.get_runner(runner_id)
return next((i for i in produce_class_result(self, runner.o_class).runner_results if i.id == runner_id), None)
def get_runner_o_class(self, id):
runner = self.get_runner(id)
return next((copy.copy(i) for i in self.o_classes if i.name == runner.o_class), None)
def get_runner_time(self, id):
runner = self.get_runner(id)
card_dump = self.get_card_dump(runner.card_id)
course = self.get_course(self.get_runner_o_class(id).course)
if card_dump == None or course == None:
return False
f_control = course.codes[runner.fork][-1]
# TODO: Må gjøres mer robust
if f_control not in card_dump.controls:
# Hvis løperen ikke har vært på sistepost tar vi siste stempling istedet
return card_dump.splits[-1]
index = card_dump.controls.index(f_control)
# Hvis løperen ikke har en startid spesifisert brukes brikketid
if runner.start_time is None:
return card_dump.splits[index]
#Hvis det er en startid finner jeg tidsforskjellen mellom brikkestart og faktisk start og trekker den fra totaltida
else:
time_list = runner.start_time.split(':') # hour, minute, second
start_datetime = self.start_time.replace(hour=int(time_list[0]), minute=int(time_list[1]), second=int(time_list[2]))
diff = start_datetime - card_dump.s_time
return card_dump.splits[index] - diff.total_seconds()
def get_runner_splits(self, id):
# Tida brukt frem til hver post, ikke tida fra forrige post
try:
runner = self.get_runner(id)
card_dump = self.get_card_dump(runner.card_id)
course = self.get_course(self.get_o_class(runner.o_class).course)
codes = course.codes[runner.fork]
except AttributeError:
return None
if card_dump == None:
return None
split_iter = zip(card_dump.controls, card_dump.splits).__iter__()
splits = [0] * len(codes)
for n, control in enumerate(codes):
if control not in card_dump.controls:
continue
while True:
try:
punched_control, split = next(split_iter)
except StopIteration:
break
if punched_control == control:
splits[n] = split
break
return splits
def get_runner_controls(self, id):
try:
return self.get_card_dump(self.get_runner(id).card_id).controls
except AttributeError:
return None
def get_runner_end_clock(self, id):
try:
return self.get_card_dump(self.get_runner(id).card_id).f_time
except AttributeError:
return None
def get_result(self, o_classes: list[str] = []) -> list[ClassResult]:
if not o_classes:
o_classes = [i.name for i in self.o_classes]
return [produce_class_result(self, i) for i in o_classes]
def read_xml_entries(self, xml_file):
self.add_runners(*runners_from_xml_entries(xml_file))
self.add_fees(*fees_from_xml_entries(xml_file))
# Må endres
def read_xml_courses(self, xml_file):
self.courses = courses_from_xml(xml_file)
def read_ttime_cnf(self, ttime_file):
self.add_course(*courses_from_ttime_conf(ttime_file))
self.add_o_class(*classes_from_ttime_conf(ttime_file, self.courses))
def read_ttime_db(self, ttime_file):
with open(ttime_file) as f:
data = f.readlines()
self.runners = [Runner.from_ttime_string(i) for i in data if i]
def read_mtr_file(self, mtr_file):
self.card_dumps = CardDump.list_from_mtr_f(mtr_file)
def create_start_list_pdf(self, file_path):
pdf.create_start_list(copy.deepcopy(self), file_path)
def create_result_pdf(self, file_path):
pdf.create_result_list(copy.deepcopy(self), file_path)
class Fee:
def __init__(self, fee_id, name, currency, amount, from_birth_date=None,
to_birth_date=None):
self.id = fee_id
self.name = name
self.currency = currency
self.amount = amount
self.from_birth_date = from_birth_date
self.to_birth_date = to_birth_date
def produce_class_result(event, o_class_name) -> ClassResult:
o_class = event.get_o_class(o_class_name)
runners = event.get_runners_in_o_class(o_class_name)
ok_runners = [i for i in runners if event.get_runner_status(i.id) == 'OK']
ok_runners.sort(key=lambda i: event.get_runner_time(i.id))
dsq_runners = [i for i in runners if event.get_runner_status(i.id) == 'MissingPunch' or event.get_runner_status(i.id) == 'Disqualified']
other_runners = [i for i in runners if i not in ok_runners and i not in dsq_runners]
results = [RunnerResult(i.id, i.first, i.last, event.get_runner_status(i.id), ok_runners.index(i)+1, event.get_runner_time(i.id), event.get_runner_splits(i.id),
event.get_runner_end_clock(i.id), i.club, 0, i.country, i.card_id, i.o_class, event.get_runner_controls(i.id), start_time=event.get_card_dump(i.card_id).s_time) for i in ok_runners]
results += [RunnerResult(i.id, i.first, i.last, event.get_runner_status(i.id), 0, event.get_runner_time(i.id), event.get_runner_splits(i.id),
event.get_runner_end_clock(i.id), i.club, 0, i.country, i.card_id, i.o_class, event.get_runner_controls(i.id),
start_time=event.get_card_dump(i.card_id).s_time,
missed_controls=find_missed_controls(event.get_runner_controls(i.id), event.get_course(o_class.course).codes[i.fork]))
for i in dsq_runners]
results += [RunnerResult(i.id, i.first, i.last, event.get_runner_status(i.id), 0, event.get_runner_time(i.id), event.get_runner_splits(i.id),
event.get_runner_end_clock(i.id), i.club, 0, i.country, i.card_id, i.o_class, event.get_runner_controls(i.id)) for i in other_runners]
return ClassResult(o_class.name, event.get_course(o_class.course), results)
# TODO: Take string instead of file.
def courses_from_ttime_conf(ttime_file):
with open(ttime_file, 'r') as f:
data = f.readlines()
courses = []
for line in data:
if '-codes' in line:
code_list = re.search(r'(?<=\")(.*?)(?=\")', line).group().split(';')
loops = 0
for n in code_list:
n = n.split(',')
loops += 1
n = list(map(int, n))
courses.append(Course('course_'+str(loops), [n]))
return courses
def classes_from_ttime_conf(ttime_file, courses):
with open(ttime_file, 'r') as f:
data = f.readlines()
o_classes = []
for line in data:
if '-courses' in line:
raw_courselist = re.search(r'(?<=\")(.*?)(?=\")', line).group().split(';')
loops = 0
for n in raw_courselist:
split = n.split(',')
for i in split:
o_classes.append(OClass(i, courses[loops].name))
loops += 1
return o_classes
# Checks if small list is in big list
def contains(small, big):
valid = True
mark = 0
map_bl = []
for i in small:
for n, control in enumerate(big[mark:]):
if i == control:
mark += n
map_bl.append(mark)
break
else:
valid = False
if valid:
return map_bl
else:
return False
def find_missed_controls(punches, codes):
return [i for i in codes if i not in punches]