boris-behav-obs 8.12__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 (128) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +28 -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 +141 -65
  24. boris/config_file.py +58 -67
  25. boris/connections.py +107 -61
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2373 -1786
  30. boris/core_qrc.py +15895 -10743
  31. boris/core_ui.py +943 -798
  32. boris/db_functions.py +17 -42
  33. boris/dev.py +109 -8
  34. boris/dialog.py +482 -236
  35. boris/duration_widget.py +9 -14
  36. boris/edit_event.py +61 -31
  37. boris/edit_event_ui.py +208 -97
  38. boris/event_operations.py +408 -293
  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 +184 -223
  43. boris/export_observation.py +74 -100
  44. boris/external_processes.py +123 -98
  45. boris/geometric_measurement.py +644 -290
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +4 -4
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +20 -57
  51. boris/latency.py +31 -24
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +17 -19
  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 +533 -221
  60. boris/observation_operations.py +1025 -390
  61. boris/observation_ui.py +572 -362
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -68
  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 +25 -33
  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 +684 -227
  81. boris/project.py +448 -293
  82. boris/project_functions.py +671 -238
  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 -198
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +52 -35
  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 +627 -236
  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 +95 -29
  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.12.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/converters.ui +0 -289
  111. boris/core.qrc +0 -36
  112. boris/core.ui +0 -1556
  113. boris/edit_event.ui +0 -233
  114. boris/icons/logo_eye.ico +0 -0
  115. boris/map_creator.py +0 -850
  116. boris/observation.ui +0 -814
  117. boris/param_panel.ui +0 -379
  118. boris/preferences.ui +0 -537
  119. boris/project.ui +0 -1069
  120. boris/project_server.py +0 -236
  121. boris/vlc.py +0 -10343
  122. boris/vlc_local.py +0 -90
  123. boris_behav_obs-8.12.dist-info/LICENSE.TXT +0 -674
  124. boris_behav_obs-8.12.dist-info/METADATA +0 -128
  125. boris_behav_obs-8.12.dist-info/RECORD +0 -108
  126. boris_behav_obs-8.12.dist-info/entry_points.txt +0 -3
  127. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  128. {boris_behav_obs-8.12.dist-info → boris_behav_obs-9.7.6.dist-info}/top_level.txt +0 -0
@@ -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
  This program is free software; you can redistribute it and/or modify
7
7
  it under the terms of the GNU General Public License as published by
@@ -19,20 +19,23 @@ Copyright 2012-2023 Olivier Friard
19
19
  MA 02110-1301, USA.
20
20
  """
21
21
 
22
- from math import log2
23
- import os
24
22
  import logging
25
- import time
26
- import tempfile
27
- import subprocess
28
- import sys
23
+ from collections import deque
24
+ import datetime as dt
29
25
  from decimal import Decimal as dec
26
+ import json
27
+ from math import log2, floor
28
+ import os
30
29
  import pathlib as pl
31
- import datetime
32
- from typing import List, Tuple, Dict, Optional
30
+ import socket
31
+ import subprocess
32
+ import sys
33
+ import tempfile
34
+ import time
35
+ from typing import List, Tuple, Optional
33
36
 
34
37
 
35
- from PyQt5.QtWidgets import (
38
+ from PySide6.QtWidgets import (
36
39
  QMessageBox,
37
40
  QFileDialog,
38
41
  QDateTimeEdit,
@@ -41,11 +44,12 @@ from PyQt5.QtWidgets import (
41
44
  QSlider,
42
45
  QMainWindow,
43
46
  QDockWidget,
47
+ QWidget,
44
48
  )
45
- from PyQt5.QtCore import Qt, QDateTime, QTimer
46
- from PyQt5.QtGui import QFont, QIcon
49
+ from PySide6.QtCore import Qt, QDateTime, QTimer
50
+ from PySide6.QtGui import QFont, QIcon, QTextCursor
47
51
 
48
- from PyQt5 import QtTest
52
+ from PySide6 import QtTest
49
53
 
50
54
  from . import menu_options
51
55
  from . import config as cfg
@@ -58,6 +62,7 @@ from . import plot_data_module
58
62
  from . import player_dock_widget
59
63
  from . import gui_utilities
60
64
  from . import video_operations
65
+ from . import state_events
61
66
 
62
67
 
63
68
  def export_observations_list_clicked(self):
@@ -78,9 +83,7 @@ def export_observations_list_clicked(self):
78
83
  cfg.HTML,
79
84
  ]
80
85
 
81
- file_name, filter_ = QFileDialog().getSaveFileName(
82
- self, "Export list of selected observations", "", ";;".join(file_formats)
83
- )
86
+ file_name, filter_ = QFileDialog().getSaveFileName(self, "Export list of selected observations", "", ";;".join(file_formats))
84
87
 
85
88
  if not file_name:
86
89
  return
@@ -90,12 +93,7 @@ def export_observations_list_clicked(self):
90
93
  file_name = str(pl.Path(file_name)) + "." + output_format
91
94
  # check if file name with extension already exists
92
95
  if pl.Path(file_name).is_file():
93
- if (
94
- dialog.MessageDialog(
95
- cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE]
96
- )
97
- == cfg.CANCEL
98
- ):
96
+ if dialog.MessageDialog(cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE]) == cfg.CANCEL:
99
97
  return
100
98
 
101
99
  if not project_functions.export_observations_list(self.pj, selected_observations, file_name, output_format):
@@ -107,7 +105,7 @@ def observations_list(self):
107
105
  show list of all observations of current project
108
106
  """
109
107
 
110
- logging.debug(f"observations list")
108
+ logging.debug("observations list")
111
109
 
112
110
  if self.playerType in cfg.VIEWERS:
113
111
  close_observation(self)
@@ -115,20 +113,25 @@ def observations_list(self):
115
113
  result, selected_obs = select_observations.select_observations2(self, cfg.SINGLE)
116
114
 
117
115
  if not selected_obs:
116
+ # activate main window
117
+ self.activateWindow()
118
118
  return
119
119
 
120
120
  if self.observationId:
121
-
122
121
  self.hide_data_files()
123
122
  response = dialog.MessageDialog(
124
123
  cfg.programName, "The current observation will be closed. Do you want to continue?", (cfg.YES, cfg.NO)
125
124
  )
126
125
  if response == cfg.NO:
127
126
  self.show_data_files()
127
+ # activate main window
128
+ self.activateWindow()
129
+
128
130
  return ""
129
131
  else:
130
132
  close_observation(self)
131
133
 
134
+
132
135
  QtTest.QTest.qWait(1000)
133
136
 
134
137
  if result == cfg.OPEN:
@@ -144,10 +147,12 @@ def observations_list(self):
144
147
  QMessageBox.warning(
145
148
  self,
146
149
  cfg.programName,
147
- (f"The observation <b>{self.observationId}</b> is running!<br>" "Close it before editing."),
150
+ (f"The observation <b>{self.observationId}</b> is running!<br>Close it before editing."),
148
151
  )
149
152
 
150
- logging.debug(f"end observations list")
153
+ logging.debug("end observations list")
154
+ # activate main window
155
+ self.activateWindow()
151
156
 
152
157
 
153
158
  def open_observation(self, mode: str) -> str:
@@ -159,11 +164,10 @@ def open_observation(self, mode: str) -> str:
159
164
  "view" to view observation
160
165
  """
161
166
 
162
- logging.debug(f"open observation")
167
+ logging.debug("open observation")
163
168
 
164
169
  # check if current observation must be closed to open a new one
165
170
  if self.observationId:
166
-
167
171
  self.hide_data_files()
168
172
  response = dialog.MessageDialog(
169
173
  cfg.programName, "The current observation will be closed. Do you want to continue?", (cfg.YES, cfg.NO)
@@ -195,7 +199,7 @@ def load_observation(self, obs_id: str, mode: str = cfg.OBS_START) -> str:
195
199
  "view" to view observation
196
200
  """
197
201
 
198
- logging.debug(f"load observation")
202
+ logging.debug("load observation")
199
203
 
200
204
  if obs_id not in self.pj[cfg.OBSERVATIONS]:
201
205
  return "Error: Observation not found"
@@ -206,7 +210,6 @@ def load_observation(self, obs_id: str, mode: str = cfg.OBS_START) -> str:
206
210
  self.observationId = obs_id
207
211
 
208
212
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
209
-
210
213
  self.image_idx = 0
211
214
  self.images_list = []
212
215
 
@@ -219,7 +222,6 @@ def load_observation(self, obs_id: str, mode: str = cfg.OBS_START) -> str:
219
222
  self.dwEvents.setVisible(True)
220
223
 
221
224
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.LIVE:
222
-
223
225
  if mode == cfg.OBS_START:
224
226
  initialize_new_live_observation(self)
225
227
 
@@ -228,12 +230,12 @@ def load_observation(self, obs_id: str, mode: str = cfg.OBS_START) -> str:
228
230
  self.dwEvents.setVisible(True)
229
231
 
230
232
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
231
-
232
233
  if mode == cfg.OBS_START:
233
234
  if not initialize_new_media_observation(self):
234
- self.observationId = ""
235
- self.twEvents.setRowCount(0)
236
- menu_options.update_menu(self)
235
+ close_observation(self)
236
+ # self.observationId = ""
237
+ # self.twEvents.setRowCount(0)
238
+ # menu_options.update_menu(self)
237
239
  return "Error: loading observation problem"
238
240
 
239
241
  if mode == cfg.VIEW:
@@ -246,7 +248,7 @@ def load_observation(self, obs_id: str, mode: str = cfg.OBS_START) -> str:
246
248
  # title of dock widget “ ”
247
249
  self.dwEvents.setWindowTitle(f"Events for “{self.observationId}” observation")
248
250
 
249
- logging.debug(f"end load observation")
251
+ logging.debug("end load observation")
250
252
  return ""
251
253
 
252
254
 
@@ -260,9 +262,7 @@ def edit_observation(self):
260
262
  # hide data plot
261
263
  self.hide_data_files()
262
264
  if (
263
- dialog.MessageDialog(
264
- cfg.programName, "The current observation will be closed. Do you want to continue?", (cfg.YES, cfg.NO)
265
- )
265
+ dialog.MessageDialog(cfg.programName, "The current observation will be closed. Do you want to continue?", (cfg.YES, cfg.NO))
266
266
  == cfg.NO
267
267
  ):
268
268
  # restore plots
@@ -271,9 +271,7 @@ def edit_observation(self):
271
271
  else:
272
272
  close_observation(self)
273
273
 
274
- _, selected_observations = select_observations.select_observations2(
275
- self, cfg.EDIT, windows_title="Edit observation"
276
- )
274
+ _, selected_observations = select_observations.select_observations2(self, cfg.EDIT, windows_title="Edit observation")
277
275
 
278
276
  if selected_observations:
279
277
  new_observation(self, mode=cfg.EDIT, obsId=selected_observations[0])
@@ -284,9 +282,7 @@ def remove_observations(self):
284
282
  remove observations from project file
285
283
  """
286
284
 
287
- _, selected_observations = select_observations.select_observations2(
288
- self, cfg.MULTIPLE, windows_title="Remove observations"
289
- )
285
+ _, selected_observations = select_observations.select_observations2(self, cfg.MULTIPLE, windows_title="Remove observations")
290
286
  if not selected_observations:
291
287
  return
292
288
 
@@ -329,11 +325,7 @@ def coding_time(observations: dict, observations_list: list) -> Tuple[Optional[d
329
325
  observation = observations[obs_id]
330
326
  if observation[cfg.EVENTS]:
331
327
  # check if events contain a NA timestamp
332
- if [
333
- event[cfg.EVENT_TIME_FIELD_IDX]
334
- for event in observation[cfg.EVENTS]
335
- if event[cfg.EVENT_TIME_FIELD_IDX].is_nan()
336
- ]:
328
+ if [event[cfg.EVENT_TIME_FIELD_IDX] for event in observation[cfg.EVENTS] if event[cfg.EVENT_TIME_FIELD_IDX].is_nan()]:
337
329
  return dec("NaN"), dec("NaN"), dec("NaN")
338
330
  start_coding_list.append(observation[cfg.EVENTS][0][cfg.EVENT_TIME_FIELD_IDX])
339
331
  end_coding_list.append(observation[cfg.EVENTS][-1][cfg.EVENT_TIME_FIELD_IDX])
@@ -364,6 +356,47 @@ def coding_time(observations: dict, observations_list: list) -> Tuple[Optional[d
364
356
  return start_coding, end_coding, coding_duration
365
357
 
366
358
 
359
+ def time_intervals_range(observations: dict, observations_list: list) -> Tuple[Optional[dec], Optional[dec]]:
360
+ """
361
+ returns earliest start interval and latest end interval
362
+
363
+ Args:
364
+ observations (dict): observations of project
365
+ observations_list (list): list of selected observations
366
+
367
+ Returns:
368
+ decimal.Decimal: time of earliest start interval
369
+ decimal.Decimal: time of latest end interval
370
+
371
+ """
372
+ start_interval_list: list = []
373
+ end_interval_list: list = []
374
+ for obs_id in observations_list:
375
+ observation = observations[obs_id]
376
+ offset = observation[cfg.TIME_OFFSET]
377
+ # check if observation interval is defined
378
+ if (
379
+ not observation.get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[0]
380
+ and not observation.get(cfg.OBSERVATION_TIME_INTERVAL, [None, None])[1]
381
+ ):
382
+ return None, None
383
+
384
+ start_interval_list.append(dec(observation[cfg.OBSERVATION_TIME_INTERVAL][0]) + offset)
385
+ end_interval_list.append(dec(observation[cfg.OBSERVATION_TIME_INTERVAL][1]) + offset)
386
+
387
+ if not start_interval_list:
388
+ earliest_start_interval = None
389
+ else:
390
+ earliest_start_interval = min([x for x in start_interval_list])
391
+
392
+ if not end_interval_list:
393
+ latest_end_interval = None
394
+ else:
395
+ latest_end_interval = max([x for x in end_interval_list])
396
+
397
+ return earliest_start_interval, latest_end_interval
398
+
399
+
367
400
  def observation_total_length(observation: dict) -> dec:
368
401
  """
369
402
  Observation media duration (if any)
