boris-behav-obs 9.3.2__py2.py3-none-any.whl → 9.3.4__py2.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/dialog.py CHANGED
@@ -88,7 +88,7 @@ def MessageDialog(title: str, text: str, buttons: tuple) -> str:
88
88
  for button in buttons:
89
89
  message.addButton(button, QMessageBox.YesRole)
90
90
 
91
- message.setWindowFlags(Qt.WindowStaysOnTopHint)
91
+ message.setWindowFlags(message.windowFlags() | Qt.WindowStaysOnTopHint)
92
92
  message.exec()
93
93
  return message.clickedButton().text()
94
94
 
@@ -184,7 +184,7 @@ class Info_widget(QWidget):
184
184
 
185
185
  class get_time_widget(QWidget):
186
186
  """
187
- widget for selecting a time in various formats: secondes, HH:MM:SS:ZZZ or YYYY-mm-DD HH:MM:SS:ZZZ
187
+ widget for selecting a time in various formats: seconds, HH:MM:SS:ZZZ or YYYY-mm-DD HH:MM:SS:ZZZ
188
188
  """
189
189
 
190
190
  def __init__(self, time_value=dec(0), parent=None):
@@ -408,7 +408,7 @@ class get_time_widget(QWidget):
408
408
  if self.pb_sign.text() == "-":
409
409
  time_sec = -time_sec
410
410
 
411
- return dec(time_sec) if time_sec is not None else None
411
+ return dec(time_sec).quantize(dec("0.001")) if time_sec is not None else None
412
412
 
413
413
  def set_time(self, new_time: dec) -> None:
414
414
  """
@@ -440,6 +440,11 @@ class get_time_widget(QWidget):
440
440
 
441
441
 
442
442
  class Ask_time(QDialog):
443
+ """
444
+ Qdialog class for asking time to user
445
+ User can select a time format between seconds, HHMMSS.zzz or YYY-mm-DD HH:MM:SS.zzz
446
+ """
447
+
443
448
  def __init__(self, time_value=0):
444
449
  super().__init__()
445
450
  self.setWindowTitle("")
@@ -453,10 +458,10 @@ class Ask_time(QDialog):
453
458
 
454
459
  hbox.addWidget(self.time_widget)
455
460
 
456
- self.pbOK = QPushButton("OK", clicked=self.pb_ok_clicked)
461
+ self.pbOK = QPushButton(cfg.OK, clicked=self.pb_ok_clicked)
457
462
  self.pbOK.setDefault(True)
458
463
 
459
- self.pbCancel = QPushButton("Cancel")
464
+ self.pbCancel = QPushButton(cfg.CANCEL)
460
465
  self.pbCancel.clicked.connect(self.reject)
461
466
 
462
467
  self.hbox2 = QHBoxLayout(self)
@@ -975,6 +980,70 @@ class Results_dialog(QDialog):
975
980
  self.done(cfg.SAVE_DATASET)
976
981
 
977
982
 
983
+ class Results_dialog_exit_code(QDialog):
984
+ """
985
+ widget for visualizing text output
986
+ """
987
+
988
+ def __init__(self):
989
+ super().__init__()
990
+
991
+ self.dataset = False
992
+
993
+ self.setWindowTitle("")
994
+
995
+ hbox = QVBoxLayout()
996
+
997
+ self.lb = QLabel("")
998
+ hbox.addWidget(self.lb)
999
+
1000
+ self.ptText = QPlainTextEdit()
1001
+ self.ptText.setReadOnly(True)
1002
+ hbox.addWidget(self.ptText)
1003
+
1004
+ hbox2 = QHBoxLayout()
1005
+ hbox2.addItem(QSpacerItem(241, 20, QSizePolicy.Expanding, QSizePolicy.Minimum))
1006
+
1007
+ self.pbSave = QPushButton("Save results", clicked=self.save_results)
1008
+ hbox2.addWidget(self.pbSave)
1009
+
1010
+ self.pb1 = QPushButton("1", clicked=lambda: self.done_(1))
1011
+ hbox2.addWidget(self.pb1)
1012
+
1013
+ self.pb2 = QPushButton("2", clicked=lambda: self.done_(2))
1014
+ hbox2.addWidget(self.pb2)
1015
+
1016
+ self.pb3 = QPushButton("3", clicked=lambda: self.done_(3))
1017
+ hbox2.addWidget(self.pb3)
1018
+
1019
+ hbox.addLayout(hbox2)
1020
+ self.setLayout(hbox)
1021
+
1022
+ self.resize(800, 640)
1023
+
1024
+ def done_(self, status):
1025
+ self.done(status)
1026
+
1027
+ def save_results(self):
1028
+ """
1029
+ save content of self.ptText
1030
+ """
1031
+
1032
+ if not self.dataset:
1033
+ file_name, _ = QFileDialog().getSaveFileName(self, "Save results", "", "Text files (*.txt *.tsv);;All files (*)")
1034
+
1035
+ if not file_name:
1036
+ return
1037
+ try:
1038
+ with open(file_name, "w") as f:
1039
+ f.write(self.ptText.toPlainText())
1040
+ except Exception:
1041
+ QMessageBox.critical(self, cfg.programName, f"The file {file_name} can not be saved")
1042
+
1043
+ else:
1044
+ self.done(cfg.SAVE_DATASET)
1045
+
1046
+
978
1047
  class View_data(QDialog):
979
1048
  """
