def tabWidgetModifiersSets_changed(self, tabIndex): """ user changed the tab widget """ # check if modifier field empty if self.leModifier.text() and tabIndex != self.tabMem: if dialog.MessageDialog( programName, ("You are working on a behavior.<br>" "If you change the modifier's set it will be lost.<br>" "Do you want to change modifiers set"), [YES, NO]) == NO: self.tabWidgetModifiersSets.setCurrentIndex(self.tabMem) return if tabIndex != self.tabMem: self.lwModifiers.clear() self.leCode.clear() self.leModifier.clear() self.tabMem = tabIndex if tabIndex != -1: self.leSetName.setText( self.modifiers_sets_dict[str(tabIndex)]["name"]) self.cbType.setCurrentIndex( self.modifiers_sets_dict[str(tabIndex)]["type"]) self.lwModifiers.addItems( self.modifiers_sets_dict[str(tabIndex)]["values"])
def import_subjects_from_clipboard(self): """ import subjects from clipboard """ try: cb = QApplication.clipboard() cb_text = cb.text() if not cb_text: QMessageBox.warning(None, programName, "The clipboard is empty", QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton) return if self.twSubjects.rowCount(): response = dialog.MessageDialog( programName, "Some subjects are already configured. Do you want to append subjects or replace them?", ["Append", "Replace", CANCEL]) if response == CANCEL: return if response == "Replace": self.twSubjects.setRowCount(0) cb_text_splitted = cb_text.split("\n") if len(set([len(x.split("\t")) for x in cb_text_splitted])) != 1: QMessageBox.warning(None, programName, ( "The clipboard content does not have a constant number of fields.<br>" "From your spreadsheet: CTRL + A (select all cells), CTRL + C (copy to clipboard)" ), QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton) return for row in cb_text_splitted: if set(row.split("\t")) != set([""]): subject = {} for idx, field in enumerate(row.split("\t")): if idx == 0: subject["key"] = field.strip() if len( field.strip()) == 1 else "" if idx == 1: subject[SUBJECT_NAME] = field.strip() if idx == 2: subject["description"] = field.strip() self.twSubjects.setRowCount(self.twSubjects.rowCount() + 1) for idx, field_name in enumerate(subjectsFields): item = QTableWidgetItem(subject.get(field_name, "")) self.twSubjects.setItem(self.twSubjects.rowCount() - 1, idx, item) except Exception: error_type, error_file_name, error_lineno = utilities.error_info( sys.exc_info()) logging.critical( f"Error in function '{sys._getframe().f_code.co_name}': {error_type} {error_file_name} {error_lineno}" ) dialog.error_message_box(sys._getframe().f_code.co_name, error_type, error_file_name, error_lineno)
def import_subjects_from_project(self): """ import subjects from another project """ try: fn = QFileDialog().getOpenFileName( self, "Import subjects from project file", "", ("Project files (*.boris *.boris.gz);;" "All files (*)")) file_name = fn[0] if type(fn) is tuple else fn if file_name: _, _, project, _ = project_functions.open_project_json(file_name) if "error" in project: logging.debug(project["error"]) QMessageBox.critical(self, programName, project["error"]) return # configuration of behaviours if SUBJECTS in project and project[SUBJECTS]: if self.twSubjects.rowCount(): response = dialog.MessageDialog( programName, ("There are subjects already configured. " "Do you want to append subjects or replace them?"), ['Append', 'Replace', 'Cancel']) if response == "Replace": self.twSubjects.setRowCount(0) if response == CANCEL: return for idx in utilities.sorted_keys(project[SUBJECTS]): self.twSubjects.setRowCount(self.twSubjects.rowCount() + 1) for idx2, sbjField in enumerate(subjectsFields): if sbjField in project[SUBJECTS][idx]: self.twSubjects.setItem( self.twSubjects.rowCount() - 1, idx2, QTableWidgetItem( project[SUBJECTS][idx][sbjField])) else: self.twSubjects.setItem( self.twSubjects.rowCount() - 1, idx2, QTableWidgetItem("")) self.twSubjects.resizeColumnsToContents() else: QMessageBox.warning( self, programName, "No subjects configuration found in project") except Exception: dialog.error_message(sys._getframe().f_code.co_name, sys.exc_info())
def pb_pushed(self, button): if self.leModifier.text(): if dialog.MessageDialog(cfg.programName, ("You are working on a behavior.<br>" "If you close the window it will be lost.<br>" "Do you want to change modifiers set"), ["Close", cfg.CANCEL]) == cfg.CANCEL: return if button == "ok": self.accept() if button == "cancel": self.reject()
def pbClose_clicked(self): if not self.flagSaved: response = dialog.MessageDialog( programName, "The current results are not saved. Do you want to save results before closing?", [YES, NO, CANCEL]) if response == YES: self.pbSave_clicked() if response == CANCEL: return self.closeSignal.emit()
def save_results(self): """ save results """ extended_file_formats = ["Tab Separated Values (*.tsv)", "Comma Separated Values (*.csv)", "Open Document Spreadsheet ODS (*.ods)", "Microsoft Excel Spreadsheet XLSX (*.xlsx)", "Legacy Microsoft Excel Spreadsheet XLS (*.xls)", "HTML (*.html)"] file_formats = ["tsv", "csv", "ods", "xlsx", "xls", "html"] file_name, filter_ = QFileDialog().getSaveFileName(None, "Save results", "", ";;".join(extended_file_formats)) if not file_name: return output_format = file_formats[extended_file_formats.index(filter_)] if pathlib.Path(file_name).suffix != "." + output_format: file_name = str(pathlib.Path(file_name)) + "." + output_format # check if file with new extension already exists if pathlib.Path(file_name).is_file(): if dialog.MessageDialog(programName, f"The file {file_name} already exists.", [CANCEL, OVERWRITE]) == CANCEL: return if self.rb_details.isChecked(): tablib_dataset = tablib.Dataset(headers=self.details_header) if self.rb_summary.isChecked(): tablib_dataset = tablib.Dataset(headers=self.summary_header) tablib_dataset.title = utilities.safe_xl_worksheet_title(self.logic.text(), output_format) [tablib_dataset.append(x) for x in self.out] try: if output_format in ["csv", "tsv", "html"]: with open(file_name, "wb") as f: f.write(str.encode(tablib_dataset.export(output_format))) if output_format in ["ods", "xlsx", "xls"]: with open(file_name, "wb") as f: f.write(tablib_dataset.export(output_format)) except Exception: QMessageBox.critical(self, programName, f"The file {file_name} can not be saved")
def newMap(self): """ create a new map """ if self.flagMapChanged: response = dialog.MessageDialog( programName + " - Behaviors coding map creator", "What to do about the current unsaved coding map?", [SAVE, DISCARD, CANCEL]) if response == SAVE: if not self.saveMap_clicked(): return if response == CANCEL: return self.cancelMap() while True: map_name, ok = QInputDialog.getText( self, "Behaviors coding map name", "Enter a name for the new coding map") if map_name.upper() in self.bcm_list: QMessageBox.critical(self, "", ( "The name for the new coding map already exists.<br>" f"{', '.join(self.bcm_list)} are already defined.<br>" "To reuse the same name the existing coding map must be deleted (File > Edit project)" )) if ok and map_name and map_name.upper() not in self.bcm_list: self.mapName = map_name break if not ok: return ''' if not self.mapName: QMessageBox.critical(self, "", "You must define a name for the new coding map") return ''' self.setWindowTitle( f"{programName} - Behaviors coding map creator tool - {self.mapName}" ) self.btLoad.setVisible(True) self.statusBar().showMessage( 'Click "Load bitmap" button to select and load a bitmap into the viewer' )
def closeEvent(self, event): if self.flagMapChanged: response = dialog.MessageDialog("BORIS - Modifiers map creator", "What to do about the current unsaved modifiers coding map?", ["Save", "Discard", "Cancel"]) if response == "Save": if not self.saveMap_clicked(): event.ignore() if response == "Cancel": event.ignore() return self.closed.emit() event.accept()
def observation_length(pj, selected_observations: list) -> tuple: """ max length of selected observations total media length Args: selected_observations (list): list of selected observations Returns: float: maximum media length for all observations float: total media length for all observations """ selectedObsTotalMediaLength = dec("0.0") max_obs_length = 0 for obs_id in selected_observations: obs_length = observation_total_length(pj[OBSERVATIONS][obs_id]) if obs_length in [dec("0"), dec("-1")]: selectedObsTotalMediaLength = -1 break max_obs_length = max(max_obs_length, obs_length) selectedObsTotalMediaLength += obs_length # an observation media length is not available if selectedObsTotalMediaLength == -1: # propose to user to use max event time if dialog.MessageDialog( programName, (f"A media length is not available for the observation <b>{obs_id}</b>.<br>" "Use last event time as media length?"), [YES, NO]) == YES: maxTime = 0 # max length for all events all subjects max_length = 0 for obs_id in selected_observations: if pj[OBSERVATIONS][obs_id][EVENTS]: maxTime += max(pj[OBSERVATIONS][obs_id][EVENTS])[0] max_length = max(max_length, max(pj[OBSERVATIONS][obs_id][EVENTS])[0]) logging.debug(f"max time all events all subjects: {maxTime}") max_obs_length = max_length selectedObsTotalMediaLength = maxTime else: max_obs_length = -1 selectedObsTotalMediaLength = dec("-1") return max_obs_length, selectedObsTotalMediaLength
def newMap(self): """ create a new map """ if self.flagMapChanged: response = dialog.MessageDialog( programName + ' - Modifiers map creator', 'What to do about the current unsaved coding map?', ['Save', 'Discard', 'Cancel']) if response == 'Save': if not self.saveMap_clicked(): return if response == 'Cancel': return self.cancelMap() text, ok = QInputDialog.getText(self, 'Map name', 'Enter a name for the new map') if ok: self.mapName = text else: return if self.mapName == '': QMessageBox.critical(self, '', 'You must define a name for the new map') return if self.mapName in ['areas', 'bitmap']: QMessageBox.critical(self, '', 'This name is not allowed') return self.setWindowTitle(programName + ' - Map creator tool - ' + self.mapName) self.btLoad.setVisible(True) '''self.btCancelMap.setVisible(True)''' self.statusBar().showMessage( 'Click "Load bitmap" button to select and load a bitmap into the viewer' )
def add_media_from_dir(self, flag_path): """ add all media from a selected directory Args: flag_path (bool): True include full path of media else only basename """ # check if project saved if (not flag_path) and (not self.project_file_name): QMessageBox.critical( self, programName, ("It is not possible to add media without full path " "if the project is not saved")) return fd = QFileDialog() fd.setDirectory( os.path.expanduser("~") if flag_path else str( Path(self.project_path).parent)) dir_name = fd.getExistingDirectory(self, "Select directory") if dir_name: r, response = "", "" for file_path in glob.glob(dir_name + os.sep + "*"): r, msg = self.check_media(file_path, flag_path) if not r: if response != "Skip all non media files": response = dialog.MessageDialog( programName, f"<b>{file_path}</b> {msg}", ["Continue", "Skip all non media files", "Cancel"]) if response == "Cancel": break for w in [ self.cbVisualizeSpectrogram, self.cb_visualize_waveform, self.cb_observation_time_interval, self.cbCloseCurrentBehaviorsBetweenVideo ]: w.setEnabled(self.twVideo1.rowCount() > 0) # disabled for problems self.cbCloseCurrentBehaviorsBetweenVideo.setEnabled(False)
def removeSet(self): """ remove set of modifiers """ if self.tabWidgetModifiersSets.currentIndex() != -1: if dialog.MessageDialog(cfg.programName, "Confirm deletion of this set of modifiers?", [cfg.YES, cfg.NO]) == cfg.YES: index_to_delete = self.tabWidgetModifiersSets.currentIndex() for k in range(index_to_delete, len(self.modifiers_sets_dict) - 1): self.modifiers_sets_dict[str(k)] = self.modifiers_sets_dict[str(k + 1)] # del last key del self.modifiers_sets_dict[str(len(self.modifiers_sets_dict) - 1)] # remove all tabs while self.tabWidgetModifiersSets.count(): self.tabWidgetModifiersSets.removeTab(0) # recreate tabs for idx in sorted_keys(self.modifiers_sets_dict): self.tabWidgetModifiersSets.addTab(QWidget(), f"Set #{int(idx) + 1}") # set not visible and not available buttons and others elements if self.tabWidgetModifiersSets.currentIndex() == -1: for w in [self.lbSetName, self.lbType, self.lbValues, self.leSetName, self.cbType, self.lwModifiers, self.pbMoveUp, self.pbMoveDown, self.pbRemoveModifier, self.pbRemoveSet, self.pbMoveSetLeft, self.pbMoveSetRight]: w.setVisible(False) for w in [self.leModifier, self.leCode, self.pbAddModifier, self.pbModifyModifier]: w.setEnabled(False) if not len(self.modifiers_sets_dict): # set invisible and unavailable buttons and others elements for w in [self.lbSetName, self.lbType, self.lbValues, self.leSetName, self.cbType, self.lwModifiers, self.pbMoveUp, self.pbMoveDown, self.pbRemoveModifier, self.pbRemoveSet, self.pbMoveSetLeft, self.pbMoveSetRight, self.pb_add_subjects, self.pb_load_file, self.pb_sort_modifiers]: w.setVisible(False) for w in [self.leModifier, self.leCode, self.pbAddModifier, self.pbModifyModifier]: w.setEnabled(False) return else: QMessageBox.information(self, cfg.programName, "It is not possible to remove the last modifiers' set.")
def newMap(self): """ create a new map """ if self.flagMapChanged: response = dialog.MessageDialog( programName + " - Modifiers map creator", "What to do about the current unsaved coding map?", [SAVE, DISCARD, CANCEL]) if response == SAVE: if not self.saveMap_clicked(): return if response == CANCEL: return self.cancelMap() text, ok = QInputDialog.getText(self, "Behaviors coding map name", "Enter a name for the new coding map") if ok: self.mapName = text else: return if not self.mapName: QMessageBox.critical( self, "", "You must define a name for the new coding map") return self.setWindowTitle( "{} - Behaviors coding map creator tool - {}".format( programName, self.mapName)) self.btLoad.setVisible(True) self.statusBar().showMessage( 'Click "Load bitmap" button to select and load a bitmap into the viewer' )
def view_doubleClicked(self, index): if self.mode == config.MULTIPLE: return if self.mode == config.OPEN or self.mode == config.EDIT: self.done(2) return if self.mode == config.SELECT1: self.done(2) return response = dialog.MessageDialog( config.programName, "What do you want to do with this observation?", list(commands_index.keys()) + [config.CANCEL]) if response == config.CANCEL: return else: self.done(commands_index[response])
def import_behaviors_from_clipboard(self): """ import ethogram from clipboard """ try: cb = QApplication.clipboard() cb_text = cb.text() if not cb_text: QMessageBox.warning(None, programName, "The clipboard is empty", QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton) return if self.twBehaviors.rowCount(): response = dialog.MessageDialog( programName, "Some behaviors are already configured. Do you want to append behaviors or replace them?", ["Append", "Replace", CANCEL]) if response == CANCEL: return if response == "Replace": self.twBehaviors.setRowCount(0) cb_text_splitted = cb_text.split("\n") while "" in cb_text_splitted: cb_text_splitted.remove("") if len(set([len(x.split("\t")) for x in cb_text_splitted])) != 1: QMessageBox.warning(None, programName, ( "The clipboard content does not have a constant number of fields.<br>" "From your spreadsheet: CTRL + A (select all cells), CTRL + C (copy to clipboard)" ), QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton) return for row in cb_text_splitted: if set(row.split("\t")) != set([""]): behavior = {"type": DEFAULT_BEHAVIOR_TYPE} for idx, field in enumerate(row.split("\t")): if idx == 0: behavior["type"] = STATE_EVENT if STATE in field.upper( ) else (POINT_EVENT if POINT in field.upper() else "") if idx == 1: behavior["key"] = field.strip() if len( field.strip()) == 1 else "" if idx == 2: behavior["code"] = field.strip() if idx == 3: behavior["description"] = field.strip() if idx == 4: behavior["category"] = field.strip() self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1) for field_type in behavioursFields: if field_type == TYPE: item = QTableWidgetItem( behavior.get("type", DEFAULT_BEHAVIOR_TYPE)) else: item = QTableWidgetItem(behavior.get(field_type, "")) if field_type not in ETHOGRAM_EDITABLE_FIELDS: # [TYPE, "excluded", "coding map", "modifiers", "category"]: item.setFlags(Qt.ItemIsEnabled) item.setBackground(QColor(230, 230, 230)) self.twBehaviors.setItem(self.twBehaviors.rowCount() - 1, behavioursFields[field_type], item) except Exception: dialog.error_message(sys._getframe().f_code.co_name, sys.exc_info())
def openMap(self): """ open a coding map from file load bitmap from data show it in view scene """ if self.flagMapChanged: response = dialog.MessageDialog( programName + " - Behaviors coding map creator", "What to do about the current unsaved coding map?", ['Save', 'Discard', 'Cancel']) if (response == "Save" and not self.saveMap_clicked()) or (response == "Cancel"): return fn = QFileDialog(self).getOpenFileName( self, "Open a behaviors coding map", "", "Behaviors coding map (*.behav_coding_map);;All files (*)") fileName = fn[0] if type(fn) is tuple else fn if fileName: try: self.codingMap = json.loads(open(fileName, "r").read()) except Exception: QMessageBox.critical( self, programName, f"The file {fileName} is not a behaviors coding map.") return if "coding_map_type" not in self.codingMap or self.codingMap[ "coding_map_type"] != "BORIS behaviors coding map": QMessageBox.critical( self, programName, f"The file {fileName} is not a BORIS behaviors coding map." ) self.cancelMap() self.mapName = self.codingMap["name"] self.setWindowTitle( f"{programName} - Behaviors coding map creator - {self.mapName}" ) self.bitmapFileName = True self.fileName = fileName bitmapContent = binascii.a2b_base64(self.codingMap["bitmap"]) self.pixmap.loadFromData(bitmapContent) self.view.setSceneRect(0, 0, self.pixmap.size().width(), self.pixmap.size().height()) self.view.setMinimumHeight(self.pixmap.size().height()) # self.view.setMaximumHeight(self.pixmap.size().height()) pixItem = QGraphicsPixmapItem(self.pixmap) pixItem.setPos(0, 0) self.view.scene().addItem(pixItem) for key in self.codingMap["areas"]: areaCode = self.codingMap["areas"][key]["code"] points = self.codingMap["areas"][key]["geometry"] newPolygon = QPolygonF() for p in points: newPolygon.append(QPoint(p[0], p[1])) # draw polygon '''polygon = QGraphicsPolygonItem(None, None) if QT_VERSION_STR[0] == "4" else QGraphicsPolygonItem()''' polygon = QGraphicsPolygonItem() polygon.setPolygon(newPolygon) clr = QColor() clr.setRgba(self.codingMap["areas"][key]["color"]) polygon.setPen( QPen(clr, penWidth, penStyle, Qt.RoundCap, Qt.RoundJoin)) polygon.setBrush(QBrush(clr, Qt.SolidPattern)) self.view.scene().addItem(polygon) self.polygonsList2.append([areaCode, polygon]) self.btNewArea.setVisible(True) self.btLoad.setVisible(False) self.saveMapAction.setEnabled(True) self.saveAsMapAction.setEnabled(True) self.addToProject.setEnabled(True) self.mapNameAction.setEnabled(True) self.update_area_list() else: self.statusBar().showMessage("No file", 5000)
def import_from_JWatcher(self): """ import behaviors configuration from JWatcher (GDF file) """ try: if self.twBehaviors.rowCount(): response = dialog.MessageDialog( programName, "There are behaviors already configured. Do you want to append behaviors or replace them?", ["Append", "Replace", CANCEL]) if response == CANCEL: return fn = QFileDialog().getOpenFileName( self, "Import behaviors from JWatcher", "", "Global Definition File (*.gdf);;All files (*)") fileName = fn[0] if type(fn) is tuple else fn if fileName: if self.twBehaviors.rowCount() and response == "Replace": self.twBehaviors.setRowCount(0) with open(fileName, "r") as f: rows = f.readlines() for idx, row in enumerate(rows): if row and row[0] == "#": continue if "Behavior.name." in row and "=" in row: key, code = row.split('=') key = key.replace("Behavior.name.", "") # read description if idx < len(rows) and "Behavior.description." in rows[ idx + 1]: description = rows[idx + 1].split("=")[-1] behavior = { "key": key, "code": code, "description": description, "modifiers": "", "excluded": "", "coding map": "", "category": "" } self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1) for field_type in behavioursFields: if field_type == TYPE: item = QTableWidgetItem(DEFAULT_BEHAVIOR_TYPE) else: item = QTableWidgetItem(behavior[field_type]) if field_type in [ TYPE, "excluded", "category", "coding map", "modifiers" ]: item.setFlags(Qt.ItemIsEnabled) item.setBackground(QColor(230, 230, 230)) self.twBehaviors.setItem( self.twBehaviors.rowCount() - 1, behavioursFields[field_type], item) except Exception: dialog.error_message(sys._getframe().f_code.co_name, sys.exc_info())
def behavior_binary_table(pj: dict): """ ask user for parameters for behavior binary table call create_behavior_binary_table """ result, selected_observations = select_observations.select_observations( pj, MULTIPLE, "Select observations for the behavior binary table") if not selected_observations: return # check if state events are paired out = "" not_paired_obs_list = [] for obs_id in selected_observations: r, msg = project_functions.check_state_events_obs( obs_id, pj[ETHOGRAM], pj[OBSERVATIONS][obs_id]) if not r: out += f"Observation: <strong>{obs_id}</strong><br>{msg}<br>" not_paired_obs_list.append(obs_id) if out: out = f"The observations with UNPAIRED state events will be removed from the analysis<br><br>{out}" results = dialog.Results_dialog() results.setWindowTitle(f"{programName} - Check selected observations") results.ptText.setReadOnly(True) results.ptText.appendHtml(out) results.pbSave.setVisible(False) results.pbCancel.setVisible(True) if not results.exec_(): return selected_observations = [ x for x in selected_observations if x not in not_paired_obs_list ] if not selected_observations: return max_obs_length, selectedObsTotalMediaLength = project_functions.observation_length( pj, selected_observations) if max_obs_length == -1: # media length not available, user choose to not use events return parameters = dialog.choose_obs_subj_behav_category( pj, selected_observations, maxTime=max_obs_length, flagShowIncludeModifiers=True, flagShowExcludeBehaviorsWoEvents=True, by_category=False) if not parameters[SELECTED_SUBJECTS] or not parameters[SELECTED_BEHAVIORS]: QMessageBox.warning(None, programName, "Select subject(s) and behavior(s) to analyze") return # ask for time interval i, ok = QInputDialog.getDouble(None, "Behavior binary table", "Time interval (in seconds):", 1.0, 0.001, 86400, 3) if not ok: return time_interval = utilities.float2decimal(i) ''' iw = dialog.Info_widget() iw.lwi.setVisible(False) iw.resize(350, 200) iw.setWindowFlags(Qt.WindowStaysOnTopHint) iw.setWindowTitle("Behavior binary table") iw.label.setText("Creating the behavior binary table...") iw.show() QApplication.processEvents() ''' results_df = create_behavior_binary_table(pj, selected_observations, parameters, time_interval) ''' iw.hide() ''' if "error" in results_df: QMessageBox.warning(None, programName, results_df["msg"]) return # save results if len(selected_observations) == 1: extended_file_formats = [ "Tab Separated Values (*.tsv)", "Comma Separated Values (*.csv)", "Open Document Spreadsheet ODS (*.ods)", "Microsoft Excel Spreadsheet XLSX (*.xlsx)", "Legacy Microsoft Excel Spreadsheet XLS (*.xls)", "HTML (*.html)" ] file_formats = ["tsv", "csv", "ods", "xlsx", "xls", "html"] file_name, filter_ = QFileDialog().getSaveFileName( None, "Save results", "", ";;".join(extended_file_formats)) if not file_name: return output_format = file_formats[extended_file_formats.index(filter_)] if pathlib.Path(file_name).suffix != "." + output_format: file_name = str(pathlib.Path(file_name)) + "." + output_format # check if file with new extension already exists if pathlib.Path(file_name).is_file(): if dialog.MessageDialog( programName, f"The file {file_name} already exists.", [CANCEL, OVERWRITE]) == CANCEL: return else: items = ("Tab Separated Values (*.tsv)", "Comma separated values (*.csv)", "Open Document Spreadsheet (*.ods)", "Microsoft Excel Spreadsheet XLSX (*.xlsx)", "Legacy Microsoft Excel Spreadsheet XLS (*.xls)", "HTML (*.html)") item, ok = QInputDialog.getItem(None, "Save results", "Available formats", items, 0, False) if not ok: return output_format = re.sub(".* \(\*\.", "", item)[:-1] export_dir = QFileDialog().getExistingDirectory( None, "Choose a directory to save results", os.path.expanduser("~"), options=QFileDialog.ShowDirsOnly) if not export_dir: return mem_command = "" for obs_id in results_df: for subject in results_df[obs_id]: if len(selected_observations) > 1: file_name_with_subject = str( pathlib.Path(export_dir) / utilities.safeFileName(obs_id + "_" + subject)) + "." + output_format else: file_name_with_subject = str( os.path.splitext(file_name)[0] + utilities.safeFileName("_" + subject)) + "." + output_format # check if file with new extension already exists if mem_command != OVERWRITE_ALL and pathlib.Path( file_name_with_subject).is_file(): if mem_command == "Skip all": continue mem_command = dialog.MessageDialog( programName, f"The file {file_name_with_subject} already exists.", [OVERWRITE, OVERWRITE_ALL, "Skip", "Skip all", CANCEL]) if mem_command == CANCEL: return if mem_command in ["Skip", "Skip all"]: continue try: if output_format in ["csv", "tsv", "html"]: with open(file_name_with_subject, "wb") as f: f.write( str.encode(results_df[obs_id][subject].export( output_format))) if output_format in ["ods", "xlsx", "xls"]: with open(file_name_with_subject, "wb") as f: f.write( results_df[obs_id][subject].export(output_format)) except Exception: error_type, error_file_name, error_lineno = utilities.error_info( sys.exc_info()) logging.critical( f"Error in behavior binary table function: {error_type} {error_file_name} {error_lineno}" ) QMessageBox.critical(None, programName, f"Error saving file: {error_type}") return
def import_behaviors_from_project(self): try: fn = QFileDialog().getOpenFileName( self, "Import behaviors from project file", "", ("Project files (*.boris *.boris.gz);;" "All files (*)")) file_name = fn[0] if type(fn) is tuple else fn if file_name: _, _, project, _ = project_functions.open_project_json(file_name) # import behavioral_categories if BEHAVIORAL_CATEGORIES in project: self.pj[BEHAVIORAL_CATEGORIES] = list( project[BEHAVIORAL_CATEGORIES]) # configuration of behaviours if ETHOGRAM in project and project[ETHOGRAM]: if self.twBehaviors.rowCount(): response = dialog.MessageDialog( programName, ("Some behaviors are already configured. " "Do you want to append behaviors or replace them?"), ["Append", "Replace", CANCEL]) if response == "Replace": self.twBehaviors.setRowCount(0) if response == CANCEL: return behaviors_to_import = select_behaviors( title="Select the behaviors to import", text="Behaviors", behavioral_categories=list(project[BEHAVIORAL_CATEGORIES]), ethogram=dict(project[ETHOGRAM]), behavior_type=[STATE_EVENT, POINT_EVENT]) for i in utilities.sorted_keys(project[ETHOGRAM]): if project[ETHOGRAM][i][ BEHAVIOR_CODE] not in behaviors_to_import: continue self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1) for field in project[ETHOGRAM][i]: item = QTableWidgetItem() if field == TYPE: item.setText(project[ETHOGRAM][i][field]) item.setFlags(Qt.ItemIsEnabled) item.setBackground(QColor(230, 230, 230)) else: if field == "modifiers" and isinstance( project[ETHOGRAM][i][field], str): modif_set_dict = {} if project[ETHOGRAM][i][field]: modif_set_list = project[ETHOGRAM][i][ field].split("|") for modif_set in modif_set_list: modif_set_dict[str( len(modif_set_dict))] = { "name": "", "type": SINGLE_SELECTION, "values": modif_set.split(",") } project[ETHOGRAM][i][field] = dict( modif_set_dict) item.setText(str(project[ETHOGRAM][i][field])) if field not in ETHOGRAM_EDITABLE_FIELDS: item.setFlags(Qt.ItemIsEnabled) item.setBackground(QColor(230, 230, 230)) self.twBehaviors.setItem( self.twBehaviors.rowCount() - 1, behavioursFields[field], item) self.twBehaviors.resizeColumnsToContents() else: QMessageBox.warning( self, programName, "No behaviors configuration found in project") except Exception: dialog.error_message(sys._getframe().f_code.co_name, sys.exc_info())
def openMap(self): """ load bitmap from data show it in view scene """ if self.flagMapChanged: response = dialog.MessageDialog(programName + ' - Map creator', 'What to do about the current unsaved coding map?', ['Save', 'Discard', 'Cancel']) if response == "Save": if not self.saveMap_clicked(): return if response == "Cancel": return fn = QFileDialog().getOpenFileName(self, 'Open a coding map', '', 'BORIS coding map (*.boris_map);;All files (*)') fileName = fn[0] if type(fn) is tuple else fn if fileName: try: self.codingMap = json.loads( open( fileName , 'r').read() ) except: QMessageBox.critical(self, programName, "The file {} seems not a behaviors coding map...".format(fileName)) return self.cancelMap() self.mapName = self.codingMap['name'] self.setWindowTitle(programName + ' - Map creator tool - ' + self.mapName) self.bitmapFileName = True self.fileName = fileName self.areasList = self.codingMap['areas'] # dictionary of dictionaries bitmapContent = binascii.a2b_base64( self.codingMap['bitmap'] ) self.pixmap.loadFromData(bitmapContent) self.btDeleteArea.setEnabled(False) self.view.setSceneRect(0, 0, self.pixmap.size().width(), self.pixmap.size().height()) pixItem = QGraphicsPixmapItem(self.pixmap) pixItem.setPos(0,0) self.view.scene().addItem(pixItem) for areaCode in self.areasList: points = self.areasList[ areaCode ]['geometry'] newPolygon = QPolygonF() for p in points: newPolygon.append(QPoint(p[0], p[1])) clr = QColor( ) clr.setRgba( self.areasList[ areaCode ]['color'] ) # draw polygon polygon = QGraphicsPolygonItem() polygon.setPolygon(newPolygon) polygon.setPen(QPen(clr, penWidth, penStyle, Qt.RoundCap, Qt.RoundJoin)) polygon.setBrush( QBrush( clr, Qt.SolidPattern ) ) self.view.scene().addItem( polygon ) self.polygonsList2[ areaCode ] = polygon self.btNewArea.setVisible(True) self.btLoad.setVisible(False) self.saveMapAction.setEnabled(True) self.saveAsMapAction.setEnabled(True) self.mapNameAction.setEnabled(True) self.statusBar().showMessage('Click "New area" to create a new area') else: self.statusBar().showMessage('No file', 5000)
def import_from_text_file(self): if self.twBehaviors.rowCount(): response = dialog.MessageDialog( programName, "There are behaviors already configured. Do you want to append behaviors or replace them?", ['Append', 'Replace', CANCEL]) if response == CANCEL: return fn = QFileDialog().getOpenFileName( self, "Import behaviors from text file", "", "Text files (*.txt *.tsv *.csv);;All files (*)") fileName = fn[0] if type(fn) is tuple else fn if fileName: if self.twBehaviors.rowCount() and response == "Replace": self.twBehaviors.setRowCount(0) try: with open(fileName, mode="rb") as f: rows_b = f.read().splitlines() rows = [] idx = 1 for row in rows_b: try: rows.append(row.decode("utf-8")) except Exception: QMessageBox.critical( None, programName, (f"Error while reading file\nThe line # {idx}\n" f"{row}\ncontains characters that are not readable."), QMessageBox.Ok | QMessageBox.Default, QMessageBox.NoButton) return idx += 1 fieldSeparator, fieldsNumber = check_text_file_type(rows) logging.debug( f"fields separator: {fieldSeparator} fields number: {fieldsNumber}" ) if fieldSeparator is None: QMessageBox.critical( self, programName, "Separator character not found! Use plain text file and TAB or comma as value separator" ) else: for row in rows: type_, key, code, description = "", "", "", "" if fieldsNumber == 3: # fields: type, key, code type_, key, code = row.split(fieldSeparator) description = "" if fieldsNumber == 4: # fields: type, key, code, description type_, key, code, description = row.split( fieldSeparator) if fieldsNumber > 4: type_, key, code, description = row.split( fieldSeparator)[:4] behavior = { "key": key, "code": code, "description": description, "modifiers": "", "excluded": "", "coding map": "", "category": "" } self.twBehaviors.setRowCount(self.twBehaviors.rowCount() + 1) for field_type in behavioursFields: if field_type == TYPE: item = QTableWidgetItem(DEFAULT_BEHAVIOR_TYPE) # add type combobox if POINT in type_.upper(): item = QTableWidgetItem(POINT_EVENT) if STATE in type_.upper(): item = QTableWidgetItem(STATE_EVENT) else: item = QTableWidgetItem(behavior[field_type]) if field_type not in ETHOGRAM_EDITABLE_FIELDS: item.setFlags(Qt.ItemIsEnabled) item.setBackground(QColor(230, 230, 230)) self.twBehaviors.setItem( self.twBehaviors.rowCount() - 1, behavioursFields[field_type], item) except Exception: dialog.error_message(sys._getframe().f_code.co_name, sys.exc_info())
def export_events_jwatcher(parameters: list, obsId: str, observation: list, ethogram: dict, file_name: str, output_format: str): """ export events jwatcher .dat format Args: parameters (dict): subjects, behaviors obsId (str): observation id observation (dict): observation ethogram (dict): ethogram of project file_name (str): file name for exporting events output_format (str): Not used for compatibility with export_events function Returns: bool: result: True if OK else False str: error message """ try: for subject in parameters["selected subjects"]: # select events for current subject events = [] for event in observation[EVENTS]: if event[SUBJECT_EVENT_FIELD] == subject or (subject == "No focal subject" and event[SUBJECT_EVENT_FIELD] == ""): events.append(event) if not events: continue total_length = 0 # in seconds if observation[EVENTS]: total_length = observation[EVENTS][-1][0] - observation[EVENTS][0][0] # last event time - first event time file_name_subject = str(pathlib.Path(file_name).parent / pathlib.Path(file_name).stem) + "_" + subject + ".dat" rows = ["FirstLineOfData"] # to be completed rows.append("#-----------------------------------------------------------") rows.append(f"# Name: {pathlib.Path(file_name_subject).name}") rows.append("# Format: Focal Data File 1.0") rows.append(f"# Updated: {datetime.datetime.now().isoformat()}") rows.append("#-----------------------------------------------------------") rows.append("") rows.append(f"FocalMasterFile={pathlib.Path(file_name_subject).with_suffix('.fmf')}") rows.append("") rows.append(f"# Observation started: {observation['date']}") try: start_time = datetime.datetime.strptime(observation["date"], '%Y-%m-%dT%H:%M:%S') except ValueError: start_time = datetime.datetime(1970, 1, 1, 0, 0) start_time_epoch = int((start_time - datetime.datetime(1970, 1, 1, 0, 0)).total_seconds() * 1000) rows.append(f"StartTime={start_time_epoch}") stop_time = (start_time + datetime.timedelta(seconds=float(total_length))).isoformat() stop_time_epoch = int(start_time_epoch + float(total_length) * 1000) rows.append(f"# Observation stopped: {stop_time}") rows.append(f"StopTime={stop_time_epoch}") rows.extend([""] * 3) rows.append("#BEGIN DATA") rows[0] = f"FirstLineOfData={len(rows) + 1}" all_observed_behaviors = [] mem_number_of_state_events = {} for event in events: behav_code = event[EVENT_BEHAVIOR_FIELD_IDX] try: behavior_key = [ethogram[k][BEHAVIOR_KEY] for k in ethogram if ethogram[k][BEHAVIOR_CODE] == behav_code][0] except Exception: # coded behavior not defined in ethogram continue if [ethogram[k][TYPE] for k in ethogram if ethogram[k][BEHAVIOR_CODE] == behav_code] == [STATE_EVENT]: if behav_code in mem_number_of_state_events: mem_number_of_state_events[behav_code] += 1 else: mem_number_of_state_events[behav_code] = 1 # skip the STOP event in case of STATE if mem_number_of_state_events[behav_code] % 2 == 0: continue rows.append(f"{int(event[EVENT_TIME_FIELD_IDX] * 1000)}, {behavior_key}") if (event[EVENT_BEHAVIOR_FIELD_IDX], behavior_key) not in all_observed_behaviors: all_observed_behaviors.append((event[EVENT_BEHAVIOR_FIELD_IDX], behavior_key)) rows.append(f"{int(events[-1][0] * 1000)}, EOF\n") try: with open(file_name_subject, "w") as f_out: f_out.write("\n".join(rows)) except Exception: return False, f"File DAT not created for subject {subject}: {sys.exc_info()[1]}" # create fmf file fmf_file_path = pathlib.Path(file_name_subject).with_suffix(".fmf") fmf_creation_answer = "" if fmf_file_path.exists(): fmf_creation_answer = dialog.MessageDialog( programName, (f"The {fmf_file_path} file already exists.<br>" "What do you want to do?"), [OVERWRITE, "Skip file creation", CANCEL]) if fmf_creation_answer == CANCEL: return True, "" rows = [] rows.append("#-----------------------------------------------------------") rows.append(f"# Name: {pathlib.Path(file_name_subject).with_suffix('.fmf').name}") rows.append("# Format: Focal Master File 1.0") rows.append(f"# Updated: {datetime.datetime.now().isoformat()}") rows.append("#-----------------------------------------------------------") for (behav, key) in all_observed_behaviors: rows.append(f"Behaviour.name.{key}={behav}") behav_description = [ethogram[k][DESCRIPTION] for k in ethogram if ethogram[k][BEHAVIOR_CODE] == behav][0] rows.append(f"Behaviour.description.{key}={behav_description}") rows.append(f"DurationMilliseconds={int(float(total_length) * 1000)}") rows.append("CountUp=false") rows.append("Question.1=") rows.append("Question.2=") rows.append("Question.3=") rows.append("Question.4=") rows.append("Question.5=") rows.append("Question.6=") rows.append("Notes=") rows.append("Supplementary=\n") if fmf_creation_answer == OVERWRITE or fmf_creation_answer == "": try: with open(fmf_file_path, "w") as f_out: f_out.write("\n".join(rows)) except Exception: return False, f"File FMF not created: {sys.exc_info()[1]}" # create FAF file faf_file_path = pathlib.Path(file_name_subject).with_suffix(".faf") faf_creation_answer = "" if faf_file_path.exists(): faf_creation_answer = dialog.MessageDialog(programName, (f"The {faf_file_path} file already exists.<br>" "What do you want to do?"), [OVERWRITE, "Skip file creation", CANCEL]) if faf_creation_answer == CANCEL: return True, "" rows = [] rows.append("#-----------------------------------------------------------") rows.append("# Name: {}".format(pathlib.Path(file_name_subject).with_suffix(".faf").name)) rows.append("# Format: Focal Analysis Master File 1.0") rows.append("# Updated: {}".format(datetime.datetime.now().isoformat())) rows.append("#-----------------------------------------------------------") rows.append("FocalMasterFile={}".format(str(pathlib.Path(file_name_subject).with_suffix(".fmf")))) rows.append("") rows.append("TimeBinDuration=0.0") rows.append("EndWithLastCompleteBin=true") rows.append("") rows.append("ScoreFromBeginning=true") rows.append("ScoreFromBehavior=false") rows.append("ScoreFromFirstBehavior=false") rows.append("ScoreFromOffset=false") rows.append("") rows.append("Offset=0.0") rows.append("BehaviorToScoreFrom=") rows.append("") rows.append("OutOfSightCode=") rows.append("") rows.append("Report.StateNaturalInterval.Occurrence=false") rows.append("Report.StateNaturalInterval.TotalTime=false") rows.append("Report.StateNaturalInterval.Average=false") rows.append("Report.StateNaturalInterval.StandardDeviation=false") rows.append("Report.StateNaturalInterval.ProportionOfTime=false") rows.append("Report.StateNaturalInterval.ProportionOfTimeInSight=false") rows.append("Report.StateNaturalInterval.ConditionalProportionOfTime=false") rows.append("") rows.append("Report.StateNaturalDuration.Occurrence=false") rows.append("Report.StateNaturalDuration.TotalTime=false") rows.append("Report.StateNaturalDuration.Average=false") rows.append("Report.StateNaturalDuration.StandardDeviation=false") rows.append("Report.StateNaturalDuration.ProportionOfTime=false") rows.append("Report.StateNaturalDuration.ProportionOfTimeInSight=false") rows.append("Report.StateNaturalDuration.ConditionalProportionOfTime=false") rows.append("") rows.append("Report.StateAllInterval.Occurrence=false") rows.append("Report.StateAllInterval.TotalTime=false") rows.append("Report.StateAllInterval.Average=false") rows.append("Report.StateAllInterval.StandardDeviation=false") rows.append("Report.StateAllInterval.ProportionOfTime=false") rows.append("Report.StateAllInterval.ProportionOfTimeInSight=false") rows.append("Report.StateAllInterval.ConditionalProportionOfTime=false") rows.append("") rows.append("Report.StateAllDuration.Occurrence=true") rows.append("Report.StateAllDuration.TotalTime=true") rows.append("Report.StateAllDuration.Average=true") rows.append("Report.StateAllDuration.StandardDeviation=false") rows.append("Report.StateAllDuration.ProportionOfTime=false") rows.append("Report.StateAllDuration.ProportionOfTimeInSight=true") rows.append("Report.StateAllDuration.ConditionalProportionOfTime=false") rows.append("") rows.append("Report.EventNaturalInterval.EventCount=false") rows.append("Report.EventNaturalInterval.Occurrence=false") rows.append("Report.EventNaturalInterval.Average=false") rows.append("Report.EventNaturalInterval.StandardDeviation=false") rows.append("Report.EventNaturalInterval.ConditionalNatEventCount=false") rows.append("Report.EventNaturalInterval.ConditionalNatRate=false") rows.append("Report.EventNaturalInterval.ConditionalNatIntervalOccurance=false") rows.append("Report.EventNaturalInterval.ConditionalNatIntervalAverage=false") rows.append("Report.EventNaturalInterval.ConditionalNatIntervalStandardDeviation=false") rows.append("Report.EventNaturalInterval.ConditionalAllEventCount=false") rows.append("Report.EventNaturalInterval.ConditionalAllRate=false") rows.append("Report.EventNaturalInterval.ConditionalAllIntervalOccurance=false") rows.append("Report.EventNaturalInterval.ConditionalAllIntervalAverage=false") rows.append("Report.EventNaturalInterval.ConditionalAllIntervalStandardDeviation=false") rows.append("") rows.append("AllCodesMutuallyExclusive=true") rows.append("") for (behav, key) in all_observed_behaviors: rows.append(f"Behavior.isModified.{key}=false") rows.append(f"Behavior.isSubtracted.{key}=false") rows.append(f"Behavior.isIgnored.{key}=false") rows.append(f"Behavior.isEventAnalyzed.{key}=false") rows.append(f"Behavior.switchesOff.{key}=") rows.append("") if faf_creation_answer == "" or faf_creation_answer == OVERWRITE: try: with open(pathlib.Path(file_name_subject).with_suffix(".faf"), "w") as f_out: f_out.write("\n".join(rows)) except Exception: return False, f"File FAF not created: {sys.exc_info()[1]}" return True, "" except Exception: logging.critical("Error during exporting the events for JWatcher") dialog.error_message("exporting the events for JWatcher", sys.exc_info()) return False, ""
def pbSave_clicked(self): """ save time budget analysis results in TSV, CSV, ODS, XLS format """ def complete(l: list, max_: int) -> list: """ complete list with empty string until len = max Args: l (list): list to complete max_ (int): length of the returned list Returns: list: completed list """ while len(l) < max_: l.append("") return l logging.debug("save time budget results to file") extended_file_formats = [ "Tab Separated Values (*.tsv)", "Comma Separated Values (*.csv)", "Open Document Spreadsheet ODS (*.ods)", "Microsoft Excel Spreadsheet XLSX (*.xlsx)", "Legacy Microsoft Excel Spreadsheet XLS (*.xls)", "HTML (*.html)" ] file_formats = ["tsv", "csv", "ods", "xlsx", "xls", "html"] file_name, filter_ = QFileDialog().getSaveFileName( self, "Save Time budget analysis", "", ";;".join(extended_file_formats)) if not file_name: return outputFormat = file_formats[extended_file_formats.index(filter_)] if pathlib.Path(file_name).suffix != "." + outputFormat: file_name = str(pathlib.Path(file_name)) + "." + outputFormat # check if file with new extension already exists if pathlib.Path(file_name).is_file(): if dialog.MessageDialog( programName, f"The file {file_name} already exists.", [CANCEL, OVERWRITE]) == CANCEL: return rows = [] # 1 observation if (self.lw.count() == 1 and self.config_param.get( TIME_BUDGET_FORMAT, DEFAULT_TIME_BUDGET_FORMAT) == COMPACT_TIME_BUDGET_FORMAT): col1, indep_var_label = [], [] # add obs id col1.append(self.lw.item(0).text()) # add obs date col1.append(self.pj[OBSERVATIONS][self.lw.item(0).text()].get( "date", "")) # description col1.append( utilities.eol2space( self.pj[OBSERVATIONS][self.lw.item(0).text()].get( DESCRIPTION, ""))) header = ["Observation id", "Observation date", "Description"] # indep var for var in self.pj[OBSERVATIONS][self.lw.item(0).text()].get( INDEPENDENT_VARIABLES, {}): indep_var_label.append(var) col1.append(self.pj[OBSERVATIONS][self.lw.item(0).text()] [INDEPENDENT_VARIABLES][var]) header.extend(indep_var_label) col1.extend([ f"{self.min_time:0.3f}", f"{self.max_time:0.3f}", f"{self.max_time - self.min_time:0.3f}" ]) header.extend([ "Time budget start", "Time budget stop", "Time budget duration" ]) for col_idx in range(self.twTB.columnCount()): header.append(self.twTB.horizontalHeaderItem(col_idx).text()) rows.append(header) for row_idx in range(self.twTB.rowCount()): values = [] for col_idx in range(self.twTB.columnCount()): values.append( intfloatstr(self.twTB.item(row_idx, col_idx).text())) rows.append(col1 + values) else: # observations list rows.append(["Observations:"]) for idx in range(self.lw.count()): rows.append([""]) rows.append(["Observation id", self.lw.item(idx).text()]) rows.append([ "Observation date", self.pj[OBSERVATIONS][self.lw.item(idx).text()].get( "date", "") ]) rows.append([ "Description", utilities.eol2space( self.pj[OBSERVATIONS][self.lw.item(idx).text()].get( DESCRIPTION, "")) ]) if INDEPENDENT_VARIABLES in self.pj[OBSERVATIONS][self.lw.item( idx).text()]: rows.append(["Independent variables:"]) for var in self.pj[OBSERVATIONS][self.lw.item( idx).text()][INDEPENDENT_VARIABLES]: rows.append([ var, self.pj[OBSERVATIONS][self.lw.item( idx).text()][INDEPENDENT_VARIABLES][var] ]) if self.excluded_behaviors_list.text(): s1, s2 = self.excluded_behaviors_list.text().split(": ") rows.extend([[""], [s1] + s2.split(", ")]) rows.extend([[""], [""], ["Time budget:"]]) # write header header = [] for col_idx in range(self.twTB.columnCount()): header.append(self.twTB.horizontalHeaderItem(col_idx).text()) rows.append(header) rows.append([""]) for row in range(self.twTB.rowCount()): values = [] for col_idx in range(self.twTB.columnCount()): values.append( intfloatstr(self.twTB.item(row, col_idx).text())) rows.append(values) max_row_length = max([len(r) for r in rows]) data = tablib.Dataset() data.title = "Time budget" for row in rows: data.append(complete(row, max_row_length)) if outputFormat in ["tsv", "csv", "html"]: with open(file_name, "wb") as f: f.write(str.encode(data.export(outputFormat))) return if outputFormat in ["ods", "xlsx", "xls"]: with open(file_name, "wb") as f: f.write(data.export(outputFormat)) return