boris-behav-obs 9.7.7__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 +26 -0
- boris/__main__.py +25 -0
- boris/about.py +143 -0
- boris/add_modifier.py +635 -0
- boris/add_modifier_ui.py +303 -0
- boris/advanced_event_filtering.py +455 -0
- 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 +1110 -0
- boris/behavior_binary_table.py +305 -0
- boris/behaviors_coding_map.py +239 -0
- boris/boris_cli.py +340 -0
- boris/cmd_arguments.py +49 -0
- boris/coding_pad.py +280 -0
- boris/config.py +785 -0
- boris/config_file.py +356 -0
- boris/connections.py +409 -0
- boris/converters.py +333 -0
- boris/converters_ui.py +225 -0
- boris/cooccurence.py +250 -0
- boris/core.py +5901 -0
- boris/core_qrc.py +15958 -0
- boris/core_ui.py +1107 -0
- boris/db_functions.py +324 -0
- boris/dev.py +134 -0
- boris/dialog.py +1108 -0
- boris/duration_widget.py +238 -0
- boris/edit_event.py +245 -0
- boris/edit_event_ui.py +233 -0
- boris/event_operations.py +1040 -0
- boris/events_cursor.py +61 -0
- boris/events_snapshots.py +596 -0
- boris/exclusion_matrix.py +141 -0
- boris/export_events.py +1006 -0
- boris/export_observation.py +1203 -0
- boris/external_processes.py +332 -0
- boris/geometric_measurement.py +941 -0
- boris/gui_utilities.py +135 -0
- boris/image_overlay.py +72 -0
- boris/import_observations.py +242 -0
- boris/ipc_mpv.py +325 -0
- boris/irr.py +634 -0
- boris/latency.py +244 -0
- boris/measurement_widget.py +161 -0
- boris/media_file.py +115 -0
- boris/menu_options.py +213 -0
- boris/modifier_coding_map_creator.py +1013 -0
- boris/modifiers_coding_map.py +157 -0
- boris/mpv.py +2016 -0
- boris/mpv2.py +2193 -0
- boris/observation.py +1453 -0
- boris/observation_operations.py +2538 -0
- boris/observation_ui.py +679 -0
- boris/observations_list.py +337 -0
- boris/otx_parser.py +442 -0
- boris/param_panel.py +201 -0
- boris/param_panel_ui.py +305 -0
- boris/player_dock_widget.py +198 -0
- boris/plot_data_module.py +536 -0
- boris/plot_events.py +634 -0
- boris/plot_events_rt.py +237 -0
- boris/plot_spectrogram_rt.py +316 -0
- boris/plot_waveform_rt.py +230 -0
- boris/plugins.py +431 -0
- boris/portion/__init__.py +31 -0
- boris/portion/const.py +95 -0
- boris/portion/dict.py +365 -0
- boris/portion/func.py +52 -0
- boris/portion/interval.py +581 -0
- boris/portion/io.py +181 -0
- boris/preferences.py +510 -0
- boris/preferences_ui.py +770 -0
- boris/project.py +2007 -0
- boris/project_functions.py +2041 -0
- boris/project_import_export.py +1096 -0
- boris/project_ui.py +794 -0
- boris/qrc_boris.py +10389 -0
- boris/qrc_boris5.py +2579 -0
- boris/select_modifiers.py +312 -0
- boris/select_observations.py +210 -0
- boris/select_subj_behav.py +286 -0
- boris/state_events.py +197 -0
- boris/subjects_pad.py +106 -0
- boris/synthetic_time_budget.py +290 -0
- boris/time_budget_functions.py +1136 -0
- boris/time_budget_widget.py +1039 -0
- boris/transitions.py +365 -0
- boris/utilities.py +1810 -0
- boris/version.py +24 -0
- boris/video_equalizer.py +159 -0
- boris/video_equalizer_ui.py +248 -0
- boris/video_operations.py +310 -0
- boris/view_df.py +104 -0
- boris/view_df_ui.py +75 -0
- boris/write_event.py +538 -0
- boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
- boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
- boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
- boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
- boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
- boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
boris/observation.py
ADDED
|
@@ -0,0 +1,1453 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
This file is part of BORIS.
|
|
7
|
+
|
|
8
|
+
BORIS is free software; you can redistribute it and/or modify
|
|
9
|
+
it under the terms of the GNU General Public License as published by
|
|
10
|
+
the Free Software Foundation; either version 3 of the License, or
|
|
11
|
+
any later version.
|
|
12
|
+
|
|
13
|
+
BORIS is distributed in the hope that it will be useful,
|
|
14
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
15
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
16
|
+
GNU General Public License for more details.
|
|
17
|
+
|
|
18
|
+
You should have received a copy of the GNU General Public License
|
|
19
|
+
along with this program; if not see <http://www.gnu.org/licenses/>.
|
|
20
|
+
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
import pandas as pd
|
|
26
|
+
import pathlib as pl
|
|
27
|
+
|
|
28
|
+
from PySide6.QtCore import Qt
|
|
29
|
+
from PySide6.QtGui import QColor
|
|
30
|
+
from PySide6.QtWidgets import (
|
|
31
|
+
QDialog,
|
|
32
|
+
QVBoxLayout,
|
|
33
|
+
QHBoxLayout,
|
|
34
|
+
QLabel,
|
|
35
|
+
QComboBox,
|
|
36
|
+
QPushButton,
|
|
37
|
+
QMessageBox,
|
|
38
|
+
QSpacerItem,
|
|
39
|
+
QSizePolicy,
|
|
40
|
+
QFileDialog,
|
|
41
|
+
QTableWidgetItem,
|
|
42
|
+
QApplication,
|
|
43
|
+
QMenu,
|
|
44
|
+
QListWidgetItem,
|
|
45
|
+
QHeaderView,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
from . import config as cfg
|
|
49
|
+
from . import dialog, plot_data_module, project_functions
|
|
50
|
+
from . import utilities as util
|
|
51
|
+
from . import gui_utilities
|
|
52
|
+
from .observation_ui import Ui_Form
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AssignConverter(QDialog):
|
|
56
|
+
"""
|
|
57
|
+
dialog for assigning converter to selected column
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, columns, converters, col_conv):
|
|
61
|
+
super().__init__()
|
|
62
|
+
|
|
63
|
+
self.setWindowTitle("Converters")
|
|
64
|
+
|
|
65
|
+
self.vbox = QVBoxLayout()
|
|
66
|
+
|
|
67
|
+
self.label = QLabel()
|
|
68
|
+
self.label.setText("Assign converter to column")
|
|
69
|
+
self.vbox.addWidget(self.label)
|
|
70
|
+
|
|
71
|
+
self.cbb = []
|
|
72
|
+
for column_idx in columns.split(","):
|
|
73
|
+
hbox = QHBoxLayout()
|
|
74
|
+
hbox.addWidget(QLabel(f"Column #{column_idx}:"))
|
|
75
|
+
self.cbb.append(QComboBox())
|
|
76
|
+
self.cbb[-1].addItems(["None"] + sorted(converters.keys()))
|
|
77
|
+
|
|
78
|
+
if column_idx in col_conv:
|
|
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)
|
|
83
|
+
else:
|
|
84
|
+
self.cbb[-1].setCurrentIndex(0)
|
|
85
|
+
hbox.addWidget(self.cbb[-1])
|
|
86
|
+
self.vbox.addLayout(hbox)
|
|
87
|
+
|
|
88
|
+
hbox1 = QHBoxLayout()
|
|
89
|
+
self.pbOK = QPushButton("OK")
|
|
90
|
+
self.pbOK.clicked.connect(self.accept)
|
|
91
|
+
self.pbCancel = QPushButton("Cancel")
|
|
92
|
+
self.pbCancel.clicked.connect(self.reject)
|
|
93
|
+
spacerItem = QSpacerItem(40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum)
|
|
94
|
+
hbox1.addItem(spacerItem)
|
|
95
|
+
hbox1.addWidget(self.pbCancel)
|
|
96
|
+
hbox1.addWidget(self.pbOK)
|
|
97
|
+
self.vbox.addLayout(hbox1)
|
|
98
|
+
|
|
99
|
+
self.setLayout(self.vbox)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Observation(QDialog, Ui_Form):
|
|
103
|
+
def __init__(self, tmp_dir: str, project_path: str = "", converters: dict = {}, time_format: str = cfg.S, parent=None):
|
|
104
|
+
"""
|
|
105
|
+
Args:
|
|
106
|
+
tmp_dir (str): path of temporary directory
|
|
107
|
+
project_path (str): path of project
|
|
108
|
+
converters (dict): converters dictionary
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
super().__init__()
|
|
112
|
+
|
|
113
|
+
self.tmp_dir = tmp_dir
|
|
114
|
+
self.project_path = project_path
|
|
115
|
+
self.converters = converters
|
|
116
|
+
self.time_format = time_format
|
|
117
|
+
self.observation_time_interval: tuple = [0, 0]
|
|
118
|
+
self.mem_dir = ""
|
|
119
|
+
self.test = None
|
|
120
|
+
|
|
121
|
+
self.setupUi(self)
|
|
122
|
+
|
|
123
|
+
# insert duration widget for time offset
|
|
124
|
+
# self.obs_time_offset = duration_widget.Duration_widget(0)
|
|
125
|
+
self.obs_time_offset = dialog.get_time_widget(0)
|
|
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)"""
|
|
133
|
+
|
|
134
|
+
# observation type
|
|
135
|
+
self.rb_media_files.toggled.connect(self.obs_type_changed)
|
|
136
|
+
self.rb_live.toggled.connect(self.obs_type_changed)
|
|
137
|
+
self.rb_images.toggled.connect(self.obs_type_changed)
|
|
138
|
+
|
|
139
|
+
# button menu for media
|
|
140
|
+
|
|
141
|
+
add_media_menu_items = [
|
|
142
|
+
"media abs path|with absolute path",
|
|
143
|
+
"media rel path|with relative path",
|
|
144
|
+
{
|
|
145
|
+
"from directory": [
|
|
146
|
+
"dir abs path|with absolute path ",
|
|
147
|
+
"dir rel path|wih relative path ",
|
|
148
|
+
]
|
|
149
|
+
},
|
|
150
|
+
]
|
|
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)
|
|
175
|
+
|
|
176
|
+
self.pbRemoveVideo.clicked.connect(self.remove_media)
|
|
177
|
+
|
|
178
|
+
# button menu for data file
|
|
179
|
+
data_menu_items = [
|
|
180
|
+
"data abs path|with absolute path",
|
|
181
|
+
"data rel path|with relative path",
|
|
182
|
+
]
|
|
183
|
+
|
|
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)
|
|
213
|
+
|
|
214
|
+
self.pb_remove_data_file.clicked.connect(self.remove_data_file)
|
|
215
|
+
self.pb_view_data_head.clicked.connect(self.view_data_file_head_tail)
|
|
216
|
+
self.pb_plot_data.clicked.connect(self.plot_data_file)
|
|
217
|
+
|
|
218
|
+
self.pb_use_media_file_name_as_obsid.clicked.connect(self.use_media_file_name_as_obsid)
|
|
219
|
+
self.pb_use_img_dir_as_obsid.clicked.connect(self.use_img_dir_as_obsid)
|
|
220
|
+
|
|
221
|
+
self.cbVisualizeSpectrogram.clicked.connect(self.extract_wav)
|
|
222
|
+
self.cb_visualize_waveform.clicked.connect(self.extract_wav)
|
|
223
|
+
|
|
224
|
+
self.cb_observation_time_interval.clicked.connect(self.limit_time_interval)
|
|
225
|
+
|
|
226
|
+
self.pbSave.clicked.connect(self.pbSave_clicked)
|
|
227
|
+
self.pbLaunch.clicked.connect(self.pbLaunch_clicked)
|
|
228
|
+
self.pbCancel.clicked.connect(self.pbCancel_clicked)
|
|
229
|
+
|
|
230
|
+
self.tw_data_files.cellDoubleClicked[int, int].connect(self.tw_data_files_cellDoubleClicked)
|
|
231
|
+
self.tw_data_files.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
|
|
232
|
+
|
|
233
|
+
self.twVideo1.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
|
|
234
|
+
|
|
235
|
+
self.mediaDurations, self.mediaFPS, self.mediaHasVideo, self.mediaHasAudio, self.media_creation_time = {}, {}, {}, {}, {}
|
|
236
|
+
|
|
237
|
+
for w in (
|
|
238
|
+
self.cbVisualizeSpectrogram,
|
|
239
|
+
self.cb_visualize_waveform,
|
|
240
|
+
self.cb_observation_time_interval,
|
|
241
|
+
self.cb_media_creation_date_as_offset,
|
|
242
|
+
self.cbCloseCurrentBehaviorsBetweenVideo,
|
|
243
|
+
):
|
|
244
|
+
w.setEnabled(False)
|
|
245
|
+
|
|
246
|
+
self.cb_observation_time_interval.setEnabled(True)
|
|
247
|
+
|
|
248
|
+
self.cb_start_from_current_time.stateChanged.connect(self.cb_start_from_current_time_changed)
|
|
249
|
+
|
|
250
|
+
# images
|
|
251
|
+
# self.pb_add_directory.clicked.connect(self.add_images_directory)
|
|
252
|
+
self.pb_remove_directory.clicked.connect(self.remove_images_directory)
|
|
253
|
+
|
|
254
|
+
self.tabWidget.setCurrentIndex(0)
|
|
255
|
+
|
|
256
|
+
# geometry
|
|
257
|
+
gui_utilities.restore_geometry(self, "new observation", (800, 650))
|
|
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
|
+
|
|
313
|
+
def use_media_file_name_as_obsid(self) -> None:
|
|
314
|
+
"""
|
|
315
|
+
set observation id with the media file name value (without path)
|
|
316
|
+
"""
|
|
317
|
+
if not self.twVideo1.rowCount():
|
|
318
|
+
QMessageBox.critical(self, cfg.programName, "A media file must be loaded in player #1")
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
first_media_file: str = ""
|
|
322
|
+
for row in range(self.twVideo1.rowCount()):
|
|
323
|
+
if int(self.twVideo1.cellWidget(row, 0).currentText()) == 1:
|
|
324
|
+
first_media_file = self.twVideo1.item(row, 2).text()
|
|
325
|
+
break
|
|
326
|
+
# check if player #1 is used
|
|
327
|
+
if not first_media_file:
|
|
328
|
+
QMessageBox.critical(self, cfg.programName, "A media file must be loaded in player #1")
|
|
329
|
+
return
|
|
330
|
+
|
|
331
|
+
self.leObservationId.setText(pl.Path(first_media_file).name)
|
|
332
|
+
|
|
333
|
+
def use_img_dir_as_obsid(self) -> None:
|
|
334
|
+
"""
|
|
335
|
+
set observation id with the images directory (without path)
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
if not self.lw_images_directory.count():
|
|
339
|
+
QMessageBox.critical(self, cfg.programName, "You have to select at least one images directory")
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
self.leObservationId.setText(pl.Path(self.lw_images_directory.item(0).text()).name)
|
|
343
|
+
|
|
344
|
+
def obs_type_changed(self) -> None:
|
|
345
|
+
"""
|
|
346
|
+
change stacked widget page in base at the observation type
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
for idx, rb in enumerate((self.rb_media_files, self.rb_live, self.rb_images)):
|
|
350
|
+
if rb.isChecked():
|
|
351
|
+
self.sw_observation_type.setCurrentIndex(idx + 1)
|
|
352
|
+
|
|
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):
|
|
357
|
+
"""
|
|
358
|
+
add path to images directory
|
|
359
|
+
"""
|
|
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
|
+
|
|
389
|
+
result = util.dir_images_number(dir_path)
|
|
390
|
+
if not result.get("number of images", 0):
|
|
391
|
+
response = dialog.MessageDialog(
|
|
392
|
+
cfg.programName,
|
|
393
|
+
f"The directory does not contain images ({','.join(cfg.IMAGE_EXTENSIONS)})",
|
|
394
|
+
["Cancel", "Add directory"],
|
|
395
|
+
)
|
|
396
|
+
if response == "Cancel":
|
|
397
|
+
return
|
|
398
|
+
|
|
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)}")
|
|
419
|
+
|
|
420
|
+
def remove_images_directory(self):
|
|
421
|
+
"""
|
|
422
|
+
remove dir path from the list
|
|
423
|
+
"""
|
|
424
|
+
self.lw_images_directory.takeItem(self.lw_images_directory.currentRow())
|
|
425
|
+
|
|
426
|
+
def add_button_menu(self, data, menu_obj):
|
|
427
|
+
"""
|
|
428
|
+
add menu option from dictionary
|
|
429
|
+
"""
|
|
430
|
+
if isinstance(data, dict):
|
|
431
|
+
for k, v in data.items():
|
|
432
|
+
sub_menu = QMenu(k, menu_obj)
|
|
433
|
+
menu_obj.addMenu(sub_menu)
|
|
434
|
+
self.add_button_menu(v, sub_menu)
|
|
435
|
+
elif isinstance(data, list):
|
|
436
|
+
for element in data:
|
|
437
|
+
self.add_button_menu(element, menu_obj)
|
|
438
|
+
else:
|
|
439
|
+
action = menu_obj.addAction(data.split("|")[1])
|
|
440
|
+
# tips are used to discriminate the menu option
|
|
441
|
+
action.setStatusTip(data.split("|")[0])
|
|
442
|
+
action.setIconVisibleInMenu(False)
|
|
443
|
+
|
|
444
|
+
def cb_start_from_current_time_changed(self):
|
|
445
|
+
"""
|
|
446
|
+
enable/disable radiobox for type of time selection
|
|
447
|
+
"""
|
|
448
|
+
self.rb_day_time.setEnabled(self.cb_start_from_current_time.isChecked())
|
|
449
|
+
self.rb_epoch_time.setEnabled(self.cb_start_from_current_time.isChecked())
|
|
450
|
+
|
|
451
|
+
def limit_time_interval(self):
|
|
452
|
+
"""
|
|
453
|
+
ask user a time interval for limiting the media observation
|
|
454
|
+
"""
|
|
455
|
+
|
|
456
|
+
if self.cb_observation_time_interval.isChecked():
|
|
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)
|
|
462
|
+
time_interval_dialog.time_widget.set_time(0)
|
|
463
|
+
time_interval_dialog.setWindowTitle("Start observation at")
|
|
464
|
+
time_interval_dialog.label.setText("<b>Start</b> observation at")
|
|
465
|
+
start_time, stop_time = 0, 0
|
|
466
|
+
if time_interval_dialog.exec_():
|
|
467
|
+
start_time = time_interval_dialog.time_widget.get_time()
|
|
468
|
+
else:
|
|
469
|
+
self.cb_observation_time_interval.setChecked(False)
|
|
470
|
+
return
|
|
471
|
+
time_interval_dialog.time_widget.set_time(0)
|
|
472
|
+
time_interval_dialog.setWindowTitle("Stop observation at")
|
|
473
|
+
time_interval_dialog.label.setText("<b>Stop</b> observation at")
|
|
474
|
+
if time_interval_dialog.exec_():
|
|
475
|
+
stop_time = time_interval_dialog.time_widget.get_time()
|
|
476
|
+
else:
|
|
477
|
+
self.cb_observation_time_interval.setChecked(False)
|
|
478
|
+
return
|
|
479
|
+
|
|
480
|
+
if start_time or stop_time:
|
|
481
|
+
if stop_time <= start_time:
|
|
482
|
+
QMessageBox.critical(self, cfg.programName, "The stop time comes before the start time")
|
|
483
|
+
self.cb_observation_time_interval.setChecked(False)
|
|
484
|
+
return
|
|
485
|
+
self.observation_time_interval = [start_time, stop_time]
|
|
486
|
+
self.cb_observation_time_interval.setText(
|
|
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
|
+
)
|
|
491
|
+
)
|
|
492
|
+
else:
|
|
493
|
+
self.observation_time_interval = [0, 0]
|
|
494
|
+
self.cb_observation_time_interval.setText("Limit observation to a time interval")
|
|
495
|
+
|
|
496
|
+
def tw_data_files_cellDoubleClicked(self, row, column):
|
|
497
|
+
"""
|
|
498
|
+
double click on "Converters column"
|
|
499
|
+
"""
|
|
500
|
+
if column == cfg.PLOT_DATA_CONVERTERS_IDX:
|
|
501
|
+
if self.tw_data_files.item(row, cfg.PLOT_DATA_COLUMNS_IDX).text():
|
|
502
|
+
w = AssignConverter(
|
|
503
|
+
self.tw_data_files.item(row, cfg.PLOT_DATA_COLUMNS_IDX).text(),
|
|
504
|
+
self.converters,
|
|
505
|
+
eval(self.tw_data_files.item(row, cfg.PLOT_DATA_CONVERTERS_IDX).text())
|
|
506
|
+
if self.tw_data_files.item(row, cfg.PLOT_DATA_CONVERTERS_IDX).text()
|
|
507
|
+
else "",
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
if w.exec_():
|
|
511
|
+
d = {}
|
|
512
|
+
for col_idx, cb in zip(self.tw_data_files.item(row, cfg.PLOT_DATA_COLUMNS_IDX).text().split(","), w.cbb):
|
|
513
|
+
if cb.currentText() != "None":
|
|
514
|
+
d[col_idx] = cb.currentText()
|
|
515
|
+
self.tw_data_files.item(row, cfg.PLOT_DATA_CONVERTERS_IDX).setText(str(d))
|
|
516
|
+
else:
|
|
517
|
+
QMessageBox.critical(self, cfg.programName, "Select the columns to plot (time,value)")
|
|
518
|
+
|
|
519
|
+
def plot_data_file(self):
|
|
520
|
+
"""
|
|
521
|
+
show plot
|
|
522
|
+
check if data can be plotted
|
|
523
|
+
"""
|
|
524
|
+
|
|
525
|
+
if self.pb_plot_data.text() != "Show plot":
|
|
526
|
+
self.test.close_plot()
|
|
527
|
+
self.text = None
|
|
528
|
+
# update button text
|
|
529
|
+
self.pb_plot_data.setText("Show plot")
|
|
530
|
+
return
|
|
531
|
+
|
|
532
|
+
if self.tw_data_files.selectedIndexes() or self.tw_data_files.rowCount() == 1:
|
|
533
|
+
if self.tw_data_files.rowCount() == 1:
|
|
534
|
+
row_idx = 0
|
|
535
|
+
else:
|
|
536
|
+
row_idx = self.tw_data_files.selectedIndexes()[0].row()
|
|
537
|
+
|
|
538
|
+
filename = self.tw_data_files.item(row_idx, cfg.PLOT_DATA_FILEPATH_IDX).text()
|
|
539
|
+
columns_to_plot = self.tw_data_files.item(row_idx, cfg.PLOT_DATA_COLUMNS_IDX).text()
|
|
540
|
+
plot_title = self.tw_data_files.item(row_idx, cfg.PLOT_DATA_PLOTTITLE_IDX).text()
|
|
541
|
+
|
|
542
|
+
# load converters in dictionary
|
|
543
|
+
if self.tw_data_files.item(row_idx, cfg.PLOT_DATA_CONVERTERS_IDX).text():
|
|
544
|
+
column_converter = eval(self.tw_data_files.item(row_idx, cfg.PLOT_DATA_CONVERTERS_IDX).text())
|
|
545
|
+
else:
|
|
546
|
+
column_converter = {}
|
|
547
|
+
|
|
548
|
+
variable_name = self.tw_data_files.item(row_idx, cfg.PLOT_DATA_VARIABLENAME_IDX).text()
|
|
549
|
+
time_interval = int(self.tw_data_files.item(row_idx, cfg.PLOT_DATA_TIMEINTERVAL_IDX).text())
|
|
550
|
+
time_offset = int(self.tw_data_files.item(row_idx, cfg.PLOT_DATA_TIMEOFFSET_IDX).text())
|
|
551
|
+
|
|
552
|
+
substract_first_value = self.tw_data_files.cellWidget(row_idx, cfg.PLOT_DATA_SUBSTRACT1STVALUE_IDX).currentText()
|
|
553
|
+
|
|
554
|
+
plot_color = self.tw_data_files.cellWidget(row_idx, cfg.PLOT_DATA_PLOTCOLOR_IDX).currentText()
|
|
555
|
+
|
|
556
|
+
data_file_path = project_functions.full_path(filename, self.project_path)
|
|
557
|
+
|
|
558
|
+
if not data_file_path:
|
|
559
|
+
QMessageBox.critical(
|
|
560
|
+
self,
|
|
561
|
+
cfg.programName,
|
|
562
|
+
(
|
|
563
|
+
f"Data file not found:\n{filename}\n"
|
|
564
|
+
"If the file path is not stored the data file "
|
|
565
|
+
"must be in the same directory than your project"
|
|
566
|
+
),
|
|
567
|
+
)
|
|
568
|
+
return
|
|
569
|
+
|
|
570
|
+
self.test = plot_data_module.Plot_data(
|
|
571
|
+
data_file_path,
|
|
572
|
+
time_interval, # time interval
|
|
573
|
+
time_offset, # time offset
|
|
574
|
+
plot_color, # plot style
|
|
575
|
+
plot_title, # plot title
|
|
576
|
+
variable_name,
|
|
577
|
+
columns_to_plot,
|
|
578
|
+
substract_first_value,
|
|
579
|
+
self.converters,
|
|
580
|
+
column_converter,
|
|
581
|
+
log_level=logging.getLogger().getEffectiveLevel(),
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
if self.test.error_msg:
|
|
585
|
+
QMessageBox.critical(self, cfg.programName, f"Impossible to plot data:\n{self.test.error_msg}")
|
|
586
|
+
self.test = None
|
|
587
|
+
return
|
|
588
|
+
|
|
589
|
+
# self.test.setWindowFlags(self.test.windowFlags() | Qt.WindowStaysOnTopHint)
|
|
590
|
+
self.test.show()
|
|
591
|
+
self.test.update_plot(0)
|
|
592
|
+
# update button text
|
|
593
|
+
self.pb_plot_data.setText("Close plot")
|
|
594
|
+
else:
|
|
595
|
+
QMessageBox.warning(self, cfg.programName, "Select a data file")
|
|
596
|
+
|
|
597
|
+
def not_editable_column_color(self):
|
|
598
|
+
"""
|
|
599
|
+
return a color for the not editable column
|
|
600
|
+
"""
|
|
601
|
+
window_color = QApplication.instance().palette().window().color()
|
|
602
|
+
return QColor(window_color.red() - 5, window_color.green() - 5, window_color.blue() - 5)
|
|
603
|
+
|
|
604
|
+
def add_data_file(self, mode: str):
|
|
605
|
+
"""
|
|
606
|
+
user select a data file to be plotted synchronously with media file
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
mode (str): statusTip() data abs path / data rel path
|
|
610
|
+
"""
|
|
611
|
+
|
|
612
|
+
if mode.split("|")[0] not in (
|
|
613
|
+
"data abs path",
|
|
614
|
+
"data rel path",
|
|
615
|
+
):
|
|
616
|
+
QMessageBox.critical(
|
|
617
|
+
self,
|
|
618
|
+
cfg.programName,
|
|
619
|
+
(f"Wrong mode to add a data file {mode}"),
|
|
620
|
+
)
|
|
621
|
+
return
|
|
622
|
+
|
|
623
|
+
# check if project saved
|
|
624
|
+
if (" w/o" in mode or " rel " in mode) and (not self.project_file_name):
|
|
625
|
+
QMessageBox.critical(
|
|
626
|
+
self,
|
|
627
|
+
cfg.programName,
|
|
628
|
+
("It is not possible to add a data file with a relative path if the project is not already saved"),
|
|
629
|
+
)
|
|
630
|
+
return
|
|
631
|
+
|
|
632
|
+
# limit to 2 files
|
|
633
|
+
if self.tw_data_files.rowCount() >= 2:
|
|
634
|
+
QMessageBox.warning(
|
|
635
|
+
self,
|
|
636
|
+
cfg.programName,
|
|
637
|
+
("It is not yet possible to plot more than 2 external data sourcesThis limitation will be removed in future"),
|
|
638
|
+
)
|
|
639
|
+
return
|
|
640
|
+
|
|
641
|
+
fd = QFileDialog()
|
|
642
|
+
fd.setDirectory(os.path.expanduser("~") if (" abs " in mode) else str(pl.Path(self.project_path).parent))
|
|
643
|
+
|
|
644
|
+
file_name, _ = fd.getOpenFileName(self, "Add data file", "", "All files (*)")
|
|
645
|
+
if not file_name:
|
|
646
|
+
return
|
|
647
|
+
|
|
648
|
+
columns_to_plot = "1,2" # columns to plot by default
|
|
649
|
+
|
|
650
|
+
# check data file
|
|
651
|
+
file_parameters = util.check_txt_file(file_name)
|
|
652
|
+
|
|
653
|
+
if "error" in file_parameters:
|
|
654
|
+
QMessageBox.critical(self, cfg.programName, f"Error on file {file_name}: {file_parameters['error']}")
|
|
655
|
+
return
|
|
656
|
+
|
|
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
|
|
660
|
+
|
|
661
|
+
header, footer = util.return_file_header_footer(file_name, file_row_number=file_parameters["rows number"], row_number=5)
|
|
662
|
+
|
|
663
|
+
if not header:
|
|
664
|
+
QMessageBox.critical(self, cfg.programName, f"Error on file {pl.Path(file_name).name}")
|
|
665
|
+
return
|
|
666
|
+
|
|
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")
|
|
670
|
+
|
|
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
|
|
677
|
+
w.tw.setRowCount(len(header))
|
|
678
|
+
|
|
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
|
|
707
|
+
break
|
|
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
|
|
714
|
+
else:
|
|
715
|
+
return
|
|
716
|
+
|
|
717
|
+
else:
|
|
718
|
+
return
|
|
719
|
+
|
|
720
|
+
self.tw_data_files.setRowCount(self.tw_data_files.rowCount() + 1)
|
|
721
|
+
|
|
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
|
|
732
|
+
|
|
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:
|
|
766
|
+
"""
|
|
767
|
+
view first and last rows of data file
|
|
768
|
+
"""
|
|
769
|
+
|
|
770
|
+
if not self.tw_data_files.selectedIndexes() and self.tw_data_files.rowCount() != 1:
|
|
771
|
+
QMessageBox.warning(self, cfg.programName, "Select a data file")
|
|
772
|
+
|
|
773
|
+
if self.tw_data_files.rowCount() == 1:
|
|
774
|
+
data_file_path = project_functions.full_path(self.tw_data_files.item(0, 0).text(), self.project_path)
|
|
775
|
+
columns_to_plot = self.tw_data_files.item(0, 1).text()
|
|
776
|
+
else: # selected file
|
|
777
|
+
data_file_path = project_functions.full_path(
|
|
778
|
+
self.tw_data_files.item(self.tw_data_files.selectedIndexes()[0].row(), 0).text(), self.project_path
|
|
779
|
+
)
|
|
780
|
+
columns_to_plot = self.tw_data_files.item(self.tw_data_files.selectedIndexes()[0].row(), 1).text()
|
|
781
|
+
|
|
782
|
+
file_parameters = util.check_txt_file(data_file_path)
|
|
783
|
+
|
|
784
|
+
if "error" in file_parameters:
|
|
785
|
+
QMessageBox.critical(self, cfg.programName, f"Error on file {data_file_path}: {file_parameters['error']}")
|
|
786
|
+
return
|
|
787
|
+
header, footer = util.return_file_header_footer(data_file_path, file_row_number=file_parameters["rows number"], row_number=5)
|
|
788
|
+
|
|
789
|
+
if not header:
|
|
790
|
+
QMessageBox.critical(self, cfg.programName, f"Error on file {pl.Path(data_file_path).name}")
|
|
791
|
+
return
|
|
792
|
+
|
|
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
|
|
808
|
+
w.tw.setRowCount(len(header))
|
|
809
|
+
|
|
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)
|
|
815
|
+
|
|
816
|
+
# stats
|
|
817
|
+
try:
|
|
818
|
+
df = pd.read_csv(
|
|
819
|
+
data_file_path,
|
|
820
|
+
sep=file_parameters["separator"],
|
|
821
|
+
header=None if not file_parameters["has header"] else [0],
|
|
822
|
+
)
|
|
823
|
+
# set columns names to based 1 index
|
|
824
|
+
if not file_parameters["has header"]:
|
|
825
|
+
df.columns = range(1, len(df.columns) + 1)
|
|
826
|
+
|
|
827
|
+
stats_out = str(df.describe())
|
|
828
|
+
except Exception:
|
|
829
|
+
stats_out = "Not available"
|
|
830
|
+
w.stats.setPlainText(stats_out)
|
|
831
|
+
|
|
832
|
+
w.exec_()
|
|
833
|
+
|
|
834
|
+
def extract_wav(self):
|
|
835
|
+
"""
|
|
836
|
+
extract wav of all media files loaded in player #1
|
|
837
|
+
"""
|
|
838
|
+
|
|
839
|
+
if not self.cbVisualizeSpectrogram.isChecked() and not self.cb_visualize_waveform.isChecked():
|
|
840
|
+
return
|
|
841
|
+
|
|
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
|
|
848
|
+
|
|
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
|
|
854
|
+
|
|
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...")
|
|
861
|
+
|
|
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) == "":
|
|
878
|
+
QMessageBox.critical(
|
|
879
|
+
self,
|
|
880
|
+
cfg.programName,
|
|
881
|
+
f"Error during extracting WAV of the media file {media_file_path}",
|
|
882
|
+
)
|
|
883
|
+
flag_wav_produced = False
|
|
884
|
+
break
|
|
885
|
+
|
|
886
|
+
w.hide()
|
|
887
|
+
|
|
888
|
+
flag_wav_produced = True
|
|
889
|
+
else:
|
|
890
|
+
QMessageBox.warning(self, cfg.programName, f"<b>{media_file_path}</b> file not found")
|
|
891
|
+
|
|
892
|
+
if not flag_wav_produced:
|
|
893
|
+
self.cbVisualizeSpectrogram.setChecked(False)
|
|
894
|
+
self.cb_visualize_waveform.setChecked(False)
|
|
895
|
+
|
|
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
|
|
900
|
+
|
|
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
|
|
964
|
+
else:
|
|
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
|
|
972
|
+
|
|
973
|
+
def closeEvent(self, event):
|
|
974
|
+
"""
|
|
975
|
+
close observation windows
|
|
976
|
+
"""
|
|
977
|
+
if self.test is not None:
|
|
978
|
+
self.test.close_plot()
|
|
979
|
+
self.text = None
|
|
980
|
+
|
|
981
|
+
def pbCancel_clicked(self):
|
|
982
|
+
"""
|
|
983
|
+
observation creation cancelled
|
|
984
|
+
"""
|
|
985
|
+
if self.test is not None:
|
|
986
|
+
self.test.close_plot()
|
|
987
|
+
self.text = None
|
|
988
|
+
self.reject()
|
|
989
|
+
|
|
990
|
+
def check_parameters(self) -> bool:
|
|
991
|
+
"""
|
|
992
|
+
check observation parameters
|
|
993
|
+
|
|
994
|
+
Returns:
|
|
995
|
+
bool: True if everything is OK else False
|
|
996
|
+
|
|
997
|
+
"""
|
|
998
|
+
|
|
999
|
+
def is_numeric(s):
|
|
1000
|
+
"""
|
|
1001
|
+
check if s is numeric (float)
|
|
1002
|
+
|
|
1003
|
+
Args:
|
|
1004
|
+
s (str/int/float): value to test
|
|
1005
|
+
|
|
1006
|
+
Returns:
|
|
1007
|
+
boolean: True if numeric else False
|
|
1008
|
+
"""
|
|
1009
|
+
try:
|
|
1010
|
+
float(s)
|
|
1011
|
+
return True
|
|
1012
|
+
except ValueError:
|
|
1013
|
+
return False
|
|
1014
|
+
|
|
1015
|
+
# check if observation id not empty
|
|
1016
|
+
if not self.leObservationId.text():
|
|
1017
|
+
QMessageBox.critical(
|
|
1018
|
+
self,
|
|
1019
|
+
cfg.programName,
|
|
1020
|
+
"The <b>observation id</b> is mandatory and must be unique.",
|
|
1021
|
+
)
|
|
1022
|
+
return False
|
|
1023
|
+
|
|
1024
|
+
# check if observation_type
|
|
1025
|
+
if not any((self.rb_media_files.isChecked(), self.rb_live.isChecked(), self.rb_images.isChecked())):
|
|
1026
|
+
QMessageBox.critical(
|
|
1027
|
+
self,
|
|
1028
|
+
cfg.programName,
|
|
1029
|
+
"Choose an observation type.",
|
|
1030
|
+
)
|
|
1031
|
+
return False
|
|
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
|
+
|
|
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
|
+
|
|
1051
|
+
# check player number
|
|
1052
|
+
players_list: list = []
|
|
1053
|
+
players: dict = {} # for storing duration
|
|
1054
|
+
for row in range(self.twVideo1.rowCount()):
|
|
1055
|
+
player_idx = int(self.twVideo1.cellWidget(row, 0).currentText())
|
|
1056
|
+
players_list.append(player_idx)
|
|
1057
|
+
if player_idx not in players:
|
|
1058
|
+
players[player_idx] = []
|
|
1059
|
+
players[player_idx].append(util.time2seconds(self.twVideo1.item(row, 3).text()))
|
|
1060
|
+
|
|
1061
|
+
# check if player #1 is used
|
|
1062
|
+
if not players_list or min(players_list) > 1:
|
|
1063
|
+
QMessageBox.critical(
|
|
1064
|
+
self,
|
|
1065
|
+
cfg.programName,
|
|
1066
|
+
"A media file must be loaded in player #1",
|
|
1067
|
+
)
|
|
1068
|
+
return False
|
|
1069
|
+
|
|
1070
|
+
# check if players are used in crescent order
|
|
1071
|
+
if set(list(range(min(players_list), max(players_list) + 1))) != set(players_list):
|
|
1072
|
+
QMessageBox.critical(
|
|
1073
|
+
self,
|
|
1074
|
+
cfg.programName,
|
|
1075
|
+
"Some player are not used. Please reorganize your media files",
|
|
1076
|
+
)
|
|
1077
|
+
return False
|
|
1078
|
+
|
|
1079
|
+
# check if more media in player #1 and media in other players
|
|
1080
|
+
"""
|
|
1081
|
+
if len(players[1]) > 1 and set(players.keys()) != {1}:
|
|
1082
|
+
QMessageBox.critical(
|
|
1083
|
+
self,
|
|
1084
|
+
cfg.programName,
|
|
1085
|
+
(
|
|
1086
|
+
"It is not possible to play another media synchronously "
|
|
1087
|
+
"when many media are queued in the first media player"
|
|
1088
|
+
),
|
|
1089
|
+
)
|
|
1090
|
+
return False
|
|
1091
|
+
"""
|
|
1092
|
+
|
|
1093
|
+
# check if more media enqueued on many players
|
|
1094
|
+
if len(set(players.keys())) > 1: # many players used
|
|
1095
|
+
if max([len(players[x]) for x in players]) > 1:
|
|
1096
|
+
QMessageBox.critical(
|
|
1097
|
+
self,
|
|
1098
|
+
cfg.programName,
|
|
1099
|
+
(
|
|
1100
|
+
"It is not possible to enqueue media files "
|
|
1101
|
+
"on more than one player.<br>"
|
|
1102
|
+
"You can use the <b>Merge media files</b> tool (see Tools > Media file > Merge media files"
|
|
1103
|
+
),
|
|
1104
|
+
)
|
|
1105
|
+
return False
|
|
1106
|
+
|
|
1107
|
+
# check that the longuest media is in player #1
|
|
1108
|
+
durations: list = []
|
|
1109
|
+
for i in sorted(list(players.keys())):
|
|
1110
|
+
durations.append(sum(players[i]))
|
|
1111
|
+
if [x for x in durations[1:] if x > durations[0]]:
|
|
1112
|
+
QMessageBox.critical(self, cfg.programName, "The longuest media file(s) must be loaded in player #1")
|
|
1113
|
+
return False
|
|
1114
|
+
|
|
1115
|
+
# check offset for media files
|
|
1116
|
+
for row in range(self.twVideo1.rowCount()):
|
|
1117
|
+
if not is_numeric(self.twVideo1.item(row, 1).text()):
|
|
1118
|
+
QMessageBox.critical(
|
|
1119
|
+
self,
|
|
1120
|
+
cfg.programName,
|
|
1121
|
+
(
|
|
1122
|
+
"The offset value "
|
|
1123
|
+
f"<b>{self.twVideo1.item(row, 1).text()}</b>"
|
|
1124
|
+
" is not recognized as a numeric value.<br>"
|
|
1125
|
+
"Use decimal number of seconds (e.g. -58.5 or 32)"
|
|
1126
|
+
),
|
|
1127
|
+
)
|
|
1128
|
+
return False
|
|
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
|
+
|
|
1144
|
+
# check offset for external data files
|
|
1145
|
+
for row in range(self.tw_data_files.rowCount()):
|
|
1146
|
+
if not is_numeric(self.tw_data_files.item(row, cfg.PLOT_DATA_TIMEOFFSET_IDX).text()):
|
|
1147
|
+
QMessageBox.critical(
|
|
1148
|
+
self,
|
|
1149
|
+
cfg.programName,
|
|
1150
|
+
(
|
|
1151
|
+
"The external data file start value "
|
|
1152
|
+
f"<b>{self.tw_data_files.item(row, cfg.PLOT_DATA_TIMEOFFSET_IDX).text()}</b>"
|
|
1153
|
+
" is not recognized as a numeric value.<br>"
|
|
1154
|
+
"Use decimal number of seconds (e.g. -58.5 or 32)"
|
|
1155
|
+
),
|
|
1156
|
+
)
|
|
1157
|
+
return False
|
|
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
|
+
|
|
1171
|
+
if self.rb_images.isChecked(): # observation based on images directory
|
|
1172
|
+
if not self.lw_images_directory.count():
|
|
1173
|
+
QMessageBox.critical(self, cfg.programName, "You have to select at least one images directory")
|
|
1174
|
+
return False
|
|
1175
|
+
|
|
1176
|
+
# check if indep variables are correct type
|
|
1177
|
+
for row in range(self.twIndepVariables.rowCount()):
|
|
1178
|
+
if self.twIndepVariables.item(row, 1).text() == cfg.NUMERIC:
|
|
1179
|
+
if self.twIndepVariables.item(row, 2).text() and not is_numeric(self.twIndepVariables.item(row, 2).text()):
|
|
1180
|
+
QMessageBox.critical(
|
|
1181
|
+
self,
|
|
1182
|
+
cfg.programName,
|
|
1183
|
+
f"The <b>{self.twIndepVariables.item(row, 0).text()}</b> variable must be numeric!",
|
|
1184
|
+
)
|
|
1185
|
+
return False
|
|
1186
|
+
|
|
1187
|
+
# check if new obs and observation id already present or if edit obs and id changed
|
|
1188
|
+
if (self.mode == "new") or (self.mode == "edit" and self.leObservationId.text() != self.mem_obs_id):
|
|
1189
|
+
if self.leObservationId.text() in self.pj[cfg.OBSERVATIONS]:
|
|
1190
|
+
QMessageBox.critical(
|
|
1191
|
+
self,
|
|
1192
|
+
cfg.programName,
|
|
1193
|
+
(
|
|
1194
|
+
f"The observation id <b>{self.leObservationId.text()}</b> is already used!<br>"
|
|
1195
|
+
f"{self.pj[cfg.OBSERVATIONS][self.leObservationId.text()]['description']}<br>"
|
|
1196
|
+
f"{self.pj[cfg.OBSERVATIONS][self.leObservationId.text()]['date']}"
|
|
1197
|
+
),
|
|
1198
|
+
)
|
|
1199
|
+
return False
|
|
1200
|
+
|
|
1201
|
+
# check if numeric indep variable values are numeric
|
|
1202
|
+
for row in range(self.twIndepVariables.rowCount()):
|
|
1203
|
+
if self.twIndepVariables.item(row, 1).text() == cfg.NUMERIC:
|
|
1204
|
+
if self.twIndepVariables.item(row, 2).text() and not is_numeric(self.twIndepVariables.item(row, 2).text()):
|
|
1205
|
+
QMessageBox.critical(
|
|
1206
|
+
self,
|
|
1207
|
+
cfg.programName,
|
|
1208
|
+
f"The <b>{self.twIndepVariables.item(row, 0).text()}</b> variable must be numeric!",
|
|
1209
|
+
)
|
|
1210
|
+
return False
|
|
1211
|
+
|
|
1212
|
+
return True
|
|
1213
|
+
|
|
1214
|
+
def pbLaunch_clicked(self):
|
|
1215
|
+
"""
|
|
1216
|
+
Close dialog and start the observation
|
|
1217
|
+
"""
|
|
1218
|
+
|
|
1219
|
+
if self.check_parameters():
|
|
1220
|
+
if self.test is not None:
|
|
1221
|
+
self.test.close_plot()
|
|
1222
|
+
self.text = None
|
|
1223
|
+
self.done(2)
|
|
1224
|
+
|
|
1225
|
+
def pbSave_clicked(self):
|
|
1226
|
+
"""
|
|
1227
|
+
Close window and save observation
|
|
1228
|
+
"""
|
|
1229
|
+
if self.check_parameters():
|
|
1230
|
+
self.state = "accepted"
|
|
1231
|
+
if self.test is not None:
|
|
1232
|
+
self.test.close_plot()
|
|
1233
|
+
self.text = None
|
|
1234
|
+
self.accept()
|
|
1235
|
+
else:
|
|
1236
|
+
self.state = "refused"
|
|
1237
|
+
|
|
1238
|
+
def check_media(self, file_path: str, mode: str) -> tuple:
|
|
1239
|
+
"""
|
|
1240
|
+
check media and add them to list view if duration > 0
|
|
1241
|
+
|
|
1242
|
+
Args:
|
|
1243
|
+
file_path (str): media file path to be checked
|
|
1244
|
+
mode (str): mode for adding media file
|
|
1245
|
+
|
|
1246
|
+
Returns:
|
|
1247
|
+
bool: False if file is media else True
|
|
1248
|
+
str: error message or empty string
|
|
1249
|
+
"""
|
|
1250
|
+
|
|
1251
|
+
logging.debug(f"check_media function for {file_path}")
|
|
1252
|
+
|
|
1253
|
+
media_info = util.accurate_media_analysis(self.ffmpeg_bin, file_path)
|
|
1254
|
+
|
|
1255
|
+
logging.debug(f"{media_info=}")
|
|
1256
|
+
|
|
1257
|
+
if "error" in media_info:
|
|
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"])
|
|
1271
|
+
else:
|
|
1272
|
+
return (True, "Media duration not available")
|
|
1273
|
+
|
|
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()
|
|
1299
|
+
|
|
1300
|
+
def add_media(self, mode: str):
|
|
1301
|
+
"""
|
|
1302
|
+
add media
|
|
1303
|
+
|
|
1304
|
+
Args:
|
|
1305
|
+
mode (str): mode for adding the media file
|
|
1306
|
+
"""
|
|
1307
|
+
|
|
1308
|
+
if mode.split("|")[0] not in (
|
|
1309
|
+
"media abs path",
|
|
1310
|
+
"media rel path",
|
|
1311
|
+
"dir abs path",
|
|
1312
|
+
"dir rel path",
|
|
1313
|
+
):
|
|
1314
|
+
QMessageBox.critical(
|
|
1315
|
+
self,
|
|
1316
|
+
cfg.programName,
|
|
1317
|
+
(f"Wrong mode to add media {mode}"),
|
|
1318
|
+
)
|
|
1319
|
+
return
|
|
1320
|
+
|
|
1321
|
+
# check if project saved
|
|
1322
|
+
if (" w/o" in mode or " rel " in mode) and (not self.project_file_name):
|
|
1323
|
+
QMessageBox.critical(
|
|
1324
|
+
self,
|
|
1325
|
+
cfg.programName,
|
|
1326
|
+
("It is not possible to add a media file without path or with a relative path if the project is not already saved"),
|
|
1327
|
+
)
|
|
1328
|
+
return
|
|
1329
|
+
|
|
1330
|
+
fd = QFileDialog()
|
|
1331
|
+
if self.mem_dir:
|
|
1332
|
+
fd.setDirectory(self.mem_dir if (" abs " in mode) else str(pl.Path(self.project_path).parent))
|
|
1333
|
+
else:
|
|
1334
|
+
fd.setDirectory(os.path.expanduser("~") if (" abs " in mode) else str(pl.Path(self.project_path).parent))
|
|
1335
|
+
|
|
1336
|
+
if "media " in mode:
|
|
1337
|
+
file_paths, _ = fd.getOpenFileNames(self, "Add media file(s)", "", "All files (*)")
|
|
1338
|
+
|
|
1339
|
+
logging.debug(f"{file_paths=}")
|
|
1340
|
+
|
|
1341
|
+
if file_paths:
|
|
1342
|
+
# store directory for next usage
|
|
1343
|
+
self.mem_dir = str(pl.Path(file_paths[0]).parent)
|
|
1344
|
+
# check if media dir in contained in the BORIS file project dir
|
|
1345
|
+
if " rel " in mode:
|
|
1346
|
+
try:
|
|
1347
|
+
pl.Path(file_paths[0]).parent.relative_to(pl.Path(self.project_path).parent)
|
|
1348
|
+
except ValueError:
|
|
1349
|
+
QMessageBox.critical(
|
|
1350
|
+
self,
|
|
1351
|
+
cfg.programName,
|
|
1352
|
+
f"The directory <b>{pl.Path(file_paths[0]).parent}</b> is not contained in <b>{pl.Path(self.project_path).parent}</b>.",
|
|
1353
|
+
)
|
|
1354
|
+
return
|
|
1355
|
+
|
|
1356
|
+
for file_path in file_paths:
|
|
1357
|
+
(error, msg) = self.check_media(file_path, mode)
|
|
1358
|
+
if error:
|
|
1359
|
+
QMessageBox.critical(self, cfg.programName, f"<b>{file_path}</b>. {msg}")
|
|
1360
|
+
|
|
1361
|
+
if "dir " in mode: # add media from dir
|
|
1362
|
+
dir_name = fd.getExistingDirectory(self, "Select directory")
|
|
1363
|
+
if dir_name:
|
|
1364
|
+
response = ""
|
|
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)
|
|
1369
|
+
if error:
|
|
1370
|
+
if response != "Skip all non media files":
|
|
1371
|
+
response = dialog.MessageDialog(
|
|
1372
|
+
cfg.programName,
|
|
1373
|
+
f"<b>{file_path}</b> {msg}",
|
|
1374
|
+
["Continue", "Skip all non media files", cfg.CANCEL],
|
|
1375
|
+
)
|
|
1376
|
+
if response == cfg.CANCEL:
|
|
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)
|
|
1387
|
+
|
|
1388
|
+
self.update_media_options()
|
|
1389
|
+
|
|
1390
|
+
def add_media_to_listview(self, file_name):
|
|
1391
|
+
"""
|
|
1392
|
+
add media file path to list widget
|
|
1393
|
+
"""
|
|
1394
|
+
# add a row
|
|
1395
|
+
self.twVideo1.setRowCount(self.twVideo1.rowCount() + 1)
|
|
1396
|
+
|
|
1397
|
+
for col_idx, s in enumerate(
|
|
1398
|
+
(
|
|
1399
|
+
None,
|
|
1400
|
+
0,
|
|
1401
|
+
file_name,
|
|
1402
|
+
util.seconds2time(self.mediaDurations[file_name]),
|
|
1403
|
+
f"{self.mediaFPS[file_name]:.2f}",
|
|
1404
|
+
self.mediaHasVideo[file_name],
|
|
1405
|
+
self.mediaHasAudio[file_name],
|
|
1406
|
+
)
|
|
1407
|
+
):
|
|
1408
|
+
if col_idx == 0: # player combobox
|
|
1409
|
+
combobox = QComboBox()
|
|
1410
|
+
combobox.addItems(cfg.ALL_PLAYERS)
|
|
1411
|
+
self.twVideo1.setCellWidget(self.twVideo1.rowCount() - 1, col_idx, combobox)
|
|
1412
|
+
else:
|
|
1413
|
+
item = QTableWidgetItem(f"{s}")
|
|
1414
|
+
if col_idx != 1: # only offset is editable by user
|
|
1415
|
+
item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable)
|
|
1416
|
+
|
|
1417
|
+
self.twVideo1.setItem(self.twVideo1.rowCount() - 1, col_idx, item)
|
|
1418
|
+
|
|
1419
|
+
def remove_data_file(self):
|
|
1420
|
+
"""
|
|
1421
|
+
remove all selected data file from list widget
|
|
1422
|
+
"""
|
|
1423
|
+
if self.tw_data_files.selectedIndexes():
|
|
1424
|
+
rows_to_delete = set([x.row() for x in self.tw_data_files.selectedIndexes()])
|
|
1425
|
+
for row in sorted(rows_to_delete, reverse=True):
|
|
1426
|
+
self.tw_data_files.removeRow(row)
|
|
1427
|
+
else:
|
|
1428
|
+
QMessageBox.warning(self, cfg.programName, "No data file selected")
|
|
1429
|
+
|
|
1430
|
+
def remove_media(self):
|
|
1431
|
+
"""
|
|
1432
|
+
remove all selected media files from list widget
|
|
1433
|
+
"""
|
|
1434
|
+
|
|
1435
|
+
if not self.twVideo1.selectedIndexes():
|
|
1436
|
+
QMessageBox.warning(self, cfg.programName, "No media file selected")
|
|
1437
|
+
return
|
|
1438
|
+
|
|
1439
|
+
rows_to_delete = set([x.row() for x in self.twVideo1.selectedIndexes()])
|
|
1440
|
+
for row in sorted(rows_to_delete, reverse=True):
|
|
1441
|
+
media_path = self.twVideo1.item(row, cfg.MEDIA_FILE_PATH_IDX).text()
|
|
1442
|
+
self.twVideo1.removeRow(row)
|
|
1443
|
+
if media_path not in [self.twVideo1.item(idx, cfg.MEDIA_FILE_PATH_IDX).text() for idx in range(self.twVideo1.rowCount())]:
|
|
1444
|
+
try:
|
|
1445
|
+
del self.mediaDurations[media_path]
|
|
1446
|
+
except NameError:
|
|
1447
|
+
pass
|
|
1448
|
+
try:
|
|
1449
|
+
del self.mediaFPS[media_path]
|
|
1450
|
+
except NameError:
|
|
1451
|
+
pass
|
|
1452
|
+
|
|
1453
|
+
self.update_media_options()
|