980
1049
  widget for visualizing rows of data file
boris/event_operations.py CHANGED
@@ -576,6 +576,8 @@ def edit_event(self):
576
576
 
577
577
  pj_event_idx = self.tv_idx2events_idx[tvevents_row]
578
578
 
579
+ print(f"{self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS]=}")
580
+
579
581
  time_value = self.pj[cfg.OBSERVATIONS][self.observationId][cfg.EVENTS][pj_event_idx][
580
582
  cfg.PJ_OBS_FIELDS[self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE]][cfg.TIME]
581
583
  ]
@@ -601,9 +603,12 @@ def edit_event(self):
601
603
  )
602
604
  edit_window.setWindowTitle("Edit event")
603
605
 
606
+ print(f"{time_value=}")
607
+
604
608
  # time
605
609
  if time_value.is_nan():
606
610
  edit_window.cb_set_time_na.setChecked(True)
611
+
607
612
  if self.playerType in (cfg.VIEWER_MEDIA, cfg.VIEWER_LIVE, cfg.VIEWER_IMAGES):
608
613
  edit_window.pb_set_to_current_time.setVisible(False)
609
614
 
@@ -673,12 +678,15 @@ def edit_event(self):
673
678
 
674
679
  flag_ok = False # for looping until event is OK or Cancel pressed
675
680
  while True:
676
- if edit_window.exec_(): # button OK
681
+ if edit_window.exec(): # button OK
677
682
  self.project_changed()
678
683
 
679
684
  # MEDIA / LIVE
