forked from ncssar/radiolog
-
Notifications
You must be signed in to change notification settings - Fork 0
/
radiolog.py
6794 lines (6297 loc) · 311 KB
/
radiolog.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# #############################################################################
#
# radiolog.py - SAR Radio Log program, based on PyQt 5.4, Python 3.4.2
#
# developed for Nevada County Sheriff's Search and Rescue
# Copyright (c) 2015-2018 Tom Grundy
#
# http://github.com/ncssar/radiolog
#
# Contact the author at [email protected]
# Attribution, feedback, bug reports and feature requests are appreciated
#
# REVISION HISTORY
#-----------------------------------------------------------------------------
# DATE | AUTHOR | NOTES
#-----------------------------------------------------------------------------
# 2-22-15 TMG First version installed and working on NCSSAR Computer 2
# 4-9-15 TMG Feature-complete release candidate
# 4-10-15 TMG enable reading from a second com port (two radios)
# 4-28-15 TMG First release; initial commit to git
# 5-11-15 TMG fix bug 10: don't clear team timer unless the message
# is 'FROM' with non-blank message text; change more
# app-wide stylesheet font sizes in fontsChanged, and
# change in children to override as needed
# 11-27-16 TMG fix 307 (help window flashing colors are bouncing); also
# verified no ill effects in team tabs
# 12-1-16 TMG fix 268 (throb crash on oldest item in non-empty stack)
# 12-1-16 TMG add -nosend option to disable sending of GET requests,
# to avoid lag when network is not present
# 12-10-16 TMG fix 267 (filename space handler) - remove spaces from
# incident name for purposes of filenames
# 12-10-16 TMG fix 25 (print dialog button wording should change when
# called from main window exit button)
# 12-10-16 TMG fix 306 (attached callsigns)
# 1-17-17 TMG fix 41 (USB hot-plug / hot-unplug)
# 2-26-17 TMG fix 311 (attached callsign bug)
# 2-27-17 TMG fix 314 (NED focus / two-blinking-cursors)
# 2-27-17 TMG fix 315 (ctrl-Z crash on empty list) for NED and for clue report
# 2-27-17 TMG fix 312 (prevent orphaned clue/subject dialogs);
# fix 317 (crash on cancel of cancel for clue/subject dialogs);
# this involved changing dialog cancel buttons to just call
# close() instead of reject()
# 2-28-17 TMG fix 316,318,320 (add dialog open/cancel radio log entries)
# and extend closeEvent functionality (above) to
# nonRadioClueDialog and changeCallsignDialog
# 4-7-17 TMG stopgap for 310 - disable attached callsign handling for now
# 4-11-17 TMG fix 322 (restore crashes - file not found) - give a message
# and return gracefully if the file specified in the rc file
# does not exist, and, take steps to make sure the correct
# filename is saved to the rc file at the correct times
# 4-15-17 TMG fix 323 (load dialog should only show non-fleetsync and
# non-clueLog .csv files)
# 4-26-17 TMG fix 326 (zero-left-padded tabs when callsigns are digits-only)
# 4-29-17 TMG fix 34 (fleetsync mute)
# 5-2-17 TMG fix 325 (cancel-confirm bypass if message is blank)
# 5-13-17 TMG fix 338 (esc key in clue/subject dialogs closes w/out confirm):
# add keyPressEvents to ignore the esc key; note that the
# Qt docs say that .reject() is called by the esc key but
# that has other repercussions in this case; also serves
# as interim fix for #337 (crash due to the above) until
# a strong parent-child dialog relationship can be established
# (see http://stackoverflow.com/questions/43956587)
# 5-13-17 TMG fix #333 (crash on throb after widget has been closed)
# 5-13-17 TMG further fix on #333: don't try to stop the timer if it
# does not exist, i.e. if not currently mid-throb; caused
# similar crash to #333 during auto-cleanup of delayed stack
# 5-19-17 TMG move loadFlag settings closer to core load functionality
# to at least partially address #340
# 5-21-17 TMG fix #257, fix #260: 90+% lag reduction: do not do any sorting;
# instead, calculate the correct index to insert a new row
# during newEntry, and use beginInsertRows and endInsertRows
# instead of layoutChanged; see notes in newEntry function
# 6-15-17 TMG fix #336 by simply ignoring keyPressEvents that happen
# before newEntryWidget is responsive, and get fielded by
# MyWindows instead; would be nice to find a better long-term
# solution; see https://stackoverflow.com/questions/44148992
# and see notes inline below
# 7-1-17 TMG fix #342 (focus-follows-mouse freeze); 'fix' #332 (freeze
# due to modal dialogs displayed underneath other windows)
# by doing full audit and recode and test of all QMessageBox calls
# 7-1-17 TMG fix #343 (crash on print clue log when there are no clues):
# show an error message when the print button is clicked if
# there are no operational periods that have clues, and only
# populate the print clue log operational period cyclic field
# with op periods that do have clues
# 7-3-17 TMG fix #341 (add checkbox for fleetsync mute)
# 9-24-17 TMG fix #346 (slash in incident name kills everything) using
# normName function; get rid of leading space in loaded
# incident name due to incorrect index (17 instead of 18);
# fix #349 (save filename not updated after load)
# 9-24-17 TMG fix #350 (do not try to read fleetsync file on restore) by
# adding hideWarnings argument to fsLoadLookup
# 9-24-17 TMG fix #345 (get rid of 'printing' message dialog) by commenting
# out all print dialog lines which also fixes # 33 and #263;
# time will tell if this is sufficient, or if we need to
# bring back some less-invasive and less-confusing notification,
# like a line in the main dialog or such
# 11-5-17 TMG fix #32 (add fleetsync device filtering) - affects several
# parts of the code and several files
# 11-15-17 TMG fix #354 (stolen focus / hold time failure); fix #355 (space bar error);
# add focus rules and timeline documentation; change hold time
# to 20 sec (based on observations during class); set focus
# to the active stack item's message field on changeCallsignDialog close
# 11-23-17 TMG address #31 (css / font size issues) - not yet checked against
# dispatch computer - only tested on home computer
# 11-23-17 TMG fix #356 (change callsign dialog should not pop up until
# its new entry widget is active (i.e. the active stack item)
# 5-1-18 TMG fix #357 (freeze after print, introduced by fix # 345)
# 5-28-18 TMG fix #360 (remove leading zeros from team tab names)
# 6-9-18 TMG allow configuration by different teams using optional local/radiolog.cfg
# (merged config branch to master)
# 7-22-18 TMG add team hotkeys (fix #370); change return/enter/space to open
# a new entry dialog with blank callsign (i.e. LEO callsigns);
# toggle team hotkeys vs normal hotkeys using F12
# 7-22-18 TMG fix #373 (esc closes NED in same manner as cancel button)
# 7-22-18 TMG fix #360 again (leading zeros still showed up in tab
# context menus, therefore in callsign field of NED created
# from tab context menus)
# 8-2-18 TMG space bar event does not reach the main window after the
# first team tab gets created, so disable it for now -
# must use enter or return to open a NED with no callsign (#370)
# 8-3-18 TMG fix #372 (combobox / cyclic callsign selection)
# 8-5-18 TMG fix #371 (amend callsign of existing message)
# 8-29-18 TMG fix #375 (crash during new entry for new team)
# 9-9-18 TMG fix #379 (subject located form - field type error; confirmed
# that all other calls to toPlainText are for valid fields)
# 9-9-18 TMG add a very short timeout to the requests.get locator update call to
# eliminate lag while completely ignoring the response
# ('fire-and-forget'); would have to use a thread-based module
# if the response were important; works well on home computer,
# hopefully this fixes #378
# 9-17-18 TMG fix and improve team hotkey selection and recycling
# 9-17-18 TMG change some dictionary lookups to use get() with a default,
# to avoid possible KeyErrors
# 9-17-18 TMG catch any sync errors during deletion of proxyModelList entries
# (which happens during team tab deletion)
# 9-17-18 TMG disallow blank callsign for new entry
# 9-23-18 TMG cleanup config file defaults handling
# 10-3-18 TMG fix #364: eliminate backup rotation lag by running it
# in the background (external powershell script on Windows
# systems; custom script can be specified in config file;
# currently there is no default backup rotation script for
# non-Windows systems)
# 10-26-18 TMG fix #380 (fleetsync CID parsing issue); add more CID parsing
# and callsign-change messages
# 11-17-18 TMG overhaul logging: use the logging module, making sure
# to show uncaught exceptions on the screen and in the
# log file
# 11-17-18 TMG fix #382 (disable locator requests from GUI);
# fix #383 (disable second working dir from GUI)
# 11-17-18 TMG fix #381 (auto-accept entry on clue report or subj located accept);
# fix #339 (don't increment clue# if clue form is canceled)
# 11-18-18 TMG fix #358 and make FS location parsing more robust
# 11-18-18 TMG fix #351 (don't show options at startup after restore)
# 12-12-18 TMG fix #384 (bad data causes unpack error)
# 12-14-18 TMG fix #385 (print team log from team tab context menu)
# 12-15-18 TMG fix #387 (file browser sort by date)
# 12-15-18 TMG simplify code for #387 fix above; also filter out _clueLog_bak
# and _fleetSync_bak files from file browser
# 12-16-18 TMG fix #388 (team log print variant team names)
# 4-11-19 TMG fix #392 (get rid of leading 'Team ' when appropriate);
# fix #393 (preserve case of new callsigns);
# fix #394 (show actual tie in log messages)
# 5-3-19 TMG fix #329 (team tab bar grouping) - default groups are just
# numbered teams, vs everything else; can specify a more
# elaborate set of group names and regular expressions
# in the config file (tabGroups)
# 5-4-19 TMG enhance #393: if typed callsign is a case-insensitive
# match with an existing callsign, use the existing callsign;
# fix #397 ('Available' status - also added 'Off Duty' status
# which does not time out and has no background and gray text;
# '10-8' changes to 'Available', '10-97' changes to 'Working',
# '10-10' changes to 'Off Duty')
# 2-8-20 TMG re-fix #41: repair hot-unplug handling for current pyserial
# 2-10-20 TMG fix #396: create default local dir and config file if needed
# 5-28-20 TMG fix #412: relayed message features
# 6-15-20 TMG fix #415: restore timeout on auto-recover (in rc file);
# fix #404: show http request response in log;
# address #413: multiple crashes - add more logging;
# improve relay features to be more intuitive
#
# #############################################################################
#
# 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.
#
# See included file LICENSE.txt for full license terms, also
# available at http://opensource.org/licenses/gpl-3.0.html
#
# ############################################################################
#
# Originally written and tested on Windows Vista Home Basic 32-bit;
# should run for Windows Vista and higher
#
# Note, this file must be encoded as UTF-8, to preserve degree signs in the code
#
# ############################################################################
#
# key sequence behavior:
#
# when radio log form has focus:
# <space> - open new entry dialog box: default values = from; first team in team combo box; time; focus goes to team field
# <t> - open new entry dialog box: defaults = to; first team in team field; time; focus goes to team field
# <f> - same as <space>
# <any number> - open new entry dialog box: defaults = from; first team in team field whose first digit matches the typed number, or, new team# if none match; time
#
# when new entry dialog box has focus:
#
# known limitations:
# cannot handle team names that have alpha characters after numeric characters (3b, 4a, etc)
#
# ############################################################################
#
# message queue / stack rules:
#
# when an incoming message happens during editing of an existing message:
# 1. add a stack entry on top of the stack and start flashing
# 2. if the current message has had no keystrokes / activity in the last n seconds,
# leave it as is and move to the new entry
# 3. if the current message has had activity in the last n seconds, leave it open
#
# when message is accepted:
# 1. go to next message upwards in the stack
# 2. if none, go the next message downwards in the stack
# 3. if none, close the dialog
#
# every n seconds of inactivity:
# 1. accept the top message on the stack, and move to the next one
#
# when any given message is set active:
# 1. stop flashing, if it was flashing
#
# ############################################################################
#
# core files of this project (until compiled/deployed):
# - radiolog.py - this file, invoke with 'python radiolog.py' if not deployed
# - radiolog_ui.py - pyuic5 compilation of radiolog.ui from QTDesigner
# - newEntryDialog_ui.py - pyuic5 compilation of newEntryDialog.ui from QTDesigner
# - options_ui.py - pyuic5 compilation of options.ui from QTDesigner
# - help_ui.py - pyuic5 compilation of help.ui from QTDesigner
# - radiolog_ui_qrc.py - pyrcc5 compilation of radiolog_ui.qrc from QTDesigner
# - QTDesigner .ui and .qrc files as listed above
#
# ancillary files shipped with and/or referenced by this code:
# - radiolog_logo.jpg - if the logo exists it is included in the printout
# - radiolog_rc.txt - resource file, keeps track of last used window geometry and font size
# - radiolog_fleetsync.csv - default CSV lookup table: fleet,device,callsign
# (any fleet/device pairs that do not exist in that file will have callsign KW-<fleet>-<device>)
#
# files generated by this code:
# NOTE: this program uses two working directories for redundancy. The first one is
# the current Windows user's Documents directory, and must be writable to avoid
# a fatal error. The second one will be written to on every new entry if it is available,
# but will not cause any error if it is unavailable. The second working directory
# is specified below with the variable 'secondWorkingDir'.
# - <incident_name>_<timestamp>.csv - CSV of entire radio log, written after each new entry
# - <incident_name>_<timestamp>_fleetsync.csv - CSV FleetSync lookup table, written on each callsign change
# - <incident_name>_<timestamp>_clueLog.csv - CSV clue log, written after each clue entry
# - <incident_name>_<timestamp>_OP#.pdf - PDF of the radio log for specified operational period
# - <incident_name>_<timestamp>_clueLog_OP#.pdf - PDF of the clue log for specified operational period
# - <incident_name>_<timestamp>_clue##.fdf - FDF of clue report form (see calls to forge_fdf)
# - <incident_name>_<timestamp>_clue##.fdf - PDF of clue report form
# NOTE: upon change of incident name, previously saved files are not deleted, but
# are kept in place as another backup. Also, the previous log file is copied
# as the new log file, so that file append on each new entry can continue
# seamlessly.
#
# required non-core python modules:
# - reportlab (for printing)
# - pyserial (for com port / usb communication)
# - requests (for forwarding locator information as a GET request for sarsoft)
#
# #############################################################################
#
# speed / performance notes
#
# once the table length gets about 500 rows or so, some delay is noticable:
# - font resize (+/- keys)
# - new entry form closed with 'OK' or Enter/Return, until the new entry appears in the table
# - window resize
# - print (delay per page)
#
# conclusions from benchmarking and speed tests on a table of around 1300 rows:
# - the single biggest source of delay can be header resize modes:
# ...verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
# will cause each row to resize whenever the contents change, which is probably
# more often than you want, and will be called in a hidden / nested manner
# inside other table view changes. So, disable any lines like this, and only
# resize when you really need to. Removing this line reduced redraw time
# on font change from 12 seconds to 3. Also, it 'completely' eliminates
# window resize delay.
# - delay on entry form close is 'eliminated' / deferred by doing all the
# redisplay work in a singleshot after the form is closed
#
# #############################################################################
# #############################################################################
import functools
import sys
import logging
import time
import re
import serial
import serial.tools.list_ports
import csv
import os.path
import os
import glob
import requests
import subprocess
import win32api
import win32gui
import win32print
import win32con
import shutil
import math
import textwrap
from reportlab.lib import colors,utils
from reportlab.lib.pagesizes import letter,landscape,portrait
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Image, Spacer
from reportlab.lib.styles import getSampleStyleSheet,ParagraphStyle
from reportlab.lib.units import inch
from PyPDF2 import PdfFileReader,PdfFileWriter
from FingerTabs import *
from pygeodesy import Datums,ellipsoidalBase,dms
__version__ = "3.0.1"
# process command-line arguments
develMode=False
noSend=False
if len(sys.argv)>1:
for arg in sys.argv[1:]:
if arg.lower()=="-devel":
develMode=True
print("Development mode enabled.")
if arg.lower()=="-min":
minMode=True
print("Minimum display size mode enabled.")
if arg.lower()=="-nosend":
noSend=True
print("Will not send any GET requests for this session.")
from ui.radiolog_ui import Ui_Dialog # normal version, for higher resolution
statusStyleDict={}
# even though tab labels are created with font-size:20px and the tab sizes and margins are created accordingly,
# something after creation time is changing font-sizes to a smaller size. So, just
# hardcode them all here to force 20px always.
tabFontSize="20px"
statusStyleDict["At IC"]="font-size:"+tabFontSize+";background:#00ff00;border:1px outset black;padding-left:0px;padding-right:0px"
statusStyleDict["In Transit"]="font-size:"+tabFontSize+";background:blue;color:white;border:1px outset black;padding-left:0px;padding-right:0px"
statusStyleDict["Working"]="font-size:"+tabFontSize+";background:none;border:1px outset black;padding-left:0px;padding-right:0px"
statusStyleDict["Off Duty"]="font-size:"+tabFontSize+";color:#aaaaaa;background:none;border:none;padding-left:0px;padding-right:0px"
# Waiting for Transport and Available should still flash even if not timed out, but they don't
# prevent a timeout. So, for this code, and alternating timer cycles (seconds):
# cycle 1: style as in the line specified for that status name
# cycle 2: if not timed out, style as "" (blank); if timed out, style as timeout as expected
statusStyleDict["Available"]="font-size:"+tabFontSize+";background:#00ffff;border:1px outset black;padding-left:0px;padding-right:0px;padding-top:-1px;padding-bottom:-1px"
statusStyleDict["Waiting for Transport"]="font-size:"+tabFontSize+";background:blue;color:white;border:1px outset black;padding-left:0px;padding-right:0px;padding-top:-1px;padding-bottom:-1px"
statusStyleDict["STANDBY"]="font-size:"+tabFontSize+";background:black;color:white;border:1px outset black;padding-left:0px;padding-right:0px;padding-top:-1px;padding-bottom:-1px"
statusStyleDict[""]="font-size:"+tabFontSize+";background:none;padding-left:1px;padding-right:1px"
statusStyleDict["TIMED_OUT_ORANGE"]="font-size:"+tabFontSize+";background:orange;border:1px outset black;padding-left:0px;padding-right:0px;padding-top:-1px;padding-bottom:-1px"
statusStyleDict["TIMED_OUT_RED"]="font-size:"+tabFontSize+";background:red;border:1px outset black;padding-left:0px;padding-right:0px;padding-top:-1px;padding-bottom:-1px"
timeoutDisplayList=[["10 sec",10]]
for n in range (1,13):
timeoutDisplayList.append([str(n*10)+" min",n*600])
teamStatusDict={}
teamFSFilterDict={}
teamTimersDict={}
teamCreatedTimeDict={}
versionDepth=5 # how many backup versions to keep; see rotateBackups
continueSec=20
holdSec=20
# log com port messages?
comLog=False
##newEntryDialogTimeoutSeconds=600
# choosing window location for newly opened dialog: just keeping a count of how many
# dialogs are open does not work, since an open dialog at a given position could
# still be completely covered by a newly opened dialog at that same position.
# Instead, create a list of valid fixed positions, then each newly opened dialog
# will just use the first available one in the list. Update the list of which ones
# are used when each dialog is opened.
#openNewEntryDialogCount=0
##newEntryDialog_x0=100
##newEntryDialog_y0=100
##newEntryDialog_dx=30
##newEntryDialog_dy=30
lastClueNumber=0
##quickTextList=[
## ["DEPARTING IC",Qt.Key_F1],
## ["STARTING ASSIGNMENT",Qt.Key_F2],
## ["COMPLETED ASSIGNMENT",Qt.Key_F3],
## ["ARRIVING AT IC",Qt.Key_F4],
## "separator",
## ["RADIO CHECK: OK",Qt.Key_F5],
## ["WELFARE CHECK: OK",Qt.Key_F6],
## ["REQUESTING TRANSPORT",Qt.Key_F7],
## "separator",
## ["STANDBY",Qt.Key_F8],
## "separator",
## ["LOCATED A CLUE",Qt.Key_F9],
## "separator",
## ["SUBJECT LOCATED",Qt.Key_F10],
## "separator",
## ["REQUESTING DEPUTY",Qt.Key_F11]]
def getExtTeamName(teamName):
if teamName.lower().startswith("all ") or teamName.lower()=="all":
return "ALL TEAMS"
# fix #459 (and other places in the code): remove all leading and trailing spaces, and change all chains of spaces to one space
name=re.sub(r' +',r' ',teamName).strip()
name=name.replace(' ','') # remove spaces to shorten the name
# find index of first number in the name; everything left of that is the 'prefix';
# assume that everything after the prefix is a number
firstNum=re.search("\d",name)
firstNumIndex=-1 # assume there is no number at all
if firstNum!=None:
firstNumIndex=firstNum.start()
if firstNumIndex>0:
prefix=name[:firstNumIndex]
else:
prefix=""
# print("FirstNumIndex:"+str(firstNumIndex)+" Prefix:'"+prefix+"'")
# allow shorthand team names (t2) to still be inserted in the same sequence as
# full team names (team2) so the tab list could be: team1 t2 team3
# but other team names starting with t (tr1, transport1) would be added at the end
if prefix.lower()=='t':
prefix='team'
# now force everything other than 'team' to be added alphabetically at the end,
# by prefixing the prefix with 'z_' (the underscore makes it unique for easy pruning later)
# if prefix!='team':
# prefix="z_"+prefix
# #503 - extTeamName should always start with 'z_' then a capital letter
prefix='z_'+prefix.capitalize()
if firstNum!=None:
rest=name[firstNumIndex:].zfill(5)
else:
rest=teamName # preserve case if there are no numbers
## rprint("prefix="+prefix+" rest="+rest+" name="+name)
extTeamName=prefix+rest
# rprint("Team Name:"+teamName+": extended team name:"+extTeamName)
return extTeamName
def getNiceTeamName(extTeamName):
# prune any leading 'z_' that may have been added for sorting purposes
extTeamName=extTeamName.replace('z_','')
# find index of first number in the name; everything left of that is the 'prefix';
# assume that everything after the prefix is a number
# (left-zero-padded to 5 digits)
firstNum=re.search("\d",extTeamName)
firstNumIndex=-1 # assume there is no number at all
prefix=""
if firstNum:
firstNumIndex=firstNum.start()
if firstNumIndex>0:
prefix=extTeamName[:firstNumIndex]
# name=prefix.capitalize()+" "+str(int(str(extTeamName[firstNumIndex:])))
name=prefix.capitalize()+" "+str(extTeamName[firstNumIndex:]).lstrip('0')
else:
name=extTeamName
# finally, remove any leading zeros (necessary for non-'Team' callsigns)
name=name.lstrip('0')
# rprint("getNiceTeamName("+extTeamName+")")
# rprint("FirstNumIndex:"+str(firstNumIndex)+" Prefix:'"+prefix+"'")
# rprint("Human Readable Name:'"+name+"'")
return name
def getShortNiceTeamName(niceTeamName):
# 1. remove spaces, then prune leading 'Team'
shortNiceTeamName=niceTeamName.replace(' ','')
shortNiceTeamName=shortNiceTeamName.replace('Team','')
# 2. remove any leading zeros since this is only used for the tab label
shortNiceTeamName=shortNiceTeamName.lstrip('0')
return shortNiceTeamName
def getFileNameBase(root):
return root+"_"+time.strftime("%Y_%m_%d_%H%M%S")
###### LOGGING CODE BEGIN ######
# do not pass ERRORs to stdout - they already show up on the screen from stderr
class LoggingFilter(logging.Filter):
def filter(self,record):
return record.levelno < logging.ERROR
logFileName=getFileNameBase("radiolog_log")+".txt"
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
fh=logging.FileHandler(logFileName)
fh.setLevel(logging.INFO)
ch=logging.StreamHandler(stream=sys.stdout)
ch.setLevel(logging.INFO)
ch.addFilter(LoggingFilter())
logger.addHandler(fh)
logger.addHandler(ch)
# redirect stderr to stdout here by overriding excepthook
# from https://stackoverflow.com/a/16993115/3577105
# and https://www.programcreek.com/python/example/1013/sys.excepthook
def handle_exception(exc_type, exc_value, exc_traceback):
if not issubclass(exc_type, KeyboardInterrupt):
logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback))
sys.__excepthook__(exc_type, exc_value, exc_traceback)
# interesting that the program no longer exits after uncaught exceptions
# if this function replaces __excepthook__. Probably a good thing but
# it would be nice to undertstand why.
return
# note: 'sys.excepthook = handle_exception' must be done inside main()
def rprint(text):
logText=time.strftime("%H%M%S")+":"+str(text)
logger.info(logText)
###### LOGGING CODE END ######
# #471: rebuild any modified Qt .ui and .qrc files at runtime
# (copied from plans_console repo - could be made into a shared module at some point)
qtDesignerSubDir='designer' # subdir containing Qt Designer source files (*.ui)
qtUiPySubDir='ui' # target subdir for compiled ui files (*_ui.py)
qtQrcSubDir='.' # subdir containing Qt qrc resource files (*.qrc)
qtRcPySubDir='.' # target subdir for compiled resource files
installDir=os.path.dirname(os.path.realpath(__file__))
qtDesignerDir=os.path.join(installDir,qtDesignerSubDir)
qtUiPyDir=os.path.join(installDir,qtUiPySubDir)
qtQrcDir=os.path.join(installDir,qtQrcSubDir)
qtRcPyDir=os.path.join(installDir,qtRcPySubDir)
# rebuild all _ui.py files from .ui files in the same directory as this script as needed
# NOTE - this will overwrite any edits in _ui.py files
for ui in glob.glob(os.path.join(qtDesignerDir,'*.ui')):
uipy=os.path.join(qtUiPyDir,os.path.basename(ui).replace('.ui','_ui.py'))
if not (os.path.isfile(uipy) and os.path.getmtime(uipy) > os.path.getmtime(ui)):
cmd='pyuic5 -o '+uipy+' '+ui
rprint('Building GUI file from '+os.path.basename(ui)+':')
rprint(' '+cmd)
os.system(cmd)
# rebuild all _rc.py files from .qrc files in the same directory as this script as needed
# NOTE - this will overwrite any edits in _rc.py files
for qrc in glob.glob(os.path.join(qtQrcDir,'*.qrc')):
rcpy=os.path.join(qtRcPyDir,os.path.basename(qrc).replace('.qrc','_rc.py'))
if not (os.path.isfile(rcpy) and os.path.getmtime(rcpy) > os.path.getmtime(qrc)):
cmd='pyrcc5 -o '+rcpy+' '+qrc
rprint('Building Qt Resource file from '+os.path.basename(qrc)+':')
rprint(' '+cmd)
os.system(cmd)
from ui.help_ui import Ui_Help
from ui.options_ui import Ui_optionsDialog
from ui.fsSendDialog_ui import Ui_fsSendDialog
from ui.fsSendMessageDialog_ui import Ui_fsSendMessageDialog
from ui.newEntryWindow_ui import Ui_newEntryWindow
from ui.newEntryWidget_ui import Ui_newEntryWidget
from ui.clueDialog_ui import Ui_clueDialog
from ui.clueLogDialog_ui import Ui_clueLogDialog
from ui.printDialog_ui import Ui_printDialog
from ui.changeCallsignDialog_ui import Ui_changeCallsignDialog
from ui.fsFilterDialog_ui import Ui_fsFilterDialog
from ui.opPeriodDialog_ui import Ui_opPeriodDialog
from ui.printClueLogDialog_ui import Ui_printClueLogDialog
from ui.nonRadioClueDialog_ui import Ui_nonRadioClueDialog
from ui.subjectLocatedDialog_ui import Ui_subjectLocatedDialog
from ui.continuedIncidentDialog_ui import Ui_continuedIncidentDialog
# function to replace only the rightmost <occurrence> occurrences of <old> in <s> with <new>
# used by the undo function when adding new entry text
# credit to 'mg.' at http://stackoverflow.com/questions/2556108
def rreplace(s,old,new,occurrence):
li=s.rsplit(old,occurrence)
return new.join(li)
def normName(name):
return re.sub("[^A-Za-z0-9_]+","_",name)
class MyWindow(QDialog,Ui_Dialog):
def __init__(self,parent):
QDialog.__init__(self)
# create the local dir if it doesn't already exist, and populate it
# with files from local_default
if not os.path.isdir("local"):
rprint("'local' directory not found; copying 'local_default' to 'local'; you may want to edit local/radiolog.cfg")
shutil.copytree("local_default","local")
if not os.path.isfile("local/radiolog.cfg"):
rprint("'local' directory was found but did not contain radiolog.cfg; copying from local_default")
shutil.copyfile("local_default/radiolog.cfg","local/radiolog.cfg")
self.setWindowFlags(self.windowFlags()|Qt.WindowMinMaxButtonsHint)
self.parent=parent
self.ui=Ui_Dialog()
self.ui.setupUi(self)
self.setAttribute(Qt.WA_DeleteOnClose)
self.loadFlag=False # set this to true during load, to prevent save on each newEntry
self.totalEntryCount=0 # rotate backups after every 5 entries; see newEntryWidget.accept
self.ui.teamHotkeysWidget.setVisible(False) # disabled by default
self.hotkeyDict={}
self.nextAvailHotkeyIndex=0
self.hotkeyPool=["1","2","3","4","5","6","7","8","9","0","q","w","e","r","t","y","u","i","o","p","a","s","d","f","g","h","j","k","l","z","x","c","v","b","n","m"]
self.homeDir=os.path.expanduser("~")
# coordinate system name translation dictionary:
# key = ASCII name in the config file
# value = utf-8 name used in the rest of this code
self.csDisplayDict={}
self.csDisplayDict["UTM 5x5"]="UTM 5x5"
self.csDisplayDict["UTM 7x7"]="UTM 7x7"
self.csDisplayDict["D.d"]="D.d°"
self.csDisplayDict["DM.m"]="D° M.m'"
self.csDisplayDict["DMS.s"]="D° M' S.s\""
self.validDatumList=["WGS84","NAD27","NAD27 CONUS"]
self.timeoutDisplaySecList=[i[1] for i in timeoutDisplayList]
self.timeoutDisplayMinList=[int(i/60) for i in self.timeoutDisplaySecList if i>=60]
# fix #342 (focus-follows-mouse causes freezes) - disable FFM here;
# restore to initial setting on shutdown (note this would leave it
# disabled after unclean shutdown)
self.initialWindowTracking=False
try:
self.initialWindowTracking=win32gui.SystemParametersInfo(win32con.SPI_GETACTIVEWINDOWTRACKING)
except:
pass
if self.initialWindowTracking:
rprint("Window Tracking was initially enabled. Disabling it for radiolog; will re-enable on exit.")
win32gui.SystemParametersInfo(win32con.SPI_SETACTIVEWINDOWTRACKING,False)
self.firstWorkingDir=os.getenv('HOMEPATH','C:\\Users\\Default')+"\\Documents"
if self.firstWorkingDir[1]!=":":
self.firstWorkingDir=os.getenv('HOMEDRIVE','C:')+self.firstWorkingDir
# self.secondWorkingDir=os.getenv('HOMEPATH','C:\\Users\\Default')+"\\Documents\\sar"
self.optionsDialog=optionsDialog(self)
self.optionsDialog.accepted.connect(self.optionsAccepted)
# config file (e.g. ./local/radiolog.cfg) stores the team standards;
# it should be created/modified by hand, and is read at radiolog startup,
# and is not modified by radiolog at any point
# resource file / 'rc file' (e.g. ./radiolog_rc.txt) stores the search-specific
# options settings; it is read at radiolog startup, and is written
# whenever the options dialog is accepted
self.configFileName="./local/radiolog.cfg"
self.readConfigFile() # defaults are set inside readConfigFile
self.incidentName="New Incident"
self.incidentNameNormalized=normName(self.incidentName)
self.opPeriod=1
self.incidentStartDate=time.strftime("%a %b %d, %Y")
self.isContinuedIncident=False
self.printDialog=printDialog(self)
self.printClueLogDialog=printClueLogDialog(self)
# #483: Check for recent radiolog sessions on this computer. If any sessions are found from the previous 4 days,
# ask the operator if this new session is intended to be a continuation of one of those previous incidents.
# If so:
# - set the incident name to the same as that in the selected previous radiolog csv file;
# - set the OP number to one more than the latest OP# in the previous csv
# (with a reminder that OP# can be changed by hand by clicking the OP button);
# - set the next clue number to one more than the latest clue number in the previous CSV
# (with a reminder that clue# can be changed in the clue dialog the next time it is raised)
self.checkForContinuedIncident()
if self.isContinuedIncident:
rlInitText='Radio Log Begins - Continued incident "'+self.incidentName+'": Operational Period '+str(self.opPeriod)+' Begins: '
else:
rlInitText='Radio Log Begins: '
rlInitText+=self.incidentStartDate
if lastClueNumber>0:
rlInitText+=' (Last clue number: '+str(lastClueNumber)+')'
self.radioLog=[[time.strftime("%H%M"),'','',rlInitText,'','',time.time(),'','',''],
['','','','','','',1e10,'','','']] # 1e10 epoch seconds will keep the blank row at the bottom when sorted
self.clueLog=[]
self.clueLog.append(['',self.radioLog[0][3],'',time.strftime("%H%M"),'','','','',''])
self.lastFileName="" # to force error in restore, in the event the resource file doesn't specify the lastFileName
# self.csvFileName=getFileNameBase(self.incidentNameNormalized)+".csv"
# self.pdfFileName=getFileNameBase(self.incidentNameNormalized)+".pdf"
self.updateFileNames()
self.lastSavedFileName="NONE"
## self.fsFileName=self.getFileNameBase(self.incidentNameNormalized)+"_fleetsync.csv"
# disable fsValidFleetList checking to allow arbitrary fleets; this
# idea is probably obsolete
# self.fsValidFleetList=[100]
self.fsLog=[]
# self.fsLog.append(['','','','',''])
self.fsMuted=False
self.noSend=noSend
self.fsMutedBlink=False
self.fsFilterBlinkState=False
self.enableSendText=True
self.enablePollGPS=True
self.getString=""
## # attempt to change to the second working dir and back again, to 'wake up'
## # any mount points, to hopefully avoid problems of second working dir
## # not always being written to, at all, for a given run of this program;
## # os.chdir dies gracefully if the specified dir does not exist
## self.cwd=os.getcwd()
## rprint("t1")
## os.chdir(self.secondWorkingDir)
## rprint("t2")
## os.chdir(self.cwd)
## rprint("t3")
self.fsSendDialog=fsSendDialog(self)
self.fsSendList=[[]]
self.fsResponseMessage=''
self.sourceCRS=0
self.targetCRS=0
# set the default lookup name - this must be after readConfigFile
# since that function accepts the options form which updates the
# lookup filename based on the current incedent name and time
self.fsFileName="radiolog_fleetsync.csv"
self.helpFont1=QFont()
self.helpFont1.setFamily("Segoe UI")
self.helpFont1.setPointSize(9)
self.helpFont1.setStrikeOut(False)
self.helpFont2=QFont()
self.helpFont2.setFamily("Segoe UI")
self.helpFont2.setPointSize(9)
self.helpFont2.setStrikeOut(True)
self.helpWindow=helpWindow()
self.helpWindow.ui.hotkeysTableWidget.setColumnWidth(1,10)
self.helpWindow.ui.hotkeysTableWidget.setColumnWidth(0,145)
# note QHeaderView.setResizeMode is deprecated in 5.4, replaced with
# .setSectionResizeMode but also has both global and column-index forms
self.helpWindow.ui.hotkeysTableWidget.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
self.helpWindow.ui.hotkeysTableWidget.resizeRowsToContents()
self.helpWindow.ui.colorLabel1.setStyleSheet(statusStyleDict["At IC"])
self.helpWindow.ui.colorLabel2.setStyleSheet(statusStyleDict["In Transit"])
self.helpWindow.ui.colorLabel3.setStyleSheet(statusStyleDict["Working"])
self.helpWindow.ui.colorLabel4.setStyleSheet(statusStyleDict["Waiting for Transport"])
self.helpWindow.ui.colorLabel5.setStyleSheet(statusStyleDict["TIMED_OUT_ORANGE"])
self.helpWindow.ui.colorLabel6.setStyleSheet(statusStyleDict["TIMED_OUT_RED"])
self.helpWindow.ui.fsSomeFilteredLabel.setFont(self.helpFont1)
self.helpWindow.ui.fsAllFilteredLabel.setFont(self.helpFont2)
self.helpWindow.ui.fsSomeFilteredLabel.setStyleSheet(statusStyleDict["Working"])
self.helpWindow.ui.fsAllFilteredLabel.setStyleSheet(statusStyleDict["Working"])
self.radioLogNeedsPrint=False # set to True with each new log entry; set to False when printed
self.clueLogNeedsPrint=False
self.fsFilterDialog=fsFilterDialog(self)
self.fsFilterDialog.ui.tableView.setColumnWidth(0,50)
self.fsFilterDialog.ui.tableView.setColumnWidth(1,75)
self.fsBuildTooltip()
self.fsLatestComPort=None
self.fsShowChannelWarning=True
self.ui.addNonRadioClueButton.clicked.connect(self.addNonRadioClue)
self.ui.helpButton.clicked.connect(self.helpWindow.show)
self.ui.optionsButton.clicked.connect(self.optionsDialog.show)
self.ui.fsFilterButton.clicked.connect(self.fsFilterDialog.show)
self.ui.printButton.clicked.connect(self.printDialog.show)
## self.ui.printButton.clicked.connect(self.testConvertCoords)
self.ui.tabList=["dummy"]
self.ui.tabGridLayoutList=["dummy"]
self.ui.tableViewList=["dummy"]
self.proxyModelList=["dummy"]
self.teamNameList=["dummy"]
self.allTeamsList=["dummy"] # same as teamNameList but hidden tabs are not deleted from this list
self.extTeamNameList=["dummy"]
self.fsLookup=[]
## self.newEntryDialogList=[]
self.blinkToggle=0
self.fontSize=10
# font size is constrained to min and max for several items
self.minLimitedFontSize=8
self.maxLimitedFontSize=20
self.x=100
self.y=100
self.w=600
self.h=400
self.clueLog_x=200
self.clueLog_y=200
self.clueLog_w=1000
self.clueLog_h=400
self.firstComPortAlive=False
self.secondComPortAlive=False
self.firstComPortFound=False
self.secondComPortFound=False
self.firstComPort=None
self.secondComPort=None
self.comPortScanInProgress=False
self.comPortTryList=[]
## if develMode:
## self.comPortTryList=[serial.Serial("\\\\.\\CNCB0")] # DEVEL
self.fsBuffer=""
self.entryHold=False
self.currentEntryLastModAge=0
self.fsAwaitingResponse=None # used as a flag: [fleet,device,text,elapsed]
self.fsAwaitingResponseTimeout=8 # give up after this many seconds
self.opPeriodDialog=opPeriodDialog(self)
self.clueLogDialog=clueLogDialog(self)
self.ui.pushButton.clicked.connect(self.openNewEntry)
self.ui.opPeriodButton.clicked.connect(self.openOpPeriodDialog)
self.ui.clueLogButton.clicked.connect(self.clueLogDialog.show) # never actually close this dialog
self.ui.splitter.setSizes([250,150]) # any remainder is distributed based on this ratio
self.ui.splitter.splitterMoved.connect(self.ui.tableView.scrollToBottom)
self.tableModel = MyTableModel(self.radioLog, self)
self.ui.tableView.setModel(self.tableModel)
self.ui.tableView.setSelectionMode(QAbstractItemView.NoSelection)
self.ui.tableView.hideColumn(6) # hide epoch seconds
self.ui.tableView.hideColumn(7) # hide fleet
self.ui.tableView.hideColumn(8) # hide device
self.ui.tableView.hideColumn(9) # hide device
self.ui.tableView.resizeRowsToContents()
self.ui.tableView.setContextMenuPolicy(Qt.CustomContextMenu)
self.ui.tableView.customContextMenuRequested.connect(self.tableContextMenu)
#downside of the following line: slow, since it results in a resize call for each column,
# when we could defer and just do one resize at the end of all the resizes
## self.ui.tableView.horizontalHeader().sectionResized.connect(self.ui.tableView.resizeRowsToContents)
self.columnResizedFlag=False
self.ui.tableView.horizontalHeader().sectionResized.connect(self.setColumnResizedFlag)
self.exitClicked=False
# NOTE - the padding numbers for ::tab take a while to figure out in conjunction with
# the stylesheets of the label widgets of each tab in order to change status; don't
# change these numbers unless you want to spend a while on trial and error to get
# them looking good again!
self.ui.tabWidget.setStyleSheet("""
QTabBar::tab {
margin:0px;
padding-left:0px;
padding-right:0px;
padding-bottom:8px;
padding-top:0px;
border-top-left-radius:4px;
border-top-right-radius:4px;
border:1px solid gray;
font-size:20px;
}
QTabBar::tab:selected {
background:white;
border-bottom-color:white;
}
QTabBar::tab:!selected {
margin-top:3px;
}
QTabBar::tab:disabled {
width:20px;
color:black;
font-weight:bold;
background:transparent;
border:transparent;
padding-bottom:3px;
}
""")
#NOTE if you do this section before the model is assigned to the tableView,
# python will crash every time!
# see the QHeaderView.ResizeMode docs for descriptions of each resize mode value
# note QHeaderView.setResizeMode is deprecated in 5.4, replaced with
# .setSectionResizeMode but also has both global and column-index forms
# NOTE also - do NOT set any to ResizeToContents - this slows things down a lot!
# only resize when needed!
self.ui.tableView.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed)
# automatically expand the 'message' column width to fill available space
self.ui.tableView.horizontalHeader().setSectionResizeMode(3,QHeaderView.Stretch)
self.updateClock()
self.fsLoadLookup(startupFlag=True)
self.teamTimer=QTimer(self)
self.teamTimer.timeout.connect(self.updateTeamTimers)
self.teamTimer.timeout.connect(self.fsCheck)
self.teamTimer.timeout.connect(self.updateClock)
self.teamTimer.start(1000)
self.fastTimer=QTimer(self)
self.fastTimer.timeout.connect(self.resizeRowsToContentsIfNeeded)
self.fastTimer.start(100)
# self.ui.tabWidget.insertTab(0,QWidget(),'TEAMS:')
# ## self.ui.tabWidget.setStyleSheet("font-size:12px")
# self.ui.tabWidget.setTabEnabled(0,False)
# self.ui.teamHotkeysHLayout.insertWidget(0,QLabel("HOTKEYS:"))
self.ui.tabWidget.setContextMenuPolicy(Qt.CustomContextMenu)
self.ui.tabWidget.customContextMenuRequested.connect(self.tabContextMenu)
self.newEntryWindow=newEntryWindow(self) # create the window but don't show it until needed
# load resource file; process default values and resource file values
self.rcFileName="radiolog_rc.txt"
self.previousCleanShutdown=self.loadRcFile()
showStartupOptions=True
if self.isContinuedIncident:
showStartupOptions=False
if not self.previousCleanShutdown:
self.reallyRestore=QMessageBox(QMessageBox.Critical,"Restore last saved files?","The previous Radio Log session may have shut down incorrectly. Do you want to restore the last saved files (Radio Log, Clue Log, and FleetSync table)?",
QMessageBox.Yes|QMessageBox.No,self,Qt.WindowTitleHint|Qt.WindowCloseButtonHint|Qt.Dialog|Qt.MSWindowsFixedSizeDialogHint|Qt.WindowStaysOnTopHint)
self.reallyRestore.show()
self.reallyRestore.raise_()
if self.reallyRestore.exec_()==QMessageBox.Yes:
self.restore()
showStartupOptions=False
# make sure x/y/w/h from resource file will fit on the available display
d=QApplication.desktop()
if (self.x+self.w > d.width()) or (self.y+self.h > d.height()):
rprint("\nThe resource file specifies a main window geometry that is\n bigger than (or not on) the available desktop.\n Using default sizes for this session.\n\n")
self.x=50
self.y=50
self.w=d.availableGeometry(self).width()-100
self.h=d.availableGeometry(self).height()-100
if (self.clueLog_x+self.clueLog_w > d.width()) or (self.clueLog_y+self.clueLog_h > d.height()):
rprint("\nThe resource file specifies a clue log window geometry that is\n bigger than (or not on) the available desktop.\n Using default sizes for this session.\n\n")
self.clueLog_x=75
self.clueLog_y=75
self.clueLog_w=d.availableGeometry(self).width()-100
self.clueLog_h=d.availableGeometry(self).height()-100
self.setGeometry(int(self.x),int(self.y),int(self.w),int(self.h))
self.clueLogDialog.setGeometry(int(self.clueLog_x),int(self.clueLog_y),int(self.clueLog_w),int(self.clueLog_h))
self.fontsChanged()
self.ui.timeoutLabel.setText("TIMEOUT:\n"+timeoutDisplayList[self.optionsDialog.ui.timeoutField.value()][0])
# pop up the options dialog to enter the incident name right away
if showStartupOptions:
QTimer.singleShot(1000,self.startupOptions)
# save current resource file, to capture lastFileName without a clean shutdown
self.saveRcFile()
# Build a nested list of radiolog session data from any sessions in the last n days;
# each list element is [incident_name,last_op#,last_clue#,filename_base]
# then let the user choose from these, or choose to start a new incident
# - only show the most recent OP of each incident, i.e. don't show both OP2 and OP1
# - add a note that the user can change OP and next clue# afterwards from the GUI
def checkForContinuedIncident(self):
continuedIncidentWindowDays=4
continuedIncidentWindowSec=continuedIncidentWindowDays*24*60*60
csvFiles=glob.glob(self.firstWorkingDir+'/*.csv')
sortedCsvFiles=sorted(csvFiles,key=os.path.getmtime,reverse=True)
now=time.time()
choices=[]
opd={} # dictionary of most recent OP#'s per incident name
# rprint('Checking '+str(len(sortedCsvFiles))+' .csv files:')
for csv in sortedCsvFiles:
# rprint(' '+csv)
mtime=os.path.getmtime(csv)
age=now-mtime
ageStr=''
if age<3600:
ageStr='< 1 hour'
elif age<86400:
ageStr='< 1 day'
else:
ageDays=int(age/86400)
ageStr=str(ageDays)+' day'
if ageDays>1:
ageStr+='s'
if ageStr:
ageStr+=' ago'
if age<continuedIncidentWindowSec:
if self.isRadioLogDataFile(csv):