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

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

Potentially problematic release.


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

Files changed (129) hide show
  1. boris/__init__.py +1 -1
  2. boris/__main__.py +1 -1
  3. boris/about.py +36 -39
  4. boris/add_modifier.py +122 -109
  5. boris/add_modifier_ui.py +239 -135
  6. boris/advanced_event_filtering.py +81 -45
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +228 -229
  18. boris/behavior_binary_table.py +33 -50
  19. boris/behaviors_coding_map.py +17 -18
  20. boris/boris_cli.py +6 -25
  21. boris/cmd_arguments.py +12 -1
  22. boris/coding_pad.py +42 -49
  23. boris/config.py +161 -77
  24. boris/config_file.py +63 -83
  25. boris/connections.py +112 -57
  26. boris/converters.py +13 -37
  27. boris/converters_ui.py +187 -110
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +2511 -1824
  30. boris/core_qrc.py +15895 -10185
  31. boris/core_ui.py +946 -792
  32. boris/db_functions.py +21 -41
  33. boris/dev.py +134 -0
  34. boris/dialog.py +505 -244
  35. boris/duration_widget.py +15 -20
  36. boris/edit_event.py +84 -28
  37. boris/edit_event_ui.py +214 -78
  38. boris/event_operations.py +517 -415
  39. boris/events_cursor.py +25 -17
  40. boris/events_snapshots.py +36 -82
  41. boris/exclusion_matrix.py +4 -9
  42. boris/export_events.py +213 -583
  43. boris/export_observation.py +98 -611
  44. boris/external_processes.py +156 -97
  45. boris/geometric_measurement.py +652 -287
  46. boris/gui_utilities.py +91 -14
  47. boris/image_overlay.py +9 -9
  48. boris/import_observations.py +190 -98
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +26 -63
  51. boris/latency.py +34 -25
  52. boris/measurement_widget.py +14 -18
  53. boris/media_file.py +52 -84
  54. boris/menu_options.py +17 -6
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +7 -9
  57. boris/mpv.py +1 -0
  58. boris/mpv2.py +732 -705
  59. boris/observation.py +655 -310
  60. boris/observation_operations.py +1036 -404
  61. boris/observation_ui.py +584 -356
  62. boris/observations_list.py +71 -53
  63. boris/otx_parser.py +74 -80
  64. boris/param_panel.py +31 -16
  65. boris/param_panel_ui.py +254 -138
  66. boris/player_dock_widget.py +90 -60
  67. boris/plot_data_module.py +43 -46
  68. boris/plot_events.py +127 -90
  69. boris/plot_events_rt.py +17 -31
  70. boris/plot_spectrogram_rt.py +95 -30
  71. boris/plot_waveform_rt.py +32 -21
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +18 -8
  74. boris/portion/const.py +35 -18
  75. boris/portion/dict.py +5 -5
  76. boris/portion/func.py +2 -2
  77. boris/portion/interval.py +21 -41
  78. boris/portion/io.py +41 -32
  79. boris/preferences.py +306 -83
  80. boris/preferences_ui.py +685 -228
  81. boris/project.py +448 -293
  82. boris/project_functions.py +689 -254
  83. boris/project_import_export.py +213 -222
  84. boris/project_ui.py +674 -438
  85. boris/qrc_boris.py +6 -3
  86. boris/qrc_boris5.py +6 -3
  87. boris/select_modifiers.py +74 -48
  88. boris/select_observations.py +20 -199
  89. boris/select_subj_behav.py +67 -39
  90. boris/state_events.py +53 -37
  91. boris/subjects_pad.py +6 -9
  92. boris/synthetic_time_budget.py +45 -28
  93. boris/time_budget_functions.py +171 -171
  94. boris/time_budget_widget.py +84 -114
  95. boris/transitions.py +41 -47
  96. boris/utilities.py +766 -266
  97. boris/version.py +3 -3
  98. boris/video_equalizer.py +16 -14
  99. boris/video_equalizer_ui.py +199 -130
  100. boris/video_operations.py +125 -28
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.6.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
  106. {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
  107. boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
  108. boris/README.TXT +0 -22
  109. boris/add_modifier.ui +0 -323
  110. boris/boris_ui.py +0 -886
  111. boris/converters.ui +0 -289
  112. boris/core.qrc +0 -35
  113. boris/core.ui +0 -1543
  114. boris/edit_event.ui +0 -175
  115. boris/icons/logo_eye.ico +0 -0
  116. boris/map_creator.py +0 -850
  117. boris/observation.ui +0 -773
  118. boris/param_panel.ui +0 -379
  119. boris/preferences.ui +0 -537
  120. boris/project.ui +0 -1069
  121. boris/project_server.py +0 -236
  122. boris/vlc.py +0 -10343
  123. boris/vlc_local.py +0 -90
  124. boris_behav_obs-8.9.16.dist-info/LICENSE.TXT +0 -674
  125. boris_behav_obs-8.9.16.dist-info/METADATA +0 -129
  126. boris_behav_obs-8.9.16.dist-info/RECORD +0 -108
  127. boris_behav_obs-8.9.16.dist-info/entry_points.txt +0 -2
  128. {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
  129. {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/top_level.txt +0 -0
boris/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 = {}, {}, {}, {}, {}
178
236
 
179
- # disabled due to problem when video goes back
180
- self.cbCloseCurrentBehaviorsBetweenVideo.setChecked(False)
181
- self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(False)
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)
245
+
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()
@@ -227,26 +346,76 @@ class Observation(QDialog, Ui_Form):
227
346
  change stacked widget page in base at the observation type
228
347
  """
229
348
 
230
- for idx, rb in enumerate([self.rb_media_files, self.rb_live, self.rb_images]):
349
+ for idx, rb in enumerate((self.rb_media_files, self.rb_live, self.rb_images)):
231
350
  if rb.isChecked():
232
351
  self.sw_observation_type.setCurrentIndex(idx + 1)
233
352
 
234
- def add_images_directory(self):
353
+ # hide 'limit observation to time interval' for images
354
+ self.cb_observation_time_interval.setEnabled(not self.rb_images.isChecked())
355
+
356
+ def add_images_directory(self, mode: str):
235
357
  """
236
358
  add path to images directory
237
359
  """
238
- 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
+
239
389
  result = util.dir_images_number(dir_path)
240
390
  if not result.get("number of images", 0):
241
391
  response = dialog.MessageDialog(
242
392
  cfg.programName,
243
- "The directory does not contain images (*.jpg, *.jpeg, *.png)",
393
+ f"The directory does not contain images ({','.join(cfg.IMAGE_EXTENSIONS)})",
244
394
  ["Cancel", "Add directory"],
245
395
  )
246
396
  if response == "Cancel":
247
397
  return
248
398
 
249
- 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))
418
+ self.lb_images_info.setText(f"Number of images in {dir_path}: {result.get('number of images', 0)}")
250
419
 
251
420
  def remove_images_directory(self):
252
421
  """
@@ -285,10 +454,14 @@ class Observation(QDialog, Ui_Form):
285
454
  """
286
455
 
287
456
  if self.cb_observation_time_interval.isChecked():
288
- 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)
289
462
  time_interval_dialog.time_widget.set_time(0)
290
463
  time_interval_dialog.setWindowTitle("Start observation at")
291
- time_interval_dialog.label.setText("Start observation at")
464
+ time_interval_dialog.label.setText("<b>Start</b> observation at")
292
465
  start_time, stop_time = 0, 0
293
466
  if time_interval_dialog.exec_():
294
467
  start_time = time_interval_dialog.time_widget.get_time()
@@ -297,7 +470,7 @@ class Observation(QDialog, Ui_Form):
297
470
  return
298
471
  time_interval_dialog.time_widget.set_time(0)
299
472
  time_interval_dialog.setWindowTitle("Stop observation at")
300
- time_interval_dialog.label.setText("Stop observation at")
473
+ time_interval_dialog.label.setText("<b>Stop</b> observation at")
301
474
  if time_interval_dialog.exec_():
302
475
  stop_time = time_interval_dialog.time_widget.get_time()
303
476
  else:
@@ -311,7 +484,10 @@ class Observation(QDialog, Ui_Form):
311
484
  return
312
485
  self.observation_time_interval = [start_time, stop_time]
313
486
  self.cb_observation_time_interval.setText(
314
- 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
+ )
315
491
  )
316
492
  else:
317
493
  self.observation_time_interval = [0, 0]
@@ -333,9 +509,7 @@ class Observation(QDialog, Ui_Form):
333
509
 
334
510
  if w.exec_():
335
511
  d = {}
336
- for col_idx, cb in zip(
337
- self.tw_data_files.item(row, cfg.PLOT_DATA_COLUMNS_IDX).text().split(","), w.cbb
338
- ):
512
+ for col_idx, cb in zip(self.tw_data_files.item(row, cfg.PLOT_DATA_COLUMNS_IDX).text().split(","), w.cbb):
339
513
  if cb.currentText() != "None":
340
514
  d[col_idx] = cb.currentText()
341
515
  self.tw_data_files.item(row, cfg.PLOT_DATA_CONVERTERS_IDX).setText(str(d))
@@ -350,12 +524,12 @@ class Observation(QDialog, Ui_Form):
350
524
 
351
525
  if self.pb_plot_data.text() != "Show plot":
352
526
  self.test.close_plot()
527
+ self.text = None
353
528
  # update button text
354
529
  self.pb_plot_data.setText("Show plot")
355
530
  return
356
531
 
357
532
  if self.tw_data_files.selectedIndexes() or self.tw_data_files.rowCount() == 1:
358
-
359
533
  if self.tw_data_files.rowCount() == 1:
360
534
  row_idx = 0
361
535
  else:
@@ -375,9 +549,7 @@ class Observation(QDialog, Ui_Form):
375
549
  time_interval = int(self.tw_data_files.item(row_idx, cfg.PLOT_DATA_TIMEINTERVAL_IDX).text())
376
550
  time_offset = int(self.tw_data_files.item(row_idx, cfg.PLOT_DATA_TIMEOFFSET_IDX).text())
377
551
 
378
- substract_first_value = self.tw_data_files.cellWidget(
379
- row_idx, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX
380
- ).currentText()
552
+ substract_first_value = self.tw_data_files.cellWidget(row_idx, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX).currentText()
381
553
 
382
554
  plot_color = self.tw_data_files.cellWidget(row_idx, cfg.PLOT_DATA_PLOTCOLOR_IDX).currentText()
383
555
 
@@ -411,7 +583,7 @@ class Observation(QDialog, Ui_Form):
411
583
 
412
584
  if self.test.error_msg:
413
585
  QMessageBox.critical(self, cfg.programName, f"Impossible to plot data:\n{self.test.error_msg}")
414
- del self.test
586
+ self.test = None
415
587
  return
416
588
 
417
589
  # self.test.setWindowFlags(self.test.windowFlags() | Qt.WindowStaysOnTopHint)
@@ -419,16 +591,22 @@ class Observation(QDialog, Ui_Form):
419
591
  self.test.update_plot(0)
420
592
  # update button text
421
593
  self.pb_plot_data.setText("Close plot")
422
-
423
594
  else:
424
595
  QMessageBox.warning(self, cfg.programName, "Select a data file")
425
596
 
426
- def add_data_file(self, mode=True):
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
+
604
+ def add_data_file(self, mode: str):
427
605
  """
428
606
  user select a data file to be plotted synchronously with media file
429
607
 
430
608
  Args:
431
- flag_path (bool): True to store path of data file else False
609
+ mode (str): statusTip() data abs path / data rel path
432
610
  """
433
611
 
434
612
  if mode.split("|")[0] not in (
@@ -447,9 +625,7 @@ class Observation(QDialog, Ui_Form):
447
625
  QMessageBox.critical(
448
626
  self,
449
627
  cfg.programName,
450
- (
451
- "It is not possible to add a data file without path or with a relative path if the project is not already saved"
452
- ),
628
+ ("It is not possible to add a data file with a relative path if the project is not already saved"),
453
629
  )
454
630
  return
455
631
 
@@ -458,127 +634,137 @@ class Observation(QDialog, Ui_Form):
458
634
  QMessageBox.warning(
459
635
  self,
460
636
  cfg.programName,
461
- (
462
- "It is not yet possible to plot more than 2 external data sources"
463
- "This limitation will be removed in future"
464
- ),
637
+ ("It is not yet possible to plot more than 2 external data sourcesThis limitation will be removed in future"),
465
638
  )
466
639
  return
467
640
 
468
641
  fd = QFileDialog()
469
642
  fd.setDirectory(os.path.expanduser("~") if (" abs " in mode) else str(pl.Path(self.project_path).parent))
470
643
 
471
- fn = fd.getOpenFileName(self, "Add data file", "", "All files (*)")
472
- file_name = fn[0] if type(fn) is tuple else fn
473
-
474
- if file_name:
644
+ file_name, _ = fd.getOpenFileName(self, "Add data file", "", "All files (*)")
645
+ if not file_name:
646
+ return
475
647
 
476
- columns_to_plot = "1,2" # columns to plot by default
648
+ columns_to_plot = "1,2" # columns to plot by default
477
649
 
478
- # check data file
479
- r = util.check_txt_file(file_name) # check_txt_file defined in utilities
650
+ # check data file
651
+ file_parameters = util.check_txt_file(file_name)
480
652
 
481
- if "error" in r:
482
- QMessageBox.critical(self, cfg.programName, r["error"])
483
- return
653
+ if "error" in file_parameters:
654
+ QMessageBox.critical(self, cfg.programName, f"Error on file {file_name}: {file_parameters['error']}")
655
+ return
484
656
 
485
- if not r["homogeneous"]: # not all rows have 2 columns
486
- QMessageBox.critical(self, cfg.programName, "This file does not contain a constant number of columns")
487
- return
657
+ if not file_parameters["homogeneous"]: # the number of columns is not constant
658
+ QMessageBox.critical(self, cfg.programName, "This file does not contain a constant number of columns")
659
+ return
488
660
 
489
- header = util.return_file_header(file_name, row_number=10)
661
+ header, footer = util.return_file_header_footer(file_name, file_row_number=file_parameters["rows number"], row_number=5)
490
662
 
491
- if not header:
492
- return # problem with header
663
+ if not header:
664
+ QMessageBox.critical(self, cfg.programName, f"Error on file {pl.Path(file_name).name}")
665
+ return
493
666
 
494
- w = dialog.View_data_head()
495
- w.setWindowTitle(f"Data file: {pl.Path(file_name).name}")
496
- """w.setWindowFlags(Qt.WindowStaysOnTopHint)"""
667
+ w = dialog.View_data()
668
+ w.setWindowTitle("View data")
669
+ w.lb.setText(f"View first and last rows of <b>{pl.Path(file_name).name}</b> file")
497
670
 
498
- w.tw.setColumnCount(r["fields number"])
671
+ w.tw.setColumnCount(file_parameters["fields number"])
672
+ if footer:
673
+ hf = header + [file_parameters["separator"].join(["..."] * file_parameters["fields number"])] + footer
674
+ w.tw.setRowCount(len(header) + len(footer) + 1)
675
+ else:
676
+ hf = header
499
677
  w.tw.setRowCount(len(header))
500
678
 
501
- for row in range(len(header)):
502
- for col, v in enumerate(header[row].split(r["separator"])):
503
- item = QTableWidgetItem(v)
504
- item.setFlags(Qt.ItemIsEnabled)
505
- w.tw.setItem(row, col, item)
506
-
507
- while True:
508
- flag_ok = True
509
- if w.exec_():
510
- columns_to_plot = w.le.text().replace(" ", "")
511
- for col in columns_to_plot.split(","):
512
- try:
513
- col_idx = int(col)
514
- except ValueError:
515
- QMessageBox.critical(
516
- self, cfg.programName, f"<b>{col}</b> does not seem to be a column index"
517
- )
518
- flag_ok = False
519
- break
520
- if col_idx <= 0 or col_idx > r["fields number"]:
521
- QMessageBox.critical(self, cfg.programName, f"<b>{col}</b> is not a valid column index")
522
- flag_ok = False
523
- break
524
- if flag_ok:
679
+ for idx, row in enumerate(hf):
680
+ for col, v in enumerate(row.split(file_parameters["separator"])):
681
+ item = QTableWidgetItem(v)
682
+ item.setFlags(Qt.ItemIsEnabled)
683
+ w.tw.setItem(idx, col, item)
684
+
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)
696
+
697
+ while True:
698
+ flag_ok = True
699
+ if w.exec_():
700
+ columns_to_plot = w.le.text().replace(" ", "")
701
+ for col in columns_to_plot.split(","):
702
+ try:
703
+ col_idx = int(col)
704
+ except ValueError:
705
+ QMessageBox.critical(self, cfg.programName, f"<b>{col}</b> does not seem to be a column index")
706
+ flag_ok = False
525
707
  break
526
- else:
527
- return
528
-
708
+ if col_idx <= 0 or col_idx > file_parameters["fields number"]:
709
+ QMessageBox.critical(self, cfg.programName, f"<b>{col}</b> is not a valid column index")
710
+ flag_ok = False
711
+ break
712
+ if flag_ok:
713
+ break
529
714
  else:
530
715
  return
531
716
 
532
- self.tw_data_files.setRowCount(self.tw_data_files.rowCount() + 1)
533
-
534
- if " rel " in mode:
535
-
536
- try:
537
- file_path = str(pl.Path(file_name).relative_to(pl.Path(self.project_path).parent))
538
- except ValueError:
539
- QMessageBox.critical(
540
- self,
541
- cfg.programName,
542
- f"The directory <b>{pl.Path(file_name).parent}</b> is not contained in <b>{pl.Path(self.project_path).parent}</b>.",
543
- )
544
- return
717
+ else:
718
+ return
545
719
 
546
- else: # save absolute path
547
- file_path = file_name
548
-
549
- for col_idx, value in zip(
550
- [
551
- cfg.PLOT_DATA_FILEPATH_IDX,
552
- cfg.PLOT_DATA_COLUMNS_IDX,
553
- cfg.PLOT_DATA_PLOTTITLE_IDX,
554
- cfg.PLOT_DATA_VARIABLENAME_IDX,
555
- cfg.PLOT_DATA_CONVERTERS_IDX,
556
- cfg.PLOT_DATA_TIMEINTERVAL_IDX,
557
- cfg.PLOT_DATA_TIMEOFFSET_IDX,
558
- ],
559
- [file_path, columns_to_plot, "", "", "", "60", "0"],
560
- ):
561
- item = QTableWidgetItem(value)
562
- if col_idx == cfg.PLOT_DATA_CONVERTERS_IDX:
563
- item.setFlags(Qt.ItemIsEnabled)
564
- item.setBackground(QColor(230, 230, 230))
565
- self.tw_data_files.setItem(self.tw_data_files.rowCount() - 1, col_idx, item)
566
-
567
- # substract first value
568
- combobox = QComboBox()
569
- combobox.addItems(["True", "False"])
570
- self.tw_data_files.setCellWidget(
571
- self.tw_data_files.rowCount() - 1, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX, combobox
572
- )
720
+ self.tw_data_files.setRowCount(self.tw_data_files.rowCount() + 1)
573
721
 
574
- # plot line color
575
- combobox = QComboBox()
576
- combobox.addItems(cfg.DATA_PLOT_STYLES)
577
- self.tw_data_files.setCellWidget(self.tw_data_files.rowCount() - 1, cfg.PLOT_DATA_PLOTCOLOR_IDX, combobox)
722
+ if " rel " in mode:
723
+ try:
724
+ file_path = str(pl.Path(file_name).relative_to(pl.Path(self.project_path).parent))
725
+ except ValueError:
726
+ QMessageBox.critical(
727
+ self,
728
+ cfg.programName,
729
+ f"The directory <b>{pl.Path(file_name).parent}</b> is not contained in <b>{pl.Path(self.project_path).parent}</b>.",
730
+ )
731
+ return
578
732
 
579
- def view_data_file_head(self):
733
+ else: # save absolute path
734
+ file_path = file_name
735
+
736
+ for col_idx, value in zip(
737
+ [
738
+ cfg.PLOT_DATA_FILEPATH_IDX,
739
+ cfg.PLOT_DATA_COLUMNS_IDX,
740
+ cfg.PLOT_DATA_PLOTTITLE_IDX,
741
+ cfg.PLOT_DATA_VARIABLENAME_IDX,
742
+ cfg.PLOT_DATA_CONVERTERS_IDX,
743
+ cfg.PLOT_DATA_TIMEINTERVAL_IDX,
744
+ cfg.PLOT_DATA_TIMEOFFSET_IDX,
745
+ ],
746
+ [file_path, columns_to_plot, "", "", "", "60", "0"],
747
+ ):
748
+ item = QTableWidgetItem(value)
749
+ if col_idx == cfg.PLOT_DATA_CONVERTERS_IDX:
750
+ item.setFlags(Qt.ItemIsEnabled)
751
+ # item.setBackground(QColor(230, 230, 230))
752
+ item.setBackground(self.not_editable_column_color())
753
+ self.tw_data_files.setItem(self.tw_data_files.rowCount() - 1, col_idx, item)
754
+
755
+ # substract first value
756
+ combobox = QComboBox()
757
+ combobox.addItems(["True", "False"])
758
+ self.tw_data_files.setCellWidget(self.tw_data_files.rowCount() - 1, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX, combobox)
759
+
760
+ # plot line color
761
+ combobox = QComboBox()
762
+ combobox.addItems(cfg.DATA_PLOT_STYLES)
763
+ self.tw_data_files.setCellWidget(self.tw_data_files.rowCount() - 1, cfg.PLOT_DATA_PLOTCOLOR_IDX, combobox)
764
+
765
+ def view_data_file_head_tail(self) -> None:
580
766
  """
581
- view first parts of data file
767
+ view first and last rows of data file
582
768
  """
583
769
 
584
770
  if not self.tw_data_files.selectedIndexes() and self.tw_data_files.rowCount() != 1:
@@ -587,111 +773,202 @@ class Observation(QDialog, Ui_Form):
587
773
  if self.tw_data_files.rowCount() == 1:
588
774
  data_file_path = project_functions.full_path(self.tw_data_files.item(0, 0).text(), self.project_path)
589
775
  columns_to_plot = self.tw_data_files.item(0, 1).text()
590
- else:
776
+ else: # selected file
591
777
  data_file_path = project_functions.full_path(
592
778
  self.tw_data_files.item(self.tw_data_files.selectedIndexes()[0].row(), 0).text(), self.project_path
593
779
  )
594
780
  columns_to_plot = self.tw_data_files.item(self.tw_data_files.selectedIndexes()[0].row(), 1).text()
595
781
 
596
782
  file_parameters = util.check_txt_file(data_file_path)
783
+
597
784
  if "error" in file_parameters:
598
785
  QMessageBox.critical(self, cfg.programName, f"Error on file {data_file_path}: {file_parameters['error']}")
599
786
  return
600
- header = util.return_file_header(data_file_path)
601
-
602
- if header:
787
+ header, footer = util.return_file_header_footer(data_file_path, file_row_number=file_parameters["rows number"], row_number=5)
603
788
 
604
- w = dialog.View_data_head()
605
- w.setWindowTitle(f"Data file: {pl.Path(data_file_path).name}")
606
- w.label.setText("Index of columns to plot")
607
- w.le.setEnabled(False)
608
- w.le.setText(columns_to_plot)
609
- w.pbCancel.setVisible(False)
789
+ if not header:
790
+ QMessageBox.critical(self, cfg.programName, f"Error on file {pl.Path(data_file_path).name}")
791
+ return
610
792
 
611
- w.tw.setColumnCount(file_parameters["fields number"])
793
+ w = dialog.View_data()
794
+ w.setWindowTitle("View data")
795
+ w.lb.setText(f"View first and last rows of <b>{pl.Path(data_file_path).name}</b> file")
796
+ w.pbOK.setText(cfg.CLOSE)
797
+ w.label.setText("Index of columns to plot")
798
+ w.le.setEnabled(False)
799
+ w.le.setText(columns_to_plot)
800
+ w.pbCancel.setVisible(False)
801
+
802
+ w.tw.setColumnCount(file_parameters["fields number"])
803
+ if footer:
804
+ hf = header + [file_parameters["separator"].join(["..."] * file_parameters["fields number"])] + footer
805
+ w.tw.setRowCount(len(header) + len(footer) + 1)
806
+ else:
807
+ hf = header
612
808
  w.tw.setRowCount(len(header))
613
809
 
614
- for row in range(len(header)):
615
- for col, v in enumerate(header[row].split(file_parameters["separator"])):
616
- w.tw.setItem(row, col, QTableWidgetItem(v))
810
+ for idx, row in enumerate(hf):
811
+ for col, v in enumerate(row.split(file_parameters["separator"])):
812
+ item = QTableWidgetItem(v)
813
+ item.setFlags(Qt.ItemIsEnabled)
814
+ w.tw.setItem(idx, col, item)
617
815
 
618
- w.exec_()
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)
619
826
 
620
- else:
621
- QMessageBox.critical(self, cfg.programName, f"Error on file {data_file_path}")
827
+ stats_out = str(df.describe())
828
+ except Exception:
829
+ stats_out = "Not available"
830
+ w.stats.setPlainText(stats_out)
831
+
832
+ w.exec_()
622
833
 
623
834
  def extract_wav(self):
624
835
  """
625
836
  extract wav of all media files loaded in player #1
626
837
  """
627
838
 
628
- if self.cbVisualizeSpectrogram.isChecked() or self.cb_visualize_waveform.isChecked():
629
- flag_wav_produced = False
630
- # check if player 1 is selected
631
- flag_player1 = False
632
- for row in range(self.twVideo1.rowCount()):
633
- if self.twVideo1.cellWidget(row, 0).currentText() == "1":
634
- flag_player1 = True
839
+ if not self.cbVisualizeSpectrogram.isChecked() and not self.cb_visualize_waveform.isChecked():
840
+ return
635
841
 
636
- if not flag_player1:
637
- QMessageBox.critical(self, cfg.programName, "The player #1 is not selected")
638
- self.cbVisualizeSpectrogram.setChecked(False)
639
- self.cb_visualize_waveform.setChecked(False)
640
- return
641
- """
642
- if dialog.MessageDialog(programName, ("You choose to visualize the spectrogram or waveform for the media in player #1.<br>"
643
- "The WAV will be extracted from the media files, be patient"), [YES, NO]) == YES:
644
- """
645
- 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
646
848
 
647
- w = dialog.Info_widget()
648
- w.resize(350, 100)
649
- # w.setWindowFlags(Qt.WindowStaysOnTopHint)
650
- w.setWindowTitle("BORIS")
651
- 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
652
854
 
653
- for row in range(self.twVideo1.rowCount()):
654
- # check if player 1
655
- if self.twVideo1.cellWidget(row, 0).currentText() != "1":
656
- 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...")
657
861
 
658
- media_file_path = project_functions.full_path(
659
- self.twVideo1.item(row, cfg.MEDIA_FILE_PATH_IDX).text(), self.project_path
660
- )
661
- 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) == "":
662
878
  QMessageBox.critical(
663
- 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}",
664
882
  )
665
883
  flag_wav_produced = False
666
884
  break
667
885
 
668
- if os.path.isfile(media_file_path):
669
- w.show()
670
- QApplication.processEvents()
886
+ w.hide()
671
887
 
672
- if util.extract_wav(self.ffmpeg_bin, media_file_path, self.tmp_dir) == "":
673
- QMessageBox.critical(
674
- self,
675
- cfg.programName,
676
- f"Error during extracting WAV of the media file {media_file_path}",
677
- )
678
- flag_wav_produced = False
679
- break
888
+ flag_wav_produced = True
889
+ else:
890
+ QMessageBox.warning(self, cfg.programName, f"<b>{media_file_path}</b> file not found")
680
891
 
681
- w.hide()
892
+ if not flag_wav_produced:
893
+ self.cbVisualizeSpectrogram.setChecked(False)
894
+ self.cb_visualize_waveform.setChecked(False)
682
895
 
683
- flag_wav_produced = True
684
- else:
685
- 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
686
900
 
687
- if not flag_wav_produced:
688
- self.cbVisualizeSpectrogram.setChecked(False)
689
- self.cb_visualize_waveform.setChecked(False)
690
- """
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
691
964
  else:
692
- self.cbVisualizeSpectrogram.setChecked(False)
693
- self.cb_visualize_waveform.setChecked(False)
694
- """
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
695
972
 
696
973
  def closeEvent(self, event):
697
974
  """
@@ -699,6 +976,7 @@ class Observation(QDialog, Ui_Form):
699
976
  """
700
977
  if self.test is not None:
701
978
  self.test.close_plot()
979
+ self.text = None
702
980
 
703
981
  def pbCancel_clicked(self):
704
982
  """
@@ -706,13 +984,16 @@ class Observation(QDialog, Ui_Form):
706
984
  """
707
985
  if self.test is not None:
708
986
  self.test.close_plot()
987
+ self.text = None
709
988
  self.reject()
710
989
 
711
- def check_parameters(self):
990
+ def check_parameters(self) -> bool:
712
991
  """
713
992
  check observation parameters
714
993
 
715
- return True if everything OK else False
994
+ Returns:
995
+ bool: True if everything is OK else False
996
+
716
997
  """
717
998
 
718
999
  def is_numeric(s):
@@ -733,21 +1014,40 @@ class Observation(QDialog, Ui_Form):
733
1014
 
734
1015
  # check if observation id not empty
735
1016
  if not self.leObservationId.text():
736
- self.qm = QMessageBox()
737
- self.qm.setIcon(QMessageBox.Critical)
738
- self.qm.setText("The <b>observation id</b> is mandatory and must be unique.")
739
- self.qm.exec_()
1017
+ QMessageBox.critical(
1018
+ self,
1019
+ cfg.programName,
1020
+ "The <b>observation id</b> is mandatory and must be unique.",
1021
+ )
740
1022
  return False
741
1023
 
742
1024
  # check if observation_type
743
1025
  if not any((self.rb_media_files.isChecked(), self.rb_live.isChecked(), self.rb_images.isChecked())):
744
- self.qm = QMessageBox()
745
- self.qm.setIcon(QMessageBox.Critical)
746
- self.qm.setText("Choose an observation type.")
747
- self.qm.exec_()
1026
+ QMessageBox.critical(
1027
+ self,
1028
+ cfg.programName,
1029
+ "Choose an observation type.",
1030
+ )
748
1031
  return False
749
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
+
750
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
+
751
1051
  # check player number
752
1052
  players_list: list = []
753
1053
  players: dict = {} # for storing duration
@@ -760,18 +1060,20 @@ class Observation(QDialog, Ui_Form):
760
1060
 
761
1061
  # check if player #1 is used
762
1062
  if not players_list or min(players_list) > 1:
763
- self.qm = QMessageBox()
764
- self.qm.setIcon(QMessageBox.Critical)
765
- self.qm.setText("A media file must be loaded in player #1")
766
- self.qm.exec_()
1063
+ QMessageBox.critical(
1064
+ self,
1065
+ cfg.programName,
1066
+ "A media file must be loaded in player #1",
1067
+ )
767
1068
  return False
768
1069
 
769
1070
  # check if players are used in crescent order
770
1071
  if set(list(range(min(players_list), max(players_list) + 1))) != set(players_list):
771
- self.qm = QMessageBox()
772
- self.qm.setIcon(QMessageBox.Critical)
773
- self.qm.setText("Some player are not used. Please reorganize your media files")
774
- self.qm.exec_()
1072
+ QMessageBox.critical(
1073
+ self,
1074
+ cfg.programName,
1075
+ "Some player are not used. Please reorganize your media files",
1076
+ )
775
1077
  return False
776
1078
 
777
1079
  # check if more media in player #1 and media in other players
@@ -803,7 +1105,7 @@ class Observation(QDialog, Ui_Form):
803
1105
  return False
804
1106
 
805
1107
  # check that the longuest media is in player #1
806
- durations = []
1108
+ durations: list = []
807
1109
  for i in sorted(list(players.keys())):
808
1110
  durations.append(sum(players[i]))
809
1111
  if [x for x in durations[1:] if x > durations[0]]:
@@ -825,6 +1127,20 @@ class Observation(QDialog, Ui_Form):
825
1127
  )
826
1128
  return False
827
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
+
828
1144
  # check offset for external data files
829
1145
  for row in range(self.tw_data_files.rowCount()):
830
1146
  if not is_numeric(self.tw_data_files.item(row, cfg.PLOT_DATA_TIMEOFFSET_IDX).text()):
@@ -840,6 +1156,18 @@ class Observation(QDialog, Ui_Form):
840
1156
  )
841
1157
  return False
842
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
+
843
1171
  if self.rb_images.isChecked(): # observation based on images directory
844
1172
  if not self.lw_images_directory.count():
845
1173
  QMessageBox.critical(self, cfg.programName, "You have to select at least one images directory")
@@ -848,9 +1176,7 @@ class Observation(QDialog, Ui_Form):
848
1176
  # check if indep variables are correct type
849
1177
  for row in range(self.twIndepVariables.rowCount()):
850
1178
  if self.twIndepVariables.item(row, 1).text() == cfg.NUMERIC:
851
- if self.twIndepVariables.item(row, 2).text() and not is_numeric(
852
- self.twIndepVariables.item(row, 2).text()
853
- ):
1179
+ if self.twIndepVariables.item(row, 2).text() and not is_numeric(self.twIndepVariables.item(row, 2).text()):
854
1180
  QMessageBox.critical(
855
1181
  self,
856
1182
  cfg.programName,
@@ -872,11 +1198,10 @@ class Observation(QDialog, Ui_Form):
872
1198
  )
873
1199
  return False
874
1200
 
1201
+ # check if numeric indep variable values are numeric
875
1202
  for row in range(self.twIndepVariables.rowCount()):
876
1203
  if self.twIndepVariables.item(row, 1).text() == cfg.NUMERIC:
877
- if self.twIndepVariables.item(row, 2).text() and not is_numeric(
878
- self.twIndepVariables.item(row, 2).text()
879
- ):
1204
+ if self.twIndepVariables.item(row, 2).text() and not is_numeric(self.twIndepVariables.item(row, 2).text()):
880
1205
  QMessageBox.critical(
881
1206
  self,
882
1207
  cfg.programName,
@@ -888,12 +1213,13 @@ class Observation(QDialog, Ui_Form):
888
1213
 
889
1214
  def pbLaunch_clicked(self):
890
1215
  """
891
- Close window and start observation
1216
+ Close dialog and start the observation
892
1217
  """
893
1218
 
894
1219
  if self.check_parameters():
895
1220
  if self.test is not None:
896
1221
  self.test.close_plot()
1222
+ self.text = None
897
1223
  self.done(2)
898
1224
 
899
1225
  def pbSave_clicked(self):
@@ -904,6 +1230,7 @@ class Observation(QDialog, Ui_Form):
904
1230
  self.state = "accepted"
905
1231
  if self.test is not None:
906
1232
  self.test.close_plot()
1233
+ self.text = None
907
1234
  self.accept()
908
1235
  else:
909
1236
  self.state = "refused"
@@ -921,24 +1248,54 @@ class Observation(QDialog, Ui_Form):
921
1248
  str: error message or empty string
922
1249
  """
923
1250
 
1251
+ logging.debug(f"check_media function for {file_path}")
1252
+
924
1253
  media_info = util.accurate_media_analysis(self.ffmpeg_bin, file_path)
1254
+
1255
+ logging.debug(f"{media_info=}")
1256
+
925
1257
  if "error" in media_info:
926
- 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"])
927
1271
  else:
928
- if media_info["duration"] > 0:
1272
+ return (True, "Media duration not available")
929
1273
 
930
- if " rel " in mode:
931
- # convert to relative path (relative to BORIS project file)
932
- file_path = str(pl.Path(file_path).relative_to(pl.Path(self.project_path).parent))
933
-
934
- self.mediaDurations[file_path] = float(media_info["duration"])
935
- self.mediaFPS[file_path] = float(media_info["fps"])
936
- self.mediaHasVideo[file_path] = media_info["has_video"]
937
- self.mediaHasAudio[file_path] = media_info["has_audio"]
938
- self.add_media_to_listview(file_path)
939
- return (False, "")
940
- else:
941
- 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()
942
1299
 
943
1300
  def add_media(self, mode: str):
944
1301
  """
@@ -966,9 +1323,7 @@ class Observation(QDialog, Ui_Form):
966
1323
  QMessageBox.critical(
967
1324
  self,
968
1325
  cfg.programName,
969
- (
970
- "It is not possible to add a media file without path or with a relative path if the project is not already saved"
971
- ),
1326
+ ("It is not possible to add a media file without path or with a relative path if the project is not already saved"),
972
1327
  )
973
1328
  return
974
1329
 
@@ -979,9 +1334,9 @@ class Observation(QDialog, Ui_Form):
979
1334
  fd.setDirectory(os.path.expanduser("~") if (" abs " in mode) else str(pl.Path(self.project_path).parent))
980
1335
 
981
1336
  if "media " in mode:
1337
+ file_paths, _ = fd.getOpenFileNames(self, "Add media file(s)", "", "All files (*)")
982
1338
 
983
- fn = fd.getOpenFileNames(self, "Add media file(s)", "", "All files (*)")
984
- file_paths = fn[0] if type(fn) is tuple else fn
1339
+ logging.debug(f"{file_paths=}")
985
1340
 
986
1341
  if file_paths:
987
1342
  # store directory for next usage
@@ -1003,39 +1358,40 @@ class Observation(QDialog, Ui_Form):
1003
1358
  if error:
1004
1359
  QMessageBox.critical(self, cfg.programName, f"<b>{file_path}</b>. {msg}")
1005
1360
 
1006
- if "dir " in mode:
1007
-
1361
+ if "dir " in mode: # add media from dir
1008
1362
  dir_name = fd.getExistingDirectory(self, "Select directory")
1009
1363
  if dir_name:
1010
1364
  response = ""
1011
- for file_path in glob.glob(dir_name + os.sep + "*"):
1012
- (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)
1013
1369
  if error:
1014
1370
  if response != "Skip all non media files":
1015
1371
  response = dialog.MessageDialog(
1016
1372
  cfg.programName,
1017
1373
  f"<b>{file_path}</b> {msg}",
1018
- ["Continue", "Skip all non media files", "Cancel"],
1374
+ ["Continue", "Skip all non media files", cfg.CANCEL],
1019
1375
  )
1020
- if response == "Cancel":
1376
+ if response == cfg.CANCEL:
1021
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)
1022
1387
 
1023
- for w in [
1024
- self.cbVisualizeSpectrogram,
1025
- self.cb_visualize_waveform,
1026
- self.cb_observation_time_interval,
1027
- self.cbCloseCurrentBehaviorsBetweenVideo,
1028
- ]:
1029
- w.setEnabled(self.twVideo1.rowCount() > 0)
1030
-
1031
- # disabled for problems
1032
- self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(False)
1388
+ self.update_media_options()
1033
1389
 
1034
1390
  def add_media_to_listview(self, file_name):
1035
1391
  """
1036
1392
  add media file path to list widget
1037
1393
  """
1038
-
1394
+ # add a row
1039
1395
  self.twVideo1.setRowCount(self.twVideo1.rowCount() + 1)
1040
1396
 
1041
1397
  for col_idx, s in enumerate(
@@ -1076,33 +1432,22 @@ class Observation(QDialog, Ui_Form):
1076
1432
  remove all selected media files from list widget
1077
1433
  """
1078
1434
 
1079
- if self.twVideo1.selectedIndexes():
1080
- rows_to_delete = set([x.row() for x in self.twVideo1.selectedIndexes()])
1081
- for row in sorted(rows_to_delete, reverse=True):
1082
- media_path = self.twVideo1.item(row, cfg.MEDIA_FILE_PATH_IDX).text()
1083
- self.twVideo1.removeRow(row)
1084
- if media_path not in [
1085
- self.twVideo1.item(idx, cfg.MEDIA_FILE_PATH_IDX).text() for idx in range(self.twVideo1.rowCount())
1086
- ]:
1087
- try:
1088
- del self.mediaDurations[media_path]
1089
- except NameError:
1090
- pass
1091
- try:
1092
- del self.mediaFPS[media_path]
1093
- except NameError:
1094
- pass
1095
-
1096
- for w in [
1097
- self.cbVisualizeSpectrogram,
1098
- self.cb_visualize_waveform,
1099
- self.cb_observation_time_interval,
1100
- self.cbCloseCurrentBehaviorsBetweenVideo,
1101
- ]:
1102
- w.setEnabled(self.twVideo1.rowCount() > 0)
1435
+ if not self.twVideo1.selectedIndexes():
1436
+ QMessageBox.warning(self, cfg.programName, "No media file selected")
1437
+ return
1103
1438
 
1104
- # disabled for problems
1105
- self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(False)
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
1106
1452
 
1107
- else:
1108
- QMessageBox.warning(self, cfg.programName, "No media file selected")
1453
+ self.update_media_options()