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.
Files changed (109) hide show
  1. boris/__init__.py +26 -0
  2. boris/__main__.py +25 -0
  3. boris/about.py +143 -0
  4. boris/add_modifier.py +635 -0
  5. boris/add_modifier_ui.py +303 -0
  6. boris/advanced_event_filtering.py +455 -0
  7. boris/analysis_plugins/__init__.py +0 -0
  8. boris/analysis_plugins/_latency.py +59 -0
  9. boris/analysis_plugins/irr_cohen_kappa.py +109 -0
  10. boris/analysis_plugins/irr_cohen_kappa_with_modifiers.py +112 -0
  11. boris/analysis_plugins/irr_weighted_cohen_kappa.py +157 -0
  12. boris/analysis_plugins/irr_weighted_cohen_kappa_with_modifiers.py +162 -0
  13. boris/analysis_plugins/list_of_dataframe_columns.py +22 -0
  14. boris/analysis_plugins/number_of_occurences.py +22 -0
  15. boris/analysis_plugins/number_of_occurences_by_independent_variable.py +54 -0
  16. boris/analysis_plugins/time_budget.py +61 -0
  17. boris/behav_coding_map_creator.py +1110 -0
  18. boris/behavior_binary_table.py +305 -0
  19. boris/behaviors_coding_map.py +239 -0
  20. boris/boris_cli.py +340 -0
  21. boris/cmd_arguments.py +49 -0
  22. boris/coding_pad.py +280 -0
  23. boris/config.py +785 -0
  24. boris/config_file.py +356 -0
  25. boris/connections.py +409 -0
  26. boris/converters.py +333 -0
  27. boris/converters_ui.py +225 -0
  28. boris/cooccurence.py +250 -0
  29. boris/core.py +5901 -0
  30. boris/core_qrc.py +15958 -0
  31. boris/core_ui.py +1107 -0
  32. boris/db_functions.py +324 -0
  33. boris/dev.py +134 -0
  34. boris/dialog.py +1108 -0
  35. boris/duration_widget.py +238 -0
  36. boris/edit_event.py +245 -0
  37. boris/edit_event_ui.py +233 -0
  38. boris/event_operations.py +1040 -0
  39. boris/events_cursor.py +61 -0
  40. boris/events_snapshots.py +596 -0
  41. boris/exclusion_matrix.py +141 -0
  42. boris/export_events.py +1006 -0
  43. boris/export_observation.py +1203 -0
  44. boris/external_processes.py +332 -0
  45. boris/geometric_measurement.py +941 -0
  46. boris/gui_utilities.py +135 -0
  47. boris/image_overlay.py +72 -0
  48. boris/import_observations.py +242 -0
  49. boris/ipc_mpv.py +325 -0
  50. boris/irr.py +634 -0
  51. boris/latency.py +244 -0
  52. boris/measurement_widget.py +161 -0
  53. boris/media_file.py +115 -0
  54. boris/menu_options.py +213 -0
  55. boris/modifier_coding_map_creator.py +1013 -0
  56. boris/modifiers_coding_map.py +157 -0
  57. boris/mpv.py +2016 -0
  58. boris/mpv2.py +2193 -0
  59. boris/observation.py +1453 -0
  60. boris/observation_operations.py +2538 -0
  61. boris/observation_ui.py +679 -0
  62. boris/observations_list.py +337 -0
  63. boris/otx_parser.py +442 -0
  64. boris/param_panel.py +201 -0
  65. boris/param_panel_ui.py +305 -0
  66. boris/player_dock_widget.py +198 -0
  67. boris/plot_data_module.py +536 -0
  68. boris/plot_events.py +634 -0
  69. boris/plot_events_rt.py +237 -0
  70. boris/plot_spectrogram_rt.py +316 -0
  71. boris/plot_waveform_rt.py +230 -0
  72. boris/plugins.py +431 -0
  73. boris/portion/__init__.py +31 -0
  74. boris/portion/const.py +95 -0
  75. boris/portion/dict.py +365 -0
  76. boris/portion/func.py +52 -0
  77. boris/portion/interval.py +581 -0
  78. boris/portion/io.py +181 -0
  79. boris/preferences.py +510 -0
  80. boris/preferences_ui.py +770 -0
  81. boris/project.py +2007 -0
  82. boris/project_functions.py +2041 -0
  83. boris/project_import_export.py +1096 -0
  84. boris/project_ui.py +794 -0
  85. boris/qrc_boris.py +10389 -0
  86. boris/qrc_boris5.py +2579 -0
  87. boris/select_modifiers.py +312 -0
  88. boris/select_observations.py +210 -0
  89. boris/select_subj_behav.py +286 -0
  90. boris/state_events.py +197 -0
  91. boris/subjects_pad.py +106 -0
  92. boris/synthetic_time_budget.py +290 -0
  93. boris/time_budget_functions.py +1136 -0
  94. boris/time_budget_widget.py +1039 -0
  95. boris/transitions.py +365 -0
  96. boris/utilities.py +1810 -0
  97. boris/version.py +24 -0
  98. boris/video_equalizer.py +159 -0
  99. boris/video_equalizer_ui.py +248 -0
  100. boris/video_operations.py +310 -0
  101. boris/view_df.py +104 -0
  102. boris/view_df_ui.py +75 -0
  103. boris/write_event.py +538 -0
  104. boris_behav_obs-9.7.7.dist-info/METADATA +139 -0
  105. boris_behav_obs-9.7.7.dist-info/RECORD +109 -0
  106. boris_behav_obs-9.7.7.dist-info/WHEEL +5 -0
  107. boris_behav_obs-9.7.7.dist-info/entry_points.txt +2 -0
  108. boris_behav_obs-9.7.7.dist-info/licenses/LICENSE.TXT +674 -0
  109. boris_behav_obs-9.7.7.dist-info/top_level.txt +1 -0