680
685
  if self.pj[cfg.OBSERVATIONS][self.observationId][cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
681
686
  new_time = edit_window.time_widget.get_time()
687
+
688
+ print(f"{new_time=}")
689
+
682
690
  if new_time is None:
683
691
  QMessageBox.warning(
684
692
  self,
@@ -747,7 +755,11 @@ def edit_event(self):
747
755
  for key in self.pj[cfg.ETHOGRAM]:
748
756
  if self.pj[cfg.ETHOGRAM][key][cfg.BEHAVIOR_CODE] == edit_window.cobCode.currentText():
749
757
  event = self.full_event(key)
750
- if edit_window.time_value == cfg.NA or (edit_window.cb_set_time_na.isChecked()):
758
+ if (
759
+ edit_window.time_value == cfg.NA
760
+ or (isinstance(edit_window.time_value, dec) and edit_window.time_value.is_nan())
761
+ or (edit_window.cb_set_time_na.isChecked())
762
+ ):
751
763
  event[cfg.TIME] = dec("NaN")
752
764
  else:
753
765
  event[cfg.TIME] = edit_window.time_widget.get_time()
boris/media_file.py CHANGED
@@ -20,11 +20,13 @@ This file is part of BORIS.
20
20
 
21
21
  """
22
22
 
23
+ from PySide6.QtWidgets import QFileDialog
24
+
23
25
  from . import config as cfg
24
26
  from . import utilities as util
25
27
  from . import dialog
26
28
  from . import project_functions
27
- from PySide6.QtWidgets import QFileDialog
29
+ from . import utilities as util
28
30
 
29
31
 
30
32
  def get_info(self) -> None:
@@ -38,17 +40,17 @@ def get_info(self) -> None:
38
40
  if "error" in r:
39
41
  ffmpeg_output = f"File path: {media_full_path}<br><br>{r['error']}<br><br>"
40
42
  else:
41
- ffmpeg_output = f"<br><b>{r['analysis_program'] } analysis</b><br>"
43
+ ffmpeg_output = f"<br><b>{r['analysis_program']} analysis</b><br>"
42
44
 
43
45
  ffmpeg_output += (
44
46
  f"File path: <b>{media_full_path}</b><br><br>"
45
47
  f"Duration: {r['duration']} seconds ({util.convertTime(self.timeFormat, r['duration'])})<br>"
48
+ f"FPS: {r['fps']}<br>"
49
+ f"Resolution: {r['resolution']} pixels<br>"
46
50
  f"Format long name: {r.get('format_long_name', cfg.NA)}<br>"
47
51
  f"Creation time: {r.get('creation_time', cfg.NA)}<br>"
48
- f"Resolution: {r['resolution']}<br>"
49
52
  f"Number of frames: {r['frames_number']}<br>"
50
53
  f"Bitrate: {util.smart_size_format(r['bitrate'])} <br>"
51
- f"FPS: {r['fps']}<br>"
52
54
  f"Has video: {r['has_video']}<br>"
53
55
  f"Has audio: {r['has_audio']}<br>"
54
56
  f"File size: {util.smart_size_format(r.get('file size', cfg.NA))}<br>"
@@ -70,6 +72,12 @@ def get_info(self) -> None:
70
72
 
71
73
  mpv_output = (
72
74
  "<b>MPV information</b><br>"
75
+ f"Duration: {dw.player.duration} seconds ({util.seconds2time(dw.player.duration)})<br>"
76
+ # "Position: {} %<br>"
77
+ f"FPS: {dw.player.container_fps}<br>"
78
+ # "Rate: {}<br>"
79
+ f"Resolution: {dw.player.width}x{dw.player.height} pixels<br>"
80
+ # "Scale: {}<br>"
73
81
  f"Video format: {dw.player.video_format}<br>"
74
82
  # "State: {}<br>"
75
83
  # "Media Resource Location: {}<br>"
@@ -77,12 +85,6 @@ def get_info(self) -> None:
77
85
  # "Track: {}/{}<br>"
78
86
  f"Number of media in media list: {dw.player.playlist_count}<br>"
79
87
  f"Current time position: {dw.player.time_pos}<br>"
80
- f"Duration: {dw.player.duration}<br>"
81
- # "Position: {} %<br>"
82
- f"FPS: {dw.player.container_fps}<br>"
83
- # "Rate: {}<br>"
84
- f"Video size: {dw.player.width}x{dw.player.height}<br>"
85
- # "Scale: {}<br>"
86
88
  f"Aspect ratio: {round(dw.player.width / dw.player.height, 3)}<br>"
87
89
  # "is seekable? {}<br>"
88
90
  # "has_vout? {}<br>"
@@ -1100,7 +1100,7 @@ def close_observation(self):
1100
1100
 
1101
1101
  logging.info(f"Close observation (player type: {self.playerType})")
1102
1102
 
1103
- # check observation events
1103
+ # check observation state events
1104
1104
 
1105
1105
  flag_ok, msg = project_functions.check_state_events_obs(
1106
1106
  self.observationId,
@@ -1111,16 +1111,22 @@ def close_observation(self):
1111
1111
 
1112
1112
  if not flag_ok:
1113
1113
  out = f"The current observation has state event(s) that are not PAIRED:<br><br>{msg}"
1114
- results = dialog.Results_dialog()
1114
+ results = dialog.Results_dialog_exit_code()
1115
1115
  results.setWindowTitle(f"{cfg.programName} - Check selected observations")
1116
1116
  results.ptText.setReadOnly(True)
1117
1117
  results.ptText.appendHtml(out)
1118
- results.pbSave.setVisible(False)
1119
- results.pbCancel.setText("Close observation")
1120
- results.pbCancel.setVisible(True)
1121
- results.pbOK.setText("Fix unpaired state events")
1122
1118
 
1123
- if results.exec_(): # fix events
1119
+ results.pb1.setText("Close observation")
1120
+ results.pb2.setText("Return to observation")
1121
+ if self.playerType == cfg.IMAGES:
1122
+ results.pb3.setVisible(False)
1123
+ else:
1124
+ results.pb3.setText("Fix unpaired state events")
1125
+
1126
+ r = results.exec()
1127
+ if r == 2: # Return to observation
1128
+ return
1129
+ if r == 3: # Fix unpaired state events
1124
1130
  state_events.fix_unpaired_events(self, silent_mode=True)
1125
1131
 
1126
1132
  self.saved_state = self.saveState()
@@ -2406,15 +2412,18 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
2406
2412
  """
2407
2413
 
2408
2414
  cumul_media_durations: list = [dec(0)]
2409
- for media_file in observation[cfg.FILE]["1"]:
2410
- media_duration = dec(str(observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]))
2411
- cumul_media_durations.append(cumul_media_durations[-1] + media_duration)
2415
+ for media_file in observation[cfg.FILE][cfg.PLAYER1]:
2416
+ try:
2417
+ media_duration = dec(str(observation[cfg.MEDIA_INFO][cfg.LENGTH][media_file]))
2418
+ cumul_media_durations.append(round(cumul_media_durations[-1] + media_duration, 3))
2419
+ except KeyError:
2420
+ return None
2412
2421
 
2413
2422
  cumul_media_durations.remove(dec(0))
2414
2423
 
2415
2424
  # test if timestamp is at end of last media
2416
2425
  if timestamp == cumul_media_durations[-1]:
2417
- player_idx = len(observation[cfg.FILE]["1"]) - 1
2426
+ player_idx = len(observation[cfg.FILE][cfg.PLAYER1]) - 1
2418
2427
  else:
2419
2428
  player_idx = -1
2420
2429
  for idx, value in enumerate(cumul_media_durations):
@@ -2424,7 +2433,7 @@ def event2media_file_name(observation: dict, timestamp: dec) -> Optional[str]:
2424
2433
  break
2425
2434
 
2426
2435
  if player_idx != -1:
2427
- video_file_name = observation[cfg.FILE]["1"][player_idx]
2436
+ video_file_name = observation[cfg.FILE][cfg.PLAYER1][player_idx]
2428
2437
  else:
2429
2438
  video_file_name = None
2430
2439
 
boris/project.py CHANGED
@@ -82,12 +82,12 @@ class BehavioralCategories(QDialog):
82
82
  # add categories
83
83
  self.lw.setColumnCount(2)
84
84
  self.lw.setHorizontalHeaderLabels(["Category name", "Color"])
85
- # self.lw.verticalHeader().hide()
86
85
  self.lw.setEditTriggers(QAbstractItemView.NoEditTriggers)
87
86
 
88
- # self.lw.setSelectionBehavior(QAbstractItemView.SelectRows)
89
87
  self.lw.setSelectionMode(QAbstractItemView.SingleSelection)
90
88
 
89
+ behavioral_categories: list = []
90
+
91
91
  if cfg.BEHAVIORAL_CATEGORIES_CONF in pj:
92
92
  self.lw.setRowCount(len(pj.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {})))
