forked from Trygve/otime
Trygve
e11e25ea81
Alle import metoder og funksjoner tar nå filobjekter istedet for paths. Runner objektet bruker nå kwargs.
640 lines
22 KiB
Python
640 lines
22 KiB
Python
import datetime
|
|
import csv
|
|
import re
|
|
import json
|
|
import xml.etree.ElementTree as ET
|
|
from fpdf import FPDF
|
|
|
|
|
|
# The event object stores all the event data.
|
|
# A .otime file is more or less just a json dump of the Event object.
|
|
class Event:
|
|
def __init__(self, eventid, name, **kwargs):
|
|
self.id = eventid
|
|
self.name = name
|
|
|
|
try:
|
|
self.courses = kwargs['courses']
|
|
except KeyError:
|
|
self.courses = []
|
|
|
|
try:
|
|
self.o_classes = kwargs['o_classes']
|
|
except KeyError:
|
|
self.o_classes = []
|
|
|
|
try:
|
|
self.runners = kwargs['runners']
|
|
except KeyError:
|
|
self.runners = []
|
|
|
|
try:
|
|
self.card_dumps = kwargs['card_dumps']
|
|
except KeyError:
|
|
self.card_dumps = []
|
|
|
|
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 import_xml_entries(self, xml_file):
|
|
self.add_runners(*runners_from_xml_entries(xml_file, self.o_classes))
|
|
|
|
def import_ttime_cnf(self, ttime_file):
|
|
self.add_course(*courses_from_ttime_conf(ttime_file))
|
|
ttime_file.seek(0)
|
|
self.add_o_class(*classes_from_ttime_conf(ttime_file, self.courses))
|
|
|
|
def import_ttime_db(self, ttime_file):
|
|
csvreader = csv.reader(ttime_file, delimiter=';',)
|
|
runnerarray = []
|
|
for row in csvreader:
|
|
if len(row) == 0 or row[1] == '':
|
|
continue
|
|
runnerarray.append(Runner.from_string(row, self.o_classes))
|
|
self.runners = runnerarray
|
|
|
|
def import_mtr_file(self, mtr_file):
|
|
self.card_dumps = CardDump.list_from_mtr_f(mtr_file)
|
|
|
|
def match_runners_cards(self):
|
|
for r in self.runners:
|
|
for d in self.card_dumps:
|
|
if r.card == d.card:
|
|
r.card_r = d
|
|
def match_runners_o_classes(self):
|
|
for r in self.runners:
|
|
for c in self.o_classes:
|
|
if r.o_class_str == c.name:
|
|
r.o_class = c
|
|
|
|
def match_o_classes_courses(self):
|
|
for oc in self.o_classes:
|
|
for c in self.courses:
|
|
if oc.course_str == c.name:
|
|
oc.course = c
|
|
def match_all(self):
|
|
self.match_runners_cards()
|
|
self.match_runners_o_classes()
|
|
self.match_o_classes_courses()
|
|
|
|
def get_xml_res(self):
|
|
root = ET.Element('ResultList')
|
|
root.set('xmlns', 'http://www.orienteering.org/datastandard/3.0')
|
|
root.set('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance')
|
|
root.set('iofVersion', '3.0')
|
|
root.set('createTime', datetime.datetime.now().isoformat(timespec='seconds'))
|
|
root.set('creator', 'oTime')
|
|
root.set('status', 'Complete')
|
|
tree = ET.ElementTree(root)
|
|
event = ET.SubElement(root, 'Event')
|
|
xml_child(event, 'Id', self.id)
|
|
xml_child(event, 'Name', self.name)
|
|
for i in self.o_classes:
|
|
# <ClassResult>
|
|
class_result = ET.SubElement(root, 'ClassResult')
|
|
# <Class>
|
|
t = ET.SubElement(class_result, 'Class')
|
|
xml_child(t, 'Name', i.name)
|
|
# <PersonResult>
|
|
runners_same_c = get_runners_in_class(self.runners, i)
|
|
runners_ranked = rank_runners(runners_same_c, i)
|
|
# Put the OK runners first and Active last
|
|
runners_sorted = [i for i in runners_same_c if i not in runners_ranked]
|
|
runners_ranked.extend(runners_sorted)
|
|
|
|
for n in runners_ranked:
|
|
person_result = ET.SubElement(class_result, 'PersonResult')
|
|
# <Person>
|
|
person = ET.SubElement(person_result, 'Person')
|
|
xml_child(person, 'Id', n.id)
|
|
# <Name>
|
|
name = ET.SubElement(person, 'Name')
|
|
xml_child(name, 'Family', n.last)
|
|
xml_child(name, 'Given', n.first)
|
|
# </Name>
|
|
# </Person>
|
|
# <Organisation>
|
|
org = ET.SubElement(person_result, 'Organisation')
|
|
xml_child(org, 'Id', n.club_id)
|
|
xml_child(org, 'Name', n.club)
|
|
country = ET.SubElement(org, 'Country')
|
|
# TODO: hent land fra løperobjektet
|
|
country.text = 'Norway'
|
|
country.set('code', 'NOR')
|
|
# </Organisation>
|
|
# <Result>
|
|
result = ET.SubElement(person_result, 'Result')
|
|
if hasattr(n, 'card_r') and len(n.card_r.splits) > 2:
|
|
xml_child(result, 'StartTime', n.card_r.s_time.isoformat())
|
|
xml_child(result, 'FinishTime', n.card_r.f_time.isoformat())
|
|
xml_child(result, 'Time', n.totaltime())
|
|
if n.status() == 'OK':
|
|
# <TimeBehind>
|
|
xml_child(result, 'TimeBehind', n.totaltime() - runners_ranked[0].totaltime())
|
|
# </TimeBehind>
|
|
xml_child(result, 'Position', n.rank(self.runners))
|
|
xml_child(result, 'Status', n.status())
|
|
# <SplitTime>
|
|
# TODO: ta utgangspunkt i løypa, ikke det brikka har stempla
|
|
for code, split in zip(n.card_r.controls, n.card_r.splits):
|
|
st = ET.SubElement(result, 'SplitTime')
|
|
xml_child(st, 'ControlCode', code)
|
|
xml_child(st, 'Time', split)
|
|
if code == n.o_class.course.codes[-1]:
|
|
break
|
|
# </SplitTime>
|
|
elif n.status() == 'Disqualified':
|
|
xml_child(result, 'Status', n.status())
|
|
for code in n.o_class.course.codes:
|
|
st = ET.SubElement(result, 'SplitTime')
|
|
xml_child(st, 'ControlCode', code)
|
|
for control, split in zip(n.card_r.controls, n.card_r.splits):
|
|
if code == control:
|
|
xml_child(st, 'Time', split)
|
|
else:
|
|
xml_child(result, 'Status', n.status())
|
|
else:
|
|
xml_child(result, 'Status', n.status())
|
|
# </Result>
|
|
# </PersonResult>
|
|
# </Class>
|
|
ET.indent(root, space=' ', level=0)
|
|
return tree
|
|
|
|
def create_json_file(self):
|
|
rdicts = []
|
|
for runner in self.runners:
|
|
rdicts.append(runner.asdict())
|
|
|
|
cdicts = []
|
|
for course in self.courses:
|
|
cdicts.append(course.asdict())
|
|
|
|
ocdicts = []
|
|
for o_class in self.o_classes:
|
|
ocdicts.append(o_class.asdict())
|
|
|
|
ddicts = []
|
|
for dump in self.card_dumps:
|
|
ddicts.append(dump.asdict())
|
|
|
|
json_data = {
|
|
'id': self.id,
|
|
'name': self.name,
|
|
'runners': rdicts,
|
|
'courses': cdicts,
|
|
'o_classes': ocdicts,
|
|
'card_dumps': ddicts
|
|
}
|
|
return json.dumps(json_data, sort_keys=True, indent=4)
|
|
|
|
# Get event object from .otime json file
|
|
def from_json(f):
|
|
data = json.load(f)
|
|
runners = []
|
|
for r in data['runners']:
|
|
runners.append(Runner(r['id'], r['first'], r['last'], club_id=r['club_id'], club=r['club'], country=r['country'], card=r['card'], o_class_str=r['o_class_str'], fork=r['fork'],start_time=r['start_time']))
|
|
|
|
courses = []
|
|
for c in data['courses']:
|
|
courses.append(Course(c['name'], c['codes']))
|
|
|
|
o_classes = []
|
|
for c in data['o_classes']:
|
|
o_classes.append(OClass(c['name'], c['course_str'], None))
|
|
|
|
card_dumps = []
|
|
for d in data['card_dumps']:
|
|
card_dumps.append(CardDump(d['card'], d['controls'], d['splits'], datetime.datetime.fromisoformat(d['read_time']), datetime.datetime.fromisoformat(d['s_time']), datetime.datetime.fromisoformat(d['f_time'])))
|
|
|
|
return Event(data['id'], data['name'], runners=runners, courses=courses, o_classes=o_classes, card_dumps=card_dumps)
|
|
|
|
def create_start_list_pdf(self, file_name):
|
|
pdf = FPDF()
|
|
pdf.add_page()
|
|
pdf.add_font("DejaVuSans", fname="/usr/share/fonts/truetype/DejaVuSans.ttf")
|
|
pdf.set_font("DejaVuSans", size=10)
|
|
line_height = pdf.font_size * 2
|
|
col_width = pdf.epw / 4 # distribute content evenly
|
|
for runner in self.runners:
|
|
pdf.multi_cell(col_width, line_height, runner.fullname(), border=1, ln=3, max_line_height=pdf.font_size, align='L')
|
|
pdf.multi_cell(col_width, line_height, runner.o_class.name, border=1, ln=3, max_line_height=pdf.font_size)
|
|
pdf.multi_cell(col_width, line_height, str(runner.card), border=1, ln=3, max_line_height=pdf.font_size)
|
|
if runner.start_time != None:
|
|
pdf.multi_cell(col_width, line_height, str(runner.start_time), border=1, ln=3, max_line_height=pdf.font_size)
|
|
else:
|
|
pdf.multi_cell(col_width, line_height, '', border=1, ln=3, max_line_height=pdf.font_size)
|
|
pdf.ln(line_height)
|
|
pdf.output(file_name)
|
|
|
|
# The runner object stores all the data specific to a runner.
|
|
class Runner:
|
|
def __init__(self, runner_id, first, last, **kwargs):
|
|
self.id = runner_id
|
|
self.first = first
|
|
self.last = last
|
|
try:
|
|
self.club = kwargs['club']
|
|
except KeyError:
|
|
self.club = None
|
|
|
|
try:
|
|
self.club_id = kwargs['club_id']
|
|
except KeyError:
|
|
self.club_id = None
|
|
|
|
try:
|
|
self.country = kwargs['country']
|
|
except KeyError:
|
|
self.country = None
|
|
|
|
try:
|
|
self.card = kwargs['card']
|
|
except KeyError:
|
|
self.card = 0
|
|
|
|
try:
|
|
self.o_class_str = kwargs['o_class_str']
|
|
except KeyError:
|
|
self.o_class = None
|
|
|
|
try:
|
|
self.o_class = kwargs['o_class']
|
|
except KeyError:
|
|
self.o_class = None
|
|
|
|
try:
|
|
self.fork = kwargs['fork']
|
|
except KeyError:
|
|
self.fork = 0
|
|
|
|
try:
|
|
self.start_time = kwargs['start_time']
|
|
except KeyError:
|
|
self.start_time = None
|
|
|
|
#self.o_class = o_class
|
|
#self.start_time = start_time
|
|
|
|
def from_string(tt_line, o_classes):
|
|
#https://web.archive.org/web/20191229124046/http://wiki.ttime.no/index.php/Developer
|
|
eventorid = tt_line[0]
|
|
country = ''
|
|
name = tt_line[2].split(',')
|
|
try:
|
|
first = name[1].strip()
|
|
except:
|
|
first = ''
|
|
last = name[0]
|
|
try:
|
|
club = tt_line[4]
|
|
except:
|
|
club = "None"
|
|
try:
|
|
card = int(tt_line[6])
|
|
except:
|
|
card = 0
|
|
runner_o_class = None
|
|
try:
|
|
raw_class_str = tt_line[3]
|
|
except:
|
|
# VELDIG MIDLERTIDIG
|
|
runner_o_class = None
|
|
else:
|
|
if raw_class_str != '':
|
|
for i in o_classes:
|
|
if i.name == raw_class_str:
|
|
runner_o_class = i
|
|
break
|
|
else:
|
|
runner_o_class = None
|
|
# TODO: Gjør sånn at den lager nye o klasser om den ikke finnes fra før
|
|
options = tt_line[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=card, o_class_str=raw_class_str,
|
|
o_class=runner_o_class, start_time=start_time)
|
|
|
|
def fullname(self):
|
|
return '{} {}'.format(self.first, self.last)
|
|
|
|
def check_codes(self):
|
|
# Returns False if not ok and touple if ok
|
|
return contains(self.o_class.course.codes, list(self.card_r.controls))
|
|
|
|
def totaltime(self):
|
|
f_control = self.o_class.course.codes[-1]
|
|
try:
|
|
index = self.card_r.controls.index(f_control)
|
|
return self.card_r.splits[index]
|
|
except:
|
|
return 0
|
|
|
|
def status(self):
|
|
if hasattr(self, 'card_r') == False or self.o_class == None:
|
|
return 'Active'
|
|
elif self.check_codes():
|
|
return 'OK'
|
|
elif self.check_codes() == False:
|
|
return 'Disqualified'
|
|
|
|
def rank(self, allrunners):
|
|
c_ranked = rank_runners(allrunners, self.o_class)
|
|
return c_ranked.index(self) + 1
|
|
|
|
def res_splits(self):
|
|
splits_cpy = self.card_r.splits.copy()
|
|
for control in self.card_r.controls:
|
|
if control not in self.o_class.course.codes:
|
|
index = self.card_r.controls.index(control)
|
|
split = self.card_r.splits[index]
|
|
splits_cpy.remove(split)
|
|
return splits_cpy # list
|
|
|
|
def asdict(self):
|
|
return {
|
|
'id': self.id,
|
|
'first': self.first,
|
|
'last': self.last,
|
|
'club_id': self.club_id,
|
|
'club': self.club_id,
|
|
'country': self.country,
|
|
'card': self.card,
|
|
'o_class_str': self.o_class_str,
|
|
'fork' : self.fork,
|
|
'start_time': self.start_time
|
|
}
|
|
|
|
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 = 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_f):
|
|
csvreader = csv.reader(mtr_f)
|
|
|
|
rows = []
|
|
cards = []
|
|
# hver rad er brikkenummer med tilhørende info
|
|
for row in csvreader:
|
|
if len(row) == 0:
|
|
continue
|
|
rows.append(row)
|
|
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
|
|
cards.append(CardDump(int(row[6]), controls, splits, read_time, s_time, f_time))
|
|
return cards
|
|
|
|
def asdict(self):
|
|
return {
|
|
'card': self.card,
|
|
'controls': self.controls,
|
|
'splits': self.splits,
|
|
'read_time': self.read_time.isoformat(),
|
|
's_time': self.s_time.isoformat(),
|
|
'f_time': self.f_time.isoformat()
|
|
}
|
|
|
|
# Stored in Event.courses
|
|
class Course:
|
|
def __init__(self, name, codes, **kwargs):
|
|
self.name = name
|
|
self.codes = codes
|
|
|
|
try:
|
|
forked = kwargs['forked']
|
|
except KeyError:
|
|
forked = False
|
|
|
|
try:
|
|
variations = kwargs['variations']
|
|
except KeyError:
|
|
variations = None
|
|
|
|
def __repr__(self):
|
|
return f'name({self.name})'
|
|
|
|
def asdict(self):
|
|
return {
|
|
'name': self.name,
|
|
'codes': self.codes
|
|
}
|
|
|
|
# Stored in Event.o_classes
|
|
class OClass:
|
|
def __init__(self, name, course_str, course):
|
|
self.name = name
|
|
self.course_str = course_str
|
|
self.course = course
|
|
|
|
def __repr__(self):
|
|
return f'name({self.name})'
|
|
|
|
def asdict(self):
|
|
return {
|
|
'name': self.name,
|
|
'course_str': self.course_str
|
|
}
|
|
|
|
# TODO: Take string instead of file.
|
|
def courses_from_ttime_conf(ttime_file):
|
|
courses = []
|
|
conf = ttime_file.readlines()
|
|
for line in conf:
|
|
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):
|
|
o_classes = []
|
|
conf = ttime_file.readlines()
|
|
for line in conf:
|
|
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, courses[loops]))
|
|
loops += 1
|
|
return o_classes
|
|
|
|
def runners_from_xml_entries(xml_file, o_classes=[]):
|
|
tree = ET.parse(xml_file)
|
|
root = tree.getroot()
|
|
runnerarray = []
|
|
|
|
for person_root in root[1:]:
|
|
rid = person_root[1][0].text
|
|
first = person_root[1][1][1].text
|
|
last = person_root[1][1][0].text
|
|
try:
|
|
club_id = person_root[2][0].text
|
|
club = person_root[2][1].text
|
|
except:
|
|
club = "None"
|
|
country = person_root[1][3].text
|
|
try:
|
|
card = int(person_root[3].text)
|
|
except:
|
|
card = 0
|
|
runner_o_class = None
|
|
try:
|
|
xml_class_str = person_root[4][1].text
|
|
except:
|
|
# VELDIG MIDLERTIDIG
|
|
runner_o_class = o_class_list[0]
|
|
else:
|
|
for i in o_classes:
|
|
if i.name == xml_class_str:
|
|
runner_o_class = i
|
|
break
|
|
# Gjør sånn at den lager nye o klasser om den ikke finnes fra før
|
|
|
|
start_time = None
|
|
runnerarray.append(Runner(rid, first, last, club=club, club_id=club_id,
|
|
country=country,card=card, o_class_str=xml_class_str,
|
|
o_class=runner_o_class, start_time=start_time))
|
|
#print(rid, first, last, club_id, club, card, xml_class_str)
|
|
|
|
return runnerarray
|
|
|
|
# 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 get_runners_in_class(runners, o_class):
|
|
# Filters out runner objects that dont have the correct o_class
|
|
list_filtrd = []
|
|
for i in runners:
|
|
marker = 0
|
|
if i.o_class == o_class:
|
|
list_filtrd.append(i)
|
|
return list_filtrd
|
|
|
|
def rank_runners(allrunners, o_class):
|
|
runners = get_runners_in_class(allrunners, o_class)
|
|
runners_ranked = []
|
|
for i in runners:
|
|
if i.status() == 'OK':
|
|
runners_ranked.append(i)
|
|
runners_ranked.sort(key=lambda x: x.totaltime())
|
|
return runners_ranked
|
|
|
|
# Used to make creating xml files easier
|
|
def xml_child(parent, tag, content):
|
|
e = ET.SubElement(parent, tag)
|
|
e.text = str(content)
|