boris-behav-obs 9.7.7__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 +26 -0
- boris/__main__.py +25 -0
- boris/about.py +143 -0
- boris/add_modifier.py +635 -0
- boris/add_modifier_ui.py +303 -0
- boris/advanced_event_filtering.py +455 -0
- 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 +1110 -0
- boris/behavior_binary_table.py +305 -0
- boris/behaviors_coding_map.py +239 -0
- boris/boris_cli.py +340 -0
- boris/cmd_arguments.py +49 -0
- boris/coding_pad.py +280 -0
- boris/config.py +785 -0
- boris/config_file.py +356 -0
- boris/connections.py +409 -0
- boris/converters.py +333 -0
- boris/converters_ui.py +225 -0
- boris/cooccurence.py +250 -0
- boris/core.py +5901 -0
- boris/core_qrc.py +15958 -0
- boris/core_ui.py +1107 -0
- boris/db_functions.py +324 -0
- boris/dev.py +134 -0
- boris/dialog.py +1108 -0
- boris/duration_widget.py +238 -0
- boris/edit_event.py +245 -0
- boris/edit_event_ui.py +233 -0
- boris/event_operations.py +1040 -0
- boris/events_cursor.py +61 -0
- boris/events_snapshots.py +596 -0
- boris/exclusion_matrix.py +141 -0
- boris/export_events.py +1006 -0
- boris/export_observation.py +1203 -0
- boris/external_processes.py +332 -0
- boris/geometric_measurement.py +941 -0
- boris/gui_utilities.py +135 -0
- boris/image_overlay.py +72 -0
- boris/import_observations.py +242 -0
- boris/ipc_mpv.py +325 -0
- boris/irr.py +634 -0
- boris/latency.py +244 -0
- boris/measurement_widget.py +161 -0
- boris/media_file.py +115 -0
- boris/menu_options.py +213 -0
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +157 -0
- boris/mpv.py +2016 -0
- boris/mpv2.py +2193 -0
- boris/observation.py +1453 -0
- boris/observation_operations.py +2538 -0
- boris/observation_ui.py +679 -0
- boris/observations_list.py +337 -0
- boris/otx_parser.py +442 -0
- boris/param_panel.py +201 -0
- boris/param_panel_ui.py +305 -0
- boris/player_dock_widget.py +198 -0
- boris/plot_data_module.py +536 -0
- boris/plot_events.py +634 -0
- boris/plot_events_rt.py +237 -0
- boris/plot_spectrogram_rt.py +316 -0
- boris/plot_waveform_rt.py +230 -0
- boris/plugins.py +431 -0
- boris/portion/__init__.py +31 -0
- boris/portion/const.py +95 -0
- boris/portion/dict.py +365 -0
- boris/portion/func.py +52 -0
- boris/portion/interval.py +581 -0
- boris/portion/io.py +181 -0
- boris/preferences.py +510 -0
- boris/preferences_ui.py +770 -0
- boris/project.py +2007 -0
- boris/project_functions.py +2041 -0
- boris/project_import_export.py +1096 -0
- boris/project_ui.py +794 -0
- boris/qrc_boris.py +10389 -0
- boris/qrc_boris5.py +2579 -0
- boris/select_modifiers.py +312 -0
- boris/select_observations.py +210 -0
- boris/select_subj_behav.py +286 -0
- boris/state_events.py +197 -0
- boris/subjects_pad.py +106 -0
- boris/synthetic_time_budget.py +290 -0
- boris/time_budget_functions.py +1136 -0
- boris/time_budget_widget.py +1039 -0
- boris/transitions.py +365 -0
- boris/utilities.py +1810 -0
- boris/version.py +24 -0
- boris/video_equalizer.py +159 -0
- boris/video_equalizer_ui.py +248 -0
- boris/video_operations.py +310 -0
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
- boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
- boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
- boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
- boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1040 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
This program is free software; you can redistribute it and/or modify
|
|
8
|
+
it under the terms of the GNU General Public License as published by
|
|
9
|
+
the Free Software Foundation; either version 2 of the License, or
|
|
10
|
+
(at your option) any later version.
|
|
11
|
+
|
|
12
|
+
This program is distributed in the hope that it will be useful,
|
|
13
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
14
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
15
|
+
GNU General Public License for more details.
|
|
16
|
+
|
|
17
|
+
You should have received a copy of the GNU General Public License
|
|
18
|
+
along with this program; if not, write to the Free Software
|
|
19
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
20
|
+
MA 02110-1301, USA.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
import copy
|
|
26
|
+
import time
|
|
27
|
+
from decimal import Decimal as dec
|
|
28
|
+
from decimal import InvalidOperation
|
|
29
|
+
from decimal import ROUND_DOWN
|
|
30
|
+
from typing import Union
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
from . import config as cfg
|
|
34
|
+
from . import utilities as util
|
|
35
|
+
from . import dialog
|
|
36
|
+
from . import select_subj_behav
|
|
37
|
+
from . import select_modifiers
|
|
38
|
+
from . import write_event
|
|
39
|
+
from .edit_event import DlgEditEvent, EditSelectedEvents
|
|
40
|
+
|
|
41
|
+
from PySide6.QtWidgets import QMessageBox, QInputDialog, QLineEdit, QAbstractItemView, QApplication
|
|
42
|
+
from PySide6.QtCore import QTime, Qt
|
|
43
|
+
from PySide6.QtGui import QClipboard
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def add_event(self):
|
|
47
|
+
"""
|
|
48
|
+
manually add event to observation
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
if not self.observationId:
|
|
52
|
+
self.no_observation()
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
if self.pause_before_addevent:
|
|
56
|
+
# pause media
|
|
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()
|
|
61
|
+
|
|
62
|
+
if not self.pj[cfg.ETHOGRAM]:
|
|
63
|
+
QMessageBox.warning(self, cfg.programName, "The ethogram is not set!")
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
67
|
+
current_time = self.image_idx + 1
|
|
68
|
+
else:
|
|
69
|
+
current_time = self.getLaps()
|
|
70
|
+
|
|
71
|
+
editWindow = DlgEditEvent(
|
|
72
|
+
observation_type=self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
|
|
73
|
+
time_value=dec("NaN"),
|
|
74
|
+
image_idx=0,
|
|
75
|
+
current_time=current_time,
|
|
76
|
+
time_format=self.timeFormat,
|
|
77
|
+
show_set_current_time=True,
|
|
78
|
+
)
|
|
79
|
+
editWindow.setWindowTitle("Add a new event")
|
|
80
|
+
|
|
81
|
+
sortedSubjects = [cfg.NO_FOCAL_SUBJECT] + sorted([self.pj[cfg.SUBJECTS][x][cfg.SUBJECT_NAME] for x in self.pj[cfg.SUBJECTS]])
|
|
82
|
+
|
|
83
|
+
editWindow.cobSubject.addItems(sortedSubjects)
|
|
84
|
+
if self.currentSubject:
|
|
85
|
+
editWindow.cobSubject.setCurrentIndex(editWindow.cobSubject.findText(self.currentSubject, Qt.MatchFixedString))
|
|
86
|
+
|
|
87
|
+
sortedCodes = sorted([self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in self.pj[cfg.ETHOGRAM]])
|
|
88
|
+
|
|
89
|
+
editWindow.cobCode.addItems(sortedCodes)
|
|
90
|
+
|
|
91
|
+
if editWindow.exec_(): # button OK
|
|
92
|
+
# MEDIA / LIVE
|
|
93
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
|
|
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
|
|
102
|
+
|
|
103
|
+
for idx in self.pj[cfg.ETHOGRAM]:
|
|
104
|
+
if self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] == editWindow.cobCode.currentText():
|
|
105
|
+
event = self.full_event(idx)
|
|
106
|
+
|
|
107
|
+
event[cfg.SUBJECT] = (
|
|
108
|
+
"" if editWindow.cobSubject.currentText() == cfg.NO_FOCAL_SUBJECT else editWindow.cobSubject.currentText()
|
|
109
|
+
)
|
|
110
|
+
if editWindow.leComment.toPlainText():
|
|
111
|
+
event[cfg.COMMENT] = editWindow.leComment.toPlainText()
|
|
112
|
+
|
|
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)
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
self.update_realtime_plot(force_plot=True)
|
|
126
|
+
"""
|
|
127
|
+
if hasattr(self, "plot_events"):
|
|
128
|
+
if not self.plot_events.visibleRegion().isEmpty():
|
|
129
|
+
self.plot_events.events_list = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]
|
|
130
|
+
self.plot_events.plot_events(float(self.getLaps()))
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
"""
|
|
134
|
+
# update subjects table
|
|
135
|
+
self.currentStates = util.get_current_states_modifiers_by_subject(
|
|
136
|
+
util.state_behavior_codes(self.pj[cfg.ETHOGRAM]),
|
|
137
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS],
|
|
138
|
+
dict(self.pj[cfg.SUBJECTS], **{"": {"name": ""}}), # add no focal subject
|
|
139
|
+
newTime,
|
|
140
|
+
include_modifiers=True,
|
|
141
|
+
)
|
|
142
|
+
subject_idx = self.subject_name_index[self.currentSubject] if self.currentSubject else ""
|
|
143
|
+
self.lbCurrentStates.setText(", ".join(self.currentStates[subject_idx]))
|
|
144
|
+
self.show_current_states_in_subjects_table()
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
# IMAGES
|
|
148
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
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
|
|
153
|
+
|
|
154
|
+
for idx in self.pj[cfg.ETHOGRAM]:
|
|
155
|
+
if self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] == editWindow.cobCode.currentText():
|
|
156
|
+
event = self.full_event(idx)
|
|
157
|
+
|
|
158
|
+
event[cfg.SUBJECT] = (
|
|
159
|
+
"" if editWindow.cobSubject.currentText() == cfg.NO_FOCAL_SUBJECT else editWindow.cobSubject.currentText()
|
|
160
|
+
)
|
|
161
|
+
if editWindow.leComment.toPlainText():
|
|
162
|
+
event[cfg.COMMENT] = editWindow.leComment.toPlainText()
|
|
163
|
+
|
|
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
|
+
|
|
169
|
+
event[cfg.IMAGE_INDEX] = new_index
|
|
170
|
+
|
|
171
|
+
time_ = dec("NaN")
|
|
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
|
|
181
|
+
|
|
182
|
+
elif self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0):
|
|
183
|
+
time_ = new_index * self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.TIME_LAPSE, 0)
|
|
184
|
+
|
|
185
|
+
write_event.write_event(self, event, dec(time_).quantize(dec("0.001"), rounding=ROUND_DOWN))
|
|
186
|
+
|
|
187
|
+
break
|
|
188
|
+
|
|
189
|
+
if self.pause_before_addevent:
|
|
190
|
+
# restart media
|
|
191
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA and self.playerType == cfg.MEDIA:
|
|
192
|
+
if memState:
|
|
193
|
+
self.play_video()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def find_events(self):
|
|
197
|
+
"""
|
|
198
|
+
find in events
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
self.find_dialog = dialog.FindInEvents()
|
|
202
|
+
# list of rows to find
|
|
203
|
+
self.find_dialog.rowsToFind = set([self.tv_idx2events_idx[item.row()] for item in self.tv_events.selectedIndexes()])
|
|
204
|
+
self.find_dialog.currentIdx = -1
|
|
205
|
+
self.find_dialog.clickSignal.connect(self.click_signal_find_in_events)
|
|
206
|
+
self.find_dialog.setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
207
|
+
self.find_dialog.show()
|
|
208
|
+
|
|
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
|
+
|
|
225
|
+
def filter_events(self):
|
|
226
|
+
"""
|
|
227
|
+
filter coded events and subjects
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
parameters = select_subj_behav.choose_obs_subj_behav_category(
|
|
231
|
+
self,
|
|
232
|
+
selected_observations=[], # empty selection of observations for selecting all subjects and behaviors
|
|
233
|
+
show_include_modifiers=False,
|
|
234
|
+
show_exclude_non_coded_behaviors=False,
|
|
235
|
+
by_category=False,
|
|
236
|
+
)
|
|
237
|
+
if parameters == {}:
|
|
238
|
+
return
|
|
239
|
+
|
|
240
|
+
self.filtered_subjects = parameters[cfg.SELECTED_SUBJECTS][:]
|
|
241
|
+
if cfg.NO_FOCAL_SUBJECT in self.filtered_subjects:
|
|
242
|
+
self.filtered_subjects.append("")
|
|
243
|
+
self.filtered_behaviors = parameters[cfg.SELECTED_BEHAVIORS][:]
|
|
244
|
+
|
|
245
|
+
logging.debug(f"self.filtered_behaviors: {self.filtered_behaviors}")
|
|
246
|
+
|
|
247
|
+
self.load_tw_events(self.observationId)
|
|
248
|
+
self.dwEvents.setWindowTitle(f"Events for “{self.observationId}” observation (filtered)")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def show_all_events(self) -> None:
|
|
252
|
+
"""
|
|
253
|
+
show all events (disable filter)
|
|
254
|
+
"""
|
|
255
|
+
self.filtered_subjects = []
|
|
256
|
+
self.filtered_behaviors = []
|
|
257
|
+
self.load_tw_events(self.observationId)
|
|
258
|
+
self.dwEvents.setWindowTitle(f"Events for “{self.observationId}” observation")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def fill_events_undo_list(self, operation_description: str) -> None:
|
|
262
|
+
"""
|
|
263
|
+
fill the undo events list for Undo function (CTRL + Z)
|
|
264
|
+
"""
|
|
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
|
+
|
|
269
|
+
self.undo_description.append(operation_description)
|
|
270
|
+
|
|
271
|
+
self.actionUndo.setText(operation_description)
|
|
272
|
+
self.actionUndo.setEnabled(True)
|
|
273
|
+
|
|
274
|
+
logging.debug(f"{operation_description} added to undo events list")
|
|
275
|
+
|
|
276
|
+
if len(self.undo_queue) > cfg.MAX_UNDO_QUEUE:
|
|
277
|
+
self.undo_queue.popleft()
|
|
278
|
+
self.undo_description.popleft()
|
|
279
|
+
logging.debug("Max events undo ")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def undo_event_operation(self) -> None:
|
|
283
|
+
"""
|
|
284
|
+
undo operation on event(s)
|
|
285
|
+
"""
|
|
286
|
+
|
|
287
|
+
logging.debug("Undo event operation function")
|
|
288
|
+
|
|
289
|
+
if len(self.undo_queue) == 0:
|
|
290
|
+
self.statusbar.showMessage("The Undo buffer is empty", 5000)
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
events = self.undo_queue.pop()
|
|
294
|
+
|
|
295
|
+
operation_description = self.undo_description.pop()
|
|
296
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = events[:]
|
|
297
|
+
self.project_changed()
|
|
298
|
+
|
|
299
|
+
self.statusbar.showMessage(operation_description, 5000)
|
|
300
|
+
|
|
301
|
+
logging.debug(operation_description)
|
|
302
|
+
|
|
303
|
+
# reload all events in tw
|
|
304
|
+
self.load_tw_events(self.observationId)
|
|
305
|
+
|
|
306
|
+
self.update_realtime_plot(force_plot=True)
|
|
307
|
+
|
|
308
|
+
if not len(self.undo_queue):
|
|
309
|
+
self.actionUndo.setText("Undo")
|
|
310
|
+
self.actionUndo.setEnabled(False)
|
|
311
|
+
else:
|
|
312
|
+
self.actionUndo.setText(self.undo_description[-1])
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def delete_all_events(self):
|
|
316
|
+
"""
|
|
317
|
+
delete all (filtered) events in current observation
|
|
318
|
+
"""
|
|
319
|
+
|
|
320
|
+
if not self.observationId:
|
|
321
|
+
self.no_observation()
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
if not self.tv_idx2events_idx:
|
|
325
|
+
QMessageBox.warning(self, cfg.programName, "No events to delete")
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
if (
|
|
329
|
+
dialog.MessageDialog(
|
|
330
|
+
cfg.programName,
|
|
331
|
+
("Confirm the deletion of all (filtered) events in the current observation?<br>Filters do not apply!"),
|
|
332
|
+
[cfg.YES, cfg.NO],
|
|
333
|
+
)
|
|
334
|
+
== cfg.YES
|
|
335
|
+
):
|
|
336
|
+
# fill the undo list
|
|
337
|
+
fill_events_undo_list(self, "Undo 'Delete all events'")
|
|
338
|
+
|
|
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
|
+
]
|
|
344
|
+
|
|
345
|
+
self.update_realtime_plot(force_plot=True)
|
|
346
|
+
|
|
347
|
+
self.project_changed()
|
|
348
|
+
self.load_tw_events(self.observationId)
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def delete_selected_events(self):
|
|
352
|
+
"""
|
|
353
|
+
delete selected events
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
if not self.observationId:
|
|
357
|
+
self.no_observation()
|
|
358
|
+
return
|
|
359
|
+
|
|
360
|
+
logging.debug("begin function delete_selected_events")
|
|
361
|
+
|
|
362
|
+
if not self.tv_events.selectedIndexes():
|
|
363
|
+
QMessageBox.warning(self, cfg.programName, "No event selected!")
|
|
364
|
+
else:
|
|
365
|
+
# list of rows to delete (set for unique)
|
|
366
|
+
# fill the undo list
|
|
367
|
+
fill_events_undo_list(self, "Undo 'Delete selected events'")
|
|
368
|
+
|
|
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])
|
|
372
|
+
|
|
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
|
+
]
|
|
378
|
+
|
|
379
|
+
self.update_realtime_plot(force_plot=True)
|
|
380
|
+
|
|
381
|
+
self.project_changed()
|
|
382
|
+
self.load_tw_events(self.observationId)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def select_events_between_activated(self):
|
|
386
|
+
"""
|
|
387
|
+
select events between a time interval
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
def parseTime(txt):
|
|
391
|
+
"""
|
|
392
|
+
parse time in string (should be 00:00:00.000 or in seconds)
|
|
393
|
+
"""
|
|
394
|
+
if ":" in txt:
|
|
395
|
+
qtime = QTime.fromString(txt, "hh:mm:ss.zzz")
|
|
396
|
+
|
|
397
|
+
if qtime.toString():
|
|
398
|
+
timeSeconds = util.time2seconds(qtime.toString("hh:mm:ss.zzz"))
|
|
399
|
+
else:
|
|
400
|
+
return None
|
|
401
|
+
else:
|
|
402
|
+
try:
|
|
403
|
+
timeSeconds = dec(txt)
|
|
404
|
+
except InvalidOperation:
|
|
405
|
+
return None
|
|
406
|
+
return timeSeconds
|
|
407
|
+
|
|
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
|
+
)
|
|
419
|
+
|
|
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
|
|
424
|
+
|
|
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
|
|
440
|
+
|
|
441
|
+
self.tv_events.clearSelection()
|
|
442
|
+
self.tv_events.setSelectionMode(QAbstractItemView.MultiSelection)
|
|
443
|
+
|
|
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
|
+
]
|
|
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
|
+
]
|
|
473
|
+
else:
|
|
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)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def edit_selected_events(self):
|
|
513
|
+
"""
|
|
514
|
+
edit one or more selected events for subject, behavior and/or comment
|
|
515
|
+
"""
|
|
516
|
+
# list of rows to edit
|
|
517
|
+
|
|
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):
|
|
521
|
+
QMessageBox.warning(self, cfg.programName, "No event selected!")
|
|
522
|
+
|
|
523
|
+
elif len(tvevents_rows_to_edit) == 1: # 1 event selected
|
|
524
|
+
edit_event(self)
|
|
525
|
+
|
|
526
|
+
else: # editing of more events
|
|
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]])
|
|
529
|
+
|
|
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()])
|
|
532
|
+
]
|
|
533
|
+
|
|
534
|
+
if dialog_window.exec_():
|
|
535
|
+
# fill the undo list
|
|
536
|
+
fill_events_undo_list(self, "Undo 'Edit selected event(s)'")
|
|
537
|
+
|
|
538
|
+
tsb_to_edit: list = []
|
|
539
|
+
for row in tvevents_rows_to_edit:
|
|
540
|
+
tsb_to_edit.append(self.tv_idx2events_idx[row])
|
|
541
|
+
|
|
542
|
+
behavior_codes: list = []
|
|
543
|
+
modifiers_mem: list = []
|
|
544
|
+
mem_event_idx: list = []
|
|
545
|
+
for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
|
|
546
|
+
if idx in tsb_to_edit:
|
|
547
|
+
new_event = list(event)
|
|
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()
|
|
557
|
+
|
|
558
|
+
if new_event[cfg.EVENT_BEHAVIOR_FIELD_IDX] not in behavior_codes:
|
|
559
|
+
behavior_codes.append(new_event[cfg.EVENT_BEHAVIOR_FIELD_IDX])
|
|
560
|
+
|
|
561
|
+
if new_event[cfg.EVENT_MODIFIER_FIELD_IDX] not in modifiers_mem:
|
|
562
|
+
modifiers_mem.append(new_event[cfg.EVENT_MODIFIER_FIELD_IDX])
|
|
563
|
+
|
|
564
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][idx] = list(new_event)
|
|
565
|
+
mem_event_idx.append(idx)
|
|
566
|
+
self.project_changed()
|
|
567
|
+
|
|
568
|
+
# check if behavior is unique for editing modifiers
|
|
569
|
+
if len(behavior_codes) == 1:
|
|
570
|
+
# get behavior index
|
|
571
|
+
for idx in self.pj[cfg.ETHOGRAM]:
|
|
572
|
+
if self.pj[cfg.ETHOGRAM][idx][cfg.BEHAVIOR_CODE] == behavior_codes[0]:
|
|
573
|
+
break
|
|
574
|
+
else:
|
|
575
|
+
return
|
|
576
|
+
|
|
577
|
+
event = self.full_event(idx)
|
|
578
|
+
|
|
579
|
+
if len(modifiers_mem) == 1:
|
|
580
|
+
current_modifier = modifiers_mem[0]
|
|
581
|
+
else:
|
|
582
|
+
current_modifier = ""
|
|
583
|
+
|
|
584
|
+
if event["modifiers"]:
|
|
585
|
+
modifiers_selector = select_modifiers.ModifiersList(behavior_codes[0], eval(str(event["modifiers"])), current_modifier)
|
|
586
|
+
|
|
587
|
+
r = modifiers_selector.exec_()
|
|
588
|
+
if r:
|
|
589
|
+
selected_modifiers = modifiers_selector.get_modifiers()
|
|
590
|
+
|
|
591
|
+
modifier_str = ""
|
|
592
|
+
for idx1 in util.sorted_keys(selected_modifiers):
|
|
593
|
+
if modifier_str:
|
|
594
|
+
modifier_str += "|"
|
|
595
|
+
if selected_modifiers[idx1]["type"] in (cfg.SINGLE_SELECTION, cfg.MULTI_SELECTION):
|
|
596
|
+
modifier_str += ",".join(selected_modifiers[idx1].get("selected", ""))
|
|
597
|
+
if selected_modifiers[idx1]["type"] in (cfg.NUMERIC_MODIFIER, cfg.EXTERNAL_DATA_MODIFIER):
|
|
598
|
+
modifier_str += selected_modifiers[idx1].get("selected", "NA")
|
|
599
|
+
|
|
600
|
+
else: # delete current modifier(s)
|
|
601
|
+
modifier_str = ""
|
|
602
|
+
|
|
603
|
+
# set new modifier
|
|
604
|
+
for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
|
|
605
|
+
if idx in mem_event_idx:
|
|
606
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][idx][cfg.EVENT_MODIFIER_FIELD_IDX] = modifier_str
|
|
607
|
+
|
|
608
|
+
self.load_tw_events(self.observationId)
|
|
609
|
+
|
|
610
|
+
self.update_realtime_plot(force_plot=True)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def edit_event(self):
|
|
614
|
+
"""
|
|
615
|
+
edit event corresponding to the selected row in tv_events
|
|
616
|
+
"""
|
|
617
|
+
|
|
618
|
+
if not self.observationId:
|
|
619
|
+
self.no_observation()
|
|
620
|
+
return
|
|
621
|
+
|
|
622
|
+
if not self.tv_events.selectionModel().selectedIndexes():
|
|
623
|
+
QMessageBox.warning(self, cfg.programName, "Select an event to edit")
|
|
624
|
+
return
|
|
625
|
+
|
|
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()
|
|
632
|
+
|
|
633
|
+
tvevents_row = self.tv_events.selectionModel().selectedIndexes()[0].row()
|
|
634
|
+
|
|
635
|
+
pj_event_idx = self.tv_idx2events_idx[tvevents_row]
|
|
636
|
+
|
|
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
|
+
|
|
641
|
+
image_idx = None
|
|
642
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.IMAGES):
|
|
643
|
+
image_idx = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
|
|
644
|
+
cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.IMAGE_INDEX]
|
|
645
|
+
]
|
|
646
|
+
|
|
647
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
648
|
+
current_value = self.image_idx + 1
|
|
649
|
+
else:
|
|
650
|
+
current_value = self.getLaps()
|
|
651
|
+
|
|
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(
|
|
659
|
+
observation_type=self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE],
|
|
660
|
+
time_value=time_value,
|
|
661
|
+
image_idx=image_idx,
|
|
662
|
+
current_time=current_value,
|
|
663
|
+
time_format=self.timeFormat,
|
|
664
|
+
show_set_current_time=True,
|
|
665
|
+
exif_date_time=exif_date_time,
|
|
666
|
+
)
|
|
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])
|
|
685
|
+
)
|
|
686
|
+
else:
|
|
687
|
+
QMessageBox.warning(
|
|
688
|
+
self,
|
|
689
|
+
cfg.programName,
|
|
690
|
+
(
|
|
691
|
+
"The subject "
|
|
692
|
+
f"<b>{self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_SUBJECT_FIELD_IDX]}</b> "
|
|
693
|
+
"does not exist more in the subject's list"
|
|
694
|
+
),
|
|
695
|
+
)
|
|
696
|
+
edit_window.cobSubject.setCurrentIndex(0)
|
|
697
|
+
|
|
698
|
+
# behaviors
|
|
699
|
+
sortedCodes = sorted([self.pj[cfg.ETHOGRAM][x][cfg.BEHAVIOR_CODE] for x in self.pj[cfg.ETHOGRAM]])
|
|
700
|
+
edit_window.cobCode.addItems(sortedCodes)
|
|
701
|
+
# check if selected code is in code's list (no modification of codes)
|
|
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])
|
|
705
|
+
)
|
|
706
|
+
else:
|
|
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"
|
|
710
|
+
)
|
|
711
|
+
logging.warning(msg)
|
|
712
|
+
|
|
713
|
+
QMessageBox.warning(
|
|
714
|
+
self,
|
|
715
|
+
cfg.programName,
|
|
716
|
+
msg,
|
|
717
|
+
)
|
|
718
|
+
edit_window.cobCode.setCurrentIndex(0)
|
|
719
|
+
|
|
720
|
+
logging.debug(
|
|
721
|
+
f"original modifiers: {self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_MODIFIER_FIELD_IDX]}"
|
|
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
|
+
|
|
734
|
+
# comment
|
|
735
|
+
edit_window.leComment.setPlainText(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][cfg.EVENT_COMMENT_FIELD_IDX])
|
|
736
|
+
|
|
737
|
+
flag_ok = False # for looping until event is OK or Cancel pressed
|
|
738
|
+
while True:
|
|
739
|
+
if edit_window.exec(): # button OK
|
|
740
|
+
self.project_changed()
|
|
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
|
+
|
|
747
|
+
# MEDIA / LIVE
|
|
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
|
|
756
|
+
|
|
757
|
+
for key in self.pj[cfg.ETHOGRAM]:
|
|
758
|
+
if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] == edit_window.cobCode.currentText():
|
|
759
|
+
event = self.full_event(key)
|
|
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][
|
|
791
|
+
cfg.PJ_OBS_FIELDS[self.playerType][cfg.MODIFIER]
|
|
792
|
+
]
|
|
793
|
+
|
|
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
|
+
|
|
800
|
+
if r == 1: # same event already present
|
|
801
|
+
continue
|
|
802
|
+
if not r:
|
|
803
|
+
flag_ok = True
|
|
804
|
+
break
|
|
805
|
+
|
|
806
|
+
self.update_realtime_plot(force_plot=True)
|
|
807
|
+
|
|
808
|
+
# IMAGES
|
|
809
|
+
if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
|
|
810
|
+
new_index = edit_window.sb_image_idx.value()
|
|
811
|
+
|
|
812
|
+
for key in self.pj[cfg.ETHOGRAM]:
|
|
813
|
+
if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] == edit_window.cobCode.currentText():
|
|
814
|
+
event = self.full_event(key)
|
|
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][
|
|
823
|
+
cfg.PJ_OBS_FIELDS[self.playerType][cfg.MODIFIER]
|
|
824
|
+
]
|
|
825
|
+
|
|
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
|
+
)
|
|
832
|
+
|
|
833
|
+
event[cfg.IMAGE_INDEX] = new_index
|
|
834
|
+
|
|
835
|
+
r = write_event.write_event(self, event, event[cfg.TIME].quantize(dec("0.001"), rounding=ROUND_DOWN))
|
|
836
|
+
if r == 1: # same event already present
|
|
837
|
+
continue
|
|
838
|
+
if not r:
|
|
839
|
+
flag_ok = True
|
|
840
|
+
break
|
|
841
|
+
|
|
842
|
+
else: # Cancel button
|
|
843
|
+
flag_ok = True
|
|
844
|
+
|
|
845
|
+
if flag_ok:
|
|
846
|
+
break
|
|
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
|
+
|
|
854
|
+
|
|
855
|
+
def edit_time_selected_events(self):
|
|
856
|
+
"""
|
|
857
|
+
shift time of one or more selected events
|
|
858
|
+
"""
|
|
859
|
+
|
|
860
|
+
tvevents_rows_to_shift = set([index.row() for index in self.tv_events.selectionModel().selectedIndexes()])
|
|
861
|
+
if not len(tvevents_rows_to_shift):
|
|
862
|
+
QMessageBox.warning(self, cfg.programName, "No event selected!")
|
|
863
|
+
return
|
|
864
|
+
|
|
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")
|
|
868
|
+
|
|
869
|
+
if not w.exec_():
|
|
870
|
+
return
|
|
871
|
+
|
|
872
|
+
d = w.time_widget.get_time()
|
|
873
|
+
if d.is_nan() or not d:
|
|
874
|
+
return
|
|
875
|
+
|
|
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],
|
|
886
|
+
)
|
|
887
|
+
== cfg.NO
|
|
888
|
+
):
|
|
889
|
+
return
|
|
890
|
+
|
|
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)
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
def copy_selected_events(self):
|
|
926
|
+
"""
|
|
927
|
+
copy selected events from project to clipboard
|
|
928
|
+
"""
|
|
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):
|
|
934
|
+
QMessageBox.warning(self, cfg.programName, "No event selected!")
|
|
935
|
+
return
|
|
936
|
+
|
|
937
|
+
pj_event_idx_to_copy: list = [self.tv_idx2events_idx[row] for row in tvevents_rows_to_copy]
|
|
938
|
+
|
|
939
|
+
copied_events: list = []
|
|
940
|
+
for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
|
|
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]))
|
|
946
|
+
|
|
947
|
+
cb = QApplication.clipboard()
|
|
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")
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def paste_clipboard_to_events(self):
|
|
955
|
+
"""
|
|
956
|
+
paste clipboard to events
|
|
957
|
+
"""
|
|
958
|
+
|
|
959
|
+
cb = QApplication.clipboard()
|
|
960
|
+
cb_text = cb.text()
|
|
961
|
+
cb_text_splitted = cb_text.split("\n")
|
|
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,
|
|
971
|
+
cfg.programName,
|
|
972
|
+
(
|
|
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"
|
|
976
|
+
),
|
|
977
|
+
)
|
|
978
|
+
msg_box.setWindowFlags(msg_box.windowFlags() | Qt.WindowStaysOnTopHint)
|
|
979
|
+
msg_box.exec()
|
|
980
|
+
|
|
981
|
+
return
|
|
982
|
+
|
|
983
|
+
for event in content:
|
|
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
|
|
994
|
+
if event in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]:
|
|
995
|
+
continue
|
|
996
|
+
|
|
997
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].append(event)
|
|
998
|
+
|
|
999
|
+
self.project_changed()
|
|
1000
|
+
|
|
1001
|
+
self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = sorted(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS])
|
|
1002
|
+
self.load_tw_events(self.observationId)
|
|
1003
|
+
|
|
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)
|