93
93
  behav_cat = pj.get(cfg.BEHAVIORAL_CATEGORIES_CONF, {})
@@ -95,7 +95,7 @@ class BehavioralCategories(QDialog):
95
95
  # name
96
96
  item = QTableWidgetItem()
97
97
  item.setText(behav_cat[key]["name"])
98
- # item.setFlags(Qt.ItemIsEnabled)
98
+ behavioral_categories.append(behav_cat[key]["name"])
99
99
  self.lw.setItem(idx, 0, item)
100
100
  # color
101
101
  item = QTableWidgetItem()
@@ -103,24 +103,21 @@ class BehavioralCategories(QDialog):
103
103
  if behav_cat[key].get(cfg.COLOR, ""):
104
104
  item.setBackground(QColor(behav_cat[key].get(cfg.COLOR, "")))
105
105
  else:
106
- # item.setBackground(QColor(230, 230, 230))
107
106
  item.setBackground(self.not_editable_column_color())
108
- # item.setFlags(Qt.ItemIsEnabled)
109
107
  self.lw.setItem(idx, 1, item)
110
108
  else:
111
109
  self.lw.setRowCount(len(pj.get(cfg.BEHAVIORAL_CATEGORIES, [])))
112
110
  for idx, category in enumerate(sorted(pj.get(cfg.BEHAVIORAL_CATEGORIES, []))):
111
+ # name
113
112
  item = QTableWidgetItem()
114
113
  item.setText(category)