@@ -394,7 +427,7 @@ def observation_total_length(observation: dict) -> dec:
394
427
  last_event = obs_length = max(observation[cfg.EVENTS])[cfg.TW_OBS_FIELD[cfg.IMAGES]["time"]]
395
428
  obs_length = last_event - first_event
396
429
  except Exception:
397
- logging.critical(f"Length of observation from images not available")
430
+ logging.critical("Length of observation from images not available")
398
431
  obs_length = dec(-2)
399
432
  else:
400
433
  obs_length = dec(0)
@@ -510,10 +543,7 @@ def observation_length(pj: dict, selected_observations: list) -> tuple:
510
543
  if (
511
544
  dialog.MessageDialog(
512
545
  cfg.programName,
513
- (
514
- f"The observation length is not available (<b>{obs_id}</b>).<br>"
515
- "Use last event time as observation length?"
516
- ),
546
+ (f"The observation length is not available (<b>{obs_id}</b>).<br>Use last event time as observation length?"),
517
547
  (cfg.YES, cfg.NO),
518
548
  )
519
549
  == cfg.YES
@@ -545,7 +575,7 @@ def observation_length(pj: dict, selected_observations: list) -> tuple:
545
575
  return (max_obs_length, selectedObsTotalMediaLength)
546
576
 
547
577
 
548
- def new_observation(self, mode=cfg.NEW, obsId=""):
578
+ def new_observation(self, mode: str = cfg.NEW, obsId: str = "") -> None:
549
579
  """
550
580
  define a new observation or edit an existing observation
551
581
 
@@ -553,18 +583,18 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
553
583
  mode (str): NEW or EDIT
554
584
  obsId (str): observation Id to be edited
555
585
 
586
+ Retruns:
587
+ None
588
+
556
589
  """
557
590
  # check if current observation must be closed to create a new one
558
591
  if mode == cfg.NEW and self.observationId:
559
592
  # hide data plot
560
593
  self.hide_data_files()
561
594
  if (
562
- dialog.MessageDialog(
563
- cfg.programName, "The current observation will be closed. Do you want to continue?", (cfg.YES, cfg.NO)
564
- )
595
+ dialog.MessageDialog(cfg.programName, "The current observation will be closed. Do you want to continue?", (cfg.YES, cfg.NO))
565
596
  == cfg.NO
566
597
  ):
567
-
568
598
  # show data plot
569
599
  self.show_data_files()
570
600
  return
@@ -572,9 +602,7 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
572
602
  close_observation(self)
573
603
 
574
604
  observationWindow = observation.Observation(
575
- tmp_dir=self.ffmpeg_cache_dir
576
- if (self.ffmpeg_cache_dir and pl.Path(self.ffmpeg_cache_dir).is_dir())
577
- else tempfile.gettempdir(),
605
+ tmp_dir=self.ffmpeg_cache_dir if (self.ffmpeg_cache_dir and pl.Path(self.ffmpeg_cache_dir).is_dir()) else tempfile.gettempdir(),
578
606
  project_path=self.projectFileName,
579
607
  converters=self.pj.get(cfg.CONVERTERS, {}),
580
608
  time_format=self.timeFormat,
@@ -586,16 +614,15 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
586
614
  observationWindow.mem_obs_id = obsId
587
615
  observationWindow.chunk_length = self.chunk_length
588
616
  observationWindow.dteDate.setDateTime(QDateTime.currentDateTime())
617
+ # observationWindow.de_date_offset.setDateTime(QDateTime.currentDateTime())
589
618
  observationWindow.ffmpeg_bin = self.ffmpeg_bin
590
619
  observationWindow.project_file_name = self.projectFileName
591
620
  observationWindow.rb_no_time.setChecked(True)
592
621
 
593
622
  # add independent variables
594
623
  if cfg.INDEPENDENT_VARIABLES in self.pj:
595
-
596
624
  observationWindow.twIndepVariables.setRowCount(0)
597
625
  for i in util.sorted_keys(self.pj[cfg.INDEPENDENT_VARIABLES]):
598
-
599
626
  observationWindow.twIndepVariables.setRowCount(observationWindow.twIndepVariables.rowCount() + 1)
600
627
 
601
628
  # label
@@ -630,22 +657,18 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
630
657
  comboBox = QComboBox()
631
658
  comboBox.addItems(self.pj[cfg.INDEPENDENT_VARIABLES][i]["possible values"].split(","))
632
659
  if txt in self.pj[cfg.INDEPENDENT_VARIABLES][i]["possible values"].split(","):
633
- comboBox.setCurrentIndex(
634
- self.pj[cfg.INDEPENDENT_VARIABLES][i]["possible values"].split(",").index(txt)
635
- )
636
- observationWindow.twIndepVariables.setCellWidget(
637
- observationWindow.twIndepVariables.rowCount() - 1, 2, comboBox
638
- )
660
+ comboBox.setCurrentIndex(self.pj[cfg.INDEPENDENT_VARIABLES][i]["possible values"].split(",").index(txt))
661
+ observationWindow.twIndepVariables.setCellWidget(observationWindow.twIndepVariables.rowCount() - 1, 2, comboBox)
639
662
 
640
663
  elif self.pj[cfg.INDEPENDENT_VARIABLES][i]["type"] == cfg.TIMESTAMP:
641
664
  cal = QDateTimeEdit()
642
- cal.setDisplayFormat("yyyy-MM-dd hh:mm:ss")
665
+ cal.setDisplayFormat("yyyy-MM-dd hh:mm:ss.zzz")
643
666
  cal.setCalendarPopup(True)
644
- if txt:
645
- cal.setDateTime(QDateTime.fromString(txt, "yyyy-MM-ddThh:mm:ss"))
646
- observationWindow.twIndepVariables.setCellWidget(
647
- observationWindow.twIndepVariables.rowCount() - 1, 2, cal
648
- )
667
+ if len(txt) == len("yyyy-MM-ddThh:mm:ss"):
668
+ txt += ".000"
669
+ cal.setDateTime(QDateTime.fromString(txt, "yyyy-MM-ddThh:mm:ss.zzz"))
670
+
671
+ observationWindow.twIndepVariables.setCellWidget(observationWindow.twIndepVariables.rowCount() - 1, 2, cal)
649
672
  else:
650
673
  item.setText(txt)
651
674
  observationWindow.twIndepVariables.setItem(observationWindow.twIndepVariables.rowCount() - 1, 2, item)
@@ -654,28 +677,34 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
654
677
 
655
678
  # adapt time offset for current time format
656
679
  if self.timeFormat == cfg.S:
657
- observationWindow.obs_time_offset.set_format_s()
680
+ observationWindow.obs_time_offset.rb_seconds.setChecked(True)
658
681
  if self.timeFormat == cfg.HHMMSS:
659
- observationWindow.obs_time_offset.set_format_hhmmss()
682
+ # observationWindow.obs_time_offset.set_format_hhmmss()
683
+ observationWindow.obs_time_offset.rb_time.setChecked(True)
660
684
 
661
- if mode == cfg.EDIT:
685
+ observationWindow.obs_time_offset.set_time(0)
662
686
 
687
+ if mode == cfg.EDIT:
663
688
  observationWindow.setWindowTitle(f'Edit observation "{obsId}"')
664
- mem_obs_id = obsId
689
+ """mem_obs_id = obsId"""
665
690
  observationWindow.leObservationId.setText(obsId)
666
691
 
667
692
  # check date format for old versions of BORIS app
668
693
  try:
669
694
  time.strptime(self.pj[cfg.OBSERVATIONS][obsId]["date"], "%Y-%m-%d %H:%M")
670
- self.pj[cfg.OBSERVATIONS][obsId]["date"] = (
671
- self.pj[cfg.OBSERVATIONS][obsId]["date"].replace(" ", "T") + ":00"
672
- )
695
+ self.pj[cfg.OBSERVATIONS][obsId]["date"] = self.pj[cfg.OBSERVATIONS][obsId]["date"].replace(" ", "T") + ":00.000"
696
+ logging.info("Old observation date/time format was converted")
673
697
  except ValueError:
674
698
  pass
675
699
 
676
- observationWindow.dteDate.setDateTime(
677
- QDateTime.fromString(self.pj[cfg.OBSERVATIONS][obsId]["date"], "yyyy-MM-ddThh:mm:ss")
678
- )
700
+ # print(f"{self.pj[cfg.OBSERVATIONS][obsId]['date']=}")
701
+
702
+ # test new date (with msec)
703
+ if len(self.pj[cfg.OBSERVATIONS][obsId]["date"]) == len("yyyy-MM-ddThh:mm:ss.zzz"):
704
+ observationWindow.dteDate.setDateTime(QDateTime.fromString(self.pj[cfg.OBSERVATIONS][obsId]["date"], "yyyy-MM-ddThh:mm:ss.zzz"))
705
+ elif len(self.pj[cfg.OBSERVATIONS][obsId]["date"]) == len("yyyy-MM-ddThh:mm:ss"):
706
+ observationWindow.dteDate.setDateTime(QDateTime.fromString(self.pj[cfg.OBSERVATIONS][obsId]["date"], "yyyy-MM-ddThh:mm:ss"))
707
+
679
708
  observationWindow.teDescription.setPlainText(self.pj[cfg.OBSERVATIONS][obsId][cfg.DESCRIPTION])
680
709
 
681
710
  try:
@@ -694,17 +723,20 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
694
723
  logging.info("No Video/Audio information")
695
724
 
696
725
  # offset
697
- observationWindow.obs_time_offset.set_time(self.pj[cfg.OBSERVATIONS][obsId][cfg.TIME_OFFSET])
726
+ if self.pj[cfg.OBSERVATIONS][obsId][cfg.TIME_OFFSET] > cfg.DATE_CUTOFF:
727
+ observationWindow.obs_time_offset.rb_datetime.setChecked(True)
728
+
729
+ # time offset
730
+ if self.pj[cfg.OBSERVATIONS][obsId][cfg.TIME_OFFSET]:
731
+ observationWindow.cb_time_offset.setChecked(True)
732
+ observationWindow.obs_time_offset.set_time(self.pj[cfg.OBSERVATIONS][obsId][cfg.TIME_OFFSET])
698
733
 
699
734
  if self.pj[cfg.OBSERVATIONS][obsId]["type"] == cfg.MEDIA:
700
735
  observationWindow.rb_media_files.setChecked(True)
701
736
 
702
737
  observationWindow.twVideo1.setRowCount(0)
703
738
  for player in self.pj[cfg.OBSERVATIONS][obsId][cfg.FILE]:
704
- if (
705
- player in self.pj[cfg.OBSERVATIONS][obsId][cfg.FILE]
706
- and self.pj[cfg.OBSERVATIONS][obsId][cfg.FILE][player]
707
- ):
739
+ if player in self.pj[cfg.OBSERVATIONS][obsId][cfg.FILE] and self.pj[cfg.OBSERVATIONS][obsId][cfg.FILE][player]:
708
740
  for mediaFile in self.pj[cfg.OBSERVATIONS][obsId][cfg.FILE][player]:
709
741
  observationWindow.twVideo1.setRowCount(observationWindow.twVideo1.rowCount() + 1)
710
742
 
@@ -713,19 +745,15 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
713
745
  combobox.setCurrentIndex(int(player) - 1)
714
746
  observationWindow.twVideo1.setCellWidget(observationWindow.twVideo1.rowCount() - 1, 0, combobox)
715
747
 
716
- # set offset
748
+ # set media file offset
717
749
  try:
718
750
  observationWindow.twVideo1.setItem(
719
751
  observationWindow.twVideo1.rowCount() - 1,
720
752
  1,
721
- QTableWidgetItem(
722
- str(self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO]["offset"][player])
723
- ),
753
+ QTableWidgetItem(str(self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO]["offset"][player])),
724
754
  )
725
755
  except Exception:
726
- observationWindow.twVideo1.setItem(
727
- observationWindow.twVideo1.rowCount() - 1, 1, QTableWidgetItem("0.0")
728
- )
756
+ observationWindow.twVideo1.setItem(observationWindow.twVideo1.rowCount() - 1, 1, QTableWidgetItem("0.0"))
729
757
 
730
758
  item = QTableWidgetItem(mediaFile)
731
759
  item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
@@ -734,16 +762,12 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
734
762
  # duration and FPS
735
763
  try:
736
764
  item = QTableWidgetItem(
737
- util.seconds2time(
738
- self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.LENGTH][mediaFile]
739
- )
765
+ util.seconds2time(self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.LENGTH][mediaFile])
740
766
  )
741
767
  item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
742
768
  observationWindow.twVideo1.setItem(observationWindow.twVideo1.rowCount() - 1, 3, item)
743
769
 
744
- item = QTableWidgetItem(
745
- f"{self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.FPS][mediaFile]:.2f}"
746
- )
770
+ item = QTableWidgetItem(f"{self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.FPS][mediaFile]:.2f}")
747
771
  item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
748
772
  observationWindow.twVideo1.setItem(observationWindow.twVideo1.rowCount() - 1, 4, item)
749
773
  except Exception:
@@ -751,41 +775,41 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
751
775
 
752
776
  # has_video has_audio
753
777
  try:
754
- item = QTableWidgetItem(
755
- str(self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.HAS_VIDEO][mediaFile])
756
- )
778
+ item = QTableWidgetItem(str(self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.HAS_VIDEO][mediaFile]))
757
779
  item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
758
780
  observationWindow.twVideo1.setItem(observationWindow.twVideo1.rowCount() - 1, 5, item)
759
781
 
760
- item = QTableWidgetItem(
761
- str(self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.HAS_AUDIO][mediaFile])
762
- )
782
+ item = QTableWidgetItem(str(self.pj[cfg.OBSERVATIONS][obsId][cfg.MEDIA_INFO][cfg.HAS_AUDIO][mediaFile]))
763
783
  item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
764
784
  observationWindow.twVideo1.setItem(observationWindow.twVideo1.rowCount() - 1, 6, item)
765
785
  except Exception:
766
786
  pass
767
787
 
788
+ observationWindow.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(observationWindow.twVideo1.rowCount() > 0)
768
789
  # spectrogram
769
790
  observationWindow.cbVisualizeSpectrogram.setEnabled(True)
