Skip to content

Commit a9411de

Browse files
gui alignment and bugfix (#38)
* update example * plotting with `matplotlib` (#36) * add plotting * manipulate plotting windows * formatting * GUI alignment and end date bugfix
1 parent 31455ad commit a9411de

File tree

5 files changed

+149
-65
lines changed

5 files changed

+149
-65
lines changed

client/gui.py

Lines changed: 95 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,11 @@ def __init__(
100100
)
101101

102102
self.__add_entry_box(
103-
configuration_frame, title='callsigns', label='Callsigns', width=55, columnspan=3
103+
configuration_frame,
104+
title='callsigns',
105+
label='Callsigns',
106+
width=55,
107+
columnspan=configuration_frame.grid_size()[0],
104108
)
105109
self.__file_selection_option = 'select file...'
106110
self.__add_combo_box(
@@ -110,7 +114,7 @@ def __init__(
110114
options=list(available_serial_ports()) + [self.__file_selection_option],
111115
option_select=self.__select_tnc,
112116
width=52,
113-
columnspan=3,
117+
columnspan=configuration_frame.grid_size()[0],
114118
sticky='w',
115119
)
116120

@@ -128,20 +132,49 @@ def __init__(
128132
title='log_file',
129133
file_select=self.__select_log_file,
130134
label='Log',
131-
width=55,
132-
columnspan=3,
135+
width=52,
136+
columnspan=configuration_frame.grid_size()[0],
133137
sticky='w',
134138
)
135139
self.__add_file_box(
136140
configuration_frame,
137141
title='output_file',
138142
file_select=self.__select_output_file,
139143
label='Output',
140-
width=55,
141-
columnspan=3,
144+
width=52,
145+
columnspan=configuration_frame.grid_size()[0],
142146
sticky='w',
143147
)
144148

149+
separator = Separator(configuration_frame, orient=tkinter.HORIZONTAL)
150+
separator.grid(
151+
row=configuration_frame.grid_size()[1],
152+
column=0,
153+
columnspan=configuration_frame.grid_size()[0] + 1,
154+
sticky='ew',
155+
pady=10,
156+
)
157+
158+
plot_label = tkinter.Label(configuration_frame, text='Plots')
159+
plot_label.grid(row=configuration_frame.grid_size()[1], column=0, sticky='w')
160+
161+
plot_checkbox_frame = tkinter.Frame(configuration_frame)
162+
plot_checkbox_frame.grid(
163+
row=plot_label.grid_info()['row'],
164+
column=0,
165+
columnspan=configuration_frame.grid_size()[0] - 1,
166+
)
167+
168+
plot_variables = ['altitude', 'ascent_rate', 'ground_speed']
169+
self.__plot_toggles = {}
170+
for plot_index, plot in enumerate(plot_variables):
171+
boolean_var = tkinter.BooleanVar()
172+
plot_checkbox = tkinter.Checkbutton(
173+
plot_checkbox_frame, text=plot, variable=boolean_var
174+
)
175+
plot_checkbox.grid(row=0, column=plot_index, padx=10)
176+
self.__plot_toggles[plot] = boolean_var
177+
145178
separator = Separator(main_window, orient=tkinter.HORIZONTAL)
146179
separator.grid(row=main_window.grid_size()[1], column=0, sticky='ew')
147180

@@ -277,7 +310,7 @@ def end_date(self, end_date: datetime):
277310
end_date = f'{end_date:%Y-%m-%d %H:%M:%S}'
278311
else:
279312
end_date = ''
280-
self.replace_text(self.__elements['start_date'], end_date)
313+
self.replace_text(self.__elements['end_date'], end_date)
281314

282315
@property
283316
def log_filename(self) -> Path:
@@ -320,9 +353,7 @@ def output_filename(self, filename: PathLike):
320353
if not isinstance(filename, Path):
321354
filename = Path(filename)
322355
if filename.expanduser().resolve().is_dir():
323-
filename = (
324-
filename / f'packetraven_output_{datetime.now():%Y%m%dT%H%M%S}.geojson'
325-
)
356+
filename = filename / f'packetraven_output_{datetime.now():%Y%m%dT%H%M%S}.txt'
326357
else:
327358
filename = ''
328359
self.replace_text(self.__elements['output_file'], filename)
@@ -354,8 +385,12 @@ def __select_output_file(self):
354385
title='Create output file...',
355386
initialdir=self.output_filename.parent,
356387
initialfile=self.output_filename.stem,
357-
defaultextension='.geojson',
358-
filetypes=[('GeoJSON', '*.geojson'), ('Keyhole Markup Language', '*.kml')],
388+
defaultextension='.txt',
389+
filetypes=[
390+
('Text', '*.txt'),
391+
('GeoJSON', '*.geojson'),
392+
('Keyhole Markup Language', '*.kml'),
393+
],
359394
)
360395

361396
def __select_tnc(self, event):
@@ -384,18 +419,43 @@ def __add_combo_box(
384419
def __add_file_box(
385420
self, frame: tkinter.Frame, title: str, file_select: Callable, **kwargs
386421
) -> tkinter.Entry:
387-
file_box = self.__add_entry_box(frame, title, **kwargs)
388-
log_file_button = tkinter.Button(frame, text='...', command=file_select)
389-
log_file_button.grid(
390-
row=file_box.grid_info()['row'],
391-
column=file_box.grid_info()['column'] + file_box.grid_info()['columnspan'],
422+
if 'row' not in kwargs:
423+
kwargs['row'] = frame.grid_size()[1]
424+
if 'column' not in kwargs:
425+
kwargs['column'] = 0
426+
file_box_kwargs = {
427+
key: value
428+
for key, value in kwargs.items()
429+
if key in ['row', 'column', 'columnspan']
430+
}
431+
if 'columnspan' in file_box_kwargs:
432+
file_box_kwargs['columnspan'] -= 1
433+
434+
if 'label' in kwargs:
435+
text_label = tkinter.Label(frame, text=kwargs['label'])
436+
text_label.grid(row=kwargs['row'], column=kwargs['column'], sticky='w')
437+
file_box_kwargs['column'] += 1
438+
439+
file_box_frame = tkinter.Frame(frame)
440+
file_box_frame.grid(**file_box_kwargs)
441+
442+
file_box = tkinter.Entry(
443+
file_box_frame, width=kwargs['width'] if 'width' in kwargs else None
392444
)
445+
file_button = tkinter.Button(file_box_frame, text='...', command=file_select)
446+
447+
file_box.pack(side='left')
448+
file_button.pack(side='left')
449+
450+
self.__elements[title] = file_box
393451
return file_box
394452

395453
def __add_entry_box(self, frame: tkinter.Frame, title: str, **kwargs) -> tkinter.Entry:
396-
width = kwargs['width'] if 'width' in kwargs else None
397-
entry_box = tkinter.Entry(frame, width=width)
398-
return self.__add_text_box(frame, title, text_box=entry_box, **kwargs)
454+
if 'text_box' not in kwargs:
455+
kwargs['text_box'] = tkinter.Entry(
456+
frame, width=kwargs['width'] if 'width' in kwargs else None
457+
)
458+
return self.__add_text_box(frame, title, **kwargs)
399459

400460
def __add_text_box(
401461
self,
@@ -414,6 +474,12 @@ def __add_text_box(
414474
if column is None:
415475
column = 0
416476

477+
if 'columnspan' in kwargs:
478+
if label is not None:
479+
kwargs['columnspan'] -= 1
480+
if units is not None:
481+
kwargs['columnspan'] -= 1
482+
417483
if label is not None:
418484
text_label = tkinter.Label(frame, text=label)
419485
text_label.grid(row=row, column=column, sticky='w')
@@ -572,6 +638,15 @@ def toggle(self):
572638
if database_password is None or len(database_password) == 0:
573639
raise ConnectionError('missing database password')
574640
database_kwargs['password'] = database_password
641+
if 'table' not in database_kwargs or database_kwargs['table'] is None:
642+
database_table = simpledialog.askstring(
643+
'Database Table',
644+
f'enter database table name',
645+
parent=self.__windows['main'],
646+
)
647+
if database_table is None or len(database_table) == 0:
648+
raise ConnectionError('missing database table name')
649+
database_kwargs['table'] = database_table
575650

576651
self.database = APRSDatabaseTable(
577652
**database_kwargs, **ssh_tunnel_kwargs, callsigns=self.callsigns

packetraven/packets.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,7 @@ def ground_speed(self) -> float:
147147
return self.__horizontal / self.seconds if self.seconds > 0 else 0
148148

149149
def __str__(self) -> str:
150-
return (
151-
f'{self.seconds}s, {self.ascent:6.2f}m vertical, {self.overground:6.2f}m horizontal'
152-
)
150+
return f'{self.seconds}s, {self.ascent:6.2f}m vertical, {self.overground:6.2f}m horizontal'
153151

154152
def __repr__(self) -> str:
155153
return (

packetraven/plotting.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,18 @@
88

99
VARIABLES = {
1010
'altitude': {'x': 'times', 'y': 'altitudes', 'xlabel': 'time', 'ylabel': 'altitude (m)'},
11-
'ascent_rate': {'x': 'times', 'y': 'ascent_rates', 'xlabel': 'time', 'ylabel': 'ascent rate (m/s)'},
12-
'ground_speed': {'x': 'ground_speeds', 'y': 'altitudes', 'xlabel': 'ground speed (m/s)', 'ylabel': 'altitude (m)'}
11+
'ascent_rate': {
12+
'x': 'times',
13+
'y': 'ascent_rates',
14+
'xlabel': 'time',
15+
'ylabel': 'ascent rate (m/s)',
16+
},
17+
'ground_speed': {
18+
'x': 'ground_speeds',
19+
'y': 'altitudes',
20+
'xlabel': 'ground speed (m/s)',
21+
'ylabel': 'altitude (m)',
22+
},
1323
}
1424

1525

@@ -43,6 +53,7 @@ def update(self, packet_tracks: {str: LocationPacketTrack} = None):
4353
getattr(packet_track, VARIABLES[self.variable]['x']),
4454
getattr(packet_track, VARIABLES[self.variable]['y']),
4555
label=packet_track.name,
56+
s=2,
4657
)
4758

4859
self.axis.legend()

packetraven/writer.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,15 @@ def write_aprs_packet_tracks(packet_tracks: [APRSTrack], output_filename: PathLi
1414
if not isinstance(output_filename, Path):
1515
output_filename = Path(output_filename)
1616
output_filename = output_filename.resolve().expanduser()
17-
extension = output_filename.suffix
18-
if extension == '.geojson':
17+
if output_filename.suffix == '.txt':
18+
packets = []
19+
for packet_track_index, packet_track in enumerate(packet_tracks):
20+
packets.extend(packet_track)
21+
packets = sorted(packets)
22+
lines = [f'{packet.time:%Y-%m-%d %H:%M:%S %Z}: {packet.frame}' for packet in packets]
23+
with open(output_filename, 'w') as output_file:
24+
output_file.writelines(lines)
25+
elif output_filename.suffix == '.geojson':
1926
import geojson
2027

2128
features = []
@@ -55,7 +62,7 @@ def write_aprs_packet_tracks(packet_tracks: [APRSTrack], output_filename: PathLi
5562

5663
with open(output_filename, 'w') as output_file:
5764
geojson.dump(features, output_file)
58-
elif extension == '.kml':
65+
elif output_filename.suffix == '.kml':
5966
from fastkml import kml
6067

6168
output_kml = kml.KML()
@@ -93,5 +100,5 @@ def write_aprs_packet_tracks(packet_tracks: [APRSTrack], output_filename: PathLi
93100
output_file.write(output_kml.to_string())
94101
else:
95102
raise NotImplementedError(
96-
f'saving to file type "{extension}" has not been implemented'
103+
f'saving to file type "{output_filename.suffix}" has not been implemented'
97104
)

tests/test_writer.py

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,50 +6,43 @@
66
from packetraven.tracks import APRSTrack
77
from packetraven.writer import write_aprs_packet_tracks
88

9+
PACKET_TRACK = APRSTrack(
10+
'W3EAX-13',
11+
[
12+
APRSPacket.from_frame(
13+
"W3EAX-8>APRS,WIDE1-1,WIDE2-1,qAR,K3DO-11:!/:Gh=:j)#O /A=026909|!Q| /W3EAX,262,0,18'C,http://www.umd.edu",
14+
packet_time=datetime(2018, 11, 11, 10, 20, 13),
15+
),
16+
APRSPacket.from_frame(
17+
"W3EAX-8>APRS,N3TJJ-12,WIDE1*,WIDE2-1,qAR,N3FYI-2:!/:GiD:jcwO /A=028365|!R| /W3EAX,267,0,18'C,"
18+
'http://www.umd.edu',
19+
packet_time=datetime(2018, 11, 11, 10, 21, 24),
20+
),
21+
APRSPacket.from_frame(
22+
"W3EAX-13>APRS,KC3FIT-1,WIDE1*,WIDE2-1,qAR,KC3AWP-10:!/:JL2:u4wO /A=043080|!j| /W3EAX,326,0,20'C,"
23+
'nearspace.umd.edu',
24+
packet_time=datetime(2019, 2, 3, 14, 39, 28),
25+
),
26+
],
27+
)
928

10-
def test_write_kml():
11-
packet_1 = APRSPacket.from_frame(
12-
"W3EAX-8>APRS,WIDE1-1,WIDE2-1,qAR,K3DO-11:!/:Gh=:j)#O /A=026909|!Q| /W3EAX,262,0,18'C,http://www.umd.edu",
13-
packet_time=datetime(2018, 11, 11, 10, 20, 13),
14-
)
15-
packet_2 = APRSPacket.from_frame(
16-
"W3EAX-8>APRS,N3TJJ-12,WIDE1*,WIDE2-1,qAR,N3FYI-2:!/:GiD:jcwO /A=028365|!R| /W3EAX,267,0,18'C,"
17-
'http://www.umd.edu',
18-
packet_time=datetime(2018, 11, 11, 10, 21, 24),
19-
)
20-
packet_3 = APRSPacket.from_frame(
21-
"W3EAX-13>APRS,KC3FIT-1,WIDE1*,WIDE2-1,qAR,KC3AWP-10:!/:JL2:u4wO /A=043080|!j| /W3EAX,326,0,20'C,"
22-
'nearspace.umd.edu',
23-
packet_time=datetime(2019, 2, 3, 14, 39, 28),
24-
)
25-
26-
track = APRSTrack('W3EAX-13', [packet_1, packet_2, packet_3])
2729

30+
def test_write_kml():
2831
with TemporaryDirectory() as temporary_directory:
2932
output_filename = Path(temporary_directory) / 'test_output.kml'
30-
write_aprs_packet_tracks([track], output_filename)
33+
write_aprs_packet_tracks([PACKET_TRACK], output_filename)
3134
assert output_filename.exists()
3235

3336

3437
def test_write_geojson():
35-
packet_1 = APRSPacket.from_frame(
36-
"W3EAX-8>APRS,WIDE1-1,WIDE2-1,qAR,K3DO-11:!/:Gh=:j)#O /A=026909|!Q| /W3EAX,262,0,18'C,http://www.umd.edu",
37-
packet_time=datetime(2018, 11, 11, 10, 20, 13),
38-
)
39-
packet_2 = APRSPacket.from_frame(
40-
"W3EAX-8>APRS,N3TJJ-12,WIDE1*,WIDE2-1,qAR,N3FYI-2:!/:GiD:jcwO /A=028365|!R| /W3EAX,267,0,18'C,"
41-
'http://www.umd.edu',
42-
packet_time=datetime(2018, 11, 11, 10, 21, 24),
43-
)
44-
packet_3 = APRSPacket.from_frame(
45-
"W3EAX-13>APRS,KC3FIT-1,WIDE1*,WIDE2-1,qAR,KC3AWP-10:!/:JL2:u4wO /A=043080|!j| /W3EAX,326,0,20'C,"
46-
'nearspace.umd.edu',
47-
packet_time=datetime(2019, 2, 3, 14, 39, 28),
48-
)
49-
50-
track = APRSTrack('W3EAX-13', [packet_1, packet_2, packet_3])
51-
5238
with TemporaryDirectory() as temporary_directory:
5339
output_filename = Path(temporary_directory) / 'test_output.geojson'
54-
write_aprs_packet_tracks([track], output_filename)
40+
write_aprs_packet_tracks([PACKET_TRACK], output_filename)
41+
assert output_filename.exists()
42+
43+
44+
def test_write_txt():
45+
with TemporaryDirectory() as temporary_directory:
46+
output_filename = Path(temporary_directory) / 'test_output.txt'
47+
write_aprs_packet_tracks([PACKET_TRACK], output_filename)
5548
assert output_filename.exists()

0 commit comments

Comments
 (0)