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
|
@@ -0,0 +1,1096 @@
|
|
|
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 urllib
|
|
25
|
+
import json
|
|
26
|
+
import pathlib as pl
|
|
27
|
+
import pandas as pd
|
|
28
|
+
import tablib
|
|
29
|
+
import pickle
|
|
30
|
+
|
|
31
|
+
from PySide6.QtCore import Qt
|
|
32
|
+
from PySide6.QtGui import QFont
|
|
33
|
+
from PySide6.QtWidgets import QApplication, QFileDialog, QListWidgetItem, QMessageBox, QTableWidgetItem
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
from . import config as cfg
|
|
37
|
+
from . import dialog, param_panel, project_functions, export_observation
|
|
38
|
+
from . import utilities as util
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def export_project_as_pickle_object(pj: dict) -> None:
|
|
42
|
+
"""
|
|
43
|
+
export the project dictionary as a pickle file
|
|
44
|
+
"""
|
|
45
|
+
file_name, _ = QFileDialog().getSaveFileName(None, "Export project as pickle file", "", "All files (*)")
|
|
46
|
+
if not file_name:
|
|
47
|
+
return
|
|
48
|
+
try:
|
|
49
|
+
with open(file_name, "wb") as f_out:
|
|
50
|
+
pickle.dump(pj, f_out)
|
|
51
|
+
except Exception:
|
|
52
|
+
QMessageBox.critical(
|
|
53
|
+
None,
|
|
54
|
+
cfg.programName,
|
|
55
|
+
"Error during file saving.",
|
|
56
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
57
|
+
QMessageBox.NoButton,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def export_ethogram(self) -> None:
|
|
62
|
+
"""
|
|
63
|
+
export ethogram in various format
|
|
64
|
+
"""
|
|
65
|
+
extended_file_formats: list = [
|
|
66
|
+
"BORIS project file (*.boris)",
|
|
67
|
+
"Tab Separated Values (*.tsv)",
|
|
68
|
+
"Comma Separated Values (*.csv)",
|
|
69
|
+
"Open Document Spreadsheet ODS (*.ods)",
|
|
70
|
+
"Microsoft Excel Spreadsheet XLSX (*.xlsx)",
|
|
71
|
+
"Legacy Microsoft Excel Spreadsheet XLS (*.xls)",
|
|
72
|
+
"HTML (*.html)",
|
|
73
|
+
]
|
|
74
|
+
file_formats: list = ["boris", cfg.TSV_EXT, cfg.CSV_EXT, cfg.ODS_EXT, cfg.XLSX_EXT, cfg.XLS_EXT, cfg.HTML_EXT]
|
|
75
|
+
|
|
76
|
+
filediag_func = QFileDialog().getSaveFileName
|
|
77
|
+
|
|
78
|
+
file_name, filter_ = filediag_func(self, "Export ethogram", "", ";;".join(extended_file_formats))
|
|
79
|
+
if not file_name:
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
output_format: str = file_formats[extended_file_formats.index(filter_)]
|
|
83
|
+
if pl.Path(file_name).suffix != "." + output_format:
|
|
84
|
+
file_name = str(pl.Path(file_name)) + "." + output_format
|
|
85
|
+
|
|
86
|
+
if output_format == "boris":
|
|
87
|
+
r = self.check_ethogram()
|
|
88
|
+
if cfg.CANCEL in r:
|
|
89
|
+
return
|
|
90
|
+
pj = dict(cfg.EMPTY_PROJECT)
|
|
91
|
+
pj[cfg.ETHOGRAM] = dict(r)
|
|
92
|
+
|
|
93
|
+
# behavioral categories
|
|
94
|
+
pj[cfg.BEHAVIORAL_CATEGORIES] = list(self.pj[cfg.BEHAVIORAL_CATEGORIES])
|
|
95
|
+
pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = dict(self.pj.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {}))
|
|
96
|
+
|
|
97
|
+
# project file indentation
|
|
98
|
+
file_indentation = self.config_param.get(cfg.PROJECT_FILE_INDENTATION, cfg.PROJECT_FILE_INDENTATION_DEFAULT_VALUE)
|
|
99
|
+
try:
|
|
100
|
+
with open(file_name, "w") as f_out:
|
|
101
|
+
f_out.write(json.dumps(pj, indent=file_indentation))
|
|
102
|
+
except Exception:
|
|
103
|
+
QMessageBox.critical(
|
|
104
|
+
None,
|
|
105
|
+
cfg.programName,
|
|
106
|
+
"Error during file saving.",
|
|
107
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
108
|
+
QMessageBox.NoButton,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
else:
|
|
112
|
+
ethogram_data = tablib.Dataset()
|
|
113
|
+
ethogram_data.title = "Ethogram"
|
|
114
|
+
if self.leProjectName.text():
|
|
115
|
+
ethogram_data.title = f"Ethogram of {self.leProjectName.text()} project"
|
|
116
|
+
|
|
117
|
+
ethogram_data.headers = [
|
|
118
|
+
"Behavior code",
|
|
119
|
+
"Behavior type",
|
|
120
|
+
"Description",
|
|
121
|
+
"Key",
|
|
122
|
+
"Color",
|
|
123
|
+
"Behavioral category",
|
|
124
|
+
"Excluded behaviors",
|
|
125
|
+
"Modifiers",
|
|
126
|
+
"Modifiers (JSON)",
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
for r in range(self.twBehaviors.rowCount()):
|
|
130
|
+
row: list = []
|
|
131
|
+
for field in ("code", cfg.TYPE, "description", "key", cfg.COLOR, "category", "excluded"):
|
|
132
|
+
row.append(self.twBehaviors.item(r, cfg.behavioursFields[field]).text())
|
|
133
|
+
|
|
134
|
+
# modifiers
|
|
135
|
+
if self.twBehaviors.item(r, cfg.behavioursFields[cfg.MODIFIERS]).text():
|
|
136
|
+
# modifiers a string
|
|
137
|
+
modifiers_dict = json.loads(self.twBehaviors.item(r, cfg.behavioursFields[cfg.MODIFIERS]).text())
|
|
138
|
+
modifiers_list = []
|
|
139
|
+
for key in modifiers_dict:
|
|
140
|
+
values = ",".join(modifiers_dict[key]["values"])
|
|
141
|
+
modifiers_list.append(f"{modifiers_dict[key]['name']}:{values}")
|
|
142
|
+
row.append(";".join(modifiers_list))
|
|
143
|
+
# modifiers as JSON
|
|
144
|
+
row.append(self.twBehaviors.item(r, cfg.behavioursFields[cfg.MODIFIERS]).text())
|
|
145
|
+
else:
|
|
146
|
+
# modifiers a string
|
|
147
|
+
row.append("")
|
|
148
|
+
# modifiers as JSON
|
|
149
|
+
row.append("")
|
|
150
|
+
|
|
151
|
+
ethogram_data.append(row)
|
|
152
|
+
|
|
153
|
+
ok, msg = export_observation.dataset_write(ethogram_data, file_name, output_format)
|
|
154
|
+
if not ok:
|
|
155
|
+
QMessageBox.critical(None, cfg.programName, msg, QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def export_subjects(self) -> None:
|
|
159
|
+
"""
|
|
160
|
+
export the subjetcs list in various format
|
|
161
|
+
"""
|
|
162
|
+
extended_file_formats: list = [
|
|
163
|
+
cfg.TSV,
|
|
164
|
+
cfg.CSV,
|
|
165
|
+
cfg.ODS,
|
|
166
|
+
cfg.XLSX,
|
|
167
|
+
cfg.XLS,
|
|
168
|
+
cfg.HTML,
|
|
169
|
+
]
|
|
170
|
+
file_formats: list = [cfg.TSV_EXT, cfg.CSV_EXT, cfg.ODS_EXT, cfg.XLSX_EXT, cfg.XLS_EXT, cfg.HTML_EXT]
|
|
171
|
+
|
|
172
|
+
filediag_func = QFileDialog().getSaveFileName
|
|
173
|
+
|
|
174
|
+
file_name, filter_ = filediag_func(self, "Export the subjects list", "", ";;".join(extended_file_formats))
|
|
175
|
+
if not file_name:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
outputFormat = file_formats[extended_file_formats.index(filter_)]
|
|
179
|
+
if pl.Path(file_name).suffix != "." + outputFormat:
|
|
180
|
+
file_name = str(pl.Path(file_name)) + "." + outputFormat
|
|
181
|
+
|
|
182
|
+
subjects_data = tablib.Dataset()
|
|
183
|
+
subjects_data.title = "Subjects"
|
|
184
|
+
if self.leProjectName.text():
|
|
185
|
+
subjects_data.title = f"Subjects defined in the {self.leProjectName.text()} project"
|
|
186
|
+
|
|
187
|
+
subjects_data.headers: list = [
|
|
188
|
+
"Key",
|
|
189
|
+
"Subject name",
|
|
190
|
+
"Description",
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
for r in range(self.twSubjects.rowCount()):
|
|
194
|
+
row: list = []
|
|
195
|
+
for idx, _ in enumerate(("Key", "Subject name", "Description")):
|
|
196
|
+
row.append(self.twSubjects.item(r, idx).text())
|
|
197
|
+
|
|
198
|
+
subjects_data.append(row)
|
|
199
|
+
|
|
200
|
+
ok, msg = export_observation.dataset_write(subjects_data, file_name, outputFormat)
|
|
201
|
+
if not ok:
|
|
202
|
+
QMessageBox.critical(None, cfg.programName, msg, QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def select_behaviors(
|
|
206
|
+
title: str = "Record value from external data file",
|
|
207
|
+
text: str = "Behaviors",
|
|
208
|
+
behavioral_categories: list = [],
|
|
209
|
+
ethogram: dict = {},
|
|
210
|
+
behavior_type=[cfg.STATE_EVENT, cfg.POINT_EVENT],
|
|
211
|
+
) -> list:
|
|
212
|
+
"""
|
|
213
|
+
allow user to select behaviors to import
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
title (str): title of dialog box
|
|
217
|
+
text (str): text of dialog box
|
|
218
|
+
behavioral_categories (list): behavioral categories
|
|
219
|
+
ethogram (dict): ethogram
|
|
220
|
+
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
paramPanelWindow = param_panel.Param_panel()
|
|
224
|
+
paramPanelWindow.resize(800, 600)
|
|
225
|
+
paramPanelWindow.setWindowTitle(title)
|
|
226
|
+
paramPanelWindow.lbBehaviors.setText(text)
|
|
227
|
+
for w in (
|
|
228
|
+
paramPanelWindow.lwSubjects,
|
|
229
|
+
paramPanelWindow.pbSelectAllSubjects,
|
|
230
|
+
paramPanelWindow.pbUnselectAllSubjects,
|
|
231
|
+
paramPanelWindow.pbReverseSubjectsSelection,
|
|
232
|
+
paramPanelWindow.lbSubjects,
|
|
233
|
+
paramPanelWindow.cbIncludeModifiers,
|
|
234
|
+
paramPanelWindow.cbExcludeBehaviors,
|
|
235
|
+
paramPanelWindow.frm_time,
|
|
236
|
+
paramPanelWindow.frm_time_bin_size,
|
|
237
|
+
):
|
|
238
|
+
w.setVisible(False)
|
|
239
|
+
|
|
240
|
+
if behavioral_categories:
|
|
241
|
+
categories = behavioral_categories
|
|
242
|
+
# check if behavior not included in a category
|
|
243
|
+
if "" in [ethogram[idx][cfg.BEHAVIOR_CATEGORY] for idx in ethogram if cfg.BEHAVIOR_CATEGORY in ethogram[idx]]:
|
|
244
|
+
categories += [""]
|
|
245
|
+
else:
|
|
246
|
+
categories = ["###no category###"]
|
|
247
|
+
|
|
248
|
+
for category in categories:
|
|
249
|
+
if category != "###no category###":
|
|
250
|
+
if category == "":
|
|
251
|
+
paramPanelWindow.item = QListWidgetItem("No category")
|
|
252
|
+
paramPanelWindow.item.setData(34, "No category")
|
|
253
|
+
else:
|
|
254
|
+
paramPanelWindow.item = QListWidgetItem(category)
|
|
255
|
+
paramPanelWindow.item.setData(34, category)
|
|
256
|
+
|
|
257
|
+
font = QFont()
|
|
258
|
+
font.setBold(True)
|
|
259
|
+
paramPanelWindow.item.setFont(font)
|
|
260
|
+
paramPanelWindow.item.setData(33, "category")
|
|
261
|
+
paramPanelWindow.item.setData(35, False)
|
|
262
|
+
|
|
263
|
+
paramPanelWindow.lwBehaviors.addItem(paramPanelWindow.item)
|
|
264
|
+
|
|
265
|
+
# check if behavior type must be shown
|
|
266
|
+
for behavior in [ethogram[x][cfg.BEHAVIOR_CODE] for x in util.sorted_keys(ethogram)]:
|
|
267
|
+
if (categories == ["###no category###"]) or (
|
|
268
|
+
behavior
|
|
269
|
+
in [
|
|
270
|
+
ethogram[x][cfg.BEHAVIOR_CODE]
|
|
271
|
+
for x in ethogram
|
|
272
|
+
if cfg.BEHAVIOR_CATEGORY in ethogram[x] and ethogram[x][cfg.BEHAVIOR_CATEGORY] == category
|
|
273
|
+
]
|
|
274
|
+
):
|
|
275
|
+
paramPanelWindow.item = QListWidgetItem(behavior)
|
|
276
|
+
paramPanelWindow.item.setCheckState(Qt.Unchecked)
|
|
277
|
+
|
|
278
|
+
if category != "###no category###":
|
|
279
|
+
paramPanelWindow.item.setData(33, "behavior")
|
|
280
|
+
if category == "":
|
|
281
|
+
paramPanelWindow.item.setData(34, "No category")
|
|
282
|
+
else:
|
|
283
|
+
paramPanelWindow.item.setData(34, category)
|
|
284
|
+
|
|
285
|
+
paramPanelWindow.lwBehaviors.addItem(paramPanelWindow.item)
|
|
286
|
+
|
|
287
|
+
if paramPanelWindow.exec_():
|
|
288
|
+
return paramPanelWindow.selectedBehaviors
|
|
289
|
+
|
|
290
|
+
return []
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def check_text_file_type(rows: list):
|
|
294
|
+
"""
|
|
295
|
+
check text file
|
|
296
|
+
returns separator and number of fields (if unique)
|
|
297
|
+
"""
|
|
298
|
+
for separator in "\t,;":
|
|
299
|
+
cs: list = []
|
|
300
|
+
for row in rows:
|
|
301
|
+
cs.append(row.count(separator))
|
|
302
|
+
if len(set(cs)) == 1:
|
|
303
|
+
return separator, cs[0] + 1
|
|
304
|
+
return None, None
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def import_ethogram_from_dict(self, project: dict):
|
|
308
|
+
"""
|
|
309
|
+
Import behaviors from a BORIS project dictionary
|
|
310
|
+
"""
|
|
311
|
+
# import behavioral_categories
|
|
312
|
+
self.pj[cfg.BEHAVIORAL_CATEGORIES] = list(project.get(cfg.BEHAVIORAL_CATEGORIES, []))
|
|
313
|
+
self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = list(project.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {}))
|
|
314
|
+
|
|
315
|
+
# configuration of behaviours
|
|
316
|
+
if not (cfg.ETHOGRAM in project and project[cfg.ETHOGRAM]):
|
|
317
|
+
QMessageBox.warning(self, cfg.programName, "No behaviors configuration found in project")
|
|
318
|
+
return
|
|
319
|
+
|
|
320
|
+
if self.twBehaviors.rowCount():
|
|
321
|
+
response = dialog.MessageDialog(
|
|
322
|
+
cfg.programName,
|
|
323
|
+
("Some behaviors are already configured. Do you want to append behaviors or replace them?"),
|
|
324
|
+
[cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
|
|
325
|
+
)
|
|
326
|
+
if response == cfg.REPLACE:
|
|
327
|
+
self.twBehaviors.setRowCount(0)
|
|
328
|
+
self.twBehaviors_cellChanged(0, 0)
|
|
329
|
+
if response == cfg.CANCEL:
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
behaviors_to_import = select_behaviors(
|
|
333
|
+
title="Select the behaviors to import",
|
|
334
|
+
text="Behaviors",
|
|
335
|
+
behavioral_categories=list(project.get(cfg.BEHAVIORAL_CATEGORIES, [])),
|
|
336
|
+
ethogram=dict(project[cfg.ETHOGRAM]),
|
|
337
|
+
behavior_type=[cfg.STATE_EVENT, cfg.POINT_EVENT],
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
for i in util.sorted_keys(project[cfg.ETHOGRAM]):
|
|
341
|
+
if project[cfg.ETHOGRAM][i][cfg.BEHAVIOR_CODE] not in behaviors_to_import:
|
|
342
|
+
continue
|
|
343
|
+
|
|
344
|
+
self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1)
|
|
345
|
+
|
|
346
|
+
for field in project[cfg.ETHOGRAM][i]:
|
|
347
|
+
item = QTableWidgetItem()
|
|
348
|
+
|
|
349
|
+
if field == cfg.TYPE:
|
|
350
|
+
item.setText(project[cfg.ETHOGRAM][i][field])
|
|
351
|
+
item.setFlags(Qt.ItemIsEnabled)
|
|
352
|
+
# item.setBackground(QColor(230, 230, 230))
|
|
353
|
+
item.setBackground(self.not_editable_column_color())
|
|
354
|
+
|
|
355
|
+
else:
|
|
356
|
+
if field == cfg.MODIFIERS:
|
|
357
|
+
if isinstance(project[cfg.ETHOGRAM][i][field], str):
|
|
358
|
+
modif_set_dict = {}
|
|
359
|
+
if project[cfg.ETHOGRAM][i][field]:
|
|
360
|
+
modif_set_list = project[cfg.ETHOGRAM][i][field].split("|")
|
|
361
|
+
for modif_set in modif_set_list:
|
|
362
|
+
modif_set_dict[str(len(modif_set_dict))] = {
|
|
363
|
+
"name": "",
|
|
364
|
+
"type": cfg.SINGLE_SELECTION,
|
|
365
|
+
"values": modif_set.split(","),
|
|
366
|
+
}
|
|
367
|
+
project[cfg.ETHOGRAM][i][field] = dict(modif_set_dict)
|
|
368
|
+
else:
|
|
369
|
+
item.setText(json.dumps(project[cfg.ETHOGRAM][i][field]))
|
|
370
|
+
else:
|
|
371
|
+
item.setText(project[cfg.ETHOGRAM][i][field])
|
|
372
|
+
|
|
373
|
+
if field not in cfg.ETHOGRAM_EDITABLE_FIELDS:
|
|
374
|
+
item.setFlags(Qt.ItemIsEnabled)
|
|
375
|
+
# item.setBackground(QColor(230, 230, 230))
|
|
376
|
+
item.setBackground(self.not_editable_column_color())
|
|
377
|
+
|
|
378
|
+
self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field], item)
|
|
379
|
+
|
|
380
|
+
self.twBehaviors.resizeColumnsToContents()
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def load_dataframe_into_behaviors_tablewidget(self, df: pd.DataFrame) -> int:
|
|
384
|
+
"""
|
|
385
|
+
Load pandas dataframe into the twBehaviors table widget
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
int: 0 if no error else error code
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
expected_labels: list = [
|
|
392
|
+
"Behavior code",
|
|
393
|
+
"Behavior type",
|
|
394
|
+
"Description",
|
|
395
|
+
"Key",
|
|
396
|
+
"Behavioral category",
|
|
397
|
+
"Excluded behaviors",
|
|
398
|
+
]
|
|
399
|
+
|
|
400
|
+
ethogram_header: dict = {
|
|
401
|
+
"code": "Behavior code",
|
|
402
|
+
"description": "Description",
|
|
403
|
+
"key": "Key",
|
|
404
|
+
"color": "Color",
|
|
405
|
+
"category": "Behavioral category",
|
|
406
|
+
"excluded": "Excluded behaviors",
|
|
407
|
+
"modifiers": "modifiers (JSON)",
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
# change all column names to uppercase
|
|
411
|
+
df.columns = df.columns.str.upper()
|
|
412
|
+
|
|
413
|
+
for column in expected_labels:
|
|
414
|
+
if column.upper() not in list(df.columns):
|
|
415
|
+
QMessageBox.warning(
|
|
416
|
+
None,
|
|
417
|
+
cfg.programName,
|
|
418
|
+
(
|
|
419
|
+
f"The {column} column was not found in the file header.<br>"
|
|
420
|
+
"For information the current file header contains the following labels:<br>"
|
|
421
|
+
f"{'<br>'.join(['<b>' + util.replace_leading_trailing_chars(x, ' ', '█') + '</b>' for x in df.columns])}<br>"
|
|
422
|
+
"<br>"
|
|
423
|
+
"The first row of the spreadsheet must contain the following labels:<br>"
|
|
424
|
+
f"{'<br>'.join(['<b>' + x + '</b>' for x in expected_labels])}<br>"
|
|
425
|
+
"<br>The order is not mandatory."
|
|
426
|
+
),
|
|
427
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
428
|
+
QMessageBox.NoButton,
|
|
429
|
+
)
|
|
430
|
+
return 1
|
|
431
|
+
|
|
432
|
+
for _, row in df.iterrows():
|
|
433
|
+
behavior = {"coding map": ""}
|
|
434
|
+
for x in ethogram_header:
|
|
435
|
+
if ethogram_header[x].upper() in row:
|
|
436
|
+
behavior[x] = row[ethogram_header[x].upper()] if str(row[ethogram_header[x].upper()]) != "nan" else ""
|
|
437
|
+
else:
|
|
438
|
+
behavior[x] = ""
|
|
439
|
+
|
|
440
|
+
self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1)
|
|
441
|
+
|
|
442
|
+
for field_type in cfg.behavioursFields:
|
|
443
|
+
if field_type == cfg.TYPE:
|
|
444
|
+
item = QTableWidgetItem(cfg.DEFAULT_BEHAVIOR_TYPE)
|
|
445
|
+
# add type combobox
|
|
446
|
+
if cfg.POINT in row["Behavior type".upper()].upper():
|
|
447
|
+
item = QTableWidgetItem(cfg.POINT_EVENT)
|
|
448
|
+
elif cfg.STATE in row["Behavior type".upper()].upper():
|
|
449
|
+
item = QTableWidgetItem(cfg.STATE_EVENT)
|
|
450
|
+
else:
|
|
451
|
+
QMessageBox.critical(
|
|
452
|
+
None,
|
|
453
|
+
cfg.programName,
|
|
454
|
+
f"{row['Behavior code']} has no behavior type (POINT or STATE)",
|
|
455
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
456
|
+
QMessageBox.NoButton,
|
|
457
|
+
)
|
|
458
|
+
return 2
|
|
459
|
+
|
|
460
|
+
else:
|
|
461
|
+
item = QTableWidgetItem(str(behavior[field_type]))
|
|
462
|
+
|
|
463
|
+
if field_type not in cfg.ETHOGRAM_EDITABLE_FIELDS:
|
|
464
|
+
item.setFlags(Qt.ItemIsEnabled)
|
|
465
|
+
# item.setBackground(QColor(230, 230, 230))
|
|
466
|
+
item.setBackground(self.not_editable_column_color())
|
|
467
|
+
|
|
468
|
+
self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field_type], item)
|
|
469
|
+
|
|
470
|
+
return 0
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def import_behaviors_from_project(self):
|
|
474
|
+
"""
|
|
475
|
+
import ethogram from a BORIS project file
|
|
476
|
+
"""
|
|
477
|
+
file_name, _ = QFileDialog.getOpenFileName(
|
|
478
|
+
self, "Import behaviors from BORIS project file", "", ("Project files (*.boris *.boris.gz);;All files (*)")
|
|
479
|
+
)
|
|
480
|
+
if not file_name:
|
|
481
|
+
return
|
|
482
|
+
_, _, project, _ = project_functions.open_project_json(file_name)
|
|
483
|
+
|
|
484
|
+
import_ethogram_from_dict(self, project)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def import_behaviors_from_text_file(self):
|
|
488
|
+
"""
|
|
489
|
+
Import ethogram from text file (CSV or TSV)
|
|
490
|
+
"""
|
|
491
|
+
|
|
492
|
+
if self.twBehaviors.rowCount():
|
|
493
|
+
response = dialog.MessageDialog(
|
|
494
|
+
cfg.programName,
|
|
495
|
+
"There are behaviors already configured. Do you want to append behaviors or replace them?",
|
|
496
|
+
[cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
|
|
497
|
+
)
|
|
498
|
+
if response == cfg.CANCEL:
|
|
499
|
+
return
|
|
500
|
+
|
|
501
|
+
file_name, _ = QFileDialog.getOpenFileName(
|
|
502
|
+
self, "Import behaviors from text file (CSV, TSV)", "", "Text files (*.txt *.tsv *.csv);;All files (*)"
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
if not file_name:
|
|
506
|
+
return
|
|
507
|
+
|
|
508
|
+
if pl.Path(file_name).suffix.upper() == ".CSV":
|
|
509
|
+
delimiter = ","
|
|
510
|
+
elif pl.Path(file_name).suffix.upper() == ".TSV":
|
|
511
|
+
delimiter = "\t"
|
|
512
|
+
else:
|
|
513
|
+
QMessageBox.warning(
|
|
514
|
+
None,
|
|
515
|
+
cfg.programName,
|
|
516
|
+
("The type of file was not recognized. Must be Comma Separated Values (,) or Tab Separated Values"),
|
|
517
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
518
|
+
QMessageBox.NoButton,
|
|
519
|
+
)
|
|
520
|
+
return
|
|
521
|
+
|
|
522
|
+
try:
|
|
523
|
+
df = pd.read_csv(file_name, delimiter=delimiter)
|
|
524
|
+
except Exception:
|
|
525
|
+
QMessageBox.warning(
|
|
526
|
+
None,
|
|
527
|
+
cfg.programName,
|
|
528
|
+
("The type of file was not recognized. Must be Comma Separated Values (,) or Tab Separated Values"),
|
|
529
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
530
|
+
QMessageBox.NoButton,
|
|
531
|
+
)
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
if self.twBehaviors.rowCount() and response == cfg.REPLACE:
|
|
535
|
+
self.twBehaviors.setRowCount(0)
|
|
536
|
+
|
|
537
|
+
load_dataframe_into_behaviors_tablewidget(self, df)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def import_behaviors_from_spreadsheet(self):
|
|
541
|
+
"""
|
|
542
|
+
Import behaviors from a spreadsheet file (XLSX)
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
if self.twBehaviors.rowCount():
|
|
546
|
+
response = dialog.MessageDialog(
|
|
547
|
+
cfg.programName,
|
|
548
|
+
"There are behaviors already configured. Do you want to append behaviors or replace them?",
|
|
549
|
+
[cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
|
|
550
|
+
)
|
|
551
|
+
if response == cfg.CANCEL:
|
|
552
|
+
return
|
|
553
|
+
|
|
554
|
+
file_name, _ = QFileDialog.getOpenFileName(
|
|
555
|
+
self, "Import behaviors from a spreadsheet file", "", "Spreadsheet files (*.xlsx *.ods);;All files (*)"
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
if not file_name:
|
|
559
|
+
return
|
|
560
|
+
|
|
561
|
+
if pl.Path(file_name).suffix.upper() == ".XLSX":
|
|
562
|
+
engine = "openpyxl"
|
|
563
|
+
elif pl.Path(file_name).suffix.upper() == ".ODS":
|
|
564
|
+
engine = "odf"
|
|
565
|
+
else:
|
|
566
|
+
QMessageBox.warning(
|
|
567
|
+
None,
|
|
568
|
+
cfg.programName,
|
|
569
|
+
("The type of file was not recognized. Must be Microsoft-Excel XLSX format or OpenDocument ODS"),
|
|
570
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
571
|
+
QMessageBox.NoButton,
|
|
572
|
+
)
|
|
573
|
+
return
|
|
574
|
+
|
|
575
|
+
try:
|
|
576
|
+
df = pd.read_excel(file_name, sheet_name=0, engine=engine)
|
|
577
|
+
except Exception:
|
|
578
|
+
QMessageBox.warning(
|
|
579
|
+
None,
|
|
580
|
+
cfg.programName,
|
|
581
|
+
("The type of file was not recognized. Must be Microsoft-Excel XLSX format or OpenDocument ODS"),
|
|
582
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
583
|
+
QMessageBox.NoButton,
|
|
584
|
+
)
|
|
585
|
+
return
|
|
586
|
+
|
|
587
|
+
if self.twBehaviors.rowCount() and response == cfg.REPLACE:
|
|
588
|
+
self.twBehaviors.setRowCount(0)
|
|
589
|
+
|
|
590
|
+
load_dataframe_into_behaviors_tablewidget(self, df)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def import_behaviors_from_clipboard(self):
|
|
594
|
+
"""
|
|
595
|
+
import ethogram from clipboard
|
|
596
|
+
"""
|
|
597
|
+
|
|
598
|
+
cb = QApplication.clipboard()
|
|
599
|
+
cb_text = cb.text()
|
|
600
|
+
if not cb_text:
|
|
601
|
+
QMessageBox.warning(
|
|
602
|
+
None,
|
|
603
|
+
cfg.programName,
|
|
604
|
+
"The clipboard is empty",
|
|
605
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
606
|
+
QMessageBox.NoButton,
|
|
607
|
+
)
|
|
608
|
+
return
|
|
609
|
+
|
|
610
|
+
if self.twBehaviors.rowCount():
|
|
611
|
+
response = dialog.MessageDialog(
|
|
612
|
+
cfg.programName,
|
|
613
|
+
"Some behaviors are already configured. Do you want to append behaviors or replace them?",
|
|
614
|
+
[cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
|
|
615
|
+
)
|
|
616
|
+
if response == cfg.CANCEL:
|
|
617
|
+
return
|
|
618
|
+
|
|
619
|
+
if response == cfg.REPLACE:
|
|
620
|
+
self.twBehaviors.setRowCount(0)
|
|
621
|
+
|
|
622
|
+
cb_text_splitted = cb_text.split("\n")
|
|
623
|
+
while "" in cb_text_splitted:
|
|
624
|
+
cb_text_splitted.remove("")
|
|
625
|
+
|
|
626
|
+
if len(set([len(x.split("\t")) for x in cb_text_splitted])) != 1:
|
|
627
|
+
QMessageBox.warning(
|
|
628
|
+
None,
|
|
629
|
+
cfg.programName,
|
|
630
|
+
(
|
|
631
|
+
"The clipboard content does not have a constant number of fields.<br>"
|
|
632
|
+
"From your spreadsheet: CTRL + A (select all cells), CTRL + C (copy to clipboard)"
|
|
633
|
+
),
|
|
634
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
635
|
+
QMessageBox.NoButton,
|
|
636
|
+
)
|
|
637
|
+
return
|
|
638
|
+
|
|
639
|
+
for row in cb_text_splitted:
|
|
640
|
+
if set(row.split("\t")) != set([""]):
|
|
641
|
+
behavior = {"type": cfg.DEFAULT_BEHAVIOR_TYPE}
|
|
642
|
+
for idx, field in enumerate(row.split("\t")):
|
|
643
|
+
if idx == 0:
|
|
644
|
+
behavior["type"] = (
|
|
645
|
+
cfg.STATE_EVENT if cfg.STATE in field.upper() else (cfg.POINT_EVENT if cfg.POINT in field.upper() else "")
|
|
646
|
+
)
|
|
647
|
+
if idx == 1:
|
|
648
|
+
behavior["key"] = field.strip() if len(field.strip()) == 1 else ""
|
|
649
|
+
if idx == 2:
|
|
650
|
+
behavior["code"] = field.strip()
|
|
651
|
+
if idx == 3:
|
|
652
|
+
behavior["description"] = field.strip()
|
|
653
|
+
if idx == 4:
|
|
654
|
+
behavior["category"] = field.strip()
|
|
655
|
+
|
|
656
|
+
self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1)
|
|
657
|
+
|
|
658
|
+
for field_type in cfg.behavioursFields:
|
|
659
|
+
if field_type == cfg.TYPE:
|
|
660
|
+
item = QTableWidgetItem(behavior.get("type", cfg.DEFAULT_BEHAVIOR_TYPE))
|
|
661
|
+
else:
|
|
662
|
+
item = QTableWidgetItem(behavior.get(field_type, ""))
|
|
663
|
+
|
|
664
|
+
if field_type not in cfg.ETHOGRAM_EDITABLE_FIELDS: # [TYPE, "excluded", "coding map", "modifiers", "category"]:
|
|
665
|
+
item.setFlags(Qt.ItemIsEnabled)
|
|
666
|
+
# item.setBackground(QColor(230, 230, 230))
|
|
667
|
+
item.setBackground(self.not_editable_column_color())
|
|
668
|
+
|
|
669
|
+
self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field_type], item)
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def import_behaviors_from_JWatcher(self):
|
|
673
|
+
"""
|
|
674
|
+
import behaviors configuration from JWatcher (GDF file)
|
|
675
|
+
"""
|
|
676
|
+
|
|
677
|
+
if self.twBehaviors.rowCount():
|
|
678
|
+
response = dialog.MessageDialog(
|
|
679
|
+
cfg.programName,
|
|
680
|
+
"There are behaviors already configured. Do you want to append behaviors or replace them?",
|
|
681
|
+
[cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
|
|
682
|
+
)
|
|
683
|
+
if response == cfg.CANCEL:
|
|
684
|
+
return
|
|
685
|
+
|
|
686
|
+
fileName, _ = QFileDialog().getOpenFileName(self, "Import behaviors from JWatcher", "", "Global Definition File (*.gdf);;All files (*)")
|
|
687
|
+
|
|
688
|
+
if not fileName:
|
|
689
|
+
return
|
|
690
|
+
if self.twBehaviors.rowCount() and response == cfg.REPLACE:
|
|
691
|
+
self.twBehaviors.setRowCount(0)
|
|
692
|
+
|
|
693
|
+
with open(fileName, "r") as f:
|
|
694
|
+
rows = f.readlines()
|
|
695
|
+
|
|
696
|
+
for idx, row in enumerate(rows):
|
|
697
|
+
if row and row[0] == "#":
|
|
698
|
+
continue
|
|
699
|
+
|
|
700
|
+
if "Behavior.name." in row and "=" in row:
|
|
701
|
+
key, code = row.split("=")
|
|
702
|
+
key = key.replace("Behavior.name.", "")
|
|
703
|
+
# read description
|
|
704
|
+
if idx < len(rows) and "Behavior.description." in rows[idx + 1]:
|
|
705
|
+
description = rows[idx + 1].split("=")[-1]
|
|
706
|
+
|
|
707
|
+
behavior = {
|
|
708
|
+
"key": key,
|
|
709
|
+
"code": code,
|
|
710
|
+
"description": description,
|
|
711
|
+
"modifiers": "",
|
|
712
|
+
"excluded": "",
|
|
713
|
+
"coding map": "",
|
|
714
|
+
"category": "",
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1)
|
|
718
|
+
|
|
719
|
+
for field_type in cfg.behavioursFields:
|
|
720
|
+
if field_type == cfg.TYPE:
|
|
721
|
+
item = QTableWidgetItem(cfg.DEFAULT_BEHAVIOR_TYPE)
|
|
722
|
+
else:
|
|
723
|
+
item = QTableWidgetItem(behavior[field_type])
|
|
724
|
+
|
|
725
|
+
if field_type in [cfg.TYPE, "excluded", "category", "coding map", "modifiers"]:
|
|
726
|
+
item.setFlags(Qt.ItemIsEnabled)
|
|
727
|
+
# item.setBackground(QColor(230, 230, 230))
|
|
728
|
+
item.setBackground(self.not_editable_column_color())
|
|
729
|
+
|
|
730
|
+
self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, cfg.behavioursFields[field_type], item)
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def import_behaviors_from_repository(self):
|
|
734
|
+
"""
|
|
735
|
+
import behaviors from the BORIS ethogram repository
|
|
736
|
+
"""
|
|
737
|
+
|
|
738
|
+
try:
|
|
739
|
+
ethogram_list = urllib.request.urlopen(f"{cfg.ETHOGRAM_REPOSITORY_URL}/ethogram_list.json").read().strip().decode("utf-8")
|
|
740
|
+
except Exception:
|
|
741
|
+
QMessageBox.critical(self, cfg.programName, "An error occured during retrieving the ethogram list from BORIS repository")
|
|
742
|
+
return
|
|
743
|
+
|
|
744
|
+
try:
|
|
745
|
+
ethogram_list_list = json.loads(ethogram_list)
|
|
746
|
+
except Exception:
|
|
747
|
+
QMessageBox.critical(self, cfg.programName, "An error occured during loading ethogram list from BORIS repository")
|
|
748
|
+
return
|
|
749
|
+
|
|
750
|
+
choice_dialog = dialog.ChooseObservationsToImport(
|
|
751
|
+
"Choose the ethogram to import:", sorted([f"{x['species']} by {x['author']}" for x in ethogram_list_list])
|
|
752
|
+
)
|
|
753
|
+
while True:
|
|
754
|
+
if not choice_dialog.exec_():
|
|
755
|
+
return
|
|
756
|
+
|
|
757
|
+
if len(choice_dialog.get_selected_observations()) == 0:
|
|
758
|
+
QMessageBox.critical(self, cfg.programName, "Choose one ethogram")
|
|
759
|
+
continue
|
|
760
|
+
|
|
761
|
+
if len(choice_dialog.get_selected_observations()) > 1:
|
|
762
|
+
QMessageBox.critical(self, cfg.programName, "Choose only one ethogram")
|
|
763
|
+
continue
|
|
764
|
+
|
|
765
|
+
break
|
|
766
|
+
|
|
767
|
+
for x in ethogram_list_list:
|
|
768
|
+
if f"{x['species']} by {x['author']}" == choice_dialog.get_selected_observations()[0]:
|
|
769
|
+
file_name = x["file name"]
|
|
770
|
+
break
|
|
771
|
+
|
|
772
|
+
try:
|
|
773
|
+
boris_project_str = urllib.request.urlopen(f"{cfg.ETHOGRAM_REPOSITORY_URL}/{file_name}").read().strip().decode("utf-8")
|
|
774
|
+
except Exception:
|
|
775
|
+
QMessageBox.critical(self, cfg.programName, f"An error occured during retrieving {file_name} from BORIS repository")
|
|
776
|
+
return
|
|
777
|
+
boris_project = json.loads(boris_project_str)
|
|
778
|
+
|
|
779
|
+
import_ethogram_from_dict(self, boris_project)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def load_dataframe_into_subjects_tablewidget(self, df: pd.DataFrame) -> int:
|
|
783
|
+
"""
|
|
784
|
+
Load pandas dataframe into the twSubjects table widget
|
|
785
|
+
|
|
786
|
+
Returns:
|
|
787
|
+
int: 0 if no error else error code
|
|
788
|
+
|
|
789
|
+
"""
|
|
790
|
+
|
|
791
|
+
expected_labels: list = ["Key", "Subject name", "Description"]
|
|
792
|
+
|
|
793
|
+
# change all column names to uppercase
|
|
794
|
+
df.columns = df.columns.str.upper()
|
|
795
|
+
|
|
796
|
+
for column in expected_labels:
|
|
797
|
+
if column.upper() not in list(df.columns):
|
|
798
|
+
QMessageBox.warning(
|
|
799
|
+
None,
|
|
800
|
+
cfg.programName,
|
|
801
|
+
(
|
|
802
|
+
f"The column {column} was not found in the file header.<br>"
|
|
803
|
+
"The first row of spreadsheet must contain the following labels:<br>"
|
|
804
|
+
"Subject name, Description, Key<br>"
|
|
805
|
+
"The order is not mandatory."
|
|
806
|
+
),
|
|
807
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
808
|
+
QMessageBox.NoButton,
|
|
809
|
+
)
|
|
810
|
+
return 1
|
|
811
|
+
|
|
812
|
+
for _, row in df.iterrows():
|
|
813
|
+
self.twSubjects.setRowCount(self.twSubjects.rowCount() + 1)
|
|
814
|
+
|
|
815
|
+
for idx, field in enumerate(expected_labels):
|
|
816
|
+
self.twSubjects.setItem(
|
|
817
|
+
self.twSubjects.rowCount() - 1,
|
|
818
|
+
idx,
|
|
819
|
+
QTableWidgetItem(str(row[field.upper()]) if str(row[field.upper()]) != "nan" else ""),
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
return 0
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def import_subjects_from_clipboard(self):
|
|
826
|
+
"""
|
|
827
|
+
import subjects from clipboard
|
|
828
|
+
"""
|
|
829
|
+
cb = QApplication.clipboard()
|
|
830
|
+
cb_text = cb.text()
|
|
831
|
+
if not cb_text:
|
|
832
|
+
QMessageBox.warning(
|
|
833
|
+
None,
|
|
834
|
+
cfg.programName,
|
|
835
|
+
"The clipboard is empty",
|
|
836
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
837
|
+
QMessageBox.NoButton,
|
|
838
|
+
)
|
|
839
|
+
return
|
|
840
|
+
|
|
841
|
+
if self.twSubjects.rowCount():
|
|
842
|
+
response = dialog.MessageDialog(
|
|
843
|
+
cfg.programName,
|
|
844
|
+
"Some subjects are already configured. Do you want to append subjects or replace them?",
|
|
845
|
+
[cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
|
|
846
|
+
)
|
|
847
|
+
if response == cfg.CANCEL:
|
|
848
|
+
return
|
|
849
|
+
|
|
850
|
+
if response == cfg.REPLACE:
|
|
851
|
+
self.twSubjects.setRowCount(0)
|
|
852
|
+
|
|
853
|
+
cb_text_splitted = cb_text.split("\n")
|
|
854
|
+
|
|
855
|
+
if len(set([len(x.split("\t")) for x in cb_text_splitted])) != 1:
|
|
856
|
+
QMessageBox.warning(
|
|
857
|
+
None,
|
|
858
|
+
cfg.programName,
|
|
859
|
+
(
|
|
860
|
+
"The clipboard content does not have a constant number of fields.<br>"
|
|
861
|
+
"From your spreadsheet: CTRL + A (select all cells), CTRL + C (copy to clipboard)"
|
|
862
|
+
),
|
|
863
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
864
|
+
QMessageBox.NoButton,
|
|
865
|
+
)
|
|
866
|
+
return
|
|
867
|
+
|
|
868
|
+
for row in cb_text_splitted:
|
|
869
|
+
if set(row.split("\t")) != set([""]):
|
|
870
|
+
subject = {}
|
|
871
|
+
for idx, field in enumerate(row.split("\t")):
|
|
872
|
+
if idx == 0:
|
|
873
|
+
subject["key"] = field.strip() if len(field.strip()) == 1 else ""
|
|
874
|
+
if idx == 1:
|
|
875
|
+
subject[cfg.SUBJECT_NAME] = field.strip()
|
|
876
|
+
if idx == 2:
|
|
877
|
+
subject["description"] = field.strip()
|
|
878
|
+
|
|
879
|
+
self.twSubjects.setRowCount(self.twSubjects.rowCount() + 1)
|
|
880
|
+
|
|
881
|
+
for idx, field_name in enumerate(cfg.subjectsFields):
|
|
882
|
+
item = QTableWidgetItem(subject.get(field_name, ""))
|
|
883
|
+
self.twSubjects.setItem(self.twSubjects.rowCount() - 1, idx, item)
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def import_subjects_from_project(self):
|
|
887
|
+
"""
|
|
888
|
+
import subjects from a BORIS project
|
|
889
|
+
"""
|
|
890
|
+
|
|
891
|
+
file_name, _ = QFileDialog().getOpenFileName(
|
|
892
|
+
self, "Import subjects from project file", "", ("Project files (*.boris *.boris.gz);;All files (*)")
|
|
893
|
+
)
|
|
894
|
+
if not file_name:
|
|
895
|
+
return
|
|
896
|
+
|
|
897
|
+
_, _, project, _ = project_functions.open_project_json(file_name)
|
|
898
|
+
|
|
899
|
+
if "error" in project:
|
|
900
|
+
logging.debug(project["error"])
|
|
901
|
+
QMessageBox.critical(self, cfg.programName, project["error"])
|
|
902
|
+
return
|
|
903
|
+
|
|
904
|
+
# configuration of subjects
|
|
905
|
+
if not (cfg.SUBJECTS in project and project[cfg.SUBJECTS]):
|
|
906
|
+
QMessageBox.warning(self, cfg.programName, "No subjects configuration found in project")
|
|
907
|
+
return
|
|
908
|
+
|
|
909
|
+
if self.twSubjects.rowCount():
|
|
910
|
+
response = dialog.MessageDialog(
|
|
911
|
+
cfg.programName,
|
|
912
|
+
("There are subjects already configured. Do you want to append subjects or replace them?"),
|
|
913
|
+
[cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
if response == cfg.REPLACE:
|
|
917
|
+
self.twSubjects.setRowCount(0)
|
|
918
|
+
|
|
919
|
+
if response == cfg.CANCEL:
|
|
920
|
+
return
|
|
921
|
+
|
|
922
|
+
for idx in util.sorted_keys(project[cfg.SUBJECTS]):
|
|
923
|
+
self.twSubjects.setRowCount(self.twSubjects.rowCount() + 1)
|
|
924
|
+
|
|
925
|
+
for idx2, sbjField in enumerate(cfg.subjectsFields):
|
|
926
|
+
if sbjField in project[cfg.SUBJECTS][idx]:
|
|
927
|
+
self.twSubjects.setItem(
|
|
928
|
+
self.twSubjects.rowCount() - 1,
|
|
929
|
+
idx2,
|
|
930
|
+
QTableWidgetItem(project[cfg.SUBJECTS][idx][sbjField]),
|
|
931
|
+
)
|
|
932
|
+
else:
|
|
933
|
+
self.twSubjects.setItem(self.twSubjects.rowCount() - 1, idx2, QTableWidgetItem(""))
|
|
934
|
+
|
|
935
|
+
self.twSubjects.resizeColumnsToContents()
|
|
936
|
+
|
|
937
|
+
|
|
938
|
+
def import_subjects_from_text_file(self):
|
|
939
|
+
"""
|
|
940
|
+
import subjects from a text file (CSV or TSV)
|
|
941
|
+
"""
|
|
942
|
+
|
|
943
|
+
if self.twSubjects.rowCount():
|
|
944
|
+
response = dialog.MessageDialog(
|
|
945
|
+
cfg.programName,
|
|
946
|
+
("There are subjects already configured. Do you want to append subjects or replace them?"),
|
|
947
|
+
[cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
if response == cfg.CANCEL:
|
|
951
|
+
return
|
|
952
|
+
|
|
953
|
+
file_name, _ = QFileDialog().getOpenFileName(
|
|
954
|
+
self, "Import behaviors from text file (CSV, TSV)", "", "Text files (*.txt *.tsv *.csv);;All files (*)"
|
|
955
|
+
)
|
|
956
|
+
if not file_name:
|
|
957
|
+
return
|
|
958
|
+
|
|
959
|
+
if self.twSubjects.rowCount() and response == cfg.REPLACE:
|
|
960
|
+
self.twSubjects.setRowCount(0)
|
|
961
|
+
|
|
962
|
+
if pl.Path(file_name).suffix.upper() == ".CSV":
|
|
963
|
+
delimiter = ","
|
|
964
|
+
elif pl.Path(file_name).suffix.upper() == ".TSV":
|
|
965
|
+
delimiter = "\t"
|
|
966
|
+
else:
|
|
967
|
+
QMessageBox.warning(
|
|
968
|
+
None,
|
|
969
|
+
cfg.programName,
|
|
970
|
+
("The type of file was not recognized. Must be Comma Separated Values (,) or Tab Separated Values"),
|
|
971
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
972
|
+
QMessageBox.NoButton,
|
|
973
|
+
)
|
|
974
|
+
return
|
|
975
|
+
|
|
976
|
+
try:
|
|
977
|
+
df = pd.read_csv(file_name, delimiter=delimiter)
|
|
978
|
+
except Exception:
|
|
979
|
+
QMessageBox.warning(
|
|
980
|
+
None,
|
|
981
|
+
cfg.programName,
|
|
982
|
+
("The type of file was not recognized. Must be Comma Separated Values (,) or Tab Separated Values"),
|
|
983
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
984
|
+
QMessageBox.NoButton,
|
|
985
|
+
)
|
|
986
|
+
return
|
|
987
|
+
|
|
988
|
+
load_dataframe_into_subjects_tablewidget(self, df)
|
|
989
|
+
|
|
990
|
+
|
|
991
|
+
def import_subjects_from_spreadsheet(self):
|
|
992
|
+
"""
|
|
993
|
+
import subjects from a spreadsheet file (XLSX or ODS)
|
|
994
|
+
"""
|
|
995
|
+
|
|
996
|
+
if self.twSubjects.rowCount():
|
|
997
|
+
response = dialog.MessageDialog(
|
|
998
|
+
cfg.programName,
|
|
999
|
+
("There are subjects already configured. Do you want to append subjects or replace them?"),
|
|
1000
|
+
[cfg.APPEND, cfg.REPLACE, cfg.CANCEL],
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
if response == cfg.CANCEL:
|
|
1004
|
+
return
|
|
1005
|
+
|
|
1006
|
+
file_name, _ = QFileDialog().getOpenFileName(
|
|
1007
|
+
self, "Import subjects from a spreadsheet file", "", "Spreadsheet files (*.xlsx *.ods);;All files (*)"
|
|
1008
|
+
)
|
|
1009
|
+
if not file_name:
|
|
1010
|
+
return
|
|
1011
|
+
|
|
1012
|
+
if self.twSubjects.rowCount() and response == cfg.REPLACE:
|
|
1013
|
+
self.twSubjects.setRowCount(0)
|
|
1014
|
+
|
|
1015
|
+
if pl.Path(file_name).suffix.upper() == ".XLSX":
|
|
1016
|
+
engine = "openpyxl"
|
|
1017
|
+
elif pl.Path(file_name).suffix.upper() == ".ODS":
|
|
1018
|
+
engine = "odf"
|
|
1019
|
+
else:
|
|
1020
|
+
QMessageBox.warning(
|
|
1021
|
+
None,
|
|
1022
|
+
cfg.programName,
|
|
1023
|
+
("The type of file was not recognized. Must be Microsoft-Excel XLSX format or OpenDocument ODS"),
|
|
1024
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
1025
|
+
QMessageBox.NoButton,
|
|
1026
|
+
)
|
|
1027
|
+
return
|
|
1028
|
+
|
|
1029
|
+
try:
|
|
1030
|
+
df = pd.read_excel(file_name, sheet_name=0, engine=engine)
|
|
1031
|
+
except Exception:
|
|
1032
|
+
QMessageBox.warning(
|
|
1033
|
+
None,
|
|
1034
|
+
cfg.programName,
|
|
1035
|
+
("The type of file was not recognized. Must be Microsoft-Excel XLSX format or OpenDocument ODS"),
|
|
1036
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
1037
|
+
QMessageBox.NoButton,
|
|
1038
|
+
)
|
|
1039
|
+
return
|
|
1040
|
+
|
|
1041
|
+
load_dataframe_into_subjects_tablewidget(self, df)
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
def import_indep_variables_from_project(self):
|
|
1045
|
+
"""
|
|
1046
|
+
import independent variables from another project
|
|
1047
|
+
"""
|
|
1048
|
+
|
|
1049
|
+
file_name, _ = QFileDialog().getOpenFileName(
|
|
1050
|
+
self,
|
|
1051
|
+
"Import independent variables from project file",
|
|
1052
|
+
"",
|
|
1053
|
+
("Project files (*.boris *.boris.gz);;All files (*)"),
|
|
1054
|
+
)
|
|
1055
|
+
if not file_name:
|
|
1056
|
+
return
|
|
1057
|
+
|
|
1058
|
+
_, _, project, _ = project_functions.open_project_json(file_name)
|
|
1059
|
+
|
|
1060
|
+
if "error" in project:
|
|
1061
|
+
logging.debug(project["error"])
|
|
1062
|
+
QMessageBox.critical(self, cfg.programName, project["error"])
|
|
1063
|
+
return
|
|
1064
|
+
|
|
1065
|
+
# independent variables
|
|
1066
|
+
if not (cfg.INDEPENDENT_VARIABLES in project and project[cfg.INDEPENDENT_VARIABLES]):
|
|
1067
|
+
QMessageBox.warning(self, cfg.programName, "No independent variables found in project")
|
|
1068
|
+
return
|
|
1069
|
+
|
|
1070
|
+
# check if variables are already present
|
|
1071
|
+
existing_var = []
|
|
1072
|
+
|
|
1073
|
+
for r in range(self.twVariables.rowCount()):
|
|
1074
|
+
existing_var.append(self.twVariables.item(r, 0).text().strip().upper())
|
|
1075
|
+
|
|
1076
|
+
for i in util.sorted_keys(project[cfg.INDEPENDENT_VARIABLES]):
|
|
1077
|
+
self.twVariables.setRowCount(self.twVariables.rowCount() + 1)
|
|
1078
|
+
flag_renamed = False
|
|
1079
|
+
for idx, field in enumerate(cfg.tw_indVarFields):
|
|
1080
|
+
item = QTableWidgetItem()
|
|
1081
|
+
if field in project[cfg.INDEPENDENT_VARIABLES][i]:
|
|
1082
|
+
if field == "label":
|
|
1083
|
+
txt = project[cfg.INDEPENDENT_VARIABLES][i]["label"].strip()
|
|
1084
|
+
while txt.upper() in existing_var:
|
|
1085
|
+
txt += "_2"
|
|
1086
|
+
flag_renamed = True
|
|
1087
|
+
else:
|
|
1088
|
+
txt = project[cfg.INDEPENDENT_VARIABLES][i][field].strip()
|
|
1089
|
+
item.setText(txt)
|
|
1090
|
+
else:
|
|
1091
|
+
item.setText("")
|
|
1092
|
+
self.twVariables.setItem(self.twVariables.rowCount() - 1, idx, item)
|
|
1093
|
+
|
|
1094
|
+
self.twVariables.resizeColumnsToContents()
|
|
1095
|
+
if flag_renamed:
|
|
1096
|
+
QMessageBox.information(self, cfg.programName, "Some variables already present were renamed")
|