class SurfaceWorker(QtCore.QThread): """ Executes code in a seperate thread. """ started = Signal(bool) finished = Signal(bool) def __init__(self, parent=None): super().__init__(parent) self.fqpr_instances = None self.fqpr_surface = None self.opts = {} self.error = False self.exceptiontxt = None def populate(self, fqpr_instances, opts): self.fqpr_instances = fqpr_instances self.fqpr_surface = None self.opts = opts self.error = False self.exceptiontxt = None def run(self): self.started.emit(True) try: self.fqpr_surface = generate_new_surface(self.fqpr_instances, **self.opts) except Exception as e: self.error = True self.exceptiontxt = traceback.format_exc() self.finished.emit(True)
class OverwriteNavigationWorker(QtCore.QThread): """ Executes code in a seperate thread. """ started = Signal(bool) finished = Signal(bool) def __init__(self, parent=None): super().__init__(parent) self.fq_chunks = None self.fqpr_instances = [] self.error = False self.exceptiontxt = None def populate(self, fq_chunks): self.fq_chunks = fq_chunks self.error = False self.exceptiontxt = None self.fqpr_instances = [] def run(self): self.started.emit(True) try: for chnk in self.fq_chunks: self.fqpr_instances.append( overwrite_raw_navigation(chnk[0], **chnk[1])) except Exception as e: self.error = True self.exceptiontxt = traceback.format_exc() self.finished.emit(True)
class ExportGridWorker(QtCore.QThread): """ Executes code in a seperate thread. """ started = Signal(bool) finished = Signal(bool) def __init__(self, parent=None): super().__init__(parent) self.surf_instance = None self.export_type = '' self.output_path = '' self.z_pos_up = True self.bag_kwargs = {} def populate(self, surf_instance, export_type, output_path, z_pos_up, bag_kwargs): self.surf_instance = surf_instance self.export_type = export_type self.output_path = output_path self.bag_kwargs = bag_kwargs self.z_pos_up = z_pos_up def run(self): self.started.emit(True) self.surf_instance.export(self.output_path, self.export_type, self.z_pos_up, **self.bag_kwargs) self.finished.emit(True)
class LoadPointsWorker(QtCore.QThread): """ Executes code in a seperate thread. """ started = Signal(bool) finished = Signal(bool) def __init__(self, parent=None): super().__init__(parent) self.polygon = None self.azimuth = None self.project = None self.points_data = None self.error = False self.exceptiontxt = None def populate(self, polygon=None, azimuth=None, project=None): self.polygon = polygon self.azimuth = azimuth self.project = project self.points_data = None self.error = False self.exceptiontxt = None def run(self): self.started.emit(True) try: self.points_data = self.project.return_soundings_in_polygon( self.polygon) except Exception as e: self.error = True self.exceptiontxt = traceback.format_exc() self.finished.emit(True)
class ActionWorker(QtCore.QThread): """ Executes code in a seperate thread. """ started = Signal(bool) finished = Signal(bool) def __init__(self, parent=None): super().__init__(parent) self.action_container = None self.action_index = None self.result = None self.action_type = None def populate(self, action_container, action_index): self.action_container = action_container self.action_index = action_index def run(self): self.started.emit(True) # turn off progress, it creates too much clutter in the output window self.action_type = self.action_container.actions[ self.action_index].action_type self.result = self.action_container.execute_action(self.action_index) self.finished.emit(True)
class ActionWorker(QtCore.QThread): """ Executes code in a seperate thread. """ started = Signal(bool) finished = Signal(bool) def __init__(self, parent=None): super().__init__(parent) self.action_container = None self.action_index = None self.result = None self.action_type = None self.error = False self.exceptiontxt = None def populate(self, action_container, action_index): self.action_container = action_container self.action_index = action_index self.result = None self.error = False self.exceptiontxt = None def run(self): self.started.emit(True) try: self.action_type = self.action_container.actions[ self.action_index].action_type self.result = self.action_container.execute_action( self.action_index) except Exception as e: self.error = True self.exceptiontxt = traceback.format_exc() self.finished.emit(True)
class DrawSurfaceWorker(QtCore.QThread): """ On opening a new surface, you have to get the surface tiles to display as in memory geotiffs in kluster_main """ started = Signal(bool) finished = Signal(bool) def __init__(self, parent=None): super().__init__(parent) self.surface_path = None self.surf_object = None self.resolution = None self.surface_layer_name = None self.surface_data = {} self.error = False self.exceptiontxt = None def populate(self, surface_path, surf_object, resolution, surface_layer_name): self.surface_path = surface_path self.surf_object = surf_object self.resolution = resolution # handle optional hillshade layer self.surface_layer_name = surface_layer_name self.error = False self.exceptiontxt = None self.surface_data = {} def run(self): self.started.emit(True) try: if self.surface_layer_name == 'tiles': x, y = self.surf_object.get_tile_boundaries() self.surface_data = [x, y] else: if self.surface_layer_name == 'hillshade': surface_layer_name = 'depth' else: surface_layer_name = self.surface_layer_name for resolution in self.resolution: self.surface_data[resolution] = {} chunk_count = 1 for geo_transform, maxdim, data in self.surf_object.get_chunks_of_tiles( resolution=resolution, layer=surface_layer_name, nodatavalue=np.float32(np.nan), z_positive_up=False, for_gdal=True): data = list(data.values()) self.surface_data[resolution][ self.surface_layer_name + '_{}'.format(chunk_count)] = [data, geo_transform] chunk_count += 1 except Exception as e: self.error = True self.exceptiontxt = traceback.format_exc() self.finished.emit(True)
class ExportTracklinesWorker(QtCore.QThread): """ Executes code in a seperate thread. """ started = Signal(bool) finished = Signal(bool) def __init__(self, parent=None): super().__init__(parent) self.fq_chunks = None self.line_names = None self.fqpr_instances = [] self.export_type = '' self.mode = '' self.output_path = '' self.error = False self.exceptiontxt = None def populate(self, fq_chunks, line_names, export_type, basic_mode, line_mode, output_path): if basic_mode: self.mode = 'basic' elif line_mode: self.mode = 'line' self.fqpr_instances = [] self.line_names = line_names self.fq_chunks = fq_chunks self.export_type = export_type self.output_path = output_path self.error = False self.exceptiontxt = None def export_process(self, fq): if self.mode == 'basic': fq.export_tracklines_to_file(linenames=None, output_file=self.output_path, file_format=self.export_type) elif self.mode == 'line': fq.export_tracklines_to_file(linenames=self.line_names, output_file=self.output_path, file_format=self.export_type) return fq def run(self): self.started.emit(True) try: for chnk in self.fq_chunks: self.fqpr_instances.append(self.export_process(chnk[0])) except Exception as e: self.error = True self.exceptiontxt = traceback.format_exc() self.finished.emit(True)
class PatchTestUpdateWorker(QtCore.QThread): """ Executes code in a seperate thread. """ started = Signal(bool) finished = Signal(bool) def __init__(self, parent=None): super().__init__(parent) self.fqprs = None self.newvalues = [] self.headindex = None self.prefixes = None self.timestamps = None self.serial_number = None self.polygon = None self.result = [] self.error = False self.exceptiontxt = None def populate(self, fqprs=None, newvalues=None, headindex=None, prefixes=None, timestamps=None, serial_number=None, polygon=None): self.fqprs = fqprs self.newvalues = newvalues self.headindex = headindex self.prefixes = prefixes self.timestamps = timestamps self.serial_number = serial_number self.polygon = polygon self.result = [] self.error = False self.exceptiontxt = None def run(self): self.started.emit(True) try: self.result = reprocess_fqprs(self.fqprs, self.newvalues, self.headindex, self.prefixes, self.timestamps, self.serial_number, self.polygon) except Exception as e: self.error = True self.exceptiontxt = traceback.format_exc() self.finished.emit(True)
class OutputWrapper(QtCore.QObject): outputWritten = Signal(object, object) def __init__(self, parent, stdout=True): QtCore.QObject.__init__(self, parent) if stdout: self._stream = sys.stdout sys.stdout = self else: self._stream = sys.stderr sys.stderr = self self._stdout = stdout def write(self, text): # self._stream.write(text) self.outputWritten.emit(text, self._stdout) def __getattr__(self, name): return getattr(self._stream, name) def __del__(self): try: if self._stdout: sys.stdout = self._stream else: sys.stderr = self._stream except AttributeError: pass
class ExportWorker(QtCore.QThread): """ Executes code in a seperate thread. """ started = Signal(bool) finished = Signal(bool) def __init__(self, parent=None): super().__init__(parent) self.fq_chunks = None self.fqpr_instances = [] self.export_type = '' self.z_pos_down = False self.delimiter = ' ' self.filterset = False self.separateset = False def populate(self, fq_chunks, export_type, z_pos_down, delimiter, filterset, separateset): self.fq_chunks = fq_chunks self.export_type = export_type self.z_pos_down = z_pos_down if delimiter == 'comma': self.delimiter = ',' elif delimiter == 'space': self.delimiter = ' ' else: raise ValueError( 'ExportWorker: Expected either "comma" or "space", received {}' .format(delimiter)) self.filterset = filterset self.separateset = separateset def export_process(self, fq): fq.export_pings_to_file(file_format=self.export_type, csv_delimiter=self.delimiter, filter_by_detection=self.filterset, z_pos_down=self.z_pos_down, export_by_identifiers=self.separateset) return fq def run(self): self.started.emit(True) for chnk in self.fq_chunks: self.fqpr_instances.append(self.export_process(chnk[0])) self.finished.emit(True)
class OverwriteNavigationWorker(QtCore.QThread): """ Executes code in a seperate thread. """ started = Signal(bool) finished = Signal(bool) def __init__(self, parent=None): super().__init__(parent) self.fq_chunks = None self.fqpr_instances = [] def populate(self, fq_chunks): self.fq_chunks = fq_chunks def run(self): self.started.emit(True) for chnk in self.fq_chunks: self.fqpr_instances.append( overwrite_raw_navigation(chnk[0], **chnk[1])) self.finished.emit(True)
class ExportGridWorker(QtCore.QThread): """ Executes code in a seperate thread. """ started = Signal(bool) finished = Signal(bool) def __init__(self, parent=None): super().__init__(parent) self.surf_instance = None self.export_type = '' self.output_path = '' self.z_pos_up = True self.bag_kwargs = {} self.error = False self.exceptiontxt = None def populate(self, surf_instance, export_type, output_path, z_pos_up, bag_kwargs): self.surf_instance = surf_instance self.export_type = export_type self.output_path = output_path self.bag_kwargs = bag_kwargs self.z_pos_up = z_pos_up self.error = False self.exceptiontxt = None def run(self): self.started.emit(True) try: # None in the 4th arg to indicate you want to export all resolutions self.surf_instance.export(self.output_path, self.export_type, self.z_pos_up, None, **self.bag_kwargs) except Exception as e: self.error = True self.exceptiontxt = traceback.format_exc() self.finished.emit(True)
class SurfaceWorker(QtCore.QThread): """ Executes code in a seperate thread. """ started = Signal(bool) finished = Signal(bool) def __init__(self, parent=None): super().__init__(parent) self.fqpr_instances = None self.fqpr_surface = None self.opts = {} def populate(self, fqpr_instances, opts): self.fqpr_instances = fqpr_instances self.opts = opts def run(self): self.started.emit(True) self.fqpr_surface = generate_new_surface(self.fqpr_instances, **self.opts) self.finished.emit(True)
class DrawNavigationWorker(QtCore.QThread): """ On opening a project, you have to get the navigation for each line and draw it in the 2d view """ started = Signal(bool) finished = Signal(bool) def __init__(self, parent=None): super().__init__(parent) self.project = None self.new_fqprs = None self.line_data = {} self.error = False self.exceptiontxt = None def populate(self, project, new_fqprs): self.project = project self.new_fqprs = new_fqprs self.error = False self.exceptiontxt = None self.line_data = {} def run(self): self.started.emit(True) try: for fq in self.new_fqprs: print('building tracklines for {}...'.format(fq)) for ln in self.project.return_project_lines( proj=fq, relative_path=True): lats, lons = self.project.return_line_navigation(ln) self.line_data[ln] = [lats, lons] except Exception as e: self.error = True self.exceptiontxt = traceback.format_exc() self.finished.emit(True)
class DeletableListWidget(QtWidgets.QListWidget): """ Inherit from the ListWidget and allow the user to press delete or backspace key to remove items """ files_updated = Signal(bool) def __init__(self, *args, **kwrds): super().__init__(*args, **kwrds) self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) def keyReleaseEvent(self, event): if event.matches(QtGui.QKeySequence.Delete) or event.matches(QtGui.QKeySequence.Back): for itm in self.selectedItems(): self.takeItem(self.row(itm)) self.files_updated.emit(True)
class KlusterActions(QtWidgets.QTreeView): """ Tree view showing the currently available actions and files generated from fqpr_intelligence """ execute_action = Signal(object) exclude_queued_file = Signal(str) exclude_unmatched_file = Signal(str) undo_exclude_file = Signal(list) def __init__(self, parent=None, settings=None): super().__init__(parent) self.external_settings = settings self.parent = parent self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) self.model = QtGui.QStandardItemModel() # row can be 0 even when there are more than 0 rows self.setModel(self.model) self.setUniformRowHeights(False) self.setAcceptDrops(False) self.viewport().setAcceptDrops(False) # viewport is the total rendered area, this is recommended from my reading # ExtendedSelection - allows multiselection with shift/ctrl self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) # set up the context menu per item self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.right_click_menu_files = None self.setup_menu() # makes it so no editing is possible with the table self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) self.categories = ['Next Action', 'All Actions', 'Queued Files', 'Unmatched Files'] self.tree_data = {} self.actions = None self.unmatched = None self.exclude_buffer = [] self.start_button = QtWidgets.QPushButton('Start Process') self.start_button.clicked.connect(self.start_process) self.start_button.setDisabled(True) self.auto_checkbox = QtWidgets.QCheckBox('Auto') self.auto_checkbox.setCheckable(True) self.auto_checkbox.clicked.connect(self.auto_process) self.button_widget = QtWidgets.QWidget() self.button_sizer = QtWidgets.QHBoxLayout() self.button_sizer.addWidget(self.start_button) self.button_sizer.addWidget(self.auto_checkbox) self.button_sizer.setAlignment(QtCore.Qt.AlignLeft) self.button_widget.setLayout(self.button_sizer) self.button_widget.setToolTip('Start the action below by clicking "Start Process".\n' + 'If the "Start Process" button is greyed out, there is no viable action to run.\n\n' + 'If the "Auto" check box is checked, Kluster will automatically run all actions as they appear.\n' + 'You will not need to use the "Start Process" button with "Auto" enabled.') self.stop_auto = Event() self.stop_auto.set() self.auto_thread = AutoThread(self.stop_auto, self.emit_auto_signal) self.customContextMenuRequested.connect(self.show_context_menu) self.configure() self.read_settings() @property def settings_object(self): if self.external_settings: return self.external_settings else: return QtCore.QSettings("NOAA", "Kluster") @property def is_auto(self): return self.auto_checkbox.isChecked() def setup_menu(self): """ Setup the menu that is generated on right clicking in the action tree. """ self.right_click_menu_files = QtWidgets.QMenu('menu', self) exclude_dat = QtWidgets.QAction('Exclude File', self) exclude_dat.triggered.connect(self.exclude_file_event) undo_exclude_dat = QtWidgets.QAction('Undo Exclude', self) undo_exclude_dat.triggered.connect(self.undo_exclude) self.right_click_menu_files.addAction(exclude_dat) self.right_click_menu_files.addAction(undo_exclude_dat) def show_context_menu(self): """ Open the right click menu if you right click a queued or unmatched file """ index = self.currentIndex() parent_name = index.parent().data() if parent_name in ['Queued Files', 'Unmatched Files']: self.right_click_menu_files.exec_(QtGui.QCursor.pos()) def exclude_file_event(self, e): """ If user right clicks on a queued or unmatched file and selects exclude file, triggers this event. Emit signals depending on what kind of item the user selects. Parameters ---------- e: QEvent on menu button click """ selected_indexes = self.selectionModel().selectedIndexes() all_data = [] xclude_data = [] # allow multiselect, will emit for each selected and append the chunk to the buffer # have to do this in a first pass, as if we emit in the loop, the index will change when the line is removed for index in selected_indexes: parent_name = index.parent().data() sel_data = index.data() all_data.append([sel_data, parent_name]) for sel_data, parent_name in all_data: if parent_name == 'Queued Files': self.exclude_queued_file.emit(sel_data) xclude_data.append(sel_data) elif parent_name == 'Unmatched Files': self.exclude_unmatched_file.emit(sel_data) xclude_data.append(sel_data) self.exclude_buffer.append(xclude_data) def undo_exclude(self, e): if self.exclude_buffer: self.undo_exclude_file.emit(self.exclude_buffer[-1]) self.exclude_buffer = self.exclude_buffer[:-1] def configure(self): """ Clears all data currently in the tree and repopulates with loaded actions """ self.model.clear() self.model.setHorizontalHeaderLabels(['Actions']) for cnt, c in enumerate(self.categories): parent = QtGui.QStandardItem(c) self.tree_data[c] = [parent] self.model.appendRow(parent) self.setFirstColumnSpanned(cnt, self.rootIndex(), True) if c == 'Next Action': proj_child = QtGui.QStandardItem('') # empty entry to overwrite with setIndexWidget parent.appendRow(proj_child) qindex_button = parent.child(0, 0).index() self.setIndexWidget(qindex_button, self.button_widget) self.expand(parent.index()) def _update_next_action(self, parent: QtGui.QStandardItem, actions: list): """ Take the provided actions and populate the 'Next Action' Tree item Parameters ---------- parent The parent item we are adding to actions list of FqprActions sorted by priority, we are only interested in the first (the next one) """ parent.removeRows(1, parent.rowCount() - 1) if actions: next_action = actions[0] action_text = next_action.text if next_action.input_files: input_files = ['Input Files:'] + ['- ' + f for f in next_action.input_files] else: input_files = ['Input Files: None'] data = [action_text] + input_files for d in data: proj_child = QtGui.QStandardItem(d) ttip = self._build_action_tooltip(next_action) proj_child.setToolTip(ttip) parent.appendRow(proj_child) self.tree_data['Next Action'].append(d) self.start_button.setDisabled(False) self.expand(parent.index()) def _update_all_actions(self, parent: QtGui.QStandardItem, actions: list): """ Take the provided actions and populate the 'All Actions' Tree item with the text attribute from each action Parameters ---------- parent The parent item we are adding to actions list of FqprActions sorted by priority, we are only interested in the text attribute of each """ parent.removeRows(0, parent.rowCount()) self.tree_data['All Actions'] = [self.tree_data['All Actions'][0]] if actions: for act in actions: proj_child = QtGui.QStandardItem(act.text) ttip = self._build_action_tooltip(act) proj_child.setToolTip(ttip) parent.appendRow(proj_child) self.tree_data['All Actions'].append(act.text) def _build_action_tooltip(self, action): """ Take the provided action and build a summary tooltip string Parameters ---------- action FqprAction Returns ------- str tooltip string """ if action.input_files: ttip = '{}\n\nPriority:{}\nInput Files:\n-{}'.format(action.text, action.priority, '\n-'.join(action.input_files)) elif action.priority == 5: # process multibeam action ttip = '{}\n\nPriority:{}\nRun Orientation:{}\nRun Correct Beam Vectors:{}\n'.format(action.text, action.priority, action.kwargs['run_orientation'], action.kwargs['run_beam_vec']) ttip += 'Run Sound Velocity:{}\nRun Georeference:{}\nRun TPU:{}'.format(action.kwargs['run_svcorr'], action.kwargs['run_georef'], action.kwargs['run_tpu']) if action.kwargs['run_georef']: if action.kwargs['use_epsg']: ttip += '\nEPSG: {}\nVertical Reference: {}'.format(action.kwargs['epsg'], action.kwargs['vert_ref']) else: ttip += '\nCoordinate System: {}\nVertical Reference: {}'.format(action.kwargs['coord_system'], action.kwargs['vert_ref']) if 'only_this_line' in action.kwargs: if action.kwargs['only_this_line']: ttip += '\nLine: {}'.format(action.kwargs['only_this_line']) else: ttip = '{}\n\nPriority:{}'.format(action.text, action.priority) return ttip def _update_queued_files(self, parent: QtGui.QStandardItem, actions: list): """ Take the provided actions and populate the 'Queued Files' Tree item with the input_files attribute from each action Parameters ---------- parent The parent item we are adding to actions list of FqprActions sorted by priority, we are only interested in the input_files attribute of each """ parent.removeRows(0, parent.rowCount()) self.tree_data['Queued Files'] = [self.tree_data['Queued Files'][0]] fils = [] if actions: for act in actions: fils += act.input_files for f in fils: proj_child = QtGui.QStandardItem(f) parent.appendRow(proj_child) self.tree_data['Queued Files'].append(f) def _update_unmatched(self, parent: QtGui.QStandardItem, unmatched: dict): """ Take the provided actions and populate the 'Queued Files' Tree item with the input_files attribute from each action Parameters ---------- parent The parent item we are adding to unmatched dict of 'filename: reason not matched' for each unmatched file """ parent.removeRows(0, parent.rowCount()) self.tree_data['Unmatched Files'] = [self.tree_data['Unmatched Files'][0]] if unmatched: for unmatched_file, reason in unmatched.items(): proj_child = QtGui.QStandardItem(unmatched_file) proj_child.setToolTip(reason) parent.appendRow(proj_child) self.tree_data['Unmatched Files'].append(unmatched_file) def update_actions(self, actions: list = None, unmatched: dict = None, process_mode: str = None): """ Method driven by kluster_intelligence, can be used to either update actions, unmatched, or both. Parameters ---------- actions optional, list of FqprActions sorted by priority unmatched optional, dict of 'filename: reason not matched' for each unmatched file """ # check against None here, as there are three possible states for actions/unmatched, ex: # - actions is None -> do not update actions # - actions is an empty list -> update actions with empty (clear actions) # - actions is a populated list -> update actions with new actions if actions is not None: self.actions = actions if unmatched is not None: self.unmatched = unmatched self.model.setHorizontalHeaderLabels(['Actions ({})'.format(process_mode)]) for cnt, c in enumerate(self.categories): parent = self.tree_data[c][0] if c == 'Next Action' and actions is not None: self._update_next_action(parent, actions) elif c == 'All Actions' and actions is not None: self._update_all_actions(parent, actions) elif c == 'Queued Files' and actions is not None: self._update_queued_files(parent, actions) elif c == 'Unmatched Files' and unmatched is not None: self._update_unmatched(parent, unmatched) def start_process(self): """ Emit the execute_action signal to trigger processing in kluster_main """ self.start_button.setDisabled(True) self.execute_action.emit(False) def auto_process(self): if self.is_auto: print('Enabling autoprocessing') self.auto_thread = AutoThread(self.stop_auto, self.emit_auto_signal) self.stop_auto.clear() self.auto_thread.start() else: self.stop_auto.set() self.save_settings() def emit_auto_signal(self): if self.is_auto: self.start_button.setDisabled(True) self.execute_action.emit(True) def save_settings(self): """ Save the settings to the Qsettings registry """ settings = self.settings_object settings.setValue('Kluster/actions_window_auto', self.is_auto) def read_settings(self): """ Read from the Qsettings registry """ settings = self.settings_object try: self.auto_checkbox.setChecked(settings.value('Kluster/actions_window_auto').lower() == 'true') self.auto_process() except AttributeError: # no settings exist yet for this app, .lower failed pass
class ExportWorker(QtCore.QThread): """ Executes code in a seperate thread. """ started = Signal(bool) finished = Signal(bool) def __init__(self, parent=None): super().__init__(parent) self.fq_chunks = None self.line_names = None self.datablock = [] self.fqpr_instances = [] self.export_type = '' self.mode = '' self.z_pos_down = False self.delimiter = ' ' self.filterset = False self.separateset = False self.error = False self.exceptiontxt = None def populate(self, fq_chunks, line_names, datablock, export_type, z_pos_down, delimiter, filterset, separateset, basic_mode, line_mode, points_mode): if basic_mode: self.mode = 'basic' elif line_mode: self.mode = 'line' elif points_mode: self.mode = 'points' self.fqpr_instances = [] self.line_names = line_names self.datablock = datablock self.fq_chunks = fq_chunks self.export_type = export_type self.z_pos_down = z_pos_down if delimiter == 'comma': self.delimiter = ',' elif delimiter == 'space': self.delimiter = ' ' else: raise ValueError( 'ExportWorker: Expected either "comma" or "space", received {}' .format(delimiter)) self.filterset = filterset self.separateset = separateset self.error = False self.exceptiontxt = None def export_process(self, fq, datablock=None): if self.mode == 'basic': fq.export_pings_to_file(file_format=self.export_type, csv_delimiter=self.delimiter, filter_by_detection=self.filterset, z_pos_down=self.z_pos_down, export_by_identifiers=self.separateset) elif self.mode == 'line': fq.export_lines_to_file(linenames=self.line_names, file_format=self.export_type, csv_delimiter=self.delimiter, filter_by_detection=self.filterset, z_pos_down=self.z_pos_down, export_by_identifiers=self.separateset) else: fq.export_soundings_to_file(datablock=datablock, file_format=self.export_type, csv_delimiter=self.delimiter, filter_by_detection=self.filterset, z_pos_down=self.z_pos_down) return fq def run(self): self.started.emit(True) try: if self.mode in ['basic', 'line']: for chnk in self.fq_chunks: self.fqpr_instances.append(self.export_process(chnk[0])) else: fq = self.fq_chunks[0][0] self.fqpr_instances.append( self.export_process(fq, datablock=self.datablock)) except Exception as e: self.error = True self.exceptiontxt = traceback.format_exc() self.finished.emit(True)
class KlusterExplorer(QtWidgets.QTableWidget): """ Instance of QTableWidget designed to display the converted Kluster attribution on a line by line basis. Allows for some drag and drop sorting and other features. Selecting a row will feed the attribution for that converted zarr object to the KlusterAttribution object. """ # signals must be defined on the class, not the instance of the class row_selected = Signal(object) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setObjectName('kluster_explorer') self.setDragEnabled(True) # enable support for dragging table items self.setAcceptDrops(True) # enable drop events self.viewport().setAcceptDrops( True ) # viewport is the total rendered area, this is recommended from my reading self.setDragDropOverwriteMode( False) # False makes sure we don't overwrite rows on dragging self.setDropIndicatorShown(True) self.setSortingEnabled(True) # ExtendedSelection - allows multiselection with shift/ctrl self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) self.setDragDropMode(QtWidgets.QAbstractItemView.DragDrop) # makes it so no editing is possible with the table self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) self.cellDoubleClicked.connect(self.view_full_attribution) self.cellClicked.connect(self.update_attribution) self.mode = '' self.headr = [] self.set_mode('line') self.row_full_attribution = {} self.row_translated_attribution = {} def keyReleaseEvent(self, e): """ Catch keyboard driven events to delete entries or select new rows Parameters ---------- e: QEvent generated on keyboard key release """ if e.matches(QtGui.QKeySequence.Delete) or e.matches( QtGui.QKeySequence.Back): rows = sorted(set(item.row() for item in self.selectedItems())) for row in rows: self.removeRow(row) elif int(e.key()) in [ 16777237, 16777235 ]: # 237 is down arrow, 235 is up arrow, user selected a new row with arrow keys rows = sorted(set(item.row() for item in self.selectedItems())) self.update_attribution(rows[0], 0) def dragEnterEvent(self, e): """ Catch mouse drag enter events to block things not move/read related Parameters ---------- e: QEvent which is sent to a widget when a drag and drop action enters it """ if e.source( ) == self: # allow MIME type files, have a 'file://', 'http://', etc. e.accept() else: e.ignore() def dragMoveEvent(self, e): """ Catch mouse drag enter events to block things not move/read related Parameters ---------- e: QEvent which is sent while a drag and drop action is in progress """ if e.source() == self: e.accept() else: e.ignore() def dropEvent(self, e): """ On drag and drop, handle either reordering of rows or incoming new data from zarr store Parameters ---------- e: QEvent which is sent when a drag and drop action is completed """ if not e.isAccepted() and e.source() == self: e.setDropAction(QtCore.Qt.MoveAction) drop_row = self.drop_on(e) self.custom_move_row(drop_row) else: e.ignore() def drop_on(self, e): """ Returns the integer row index of the insertion point on drag and drop Parameters ---------- e: QEvent which is sent when a drag and drop action is completed Returns ------- int: row index """ index = self.indexAt(e.pos()) if not index.isValid(): return self.rowCount() return index.row() + 1 if self.is_below(e.pos(), index) else index.row() def is_below(self, pos, index): """ Using the event position and the row rect shape, figure out if the new row should go above the index row or below. Parameters ---------- pos: position of the cursor at the event time index: row index at the cursor Returns ------- bool: True if new row should go below, False otherwise """ rect = self.visualRect(index) margin = 2 if pos.y() - rect.top() < margin: return False elif rect.bottom() - pos.y() < margin: return True return rect.contains(pos, True) and pos.y() >= rect.center().y() def custom_move_row(self, drop_row): """ Something I stole from someone online. Will get the row indices of the selected rows and insert those rows at the drag-n-drop mouse cursor location. Will even account for relative cursor position to the center of the row, see is_below. Parameters ---------- drop_row: int, row index of the insertion point for the drag and drop """ self.setSortingEnabled(False) rows = sorted(set( item.row() for item in self.selectedItems())) # pull all the selected rows rows_to_move = [[ QtWidgets.QTableWidgetItem(self.item(row_index, column_index)) for column_index in range(self.columnCount()) ] for row_index in rows] # get the data for the rows for row_index in reversed(rows): self.removeRow(row_index) if row_index < drop_row: drop_row -= 1 for row_index, data in enumerate(rows_to_move): row_index += drop_row self.insertRow(row_index) for column_index, column_data in enumerate(data): self.setItem(row_index, column_index, column_data) for row_index in range(len(rows_to_move)): for i in range(int(len(self.headr))): self.item(drop_row + row_index, i).setSelected(True) self.setSortingEnabled(True) def set_mode(self, explorer_mode: str): """ Use this option to toggle between line mode (for selecting and displaying line attribution) and point mode (for displaying data for points selected in 3d view) Parameters ---------- explorer_mode one of 'line' and 'point' """ self.mode = explorer_mode self.clear_explorer_data() if explorer_mode == 'line': self.setColumnCount(6) self.headr = [ 'Name', 'Survey Identifier', 'EPSG', 'Min Time', 'Max Time', 'Source' ] self.setHorizontalHeaderLabels(self.headr) self.horizontalHeader().setStretchLastSection(True) self.setColumnWidth(0, 250) self.setColumnWidth(1, 150) self.setColumnWidth(2, 80) self.setColumnWidth(3, 150) self.setColumnWidth(4, 150) self.setColumnWidth(5, 200) elif explorer_mode == 'point': self.setColumnCount(10) self.headr = [ 'index', 'line', 'time', 'beam', 'x', 'y', 'z', 'tvu', 'status', 'Source' ] self.setHorizontalHeaderLabels(self.headr) self.horizontalHeader().setStretchLastSection(True) self.setColumnWidth(0, 60) self.setColumnWidth(1, 250) self.setColumnWidth(2, 200) self.setColumnWidth(3, 50) self.setColumnWidth(4, 80) self.setColumnWidth(5, 80) self.setColumnWidth(6, 80) self.setColumnWidth(7, 80) self.setColumnWidth(8, 80) self.setColumnWidth(9, 150) def update_attribution(self, row, column): """ If in point mode, emit the index for the point that the user selected so that we can see it highlighted in the 3d view. Parameters ---------- row: int, row number column: int, column number """ if self.mode == 'point': point_index = int(self.item(row, 0).text()) self.row_selected.emit(point_index) def view_full_attribution(self, row, column): """ On double click, this will bring up a message box containing the full attribution for the converted object in a message box. It's pretty ugly at this point, will need to make something better from this in the future. Parameters ---------- row: int, row number column: int, column number """ if self.mode == 'line': name_item = self.item(row, 0) linename = name_item.text() info = QtWidgets.QMessageBox() info.setWindowTitle('Full Attribution') info.setText(pprint.pformat(self.row_full_attribution[linename])) info.setTextInteractionFlags(QtCore.Qt.TextSelectableByMouse) result = info.exec_() def translate_fqpr_attribution(self, attrs): """ Gets the attribution from the provided fqpr_generation.FPQR object and translates it for viewing Will return the selection of attributes that are needed to populate the table fqpr = fully qualified ping record, the term for the datastore in kluster Parameters ---------- attrs: dict, fqpr_generation.FPQR attribution Returns ------- translated_attrs: list of OrderedDict, attributes by line that match self.headr order attrs: OrderedDict, raw attribution from the zarr store """ translated_attrs = [] if 'multibeam_files' in attrs: line_names = list(attrs['multibeam_files'].items()) for cnt, ln in enumerate(line_names): # ln is tuple like ('0015_20200304_070725_S250.all', [1583305645.423, 1583305889.905]) newline_attr = OrderedDict([(h, '') for h in self.headr]) newline_attr['Source'] = os.path.split(attrs['output_path'])[1] newline_attr['Name'] = ln[0] if 'survey_number' in attrs: newline_attr['Survey Identifier'] = ', '.join( attrs['survey_number']) else: newline_attr['Survey Identifier'] = '' min_line_time = ln[1][0] max_line_time = ln[1][1] newline_attr['Min Time'] = datetime.utcfromtimestamp( min_line_time).strftime('%D %H%M%S') newline_attr['Max Time'] = datetime.utcfromtimestamp( max_line_time).strftime('%D %H%M%S') if 'horizontal_crs' in attrs: newline_attr['EPSG'] = str(attrs['horizontal_crs']) translated_attrs.append(newline_attr) return translated_attrs, attrs def build_line_attribution(self, linename, raw_attrs): """ Uses line name and attribution for the project that line is associated with. Returns translated attribution for that line. If it is the first time seeing this line, will build out all the line attribution for all lines in raw_attrs Parameters ---------- linename: str, line name raw_attrs: dict, attribution of fqpr_generation.Fqpr instance that the linename is in """ if linename in self.row_translated_attribution: line_data = self.row_translated_attribution[linename] else: line_data = None data, raw_attribution = self.translate_fqpr_attribution(raw_attrs) for line in data: self.row_full_attribution[line['Name']] = raw_attribution self.row_translated_attribution[line['Name']] = line if line['Name'] == linename: line_data = line if line_data is None: print( 'build_line_attribution: Unable to find attribution for line {}' .format(linename)) return line_data def populate_explorer_with_lines(self, linename, raw_attrs): """ Uses line name and attribution for the project that line is associated with. Returns translated attribution for that line. If it is the first time seeing this line, will build out all the line attribution for all lines in raw_attrs Parameters ---------- linename: str, line name raw_attrs: dict, attribution of fqpr_generation.Fqpr instance that the linename is in """ self.setSortingEnabled(False) if self.mode != 'line': self.set_mode('line') line_data = self.build_line_attribution(linename, raw_attrs) if line_data is not None: next_row = self.rowCount() self.insertRow(next_row) for column_index, column_data in enumerate(line_data): item = QtWidgets.QTableWidgetItem(line_data[column_data]) if self.headr[column_index] == 'Source': item.setToolTip(raw_attrs['output_path']) self.setItem(next_row, column_index, item) self.setSortingEnabled(True) def populate_explorer_with_points(self, point_index: np.array, linenames: np.array, point_times: np.array, beam: np.array, x: np.array, y: np.array, z: np.array, tvu: np.array, status: np.array, id: np.array): """ Show the attributes for each point, where each point is in its own row. All the inputs are of the same size, where size equals the number of points Parameters ---------- point_index point index for the points, corresponds to the index of the point in the 3dview selected points linenames multibeam file name that the points come from point_times time of the soundings/points beam beam number of the points x easting of the points y northing of the points z depth of the points tvu total vertical uncertainty of the points status rejected/amplitude/phase return qualifier of the points id data container that the points come from """ self.setSortingEnabled(False) if self.mode != 'point': self.set_mode('point') self.clear_explorer_data() if z.any(): converted_status = np.full(status.shape[0], '', dtype=object) converted_status[np.where(status == 0)[0]] = 'amplitude' converted_status[np.where(status == 1)[0]] = 'phase' converted_status[np.where(status == 2)[0]] = 'rejected' for cnt, idx in enumerate(point_index): next_row = self.rowCount() self.insertRow(next_row) self.setItem(next_row, 0, QtWidgets.QTableWidgetItem(str(idx))) self.setItem(next_row, 1, QtWidgets.QTableWidgetItem(linenames[cnt])) formattedtime = datetime.fromtimestamp( float(point_times[cnt]), tz=timezone.utc).strftime('%c') self.setItem(next_row, 2, QtWidgets.QTableWidgetItem(str(formattedtime))) self.setItem(next_row, 3, QtWidgets.QTableWidgetItem(str(int(beam[cnt])))) self.setItem(next_row, 4, QtWidgets.QTableWidgetItem(str(x[cnt]))) self.setItem(next_row, 5, QtWidgets.QTableWidgetItem(str(y[cnt]))) self.setItem(next_row, 6, QtWidgets.QTableWidgetItem(str(round(z[cnt], 3)))) self.setItem( next_row, 7, QtWidgets.QTableWidgetItem(str(round(tvu[cnt], 3)))) self.setItem( next_row, 8, QtWidgets.QTableWidgetItem(str(converted_status[cnt]))) self.setItem(next_row, 9, QtWidgets.QTableWidgetItem(str(id[cnt]))) self.setSortingEnabled(True) def clear_explorer_data(self): """ Clear out the data but keep the headers. Also set the row count to zero so that the next insertRow call is at the first line. """ self.clearContents() self.setRowCount(0)
class KlusterProjectTree(QtWidgets.QTreeView): """ Tree widget to view the surfaces and converted data folders/lines associated with a FqprProject. fqpr = fully qualified ping record, the term for the datastore in kluster """ # signals must be defined on the class, not the instance of the class file_added = Signal(object) fqpr_selected = Signal(str) surface_selected = Signal(str) lines_selected = Signal(object) all_lines_selected = Signal(bool) surface_layer_selected = Signal(str, str, bool) close_fqpr = Signal(str) close_surface = Signal(str) manage_fqpr = Signal(str) manage_surface = Signal(str) load_console_fqpr = Signal(str) load_console_surface = Signal(str) show_explorer = Signal(str) zoom_extents_fqpr = Signal(str) zoom_extents_surface = Signal(str) reprocess_instance = Signal(str) update_surface = Signal(str) def __init__(self, parent=None): super().__init__(parent) self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) self.model = QtGui.QStandardItemModel() self.setModel(self.model) self.setUniformRowHeights(True) # ExtendedSelection - allows multiselection with shift/ctrl self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) self.setDragDropMode(QtWidgets.QAbstractItemView.NoDragDrop) # makes it so no editing is possible with the table self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) # set up the context menu per item self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.right_click_menu_converted = None self.right_click_menu_surfaces = None self.setup_menu() self.categories = ['Project', 'Vessel File', 'Converted', 'Surfaces'] self.tree_data = {} self.shown_layers = [] self.clicked.connect(self.item_selected) self.customContextMenuRequested.connect(self.show_context_menu) self.configure() def setup_menu(self): """ Setup the menu that is generated on right clicking in the project tree. """ self.right_click_menu_converted = QtWidgets.QMenu('menu', self) self.right_click_menu_surfaces = QtWidgets.QMenu('menu', self) close_dat = QtWidgets.QAction('Close', self) close_dat.triggered.connect(self.close_item_event) reprocess = QtWidgets.QAction('Reprocess', self) reprocess.triggered.connect(self.reprocess_event) load_in_console = QtWidgets.QAction('Load in Console', self) load_in_console.triggered.connect(self.load_in_console_event) show_explorer_action = QtWidgets.QAction('Show in Explorer', self) show_explorer_action.triggered.connect(self.show_in_explorer_event) zoom_extents = QtWidgets.QAction('Zoom Extents', self) zoom_extents.triggered.connect(self.zoom_extents_event) update_surface = QtWidgets.QAction('Update Surface', self) update_surface.triggered.connect(self.update_surface_event) manage_fqpr = QtWidgets.QAction('Manage', self) manage_fqpr.triggered.connect(self.manage_data_event) self.right_click_menu_converted.addAction(manage_fqpr) self.right_click_menu_converted.addAction(load_in_console) self.right_click_menu_converted.addAction(show_explorer_action) self.right_click_menu_converted.addAction(zoom_extents) self.right_click_menu_converted.addSeparator() self.right_click_menu_converted.addAction(reprocess) self.right_click_menu_converted.addAction(close_dat) self.right_click_menu_surfaces.addAction(manage_fqpr) self.right_click_menu_surfaces.addAction(load_in_console) self.right_click_menu_surfaces.addAction(show_explorer_action) self.right_click_menu_surfaces.addAction(zoom_extents) self.right_click_menu_surfaces.addSeparator() self.right_click_menu_surfaces.addAction(update_surface) self.right_click_menu_surfaces.addAction(close_dat) def show_context_menu(self): """ Generate a close option when you right click on a mid level item (an fqpr instance or a fqpr surface instance). Emit the appropriate signal and let kluster_main handle the rest. """ index = self.currentIndex() sel_name = index.data() mid_lvl_name = index.parent().data() if mid_lvl_name == 'Converted': self.right_click_menu_converted.exec_(QtGui.QCursor.pos()) elif mid_lvl_name == 'Surfaces': self.right_click_menu_surfaces.exec_(QtGui.QCursor.pos()) def reprocess_event(self, e: QtCore.QEvent): """ Trigger full reprocessing of the selected fqpr instance Parameters ---------- e QEvent on menu button click """ index = self.currentIndex() mid_lvl_name = index.parent().data() sel_data = index.data() if mid_lvl_name == 'Converted': self.reprocess_instance.emit(sel_data) def load_in_console_event(self, e: QtCore.QEvent): """ We want the ability for the user to right click an object and load it in the console. Here we emit the correct signal for the main to determine how to load it in the console. Parameters ---------- e QEvent on menu button click """ index = self.currentIndex() mid_lvl_name = index.parent().data() sel_data = index.data() if mid_lvl_name == 'Converted': self.load_console_fqpr.emit(sel_data) elif mid_lvl_name == 'Surfaces': self.load_console_surface.emit(sel_data) def show_in_explorer_event(self, e: QtCore.QEvent): """ We want the ability for the user to right click an object and load it in the console. Here we emit the correct signal for the main to determine how to load it in the console. Parameters ---------- e QEvent on menu button click """ index = self.currentIndex() mid_lvl_name = index.parent().data() sel_data = index.data() self.show_explorer.emit(sel_data) def zoom_extents_event(self, e): """ Zoom to the extents of the layer selected Parameters ---------- e QEvent on menu button click """ index = self.currentIndex() mid_lvl_name = index.parent().data() sel_data = index.data() if mid_lvl_name == 'Converted': self.zoom_extents_fqpr.emit(sel_data) elif mid_lvl_name == 'Surfaces': self.zoom_extents_surface.emit(sel_data) def update_surface_event(self, e): """ If user right clicks a surface and selects update surface, triggers this event. Parameters ---------- e: QEvent on menu button click """ index = self.currentIndex() mid_lvl_name = index.parent().data() sel_data = index.data() if mid_lvl_name == 'Surfaces': self.update_surface.emit(sel_data) def manage_data_event(self, e): """ If a user right clicks on the converted data instance and selects manage, triggers this event Parameters ---------- e: QEvent on menu button click """ index = self.currentIndex() mid_lvl_name = index.parent().data() sel_data = index.data() if mid_lvl_name == 'Converted': self.manage_fqpr.emit(sel_data) elif mid_lvl_name == 'Surfaces': self.manage_surface.emit(sel_data) def close_item_event(self, e): """ If user right clicks on a project tree item and selects close, triggers this event. Emit signals depending on what kind of item the user selects. Parameters ---------- e: QEvent on menu button click """ index = self.currentIndex() mid_lvl_name = index.parent().data() sel_data = index.data() if mid_lvl_name == 'Converted': self.close_fqpr.emit(sel_data) elif mid_lvl_name == 'Surfaces': self.close_surface.emit(sel_data) def configure(self): """ Clears all data currently in the tree and repopulates with loaded datasets and surfaces. """ self.model.clear() self.model.setHorizontalHeaderLabels(['Project Tree']) for cnt, c in enumerate(self.categories): parent = QtGui.QStandardItem(c) self.tree_data[c] = [parent] self.model.appendRow(parent) self.setFirstColumnSpanned(cnt, self.rootIndex(), True) def _add_new_fqpr_from_proj(self, parent: QtGui.QStandardItem, line_data): """ Read from the kluster_main FqprProject (provided here is the line_data from that project) and add the lines that are not currently in project tree. self.tree_data contains the record of the data in the tree. Parameters ---------- parent: PySide2.QtGui.QStandardItem, the item that represents the 'Converted' entry in the tree. All fqpr projects go underneath. line_data: dict, a dictionary of project paths: multibeam lines. ex: {'C:\\collab\\dasktest\\data_dir\\hassler_acceptance\\refsurf\\converted': {'0015_20200304_070725_S250.all': [1583305645.423, 1583305889.905]} """ current_fq_proj = self.tree_data['Converted'][1:] for fq_proj in line_data: if fq_proj not in current_fq_proj: proj_child = QtGui.QStandardItem(fq_proj) parent.appendRow(proj_child) for fq_line in line_data[fq_proj]: line_child = QtGui.QStandardItem(fq_line) proj_child.appendRow([line_child]) self.tree_data['Converted'].append(fq_proj) else: # see if there are new lines to display idx = self.tree_data['Converted'][1:].index(fq_proj) proj_child = parent.child(idx) tst = proj_child.rowCount() tsttwo = len(line_data[fq_proj]) if proj_child.rowCount() != len(line_data[fq_proj]): # new lines tree_lines = [proj_child.child(rw).text() for rw in range(proj_child.rowCount())] for fq_line in line_data[fq_proj]: if fq_line not in tree_lines: line_child = QtGui.QStandardItem(fq_line) proj_child.appendRow([line_child]) parent.sortChildren(0, order=QtCore.Qt.AscendingOrder) def _add_new_surf_from_proj(self, parent: QtGui.QStandardItem, surf_data): """ Read from the kluster_main FqprProject (provided here is the line_data from that project) and add the surfaces that are not currently in project tree. self.tree_data contains the record of the data in the tree. Parameters ---------- parent: PySide2.QtGui.QStandardItem, the item that represents the 'Surfaces' entry in the tree. All fqpr projects go underneath. surf_data: dict, a dictionary of surface paths: surface objects. ex: {'C:/collab/dasktest/data_dir/hassler_acceptance/refsurf/refsurf.npz': <HSTB.kluster.fqpr_surface.BaseSurface object at 0x0000019CFFF1A520>} """ current_surfs = self.tree_data['Surfaces'][1:] for surf in surf_data: if surf not in current_surfs: surf_child = QtGui.QStandardItem(surf) parent.appendRow(surf_child) for lyr in surf_data[surf].return_layer_names(): lyr_child = QtGui.QStandardItem(lyr) lyr_child.setCheckable(True) surf_child.appendRow([lyr_child]) if lyr == 'depth': # add optional hillshade layer lyr_child = QtGui.QStandardItem('hillshade') lyr_child.setCheckable(True) surf_child.appendRow([lyr_child]) try: # add the ability to draw the grid outline, new in bathygrid 1.1.2 surf_data[surf].get_tile_boundaries lyr_child = QtGui.QStandardItem('tiles') lyr_child.setCheckable(True) surf_child.appendRow([lyr_child]) except AttributeError: # bathygrid does not support this method pass self.tree_data['Surfaces'].append(surf) parent.sortChildren(0, order=QtCore.Qt.AscendingOrder) def _remove_fqpr_not_in_proj(self, parent, line_data): """ Read from the kluster_main FqprProject (provided here is the line_data from that project) and remove the lines that are not currently in project tree. self.tree_data contains the record of the data in the tree. Parameters ---------- parent: PySide2.QtGui.QStandardItem, the item that represents the 'Converted' entry in the tree. All fqpr projects go underneath. line_data: dict, a dictionary of project paths: multibeam lines. ex: {'C:\\collab\\dasktest\\data_dir\\hassler_acceptance\\refsurf\\converted': {'0015_20200304_070725_S250.all': [1583305645.423, 1583305889.905]} """ current_fq_proj = self.tree_data['Converted'][1:] needs_removal = [f for f in current_fq_proj if f not in line_data] for remv in needs_removal: try: idx = self.tree_data['Converted'][1:].index(remv) except ValueError: print('Unable to close {} in project tree, not found in kluster_project_tree.tree_data'.format(remv)) continue if idx != -1: self.tree_data['Converted'].pop(idx + 1) tree_idx = [idx for idx in range(parent.rowCount()) if parent.child(idx).text() == remv] if tree_idx and len(tree_idx) == 1: parent.removeRow(tree_idx[0]) else: print('Unable to remove "{}"'.format(remv)) def _remove_surf_not_in_proj(self, parent, surf_data): """ Read from the kluster_main FqprProject (provided here is the line_data from that project) and remove the surfaces that are not currently in project tree. self.tree_data contains the record of the data in the tree. Parameters ---------- parent: PySide2.QtGui.QStandardItem, the item that represents the 'Surfaces' entry in the tree. All fqpr projects go underneath. surf_data: dict, a dictionary of surface paths: surface objects. ex: {'C:/collab/dasktest/data_dir/hassler_acceptance/refsurf/refsurf.npz': <HSTB.kluster.fqpr_surface.BaseSurface object at 0x0000019CFFF1A520>} """ current_surfs = self.tree_data['Surfaces'][1:] needs_removal = [f for f in current_surfs if f not in surf_data] for remv in needs_removal: try: idx = self.tree_data['Surfaces'][1:].index(remv) except ValueError: print('Unable to close {} in project tree, not found in kluster_project_tree.tree_data'.format(remv)) continue if idx != -1: self.tree_data['Surfaces'].pop(idx + 1) tree_idx = [idx for idx in range(parent.rowCount()) if parent.child(idx).text() == remv] if tree_idx and len(tree_idx) == 1: parent.removeRow(tree_idx[0]) else: print('Unable to remove "{}"'.format(remv)) def _setup_project(self, parent, proj_directory): if len(self.tree_data['Project']) == 1: proj_child = QtGui.QStandardItem(proj_directory) parent.appendRow(proj_child) self.tree_data['Project'].append(proj_directory) else: parent.removeRow(0) proj_child = QtGui.QStandardItem(proj_directory) parent.appendRow(proj_child) self.tree_data['Project'][1] = proj_directory def _setup_vessel_file(self, parent, vessel_path): if len(self.tree_data['Vessel File']) == 1: if vessel_path: proj_child = QtGui.QStandardItem(vessel_path) parent.appendRow(proj_child) self.tree_data['Vessel File'].append(vessel_path) else: parent.removeRow(0) if vessel_path: proj_child = QtGui.QStandardItem(vessel_path) parent.appendRow(proj_child) self.tree_data['Vessel File'][1] = vessel_path def refresh_project(self, proj): """ Loading from a FqprProject will update the tree, triggered on dragging in a converted data folder Parameters ---------- proj: fqpr_project.FqprProject """ for cnt, c in enumerate(self.categories): parent = self.tree_data[c][0] if c == 'Converted': line_data = proj.return_project_lines() self._add_new_fqpr_from_proj(parent, line_data) self._remove_fqpr_not_in_proj(parent, line_data) elif c == 'Surfaces': surf_data = proj.surface_instances self._add_new_surf_from_proj(parent, surf_data) self._remove_surf_not_in_proj(parent, surf_data) elif c == 'Project': if proj.path: self._setup_project(parent, proj.path) elif c == 'Vessel File': if proj.vessel_file: self._setup_vessel_file(parent, proj.vessel_file) def select_multibeam_lines(self, line_names: list, clear_existing_selection: bool = True): parent = self.tree_data['Converted'][0] num_containers = parent.rowCount() clrfirst = clear_existing_selection if line_names: for cnt in range(num_containers): container_item = parent.child(cnt, 0) numlines = container_item.rowCount() for lcnt in range(numlines): lineitem = container_item.child(lcnt, 0) if lineitem.text() in line_names: sel = lineitem.index() if clrfirst: # we programmatically select it with ClearAndSelect self.selectionModel().select(sel, QtCore.QItemSelectionModel.ClearAndSelect | QtCore.QItemSelectionModel.Rows) clrfirst = False else: self.selectionModel().select(sel, QtCore.QItemSelectionModel.Select | QtCore.QItemSelectionModel.Rows) self.item_selected(sel) else: self.selectionModel().select(parent.index(), QtCore.QItemSelectionModel.Clear | QtCore.QItemSelectionModel.Rows) def item_selected(self, index): """ Selecting one of the items in the tree will activate an event depending on the item type. See comments below. Parameters ---------- index: PySide2.QtCore.QModelIndex, index of selected item """ top_lvl_name = index.parent().parent().data() mid_lvl_name = index.parent().data() selected_name = index.data() if top_lvl_name in self.categories: # this is a sub sub item, something like a line name or a surface name if top_lvl_name == 'Converted': self.lines_selected.emit(self.return_selected_lines()) elif top_lvl_name == 'Surfaces': lname = mid_lvl_name + selected_name ischecked = self.model.itemFromIndex(index).checkState() # if ischecked and lname in self.shown_layers: # don't do anything, it is already shown # pass # elif not ischecked and lname not in self.shown_layers: # don't do anything, it is already hidden # pass # else: # if ischecked: # self.shown_layers.append(lname) # else: # self.shown_layers.remove(lname) self.surface_layer_selected.emit(mid_lvl_name, selected_name, ischecked) elif mid_lvl_name in self.categories: # this is a sub item, like a converted fqpr path if mid_lvl_name == 'Converted': self.fqpr_selected.emit(selected_name) elif mid_lvl_name == 'Surfaces': self.surface_selected.emit(selected_name) elif selected_name in self.categories: if selected_name == 'Converted': # self.all_lines_selected.emit(True) pass def return_selected_fqprs(self, force_line_list: bool = False): """ Return all the selected fqpr instances that are selected. If the user selects a line (a child of the fqpr), return the line owner fqpr. Only returns unique fqpr instances Parameters ---------- force_line_list if you want to force the return of all the lines when a parent Fqpr converted instance is selected, use this option. Returns ------- list list of all str paths to fqpr instances selected, either directly or through selecting a line dict dictionary of all selected lines, with the fqpr as key """ fqprs = [] line_list = {} idxs = self.selectedIndexes() for idx in idxs: new_fqpr = '' top_lvl_name = idx.parent().parent().data() mid_lvl_name = idx.parent().data() low_lvl_name = idx.data() if mid_lvl_name == 'Converted': # user has selected a fqpr instance new_fqpr = low_lvl_name if force_line_list: cont_index = idx.row() parent = self.tree_data['Converted'][0] container_item = parent.child(cont_index, 0) numlines = container_item.rowCount() for lcnt in range(numlines): linename = container_item.child(lcnt, 0).text() if new_fqpr in line_list: line_list[new_fqpr].append(linename) else: line_list[new_fqpr] = [linename] elif top_lvl_name == 'Converted': # user selected a line new_fqpr = mid_lvl_name if new_fqpr in line_list: line_list[new_fqpr].append(low_lvl_name) else: line_list[new_fqpr] = [low_lvl_name] if new_fqpr and (new_fqpr not in fqprs): fqprs.append(new_fqpr) return fqprs, line_list def return_selected_surfaces(self): """ Return all the selected surface instances that are selected. Only returns unique surface instances Returns ------- list list of all str paths to surface instance folders selected """ surfs = [] new_surf = '' idxs = self.selectedIndexes() for idx in idxs: mid_lvl_name = idx.parent().data() if mid_lvl_name == 'Surfaces': # user has selected a surface new_surf = self.model.data(idx) if new_surf not in surfs: surfs.append(new_surf) return surfs def return_selected_lines(self): """ Return all the selected line instances that are selected. Only returns unique line instances Returns ------- list list of all str line names selected """ linenames = [] idxs = self.selectedIndexes() for idx in idxs: new_line = '' top_lvl_name = idx.parent().parent().data() if top_lvl_name == 'Converted': # user selected a line new_line = self.model.data(idx) if new_line and (new_line not in linenames): linenames.append(new_line) return linenames
class RangeSlider(QtWidgets.QWidget): """ Build a custom slider with two handles, allowing you to specify a range. Utilize the QStyleOptionSlider widget to do so. """ mouse_move = Signal(int, int) def __init__(self, parent=None): super().__init__(parent) self.first_position = 1 self.second_position = 8 self.opt = QtWidgets.QStyleOptionSlider() self.opt.minimum = 0 self.opt.maximum = 10 self.setTickPosition(QtWidgets.QSlider.TicksAbove) self.setTickInterval(1) self.setSizePolicy( QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Slider)) def setRangeLimit(self, minimum: int, maximum: int): """ Set the maximum range of the slider bar """ self.opt.minimum = minimum self.opt.maximum = maximum def setRange(self, start: int, end: int): """ Set the position of the two handles, the range of the selection """ self.first_position = start self.second_position = end self.update() def getRange(self): """ Get the positions of the handles """ return self.first_position, self.second_position def setTickPosition(self, position: QtWidgets.QSlider.TickPosition): self.opt.tickPosition = position def setTickInterval(self, ti: int): self.opt.tickInterval = ti def paintEvent(self, event: QtGui.QPaintEvent): painter = QtGui.QPainter(self) # Draw rule self.opt.initFrom(self) self.opt.rect = self.rect() self.opt.sliderPosition = 0 self.opt.subControls = QtWidgets.QStyle.SC_SliderGroove | QtWidgets.QStyle.SC_SliderTickmarks # Draw GROOVE self.style().drawComplexControl(QtWidgets.QStyle.CC_Slider, self.opt, painter) # Draw INTERVAL color = self.palette().color(QtGui.QPalette.Highlight) color.setAlpha(160) painter.setBrush(QtGui.QBrush(color)) painter.setPen(QtCore.Qt.NoPen) self.opt.sliderPosition = self.first_position x_left_handle = (self.style().subControlRect( QtWidgets.QStyle.CC_Slider, self.opt, QtWidgets.QStyle.SC_SliderHandle).right()) self.opt.sliderPosition = self.second_position x_right_handle = (self.style().subControlRect( QtWidgets.QStyle.CC_Slider, self.opt, QtWidgets.QStyle.SC_SliderHandle).left()) groove_rect = self.style().subControlRect( QtWidgets.QStyle.CC_Slider, self.opt, QtWidgets.QStyle.SC_SliderGroove) selection = QtCore.QRect( x_left_handle, groove_rect.y(), x_right_handle - x_left_handle, groove_rect.height(), ).adjusted(-1, 1, 1, -1) painter.drawRect(selection) # Draw first handle self.opt.subControls = QtWidgets.QStyle.SC_SliderHandle self.opt.sliderPosition = self.first_position self.style().drawComplexControl(QtWidgets.QStyle.CC_Slider, self.opt, painter) # Draw second handle self.opt.sliderPosition = self.second_position self.style().drawComplexControl(QtWidgets.QStyle.CC_Slider, self.opt, painter) def mousePressEvent(self, event: QtGui.QMouseEvent): self.opt.sliderPosition = self.first_position self._first_sc = self.style().hitTestComplexControl( QtWidgets.QStyle.CC_Slider, self.opt, event.pos(), self) self.opt.sliderPosition = self.second_position self._second_sc = self.style().hitTestComplexControl( QtWidgets.QStyle.CC_Slider, self.opt, event.pos(), self) def mouseMoveEvent(self, event: QtGui.QMouseEvent): distance = self.opt.maximum - self.opt.minimum pos = self.style().sliderValueFromPosition(0, distance, event.pos().x(), self.rect().width()) if self._first_sc == QtWidgets.QStyle.SC_SliderHandle: if pos <= self.second_position: self.first_position = pos self.update() self.mouse_move.emit(self.first_position, self.second_position) return if self._second_sc == QtWidgets.QStyle.SC_SliderHandle: if pos >= self.first_position: self.second_position = pos self.update() self.mouse_move.emit(self.first_position, self.second_position) def sizeHint(self): """ override """ SliderLength = 84 TickSpace = 5 w = SliderLength h = self.style().pixelMetric(QtWidgets.QStyle.PM_SliderThickness, self.opt, self) if (self.opt.tickPosition & QtWidgets.QSlider.TicksAbove or self.opt.tickPosition & QtWidgets.QSlider.TicksBelow): h += TickSpace return (self.style().sizeFromContents( QtWidgets.QStyle.CT_Slider, self.opt, QtCore.QSize(w, h), self).expandedTo(QtWidgets.QApplication.globalStrut()))
class SetupWidget(QtWidgets.QWidget): changed_parameter_sig = Signal(Paramlist) def __init__(self, parent=None): """Widget for holding all the parameter options in neat lists. Based on methods from ../gloo/primitive_mesh_viewer_qt. """ super(SetupWidget, self).__init__(parent) # Create the parameter list from the default parameters given here self.param = Paramlist(PARAMETERS) # Checkbox for whether or not the pivot point is visible self.pivot_chk = QtWidgets.QCheckBox(u"Show pivot point") self.pivot_chk.setChecked(self.param.props['pivot']) self.pivot_chk.toggled.connect(self.update_parameters) # A drop-down menu for selecting which method to use for updating self.method_list = ['Euler', 'Runge-Kutta'] self.method_options = QtWidgets.QComboBox() self.method_options.addItems(self.method_list) self.method_options.setCurrentIndex( self.method_list.index((self.param.props['method']))) self.method_options.currentIndexChanged.connect(self.update_parameters) # Separate the different parameters into groupboxes, # so there's a clean visual appearance self.parameter_groupbox = QtWidgets.QGroupBox(u"System Parameters") self.conditions_groupbox = QtWidgets.QGroupBox(u"Initial Conditions") self.display_groupbox = QtWidgets.QGroupBox(u"Display Parameters") self.groupbox_list = [ self.parameter_groupbox, self.conditions_groupbox, self.display_groupbox ] self.splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical) # Get ready to create all the spinboxes with appropriate labels plist = [] self.psets = [] # important_positions is used to separate the # parameters into their appropriate groupboxes important_positions = [ 0, ] param_boxes_layout = [ QtWidgets.QGridLayout(), QtWidgets.QGridLayout(), QtWidgets.QGridLayout() ] for nameV, minV, maxV, typeV, iniV in self.param.parameters: # Create Labels for each element plist.append(QtWidgets.QLabel(nameV)) if nameV == 'x' or nameV == 'scale': # 'x' is the start of the 'Initial Conditions' groupbox, # 'scale' is the start of the 'Display Parameters' groupbox important_positions.append(len(plist) - 1) # Create Spinboxes based on type - doubles get a DoubleSpinBox, # ints get regular SpinBox. # Step sizes are the same for every parameter except font size. if typeV == 'double': self.psets.append(QtWidgets.QDoubleSpinBox()) self.psets[-1].setDecimals(3) if nameV == 'font size': self.psets[-1].setSingleStep(1.0) else: self.psets[-1].setSingleStep(0.01) elif typeV == 'int': self.psets.append(QtWidgets.QSpinBox()) # Set min, max, and initial values self.psets[-1].setMaximum(maxV) self.psets[-1].setMinimum(minV) self.psets[-1].setValue(iniV) pidx = -1 for pos in range(len(plist)): if pos in important_positions: pidx += 1 param_boxes_layout[pidx].addWidget(plist[pos], pos + pidx, 0) param_boxes_layout[pidx].addWidget(self.psets[pos], pos + pidx, 1) self.psets[pos].valueChanged.connect(self.update_parameters) param_boxes_layout[0].addWidget(QtWidgets.QLabel('Method: '), 8, 0) param_boxes_layout[0].addWidget(self.method_options, 8, 1) param_boxes_layout[-1].addWidget(self.pivot_chk, 2, 0, 3, 0) for groupbox, layout in zip(self.groupbox_list, param_boxes_layout): groupbox.setLayout(layout) for groupbox in self.groupbox_list: self.splitter.addWidget(groupbox) vbox = QtWidgets.QVBoxLayout() hbox = QtWidgets.QHBoxLayout() hbox.addWidget(self.splitter) hbox.addStretch(5) vbox.addLayout(hbox) vbox.addStretch(1) self.setLayout(vbox) def update_parameters(self, option): """When the system parameters change, get the state and emit it.""" self.param.props['pivot'] = self.pivot_chk.isChecked() self.param.props['method'] = self.method_list[ self.method_options.currentIndex()] keys = map(lambda x: x[0], self.param.parameters) for pos, nameV in enumerate(keys): self.param.props[CONVERSION_DICT[nameV]] = self.psets[pos].value() self.changed_parameter_sig.emit(self.param)
class FilterWorker(QtCore.QThread): """ Executes code in a seperate thread. """ started = Signal(bool) finished = Signal(bool) def __init__(self, parent=None): super().__init__(parent) self.fq_chunks = None self.line_names = None self.fqpr_instances = [] self.new_status = [] self.mode = '' self.selected_index = None self.filter_name = '' self.save_to_disk = True self.kwargs = None self.selected_index = [] self.error = False self.exceptiontxt = None def populate(self, fq_chunks, line_names, filter_name, basic_mode, line_mode, points_mode, save_to_disk, kwargs): if basic_mode: self.mode = 'basic' elif line_mode: self.mode = 'line' elif points_mode: self.mode = 'points' self.fqpr_instances = [] self.new_status = [] self.line_names = line_names self.fq_chunks = fq_chunks self.filter_name = filter_name self.save_to_disk = save_to_disk self.kwargs = kwargs if self.kwargs is None: self.kwargs = {} self.selected_index = [] self.error = False self.exceptiontxt = None def filter_process(self, fq, subset_time=None, subset_beam=None): if self.mode == 'basic': new_status = fq.run_filter(self.filter_name, **self.kwargs) fq.multibeam.reload_pingrecords() elif self.mode == 'line': fq.subset_by_lines(self.line_names) new_status = fq.run_filter(self.filter_name, **self.kwargs) fq.restore_subset() fq.multibeam.reload_pingrecords() else: # take the provided Points View time and subset the provided fqpr to just those times,beams selected_index = fq.subset_by_time_and_beam( subset_time, subset_beam) new_status = fq.run_filter(self.filter_name, selected_index=selected_index, save_to_disk=self.save_to_disk, **self.kwargs) fq.restore_subset() if self.save_to_disk: fq.multibeam.reload_pingrecords() self.selected_index.append(selected_index) return fq, new_status def run(self): self.started.emit(True) try: if self.mode in ['basic', 'line']: for chnk in self.fq_chunks: fq, new_status = self.filter_process(chnk[0]) self.fqpr_instances.append(fq) self.new_status.append(new_status) else: for chnk in self.fq_chunks: fq, subset_time, subset_beam = chnk[0], chnk[1], chnk[2] fq, new_status = self.filter_process( fq, subset_time, subset_beam) self.fqpr_instances.append(fq) self.new_status.append(new_status) except Exception as e: self.error = True self.exceptiontxt = traceback.format_exc() self.finished.emit(True)
class OpenProjectWorker(QtCore.QThread): """ Thread that runs when the user drags in a new project file or opens a project using the menu """ started = Signal(bool) finished = Signal(bool) def __init__(self, parent=None): super().__init__(parent) self.new_project_path = None self.force_add_fqprs = None self.force_add_surfaces = None self.new_fqprs = [] self.new_surfaces = [] self.error = False self.exceptiontxt = None def populate(self, new_project_path=None, force_add_fqprs=None, force_add_surfaces=None): self.new_project_path = new_project_path self.force_add_fqprs = force_add_fqprs self.force_add_surfaces = force_add_surfaces self.new_fqprs = [] self.new_surfaces = [] self.error = False self.exceptiontxt = None def run(self): self.started.emit(True) try: self.new_fqprs = [] if self.new_project_path: data = return_project_data(self.new_project_path) else: data = {'fqpr_paths': [], 'surface_paths': []} if self.force_add_fqprs: data['fqpr_paths'] = self.force_add_fqprs if self.force_add_surfaces: data['surface_paths'] = self.force_add_surfaces for pth in data['fqpr_paths']: fqpr_entry = reload_data(pth, skip_dask=True, silent=True, show_progress=True) if fqpr_entry is not None: # no fqpr instance successfully loaded self.new_fqprs.append(fqpr_entry) else: print('Unable to load converted data from {}'.format(pth)) for pth in data['surface_paths']: surf_entry = reload_surface(pth) if surf_entry is not None: # no grid instance successfully loaded self.new_surfaces.append(surf_entry) else: print('Unable to load surface from {}'.format(pth)) except Exception as e: self.error = True self.exceptiontxt = traceback.format_exc() self.finished.emit(True)
class ManageSurfaceDialog(QtWidgets.QWidget): """ Dialog contains a summary of the surface data and some options for altering the data contained within. """ update_surface = Signal(str) def __init__(self, parent=None): super().__init__(parent) self.setMinimumWidth(500) self.setMinimumHeight(400) self.setWindowTitle('Manage Surface') layout = QtWidgets.QVBoxLayout() self.basicdata = QtWidgets.QTextEdit() self.basicdata.setReadOnly(True) self.basicdata.setText('') layout.addWidget(self.basicdata) self.managelabel = QtWidgets.QLabel('Manage: ') layout.addWidget(self.managelabel) calclayout = QtWidgets.QHBoxLayout() self.calcbutton = QtWidgets.QPushButton('Calculate') calclayout.addWidget(self.calcbutton) self.calcdropdown = QtWidgets.QComboBox() self.calcdropdown.addItems(['area, sq nm', 'area, sq meters']) calclayout.addWidget(self.calcdropdown) self.calcanswer = QtWidgets.QLineEdit('') self.calcanswer.setReadOnly(True) calclayout.addWidget(self.calcanswer) layout.addLayout(calclayout) plotlayout = QtWidgets.QHBoxLayout() self.plotbutton = QtWidgets.QPushButton('Plot') plotlayout.addWidget(self.plotbutton) self.plotdropdown = QtWidgets.QComboBox() szepolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) szepolicy.setHorizontalStretch(2) self.plotdropdown.setSizePolicy(szepolicy) plotlayout.addWidget(self.plotdropdown) layout.addLayout(plotlayout) self.calcbutton.clicked.connect(self.calculate_statistic) self.plotbutton.clicked.connect(self.generate_plot) self.setLayout(layout) self.surf = None def populate(self, surf): """ Examine the surface and populate the controls with the correct data """ self.surf = surf self.managelabel.setText('Manage: {}'.format( os.path.split(surf.output_folder)[1])) self.basicdata.setText(surf.__repr__()) allplots = [ 'Histogram, Density (count)', 'Histogram, Density (sq meters)' ] if self.surf.is_backscatter: allplots += ['Histogram, Intensity (dB)'] else: allplots += [ 'Histogram, Depth (meters)', 'Depth vs Density (count)', 'Depth vs Density (sq meters)' ] if 'vertical_uncertainty' in self.surf.layer_names: allplots += [ 'Histogram, vertical uncertainty (2 sigma, meters)', 'Histogram, horizontal uncertainty (2 sigma, meters)' ] allplots.sort() self.plotdropdown.addItems(allplots) def calculate_statistic(self, e): stat = self.calcdropdown.currentText() if stat == 'area, sq nm': self.calcanswer.setText( str(round(self.surf.coverage_area_square_nm, 3))) elif stat == 'area, sq meters': self.calcanswer.setText( str(round(self.surf.coverage_area_square_meters, 3))) else: raise ValueError( f'Unrecognized input for calculating statistic: {stat}') def generate_plot(self, e): plotname = self.plotdropdown.currentText() plt.figure() if plotname in [ 'Histogram, Intensity (dB)', 'Histogram, Depth (meters)' ]: self.surf.plot_z_histogram() elif plotname in ['Histogram, Density (count)']: self.surf.plot_density_histogram() elif plotname in ['Histogram, Density (sq meters)']: self.surf.plot_density_per_square_meter_histogram() elif plotname in ['Depth vs Density (count)']: self.surf.plot_density_vs_depth() elif plotname in ['Depth vs Density (sq meters)']: self.surf.plot_density_per_square_meter_vs_depth() elif plotname in ['Histogram, vertical uncertainty (2 sigma, meters)']: self.surf.plot_vertical_uncertainty_histogram() elif plotname in [ 'Histogram, horizontal uncertainty (2 sigma, meters)' ]: self.surf.plot_horizontal_uncertainty_histogram()
class KlusterMonitorWidget(QtWidgets.QWidget): """ Widget for holding the folder path entered, the start stop buttons, etc. for the monitor tool. Hook up to the two events to get the data from the controls. """ monitor_file_event = Signal(str, str) monitor_start = Signal(str) def __init__(self, parent=None): super().__init__(parent) self.parent = parent self.monitor_layout = QtWidgets.QVBoxLayout() self.monitorone_layout = QtWidgets.QHBoxLayout() self.monitorone = MonitorPath(self) self.monitorone_layout.addWidget(self.monitorone) self.monitor_layout.addLayout(self.monitorone_layout) self.monitortwo_layout = QtWidgets.QHBoxLayout() self.monitortwo = MonitorPath(self) self.monitortwo_layout.addWidget(self.monitortwo) self.monitor_layout.addLayout(self.monitortwo_layout) self.monitorthree_layout = QtWidgets.QHBoxLayout() self.monitorthree = MonitorPath(self) self.monitorthree_layout.addWidget(self.monitorthree) self.monitor_layout.addLayout(self.monitorthree_layout) self.monitorfour_layout = QtWidgets.QHBoxLayout() self.monitorfour = MonitorPath(self) self.monitorfour_layout.addWidget(self.monitorfour) self.monitor_layout.addLayout(self.monitorfour_layout) self.monitorfive_layout = QtWidgets.QHBoxLayout() self.monitorfive = MonitorPath(self) self.monitorfive_layout.addWidget(self.monitorfive) self.monitor_layout.addLayout(self.monitorfive_layout) self.monitor_layout.addStretch() self.monitorone.monitor_file_event.connect(self.emit_file_event) self.monitortwo.monitor_file_event.connect(self.emit_file_event) self.monitorthree.monitor_file_event.connect(self.emit_file_event) self.monitorfour.monitor_file_event.connect(self.emit_file_event) self.monitorfive.monitor_file_event.connect(self.emit_file_event) self.monitorone.monitor_start.connect(self.emit_monitor_start) self.monitortwo.monitor_start.connect(self.emit_monitor_start) self.monitorthree.monitor_start.connect(self.emit_monitor_start) self.monitorfour.monitor_start.connect(self.emit_monitor_start) self.monitorfive.monitor_start.connect(self.emit_monitor_start) self.setLayout(self.monitor_layout) self.layout() def emit_file_event(self, newfile: str, file_event: str): """ Triggered on a new file showing up in the MonitorPath Parameters ---------- newfile file path file_event one of 'created', 'deleted' """ self.monitor_file_event.emit(newfile, file_event) def emit_monitor_start(self, pth: str): """ Triggered on the start button being pressed, emits the folder path Parameters ---------- pth folder path as string """ self.monitor_start.emit(pth) def stop_all_monitoring(self): """ Stop all the monitors if they are running, this is triggered on closing the main gui """ self.monitorone.stop_monitoring() self.monitortwo.stop_monitoring() self.monitorthree.stop_monitoring() self.monitorfour.stop_monitoring() self.monitorfive.stop_monitoring() def save_settings(self, settings: QtCore.QSettings): """ Save the settings to the Qsettings """ settings.setValue('Kluster/monitor_one_path', self.monitorone.fil_text.text()) settings.setValue('Kluster/monitor_two_path', self.monitortwo.fil_text.text()) settings.setValue('Kluster/monitor_three_path', self.monitorthree.fil_text.text()) settings.setValue('Kluster/monitor_four_path', self.monitorfour.fil_text.text()) settings.setValue('Kluster/monitor_five_path', self.monitorfive.fil_text.text()) settings.setValue('Kluster/monitor_one_subdir', self.monitorone.include_subdirectories.isChecked()) settings.setValue('Kluster/monitor_two_subdir', self.monitortwo.include_subdirectories.isChecked()) settings.setValue('Kluster/monitor_three_subdir', self.monitorthree.include_subdirectories.isChecked()) settings.setValue('Kluster/monitor_four_subdir', self.monitorfour.include_subdirectories.isChecked()) settings.setValue('Kluster/monitor_five_subdir', self.monitorfive.include_subdirectories.isChecked()) def read_settings(self, settings: QtCore.QSettings): """ Read from the Qsettings """ try: if settings.value('Kluster/monitor_one_path'): self.monitorone.fil_text.setText( settings.value('Kluster/monitor_one_path')) if settings.value('Kluster/monitor_two_path'): self.monitortwo.fil_text.setText( settings.value('Kluster/monitor_two_path')) if settings.value('Kluster/monitor_three_path'): self.monitorthree.fil_text.setText( settings.value('Kluster/monitor_three_path')) if settings.value('Kluster/monitor_four_path'): self.monitorfour.fil_text.setText( settings.value('Kluster/monitor_four_path')) if settings.value('Kluster/monitor_five_path'): self.monitorfive.fil_text.setText( settings.value('Kluster/monitor_five_path')) # loads as the word 'false' or 'true'...ugh self.monitorone.include_subdirectories.setChecked( settings.value('Kluster/monitor_one_subdir').lower() == 'true') self.monitortwo.include_subdirectories.setChecked( settings.value('Kluster/monitor_two_subdir').lower() == 'true') self.monitorthree.include_subdirectories.setChecked( settings.value('Kluster/monitor_three_subdir').lower() == 'true') self.monitorfour.include_subdirectories.setChecked( settings.value('Kluster/monitor_four_subdir').lower() == 'true') self.monitorfive.include_subdirectories.setChecked( settings.value('Kluster/monitor_five_subdir').lower() == 'true') except AttributeError: # no settings exist yet for this app, .lower failed pass
class PlotDataHandler(QtWidgets.QWidget): """ Widget allowing the user to provide a directory of kluster converted data and specify a time range in a number of different ways. - specify time range by manually by sliding the rangeslider handles around - specify time by typing in the min time, max time - specify time by selecting the line you are interested in """ fqpr_loaded = Signal(bool) ping_count_changed = Signal(int) def __init__(self, parent=None): super().__init__(parent) self.fqpr = None self.fqpr_path = None self.fqpr_mintime = 0 self.fqpr_maxtime = 0 self.fqpr_line_dict = None self.slider_mintime = 0 self.slider_maxtime = 0 self.translate_time = False self.setWindowTitle('Basic Plot') layout = QtWidgets.QVBoxLayout() self.start_msg = QtWidgets.QLabel( 'Select the converted data to plot (a converted folder):') self.hlayout_one = QtWidgets.QHBoxLayout() self.fil_text = QtWidgets.QLineEdit('', self) self.fil_text.setMinimumWidth(400) self.fil_text.setReadOnly(True) self.hlayout_one.addWidget(self.fil_text) self.browse_button = QtWidgets.QPushButton("Browse", self) self.hlayout_one.addWidget(self.browse_button) self.trim_time_check = QtWidgets.QGroupBox('Trim by time') self.trim_time_check.setCheckable(True) self.trim_time_check.setChecked(False) self.hlayout_two = QtWidgets.QHBoxLayout() self.trim_time_start_lbl = QtWidgets.QLabel('Start time (utc seconds)') self.hlayout_two.addWidget(self.trim_time_start_lbl) self.trim_time_start = QtWidgets.QLineEdit('', self) self.hlayout_two.addWidget(self.trim_time_start) self.trim_time_end_lbl = QtWidgets.QLabel('End time (utc seconds)') self.hlayout_two.addWidget(self.trim_time_end_lbl) self.trim_time_end = QtWidgets.QLineEdit('', self) self.hlayout_two.addWidget(self.trim_time_end) self.trim_time_datetime_start_lbl = QtWidgets.QLabel( 'Start time (utc)') self.trim_time_datetime_start_lbl.hide() self.hlayout_two.addWidget(self.trim_time_datetime_start_lbl) self.trim_time_datetime_start = QtWidgets.QDateTimeEdit(self) self.trim_time_datetime_start.setDisplayFormat("MM/dd/yyyy hh:mm:ss") self.trim_time_datetime_start.hide() self.hlayout_two.addWidget(self.trim_time_datetime_start) self.trim_time_datetime_end_lbl = QtWidgets.QLabel('End time (utc)') self.trim_time_datetime_end_lbl.hide() self.hlayout_two.addWidget(self.trim_time_datetime_end_lbl) self.trim_time_datetime_end = QtWidgets.QDateTimeEdit(self) self.trim_time_datetime_end.setDisplayFormat("MM/dd/yyyy hh:mm:ss") self.trim_time_datetime_end.hide() self.hlayout_two.addWidget(self.trim_time_datetime_end) self.hlayout_two.addStretch() self.trim_time_check.setLayout(self.hlayout_two) self.trim_line_check = QtWidgets.QGroupBox('Trim by line') self.trim_line_check.setCheckable(True) self.trim_line_check.setChecked(False) self.hlayout_three = QtWidgets.QHBoxLayout() self.trim_lines_lbl = QtWidgets.QLabel('Line Name') self.hlayout_three.addWidget(self.trim_lines_lbl) self.trim_lines = QtWidgets.QComboBox(self) self.trim_lines.setMinimumWidth(350) self.hlayout_three.addWidget(self.trim_lines) self.trim_line_check.setLayout(self.hlayout_three) self.hlayout_four = QtWidgets.QHBoxLayout() self.ping_count_label = QtWidgets.QLabel('Ping count') self.hlayout_four.addWidget(self.ping_count_label) self.ping_count = QtWidgets.QLineEdit('', self) self.ping_count.setMinimumWidth(80) self.ping_count.setReadOnly(True) self.hlayout_four.addWidget(self.ping_count) self.time_as_label = QtWidgets.QLabel('Time as') self.hlayout_four.addWidget(self.time_as_label) self.time_as_dropdown = QtWidgets.QComboBox(self) self.time_as_dropdown.addItems(['utc seconds', 'utc datetime']) self.hlayout_four.addWidget(self.time_as_dropdown) self.hlayout_four.addStretch(2) self.hlayout_four_one = QtWidgets.QHBoxLayout() self.display_start_time = QtWidgets.QLabel('0.0', self) self.hlayout_four_one.addWidget(self.display_start_time) self.hlayout_four_one.addStretch() self.display_range = QtWidgets.QLabel('(0.0, 0.0)', self) self.hlayout_four_one.addWidget(self.display_range) self.hlayout_four_one.addStretch() self.display_end_time = QtWidgets.QLabel('0.0', self) self.hlayout_four_one.addWidget(self.display_end_time) self.hlayout_five = QtWidgets.QHBoxLayout() self.sliderbar = RangeSlider(self) self.sliderbar.setTickInterval(1000) self.sliderbar.setRangeLimit(0, 1000) self.sliderbar.setRange(20, 200) self.hlayout_five.addWidget(self.sliderbar) self.hlayout_six = QtWidgets.QHBoxLayout() self.warning_message = QtWidgets.QLabel('', self) self.warning_message.setStyleSheet("{};".format( kluster_variables.error_color)) self.hlayout_six.addWidget(self.warning_message) layout.addWidget(self.start_msg) layout.addLayout(self.hlayout_one) layout.addWidget(self.trim_time_check) layout.addWidget(self.trim_line_check) layout.addLayout(self.hlayout_four) layout.addLayout(self.hlayout_four_one) layout.addLayout(self.hlayout_five) layout.addLayout(self.hlayout_six) self.setLayout(layout) self.browse_button.clicked.connect(self.file_browse) self.sliderbar.mouse_move.connect(self.update_from_slider) self.trim_time_start.textChanged.connect(self.update_from_trim_time) self.trim_time_end.textChanged.connect(self.update_from_trim_time) self.trim_time_datetime_start.dateTimeChanged.connect( self.update_from_trim_datetime) self.trim_time_datetime_end.dateTimeChanged.connect( self.update_from_trim_datetime) self.trim_lines.currentTextChanged.connect(self.update_from_line) self.trim_time_check.toggled.connect(self.trim_time_toggled) self.trim_line_check.toggled.connect(self.trim_line_toggled) self.time_as_dropdown.currentTextChanged.connect( self.update_translate_mode) def file_browse(self): """ Browse to a Kluster converted data folder. Structure should look something like: C:\collab\dasktest\data_dir\kmall_test\mbes\converted C:\collab\dasktest\data_dir\kmall_test\mbes\converted\attitude.zarr C:\collab\dasktest\data_dir\kmall_test\mbes\converted\navigation.zarr C:\collab\dasktest\data_dir\kmall_test\mbes\converted\ping_53011.zarr You would point at the converted folder using this browse button. """ # dirpath will be None or a string msg, fqpr_path = RegistryHelpers.GetDirFromUserQT( self, RegistryKey='Kluster', Title='Select converted data directory', AppName='\\reghelp') if fqpr_path: self.new_fqpr_path(fqpr_path) self.initialize_controls() def update_from_slider(self, first_pos, second_pos): """ Using the slider, we update the printed time in the widget """ if self.fqpr is not None: self.slider_mintime = self.fqpr_mintime + first_pos self.slider_maxtime = self.fqpr_mintime + second_pos totalpings = self.fqpr.return_total_pings(self.slider_mintime, self.slider_maxtime) self._set_display_range(self.slider_mintime, self.slider_maxtime) self.ping_count.setText(str(totalpings)) if totalpings == 0: self.warning_message.setText( 'ERROR: Found 0 total pings for this time range') else: self.ping_count_changed.emit(totalpings) def update_from_trim_time(self, e): """ User typed in a new start time or end time """ if self.fqpr is not None and self.trim_time_check.isChecked(): try: set_mintime = int(float(self.trim_time_start.text())) if not self.fqpr_maxtime >= set_mintime >= self.fqpr_mintime: self.warning_message.setText( 'Invalid start time, must be inbetween max and minimum time' ) return except ValueError: self.warning_message.setText( 'Invalid start time, must be a number: {}'.format( self.trim_time_start.text())) return try: set_maxtime = int(float(self.trim_time_end.text())) if not self.fqpr_maxtime >= set_maxtime >= self.fqpr_mintime: self.warning_message.setText( 'Invalid end time, must be inbetween max and minimum time' ) return except ValueError: self.warning_message.setText( 'Invalid end time, must be a number: {}'.format( self.trim_time_end.text())) return self.warning_message.setText('') self._set_new_times(set_mintime, set_maxtime) def update_from_trim_datetime(self, e): """ User entered a new start or end datetime """ if self.fqpr is not None and self.trim_time_check.isChecked(): try: try: # pyside set_datetime = self.trim_time_datetime_start.dateTime( ).toPython() except AttributeError: # pyqt5 set_datetime = self.trim_time_datetime_start.dateTime( ).toPyDateTime() set_datetime = set_datetime.replace(tzinfo=timezone.utc) set_mintime = int(float(set_datetime.timestamp())) if not self.fqpr_maxtime >= set_mintime >= self.fqpr_mintime: self.warning_message.setText( 'Invalid start time, must be inbetween max and minimum time' ) return except ValueError: self.warning_message.setText( 'Invalid start time, must be a number: {}'.format( self.trim_time_start.text())) return try: try: # pyside set_datetime = self.trim_time_datetime_end.dateTime( ).toPython() except AttributeError: # pyqt5 set_datetime = self.trim_time_datetime_end.dateTime( ).toPyDateTime() set_datetime = set_datetime.replace(tzinfo=timezone.utc) set_maxtime = int(float(set_datetime.timestamp())) if not self.fqpr_maxtime >= set_maxtime >= self.fqpr_mintime: self.warning_message.setText( 'Invalid end time, must be inbetween max and minimum time' ) return except ValueError: self.warning_message.setText( 'Invalid end time, must be a number: {}'.format( self.trim_time_end.text())) return self.warning_message.setText('') self._set_new_times(set_mintime, set_maxtime) def trim_time_toggled(self, state): """ Triggered if the 'trim by time' checkbox is checked. Automatically turns off the 'trim by line' checkbox and populates the trim time controls Parameters ---------- state if True, the 'trim by time' checkbox has been checked """ if state: self.trim_line_check.setChecked(False) starttme = self.slider_mintime endtme = self.slider_maxtime self.trim_time_start.setText(str(starttme)) self.trim_time_end.setText(str(endtme)) self.trim_time_datetime_start.setDateTime( QtCore.QDateTime.fromSecsSinceEpoch(int(starttme), QtCore.QTimeZone(0))) self.trim_time_datetime_end.setDateTime( QtCore.QDateTime.fromSecsSinceEpoch(int(endtme), QtCore.QTimeZone(0))) def trim_line_toggled(self, state): """ Triggered if the 'trim by line' checkbox is checked. Automatically turns off the 'trim by time' checkbox and populates the trim by line controls Parameters ---------- state if True, the 'trim by line' checkbox has been checked """ if state: self.trim_time_check.setChecked(False) self.update_from_line(None) def _set_new_times(self, starttime: int, endtime: int): """ Set the slider range and the associated text controls Parameters ---------- starttime start time of the selection in utc seconds endtime end time of the selection in utc seconds """ set_minslider_position = int(starttime - self.fqpr_mintime) set_maxslider_position = int(endtime - self.fqpr_mintime) self.sliderbar.setRange(set_minslider_position, set_maxslider_position) self.slider_mintime = starttime self.slider_maxtime = endtime self._set_display_range(self.slider_mintime, self.slider_maxtime) pingcount = int( self.fqpr.return_total_pings(self.slider_mintime, self.slider_maxtime)) self.ping_count.setText(str(pingcount)) self.ping_count_changed.emit(pingcount) def _set_display_range(self, mintime: int, maxtime: int): """ Set the control that displays the selected time range Parameters ---------- mintime start time of the selection in utc seconds maxtime end time of the selection in utc seconds """ if self.translate_time: self.display_range.setText( str('({}, {})'.format( datetime.fromtimestamp(mintime, tz=timezone.utc).strftime('%c'), datetime.fromtimestamp(maxtime, tz=timezone.utc).strftime('%c')))) else: self.display_range.setText(str('({}, {})'.format(mintime, maxtime))) def _set_display_minmax(self, mintime: int, maxtime: int): """ Set the controls that display the start and end time of the range Parameters ---------- mintime start time of the selection in utc seconds maxtime end time of the selection in utc seconds """ if self.translate_time: self.display_start_time.setText( datetime.fromtimestamp(mintime, tz=timezone.utc).strftime('%c')) self.display_end_time.setText( datetime.fromtimestamp(maxtime, tz=timezone.utc).strftime('%c')) else: self.display_start_time.setText(str(mintime)) self.display_end_time.setText(str(maxtime)) def update_from_line(self, e): """ User selected a line to trim the times by """ if self.fqpr is not None and self.trim_line_check.isChecked(): linename = self.trim_lines.currentText() if self.fqpr_line_dict is not None and linename: linetimes = self.fqpr_line_dict[linename] self.warning_message.setText('') self._set_new_times(linetimes[0], linetimes[1]) def update_translate_mode(self, mode: str): """ Driven by the mode dropdown control, goes between showing times in utc seconds and showing times as a datetime Parameters ---------- mode dropdown selection """ if mode == 'utc seconds': self.trim_time_datetime_start_lbl.hide() self.trim_time_start_lbl.show() self.trim_time_datetime_start.hide() self.trim_time_start.show() self.trim_time_datetime_end_lbl.hide() self.trim_time_end_lbl.show() self.trim_time_datetime_end.hide() self.trim_time_end.show() self.translate_time = False elif mode == 'utc datetime': self.trim_time_datetime_start_lbl.show() self.trim_time_start_lbl.hide() self.trim_time_datetime_start.show() self.trim_time_start.hide() self.trim_time_datetime_end_lbl.show() self.trim_time_end_lbl.hide() self.trim_time_datetime_end.show() self.trim_time_end.hide() self.translate_time = True if self.fqpr is not None: self._set_display_range(self.slider_mintime, self.slider_maxtime) self._set_display_minmax(self.fqpr_mintime, self.fqpr_maxtime) def new_fqpr_path(self, fqpr_path: str): """ User selected a new fqpr instance (fqpr = the converted datastore, see file_browse) """ try: self.fqpr = reload_data(fqpr_path, skip_dask=True, silent=True) self.fil_text.setText(fqpr_path) if self.fqpr is not None: self.fqpr_path = fqpr_path else: self.fqpr_path = None self.warning_message.setText( 'ERROR: Invalid path to converted data store') except: return def initialize_controls(self): """ On start up, we initialize all the controls (or clear all controls if the fqpr provided was invalid) """ if self.fqpr is not None: self.fqpr_mintime = int( np.floor( np.min([ rp.time.values[0] for rp in self.fqpr.multibeam.raw_ping ]))) self.fqpr_maxtime = int( np.ceil( np.max([ rp.time.values[-1] for rp in self.fqpr.multibeam.raw_ping ]))) self.slider_mintime = self.fqpr_mintime self.slider_maxtime = self.fqpr_maxtime self.sliderbar.setTickInterval( int(self.fqpr_maxtime - self.fqpr_mintime)) self.sliderbar.setRangeLimit(0, self.fqpr_maxtime - self.fqpr_mintime) self.sliderbar.setRange(0, self.fqpr_maxtime - self.fqpr_mintime) self._set_display_range(self.slider_mintime, self.slider_maxtime) self._set_display_minmax(self.fqpr_mintime, self.fqpr_maxtime) self.trim_time_start.setText(str(self.fqpr_mintime)) self.trim_time_end.setText(str(self.fqpr_maxtime)) self.fqpr_line_dict = self.fqpr.multibeam.raw_ping[ 0].multibeam_files self.fqpr_line_dict = { t: [ int(np.max([self.fqpr_mintime, self.fqpr_line_dict[t][0]])), int( np.min([ self.fqpr_maxtime, np.ceil(self.fqpr_line_dict[t][1]) ])) ] for t in self.fqpr_line_dict } self.trim_lines.clear() self.trim_lines.addItems(sorted(list(self.fqpr_line_dict.keys()))) totalpings = self.fqpr.return_total_pings() self.ping_count.setText(str(totalpings)) self.fqpr_loaded.emit(True) self.ping_count_changed.emit(totalpings) else: self.fqpr_mintime = 0 self.fqpr_maxtime = 0 self.sliderbar.setTickInterval(1000) self.sliderbar.setRangeLimit(0, 1000) self.sliderbar.setRange(20, 200) self.display_start_time.setText('0.0') self.display_end_time.setText('0.0') self.display_range.setText('(0.0, 0.0)') self.trim_time_start.setText('') self.trim_time_end.setText('') self.fqpr_line_dict = None self.trim_lines.clear() self.ping_count.setText('') self.ping_count_changed.emit(0) self.fqpr_loaded.emit(False) def return_trim_times(self): """ Return the time range specified by one of the 3 ways to specify range, or None if there is no valid range """ if np.abs(self.slider_mintime - self.fqpr_mintime) >= 1: valid_min = self.slider_mintime else: valid_min = self.fqpr_mintime if np.abs(self.slider_maxtime - self.fqpr_maxtime) >= 1: valid_max = self.slider_maxtime else: valid_max = self.fqpr_maxtime if (valid_max != self.fqpr_maxtime) or (valid_min != self.fqpr_mintime): return valid_min, valid_max else: return None
class MonitorPath(QtWidgets.QWidget): """ Base widget for interacting with the fqpr_intelligence.DirectoryMonitor object. Each instance of this class has a file browse button, status light, etc. """ monitor_file_event = Signal(str, str) monitor_start = Signal(str) def __init__(self, parent: QtWidgets.QWidget = None): """ initialize Parameters ---------- parent MonitorDashboard """ super().__init__() self.parent = parent self.vlayout = QtWidgets.QVBoxLayout() self.hlayoutone = QtWidgets.QHBoxLayout() self.statuslight = QtWidgets.QCheckBox('') self.statuslight.setStyleSheet( "QCheckBox::indicator {background-color : black;}") self.statuslight.setDisabled(True) self.hlayoutone.addWidget(self.statuslight) self.fil_text = QtWidgets.QLineEdit('') self.fil_text.setReadOnly(True) self.hlayoutone.addWidget(self.fil_text) self.browse_button = QtWidgets.QPushButton("Browse") self.hlayoutone.addWidget(self.browse_button) self.hlayouttwo = QtWidgets.QHBoxLayout() self.start_button = QtWidgets.QPushButton('Start') self.hlayouttwo.addWidget(self.start_button) self.stop_button = QtWidgets.QPushButton('Stop') self.hlayouttwo.addWidget(self.stop_button) spcr = QtWidgets.QLabel(' ') self.hlayouttwo.addWidget(spcr) self.include_subdirectories = QtWidgets.QCheckBox( 'Include Subdirectories') self.hlayouttwo.addWidget(self.include_subdirectories) self.vlayout.addLayout(self.hlayoutone) self.vlayout.addLayout(self.hlayouttwo) self.setLayout(self.vlayout) self.browse_button.clicked.connect(self.dir_browse) self.start_button.clicked.connect(self.start_monitoring) self.stop_button.clicked.connect(self.stop_monitoring) self.monitor = None def dir_browse(self): """ As long as you aren't currently running the monitoring, this will get the directory you want to monitor """ if not self.is_running(): # dirpath will be None or a string msg, pth = RegistryHelpers.GetDirFromUserQT( self, RegistryKey='klusterintel', Title='Select directory to monitor', AppName='klusterintel') if pth is not None: self.fil_text.setText(pth) else: print('You have to stop monitoring before you can change the path') def return_monitoring_path(self): """ Return the path we are monitoring Returns ------- str directory path we are monitoring """ return self.fil_text.displayText() def is_recursive(self): """ Return whether or not the include_subdirectories checkbox is checked Returns ------- bool True if checked """ return self.include_subdirectories.isChecked() def is_running(self): """ Return whether or not the monitor is running Returns ------- bool True if the monitor is running """ if self.monitor is not None: if self.monitor.watchdog_observer.is_alive(): return True return False def start_monitoring(self): """ Start a new DirectoryMonitor. A stopped DirectoryMonitor will have to be re-instantiated, you can't restart a watchdog observer. Also sets the status light to green """ pth = self.return_monitoring_path() is_recursive = self.is_recursive() if os.path.exists(pth): # you can't restart a watchdog observer, have to create a new one self.stop_monitoring() self.monitor = monitor.DirectoryMonitor(pth, is_recursive) self.monitor.bind_to(self.emit_monitor_event) self.monitor.start() self.monitor_start.emit(pth) self.include_subdirectories.setEnabled(False) self.statuslight.setStyleSheet( "QCheckBox::indicator {background-color : green;}") print('Monitoring {}'.format(pth)) else: print('MonitorPath: Path does not exist: {}'.format(pth)) def stop_monitoring(self): """ If the DirectoryMonitor object is running, stop it """ if self.is_running(): self.monitor.stop() self.include_subdirectories.setEnabled(True) self.statuslight.setStyleSheet( "QCheckBox::indicator {background-color : black;}") print('No longer monitoring {}'.format( self.return_monitoring_path())) def emit_monitor_event(self, newfile: str, file_event: str): """ Triggered when self.monitor sees a newfile, passes in the file path and the event Parameters ---------- newfile file path file_event one of 'created', 'deleted' """ self.monitor_file_event.emit(newfile, file_event)
class BrowseListWidget(QtWidgets.QWidget): """ List widget with insert/remove buttons to add or remove browsed file paths. Will emit a signal on adding/removing items so you can connect it to other widgets. """ files_updated = Signal(bool) def __init__(self, parent): super().__init__(parent) self.layout = QtWidgets.QHBoxLayout() self.list_widget = DeletableListWidget(self) list_size_policy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Preferred) list_size_policy.setHorizontalStretch(2) self.list_widget.setSizePolicy(list_size_policy) self.layout.addWidget(self.list_widget) self.button_layout = QtWidgets.QVBoxLayout() self.button_layout.addStretch(1) self.insert_button = QtWidgets.QPushButton("Insert", self) self.button_layout.addWidget(self.insert_button) self.remove_button = QtWidgets.QPushButton("Remove", self) self.button_layout.addWidget(self.remove_button) self.button_layout.addStretch(1) self.layout.addLayout(self.button_layout) self.setLayout(self.layout) self.opts = {} self.setup() self.insert_button.clicked.connect(self.file_browse) self.remove_button.clicked.connect(self.remove_item) self.list_widget.files_updated.connect(self.files_changed) def setup(self, mode='file', registry_key='pydro', app_name='browselistwidget', supported_file_extension='*.*', multiselect=True, filebrowse_title='Select files', filebrowse_filter='all files (*.*)'): """ keyword arguments for the widget. """ self.opts = vars() def file_browse(self): """ select a file and add it to the list. """ fils = [] if self.opts['mode'] == 'file': msg, fils = RegistryHelpers.GetFilenameFromUserQT( self, RegistryKey=self.opts['registry_key'], Title=self.opts['filebrowse_title'], AppName=self.opts['app_name'], bMulti=self.opts['multiselect'], bSave=False, fFilter=self.opts['filebrowse_filter']) elif self.opts['mode'] == 'directory': msg, fils = RegistryHelpers.GetDirFromUserQT( self, RegistryKey=self.opts['registry_key'], Title=self.opts['filebrowse_title'], AppName=self.opts['app_name']) fils = [fils] if fils: self.add_new_files(fils) self.files_changed() def add_new_files(self, files): """ Add some new files to the widget, assuming they pass the supported extension option Parameters ---------- files: list, list of string paths to files """ files = sorted(files) supported_ext = self.opts['supported_file_extension'] for f in files: if self.list_widget.findItems( f, QtCore.Qt.MatchExactly): # no duplicates allowed continue if self.opts['mode'] == 'file': fil_extension = os.path.splitext(f)[1] if supported_ext == '*.*': self.list_widget.addItem(f) elif type(supported_ext ) is str and fil_extension == supported_ext: self.list_widget.addItem(f) elif type(supported_ext ) is list and fil_extension in supported_ext: self.list_widget.addItem(f) else: print( '{} is not a supported file extension. Must be a string or list of file extensions.' .format(supported_ext)) return else: self.list_widget.addItem(f) def return_all_items(self): """ Return all the items in the list widget Returns ------- list list of strings for all items in the widget """ items = [ self.list_widget.item(i).text() for i in range(self.list_widget.count()) ] return items def remove_item(self): """ remove a file from the list """ for itm in self.list_widget.selectedItems(): self.list_widget.takeItem(self.list_widget.row(itm)) self.files_changed() def files_changed(self): self.files_updated.emit(True)
class MapView(FigureCanvasQTAgg): """ Map view using cartopy/matplotlib to view multibeam tracklines and surfaces with a map context. """ box_select = Signal(float, float, float, float) def __init__(self, parent=None, width: int = 5, height: int = 4, dpi: int = 100, map_proj=ccrs.PlateCarree(), settings=None): self.fig = Figure(figsize=(width, height), dpi=dpi) self.map_proj = map_proj self.axes = self.fig.add_subplot(projection=map_proj) # self.axes.coastlines(resolution='10m') self.fig.add_axes(self.axes) #self.fig.subplots_adjust(left=0, right=1, bottom=0, top=1) self.axes.gridlines(draw_labels=True, crs=self.map_proj) self.axes.add_feature(cfeature.LAND) self.axes.add_feature(cfeature.COASTLINE) self.line_objects = {} # dict of {line name: [lats, lons, lineplot]} self.surface_objects = { } # nested dict {surfname: {layername: [lats, lons, surfplot]}} self.active_layers = {} # dict of {surfname: [layername1, layername2]} self.data_extents = { 'min_lat': 999, 'max_lat': -999, 'min_lon': 999, 'max_lon': -999 } self.selected_line_objects = [] super(MapView, self).__init__(self.fig) self.navi_toolbar = NavigationToolbar2QT(self.fig.canvas, self) self.rs = RectangleSelector(self.axes, self._line_select_callback, drawtype='box', useblit=False, button=[1], minspanx=5, minspany=5, spancoords='pixels', interactive=True) self.set_extent(90, -90, 100, -100) def set_background(self, layername: str, transparency: float, surf_transparency: float): """ A function for rendering different background layers in QGIS. Disabled for cartopy """ pass def set_extent(self, max_lat: float, min_lat: float, max_lon: float, min_lon: float, buffer: bool = True): """ Set the extent of the 2d window Parameters ---------- max_lat set the maximum latitude of the displayed map min_lat set the minimum latitude of the displayed map max_lon set the maximum longitude of the displayed map min_lon set the minimum longitude of the displayed map buffer if True, will extend the extents by half the current width/height """ self.data_extents['min_lat'] = np.min( [min_lat, self.data_extents['min_lat']]) self.data_extents['max_lat'] = np.max( [max_lat, self.data_extents['max_lat']]) self.data_extents['min_lon'] = np.min( [min_lon, self.data_extents['min_lon']]) self.data_extents['max_lon'] = np.max( [max_lon, self.data_extents['max_lon']]) if self.data_extents['min_lat'] != 999 and self.data_extents[ 'max_lat'] != -999 and self.data_extents[ 'min_lon'] != 999 and self.data_extents['max_lon'] != -999: if buffer: lat_buffer = np.max([(max_lat - min_lat) * 0.5, 0.5]) lon_buffer = np.max([(max_lon - min_lon) * 0.5, 0.5]) else: lat_buffer = 0 lon_buffer = 0 self.axes.set_extent([ np.clip(min_lon - lon_buffer, -179.999999999, 179.999999999), np.clip(max_lon + lon_buffer, -179.999999999, 179.999999999), np.clip(min_lat - lat_buffer, -90, 90), np.clip(max_lat + lat_buffer, -90, 90) ], crs=ccrs.Geodetic()) def add_line(self, line_name: str, lats: np.ndarray, lons: np.ndarray, refresh: bool = False): """ Draw a new multibeam trackline on the cartopy display, unless it is already there Parameters ---------- line_name name of the multibeam line lats numpy array of latitude values to plot lons numpy array of longitude values to plot refresh set to True if you want to show the line after adding here, kluster will redraw the screen after adding lines itself """ if line_name in self.line_objects: return # this is about 3x slower, use transform_points instead # lne = self.axes.plot(lons, lats, color='blue', linewidth=2, transform=ccrs.Geodetic()) ret = self.axes.projection.transform_points(ccrs.Geodetic(), lons, lats) x = ret[..., 0] y = ret[..., 1] lne = self.axes.plot(x, y, color='blue', linewidth=2) self.line_objects[line_name] = [lats, lons, lne[0]] if refresh: self.refresh_screen() def remove_line(self, line_name, refresh=False): """ Remove a multibeam line from the cartopy display Parameters ---------- line_name name of the multibeam line refresh optional screen refresh, True most of the time, unless you want to remove multiple lines and then refresh at the end """ if line_name in self.line_objects: lne = self.line_objects[line_name][2] lne.remove() self.line_objects.pop(line_name) if refresh: self.refresh_screen() def add_surface(self, surfname: str, lyrname: str, surfx: np.ndarray, surfy: np.ndarray, surfz: np.ndarray, surf_crs: int): """ Add a new surface/layer with the provided data Parameters ---------- surfname path to the surface that is used as a name lyrname band layer name for the provided data surfx 1 dim numpy array for the grid x values surfy 1 dim numpy array for the grid y values surfz 2 dim numpy array for the grid values (depth, uncertainty, etc.) surf_crs integer epsg code """ try: addlyr = True if lyrname in self.active_layers[surfname]: addlyr = False except KeyError: addlyr = True if addlyr: self._add_surface_layer(surfname, lyrname, surfx, surfy, surfz, surf_crs) self.refresh_screen() def hide_surface(self, surfname: str, lyrname: str): """ Hide the surface layer that corresponds to the given names. Parameters ---------- surfname path to the surface that is used as a name lyrname band layer name for the provided data """ try: hidelyr = True if lyrname not in self.active_layers[surfname]: hidelyr = False except KeyError: hidelyr = False if hidelyr: self._hide_surface_layer(surfname, lyrname) return True else: return False def show_surface(self, surfname: str, lyrname: str): """ Cartopy backend currently just deletes/adds surface data, doesn't really hide or show. Return False here to signal we did not hide """ return False def remove_surface(self, surfname: str): """ Remove the surface from memory by removing the name from the surface_objects dict Parameters ---------- surfname path to the surface that is used as a name """ if surfname in self.surface_objects: for lyr in self.surface_objects[surfname]: self.hide_surface(surfname, lyr) surf = self.surface_objects[surfname][lyr][2] surf.remove() self.surface_objects.pop(surfname) self.refresh_screen() def _add_surface_layer(self, surfname: str, lyrname: str, surfx: np.ndarray, surfy: np.ndarray, surfz: np.ndarray, surf_crs: int): """ Add a new surface/layer with the provided data Parameters ---------- surfname path to the surface that is used as a name lyrname band layer name for the provided data surfx 1 dim numpy array for the grid x values surfy 1 dim numpy array for the grid y values surfz 2 dim numpy array for the grid values (depth, uncertainty, etc.) surf_crs integer epsg code """ try: makelyr = True if lyrname in self.surface_objects[surfname]: makelyr = False except KeyError: makelyr = True if makelyr: desired_crs = self.map_proj lon2d, lat2d = np.meshgrid(surfx, surfy) xyz = desired_crs.transform_points(ccrs.epsg(int(surf_crs)), lon2d, lat2d) lons = xyz[..., 0] lats = xyz[..., 1] if lyrname != 'depth': vmin, vmax = np.nanmin(surfz), np.nanmax(surfz) else: # need an outlier resistant min max depth range value twostd = np.nanstd(surfz) med = np.nanmedian(surfz) vmin, vmax = med - twostd, med + twostd # print(vmin, vmax) surfplt = self.axes.pcolormesh(lons, lats, surfz.T, vmin=vmin, vmax=vmax, zorder=10) setextents = False if not self.line_objects and not self.surface_objects: # if this is the first thing you are loading, jump to it's extents setextents = True self._add_to_active_layers(surfname, lyrname) self._add_to_surface_objects(surfname, lyrname, [lats, lons, surfplt]) if setextents: self.set_extents_from_surfaces() else: surfplt = self.surface_objects[surfname][lyrname][2] newsurfplt = self.axes.add_artist(surfplt) # update the object with the newly added artist self.surface_objects[surfname][lyrname][2] = newsurfplt self._add_to_active_layers(surfname, lyrname) def _hide_surface_layer(self, surfname: str, lyrname: str): """ Hide the surface layer that corresponds to the given names. Parameters ---------- surfname path to the surface that is used as a name lyrname band layer name for the provided data """ surfplt = self.surface_objects[surfname][lyrname][2] surfplt.remove() self._remove_from_active_layers(surfname, lyrname) self.refresh_screen() def _add_to_active_layers(self, surfname: str, lyrname: str): """ Add the surface layer to the active layers dict Parameters ---------- surfname path to the surface that is used as a name lyrname band layer name for the provided data """ if surfname in self.active_layers: self.active_layers[surfname].append(lyrname) else: self.active_layers[surfname] = [lyrname] def _add_to_surface_objects(self, surfname: str, lyrname: str, data: list): """ Add the surface layer data to the surface objects dict Parameters ---------- surfname path to the surface that is used as a name lyrname band layer name for the provided data data list of [2dim y values for the grid, 2dim x values for the grid, matplotlib.collections.QuadMesh] """ if surfname in self.surface_objects: self.surface_objects[surfname][lyrname] = data else: self.surface_objects[surfname] = {lyrname: data} def _remove_from_active_layers(self, surfname: str, lyrname: str): """ Remove the surface layer from the active layers dict Parameters ---------- surfname path to the surface that is used as a name lyrname band layer name for the provided data """ if surfname in self.active_layers: if lyrname in self.active_layers[surfname]: self.active_layers[surfname].remove(lyrname) def _remove_from_surface_objects(self, surfname, lyrname): """ Remove the surface layer from the surface objects dict Parameters ---------- surfname path to the surface that is used as a name lyrname band layer name for the provided data """ if surfname in self.surface_objects: if lyrname in self.surface_objects[surfname]: self.surface_objects[surfname].pop(lyrname) def change_line_colors(self, line_names: list, color: str): """ Change the provided line names to the provided color Parameters ---------- line_names list of line names to use as keys in the line objects dict color string color identifier, ex: 'r' or 'red' """ for line in line_names: lne = self.line_objects[line][2] lne.set_color(color) self.selected_line_objects.append(lne) self.refresh_screen() def reset_line_colors(self): """ Reset all lines back to the default color """ for lne in self.selected_line_objects: lne.set_color('b') self.selected_line_objects = [] self.refresh_screen() def _line_select_callback(self, eclick: MouseEvent, erelease: MouseEvent): """ Handle the return of the Matplotlib RectangleSelector, provides an event with the location of the click and an event with the location of the release Parameters ---------- eclick MouseEvent with the position of the initial click erelease MouseEvent with the position of the final release of the mouse button """ x1, y1 = eclick.xdata, eclick.ydata x2, y2 = erelease.xdata, erelease.ydata self.rs.set_visible(False) # set the visible property back to True so that the next move event shows the box self.rs.visible = True # signal with min lat, max lat, min lon, max lon self.box_select.emit(y1, y2, x1, x2) # print("(%3.2f, %3.2f) --> (%3.2f, %3.2f)" % (x1, y1, x2, y2)) def set_extents_from_lines(self): """ Set the maximum extent based on the line_object coordinates """ lats = [] lons = [] for ln in self.line_objects: lats.append(self.line_objects[ln][0]) lons.append(self.line_objects[ln][1]) if not lats or not lons: self.set_extent(90, -90, 100, -100) else: lats = np.concatenate(lats) lons = np.concatenate(lons) self.set_extent(np.max(lats), np.min(lats), np.max(lons), np.min(lons)) self.refresh_screen() def set_extents_from_surfaces(self): """ Set the maximum extent based on the surface_objects coordinates """ lats = [] lons = [] for surf in self.surface_objects: for lyrs in self.surface_objects[surf]: lats.append(self.surface_objects[surf][lyrs][0]) lons.append(self.surface_objects[surf][lyrs][1]) if not lats or not lons: self.set_extent(90, -90, 100, -100) else: lats = np.concatenate(lats) lons = np.concatenate(lons) self.set_extent(np.max(lats), np.min(lats), np.max(lons), np.min(lons)) self.refresh_screen() def clear(self): """ Clear all loaded data including surfaces and lines and refresh the screen """ self.line_objects = {} self.surface_objects = {} self.active_layers = {} self.data_extents = { 'min_lat': 999, 'max_lat': -999, 'min_lon': 999, 'max_lon': -999 } self.selected_line_objects = [] self.set_extent(90, -90, 100, -100) self.refresh_screen() def refresh_screen(self): """ Reset to the original zoom/extents """ self.axes.relim() self.axes.autoscale_view() self.fig.canvas.draw_idle() self.fig.canvas.flush_events()