115
- # item.setFlags(Qt.ItemIsEnabled)
114
+ behavioral_categories.append(category)
116
115
  self.lw.setItem(idx, 0, item)
117
-
116
+ # color
118
117
  item = QTableWidgetItem()
119
118
  item.setText("")
120
- # item.setFlags(Qt.ItemIsEnabled)
121
- self.lw.setItem(idx, 1, item)
122
119
 
123
- # self.lw.addItem(QListWidgetItem(category))
120
+ self.lw.setItem(idx, 1, item)
124
121
 
125
122
  self.vbox.addWidget(self.lw)
126
123
 
@@ -148,6 +145,43 @@ class BehavioralCategories(QDialog):
148
145
 
149
146
  self.setLayout(self.vbox)
150
147
 
148
+ # check if behavioral categories are present in events
149
+ behavioral_categories_in_ethogram = set(
150
+ sorted([pj[cfg.ETHOGRAM][idx].get(cfg.BEHAVIOR_CATEGORY, "") for idx in pj.get(cfg.ETHOGRAM, {})])
151
+ )
152
+
153
+ if behavioral_categories_in_ethogram.difference(set(behavioral_categories)) and behavioral_categories_in_ethogram.difference(
154
+ set(behavioral_categories)
155
+ ) != {""}:
156
+ if (
157
+ dialog.MessageDialog(
158
+ cfg.programName,
159
+ (
160
+ "There are behavioral categories that are present in ethogram but not defined.<br>"
161
+ f"{behavioral_categories_in_ethogram.difference(set(behavioral_categories))}<br>"
162
+ "<br>"
163
+ "Do you want to add them in the behavioral categories list?"
164
+ ),
165
+ [cfg.YES, cfg.NO],
166
+ )
167
+ == cfg.YES
168
+ ):
169
+ # add behavioral categories present in ethogram in behavioal categories list
170
+ rc = self.lw.rowCount()
171
+ self.lw.setRowCount(rc + len(behavioral_categories_in_ethogram.difference(set(behavioral_categories))))
172
+ for idx, category in enumerate(sorted(list(behavioral_categories_in_ethogram.difference(set(behavioral_categories))))):
173
+ print(category)
174
+ # name
175
+ item = QTableWidgetItem()
176
+ item.setText(category)
177
+ # behavioral_categories.append(category)
178
+ self.lw.setItem(rc + idx, 0, item)
179
+ # color
180
+ item = QTableWidgetItem()
181
+ item.setText("")
182
+
183
+ self.lw.setItem(rc + idx, 1, item)
184
+
151
185
  def not_editable_column_color(self):
152
186
  """
153
187
  return a color for the not editable column
@@ -178,7 +212,6 @@ class BehavioralCategories(QDialog):
178
212
  color = col_diag.currentColor()
179
213
  if color.name() == "#000000": # black -> delete color
180
214
  self.lw.item(row, 1).setText("")
181
- # self.lw.item(row, 1).setBackground(QColor(230, 230, 230))
182
215
  self.lw.item(row, 1).setBackground(self.not_editable_column_color())
183
216
  elif color.isValid():
184
217
  self.lw.item(row, 1).setText(color.name())
@@ -663,7 +696,7 @@ class projectDialog(QDialog, Ui_dlgProject):
663
696
  QMessageBox.warning(
664
697
  self,
665
698
  cfg.programName,
666
- ("The following behavior{} are not defined in the ethogram:<br>" "{}").format(
699
+ ("The following behavior{} are not defined in the ethogram:<br>{}").format(
667
700
  "s" if len(bcm_code_not_found) > 1 else "", ",".join(bcm_code_not_found)
668
701
  ),
669
702
  )
@@ -733,7 +766,7 @@ class projectDialog(QDialog, Ui_dlgProject):
733
766
  behavioral categories manager
734
767
  """
735
768
 
736
- bc = BehavioralCategories(self.pj) # self.config_param.get(cfg.DARK_MODE, cfg.DEFAULT_FRAME_MODE)
769
+ bc = BehavioralCategories(self.pj)
737
770
 
738
771
  if bc.exec_():
739
772
  self.pj[cfg.BEHAVIORAL_CATEGORIES] = []
@@ -788,18 +821,18 @@ class projectDialog(QDialog, Ui_dlgProject):
788
821
  column (int): column double-clicked