770
- observationWindow.cbVisualizeSpectrogram.setChecked(
771
- self.pj[cfg.OBSERVATIONS][obsId].get(cfg.VISUALIZE_SPECTROGRAM, False)
772
- )
773
-
791
+ observationWindow.cbVisualizeSpectrogram.setChecked(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.VISUALIZE_SPECTROGRAM, False))
774
792
  # waveform
775
793
  observationWindow.cb_visualize_waveform.setEnabled(True)
776
- observationWindow.cb_visualize_waveform.setChecked(
777
- self.pj[cfg.OBSERVATIONS][obsId].get(cfg.VISUALIZE_WAVEFORM, False)
794
+ observationWindow.cb_visualize_waveform.setChecked(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.VISUALIZE_WAVEFORM, False))
795
+ # use Creation date metadata tag as offset
796
+ observationWindow.cb_media_creation_date_as_offset.setEnabled(True)
797
+
798
+ # DEVELOPMENT (REMOVE BEFORE RELEASE)
799
+ # observationWindow.cb_media_creation_date_as_offset.setEnabled(False)
800
+
801
+ observationWindow.cb_media_creation_date_as_offset.setChecked(
802
+ self.pj[cfg.OBSERVATIONS][obsId].get(cfg.MEDIA_CREATION_DATE_AS_OFFSET, False)
778
803
  )
779
804
 
805
+ # scan sampling
806
+ observationWindow.sb_media_scan_sampling.setValue(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.MEDIA_SCAN_SAMPLING_DURATION, 0))
780
807
  # image display duration
781
- observationWindow.sb_image_display_duration.setValue(
782
- self.pj[cfg.OBSERVATIONS][obsId].get(cfg.IMAGE_DISPLAY_DURATION, 1)
783
- )
808
+ observationWindow.sb_image_display_duration.setValue(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.IMAGE_DISPLAY_DURATION, 1))
784
809
 
785
810
  # plot data
786
811
  if cfg.PLOT_DATA in self.pj[cfg.OBSERVATIONS][obsId]:
787
812
  if self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA]:
788
-
789
813
  observationWindow.tw_data_files.setRowCount(0)
790
814
  for idx2 in util.sorted_keys(self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA]):
791
815
  observationWindow.tw_data_files.setRowCount(observationWindow.tw_data_files.rowCount() + 1)
@@ -795,9 +819,7 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
795
819
  combobox.addItems(cfg.DATA_PLOT_STYLES)
796
820
  combobox.setCurrentIndex(
797
821
  cfg.DATA_PLOT_STYLES.index(
798
- self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][
799
- cfg.DATA_PLOT_FIELDS[idx3]
800
- ]
822
+ self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][cfg.DATA_PLOT_FIELDS[idx3]]
801
823
  )
802
824
  )
803
825
 
@@ -811,9 +833,7 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
811
833
  combobox2.addItems(["False", "True"])
812
834
  combobox2.setCurrentIndex(
813
835
  ["False", "True"].index(
814
- self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][
815
- cfg.DATA_PLOT_FIELDS[idx3]
816
- ]
836
+ self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][cfg.DATA_PLOT_FIELDS[idx3]]
817
837
  )
818
838
  )
819
839
 
@@ -828,11 +848,7 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
828
848
  observationWindow.tw_data_files.rowCount() - 1,
829
849
  idx3,
830
850
  QTableWidgetItem(
831
- str(
832
- self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][
833
- cfg.DATA_PLOT_FIELDS[idx3]
834
- ]
835
- )
851
+ str(self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][cfg.DATA_PLOT_FIELDS[idx3]])
836
852
  ),
837
853
  )
838
854
 
@@ -840,24 +856,18 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
840
856
  observationWindow.tw_data_files.setItem(
841
857
  observationWindow.tw_data_files.rowCount() - 1,
842
858
  idx3,
843
- QTableWidgetItem(
844
- self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][
845
- cfg.DATA_PLOT_FIELDS[idx3]
846
- ]
847
- ),
859
+ QTableWidgetItem(self.pj[cfg.OBSERVATIONS][obsId][cfg.PLOT_DATA][idx2][cfg.DATA_PLOT_FIELDS[idx3]]),
848
860
  )
849
861
 
850
862
  if self.pj[cfg.OBSERVATIONS][obsId]["type"] == cfg.IMAGES:
851
863
  observationWindow.rb_images.setChecked(True)
852
- observationWindow.lw_images_directory.addItems(
853
- self.pj[cfg.OBSERVATIONS][obsId].get(cfg.DIRECTORIES_LIST, [])
854
- )
864
+ observationWindow.lw_images_directory.addItems(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.DIRECTORIES_LIST, []))
855
865
  observationWindow.rb_use_exif.setChecked(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.USE_EXIF_DATE, False))
856
866
  if self.pj[cfg.OBSERVATIONS][obsId].get(cfg.TIME_LAPSE, 0):
857
867
  observationWindow.rb_time_lapse.setChecked(True)
858
868
  observationWindow.sb_time_lapse.setValue(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.TIME_LAPSE, 0))
859
869
 
860
- if self.pj[cfg.OBSERVATIONS][obsId]["type"] in [cfg.LIVE]:
870
+ if self.pj[cfg.OBSERVATIONS][obsId]["type"] == cfg.LIVE:
861
871
  observationWindow.rb_live.setChecked(True)
862
872
  # sampling time
863
873
  observationWindow.sbScanSampling.setValue(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.SCAN_SAMPLING_TIME, 0))
@@ -867,39 +877,33 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
867
877
  or self.pj[cfg.OBSERVATIONS][obsId].get(cfg.START_FROM_CURRENT_EPOCH_TIME, False)
868
878
  )
869
879
  # day/epoch time
870
- observationWindow.rb_day_time.setChecked(
871
- self.pj[cfg.OBSERVATIONS][obsId].get(cfg.START_FROM_CURRENT_TIME, False)
872
- )
873
- observationWindow.rb_epoch_time.setChecked(
874
- self.pj[cfg.OBSERVATIONS][obsId].get(cfg.START_FROM_CURRENT_EPOCH_TIME, False)
875
- )
880
+ observationWindow.rb_day_time.setChecked(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.START_FROM_CURRENT_TIME, False))
881
+ observationWindow.rb_epoch_time.setChecked(self.pj[cfg.OBSERVATIONS][obsId].get(cfg.START_FROM_CURRENT_EPOCH_TIME, False))
876
882
 
877
883
  # observation time interval
878
884
  observationWindow.cb_observation_time_interval.setEnabled(True)
879
885
  if self.pj[cfg.OBSERVATIONS][obsId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0]) != [0, 0]:
880
886
  observationWindow.cb_observation_time_interval.setChecked(True)
881
- observationWindow.observation_time_interval = self.pj[cfg.OBSERVATIONS][obsId].get(
882
- cfg.OBSERVATION_TIME_INTERVAL, [0, 0]
883
- )
887
+ observationWindow.observation_time_interval = self.pj[cfg.OBSERVATIONS][obsId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
884
888
  observationWindow.cb_observation_time_interval.setText(
885
889
  (
886
890
  "Limit observation to a time interval: "
887
- f"{self.pj[cfg.OBSERVATIONS][obsId][cfg.OBSERVATION_TIME_INTERVAL][0]} - "
888
- f"{self.pj[cfg.OBSERVATIONS][obsId][cfg.OBSERVATION_TIME_INTERVAL][1]}"
891
+ f"{self.pj[cfg.OBSERVATIONS][obsId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]} - "
892
+ f"{self.pj[cfg.OBSERVATIONS][obsId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[1]}"
889
893
  )
890
894
  )
891
895
 
892
- # disabled due to problem when video goes back
893
- # if CLOSE_BEHAVIORS_BETWEEN_VIDEOS in self.pj[OBSERVATIONS][obsId]:
894
- # observationWindow.cbCloseCurrentBehaviorsBetweenVideo.setChecked(self.pj[OBSERVATIONS][obsId][CLOSE_BEHAVIORS_BETWEEN_VIDEOS])
896
+ if cfg.CLOSE_BEHAVIORS_BETWEEN_VIDEOS in self.pj[cfg.OBSERVATIONS][obsId]:
897
+ observationWindow.cbCloseCurrentBehaviorsBetweenVideo.setChecked(
898
+ self.pj[cfg.OBSERVATIONS][obsId][cfg.CLOSE_BEHAVIORS_BETWEEN_VIDEOS]
899
+ )
895
900
 
896
- rv = observationWindow.exec_()
901
+ rv = observationWindow.exec()
897
902
 
898
903
  # save geometry
899
904
  gui_utilities.save_geometry(observationWindow, "new observation")
900
905
 
901
906
  if rv:
902
-
903
907
  self.project_changed()
904
908
 
905
909
  new_obs_id = observationWindow.leObservationId.text().strip()
@@ -918,14 +922,14 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
918
922
 
919
923
  # check if id changed
920
924
  if mode == cfg.EDIT and new_obs_id != obsId:
921
-
922
925
  logging.info(f"observation id {obsId} changed in {new_obs_id}")
923
926
 
924
927
  self.pj[cfg.OBSERVATIONS][new_obs_id] = dict(self.pj[cfg.OBSERVATIONS][obsId])
925
928
  del self.pj[cfg.OBSERVATIONS][obsId]
926
929
 
927
930
  # observation date
928
- self.pj[cfg.OBSERVATIONS][new_obs_id]["date"] = observationWindow.dteDate.dateTime().toString(Qt.ISODate)
931
+ self.pj[cfg.OBSERVATIONS][new_obs_id]["date"] = observationWindow.dteDate.dateTime().toString("yyyy-MM-ddTHH:mm:ss.zzz")
932
+ # observation description
929
933
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.DESCRIPTION] = observationWindow.teDescription.toPlainText()
930
934
 
931
935
  # observation type: read project type from radio buttons
@@ -939,48 +943,53 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
939
943
  # independent variables for observation
940
944
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.INDEPENDENT_VARIABLES] = {}
941
945
  for r in range(observationWindow.twIndepVariables.rowCount()):
942
-
943
946
  # set dictionary as label (col 0) => value (col 2)
944
947
  if observationWindow.twIndepVariables.item(r, 1).text() == cfg.SET_OF_VALUES:
945
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.INDEPENDENT_VARIABLES][
946
- observationWindow.twIndepVariables.item(r, 0).text()
947
- ] = observationWindow.twIndepVariables.cellWidget(r, 2).currentText()
948
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.INDEPENDENT_VARIABLES][observationWindow.twIndepVariables.item(r, 0).text()] = (
949
+ observationWindow.twIndepVariables.cellWidget(r, 2).currentText()
950
+ )
948
951
  elif observationWindow.twIndepVariables.item(r, 1).text() == cfg.TIMESTAMP:
949
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.INDEPENDENT_VARIABLES][
950
- observationWindow.twIndepVariables.item(r, 0).text()
951
- ] = (observationWindow.twIndepVariables.cellWidget(r, 2).dateTime().toString(Qt.ISODate))
952
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.INDEPENDENT_VARIABLES][observationWindow.twIndepVariables.item(r, 0).text()] = (
953
+ observationWindow.twIndepVariables.cellWidget(r, 2).dateTime().toString(Qt.ISODate)
954
+ )
952
955
  else:
953
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.INDEPENDENT_VARIABLES][
954
- observationWindow.twIndepVariables.item(r, 0).text()
955
- ] = observationWindow.twIndepVariables.item(r, 2).text()
956
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.INDEPENDENT_VARIABLES][observationWindow.twIndepVariables.item(r, 0).text()] = (
957
+ observationWindow.twIndepVariables.item(r, 2).text()
958
+ )
956
959
 
957
960
  # observation time offset
958
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TIME_OFFSET] = observationWindow.obs_time_offset.get_time()
961
+ if observationWindow.cb_time_offset.isChecked():
962
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TIME_OFFSET] = observationWindow.obs_time_offset.get_time()
963
+ else:
964
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TIME_OFFSET] = dec("0.0")
965
+
966
+ # add date (epoch) if date offset checked
967
+ # if observationWindow.cb_date_offset.isChecked():
968
+ # print(f"{observationWindow.de_date_offset.date().toString(Qt.ISODate)=}")
969
+ # date_timestamp = dec(dt.datetime.strptime(observationWindow.de_date_offset.date().toString(Qt.ISODate), "%Y-%m-%d").timestamp())
970
+ # self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TIME_OFFSET] += date_timestamp
959
971
 
960
972
  if observationWindow.cb_observation_time_interval.isChecked():
961
- self.pj[cfg.OBSERVATIONS][new_obs_id][
962
- cfg.OBSERVATION_TIME_INTERVAL
963
- ] = observationWindow.observation_time_interval
973
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.OBSERVATION_TIME_INTERVAL] = observationWindow.observation_time_interval
964
974
 
965
975
  self.display_statusbar_info(new_obs_id)
966
976
 
967
977
  # visualize spectrogram
968
- self.pj[cfg.OBSERVATIONS][new_obs_id][
969
- cfg.VISUALIZE_SPECTROGRAM
970
- ] = observationWindow.cbVisualizeSpectrogram.isChecked()
971
- # visualize spectrogram
972
- self.pj[cfg.OBSERVATIONS][new_obs_id][
973
- cfg.VISUALIZE_WAVEFORM
974
- ] = observationWindow.cb_visualize_waveform.isChecked()
978
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.VISUALIZE_SPECTROGRAM] = observationWindow.cbVisualizeSpectrogram.isChecked()
979
+ # visualize waveform
980
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.VISUALIZE_WAVEFORM] = observationWindow.cb_visualize_waveform.isChecked()
981
+ # use Creation date metadata tag as offset
982
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_CREATION_DATE_AS_OFFSET] = (
983
+ observationWindow.cb_media_creation_date_as_offset.isChecked()
984
+ )
985
+
986
+ # media scan sampling
987
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_SCAN_SAMPLING_DURATION] = observationWindow.sb_media_scan_sampling.value()
975
988
  # image display duration
976
- self.pj[cfg.OBSERVATIONS][new_obs_id][
977
- cfg.IMAGE_DISPLAY_DURATION
978
- ] = observationWindow.sb_image_display_duration.value()
989
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.IMAGE_DISPLAY_DURATION] = observationWindow.sb_image_display_duration.value()
979
990
 
980
991
  # time interval for observation
