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)
示例#3
0
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)
示例#5
0
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
示例#11
0
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)
示例#12
0
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)
示例#14
0
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)
示例#19
0
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)
示例#20
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
示例#21
0
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
示例#27
0
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)
示例#29
0
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)
示例#30
0
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()