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.

Files changed (129) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +36 -39
  4. boris/add_modifier.py +122 -109
  5. boris/add_modifier_ui.py +239 -135
  6. boris/advanced_event_filtering.py +81 -45
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +228 -229
  18. boris/behavior_binary_table.py +33 -50
  19. boris/behaviors_coding_map.py +17 -18
  20. boris/boris_cli.py +6 -25
  21. boris/cmd_arguments.py +12 -1
  22. boris/coding_pad.py +42 -49
  23. boris/config.py +161 -77
  24. boris/config_file.py +63 -83
  25. boris/connections.py +112 -57
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2511 -1824
  30. boris/core_qrc.py +15895 -10185
  31. boris/core_ui.py +946 -792
  32. boris/db_functions.py +21 -41
  33. boris/dev.py +134 -0
  34. boris/dialog.py +505 -244
  35. boris/duration_widget.py +15 -20
  36. boris/edit_event.py +84 -28
  37. boris/edit_event_ui.py +214 -78
  38. boris/event_operations.py +517 -415
  39. boris/events_cursor.py +25 -17
  40. boris/events_snapshots.py +36 -82
  41. boris/exclusion_matrix.py +4 -9
  42. boris/export_events.py +213 -583
  43. boris/export_observation.py +98 -611
  44. boris/external_processes.py +156 -97
  45. boris/geometric_measurement.py +652 -287
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +9 -9
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +26 -63
  51. boris/latency.py +34 -25
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +52 -84
  54. boris/menu_options.py +17 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv.py +1 -0
  58. boris/mpv2.py +732 -705
  59. boris/observation.py +655 -310
  60. boris/observation_operations.py +1036 -404
  61. boris/observation_ui.py +584 -356
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -80
  64. boris/param_panel.py +31 -16
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +90 -60
  67. boris/plot_data_module.py +43 -46
  68. boris/plot_events.py +127 -90
  69. boris/plot_events_rt.py +17 -31
  70. boris/plot_spectrogram_rt.py +95 -30
  71. boris/plot_waveform_rt.py +32 -21
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +18 -8
  74. boris/portion/const.py +35 -18
  75. boris/portion/dict.py +5 -5
  76. boris/portion/func.py +2 -2
  77. boris/portion/interval.py +21 -41
  78. boris/portion/io.py +41 -32
  79. boris/preferences.py +306 -83
  80. boris/preferences_ui.py +685 -228
  81. boris/project.py +448 -293
  82. boris/project_functions.py +689 -254
  83. boris/project_import_export.py +213 -222
  84. boris/project_ui.py +674 -438
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +74 -48
  88. boris/select_observations.py +20 -199
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +53 -37
  91. boris/subjects_pad.py +6 -9
  92. boris/synthetic_time_budget.py +45 -28
  93. boris/time_budget_functions.py +171 -171
  94. boris/time_budget_widget.py +84 -114
  95. boris/transitions.py +41 -47
  96. boris/utilities.py +766 -266
  97. boris/version.py +3 -3
  98. boris/video_equalizer.py +16 -14
  99. boris/video_equalizer_ui.py +199 -130
  100. boris/video_operations.py +125 -28
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.6.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
  106. {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
  108. boris/README.TXT +0 -22
  109. boris/add_modifier.ui +0 -323
  110. boris/boris_ui.py +0 -886
  111. boris/converters.ui +0 -289
  112. boris/core.qrc +0 -35
  113. boris/core.ui +0 -1543
  114. boris/edit_event.ui +0 -175
  115. boris/icons/logo_eye.ico +0 -0
  116. boris/map_creator.py +0 -850
  117. boris/observation.ui +0 -773
  118. boris/param_panel.ui +0 -379
  119. boris/preferences.ui +0 -537
  120. boris/project.ui +0 -1069
  121. boris/project_server.py +0 -236
  122. boris/vlc.py +0 -10343
  123. boris/vlc_local.py +0 -90
  124. boris_behav_obs-8.9.16.dist-info/LICENSE.TXT +0 -674
  125. boris_behav_obs-8.9.16.dist-info/METADATA +0 -129
  126. boris_behav_obs-8.9.16.dist-info/RECORD +0 -108
  127. boris_behav_obs-8.9.16.dist-info/entry_points.txt +0 -2
  128. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  129. {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-2023 Olivier Friard
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 PyQt5.QtWidgets import QMessageBox, QInputDialog, QLineEdit, QAbstractItemView, QApplication
36
- from PyQt5.QtCore import QTime, Qt
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
- if self.playerType == cfg.MEDIA:
52
- memState = self.is_playing()
53
- if memState:
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=0,
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 = [""] + sorted([self.pj[cfg.SUBJECTS][x][cfg.SUBJECT_NAME] for x in self.pj[cfg.SUBJECTS]])
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
- editWindow.cobSubject.setCurrentIndex(editWindow.cobSubject.findText(self.currentSubject, Qt.MatchFixedString))
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] = editWindow.cobSubject.currentText()
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
- self.write_event(event, newTime)
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.img_idx_widget.value()
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] = editWindow.cobSubject.currentText()
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
- event[cfg.IMAGE_PATH] = self.images_list[new_index]
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.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.USE_EXIF_DATE, False)
142
- and self.extract_exif_DateTimeOriginal(self.images_list[new_index]) != -1
143
- ):
144
- time_ = self.extract_exif_DateTimeOriginal(self.images_list[new_index]) - self.image_time_ref
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
- self.write_event(event, dec(time_).quantize(dec("0.001"), rounding=ROUND_DOWN))
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.twEvents.selectedIndexes()])
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
- start_coding=dec("NaN"),
182
- end_coding=dec("NaN"),
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
- self.undo_queue.append(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][:])
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(f"Max events undo ")
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(f"The Undo buffer is empty", 5000)
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.twEvents.rowCount():
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>" "Filters do not apply!"),
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
- rows_to_delete = []
284
- if self.playerType in (cfg.MEDIA, cfg.VIEWER_MEDIA, cfg.LIVE, cfg.VIEWER_LIVE):
285
- for row in range(self.twEvents.rowCount()):
286
- rows_to_delete.append(
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.twEvents.selectedIndexes():
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
- if self.playerType in (cfg.MEDIA, cfg.VIEWER_MEDIA, cfg.LIVE, cfg.VIEWER_LIVE):
354
- for row in set([item.row() for item in self.twEvents.selectedIndexes()]):
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
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = [
366
- event
367
- for event in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]
368
- if [
369
- event[cfg.PJ_OBS_FIELDS[self.playerType]["time"]],
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.twEvents.rowCount():
427
- text, ok = QInputDialog.getText(
428
- self,
429
- "Select events in time interval",
430
- "Interval: (example: 12.5-14.7 or 02:45.780-03:15.120)",
431
- QLineEdit.Normal,
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
- if ok and text != "":
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
- if "-" not in text:
438
- QMessageBox.critical(
439
- self, cfg.programName, "Use minus sign (-) to separate initial value from final value"
440
- )
441
- return
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
- while " " in text:
444
- text = text.replace(" ", "")
441
+ self.tv_events.clearSelection()
442
+ self.tv_events.setSelectionMode(QAbstractItemView.MultiSelection)
445
443
 
446
- from_, to_ = text.split("-")[0:2]
447
- from_sec = parseTime(from_)
448
- if not from_sec:
449
- QMessageBox.critical(self, cfg.programName, f"Time value not recognized: {from_}")
450
- return
451
- to_sec = parseTime(to_)
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
- QMessageBox.warning(self, cfg.programName, "There are no events to select")
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
- if not len(twEvents_rows_to_edit):
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
- elif len(twEvents_rows_to_edit) == 1: # 1 event selected
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
- dialogWindow = EditSelectedEvents()
485
- dialogWindow.all_behaviors = sorted(
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
- dialogWindow.all_subjects = [
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 dialogWindow.exec_():
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 twEvents_rows_to_edit:
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 dialogWindow.rbSubject.isChecked():
522
- new_event[cfg.EVENT_SUBJECT_FIELD_IDX] = dialogWindow.newText.selectedItems()[0].text()
523
- if dialogWindow.rbBehavior.isChecked():
524
- new_event[cfg.EVENT_BEHAVIOR_FIELD_IDX] = dialogWindow.newText.selectedItems()[0].text()
525
- if dialogWindow.rbComment.isChecked():
526
- new_event[cfg.EVENT_COMMENT_FIELD_IDX] = dialogWindow.commentText.text()
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 twEvents
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.twEvents.selectedItems():
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
- twEvents_row = self.twEvents.selectedItems()[0].row()
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
- if self.playerType in (cfg.MEDIA, cfg.LIVE, cfg.VIEWER_MEDIA, cfg.VIEWER_LIVE):
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
- row = [
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
- row = [
631
- idx
632
- for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS])
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
- value = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][row][
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
- current_time = self.image_idx + 1
648
+ current_value = self.image_idx + 1
659
649
  else:
660
- current_time = self.getLaps()
650
+ current_value = self.getLaps()
661
651
 
662
- editWindow = DlgEditEvent(
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=value,
665
- current_time=current_time,
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
- editWindow.setWindowTitle("Edit event")
670
-
671
- sortedSubjects = [""] + sorted([self.pj[cfg.SUBJECTS][x][cfg.SUBJECT_NAME] for x in self.pj[cfg.SUBJECTS]])
672
-
673
- editWindow.cobSubject.addItems(sortedSubjects)
674
-
675
- if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][row][cfg.EVENT_SUBJECT_FIELD_IDX] in sortedSubjects:
676
- editWindow.cobSubject.setCurrentIndex(
677
- sortedSubjects.index(
678
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][row][cfg.EVENT_SUBJECT_FIELD_IDX]
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
- f"The subject <b>{self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][row][cfg.EVENT_SUBJECT_FIELD_IDX]}</b> "
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
- editWindow.cobSubject.setCurrentIndex(0)
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
- editWindow.cobCode.addItems(sortedCodes)
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][row][cfg.EVENT_BEHAVIOR_FIELD_IDX] in sortedCodes:
697
- editWindow.cobCode.setCurrentIndex(
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
- logging.warning(
704
- (
705
- f"The behaviour {self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][row][cfg.EVENT_BEHAVIOR_FIELD_IDX]} "
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
- editWindow.cobCode.setCurrentIndex(0)
718
+ edit_window.cobCode.setCurrentIndex(0)
718
719
 
719
720
  logging.debug(
720
- f"original modifiers: {self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][row][cfg.EVENT_MODIFIER_FIELD_IDX]}"
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
- editWindow.leComment.setPlainText(
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 editWindow.exec_(): # button OK
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] == editWindow.cobCode.currentText():
758
+ if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] == edit_window.cobCode.currentText():
740
759
  event = self.full_event(key)
741
- event[cfg.SUBJECT] = editWindow.cobSubject.currentText()
742
- event[cfg.COMMENT] = editWindow.leComment.toPlainText()
743
- event["row"] = row
744
- event["original_modifiers"] = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][row][
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 = self.write_event(event, new_time)
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] in (cfg.IMAGES):
759
- new_index = editWindow.img_idx_widget.value()
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] == editWindow.cobCode.currentText():
813
+ if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] == edit_window.cobCode.currentText():
763
814
  event = self.full_event(key)
764
- event[cfg.SUBJECT] = editWindow.cobSubject.currentText()
765
- event[cfg.COMMENT] = editWindow.leComment.toPlainText()
766
- event["row"] = row
767
- event["original_modifiers"] = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][row][
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
- try:
772
- event[cfg.IMAGE_PATH] = self.images_list[new_index]
773
- except IndexError:
774
- event[cfg.IMAGE_PATH] = ""
775
- event[cfg.IMAGE_INDEX] = new_index
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
- r = self.write_event(event, dec(time_).quantize(dec("0.001"), rounding=ROUND_DOWN))
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
- edit time of one or more selected events
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
- if not len(twEvents_rows_to_shift):
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
- d, ok = QInputDialog.getDouble(
817
- self, "Time value", "Value to add or substract (use negative value):", 0, -86400, 86400, 3
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
- tsb_to_shift = []
834
- for row in twEvents_rows_to_shift:
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
- for idx, event in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]):
846
- if [
847
- event[cfg.EVENT_TIME_FIELD_IDX],
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
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS] = sorted(
857
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]
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
- self.load_tw_events(self.observationId)
887
+ == cfg.NO
888
+ ):
889
+ return
860
890
 
861
- self.update_realtime_plot(force_plot=True)
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
- twEvents_rows_to_copy = set([item.row() for item in self.twEvents.selectedIndexes()])
869
- if not len(twEvents_rows_to_copy):
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
- tsb_to_copy = []
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[cfg.EVENT_TIME_FIELD_IDX],
889
- event[cfg.EVENT_SUBJECT_FIELD_IDX],
890
- event[cfg.EVENT_BEHAVIOR_FIELD_IDX],
891
- ] in tsb_to_copy:
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=cb.Clipboard)
898
- cb.setText("\n".join(copied_events), mode=cb.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")
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 l in cb_text_splitted:
912
- length.append(len(l.split("\t")))
913
- content.append(l.split("\t"))
914
- if set(length) != set([5]):
915
- QMessageBox.warning(
916
- self,
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!\n"
920
- "Events must be organized in 5 columns separated by TAB character"
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
- event[0] = dec(event[0])
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)