Compare commits

21 Commits

Author SHA1 Message Date
865abb5213 La til gtk tabeell 2023-12-04 12:14:00 +01:00
5e899fca62 Fikser fra løpsdagen 2023-12-02 13:25:17 +01:00
a75af1a99b La til støtte for starttid i tidsberegninga 2023-11-28 16:38:04 +01:00
18f49c1a0c Bug fikser med totaltid og strekktid 2023-11-28 01:34:18 +01:00
4048cfe94f la til forklaring i csven 2023-11-28 01:33:53 +01:00
a39c5e4615 La til gen kommando 2023-11-28 01:33:25 +01:00
f1612cf838 Starta på søke tui 2023-11-24 00:42:02 +01:00
24f8c30c81 Fiksa gaflinger 2023-11-24 00:41:38 +01:00
95f10000bb Småforbedringer 2023-11-21 22:34:40 +01:00
056b97c0e2 Oppdaterte requirements 2023-11-21 22:33:31 +01:00
568c935a55 La til git og fargelegging 2023-11-21 22:32:14 +01:00
c280c5f6a0 La til __main__ entrypoint 2023-11-20 15:00:32 +01:00
7fab832321 mtr simulator 2023-11-19 17:54:43 +01:00
9e35cc55af Første veson av mtr leser 2023-11-19 17:54:27 +01:00
3894eb7284 Starta på innganspunktet for programmet, lagde en init funksjon 2023-11-17 21:52:44 +01:00
de4f288a4a Fiksa iof xml import 2023-11-17 21:52:19 +01:00
7591523b37 Lagde funskjoner for å lese inn og ut seperate csv, mtr og db filer 2023-11-17 21:49:33 +01:00
e5a672ac70 Starta på yaml io for separate mtr og config filer 2023-11-17 13:55:21 +01:00
3f4051426e Fiksa strekktidsgenerering 2023-11-08 17:31:11 +01:00
bf0b400708 Dytta xml koden inn i egen fil. 2023-11-07 15:57:44 +01:00
d008355498 Opprenskning 2023-11-07 14:24:45 +01:00
13 changed files with 1402 additions and 343 deletions

2
otime/__main__.py Normal file
View File

@@ -0,0 +1,2 @@
from cli import main
main()

141
otime/cli.py Normal file
View File

@@ -0,0 +1,141 @@
import argparse
import file_io
import iof_xml
import serial
import subprocess
import os
import otime
import iof_xml
import pdf
from pdf import format_m_s
from rich import print
import search_tui
from rich.traceback import install
install(show_locals=True)
# Main entrypoint for now. Cli with two options; init will make the files needed and run will start the program from the specified directory
def main():
parser = argparse.ArgumentParser(description='Otime very alpha version 😁')
subparsers = parser.add_subparsers(dest='command')
parser_init = subparsers.add_parser('init', help='Create project files')
parser_init.add_argument('--entries', required=True, dest='entries_file', action='store', default='./',
help='The xml file with entries')
parser_init.add_argument('--courses', required=True, dest='courses_file', action='store', default='./',
help='The xml with courses')
parser_init.add_argument('--dir', required=False, dest='dir', action='store', default='./',
help='Specify a directort, if not set the files are created in the current directory')
parser_init = subparsers.add_parser('run', help='run otime')
parser_init.add_argument('--dir', required=False, dest='dir', action='store', default='./', help='specify a directory')
parser_init.add_argument('--port', required=False, dest='port', action='store', help='specify a serial port')
parser_init.add_argument('--xml', required=False, dest='xml_path', action='store', default=None, help='Where the xml result file should be saved')
parser_init = subparsers.add_parser('gen', help='Generate result files')
parser_init.add_argument('--dir', required=False, dest='dir', action='store', default='./', help='specify a directory')
parser_init.add_argument('--xml', required=False, dest='xml_path', action='store', default=None, help='Where the xml result file should be saved')
parser_init = subparsers.add_parser('view', help='View otime data')
parser_init.add_argument('--dir', required=False, dest='dir', action='store', default='./', help='specify a directory')
parser_init.add_argument('--xml', required=False, dest='xml_path', action='store', default=None, help='Where the xml result file should be saved')
parser_init = subparsers.add_parser('mtr', help='run mtr commands')
parser_init.add_argument('--port', required=False, dest='port', action='store', help='specify a serial port')
args = parser.parse_args()
try:
if not args.xml_path:
args.xml_path = args.dir + '/output'
except AttributeError:
pass
match args.command:
case 'init':
init_dir(args.dir, args.entries_file, args.courses_file)
case 'run':
run(args.port, args.dir, args.xml_path)
case 'view':
search_tui.main(args.dir)
case 'gen':
gen(args.dir, args.xml_path)
case 'mtr':
mtr = serial.Serial(port=args.port, baudrate=9600, timeout=40)
mtr.write(b'/SA')
def init_dir(project_dir, entries_xml_file, courses_xml_file):
# Lager mappe med en config fil, en csv fil med løpere og en fil med mtr data
csv_db_path = project_dir + '/runners.csv'
config_path = project_dir + '/config.yaml'
mtr_path = project_dir + 'mtr.yaml'
event = iof_xml.event_from_xml_entries(entries_xml_file)
event.courses = iof_xml.courses_from_xml(courses_xml_file)
print(f'Read {len(event.runners)} runners, {len(event.card_dumps)} ecards, {len(event.courses)} courses and {len(event.o_classes)} classes')
print('Remember to manually link the courses and classes in the config file')
file_io.write_runners_csv(event, csv_db_path)
file_io.write_config(event, config_path)
file_io.write_card_dumps(event, mtr_path)
os.makedirs(project_dir+'/output')
subprocess.run(['git', 'init', project_dir])
subprocess.run(['git', 'add', './*'], cwd=project_dir)
subprocess.run(['git', 'commit', '-m', f'Project initiated by otime'], cwd=project_dir)
def run(port='/dev/ttyUSB0', project_dir='./', xml_path='./output/'):
mtr = serial.Serial(port=port, baudrate=9600, timeout=40)
config_path = project_dir + '/config.yaml'
mtr_path = project_dir + '/mtr.yaml'
csv_path = project_dir + '/runners.csv'
while True:
if mtr.in_waiting > 0:
mtr.read_until(expected=b'\xFF\xFF\xFF\xFF')
size = mtr.read(size=1)
if size == b'\xe6':
event = file_io.event_from_yaml_and_csv(config_path, mtr_path, csv_path)
meat = mtr.read(229)
full = b'\xFF\xFF\xFF\xFF' + size + meat
card_dump = otime.CardDump.from_mtr_bytes(full)
event.card_dumps.append(card_dump)
file_io.write_card_dumps(event, mtr_path)
print(runner_info(event, card_dump))
subprocess.run(['git', 'add', './*'], cwd=project_dir, stdout=subprocess.DEVNULL)
subprocess.run(['git', 'commit', '-m', f'Added {card_dump.card}'], cwd=project_dir, stdout=subprocess.DEVNULL)
iof_xml.create_result_file(event, xml_path + '/results.xml')
pdf.create_result_list(event, project_dir + '/output/results.pdf')
elif size == b'\x37':
meat = mtr.read(55)
inspect(status)
def gen(project_dir='./', xml_path='./output/'):
config_path = project_dir + '/config.yaml'
mtr_path = project_dir + '/mtr.yaml'
csv_path = project_dir + '/runners.csv'
event = file_io.event_from_yaml_and_csv(config_path, mtr_path, csv_path)
subprocess.run(['git', 'add', './*'], cwd=project_dir, stdout=subprocess.DEVNULL)
subprocess.run(['git', 'commit', '-m', f'Manually run'], cwd=project_dir, stdout=subprocess.DEVNULL)
iof_xml.create_result_file(event, xml_path + '/results.xml')
pdf.create_result_list(event, project_dir + '/output/results.pdf')
def runner_info(event, card_dump):
runner = next((i for i in event.runners if str(i.card_id) == str(card_dump.card)), None)
if runner is None:
return card_dump
result = event.get_runner_result(runner.id)
if result is None:
return '🧐 Dette skal ikke skje...'
if result.status == 'OK':
result_text = f'[green]Place: {result.place}. Time: [bold][yellow]{format_m_s(result.total_time)}[/yellow][/bold][/green]'
elif result.status == 'MissingPunch':
result_text = f'[red]Missed Control! Time: [bold][yellow]{format_m_s(result.total_time)}[/yellow][/bold][/red]'
else:
result_text = f'[blue]{result.status}, Time: [bold][yellow]{result.total_time}[/yellow][/bold][/blue]'
return result_text + f' [blue]{result.fullname()}[/blue], {result.o_class}, {result.club}, [italic]Card: {result.card_id}[/italic]'
if __name__ == '__main__':
main()

