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.
- boris/__init__.py +1 -1
- boris/__main__.py +1 -1
- boris/about.py +36 -39
- boris/add_modifier.py +122 -109
- boris/add_modifier_ui.py +239 -135
- boris/advanced_event_filtering.py +81 -45
- boris/analysis_plugins/__init__.py +0 -0
- boris/analysis_plugins/_latency.py +59 -0
- boris/analysis_plugins/irr_cohen_kappa.py +109 -0
- boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
- boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
- boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
- boris/analysis_plugins/number_of_occurences.py +22 -0
- boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
- boris/analysis_plugins/time_budget.py +61 -0
- boris/behav_coding_map_creator.py +228 -229
- boris/behavior_binary_table.py +33 -50
- boris/behaviors_coding_map.py +17 -18
- boris/boris_cli.py +6 -25
- boris/cmd_arguments.py +12 -1
- boris/coding_pad.py +42 -49
- boris/config.py +161 -77
- boris/config_file.py +63 -83
- boris/connections.py +112 -57
- boris/converters.py +13 -37
- boris/converters_ui.py +187 -110
- boris/cooccurence.py +250 -0
- boris/core.py +2511 -1824
- boris/core_qrc.py +15895 -10185
- boris/core_ui.py +946 -792
- boris/db_functions.py +21 -41
- boris/dev.py +134 -0
- boris/dialog.py +505 -244
- boris/duration_widget.py +15 -20
- boris/edit_event.py +84 -28
- boris/edit_event_ui.py +214 -78
- boris/event_operations.py +517 -415
- boris/events_cursor.py +25 -17
- boris/events_snapshots.py +36 -82
- boris/exclusion_matrix.py +4 -9
- boris/export_events.py +213 -583
- boris/export_observation.py +98 -611
- boris/external_processes.py +156 -97
- boris/geometric_measurement.py +652 -287
- boris/gui_utilities.py +91 -14
- boris/image_overlay.py +9 -9
- boris/import_observations.py +190 -98
- boris/ipc_mpv.py +325 -0
- boris/irr.py +26 -63
- boris/latency.py +34 -25
- boris/measurement_widget.py +14 -18
- boris/media_file.py +52 -84
- boris/menu_options.py +17 -6
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +7 -9
- boris/mpv.py +1 -0
- boris/mpv2.py +732 -705
- boris/observation.py +655 -310
- boris/observation_operations.py +1036 -404
- boris/observation_ui.py +584 -356
- boris/observations_list.py +71 -53
- boris/otx_parser.py +74 -80
- boris/param_panel.py +31 -16
- boris/param_panel_ui.py +254 -138
- boris/player_dock_widget.py +90 -60
- boris/plot_data_module.py +43 -46
- boris/plot_events.py +127 -90
- boris/plot_events_rt.py +17 -31
- boris/plot_spectrogram_rt.py +95 -30
- boris/plot_waveform_rt.py +32 -21
- boris/plugins.py +431 -0
- boris/portion/__init__.py +18 -8
- boris/portion/const.py +35 -18
- boris/portion/dict.py +5 -5
- boris/portion/func.py +2 -2
- boris/portion/interval.py +21 -41
- boris/portion/io.py +41 -32
- boris/preferences.py +306 -83
- boris/preferences_ui.py +685 -228
- boris/project.py +448 -293
- boris/project_functions.py +689 -254
- boris/project_import_export.py +213 -222
- boris/project_ui.py +674 -438
- boris/qrc_boris.py +6 -3
- boris/qrc_boris5.py +6 -3
- boris/select_modifiers.py +74 -48
- boris/select_observations.py +20 -199
- boris/select_subj_behav.py +67 -39
- boris/state_events.py +53 -37
- boris/subjects_pad.py +6 -9
- boris/synthetic_time_budget.py +45 -28
- boris/time_budget_functions.py +171 -171
- boris/time_budget_widget.py +84 -114
- boris/transitions.py +41 -47
- boris/utilities.py +766 -266
- boris/version.py +3 -3
- boris/video_equalizer.py +16 -14
- boris/video_equalizer_ui.py +199 -130
- boris/video_operations.py +125 -28
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.6.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.6.dist-info/RECORD +109 -0
- {boris_behav_obs-8.9.16.dist-info → boris_behav_obs-9.7.6.dist-info}/WHEEL +1 -1
- boris_behav_obs-9.7.6.dist-info/entry_points.txt +2 -0
- boris/README.TXT +0 -22
- boris/add_modifier.ui +0 -323
- boris/boris_ui.py +0 -886
- boris/converters.ui +0 -289
- boris/core.qrc +0 -35
- boris/core.ui +0 -1543
- boris/edit_event.ui +0 -175
- boris/icons/logo_eye.ico +0 -0
- boris/map_creator.py +0 -850
- boris/observation.ui +0 -773
- boris/param_panel.ui +0 -379
- boris/preferences.ui +0 -537
- boris/project.ui +0 -1069
- boris/project_server.py +0 -236
- boris/vlc.py +0 -10343
- boris/vlc_local.py +0 -90
- boris_behav_obs-8.9.16.dist-info/LICENSE.TXT +0 -674
- boris_behav_obs-8.9.16.dist-info/METADATA +0 -129
- boris_behav_obs-8.9.16.dist-info/RECORD +0 -108
- boris_behav_obs-8.9.16.dist-info/entry_points.txt +0 -2
- {boris → boris_behav_obs-9.7.6.dist-info/licenses}/LICENSE.TXT +0 -0
- {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-
|
|
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
|
|
29
|
-
from
|
|
30
|
-
from
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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.
|
|
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.
|
|
233
|
+
self.twVideo1.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
|
|
174
234
|
|
|
175
|
-
self.
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
472
|
-
|
|
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
|
-
|
|
648
|
+
columns_to_plot = "1,2" # columns to plot by default
|
|
477
649
|
|
|
478
|
-
|
|
479
|
-
|
|
650
|
+
# check data file
|
|
651
|
+
file_parameters = util.check_txt_file(file_name)
|
|
480
652
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
661
|
+
header, footer = util.return_file_header_footer(file_name, file_row_number=file_parameters["rows number"], row_number=5)
|
|
490
662
|
|
|
491
|
-
|
|
492
|
-
|
|
663
|
+
if not header:
|
|
664
|
+
QMessageBox.critical(self, cfg.programName, f"Error on file {pl.Path(file_name).name}")
|
|
665
|
+
return
|
|
493
666
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
527
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
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
|
-
|
|
621
|
-
|
|
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()
|
|
629
|
-
|
|
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
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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,
|
|
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
|
-
|
|
669
|
-
w.show()
|
|
670
|
-
QApplication.processEvents()
|
|
886
|
+
w.hide()
|
|
671
887
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
892
|
+
if not flag_wav_produced:
|
|
893
|
+
self.cbVisualizeSpectrogram.setChecked(False)
|
|
894
|
+
self.cb_visualize_waveform.setChecked(False)
|
|
682
895
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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.
|
|
693
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1272
|
+
return (True, "Media duration not available")
|
|
929
1273
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
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
|
-
|
|
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
|
|
1012
|
-
|
|
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",
|
|
1374
|
+
["Continue", "Skip all non media files", cfg.CANCEL],
|
|
1019
1375
|
)
|
|
1020
|
-
if response ==
|
|
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
|
-
|
|
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
|
-
|
|
1081
|
-
|
|
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
|
-
|
|
1105
|
-
|
|
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
|
-
|
|
1108
|
-
QMessageBox.warning(self, cfg.programName, "No media file selected")
|
|
1453
|
+
self.update_media_options()
|