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.
- 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,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BORIS
|
|
3
|
+
Behavioral Observation Research Interactive Software
|
|
4
|
+
Copyright 2012-2025 Olivier Friard
|
|
5
|
+
|
|
6
|
+
This program is free software; you can redistribute it and/or modify
|
|
7
|
+
it under the terms of the GNU General Public License as published by
|
|
8
|
+
the Free Software Foundation; either version 2 of the License, or
|
|
9
|
+
(at your option) any later version.
|
|
10
|
+
|
|
11
|
+
This program is distributed in the hope that it will be useful,
|
|
12
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
13
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
14
|
+
GNU General Public License for more details.
|
|
15
|
+
|
|
16
|
+
You should have received a copy of the GNU General Public License
|
|
17
|
+
along with this program; if not, write to the Free Software
|
|
18
|
+
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
19
|
+
MA 02110-1301, USA.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import pathlib
|
|
24
|
+
from decimal import Decimal as dec
|
|
25
|
+
|
|
26
|
+
import tablib
|
|
27
|
+
from PySide6.QtWidgets import QFileDialog, QInputDialog, QMessageBox
|
|
28
|
+
|
|
29
|
+
from . import observation_operations
|
|
30
|
+
|
|
31
|
+
from . import dialog
|
|
32
|
+
from . import project_functions
|
|
33
|
+
from . import select_observations
|
|
34
|
+
from . import utilities as util
|
|
35
|
+
from . import config as cfg
|
|
36
|
+
from . import select_subj_behav
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def create_behavior_binary_table(pj: dict, selected_observations: list, parameters_obs: dict, time_interval: float) -> dict:
|
|
40
|
+
"""
|
|
41
|
+
create behavior binary table
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
pj (dict): project dictionary
|
|
45
|
+
selected_observations (list): list of selected observations
|
|
46
|
+
parameters_obs (dict): dcit of parameters
|
|
47
|
+
time_interval (float): time interval (in seconds)
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
dict: dictionary of tablib dataset
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
results_df = {}
|
|
55
|
+
|
|
56
|
+
state_behavior_codes = [x for x in util.state_behavior_codes(pj[cfg.ETHOGRAM]) if x in parameters_obs[cfg.SELECTED_BEHAVIORS]]
|
|
57
|
+
point_behavior_codes = [x for x in util.point_behavior_codes(pj[cfg.ETHOGRAM]) if x in parameters_obs[cfg.SELECTED_BEHAVIORS]]
|
|
58
|
+
if not state_behavior_codes and not point_behavior_codes:
|
|
59
|
+
return {"error": True, "msg": "No events selected"}
|
|
60
|
+
|
|
61
|
+
for obs_id in selected_observations:
|
|
62
|
+
start_time = parameters_obs[cfg.START_TIME]
|
|
63
|
+
end_time = parameters_obs[cfg.END_TIME]
|
|
64
|
+
|
|
65
|
+
# check observation interval
|
|
66
|
+
if parameters_obs["time"] == cfg.TIME_FULL_OBS:
|
|
67
|
+
max_obs_length, _ = observation_operations.observation_length(pj, [obs_id])
|
|
68
|
+
start_time = dec("0.000")
|
|
69
|
+
end_time = dec(max_obs_length)
|
|
70
|
+
|
|
71
|
+
if parameters_obs["time"] == cfg.TIME_EVENTS:
|
|
72
|
+
try:
|
|
73
|
+
start_time = dec(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][0][0])
|
|
74
|
+
except Exception:
|
|
75
|
+
start_time = dec("0.000")
|
|
76
|
+
try:
|
|
77
|
+
end_time = dec(pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS][-1][0])
|
|
78
|
+
except Exception:
|
|
79
|
+
max_obs_length, _ = observation_operations.observation_length(pj, [obs_id])
|
|
80
|
+
end_time = dec(max_obs_length)
|
|
81
|
+
|
|
82
|
+
if parameters_obs["time"] == cfg.TIME_OBS_INTERVAL:
|
|
83
|
+
obs_interval = pj[cfg.OBSERVATIONS][obs_id].get(cfg.OBSERVATION_TIME_INTERVAL, [0, 0])
|
|
84
|
+
offset = pj[cfg.OBSERVATIONS][obs_id][cfg.TIME_OFFSET]
|
|
85
|
+
start_time = dec(obs_interval[0]) + offset
|
|
86
|
+
# Use max observation length for end time if no interval is defined (=0)
|
|
87
|
+
max_obs_length, _ = observation_operations.observation_length(pj, [obs_id])
|
|
88
|
+
end_time = dec(obs_interval[1]) + offset if obs_interval[1] not in (0, None) else dec(max_obs_length)
|
|
89
|
+
|
|
90
|
+
if obs_id not in results_df:
|
|
91
|
+
results_df[obs_id] = {}
|
|
92
|
+
|
|
93
|
+
for subject in parameters_obs[cfg.SELECTED_SUBJECTS]:
|
|
94
|
+
# extract tuple (behavior, modifier)
|
|
95
|
+
behav_modif_list = [
|
|
96
|
+
(idx[2], idx[3])
|
|
97
|
+
for idx in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]
|
|
98
|
+
if idx[1] == (subject if subject != cfg.NO_FOCAL_SUBJECT else "") and idx[2] in parameters_obs[cfg.SELECTED_BEHAVIORS]
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
# extract observed subjects NOT USED at the moment
|
|
102
|
+
"""observed_subjects = [
|
|
103
|
+
event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS]
|
|
104
|
+
]"""
|
|
105
|
+
|
|
106
|
+
# add selected behavior if not found in (behavior, modifier)
|
|
107
|
+
if not parameters_obs[cfg.EXCLUDE_BEHAVIORS]:
|
|
108
|
+
# for behav in state_behavior_codes:
|
|
109
|
+
for behav in parameters_obs[cfg.SELECTED_BEHAVIORS]:
|
|
110
|
+
if behav not in [x[0] for x in behav_modif_list]:
|
|
111
|
+
behav_modif_list.append((behav, ""))
|
|
112
|
+
|
|
113
|
+
behav_modif_set = set(behav_modif_list)
|
|
114
|
+
observed_behav = [(x[0], x[1]) for x in sorted(behav_modif_set)]
|
|
115
|
+
if parameters_obs[cfg.INCLUDE_MODIFIERS]:
|
|
116
|
+
results_df[obs_id][subject] = tablib.Dataset(
|
|
117
|
+
headers=["time"] + [f"{x[0]}" + f" ({x[1]})" * (x[1] != "") for x in sorted(behav_modif_set)]
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
results_df[obs_id][subject] = tablib.Dataset(headers=["time"] + [x[0] for x in sorted(behav_modif_set)])
|
|
121
|
+
|
|
122
|
+
if subject == cfg.NO_FOCAL_SUBJECT:
|
|
123
|
+
sel_subject_dict = {"": {cfg.SUBJECT_NAME: ""}}
|
|
124
|
+
else:
|
|
125
|
+
sel_subject_dict = dict(
|
|
126
|
+
[(idx, pj[cfg.SUBJECTS][idx]) for idx in pj[cfg.SUBJECTS] if pj[cfg.SUBJECTS][idx][cfg.SUBJECT_NAME] == subject]
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
row_idx = 0
|
|
130
|
+
t = start_time
|
|
131
|
+
while t <= end_time:
|
|
132
|
+
# state events
|
|
133
|
+
current_states = util.get_current_states_modifiers_by_subject_2(
|
|
134
|
+
state_behavior_codes, pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS], sel_subject_dict, t
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# point events
|
|
138
|
+
current_point = util.get_current_points_by_subject(
|
|
139
|
+
point_behavior_codes, pj[cfg.OBSERVATIONS][obs_id][cfg.EVENTS], sel_subject_dict, t, time_interval
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
cols = [float(t)] # time
|
|
143
|
+
|
|
144
|
+
for behav in observed_behav:
|
|
145
|
+
if behav[0] in state_behavior_codes:
|
|
146
|
+
cols.append(int(behav in current_states[list(current_states.keys())[0]]))
|
|
147
|
+
|
|
148
|
+
if behav[0] in point_behavior_codes:
|
|
149
|
+
cols.append(current_point[list(current_point.keys())[0]].count(behav))
|
|
150
|
+
|
|
151
|
+
results_df[obs_id][subject].append(cols)
|
|
152
|
+
|
|
153
|
+
t += time_interval
|
|
154
|
+
row_idx += 1
|
|
155
|
+
|
|
156
|
+
return results_df
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def behavior_binary_table(self):
|
|
160
|
+
"""
|
|
161
|
+
ask user for parameters for behavior binary table
|
|
162
|
+
call create_behavior_binary_table
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
QMessageBox.warning(
|
|
166
|
+
None,
|
|
167
|
+
cfg.programName,
|
|
168
|
+
(
|
|
169
|
+
"Depending on the length of yours observations "
|
|
170
|
+
"the execution of this function may take a long time.<br>"
|
|
171
|
+
"The program interface may freeze, be patient. <br>"
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
_, selected_observations = select_observations.select_observations2(
|
|
176
|
+
self, cfg.MULTIPLE, "Select observations for the behavior binary table"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if not selected_observations:
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
# check if coded behaviors are defined in ethogram
|
|
183
|
+
if project_functions.check_coded_behaviors_in_obs_list(self.pj, selected_observations):
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
# check if state events are paired
|
|
187
|
+
not_ok, selected_observations = project_functions.check_state_events(self.pj, selected_observations)
|
|
188
|
+
if not_ok or not selected_observations:
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
"""
|
|
192
|
+
max_obs_length, _ = observation_operations.observation_length(self.pj, selected_observations)
|
|
193
|
+
if max_obs_length == dec(-1): # media length not available, user choose to not use events
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
# exit with message if events do not have timestamp
|
|
197
|
+
if max_obs_length.is_nan():
|
|
198
|
+
QMessageBox.critical(
|
|
199
|
+
None,
|
|
200
|
+
cfg.programName,
|
|
201
|
+
("This function is not available for observations with events that do not have timestamp"),
|
|
202
|
+
QMessageBox.Ok | QMessageBox.Default,
|
|
203
|
+
QMessageBox.NoButton,
|
|
204
|
+
)
|
|
205
|
+
return
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
max_media_duration_all_obs, _ = observation_operations.media_duration(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
209
|
+
|
|
210
|
+
start_coding, end_coding, _ = observation_operations.coding_time(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
211
|
+
|
|
212
|
+
start_interval, end_interval = observation_operations.time_intervals_range(self.pj[cfg.OBSERVATIONS], selected_observations)
|
|
213
|
+
|
|
214
|
+
parameters = select_subj_behav.choose_obs_subj_behav_category(
|
|
215
|
+
self,
|
|
216
|
+
selected_observations,
|
|
217
|
+
start_coding=start_coding,
|
|
218
|
+
end_coding=end_coding,
|
|
219
|
+
start_interval=start_interval,
|
|
220
|
+
end_interval=end_interval,
|
|
221
|
+
maxTime=max_media_duration_all_obs,
|
|
222
|
+
show_include_modifiers=True,
|
|
223
|
+
show_exclude_non_coded_behaviors=True,
|
|
224
|
+
by_category=False,
|
|
225
|
+
n_observations=len(selected_observations),
|
|
226
|
+
)
|
|
227
|
+
if not parameters:
|
|
228
|
+
return
|
|
229
|
+
if not parameters[cfg.SELECTED_SUBJECTS] or not parameters[cfg.SELECTED_BEHAVIORS]:
|
|
230
|
+
QMessageBox.warning(None, cfg.programName, "Select subject(s) and behavior(s) to analyze")
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
# ask for time interval
|
|
234
|
+
i, ok = QInputDialog.getDouble(None, "Behavior binary table", "Time interval (in seconds):", 1.0, 0.001, 86400, 3)
|
|
235
|
+
if not ok:
|
|
236
|
+
return
|
|
237
|
+
time_interval = util.float2decimal(i)
|
|
238
|
+
|
|
239
|
+
results_df = create_behavior_binary_table(self.pj, selected_observations, parameters, time_interval)
|
|
240
|
+
|
|
241
|
+
if "error" in results_df:
|
|
242
|
+
QMessageBox.warning(None, cfg.programName, results_df["msg"])
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
# save results
|
|
246
|
+
file_formats = [cfg.TSV, cfg.CSV, cfg.ODS, cfg.XLSX, cfg.XLS, cfg.HTML]
|
|
247
|
+
|
|
248
|
+
if len(selected_observations) == 1:
|
|
249
|
+
file_name, filter_ = QFileDialog().getSaveFileName(None, "Save results", "", ";;".join(file_formats))
|
|
250
|
+
if not file_name:
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
output_format = cfg.FILE_NAME_SUFFIX[filter_]
|
|
254
|
+
|
|
255
|
+
if pathlib.Path(file_name).suffix != "." + output_format:
|
|
256
|
+
file_name = str(pathlib.Path(file_name)) + "." + output_format
|
|
257
|
+
# check if file with new extension already exists
|
|
258
|
+
if pathlib.Path(file_name).is_file():
|
|
259
|
+
if (
|
|
260
|
+
dialog.MessageDialog(cfg.programName, f"The file {file_name} already exists.", [cfg.CANCEL, cfg.OVERWRITE])
|
|
261
|
+
== cfg.CANCEL
|
|
262
|
+
):
|
|
263
|
+
return
|
|
264
|
+
else:
|
|
265
|
+
item, ok = QInputDialog.getItem(None, "Save results", "Available formats", file_formats, 0, False)
|
|
266
|
+
if not ok:
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
output_format = cfg.FILE_NAME_SUFFIX[item]
|
|
270
|
+
|
|
271
|
+
export_dir = QFileDialog().getExistingDirectory(
|
|
272
|
+
None, "Choose a directory to save results", os.path.expanduser("~"), options=QFileDialog.ShowDirsOnly
|
|
273
|
+
)
|
|
274
|
+
if not export_dir:
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
mem_command = ""
|
|
278
|
+
for obs_id in results_df:
|
|
279
|
+
for subject in results_df[obs_id]:
|
|
280
|
+
if len(selected_observations) > 1:
|
|
281
|
+
file_name_with_subject = str(pathlib.Path(export_dir) / util.safeFileName(obs_id + "_" + subject)) + "." + output_format
|
|
282
|
+
else:
|
|
283
|
+
file_name_with_subject = str(os.path.splitext(file_name)[0] + util.safeFileName("_" + subject)) + "." + output_format
|
|
284
|
+
|
|
285
|
+
# check if file with new extension already exists
|
|
286
|
+
if mem_command != cfg.OVERWRITE_ALL and pathlib.Path(file_name_with_subject).is_file():
|
|
287
|
+
if mem_command == "Skip all":
|
|
288
|
+
continue
|
|
289
|
+
mem_command = dialog.MessageDialog(
|
|
290
|
+
cfg.programName,
|
|
291
|
+
f"The file {file_name_with_subject} already exists.",
|
|
292
|
+
[cfg.OVERWRITE, cfg.OVERWRITE_ALL, "Skip", "Skip all", cfg.CANCEL],
|
|
293
|
+
)
|
|
294
|
+
if mem_command == cfg.CANCEL:
|
|
295
|
+
return
|
|
296
|
+
if mem_command in ["Skip", "Skip all"]:
|
|
297
|
+
continue
|
|
298
|
+
|
|
299
|
+
if output_format in [cfg.CSV_EXT, cfg.TSV_EXT, cfg.HTML]:
|
|
300
|
+
with open(file_name_with_subject, "wb") as f:
|
|
301
|
+
f.write(str.encode(results_df[obs_id][subject].export(output_format)))
|
|
302
|
+
|
|
303
|
+
if output_format in [cfg.ODS_EXT, cfg.XLSX_EXT, cfg.XLS_EXT]:
|
|
304
|
+
with open(file_name_with_subject, "wb") as f:
|
|
305
|
+
f.write(results_df[obs_id][subject].export(output_format))
|
|
@@ -0,0 +1,239 @@
|
|
|
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 json
|
|
24
|
+
import binascii
|
|
25
|
+
|
|
26
|
+
from PySide6.QtGui import QMouseEvent, QPixmap, QPolygonF, QColor, QBrush, QPen
|
|
27
|
+
from PySide6.QtCore import Qt, Signal, QEvent, QPoint
|
|
28
|
+
from PySide6.QtWidgets import (
|
|
29
|
+
QLabel,
|
|
30
|
+
QHBoxLayout,
|
|
31
|
+
QGraphicsView,
|
|
32
|
+
QGraphicsScene,
|
|
33
|
+
QWidget,
|
|
34
|
+
QVBoxLayout,
|
|
35
|
+
QLineEdit,
|
|
36
|
+
QPushButton,
|
|
37
|
+
QGraphicsPixmapItem,
|
|
38
|
+
QGraphicsPolygonItem,
|
|
39
|
+
QMessageBox,
|
|
40
|
+
QInputDialog,
|
|
41
|
+
QApplication,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
from . import config as cfg
|
|
45
|
+
|
|
46
|
+
codeSeparator: str = ","
|
|
47
|
+
penWidth: int = 0
|
|
48
|
+
penStyle = Qt.NoPen
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class BehaviorsCodingMapWindowClass(QWidget):
|
|
52
|
+
class View(QGraphicsView):
|
|
53
|
+
mousePress = Signal(QMouseEvent)
|
|
54
|
+
mouseMove = Signal(QMouseEvent)
|
|
55
|
+
|
|
56
|
+
def eventFilter(self, source, event):
|
|
57
|
+
if event.type() == QEvent.MouseMove:
|
|
58
|
+
self.mouseMove.emit(event)
|
|
59
|
+
|
|
60
|
+
if event.type() == QEvent.MouseButtonPress:
|
|
61
|
+
self.mousePress.emit(event)
|
|
62
|
+
|
|
63
|
+
return QWidget.eventFilter(self, source, event)
|
|
64
|
+
|
|
65
|
+
elList, points = [], []
|
|
66
|
+
|
|
67
|
+
def __init__(self, parent):
|
|
68
|
+
QGraphicsView.__init__(self, parent)
|
|
69
|
+
self.setScene(QGraphicsScene(self))
|
|
70
|
+
self.scene().update()
|
|
71
|
+
|
|
72
|
+
self.viewport().installEventFilter(self)
|
|
73
|
+
self.setMouseTracking(True)
|
|
74
|
+
|
|
75
|
+
clickSignal = Signal(str, list) # click signal to be sent to mainwindow
|
|
76
|
+
keypressSignal = Signal(QEvent)
|
|
77
|
+
close_signal = Signal(str)
|
|
78
|
+
|
|
79
|
+
def __init__(self, behaviors_coding_map, idx=0):
|
|
80
|
+
super(BehaviorsCodingMapWindowClass, self).__init__()
|
|
81
|
+
|
|
82
|
+
self.polygonsList2 = []
|
|
83
|
+
|
|
84
|
+
self.installEventFilter(self)
|
|
85
|
+
|
|
86
|
+
self.codingMap = behaviors_coding_map
|
|
87
|
+
self.idx = idx
|
|
88
|
+
|
|
89
|
+
self.setWindowTitle(f"Behaviors coding map: {self.codingMap['name']}")
|
|
90
|
+
Vlayout = QVBoxLayout()
|
|
91
|
+
|
|
92
|
+
self.view = self.View(self)
|
|
93
|
+
self.view.mousePress.connect(self.viewMousePressEvent)
|
|
94
|
+
self.view.mouseMove.connect(self.mouse_move_event)
|
|
95
|
+
|
|
96
|
+
Vlayout.addWidget(self.view)
|
|
97
|
+
|
|
98
|
+
hBoxLayout1 = QHBoxLayout()
|
|
99
|
+
|
|
100
|
+
self.label = QLabel("Behavior(s)")
|
|
101
|
+
hBoxLayout1.addWidget(self.label)
|
|
102
|
+
|
|
103
|
+
self.leareaCode = QLineEdit(self)
|
|
104
|
+
hBoxLayout1.addWidget(self.leareaCode)
|
|
105
|
+
|
|
106
|
+
self.btClose = QPushButton(cfg.CLOSE)
|
|
107
|
+
self.btClose.clicked.connect(self.close)
|
|
108
|
+
hBoxLayout1.addWidget(self.btClose)
|
|
109
|
+
|
|
110
|
+
Vlayout.addLayout(hBoxLayout1)
|
|
111
|
+
|
|
112
|
+
self.setLayout(Vlayout)
|
|
113
|
+
|
|
114
|
+
self.loadMap()
|
|
115
|
+
|
|
116
|
+
def closeEvent(self, event):
|
|
117
|
+
self.close_signal.emit(self.codingMap["name"])
|
|
118
|
+
event.accept()
|
|
119
|
+
|
|
120
|
+
def eventFilter(self, receiver, event):
|
|
121
|
+
"""
|
|
122
|
+
send event (if keypress) to main window
|
|
123
|
+
"""
|
|
124
|
+
if event.type() == QEvent.KeyPress:
|
|
125
|
+
self.keypressSignal.emit(event)
|
|
126
|
+
return True
|
|
127
|
+
else:
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
def mouse_move_event(self, event):
|
|
131
|
+
"""
|
|
132
|
+
display behavior under mouse position
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
self.leareaCode.clear()
|
|
136
|
+
codes = []
|
|
137
|
+
test = self.view.mapToScene(event.pos()).toPoint()
|
|
138
|
+
for areaCode, pg in self.polygonsList2:
|
|
139
|
+
if pg.contains(test):
|
|
140
|
+
codes.append(areaCode)
|
|
141
|
+
self.leareaCode.setText(", ".join(codes))
|
|
142
|
+
|
|
143
|
+
def viewMousePressEvent(self, event) -> None:
|
|
144
|
+
"""
|
|
145
|
+
insert clicked areas codes
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
test = self.view.mapToScene(event.pos()).toPoint()
|
|
149
|
+
to_be_sent: list = []
|
|
150
|
+
|
|
151
|
+
for areaCode, pg in self.polygonsList2:
|
|
152
|
+
if pg.contains(test):
|
|
153
|
+
to_be_sent.append(areaCode)
|
|
154
|
+
|
|
155
|
+
if to_be_sent:
|
|
156
|
+
self.clickSignal.emit(self.codingMap["name"], to_be_sent)
|
|
157
|
+
|
|
158
|
+
def loadMap(self):
|
|
159
|
+
"""
|
|
160
|
+
load bitmap from data
|
|
161
|
+
show it in view scene
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
pixmap = QPixmap()
|
|
165
|
+
pixmap.loadFromData(binascii.a2b_base64(self.codingMap["bitmap"]))
|
|
166
|
+
|
|
167
|
+
self.view.setSceneRect(0, 0, pixmap.size().width(), pixmap.size().height())
|
|
168
|
+
pixItem = QGraphicsPixmapItem(pixmap)
|
|
169
|
+
pixItem.setPos(0, 0)
|
|
170
|
+
self.view.scene().addItem(pixItem)
|
|
171
|
+
|
|
172
|
+
for key in self.codingMap["areas"]:
|
|
173
|
+
areaCode = self.codingMap["areas"][key]["code"]
|
|
174
|
+
points = self.codingMap["areas"][key]["geometry"]
|
|
175
|
+
|
|
176
|
+
newPolygon = QPolygonF()
|
|
177
|
+
for p in points:
|
|
178
|
+
newPolygon.append(QPoint(p[0], p[1]))
|
|
179
|
+
|
|
180
|
+
# draw polygon
|
|
181
|
+
polygon = QGraphicsPolygonItem()
|
|
182
|
+
polygon.setPolygon(newPolygon)
|
|
183
|
+
clr = QColor()
|
|
184
|
+
clr.setRgba(self.codingMap["areas"][key]["color"])
|
|
185
|
+
polygon.setPen(QPen(clr, penWidth, penStyle, Qt.RoundCap, Qt.RoundJoin))
|
|
186
|
+
polygon.setBrush(QBrush(clr, Qt.SolidPattern))
|
|
187
|
+
|
|
188
|
+
self.view.scene().addItem(polygon)
|
|
189
|
+
|
|
190
|
+
self.polygonsList2.append([areaCode, polygon])
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def show_behaviors_coding_map(self):
|
|
194
|
+
"""
|
|
195
|
+
show a behavior coding map
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
if cfg.BEHAVIORS_CODING_MAP not in self.pj or not self.pj[cfg.BEHAVIORS_CODING_MAP]:
|
|
199
|
+
QMessageBox.warning(self, cfg.programName, "No behaviors coding map found in current project")
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
items = [x["name"] for x in self.pj[cfg.BEHAVIORS_CODING_MAP]]
|
|
203
|
+
if len(items) == 1:
|
|
204
|
+
coding_map_name = items[0]
|
|
205
|
+
else:
|
|
206
|
+
item, ok = QInputDialog.getItem(self, "Select a coding map", "list of coding maps", items, 0, False)
|
|
207
|
+
if ok and item:
|
|
208
|
+
coding_map_name = item
|
|
209
|
+
else:
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
if self.bcm_dict.get(coding_map_name, None) is not None:
|
|
213
|
+
# if coding_map_name in self.bcm_dict and :
|
|
214
|
+
self.bcm_dict[coding_map_name].show()
|
|
215
|
+
else:
|
|
216
|
+
self.bcm_dict[coding_map_name] = BehaviorsCodingMapWindowClass(
|
|
217
|
+
self.pj[cfg.BEHAVIORS_CODING_MAP][items.index(coding_map_name)], idx=items.index(coding_map_name)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
self.bcm_dict[coding_map_name].clickSignal.connect(self.click_signal_from_behaviors_coding_map)
|
|
221
|
+
|
|
222
|
+
# self.bcm_dict[coding_map_name].close_signal.connect(self.close_behaviors_coding_map)
|
|
223
|
+
|
|
224
|
+
self.bcm_dict[coding_map_name].resize(cfg.CODING_MAP_RESIZE_W, cfg.CODING_MAP_RESIZE_W)
|
|
225
|
+
self.bcm_dict[coding_map_name].setWindowFlags(Qt.WindowStaysOnTopHint)
|
|
226
|
+
self.bcm_dict[coding_map_name].show()
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
import sys
|
|
231
|
+
|
|
232
|
+
app = QApplication(sys.argv)
|
|
233
|
+
|
|
234
|
+
if len(sys.argv) > 1:
|
|
235
|
+
cm = json.loads(open(sys.argv[1]).read())
|
|
236
|
+
codingMapWindow = BehaviorsCodingMapWindowClass(cm)
|
|
237
|
+
codingMapWindow.resize(cfg.CODING_MAP_RESIZE_W, cfg.CODING_MAP_RESIZE_H)
|
|
238
|
+
codingMapWindow.show()
|
|
239
|
+
sys.exit(app.exec_())
|