52
otime/file_io.py Normal file
View File

@@ -0,0 +1,52 @@
from yaml import load, dump
try:
from yaml import CLoader as Loader, CDumper as Dumper
except ImportError:
from yaml import Loader, Dumper
from copy import deepcopy
import otime
# Disse funksjonene er for å kunne lese og skrive seperate config, mtr og databasefiler.
def write_config(event, file_path):
output_event = deepcopy(event)
output_event.runners = []
output_event.card_dumps = []
with open(file_path, 'w') as f:
dump(output_event, f)
def write_card_dumps(event, file_path):
card_dumps = deepcopy(event.card_dumps)
with open(file_path, 'w') as f:
dump(card_dumps, f)
def write_runners_csv(event, file_path):
with open(file_path, 'w') as f:
f.write('ID;Status;Fornavn, Etternavn;Klasse;klubb;Brikke;Gafling;Starttid\n')
for i in event.runners:
f.write(f'{i.id};{i.status_override};{i.first}, {i.last};{i.o_class};{i.club};{i.card_id};{i.fork};{i.start_time}\n')
def event_from_yaml_and_csv(config_path, mtr_path, csv_path):
try:
with open(mtr_path, 'r') as f:
card_dumps = load(f, Loader=Loader)
except FileNotFoundError:
card_dumps=[]
with open(config_path, 'r') as f:
event = load(f, Loader=Loader)
with open(csv_path, 'r') as f:
data = [i.split(';') for i in f.readlines()]
data.pop(0)
for i in data: i[2] = i[2].split(',')
for i in data:
# Setter starttid til None hvis den ikke er satt
if len(i[7]) > 8:
i[7] = i[7]
else:
i[7] = None
runners = [otime.Runner(id=i[0], status_override=i[1], first=i[2][0], last=i[2][1].strip(), o_class=i[3], club=i[4], card_id=int(i[5]), fork=int(i[6]), start_time=i[7]) for i in data]
event.card_dumps = card_dumps
event.runners = runners
return event

212
otime/iof_xml.py Normal file
View File

@@ -0,0 +1,212 @@
import datetime
import xml.etree.ElementTree as ET
import otime
def xml_child(parent, tag, content):
# Used to make creating xml files easier
e = ET.SubElement(parent, tag)
e.text = str(content)
def create_result_file(event, file_path, o_classes=[]):
results = event.get_result(o_classes)
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)
xml_event = ET.SubElement(root, 'Event')
xml_child(xml_event, 'Id', event.id)
xml_child(xml_event, 'Name', event.name)
for i in results:
# <ClassResult>
class_result = ET.SubElement(root, 'ClassResult')
# <Class>
t = ET.SubElement(class_result, 'Class')
xml_child(t, 'Name', i.name)
# <PersonResult>
for n in i.runner_results:
if n.status == 'DidNotStart':
continue
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 n.status == 'OK' or n.status == 'MissingPunch':
xml_child(result, 'StartTime', n.start_time.isoformat())
xml_child(result, 'FinishTime', n.end_time.isoformat())
xml_child(result, 'Time', n.total_time)
if n.status == 'OK':
# <TimeBehind>
xml_child(result, 'TimeBehind', n.total_time - i.runner_results[0].total_time)
# </TimeBehind>
xml_child(result, 'Position', n.place)
xml_child(result, 'Status', n.status)
# <SplitTime>
# TODO: ta utgangspunkt i løypa, ikke det brikka har stempla
for code, split in zip(i.course.codes[n.fork][:-1], n.splits[:-1]):
st = ET.SubElement(result, 'SplitTime')
xml_child(st, 'ControlCode', code)
xml_child(st, 'Time', split)
# </SplitTime>
elif n.status == 'MissingPunch':
xml_child(result, 'Status', n.status)
for code, split in zip(i.course.codes[n.fork][:-1], n.splits[:-1]):
st = ET.SubElement(result, 'SplitTime')
xml_child(st, 'ControlCode', code)
if split != 0: 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)
tree.write(file_path)
def runners_from_xml_entries(xml_file):
tree = ET.parse(xml_file)
root = tree.getroot()
url = '{http://www.orienteering.org/datastandard/3.0}'
runners = []
person_entries = root.findall(f'./{url}PersonEntry')
for p_entry in person_entries:
rid = p_entry[1][0].text
person = p_entry.find(f'./{url}Person')
name = person.find(f'./{url}Name')
first = name.find(f'./{url}Given').text
last = name.find(f'./{url}Family').text
organisation = p_entry.find(f'./{url}Organisation')
if organisation is not None:
club_id = organisation.find(f'./{url}Id').text
club_name = organisation.find(f'./{url}Name').text
club_name_short = organisation.find(f'./{url}ShortName').text
country = organisation.find(f'./{url}Country').attrib['code']
else:
club_id = club_name = club_name_short = country = None
class_el = p_entry.find(f'./{url}Class')
class_str = class_el.find(f'./{url}Name').text
fee_id = int(p_entry.find(f'./{url}AssignedFee/{url}Fee/{url}Id').text)
try:
card = int(p_entry.find(f'./{url}ControlCard').text)
except AttributeError:
card = None
start_time = None
runners.append(otime.Runner(rid, first, last, club=club_name, club_id=club_id,
country=country,card_id=card, o_class=class_str,
start_time=start_time, fee_id=fee_id))
return runners
def fees_from_xml_entries(xml_file):
tree = ET.parse(xml_file)
root = tree.getroot()
url = '{http://www.orienteering.org/datastandard/3.0}'
allfees = root.findall(f'.//{url}Fee')
added_ids = []
fee_objs = []
for fee in allfees:
f_id = int(fee.find(f'./{url}Id').text)
if f_id not in added_ids:
added_ids.append(f_id)
fee_id = f_id
name = fee.find(f'./{url}Name').text
currency = fee.find(f'./{url}Amount').attrib['currency']
amount = int(fee.find(f'./{url}Amount').text)
try:
from_birth_date = fee.find(f'./{url}FromDateOfBirth').text
except AttributeError:
from_birth_date = None
try:
to_birth_date = fee.find(f'./{url}ToDateOfBirth').text
except AttributeError:
to_birth_date = None
fee_objs.append(otime.Fee(fee_id, name, currency, amount,
from_birth_date=from_birth_date, to_birth_date=to_birth_date))
return fee_objs
def courses_from_xml(xml_file):
tree = ET.parse(xml_file)
root = tree.getroot()
url = '{http://www.orienteering.org/datastandard/3.0}'
allcourses = root.findall(f'.//{url}Course')
courseobjs = []
for c in allcourses:
name = c.find(f'./{url}Name').text
controls = []
allcontrols = c.findall(f'./{url}CourseControl')
for n in allcontrols:
controls.append(n.find(f'./{url}Control').text)
controls.remove('STA1')
controls.remove('FIN1')
controls = [int(l) for l in controls]
courseobjs.append(otime.Course(name, [controls]))
return courseobjs
def event_from_xml_entries(xml_file):
tree = ET.parse(xml_file)
root = tree.getroot()
url = '{http://www.orienteering.org/datastandard/3.0}'
event_el = root.find(f'./{url}Event')
event_id = int(event_el.find(f'./{url}Id').text)
name = event_el.find(f'./{url}Name').text
organiser = event_el.find(f'./{url}Organiser/{url}Name').text
start_ds = event_el.find(f'./{url}StartTime/{url}Date').text
start_ts = event_el.find(f'./{url}StartTime/{url}Time').text[:-1]
start_time = datetime.datetime.fromisoformat(f'{start_ds}T{start_ts}')
end_ds = event_el.find(f'./{url}EndTime/{url}Date').text
end_ts = event_el.find(f'./{url}EndTime/{url}Time').text[:-1]
end_time = datetime.datetime.fromisoformat(f'{end_ds}T{end_ts}')
person_entries = root.findall(f'./{url}PersonEntry')
class_names = []
for p_entry in person_entries:
class_names.append(p_entry.find(f'./{url}Class/{url}Name').text)
o_classes = [otime.OClass(i, []) for i in set(class_names)]
runners = runners_from_xml_entries(xml_file)
# TODO: fiks fees
#fees = fees_from_xml_entries(xml_file)
fees = []
return otime.Event(event_id, name, organiser=organiser, runners=runners,
fees=fees, start_time=start_time, end_time=end_time, o_classes=o_classes)

