boris-behav-obs 8.9.16__py3-none-any.whl → 9.7.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of boris-behav-obs might be problematic. Click here for more details.
- boris/__init__.py +1 -1
- boris/__main__.py +1 -1
- boris/about.py +36 -39
- boris/add_modifier.py +122 -109
- boris/add_modifier_ui.py +239 -135
- boris/advanced_event_filtering.py +81 -45
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_latency.py +59 -0
- boris/analysis_plugins/irr_cohen_kappa.py +109 -0
- boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
- boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
- boris/analysis_plugins/number_of_occurences.py +22 -0
- boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
- boris/analysis_plugins/time_budget.py +61 -0
- boris/behav_coding_map_creator.py +228 -229
- boris/behavior_binary_table.py +33 -50
- boris/behaviors_coding_map.py +17 -18
- boris/boris_cli.py +6 -25
- boris/cmd_arguments.py +12 -1
- boris/coding_pad.py +42 -49
- boris/config.py +161 -77
- boris/config_file.py +63 -83
- boris/connections.py +112 -57
- boris/converters.py +13 -37
- boris/converters_ui.py +187 -110
- boris/cooccurence.py +250 -0
- boris/core.py +2511 -1824
- boris/core_qrc.py +15895 -10185
- boris/core_ui.py +946 -792
- boris/db_functions.py +21 -41
- boris/dev.py +134 -0
- boris/dialog.py +505 -244
- boris/duration_widget.py +15 -20
- boris/edit_event.py +84 -28
- boris/edit_event_ui.py +214 -78
- boris/event_operations.py +517 -415
- boris/events_cursor.py +25 -17
- boris/events_snapshots.py +36 -82
- boris/exclusion_matrix.py +4 -9
- boris/export_events.py +213 -583
- boris/export_observation.py +98 -611
- boris/external_processes.py +156 -97
- boris/geometric_measurement.py +652 -287
- boris/gui_utilities.py +91 -14
- boris/image_overlay.py +9 -9
- boris/import_observations.py +190 -98
- boris/ipc_mpv.py +325 -0
- boris/irr.py +26 -63
- boris/latency.py +34 -25
- boris/measurement_widget.py +14 -18
- boris/media_file.py +52 -84
- boris/menu_options.py +17 -6
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +7 -9
- boris/mpv.py +1 -0
- boris/mpv2.py +732 -705
- boris/observation.py +655 -310
- boris/observation_operations.py +1036 -404
- boris/observation_ui.py +584 -356
- boris/observations_list.py +71 -53
- boris/otx_parser.py +74 -80
- boris/param_panel.py +31 -16
- boris/param_panel_ui.py +254 -138
- boris/player_dock_widget.py +90 -60
- boris/plot_data_module.py +43 -46
- boris/plot_events.py +127 -90
- boris/plot_events_rt.py +17 -31
- boris/plot_spectrogram_rt.py +95 -30
- boris/plot_waveform_rt.py +32 -21
- boris/plugins.py +431 -0
- boris/portion/__init__.py +18 -8
- boris/portion/const.py +35 -18
- boris/portion/dict.py +5 -5
- boris/portion/func.py +2 -2
- boris/portion/interval.py +21 -41
- boris/portion/io.py +41 -32
- boris/preferences.py +306 -83
- boris/preferences_ui.py +685 -228
- boris/project.py +448 -293
- boris/project_functions.py +689 -254
- boris/project_import_export.py +213 -222
- boris/project_ui.py +674 -438
- boris/qrc_boris.py +6 -3
- boris/qrc_boris5.py +6 -3
- boris/select_modifiers.py +74 -48
- boris/select_observations.py +20 -199
- boris/select_subj_behav.py +67 -39
- boris/state_events.py +53 -37
- boris/subjects_pad.py +6 -9
- boris/synthetic_time_budget.py +45 -28
- boris/time_budget_functions.py +171 -171
- boris/time_budget_widget.py +84 -114
- boris/transitions.py +41 -47
- boris/utilities.py +766 -266
- boris/version.py +3 -3
- boris/video_equalizer.py +16 -14
- boris/video_equalizer_ui.py +199 -130
- boris/video_operations.py +125 -28
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.6.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
- {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
- boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
- boris/README.TXT +0 -22
- boris/add_modifier.ui +0 -323
- boris/boris_ui.py +0 -886
- boris/converters.ui +0 -289
- boris/core.qrc +0 -35
- boris/core.ui +0 -1543
- boris/edit_event.ui +0 -175
- boris/icons/logo_eye.ico +0 -0
- boris/map_creator.py +0 -850
- boris/observation.ui +0 -773
- boris/param_panel.ui +0 -379
- boris/preferences.ui +0 -537
- boris/project.ui +0 -1069
- boris/project_server.py +0 -236
- boris/vlc.py +0 -10343
- boris/vlc_local.py +0 -90
- boris_behav_obs-8.9.16.dist-info/LICENSE.TXT +0 -674
- boris_behav_obs-8.9.16.dist-info/METADATA +0 -129
- boris_behav_obs-8.9.16.dist-info/RECORD +0 -108
- boris_behav_obs-8.9.16.dist-info/entry_points.txt +0 -2
- {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
- {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/top_level.txt +0 -0
boris/event_operations.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
BORIS
|
|
3
3
|
Behavioral Observation Research Interactive Software
|
|
4
|
-
Copyright 2012-
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
This program is free software; you can redistribute it and/or modify
|
|
@@ -22,18 +22,25 @@ Copyright 2012-2023 Olivier Friard
|
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
24
|
import logging
|
|
25
|
+
import copy
|
|
26
|
+
import time
|
|
25
27
|
from decimal import Decimal as dec
|
|
26
28
|
from decimal import InvalidOperation
|
|
27
29
|
from decimal import ROUND_DOWN
|
|
30
|
+
from typing import Union
|
|
31
|
+
|
|
32
|
+
|
|
28
33
|
from . import config as cfg
|
|
29
34
|
from . import utilities as util
|
|
30
35
|
from . import dialog
|
|
31
36
|
from . import select_subj_behav
|
|
32
37
|
from . import select_modifiers
|
|
38
|
+
from . import write_event
|
|
33
39
|
from .edit_event import DlgEditEvent, EditSelectedEvents
|
|
34
40
|
|
|
35
|
-
from
|
|
36
|
-
from
|
|
41
|
+
from PySide6.QtWidgets import QMessageBox, QInputDialog, QLineEdit, QAbstractItemView, QApplication
|
|
42
|
+
from PySide6.QtCore import QTime, Qt
|
|
43
|
+
from PySide6.QtGui import QClipboard
|
|
37
44
|
|
|
38
45
|
|
|
39
46
|
def add_event(self):
|
|
@@ -47,11 +54,10 @@ def add_event(self):
|
|
|
47
54
|
|
|
48
55
|
if self.pause_before_addevent:
|
|
49
56
|
# pause media
|
|
50
|
-
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
self.pause_video()
|
|
57
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA and self.playerType == cfg.MEDIA:
|
|
58
|
+
memState = self.is_playing()
|
|
59
|
+
if memState:
|
|
60
|
+
self.pause_video()
|
|
55
61
|
|
|
56
62
|
if not self.pj[cfg.ETHOGRAM]:
|
|
57
63
|
QMessageBox.warning(self, cfg.programName, "The ethogram is not set!")
|
|
@@ -64,38 +70,56 @@ def add_event(self):
|
|
|
64
70
|
|
|
65
71
|
editWindow = DlgEditEvent(
|
|
66
72
|
observation_type=self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
|
|
67
|
-
time_value=
|
|
73
|
+
time_value=dec("NaN"),
|
|
74
|
+
image_idx=0,
|
|
68
75
|
current_time=current_time,
|
|
69
76
|
time_format=self.timeFormat,
|
|
70
77
|
show_set_current_time=True,
|
|
71
78
|
)
|
|
72
79
|
editWindow.setWindowTitle("Add a new event")
|
|
73
80
|
|
|
74
|
-
sortedSubjects = [
|
|
81
|
+
sortedSubjects = [cfg.NO_FOCAL_SUBJECT] + sorted([self.pj[cfg.SUBJECTS][x][cfg.SUBJECT_NAME] for x in self.pj[cfg.SUBJECTS]])
|
|
75
82
|
|
|
76
83
|
editWindow.cobSubject.addItems(sortedSubjects)
|
|
77
|
-
|
|
84
|
+
if self.currentSubject:
|
|
85
|
+
editWindow.cobSubject.setCurrentIndex(editWindow.cobSubject.findText(self.currentSubject, Qt.MatchFixedString))
|
|
78
86
|
|
|
79
87
|
sortedCodes = sorted([self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in self.pj[cfg.ETHOGRAM]])
|
|
80
88
|
|
|
81
89
|
editWindow.cobCode.addItems(sortedCodes)
|
|
82
90
|
|
|
83
91
|
if editWindow.exec_(): # button OK
|
|
84
|
-
|
|
85
92
|
# MEDIA / LIVE
|
|
86
93
|
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
|
|
87
94
|
newTime = editWindow.time_widget.get_time()
|
|
95
|
+
if newTime is None:
|
|
96
|
+
QMessageBox.warning(
|
|
97
|
+
self,
|
|
98
|
+
cfg.programName,
|
|
99
|
+
("Select a time format"),
|
|
100
|
+
)
|
|
101
|
+
return
|
|
88
102
|
|
|
89
103
|
for idx in self.pj[cfg.ETHOGRAM]:
|
|
90
104
|
if self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] == editWindow.cobCode.currentText():
|
|
91
|
-
|
|
92
105
|
event = self.full_event(idx)
|
|
93
106
|
|
|
94
|
-
event[cfg.SUBJECT] =
|
|
107
|
+
event[cfg.SUBJECT] = (
|
|
108
|
+
"" if editWindow.cobSubject.currentText() == cfg.NO_FOCAL_SUBJECT else editWindow.cobSubject.currentText()
|
|
109
|
+
)
|
|
95
110
|
if editWindow.leComment.toPlainText():
|
|
96
111
|
event[cfg.COMMENT] = editWindow.leComment.toPlainText()
|
|
97
112
|
|
|
98
|
-
|
|
113
|
+
# determine the frame index
|
|
114
|
+
if self.playerType == cfg.MEDIA:
|
|
115
|
+
mem_time = self.getLaps()
|
|
116
|
+
if not self.seek_mediaplayer(newTime):
|
|
117
|
+
time.sleep(0.1)
|
|
118
|
+
frame_idx = self.get_frame_index()
|
|
119
|
+
event[cfg.FRAME_INDEX] = frame_idx
|
|
120
|
+
self.seek_mediaplayer(mem_time)
|
|
121
|
+
|
|
122
|
+
write_event.write_event(self, event, newTime)
|
|
99
123
|
break
|
|
100
124
|
|
|
101
125
|
self.update_realtime_plot(force_plot=True)
|
|
@@ -122,31 +146,44 @@ def add_event(self):
|
|
|
122
146
|
|
|
123
147
|
# IMAGES
|
|
124
148
|
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
125
|
-
new_index = editWindow.
|
|
149
|
+
new_index = editWindow.sb_image_idx.value()
|
|
150
|
+
if new_index == 0:
|
|
151
|
+
QMessageBox.warning(self, cfg.programName, "The image index cannot be null")
|
|
152
|
+
return
|
|
126
153
|
|
|
127
154
|
for idx in self.pj[cfg.ETHOGRAM]:
|
|
128
155
|
if self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] == editWindow.cobCode.currentText():
|
|
129
|
-
|
|
130
156
|
event = self.full_event(idx)
|
|
131
157
|
|
|
132
|
-
event[cfg.SUBJECT] =
|
|
158
|
+
event[cfg.SUBJECT] = (
|
|
159
|
+
"" if editWindow.cobSubject.currentText() == cfg.NO_FOCAL_SUBJECT else editWindow.cobSubject.currentText()
|
|
160
|
+
)
|
|
133
161
|
if editWindow.leComment.toPlainText():
|
|
134
162
|
event[cfg.COMMENT] = editWindow.leComment.toPlainText()
|
|
135
163
|
|
|
136
|
-
|
|
164
|
+
if self.playerType != cfg.VIEWER_IMAGES:
|
|
165
|
+
event[cfg.IMAGE_PATH] = self.images_list[new_index]
|
|
166
|
+
else:
|
|
167
|
+
event[cfg.IMAGE_PATH] = ""
|
|
168
|
+
|
|
137
169
|
event[cfg.IMAGE_INDEX] = new_index
|
|
138
170
|
|
|
139
171
|
time_ = dec("NaN")
|
|
140
|
-
if (
|
|
141
|
-
self.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
172
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.USE_EXIF_DATE, False):
|
|
173
|
+
if self.playerType != cfg.VIEWER_IMAGES:
|
|
174
|
+
exif_date_time = util.extract_exif_DateTimeOriginal(self.images_list[new_index])
|
|
175
|
+
if exif_date_time != -1:
|
|
176
|
+
time_ = exif_date_time
|
|
177
|
+
|
|
178
|
+
# check if first value must be substracted
|
|
179
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.SUBSTRACT_FIRST_EXIF_DATE, True):
|
|
180
|
+
time_ -= self.image_time_ref
|
|
145
181
|
|
|
146
182
|
elif self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0):
|
|
147
183
|
time_ = new_index * self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0)
|
|
148
184
|
|
|
149
|
-
|
|
185
|
+
write_event.write_event(self, event, dec(time_).quantize(dec("0.001"), rounding=ROUND_DOWN))
|
|
186
|
+
|
|
150
187
|
break
|
|
151
188
|
|
|
152
189
|
if self.pause_before_addevent:
|
|
@@ -163,13 +200,28 @@ def find_events(self):
|
|
|
163
200
|
|
|
164
201
|
self.find_dialog = dialog.FindInEvents()
|
|
165
202
|
# list of rows to find
|
|
166
|
-
self.find_dialog.rowsToFind = set([item.row() for item in self.
|
|
203
|
+
self.find_dialog.rowsToFind = set([self.tv_idx2events_idx[item.row()] for item in self.tv_events.selectedIndexes()])
|
|
167
204
|
self.find_dialog.currentIdx = -1
|
|
168
205
|
self.find_dialog.clickSignal.connect(self.click_signal_find_in_events)
|
|
169
206
|
self.find_dialog.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
170
207
|
self.find_dialog.show()
|
|
171
208
|
|
|
172
209
|
|
|
210
|
+
def find_replace_events(self):
|
|
211
|
+
"""
|
|
212
|
+
find and replace in events
|
|
213
|
+
"""
|
|
214
|
+
fill_events_undo_list(self, "Undo Find/Replace operations")
|
|
215
|
+
self.find_replace_dialog = dialog.FindReplaceEvents()
|
|
216
|
+
self.find_replace_dialog.currentIdx = -1
|
|
217
|
+
self.find_replace_dialog.currentIdx_idx = -1
|
|
218
|
+
# list of rows to find/replace
|
|
219
|
+
self.find_replace_dialog.rowsToFind = set([self.tv_idx2events_idx[item.row()] for item in self.tv_events.selectedIndexes()])
|
|
220
|
+
self.find_replace_dialog.clickSignal.connect(self.click_signal_find_replace_in_events)
|
|
221
|
+
self.find_replace_dialog.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
222
|
+
self.find_replace_dialog.show()
|
|
223
|
+
|
|
224
|
+
|
|
173
225
|
def filter_events(self):
|
|
174
226
|
"""
|
|
175
227
|
filter coded events and subjects
|
|
@@ -178,11 +230,8 @@ def filter_events(self):
|
|
|
178
230
|
parameters = select_subj_behav.choose_obs_subj_behav_category(
|
|
179
231
|
self,
|
|
180
232
|
selected_observations=[], # empty selection of observations for selecting all subjects and behaviors
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
maxTime=None,
|
|
184
|
-
flagShowIncludeModifiers=False,
|
|
185
|
-
flagShowExcludeBehaviorsWoEvents=False,
|
|
233
|
+
show_include_modifiers=False,
|
|
234
|
+
show_exclude_non_coded_behaviors=False,
|
|
186
235
|
by_category=False,
|
|
187
236
|
)
|
|
188
237
|
if parameters == {}:
|
|
@@ -213,7 +262,10 @@ def fill_events_undo_list(self, operation_description: str) -> None:
|
|
|
213
262
|
"""
|
|
214
263
|
fill the undo events list for Undo function (CTRL + Z)
|
|
215
264
|
"""
|
|
216
|
-
|
|
265
|
+
logging.debug("fill_events_undo_list function")
|
|
266
|
+
|
|
267
|
+
self.undo_queue.append(copy.deepcopy(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]))
|
|
268
|
+
|
|
217
269
|
self.undo_description.append(operation_description)
|
|
218
270
|
|
|
219
271
|
self.actionUndo.setText(operation_description)
|
|
@@ -224,17 +276,22 @@ def fill_events_undo_list(self, operation_description: str) -> None:
|
|
|
224
276
|
if len(self.undo_queue) > cfg.MAX_UNDO_QUEUE:
|
|
225
277
|
self.undo_queue.popleft()
|
|
226
278
|
self.undo_description.popleft()
|
|
227
|
-
logging.debug(
|
|
279
|
+
logging.debug("Max events undo ")
|
|
228
280
|
|
|
229
281
|
|
|
230
282
|
def undo_event_operation(self) -> None:
|
|
231
283
|
"""
|
|
232
284
|
undo operation on event(s)
|
|
233
285
|
"""
|
|
286
|
+
|
|
287
|
+
logging.debug("Undo event operation function")
|
|
288
|
+
|
|
234
289
|
if len(self.undo_queue) == 0:
|
|
235
|
-
self.statusbar.showMessage(
|
|
290
|
+
self.statusbar.showMessage("The Undo buffer is empty", 5000)
|
|
236
291
|
return
|
|
292
|
+
|
|
237
293
|
events = self.undo_queue.pop()
|
|
294
|
+
|
|
238
295
|
operation_description = self.undo_description.pop()
|
|
239
296
|
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = events[:]
|
|
240
297
|
self.project_changed()
|
|
@@ -264,66 +321,26 @@ def delete_all_events(self):
|
|
|
264
321
|
self.no_observation()
|
|
265
322
|
return
|
|
266
323
|
|
|
267
|
-
if not self.
|
|
324
|
+
if not self.tv_idx2events_idx:
|
|
268
325
|
QMessageBox.warning(self, cfg.programName, "No events to delete")
|
|
269
326
|
return
|
|
270
327
|
|
|
271
328
|
if (
|
|
272
329
|
dialog.MessageDialog(
|
|
273
330
|
cfg.programName,
|
|
274
|
-
("Confirm the deletion of all (filtered) events in the current observation?<br>
|
|
331
|
+
("Confirm the deletion of all (filtered) events in the current observation?<br>Filters do not apply!"),
|
|
275
332
|
[cfg.YES, cfg.NO],
|
|
276
333
|
)
|
|
277
334
|
== cfg.YES
|
|
278
335
|
):
|
|
279
|
-
|
|
280
336
|
# fill the undo list
|
|
281
337
|
fill_events_undo_list(self, "Undo 'Delete all events'")
|
|
282
338
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
for
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
util.time2seconds(self.twEvents.item(row, cfg.TW_OBS_FIELD[self.playerType]["time"]).text())
|
|
289
|
-
if self.timeFormat == cfg.HHMMSS
|
|
290
|
-
else dec(self.twEvents.item(row, cfg.TW_OBS_FIELD[self.playerType]["time"]).text()),
|
|
291
|
-
self.twEvents.item(row, cfg.TW_OBS_FIELD[self.playerType][cfg.SUBJECT]).text(),
|
|
292
|
-
self.twEvents.item(row, cfg.TW_OBS_FIELD[self.playerType][cfg.BEHAVIOR_CODE]).text(),
|
|
293
|
-
]
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = [
|
|
297
|
-
event
|
|
298
|
-
for event in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]
|
|
299
|
-
if [
|
|
300
|
-
event[cfg.PJ_OBS_FIELDS[self.playerType]["time"]],
|
|
301
|
-
event[cfg.PJ_OBS_FIELDS[self.playerType][cfg.SUBJECT]],
|
|
302
|
-
event[cfg.PJ_OBS_FIELDS[self.playerType][cfg.BEHAVIOR_CODE]],
|
|
303
|
-
]
|
|
304
|
-
not in rows_to_delete
|
|
305
|
-
]
|
|
306
|
-
|
|
307
|
-
if self.playerType in (cfg.IMAGES, cfg.VIEWER_IMAGES):
|
|
308
|
-
for row in range(self.twEvents.rowCount()):
|
|
309
|
-
rows_to_delete.append(
|
|
310
|
-
[
|
|
311
|
-
self.twEvents.item(row, cfg.TW_OBS_FIELD[self.playerType][cfg.SUBJECT]).text(),
|
|
312
|
-
self.twEvents.item(row, cfg.TW_OBS_FIELD[self.playerType][cfg.BEHAVIOR_CODE]).text(),
|
|
313
|
-
int(self.twEvents.item(row, cfg.TW_OBS_FIELD[self.playerType][cfg.IMAGE_INDEX]).text()),
|
|
314
|
-
]
|
|
315
|
-
)
|
|
316
|
-
|
|
317
|
-
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = [
|
|
318
|
-
event
|
|
319
|
-
for event in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]
|
|
320
|
-
if [
|
|
321
|
-
event[cfg.PJ_OBS_FIELDS[self.playerType][cfg.SUBJECT]],
|
|
322
|
-
event[cfg.PJ_OBS_FIELDS[self.playerType][cfg.BEHAVIOR_CODE]],
|
|
323
|
-
event[cfg.PJ_OBS_FIELDS[self.playerType][cfg.IMAGE_INDEX]],
|
|
324
|
-
]
|
|
325
|
-
not in rows_to_delete
|
|
326
|
-
]
|
|
339
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = [
|
|
340
|
+
event
|
|
341
|
+
for event_idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS])
|
|
342
|
+
if event_idx not in self.tv_idx2events_idx
|
|
343
|
+
]
|
|
327
344
|
|
|
328
345
|
self.update_realtime_plot(force_plot=True)
|
|
329
346
|
|
|
@@ -342,57 +359,22 @@ def delete_selected_events(self):
|
|
|
342
359
|
|
|
343
360
|
logging.debug("begin function delete_selected_events")
|
|
344
361
|
|
|
345
|
-
if not self.
|
|
362
|
+
if not self.tv_events.selectedIndexes():
|
|
346
363
|
QMessageBox.warning(self, cfg.programName, "No event selected!")
|
|
347
364
|
else:
|
|
348
365
|
# list of rows to delete (set for unique)
|
|
349
366
|
# fill the undo list
|
|
350
367
|
fill_events_undo_list(self, "Undo 'Delete selected events'")
|
|
351
368
|
|
|
352
|
-
rows_to_delete = []
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
rows_to_delete.append(
|
|
356
|
-
[
|
|
357
|
-
util.time2seconds(self.twEvents.item(row, cfg.EVENT_TIME_FIELD_IDX).text())
|
|
358
|
-
if self.timeFormat == cfg.HHMMSS
|
|
359
|
-
else dec(self.twEvents.item(row, cfg.EVENT_TIME_FIELD_IDX).text()),
|
|
360
|
-
self.twEvents.item(row, cfg.EVENT_SUBJECT_FIELD_IDX).text(),
|
|
361
|
-
self.twEvents.item(row, cfg.EVENT_BEHAVIOR_FIELD_IDX).text(),
|
|
362
|
-
]
|
|
363
|
-
)
|
|
369
|
+
rows_to_delete: list = []
|
|
370
|
+
for row in set([item.row() for item in self.tv_events.selectedIndexes()]):
|
|
371
|
+
rows_to_delete.append(self.tv_idx2events_idx[row])
|
|
364
372
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
event[cfg.PJ_OBS_FIELDS[self.playerType][cfg.SUBJECT]],
|
|
371
|
-
event[cfg.PJ_OBS_FIELDS[self.playerType][cfg.BEHAVIOR_CODE]],
|
|
372
|
-
]
|
|
373
|
-
not in rows_to_delete
|
|
374
|
-
]
|
|
375
|
-
|
|
376
|
-
if self.playerType in (cfg.IMAGES, cfg.VIEWER_IMAGES):
|
|
377
|
-
for row in set([item.row() for item in self.twEvents.selectedIndexes()]):
|
|
378
|
-
rows_to_delete.append(
|
|
379
|
-
[
|
|
380
|
-
self.twEvents.item(row, cfg.TW_OBS_FIELD[self.playerType][cfg.SUBJECT]).text(),
|
|
381
|
-
self.twEvents.item(row, cfg.TW_OBS_FIELD[self.playerType][cfg.BEHAVIOR_CODE]).text(),
|
|
382
|
-
int(self.twEvents.item(row, cfg.TW_OBS_FIELD[self.playerType][cfg.IMAGE_INDEX]).text()),
|
|
383
|
-
]
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = [
|
|
387
|
-
event
|
|
388
|
-
for event in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]
|
|
389
|
-
if [
|
|
390
|
-
event[cfg.PJ_OBS_FIELDS[self.playerType][cfg.SUBJECT]],
|
|
391
|
-
event[cfg.PJ_OBS_FIELDS[self.playerType][cfg.BEHAVIOR_CODE]],
|
|
392
|
-
event[cfg.PJ_OBS_FIELDS[self.playerType][cfg.IMAGE_INDEX]],
|
|
393
|
-
]
|
|
394
|
-
not in rows_to_delete
|
|
395
|
-
]
|
|
373
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = [
|
|
374
|
+
event
|
|
375
|
+
for event_idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS])
|
|
376
|
+
if event_idx not in rows_to_delete
|
|
377
|
+
]
|
|
396
378
|
|
|
397
379
|
self.update_realtime_plot(force_plot=True)
|
|
398
380
|
|
|
@@ -423,50 +405,108 @@ def select_events_between_activated(self):
|
|
|
423
405
|
return None
|
|
424
406
|
return timeSeconds
|
|
425
407
|
|
|
426
|
-
if self.
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
)
|
|
408
|
+
if not self.tv_idx2events_idx:
|
|
409
|
+
QMessageBox.warning(self, cfg.programName, "There are no events to select")
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
text, ok = QInputDialog.getText(
|
|
413
|
+
self,
|
|
414
|
+
"Select events in time interval",
|
|
415
|
+
"Interval: (example: 12.5-14.7 or 02:45.780-03:15.120)",
|
|
416
|
+
QLineEdit.Normal,
|
|
417
|
+
"",
|
|
418
|
+
)
|
|
434
419
|
|
|
435
|
-
|
|
420
|
+
if ok and text != "":
|
|
421
|
+
if "-" not in text:
|
|
422
|
+
QMessageBox.critical(self, cfg.programName, "Use minus sign (-) to separate initial value from final value")
|
|
423
|
+
return
|
|
436
424
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
425
|
+
while " " in text:
|
|
426
|
+
text = text.replace(" ", "")
|
|
427
|
+
|
|
428
|
+
from_, to_ = text.split("-")[0:2]
|
|
429
|
+
from_sec = parseTime(from_)
|
|
430
|
+
if not from_sec:
|
|
431
|
+
QMessageBox.critical(self, cfg.programName, f"Time value not recognized: {from_}")
|
|
432
|
+
return
|
|
433
|
+
to_sec = parseTime(to_)
|
|
434
|
+
if not to_sec:
|
|
435
|
+
QMessageBox.critical(self, cfg.programName, f"Time value not recognized: {to_}")
|
|
436
|
+
return
|
|
437
|
+
if to_sec < from_sec:
|
|
438
|
+
QMessageBox.critical(self, cfg.programName, "The initial time is greater than the final time")
|
|
439
|
+
return
|
|
442
440
|
|
|
443
|
-
|
|
444
|
-
|
|
441
|
+
self.tv_events.clearSelection()
|
|
442
|
+
self.tv_events.setSelectionMode(QAbstractItemView.MultiSelection)
|
|
445
443
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
if not to_sec:
|
|
453
|
-
QMessageBox.critical(self, cfg.programName, f"Time value not recognized: {to_}")
|
|
454
|
-
return
|
|
455
|
-
if to_sec < from_sec:
|
|
456
|
-
QMessageBox.critical(self, cfg.programName, "The initial time is greater than the final time")
|
|
457
|
-
return
|
|
458
|
-
self.twEvents.clearSelection()
|
|
459
|
-
self.twEvents.setSelectionMode(QAbstractItemView.MultiSelection)
|
|
460
|
-
for r in range(0, self.twEvents.rowCount()):
|
|
461
|
-
if ":" in self.twEvents.item(r, 0).text():
|
|
462
|
-
time = util.time2seconds(self.twEvents.item(r, 0).text())
|
|
463
|
-
else:
|
|
464
|
-
time = dec(self.twEvents.item(r, 0).text())
|
|
465
|
-
if from_sec <= time <= to_sec:
|
|
466
|
-
self.twEvents.selectRow(r)
|
|
444
|
+
# for r in range(self.tv_events.rowCount()):
|
|
445
|
+
# for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
|
|
446
|
+
for tv_idx in range(len(self.tv_idx2events_idx)):
|
|
447
|
+
time = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][self.tv_idx2events_idx[tv_idx]][
|
|
448
|
+
cfg.PJ_OBS_FIELDS[self.playerType][cfg.TIME]
|
|
449
|
+
]
|
|
467
450
|
|
|
451
|
+
if from_sec <= time <= to_sec:
|
|
452
|
+
self.tv_events.selectRow(tv_idx)
|
|
453
|
+
|
|
454
|
+
self.tv_events.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def add_comment(self):
|
|
458
|
+
"""
|
|
459
|
+
add a comment to the selected events
|
|
460
|
+
operation can be undone with Undo
|
|
461
|
+
"""
|
|
462
|
+
tvevents_rows_to_edit = set([index.row() for index in self.tv_events.selectionModel().selectedIndexes()])
|
|
463
|
+
if not len(tvevents_rows_to_edit):
|
|
464
|
+
QMessageBox.warning(self, cfg.programName, "No event selected!")
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
comment_str: str = ""
|
|
468
|
+
if len(tvevents_rows_to_edit) == 1:
|
|
469
|
+
pj_event_idx = self.tv_idx2events_idx[self.tv_events.selectionModel().selectedIndexes()[0].row()]
|
|
470
|
+
comment_str = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
|
|
471
|
+
cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.COMMENT]
|
|
472
|
+
]
|
|
468
473
|
else:
|
|
469
|
-
|
|
474
|
+
# check if comment is the same in all selected events
|
|
475
|
+
|
|
476
|
+
if (
|
|
477
|
+
len(
|
|
478
|
+
set(
|
|
479
|
+
[
|
|
480
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][self.tv_idx2events_idx[tvevents_row]][
|
|
481
|
+
cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.COMMENT]
|
|
482
|
+
]
|
|
483
|
+
for tvevents_row in tvevents_rows_to_edit
|
|
484
|
+
]
|
|
485
|
+
)
|
|
486
|
+
)
|
|
487
|
+
== 1
|
|
488
|
+
):
|
|
489
|
+
pj_event_idx = self.tv_idx2events_idx[self.tv_events.selectionModel().selectedIndexes()[0].row()]
|
|
490
|
+
comment_str = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
|
|
491
|
+
cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.COMMENT]
|
|
492
|
+
]
|
|
493
|
+
|
|
494
|
+
new_comment, ok = QInputDialog.getText(self, "Add/Edit a comment", "Comment:", text=comment_str)
|
|
495
|
+
if not ok:
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
# fill the undo list
|
|
499
|
+
fill_events_undo_list(self, "Undo last comment operation")
|
|
500
|
+
|
|
501
|
+
for tvevents_row in tvevents_rows_to_edit:
|
|
502
|
+
pj_event_idx = self.tv_idx2events_idx[tvevents_row]
|
|
503
|
+
|
|
504
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
|
|
505
|
+
cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.COMMENT]
|
|
506
|
+
] = new_comment
|
|
507
|
+
|
|
508
|
+
# reload all events in tw
|
|
509
|
+
self.load_tw_events(self.observationId)
|
|
470
510
|
|
|
471
511
|
|
|
472
512
|
def edit_selected_events(self):
|
|
@@ -474,56 +514,46 @@ def edit_selected_events(self):
|
|
|
474
514
|
edit one or more selected events for subject, behavior and/or comment
|
|
475
515
|
"""
|
|
476
516
|
# list of rows to edit
|
|
477
|
-
twEvents_rows_to_edit = set([item.row() for item in self.twEvents.selectedIndexes()])
|
|
478
517
|
|
|
479
|
-
|
|
518
|
+
tvevents_rows_to_edit = set([index.row() for index in self.tv_events.selectionModel().selectedIndexes()])
|
|
519
|
+
|
|
520
|
+
if not len(tvevents_rows_to_edit):
|
|
480
521
|
QMessageBox.warning(self, cfg.programName, "No event selected!")
|
|
481
|
-
|
|
522
|
+
|
|
523
|
+
elif len(tvevents_rows_to_edit) == 1: # 1 event selected
|
|
482
524
|
edit_event(self)
|
|
525
|
+
|
|
483
526
|
else: # editing of more events
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
[self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in self.pj[cfg.ETHOGRAM]]
|
|
487
|
-
)
|
|
527
|
+
dialog_window = EditSelectedEvents()
|
|
528
|
+
dialog_window.all_behaviors = sorted([self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in self.pj[cfg.ETHOGRAM]])
|
|
488
529
|
|
|
489
|
-
|
|
490
|
-
self.pj[cfg.SUBJECTS][str(k)][cfg.SUBJECT_NAME]
|
|
491
|
-
for k in sorted([int(x) for x in self.pj[cfg.SUBJECTS].keys()])
|
|
530
|
+
dialog_window.all_subjects = [cfg.NO_FOCAL_SUBJECT] + [
|
|
531
|
+
self.pj[cfg.SUBJECTS][str(k)][cfg.SUBJECT_NAME] for k in sorted([int(x) for x in self.pj[cfg.SUBJECTS].keys()])
|
|
492
532
|
]
|
|
493
533
|
|
|
494
|
-
if
|
|
495
|
-
|
|
534
|
+
if dialog_window.exec_():
|
|
496
535
|
# fill the undo list
|
|
497
536
|
fill_events_undo_list(self, "Undo 'Edit selected event(s)'")
|
|
498
537
|
|
|
499
|
-
tsb_to_edit = []
|
|
500
|
-
for row in
|
|
501
|
-
tsb_to_edit.append(
|
|
502
|
-
[
|
|
503
|
-
util.time2seconds(self.twEvents.item(row, cfg.EVENT_TIME_FIELD_IDX).text())
|
|
504
|
-
if self.timeFormat == cfg.HHMMSS
|
|
505
|
-
else dec(self.twEvents.item(row, cfg.EVENT_TIME_FIELD_IDX).text()),
|
|
506
|
-
self.twEvents.item(row, cfg.EVENT_SUBJECT_FIELD_IDX).text(),
|
|
507
|
-
self.twEvents.item(row, cfg.EVENT_BEHAVIOR_FIELD_IDX).text(),
|
|
508
|
-
]
|
|
509
|
-
)
|
|
538
|
+
tsb_to_edit: list = []
|
|
539
|
+
for row in tvevents_rows_to_edit:
|
|
540
|
+
tsb_to_edit.append(self.tv_idx2events_idx[row])
|
|
510
541
|
|
|
511
|
-
behavior_codes = []
|
|
512
|
-
modifiers_mem = []
|
|
513
|
-
mem_event_idx = []
|
|
542
|
+
behavior_codes: list = []
|
|
543
|
+
modifiers_mem: list = []
|
|
544
|
+
mem_event_idx: list = []
|
|
514
545
|
for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
|
|
515
|
-
if
|
|
516
|
-
event[cfg.EVENT_TIME_FIELD_IDX],
|
|
517
|
-
event[cfg.EVENT_SUBJECT_FIELD_IDX],
|
|
518
|
-
event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
|
|
519
|
-
] in tsb_to_edit:
|
|
546
|
+
if idx in tsb_to_edit:
|
|
520
547
|
new_event = list(event)
|
|
521
|
-
if
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
548
|
+
if dialog_window.rbSubject.isChecked():
|
|
549
|
+
if dialog_window.newText.selectedItems()[0].text() == cfg.NO_FOCAL_SUBJECT:
|
|
550
|
+
new_event[cfg.EVENT_SUBJECT_FIELD_IDX] = ""
|
|
551
|
+
else:
|
|
552
|
+
new_event[cfg.EVENT_SUBJECT_FIELD_IDX] = dialog_window.newText.selectedItems()[0].text()
|
|
553
|
+
if dialog_window.rbBehavior.isChecked():
|
|
554
|
+
new_event[cfg.EVENT_BEHAVIOR_FIELD_IDX] = dialog_window.newText.selectedItems()[0].text()
|
|
555
|
+
if dialog_window.rbComment.isChecked():
|
|
556
|
+
new_event[cfg.EVENT_COMMENT_FIELD_IDX] = dialog_window.commentText.text()
|
|
527
557
|
|
|
528
558
|
if new_event[cfg.EVENT_BEHAVIOR_FIELD_IDX] not in behavior_codes:
|
|
529
559
|
behavior_codes.append(new_event[cfg.EVENT_BEHAVIOR_FIELD_IDX])
|
|
@@ -537,7 +567,6 @@ def edit_selected_events(self):
|
|
|
537
567
|
|
|
538
568
|
# check if behavior is unique for editing modifiers
|
|
539
569
|
if len(behavior_codes) == 1:
|
|
540
|
-
|
|
541
570
|
# get behavior index
|
|
542
571
|
for idx in self.pj[cfg.ETHOGRAM]:
|
|
543
572
|
if self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] == behavior_codes[0]:
|
|
@@ -553,9 +582,7 @@ def edit_selected_events(self):
|
|
|
553
582
|
current_modifier = ""
|
|
554
583
|
|
|
555
584
|
if event["modifiers"]:
|
|
556
|
-
modifiers_selector = select_modifiers.ModifiersList(
|
|
557
|
-
behavior_codes[0], eval(str(event["modifiers"])), current_modifier
|
|
558
|
-
)
|
|
585
|
+
modifiers_selector = select_modifiers.ModifiersList(behavior_codes[0], eval(str(event["modifiers"])), current_modifier)
|
|
559
586
|
|
|
560
587
|
r = modifiers_selector.exec_()
|
|
561
588
|
if r:
|
|
@@ -576,9 +603,7 @@ def edit_selected_events(self):
|
|
|
576
603
|
# set new modifier
|
|
577
604
|
for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
|
|
578
605
|
if idx in mem_event_idx:
|
|
579
|
-
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][idx][
|
|
580
|
-
cfg.EVENT_MODIFIER_FIELD_IDX
|
|
581
|
-
] = modifier_str
|
|
606
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][idx][cfg.EVENT_MODIFIER_FIELD_IDX] = modifier_str
|
|
582
607
|
|
|
583
608
|
self.load_tw_events(self.observationId)
|
|
584
609
|
|
|
@@ -587,165 +612,191 @@ def edit_selected_events(self):
|
|
|
587
612
|
|
|
588
613
|
def edit_event(self):
|
|
589
614
|
"""
|
|
590
|
-
edit event corresponding to the selected row in
|
|
615
|
+
edit event corresponding to the selected row in tv_events
|
|
591
616
|
"""
|
|
592
617
|
|
|
593
618
|
if not self.observationId:
|
|
594
619
|
self.no_observation()
|
|
595
620
|
return
|
|
596
621
|
|
|
597
|
-
if not self.
|
|
622
|
+
if not self.tv_events.selectionModel().selectedIndexes():
|
|
598
623
|
QMessageBox.warning(self, cfg.programName, "Select an event to edit")
|
|
599
624
|
return
|
|
600
625
|
|
|
601
|
-
|
|
626
|
+
if self.pause_before_addevent:
|
|
627
|
+
# pause media
|
|
628
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA and self.playerType == cfg.MEDIA:
|
|
629
|
+
player_mem_state = self.is_playing()
|
|
630
|
+
if player_mem_state:
|
|
631
|
+
self.pause_video()
|
|
602
632
|
|
|
603
|
-
|
|
604
|
-
tsb_to_edit = [
|
|
605
|
-
util.time2seconds(self.twEvents.item(twEvents_row, cfg.EVENT_TIME_FIELD_IDX).text())
|
|
606
|
-
if self.timeFormat == cfg.HHMMSS
|
|
607
|
-
else dec(self.twEvents.item(twEvents_row, cfg.EVENT_TIME_FIELD_IDX).text()),
|
|
608
|
-
self.twEvents.item(twEvents_row, cfg.EVENT_SUBJECT_FIELD_IDX).text(),
|
|
609
|
-
self.twEvents.item(twEvents_row, cfg.EVENT_BEHAVIOR_FIELD_IDX).text(),
|
|
610
|
-
]
|
|
633
|
+
tvevents_row = self.tv_events.selectionModel().selectedIndexes()[0].row()
|
|
611
634
|
|
|
612
|
-
|
|
613
|
-
idx
|
|
614
|
-
for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS])
|
|
615
|
-
if [
|
|
616
|
-
event[cfg.EVENT_TIME_FIELD_IDX],
|
|
617
|
-
event[cfg.EVENT_SUBJECT_FIELD_IDX],
|
|
618
|
-
event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
|
|
619
|
-
]
|
|
620
|
-
== tsb_to_edit
|
|
621
|
-
][0]
|
|
622
|
-
|
|
623
|
-
if self.playerType in (cfg.IMAGES, cfg.VIEWER_IMAGES):
|
|
624
|
-
tsb_to_edit = [
|
|
625
|
-
self.twEvents.item(twEvents_row, cfg.TW_OBS_FIELD[self.playerType][cfg.SUBJECT]).text(),
|
|
626
|
-
self.twEvents.item(twEvents_row, cfg.TW_OBS_FIELD[self.playerType][cfg.BEHAVIOR_CODE]).text(),
|
|
627
|
-
int(self.twEvents.item(twEvents_row, cfg.TW_OBS_FIELD[self.playerType][cfg.IMAGE_INDEX]).text()),
|
|
628
|
-
]
|
|
635
|
+
pj_event_idx = self.tv_idx2events_idx[tvevents_row]
|
|
629
636
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
if [
|
|
634
|
-
event[cfg.PJ_OBS_FIELDS[self.playerType][cfg.SUBJECT]],
|
|
635
|
-
event[cfg.PJ_OBS_FIELDS[self.playerType][cfg.BEHAVIOR_CODE]],
|
|
636
|
-
event[cfg.PJ_OBS_FIELDS[self.playerType][cfg.IMAGE_INDEX]],
|
|
637
|
-
]
|
|
638
|
-
== tsb_to_edit
|
|
639
|
-
][0]
|
|
637
|
+
time_value = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
|
|
638
|
+
cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.TIME]
|
|
639
|
+
]
|
|
640
640
|
|
|
641
|
+
image_idx = None
|
|
641
642
|
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.IMAGES):
|
|
642
|
-
|
|
643
|
+
image_idx = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
|
|
643
644
|
cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.IMAGE_INDEX]
|
|
644
645
|
]
|
|
645
|
-
elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.LIVE, cfg.MEDIA):
|
|
646
|
-
value = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][row][
|
|
647
|
-
cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]]["time"]
|
|
648
|
-
]
|
|
649
|
-
else:
|
|
650
|
-
QMessageBox.warning(
|
|
651
|
-
self,
|
|
652
|
-
cfg.programName,
|
|
653
|
-
f"Observation type {self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]} not recognized",
|
|
654
|
-
)
|
|
655
|
-
return
|
|
656
646
|
|
|
657
647
|
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
658
|
-
|
|
648
|
+
current_value = self.image_idx + 1
|
|
659
649
|
else:
|
|
660
|
-
|
|
650
|
+
current_value = self.getLaps()
|
|
661
651
|
|
|
662
|
-
|
|
652
|
+
# get exif date time
|
|
653
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.USE_EXIF_DATE, False):
|
|
654
|
+
exif_date_time = util.extract_exif_DateTimeOriginal(self.images_list[self.image_idx])
|
|
655
|
+
else:
|
|
656
|
+
exif_date_time = None
|
|
657
|
+
|
|
658
|
+
edit_window = DlgEditEvent(
|
|
663
659
|
observation_type=self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
|
|
664
|
-
time_value=
|
|
665
|
-
|
|
660
|
+
time_value=time_value,
|
|
661
|
+
image_idx=image_idx,
|
|
662
|
+
current_time=current_value,
|
|
666
663
|
time_format=self.timeFormat,
|
|
667
664
|
show_set_current_time=True,
|
|
665
|
+
exif_date_time=exif_date_time,
|
|
668
666
|
)
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
if
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
667
|
+
edit_window.setWindowTitle("Edit event")
|
|
668
|
+
|
|
669
|
+
# time
|
|
670
|
+
if time_value.is_nan():
|
|
671
|
+
edit_window.cb_set_time_na.setChecked(True)
|
|
672
|
+
|
|
673
|
+
# remove visibility of 'set current time' widget if VIEWER mode
|
|
674
|
+
if self.playerType in (cfg.VIEWER_MEDIA, cfg.VIEWER_LIVE, cfg.VIEWER_IMAGES):
|
|
675
|
+
edit_window.pb_set_to_current_time.setVisible(False)
|
|
676
|
+
|
|
677
|
+
# subjects
|
|
678
|
+
sorted_subjects = [cfg.NO_FOCAL_SUBJECT] + sorted([self.pj[cfg.SUBJECTS][x][cfg.SUBJECT_NAME] for x in self.pj[cfg.SUBJECTS]])
|
|
679
|
+
edit_window.cobSubject.addItems(sorted_subjects)
|
|
680
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_SUBJECT_FIELD_IDX] == "": # no focal subject
|
|
681
|
+
edit_window.cobSubject.setCurrentIndex(sorted_subjects.index(cfg.NO_FOCAL_SUBJECT))
|
|
682
|
+
elif self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_SUBJECT_FIELD_IDX] in sorted_subjects:
|
|
683
|
+
edit_window.cobSubject.setCurrentIndex(
|
|
684
|
+
sorted_subjects.index(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_SUBJECT_FIELD_IDX])
|
|
680
685
|
)
|
|
681
686
|
else:
|
|
682
687
|
QMessageBox.warning(
|
|
683
688
|
self,
|
|
684
689
|
cfg.programName,
|
|
685
690
|
(
|
|
686
|
-
|
|
691
|
+
"The subject "
|
|
692
|
+
f"<b>{self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_SUBJECT_FIELD_IDX]}</b> "
|
|
687
693
|
"does not exist more in the subject's list"
|
|
688
694
|
),
|
|
689
695
|
)
|
|
690
|
-
|
|
696
|
+
edit_window.cobSubject.setCurrentIndex(0)
|
|
691
697
|
|
|
698
|
+
# behaviors
|
|
692
699
|
sortedCodes = sorted([self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in self.pj[cfg.ETHOGRAM]])
|
|
693
|
-
|
|
694
|
-
|
|
700
|
+
edit_window.cobCode.addItems(sortedCodes)
|
|
695
701
|
# check if selected code is in code's list (no modification of codes)
|
|
696
|
-
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][
|
|
697
|
-
|
|
698
|
-
sortedCodes.index(
|
|
699
|
-
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][row][cfg.EVENT_BEHAVIOR_FIELD_IDX]
|
|
700
|
-
)
|
|
702
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_BEHAVIOR_FIELD_IDX] in sortedCodes:
|
|
703
|
+
edit_window.cobCode.setCurrentIndex(
|
|
704
|
+
sortedCodes.index(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_BEHAVIOR_FIELD_IDX])
|
|
701
705
|
)
|
|
702
706
|
else:
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
"does not exist more in the ethogram"
|
|
707
|
-
)
|
|
707
|
+
msg: str = (
|
|
708
|
+
f"The behaviour {self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_BEHAVIOR_FIELD_IDX]} "
|
|
709
|
+
"does not exist longer in the ethogram"
|
|
708
710
|
)
|
|
711
|
+
logging.warning(msg)
|
|
712
|
+
|
|
709
713
|
QMessageBox.warning(
|
|
710
714
|
self,
|
|
711
715
|
cfg.programName,
|
|
712
|
-
|
|
713
|
-
f"The behaviour <b>{self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][row][cfg.EVENT_BEHAVIOR_FIELD_IDX]}</b> "
|
|
714
|
-
"does not exist more in the ethogram"
|
|
715
|
-
),
|
|
716
|
+
msg,
|
|
716
717
|
)
|
|
717
|
-
|
|
718
|
+
edit_window.cobCode.setCurrentIndex(0)
|
|
718
719
|
|
|
719
720
|
logging.debug(
|
|
720
|
-
f"original modifiers: {self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][
|
|
721
|
+
f"original modifiers: {self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_MODIFIER_FIELD_IDX]}"
|
|
721
722
|
)
|
|
722
723
|
|
|
724
|
+
# # frame index
|
|
725
|
+
# frame_idx = read_event_field(
|
|
726
|
+
# self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx],
|
|
727
|
+
# self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
|
|
728
|
+
# cfg.FRAME_INDEX,
|
|
729
|
+
# )
|
|
730
|
+
# edit_window.sb_frame_idx.setValue(0 if frame_idx in (cfg.NA, None) else frame_idx)
|
|
731
|
+
# if frame_idx in (cfg.NA, None):
|
|
732
|
+
# edit_window.cb_set_frame_idx_na.setChecked(True)
|
|
733
|
+
|
|
723
734
|
# comment
|
|
724
|
-
|
|
725
|
-
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][row][cfg.EVENT_COMMENT_FIELD_IDX]
|
|
726
|
-
)
|
|
735
|
+
edit_window.leComment.setPlainText(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_COMMENT_FIELD_IDX])
|
|
727
736
|
|
|
728
737
|
flag_ok = False # for looping until event is OK or Cancel pressed
|
|
729
738
|
while True:
|
|
730
|
-
if
|
|
731
|
-
|
|
739
|
+
if edit_window.exec(): # button OK
|
|
732
740
|
self.project_changed()
|
|
733
741
|
|
|
742
|
+
new_time = edit_window.time_widget.get_time()
|
|
743
|
+
|
|
744
|
+
if edit_window.cb_set_time_na.isChecked():
|
|
745
|
+
new_time = dec("NaN")
|
|
746
|
+
|
|
734
747
|
# MEDIA / LIVE
|
|
735
748
|
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
|
|
749
|
+
if new_time.is_nan():
|
|
750
|
+
QMessageBox.warning(
|
|
751
|
+
self,
|
|
752
|
+
cfg.programName,
|
|
753
|
+
("Select a time format"),
|
|
754
|
+
)
|
|
755
|
+
continue
|
|
736
756
|
|
|
737
|
-
new_time = editWindow.time_widget.get_time()
|
|
738
757
|
for key in self.pj[cfg.ETHOGRAM]:
|
|
739
|
-
if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] ==
|
|
758
|
+
if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] == edit_window.cobCode.currentText():
|
|
740
759
|
event = self.full_event(key)
|
|
741
|
-
|
|
742
|
-
event[cfg.
|
|
743
|
-
|
|
744
|
-
|
|
760
|
+
# subject
|
|
761
|
+
event[cfg.SUBJECT] = (
|
|
762
|
+
"" if edit_window.cobSubject.currentText() == cfg.NO_FOCAL_SUBJECT else edit_window.cobSubject.currentText()
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
event[cfg.COMMENT] = edit_window.leComment.toPlainText()
|
|
766
|
+
|
|
767
|
+
# determine the new frame index
|
|
768
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
|
|
769
|
+
if self.playerType == cfg.MEDIA:
|
|
770
|
+
mem_time = self.getLaps()
|
|
771
|
+
if not self.seek_mediaplayer(new_time):
|
|
772
|
+
time.sleep(0.1)
|
|
773
|
+
frame_idx = self.get_frame_index()
|
|
774
|
+
event[cfg.FRAME_INDEX] = frame_idx
|
|
775
|
+
self.seek_mediaplayer(mem_time)
|
|
776
|
+
|
|
777
|
+
# if not edit_window.sb_frame_idx.value() or edit_window.cb_set_frame_idx_na.isChecked():
|
|
778
|
+
# event[cfg.FRAME_INDEX] = cfg.NA
|
|
779
|
+
# else:
|
|
780
|
+
# if self.playerType == cfg.MEDIA:
|
|
781
|
+
# mem_time = self.getLaps()
|
|
782
|
+
# if not self.seek_mediaplayer(new_time):
|
|
783
|
+
# frame_idx = self.get_frame_index()
|
|
784
|
+
# event[cfg.FRAME_INDEX] = frame_idx
|
|
785
|
+
# self.seek_mediaplayer(mem_time)
|
|
786
|
+
#
|
|
787
|
+
# # event[cfg.FRAME_INDEX] = edit_window.sb_frame_idx.value()
|
|
788
|
+
|
|
789
|
+
event["row"] = pj_event_idx
|
|
790
|
+
event["original_modifiers"] = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
|
|
745
791
|
cfg.PJ_OBS_FIELDS[self.playerType][cfg.MODIFIER]
|
|
746
792
|
]
|
|
747
793
|
|
|
748
|
-
r =
|
|
794
|
+
r = write_event.write_event(self, event, new_time)
|
|
795
|
+
|
|
796
|
+
# scroll tv events
|
|
797
|
+
index = self.tv_events.model().index(pj_event_idx, 0)
|
|
798
|
+
self.tv_events.scrollTo(index, QAbstractItemView.EnsureVisible)
|
|
799
|
+
|
|
749
800
|
if r == 1: # same event already present
|
|
750
801
|
continue
|
|
751
802
|
if not r:
|
|
@@ -755,44 +806,37 @@ def edit_event(self):
|
|
|
755
806
|
self.update_realtime_plot(force_plot=True)
|
|
756
807
|
|
|
757
808
|
# IMAGES
|
|
758
|
-
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]
|
|
759
|
-
new_index =
|
|
809
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
810
|
+
new_index = edit_window.sb_image_idx.value()
|
|
760
811
|
|
|
761
812
|
for key in self.pj[cfg.ETHOGRAM]:
|
|
762
|
-
if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] ==
|
|
813
|
+
if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] == edit_window.cobCode.currentText():
|
|
763
814
|
event = self.full_event(key)
|
|
764
|
-
event[cfg.
|
|
765
|
-
|
|
766
|
-
event[
|
|
767
|
-
|
|
815
|
+
event[cfg.TIME] = new_time
|
|
816
|
+
|
|
817
|
+
event[cfg.SUBJECT] = (
|
|
818
|
+
"" if edit_window.cobSubject.currentText() == cfg.NO_FOCAL_SUBJECT else edit_window.cobSubject.currentText()
|
|
819
|
+
)
|
|
820
|
+
event[cfg.COMMENT] = edit_window.leComment.toPlainText()
|
|
821
|
+
event["row"] = pj_event_idx
|
|
822
|
+
event["original_modifiers"] = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
|
|
768
823
|
cfg.PJ_OBS_FIELDS[self.playerType][cfg.MODIFIER]
|
|
769
824
|
]
|
|
770
825
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
time_ = dec("NaN")
|
|
778
|
-
if (
|
|
779
|
-
self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.USE_EXIF_DATE, False)
|
|
780
|
-
and self.extract_exif_DateTimeOriginal(self.images_list[new_index]) != -1
|
|
781
|
-
):
|
|
782
|
-
time_ = (
|
|
783
|
-
self.extract_exif_DateTimeOriginal(self.images_list[new_index]) - self.image_time_ref
|
|
784
|
-
)
|
|
785
|
-
|
|
786
|
-
elif self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0):
|
|
787
|
-
time_ = new_index * self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0)
|
|
826
|
+
# not editable yet. Read previous value
|
|
827
|
+
event[cfg.IMAGE_PATH] = read_event_field(
|
|
828
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx],
|
|
829
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
|
|
830
|
+
cfg.IMAGE_PATH,
|
|
831
|
+
)
|
|
788
832
|
|
|
789
|
-
|
|
833
|
+
event[cfg.IMAGE_INDEX] = new_index
|
|
790
834
|
|
|
835
|
+
r = write_event.write_event(self, event, event[cfg.TIME].quantize(dec("0.001"), rounding=ROUND_DOWN))
|
|
791
836
|
if r == 1: # same event already present
|
|
792
837
|
continue
|
|
793
838
|
if not r:
|
|
794
839
|
flag_ok = True
|
|
795
|
-
|
|
796
840
|
break
|
|
797
841
|
|
|
798
842
|
else: # Cancel button
|
|
@@ -801,101 +845,110 @@ def edit_event(self):
|
|
|
801
845
|
if flag_ok:
|
|
802
846
|
break
|
|
803
847
|
|
|
848
|
+
if self.pause_before_addevent:
|
|
849
|
+
# restart media
|
|
850
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA and self.playerType == cfg.MEDIA:
|
|
851
|
+
if player_mem_state:
|
|
852
|
+
self.play_video()
|
|
853
|
+
|
|
804
854
|
|
|
805
855
|
def edit_time_selected_events(self):
|
|
806
856
|
"""
|
|
807
|
-
|
|
857
|
+
shift time of one or more selected events
|
|
808
858
|
"""
|
|
809
|
-
# list of rows to edit
|
|
810
|
-
twEvents_rows_to_shift = set([item.row() for item in self.twEvents.selectedIndexes()])
|
|
811
859
|
|
|
812
|
-
|
|
860
|
+
tvevents_rows_to_shift = set([index.row() for index in self.tv_events.selectionModel().selectedIndexes()])
|
|
861
|
+
if not len(tvevents_rows_to_shift):
|
|
813
862
|
QMessageBox.warning(self, cfg.programName, "No event selected!")
|
|
814
863
|
return
|
|
815
864
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
)
|
|
819
|
-
if ok and d:
|
|
820
|
-
if (
|
|
821
|
-
dialog.MessageDialog(
|
|
822
|
-
cfg.programName,
|
|
823
|
-
(
|
|
824
|
-
f"Confirm the {'addition' if d > 0 else 'subtraction'} of {abs(d)} seconds "
|
|
825
|
-
"to all selected events in the current observation?"
|
|
826
|
-
),
|
|
827
|
-
[cfg.YES, cfg.NO],
|
|
828
|
-
)
|
|
829
|
-
== cfg.NO
|
|
830
|
-
):
|
|
831
|
-
return
|
|
865
|
+
w = dialog.Ask_time(0)
|
|
866
|
+
w.setWindowTitle("Shift time of selected event(s)")
|
|
867
|
+
w.label.setText("Amount of time to add or substract")
|
|
832
868
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
tsb_to_shift.append(
|
|
836
|
-
[
|
|
837
|
-
util.time2seconds(self.twEvents.item(row, cfg.EVENT_TIME_FIELD_IDX).text())
|
|
838
|
-
if self.timeFormat == cfg.HHMMSS
|
|
839
|
-
else dec(self.twEvents.item(row, cfg.EVENT_TIME_FIELD_IDX).text()),
|
|
840
|
-
self.twEvents.item(row, cfg.EVENT_SUBJECT_FIELD_IDX).text(),
|
|
841
|
-
self.twEvents.item(row, cfg.EVENT_BEHAVIOR_FIELD_IDX).text(),
|
|
842
|
-
]
|
|
843
|
-
)
|
|
869
|
+
if not w.exec_():
|
|
870
|
+
return
|
|
844
871
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
event[cfg.EVENT_SUBJECT_FIELD_IDX],
|
|
849
|
-
event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
|
|
850
|
-
] in tsb_to_shift:
|
|
851
|
-
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][idx][cfg.EVENT_TIME_FIELD_IDX] += dec(
|
|
852
|
-
f"{d:.3f}"
|
|
853
|
-
)
|
|
854
|
-
self.project_changed()
|
|
872
|
+
d = w.time_widget.get_time()
|
|
873
|
+
if d.is_nan() or not d:
|
|
874
|
+
return
|
|
855
875
|
|
|
856
|
-
|
|
857
|
-
|
|
876
|
+
if ":" in util.smart_time_format(abs(d)):
|
|
877
|
+
smart_d = f"{util.smart_time_format(abs(d))}"
|
|
878
|
+
else:
|
|
879
|
+
smart_d = f"{d} seconds"
|
|
880
|
+
|
|
881
|
+
if (
|
|
882
|
+
dialog.MessageDialog(
|
|
883
|
+
cfg.programName,
|
|
884
|
+
(f"Confirm the {'addition' if d > 0 else 'subtraction'} of {smart_d} to all selected events in the current observation?"),
|
|
885
|
+
[cfg.YES, cfg.NO],
|
|
858
886
|
)
|
|
859
|
-
|
|
887
|
+
== cfg.NO
|
|
888
|
+
):
|
|
889
|
+
return
|
|
860
890
|
|
|
861
|
-
|
|
891
|
+
# fill the undo list
|
|
892
|
+
fill_events_undo_list(self, "Undo 'Edit time'")
|
|
893
|
+
|
|
894
|
+
mem_time = self.getLaps()
|
|
895
|
+
for tw_event_idx in tvevents_rows_to_shift:
|
|
896
|
+
pj_event_idx = self.tv_idx2events_idx[tw_event_idx]
|
|
897
|
+
|
|
898
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.PJ_OBS_FIELDS[self.playerType][cfg.TIME]] += dec(
|
|
899
|
+
f"{d:.3f}"
|
|
900
|
+
)
|
|
901
|
+
# set new frame index
|
|
902
|
+
if self.playerType == cfg.MEDIA:
|
|
903
|
+
if not self.seek_mediaplayer(
|
|
904
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.PJ_OBS_FIELDS[self.playerType][cfg.TIME]]
|
|
905
|
+
):
|
|
906
|
+
# determine the new frame index
|
|
907
|
+
time.sleep(0.1)
|
|
908
|
+
frame_idx = self.get_frame_index()
|
|
909
|
+
if len(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx]) == 6:
|
|
910
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][-1] = frame_idx
|
|
911
|
+
elif len(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx]) == 5:
|
|
912
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx].append(frame_idx)
|
|
913
|
+
|
|
914
|
+
self.project_changed()
|
|
915
|
+
|
|
916
|
+
if self.playerType == cfg.MEDIA:
|
|
917
|
+
self.seek_mediaplayer(mem_time)
|
|
918
|
+
|
|
919
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = sorted(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS])
|
|
920
|
+
self.load_tw_events(self.observationId)
|
|
921
|
+
|
|
922
|
+
self.update_realtime_plot(force_plot=True)
|
|
862
923
|
|
|
863
924
|
|
|
864
925
|
def copy_selected_events(self):
|
|
865
926
|
"""
|
|
866
|
-
copy selected events to clipboard
|
|
927
|
+
copy selected events from project to clipboard
|
|
867
928
|
"""
|
|
868
|
-
|
|
869
|
-
|
|
929
|
+
|
|
930
|
+
logging.debug("Copy selected events to clipboard")
|
|
931
|
+
|
|
932
|
+
tvevents_rows_to_copy = set([index.row() for index in self.tv_events.selectionModel().selectedIndexes()])
|
|
933
|
+
if not len(tvevents_rows_to_copy):
|
|
870
934
|
QMessageBox.warning(self, cfg.programName, "No event selected!")
|
|
871
935
|
return
|
|
872
936
|
|
|
873
|
-
|
|
874
|
-
for row in twEvents_rows_to_copy:
|
|
875
|
-
tsb_to_copy.append(
|
|
876
|
-
[
|
|
877
|
-
util.time2seconds(self.twEvents.item(row, cfg.EVENT_TIME_FIELD_IDX).text())
|
|
878
|
-
if self.timeFormat == cfg.HHMMSS
|
|
879
|
-
else dec(self.twEvents.item(row, cfg.EVENT_TIME_FIELD_IDX).text()),
|
|
880
|
-
self.twEvents.item(row, cfg.EVENT_SUBJECT_FIELD_IDX).text(),
|
|
881
|
-
self.twEvents.item(row, cfg.EVENT_BEHAVIOR_FIELD_IDX).text(),
|
|
882
|
-
]
|
|
883
|
-
)
|
|
937
|
+
pj_event_idx_to_copy: list = [self.tv_idx2events_idx[row] for row in tvevents_rows_to_copy]
|
|
884
938
|
|
|
885
|
-
copied_events = []
|
|
939
|
+
copied_events: list = []
|
|
886
940
|
for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
|
|
887
|
-
if
|
|
888
|
-
event
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
copied_events.append(
|
|
893
|
-
"\t".join([str(x) for x in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][idx]])
|
|
894
|
-
)
|
|
941
|
+
if idx in pj_event_idx_to_copy:
|
|
942
|
+
if self.playerType in (cfg.MEDIA, cfg.VIEWER_MEDIA) and len(event) < len(cfg.MEDIA_PJ_EVENTS_FIELDS):
|
|
943
|
+
copied_events.append("\t".join([str(x) for x in event + [cfg.NA]]))
|
|
944
|
+
else:
|
|
945
|
+
copied_events.append("\t".join([str(x) for x in event]))
|
|
895
946
|
|
|
896
947
|
cb = QApplication.clipboard()
|
|
897
|
-
cb.clear(mode=
|
|
898
|
-
cb.setText("\n".join(copied_events), mode=
|
|
948
|
+
cb.clear(mode=QClipboard.Mode.Clipboard)
|
|
949
|
+
cb.setText("\n".join(copied_events), mode=QClipboard.Mode.Clipboard)
|
|
950
|
+
|
|
951
|
+
logging.debug("Selected events copied in clipboard")
|
|
899
952
|
|
|
900
953
|
|
|
901
954
|
def paste_clipboard_to_events(self):
|
|
@@ -906,33 +959,82 @@ def paste_clipboard_to_events(self):
|
|
|
906
959
|
cb = QApplication.clipboard()
|
|
907
960
|
cb_text = cb.text()
|
|
908
961
|
cb_text_splitted = cb_text.split("\n")
|
|
909
|
-
length = []
|
|
910
|
-
content = []
|
|
911
|
-
for
|
|
912
|
-
length.append(len(
|
|
913
|
-
content.append(
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
962
|
+
length: list = []
|
|
963
|
+
content: list = []
|
|
964
|
+
for line in cb_text_splitted:
|
|
965
|
+
length.append(len(line.split("\t")))
|
|
966
|
+
content.append(line.split("\t"))
|
|
967
|
+
|
|
968
|
+
if set(length) != set([len(cfg.PJ_EVENTS_FIELDS[self.playerType])]):
|
|
969
|
+
msg_box = QMessageBox(
|
|
970
|
+
QMessageBox.Warning,
|
|
917
971
|
cfg.programName,
|
|
918
972
|
(
|
|
919
|
-
"The clipboard does not contain events
|
|
920
|
-
"
|
|
973
|
+
"The clipboard does not contain events!<br>"
|
|
974
|
+
f"For an observation from <b>{self.playerType}</b> "
|
|
975
|
+
f"the events must be organized in {len(cfg.PJ_EVENTS_FIELDS[self.playerType])} columns separated by <TAB> character"
|
|
921
976
|
),
|
|
922
977
|
)
|
|
978
|
+
msg_box.setWindowFlags(msg_box.windowFlags() | Qt.WindowStaysOnTopHint)
|
|
979
|
+
msg_box.exec()
|
|
980
|
+
|
|
923
981
|
return
|
|
924
982
|
|
|
925
983
|
for event in content:
|
|
926
|
-
|
|
984
|
+
# convert time in decimal
|
|
985
|
+
event[cfg.EVENT_TIME_FIELD_IDX] = dec(event[cfg.EVENT_TIME_FIELD_IDX])
|
|
986
|
+
for idx, _ in enumerate(event):
|
|
987
|
+
if cfg.PJ_EVENTS_FIELDS[self.playerType][idx] in (cfg.FRAME_INDEX, cfg.IMAGE_INDEX):
|
|
988
|
+
try:
|
|
989
|
+
event[idx] = int(event[idx])
|
|
990
|
+
except ValueError:
|
|
991
|
+
pass
|
|
992
|
+
|
|
993
|
+
# skip if event already present
|
|
927
994
|
if event in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]:
|
|
928
995
|
continue
|
|
996
|
+
|
|
929
997
|
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].append(event)
|
|
930
998
|
|
|
931
999
|
self.project_changed()
|
|
932
1000
|
|
|
933
|
-
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = sorted(
|
|
934
|
-
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]
|
|
935
|
-
)
|
|
1001
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = sorted(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS])
|
|
936
1002
|
self.load_tw_events(self.observationId)
|
|
937
1003
|
|
|
938
1004
|
self.update_realtime_plot(force_plot=True)
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
def read_event_field(event: list, player_type: str, field_type: str) -> Union[str, None, int, dec]:
|
|
1008
|
+
"""
|
|
1009
|
+
return value of field for event or NA if not available
|
|
1010
|
+
"""
|
|
1011
|
+
if field_type not in cfg.PJ_EVENTS_FIELDS[player_type]:
|
|
1012
|
+
return None
|
|
1013
|
+
if cfg.PJ_OBS_FIELDS[player_type][field_type] < len(event):
|
|
1014
|
+
return event[cfg.PJ_OBS_FIELDS[player_type][field_type]]
|
|
1015
|
+
else:
|
|
1016
|
+
return cfg.NA
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
def add_frame_indexes(self):
|
|
1020
|
+
"""
|
|
1021
|
+
add frame indexes for all events
|
|
1022
|
+
"""
|
|
1023
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] != cfg.MEDIA:
|
|
1024
|
+
return
|
|
1025
|
+
if self.playerType != cfg.MEDIA:
|
|
1026
|
+
return
|
|
1027
|
+
|
|
1028
|
+
mem_time = self.getLaps()
|
|
1029
|
+
for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
|
|
1030
|
+
if event[0] == "NA":
|
|
1031
|
+
continue
|
|
1032
|
+
if not self.seek_mediaplayer(event[0]):
|
|
1033
|
+
time.sleep(0.1)
|
|
1034
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][idx][cfg.PJ_OBS_FIELDS[cfg.MEDIA][cfg.FRAME_INDEX]] = (
|
|
1035
|
+
self.get_frame_index()
|
|
1036
|
+
)
|
|
1037
|
+
|
|
1038
|
+
self.seek_mediaplayer(mem_time)
|
|
1039
|
+
|
|
1040
|
+
self.load_tw_events(self.observationId)
|