boris/otx_parser.py ADDED
@@ -0,0 +1,442 @@
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
+ Parse an OTX/ODX or OTB (compressed OTX) file and convert the ethogram, modifiers, subjects
23
+ and independent variables to BORIS format
24
+
25
+ """
26
+
27
+ import datetime as dt
28
+ from decimal import Decimal as dec
29
+ import re
30
+ import zipfile
31
+ import pathlib as pl
32
+ from xml.dom import minidom
33
+ import logging
34
+ from typing import Tuple
35
+
36
+ try:
37
+ from . import config as cfg
38
+ except Exception:
39
+ import config as cfg
40
+
41
+
42
+ def otx_to_boris(file_path: str) -> Tuple[dict, list]:
43
+ """
44
+ convert otx/otb/odx file in a BORIS project
45
+
46
+ For ODX files ask to import observations
47
+
48
+ Args:
49
+ file_path (str): path to otx/otb/odx file
50
+
51
+ Returns:
52
+ dict: BORIS project
53
+ list: list of errors during importation
54
+ """
55
+
56
+ if pl.Path(file_path).suffix == ".otb":
57
+ with zipfile.ZipFile(file_path) as file_zip:
58
+ files_list = file_zip.namelist()
59
+ if files_list:
60
+ try:
61
+ file_zip.extract(files_list[0])
62
+ except Exception:
63
+ return {"fatal": True}, ["Error when extracting file from OTB"]
64
+ else:
65
+ return {"fatal": True}, ["Error when extracting file"]
66
+
67
+ try:
68
+ xmldoc = minidom.parse(files_list[0])
69
+ except Exception:
70
+ return {"fatal": True}, ["XML parsing error"]
71
+
72
+ elif pl.Path(file_path).suffix in (".odx", ".otx"):
73
+ try:
74
+ xmldoc = minidom.parse(file_path)
75
+ except Exception:
76
+ return {"fatal": True}, ["XML parsing error"]
77
+
78
+ else:
79
+ return {"fatal": True}, ["The file must be in OTB, OTX or ODX format"]
80
+
81
+ flag_long_key: bool = False
82
+ error_list: list = []
83
+
84
+ # metadata
85
+ for item in xmldoc.getElementsByTagName("MET_METADATA"):
86
+ metadata = minidom.parseString(item.toxml())
87
+ try:
88
+ project_name = re.sub("<[^>]*>", "", metadata.getElementsByTagName("MET_PROJECT_NAME")[0].toxml())
89
+ except Exception:
90
+ project_name = ""
91
+ try:
92
+ project_description = re.sub("<[^>]*>", "", metadata.getElementsByTagName("MET_PROJECT_DESCRIPTION")[0].toxml())
93
+ except Exception:
94
+ project_description = ""
95
+
96
+ try:
97
+ project_creation_date = re.sub("<[^>]*>", "", metadata.getElementsByTagName("MET_CREATION_DATETIME")[0].toxml())
98
+ except Exception:
99
+ project_creation_date = ""
100
+
101
+ # modifiers
102
+ modifiers: dict = {}
103
+ # modifiers_set = {}
104
+ itemlist = xmldoc.getElementsByTagName("CDS_MODIFIER")
105
+ for item in itemlist:
106
+ modif = minidom.parseString(item.toxml())
107
+
108
+ modif_code = re.sub("<[^>]*>", "", modif.getElementsByTagName("CDS_ELE_NAME")[0].toxml())
109
+
110
+ modif_id = re.sub("<[^>]*>", "", modif.getElementsByTagName("CDS_ELE_ID")[0].toxml())
111
+
112
+ try:
113
+ modif_parent_id = re.sub("<[^>]*>", "", modif.getElementsByTagName("CDS_ELE_PARENT_ID")[0].toxml())
114
+ except Exception:
115
+ modif_parent_id = ""
116
+
117
+ try:
118
+ description = re.sub("<[^>]*>", "", modif.getElementsByTagName("CDS_ELE_DESCRIPTION")[0].toxml())
119
+ except Exception:
120
+ description = ""
121
+ try:
122
+ key = re.sub("<[^>]*>", "", modif.getElementsByTagName("CDS_ELE_START_KEYCODE")[0].toxml())
123
+ except Exception:
124
+ key = ""
125
+
126
+ if modif_parent_id:
127
+ modifiers[modif_parent_id]["values"].append(modif_code)
128
+ else:
129
+ if len(key) > 1:
130
+ key = ""
131
+ flag_long_key = True
132
+ modifiers[modif_id] = {"set_name": modif_code, "key": key, "description": description, "values": []}
133
+
134
+ logging.debug(modifiers)
135
+
136
+ # connect modifiers to behaviors
137
+ connections: dict = {}
138
+ itemlist = xmldoc.getElementsByTagName("CDS_CONNECTION")
139
+ for item in itemlist:
140
+ if item.attributes["CDS_ELEMENT_ID"].value not in connections:
141
+ connections[item.attributes["CDS_ELEMENT_ID"].value] = []
142
+ connections[item.attributes["CDS_ELEMENT_ID"].value].append(item.attributes["CDS_MODIFIER_ID"].value)
143
+
144
+ logging.debug(connections)
145
+
146
+ # behaviors
147
+ behaviors: dict = {}
148
+ behaviors_list: list = []
149
+ behav_category: list = []
150
+ mutually_exclusive_list: list = []
151
+ itemlist = xmldoc.getElementsByTagName("CDS_BEHAVIOR")
152
+ for item in itemlist:
153
+ behav = minidom.parseString(item.toxml())
154
+
155
+ behav_code = re.sub("<[^>]*>", "", behav.getElementsByTagName("CDS_ELE_NAME")[0].toxml())
156
+
157
+ behav_id = re.sub("<[^>]*>", "", behav.getElementsByTagName("CDS_ELE_ID")[0].toxml())
158
+
159
+ try:
160
+ description = re.sub("<[^>]*>", "", behav.getElementsByTagName("CDS_ELE_DESCRIPTION")[0].toxml())
161
+ except Exception:
162
+ description = ""
163
+ try:
164
+ key = re.sub("<[^>]*>", "", behav.getElementsByTagName("CDS_ELE_START_KEYCODE")[0].toxml())
165
+ except Exception:
166
+ key = ""
167
+
168
+ try:
169
+ stop_key = re.sub("<[^>]*>", "", behav.getElementsByTagName("CDS_ELE_STOP_KEYCODE")[0].toxml())
170
+ except Exception:
171
+ stop_key = ""
172
+
173
+ try:
174
+ parent_name = re.sub("<[^>]*>", "", behav.getElementsByTagName("CDS_ELE_PARENT_NAME")[0].toxml())
175
+ except Exception:
176
+ parent_name = ""
177
+
178
+ try:
179
+ mutually_exclusive = re.sub("<[^>]*>", "", behav.getElementsByTagName("CDS_ELE_MUT_EXCLUSIVE")[0].toxml())
180
+ except Exception:
181
+ mutually_exclusive = ""
182
+
183
+ if mutually_exclusive == "Y" and parent_name:
184
+ mutually_exclusive_list.append(behav_code)
185
+
186
+ if behav_id in connections:
187
+ modifier_sets = [modifiers[modifier_set]["set_name"] for modifier_set in connections[behav_id]]
188
+ else:
189
+ modifier_sets = []
190
+
191
+ if parent_name: # behavior
192
+ if (not key or len(key) > 1) and stop_key:
193
+ key = stop_key
194
+
195
+ if len(key) > 1:
196
+ key = ""
197
+ flag_long_key = True
198
+
199
+ behaviors[str(len(behaviors))] = {
200
+ "id": int(behav_id),
201
+ "code": behav_code,
202
+ "key": key,
203
+ "description": description,
204
+ "modifiers": modifier_sets,
205
+ "category": parent_name,
206
+ }
207
+ behaviors_list.append(behav_code)
208
+
209
+ else: # behavioral category
210
+ behav_category.append(behav_code)
211
+
212
+ behaviors_boris: dict = {}
213
+ for k in behaviors:
214
+ behaviors_boris[k] = {
215
+ "code": behaviors[k]["code"],
216
+ "type": cfg.POINT_EVENT,
217
+ "key": behaviors[k]["key"],
218
+ "description": behaviors[k]["description"],
219
+ "category": behaviors[k]["category"],
220
+ "excluded": "",
221
+ "coding map": "",
222
+ }
223
+
224
+ if behaviors[k]["code"] in mutually_exclusive_list:
225
+ behaviors_boris[k]["excluded"] = ",".join([x for x in behaviors_list if x != behaviors[k]["code"]])
226
+
227
+ behaviors_boris[k]["modifiers"] = {}
228
+ if behaviors[k]["modifiers"]:
229
+ for modif_key in modifiers:
230
+ if modifiers[modif_key]["set_name"] in behaviors[k]["modifiers"]:
231
+ new_index = str(len(behaviors_boris[k]["modifiers"]))
232
+ behaviors_boris[k]["modifiers"][new_index] = {
233
+ "name": modifiers[modif_key]["set_name"],
234
+ "type": cfg.SINGLE_SELECTION,
235
+ "values": modifiers[modif_key]["values"],
236
+ "description": modifiers[modif_key]["description"],
237
+ }
238
+
239
+ logging.debug(behaviors_boris)
240
+
241
+ # subjects
242
+ subjects = {}
243
+ itemlist = xmldoc.getElementsByTagName("CDS_SUBJECT")
244
+ for item in itemlist:
245
+ subject = minidom.parseString(item.toxml())
246
+ subject_name = re.sub("<[^>]*>", "", subject.getElementsByTagName("CDS_ELE_NAME")[0].toxml())
247
+ try:
248
+ key = re.sub("<[^>]*>", "", subject.getElementsByTagName("CDS_ELE_START_KEYCODE")[0].toxml())
249
+ except Exception:
250
+ key = ""
251
+ try:
252
+ parent_name = re.sub("<[^>]*>", "", subject.getElementsByTagName("CDS_ELE_PARENT_NAME")[0].toxml())
253
+ except Exception:
254
+ parent_name = ""
255
+
256
+ if parent_name:
257
+ if len(key) > 1:
258
+ key = ""
259
+ flag_long_key = True
260
+ subjects[str(len(subjects))] = {"key": key, "name": subject_name, "description": ""}
261
+
262
+ # independent variables
263
+ variables = {}
264
+ itemlist = xmldoc.getElementsByTagName("VL_VARIABLE")
265
+ for item in itemlist:
266
+ variable = minidom.parseString(item.toxml())
267
+
268
+ variable_label = re.sub("<[^>]*>", "", variable.getElementsByTagName("VL_LABEL")[0].toxml())
269
+
270
+ variable_id = re.sub("<[^>]*>", "", variable.getElementsByTagName("VL_ID")[0].toxml())
271
+
272
+ variable_type = re.sub("<[^>]*>", "", variable.getElementsByTagName("VL_TYPE")[0].toxml())
273
+ if variable_type.upper() == "TEXT":
274
+ variable_type = cfg.TEXT
275
+ if variable_type.upper() == "DOUBLE":
276
+ variable_type = cfg.NUMERIC
277
+ if variable_type.upper() == "FILEREFERENCE":
278
+ variable_type = cfg.TEXT
279
+ if variable_type.upper() == "DURATION":
280
+ variable_type = cfg.TEXT
281
+ if variable_type.upper() == "TIMESTAMP":
282
+ variable_type = cfg.TIMESTAMP
283
+ if variable_type.upper() == "BOOLEAN":
284
+ variable_type = cfg.TEXT
285
+
286
+ try:
287
+ variable_description = re.sub("<[^>]*>", "", modif.getElementsByTagName("VL_DESCRIPTION")[0].toxml())
288
+ except Exception:
289
+ variable_description = ""
290
+
291
+ try:
292
+ values = variable.getElementsByTagName("VL_VALUE")
293
+ values_list = []
294
+ for value in values:
295
+ values_list.append(re.sub("<[^>]*>", "", value.toxml()))
296
+ values_str = ",".join(values_list)
297
+
298
+ except Exception:
299
+ values_str = ""
300
+
301
+ variables[variable_id] = {
302
+ "label": variable_label,
303
+ "type": variable_type.lower(),
304
+ "description": variable_description,
305
+ }
306
+ if values_str:
307
+ variables[variable_id]["predefined_values"] = values_str
308
+ variables[variable_id]["type"] = "value from set"
309
+
310
+ variables_boris = {}
311
+ for k in variables:
312
+ variables_boris[k] = {
313
+ "label": variables[k]["label"],
314
+ "description": variables[k]["description"],
315
+ "type": variables[k]["type"],
316
+ "default value": "",
317
+ "possible values": variables[k]["predefined_values"] if "predefined_values" in variables[k] else "",
318
+ }
319
+
320
+ # create empty project from template
321
+ project = dict(cfg.EMPTY_PROJECT)
322
+ project[cfg.OBSERVATIONS] = {}
323
+
324
+ observations = xmldoc.getElementsByTagName("OBS_OBSERVATION")
325
+
326
+ for OBS_OBSERVATION in observations:
327
+ # OBS_OBSERVATION = minidom.parseString(OBS_OBSERVATION.toxml())
328
+
329
+ obs_id = OBS_OBSERVATION.getAttribute("NAME")
330
+
331
+ project[cfg.OBSERVATIONS][obs_id] = dict(
332
+ {
333
+ "file": {},
334
+ "type": "LIVE",
335
+ "description": "",
336
+ "time offset": 0,
337
+ cfg.EVENTS: [],
338
+ "observation time interval": [0, 0],
339
+ "independent_variables": {},
340
+ "visualize_spectrogram": False,
341
+ "visualize_waveform": False,
342
+ "close_behaviors_between_videos": False,
343
+ "scan_sampling_time": 0,
344
+ "start_from_current_time": False,
345
+ "start_from_current_epoch_time": False,
346
+ }
347
+ )
348
+
349
+ OBS_EVENT_LOGS = OBS_OBSERVATION.getElementsByTagName("OBS_EVENT_LOGS")[0]
350
+
351
+ for OBS_EVENT_LOG in OBS_EVENT_LOGS.getElementsByTagName("OBS_EVENT_LOG"):
352
+ CREATION_DATETIME = OBS_EVENT_LOG.getAttribute("CREATION_DATETIME")
353
+
354
+ CREATION_DATETIME = CREATION_DATETIME.replace(" ", "T") # .split(".")[0]
355
+
356
+ logging.debug(f"{CREATION_DATETIME=}") # ex: 2022-05-18 10:04:09.474512"""
357
+
358
+ project[cfg.OBSERVATIONS][obs_id]["date"] = CREATION_DATETIME
359
+
360
+ for event in OBS_EVENT_LOG.getElementsByTagName("OBS_EVENT"):
361
+ OBS_EVENT_TIMESTAMP = event.getElementsByTagName("OBS_EVENT_TIMESTAMP")[0].childNodes[0].data
362
+
363
+ full_timestamp = dt.datetime.strptime(OBS_EVENT_TIMESTAMP, "%Y-%m-%d %H:%M:%S.%f").timestamp()
364
+ logging.debug(f"{full_timestamp=}")
365
+
366
+ # day_timestamp = dt.datetime.strptime(OBS_EVENT_TIMESTAMP.split(" ")[0], "%Y-%m-%d").timestamp()
367
+ # timestamp = dec(str(round(full_timestamp - day_timestamp, 3)))
368
+ timestamp = dec(full_timestamp).quantize(dec(".001"))
369
+
370
+ try:
371
+ OBS_EVENT_SUBJECT = event.getElementsByTagName("OBS_EVENT_SUBJECT")[0].getAttribute("NAME")
372
+ except Exception:
373
+ OBS_EVENT_SUBJECT = ""
374
+
375
+ OBS_EVENT_BEHAVIOR = event.getElementsByTagName("OBS_EVENT_BEHAVIOR")[0].getAttribute("NAME")
376
+ logging.debug(f"{OBS_EVENT_BEHAVIOR=}")
377
+ if not OBS_EVENT_BEHAVIOR:
378
+ logging.warning(f"Behavior missing in observation {obs_id} at {timestamp}")
379
+ error_list.append(f"Behavior missing in observation {obs_id} at {timestamp}")
380
+ continue
381
+
382
+ # modifier
383
+ try:
384
+ OBS_EVENT_BEHAVIOR_MODIFIER = (
385
+ event.getElementsByTagName("OBS_EVENT_BEHAVIOR")[0]
386
+ .getElementsByTagName("OBS_EVENT_BEHAVIOR_MODIFIER")[0]
387
+ .childNodes[0]
388
+ .data
389
+ )
390
+ except Exception:
391
+ OBS_EVENT_BEHAVIOR_MODIFIER: str = ""
392
+
393
+ # comment
394
+ try:
395
+ OBS_EVENT_COMMENT: str = event.getElementsByTagName("OBS_EVENT_COMMENT")[0].childNodes[0].data
396
+ except Exception:
397
+ OBS_EVENT_COMMENT: str = ""
398
+
399
+ logging.debug(f"{timestamp=}")
400
+ logging.debug(f"{OBS_EVENT_SUBJECT=}")
401
+ logging.debug(f"{OBS_EVENT_BEHAVIOR=}")
402
+ logging.debug(f"{OBS_EVENT_BEHAVIOR_MODIFIER=}")
403
+ logging.debug(f"{OBS_EVENT_COMMENT=}")
404
+
405
+ project[cfg.OBSERVATIONS][obs_id][cfg.EVENTS].append(
406
+ [
407
+ timestamp,
408
+ OBS_EVENT_SUBJECT,
409
+ OBS_EVENT_BEHAVIOR,
410
+ OBS_EVENT_BEHAVIOR_MODIFIER,
411
+ OBS_EVENT_COMMENT,
412
+ ]
413
+ )
414
+
415
+ project[cfg.PROJECT_NAME] = project_name
416
+ project[cfg.PROJECT_DATE] = project_creation_date.replace(" ", "T")
417
+ project[cfg.ETHOGRAM] = behaviors_boris
418
+ project[cfg.PROJECT_DESCRIPTION] = project_description
419
+ project[cfg.BEHAVIORAL_CATEGORIES] = behav_category
420
+ project[cfg.SUBJECTS] = subjects
421
+ project[cfg.INDEPENDENT_VARIABLES] = variables_boris
422
+
423
+ if flag_long_key:
424
+ error_list.append("The keys longer than one char were deleted.")
425
+ logging.debug("The keys longer than one char were deleted.")
426
+
427
+ return project, error_list
428
+
429
+
430
+ if __name__ == "__main__":
431
+ import sys
432
+ import pprint
433
+
434
+ logging.basicConfig(
435
+ format="%(asctime)s,%(msecs)d %(module)s l.%(lineno)d %(levelname)s %(message)s",
436
+ datefmt="%H:%M:%S",
437
+ level=logging.DEBUG,
438
+ )
439
+ project, errors = otx_to_boris(sys.argv[1])
440
+
441
+ pprint.pprint(project)
442
+ pprint.pprint(errors)
boris/param_panel.py ADDED
@@ -0,0 +1,201 @@
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
+ from PySide6.QtCore import Qt
24
+ from PySide6.QtWidgets import QDialog
25
+
26
+ from . import config as cfg
27
+ from .param_panel_ui import Ui_Dialog
28
+ from . import dialog
29
+
30
+
31
+ class Param_panel(QDialog, Ui_Dialog):
32
+ def __init__(self, parent=None):
33
+ super().__init__()
34
+ self.setupUi(self)
35
+
36
+ self.media_duration = None
37
+
38
+ # insert duration widget for time offset
39
+ self.start_time = dialog.get_time_widget(0)
40
+ self.horizontalLayout.insertWidget(1, self.start_time)
41
+ self.end_time = dialog.get_time_widget(0)
42
+ self.horizontalLayout_6.insertWidget(1, self.end_time)
43
+
44
+ self.pbSelectAllSubjects.clicked.connect(lambda: self.subjects_button_clicked("select all"))
45
+ self.pbUnselectAllSubjects.clicked.connect(lambda: self.subjects_button_clicked("unselect all"))
46
+ self.pbReverseSubjectsSelection.clicked.connect(lambda: self.subjects_button_clicked("reverse selection"))
47
+
48
+ self.pbSelectAllBehaviors.clicked.connect(lambda: self.behaviors_button_clicked("select all"))
49
+ self.pbUnselectAllBehaviors.clicked.connect(lambda: self.behaviors_button_clicked("unselect all"))
50
+ self.pbReverseBehaviorsSelection.clicked.connect(lambda: self.behaviors_button_clicked("reverse selection"))
51
+
52
+ self.pbOK.clicked.connect(self.ok)
53
+ self.pbCancel.clicked.connect(self.reject)
54
+
55
+ self.lwBehaviors.itemClicked.connect(self.behavior_item_clicked)
56
+
57
+ self.rb_observed_events.setChecked(True)
58
+
59
+ self.rb_media_duration.clicked.connect(lambda: self.rb_time_interval_selection(cfg.TIME_FULL_OBS))
60
+ self.rb_observed_events.clicked.connect(lambda: self.rb_time_interval_selection(cfg.TIME_EVENTS))
61
+ self.rb_user_defined.clicked.connect(lambda: self.rb_time_interval_selection(cfg.TIME_ARBITRARY_INTERVAL))
62
+ self.rb_obs_interval.clicked.connect(lambda: self.rb_time_interval_selection(cfg.TIME_OBS_INTERVAL))
63
+
64
+ self.cbIncludeModifiers.stateChanged.connect(self.cb_exclude_non_coded_modifiers_visibility)
65
+
66
+ self.cb_exclude_non_coded_modifiers.setVisible(False)
67
+
68
+ def cb_exclude_non_coded_modifiers_visibility(self):
69
+ """
70
+ set visibility of cb_exclude_non_coded_modifiers
71
+ """
72
+ self.cb_exclude_non_coded_modifiers.setEnabled(self.cbIncludeModifiers.isChecked())
73
+
74
+ def rb_time_interval_selection(self, button):
75
+ """
76
+ select the time interval for operation
77
+ """
78
+ if button == cfg.TIME_ARBITRARY_INTERVAL:
79
+ self.frm_time_interval.setEnabled(True)
80
+ self.frm_time_interval.setVisible(True)
81
+
82
+ elif button == cfg.TIME_EVENTS and len(self.selectedObservations) == 1:
83
+ self.start_time.set_time(self.start_coding)
84
+ self.end_time.set_time(self.end_coding)
85
+ self.frm_time_interval.setEnabled(False)
86
+ self.frm_time_interval.setVisible(True)
87
+
88
+ elif button == cfg.TIME_FULL_OBS and len(self.selectedObservations) == 1 and self.media_duration is not None:
89
+ self.start_time.set_time(0)
90
+ self.end_time.set_time(self.media_duration)
91
+ self.frm_time_interval.setEnabled(False)
92
+ self.frm_time_interval.setVisible(True)
93
+
94
+ elif button == cfg.TIME_OBS_INTERVAL:
95
+ if not ((self.start_interval is None) or self.start_interval.is_nan()):
96
+ # Set start_time and end_time widgets values even if it is not shown with
97
+ # more than 1 observation as some analyses might use it (eg: advanced event filtering)
98
+
99
+ end_interval = self.end_interval if self.end_interval != 0 else self.media_duration
100
+
101
+ self.start_time.set_time(self.start_interval)
102
+ self.end_time.set_time(end_interval)
103
+ self.frm_time_interval.setEnabled(False)
104
+ if len(self.selectedObservations) == 1:
105
+ self.frm_time_interval.setVisible(True)
106
+
107
+ else:
108
+ self.frm_time_interval.setVisible(False)
109
+
110
+ def subjects_button_clicked(self, command):
111
+ for idx in range(self.lwSubjects.count()):
112
+ cb = self.lwSubjects.itemWidget(self.lwSubjects.item(idx))
113
+ if command == "select all":
114
+ cb.setChecked(True)
115
+ if command == "unselect all":
116
+ cb.setChecked(False)
117
+ if command == "reverse selection":
118
+ cb.setChecked(not cb.isChecked())
119
+
120
+ def behaviors_button_clicked(self, command):
121
+ for idx in range(self.lwBehaviors.count()):
122
+ if self.lwBehaviors.item(idx).data(33) != "category":
123
+ if command == "select all":
124
+ self.lwBehaviors.item(idx).setCheckState(Qt.Checked)
125
+
126
+ if command == "unselect all":
127
+ self.lwBehaviors.item(idx).setCheckState(Qt.Unchecked)
128
+
129
+ if command == "reverse selection":
130
+ if self.lwBehaviors.item(idx).checkState() == Qt.Checked:
131
+ self.lwBehaviors.item(idx).setCheckState(Qt.Unchecked)
132
+ else:
133
+ self.lwBehaviors.item(idx).setCheckState(Qt.Checked)
134
+
135
+ def ok(self):
136
+ selectedSubjects = []
137
+ for idx in range(self.lwSubjects.count()):
138
+ cb = self.lwSubjects.itemWidget(self.lwSubjects.item(idx))
139
+ if cb.isChecked():
140
+ selectedSubjects.append(cb.text())
141
+ self.selectedSubjects = selectedSubjects
142
+
143
+ selectedBehaviors = []
144
+ for idx in range(self.lwBehaviors.count()):
145
+ if self.lwBehaviors.item(idx).checkState() == Qt.Checked:
146
+ selectedBehaviors.append(self.lwBehaviors.item(idx).text())
147
+ self.selectedBehaviors = selectedBehaviors
148
+
149
+ self.accept()
150
+
151
+ def behavior_item_clicked(self, item):
152
+ """
153
+ check / uncheck behaviors belonging to the clicked category
154
+ """
155
+
156
+ if item.data(33) == "category":
157
+ category = item.data(34)
158
+ for i in range(self.lwBehaviors.count()):
159
+ if self.lwBehaviors.item(i).data(34) == category and self.lwBehaviors.item(i).data(33) != "category":
160
+ if item.data(35):
161
+ self.lwBehaviors.item(i).setCheckState(Qt.Unchecked)
162
+ else:
163
+ self.lwBehaviors.item(i).setCheckState(Qt.Checked)
164
+
165
+ item.setData(35, not item.data(35))
166
+
167
+ def extract_observed_behaviors(self, selected_observations, selected_subjects):
168
+ """
169
+ extract unique behaviors codes from obs_id observation and selected subjects
170
+ """
171
+
172
+ observed_behaviors = []
173
+
174
+ # extract events from selected observations
175
+ all_events = [self.pj[cfg.OBSERVATIONS][x][cfg.EVENTS] for x in self.pj[cfg.OBSERVATIONS] if x in selected_observations]
176
+
177
+ for events in all_events:
178
+ for event in events:
179
+ if event[cfg.EVENT_SUBJECT_FIELD_IDX] in selected_subjects or (
180
+ not event[cfg.EVENT_SUBJECT_FIELD_IDX] and cfg.NO_FOCAL_SUBJECT in selected_subjects
181
+ ):
182
+ observed_behaviors.append(event[cfg.EVENT_BEHAVIOR_FIELD_IDX])
183
+
184
+ # remove duplicate
185
+ return list(set(observed_behaviors))
186
+
187
+ def cb_changed(self):
188
+ selected_subjects: list = []
189
+ for idx in range(self.lwSubjects.count()):
190
+ cb = self.lwSubjects.itemWidget(self.lwSubjects.item(idx))
191
+ if cb and cb.isChecked():
192
+ selected_subjects.append(cb.text())
193
+
194
+ observed_behaviors = self.extract_observed_behaviors(self.selectedObservations, selected_subjects)
195
+
196
+ for idx in range(self.lwBehaviors.count()):
197
+ if self.lwBehaviors.item(idx).data(33) != "category":
198
+ if self.lwBehaviors.item(idx).text() in observed_behaviors:
199
+ self.lwBehaviors.item(idx).setCheckState(Qt.Checked)
200
+ else:
201
+ self.lwBehaviors.item(idx).setCheckState(Qt.Unchecked)