View File

@@ -2,12 +2,11 @@ import copy
import datetime import datetime
import re import re
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
import pdf
class Runner: class Runner:
def __init__(self, id: int, first: str, last: str, club=None, club_id=None, 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, country=None, card_id=None, o_class_str=None, o_class=None,
fork=0, start_time=None, fee_id=None, fee=None): fork=0, start_time=None, fee_id=None, fee=None, status_override=''):
self.id = id self.id = id
self.first = first self.first = first
self.last = last self.last = last
@@ -19,7 +18,8 @@ class Runner:
self.fork = fork self.fork = fork
self.start_time = start_time self.start_time = start_time
self.fee_id = fee_id self.fee_id = fee_id
self.status_override = '' self.status_override = status_override
def __repr__(self): def __repr__(self):
return(f'name({self.fullname()})') return(f'name({self.fullname()})')
@@ -62,71 +62,6 @@ class Runner:
def fullname(self): def fullname(self):
return '{} {}'.format(self.first, self.last) return '{} {}'.format(self.first, self.last)
# TODO: må forbedres
def rank(self, allrunners):
c_ranked = rank_runners(allrunners, self.o_class)
try:
return c_ranked.index(self) + 1
except ValueError:
return None
def get_codes(self):
if self.o_class.course.forked is False:
return self.o_class.course.codes
else:
return self.o_class.course.variations[self.fork]
# TODO: Mange bugs med løyper som har samme post flere ganger
# Used for making result files and tables
def get_splits(self):
if self.status() == 'OK':
splits_cpy = self.card_r.splits.copy()
for control in self.card_r.controls:
if control not in self.res_codes():
index = self.card_r.controls.index(control)
split = self.card_r.splits[index]
splits_cpy.remove(split)
return splits_cpy
else:
splits_cpy = self.card_r.splits.copy()
for control in self.card_r.controls:
if control not in self.res_codes():
index = self.card_r.controls.index(control)
split = self.card_r.splits[index]
try:
splits_cpy.remove(split)
except Exception:
print('aaaaaa')
punches = self.card_r.controls.copy()
splits = []
for code in self.res_codes():
if punches[0] == code:
splits.append(splits_cpy[0])
splits_cpy.pop(0)
punches.pop(0)
continue
else:
splits.append(None)
return splits
def asdict(self):
return {
'id': self.id,
'first': self.first,
'last': self.last,
'club_id': self.club_id,
'club': self.club,
'country': self.country,
'card': self.card,
'o_class_str': self.o_class_str,
'fork' : self.fork,
'start_time': self.start_time,
'fee_id': self.fee_id
}
class CardDump: class CardDump:
def __init__(self, card, controls, splits, read_time, s_time, f_time): def __init__(self, card, controls, splits, read_time, s_time, f_time):
self.card = card self.card = card
@@ -159,7 +94,7 @@ class CardDump:
splits.append(time) splits.append(time)
# Extract start time: # Extract start time:
year = int.from_bytes(datamsg[8:9], 'little') year = 2000 + int.from_bytes(datamsg[8:9], 'little')
month = int.from_bytes(datamsg[9:10], 'little') month = int.from_bytes(datamsg[9:10], 'little')
day = int.from_bytes(datamsg[10:11], 'little') day = int.from_bytes(datamsg[10:11], 'little')
hours = int.from_bytes(datamsg[11:12], 'little') hours = int.from_bytes(datamsg[11:12], 'little')
@@ -216,39 +151,25 @@ class CardDump:
else: else:
s_time = read_time s_time = read_time
f_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)) cards.append(CardDump(int(row[6]), controls, splits, read_time, s_time, f_time))
return cards 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 # Stored in Event.courses
class Course: class Course:
def __init__(self, name, codes, forked=False, variations=None): def __init__(self, name, codes, forked=False):
self.name = name self.name = name
# Codes is a list of courses
self.codes = codes self.codes = codes
self.forked = forked self.forked = forked
# Variations is a list
self.variations = variations
def __repr__(self): def __repr__(self):
return f'name({self.name})' return f'name({self.name})'
def asdict(self):
return {
'name': self.name,
'codes': self.codes,
'forked': self.forked,
'variations': self.variations
}
# Stored in Event.o_classes # Stored in Event.o_classes
class OClass: class OClass:
def __init__(self, name, course): def __init__(self, name, course):
@@ -260,11 +181,12 @@ class OClass:
class RunnerResult: class RunnerResult:
def __init__(self, runner_id: int, first: str, last: str, status: str, place: int, total_time: int, splits: list[int], end_time, 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=None, fee_id=None): 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):
self.id = runner_id self.id = runner_id
self.first = first self.first = first
self.last = last self.last = last
self.club = club self.club = club
self.club_id = club_id
self.country = country self.country = country
self.card_id = card_id self.card_id = card_id
self.o_class = o_class self.o_class = o_class
@@ -289,7 +211,7 @@ class ClassResult:
self.runner_results = runner_results self.runner_results = runner_results
class Event: class Event:
def __init__(self, eventid, name, start_time=None, end_time=None, def __init__(self, eventid=0, name=None, start_time=None, end_time=None,
organiser=None, courses=[], o_classes=[], runners=[], organiser=None, courses=[], o_classes=[], runners=[],
card_dumps=[], fees=[]): card_dumps=[], fees=[]):
self.id = eventid self.id = eventid
@@ -342,33 +264,54 @@ class Event:
runner = self.get_runner(id) runner = self.get_runner(id)
if runner.status_override: return runner.status_override if runner.status_override: return runner.status_override
o_class = self.get_o_class(runner.o_class) o_class = self.get_o_class(runner.o_class)
if not o_class:
return 'Inactive'
course = self.get_course(o_class.course) course = self.get_course(o_class.course)
if self.get_card_dump(runner.card_id) == None: if not self.get_card_dump(runner.card_id):
return 'Active' return 'Active'
if contains(course.codes, self.get_card_dump(runner.card_id).controls): if contains(course.codes[runner.fork], self.get_card_dump(runner.card_id).controls):
return 'OK' return 'OK'
else: else:
return 'MissingPunch' 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): def get_runner_time(self, id):
runner = self.get_runner(id) runner = self.get_runner(id)
card_dump = self.get_card_dump(runner.card_id) card_dump = self.get_card_dump(runner.card_id)
if card_dump == None: course = self.get_course(self.get_runner_o_class(id).course)
if card_dump == None or course == None:
return False return False
f_control = card_dump.controls[-1] f_control = course.codes[runner.fork][-1]
# TODO: Må gjøres mer robust # TODO: Må gjøres mer robust
try: 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) 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] return card_dump.splits[index]
except: #Hvis det er en startid finner jeg tidsforskjellen mellom brikkestart og faktisk start og trekker den fra totaltida
return False 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): def get_runner_splits(self, id):
# Tida brukt frem til hver post, ikke tida fra forrige post
try: try:
runner = self.get_runner(id) runner = self.get_runner(id)
card_dump = self.get_card_dump(runner.card_id) card_dump = self.get_card_dump(runner.card_id)
course = self.get_course(self.get_o_class(runner.o_class).course) course = self.get_course(self.get_o_class(runner.o_class).course)
codes = course.codes[runner.fork]
except AttributeError: except AttributeError:
return None return None
@@ -376,21 +319,18 @@ class Event:
return None return None
split_iter = zip(card_dump.controls, card_dump.splits).__iter__() split_iter = zip(card_dump.controls, card_dump.splits).__iter__()
splits = [0] * len(course.codes) splits = [0] * len(codes)
for n, control in enumerate(course.codes): for n, control in enumerate(codes):
if control not in card_dump.controls: if control not in card_dump.controls:
continue continue
split_debt = 0
while True: while True:
try: try:
punched_control, split = next(split_iter) punched_control, split = next(split_iter)
except StopIteration: except StopIteration:
break break
if punched_control == control: if punched_control == control:
splits[n] = split + split_debt splits[n] = split
break break
else:
split_debt += split
return splits return splits
def get_runner_controls(self, id): def get_runner_controls(self, id):
@@ -408,36 +348,13 @@ class Event:
def get_result(self, o_classes: list[str] = []) -> list[ClassResult]: def get_result(self, o_classes: list[str] = []) -> list[ClassResult]:
if not o_classes: if not o_classes:
o_classes = [i.name for i in self.o_classes] o_classes = [i.name for i in self.o_classes]
return [produce_class_result(copy.deepcopy(self), i) for i in o_classes] return [produce_class_result(self, i) for i in o_classes]
def read_xml_entries(self, xml_file): def read_xml_entries(self, xml_file):
self.add_runners(*runners_from_xml_entries(xml_file)) self.add_runners(*runners_from_xml_entries(xml_file))
self.add_fees(*fees_from_xml_entries(xml_file)) self.add_fees(*fees_from_xml_entries(xml_file))
# Må endres # Må endres
def from_xml_entries(xml_file):
tree = ET.parse(xml_file)
root = tree.getroot()
url = '{http://www.orienteering.org/datastandard/3.0}'
event_el = root.find(f'./{url}Event')
event_id = int(event_el.find(f'./{url}Id').text)
name = event_el.find(f'./{url}Name').text
organiser = event_el.find(f'./{url}Organiser/{url}Name').text
start_ds = event_el.find(f'./{url}StartTime/{url}Date').text
start_ts = event_el.find(f'./{url}StartTime/{url}Time').text[:-1]
start_time = datetime.datetime.fromisoformat(f'{start_ds}T{start_ts}')
end_ds = event_el.find(f'./{url}EndTime/{url}Date').text
end_ts = event_el.find(f'./{url}EndTime/{url}Time').text[:-1]
end_time = datetime.datetime.fromisoformat(f'{end_ds}T{end_ts}')
runners = runners_from_xml_entries(xml_file)
fees = fees_from_xml_entries(xml_file)
return Event(event_id, name, organiser=organiser, runners=runners,
fees=fees, start_time=start_time, end_time=end_time)
def read_xml_courses(self, xml_file): def read_xml_courses(self, xml_file):
self.courses = courses_from_xml(xml_file) self.courses = courses_from_xml(xml_file)
@@ -460,92 +377,6 @@ class Event:
def create_result_pdf(self, file_path): def create_result_pdf(self, file_path):
pdf.create_result_list(copy.deepcopy(self), file_path) pdf.create_result_list(copy.deepcopy(self), file_path)
def create_result_xml(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:
print('Hmmmmmm')
# <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')
# TODO: Dette bør skrives om til å bruke Runner metoder så mye som mulig.
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.res_codes()[-1]:
break
# </SplitTime>
elif n.status() == 'Disqualified':
xml_child(result, 'Status', n.status())
for code, split in zip(n.res_codes(), n.res_splits()):
st = ET.SubElement(result, 'SplitTime')
xml_child(st, 'ControlCode', code)
if split is not None:
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
class Fee: class Fee:
def __init__(self, fee_id, name, currency, amount, from_birth_date=None, def __init__(self, fee_id, name, currency, amount, from_birth_date=None,
to_birth_date=None): to_birth_date=None):
@@ -556,16 +387,6 @@ class Fee:
self.from_birth_date = from_birth_date self.from_birth_date = from_birth_date
self.to_birth_date = to_birth_date self.to_birth_date = to_birth_date
def asdict(self):
return {
'id': self.id,
'name': self.name,
'currency': self.currency,
'amount': self.amount,
'from_birth_date': self.from_birth_date,
'to_birth_date': self.to_birth_date
}
def produce_class_result(event, o_class_name) -> ClassResult: def produce_class_result(event, o_class_name) -> ClassResult:
o_class = event.get_o_class(o_class_name) o_class = event.get_o_class(o_class_name)
runners = event.get_runners_in_o_class(o_class_name) runners = event.get_runners_in_o_class(o_class_name)
@@ -575,13 +396,13 @@ def produce_class_result(event, o_class_name) -> ClassResult:
other_runners = [i for i in runners if i not in ok_runners and i not in dsq_runners] 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), 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, i.country, i.card_id, i.o_class, event.get_runner_controls(i.id)) for i in ok_runners] 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), 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, i.country, i.card_id, i.o_class, event.get_runner_controls(i.id)) for i in dsq_runners] 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 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), 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, i.country, i.card_id, i.o_class, event.get_runner_controls(i.id)) for i in other_runners] 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, o_class.course, results) return ClassResult(o_class.name, event.get_course(o_class.course), results)
# TODO: Take string instead of file. # TODO: Take string instead of file.
def courses_from_ttime_conf(ttime_file): def courses_from_ttime_conf(ttime_file):
@@ -596,7 +417,7 @@ def courses_from_ttime_conf(ttime_file):
n = n.split(',') n = n.split(',')
loops += 1 loops += 1
n = list(map(int, n)) n = list(map(int, n))
courses.append(Course('course_'+str(loops), n)) courses.append(Course('course_'+str(loops), [n]))
return courses return courses
def classes_from_ttime_conf(ttime_file, courses): def classes_from_ttime_conf(ttime_file, courses):
@@ -614,96 +435,6 @@ def classes_from_ttime_conf(ttime_file, courses):
loops += 1 loops += 1
return o_classes return o_classes
def runners_from_xml_entries(xml_file):
tree = ET.parse(xml_file)
root = tree.getroot()
url = '{http://www.orienteering.org/datastandard/3.0}'
runners = []
person_entries = root.findall(f'./{url}PersonEntry')
for p_entry in person_entries:
rid = p_entry[1][0].text
person = p_entry.find(f'./{url}Person')
name = person.find(f'./{url}Name')
first = name.find(f'./{url}Given').text
last = name.find(f'./{url}Family').text
organisation = p_entry.find(f'./{url}Organisation')
if organisation is not None:
club_id = organisation.find(f'./{url}Id').text
club_name = organisation.find(f'./{url}Name').text
club_name_short = organisation.find(f'./{url}ShortName').text
country = organisation.find(f'./{url}Country').attrib['code']
else:
club_id = club_name = club_name_short = country = None
class_el = p_entry.find(f'./{url}Class')
class_str = class_el.find(f'./{url}Name').text
fee_id = int(p_entry.find(f'./{url}AssignedFee/{url}Fee/{url}Id').text)
try:
card = int(p_entry.find(f'./{url}ControlCard').text)
except AttributeError:
card = None
start_time = None
runners.append(Runner(rid, first, last, club=club_name, club_id=club_id,
country=country,card=card, o_class_str=class_str,
start_time=start_time, fee_id=fee_id))
return runners
def fees_from_xml_entries(xml_file):
tree = ET.parse(xml_file)
root = tree.getroot()
url = '{http://www.orienteering.org/datastandard/3.0}'
allfees = root.findall(f'.//{url}Fee')
added_ids = []
fee_objs = []
for fee in allfees:
f_id = int(fee.find(f'./{url}Id').text)
if f_id not in added_ids:
added_ids.append(f_id)
fee_id = f_id
name = fee.find(f'./{url}Name').text
currency = fee.find(f'./{url}Amount').attrib['currency']
amount = int(fee.find(f'./{url}Amount').text)
try:
from_birth_date = fee.find(f'./{url}FromDateOfBirth').text
except AttributeError:
from_birth_date = None
try:
to_birth_date = fee.find(f'./{url}ToDateOfBirth').text
except AttributeError:
to_birth_date = None
fee_objs.append(Fee(fee_id, name, currency, amount,
from_birth_date=from_birth_date, to_birth_date=to_birth_date))
return fee_objs
def courses_from_xml(xml_file):
tree = ET.parse(xml_file)
root = tree.getroot()
url = '{http://www.orienteering.org/datastandard/3.0}'
allcourses = root.findall(f'.//{url}Course')
courseobjs = []
for c in allcourses:
name = c.find(f'./{url}Name').text
controls = []
allcontrols = c.findall(f'./{url}CourseControl')
for n in allcontrols:
controls.append(n.find(f'./{url}Control').text)
controls.remove('STA1')
controls.remove('FIN1')
controls = [int(l) for l in controls]
courseobjs.append(Course(name, controls))
return courseobjs
# Checks if small list is in big list # Checks if small list is in big list
def contains(small, big): def contains(small, big):
valid = True valid = True
@@ -721,18 +452,3 @@ def contains(small, big):
return map_bl return map_bl
else: else:
return False return False
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)

