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