boris-behav-obs 8.12__py3-none-any.whl → 9.7.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of boris-behav-obs might be problematic. Click here for more details.

Files changed (128) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +28 -39
  4. boris/add_modifier.py +122 -109
  5. boris/add_modifier_ui.py +239 -135
  6. boris/advanced_event_filtering.py +81 -45
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +228 -229
  18. boris/behavior_binary_table.py +33 -50
  19. boris/behaviors_coding_map.py +17 -18
  20. boris/boris_cli.py +6 -25
  21. boris/cmd_arguments.py +12 -1
  22. boris/coding_pad.py +42 -49
  23. boris/config.py +141 -65
  24. boris/config_file.py +58 -67
  25. boris/connections.py +107 -61
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2373 -1786
  30. boris/core_qrc.py +15895 -10743
  31. boris/core_ui.py +943 -798
  32. boris/db_functions.py +17 -42
  33. boris/dev.py +109 -8
  34. boris/dialog.py +482 -236
  35. boris/duration_widget.py +9 -14
  36. boris/edit_event.py +61 -31
  37. boris/edit_event_ui.py +208 -97
  38. boris/event_operations.py +408 -293
  39. boris/events_cursor.py +25 -17
  40. boris/events_snapshots.py +36 -82
  41. boris/exclusion_matrix.py +4 -9
  42. boris/export_events.py +184 -223
  43. boris/export_observation.py +74 -100
  44. boris/external_processes.py +123 -98
  45. boris/geometric_measurement.py +644 -290
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +4 -4
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +20 -57
  51. boris/latency.py +31 -24
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +17 -19
  54. boris/menu_options.py +17 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv.py +1 -0
  58. boris/mpv2.py +732 -705
  59. boris/observation.py +533 -221
  60. boris/observation_operations.py +1025 -390
  61. boris/observation_ui.py +572 -362
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -68
  64. boris/param_panel.py +31 -16
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +90 -60
  67. boris/plot_data_module.py +25 -33
  68. boris/plot_events.py +127 -90
  69. boris/plot_events_rt.py +17 -31
  70. boris/plot_spectrogram_rt.py +95 -30
  71. boris/plot_waveform_rt.py +32 -21
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +18 -8
  74. boris/portion/const.py +35 -18
  75. boris/portion/dict.py +5 -5
  76. boris/portion/func.py +2 -2
  77. boris/portion/interval.py +21 -41
  78. boris/portion/io.py +41 -32
  79. boris/preferences.py +306 -83
  80. boris/preferences_ui.py +684 -227
  81. boris/project.py +448 -293
  82. boris/project_functions.py +671 -238
  83. boris/project_import_export.py +213 -222
  84. boris/project_ui.py +674 -438
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +74 -48
  88. boris/select_observations.py +20 -198
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +52 -35
  91. boris/subjects_pad.py +6 -9
  92. boris/synthetic_time_budget.py +45 -28
  93. boris/time_budget_functions.py +171 -171
  94. boris/time_budget_widget.py +84 -114
  95. boris/transitions.py +41 -47
  96. boris/utilities.py +627 -236
  97. boris/version.py +3 -3
  98. boris/video_equalizer.py +16 -14
  99. boris/video_equalizer_ui.py +199 -130
  100. boris/video_operations.py +95 -29
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.6.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
  106. {boris_behav_obs-8.12.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
  108. boris/README.TXT +0 -22
  109. boris/add_modifier.ui +0 -323
  110. boris/converters.ui +0 -289
  111. boris/core.qrc +0 -36
  112. boris/core.ui +0 -1556
  113. boris/edit_event.ui +0 -233
  114. boris/icons/logo_eye.ico +0 -0
  115. boris/map_creator.py +0 -850
  116. boris/observation.ui +0 -814
  117. boris/param_panel.ui +0 -379
  118. boris/preferences.ui +0 -537
  119. boris/project.ui +0 -1069
  120. boris/project_server.py +0 -236
  121. boris/vlc.py +0 -10343
  122. boris/vlc_local.py +0 -90
  123. boris_behav_obs-8.12.dist-info/LICENSE.TXT +0 -674
  124. boris_behav_obs-8.12.dist-info/METADATA +0 -128
  125. boris_behav_obs-8.12.dist-info/RECORD +0 -108
  126. boris_behav_obs-8.12.dist-info/entry_points.txt +0 -3
  127. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  128. {boris_behav_obs-8.12.dist-info → boris_behav_obs-9.7.6.dist-info}/top_level.txt +0 -0
boris/observation.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """
2
2
  BORIS
3
3
  Behavioral Observation Research Interactive Software
4
- Copyright 2012-2023 Olivier Friard
4
+ Copyright 2012-2025 Olivier Friard
5
5
 
6
6
  This file is part of BORIS.
7
7
 
@@ -20,14 +20,14 @@ This file is part of BORIS.
20
20
 
21
21
  """
22
22
 
23
- import glob
24
23
  import logging
25
24
  import os
25
+ import pandas as pd
26
26
  import pathlib as pl
27
27
 
28
- from PyQt5.QtCore import Qt
29
- from PyQt5.QtGui import QColor
30
- from PyQt5.QtWidgets import (
28
+ from PySide6.QtCore import Qt
29
+ from PySide6.QtGui import QColor
30
+ from PySide6.QtWidgets import (
31
31
  QDialog,
32
32
  QVBoxLayout,
33
33
  QHBoxLayout,
@@ -42,10 +42,11 @@ from PyQt5.QtWidgets import (
42
42
  QApplication,
43
43
  QMenu,
44
44
  QListWidgetItem,
45
+ QHeaderView,
45
46
  )
46
47
 
47
48
  from . import config as cfg
48
- from . import dialog, duration_widget, plot_data_module, project_functions
49
+ from . import dialog, plot_data_module, project_functions
49
50
  from . import utilities as util
50
51
  from . import gui_utilities
51
52
  from .observation_ui import Ui_Form
@@ -75,7 +76,10 @@ class AssignConverter(QDialog):
75
76
  self.cbb[-1].addItems(["None"] + sorted(converters.keys()))
76
77
 
77
78
  if column_idx in col_conv:
78
- self.cbb[-1].setCurrentIndex((["None"] + sorted(converters.keys())).index(col_conv[column_idx]))
79
+ if col_conv[column_idx] in (["None"] + sorted(converters.keys())):
80
+ self.cbb[-1].setCurrentIndex((["None"] + sorted(converters.keys())).index(col_conv[column_idx]))
81
+ else:
82
+ self.cbb[-1].setCurrentIndex(0)
79
83
  else:
80
84
  self.cbb[-1].setCurrentIndex(0)
81
85
  hbox.addWidget(self.cbb[-1])
@@ -96,7 +100,7 @@ class AssignConverter(QDialog):
96
100
 
97
101
 
98
102
  class Observation(QDialog, Ui_Form):
99
- def __init__(self, tmp_dir, project_path="", converters={}, time_format=cfg.S, parent=None):
103
+ def __init__(self, tmp_dir: str, project_path: str = "", converters: dict = {}, time_format: str = cfg.S, parent=None):
100
104
  """
101
105
  Args:
102
106
  tmp_dir (str): path of temporary directory
@@ -110,22 +114,31 @@ class Observation(QDialog, Ui_Form):
110
114
  self.project_path = project_path
111
115
  self.converters = converters
112
116
  self.time_format = time_format
113
- self.observation_time_interval = [0, 0]
117
+ self.observation_time_interval: tuple = [0, 0]
114
118
  self.mem_dir = ""
115
119
  self.test = None
116
120
 
117
121
  self.setupUi(self)
118
122
 
119
123
  # insert duration widget for time offset
120
- self.obs_time_offset = duration_widget.Duration_widget(0)
124
+ # self.obs_time_offset = duration_widget.Duration_widget(0)
125
+ self.obs_time_offset = dialog.get_time_widget(0)
121
126
  self.horizontalLayout_6.insertWidget(1, self.obs_time_offset)
127
+ self.obs_time_offset.setEnabled(False)
128
+
129
+ # time offset
130
+ self.cb_time_offset.stateChanged.connect(self.cb_time_offset_changed)
131
+ # date offset
132
+ """self.cb_date_offset.stateChanged.connect(self.cb_date_offset_changed)"""
122
133
 
123
134
  # observation type
124
135
  self.rb_media_files.toggled.connect(self.obs_type_changed)
125
136
  self.rb_live.toggled.connect(self.obs_type_changed)
126
137
  self.rb_images.toggled.connect(self.obs_type_changed)
127
138
 
128
- menu_items = [
139
+ # button menu for media
140
+
141
+ add_media_menu_items = [
129
142
  "media abs path|with absolute path",
130
143
  "media rel path|with relative path",
131
144
  {
@@ -135,26 +148,71 @@ class Observation(QDialog, Ui_Form):
135
148
  ]
136
149
  },
137
150
  ]
138
- menu = QMenu()
139
- menu.triggered.connect(lambda x: self.add_media(mode=x.statusTip()))
140
- self.add_button_menu(menu_items, menu)
141
- self.pbAddVideo.setMenu(menu)
151
+
152
+ self.media_menu = QMenu()
153
+ # Add actions to the menu
154
+ """
155
+ self.action1 = QAction("with absolute path")
156
+ self.action2 = QAction("with relative path")
157
+ self.action3 = QAction("directory with absolute path")
158
+ self.action4 = QAction("directory with relative path")
159
+
160
+ self.menu.addAction(self.action1)
161
+ self.menu.addAction(self.action2)
162
+ self.menu.addAction(self.action3)
163
+ self.menu.addAction(self.action4)
164
+
165
+ # Connect actions to functions
166
+ self.action1.triggered.connect(lambda: self.add_media(mode="media abs path|with absolute path"))
167
+ self.action2.triggered.connect(lambda: self.add_media(mode="media rel path|with relative path"))
168
+ self.action3.triggered.connect(lambda: self.add_media(mode="dir abs path|with absolute path"))
169
+ self.action4.triggered.connect(lambda: self.add_media(mode="dir rel path|wih relative path"))
170
+ """
171
+
172
+ self.media_menu.triggered.connect(lambda x: self.add_media(mode=x.statusTip()))
173
+ self.add_button_menu(add_media_menu_items, self.media_menu)
174
+ self.pbAddVideo.setMenu(self.media_menu)
142
175
 
143
176
  self.pbRemoveVideo.clicked.connect(self.remove_media)
144
177
 
145
- # add data file
178
+ # button menu for data file
146
179
  data_menu_items = [
147
180
  "data abs path|with absolute path",
148
181
  "data rel path|with relative path",
149
182
  ]
150
183
 
151
- menu_data = QMenu()
152
- menu_data.triggered.connect(lambda x: self.add_data_file(mode=x.statusTip()))
153
- self.add_button_menu(data_menu_items, menu_data)
154
- self.pb_add_data_file.setMenu(menu_data)
184
+ self.menu_data = QMenu()
185
+
186
+ # Add actions to the menu
187
+ """
188
+ self.data_action1 = QAction("with absolute path")
189
+ self.data_action2 = QAction("with relative path")
190
+ self.menu_data.addAction(self.data_action1)
191
+ self.menu_data.addAction(self.data_action2)
192
+
193
+ # Connect actions to functions
194
+ self.data_action1.triggered.connect(lambda: self.add_data_file(mode="data abs path|with absolute path"))
195
+ self.data_action2.triggered.connect(lambda: self.add_data_file(mode="data rel path|with relative path"))
196
+ """
197
+
198
+ self.menu_data.triggered.connect(lambda x: self.add_data_file(mode=x.statusTip()))
199
+ self.add_button_menu(data_menu_items, self.menu_data)
200
+ self.pb_add_data_file.setMenu(self.menu_data)
201
+
202
+ # button menu for images
203
+ images_menu_items = [
204
+ "images abs path|with absolute path",
205
+ "images rel path|with relative path",
206
+ ]
207
+
208
+ self.menu_images = QMenu()
209
+
210
+ self.menu_images.triggered.connect(lambda x: self.add_images_directory(mode=x.statusTip()))
211
+ self.add_button_menu(images_menu_items, self.menu_images)
212
+ self.pb_add_directory.setMenu(self.menu_images)
155
213
 
156
214
  self.pb_remove_data_file.clicked.connect(self.remove_data_file)
157
- self.pb_view_data_head.clicked.connect(self.view_data_file_head)
215
+ self.pb_view_data_head.clicked.connect(self.view_data_file_head_tail)
158
216
  self.pb_plot_data.clicked.connect(self.plot_data_file)
159
217
 
160
218
  self.pb_use_media_file_name_as_obsid.clicked.connect(self.use_media_file_name_as_obsid)
@@ -162,6 +220,7 @@ class Observation(QDialog, Ui_Form):
162
220
 
163
221
  self.cbVisualizeSpectrogram.clicked.connect(self.extract_wav)
164
222
  self.cb_visualize_waveform.clicked.connect(self.extract_wav)
223
+
165
224
  self.cb_observation_time_interval.clicked.connect(self.limit_time_interval)
166
225
 
167
226
  self.pbSave.clicked.connect(self.pbSave_clicked)
@@ -169,21 +228,27 @@ class Observation(QDialog, Ui_Form):
169
228
  self.pbCancel.clicked.connect(self.pbCancel_clicked)
170
229
 
171
230
  self.tw_data_files.cellDoubleClicked[int, int].connect(self.tw_data_files_cellDoubleClicked)
231
+ self.tw_data_files.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
172
232
 
173
- self.mediaDurations, self.mediaFPS, self.mediaHasVideo, self.mediaHasAudio = {}, {}, {}, {}
233
+ self.twVideo1.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
174
234
 
175
- self.cbVisualizeSpectrogram.setEnabled(False)
176
- self.cb_visualize_waveform.setEnabled(False)
177
- self.cb_observation_time_interval.setEnabled(True)
235
+ self.mediaDurations, self.mediaFPS, self.mediaHasVideo, self.mediaHasAudio, self.media_creation_time = {}, {}, {}, {}, {}
236
+
237
+ for w in (
238
+ self.cbVisualizeSpectrogram,
239
+ self.cb_visualize_waveform,
240
+ self.cb_observation_time_interval,
241
+ self.cb_media_creation_date_as_offset,
242
+ self.cbCloseCurrentBehaviorsBetweenVideo,
243
+ ):
244
+ w.setEnabled(False)
178
245
 
179
- # disabled due to problem when video goes back
180
- self.cbCloseCurrentBehaviorsBetweenVideo.setChecked(False)
181
- self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(False)
246
+ self.cb_observation_time_interval.setEnabled(True)
182
247
 
183
248
  self.cb_start_from_current_time.stateChanged.connect(self.cb_start_from_current_time_changed)
184
249
 
185
250
  # images
186
- self.pb_add_directory.clicked.connect(self.add_images_directory)
251
+ # self.pb_add_directory.clicked.connect(self.add_images_directory)
187
252
  self.pb_remove_directory.clicked.connect(self.remove_images_directory)
188
253
 
189
254
  self.tabWidget.setCurrentIndex(0)
@@ -191,6 +256,60 @@ class Observation(QDialog, Ui_Form):
191
256
  # geometry
192
257
  gui_utilities.restore_geometry(self, "new observation", (800, 650))
193
258
 
259
+ # def cb_date_offset_changed(self):
260
+ # """
261
+ # activate/desactivate time value
262
+ # """
263
+ # self.de_date_offset.setEnabled(self.cb_date_offset.isChecked())
264
+
265
+ def check_media_creation_date(self):
266
+ """
267
+ check if all media files contain creation date time
268
+ search in metadata then in filename
269
+ """
270
+
271
+ creation_date_not_found: list = []
272
+ flag_filename_used = False
273
+
274
+ self.media_creation_time = {}
275
+
276
+ if self.cb_media_creation_date_as_offset.isChecked():
277
+ for row in range(self.twVideo1.rowCount()):
278
+ if self.twVideo1.item(row, 2).text(): # media file path
279
+ date_time_original = util.extract_video_creation_date(
280
+ project_functions.full_path(self.twVideo1.item(row, 2).text(), self.project_path)
281
+ )
282
+ if date_time_original is None:
283
+ date_time_file_name = util.extract_date_time_from_file_name(self.twVideo1.item(row, 2).text())
284
+ if date_time_file_name is None:
285
+ creation_date_not_found.append(self.twVideo1.item(row, 2).text())
286
+ else:
287
+ self.media_creation_time[self.twVideo1.item(row, 2).text()] = date_time_file_name
288
+ flag_filename_used = True
289
+ else:
290
+ self.media_creation_time[self.twVideo1.item(row, 2).text()] = date_time_original
291
+
292
+ if creation_date_not_found:
293
+ QMessageBox.warning(
294
+ self, cfg.programName, "The creation date time was not found for all media file(s).\nThe option was disabled."
295
+ )
296
+ self.cb_media_creation_date_as_offset.setChecked(False)
297
+ self.media_creation_time = {}
298
+ return 1
299
+
300
+ elif flag_filename_used:
301
+ QMessageBox.information(
302
+ self, cfg.programName, "The creation date time was not found in metadata. The media file name(s) was/were used"
303
+ )
304
+
305
+ return 0
306
+
307
+ def cb_time_offset_changed(self):
308
+ """
309
+ activate/desactivate date value
310
+ """
311
+ self.obs_time_offset.setEnabled(self.cb_time_offset.isChecked())
312
+
194
313
  def use_media_file_name_as_obsid(self) -> None:
195
314
  """
196
315
  set observation id with the media file name value (without path)
@@ -199,7 +318,7 @@ class Observation(QDialog, Ui_Form):
199
318
  QMessageBox.critical(self, cfg.programName, "A media file must be loaded in player #1")
200
319
  return
201
320
 
202
- first_media_file = ""
321
+ first_media_file: str = ""
203
322
  for row in range(self.twVideo1.rowCount()):
204
323
  if int(self.twVideo1.cellWidget(row, 0).currentText()) == 1:
205
324
  first_media_file = self.twVideo1.item(row, 2).text()
@@ -234,11 +353,39 @@ class Observation(QDialog, Ui_Form):
234
353
  # hide 'limit observation to time interval' for images
235
354
  self.cb_observation_time_interval.setEnabled(not self.rb_images.isChecked())
236
355
 
237
- def add_images_directory(self):
356
+ def add_images_directory(self, mode: str):
238
357
  """
239
358
  add path to images directory
240
359
  """
241
- dir_path = QFileDialog.getExistingDirectory(None, "Select directory", os.getenv("HOME"))
360
+
361
+ if mode.split("|")[0] not in (
362
+ "images abs path",
363
+ "images rel path",
364
+ ):
365
+ QMessageBox.critical(
366
+ self,
367
+ cfg.programName,
368
+ (f"Wrong mode to add a pictures directory {mode}"),
369
+ )
370
+ return
371
+
372
+ # check if project saved
373
+ if (" w/o" in mode or " rel " in mode) and (not self.project_file_name):
374
+ QMessageBox.critical(
375
+ self,
376
+ cfg.programName,
377
+ ("It is not possible to add a pictures directory with a relative path if the project is not already saved"),
378
+ )
379
+ return
380
+
381
+ fd = QFileDialog()
382
+ fd.setDirectory(os.path.expanduser("~") if (" abs " in mode) else str(pl.Path(self.project_path).parent))
383
+
384
+ dir_path = fd.getExistingDirectory(None, "Select directory")
385
+
386
+ if not dir_path:
387
+ return
388
+
242
389
  result = util.dir_images_number(dir_path)
243
390
  if not result.get("number of images", 0):
244
391
  response = dialog.MessageDialog(
@@ -249,7 +396,25 @@ class Observation(QDialog, Ui_Form):
249
396
  if response == "Cancel":
250
397
  return
251
398
 
252
- self.lw_images_directory.addItem(QListWidgetItem(dir_path))
399
+ # store directory for next usage
400
+ self.mem_dir = str(pl.Path(dir_path))
401
+
402
+ if " rel " in mode:
403
+ try:
404
+ pl.Path(dir_path).parent.relative_to(pl.Path(self.project_path).parent)
405
+ except ValueError:
406
+ QMessageBox.critical(
407
+ self,
408
+ cfg.programName,
409
+ f"The directory <b>{pl.Path(dir_path).parent}</b> is not contained in <b>{pl.Path(self.project_path).parent}</b>.",
410
+ )
411
+ return
412
+
413
+ if " rel " in mode:
414
+ # convert to relative path (relative to BORIS project file)
415
+ self.lw_images_directory.addItem(QListWidgetItem(str(pl.Path(dir_path).relative_to(pl.Path(self.project_path).parent))))
416
+ else:
417
+ self.lw_images_directory.addItem(QListWidgetItem(dir_path))
253
418
  self.lb_images_info.setText(f"Number of images in {dir_path}: {result.get('number of images', 0)}")
254
419
 
255
420
  def remove_images_directory(self):
@@ -289,10 +454,14 @@ class Observation(QDialog, Ui_Form):
289
454
  """
290
455
 
291
456
  if self.cb_observation_time_interval.isChecked():
292
- time_interval_dialog = dialog.Ask_time(self.time_format)
457
+ time_interval_dialog = dialog.Ask_time(0)
458
+ if self.time_format == cfg.S:
459
+ time_interval_dialog.time_widget.rb_seconds.setChecked(True)
460
+ if self.time_format == cfg.HHMMSS:
461
+ time_interval_dialog.time_widget.rb_time.setChecked(True)
293
462
  time_interval_dialog.time_widget.set_time(0)
294
463
  time_interval_dialog.setWindowTitle("Start observation at")
295
- time_interval_dialog.label.setText("Start observation at")
464
+ time_interval_dialog.label.setText("<b>Start</b> observation at")
296
465
  start_time, stop_time = 0, 0
297
466
  if time_interval_dialog.exec_():
298
467
  start_time = time_interval_dialog.time_widget.get_time()
@@ -301,7 +470,7 @@ class Observation(QDialog, Ui_Form):
301
470
  return
302
471
  time_interval_dialog.time_widget.set_time(0)
303
472
  time_interval_dialog.setWindowTitle("Stop observation at")
304
- time_interval_dialog.label.setText("Stop observation at")
473
+ time_interval_dialog.label.setText("<b>Stop</b> observation at")
305
474
  if time_interval_dialog.exec_():
306
475
  stop_time = time_interval_dialog.time_widget.get_time()
307
476
  else:
@@ -315,7 +484,10 @@ class Observation(QDialog, Ui_Form):
315
484
  return
316
485
  self.observation_time_interval = [start_time, stop_time]
317
486
  self.cb_observation_time_interval.setText(
318
- f"Limit observation to a time interval: {start_time} - {stop_time}"
487
+ (
488
+ "Limit observation to a time interval: "
489
+ f"{util.smart_time_format(start_time, self.time_format)} - {util.smart_time_format(stop_time, self.time_format)}"
490
+ )
319
491
  )
320
492
  else:
321
493
  self.observation_time_interval = [0, 0]
@@ -337,9 +509,7 @@ class Observation(QDialog, Ui_Form):
337
509
 
338
510
  if w.exec_():
339
511
  d = {}
340
- for col_idx, cb in zip(
341
- self.tw_data_files.item(row, cfg.PLOT_DATA_COLUMNS_IDX).text().split(","), w.cbb
342
- ):
512
+ for col_idx, cb in zip(self.tw_data_files.item(row, cfg.PLOT_DATA_COLUMNS_IDX).text().split(","), w.cbb):
343
513
  if cb.currentText() != "None":
344
514
  d[col_idx] = cb.currentText()
345
515
  self.tw_data_files.item(row, cfg.PLOT_DATA_CONVERTERS_IDX).setText(str(d))
@@ -360,7 +530,6 @@ class Observation(QDialog, Ui_Form):
360
530
  return
361
531
 
362
532
  if self.tw_data_files.selectedIndexes() or self.tw_data_files.rowCount() == 1:
363
-
364
533
  if self.tw_data_files.rowCount() == 1:
365
534
  row_idx = 0
366
535
  else:
@@ -380,9 +549,7 @@ class Observation(QDialog, Ui_Form):
380
549
  time_interval = int(self.tw_data_files.item(row_idx, cfg.PLOT_DATA_TIMEINTERVAL_IDX).text())
381
550
  time_offset = int(self.tw_data_files.item(row_idx, cfg.PLOT_DATA_TIMEOFFSET_IDX).text())
382
551
 
383
- substract_first_value = self.tw_data_files.cellWidget(
384
- row_idx, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX
385
- ).currentText()
552
+ substract_first_value = self.tw_data_files.cellWidget(row_idx, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX).currentText()
386
553
 
387
554
  plot_color = self.tw_data_files.cellWidget(row_idx, cfg.PLOT_DATA_PLOTCOLOR_IDX).currentText()
388
555
 
@@ -427,6 +594,13 @@ class Observation(QDialog, Ui_Form):
427
594
  else:
428
595
  QMessageBox.warning(self, cfg.programName, "Select a data file")
429
596
 
597
+ def not_editable_column_color(self):
598
+ """
599
+ return a color for the not editable column
600
+ """
601
+ window_color = QApplication.instance().palette().window().color()
602
+ return QColor(window_color.red() - 5, window_color.green() - 5, window_color.blue() - 5)
603
+
430
604
  def add_data_file(self, mode: str):
431
605
  """
432
606
  user select a data file to be plotted synchronously with media file
@@ -460,19 +634,14 @@ class Observation(QDialog, Ui_Form):
460
634
  QMessageBox.warning(
461
635
  self,
462
636
  cfg.programName,
463
- (
464
- "It is not yet possible to plot more than 2 external data sources"
465
- "This limitation will be removed in future"
466
- ),
637
+ ("It is not yet possible to plot more than 2 external data sourcesThis limitation will be removed in future"),
467
638
  )
468
639
  return
469
640
 
470
641
  fd = QFileDialog()
471
642
  fd.setDirectory(os.path.expanduser("~") if (" abs " in mode) else str(pl.Path(self.project_path).parent))
472
643
 
473
- fn = fd.getOpenFileName(self, "Add data file", "", "All files (*)")
474
- file_name = fn[0] if type(fn) is tuple else fn
475
-
644
+ file_name, _ = fd.getOpenFileName(self, "Add data file", "", "All files (*)")
476
645
  if not file_name:
477
646
  return
478
647
 
@@ -489,18 +658,15 @@ class Observation(QDialog, Ui_Form):
489
658
  QMessageBox.critical(self, cfg.programName, "This file does not contain a constant number of columns")
490
659
  return
491
660
 
492
- header, footer = util.return_file_header_footer(
493
- file_name, file_row_number=file_parameters["rows number"], row_number=5
494
- )
661
+ header, footer = util.return_file_header_footer(file_name, file_row_number=file_parameters["rows number"], row_number=5)
495
662
 
496
663
  if not header:
497
664
  QMessageBox.critical(self, cfg.programName, f"Error on file {pl.Path(file_name).name}")
498
665
  return
499
666
 
500
- w = dialog.View_data_head()
501
- w.setWindowTitle(f"View data")
667
+ w = dialog.View_data()
668
+ w.setWindowTitle("View data")
502
669
  w.lb.setText(f"View first and last rows of <b>{pl.Path(file_name).name}</b> file")
503
- """w.setWindowFlags(Qt.WindowStaysOnTopHint)"""
504
670
 
505
671
  w.tw.setColumnCount(file_parameters["fields number"])
506
672
  if footer:
@@ -516,13 +682,17 @@ class Observation(QDialog, Ui_Form):
516
682
  item.setFlags(Qt.ItemIsEnabled)
517
683
  w.tw.setItem(idx, col, item)
518
684
 
519
- """
520
- for row in range(len(header)):
521
- for col, v in enumerate(header[row].split(file_parameters["separator"])):
522
- item = QTableWidgetItem(v)
523
- item.setFlags(Qt.ItemIsEnabled)
524
- w.tw.setItem(row, col, item)
525
- """
685
+ # stats
686
+ try:
687
+ df = pd.read_csv(file_name, sep=file_parameters["separator"], header=None if not file_parameters["has header"] else [0])
688
+ # set columns names to based 1 index
689
+ if not file_parameters["has header"]:
690
+ df.columns = range(1, len(df.columns) + 1)
691
+
692
+ stats_out = str(df.describe())
693
+ except Exception:
694
+ stats_out = "Not available"
695
+ w.stats.setPlainText(stats_out)
526
696
 
527
697
  while True:
528
698
  flag_ok = True
@@ -550,7 +720,6 @@ class Observation(QDialog, Ui_Form):
550
720
  self.tw_data_files.setRowCount(self.tw_data_files.rowCount() + 1)
551
721
 
552
722
  if " rel " in mode:
553
-
554
723
  try:
555
724
  file_path = str(pl.Path(file_name).relative_to(pl.Path(self.project_path).parent))
556
725
  except ValueError:
@@ -579,22 +748,21 @@ class Observation(QDialog, Ui_Form):
579
748
  item = QTableWidgetItem(value)
580
749
  if col_idx == cfg.PLOT_DATA_CONVERTERS_IDX:
581
750
  item.setFlags(Qt.ItemIsEnabled)
582
- item.setBackground(QColor(230, 230, 230))
751
+ # item.setBackground(QColor(230, 230, 230))
752
+ item.setBackground(self.not_editable_column_color())
583
753
  self.tw_data_files.setItem(self.tw_data_files.rowCount() - 1, col_idx, item)
584
754
 
585
755
  # substract first value
586
756
  combobox = QComboBox()
587
757
  combobox.addItems(["True", "False"])
588
- self.tw_data_files.setCellWidget(
589
- self.tw_data_files.rowCount() - 1, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX, combobox
590
- )
758
+ self.tw_data_files.setCellWidget(self.tw_data_files.rowCount() - 1, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX, combobox)
591
759
 
592
760
  # plot line color
593
761
  combobox = QComboBox()
594
762
  combobox.addItems(cfg.DATA_PLOT_STYLES)
595
763
  self.tw_data_files.setCellWidget(self.tw_data_files.rowCount() - 1, cfg.PLOT_DATA_PLOTCOLOR_IDX, combobox)
596
764
 
597
- def view_data_file_head(self):
765
+ def view_data_file_head_tail(self) -> None:
598
766
  """
599
767
  view first and last rows of data file
600
768
  """
@@ -605,7 +773,7 @@ class Observation(QDialog, Ui_Form):
605
773
  if self.tw_data_files.rowCount() == 1:
606
774
  data_file_path = project_functions.full_path(self.tw_data_files.item(0, 0).text(), self.project_path)
607
775
  columns_to_plot = self.tw_data_files.item(0, 1).text()
608
- else:
776
+ else: # selected file
609
777
  data_file_path = project_functions.full_path(
610
778
  self.tw_data_files.item(self.tw_data_files.selectedIndexes()[0].row(), 0).text(), self.project_path
611
779
  )
@@ -616,18 +784,16 @@ class Observation(QDialog, Ui_Form):
616
784
  if "error" in file_parameters:
617
785
  QMessageBox.critical(self, cfg.programName, f"Error on file {data_file_path}: {file_parameters['error']}")
618
786
  return
619
- header, footer = util.return_file_header_footer(
620
- data_file_path, file_row_number=file_parameters["rows number"], row_number=5
621
- )
787
+ header, footer = util.return_file_header_footer(data_file_path, file_row_number=file_parameters["rows number"], row_number=5)
622
788
 
623
789
  if not header:
624
790
  QMessageBox.critical(self, cfg.programName, f"Error on file {pl.Path(data_file_path).name}")
625
791
  return
626
792
 
627
- w = dialog.View_data_head()
628
- w.setWindowTitle(f"View data")
793
+ w = dialog.View_data()
794
+ w.setWindowTitle("View data")
629
795
  w.lb.setText(f"View first and last rows of <b>{pl.Path(data_file_path).name}</b> file")
630
- w.pbOK.setText("Close")
796
+ w.pbOK.setText(cfg.CLOSE)
631
797
  w.label.setText("Index of columns to plot")
632
798
  w.le.setEnabled(False)
633
799
  w.le.setText(columns_to_plot)
@@ -647,6 +813,22 @@ class Observation(QDialog, Ui_Form):
647
813
  item.setFlags(Qt.ItemIsEnabled)
648
814
  w.tw.setItem(idx, col, item)
649
815
 
816
+ # stats
817
+ try:
818
+ df = pd.read_csv(
819
+ data_file_path,
820
+ sep=file_parameters["separator"],
821
+ header=None if not file_parameters["has header"] else [0],
822
+ )
823
+ # set columns names to based 1 index
824
+ if not file_parameters["has header"]:
825
+ df.columns = range(1, len(df.columns) + 1)
826
+
827
+ stats_out = str(df.describe())
828
+ except Exception:
829
+ stats_out = "Not available"
830
+ w.stats.setPlainText(stats_out)
831
+
650
832
  w.exec_()
651
833
 
652
834
  def extract_wav(self):
@@ -654,73 +836,139 @@ class Observation(QDialog, Ui_Form):
654
836
  extract wav of all media files loaded in player #1
655
837
  """
656
838
 
657
- if self.cbVisualizeSpectrogram.isChecked() or self.cb_visualize_waveform.isChecked():
658
- flag_wav_produced = False
659
- # check if player 1 is selected
660
- flag_player1 = False
661
- for row in range(self.twVideo1.rowCount()):
662
- if self.twVideo1.cellWidget(row, 0).currentText() == "1":
663
- flag_player1 = True
839
+ if not self.cbVisualizeSpectrogram.isChecked() and not self.cb_visualize_waveform.isChecked():
840
+ return
664
841
 
665
- if not flag_player1:
666
- QMessageBox.critical(self, cfg.programName, "The player #1 is not selected")
667
- self.cbVisualizeSpectrogram.setChecked(False)
668
- self.cb_visualize_waveform.setChecked(False)
669
- return
670
- """
671
- if dialog.MessageDialog(programName, ("You choose to visualize the spectrogram or waveform for the media in player #1.<br>"
672
- "The WAV will be extracted from the media files, be patient"), [YES, NO]) == YES:
673
- """
674
- if True:
842
+ flag_wav_produced = False
843
+ # check if player 1 is selected
844
+ flag_player1 = False
845
+ for row in range(self.twVideo1.rowCount()):
846
+ if self.twVideo1.cellWidget(row, 0).currentText() == "1":
847
+ flag_player1 = True
675
848
 
676
- w = dialog.Info_widget()
677
- w.resize(350, 100)
678
- # w.setWindowFlags(Qt.WindowStaysOnTopHint)
679
- w.setWindowTitle("BORIS")
680
- w.label.setText("Extracting WAV from media files...")
849
+ if not flag_player1:
850
+ QMessageBox.critical(self, cfg.programName, "The player #1 is not selected")
851
+ self.cbVisualizeSpectrogram.setChecked(False)
852
+ self.cb_visualize_waveform.setChecked(False)
853
+ return
681
854
 
682
- for row in range(self.twVideo1.rowCount()):
683
- # check if player 1
684
- if self.twVideo1.cellWidget(row, 0).currentText() != "1":
685
- continue
855
+ if True:
856
+ w = dialog.Info_widget()
857
+ w.resize(350, 100)
858
+ # w.setWindowFlags(Qt.WindowStaysOnTopHint)
859
+ w.setWindowTitle("BORIS")
860
+ w.label.setText("Extracting WAV from media files...")
686
861
 
687
- media_file_path = project_functions.full_path(
688
- self.twVideo1.item(row, cfg.MEDIA_FILE_PATH_IDX).text(), self.project_path
689
- )
690
- if self.twVideo1.item(row, cfg.HAS_AUDIO_IDX).text() == "False":
862
+ for row in range(self.twVideo1.rowCount()):
863
+ # check if player 1
864
+ if self.twVideo1.cellWidget(row, 0).currentText() != "1":
865
+ continue
866
+
867
+ media_file_path = project_functions.full_path(self.twVideo1.item(row, cfg.MEDIA_FILE_PATH_IDX).text(), self.project_path)
868
+ if self.twVideo1.item(row, cfg.HAS_AUDIO_IDX).text() == "False":
869
+ QMessageBox.critical(self, cfg.programName, f"The media file {media_file_path} does not seem to have audio")
870
+ flag_wav_produced = False
871
+ break
872
+
873
+ if os.path.isfile(media_file_path):
874
+ w.show()
875
+ QApplication.processEvents()
876
+
877
+ if util.extract_wav(self.ffmpeg_bin, media_file_path, self.tmp_dir) == "":
691
878
  QMessageBox.critical(
692
- self, cfg.programName, f"The media file {media_file_path} does not seem to have audio"
879
+ self,
880
+ cfg.programName,
881
+ f"Error during extracting WAV of the media file {media_file_path}",
693
882
  )
694
883
  flag_wav_produced = False
695
884
  break
696
885
 
697
- if os.path.isfile(media_file_path):
698
- w.show()
699
- QApplication.processEvents()
886
+ w.hide()
700
887
 
701
- if util.extract_wav(self.ffmpeg_bin, media_file_path, self.tmp_dir) == "":
702
- QMessageBox.critical(
703
- self,
704
- cfg.programName,
705
- f"Error during extracting WAV of the media file {media_file_path}",
706
- )
707
- flag_wav_produced = False
708
- break
888
+ flag_wav_produced = True
889
+ else:
890
+ QMessageBox.warning(self, cfg.programName, f"<b>{media_file_path}</b> file not found")
709
891
 
710
- w.hide()
892
+ if not flag_wav_produced:
893
+ self.cbVisualizeSpectrogram.setChecked(False)
894
+ self.cb_visualize_waveform.setChecked(False)
711
895
 
712
- flag_wav_produced = True
713
- else:
714
- QMessageBox.warning(self, cfg.programName, f"<b>{media_file_path}</b> file not found")
896
+ def check_creation_date(self) -> int:
897
+ """
898
+ check if media file exists
899
+ check if Creation Date tag is present in metadata of media file
715
900
 
716
- if not flag_wav_produced:
717
- self.cbVisualizeSpectrogram.setChecked(False)
718
- self.cb_visualize_waveform.setChecked(False)
719
- """
901
+ Returns:
902
+ int: 0 if OK else error code: 1 -> media file date not used, 2 -> media file not found
903
+
904
+ """
905
+
906
+ # check if media files exist
907
+
908
+ media_not_found_list: list = []
909
+ for row in range(self.twVideo1.rowCount()):
910
+ if not pl.Path(self.twVideo1.item(row, 2).text()).is_file():
911
+ media_not_found_list.append(self.twVideo1.item(row, 2).text())
912
+
913
+ """
914
+ if media_list:
915
+ dlg = dialog.Results_dialog()
916
+ dlg.setWindowTitle("BORIS")
917
+ dlg.pbOK.setText("OK")
918
+ dlg.pbCancel.setVisible(False)
919
+ dlg.ptText.clear()
920
+ dlg.ptText.appendHtml(
921
+ (
922
+ "Some media file(s) were not found:<br>"
923
+ f"{'<br>'.join(media_list)}<br><br>"
924
+ "You cannot select the <b>Use the media creation date/time option</b>."
925
+ )
926
+ )
927
+ dlg.ptText.moveCursor(QTextCursor.Start)
928
+ ret = dlg.exec_()
929
+ """
930
+
931
+ """
932
+ not_tagged_media_list: list = []
933
+ for row in range(self.twVideo1.rowCount()):
934
+ if self.twVideo1.item(row, 2).text() not in media_not_found_list:
935
+ media_info = util.accurate_media_analysis(self.ffmpeg_bin, self.twVideo1.item(row, 2).text())
936
+ if cfg.MEDIA_CREATION_TIME not in media_info or media_info[cfg.MEDIA_CREATION_TIME] == cfg.NA:
937
+ not_tagged_media_list.append(self.twVideo1.item(row, 2).text())
938
+ else:
939
+ creation_time_epoch = int(dt.datetime.strptime(media_info[cfg.MEDIA_CREATION_TIME], "%Y-%m-%d %H:%M:%S").timestamp())
940
+ self.media_creation_time[self.twVideo1.item(row, 2).text()] = creation_time_epoch
941
+
942
+ if not_tagged_media_list:
943
+ dlg = dialog.Results_dialog()
944
+ dlg.setWindowTitle("BORIS")
945
+ dlg.pbOK.setText("Yes")
946
+ dlg.pbCancel.setVisible(True)
947
+ dlg.pbCancel.setText("No")
948
+
949
+ dlg.ptText.clear()
950
+ dlg.ptText.appendHtml(
951
+ (
952
+ "Some media file does not contain the <b>Creation date/time</b> metadata tag:<br>"
953
+ f"{'<br>'.join(not_tagged_media_list)}<br><br>"
954
+ "Use the media file date/time instead?"
955
+ )
956
+ )
957
+ dlg.ptText.moveCursor(QTextCursor.Start)
958
+ ret = dlg.exec_()
959
+
960
+ if ret == 1: # use file creation time
961
+ for media in not_tagged_media_list:
962
+ self.media_creation_time[media] = pl.Path(media).stat().st_ctime
963
+ return 0 # OK use media file creation date/time
720
964
  else:
721
- self.cbVisualizeSpectrogram.setChecked(False)
722
- self.cb_visualize_waveform.setChecked(False)
723
- """
965
+ self.cb_media_creation_date_as_offset.setChecked(False)
966
+ self.media_creation_time = {}
967
+ return 1
968
+ else:
969
+ return 0 # OK all media have a 'creation time' tag
970
+ """
971
+ return 0
724
972
 
725
973
  def closeEvent(self, event):
726
974
  """
@@ -739,11 +987,13 @@ class Observation(QDialog, Ui_Form):
739
987
  self.text = None
740
988
  self.reject()
741
989
 
742
- def check_parameters(self):
990
+ def check_parameters(self) -> bool:
743
991
  """
744
992
  check observation parameters
745
993
 
746
- return True if everything OK else False
994
+ Returns:
995
+ bool: True if everything is OK else False
996
+
747
997
  """
748
998
 
749
999
  def is_numeric(s):
@@ -764,21 +1014,40 @@ class Observation(QDialog, Ui_Form):
764
1014
 
765
1015
  # check if observation id not empty
766
1016
  if not self.leObservationId.text():
767
- self.qm = QMessageBox()
768
- self.qm.setIcon(QMessageBox.Critical)
769
- self.qm.setText("The <b>observation id</b> is mandatory and must be unique.")
770
- self.qm.exec_()
1017
+ QMessageBox.critical(
1018
+ self,
1019
+ cfg.programName,
1020
+ "The <b>observation id</b> is mandatory and must be unique.",
1021
+ )
771
1022
  return False
772
1023
 
773
1024
  # check if observation_type
774
1025
  if not any((self.rb_media_files.isChecked(), self.rb_live.isChecked(), self.rb_images.isChecked())):
775
- self.qm = QMessageBox()
776
- self.qm.setIcon(QMessageBox.Critical)
777
- self.qm.setText("Choose an observation type.")
778
- self.qm.exec_()
1026
+ QMessageBox.critical(
1027
+ self,
1028
+ cfg.programName,
1029
+ "Choose an observation type.",
1030
+ )
779
1031
  return False
780
1032
 
1033
+ # check if offset is correct
1034
+ if self.cb_time_offset.isChecked():
1035
+ if self.obs_time_offset.get_time() is None:
1036
+ QMessageBox.critical(
1037
+ self,
1038
+ cfg.programName,
1039
+ "Check the time offset value.",
1040
+ )
1041
+ return False
1042
+
781
1043
  if self.rb_media_files.isChecked(): # observation based on media file(s)
1044
+ # check if media file exists
1045
+ media_file_not_found: list = []
1046
+ for row in range(self.twVideo1.rowCount()):
1047
+ # check if media file exists
1048
+ if not pl.Path(self.twVideo1.item(row, 2).text()).is_file():
1049
+ media_file_not_found.append(self.twVideo1.item(row, 2).text())
1050
+
782
1051
  # check player number
783
1052
  players_list: list = []
784
1053
  players: dict = {} # for storing duration
@@ -791,18 +1060,20 @@ class Observation(QDialog, Ui_Form):
791
1060
 
792
1061
  # check if player #1 is used
793
1062
  if not players_list or min(players_list) > 1:
794
- self.qm = QMessageBox()
795
- self.qm.setIcon(QMessageBox.Critical)
796
- self.qm.setText("A media file must be loaded in player #1")
797
- self.qm.exec_()
1063
+ QMessageBox.critical(
1064
+ self,
1065
+ cfg.programName,
1066
+ "A media file must be loaded in player #1",
1067
+ )
798
1068
  return False
799
1069
 
800
1070
  # check if players are used in crescent order
801
1071
  if set(list(range(min(players_list), max(players_list) + 1))) != set(players_list):
802
- self.qm = QMessageBox()
803
- self.qm.setIcon(QMessageBox.Critical)
804
- self.qm.setText("Some player are not used. Please reorganize your media files")
805
- self.qm.exec_()
1072
+ QMessageBox.critical(
1073
+ self,
1074
+ cfg.programName,
1075
+ "Some player are not used. Please reorganize your media files",
1076
+ )
806
1077
  return False
807
1078
 
808
1079
  # check if more media in player #1 and media in other players
@@ -834,7 +1105,7 @@ class Observation(QDialog, Ui_Form):
834
1105
  return False
835
1106
 
836
1107
  # check that the longuest media is in player #1
837
- durations = []
1108
+ durations: list = []
838
1109
  for i in sorted(list(players.keys())):
839
1110
  durations.append(sum(players[i]))
840
1111
  if [x for x in durations[1:] if x > durations[0]]:
@@ -856,6 +1127,20 @@ class Observation(QDialog, Ui_Form):
856
1127
  )
857
1128
  return False
858
1129
 
1130
+ # check if offset set and only player #1 is used
1131
+ if len(set(players_list)) == 1:
1132
+ for row in range(self.twVideo1.rowCount()):
1133
+ if float(self.twVideo1.item(row, 1).text()):
1134
+ QMessageBox.critical(
1135
+ self,
1136
+ cfg.programName,
1137
+ (
1138
+ "It is not possible to use offset value(s) with only one player,<br>"
1139
+ "The offset values are use to synchronise various players."
1140
+ ),
1141
+ )
1142
+ return False
1143
+
859
1144
  # check offset for external data files
860
1145
  for row in range(self.tw_data_files.rowCount()):
861
1146
  if not is_numeric(self.tw_data_files.item(row, cfg.PLOT_DATA_TIMEOFFSET_IDX).text()):
@@ -871,6 +1156,18 @@ class Observation(QDialog, Ui_Form):
871
1156
  )
872
1157
  return False
873
1158
 
1159
+ # check media creation time tag in metadata
1160
+ # Disable because the check will be made at the observation start
1161
+ """
1162
+ if self.cb_media_creation_date_as_offset.isChecked():
1163
+ if self.check_creation_date():
1164
+ return False
1165
+ """
1166
+
1167
+ # check media creation date time (if option enabled)
1168
+ if self.check_media_creation_date():
1169
+ return False
1170
+
874
1171
  if self.rb_images.isChecked(): # observation based on images directory
875
1172
  if not self.lw_images_directory.count():
876
1173
  QMessageBox.critical(self, cfg.programName, "You have to select at least one images directory")
@@ -879,9 +1176,7 @@ class Observation(QDialog, Ui_Form):
879
1176
  # check if indep variables are correct type
880
1177
  for row in range(self.twIndepVariables.rowCount()):
881
1178
  if self.twIndepVariables.item(row, 1).text() == cfg.NUMERIC:
882
- if self.twIndepVariables.item(row, 2).text() and not is_numeric(
883
- self.twIndepVariables.item(row, 2).text()
884
- ):
1179
+ if self.twIndepVariables.item(row, 2).text() and not is_numeric(self.twIndepVariables.item(row, 2).text()):
885
1180
  QMessageBox.critical(
886
1181
  self,
887
1182
  cfg.programName,
@@ -903,11 +1198,10 @@ class Observation(QDialog, Ui_Form):
903
1198
  )
904
1199
  return False
905
1200
 
1201
+ # check if numeric indep variable values are numeric
906
1202
  for row in range(self.twIndepVariables.rowCount()):
907
1203
  if self.twIndepVariables.item(row, 1).text() == cfg.NUMERIC:
908
- if self.twIndepVariables.item(row, 2).text() and not is_numeric(
909
- self.twIndepVariables.item(row, 2).text()
910
- ):
1204
+ if self.twIndepVariables.item(row, 2).text() and not is_numeric(self.twIndepVariables.item(row, 2).text()):
911
1205
  QMessageBox.critical(
912
1206
  self,
913
1207
  cfg.programName,
@@ -919,7 +1213,7 @@ class Observation(QDialog, Ui_Form):
919
1213
 
920
1214
  def pbLaunch_clicked(self):
921
1215
  """
922
- Close window and start observation
1216
+ Close dialog and start the observation
923
1217
  """
924
1218
 
925
1219
  if self.check_parameters():
@@ -954,24 +1248,54 @@ class Observation(QDialog, Ui_Form):
954
1248
  str: error message or empty string
955
1249
  """
956
1250
 
1251
+ logging.debug(f"check_media function for {file_path}")
1252
+
957
1253
  media_info = util.accurate_media_analysis(self.ffmpeg_bin, file_path)
1254
+
1255
+ logging.debug(f"{media_info=}")
1256
+
958
1257
  if "error" in media_info:
959
- return False, media_info["error"]
1258
+ return (True, media_info["error"])
1259
+
1260
+ if media_info["format_long_name"] == "Tele-typewriter":
1261
+ return (True, "Text file")
1262
+
1263
+ if media_info["duration"] > 0:
1264
+ if " rel " in mode:
1265
+ # convert to relative path (relative to BORIS project file)
1266
+ file_path = str(pl.Path(file_path).relative_to(pl.Path(self.project_path).parent))
1267
+
1268
+ self.mediaDurations[file_path] = float(media_info["duration"])
1269
+ elif media_info["has_video"] is False and media_info["audio_duration"]:
1270
+ self.mediaDurations[file_path] = float(media_info["audio_duration"])
960
1271
  else:
961
- if media_info["duration"] > 0:
1272
+ return (True, "Media duration not available")
962
1273
 
963
- if " rel " in mode:
964
- # convert to relative path (relative to BORIS project file)
965
- file_path = str(pl.Path(file_path).relative_to(pl.Path(self.project_path).parent))
966
-
967
- self.mediaDurations[file_path] = float(media_info["duration"])
968
- self.mediaFPS[file_path] = float(media_info["fps"])
969
- self.mediaHasVideo[file_path] = media_info["has_video"]
970
- self.mediaHasAudio[file_path] = media_info["has_audio"]
971
- self.add_media_to_listview(file_path)
972
- return (False, "")
973
- else:
974
- return (True, "Media duration not available")
1274
+ self.mediaFPS[file_path] = float(media_info["fps"])
1275
+ self.mediaHasVideo[file_path] = media_info["has_video"]
1276
+ self.mediaHasAudio[file_path] = media_info["has_audio"]
1277
+
1278
+ logging.debug(f"{file_path=}")
1279
+
1280
+ self.add_media_to_listview(file_path)
1281
+ return (False, "")
1282
+
1283
+ def update_media_options(self):
1284
+ """
1285
+ update the media options
1286
+ """
1287
+ for w in (
1288
+ self.cbVisualizeSpectrogram,
1289
+ self.cb_visualize_waveform,
1290
+ self.cb_observation_time_interval,
1291
+ self.cb_media_creation_date_as_offset,
1292
+ ):
1293
+ w.setEnabled(self.twVideo1.rowCount() > 0)
1294
+
1295
+ # enable stop ongoing state events if n. media > 1
1296
+ self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(self.twVideo1.rowCount() > 0)
1297
+
1298
+ # self.creation_date_as_offset()
975
1299
 
976
1300
  def add_media(self, mode: str):
977
1301
  """
@@ -999,9 +1323,7 @@ class Observation(QDialog, Ui_Form):
999
1323
  QMessageBox.critical(
1000
1324
  self,
1001
1325
  cfg.programName,
1002
- (
1003
- "It is not possible to add a media file without path or with a relative path if the project is not already saved"
1004
- ),
1326
+ ("It is not possible to add a media file without path or with a relative path if the project is not already saved"),
1005
1327
  )
1006
1328
  return
1007
1329
 
@@ -1012,9 +1334,9 @@ class Observation(QDialog, Ui_Form):
1012
1334
  fd.setDirectory(os.path.expanduser("~") if (" abs " in mode) else str(pl.Path(self.project_path).parent))
1013
1335
 
1014
1336
  if "media " in mode:
1337
+ file_paths, _ = fd.getOpenFileNames(self, "Add media file(s)", "", "All files (*)")
1015
1338
 
1016
- fn = fd.getOpenFileNames(self, "Add media file(s)", "", "All files (*)")
1017
- file_paths = fn[0] if type(fn) is tuple else fn
1339
+ logging.debug(f"{file_paths=}")
1018
1340
 
1019
1341
  if file_paths:
1020
1342
  # store directory for next usage
@@ -1036,39 +1358,40 @@ class Observation(QDialog, Ui_Form):
1036
1358
  if error:
1037
1359
  QMessageBox.critical(self, cfg.programName, f"<b>{file_path}</b>. {msg}")
1038
1360
 
1039
- if "dir " in mode:
1040
-
1361
+ if "dir " in mode: # add media from dir
1041
1362
  dir_name = fd.getExistingDirectory(self, "Select directory")
1042
1363
  if dir_name:
1043
1364
  response = ""
1044
- for file_path in glob.glob(dir_name + os.sep + "*"):
1045
- (error, msg) = self.check_media(file_path, mode)
1365
+ for file_path in sorted(pl.Path(dir_name).glob("*")):
1366
+ if not file_path.is_file():
1367
+ continue
1368
+ (error, msg) = self.check_media(str(file_path), mode)
1046
1369
  if error:
1047
1370
  if response != "Skip all non media files":
1048
1371
  response = dialog.MessageDialog(
1049
1372
  cfg.programName,
1050
1373
  f"<b>{file_path}</b> {msg}",
1051
- ["Continue", "Skip all non media files", "Cancel"],
1374
+ ["Continue", "Skip all non media files", cfg.CANCEL],
1052
1375
  )
1053
- if response == "Cancel":
1376
+ if response == cfg.CANCEL:
1054
1377
  break
1378
+ # ask to use directory name / path as observation id
1379
+ if response != cfg.CANCEL:
1380
+ selected_obs_id = dialog.MessageDialog(
1381
+ cfg.programName,
1382
+ "Select the observation id",
1383
+ [dir_name, str(pl.Path(dir_name).name), cfg.CANCEL],
1384
+ )
1385
+ if selected_obs_id != cfg.CANCEL:
1386
+ self.leObservationId.setText(selected_obs_id)
1055
1387
 
1056
- for w in [
1057
- self.cbVisualizeSpectrogram,
1058
- self.cb_visualize_waveform,
1059
- self.cb_observation_time_interval,
1060
- self.cbCloseCurrentBehaviorsBetweenVideo,
1061
- ]:
1062
- w.setEnabled(self.twVideo1.rowCount() > 0)
1063
-
1064
- # disabled for problems
1065
- self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(False)
1388
+ self.update_media_options()
1066
1389
 
1067
1390
  def add_media_to_listview(self, file_name):
1068
1391
  """
1069
1392
  add media file path to list widget
1070
1393
  """
1071
-
1394
+ # add a row
1072
1395
  self.twVideo1.setRowCount(self.twVideo1.rowCount() + 1)
1073
1396
 
1074
1397
  for col_idx, s in enumerate(
@@ -1109,33 +1432,22 @@ class Observation(QDialog, Ui_Form):
1109
1432
  remove all selected media files from list widget
1110
1433
  """
1111
1434
 
1112
- if self.twVideo1.selectedIndexes():
1113
- rows_to_delete = set([x.row() for x in self.twVideo1.selectedIndexes()])
1114
- for row in sorted(rows_to_delete, reverse=True):
1115
- media_path = self.twVideo1.item(row, cfg.MEDIA_FILE_PATH_IDX).text()
1116
- self.twVideo1.removeRow(row)
1117
- if media_path not in [
1118
- self.twVideo1.item(idx, cfg.MEDIA_FILE_PATH_IDX).text() for idx in range(self.twVideo1.rowCount())
1119
- ]:
1120
- try:
1121
- del self.mediaDurations[media_path]
1122
- except NameError:
1123
- pass
1124
- try:
1125
- del self.mediaFPS[media_path]
1126
- except NameError:
1127
- pass
1128
-
1129
- for w in [
1130
- self.cbVisualizeSpectrogram,
1131
- self.cb_visualize_waveform,
1132
- self.cb_observation_time_interval,
1133
- self.cbCloseCurrentBehaviorsBetweenVideo,
1134
- ]:
1135
- w.setEnabled(self.twVideo1.rowCount() > 0)
1136
-
1137
- # disabled for problems
1138
- self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(False)
1139
-
1140
- else:
1435
+ if not self.twVideo1.selectedIndexes():
1141
1436
  QMessageBox.warning(self, cfg.programName, "No media file selected")
1437
+ return
1438
+
1439
+ rows_to_delete = set([x.row() for x in self.twVideo1.selectedIndexes()])
1440
+ for row in sorted(rows_to_delete, reverse=True):
1441
+ media_path = self.twVideo1.item(row, cfg.MEDIA_FILE_PATH_IDX).text()
1442
+ self.twVideo1.removeRow(row)
1443
+ if media_path not in [self.twVideo1.item(idx, cfg.MEDIA_FILE_PATH_IDX).text() for idx in range(self.twVideo1.rowCount())]:
1444
+ try:
1445
+ del self.mediaDurations[media_path]
1446
+ except NameError:
1447
+ pass
1448
+ try:
1449
+ del self.mediaFPS[media_path]
1450
+ except NameError:
1451
+ pass
1452
+
1453
+ self.update_media_options()