View File

@@ -11,7 +11,7 @@ def create_result_list(event, file_path, o_classes=[]):
pdf = FPDF() pdf = FPDF()
pdf.add_page() pdf.add_page()
pdf.add_font("LiberationSans", fname="../../otime/data/fonts/LiberationSans-Regular.ttf") pdf.add_font("LiberationSans", fname="otime/data/fonts/LiberationSans-Regular.ttf")
pdf.set_font("LiberationSans", size=10) pdf.set_font("LiberationSans", size=10)
line_height = pdf.font_size * 1.5 line_height = pdf.font_size * 1.5
col_width = pdf.epw / 4 # distribute content evenly col_width = pdf.epw / 4 # distribute content evenly
@@ -38,13 +38,13 @@ def create_split_result_list(event, file_path, o_classes=[]):
pdf = FPDF() pdf = FPDF()
pdf.add_page(orientation='L') pdf.add_page(orientation='L')
pdf.add_font("LiberationSans", fname="../../otime/data/fonts/LiberationSans-Regular.ttf") pdf.add_font("LiberationSans", fname="otime/data/fonts/LiberationSans-Regular.ttf")
pdf.set_font("LiberationSans", size=8) pdf.set_font("LiberationSans", size=8)
line_height = pdf.font_size * 1.5 line_height = pdf.font_size * 1.5
col_width = pdf.epw / 4 # distribute content evenly col_width = pdf.epw / 4 # distribute content evenly
for class_result in results: for class_result in results:
col_width = pdf.epw / (21+len(class_result.course)) col_width = 10
pdf.write(txt=class_result.name) pdf.write(txt=class_result.name)
pdf.ln(line_height) pdf.ln(line_height)
for runner in class_result.runner_results: for runner in class_result.runner_results:

30
otime/search_tui.py Normal file
View File

@@ -0,0 +1,30 @@
from textual.app import App, ComposeResult
from textual.widgets import DataTable
import otime
import file_io
from pdf import format_m_s
global ROWS
class TableApp(App):
def compose(self) -> ComposeResult:
yield DataTable()
def on_mount(self) -> None:
global ROWS
table = self.query_one(DataTable)
table.add_columns(*ROWS[0])
table.add_rows(ROWS[1:])
def main(path):
event = file_io.event_from_yaml_and_csv(path + '/config.yaml', path + '/mtr.yaml', path + '/runners.csv')
result = event.get_result()
global ROWS
ROWS = []
for o_class in result:
ROWS += [(str(i.id), str(i.place), i.fullname(), i.o_class, i.club, str(i.card_id), format_m_s(i.total_time), str(i.controls)) for i in o_class.runner_results]
print(ROWS)
app = TableApp()
app.run()
if __name__ == "__main__":
main()

260
otime/ui.py Normal file
View File

@@ -0,0 +1,260 @@
# Copyright (C) 2021 Tim Lauridsen < tla[at]rasmil.dk >
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to
# the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
"""
Sample Python Gtk4 Application
"""
import sys
import time
from typing import List
import copy
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Gtk, GObject, Gio, Adw
from widgets import ColumnViewListStore
import otime
import file_io
class ColumnElem(GObject.GObject):
"""custom data element for a ColumnView model (Must be based on GObject)"""
def __init__(self, name: str):
super(ColumnElem, self).__init__()
self.name = name
def __repr__(self):
return f"ColumnElem(name: {self.name})"
"""
class OtimeRunnerView(Gtk.ListView):
__gtype_name__ = "OtimeRunnerRowView"
__gsignals__ = {"refresh": (GObject.SignalFlags.RUN_FIRST, None, ())}
selection = Gtk.Template.Child()
class YumexQueueRow(Gtk.Box):
__gtype_name__ = "YumexQueueRow"
icon = Gtk.Template.Child()
text = Gtk.Template.Child()
dep = Gtk.Template.Child()
def __init__(self, view, **kwargs):
super().__init__(**kwargs)
self.view: OtimeRunnerView = view
self.pkg: YumexPackage = None
"""
class MyColumnViewColumn(ColumnViewListStore):
"""Custom ColumnViewColumn"""
def __init__(
self, win: Gtk.ApplicationWindow, col_view: Gtk.ColumnView, data: List
):
# Init ListView with store model class.
super(MyColumnViewColumn, self).__init__(ColumnElem, col_view)
self.win = win
# put some data into the model
for elem in data:
self.add(ColumnElem(elem))
def factory_setup(self, widget, item: Gtk.ListItem):
"""Gtk.SignalListItemFactory::setup signal callback
Handles the creation widgets to put in the ColumnViewColumn
"""
label = Gtk.Text()
label.set_halign(Gtk.Align.START)
label.set_hexpand(True)
label.set_margin_start(10)
item.set_child(label)
def factory_bind(self, widget, item: Gtk.ListItem):
"""Gtk.SignalListItemFactory::bind signal callback
Handles adding data for the model to the widgets created in setup
"""
label = item.get_child() # Get the Gtk.Label stored in the ListItem
data = item.get_item() # get the model item, connected to current ListItem
label.set_text(data.name) # Update Gtk.Label with data from model item
class MyWindow(Adw.ApplicationWindow):
def __init__(self, title, width, height, **kwargs):
super(MyWindow, self).__init__(**kwargs)
self.set_default_size(width, height)
box = Gtk.Box()
box.props.orientation = Gtk.Orientation.VERTICAL
header = Gtk.HeaderBar()
stack = Adw.ViewStack()
switcher = Adw.ViewSwitcherTitle()
switcher.set_stack(stack)
header.set_title_widget(switcher)
box.append(header)
content = self.setup_content()
page1 = stack.add_titled(content, "Løpere", "Løpere")
box_p2 = Gtk.Box()
page2 = stack.add_titled(box_p2, "page2", "Page 2")
box.append(stack)
self.set_content(box)
def setup_content(self):
"""Add a page with a text selector to the stack"""
# ColumnView with custom columns
self.columnview = Gtk.ColumnView()
self.columnview.set_show_column_separators(True)
sw = Gtk.ScrolledWindow()
self.columnview = Gtk.ColumnView()
factory_c1 = Gtk.SignalListItemFactory()
factory_c1.connect("setup", setup_c)
factory_c1.connect("bind", bind_c1)
factory_c2 = Gtk.SignalListItemFactory()
factory_c2.connect("setup", setup_c)
factory_c2.connect("bind", bind_c2)
factory_c3 = Gtk.SignalListItemFactory()
factory_c3.connect("setup", setup_c)
factory_c3.connect("bind", bind_c3)
factory_c4 = Gtk.SignalListItemFactory()
factory_c4.connect("setup", setup_c)
factory_c4.connect("bind", bind_c4)
factory_c5 = Gtk.SignalListItemFactory()
factory_c5.connect("setup", setup_c)
factory_c5.connect("bind", bind_c5)
selection = Gtk.SingleSelection()
store = Gio.ListStore.new(DataObject)
selection.set_model(store)
self.columnview.set_model(selection)
columns = []
columns.append(Gtk.ColumnViewColumn.new("Fornavn", factory_c1))
columns.append(Gtk.ColumnViewColumn.new("Etternavn ", factory_c2))
columns.append(Gtk.ColumnViewColumn.new("Klubb", factory_c3))
columns.append(Gtk.ColumnViewColumn.new("klasse", factory_c4))
columns.append(Gtk.ColumnViewColumn.new("eCard", factory_c5))
for i in columns:
self.columnview.append_column(i)
project_dir = '/home/trygve/Dokumenter/ÅbN&FC_2'
config_path = project_dir + '/config.yaml'
mtr_path = project_dir + '/mtr.yaml'
csv_path = project_dir + '/runners.csv'
event = file_io.event_from_yaml_and_csv(config_path, mtr_path, csv_path)
data = [DataObject(i.first, i.last, i.club, i.o_class, i.card_id) for i in event.runners]
for i in data:
store.append(i)
lw_frame = Gtk.Frame()
lw_frame.set_valign(Gtk.Align.FILL)
lw_frame.set_vexpand(True)
lw_frame.set_margin_start(20)
lw_frame.set_margin_end(20)
lw_frame.set_margin_top(10)
lw_frame.set_margin_bottom(10)
sw.set_child(self.columnview)
lw_frame.set_child(sw)
return lw_frame
class DataObject(GObject.GObject):
__gtype_name__ = 'DataObject'
text = GObject.Property(type=str, default=None)
def __init__(self, f_name, l_name, club, o_class, card):
super().__init__()
self.f_name = f_name
self.l_name = l_name
self.club = club
self.o_class = o_class
self.card = str(card)
def setup_c(widget, item):
"""Setup the widget to show in the Gtk.Listview"""
text = Gtk.Text()
item.set_child(text)
def bind_c1(widget, item):
"""bind data from the store object to the widget"""
label = item.get_child()
obj = item.get_item()
label.set_text(obj.f_name)
label.bind_property("f_name", obj, "f_name")
def bind_c2(widget, item):
"""bind data from the store object to the widget"""
text = item.get_child()
obj = item.get_item()
text.set_text(obj.l_name)
def bind_c3(widget, item):
"""bind data from the store object to the widget"""
text = item.get_child()
obj = item.get_item()
text.set_text(obj.club)
def bind_c4(widget, item):
"""bind data from the store object to the widget"""
text = item.get_child()
obj = item.get_item()
text.set_text(obj.o_class)
def bind_c5(widget, item):
"""bind data from the store object to the widget"""
text = item.get_child()
obj = item.get_item()
text.set_text(obj.card)
class Application(Adw.Application):
"""Main Aplication class"""
def __init__(self):
super().__init__(
application_id="net.trygve.otime.alpha", flags=Gio.ApplicationFlags.FLAGS_NONE
)
def do_activate(self):
win = self.props.active_window
if not win:
win = MyWindow("Otime veldig alpha", 800, 800, application=self)
win.present()
def main():
"""Run the main application"""
app = Application()
return app.run(sys.argv)
if __name__ == "__main__":
main()