981
- self.pj[cfg.OBSERVATIONS][new_obs_id][
982
- cfg.OBSERVATION_TIME_INTERVAL
983
- ] = observationWindow.observation_time_interval
992
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.OBSERVATION_TIME_INTERVAL] = observationWindow.observation_time_interval
984
993
 
985
994
  # plot data
986
995
  if observationWindow.tw_data_files.rowCount():
@@ -989,30 +998,27 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
989
998
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)] = {}
990
999
  for idx2 in cfg.DATA_PLOT_FIELDS:
991
1000
  if idx2 in [cfg.PLOT_DATA_PLOTCOLOR_IDX, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX]:
992
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)][
993
- cfg.DATA_PLOT_FIELDS[idx2]
994
- ] = observationWindow.tw_data_files.cellWidget(row, idx2).currentText()
1001
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)][cfg.DATA_PLOT_FIELDS[idx2]] = (
1002
+ observationWindow.tw_data_files.cellWidget(row, idx2).currentText()
1003
+ )
995
1004
 
996
1005
  elif idx2 == cfg.PLOT_DATA_CONVERTERS_IDX:
997
1006
  if observationWindow.tw_data_files.item(row, idx2).text():
998
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)][
999
- cfg.DATA_PLOT_FIELDS[idx2]
1000
- ] = eval(observationWindow.tw_data_files.item(row, idx2).text())
1007
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)][cfg.DATA_PLOT_FIELDS[idx2]] = eval(
1008
+ observationWindow.tw_data_files.item(row, idx2).text()
1009
+ )
1001
1010
  else:
1002
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)][
1003
- cfg.DATA_PLOT_FIELDS[idx2]
1004
- ] = {}
1011
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)][cfg.DATA_PLOT_FIELDS[idx2]] = {}
1005
1012
 
1006
1013
  else:
1007
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)][
1008
- cfg.DATA_PLOT_FIELDS[idx2]
1009
- ] = observationWindow.tw_data_files.item(row, idx2).text()
1014
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.PLOT_DATA][str(row)][cfg.DATA_PLOT_FIELDS[idx2]] = (
1015
+ observationWindow.tw_data_files.item(row, idx2).text()
1016
+ )
1010
1017
 
1011
1018
  # Close current behaviors between video
1012
- # disabled due to problem when video goes back
1013
- # self.pj[OBSERVATIONS][new_obs_id][CLOSE_BEHAVIORS_BETWEEN_VIDEOS] =
1014
- # observationWindow.cbCloseCurrentBehaviorsBetweenVideo.isChecked()
1015
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.CLOSE_BEHAVIORS_BETWEEN_VIDEOS] = False
1019
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.CLOSE_BEHAVIORS_BETWEEN_VIDEOS] = (
1020
+ observationWindow.cbCloseCurrentBehaviorsBetweenVideo.isChecked()
1021
+ )
1016
1022
 
1017
1023
  if self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TYPE] == cfg.LIVE:
1018
1024
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.SCAN_SAMPLING_TIME] = observationWindow.sbScanSampling.value()
@@ -1026,10 +1032,23 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
1026
1032
  # images dir
1027
1033
  if self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TYPE] == cfg.IMAGES:
1028
1034
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.DIRECTORIES_LIST] = [
1029
- observationWindow.lw_images_directory.item(i).text()
1030
- for i in range(observationWindow.lw_images_directory.count())
1035
+ observationWindow.lw_images_directory.item(i).text() for i in range(observationWindow.lw_images_directory.count())
1031
1036
  ]
1037
+
1038
+ # check if exif data must be used
1032
1039
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.USE_EXIF_DATE] = observationWindow.rb_use_exif.isChecked()
1040
+
1041
+ # ask if the value of the exif date time of the first picture must be substracted
1042
+ # TODO: improve this
1043
+ if self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.USE_EXIF_DATE]:
1044
+ response = dialog.MessageDialog(
1045
+ cfg.programName,
1046
+ "You choose to use the EXIF metadata. Do you want to substract the date time value of the first picture?",
1047
+ (cfg.YES, cfg.NO),
1048
+ )
1049
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.SUBSTRACT_FIRST_EXIF_DATE] = response == cfg.YES
1050
+
1051
+ # check if time lapse
1033
1052
  if observationWindow.rb_time_lapse.isChecked():
1034
1053
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TIME_LAPSE] = observationWindow.sb_time_lapse.value()
1035
1054
  else:
@@ -1040,17 +1059,19 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
1040
1059
 
1041
1060
  # media
1042
1061
  if self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.TYPE] == cfg.MEDIA:
1043
-
1044
1062
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO] = {
1045
1063
  cfg.LENGTH: observationWindow.mediaDurations,
1046
1064
  cfg.FPS: observationWindow.mediaFPS,
1047
1065
  }
1048
1066
 
1067
+ if self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_CREATION_DATE_AS_OFFSET]:
1068
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO][cfg.MEDIA_CREATION_TIME] = observationWindow.media_creation_time
1069
+
1049
1070
  try:
1050
1071
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO][cfg.HAS_VIDEO] = observationWindow.mediaHasVideo
1051
1072
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO][cfg.HAS_AUDIO] = observationWindow.mediaHasAudio
1052
1073
  except Exception:
1053
- logging.info("error with media_info information")
1074
+ logging.warning("error with media_info information")
1054
1075
 
1055
1076
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO]["offset"] = {}
1056
1077
 
@@ -1060,9 +1081,9 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
1060
1081
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.FILE][str(i + 1)] = []
1061
1082
 
1062
1083
  for row in range(observationWindow.twVideo1.rowCount()):
1063
- self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.FILE][
1064
- observationWindow.twVideo1.cellWidget(row, 0).currentText()
1065
- ].append(observationWindow.twVideo1.item(row, 2).text())
1084
+ self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.FILE][observationWindow.twVideo1.cellWidget(row, 0).currentText()].append(
1085
+ observationWindow.twVideo1.item(row, 2).text()
1086
+ )
1066
1087
  # store offset for media player
1067
1088
  self.pj[cfg.OBSERVATIONS][new_obs_id][cfg.MEDIA_INFO]["offset"][
1068
1089
  observationWindow.twVideo1.cellWidget(row, 0).currentText()
@@ -1084,11 +1105,11 @@ def new_observation(self, mode=cfg.NEW, obsId=""):
1084
1105
 
1085
1106
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.MEDIA:
1086
1107
  self.playerType = cfg.MEDIA
1087
- # load events in table widget
1088
- initialize_new_media_observation(self)
1108
+ if not initialize_new_media_observation(self):
1109
+ close_observation(self)
1110
+ return "Observation not launched"
1089
1111
 
1090
1112
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] == cfg.IMAGES:
1091
- # QMessageBox.critical(self, cfg.programName, "Observation from images directory is not yet implemented")
1092
1113
  initialize_new_images_observation(self)
1093
1114
 
1094
1115
  self.load_tw_events(self.observationId)
@@ -1100,10 +1121,10 @@ def close_observation(self):
1100
1121
  close current observation
1101
1122
  """
1102
1123
 
1103
- logging.info(f"Close observation {self.playerType}")
1124
+ logging.info(f"Close observation (player type: {self.playerType})")
1125
+
1126
+ # check observation state events
1104
1127
 
1105
- logging.info(f"Check state events")
1106
- # check observation events
1107
1128
  flag_ok, msg = project_functions.check_state_events_obs(
1108
1129
  self.observationId,
1109
1130
  self.pj[cfg.ETHOGRAM],
@@ -1112,58 +1133,35 @@ def close_observation(self):
1112
1133
  )
1113
1134
 
1114
1135
  if not flag_ok:
1115
-
1116
1136
  out = f"The current observation has state event(s) that are not PAIRED:<br><br>{msg}"
1117
- results = dialog.Results_dialog()
1137
+ results = dialog.Results_dialog_exit_code()
1118
1138
  results.setWindowTitle(f"{cfg.programName} - Check selected observations")
1119
1139
  results.ptText.setReadOnly(True)
1120
1140
  results.ptText.appendHtml(out)
1121
- results.pbSave.setVisible(False)
1122
- results.pbCancel.setText("Close observation")
1123
- results.pbCancel.setVisible(True)
1124
- results.pbOK.setText("Fix unpaired state events")
1125
-
1126
- if results.exec_(): # fix events
1127
-
1128
- w = dialog.Ask_time(self.timeFormat)
1129
- w.setWindowTitle("Fix UNPAIRED state events")
1130
- w.label.setText("Fix UNPAIRED events at time")
1131
-
1132
- if w.exec_():
1133
- fix_at_time = w.time_widget.get_time()
1134
- events_to_add = project_functions.fix_unpaired_state_events(
1135
- self.pj[cfg.ETHOGRAM],
1136
- self.pj[cfg.OBSERVATIONS][self.observationId],
1137
- fix_at_time - dec("0.001"),
1138
- )
1139
- if events_to_add:
1140
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].extend(events_to_add)
1141
- self.project_changed()
1142
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS].sort()
1143
-
1144
- self.load_tw_events(self.observationId)
1145
- item = self.twEvents.item(
1146
- [
1147
- i
1148
- for i, t in enumerate(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS])
1149
- if t[0] == fix_at_time
1150
- ][0],
1151
- 0,
1152
- )
1153
- self.twEvents.scrollToItem(item)
1154
- return
1155
- else:
1156
- return
1157
1141
 
1158
- logging.info(f"Check state events done")
1142
+ results.pb1.setText("Close observation")
1143
+ results.pb2.setText("Return to observation")
1144
+ if self.playerType == cfg.IMAGES:
1145
+ results.pb3.setVisible(False)
1146
+ else:
1147
+ results.pb3.setText("Fix unpaired state events")
1148
+
1149
+ r = results.exec()
1150
+ if r == 2: # Return to observation
1151
+ return
1152
+ if r == 3: # Fix unpaired state events
1153
+ state_events.fix_unpaired_events(self, silent_mode=True)
1159
1154
 
1160
1155
  self.saved_state = self.saveState()
1161
1156
 
1162
1157
  if self.playerType == cfg.MEDIA:
1163
-
1164
- logging.info(f"Stop plot timer")
1158
+ self.media_scan_sampling_mem = []
1159
+ logging.info("Stop plot timer")
1165
1160
  self.plot_timer.stop()
1166
1161
 
1162
+ if self.MPV_IPC_MODE:
1163
+ self.main_window_activation_timer.stop()
1164
+
1167
1165
  for i, player in enumerate(self.dw_player):
1168
1166
  if (
1169
1167
  str(i + 1) in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE]
@@ -1172,6 +1170,16 @@ def close_observation(self):
1172
1170
  logging.info(f"Stop player #{i + 1}")
1173
1171
  player.player.stop()
1174
1172
 
1173
+ if self.MPV_IPC_MODE:
1174
+ try:
1175
+ player.player.process.terminate()
1176
+ try:
1177
+ player.player.process.wait(timeout=3) # wait up to 3s
1178
+ except subprocess.TimeoutExpired:
1179
+ player.player.process.kill() # force if still alive
1180
+ except Exception as e:
1181
+ logging.warning(f"Error stopping MPV process #{i}: {e}")
1182
+
1175
1183
  self.verticalLayout_3.removeWidget(self.video_slider)
1176
1184
 
1177
1185
  if self.video_slider is not None:
@@ -1186,21 +1194,22 @@ def close_observation(self):
1186
1194
  self.liveObservationStarted = False
1187
1195
  self.liveStartTime = None
1188
1196
 
1189
- if (
1190
- cfg.PLOT_DATA in self.pj[cfg.OBSERVATIONS][self.observationId]
1191
- and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA]
1192
- ):
1197
+ if cfg.PLOT_DATA in self.pj[cfg.OBSERVATIONS][self.observationId] and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA]:
1193
1198
  for x in self.ext_data_timer_list:
1194
1199
  x.stop()
1195
1200
  for pd in self.plot_data:
1196
1201
  self.plot_data[pd].close_plot()
1197
1202
 
1198
- logging.info(f"close tool window")
1203
+ logging.info("close tool window")
1199
1204
 
1200
1205
  self.close_tool_windows()
1201
1206
 
1202
1207
  self.observationId = ""
1203
1208
 
1209
+ # delete undo queue
1210
+ self.undo_queue = deque()
1211
+ self.undo_description = deque()
1212
+
1204
1213
  if self.playerType in (cfg.MEDIA, cfg.IMAGES):
1205
1214
  """
1206
1215
  for idx, _ in enumerate(self.dw_player):