789
822
  """
790
823
 
791
- # check if double click on excluded column
824
+ # excluded column
792
825
  if column == cfg.behavioursFields[cfg.EXCLUDED]:
793
826
  self.exclusion_matrix()
794
827
 
795
- # check if double click on 'coding map' column
828
+ # coding map
796
829
  if column == cfg.behavioursFields[cfg.CODING_MAP_sp]:
797
830
  if "with coding map" in self.twBehaviors.item(row, cfg.behavioursFields[cfg.TYPE]).text():
798
831
  self.behavior_type_changed(row)
799
832
  else:
800
833
  QMessageBox.information(self, cfg.programName, "Change the behavior type on first column to select a coding map")
801
834
 
802
- # check if double click on behavior type
835
+ # behavior type
803
836
  if column == cfg.behavioursFields["type"]:
804
837
  self.behavior_type_doubleclicked(row)
805
838
 
@@ -811,7 +844,7 @@ class projectDialog(QDialog, Ui_dlgProject):
811
844
  if column == cfg.behavioursFields[cfg.BEHAVIOR_CATEGORY]:
812
845
  self.category_doubleclicked(row)
813
846
 
814
- # check if double click on modifiers
847
+ # modifiers
815
848
  if column == cfg.behavioursFields[cfg.MODIFIERS]:
816
849
  # check if behavior has coding map
817
850
  if (
@@ -865,16 +898,17 @@ class projectDialog(QDialog, Ui_dlgProject):
865
898
  if self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).text():
866
899
  current_color = QColor(self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).text())
867
900
  if current_color.isValid():
901
+ print(f"{current_color=}")
868
902
  col_diag.setCurrentColor(current_color)
869
903
 
870
- if col_diag.exec_():
904
+ if col_diag.exec():
871
905
  color = col_diag.currentColor()
872
906
  if color.name() == "#000000": # black -> delete color
873
907
  self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setText("")
874
908
  self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(self.not_editable_column_color())
875
909
  elif color.isValid():
910
+ self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(QColor(color.name()))
876
911
  self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setText(color.name())
877
- self.twBehaviors.item(row, cfg.behavioursFields[cfg.COLOR]).setBackground(color)
878
912
 
879
913
  def category_doubleclicked(self, row):
880
914
  """
@@ -1377,7 +1411,7 @@ class projectDialog(QDialog, Ui_dlgProject):
1377
1411
  # let user select a coding maop
1378
1412
  file_name, _ = QFileDialog().getOpenFileName(
1379
1413
  self,
1380
- "Select a modifier coding map for " f"{self.twBehaviors.item(row, cfg.behavioursFields['code']).text()} behavior",
1414
+ f"Select a modifier coding map for {self.twBehaviors.item(row, cfg.behavioursFields['code']).text()} behavior",
1381
1415
  "",
1382
1416
  "BORIS map files (*.boris_map);;All files (*)",
1383
1417
  )
@@ -1759,24 +1793,40 @@ class projectDialog(QDialog, Ui_dlgProject):
1759
1793
  return {cfg.CANCEL: True}
1760
1794
 
1761
1795
  # check if behavior belong to category that is not in categories list
1762
- behavior_category: list = []
1796
+ missing_behavior_category: list = []
1763
1797
  for idx in checked_ethogram:
1764
1798
  if cfg.BEHAVIOR_CATEGORY in checked_ethogram[idx]:
1765
1799
  if checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY]:
1766
1800
  if checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY] not in self.pj[cfg.BEHAVIORAL_CATEGORIES]:
1767
- behavior_category.append((checked_ethogram[idx][cfg.BEHAVIOR_CODE], checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY]))
1768
- if behavior_category:
1801
+ missing_behavior_category.append(
1802
+ (checked_ethogram[idx][cfg.BEHAVIOR_CODE], checked_ethogram[idx][cfg.BEHAVIOR_CATEGORY])
1803
+ )
1804
+ if missing_behavior_category:
1769
1805
  response = dialog.MessageDialog(
1770
1806
  f"{cfg.programName} - Behavioral categories",
1771
1807
  (
1772
- "The behavioral categorie(s) "
1773
- f"{', '.join(set(['<b>' + x[1] + '</b>' + ' (used with <b>' + x[0] + '</b>)' for x in behavior_category]))} "
1774
- "are no more defined in behavioral categories list"
1808
+ "The behavioral category/ies<br> "
1809
+ f"{', '.join(set(['<b>' + x[1] + '</b>' + ' (used with <b>' + x[0] + '</b>)<br>' for x in missing_behavior_category]))} "
1810
+ "are not defined in behavioral categories list.<br>"
1775
1811
  ),
1776
- ["Add behavioral category/ies", "Ignore", cfg.CANCEL],
1812
+ ["Add behavioral category/ies", cfg.IGNORE, cfg.CANCEL],
1777
1813
  )
