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