@@ -1211,10 +1220,11 @@ def close_observation(self):
1211
1220
  """
1212
1221
 
1213
1222
  for dw in self.dw_player:
1214
-
1215
- logging.info(f"remove dock widget")
1216
-
1223
+ logging.info("remove dock widget")
1224
+ dw.player.log_handler = None
1217
1225
  self.removeDockWidget(dw)
1226
+
1227
+ del dw
1218
1228
  # sip.delete(dw)
1219
1229
  # dw = None
1220
1230
 
@@ -1226,10 +1236,12 @@ def close_observation(self):
1226
1236
 
1227
1237
  self.w_obs_info.setVisible(False)
1228
1238
 
1229
- self.twEvents.setRowCount(0)
1239
+ # self.twEvents.setRowCount(0)
1230
1240
 
1231
1241
  self.lb_current_media_time.clear()
1232
1242
  self.lb_player_status.clear()
1243
+ self.lb_video_info.clear()
1244
+ self.lb_zoom_level.clear()
1233
1245
 
1234
1246
  self.currentSubject = ""
1235
1247
  self.lbFocalSubject.setText(cfg.NO_FOCAL_SUBJECT)
@@ -1238,7 +1250,7 @@ def close_observation(self):
1238
1250
  for i in range(self.twSubjects.rowCount()):
1239
1251
  self.twSubjects.item(i, len(cfg.subjectsFields)).setText("")
1240
1252
 
1241
- for w in [self.lbTimeOffset, self.lbSpeed, self.lb_obs_time_interval]:
1253
+ for w in (self.lbTimeOffset, self.lb_obs_time_interval):
1242
1254
  w.clear()
1243
1255
  self.play_rate, self.playerType = 1, ""
1244
1256
 
@@ -1247,6 +1259,70 @@ def close_observation(self):
1247
1259
  logging.info(f"Observation {self.playerType} closed")
1248
1260
 
1249
1261
 
1262
+ def check_creation_date(self) -> Tuple[int, dict]:
1263
+ """
1264
+ check if media file exists
1265
+ check if Creation Date tag is present in metadata of media file
1266
+
1267
+ Returns:
1268
+ int: 0 if OK else error code: 1 -> media file date not used, 2 -> media file not found
1269
+
1270
+ """
1271
+
1272
+ not_tagged_media_list: list = []
1273
+ media_creation_time: dict = {}
1274
+
1275
+ for nplayer in cfg.ALL_PLAYERS:
1276
+ if nplayer in self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.FILE, {}):
1277
+ for media_file in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][nplayer]:
1278
+ media_path = project_functions.full_path(media_file, self.projectFileName)
1279
+ media_info = util.accurate_media_analysis(self.ffmpeg_bin, media_path)
1280
+
1281
+ if cfg.MEDIA_CREATION_TIME not in media_info or media_info[cfg.MEDIA_CREATION_TIME] == cfg.NA:
1282
+ not_tagged_media_list.append(media_path)
1283
+ else:
1284
+ creation_time_epoch = int(dt.datetime.strptime(media_info[cfg.MEDIA_CREATION_TIME], "%Y-%m-%d %H:%M:%S").timestamp())
1285
+ media_creation_time[media_path] = creation_time_epoch
1286
+
1287
+ """
1288
+ for row in range(self.twVideo1.rowCount()):
1289
+ if self.twVideo1.item(row, 2).text() not in media_not_found_list:
1290
+ media_info = util.accurate_media_analysis(self.ffmpeg_bin, self.twVideo1.item(row, 2).text())
1291
+ if cfg.MEDIA_CREATION_TIME not in media_info or media_info[cfg.MEDIA_CREATION_TIME] == cfg.NA:
1292
+ not_tagged_media_list.append(self.twVideo1.item(row, 2).text())
1293
+ else:
1294
+ creation_time_epoch = int(dt.datetime.strptime(media_info[cfg.MEDIA_CREATION_TIME], "%Y-%m-%d %H:%M:%S").timestamp())
1295
+ self.media_creation_time[self.twVideo1.item(row, 2).text()] = creation_time_epoch
1296
+ """
1297
+
1298
+ if not_tagged_media_list:
1299
+ dlg = dialog.Results_dialog()
1300
+ dlg.setWindowTitle("BORIS")
1301
+ dlg.pbOK.setText("Yes")
1302
+ dlg.pbCancel.setVisible(True)
1303
+ dlg.pbCancel.setText("No")
1304
+
1305
+ dlg.ptText.clear()
1306
+ dlg.ptText.appendHtml(
1307
+ (
1308
+ "Some media file does not contain the <b>Creation date/time</b> metadata tag:<br>"
1309
+ f"{'<br>'.join(not_tagged_media_list)}<br><br>"
1310
+ "Use the media file date/time instead?"
1311
+ )
1312
+ )
1313
+ dlg.ptText.moveCursor(QTextCursor.Start)
1314
+ ret = dlg.exec_()
1315
+
1316
+ if ret == 1: # use file creation time
1317
+ for media in not_tagged_media_list:
1318
+ media_creation_time[media] = pl.Path(media).stat().st_ctime
1319
+ return (0, media_creation_time) # OK use media file creation date/time
1320
+ else:
1321
+ return (1, {})
1322
+ else:
1323
+ return (0, media_creation_time) # OK all media have a 'creation time' tag
1324
+
1325
+
1250
1326
  def initialize_new_media_observation(self) -> bool:
1251
1327
  """
1252
1328
  initialize new observation from media file(s)
@@ -1254,12 +1330,10 @@ def initialize_new_media_observation(self) -> bool:
1254
1330
 
1255
1331
  logging.debug("function: initialize new observation for media file(s)")
1256
1332
 
1257
- for dw in [self.dwEthogram, self.dwSubjects, self.dwEvents]:
1333
+ for dw in (self.dwEthogram, self.dwSubjects, self.dwEvents):
1258
1334
  dw.setVisible(True)
1259
1335
 
1260
- ok, msg = project_functions.check_if_media_available(
1261
- self.pj[cfg.OBSERVATIONS][self.observationId], self.projectFileName
1262
- )
1336
+ ok, msg = project_functions.check_if_media_available(self.pj[cfg.OBSERVATIONS][self.observationId], self.projectFileName)
1263
1337
 
1264
1338
  if not ok:
1265
1339
  QMessageBox.critical(
@@ -1287,6 +1361,8 @@ def initialize_new_media_observation(self) -> bool:
1287
1361
  font = QFont()
1288
1362
  font.setPointSize(15)
1289
1363
  self.lb_current_media_time.setFont(font)
1364
+ self.lb_video_info.setFont(font)
1365
+ self.lb_zoom_level.setFont(font)
1290
1366
 
1291
1367
  # initialize video slider
1292
1368
  self.video_slider = QSlider(Qt.Horizontal, self)
@@ -1298,9 +1374,20 @@ def initialize_new_media_observation(self) -> bool:
1298
1374
 
1299
1375
  # add all media files to media lists
1300
1376
  self.setDockOptions(QMainWindow.AnimatedDocks | QMainWindow.AllowNestedDocks)
1301
- self.dw_player: list = []
1302
- # create dock widgets for players
1377
+ self.dw_player = []
1378
+
1379
+ # check if media creation time used as offset
1380
+ # TODO check if cfg.MEDIA_CREATION_TIME dict is present
1381
+ """
1382
+ if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.MEDIA_CREATION_DATE_AS_OFFSET, False):
1383
+ r, media_creation_time = check_creation_date(self)
1303
1384
 
1385
+ if r:
1386
+ return False
1387
+ self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.MEDIA_CREATION_TIME] = dict(media_creation_time)
1388
+ """
1389
+
1390
+ # create dock widgets for players
1304
1391
  for i in range(cfg.N_PLAYER):
1305
1392
  n_player = str(i + 1)
1306
1393
  if (
@@ -1309,17 +1396,424 @@ def initialize_new_media_observation(self) -> bool:
1309
1396
  ):
1310
1397
  continue
1311
1398
 
1399
+ # Not pretty but the unique solution I have found to capture the click signal for each player
1400
+
1312
1401
  if i == 0: # first player
1313
- p = player_dock_widget.DW_player(i, self)
1314
- self.dw_player.append(p)
1402
+ p0 = player_dock_widget.DW_player(0, self)
1315
1403
 
1316
- @p.player.property_observer("time-pos")
1317
- def time_observer(_name, value):
1318
- if value is not None:
1319
- self.time_observer_signal.emit(value)
1404
+ if not self.MPV_IPC_MODE:
1320
1405
 
1321
- else:
1322
- self.dw_player.append(player_dock_widget.DW_player(i, self))
1406
+ @p0.player.property_observer("time-pos")
1407
+ def time_observer(_name, value):
1408
+ if value is not None:
1409
+ self.time_observer_signal.emit(value)
1410
+
1411
+ @p0.player.property_observer("eof-reached")
1412
+ def eof_reached(_name, value):
1413
+ if value is not None:
1414
+ self.mpv_eof_reached_signal.emit(value)
1415
+
1416
+ @p0.player.on_key_press("MBTN_LEFT")
1417
+ def mbtn_left0():
1418
+ self.video_click_signal.emit(0, "MBTN_LEFT")
1419
+
1420
+ @p0.player.on_key_press("MBTN_RIGHT")
1421
+ def mbtn_right0():
1422
+ self.video_click_signal.emit(0, "MBTN_RIGHT")
1423
+
1424
+ @p0.player.on_key_press("MBTN_LEFT_DBL")
1425
+ def mbtn_left_dbl0():
1426
+ self.video_click_signal.emit(0, "MBTN_LEFT_DBL")
1427
+
1428
+ @p0.player.on_key_press("MBTN_RIGHT_DBL")
1429
+ def mbtn_right_dbl0():
1430
+ self.video_click_signal.emit(0, "MBTN_RIGHT_DBL")
1431
+
1432
+ @p0.player.on_key_press("Ctrl+WHEEL_UP")
1433
+ def ctrl_wheel_up0():
1434
+ self.video_click_signal.emit(0, "Ctrl+WHEEL_UP")
1435
+
1436
+ @p0.player.on_key_press("Ctrl+WHEEL_DOWN")
1437
+ def ctrl_wheel_down0():
1438
+ self.video_click_signal.emit(0, "Ctrl+WHEEL_DOWN")
1439
+
1440
+ @p0.player.on_key_press("WHEEL_UP")
1441
+ def wheel_up0():
1442
+ self.video_click_signal.emit(0, "WHEEL_UP")
1443
+
1444
+ @p0.player.on_key_press("WHEEL_DOWN")
1445
+ def wheel_down0():
1446
+ self.video_click_signal.emit(0, "WHEEL_DOWN")
1447
+
1448
+ @p0.player.on_key_press("Shift+WHEEL_UP")
1449
+ def shift_wheel_up0():
1450
+ self.video_click_signal.emit(0, "Shift+WHEEL_UP")
1451
+
1452
+ @p0.player.on_key_press("Shift+WHEEL_DOWN")
1453
+ def shift_wheel_down0():
1454
+ self.video_click_signal.emit(0, "Shift+WHEEL_DOWN")
1455
+
1456
+ @p0.player.on_key_press("Shift+MBTN_LEFT")
1457
+ def shift_mbtn_left0():
1458
+ self.video_click_signal.emit(0, "Shift+MBTN_LEFT")
1459
+
1460
+ self.dw_player.append(p0)
1461
+
1462
+ if i == 1: # second player
1463
+ p1 = player_dock_widget.DW_player(1, self)
1464
+
1465
+ if not self.MPV_IPC_MODE:
1466
+
1467
+ @p1.player.on_key_press("MBTN_LEFT")
1468
+ def mbtn_left1():
1469
+ self.video_click_signal.emit(1, "MBTN_LEFT")
1470
+
1471
+ @p1.player.on_key_press("MBTN_RIGHT")
1472
+ def mbtn_right1():
1473
+ self.video_click_signal.emit(1, "MBTN_RIGHT")
1474
+
1475
+ @p1.player.on_key_press("MBTN_LEFT_DBL")
1476
+ def mbtn_left_dbl1():
1477
+ self.video_click_signal.emit(1, "MBTN_LEFT_DBL")
1478
+
1479
+ @p1.player.on_key_press("MBTN_RIGHT_DBL")
1480
+ def mbtn_right_dbl1():
1481
+ self.video_click_signal.emit(1, "MBTN_RIGHT_DBL")
1482
+
1483
+ @p1.player.on_key_press("Ctrl+WHEEL_UP")
1484
+ def ctrl_wheel_up1():
1485
+ self.video_click_signal.emit(1, "Ctrl+WHEEL_UP")
1486
+
1487
+ @p1.player.on_key_press("Ctrl+WHEEL_DOWN")
1488
+ def ctrl_wheel_down1():
1489
+ self.video_click_signal.emit(1, "Ctrl+WHEEL_DOWN")
1490
+
1491
+ @p1.player.on_key_press("WHEEL_UP")
1492
+ def wheel_up1():
1493
+ self.video_click_signal.emit(1, "WHEEL_UP")
1494
+
1495
+ @p1.player.on_key_press("WHEEL_DOWN")
1496
+ def wheel_down1():
1497
+ self.video_click_signal.emit(1, "WHEEL_DOWN")
1498
+
1499
+ @p1.player.on_key_press("Shift+WHEEL_UP")
1500
+ def shift_wheel_up1():
1501
+ self.video_click_signal.emit(1, "Shift+WHEEL_UP")
1502
+
1503
+ @p1.player.on_key_press("Shift+WHEEL_DOWN")
1504
+ def shift_wheel_down1():
1505
+ self.video_click_signal.emit(1, "Shift+WHEEL_DOWN")
1506
+
1507
+ @p1.player.on_key_press("Shift+MBTN_LEFT")
1508
+ def shift_mbtn_left1():
1509
+ self.video_click_signal.emit(1, "Shift+MBTN_LEFT")
1510
+
1511
+ self.dw_player.append(p1)
1512
+
1513
+ if i == 2:
1514
+ p2 = player_dock_widget.DW_player(2, self)
1515
+
1516
+ if not self.MPV_IPC_MODE:
1517
+
1518
+ @p2.player.on_key_press("MBTN_LEFT")
1519
+ def mbtn_left2():
1520
+ self.video_click_signal.emit(2, "MBTN_LEFT")
1521
+
1522
+ @p2.player.on_key_press("MBTN_RIGHT")
1523
+ def mbtn_right2():
1524
+ self.video_click_signal.emit(2, "MBTN_RIGHT")
1525
+
1526
+ @p2.player.on_key_press("MBTN_LEFT_DBL")
1527
+ def mbtn_left_dbl2():
1528
+ self.video_click_signal.emit(2, "MBTN_LEFT_DBL")
1529
+
1530
+ @p2.player.on_key_press("MBTN_RIGHT_DBL")
1531
+ def mbtn_right_dbl2():
1532
+ self.video_click_signal.emit(2, "MBTN_RIGHT_DBL")
1533
+
1534
+ @p2.player.on_key_press("Ctrl+WHEEL_UP")
1535
+ def ctrl_wheel_up2():
1536
+ self.video_click_signal.emit(2, "Ctrl+WHEEL_UP")
1537
+
1538
+ @p2.player.on_key_press("Ctrl+WHEEL_DOWN")
1539
+ def ctrl_wheel_down2():
1540
+ self.video_click_signal.emit(2, "Ctrl+WHEEL_DOWN")
1541
+
1542
+ @p2.player.on_key_press("WHEEL_UP")
1543
+ def wheel_up2():
1544
+ self.video_click_signal.emit(2, "WHEEL_UP")
1545
+
1546
+ @p2.player.on_key_press("WHEEL_DOWN")
1547
+ def wheel_down2():
1548
+ self.video_click_signal.emit(2, "WHEEL_DOWN")
1549
+
1550
+ @p2.player.on_key_press("Shift+WHEEL_UP")
1551
+ def shift_wheel_up2():
1552
+ self.video_click_signal.emit(2, "Shift+WHEEL_UP")
1553
+
1554
+ @p2.player.on_key_press("Shift+WHEEL_DOWN")
1555
+ def shift_wheel_down2():
1556
+ self.video_click_signal.emit(2, "Shift+WHEEL_DOWN")
1557
+
1558
+ @p2.player.on_key_press("Shift+MBTN_LEFT")
1559
+ def shift_mbtn_left2():
1560
+ self.video_click_signal.emit(2, "Shift+MBTN_LEFT")
1561
+
1562
+ self.dw_player.append(p2)
1563
+
1564
+ if i == 3:
1565
+ p3 = player_dock_widget.DW_player(3, self)
1566
+
1567
+ if not self.MPV_IPC_MODE:
1568
+
1569
+ @p3.player.on_key_press("MBTN_LEFT")
1570
+ def mbtn_left3():
1571
+ self.video_click_signal.emit(3, "MBTN_LEFT")
1572
+
1573
+ @p3.player.on_key_press("MBTN_RIGHT")
1574
+ def mbtn_right3():
1575
+ self.video_click_signal.emit(3, "MBTN_RIGHT")
1576
+
1577
+ @p3.player.on_key_press("MBTN_LEFT_DBL")
1578
+ def mbtn_left_dbl3():
1579
+ self.video_click_signal.emit(3, "MBTN_LEFT_DBL")
1580
+
1581
+ @p3.player.on_key_press("MBTN_RIGHT_DBL")
1582
+ def mbtn_right_dbl3():
1583
+ self.video_click_signal.emit(3, "MBTN_RIGHT_DBL")
1584
+
1585
+ @p3.player.on_key_press("Ctrl+WHEEL_UP")
1586
+ def ctrl_wheel_up3():
1587
+ self.video_click_signal.emit(3, "Ctrl+WHEEL_UP")
1588
+
1589
+ @p3.player.on_key_press("Ctrl+WHEEL_DOWN")
1590
+ def ctrl_wheel_down3():
1591
+ self.video_click_signal.emit(3, "Ctrl+WHEEL_DOWN")
1592
+
1593
+ @p3.player.on_key_press("WHEEL_UP")
1594
+ def wheel_up3():
1595
+ self.video_click_signal.emit(3, "WHEEL_UP")
1596
+
1597
+ @p3.player.on_key_press("WHEEL_DOWN")
1598
+ def wheel_down3():
1599
+ self.video_click_signal.emit(3, "WHEEL_DOWN")
1600
+
1601
+ @p3.player.on_key_press("Shift+WHEEL_UP")
1602
+ def shift_wheel_up3():
1603
+ self.video_click_signal.emit(3, "Shift+WHEEL_UP")
1604
+
1605
+ @p3.player.on_key_press("Shift+WHEEL_DOWN")
1606
+ def shift_wheel_down3():
1607
+ self.video_click_signal.emit(3, "Shift+WHEEL_DOWN")
1608
+
1609
+ @p3.player.on_key_press("Shift+MBTN_LEFT")
1610
+ def shift_mbtn_left3():
1611
+ self.video_click_signal.emit(3, "Shift+MBTN_LEFT")
1612
+
1613
+ self.dw_player.append(p3)
1614
+
1615
+ if i == 4:
1616
+ p4 = player_dock_widget.DW_player(4, self)
1617
+
1618
+ if not self.MPV_IPC_MODE:
1619
+
1620
+ @p4.player.on_key_press("MBTN_LEFT")
1621
+ def mbtn_left4():
1622
+ self.video_click_signal.emit(4, "MBTN_LEFT")
1623
+
1624
+ @p4.player.on_key_press("MBTN_RIGHT")
1625
+ def mbtn_right4():
1626
+ self.video_click_signal.emit(4, "MBTN_RIGHT")
1627
+
1628
+ @p4.player.on_key_press("MBTN_LEFT_DBL")
1629
+ def mbtn_left_dbl4():
1630
+ self.video_click_signal.emit(4, "MBTN_LEFT_DBL")
1631
+
1632
+ @p4.player.on_key_press("MBTN_RIGHT_DBL")
1633
+ def mbtn_right_dbl4():
1634
+ self.video_click_signal.emit(4, "MBTN_RIGHT_DBL")
1635
+
1636
+ @p4.player.on_key_press("Ctrl+WHEEL_UP")
1637
+ def ctrl_wheel_up4():
1638
+ self.video_click_signal.emit(4, "Ctrl+WHEEL_UP")
1639
+
1640
+ @p4.player.on_key_press("Ctrl+WHEEL_DOWN")
1641
+ def ctrl_wheel_down4():
1642
+ self.video_click_signal.emit(4, "Ctrl+WHEEL_DOWN")
1643
+
1644
+ @p4.player.on_key_press("WHEEL_UP")
1645
+ def wheel_up4():
1646
+ self.video_click_signal.emit(4, "WHEEL_UP")
1647
+
1648
+ @p4.player.on_key_press("WHEEL_DOWN")
1649
+ def wheel_down4():
1650
+ self.video_click_signal.emit(4, "WHEEL_DOWN")
1651
+
1652
+ @p4.player.on_key_press("Shift+WHEEL_UP")
1653
+ def shift_wheel_up4():
1654
+ self.video_click_signal.emit(4, "Shift+WHEEL_UP")
1655
+
1656
+ @p4.player.on_key_press("Shift+WHEEL_DOWN")
1657
+ def shift_wheel_down4():
1658
+ self.video_click_signal.emit(4, "Shift+WHEEL_DOWN")
1659
+
1660
+ @p4.player.on_key_press("Shift+MBTN_LEFT")
1661
+ def shift_mbtn_left4():
1662
+ self.video_click_signal.emit(4, "Shift+MBTN_LEFT")
1663
+
1664
+ self.dw_player.append(p4)
1665
+
1666
+ if i == 5:
1667
+ p5 = player_dock_widget.DW_player(5, self)
1668
+
1669
+ if not self.MPV_IPC_MODE:
1670
+
1671
+ @p5.player.on_key_press("MBTN_LEFT")
1672
+ def mbtn_left5():
1673
+ self.video_click_signal.emit(5, "MBTN_LEFT")
1674
+
1675
+ @p5.player.on_key_press("MBTN_RIGHT")
1676
+ def mbtn_right5():
1677
+ self.video_click_signal.emit(5, "MBTN_RIGHT")
1678
+
1679
+ @p5.player.on_key_press("MBTN_LEFT_DBL")
1680
+ def mbtn_left_dbl5():
1681
+ self.video_click_signal.emit(5, "MBTN_LEFT_DBL")
1682
+
1683
+ @p5.player.on_key_press("MBTN_RIGHT_DBL")
1684
+ def mbtn_right_dbl5():
1685
+ self.video_click_signal.emit(5, "MBTN_RIGHT_DBL")
1686
+
1687
+ @p5.player.on_key_press("Ctrl+WHEEL_UP")
1688
+ def ctrl_wheel_up5():
1689
+ self.video_click_signal.emit(5, "Ctrl+WHEEL_UP")
1690
+
1691
+ @p5.player.on_key_press("Ctrl+WHEEL_DOWN")
1692
+ def ctrl_wheel_down5():
1693
+ self.video_click_signal.emit(5, "Ctrl+WHEEL_DOWN")
1694
+
1695
+ @p5.player.on_key_press("WHEEL_UP")
1696
+ def wheel_up5():
1697
+ self.video_click_signal.emit(5, "WHEEL_UP")
1698
+
1699
+ @p5.player.on_key_press("WHEEL_DOWN")
1700
+ def wheel_down5():
1701
+ self.video_click_signal.emit(5, "WHEEL_DOWN")
1702
+
1703
+ @p5.player.on_key_press("Shift+WHEEL_UP")
1704
+ def shift_wheel_up5():
1705
+ self.video_click_signal.emit(5, "Shift+WHEEL_UP")
1706
+
1707
+ @p5.player.on_key_press("Shift+WHEEL_DOWN")
1708
+ def shift_wheel_down5():
1709
+ self.video_click_signal.emit(5, "Shift+WHEEL_DOWN")
1710
+
1711
+ @p5.player.on_key_press("Shift+MBTN_LEFT")
1712
+ def shift_mbtn_left5():
1713
+ self.video_click_signal.emit(5, "Shift+MBTN_LEFT")
1714
+
1715
+ self.dw_player.append(p5)
1716
+
1717
+ if i == 6:
1718
+ p6 = player_dock_widget.DW_player(6, self)
1719
+ if not self.MPV_IPC_MODE:
1720
+
1721
+ @p6.player.on_key_press("MBTN_LEFT")
1722
+ def mbtn_left6():
1723
+ self.video_click_signal.emit(6, "MBTN_LEFT")
1724
+
1725
+ @p6.player.on_key_press("MBTN_RIGHT")
1726
+ def mbtn_right6():
1727
+ self.video_click_signal.emit(6, "MBTN_RIGHT")
1728
+
1729
+ @p6.player.on_key_press("MBTN_LEFT_DBL")
1730
+ def mbtn_left_dbl6():
1731
+ self.video_click_signal.emit(6, "MBTN_LEFT_DBL")
1732
+
1733
+ @p6.player.on_key_press("MBTN_RIGHT_DBL")
1734
+ def mbtn_right_dbl6():
1735
+ self.video_click_signal.emit(6, "MBTN_RIGHT_DBL")
1736
+
1737
+ @p6.player.on_key_press("Ctrl+WHEEL_UP")
1738
+ def ctrl_wheel_up6():
1739
+ self.video_click_signal.emit(6, "Ctrl+WHEEL_UP")
1740
+
1741
+ @p6.player.on_key_press("Ctrl+WHEEL_DOWN")
1742
+ def ctrl_wheel_down6():
1743
+ self.video_click_signal.emit(6, "Ctrl+WHEEL_DOWN")
1744
+
1745
+ @p6.player.on_key_press("WHEEL_UP")
1746
+ def wheel_up6():
1747
+ self.video_click_signal.emit(6, "WHEEL_UP")
1748
+
1749
+ @p6.player.on_key_press("WHEEL_DOWN")
1750
+ def wheel_down6():
1751
+ self.video_click_signal.emit(6, "WHEEL_DOWN")
1752
+
1753
+ @p6.player.on_key_press("Shift+WHEEL_UP")
1754
+ def shift_wheel_up6():
1755
+ self.video_click_signal.emit(6, "Shift+WHEEL_UP")
1756
+
1757
+ @p6.player.on_key_press("Shift+WHEEL_DOWN")
1758
+ def shift_wheel_down6():
1759
+ self.video_click_signal.emit(6, "Shift+WHEEL_DOWN")
1760
+
1761
+ @p6.player.on_key_press("Shift+MBTN_LEFT")
1762
+ def shift_mbtn_left6():
1763
+ self.video_click_signal.emit(6, "Shift+MBTN_LEFT")
1764
+
1765
+ self.dw_player.append(p6)
1766
+
1767
+ if i == 7:
1768
+ p7 = player_dock_widget.DW_player(7, self)
1769
+
1770
+ if not self.MPV_IPC_MODE:
1771
+
1772
+ @p7.player.on_key_press("MBTN_LEFT")
1773
+ def mbtn_left7():
1774
+ self.video_click_signal.emit(7, "MBTN_LEFT")
1775
+
1776
+ @p7.player.on_key_press("MBTN_RIGHT")
1777
+ def mbtn_right7():
1778
+ self.video_click_signal.emit(7, "MBTN_RIGHT")
1779
+
1780
+ @p7.player.on_key_press("MBTN_LEFT_DBL")
1781
+ def mbtn_left_dbl7():
1782
+ self.video_click_signal.emit(7, "MBTN_LEFT_DBL")
1783
+
1784
+ @p7.player.on_key_press("MBTN_RIGHT_DBL")
1785
+ def mbtn_right_dbl7():
1786
+ self.video_click_signal.emit(7, "MBTN_RIGHT_DBL")
1787
+
1788
+ @p7.player.on_key_press("Ctrl+WHEEL_UP")
1789
+ def ctrl_wheel_up7():
1790
+ self.video_click_signal.emit(7, "Ctrl+WHEEL_UP")
1791
+
1792
+ @p7.player.on_key_press("Ctrl+WHEEL_DOWN")
1793
+ def ctrl_wheel_down7():
1794
+ self.video_click_signal.emit(7, "Ctrl+WHEEL_DOWN")
1795
+
1796
+ @p7.player.on_key_press("WHEEL_UP")
1797
+ def wheel_up7():
1798
+ self.video_click_signal.emit(7, "WHEEL_UP")
1799
+
1800
+ @p7.player.on_key_press("WHEEL_DOWN")
1801
+ def wheel_down7():
1802
+ self.video_click_signal.emit(7, "WHEEL_DOWN")
1803
+
1804
+ @p7.player.on_key_press("Shift+WHEEL_UP")
1805
+ def shift_wheel_up7():
1806
+ self.video_click_signal.emit(7, "Shift+WHEEL_UP")
1807
+
1808
+ @p7.player.on_key_press("Shift+WHEEL_DOWN")
1809
+ def shift_wheel_down7():
1810
+ self.video_click_signal.emit(7, "Shift+WHEEL_DOWN")
1811
+
1812
+ @p7.player.on_key_press("Shift+MBTN_LEFT")
1813
+ def shift_mbtn_left7():
1814
+ self.video_click_signal.emit(7, "Shift+MBTN_LEFT")
1815
+
1816
+ self.dw_player.append(p7)
1323
1817
 
1324
1818
  self.dw_player[-1].setFloating(False)
1325
1819
  self.dw_player[-1].setVisible(False)
@@ -1339,12 +1833,11 @@ def initialize_new_media_observation(self) -> bool:
1339
1833
  # for receiving event from volume slider
1340
1834
  self.dw_player[i].volume_slider_moved_signal.connect(self.set_volume)
1341
1835
 
1836
+ # for receiving event from mute toolbutton
1837
+ self.dw_player[i].mute_action_triggered_signal.connect(self.set_mute)
1838
+
1342
1839
  # for receiving resize event from dock widget
1343
1840
  self.dw_player[i].resize_signal.connect(self.resize_dw)
1344
- """
1345
- # for receiving event resize and clicked (Zoom - crop)
1346
- self.dw_player[i].view_signal.connect(self.signal_from_dw)
1347
- """
1348
1841
 
1349
1842
  # add durations list
1350
1843
  self.dw_player[i].media_durations = []
@@ -1353,8 +1846,21 @@ def initialize_new_media_observation(self) -> bool:
1353
1846
  # add fps list
1354
1847
  self.dw_player[i].fps = {}
1355
1848
 
1356
- for mediaFile in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][n_player]:
1849
+ if self.MPV_IPC_MODE:
1850
+ while True:
1851
+ r = util.test_mpv_ipc(f"{cfg.MPV_SOCKET}{i}")
1852
+ logging.debug(f"MPV IPC started: {r}")
1853
+ if r:
1854
+ break
1855
+
1856
+ # start timer for activating the main window
1857
+ self.main_window_activation_timer = QTimer()
1858
+ self.main_window_activation_timer.setInterval(500)
1859
+ self.main_window_activation_timer.timeout.connect(self.activateWindow)
1860
+ self.main_window_activation_timer.start()
1861
+
1357
1862
 