1778
1814
  if response == "Add behavioral category/ies":
1779
- [self.pj[cfg.BEHAVIORAL_CATEGORIES].append(x1) for x1 in set(x[1] for x in behavior_category)]
1815
+ if cfg.BEHAVIORAL_CATEGORIES_CONF not in self.pj:
1816
+ self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF] = {}
1817
+ for x1 in set(x[1] for x in missing_behavior_category):
1818
+ self.pj[cfg.BEHAVIORAL_CATEGORIES].append(x1)
1819
+
1820
+ if self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF]:
1821
+ index = str(max([int(k) for k in self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF]]) + 1)
1822
+ else:
1823
+ index = "0"
1824
+
1825
+ self.pj[cfg.BEHAVIORAL_CATEGORIES_CONF][index] = {
1826
+ "name": x1,
1827
+ cfg.COLOR: "",
1828
+ }
1829
+
1780
1830
  if response == cfg.CANCEL:
1781
1831
  return {cfg.CANCEL: True}
1782
1832
 
@@ -356,7 +356,7 @@ def check_state_events(pj: dict, observations_list: list) -> Tuple[bool, tuple]:
356
356
  use check_state_events_obs function
357
357
  """
358
358
 
359
- logging.info("Check state events")
359
+ logging.info("Check state events function")
360
360
 
361
361
  out = ""
362
362
  not_paired_obs_list = []
@@ -404,7 +404,7 @@ def check_project_integrity(
404
404
  * check if media file are available (optional)
405
405
  * check if media length available
406
406
  * check independent variables
407
- * check if coded subjects are defines
407
+ * check if coded subjects are defined
408
408
 
409
409
  Args:
410
410
  pj (dict): BORIS project
@@ -474,6 +474,7 @@ def check_project_integrity(
474
474
  if not timestamp.is_nan() and not (-2147483647 <= timestamp <= 2147483647):
475
475
  out_events += f"Observation: <b>{obs_id}</b><br>The timestamp {timestamp} is not between -2147483647 and 2147483647.<br>"
476
476
 
477
+ """
477
478
  # check if media length available
478
479
  if pj[cfg.OBSERVATIONS][obs_id][cfg.TYPE] == cfg.MEDIA:
479
480
  for nplayer in cfg.ALL_PLAYERS:
@@ -484,6 +485,7 @@ def check_project_integrity(
484
485
  except KeyError:
485
486
  out += "<br><br>" if out else ""
486
487
  out += f"Observation: <b>{obs_id}</b><br>Length not available for media file <b>{media_file}</b>"
488
+ """
487
489
 
488
490
  out += "<br><br>" if out else ""
489
491
  out += out_events
@@ -530,25 +532,46 @@ def check_project_integrity(
530
532
  ]
531
533
  )
532
534
 
533
- out += "<br><br>" if out else ""
535
+ tmp_out: str = ""
534
536
  for obs_id in pj[cfg.OBSERVATIONS]:
535
537
  if cfg.INDEPENDENT_VARIABLES not in pj[cfg.OBSERVATIONS][obs_id]:
536
538
  continue
537
539
  for var_label in pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES]:
538
540
  if var_label in defined_set_var_label:
539
541
  if pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label] not in defined_set_var_label[var_label].split(","):
540
- out += (
542
+ tmp_out += (
541
543
  f"{obs_id}: the <b>{pj[cfg.OBSERVATIONS][obs_id][cfg.INDEPENDENT_VARIABLES][var_label]}</b> value "
542
544
  f" is not allowed for {var_label} (choose between {defined_set_var_label[var_label]})<br>"
543
545
  )
546
+ if tmp_out:
547
+ out += "<br><br>" if out else ""
548
+ out += tmp_out
544
549
 
545
550
  # check if coded subjects are defined in the subjects list
551
+ tmp_out: str = ""
546
552
  subjects_list: list = [pj[cfg.SUBJECTS][x]["name"] for x in pj[cfg.SUBJECTS]]
547
553
  coded_subjects = set(util.flatten_list([[y[1] for y in pj[cfg.OBSERVATIONS][x].get(cfg.EVENTS, [])] for x in pj[cfg.OBSERVATIONS]]))
548
554
 
549
555
  for subject in coded_subjects:
550
556
  if subject and subject not in subjects_list:
551
- out += f"The coded subject <b>{subject}</b> is not defined in the subjects list.<br>You can use the <b>Explore project</b> to fix it.<br><br>"
557
+ tmp_out += f"The coded subject <b>{subject}</b> is not defined in the subjects list.<br>You can use the <b>Explore project</b> to fix it.<br><br>"
558
+ if tmp_out:
559
+ out += "<br><br>" if out else ""
560
+ out += tmp_out
561
+
562
+ # check if media file have info in media_info section of project
563
+ tmp_out: str = ""
564
+ for obs_id in pj[cfg.OBSERVATIONS]:
565
+ for player in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE]:
566
+ for media_file in pj[cfg.OBSERVATIONS][obs_id][cfg.FILE][player]:
567
+ for info in (cfg.LENGTH, cfg.FPS, cfg.HAS_AUDIO, cfg.HAS_VIDEO):
568
+ if media_file not in pj[cfg.OBSERVATIONS][obs_id][cfg.MEDIA_INFO][info]:
569
+ tmp_out += f"Observation <b>{obs_id}</b>:<br>"
570
+ tmp_out += f"The media file {media_file} has no <b>{info}</b> info.<br>"
571
+ if tmp_out:
572
+ tmp_out += "<br>You should repick the media file to fix this issue."
573
+ out += "<br><br>" if out else ""
574
+ out += tmp_out
552
575
 
553
576
  return out
554
577
 
@@ -1168,13 +1191,8 @@ def observed_interval(observation: dict) -> Tuple[dec, dec]:
1168
1191
  """
1169
1192
  if not observation[cfg.EVENTS]:
1170
1193
  return (dec("0.0"), dec("0.0"))
1171
- if observation[cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
1172
- """
1173
- print("=" * 120)
1174
- print(observation[cfg.EVENTS])
1175
- print("=" * 120)
1176
- """
1177
1194
 
1195
+ if observation[cfg.TYPE] in (cfg.MEDIA, cfg.LIVE):
1178
1196
  event_timestamp = [event[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.TIME]] for event in observation[cfg.EVENTS]]
1179
1197
 
1180
1198
  return (
@@ -1183,8 +1201,12 @@ def observed_interval(observation: dict) -> Tuple[dec, dec]:
1183
1201
  )
1184
1202
  if observation[cfg.TYPE] == cfg.IMAGES:
1185
1203
  events = [x[cfg.PJ_OBS_FIELDS[observation[cfg.TYPE]][cfg.IMAGE_INDEX]] for x in observation[cfg.EVENTS]]
1186
-
1187
- return (dec(min(events)), dec(max(events)))
1204
+ # test if indexes contain NA
1205
+ try:
1206
+ dec(min(events))
1207
+ return (dec(min(events)), dec(max(events)))
1208
+ except Exception:
1209
+ return (dec("NaN"), dec("NaN"))
1188
1210
 
1189
1211
 
1190
1212
  def events_start_stop(ethogram: dict, events: list, obs_type: str) -> List[tuple]:
@@ -1273,7 +1295,7 @@ def open_project_json(project_file_name: str) -> tuple:
1273
1295
  str: message
1274
1296
  """
1275
1297
 
1276
- logging.debug(f"open project: {project_file_name}")
1298
+ logging.debug(f"open_project_json function: {project_file_name}")
1277
1299
 
1278
1300
  projectChanged: bool = False
1279
1301
  msg: str = ""
@@ -1612,6 +1634,8 @@ def fix_unpaired_state_events2(ethogram: dict, events: list, fix_at_time: dec) -
1612
1634
  list: list of events with state events fixed
1613
1635
  """
1614
1636
 
1637
+ logging.debug("fix_unpaired_state_events2 function")
1638
+
1615
1639
  closing_events_to_add: list = []
1616
1640
  subjects: list = [event[cfg.EVENT_SUBJECT_FIELD_IDX] for event in events]
1617
1641
  ethogram_behaviors: dict = {ethogram[idx][cfg.BEHAVIOR_CODE] for idx in ethogram}