547
otime/widgets.py Normal file
View File

@@ -0,0 +1,547 @@
# Copyright (C) 2021 Tim Lauridsen < tla[at]rasmil.dk >
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to
# the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
"""
Misc classes to build Gtk4 Applications in python 3.9
It takes some of the hassle away from building Gtk4 application in Python
So you can create an cool application, without all the boilerplate code
"""
import os.path
from abc import abstractmethod
import gi
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk, Gio, GLib, Gdk
from material import MATERIAL
def rgb_to_hex(r, g, b):
if isinstance(r, float):
r *= 255
g *= 255
b *= 255
return "#{0:02X}{1:02X}{2:02X}".format(int(r), int(g), int(b))
def color_to_hex(color):
return rgb_to_hex(color.red, color.green, color.blue)
def get_font_markup(fontdesc, text):
return f'<span font_desc="{fontdesc}">{text}</span>'
class MaterialColorDialog(Gtk.ColorChooserDialog):
""" Color chooser dialog with Material design colors """
def __init__(self, title, parent):
Gtk.ColorChooserDialog.__init__(self)
self.set_title(title)
self.set_transient_for(parent)
self.set_modal(True)
# build list of material colors in Gdk.RGBA format
color_values = []
for color_name in MATERIAL.colors:
colors = MATERIAL.get_palette(color_name)
for col in colors:
color = Gdk.RGBA()
color.parse(col)
color_values.append(color)
num_colors = 14
self.add_palette(Gtk.Orientation.HORIZONTAL, num_colors, color_values)
self.set_property('show-editor', False)
def get_color(self):
selected_color = self.get_rgba()
return color_to_hex(selected_color)
class ListViewBase(Gtk.ListView):
""" ListView base class, it setup the basic factory, selection model & data model
handlers must be overloaded & implemented in a sub class
"""
def __init__(self, model_cls):
Gtk.ListView.__init__(self)
# Use the signal Factory, so we can connect our own methods to setup
self.factory = Gtk.SignalListItemFactory()
# connect to Gtk.SignalListItemFactory signals
# check https://docs.gtk.org/gtk4/class.SignalListItemFactory.html for details
self.factory.connect('setup', self.on_factory_setup)
self.factory.connect('bind', self.on_factory_bind)
self.factory.connect('unbind', self.on_factory_unbind)
self.factory.connect('teardown', self.on_factory_teardown)
# Create data model, use our own class as elements
self.set_factory(self.factory)
self.store = self.setup_store(model_cls)
# create a selection model containing our data model
self.model = self.setup_model(self.store)
self.model.connect('selection-changed', self.on_selection_changed)
# set the selection model to the view
self.set_model(self.model)
def setup_model(self, store: Gio.ListModel) -> Gtk.SelectionModel:
""" Setup the selection model to use in Gtk.ListView
Can be overloaded in subclass to use another Gtk.SelectModel model
"""
return Gtk.SingleSelection.new(store)
@abstractmethod
def setup_store(self, model_cls) -> Gio.ListModel:
""" Setup the data model
must be overloaded in subclass to use another Gio.ListModel
"""
raise NotImplemented
def add(self, elem):
""" add element to the data model """
self.store.append(elem)
# Gtk.SignalListItemFactory signal callbacks
# transfer to some some callback stubs, there can be overloaded in
# a subclass.
def on_factory_setup(self, widget, item: Gtk.ListItem):
""" GtkSignalListItemFactory::setup signal callback
Setup the widgets to go into the ListView """
self.factory_setup(widget, item)
def on_factory_bind(self, widget: Gtk.ListView, item: Gtk.ListItem):
""" GtkSignalListItemFactory::bind signal callback
apply data from model to widgets set in setup"""
self.factory_bind(widget, item)
def on_factory_unbind(self, widget, item: Gtk.ListItem):
""" GtkSignalListItemFactory::unbind signal callback
Undo the the binding done in ::bind if needed
"""
self.factory_unbind(widget, item)
def on_factory_teardown(self, widget, item: Gtk.ListItem):
""" GtkSignalListItemFactory::setup signal callback
Undo the creation done in ::setup if needed
"""
self.factory_teardown(widget, item)
def on_selection_changed(self, widget, position, n_items):
# get the current selection (GtkBitset)
selection = widget.get_selection()
# the the first value in the GtkBitset, that contain the index of the selection in the data model
# as we use Gtk.SingleSelection, there can only be one ;-)
ndx = selection.get_nth(0)
self.selection_changed(widget, ndx)
# --------------------> abstract callback methods <--------------------------------
# Implement these methods in your subclass
@abstractmethod
def factory_setup(self, widget: Gtk.ListView, item: Gtk.ListItem):
""" Setup the widgets to go into the ListView (Overload in subclass) """
pass
@abstractmethod
def factory_bind(self, widget: Gtk.ListView, item: Gtk.ListItem):
""" apply data from model to widgets set in setup (Overload in subclass)"""
pass
@abstractmethod
def factory_unbind(self, widget: Gtk.ListView, item: Gtk.ListItem):
pass
@abstractmethod
def factory_teardown(self, widget: Gtk.ListView, item: Gtk.ListItem):
pass
@abstractmethod
def selection_changed(self, widget, ndx):
""" trigged when selecting in listview is changed
ndx: is the index in the data store model that is selected
"""
pass
class ListViewListStore(ListViewBase):
""" ListView base with an Gio.ListStore as data model
It can contain misc objects derived from GObject
"""
def __init__(self, model_cls):
super(ListViewListStore, self).__init__(model_cls)
def setup_store(self, model_cls) -> Gio.ListModel:
""" Setup the data model """
return Gio.ListStore.new(model_cls)
class ListViewStrings(ListViewBase):
""" Add ListView with only strings """
def __init__(self):
super(ListViewStrings, self).__init__(Gtk.StringObject)
def setup_store(self, model_cls) -> Gio.ListModel:
""" Setup the data model
Can be overloaded in subclass to use another Gio.ListModel
"""
return Gtk.StringList()
class SelectorBase(Gtk.ListBox):
""" Selector base class """
def __init__(self):
Gtk.ListBox.__init__(self)
# Setup the listbox
self.set_selection_mode(Gtk.SelectionMode.SINGLE)
self.connect('row-selected', self.on_row_changes)
self._rows = {}
self.ndx = 0
self.callback = None
def add_row(self, name, markup):
""" Overload this in a subclass"""
raise NotImplemented
def on_row_changes(self, widget, row):
ndx = row.get_index()
if self.callback:
self.callback(self._rows[ndx])
else:
print(f'Row Selected : {self._rows[ndx]}')
def set_callback(self, callback):
self.callback = callback
class TextSelector(SelectorBase):
""" Vertical Selector Widget that contains a number of strings where one can be selected """
def add_row(self, name: str, markup: str):
""" Add a named row to the selector with at given icon name"""
# get the image
label = Gtk.Label()
label.set_markup(markup)
# set the widget size request to 32x32 px, so we get some margins
# label.set_size_request(100, 24)
label.set_single_line_mode(True)
label.set_halign(Gtk.Align.START)
label.set_hexpand(True)
label.set_xalign(0)
label.set_margin_start(5)
label.set_margin_end(10)
row = self.append(label)
# store the index names, so we can find it on selection
self._rows[self.ndx] = name
self.ndx += 1
class IconSelector(SelectorBase):
""" Vertical Selector Widget that contains a number of icons where one can be selected """
def add_row(self, name, icon_name):
""" Add a named row to the selector with at given icon name"""
# get the image
pix = Gtk.Image.new_from_icon_name(icon_name)
# set the widget size request to 32x32 px, so we get some margins
pix.set_size_request(32, 32)
row = self.append(pix)
# store the index names, so we can find it on selection
self._rows[self.ndx] = name
self.ndx += 1
class ViewColumnBase(Gtk.ColumnViewColumn):
""" ColumnViewColumn base class, it setup the basic factory, selection model & data model
handlers must be overloaded & implemented in a sub class
"""
def __init__(self, model_cls, col_view):
Gtk.ColumnViewColumn.__init__(self)
self.col_view = col_view
# Use the signal Factory, so we can connect our own methods to setup
self.factory = Gtk.SignalListItemFactory()
# connect to Gtk.SignalListItemFactory signals
# check https://docs.gtk.org/gtk4/class.SignalListItemFactory.html for details
self.factory.connect('setup', self.on_factory_setup)
self.factory.connect('bind', self.on_factory_bind)
self.factory.connect('unbind', self.on_factory_unbind)
self.factory.connect('teardown', self.on_factory_teardown)
# Create data model, use our own class as elements
self.set_factory(self.factory)
self.store = self.setup_store(model_cls)
# create a selection model containing our data model
self.model = self.setup_model(self.store)
self.model.connect('selection-changed', self.on_selection_changed)
# add model to the ColumnView
self.col_view.set_model(self.model)
def setup_model(self, store: Gio.ListModel) -> Gtk.SelectionModel:
""" Setup the selection model to use in Gtk.ListView
Can be overloaded in subclass to use another Gtk.SelectModel model
"""
return Gtk.SingleSelection.new(store)
@abstractmethod
def setup_store(self, model_cls) -> Gio.ListModel:
""" Setup the data model
must be overloaded in subclass to use another Gio.ListModel
"""
raise NotImplemented
def add(self, elem):
""" add element to the data model """
self.store.append(elem)
# Gtk.SignalListItemFactory signal callbacks
# transfer to some some callback stubs, there can be overloaded in
# a subclass.
def on_factory_setup(self, widget, item: Gtk.ListItem):
""" GtkSignalListItemFactory::setup signal callback
Setup the widgets to go into the ListView """
self.factory_setup(widget, item)
def on_factory_bind(self, widget: Gtk.ListView, item: Gtk.ListItem):
""" GtkSignalListItemFactory::bind signal callback
apply data from model to widgets set in setup"""
self.factory_bind(widget, item)
def on_factory_unbind(self, widget, item: Gtk.ListItem):
""" GtkSignalListItemFactory::unbind signal callback
Undo the the binding done in ::bind if needed
"""
self.factory_unbind(widget, item)
def on_factory_teardown(self, widget, item: Gtk.ListItem):
""" GtkSignalListItemFactory::setup signal callback
Undo the creation done in ::setup if needed
"""
self.factory_teardown(widget, item)
def on_selection_changed(self, widget, position, n_items):
# get the current selection (GtkBitset)
selection = widget.get_selection()
# the the first value in the GtkBitset, that contain the index of the selection in the data model
# as we use Gtk.SingleSelection, there can only be one ;-)
ndx = selection.get_nth(0)
self.selection_changed(widget, ndx)
# --------------------> abstract callback methods <--------------------------------
# Implement these methods in your subclass
@abstractmethod
def factory_setup(self, widget: Gtk.ColumnViewColumn, item: Gtk.ListItem):
""" Setup the widgets to go into the ColumnViewColumn (Overload in subclass) """
pass
@abstractmethod
def factory_bind(self, widget: Gtk.ColumnViewColumn, item: Gtk.ListItem):
""" apply data from model to widgets set in setup (Overload in subclass)"""
pass
@abstractmethod
def factory_unbind(self, widget: Gtk.ColumnViewColumn, item: Gtk.ListItem):
pass
@abstractmethod
def factory_teardown(self, widget: Gtk.ColumnViewColumn, item: Gtk.ListItem):
pass
@abstractmethod
def selection_changed(self, widget, ndx):
""" trigged when selecting in listview is changed
ndx: is the index in the data store model that is selected
"""
pass
class ColumnViewListStore(ViewColumnBase):
""" ColumnViewColumn base with an Gio.ListStore as data model
It can contain misc objects derived from GObject
"""
def __init__(self, model_cls, col_view):
super(ColumnViewListStore, self).__init__(model_cls, col_view)
def setup_store(self, model_cls) -> Gio.ListModel:
""" Setup the data model """
return Gio.ListStore.new(model_cls)
class SearchBar(Gtk.SearchBar):
""" Wrapper for Gtk.Searchbar Gtk.SearchEntry"""
def __init__(self, win=None):
super(SearchBar, self).__init__()
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
box.set_spacing(10)
# Add SearchEntry
self.entry = Gtk.SearchEntry()
self.entry.set_hexpand(True)
box.append(self.entry)
# Add Search Options button (Menu content need to be added)
btn = Gtk.MenuButton()
btn.set_icon_name('preferences-other-symbolic')
self.search_options = btn
box.append(btn)
self.set_child(box)
# connect search entry to seach bar
self.connect_entry(self.entry)
if win:
# set key capture from main window, it will show searchbar, when you start typing
self.set_key_capture_widget(win)
# show close button in search bar
self.set_show_close_button(True)
# Set search mode to off by default
self.set_search_mode(False)
def set_callback(self, callback):
""" Connect the search entry activate to an callback handler"""
self.entry.connect('activate', callback)
class ButtonRow(Gtk.Box):
""" Row of button"""
def __init__(self, btn_list: list, callback):
super(ButtonRow, self).__init__(orientation=Gtk.Orientation.HORIZONTAL)
# self.set_halign(Gtk.Align.CENTER)
self.set_margin_start(20)
self.set_margin_top(20)
self.set_spacing(10)
for title in btn_list:
btn = Gtk.Button()
btn.set_label(f'{title}')
btn.connect('clicked', callback)
self.append(btn)
class SwitchRow(Gtk.Box):
def __init__(self, text):
super(SwitchRow, self).__init__(orientation=Gtk.Orientation.HORIZONTAL)
# Switch to control overlay visibility
self.label = Gtk.Label.new(text)
self.label.set_halign(Gtk.Align.FILL)
self.label.set_valign(Gtk.Align.CENTER)
self.label.set_hexpand(True)
self.label.set_xalign(0.0)
self.label.set_margin_start(20)
self.label.set_margin_bottom(20)
self.append(self.label)
self.switch = Gtk.Switch()
self.switch.set_halign(Gtk.Align.END)
self.switch.set_margin_end(20)
self.switch.set_margin_bottom(20)
self.append(self.switch)
def connect(self, signal, callback):
self.switch.connect(signal, callback)
def set_state(self, state):
self.switch.set_state(state)
class MenuButton(Gtk.MenuButton):
"""
Wrapper class for at Gtk.Menubutton with a menu defined
in a Gtk.Builder xml string
"""
def __init__(self, xml, name, icon_name='open-menu-symbolic'):
super(MenuButton, self).__init__()
builder = Gtk.Builder()
builder.add_from_string(xml)
menu = builder.get_object(name)
self.set_menu_model(menu)
self.set_icon_name(icon_name)
class Stack(Gtk.Stack):
""" Wrapper for Gtk.Stack with with a StackSwitcher """
def __init__(self):
super(Stack, self).__init__()
self.switcher = Gtk.StackSwitcher()
self.switcher.set_stack(self)
self._pages = {}
def add_page(self, name, title, widget):
page = self.add_child(widget)
page.set_name(name)
page.set_title(title)
self._pages[name] = page
return page
class Window(Gtk.ApplicationWindow):
""" custom Gtk.ApplicationWindow with a headerbar"""
def __init__(self, title, width, height, **kwargs):
super(Window, self).__init__(**kwargs)
self.set_default_size(width, height)
self.headerbar = Gtk.HeaderBar()
self.set_titlebar(self.headerbar)
label = Gtk.Label()
label.set_text(title)
self.headerbar.set_title_widget(label)
# custom CSS provider
self.css_provider = None
def load_css(self, css_fn):
"""create a provider for custom styling"""
if css_fn and os.path.exists(css_fn):
css_provider = Gtk.CssProvider()
try:
css_provider.load_from_path(css_fn)
except GLib.Error as e:
print(f"Error loading CSS : {e} ")
return None
print(f'loading custom styling : {css_fn}')
self.css_provider = css_provider
def _add_widget_styling(self, widget):
if self.css_provider:
context = widget.get_style_context()
context.add_provider(
self.css_provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
def add_custom_styling(self, widget):
self._add_widget_styling(widget)
# iterate children recursive
for child in widget:
self.add_custom_styling(child)
def create_action(self, name, callback):
""" Add an Action and connect to a callback """
action = Gio.SimpleAction.new(name, None)
action.connect("activate", callback)
self.add_action(action)

View File

@@ -1 +1,3 @@
fpdf2 fpdf2
pyserial
rich

View File

@@ -0,0 +1,2 @@
#!/bin/bash
socat -d -d pty,rawer,b9600 pty,rawer,b9600

View File

@@ -0,0 +1,78 @@
import sys
sys.path.insert(0, '../../otime')
import argparse
import serial
import otime
from rich import inspect
def main():
global package_n
package_n = 0
argparser = argparse.ArgumentParser(
description=("Acts as a live mtr reading data line by line from a log file and sending them over serial"))
argparser.add_argument('port', help='Serial port identifier')
argparser.add_argument(
'-f', '--file',
help=(
"Mtr log file to use"))
argparser.add_argument(
'-v', '--verbose', action='store_true', help='Verbose output')
args = argparser.parse_args()
port = serial.Serial(port=args.port, baudrate=9600)
event = otime.Event(0, 'NoName')
event.read_mtr_file(args.file)
for card in event.card_dumps:
input('Press enter to send next card')
if args.verbose == True:
inspect(card)
data = card_to_bytes(card)
port.write(data)
def card_to_bytes(card):
global package_n
package_n += 1
msg = b'\xFF\xFF\xFF\xFF\xE6\x4D\x9C\x3E'
year = int(str(card.read_time.year)[2:4])
month = card.read_time.month
day = card.read_time.day
hour = card.read_time.hour
minute = card.read_time.minute
second = card.read_time.second
msg += year.to_bytes(1, byteorder='little')
msg += month.to_bytes(1, byteorder='little')
msg += day.to_bytes(1, byteorder='little')
msg += hour.to_bytes(1, byteorder='little')
msg += minute.to_bytes(1, byteorder='little')
msg += second.to_bytes(1, byteorder='little')
#ms
msg += b'\x00\x00'
msg += package_n.to_bytes(4, byteorder='little')
msg += card.card.to_bytes(3, byteorder='little')
# Gidder ikke å finne ut hva Producweek Producyear er
msg += b'\x00\x00'
# Hopper overogså over ECardHeadSum
msg += b'\x00'
for n in range(50):
try:
control = card.controls[n]
split = card.splits[n]
except:
control = 0
split = 0
msg += control.to_bytes(1, byteorder='little')
msg += split.to_bytes(2, byteorder='little')
# 56 byte string
msg += int(0).to_bytes(56, byteorder='little')
# Checksum
csum = sum(msg) % 256
msg += csum.to_bytes(1, byteorder='little')
msg += b'\x00'
return msg
if __name__ == '__main__':
main()

View File

@@ -2,15 +2,32 @@ import sys
sys.path.insert(0, '../../otime') sys.path.insert(0, '../../otime')
import otime import otime
import pdf import pdf
import iof_xml
import file_io
def main(): def ttime_testing():
event = otime.Event(0, 'TEEEST', start_time=None, end_time=None,organiser='Tygbe') event = otime.Event(0, 'TEEEST', start_time=None, end_time=None,organiser='Tygbe')
event.read_ttime_cnf('tt.cnf') event.read_ttime_cnf('tt.cnf')
event.read_ttime_db('db.csv') event.read_ttime_db('db.csv')
event.read_mtr_file('mtr.csv') event.read_mtr_file('mtr.csv')
pdf.create_split_result_list(event, 'output/result.pdf') pdf.create_split_result_list(event, 'output/result.pdf')
iof_xml.create_result_file(event, '/home/trygve/Prosjekter/simple-liveresults/resultater/Resultater.xml')
print(event.get_runner_status('1400')) print(event.get_runner_status('1400'))
results = event.get_result() results = event.get_result()
print([pdf.format_m_s(i) for i in event.get_runner_splits('17')])
print([pdf.format_m_s(i) for i in event.get_card_dump(event.get_runner('17').card_id).splits])
file_io.write_config(event, 'output/config.yaml')
file_io.write_card_dumps(event, 'output/mtr.yaml')
file_io.write_runners_csv(event, 'output/runners.csv')
print(file_io.event_from_yaml_and_csv('output/config.yaml', 'output/mtr.yaml', 'output/runners.csv'))
def xml_testing():
event = iof_xml.event_from_xml_entries('entries.xml')
event.courses = iof_xml.courses_from_xml('course.xml')
file_io.write_config(event, 'output/config.yaml')
file_io.write_card_dumps(event, 'output/mtr.yaml')
file_io.write_runners_csv(event, 'output/runners.csv')
print(file_io.event_from_yaml_and_csv('output/config.yaml', 'output/mtr.yaml', 'output/runners.csv'))
if __name__ == '__main__': if __name__ == '__main__':
main() xml_testing()