1863
+ for mediaFile in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.FILE][n_player]:
1358
1864
  logging.debug(f"media file: {mediaFile}")
1359
1865
 
1360
1866
  media_full_path = project_functions.full_path(mediaFile, self.projectFileName)
@@ -1363,13 +1869,10 @@ def initialize_new_media_observation(self) -> bool:
1363
1869
 
1364
1870
  # media duration
1365
1871
  try:
1366
- mediaLength = (
1367
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.LENGTH][mediaFile] * 1000
1368
- )
1872
+ mediaLength = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.LENGTH][mediaFile] * 1000
1369
1873
  mediaFPS = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.FPS][mediaFile]
1370
1874
  except Exception:
1371
-
1372
- logging.debug("media_info key not found")
1875
+ logging.debug("media_info key not found in project")
1373
1876
 
1374
1877
  r = util.accurate_media_analysis(self.ffmpeg_bin, media_full_path)
1375
1878
  if "error" not in r:
@@ -1392,67 +1895,76 @@ def initialize_new_media_observation(self) -> bool:
1392
1895
  self.project_changed()
1393
1896
 
1394
1897
  self.dw_player[i].media_durations.append(int(mediaLength))
1395
- self.dw_player[i].cumul_media_durations.append(
1396
- self.dw_player[i].cumul_media_durations[-1] + int(mediaLength)
1397
- )
1898
+ self.dw_player[i].cumul_media_durations.append(self.dw_player[i].cumul_media_durations[-1] + int(mediaLength))
1398
1899
 
1399
1900
  self.dw_player[i].fps[mediaFile] = mediaFPS
1400
1901
 
1902
+ # add media file to playlist
1401
1903
  self.dw_player[i].player.playlist_append(media_full_path)
1402
- # self.dw_player[i].player.loadfile(media_full_path)
1403
- # self.dw_player[i].player.pause = True
1404
1904
 
1405
- # check if BORIS is running on a Windows VM with WMIC COMPUTERSYSTEM GET SERIALNUMBER
1905
+ # add media file name to player window title
1906
+ self.dw_player[i].setWindowTitle(f"Player #{i + 1} ({pl.Path(media_full_path).name})")
1907
+
1908
+ # media duration cumuled in seconds
1909
+ self.dw_player[i].cumul_media_durations_sec = [round(dec(x / 1000), 3) for x in self.dw_player[i].cumul_media_durations]
1910
+
1911
+ # check if BORIS is running on a Windows VM with the 'WMIC COMPUTERSYSTEM GET SERIALNUMBER' command
1406
1912
  # because "auto" or "auto-safe" crash in Windows VM
1407
1913
  # see https://superuser.com/questions/1128339/how-can-i-detect-if-im-within-a-vm-or-not
1408
1914
 
1409
- flag_vm = False
1410
- if sys.platform.startswith("win"):
1411
- p = subprocess.Popen(
1412
- ["WMIC", "BIOS", "GET", "SERIALNUMBER"],
1413
- stdout=subprocess.PIPE,
1414
- stderr=subprocess.PIPE,
1415
- shell=True,
1416
- )
1417
- out, _ = p.communicate()
1418
- flag_vm = b"SerialNumber \r\r\n0 " in out
1419
- logging.debug(f"Running on Windows VM: {flag_vm}")
1915
+ if not self.MPV_IPC_MODE:
1916
+ flag_vm = False
1917
+ if sys.platform.startswith("win"):
1918
+ p = subprocess.Popen(
1919
+ ["WMIC", "BIOS", "GET", "SERIALNUMBER"],
1920
+ stdout=subprocess.PIPE,
1921
+ stderr=subprocess.PIPE,
1922
+ shell=True,
1923
+ )
1924
+ out, _ = p.communicate()
1925
+ flag_vm = b"SerialNumber \r\r\n0 " in out
1926
+ logging.debug(f"Running on Windows VM: {flag_vm}")
1420
1927
 
1421
- if not flag_vm:
1422
- self.dw_player[i].player.hwdec = self.config_param.get(cfg.MPV_HWDEC, cfg.MPV_HWDEC_DEFAULT_VALUE)
1423
- else:
1424
- self.dw_player[i].player.hwdec = "no"
1928
+ if not flag_vm:
1929
+ self.dw_player[i].player.hwdec = self.config_param.get(cfg.MPV_HWDEC, cfg.MPV_HWDEC_DEFAULT_VALUE)
1930
+ else:
1931
+ self.dw_player[i].player.hwdec = cfg.MPV_HWDEC_NO
1932
+
1933
+ logging.debug(f"Player hwdec of player #{i} set to: {self.dw_player[i].player.hwdec}")
1934
+ self.config_param[cfg.MPV_HWDEC] = self.dw_player[i].player.hwdec
1425
1935
 
1426
1936
  self.dw_player[i].player.playlist_pos = 0
1427
1937
  self.dw_player[i].player.wait_until_playing()
1428
1938
  self.dw_player[i].player.pause = True
1429
- self.dw_player[i].player.wait_until_paused()
1939
+ time.sleep(0.2)
1940
+ # self.dw_player[i].player.wait_until_paused()
1430
1941
  self.dw_player[i].player.seek(0, "absolute")
1431
1942
  # do not close when playing finished
1432
1943
  self.dw_player[i].player.keep_open = True
1433
1944
  self.dw_player[i].player.keep_open_pause = False
1434
1945
 
1435
- self.dw_player[i].player.image_display_duration = self.pj[cfg.OBSERVATIONS][self.observationId].get(
1436
- cfg.IMAGE_DISPLAY_DURATION, 1
1437
- )
1946
+ self.dw_player[i].player.image_display_duration = self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.IMAGE_DISPLAY_DURATION, 1)
1438
1947
 
1439
1948
  # position media
1440
- if cfg.OBSERVATION_TIME_INTERVAL in self.pj[cfg.OBSERVATIONS][self.observationId]:
1441
- self.seek_mediaplayer(
1442
- int(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.OBSERVATION_TIME_INTERVAL][0]), player=i
1443
- )
1949
+ self.seek_mediaplayer(int(self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])[0]), player=i)
1444
1950
 
1445
- # restore zoom level
1951
+ # restore video zoom level
1446
1952
  if cfg.ZOOM_LEVEL in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
1447
1953
  self.dw_player[i].player.video_zoom = log2(
1448
1954
  self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.ZOOM_LEVEL].get(n_player, 0)
1449
1955
  )
1450
1956
 
1957
+ # restore video pan
1958
+ if cfg.PAN_X in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
1959
+ self.dw_player[i].player.video_pan_x = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.PAN_X].get(n_player, 0)
1960
+ if cfg.PAN_Y in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
1961
+ self.dw_player[i].player.video_pan_y = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.PAN_Y].get(n_player, 0)
1962
+
1451
1963
  # restore rotation angle
1452
1964
  if cfg.ROTATION_ANGLE in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
1453
- self.dw_player[i].player.video_rotate = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][
1454
- cfg.ROTATION_ANGLE
1455
- ].get(n_player, 0)
1965
+ self.dw_player[i].player.video_rotate = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.ROTATION_ANGLE].get(
1966
+ n_player, 0
1967
+ )
1456
1968
 
1457
1969
  # restore subtitle visibility
1458
1970
  if cfg.DISPLAY_MEDIA_SUBTITLES in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
@@ -1461,7 +1973,6 @@ def initialize_new_media_observation(self) -> bool:
1461
1973
  ].get(n_player, True)
1462
1974
 
1463
1975
  # restore overlays
1464
-
1465
1976
  if cfg.OVERLAY in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO]:
1466
1977
  if n_player in self.pj[cfg.OBSERVATIONS][self.observationId][cfg.MEDIA_INFO][cfg.OVERLAY]:
1467
1978
  self.overlays[i] = self.dw_player[i].player.create_image_overlay()
@@ -1469,9 +1980,20 @@ def initialize_new_media_observation(self) -> bool:
1469
1980
 
1470
1981
  menu_options.update_menu(self)
1471
1982
 
1472
- self.time_observer_signal.connect(self.mpv_timer_out)
1983
+ if self.MPV_IPC_MODE:
1984
+ # activate timer
1985
+ self.ipc_mpv_timer = QTimer()
1986
+ self.ipc_mpv_timer.setInterval(500)
1987
+ self.ipc_mpv_timer.timeout.connect(self.mpv_timer_out)
1988
+
1989
+ else:
1990
+ self.ipc_mpv_timer = None
1991
+ self.time_observer_signal.connect(self.mpv_timer_out)
1992
+
1993
+ self.mpv_eof_reached_signal.connect(self.mpv_eof_reached)
1994
+ self.video_click_signal.connect(self.player_clicked)
1473
1995
 
1474
- self.actionPlay.setIcon(QIcon(":/play"))
1996
+ self.actionPlay.setIcon(QIcon(f":/play_{gui_utilities.theme_mode()}"))
1475
1997
 
1476
1998
  self.display_statusbar_info(self.observationId)
1477
1999
 
@@ -1480,24 +2002,17 @@ def initialize_new_media_observation(self) -> bool:
1480
2002
  self.state_behaviors_codes = tuple(util.state_behavior_codes(self.pj[cfg.ETHOGRAM]))
1481
2003
 
1482
2004
  video_operations.display_play_rate(self)
2005
+ video_operations.display_zoom_level(self)
1483
2006
 
1484
2007
  # spectrogram
1485
2008
  if (
1486
2009
  cfg.VISUALIZE_SPECTROGRAM in self.pj[cfg.OBSERVATIONS][self.observationId]
1487
2010
  and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.VISUALIZE_SPECTROGRAM]
1488
2011
  ):
1489
-
1490
- tmp_dir = (
1491
- self.ffmpeg_cache_dir
1492
- if self.ffmpeg_cache_dir and os.path.isdir(self.ffmpeg_cache_dir)
1493
- else tempfile.gettempdir()
1494
- )
2012
+ tmp_dir = self.ffmpeg_cache_dir if self.ffmpeg_cache_dir and os.path.isdir(self.ffmpeg_cache_dir) else tempfile.gettempdir()
1495
2013
 
1496
2014
  wav_file_path = (
1497
- pl.Path(tmp_dir)
1498
- / pl.Path(
1499
- self.dw_player[0].player.playlist[self.dw_player[0].player.playlist_pos]["filename"] + ".wav"
1500
- ).name
2015
+ pl.Path(tmp_dir) / pl.Path(self.dw_player[0].player.playlist[self.dw_player[0].player.playlist_pos]["filename"] + ".wav").name
1501
2016
  )
1502
2017
 
1503
2018
  if not wav_file_path.is_file():
@@ -1510,18 +2025,10 @@ def initialize_new_media_observation(self) -> bool:
1510
2025
  cfg.VISUALIZE_WAVEFORM in self.pj[cfg.OBSERVATIONS][self.observationId]
1511
2026
  and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.VISUALIZE_WAVEFORM]
1512
2027
  ):
1513
-
1514
- tmp_dir = (
1515
- self.ffmpeg_cache_dir
1516
- if self.ffmpeg_cache_dir and os.path.isdir(self.ffmpeg_cache_dir)
1517
- else tempfile.gettempdir()
1518
- )
2028
+ tmp_dir = self.ffmpeg_cache_dir if self.ffmpeg_cache_dir and os.path.isdir(self.ffmpeg_cache_dir) else tempfile.gettempdir()
1519
2029
 
1520
2030
  wav_file_path = (
1521
- pl.Path(tmp_dir)
1522
- / pl.Path(
1523
- self.dw_player[0].player.playlist[self.dw_player[0].player.playlist_pos]["filename"] + ".wav"
1524
- ).name
2031
+ pl.Path(tmp_dir) / pl.Path(self.dw_player[0].player.playlist[self.dw_player[0].player.playlist_pos]["filename"] + ".wav").name
1525
2032
  )
1526
2033
 
1527
2034
  if not wav_file_path.is_file():
@@ -1530,11 +2037,7 @@ def initialize_new_media_observation(self) -> bool:
1530
2037
  self.show_plot_widget("waveform", warning=False)
1531
2038
 
1532
2039
  # external data plot
1533
- if (
1534
- cfg.PLOT_DATA in self.pj[cfg.OBSERVATIONS][self.observationId]
1535
- and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA]
1536
- ):
1537
-
2040
+ if cfg.PLOT_DATA in self.pj[cfg.OBSERVATIONS][self.observationId] and self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA]:
1538
2041
  self.plot_data = {}
1539
2042
  self.ext_data_timer_list = []
1540
2043
  count = 0
@@ -1549,9 +2052,7 @@ def initialize_new_media_observation(self) -> bool:
1549
2052
  QMessageBox.critical(
1550
2053
  self,
1551
2054
  cfg.programName,
1552
- "Data file not found:\n{}".format(
1553
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["file_path"]
1554
- ),
2055
+ "Data file not found:\n{}".format(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["file_path"]),
1555
2056
  )
1556
2057
  data_ok = False
1557
2058
  # return False
@@ -1575,7 +2076,8 @@ def initialize_new_media_observation(self) -> bool:
1575
2076
  self,
1576
2077
  cfg.programName,
1577
2078
  (
1578
- f"Impossible to plot data from file {os.path.basename(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]['file_path'])}:\n"
2079
+ "Impossible to plot data from file "
2080
+ f"{os.path.basename(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]['file_path'])}:\n"
1579
2081
  f"{w1.error_msg}"
1580
2082
  ),
1581
2083
  )
@@ -1606,9 +2108,7 @@ def initialize_new_media_observation(self) -> bool:
1606
2108
  QMessageBox.critical(
1607
2109
  self,
1608
2110
  cfg.programName,
1609
- "Data file not found:\n{}".format(
1610
- self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["file_path"]
1611
- ),
2111
+ "Data file not found:\n{}".format(self.pj[cfg.OBSERVATIONS][self.observationId][cfg.PLOT_DATA][idx]["file_path"]),
1612
2112
  )
1613
2113
  data_ok = False
1614
2114
  # return False
@@ -1673,9 +2173,12 @@ def initialize_new_media_observation(self) -> bool:
1673
2173
  for player in self.dw_player:
1674
2174
  player.setVisible(True)
1675
2175
 
2176
+ self.load_tw_events(self.observationId)
2177
+
1676
2178
  # initial synchro
1677
- for n_player in range(1, len(self.dw_player)):
1678
- self.sync_time(n_player, 0)
2179
+ if not self.MPV_IPC_MODE:
2180
+ for n_player in range(1, len(self.dw_player)):
2181
+ self.sync_time(n_player, 0)
1679
2182
 
1680
2183
  self.mpv_timer_out(value=0.0)
1681
2184
 
@@ -1710,6 +2213,7 @@ def initialize_new_live_observation(self):
1710
2213
 
1711
2214
  # button start enabled
1712
2215
  self.pb_live_obs.setEnabled(True)
2216
+
1713
2217
  self.w_live.setVisible(True)
1714
2218
  self.w_obs_info.setVisible(True)
1715
2219
 
@@ -1719,9 +2223,9 @@ def initialize_new_live_observation(self):
1719
2223
  self.pb_live_obs.setText("Start live observation")
1720
2224
 
1721
2225
  if self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.START_FROM_CURRENT_TIME, False):
1722
- current_time = util.seconds_of_day(datetime.datetime.now())
2226
+ current_time = util.seconds_of_day(dt.datetime.now())
1723
2227
  elif self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.START_FROM_CURRENT_EPOCH_TIME, False):
1724
- current_time = time.mktime(datetime.datetime.now().timetuple())
2228
+ current_time = time.mktime(dt.datetime.now().timetuple())
1725
2229
  else:
1726
2230
  current_time = 0
1727
2231
 
@@ -1740,6 +2244,8 @@ def initialize_new_live_observation(self):
1740
2244
  self.liveStartTime = None
1741
2245
  self.liveTimer.stop()
1742
2246
 
2247
+ self.load_tw_events(self.observationId)
2248
+
1743
2249
  self.get_events_current_row()
1744
2250
 
1745
2251
 
@@ -1748,16 +2254,14 @@ def initialize_new_images_observation(self):
1748
2254
  initialize a new observation from directories of images
1749
2255
  """
1750
2256
 
1751
- for dw in [self.dwEthogram, self.dwSubjects, self.dwEvents]:
2257
+ for dw in (self.dwEthogram, self.dwSubjects, self.dwEvents):
1752
2258
  dw.setVisible(True)
1753
2259
  # disable start live button
1754
2260
  self.pb_live_obs.setEnabled(False)
1755
2261
  self.w_live.setVisible(False)
1756
2262
 
1757
2263
  # check if directories are available
1758
- ok, msg = project_functions.check_directories_availability(
1759
- self.pj[cfg.OBSERVATIONS][self.observationId], self.projectFileName
1760
- )
2264
+ ok, msg = project_functions.check_directories_availability(self.pj[cfg.OBSERVATIONS][self.observationId], self.projectFileName)
1761
2265
 
1762
2266
  if not ok:
1763
2267
  QMessageBox.critical(
@@ -1777,7 +2281,8 @@ def initialize_new_images_observation(self):
1777
2281
  # count number of images in all directories
1778
2282
  tot_images_number = 0
1779
2283
  for dir_path in self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.DIRECTORIES_LIST, []):
1780
- result = util.dir_images_number(dir_path)
2284
+ full_dir_path = project_functions.full_path(dir_path, self.projectFileName)
2285
+ result = util.dir_images_number(full_dir_path)
1781
2286
  tot_images_number += result.get("number of images", 0)
1782
2287
 
1783
2288
  if not tot_images_number:
@@ -1785,7 +2290,7 @@ def initialize_new_images_observation(self):
1785
2290
  self,
1786
2291
  cfg.programName,
1787
2292
  (
1788
- f"No images were found in directory(ies).<br><br>The observation will be opened in VIEW mode.<br>"
2293
+ "No images were found in directory(ies).<br><br>The observation will be opened in VIEW mode.<br>"
1789
2294
  "It will not be possible to log events.<br>"
1790
2295
  "Modify the directoriy path(s) to point existing directory "
1791
2296
  ),
@@ -1799,15 +2304,16 @@ def initialize_new_images_observation(self):
1799
2304
  # load image paths
1800
2305
  # directories user order is maintained
1801
2306
  # images are sorted inside each directory
1802
- self.images_list = []
2307
+ self.images_list: list = []
1803
2308
  for dir_path in self.pj[cfg.OBSERVATIONS][self.observationId].get(cfg.DIRECTORIES_LIST, []):
2309
+ full_dir_path = project_functions.full_path(dir_path, self.projectFileName)
1804
2310
  for pattern in cfg.IMAGE_EXTENSIONS:
1805
2311
  self.images_list.extend(
1806
2312
  sorted(
1807
2313
  list(
1808
2314
  set(
1809
- [str(x) for x in pl.Path(dir_path).glob(pattern)]
1810
- + [str(x) for x in pl.Path(dir_path).glob(pattern.upper())]
2315
+ [str(x) for x in pl.Path(full_dir_path).glob(pattern)]
2316
+ + [str(x) for x in pl.Path(full_dir_path).glob(pattern.upper())]
1811
2317
  )
1812
2318
  )
1813
2319
  )
@@ -1872,31 +2378,160 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
1872
2378
  """
1873
2379
  returns the media file name corresponding to the event (start time in case of state event)
1874
2380
 
2381
+ Args:
2382
+ observation (dict): observation
2383
+ timestamp (dec): time stamp
2384
+
1875
2385
  Returns:
1876
- str: name of media file containing the event
2386
+ str: path of media file containing the event
1877
2387
  """
2388
+ if observation.get(cfg.MEDIA_CREATION_DATE_AS_OFFSET, False):
2389
+ # media creation date/time was used for coding
2390
+ video_file_name = None
2391
+ for media_path in observation[cfg.MEDIA_INFO].get(cfg.MEDIA_CREATION_TIME, {}):
2392
+ start_media = observation[cfg.MEDIA_INFO][cfg.MEDIA_CREATION_TIME][media_path]
2393
+ duration = observation[cfg.MEDIA_INFO][cfg.LENGTH][media_path]
2394
+ if start_media <= timestamp <= start_media + duration:
2395
+ video_file_name = media_path
2396
+ break
2397
+
2398
+ else: # no media creation date
2399
+ cumul_media_durations: list = [dec(0)]
2400
+ for media_file in observation[cfg.FILE][cfg.PLAYER1]:
2401
+ try:
2402
+ media_duration = observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]
2403
+ # cut off media duration to 3 decimal places as that is how fine the player is
2404
+ media_duration = floor(media_duration * 10**3) / dec(10**3)
2405
+ cumul_media_durations.append(floor((cumul_media_durations[-1] + media_duration) * 10**3) / dec(10**3))
2406
+ except KeyError:
2407
+ return None
2408
+
2409
+ """
2410
+ cumul_media_durations: list = [dec(0)]
2411
+ for media_file in observation[cfg.FILE][cfg.PLAYER1]:
2412
+ try:
2413
+ media_duration = dec(str(observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]))
2414
+ cumul_media_durations.append(round(cumul_media_durations[-1] + media_duration, 3))
2415
+ except KeyError:
2416
+ return None
2417
+ """
2418
+
2419
+ cumul_media_durations.remove(dec(0))
2420
+
2421
+ logging.debug(f"{cumul_media_durations=}")
2422
+
2423
+ # test if timestamp is at end of last media
2424
+ if timestamp == cumul_media_durations[-1]:
2425
+ player_idx = len(observation[cfg.FILE][cfg.PLAYER1]) - 1
2426
+ else:
2427
+ player_idx = None
2428
+ for idx, value in enumerate(cumul_media_durations):
2429
+ start = 0 if idx == 0 else cumul_media_durations[idx - 1]
2430
+ if start <= timestamp < value:
2431
+ player_idx = idx
2432
+ break
1878
2433
 
1879
- cumul_media_durations: list = [dec(0)]
1880
- for media_file in observation[cfg.FILE]["1"]:
1881
- media_duration = dec(str(observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]))
1882
- cumul_media_durations.append(cumul_media_durations[-1] + media_duration)
2434
+ video_file_name = observation[cfg.FILE][cfg.PLAYER1][player_idx] if player_idx is not None else None
2435
+
2436
+ return video_file_name
2437
+
2438
+
2439
+ def create_observations(self):
2440
+ """
2441
+ Create observations from a media file directory
2442
+ """
2443
+ # print(self.pj[cfg.OBSERVATIONS])
2444
+
2445
+ dir_path = QFileDialog.getExistingDirectory(None, "Select directory", os.getenv("HOME"))
2446
+ if not dir_path:
2447
+ return
2448
+
2449
+ dlg = dialog.Input_dialog(
2450
+ label_caption="Set the following observation parameters",
2451
+ elements_list=[
2452
+ ("cb", "Recurse the subdirectories", False),
2453
+ ("cb", "Save the absolute media file path", True),
2454
+ ("cb", "Visualize spectrogram", False),
2455
+ ("cb", "Visualize waveform", False),
2456
+ ("cb", "Media creation date as offset", False),
2457
+ ("cb", "Close behaviors between videos", False),
2458
+ ("dsb", "Time offset (in seconds)", 0.0, 86400, 1, 0, 3),
2459
+ ("dsb", "Media scan sampling duration (in seconds)", 0.0, 86400, 1, 0, 3),
2460
+ ],
2461
+ title="Observation parameters",
2462
+ )
2463
+ if not dlg.exec_():
2464
+ return
1883
2465
 
1884
- cumul_media_durations.remove(dec(0))
2466
+ file_count: int = 0
1885
2467
 
1886
- # test if timestamp is at end of last media
1887
- if timestamp == cumul_media_durations[-1]:
1888
- player_idx = len(observation[cfg.FILE]["1"]) - 1
2468
+ if dlg.elements["Recurse the subdirectories"].isChecked():
2469
+ files_list = pl.Path(dir_path).rglob("*")
1889
2470
  else:
1890
- player_idx = -1
1891
- for idx, value in enumerate(cumul_media_durations):
1892
- start = 0 if idx == 0 else cumul_media_durations[idx - 1]
1893
- if start <= timestamp < value:
1894
- player_idx = idx
1895
- break
2471
+ files_list = pl.Path(dir_path).glob("*")
2472
+
2473
+ for file in files_list:
2474
+ if not file.is_file():
2475
+ continue
2476
+ r = util.accurate_media_analysis(ffmpeg_bin=self.ffmpeg_bin, file_name=file)
2477
+ if "error" not in r:
2478
+ if not r.get("frames_number", 0):
2479
+ continue
2480
+
2481
+ if dlg.elements["Save the absolute media file path"].isChecked():
2482
+ media_file = str(file)
2483
+ else:
2484
+ try:
2485
+ media_file = str(file.relative_to(pl.Path(self.projectFileName).parent))
2486
+ except ValueError:
2487
+ QMessageBox.critical(
2488
+ self,
2489
+ cfg.programName,
2490
+ (
2491
+ f"the media file <b>{file}</b> can not be relative to the project directory "
2492
+ f"(<b>{pl.Path(self.projectFileName).parent}</b>)"
2493
+ "<br><br>Aborting the creation of observations"
2494
+ ),
2495
+ )
2496
+ return
2497
+
2498
+ if media_file in self.pj[cfg.OBSERVATIONS]:
2499
+ QMessageBox.critical(
2500
+ self,
2501
+ cfg.programName,
2502
+ (f"The observation <b>{media_file}</b> already exists.<br><br>Aborting the creation of observations"),
2503
+ )
2504
+ return
1896
2505
 
1897
- if player_idx != -1:
1898
- video_file_name = observation[cfg.FILE]["1"][player_idx]
2506
+ self.pj[cfg.OBSERVATIONS][media_file] = {
2507
+ "file": {"1": [media_file], "2": [], "3": [], "4": [], "5": [], "6": [], "7": [], "8": []},
2508
+ "type": "MEDIA",
2509
+ "date": dt.datetime.now().replace(microsecond=0).isoformat(),
2510
+ "description": "",
2511
+ "time offset": dec(str(round(dlg.elements["Time offset (in seconds)"].value(), 3))),
2512
+ "events": [],
2513
+ "observation time interval": [0, 0],
2514
+ "independent_variables": {},
2515
+ "visualize_spectrogram": dlg.elements["Visualize spectrogram"].isChecked(),
2516
+ "visualize_waveform": dlg.elements["Visualize waveform"].isChecked(),
2517
+ "media_creation_date_as_offset": dlg.elements["Media creation date as offset"].isChecked(),
2518
+ "media_scan_sampling_duration": dec(str(round(dlg.elements["Media scan sampling duration (in seconds)"].value(), 3))),
2519
+ "image_display_duration": 1,
2520
+ "close_behaviors_between_videos": dlg.elements["Close behaviors between videos"].isChecked(),
2521
+ "media_info": {
2522
+ "length": {media_file: r["duration"]},
2523
+ "fps": {media_file: r["duration"]},
2524
+ "hasVideo": {media_file: r["has_video"]},
2525
+ "hasAudio": {media_file: r["has_audio"]},
2526
+ "offset": {"1": 0.0},
2527
+ },
2528
+ }
2529
+ file_count += 1
2530
+ self.project_changed()
2531
+
2532
+ if file_count:
2533
+ message: str = f"{file_count} observation(s) were created" if file_count > 1 else "One observation was created"
1899
2534
  else:
1900
- video_file_name = None
2535
+ message: str = f"No media file were found in {dir_path}"
1901
2536
 
1902
- return video_file_name
2537
+ QMessageBox.information(self, cfg.programName, message)