예제 #1
1
class WeatherStationBrowser(QWidget):
    """
    Widget that allows the user to browse and select ECCC climate stations.
    """

    ConsoleSignal = QSignal(str)
    staListSignal = QSignal(list)

    PROV_NAME = [x[0].title() for x in PROV_NAME_ABB]
    PROV_ABB = [x[1] for x in PROV_NAME_ABB]

    def __init__(self, parent=None):
        super(WeatherStationBrowser, self).__init__(parent)
        self.stn_finder_worker = WeatherStationFinder()
        self.stn_finder_worker.sig_load_database_finished.connect(
            self.receive_load_database)
        self.stn_finder_thread = QThread()
        self.stn_finder_worker.moveToThread(self.stn_finder_thread)

        self.station_table = WeatherSationView()
        self.waitspinnerbar = WaitSpinnerBar()
        self.stn_finder_worker.sig_progress_msg.connect(
            self.waitspinnerbar.set_label)
        self.__initUI__()

        self.start_load_database()

    def __initUI__(self):
        self.setWindowTitle('Weather Stations Browser')
        self.setWindowIcon(icons.get_icon('master'))
        self.setWindowFlags(Qt.Window)

        now = datetime.now()

        # ---- Tab Widget Search

        # ---- Proximity filter groupbox

        label_Lat = QLabel('Latitude :')
        label_Lat2 = QLabel('North')

        self.lat_spinBox = QDoubleSpinBox()
        self.lat_spinBox.setAlignment(Qt.AlignCenter)
        self.lat_spinBox.setSingleStep(0.1)
        self.lat_spinBox.setValue(0)
        self.lat_spinBox.setMinimum(0)
        self.lat_spinBox.setMaximum(180)
        self.lat_spinBox.setSuffix(u' °')
        self.lat_spinBox.valueChanged.connect(self.proximity_grpbox_toggled)

        label_Lon = QLabel('Longitude :')
        label_Lon2 = QLabel('West')

        self.lon_spinBox = QDoubleSpinBox()
        self.lon_spinBox.setAlignment(Qt.AlignCenter)
        self.lon_spinBox.setSingleStep(0.1)
        self.lon_spinBox.setValue(0)
        self.lon_spinBox.setMinimum(0)
        self.lon_spinBox.setMaximum(180)
        self.lon_spinBox.setSuffix(u' °')
        self.lon_spinBox.valueChanged.connect(self.proximity_grpbox_toggled)

        self.radius_SpinBox = QComboBox()
        self.radius_SpinBox.addItems(['25 km', '50 km', '100 km', '200 km'])
        self.radius_SpinBox.currentIndexChanged.connect(
            self.search_filters_changed)

        prox_search_grid = QGridLayout()
        row = 0
        prox_search_grid.addWidget(label_Lat, row, 1)
        prox_search_grid.addWidget(self.lat_spinBox, row, 2)
        prox_search_grid.addWidget(label_Lat2, row, 3)
        row += 1
        prox_search_grid.addWidget(label_Lon, row, 1)
        prox_search_grid.addWidget(self.lon_spinBox, row, 2)
        prox_search_grid.addWidget(label_Lon2, row, 3)
        row += 1
        prox_search_grid.addWidget(QLabel('Search Radius :'), row, 1)
        prox_search_grid.addWidget(self.radius_SpinBox, row, 2)

        prox_search_grid.setColumnStretch(0, 100)
        prox_search_grid.setColumnStretch(4, 100)
        prox_search_grid.setRowStretch(row + 1, 100)
        prox_search_grid.setHorizontalSpacing(20)
        prox_search_grid.setContentsMargins(10, 10, 10, 10)  # (L, T, R, B)

        self.prox_grpbox = QGroupBox("Proximity filter :")
        self.prox_grpbox.setCheckable(True)
        self.prox_grpbox.setChecked(False)
        self.prox_grpbox.toggled.connect(self.proximity_grpbox_toggled)
        self.prox_grpbox.setLayout(prox_search_grid)

        # ---- Province filter

        prov_names = ['All']
        prov_names.extend(self.PROV_NAME)
        self.prov_widg = QComboBox()
        self.prov_widg.addItems(prov_names)
        self.prov_widg.setCurrentIndex(0)
        self.prov_widg.currentIndexChanged.connect(self.search_filters_changed)

        layout = QGridLayout()
        layout.addWidget(self.prov_widg, 2, 1)
        layout.setColumnStretch(2, 100)
        layout.setVerticalSpacing(10)

        prov_grpbox = QGroupBox("Province filter :")
        prov_grpbox.setLayout(layout)

        # ---- Data availability filter

        # Number of years with data

        self.nbrYear = QSpinBox()
        self.nbrYear.setAlignment(Qt.AlignCenter)
        self.nbrYear.setSingleStep(1)
        self.nbrYear.setMinimum(0)
        self.nbrYear.setValue(3)
        self.nbrYear.valueChanged.connect(self.search_filters_changed)

        subgrid1 = QGridLayout()
        subgrid1.addWidget(self.nbrYear, 0, 0)
        subgrid1.addWidget(QLabel('years of data between'), 0, 1)

        subgrid1.setHorizontalSpacing(10)
        subgrid1.setContentsMargins(0, 0, 0, 0)  # (L, T, R, B)
        subgrid1.setColumnStretch(2, 100)

        # Year range

        self.minYear = QSpinBox()
        self.minYear.setAlignment(Qt.AlignCenter)
        self.minYear.setSingleStep(1)
        self.minYear.setMinimum(1840)
        self.minYear.setMaximum(now.year)
        self.minYear.setValue(1840)
        self.minYear.valueChanged.connect(self.minYear_changed)

        label_and = QLabel('and')
        label_and.setAlignment(Qt.AlignCenter)

        self.maxYear = QSpinBox()
        self.maxYear.setAlignment(Qt.AlignCenter)
        self.maxYear.setSingleStep(1)
        self.maxYear.setMinimum(1840)
        self.maxYear.setMaximum(now.year)
        self.maxYear.setValue(now.year)
        self.maxYear.valueChanged.connect(self.maxYear_changed)

        subgrid2 = QGridLayout()
        subgrid2.addWidget(self.minYear, 0, 0)
        subgrid2.addWidget(label_and, 0, 1)
        subgrid2.addWidget(self.maxYear, 0, 2)

        subgrid2.setHorizontalSpacing(10)
        subgrid2.setContentsMargins(0, 0, 0, 0)  # (L, T, R, B)
        subgrid2.setColumnStretch(4, 100)

        # Subgridgrid assembly

        grid = QGridLayout()

        grid.addWidget(QLabel('Search for stations with at least'), 0, 0)
        grid.addLayout(subgrid1, 1, 0)
        grid.addLayout(subgrid2, 2, 0)

        grid.setVerticalSpacing(5)
        grid.setRowStretch(0, 100)
        # grid.setContentsMargins(0, 0, 0, 0)  # (L, T, R, B)

        self.year_widg = QGroupBox("Data Availability filter :")
        self.year_widg.setLayout(grid)

        # ---- Toolbar

        self.btn_addSta = btn_addSta = QPushButton('Add')
        btn_addSta.setIcon(icons.get_icon('add2list'))
        btn_addSta.setIconSize(icons.get_iconsize('small'))
        btn_addSta.setToolTip('Add selected found weather stations to the '
                              'current list of weather stations.')
        btn_addSta.clicked.connect(self.btn_addSta_isClicked)

        btn_save = QPushButton('Save')
        btn_save.setIcon(icons.get_icon('save'))
        btn_save.setIconSize(icons.get_iconsize('small'))
        btn_save.setToolTip('Save current found stations info in a csv file.')
        btn_save.clicked.connect(self.btn_save_isClicked)

        self.btn_fetch = btn_fetch = QPushButton('Fetch')
        btn_fetch.setIcon(icons.get_icon('refresh'))
        btn_fetch.setIconSize(icons.get_iconsize('small'))
        btn_fetch.setToolTip("Updates the climate station database by"
                             " fetching it again from the ECCC ftp server.")
        btn_fetch.clicked.connect(self.btn_fetch_isClicked)

        toolbar_grid = QGridLayout()
        toolbar_widg = QWidget()

        for col, btn in enumerate([btn_addSta, btn_save, btn_fetch]):
            toolbar_grid.addWidget(btn, 0, col + 1)

        toolbar_grid.setColumnStretch(toolbar_grid.columnCount(), 100)
        toolbar_grid.setSpacing(5)
        toolbar_grid.setContentsMargins(0, 30, 0, 0)  # (L, T, R, B)

        toolbar_widg.setLayout(toolbar_grid)

        # ---- Left Panel

        panel_title = QLabel('<b>Weather Station Search Criteria :</b>')

        left_panel = QFrame()
        left_panel_grid = QGridLayout()

        left_panel_grid.addWidget(panel_title, 0, 0)
        left_panel_grid.addWidget(self.prox_grpbox, 1, 0)
        left_panel_grid.addWidget(prov_grpbox, 2, 0)
        left_panel_grid.addWidget(self.year_widg, 3, 0)
        left_panel_grid.setRowStretch(4, 100)
        left_panel_grid.addWidget(toolbar_widg, 5, 0)

        left_panel_grid.setVerticalSpacing(20)
        left_panel_grid.setContentsMargins(0, 0, 0, 0)  # (L, T, R, B)
        left_panel.setLayout(left_panel_grid)

        # ----- Main grid

        # Widgets

        vLine1 = QFrame()
        vLine1.setFrameStyle(StyleDB().VLine)

        # Grid

        main_layout = QGridLayout(self)

        main_layout.addWidget(left_panel, 0, 0)
        main_layout.addWidget(vLine1, 0, 1)
        main_layout.addWidget(self.station_table, 0, 2)
        main_layout.addWidget(self.waitspinnerbar, 0, 2)

        main_layout.setContentsMargins(10, 10, 10, 10)  # (L,T,R,B)
        main_layout.setRowStretch(0, 100)
        main_layout.setHorizontalSpacing(15)
        main_layout.setVerticalSpacing(5)
        main_layout.setColumnStretch(col, 100)

    @property
    def stationlist(self):
        return self.station_table.get_stationlist()

    @property
    def search_by(self):
        return ['proximity', 'province'][self.tab_widg.currentIndex()]

    @property
    def prov(self):
        if self.prov_widg.currentIndex() == 0:
            return self.PROV_ABB
        else:
            return self.PROV_ABB[self.prov_widg.currentIndex() - 1]

    @property
    def lat(self):
        return self.lat_spinBox.value()

    def set_lat(self, x, silent=True):
        if silent:
            self.lat_spinBox.blockSignals(True)
        self.lat_spinBox.setValue(x)
        self.lat_spinBox.blockSignals(False)
        self.proximity_grpbox_toggled()

    @property
    def lon(self):
        return self.lon_spinBox.value()

    def set_lon(self, x, silent=True):
        if silent:
            self.lon_spinBox.blockSignals(True)
        self.lon_spinBox.setValue(x)
        self.lon_spinBox.blockSignals(False)
        self.proximity_grpbox_toggled()

    @property
    def rad(self):
        return int(self.radius_SpinBox.currentText()[:-3])

    @property
    def prox(self):
        if self.prox_grpbox.isChecked():
            return (self.lat, -self.lon, self.rad)
        else:
            return None

    @property
    def year_min(self):
        return int(self.minYear.value())

    def set_yearmin(self, x, silent=True):
        if silent:
            self.minYear.blockSignals(True)
        self.minYear.setValue(x)
        self.minYear.blockSignals(False)

    @property
    def year_max(self):
        return int(self.maxYear.value())

    def set_yearmax(self, x, silent=True):
        if silent:
            self.maxYear.blockSignals(True)
        self.maxYear.setValue(x)
        self.maxYear.blockSignals(False)

    @property
    def nbr_of_years(self):
        return int(self.nbrYear.value())

    def set_yearnbr(self, x, silent=True):
        if silent:
            self.nbrYear.blockSignals(True)
        self.nbrYear.setValue(x)
        self.nbrYear.blockSignals(False)

    # ---- Weather Station Finder Handlers

    def start_load_database(self, force_fetch=False):
        """Start the process of loading the climate station database."""
        if self.stn_finder_thread.isRunning():
            return

        self.station_table.clear()
        self.waitspinnerbar.show()

        # Start the downloading process.
        if force_fetch:
            self.stn_finder_thread.started.connect(
                self.stn_finder_worker.fetch_database)
        else:
            self.stn_finder_thread.started.connect(
                self.stn_finder_worker.load_database)
        self.stn_finder_thread.start()

    @QSlot()
    def receive_load_database(self):
        """Handles when loading the database is finished."""
        # Disconnect the thread.
        self.stn_finder_thread.started.disconnect()

        # Quit the thread.
        self.stn_finder_thread.quit()
        waittime = 0
        while self.stn_finder_thread.isRunning():
            sleep(0.1)
            waittime += 0.1
            if waittime > 15:  # pragma: no cover
                print("Unable to quit the thread.")
                break
        # Force an update of the GUI.
        self.proximity_grpbox_toggled()
        if self.stn_finder_worker.data is None:
            self.waitspinnerbar.show_warning_icon()
        else:
            self.waitspinnerbar.hide()

    # ---- GUI handlers

    def show(self):
        super(WeatherStationBrowser, self).show()
        qr = self.frameGeometry()
        if self.parent():
            parent = self.parent()
            wp = parent.frameGeometry().width()
            hp = parent.frameGeometry().height()
            cp = parent.mapToGlobal(QPoint(wp / 2, hp / 2))
        else:
            cp = QDesktopWidget().availableGeometry().center()

        qr.moveCenter(cp)
        self.move(qr.topLeft())

    # -------------------------------------------------------------------------

    def minYear_changed(self):
        min_yr = min_yr = max(self.minYear.value(), 1840)

        now = datetime.now()
        max_yr = now.year

        self.maxYear.setRange(min_yr, max_yr)
        self.search_filters_changed()

    def maxYear_changed(self):
        min_yr = 1840

        now = datetime.now()
        max_yr = min(self.maxYear.value(), now.year)

        self.minYear.setRange(min_yr, max_yr)
        self.search_filters_changed()

    # ---- Toolbar Buttons Handlers

    def btn_save_isClicked(self):
        ddir = os.path.join(os.getcwd(), 'weather_station_list.csv')
        filename, ftype = QFileDialog().getSaveFileName(
            self, 'Save normals', ddir, '*.csv;;*.xlsx;;*.xls')
        self.station_table.save_stationlist(filename)

    def btn_addSta_isClicked(self):
        rows = self.station_table.get_checked_rows()
        if len(rows) > 0:
            staList = self.station_table.get_content4rows(rows)
            self.staListSignal.emit(staList)
            print('Selected stations sent to list')
        else:
            print('No station currently selected')

    def btn_fetch_isClicked(self):
        """Handles when the button fetch is clicked."""
        self.start_load_database(force_fetch=True)

    # ---- Search Filters Handlers

    def proximity_grpbox_toggled(self):
        """
        Set the values for the reference geo coordinates that are used in the
        WeatherSationView to calculate the proximity values and forces a
        refresh of the content of the table.
        """
        if self.prox_grpbox.isChecked():
            self.station_table.set_geocoord((self.lat, -self.lon))
        else:
            self.station_table.set_geocoord(None)
        self.search_filters_changed()

    def search_filters_changed(self):
        """
        Search for weather stations with the current filter values and forces
        an update of the station table content.
        """
        if self.stn_finder_worker.data is not None:
            stnlist = self.stn_finder_worker.get_stationlist(
                prov=self.prov,
                prox=self.prox,
                yrange=(self.year_min, self.year_max, self.nbr_of_years))
            self.station_table.populate_table(stnlist)
예제 #2
0
class FilterButton(QToolButtonBase):
    """
    A tool button to that contains a menu with a list of all projects and tags
    that can be checked in order to filter which activities are shown in the
    overview table.
    """
    sig_projects_checkstate_changed = QSignal(dict)
    sig_tags_checkstate_changed = QSignal(dict)

    def __init__(self, client=None):
        super().__init__('filters', 'small', None)
        self.setPopupMode(self.InstantPopup)
        self.client = client
        self.setup_menu()

    def setup_menu(self):
        """Setup the button menu."""
        menu = QMenu()

        self.projects_menu = FilterProjectsMenu(self.client, self)
        menu.addMenu(self.projects_menu)
        self.projects_menu.sig_items_checkstate_changed.connect(
            self.sig_projects_checkstate_changed.emit)

        self.tags_menu = FilterTagsMenu(self.client, self)
        menu.addMenu(self.tags_menu)
        self.tags_menu.sig_items_checkstate_changed.connect(
            self.sig_tags_checkstate_changed.emit)

        self.setMenu(menu)
예제 #3
0
class DropDownButton(QToolButton):
    """QToolButton with QComboBox dropdown-like capability."""
    sig_year_selected = QSignal(int)

    def __init__(self, parent=None, icon=None, icon_size=QSize(28, 28)):
        super(QToolButton, self).__init__(parent=None)
        if icon:
            self.setIcon(icon)
        self.setIconSize(icon_size)
        self.setAutoRaise(True)
        self.setFocusPolicy(Qt.NoFocus)

        self.droplist = DropDownList(self)

        self.clicked.connect(self.show_dropdown)
        self.sig_year_selected = self.droplist.sig_year_selected

    def addItems(self, items):
        """Clear and add items to the button dropdown list."""
        self.droplist.clear()
        self.droplist.addItems(items)

    def show_dropdown(self):
        """Show and set focus on the dropdown list."""
        self.droplist.show()
        self.droplist.setFocus()
예제 #4
0
class LangToolButton(DropdownToolButton):
    """A toolbutton that allow language selection."""
    TOOLTIP = ""
    LANGUAGES = {'french': 'FR', 'english': 'EN'}
    sig_lang_changed = QSignal(str)

    def __init__(self, parent=None):
        DropdownToolButton.__init__(self, 'language', parent)
        self.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
        self.__language = 'english'
        self.setText(self.LANGUAGES[self.language])

    def setup_menu(self):
        """Setup the languages in the menu."""
        super(LangToolButton, self).setup_menu()
        self.menu().addAction('English', lambda: self.set_language('english'))
        self.menu().addAction('French', lambda: self.set_language('french'))

    @property
    def language(self):
        """Return the current language."""
        return self.__language

    def set_language(self, lang):
        """Set the namespace for the language."""
        lang = lang.lower()
        lang = lang if lang in list(self.LANGUAGES.keys()) else 'english'
        if lang != self.__language:
            self.__language = lang
            self.setText(self.LANGUAGES[lang])
            self.sig_lang_changed.emit(lang)
예제 #5
0
class OnOffToolButton(QToolButtonBase):
    """A tool button that can be toggled on or off by clicking on it."""
    sig_value_changed = QSignal(bool)

    def __init__(self, icon, icon_raised=None, size=None, parent=None):
        self._icon = icons.get_icon(icon) if isinstance(icon, str) else icon
        self._icon_raised = (icons.get_icon(icon_raised) if isinstance(
            icon_raised, str) else icon_raised)
        super(OnOffToolButton, self).__init__(self._icon, parent)
        self.installEventFilter(self)
        if size is not None:
            self.setIconSize(icons.get_iconsize(size))

    def eventFilter(self, widget, event):
        if event.type() == QEvent.MouseButtonPress and self.isEnabled():
            self.setValue(not self.value())
        return super(OnOffToolButton, self).eventFilter(widget, event)

    def value(self):
        """Return True if autoRaise is False and False if True."""
        return not self.autoRaise()

    def setValue(self, value, silent=False):
        """Set autoRaise to False if value is True and to True if False."""
        self.setAutoRaise(not bool(value))
        self._setup_icon()
        if not silent:
            self.sig_value_changed.emit(self.value())

    def _setup_icon(self):
        """Setup the icon of the button according to its auto raise state."""
        if self._icon_raised is None:
            return
        icon = self._icon_raised if self.value() else self._icon
        self.setIcon(icon)
예제 #6
0
class WatsonProjectModel(QAbstractListModel):
    sig_model_changed = QSignal()

    def __init__(self, client):
        super(WatsonProjectModel, self).__init__()
        self.client = client

        self.dataChanged.connect(self.model_changed)
        self.rowsInserted.connect(self.model_changed)
        self.modelReset.connect(self.model_changed)
        self.rowsRemoved.connect(self.model_changed)

    def model_changed(self):
        """Emit a signal whenever the model is changed."""
        self.sig_model_changed.emit()

    def rowCount(self, parent=QModelIndex()):
        """Qt method override. Return the number of row of the table."""
        return len(self.client.projects)

    def data(self, index, role=Qt.DisplayRole):
        """Qt method override."""
        if role == Qt.DisplayRole:
            return self.client.projects[index.row()]
        elif role == Qt.ToolTipRole:
            return self.client.projects[index.row()]
        else:
            return QVariant()

    # ---- Utils

    @property
    def projects(self):
        return self.client.projects
예제 #7
0
class DropDownToolButton(QToolButtonBase):
    """A QToolButton with QComboBox dropdown-like capability."""
    sig_item_selected = QSignal(str)

    def __init__(self,
                 icon=None,
                 iconsize='normal',
                 style='icon_only',
                 parent=None):
        super(DropDownToolButton, self).__init__(icon, iconsize, parent)
        self.set_style(style)

        # Set an empty menu to show the drowdown arrow.
        self.setMenu(QMenu())

        self.droplist = DropDownList(self)
        self.clicked.connect(self.show_dropdown)
        self.droplist.sig_item_selected.connect(self.new_item_selected)

    def set_style(self, style):
        """Set whether text is shown next to the icon or not."""
        self.setToolButtonStyle({
            'icon_only': Qt.ToolButtonIconOnly,
            'text_only': Qt.ToolButtonTextOnly,
            'text_beside': Qt.ToolButtonTextBesideIcon,
            'text_under': Qt.ToolButtonTextUnderIcon
        }[style])

    def addItems(self, items):
        """Clear and add items to the button dropdown list."""
        self.droplist.clear()
        self.droplist.addItems(items)
        self.setCurrentIndex(0)

    def currentIndex(self):
        """Return the index of the current item in the dropdown button."""
        return self.droplist.currentRow()

    def setCurrentIndex(self, index):
        """Set the index of the current item in the dropdown button."""
        if index != self.currentIndex():
            self.droplist.setCurrentRow(index)
            current_item = self.droplist.currentItem()
            if current_item is not None:
                self.new_item_selected(current_item.text())
            else:
                self.new_item_selected('')

    def show_dropdown(self):
        """Show and set focus on the dropdown list."""
        self.droplist.show()
        self.droplist.setFocus()

    def new_item_selected(self, text):
        """handle when a new item is selected"""
        self.setText(text)
        self.sig_item_selected.emit(text)
예제 #8
0
class DropDownList(QListWidget):
    sig_year_selected = QSignal(int)

    def __init__(self, parent):
        super(DropDownList, self).__init__(parent)
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.Window)
        self.hide()

    def show(self):
        """
        Qt method override to show the dropdown list under its parent,
        aligned to its left edge.
        """
        point = self.parent().rect().bottomLeft()
        global_point = self.parent().mapToGlobal(point)
        self.move(global_point)
        self.resizeColumnsToContents()
        super(DropDownList, self).show()

    def resizeColumnsToContents(self):
        """Adjust the width of the list to its content."""
        self.setFixedWidth(
            self.sizeHintForColumn(0) + 2 * self.frameWidth() +
            QApplication.style().pixelMetric(QStyle.PM_ScrollBarExtent))

    def keyPressEvent(self, event):
        """
        Qt method override to select the highlighted item and hide the list
        if the Enter key is pressed.
        """
        super(DropDownList, self).keyPressEvent(event)
        if event.key() == Qt.Key_Return:
            self.sig_year_selected.emit(int(self.currentItem().text()))
            self.hide()

    def mousePressEvent(self, event):
        """
        Qt method override to select and hide the list if an item is clicked
        with the left button of the mouse.
        """
        super(DropDownList, self).mousePressEvent(event)
        if event.button() == 1:
            self.sig_year_selected.emit(int(self.currentItem().text()))
            self.hide()

    def focusOutEvent(self, event):
        """Qt method override to hide the list when focus is lost."""
        event.ignore()
        # Don't hide it on Mac when main window loses focus because
        # keyboard input is lost
        if sys.platform == "darwin":
            if event.reason() != Qt.ActiveWindowFocusReason:
                self.hide()
        else:
            self.hide()
예제 #9
0
class TabBar(QTabBar):

    sig_resized = QSignal()
    sig_tab_layout_changed = QSignal()

    def __init__(self, tab_widget, parent=None):
        super(TabBar, self).__init__(parent=None)

        self.__tab_widget = tab_widget

        self.__oldIndex = -1
        self.__newIndex = -1
        self.currentChanged.connect(self.storeIndex)

    def tabWidget(self):
        return self.__tab_widget

    def tabSizeHint(self, index):
        width = QTabBar.tabSizeHint(self, index).width()
        return QSize(width, 32)

    def sizeHint(self):
        sizeHint = QTabBar.sizeHint(self)
        w = sizeHint.width() + self.tabWidget().about_btn.size().width()
        return QSize(w, 32)

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self.sig_resized.emit()

    def tabLayoutChange(self):
        super().tabLayoutChange()
        self.sig_resized.emit()

    def storeIndex(self, index):
        self.__oldIndex = copy.copy(self.__newIndex)
        self.__newIndex = index

    def previousIndex(self):
        return self.__oldIndex
예제 #10
0
class RangeSpinBoxes(QWidget):
    """
    Consists of two spinboxes that are linked togheter so that one represent a
    lower boundary and the other one the upper boundary of a range.
    """
    sig_range_changed = QSignal(tuple)

    def __init__(self,
                 min_value=0,
                 max_value=100,
                 orientation=Qt.Horizontal,
                 parent=None):
        super(RangeSpinBoxes, self).__init__(parent)
        self.spb_lower = SmartSpinBox()
        self.spb_upper = SmartSpinBox()

        self.spb_lower.sig_value_changed.connect(
            self.constrain_bounds_to_minmax)
        self.spb_upper.sig_value_changed.connect(
            self.constrain_bounds_to_minmax)

        self.setRange(min_value, max_value)

    @property
    def lower_bound(self):
        return self.spb_lower.value()

    @property
    def upper_bound(self):
        return self.spb_upper.value()

    def setRange(self, min_value, max_value):
        """Set the min max values of the range for both spin boxes."""
        if min_value > max_value:
            raise ValueError("min_value > max_value")
        self.spb_lower.setRange(min_value, max_value)
        self.spb_upper.setRange(min_value, max_value)
        self.spb_lower.setValueSilently(min_value)
        self.spb_upper.setValueSilently(max_value)

    @QSlot(float, float)
    def constrain_bounds_to_minmax(self, new_value, old_value):
        """
        Makes sure that the new value is within the min and max values that
        were set for the range. Also makes sure that the
        """
        if new_value > self.spb_upper.value():
            self.spb_upper.setValueSilently(new_value)
        if new_value < self.spb_lower.value():
            self.spb_lower.setValueSilently(new_value)
        self.sig_range_changed.emit((self.lower_bound, self.upper_bound))
예제 #11
0
class WorkerUpdates(QObject):
    """
    Worker that checks for releases using the Github API.
    """
    sig_ready = QSignal()

    def __init__(self):
        super(WorkerUpdates, self).__init__()
        self.error = None
        self.latest_release = None
        self.update_available = False

    def start(self):
        """Main method of the WorkerUpdates worker."""
        self.update_available = False
        self.latest_release = __version__
        self.error = None
        try:
            page = requests.get(__releases_api__)
            data = page.json()
            releases = [
                item['tag_name'].replace('gwhat-', '') for item in data
                if item['tag_name'].startswith("gwhat")
            ]
            result = check_update_available(__version__, releases)
            self.update_available, self.latest_release = result
        except requests.exceptions.HTTPError:
            self.error = ('Unable to retrieve information because of.'
                          ' an http error.')
        except requests.exceptions.ConnectionError:
            self.error = ('Unable to connect to the internet.<br><br>Make '
                          'sure the connection is working properly.')
        except requests.exceptions.Timeout:
            self.error = ('Unable to retrieve information because the'
                          ' connection timed out.')
        except Exception:
            self.error = ('Unable to check for updates because of'
                          ' an unexpected error.')
        self.sig_ready.emit()
예제 #12
0
class WatsonSortFilterProxyModel(QSortFilterProxyModel):
    sig_sourcemodel_changed = QSignal()
    sig_total_seconds_changed = QSignal(float)

    def __init__(self, source_model, date_span=None):
        super(WatsonSortFilterProxyModel, self).__init__()
        self.setSourceModel(source_model)
        self.date_span = date_span
        self.total_seconds = None
        self.project_filters = None
        self.tag_filters = None

        source_model.dataChanged.connect(self.source_model_changed)
        source_model.rowsInserted.connect(self.source_model_changed)
        source_model.rowsRemoved.connect(self.source_model_changed)
        source_model.modelReset.connect(self.source_model_changed)

    def source_model_changed(self):
        """Emit a signal whenever the source model changes."""
        self.sig_sourcemodel_changed.emit()
        self.calcul_total_seconds()

    def set_date_span(self, date_span):
        """Set the date span to use to filter the row of the source model."""
        if date_span != self.date_span:
            self.date_span = date_span
            self.invalidateFilter()
            self.calcul_total_seconds()

    def set_project_filters(self, project_filters):
        """
        Set the list of project for which activies are shown in the table.
        """
        if project_filters != self.project_filters:
            self.project_filters = project_filters
            self.invalidateFilter()
            self.calcul_total_seconds()

    def set_tag_filters(self, tag_filters):
        """
        Set the list of tags for which activies are shown in the table.
        """
        if tag_filters != self.tag_filters:
            self.tag_filters = tag_filters
            self.invalidateFilter()
            self.calcul_total_seconds()

    def filterAcceptsRow(self, source_row, source_parent):
        """Qt method override."""
        if self.project_filters is not None:
            project = self.sourceModel().client.frames[source_row].project
            if not self.project_filters.get(project, True):
                return False
        if self.tag_filters is not None:
            tags = self.sourceModel().client.frames[source_row].tags or ['']
            if not any([self.tag_filters.get(tag, True) for tag in tags]):
                return False
        if self.date_span is None:
            return True
        else:
            return self.is_in_date_span(source_row, self.date_span)

    def is_in_date_span(self, source_row, date_span):
        """
        Return whether the start time of the frame stored at the specified
        row of the source model is within the specified date_span.
        """
        frame_start = self.sourceModel().client.frames[source_row].start
        return (frame_start >= date_span[0] and frame_start <= date_span[1])

    def calcul_total_seconds(self):
        """
        Return the total number of seconds of all the activities accepted
        by the proxy model.
        """
        timedelta = datetime.timedelta()
        for i in range(self.rowCount()):
            source_row = self.mapToSource(self.index(i, 0)).row()
            frame = self.sourceModel().client.frames[source_row]
            timedelta = timedelta + (frame.stop - frame.start)

        total_seconds_old = self.total_seconds
        total_seconds_new = timedelta.total_seconds()
        if total_seconds_new != total_seconds_old:
            self.total_seconds = total_seconds_new
            total_seconds_old = total_seconds_old or 0

            self.sig_total_seconds_changed.emit(self.total_seconds)
            self.sourceModel().sig_total_seconds_changed.emit(
                total_seconds_new - total_seconds_old)

    def get_accepted_row_count(self):
        """Return the number of rows that were accepted by the proxy."""
        return self.rowCount()

    # ---- Map proxy to source

    @property
    def projects(self):
        return self.sourceModel().client.projects

    def get_frame_from_index(self, proxy_index):
        """Return the frame stored at the row of index."""
        return self.sourceModel().get_frame_from_index(
            self.mapToSource(proxy_index))

    def get_project_from_index(self, proxy_index):
        """Return the project of the frame corresponding to the model index."""
        return self.sourceModel().get_project_from_index(
            self.mapToSource(proxy_index))

    def get_tags_from_index(self, proxy_index):
        """Return a list of tags for the frame from a table index."""
        return self.sourceModel().get_tags_from_index(
            self.mapToSource(proxy_index))

    def get_frameid_from_index(self, proxy_index):
        """Return the frame id from a table index."""
        return self.sourceModel().get_frameid_from_index(
            self.mapToSource(proxy_index))

    def emit_btn_delrow_clicked(self, proxy_index):
        """
        Send a signal via the source model with the model index where the
        button to delete an activity has been clicked.
        """
        self.sourceModel().emit_btn_delrow_clicked(
            self.mapToSource(proxy_index))

    def editFrame(self,
                  proxy_index,
                  start=None,
                  stop=None,
                  project=None,
                  message=None,
                  tags=None):
        """Map proxy method to source."""
        self.sourceModel().editFrame(self.mapToSource(proxy_index),
                                     start=start,
                                     stop=stop,
                                     project=project,
                                     message=message,
                                     tags=tags)

    def editDateTime(self, proxy_index, date_time):
        """Map proxy method to source."""
        self.sourceModel().editDateTime(self.mapToSource(proxy_index),
                                        date_time)
예제 #13
0
class WeatherData(QObject):
    """
    This class contains all the weather data and weather station info
    that are needed for the gapfilling algorithm that is defined in the
    *GapFillWeather* class.
    """
    sig_task_progress = QSignal(int)
    sig_status_message = QSignal(str)
    sig_corrcoeff_calculated = QSignal()

    def __init__(self):
        super().__init__()

        self.data = None
        self.metadata = None
        self._corrcoef = None

    @property
    def filenames(self):
        """
        Return the list of file paths from which data were loaded.
        """
        if self.metadata is None or self.metadata.empty:
            return []
        else:
            return self.metadata['filename'].tolist()

    @property
    def station_names(self):
        """
        Return the list of station names for which data are loaded in memory.
        """
        if self.metadata is None or self.metadata.empty:
            return []
        else:
            return self.metadata['Station Name'].tolist()

    @property
    def station_ids(self):
        """
        Return the list of station IDs for which data are loaded in memory.
        """
        if self.metadata is None or self.metadata.empty:
            return []
        else:
            return self.metadata.index.tolist()

    @property
    def datetimes(self):
        return (self.data['Ptot'].index.values
                if self.data is not None else [])

    def count(self):
        """
        Return the number of datasets that are currently loaded.
        """
        return len(self.station_ids)

    # ---- Load and format data.
    def load_and_format_data(self, paths):
        """
        Parameters
        ----------
        paths: list
            A list of absolute paths containing daily weater data files
        """
        self.data = {var: pd.DataFrame([]) for var in VARNAMES}
        self.metadata = pd.DataFrame([])
        self._corrcoef = None
        if len(paths) == 0:
            return

        self.sig_status_message.emit('Reading data from csv files... 0%')
        for i, path in enumerate(paths):
            try:
                sta_metadata, sta_data = read_weather_datafile(path)
            except Exception:
                print("Unable to read data from '{}'".format(
                    osp.basename(path)))
            else:
                # Add the first and last date of the dataset to the metadata.
                sta_metadata['first_date'] = min(sta_data.index).date()
                sta_metadata['last_date'] = max(sta_data.index).date()

                # Append the metadata of this station to that of the others.
                sta_id = sta_metadata['Station ID']
                if ('Station ID' in self.metadata.columns
                        and sta_id in self.metadata['Station ID']):
                    print(("A dataset for station '{}' already exists. "
                           "Skipping reading data from '{}'.").format(
                               sta_id, osp.basename(path)))
                    continue
                self.metadata = self.metadata.append(sta_metadata,
                                                     ignore_index=True)

                # Append the data of this station to that of the others.
                for name in VARNAMES:
                    self.data[name] = self.data[name].merge(
                        sta_data[[name]].rename(columns={name: sta_id}),
                        left_index=True,
                        right_index=True,
                        how='outer')
            percent_progress = int(i / len(paths) * 100)
            self.sig_task_progress.emit(percent_progress)
            self.sig_status_message.emit(
                'Reading data from csv files... {:d}%'.format(
                    percent_progress))

        # Make the daily time series continuous.
        for name in VARNAMES:
            self.data[name] = self.data[name].resample('1D').asfreq()

        # Set the index of the metadata.
        self.metadata = self.metadata.set_index('Station ID', drop=True)

    def load_from_binary(self, dirname):
        """Load the data and metadata from binary files."""
        A = np.load(osp.join(dirname, '__cache__', 'fdata.npy'),
                    allow_pickle=True).item()
        self.data = A['data']
        self.metadata = A['metadata']
        self._corrcoef = A.get('corrcoef', None)

    def save_to_binary(self, dirname):
        """Save the data and metadata to binary files."""
        print('Caching data...')
        os.makedirs(osp.join(dirname, '__cache__'), exist_ok=True)
        A = {
            'data': self.data,
            'metadata': self.metadata,
            'corrcoef': self._corrcoef
        }
        np.save(osp.join(dirname, '__cache__', 'fdata.npy'), A)
        print('Data cached succesfully.')

    # ---- Utilities
    def alt_and_dist_calc(self, target_station_id):
        """
        Compute the horizontal distances in km and the altitude differences
        in m between the target station and each neighboring station.
        """
        alt_and_dist = (self.metadata[['Latitude', 'Longitude',
                                       'Elevation']].copy())

        # Calcul horizontal and vertical distances of neighboring stations
        # from target.
        alt_and_dist['hordist'] = calc_dist_from_coord(
            alt_and_dist.loc[target_station_id, 'Latitude'],
            alt_and_dist.loc[target_station_id, 'Longitude'],
            alt_and_dist['Latitude'].values, alt_and_dist['Longitude'].values)

        alt_and_dist['altdiff'] = (
            alt_and_dist['Elevation'].values -
            alt_and_dist.loc[target_station_id, 'Elevation'])

        return alt_and_dist

    def compute_correlation_coeff(self, target_station_id):
        """
        Compute the correlation coefficients between the target
        station and the neighboring stations for each meteorological variable.
        """
        if self._corrcoef is None:
            message = "Calculating correlation coefficients..."
            print(message)
            self.sig_status_message.emit(message)
            self._corrcoef = {}
            for var in VARNAMES:
                self._corrcoef[var] = (self.data[var].corr(min_periods=365 //
                                                           2))
            print("Correlation coefficients calculated sucessfully.")
            self.sig_corrcoeff_calculated.emit()
            self.sig_status_message.emit('')

        correl_target = None
        for var in VARNAMES:
            corr_var_sta = (self._corrcoef[var][[target_station_id]].rename(
                {target_station_id: var}, axis='columns'))
            if correl_target is None:
                correl_target = corr_var_sta
            else:
                correl_target = correl_target.join(corr_var_sta)
        return correl_target

    def generate_html_summary_table(self):
        """
        Generate a Html table showing a summary of available and missing
        weather data for all the stations for which data were loaded in the
        current session.
        """
        table = '''
                <table border="0" cellpadding="3" cellspacing="0"
                 align="center">
                  <tr>
                    <td colspan="10"><hr></td>
                  </tr>
                  <tr>
                    <td align="center" valign="bottom"  width=30 rowspan="3">
                      #
                    </td>
                    <td align="left" valign="bottom" rowspan="3">
                      Station
                    </td>
                    <td align="center" valign="bottom" rowspan="3">
                      Climate<br>ID
                    </td>
                    <td align="center" valign="bottom" rowspan="3">
                      From<br>year
                    </td>
                    <td align="center" valign="bottom" rowspan="3">
                      To<br>year
                    </td>
                    <td align="center" valign="bottom" rowspan="3">
                      Nbr.<br>of<br>years
                    <td align="center" valign="middle" colspan="4">
                      % of missing data for
                    </td>
                  </tr>
                  <tr>
                    <td colspan="4"><hr></td>
                  </tr>
                  <tr>
                    <td align="center" valign="middle">
                      T<sub>max</sub>
                    </td>
                    <td align="center" valign="middle">
                      T<sub>min</sub>
                    </td>
                    <td align="center" valign="middle">
                      T<sub>mean</sub>
                    </td>
                    <td align="center" valign="middle">
                      P<sub>tot</sub>
                    </td>
                  </tr>
                  <tr>
                    <td colspan="10"><hr></td>
                  </tr>
                '''
        for i, station_id in enumerate(self.station_ids):
            station_metadata = self.metadata.loc[station_id]
            color = ['transparent', LIGHTGRAY][i % 2]
            first_date = station_metadata['first_date']
            datetime_start = datetime(first_date.year, first_date.month,
                                      first_date.day)
            last_date = station_metadata['last_date']
            datetime_end = datetime(last_date.year, last_date.month,
                                    last_date.day)
            total_nbr_data = (
                (datetime_end - datetime_start).total_seconds() / 3600 / 24 +
                1)
            firstyear = datetime_start.year
            lastyear = datetime_end.year
            nyears = lastyear - firstyear + 1

            ptot_data = self.data['Ptot'][station_id]
            ptot_nan_percent = round(
                len(ptot_data[(ptot_data.index >= datetime_start)
                              & (ptot_data.index <= datetime_end) &
                              (ptot_data.isnull())]) / total_nbr_data * 100, 1)
            tmax_data = self.data['Tmax'][station_id]
            tmax_nan_percent = round(
                len(tmax_data[(tmax_data.index >= datetime_start)
                              & (tmax_data.index <= datetime_end) &
                              (tmax_data.isnull())]) / total_nbr_data * 100, 1)
            tmin_data = self.data['Tmax'][station_id]
            tmin_nan_percent = round(
                len(tmin_data[(tmin_data.index >= datetime_start)
                              & (tmin_data.index <= datetime_end) &
                              (tmin_data.isnull())]) / total_nbr_data * 100, 1)
            tmean_data = self.data['Tmax'][station_id]
            tmean_nan_percent = round(
                len(tmean_data[(tmean_data.index >= datetime_start)
                               & (tmean_data.index <= datetime_end) &
                               (tmean_data.isnull())]) / total_nbr_data * 100,
                1)

            table += '''
                     <tr bgcolor="%s">
                       <td align="center" valign="middle">
                         %02d
                       </td>
                       <td align="left" valign="middle">
                         <font size="3">%s</font>
                       </td>
                       <td align="center" valign="middle">
                         <font size="3">%s</font>
                       </td>
                       <td align="center" valign="middle">
                         <font size="3">%s</font>
                       </td>
                       <td align="center" valign="middle">
                         <font size="3">%s</font>
                       </td>
                       <td align="center" valign="middle">
                         <font size="3">%0.0f</font>
                       </td>
                       <td align="center" valign="middle">%0.0f</td>
                       <td align="center" valign="middle">%0.0f</td>
                       <td align="center" valign="middle">%0.0f</td>
                       <td align="center" valign="middle">%0.0f</td>
                     </tr>
                     ''' % (color, i + 1, station_metadata['Station Name'],
                            station_id, firstyear, lastyear, nyears,
                            tmax_nan_percent, tmin_nan_percent,
                            tmean_nan_percent, ptot_nan_percent)
        table += """
                   <tr>
                     <td colspan="10"><hr></td>
                   </tr>
                 </table>
                 """
        return table
예제 #14
0
class HydroprintGUI(myqt.DialogWindow):

    ConsoleSignal = QSignal(str)

    def __init__(self, datamanager, parent=None):
        super(HydroprintGUI, self).__init__(parent, maximize=True)

        self.__updateUI = True

        # Child widgets:

        self.dmngr = datamanager
        self.dmngr.wldsetChanged.connect(self.wldset_changed)
        self.dmngr.wxdsetChanged.connect(self.wxdset_changed)

        self.page_setup_win = PageSetupWin(self)
        self.page_setup_win.newPageSetupSent.connect(self.layout_changed)

        self.color_palette_win = ColorsSetupWin(self)
        self.color_palette_win.newColorSetupSent.connect(self.update_colors)

        # Memory path variable:

        self.save_fig_dir = self.workdir

        # Generate UI:

        self.__initUI__()

    def __initUI__(self):

        # ---- Toolbar

        self.btn_save = btn_save = QToolButtonNormal(icons.get_icon('save'))
        btn_save.setToolTip('Save the well hydrograph')

        # btn_draw is usefull for debugging purposes
        btn_draw = QToolButtonNormal(icons.get_icon('refresh'))
        btn_draw.setToolTip('Force a refresh of the well hydrograph')
        btn_draw.hide()

        self.btn_load_layout = QToolButtonNormal(
            icons.get_icon('load_graph_config'))
        self.btn_load_layout.setToolTip(
            "<p>Load graph layout for the current water level "
            " datafile if it exists</p>")
        self.btn_load_layout.clicked.connect(self.load_layout_isClicked)

        self.btn_save_layout = QToolButtonNormal(
            icons.get_icon('save_graph_config'))
        self.btn_save_layout.setToolTip('Save current graph layout')
        self.btn_save_layout.clicked.connect(self.save_layout_isClicked)

        btn_bestfit_waterlvl = QToolButtonNormal(icons.get_icon('fit_y'))
        btn_bestfit_waterlvl.setToolTip('Best fit the water level scale')

        btn_bestfit_time = QToolButtonNormal(icons.get_icon('fit_x'))
        btn_bestfit_time.setToolTip('Best fit the time scale')

        self.btn_page_setup = QToolButtonNormal(icons.get_icon('page_setup'))
        self.btn_page_setup.setToolTip('Show the page setup window')
        self.btn_page_setup.clicked.connect(self.page_setup_win.show)

        btn_color_pick = QToolButtonNormal(icons.get_icon('color_picker'))
        btn_color_pick.setToolTip('<p>Show a window to setup the color palette'
                                  ' used to draw the hydrograph</p.')
        btn_color_pick.clicked.connect(self.color_palette_win.show)

        self.btn_language = LangToolButton()
        self.btn_language.setToolTip(
            "Set the language of the text shown in the graph.")
        self.btn_language.sig_lang_changed.connect(self.layout_changed)
        self.btn_language.setIconSize(icons.get_iconsize('normal'))

        # ---- Zoom Panel

        btn_zoom_out = QToolButtonSmall(icons.get_icon('zoom_out'))
        btn_zoom_out.setToolTip('Zoom out (ctrl + mouse-wheel-down)')
        btn_zoom_out.clicked.connect(self.zoom_out)

        btn_zoom_in = QToolButtonSmall(icons.get_icon('zoom_in'))
        btn_zoom_in.setToolTip('Zoom in (ctrl + mouse-wheel-up)')
        btn_zoom_in.clicked.connect(self.zoom_in)

        self.zoom_disp = QSpinBox()
        self.zoom_disp.setAlignment(Qt.AlignCenter)
        self.zoom_disp.setButtonSymbols(QAbstractSpinBox.NoButtons)
        self.zoom_disp.setReadOnly(True)
        self.zoom_disp.setSuffix(' %')
        self.zoom_disp.setRange(0, 9999)
        self.zoom_disp.setValue(100)

        zoom_pan = myqt.QFrameLayout()
        zoom_pan.setSpacing(3)
        zoom_pan.addWidget(btn_zoom_out, 0, 0)
        zoom_pan.addWidget(btn_zoom_in, 0, 1)
        zoom_pan.addWidget(self.zoom_disp, 0, 2)

        # LAYOUT :

        btn_list = [
            btn_save, btn_draw, self.btn_load_layout, self.btn_save_layout,
            VSep(), btn_bestfit_waterlvl, btn_bestfit_time,
            VSep(), self.btn_page_setup, btn_color_pick, self.btn_language,
            VSep(), zoom_pan
        ]

        subgrid_toolbar = QGridLayout()
        toolbar_widget = QWidget()

        row = 0
        for col, btn in enumerate(btn_list):
            subgrid_toolbar.addWidget(btn, row, col)

        subgrid_toolbar.setSpacing(5)
        subgrid_toolbar.setContentsMargins(0, 0, 0, 0)
        subgrid_toolbar.setColumnStretch(col + 1, 100)

        toolbar_widget.setLayout(subgrid_toolbar)

        # ---- LEFT PANEL

        # SubGrid Hydrograph Frame :

        self.hydrograph = hydrograph.Hydrograph()
        self.hydrograph_scrollarea = mplFigViewer.ImageViewer()
        self.hydrograph_scrollarea.zoomChanged.connect(self.zoom_disp.setValue)

        grid_hydrograph = QGridLayout()
        grid_hydrograph.addWidget(self.hydrograph_scrollarea, 0, 0)
        grid_hydrograph.setRowStretch(0, 500)
        grid_hydrograph.setColumnStretch(0, 500)
        grid_hydrograph.setContentsMargins(0, 0, 0, 0)  # (L, T, R, B)

        # ASSEMBLING SubGrids :

        grid_layout = QGridLayout()
        self.grid_layout_widget = QFrame()

        row = 0
        grid_layout.addWidget(toolbar_widget, row, 0)
        row += 1
        grid_layout.addLayout(grid_hydrograph, row, 0)

        grid_layout.setContentsMargins(0, 0, 0, 0)  # (L, T, R, B)
        grid_layout.setSpacing(5)
        grid_layout.setColumnStretch(0, 500)
        grid_layout.setRowStretch(1, 500)

        self.grid_layout_widget.setLayout(grid_layout)

        # ---- Right Panel

        self.tabscales = self.__init_scalesTabWidget__()

        self.right_panel = myqt.QFrameLayout()
        self.right_panel.addWidget(self.dmngr, 0, 0)
        self.right_panel.addWidget(self.tabscales, 1, 0)
        self.right_panel.setRowStretch(2, 100)

        self.right_panel.setSpacing(15)

        # ---- MAIN GRID

        mainGrid = QGridLayout()

        mainGrid.addWidget(self.grid_layout_widget, 0, 0)
        mainGrid.addWidget(VSep(), 0, 1)
        mainGrid.addWidget(self.right_panel, 0, 2)

        mainGrid.setContentsMargins(10, 10, 10, 10)  # (L, T, R, B)
        mainGrid.setSpacing(15)
        mainGrid.setColumnStretch(0, 500)
        mainGrid.setColumnMinimumWidth(2, 250)

        self.setLayout(mainGrid)

        # ---- EVENTS

        # Toolbox Layout :

        btn_bestfit_waterlvl.clicked.connect(self.best_fit_waterlvl)
        btn_bestfit_time.clicked.connect(self.best_fit_time)
        btn_draw.clicked.connect(self.draw_hydrograph)
        btn_save.clicked.connect(self.select_save_path)

        # Hydrograph Layout :

        self.Ptot_scale.valueChanged.connect(self.layout_changed)
        self.qweather_bin.currentIndexChanged.connect(self.layout_changed)

        # ---- Init Image

        self.hydrograph_scrollarea.load_mpl_figure(self.hydrograph)

    def __init_scalesTabWidget__(self):
        class QRowLayout(QGridLayout):
            def __init__(self, items, parent=None):
                super(QRowLayout, self).__init__(parent)

                for col, item in enumerate(items):
                    self.addWidget(item, 0, col)

                self.setContentsMargins(0, 0, 0, 0)
                self.setColumnStretch(0, 100)

        # ---- Time axis properties

        # Generate the widgets :

        self.date_start_widget = QDateEdit()
        self.date_start_widget.setDisplayFormat('01 / MM / yyyy')
        self.date_start_widget.setAlignment(Qt.AlignCenter)
        self.date_start_widget.dateChanged.connect(self.layout_changed)

        self.date_end_widget = QDateEdit()
        self.date_end_widget.setDisplayFormat('01 / MM / yyyy')
        self.date_end_widget.setAlignment(Qt.AlignCenter)
        self.date_end_widget.dateChanged.connect(self.layout_changed)

        self.time_scale_label = QComboBox()
        self.time_scale_label.setEditable(False)
        self.time_scale_label.setInsertPolicy(QComboBox.NoInsert)
        self.time_scale_label.addItems(['Month', 'Year'])
        self.time_scale_label.setCurrentIndex(0)
        self.time_scale_label.currentIndexChanged.connect(self.layout_changed)

        self.dateDispFreq_spinBox = QSpinBox()
        self.dateDispFreq_spinBox.setSingleStep(1)
        self.dateDispFreq_spinBox.setMinimum(1)
        self.dateDispFreq_spinBox.setMaximum(100)
        self.dateDispFreq_spinBox.setValue(self.hydrograph.date_labels_pattern)
        self.dateDispFreq_spinBox.setAlignment(Qt.AlignCenter)
        self.dateDispFreq_spinBox.setKeyboardTracking(False)
        self.dateDispFreq_spinBox.valueChanged.connect(self.layout_changed)

        # Setting up the layout :

        widget_time_scale = QFrame()
        widget_time_scale.setFrameStyle(0)
        grid_time_scale = QGridLayout()

        GRID = [[QLabel('From :'), self.date_start_widget],
                [QLabel('To :'), self.date_end_widget],
                [QLabel('Scale :'), self.time_scale_label],
                [QLabel('Date Disp. Pattern:'), self.dateDispFreq_spinBox]]

        for i, ROW in enumerate(GRID):
            grid_time_scale.addLayout(QRowLayout(ROW), i, 1)

        grid_time_scale.setVerticalSpacing(5)
        grid_time_scale.setContentsMargins(10, 10, 10, 10)

        widget_time_scale.setLayout(grid_time_scale)

        # ----- Water level axis properties

        # Widget :

        self.waterlvl_scale = QDoubleSpinBox()
        self.waterlvl_scale.setSingleStep(0.05)
        self.waterlvl_scale.setMinimum(0.05)
        self.waterlvl_scale.setSuffix('  m')
        self.waterlvl_scale.setAlignment(Qt.AlignCenter)
        self.waterlvl_scale.setKeyboardTracking(False)
        self.waterlvl_scale.valueChanged.connect(self.layout_changed)
        self.waterlvl_scale.setFixedWidth(100)

        self.waterlvl_max = QDoubleSpinBox()
        self.waterlvl_max.setSingleStep(0.1)
        self.waterlvl_max.setSuffix('  m')
        self.waterlvl_max.setAlignment(Qt.AlignCenter)
        self.waterlvl_max.setMinimum(-1000)
        self.waterlvl_max.setMaximum(1000)
        self.waterlvl_max.setKeyboardTracking(False)
        self.waterlvl_max.valueChanged.connect(self.layout_changed)
        self.waterlvl_max.setFixedWidth(100)

        self.NZGridWL_spinBox = QSpinBox()
        self.NZGridWL_spinBox.setSingleStep(1)
        self.NZGridWL_spinBox.setMinimum(1)
        self.NZGridWL_spinBox.setMaximum(50)
        self.NZGridWL_spinBox.setValue(self.hydrograph.NZGrid)
        self.NZGridWL_spinBox.setAlignment(Qt.AlignCenter)
        self.NZGridWL_spinBox.setKeyboardTracking(False)
        self.NZGridWL_spinBox.valueChanged.connect(self.layout_changed)
        self.NZGridWL_spinBox.setFixedWidth(100)

        self.datum_widget = QComboBox()
        self.datum_widget.addItems(['Ground Surface', 'Sea Level'])
        self.datum_widget.currentIndexChanged.connect(self.layout_changed)

        # Layout :

        subgrid_WLScale = QGridLayout()

        GRID = [[QLabel('Minimum :'), self.waterlvl_max],
                [QLabel('Scale :'), self.waterlvl_scale],
                [QLabel('Grid Divisions :'), self.NZGridWL_spinBox],
                [QLabel('Datum :'), self.datum_widget]]

        for i, ROW in enumerate(GRID):
            subgrid_WLScale.addLayout(QRowLayout(ROW), i, 1)

        subgrid_WLScale.setVerticalSpacing(5)
        subgrid_WLScale.setContentsMargins(10, 10, 10, 10)  # (L, T, R, B)

        WLScale_widget = QFrame()
        WLScale_widget.setFrameStyle(0)
        WLScale_widget.setLayout(subgrid_WLScale)

        # ---- Weather Axis

        # Widgets :

        self.Ptot_scale = QSpinBox()
        self.Ptot_scale.setSingleStep(5)
        self.Ptot_scale.setMinimum(5)
        self.Ptot_scale.setMaximum(500)
        self.Ptot_scale.setValue(20)
        self.Ptot_scale.setSuffix('  mm')
        self.Ptot_scale.setAlignment(Qt.AlignCenter)

        self.qweather_bin = QComboBox()
        self.qweather_bin.setEditable(False)
        self.qweather_bin.setInsertPolicy(QComboBox.NoInsert)
        self.qweather_bin.addItems(['day', 'week', 'month'])
        self.qweather_bin.setCurrentIndex(1)

        # Layout :

        layout = QGridLayout()

        GRID = [[QLabel('Precip. Scale :'), self.Ptot_scale],
                [QLabel('Resampling :'), self.qweather_bin]]

        for i, row in enumerate(GRID):
            layout.addLayout(QRowLayout(row), i, 1)

        layout.setVerticalSpacing(5)
        layout.setContentsMargins(10, 10, 10, 10)  # (L,T,R,B)
        layout.setRowStretch(i + 1, 100)

        widget_weather_scale = QFrame()
        widget_weather_scale.setFrameStyle(0)
        widget_weather_scale.setLayout(layout)

        # ---- ASSEMBLING TABS

        tabscales = QTabWidget()
        tabscales.addTab(widget_time_scale, 'Time')
        tabscales.addTab(WLScale_widget, 'Water Level')
        tabscales.addTab(widget_weather_scale, 'Weather')

        return tabscales

    @property
    def workdir(self):
        return self.dmngr.workdir

    # ---- Utilities

    def zoom_in(self):
        self.hydrograph_scrollarea.zoomIn()

    def zoom_out(self):
        self.hydrograph_scrollarea.zoomOut()

    def update_colors(self):
        self.hydrograph.update_colors()
        self.hydrograph_scrollarea.load_mpl_figure(self.hydrograph)

    # ---- Datasets Handlers

    @property
    def wldset(self):
        return self.dmngr.get_current_wldset()

    @property
    def wxdset(self):
        return self.dmngr.get_current_wxdset()

    def wldset_changed(self):
        """Handle when the water level dataset of the datamanager changes."""
        if self.wldset is None:
            self.clear_hydrograph()
            return
        else:
            wldset = self.wldset
            self.hydrograph.set_wldset(wldset)
            self.hydrograph.gluedf = self.wldset.get_glue_at(-1)

        # Load the manual measurements.

        fname = os.path.join(self.workdir, "Water Levels",
                             'waterlvl_manual_measurements')
        tmeas, wlmeas = load_waterlvl_measures(fname, wldset['Well'])
        wldset.set_wlmeas(tmeas, wlmeas)

        # Setup the layout of the hydrograph.

        layout = wldset.get_layout()
        if layout is not None:
            msg = 'Loading existing graph layout for well %s.' % wldset['Well']
            print(msg)
            self.ConsoleSignal.emit('<font color=black>%s</font>' % msg)
            self.load_graph_layout(layout)
        else:
            print('No graph layout exists for well %s.' % wldset['Well'])
            # Fit Water Level in Layout :
            self.__updateUI = False
            self.best_fit_waterlvl()
            self.best_fit_time()
            self.dmngr.set_closest_wxdset()
            self.__updateUI = True

    def wxdset_changed(self):
        """Handle when the weather dataset of the datamanager changes."""
        if self.wldset is None:
            self.clear_hydrograph()
        else:
            self.hydrograph.set_wxdset(self.wxdset)
            QCoreApplication.processEvents()
            self.draw_hydrograph()

    # ---- Draw Hydrograph Handlers

    def best_fit_waterlvl(self):
        wldset = self.dmngr.get_current_wldset()
        if wldset is not None:
            WLscale, WLmin = self.hydrograph.best_fit_waterlvl()
            self.waterlvl_scale.setValue(WLscale)
            self.waterlvl_max.setValue(WLmin)

    def best_fit_time(self):
        wldset = self.dmngr.get_current_wldset()
        if wldset is not None:
            date0, date1 = self.hydrograph.best_fit_time(wldset['Time'])
            self.date_start_widget.setDate(QDate(date0[0], date0[1], date0[2]))
            self.date_end_widget.setDate(QDate(date1[0], date1[1], date1[2]))

    @QSlot()
    def mrc_wl_changed(self):
        """
        Force a redraw of the MRC water levels after the results have
        changed for the dataset.
        """
        self.hydrograph.draw_mrc_wl()
        self.hydrograph.setup_legend()
        self.hydrograph_scrollarea.load_mpl_figure(self.hydrograph)

    @QSlot(GLUEDataFrameBase)
    def glue_wl_changed(self, gluedf):
        """
        Force a redraw of the GLUE water levels after the results have
        changed for the dataset.
        """
        self.hydrograph.set_gluedf(gluedf)
        self.hydrograph_scrollarea.load_mpl_figure(self.hydrograph)

    def layout_changed(self):
        """
        When an element of the graph layout is changed in the UI.
        """

        if self.__updateUI is False:
            return

        self.update_graph_layout_parameter()

        if self.hydrograph.isHydrographExists is False:
            return

        sender = self.sender()

        if sender == self.btn_language:
            self.hydrograph.draw_ylabels()
            self.hydrograph.setup_xticklabels()
            self.hydrograph.setup_legend()
        elif sender in [self.waterlvl_max, self.waterlvl_scale]:
            self.hydrograph.setup_waterlvl_scale()
            self.hydrograph.draw_ylabels()
        elif sender == self.NZGridWL_spinBox:
            self.hydrograph.setup_waterlvl_scale()
            self.hydrograph.update_precip_scale()
            self.hydrograph.draw_ylabels()
        elif sender == self.Ptot_scale:
            self.hydrograph.update_precip_scale()
            self.hydrograph.draw_ylabels()
        elif sender == self.datum_widget:
            yoffset = int(self.wldset['Elevation'] / self.hydrograph.WLscale)
            yoffset *= self.hydrograph.WLscale

            self.hydrograph.WLmin = (yoffset - self.hydrograph.WLmin)

            self.waterlvl_max.blockSignals(True)
            self.waterlvl_max.setValue(self.hydrograph.WLmin)
            self.waterlvl_max.blockSignals(False)

            # This is calculated so that trailing zeros in the altitude of the
            # well is not carried to the y axis labels, so that they remain a
            # int multiple of *WLscale*.

            self.hydrograph.setup_waterlvl_scale()
            self.hydrograph.draw_waterlvl()
            self.hydrograph.draw_ylabels()
        elif sender in [self.date_start_widget, self.date_end_widget]:
            self.hydrograph.set_time_scale()
            self.hydrograph.draw_weather()
            self.hydrograph.draw_figure_title()
        elif sender == self.dateDispFreq_spinBox:
            self.hydrograph.set_time_scale()
            self.hydrograph.setup_xticklabels()
        elif sender == self.page_setup_win:
            self.hydrograph.update_fig_size()
            # Implicitly call : set_margins()
            #                   draw_ylabels()
            #                   set_time_scale()
            #                   draw_figure_title
            self.hydrograph.draw_waterlvl()
            self.hydrograph.setup_legend()
        elif sender == self.qweather_bin:
            self.hydrograph.resample_bin()
            self.hydrograph.draw_weather()
            self.hydrograph.draw_ylabels()
        elif sender == self.time_scale_label:
            self.hydrograph.set_time_scale()
            self.hydrograph.draw_weather()
        else:
            print('No action for this widget yet.')

        # !!!! temporary fix until I can find a better solution !!!!

#        sender.blockSignals(True)
        if type(sender) in [QDoubleSpinBox, QSpinBox]:
            sender.setReadOnly(True)

        for i in range(10):
            QCoreApplication.processEvents()
        self.hydrograph_scrollarea.load_mpl_figure(self.hydrograph)
        for i in range(10):
            QCoreApplication.processEvents()

        if type(sender) in [QDoubleSpinBox, QSpinBox]:
            sender.setReadOnly(False)


#        sender.blockSignals(False)

    def update_graph_layout_parameter(self):

        # language :

        self.hydrograph.language = self.btn_language.language

        # Scales :

        self.hydrograph.WLmin = self.waterlvl_max.value()
        self.hydrograph.WLscale = self.waterlvl_scale.value()
        self.hydrograph.RAINscale = self.Ptot_scale.value()
        self.hydrograph.NZGrid = self.NZGridWL_spinBox.value()

        # WL Datum :

        self.hydrograph.WLdatum = self.datum_widget.currentIndex()

        # Dates :

        self.hydrograph.datemode = self.time_scale_label.currentText()

        year = self.date_start_widget.date().year()
        month = self.date_start_widget.date().month()
        self.hydrograph.TIMEmin = xldate_from_date_tuple((year, month, 1), 0)

        year = self.date_end_widget.date().year()
        month = self.date_end_widget.date().month()
        self.hydrograph.TIMEmax = xldate_from_date_tuple((year, month, 1), 0)

        self.hydrograph.date_labels_pattern = self.dateDispFreq_spinBox.value()

        # Page Setup :

        self.hydrograph.fwidth = self.page_setup_win.pageSize[0]
        self.hydrograph.fheight = self.page_setup_win.pageSize[1]
        self.hydrograph.va_ratio = self.page_setup_win.va_ratio

        self.hydrograph.trend_line = self.page_setup_win.isTrendLine
        self.hydrograph.isLegend = self.page_setup_win.isLegend
        self.hydrograph.isGraphTitle = self.page_setup_win.isGraphTitle
        self.hydrograph.set_meteo_on(self.page_setup_win.is_meteo_on)
        self.hydrograph.set_glue_wl_on(self.page_setup_win.is_glue_wl_on)
        self.hydrograph.set_mrc_wl_on(self.page_setup_win.is_mrc_wl_on)
        self.hydrograph.set_figframe_lw(self.page_setup_win.figframe_lw)

        # Weather bins :

        self.hydrograph.bwidth_indx = self.qweather_bin.currentIndex()

    def clear_hydrograph(self):
        """Clear the hydrograph figure to show only a blank canvas."""
        self.hydrograph.clf()
        self.hydrograph_scrollarea.load_mpl_figure(self.hydrograph)

    def draw_hydrograph(self):
        if self.dmngr.wldataset_count() == 0:
            msg = 'Please import a valid water level data file first.'
            self.ConsoleSignal.emit('<font color=red>%s</font>' % msg)
            self.emit_warning(msg)
            return

        self.update_graph_layout_parameter()

        # Generate and Display Graph :

        for i in range(5):
            QCoreApplication.processEvents()

        QApplication.setOverrideCursor(Qt.WaitCursor)

        self.hydrograph.set_wldset(self.dmngr.get_current_wldset())
        self.hydrograph.set_wxdset(self.dmngr.get_current_wxdset())
        self.hydrograph.generate_hydrograph()

        self.hydrograph_scrollarea.load_mpl_figure(self.hydrograph)

        QApplication.restoreOverrideCursor()

    def select_save_path(self):
        """
        Open a dialog where the user can select a file name to save the
        hydrograph.
        """
        if self.wldset is None:
            return

        ffmat = "*.pdf;;*.svg;;*.png"
        fname = find_unique_filename(
            osp.join(self.save_fig_dir,
                     'hydrograph_%s.pdf' % self.wldset['Well']))

        fname, ftype = QFileDialog.getSaveFileName(self, "Save Figure", fname,
                                                   ffmat)
        if fname:
            ftype = ftype.replace('*', '')
            fname = fname if fname.endswith(ftype) else fname + ftype
            self.save_fig_dir = os.path.dirname(fname)

            try:
                self.save_figure(fname)
            except PermissionError:
                msg = "The file is in use by another application or user."
                QMessageBox.warning(self, 'Warning', msg, QMessageBox.Ok)
                self.select_save_path()

    def save_figure(self, fname):
        """Save the hydrograph figure in a file."""
        self.hydrograph.generate_hydrograph()
        self.hydrograph.savefig(fname)

    # ---- Graph Layout Handlers

    def load_layout_isClicked(self):
        """Handle when the button to load the graph layout is clicked."""
        if self.wldset is None:
            self.emit_warning(
                "Please import a valid water level data file first.")
            return

        layout = self.wldset.get_layout()
        if layout is None:
            self.emit_warning("No graph layout exists for well %s." %
                              self.wldset['Well'])
        else:
            self.load_graph_layout(layout)

    def load_graph_layout(self, layout):
        """Load the graph layout into the GUI."""

        self.__updateUI = False

        # Scales :

        date = layout['TIMEmin']
        date = xldate_as_tuple(date, 0)
        self.date_start_widget.setDate(QDate(date[0], date[1], date[2]))

        date = layout['TIMEmax']
        date = xldate_as_tuple(date, 0)
        self.date_end_widget.setDate(QDate(date[0], date[1], date[2]))

        self.dateDispFreq_spinBox.setValue(layout['date_labels_pattern'])

        self.waterlvl_scale.setValue(layout['WLscale'])
        self.waterlvl_max.setValue(layout['WLmin'])
        self.NZGridWL_spinBox.setValue(layout['NZGrid'])
        self.Ptot_scale.setValue(layout['RAINscale'])

        x = ['mbgs', 'masl'].index(layout['WLdatum'])
        self.datum_widget.setCurrentIndex(x)

        self.qweather_bin.setCurrentIndex(layout['bwidth_indx'])
        self.time_scale_label.setCurrentIndex(
            self.time_scale_label.findText(layout['datemode']))

        # ---- Language and colors
        self.btn_language.set_language(layout['language'])
        self.color_palette_win.load_colors()

        # ---- Page Setup

        self.page_setup_win.pageSize = (layout['fwidth'], layout['fheight'])
        self.page_setup_win.va_ratio = layout['va_ratio']
        self.page_setup_win.isLegend = layout['legend_on']
        self.page_setup_win.isGraphTitle = layout['title_on']
        self.page_setup_win.isTrendLine = layout['trend_line']
        self.page_setup_win.is_meteo_on = layout['meteo_on']
        self.page_setup_win.is_glue_wl_on = layout['glue_wl_on']
        self.page_setup_win.is_mrc_wl_on = layout['mrc_wl_on']
        self.page_setup_win.figframe_lw = layout['figframe_lw']

        self.page_setup_win.legend_on.set_value(layout['legend_on'])
        self.page_setup_win.title_on.set_value(layout['title_on'])
        self.page_setup_win.wltrend_on.set_value(layout['trend_line'])
        self.page_setup_win.meteo_on.set_value(layout['meteo_on'])
        self.page_setup_win.glue_wl_on.set_value(layout['glue_wl_on'])
        self.page_setup_win.mrc_wl_on.set_value(layout['mrc_wl_on'])
        self.page_setup_win.fframe_lw_widg.setValue(layout['figframe_lw'])

        self.page_setup_win.fwidth.setValue(layout['fwidth'])
        self.page_setup_win.fheight.setValue(layout['fheight'])
        self.page_setup_win.va_ratio_spinBox.setValue(layout['va_ratio'])

        # Check if Weather Dataset :

        if layout['wxdset'] in self.dmngr.wxdsets:
            self.dmngr.set_current_wxdset(layout['wxdset'])
        else:
            self.dmngr.set_closest_wxdset()

        self.__updateUI = True

    def save_layout_isClicked(self):
        wldset = self.wldset
        if wldset is None:
            self.emit_warning(
                "Please import a valid water level data file first.")
            return

        layout = wldset.get_layout()
        if layout is not None:
            msg = ('A graph layout already exists for well %s.Do you want to'
                   ' you want to replace it?') % wldset['Well']
            reply = QMessageBox.question(self, 'Save Graph Layout', msg,
                                         QMessageBox.Yes | QMessageBox.No)
            if reply == QMessageBox.Yes:
                self.save_graph_layout()
            elif reply == QMessageBox.No:
                msg = "Graph layout not saved for well %s." % wldset['Well']
                self.ConsoleSignal.emit('<font color=black>%s' % msg)
        else:
            self.save_graph_layout()

    def save_graph_layout(self):
        """Save the graph layout in the project hdf5 file."""
        print("Saving the graph layout for well %s..." % self.wldset['Well'],
              end=" ")

        layout = {
            'WLmin': self.waterlvl_max.value(),
            'WLscale': self.waterlvl_scale.value(),
            'RAINscale': self.Ptot_scale.value(),
            'fwidth': self.page_setup_win.pageSize[0],
            'fheight': self.page_setup_win.pageSize[1],
            'va_ratio': self.page_setup_win.va_ratio,
            'NZGrid': self.NZGridWL_spinBox.value(),
            'bwidth_indx': self.qweather_bin.currentIndex(),
            'date_labels_pattern': self.dateDispFreq_spinBox.value(),
            'datemode': self.time_scale_label.currentText()
        }
        layout['wxdset'] = None if self.wxdset is None else self.wxdset.name

        year = self.date_start_widget.date().year()
        month = self.date_start_widget.date().month()
        layout['TIMEmin'] = xldate_from_date_tuple((year, month, 1), 0)

        year = self.date_end_widget.date().year()
        month = self.date_end_widget.date().month()
        layout['TIMEmax'] = xldate_from_date_tuple((year, month, 1), 0)

        if self.datum_widget.currentIndex() == 0:
            layout['WLdatum'] = 'mbgs'
        else:
            layout['WLdatum'] = 'masl'

        # ---- Page Setup

        layout['title_on'] = bool(self.page_setup_win.isGraphTitle)
        layout['legend_on'] = bool(self.page_setup_win.isLegend)
        layout['language'] = self.btn_language.language
        layout['trend_line'] = bool(self.page_setup_win.isTrendLine)
        layout['meteo_on'] = bool(self.page_setup_win.is_meteo_on)
        layout['glue_wl_on'] = bool(self.page_setup_win.is_glue_wl_on)
        layout['mrc_wl_on'] = bool(self.page_setup_win.is_mrc_wl_on)
        layout['figframe_lw'] = self.page_setup_win.figframe_lw

        # Save the colors :

        cdb = ColorsReader()
        cdb.load_colors_db()
        layout['colors'] = cdb.RGB

        # Save the layout :

        self.wldset.save_layout(layout)
        msg = 'Layout saved successfully for well %s.' % self.wldset['Well']
        self.ConsoleSignal.emit('<font color=black>%s</font>' % msg)
        print("done")
예제 #15
0
class DataManager(QWidget):

    wldsetChanged = QSignal(object)
    wxdsetChanged = QSignal(object)
    sig_workdir_changed = QSignal(str)
    sig_new_console_msg = QSignal(str)

    def __init__(self, parent=None, projet=None, pm=None, pytesting=False):
        super(DataManager, self).__init__(parent)
        self._pytesting = pytesting
        self._projet = projet
        self._confirm_before_deleting_dset = True

        self._wldset = None
        self._wxdset = None

        self.setWindowFlags(Qt.Window)
        self.setWindowIcon(icons.get_icon('master'))
        self.setMinimumWidth(250)

        self.weather_avg_graph = None

        self.new_waterlvl_win = NewDatasetDialog('water level', parent, projet)
        self.new_waterlvl_win.sig_new_dataset_imported.connect(
            self.new_wldset_imported)

        self.new_weather_win = NewDatasetDialog('daily weather', parent,
                                                projet)
        self.new_weather_win.sig_new_dataset_imported.connect(
            self.new_wxdset_imported)

        self.setup_manager()

        self.set_projet(projet)
        if pm:
            pm.currentProjetChanged.connect(self.set_projet)
            self.set_projet(pm.projet)

    def setup_manager(self):
        """Setup the layout of the manager."""
        layout = QGridLayout(self)
        layout.setSpacing(5)
        layout.setContentsMargins(0, 0, 0, 0)

        layout.addWidget(self.setup_wldset_mngr(), 0, 0)
        layout.addWidget(self.setup_wxdset_mngr(), 2, 0)

    def setup_wldset_mngr(self):
        """Setup the manager for the water level datasets."""

        # ---- Toolbar

        self.wldsets_cbox = QComboBox()
        self.wldsets_cbox.currentIndexChanged.connect(self.wldset_changed)

        self.btn_load_wl = QToolButtonSmall(icons.get_icon('importFile'))
        self.btn_load_wl.setToolTip('Import a new water level dataset...')
        self.btn_load_wl.clicked.connect(self.import_wldataset)

        self.btn_del_wldset = QToolButtonSmall('delete_data')
        self.btn_del_wldset.setToolTip('Delete current dataset.')
        self.btn_del_wldset.clicked.connect(self.del_current_wldset)

        wl_toolbar = ToolBarWidget()
        for widg in [self.btn_load_wl, self.btn_del_wldset]:
            wl_toolbar.addWidget(widg)

        # ---- Info Box

        self.well_info_widget = StrSpinBox()

        # ---- Main Layout

        grpbox = QGroupBox('Water Level Dataset : ')
        layout = QGridLayout(grpbox)
        layout.setSpacing(5)

        layout.addWidget(self.wldsets_cbox, 1, 0)
        layout.addWidget(self.well_info_widget, 2, 0)
        layout.addWidget(wl_toolbar, 3, 0)

        return grpbox

    def setup_wxdset_mngr(self):
        """Setup the manager for the weather datasets."""

        # ---- Toolbar

        self.wxdsets_cbox = QComboBox()
        self.wxdsets_cbox.currentIndexChanged.connect(self.wxdset_changed)

        self.btn_load_meteo = QToolButtonSmall(icons.get_icon('importFile'))
        self.btn_load_meteo.setToolTip('Import a new weather dataset...')
        self.btn_load_meteo.clicked.connect(self.import_wxdataset)

        self.btn_del_wxdset = QToolButtonSmall('delete_data')
        self.btn_del_wxdset.setToolTip('Delete current dataset.')
        self.btn_del_wxdset.clicked.connect(self.del_current_wxdset)

        btn_closest_meteo = QToolButtonSmall(icons.get_icon('closest_meteo'))
        btn_closest_meteo.setToolTip('<p>Select the weather station closest'
                                     ' from the observation well.</p>')
        btn_closest_meteo.clicked.connect(self.set_closest_wxdset)

        btn_weather_normals = QToolButtonSmall(icons.get_icon('meteo'))
        btn_weather_normals.setToolTip(
            "Show the normals for the current weather dataset.")
        btn_weather_normals.clicked.connect(self.show_weather_normals)

        self.btn_export_weather = ExportWeatherButton(workdir=self.workdir)
        self.btn_export_weather.setIconSize(icons.get_iconsize('small'))

        wx_toolbar = ToolBarWidget()
        for widg in [
                self.btn_load_meteo, self.btn_del_wxdset, btn_closest_meteo,
                btn_weather_normals, self.btn_export_weather
        ]:
            wx_toolbar.addWidget(widg)

        # ---- Info Box

        self.meteo_info_widget = StrSpinBox()

        # ---- Main Layout

        grpbox = QGroupBox('Weather Dataset : ')
        layout = QGridLayout(grpbox)
        layout.setSpacing(5)

        layout.addWidget(self.wxdsets_cbox, 1, 0)
        layout.addWidget(self.meteo_info_widget, 2, 0)
        layout.addWidget(wx_toolbar, 3, 0)

        return grpbox

    @property
    def workdir(self):
        """Return the path where the project hdf5 file is saved."""
        if self.projet is None:
            return osp.dirname(os.getcwd())
        else:
            return osp.dirname(self.projet.filename)

    @property
    def projet(self):
        """Return the projet object."""
        return self._projet

    def set_projet(self, projet):
        """Set the namespace for the projet hdf5 file."""
        self._projet = projet
        self._wldset = None
        self._wxdset = None
        if projet is not None:
            self.update_wldsets(projet.get_last_opened_wldset())
            self.update_wxdsets(projet.get_last_opened_wxdset())
            self.wldset_changed()

        self.btn_export_weather.set_model(self.get_current_wxdset())
        self.btn_export_weather.set_workdir(self.workdir)

        self.new_waterlvl_win.set_projet(projet)
        self.new_weather_win.set_projet(projet)

        self.sig_workdir_changed.emit(self.workdir)

    # ---- Utilities

    def emit_warning(self, msg):
        btn = QMessageBox.Ok
        QMessageBox.warning(self, 'Warning', msg, btn)

    def confirm_del_dataset(self, dsetname, dsettype):
        """
        Show a message box asking the user confirmation before deleting
        a dataset. Return the user's answer and whether the
        'do not show this message again' checkbox has been checked or not.
        """
        msg_box = QMessageBox(
            QMessageBox.Question,
            "Delete {} dataset '{}'".format(dsettype, dsetname),
            ("Do you want to delete the {} dataset <i>{}</i>?<br><br>"
             "All data will be deleted from the project, but the "
             "original data files will be preserved.<br>").format(
                 dsettype, dsetname),
            buttons=QMessageBox.Yes | QMessageBox.Cancel,
            parent=self)
        checkbox = QCheckBox("Don't show this message again.")
        msg_box.setCheckBox(checkbox)

        reply = msg_box.exec_()
        return reply, not checkbox.isChecked()

    # ---- WL Dataset
    @property
    def wldsets(self):
        """Return a list of all the wldset saved in the project."""
        return [] if self.projet is None else self.projet.wldsets

    def wldataset_count(self):
        """Return the total number of wldset saved in the project."""
        return len(self.wldsets)

    def import_wldataset(self):
        """Open a dialog window to import a water level dataset from a file."""
        if self.projet is None:
            msg = ("Please first select a valid project or create a new one.")
            btn = QMessageBox.Ok
            QMessageBox.warning(self, 'Create dataset', msg, btn)
            return
        else:
            if self._pytesting:
                self.new_waterlvl_win.show()
            else:
                self.new_waterlvl_win.exec_()

    def new_wldset_imported(self, name, dataset):
        """
        Receives the new water level dataset, saves it in the project and
        update the GUI.
        """
        print("Saving the new water level dataset in the project...", end=" ")
        self.projet.add_wldset(name, dataset)
        self.update_wldsets(name)
        self.wldset_changed()
        print("done")

    def update_wldsets(self, name=None):
        self.wldsets_cbox.blockSignals(True)
        self.wldsets_cbox.clear()
        self.wldsets_cbox.addItems(self.projet.wldsets)
        if name:
            self.wldsets_cbox.setCurrentIndex(self.wldsets_cbox.findText(name))
        self.wldsets_cbox.blockSignals(False)

    def update_wldset_info(self):
        """Update the infos of the wldset."""
        wldset = self.get_current_wldset()
        if wldset is not None:
            model = [
                "Well : %s" % wldset['Well'],
                "Well ID : %s" % wldset['Well ID'],
                "Latitude : %0.3f°" % wldset['Latitude'],
                "Longitude : %0.3f°" % wldset['Longitude'],
                "Elevation : %0.1f m" % wldset['Elevation'],
                "Municipality : %s" % wldset['Municipality'],
                "Province : %s" % wldset['Province']
            ]
        else:
            model = None
        self.well_info_widget.set_model(model)

    def wldset_changed(self):
        """Handle when the currently selected water level dataset changed."""
        QApplication.processEvents()
        self.update_wldset_info()
        self.wldsetChanged.emit(self.get_current_wldset())

    def get_current_wldset(self):
        """Return the currently selected water level dataset."""
        if self.wldsets_cbox.currentIndex() == -1:
            self._wldset = None
        else:
            cbox_text = self.wldsets_cbox.currentText()
            if self._wldset is None or self._wldset.name != cbox_text:
                self._wldset = self.projet.get_wldset(cbox_text)
        return self._wldset

    def set_current_wldset(self, name):
        """Set the current water level from its name."""
        self.wldsets_cbox.blockSignals(True)
        self.wldsets_cbox.setCurrentIndex(self.wldsets_cbox.findText(name))
        self.wldsets_cbox.blockSignals(False)
        self.wldset_changed()

    def del_current_wldset(self):
        """Delete the currently selected water level dataset."""
        if self.wldsets_cbox.count() > 0:
            dsetname = self.wldsets_cbox.currentText()
            if self._confirm_before_deleting_dset:
                reply, dont_show_again = self.confirm_del_dataset(
                    dsetname, 'water level')
                if reply == QMessageBox.Cancel:
                    return
                elif reply == QMessageBox.Yes:
                    self._confirm_before_deleting_dset = dont_show_again
            self._wldset = None
            self.projet.del_wldset(dsetname)
            self.update_wldsets()
            self.wldset_changed()
            self.sig_new_console_msg.emit(
                ("<font color=black>Water level dataset <i>{}</i> deleted "
                 "successfully.</font>").format(dsetname))

    # ---- WX Dataset
    @property
    def wxdsets(self):
        """Return a list of all the weather datasets saved in the project."""
        return [] if self.projet is None else self.projet.wxdsets

    def wxdataset_count(self):
        """Return the total number of weather datasets saved in the project."""
        return len(self.wxdsets)

    def import_wxdataset(self):
        """Open a dialog window to import a weather dataset from a file."""
        if self.projet is None:
            msg = ("Please first select a valid project or create a new one.")
            btn = QMessageBox.Ok
            QMessageBox.warning(self, 'Create dataset', msg, btn)
            return
        else:
            if self._pytesting:
                self.new_weather_win.show()
            else:
                self.new_weather_win.exec_()

    def new_wxdset_imported(self, name, dataset):
        """
        Receive the new weather dataset, save it in the project and
        update the GUI.
        """
        print("Saving the new weather dataset in the project.", end=" ")
        self.projet.add_wxdset(name, dataset)
        self.update_wxdsets(name)
        self.wxdset_changed()
        print("done")

    def update_wxdsets(self, name=None, silent=False):
        self.wxdsets_cbox.blockSignals(True)
        self.wxdsets_cbox.clear()
        self.wxdsets_cbox.addItems(self.projet.wxdsets)
        if name:
            self.wxdsets_cbox.setCurrentIndex(self.wxdsets_cbox.findText(name))
        self.wxdsets_cbox.blockSignals(False)

    def update_wxdset_info(self):
        """Update the infos of the wxdset."""
        wxdset = self.get_current_wxdset()
        if wxdset is not None:
            model = [
                "Station : %s" % wxdset.metadata['Station Name'],
                "Station ID : %s" % wxdset.metadata['Station ID'],
                "Latitude : %0.3f°" % wxdset.metadata['Latitude'],
                "Longitude : %0.3f°" % wxdset.metadata['Longitude'],
                "Elevation : %0.1f m" % wxdset.metadata['Elevation'],
                "Location : %s" % wxdset.metadata['Location']
            ]
        else:
            model = None
        self.meteo_info_widget.set_model(model)

    def wxdset_changed(self):
        """Handle when the currently selected weather dataset changed."""
        QApplication.processEvents()
        self.update_wxdset_info()
        self.btn_export_weather.set_model(self.get_current_wxdset())
        self.wxdsetChanged.emit(self.get_current_wxdset())

    def del_current_wxdset(self):
        """Delete the currently selected weather dataset."""
        if self.wxdsets_cbox.count() > 0:
            dsetname = self.wxdsets_cbox.currentText()
            if self._confirm_before_deleting_dset:
                reply, dont_show_again = self.confirm_del_dataset(
                    dsetname, 'weather')
                if reply == QMessageBox.Cancel:
                    return
                elif reply == QMessageBox.Yes:
                    self._confirm_before_deleting_dset = dont_show_again
            self._wxdset = None
            self.projet.del_wxdset(dsetname)
            self.update_wxdsets()
            self.wxdset_changed()
            self.sig_new_console_msg.emit(
                ("<font color=black>Weather dataset <i>{}</i> deleted "
                 "successfully.</font>").format(dsetname))

    def get_current_wxdset(self):
        """Return the currently selected weather dataset dataframe."""
        if self.wxdsets_cbox.currentIndex() == -1:
            self._wxdset = None
        else:
            cbox_text = self.wxdsets_cbox.currentText()
            if self._wxdset is None or self._wxdset.name != cbox_text:
                self._wxdset = self.projet.get_wxdset(cbox_text)
        return self._wxdset

    def set_current_wxdset(self, name):
        """Set the current weather dataset from its name."""
        self.wxdsets_cbox.blockSignals(True)
        self.wxdsets_cbox.setCurrentIndex(self.wxdsets_cbox.findText(name))
        self.wxdsets_cbox.blockSignals(False)
        self.wxdset_changed()

    def set_closest_wxdset(self):
        """
        Set the weather dataset of the station that is closest to the
        groundwater observation well.
        """
        if self._wldset is None or self.wxdataset_count() == 0:
            return None

        dist = calc_dist_from_coord(self._wldset['Latitude'],
                                    self._wldset['Longitude'],
                                    self.projet.get_wxdsets_lat(),
                                    self.projet.get_wxdsets_lon())
        closest_station = self.wxdsets[np.argmin(dist)]
        self.set_current_wxdset(closest_station)
        return closest_station

    def show_weather_normals(self):
        """Show the weather normals for the current weather dataset."""
        if self.get_current_wxdset() is None:
            return
        if self.weather_avg_graph is None:
            self.weather_avg_graph = WeatherViewer()

        self.weather_avg_graph.set_workdir(self.workdir)
        self.weather_avg_graph.set_weather_dataset(self.get_current_wxdset())
        self.weather_avg_graph.show()
예제 #16
0
class KGSBRFInstaller(myqt.QFrameLayout):
    """
    A simple widget to download the kgs_brf program and install it in the
    proper directory.
    http://www.kgs.ku.edu/HighPlains/OHP/index_program/brf.html
    """

    sig_kgs_brf_installed = QSignal(str)

    def __init__(self, parent=None):
        super(KGSBRFInstaller, self).__init__(parent)

        self.setAutoFillBackground(True)
        self.setAttribute(Qt.WA_DeleteOnClose)

        self.install_btn = QPushButton("Install")
        self.install_btn.clicked.connect(self.install_kgsbrf)

        self.addWidget(self.install_btn, 1, 1)
        self.setRowStretch(0, 100)
        self.setRowStretch(self.rowCount(), 100)
        self.setColumnStretch(0, 100)
        self.setColumnStretch(self.columnCount(), 100)

    @property
    def install_dir(self):
        """Path to the installation folder."""
        return __install_dir__

    @property
    def kgs_brf_name(self):
        """Name of the kgs_brf binary executable."""
        return "kgs_brf.exe"

    @property
    def kgs_brf_path(self):
        """Path to the kgs_brf binary executable."""
        return os.path.join(self.install_dir, self.kgs_brf_name)

    def kgsbrf_is_installed(self):
        """Returns whether kgs_brf is installed or not."""
        return os.path.exists(self.kgs_brf_path)

    def install_kgsbrf(self):
        """Download and install the kgs_brf software."""
        if os.name != 'nt':
            url_t = "https://github.com/jnsebgosselin/gwhat/issues"
            msg = ("This feature is currently not supported for your"
                   " operating system. Please open a ticket in our"
                   " <a href=\"%s\">Issues Tracker</a>.") % url_t
            QMessageBox.warning(self, 'Warning', msg, QMessageBox.Ok)
            return

        print("Installing KGS_BRF software...", end=" ")
        QApplication.setOverrideCursor(Qt.WaitCursor)
        url = "http://www.kgs.ku.edu/HighPlains/OHP/index_program/KGS_BRF.zip"
        request = requests.get(url)
        zfile = zipfile.ZipFile(io.BytesIO(request.content))

        if not os.path.exists(self.install_dir):
            os.makedirs(self.install_dir)

        with open(self.kgs_brf_path, 'wb') as f:
            f.write(zfile.read(self.kgs_brf_name))

        if self.kgsbrf_is_installed():
            self.sig_kgs_brf_installed.emit(self.install_dir)
            self.close()
            print("done")
        else:
            print("failed")
        QApplication.restoreOverrideCursor()
예제 #17
0
class BRFOptionsPanel(QWidget):
    """A Panel where the options to plot the graph are displayed."""

    sig_graphconf_changed = QSignal()

    def __init__(self, parent=None):
        super(BRFOptionsPanel, self).__init__(parent)
        self.__initGUI__()
        self.setVisible(False)

    def __initGUI__(self):
        # ---- Line and Markers Style Widgets

        self._errorbar = QCheckBox('Show error bars')
        self._errorbar.setCheckState(Qt.Checked)
        self._errorbar.stateChanged.connect(self._graphconf_changed)

        self._drawline = QCheckBox('Draw line')
        self._drawline.setCheckState(Qt.Unchecked)
        self._drawline.stateChanged.connect(self._graphconf_changed)

        self._markersize = {}
        self._markersize['label'] = QLabel('Marker size :')
        self._markersize['widget'] = QSpinBox()
        self._markersize['widget'].setValue(5)
        self._markersize['widget'].setRange(0, 25)
        self._markersize['widget'].valueChanged.connect(
            self._graphconf_changed)

        # ---- Y-Axis Options Widgets

        self._ylim = {}
        self._ylim['min'] = QDoubleSpinBox()
        self._ylim['min'].setValue(0)
        self._ylim['min'].setDecimals(1)
        self._ylim['min'].setSingleStep(0.1)
        self._ylim['min'].setRange(-10, 10)
        self._ylim['min'].setEnabled(True)
        self._ylim['min'].valueChanged.connect(self._graphconf_changed)
        self._ylim['max'] = QDoubleSpinBox()
        self._ylim['max'].setValue(1)
        self._ylim['max'].setDecimals(1)
        self._ylim['max'].setSingleStep(0.1)
        self._ylim['max'].setRange(-10, 10)
        self._ylim['max'].setEnabled(True)
        self._ylim['max'].valueChanged.connect(self._graphconf_changed)
        self._ylim['scale'] = QDoubleSpinBox()
        self._ylim['scale'].setValue(0.2)
        self._ylim['scale'].setDecimals(2)
        self._ylim['scale'].setSingleStep(0.05)
        self._ylim['scale'].setRange(0.01, 1)
        self._ylim['scale'].setEnabled(True)
        self._ylim['scale'].valueChanged.connect(self._graphconf_changed)
        self._ylim['auto'] = QCheckBox('')
        self._ylim['auto'].setCheckState(Qt.Checked)
        self._ylim['auto'].stateChanged.connect(self.axis_autocheck_changed)

        # ---- X-Axis Options Widgets

        self._xlim = {}
        self._xlim['units'] = QComboBox()
        self._xlim['units'].addItems(['Hours', 'Days'])
        self._xlim['units'].setCurrentIndex(1)
        self._xlim['units'].currentIndexChanged.connect(
            self.time_units_changed)
        self._xlim['min'] = QSpinBox()
        self._xlim['min'].setValue(0)
        self._xlim['min'].setSingleStep(1)
        self._xlim['min'].setRange(0, 9999)
        self._xlim['min'].setEnabled(True)
        self._xlim['min'].valueChanged.connect(self._graphconf_changed)
        self._xlim['max'] = QSpinBox()
        self._xlim['max'].setValue(1)
        self._xlim['max'].setSingleStep(1)
        self._xlim['max'].setRange(1, 9999)
        self._xlim['max'].setEnabled(True)
        self._xlim['max'].valueChanged.connect(self._graphconf_changed)
        self._xlim['scale'] = QDoubleSpinBox()
        self._xlim['scale'].setValue(1)
        self._xlim['scale'].setDecimals(1)
        self._xlim['scale'].setSingleStep(0.1)
        self._xlim['scale'].setRange(0.1, 99)
        self._xlim['scale'].setEnabled(True)
        self._xlim['scale'].valueChanged.connect(self._graphconf_changed)
        self._xlim['auto'] = QCheckBox('')
        self._xlim['auto'].setCheckState(Qt.Checked)
        self._xlim['auto'].stateChanged.connect(self.axis_autocheck_changed)

        self.axis_autocheck_changed()

        # ---- Axis Options Layout

        axlayout = QGridLayout()

        row = 0
        axlayout.addWidget(QLabel('y-axis limits:'), 0, 0, 1, 2)
        row += 1
        axlayout.addWidget(QLabel('   Minimum :'), row, 0)
        axlayout.addWidget(self._ylim['min'], row, 1)
        row += 1
        axlayout.addWidget(QLabel('   Maximum :'), row, 0)
        axlayout.addWidget(self._ylim['max'], row, 1)
        row += 1
        axlayout.addWidget(QLabel('   Scale :'), row, 0)
        axlayout.addWidget(self._ylim['scale'], row, 1)
        row += 1
        axlayout.addWidget(QLabel('   Auto :'), row, 0)
        axlayout.addWidget(self._ylim['auto'], row, 1)
        row += 1
        axlayout.setRowMinimumHeight(row, 15)
        row += 1
        axlayout.addWidget(QLabel('x-axis limits:'), row, 0, 1, 2)
        row += 1
        axlayout.addWidget(QLabel('   Time units :'), row, 0)
        axlayout.addWidget(self._xlim['units'], row, 1)
        row += 1
        axlayout.addWidget(QLabel('   Minimum :'), row, 0)
        axlayout.addWidget(self._xlim['min'], row, 1)
        row += 1
        axlayout.addWidget(QLabel('   Maximum :'), row, 0)
        axlayout.addWidget(self._xlim['max'], row, 1)
        row += 1
        axlayout.addWidget(QLabel('   Scale :'), row, 0)
        axlayout.addWidget(self._xlim['scale'], row, 1)
        row += 1
        axlayout.addWidget(QLabel('   Auto :'), row, 0)
        axlayout.addWidget(self._xlim['auto'], row, 1)

        axlayout.setColumnStretch(3, 100)
        axlayout.setContentsMargins(0, 0, 0, 0)  # (left, top, right, bottom)

        # ---- Graph Panel Layout

        layout = QGridLayout(self)
        layout.setContentsMargins(10, 0, 10, 0)  # (l, t, r, b)

        row = 0
        layout.addWidget(self._errorbar, row, 1, 1, 2)
        row += 1
        layout.addWidget(self._drawline, row, 1, 1, 2)
        row += 1
        layout.addWidget(self._markersize['label'], row, 1)
        layout.addWidget(self._markersize['widget'], row, 2)
        row += 1
        layout.addWidget(HSep(), row, 1, 1, 2)
        row += 1
        layout.addLayout(axlayout, row, 1, 1, 2)
        row += 1
        layout.setRowMinimumHeight(row, 15)
        layout.setRowStretch(row, 100)

    def _graphconf_changed(self):
        """
        Emits a signal to indicate that the graph configuration has changed.
        """
        self.sig_graphconf_changed.emit()

    # ---- Graph Panel Properties

    @property
    def time_units(self):
        if self._xlim['auto'].checkState() == Qt.Checked:
            return 'auto'
        else:
            return self._xlim['units'].currentText().lower()

    @property
    def xmin(self):
        if self._xlim['auto'].checkState() == Qt.Checked:
            return None
        else:
            if self.time_units == 'hours':
                return self._xlim['min'].value() / 24
            else:
                return self._xlim['min'].value()

    @property
    def xmax(self):
        if self._xlim['auto'].checkState() == Qt.Checked:
            return None
        else:
            if self.time_units == 'hours':
                return self._xlim['max'].value() / 24
            else:
                return self._xlim['max'].value()

    @property
    def xscale(self):
        if self._xlim['auto'].checkState() == Qt.Checked:
            return None
        else:
            if self.time_units == 'hours':
                return self._xlim['scale'].value() / 24
            else:
                return self._xlim['scale'].value()

    @property
    def ymin(self):
        if self._ylim['auto'].checkState() == Qt.Checked:
            return None
        else:
            return self._ylim['min'].value()

    @property
    def ymax(self):
        if self._ylim['auto'].checkState() == Qt.Checked:
            return None
        else:
            return self._ylim['max'].value()

    @property
    def yscale(self):
        if self._ylim['auto'].checkState() == Qt.Checked:
            return None
        else:
            return self._ylim['scale'].value()

    @property
    def show_ebar(self):
        return self._errorbar.checkState() == Qt.Checked

    @property
    def draw_line(self):
        return self._drawline.checkState() == Qt.Checked

    @property
    def markersize(self):
        return self._markersize['widget'].value()

    # ---- Handlers

    def time_units_changed(self):
        """ Handles when the time_units combobox selection changes."""
        for xlim in [
                self._xlim['min'], self._xlim['max'], self._xlim['scale']
        ]:
            xlim.blockSignals(True)
            if self._xlim['units'].currentText() == 'Hours':
                xlim.setValue(xlim.value() * 24)
            elif self._xlim['units'].currentText() == 'Days':
                xlim.setValue(xlim.value() / 24)
            xlim.blockSignals(False)

        self._graphconf_changed()

    def axis_autocheck_changed(self):
        """
        Handles when the Auto checkbox state change for the
        limits of the y-axis or the x-axis.
        """
        self._ylim['min'].setEnabled(not self._ylim['auto'].isChecked())
        self._ylim['max'].setEnabled(not self._ylim['auto'].isChecked())
        self._ylim['scale'].setEnabled(not self._ylim['auto'].isChecked())

        self._xlim['units'].setEnabled(not self._xlim['auto'].isChecked())
        self._xlim['min'].setEnabled(not self._xlim['auto'].isChecked())
        self._xlim['max'].setEnabled(not self._xlim['auto'].isChecked())
        self._xlim['scale'].setEnabled(not self._xlim['auto'].isChecked())

        self._graphconf_changed()
예제 #18
0
class DataGapfillWorker(WorkerBase):
    """
    This class manage all that is related to the gap-filling of weather data
    records, including reading the data file on the disk.

    Parameters
    ----------
    NSTAmax : int
    limitDist : float
    limitAlt : float
    regression_mode : int
    full_error_analysis : bool
    """
    sig_task_progress = QSignal(int)
    sig_status_message = QSignal(str)
    sig_console_message = QSignal(str)
    sig_gapfill_finished = QSignal(bool)

    def __init__(self):
        super().__init__()
        self.target = None
        self.alt_and_dist = None
        self.corcoef = None

        # ---- Required Inputs
        self.time_start = None
        self.time_end = None

        self.WEATHER = self.wxdatasets = WeatherData()
        self.wxdatasets.sig_task_progress.connect(self.sig_task_progress.emit)
        self.wxdatasets.sig_status_message.connect(
            self.sig_status_message.emit)
        self.wxdatasets.sig_corrcoeff_calculated.connect(
            lambda: self.wxdatasets.save_to_binary(self.inputDir))

        self.inputDir = None
        self.isParamsValid = False

        # ---- Define Parameters Default
        # Maximum number of neighboring stations that will be used to fill
        # the missing data in the target station
        self.NSTAmax = 4

        self.limitDist = 100
        self.limitAlt = 350

        # if *regression_mode* = 1: Ordinary Least Square
        # if *regression_mode* = 0: Least Absolute Deviations
        self.regression_mode = 1

        # Set whether a complete analysis of the estimation errors is
        # conducted with a cross-validation procedure while filling missing
        # data.
        self.full_error_analysis = False

    @property
    def outputdir(self):
        if self.inputDir is None:
            return None
        else:
            return osp.join(self.inputDir, 'GAPFILLED')

    @property
    def NSTAmax(self):
        return self.__NSTAmax

    @NSTAmax.setter
    def NSTAmax(self, x):
        if type(x) != int or x < 1:
            raise ValueError('!WARNING! NSTAmax must be must be an integer'
                             ' with a value greater than 0.')
        self.__NSTAmax = x

    def load_data(self, force_reload=False):
        """
        Read the csv files in the input data directory folder.

        The resulting formatted dataset is saved in a structured numpy array
        in binary format, so that loading time is improved on subsequent runs.
        Some checks are made to be sure the binary match with the current
        data files in the folder.
        """
        self.target = None
        self.alt_and_dist = None
        self.corcoef = None

        if self.inputDir is None:
            print('Please specify a valid input data file directory.')
            return
        if not osp.exists(self.inputDir):
            print('Input data directory does not exists.')
            return
        if force_reload is True:
            print('Force reloading data from csv file...')
            return self._reload_data()

        # Check if a cached binary file exists.
        binfile = os.path.join(self.inputDir, '__cache__', 'fdata.npy')
        if not osp.exists(binfile):
            return self._reload_data()

        # Try to load data from the cached binary file.
        try:
            self.wxdatasets.load_from_binary(self.inputDir)
        except Exception as e:
            print('Failed to load data from cache because '
                  'of the following error:')
            print(e)
            return self._reload_data()
        else:
            # Scan input folder for changes

            # If one of the csv data file contained within the input data
            # directory has changed since last time the binary file was
            # created, the data will be reloaded from the csv files and a
            # new binary file will be generated.

            filenames = [osp.basename(f) for f in self.wxdatasets.filenames]
            bmtime = osp.getmtime(binfile)
            count = 0
            for f in os.listdir(self.inputDir):
                if f.endswith('.csv'):
                    fmtime = osp.getmtime(osp.join(self.inputDir, f))
                    if f in filenames and fmtime <= bmtime:
                        count += 1
            if len(filenames) != count:
                print('One or more input data files in the workind '
                      'directory changed since the last time the data '
                      'were cached.')
                return self._reload_data()
            else:
                print('Data loaded from cache.')

    def _reload_data(self):
        """
        Read the csv files in the input data directory folder, format
        the datasets and save the results in a binary file.
        """
        filepaths = [
            osp.join(self.inputDir, f) for f in os.listdir(self.inputDir)
            if f.endswith('.csv')
        ]
        print('{:d} csv files were found in {}.'.format(
            len(filepaths), self.inputDir))

        message = 'Reading data from csv files...'
        print(message)
        self.sig_status_message.emit(message)

        self.wxdatasets.load_and_format_data(filepaths)
        self.wxdatasets.save_to_binary(self.inputDir)

        print('Data loaded successfully.')
        self.sig_status_message.emit('')

    def get_target_station(self):
        """
        Return the metadata related to the current target station.
        """
        return self.wxdatasets.metadata.loc[self.target]

    def set_target_station(self, station_id):
        """
        Set the target station to the station corresponding to the specified
        station id.
        """
        if station_id not in self.wxdatasets.station_ids:
            self.target = None
            self.alt_and_dist = None
            self.corcoef = None
            raise ValueError(
                "No data currently loaded for station '{}'.".format(
                    station_id))
        else:
            self.target = station_id
            self.alt_and_dist = self.wxdatasets.alt_and_dist_calc(station_id)
            self.corcoef = (
                self.wxdatasets.compute_correlation_coeff(station_id))

    def get_valid_neighboring_stations(self, hdist_limit, vdist_limit):
        """
        Return the list of neighboring stations that are within the
        horizontal and altitude range of the target station.
        """
        # If cutoff limits for the horizontal distance and altitude are set
        # to a negative number, all stations are kept regardless of their
        # distance or altitude difference with the target station.
        valid_stations = self.alt_and_dist.copy()
        if hdist_limit > 0:
            valid_stations = valid_stations[
                valid_stations['hordist'] <= hdist_limit]
        if vdist_limit > 0:
            valid_stations = valid_stations[
                valid_stations['altdiff'].abs() <= vdist_limit]
        valid_stations = valid_stations.index.values.tolist()
        valid_stations.remove(self.target)
        return valid_stations

    def gapfill_data(self, time_start, time_end, max_neighbors, hdist_limit,
                     vdist_limit, regression_mode):
        """Gapfill the data of the target station."""
        tstart_total = process_time()

        neighbors = self.get_valid_neighboring_stations(
            hdist_limit, vdist_limit)
        gapfill_date_range = pd.date_range(start=time_start,
                                           end=time_end,
                                           freq='D')
        y2fill = pd.DataFrame(np.nan,
                              index=gapfill_date_range,
                              columns=VARNAMES)
        self.sig_task_progress.emit(0)
        for i, varname in enumerate(VARNAMES):
            # When a station does not have enough data for a given variable,
            # its correlation coefficient is set to nan. If all the stations
            # have a NeN value in the correlation table for a given variable,
            # it means there is not enough data available overall to estimate
            # and fill the missing data for that variable.
            var2fill = (self.corcoef.loc[neighbors].dropna(
                axis=1, how='all').columns.tolist())
            if varname not in var2fill:
                msg = ("Variable {} will not be filled because there "
                       "is not enough data.").format(varname)
                print(msg)
                self.sig_console_message.emit('<font color=red>%s</font>' %
                                              msg)
                continue
            tstart = process_time()
            message = 'Gapfilling data for variable {}...'.format(varname)
            print(message)
            self.sig_status_message.emit(message)

            reg_models = {}
            notnull = self.wxdatasets.data[varname].loc[gapfill_date_range,
                                                        neighbors].notnull()
            notnull_groups = notnull.groupby(by=neighbors, axis=0)
            for j, group in enumerate(notnull_groups):
                group_dates = group[1].index
                group_neighbors = group[1].columns[list(group[0])]
                if len(group_neighbors) == 0:
                    # It is impossible to fill the data in this group
                    # because all neighboring stations are empty.
                    continue

                # Determines the neighboring stations to include in the
                # regression model.
                model_neighbors = list(
                    self.corcoef.loc[group_neighbors].sort_values(
                        varname, axis=0,
                        ascending=False).index)[:max_neighbors]

                neighbors_combi = ', '.join(model_neighbors)
                if neighbors_combi in reg_models:
                    # Regression coefficients and RSME are recalled
                    # from the memory matrices.
                    A = reg_models[neighbors_combi]
                else:
                    # This is the first time this neighboring stations
                    # combination is encountered in the routine,
                    # regression coefficients need to be calculated.

                    # The data for the current variable are sorted by
                    # their stations in in descending correlation
                    # coefficient.
                    YX = self.wxdatasets.data[varname][[self.target] +
                                                       model_neighbors].copy()

                    # Remove all rows containing at least one nan value.
                    YX = YX.dropna()

                    # Rows for which precipitation of the target station
                    # and all the neighboring stations is 0 are removed.
                    # This is only applicable for precipitation, not air
                    # temperature.
                    if varname in ['Ptot']:
                        YX = YX.loc[(YX != 0).any(axis=1)]

                    # Dependant variable (target)
                    Y = YX[self.target].values

                    # Independant variables (neighbors)
                    X = YX[model_neighbors].values

                    # Add a unitary array to X for the intercept term if
                    # variable is a temperature type data.
                    # (though this was questionned by G. Flerchinger)
                    if varname in ['Tmax', 'Tavg', 'Tmin']:
                        X = np.hstack((np.ones((len(Y), 1)), X))

                    # Generate the MLR Model
                    A = self.build_mlr_model(X, Y, regression_mode)

                    # Calcul the RMSE.

                    # Calculate a RMSE between the estimated and
                    # measured values of the target station.
                    # RMSE with 0 value are not accounted for
                    # in the calcultation.
                    Yp = np.dot(A, X.transpose())

                    rmse = (Y - Yp)**2  # MAE = np.abs(Y - Yp)
                    rmse = rmse[rmse != 0]  # MAE = MAE[MAE!=0]
                    rmse = np.mean(rmse)**0.5  # MAE = np.mean(MAE)
                    # print('Calcul RMSE', rmse)

                    # Store values in memory.
                    reg_models[neighbors_combi] = A

                # Calculate the missing values for the group.
                X = self.wxdatasets.data[varname].loc[group_dates,
                                                      model_neighbors].values
                if varname in ['Tmax', 'Tavg', 'Tmin']:
                    X = np.hstack((np.ones((len(X), 1)), X))

                Y = np.dot(A, X.transpose())
                # Limit precipitation to positive values.
                # This may happens when there is one or more negative
                # regression coefficients in A
                if varname in ['Ptot']:
                    Y[Y < 0] = 0

                # Store the results.
                y2fill.loc[group_dates, varname] = Y
                self.sig_task_progress.emit(
                    int((j + 1) / len(notnull_groups) * 100 / len(VARNAMES) +
                        i / len(VARNAMES) * 100))
            self.sig_task_progress.emit(int((i + 1) / len(VARNAMES) * 100))
            print('Data gapfilled for {} in {:0.1f} sec.'.format(
                varname,
                process_time() - tstart))

        # Gapfill dataset for the target station.
        gapfilled_data = pd.DataFrame([], index=gapfill_date_range)
        for varname in VARNAMES:
            # Fetch the original target data for varname.
            gapfilled_data[varname] = self.wxdatasets.data[varname].loc[
                gapfill_date_range, self.target]

            # Fill the gaps.
            isnull = gapfilled_data.index[gapfilled_data[varname].isnull()]
            gapfilled_data.loc[isnull, varname] = y2fill.loc[isnull, varname]

        message = ('Data completion for station %s completed successfully '
                   'in %0.2f sec.') % (self.target,
                                       (process_time() - tstart_total))
        print(message)
        self.sig_status_message.emit(message)
        self.sig_console_message.emit('<font color=black>%s</font>' % message)

        if gapfilled_data.isnull().values.any():
            message = ("WARNING: Some missing data were not filled because "
                       "all neighboring stations were empty for that period.")
            print(message)
            self.sig_console_message.emit('<font color=red>%s</font>' %
                                          message)

        # Save the gapfilled data to a file.

        # Add Year, Month and Day to the dataset and rename some columns.
        gapfilled_data['Year'] = gapfilled_data.index.year.astype(str)
        gapfilled_data['Month'] = gapfilled_data.index.month.astype(str)
        gapfilled_data['Day'] = gapfilled_data.index.day.astype(str)
        for varname in VARNAMES:
            gapfilled_data[varname] = gapfilled_data[varname].round(1)

        # Replace nan values by an empty string.
        gapfilled_data = gapfilled_data.fillna(value='')

        # Make sure the columns are in the right order.
        gapfilled_data = gapfilled_data[[
            'Year', 'Month', 'Day', 'Tmax', 'Tmin', 'Tavg', 'Ptot'
        ]]

        target_metadata = self.wxdatasets.metadata.loc[self.target]
        data_headers = [
            'Year', 'Month', 'Day', 'Max Temp (°C)', 'Min Temp (°C)',
            'Mean Temp (°C)', 'Total Precip (mm)'
        ]
        fcontent = [['Station Name', target_metadata['Station Name']],
                    ['Province', target_metadata['Location']],
                    ['Latitude (dd)', target_metadata['Latitude']],
                    ['Longitude (dd)', target_metadata['Longitude']],
                    ['Elevation (m)', target_metadata['Elevation']],
                    ['Climate Identifier', self.target], [],
                    ['Created by', __namever__],
                    ['Created on', strftime("%d/%m/%Y")], [], data_headers
                    ] + gapfilled_data.values.tolist()

        # Save the data to csv.
        if not osp.exists(self.outputdir):
            os.makedirs(self.outputdir)

        clean_target_name = (target_metadata['Station Name'].replace(
            '\\', '_').replace('/', '_'))
        filename = '{} ({})_{}-{}.csv'.format(clean_target_name, self.target,
                                              str(min(gapfilled_data['Year'])),
                                              str(max(gapfilled_data['Year'])))

        filepath = osp.join(self.outputdir, filename)
        with open(filepath, 'w', encoding='utf-8') as f:
            writer = csv.writer(f, delimiter=',', lineterminator='\n')
            writer.writerows(fcontent)

        self.sig_gapfill_finished.emit(True)
        return gapfilled_data

    def build_mlr_model(self, X, Y, regression_mode):
        """
        Build a multiple linear model using the provided independent (X) and
        dependent (y) variable data.
        """
        if regression_mode == 1:  # Ordinary Least Square regression

            # http://statsmodels.sourceforge.net/devel/generated/
            # statsmodels.regression.linear_model.OLS.html

            # model = OLS(Y, X)
            # results = model.fit()
            # A = results.params

            # Using Numpy function:
            A = np.linalg.lstsq(X, Y, rcond=None)[0]
        else:  # Least Absolute Deviations regression

            # http://statsmodels.sourceforge.net/devel/generated/
            # statsmodels.regression.quantile_regression.QuantReg.html

            # http://statsmodels.sourceforge.net/devel/examples/
            # notebooks/generated/quantile_regression.html

            # model = QuantReg(Y, X)
            # results = model.fit(q=0.5)
            # A = results.params

            # Using Homemade function:
            A = L1LinearRegression(X, Y)

        return A

    @staticmethod
    def postprocess_fillinfo(staName, YX, tarStaIndx):

        # Extracts info related to the target station from <YXmFull>  and the
        # info related to the neighboring stations. Xm is for the
        # neighboring stations and Ym is for the target stations.

        Yname = staName[tarStaIndx]  # target station name
        Xnames = np.delete(staName, tarStaIndx)  # neighboring station names

        Y = YX[:, tarStaIndx, :]  # Target station data
        X = np.delete(YX, tarStaIndx, axis=1)  # Neighboring station data

        # Counts how many times each neigboring station was used for
        # estimating the data of the target stations.

        Xcount_var = np.sum(~np.isnan(X), axis=0)
        Xcount_tot = np.sum(Xcount_var, axis=1)

        # Removes the neighboring stations that were not used.

        indx = np.where(Xcount_tot > 0)[0]
        Xnames = Xnames[indx]
        X = X[:, indx]

        Xcount_var = Xcount_var[indx, :]
        Xcount_tot = Xcount_tot[indx]

        # Sort the neighboring stations by importance.

        indx = np.argsort(Xcount_tot * -1)
        Xnames = Xnames[indx]
        X = X[:, indx]

        return Yname, Y, Xnames, X, Xcount_var, Xcount_tot

    def generate_html_summary_table(self):
        return self.wxdatasets.generate_html_summary_table()

    def generate_correlation_html_table(self, gapfill_parameters):
        """
        This function generate an HTML output to be displayed in the
        <Fill Data> tab display area after a target station has been
        selected by the user.
        """
        target_metadata = self.wxdatasets.metadata.loc[self.target]
        header_data = {
            'Latitude': target_metadata['Latitude'],
            'Longitude': target_metadata['Longitude'],
            'Altitude': target_metadata['Elevation'],
            'Data start': target_metadata['first_date'],
            'Data end': target_metadata['last_date']
        }
        target_info = (
            '<table border="0" cellpadding="1" cellspacing="0" align="left">')
        for field, value in header_data.items():
            target_info += '<tr>'
            target_info += '<td align="left">%s</td>' % field
            target_info += '<td align="left">&nbsp;=&nbsp;</td>'
            target_info += '<td align="left">%s</td>' % value
            target_info += '</tr>'
        target_info += '</table>'

        # Sort neighboring stations.

        # Stations best correlated with the target station are displayed toward
        # the top of the table while neighboring stations poorly correlated are
        # displayed toward the bottom.

        # Define a criteria for sorting the correlation quality
        # of the stations.

        # Generate the missing data table.
        fill_date_start = gapfill_parameters['date_start']
        fill_date_end = gapfill_parameters['date_end']
        table1 = '''
                 <p align=justify>
                   Table 1 : Number of days with missing data from
                   <b>%s</b> to <b>%s</b> for station <b>%s</b>:
                 </p>
                 ''' % (fill_date_start, fill_date_end,
                        target_metadata['Station Name'])
        table1 += '''
                  <table border="0" cellpadding="3" cellspacing="0"
                         align="center">
                    <tr>
                      <td colspan="5"><hr></td>
                    </tr>
                    <tr>
                      <td width=135 align="left">Weather Variable</td>
                      <td align="center">T<sub>max</sub></td>
                      <td align="center">T<sub>min</sub></sub></td>
                      <td align="center">T<sub>mean</sub></td>
                      <td align="center">P<sub>tot</sub></td>
                    </tr>
                    <tr>
                      <td colspan="5"><hr></td>
                    </tr>
                    <tr>
                     <td width=135 align="left">Days with<br>missing data</td>
                  '''

        datetime_start = datetime.strptime(gapfill_parameters['date_start'],
                                           '%d/%m/%Y')
        datetime_end = datetime.strptime(gapfill_parameters['date_end'],
                                         '%d/%m/%Y')
        total_nbr_data = (
            (datetime_end - datetime_start).total_seconds() / 3600 / 24 + 1)
        for var in self.wxdatasets.data.keys():
            data = self.wxdatasets.data[var][self.target]
            nbr_nan = len(
                data[(data.index >= datetime_start)
                     & (data.index <= datetime_end) & (data.isnull())])
            nan_percent = round(nbr_nan / total_nbr_data * 100, 1)

            table1 += '''
                      <td align="center">
                      %d<br>(%0.1f %%)
                      </td>
                      ''' % (nbr_nan, nan_percent)
        table1 += '''
                  </tr>
                  <tr>
                  <td colspan="5"><hr></td>
                  </tr>
                  </table>
                  <br><br>
                  '''

        # Generate the correlation coefficient table
        table2 = table1
        table2 += '''
                  <p align="justify">
                    <font size="3">
                      Table 2 : Altitude difference, horizontal distance and
                      correlation coefficients for each meteorological
                      variables, calculated between station <b>%s</b> and its
                      neighboring stations :
                    <\font>
                  </p>
                  ''' % target_metadata['Station Name']

        # Generate the horizontal header of the table.
        table2 += '''
                  <table border="0" cellpadding="3" cellspacing="0"
                         align="center" width="100%%">
                    <tr>
                      <td colspan="9"><hr></td>
                    </tr>
                    <tr>
                      <td align="center" valign="bottom" width=30 rowspan="3">
                        #
                      </td>
                      <td align="left" valign="bottom" width=200 rowspan="3">
                        Neighboring Stations
                      </td>
                      <td width=60 align="center" valign="bottom" rowspan="3">
                        &#916;Alt.<br>(m)
                      </td>
                      <td width=60 align="center" valign="bottom" rowspan="3">
                        Dist.<br>(km)
                      </td>
                      <td align="center" valign="middle" colspan="4">
                        Correlation Coefficients
                      </td>
                    </tr>
                    <tr>
                      <td colspan="4"><hr></td>
                    </tr>
                    <tr>
                      <td width=60 align="center" valign="middle">
                        T<sub>max</sub>
                      </td>
                      <td width=60 align="center" valign="middle">
                        T<sub>min</sub>
                      </td>
                      <td width=60 align="center" valign="middle">
                        T<sub>mean</sub>
                      </td>
                      <td width=60 align="center" valign="middle">
                        P<sub>tot</sub>
                      </td>
                    </tr>
                    <tr>
                      <td colspan="9"><hr></td>
                    </tr>
                  '''
        corcoef = self.corcoef.sort_values('Ptot', axis=0, ascending=False)

        stations = corcoef.index.values.tolist()
        stations.remove(self.target)
        for i, station_id in enumerate(stations):
            color = ['transparent', LIGHTGRAY][i % 2]
            metadata = self.wxdatasets.metadata.loc[station_id]

            # Neighboring station names.
            table2 += '''
                       <tr bgcolor="%s">
                         <td align="center" valign="top">%02d</td>
                         <td valign="top">
                           %s
                         </td>
                      ''' % (color, i + 1, metadata['Station Name'])

            # Check the condition for the altitude difference.
            limit_altdiff = gapfill_parameters['limitAlt']
            altdiff = self.alt_and_dist.loc[station_id]['altdiff']
            if abs(altdiff) >= limit_altdiff and limit_altdiff >= 0:
                fontcolor = RED
            else:
                fontcolor = ''

            table2 += '''
                      <td align="center" valign="top">
                        <font color="%s">%0.1f</font>
                      </td>
                      ''' % (fontcolor, altdiff)

            # Check the condition for the horizontal distance.
            limit_hordist = gapfill_parameters['limitDist']
            hordist = self.alt_and_dist.loc[station_id]['hordist']
            if hordist >= limit_hordist and limit_hordist >= 0:
                fontcolor = RED
            else:
                fontcolor = ''

            table2 += '''
                      <td align="center" valign="top">
                        <font color="%s">%0.1f</font>
                      </td>
                      ''' % (fontcolor, hordist)
            # Add the correlation coefficients to the table.
            for var in ['Tmax', 'Tmin', 'Tavg', 'Ptot']:
                value = self.corcoef.loc[station_id, var]
                fontcolor = RED if value < 0.7 else ''
                table2 += '''
                          <td align="center" valign="top">
                            <font color="%s">%0.3f</font>
                          </td>
                          ''' % (fontcolor, value)
            table2 += '</tr>'
        table2 += '''  <tr>
                         <td colspan="8"><hr></td>
                       </tr>
                       <tr>
                         <td align="justify" colspan="8">
                         <font size="2">
                           * Correlation coefficients are set to
                           <font color="#C83737">NaN</font> for a given
                           variable if there is less than
                           <font color="#C83737">%d</font> pairs of data
                           between the target and the neighboring station.
                           </font>
                         </td>
                       </tr>
                     </table>
                     ''' % (365 // 2)
        return table2, target_info
예제 #19
0
class DateRangeNavigator(ToolBarWidget):
    """A widget to navigate date spans."""

    sig_date_span_changed = QSignal(tuple)

    def __init__(self, icon_size='small', parent=None):
        super(DateRangeNavigator, self).__init__(parent)

        self.home = arrow.now().floor('week').span('week')
        self.current = self.home

        self.setup(icon_size)
        self.setup_date_range_label()

    def setup(self, icon_size):
        """Setup the widget with the provided arguments."""
        self.date_range_labl = QLabel()
        self.btn_home = QToolButtonBase('home')
        self.btn_home.clicked.connect(self.go_home)
        self.btn_next = QToolButtonBase('go-next')
        self.btn_next.clicked.connect(self.go_next_range)
        self.btn_next.setEnabled(False)
        self.btn_prev = QToolButtonBase('go-previous')
        self.btn_prev.clicked.connect(self.go_previous_range)

        # setup the layout

        self.addWidget(self.btn_prev)
        self.addWidget(self.btn_next)
        self.addWidget(self.btn_home)
        self.addWidget(self.date_range_labl)

        self.set_icon_size(icon_size)

    def set_icon_size(self, icon_size):
        """Set the size of the button icon."""
        self.btn_home.setIconSize(icons.get_iconsize(icon_size))
        self.btn_prev.setIconSize(icons.get_iconsize(icon_size))
        self.btn_next.setIconSize(icons.get_iconsize(icon_size))

    def go_next_range(self):
        """Go forward one date range step."""
        self.current = (self.current[0].shift(weeks=1),
                        self.current[1].shift(weeks=1))
        self.setup_date_range_label()
        self.btn_next.setEnabled(self.current != self.home)
        self.sig_date_span_changed.emit(self.current)

    def go_previous_range(self):
        """Go back one date range step."""
        self.current = (self.current[0].shift(weeks=-1),
                        self.current[1].shift(weeks=-1))
        self.setup_date_range_label()
        self.btn_next.setEnabled(self.current != self.home)
        self.sig_date_span_changed.emit(self.current)

    def go_home(self):
        """Go back to the range encompassing the present day."""
        self.current = self.home
        self.btn_next.setEnabled(False)
        self.setup_date_range_label()
        self.sig_date_span_changed.emit(self.current)

    def setup_date_range_label(self):
        """Setup the text in the label widget."""
        self.date_range_labl.setText(arrowspan_to_str(self.current))
예제 #20
0
class WeatherStationFinder(QObject):

    sig_progress_msg = QSignal(str)
    sig_load_database_finished = QSignal(bool)

    def __init__(self, filelist=None, *args, **kwargs):
        super(WeatherStationFinder, self).__init__(*args, **kwargs)
        self._data = None

    # ---- Load and fetch database

    @property
    def data(self):
        """Content of the ECCC database."""
        return self._data

    def load_database(self):
        """
        Load the climate station list from a file if it exist or else fetch it
        from ECCC Tor ftp server.
        """
        if os.path.exists(DATABASE_FILEPATH):
            self.sig_progress_msg.emit(
                "Loading the climate station database from file.")
            ts = time.time()
            self._data = np.load(DATABASE_FILEPATH).item()
            te = time.time()
            print("Station list loaded sucessfully in %0.2f sec." % (te - ts))
            self.sig_load_database_finished.emit(True)
        else:
            self.fetch_database()

    def fetch_database(self):
        """
        Fetch and read the list of climate stations with daily data
        from the ECCC Tor ftp server and save the result on disk.
        """
        print("Fetching station list from ECCC Tor ftp server...")
        ts = time.time()
        self._data = None
        failed_fetch_try = 0
        while True:
            self.sig_progress_msg.emit("Fetching the climate station database"
                                       " from the ECCC server...")
            self._data = read_stationlist_from_tor()
            if self._data is None:
                failed_fetch_try += 1
                if failed_fetch_try <= MAX_FAILED_FETCH_TRY:
                    print("Failed to fetch the database from "
                          " the ECCC server (%d/%d)." %
                          (failed_fetch_try, MAX_FAILED_FETCH_TRY))
                    time.sleep(3)
                else:
                    msg = "Failed to fetch the database from the ECCC server."
                    print(msg)
                    self.sig_progress_msg.emit(msg)
                    break
            else:
                np.save(DATABASE_FILEPATH, self._data)
                te = time.time()
                print("Station list fetched sucessfully in %0.2f sec." %
                      (te - ts))
                break
        self.sig_load_database_finished.emit(True)

    # ---- Utility functions

    def get_stationlist(self, status=None, prov=None, prox=None, yrange=None):
        """
        Return a list of the stations in the ECCC database that
        fulfill the conditions specified in arguments.
        """
        N = len(self.data['Name'])
        results = np.ones(N)
        if prov:
            results = results * np.isin(self.data['Province'], prov)
        if status:
            results = results * (self.data['Status'] == status)
        if prox:
            lat1, lon1, max_dist = prox
            lat2, lon2 = self.data['Latitude'], self.data['Longitude']
            dists = calc_dist_from_coord(lat1, lon1, lat2, lon2)
            results = results * (dists <= max_dist)
        if yrange:
            arr_ymin = np.max(np.vstack(
                [self.data['DLY First Year'],
                 np.ones(N) * yrange[0]]),
                              axis=0)
            arr_ymax = np.min(np.vstack(
                [self.data['DLY Last Year'],
                 np.ones(N) * yrange[1]]),
                              axis=0)
            results = results * ((arr_ymax - arr_ymin + 1) >= yrange[2])

        indexes = np.where(results == 1)[0]
        stations = np.vstack((
            self.data['Name'][indexes],
            self.data['Station ID'][indexes],
            self.data['DLY First Year'][indexes],
            self.data['DLY Last Year'][indexes],
            self.data['Province'][indexes],
            self.data['ID'][indexes],
            self.data['Latitude'][indexes],
            self.data['Longitude'][indexes],
            self.data['Elevation'][indexes],
        )).transpose().tolist()

        stationlist = WeatherSationList()
        stationlist.add_stations(stations)

        return stationlist
예제 #21
0
class ProjetManager(QWidget):

    currentProjetChanged = QSignal(ProjetReader)

    def __init__(self, parent=None, projet=None):
        super(ProjetManager, self).__init__(parent)
        self.new_projet_dialog = NewProject(parent)
        self.new_projet_dialog.sig_new_project.connect(self.load_project)

        self.__projet = None
        self.__initGUI__()
        if projet:
            self.load_project(projet)

    def __initGUI__(self):
        self.project_display = QPushButton()
        self.project_display.setFocusPolicy(Qt.NoFocus)
        self.project_display.setMinimumWidth(100)
        self.project_display.clicked.connect(self.select_project)

        ft = QApplication.instance().font()
        ft.setPointSize(ft.pointSize() - 1)
        self.project_display.setFont(ft)

        new_btn = QToolButtonSmall(icons.get_icon('new_project'))
        new_btn.setToolTip('Create a new project...')
        new_btn.clicked.connect(self.show_newproject_dialog)

        # ---- layout ----

        layout = QGridLayout(self)

        layout.addWidget(QLabel('Project :'), 0, 1)
        layout.addWidget(self.project_display, 0, 2)
        layout.addWidget(new_btn, 0, 3)

        layout.setSpacing(3)
        layout.setContentsMargins(0, 0, 0, 5)  # (L, T, R, B)
        layout.setColumnStretch(0, 500)
        layout.setRowMinimumHeight(0, 28)

    # =========================================================================

    @property
    def projet(self):
        return self.__projet

    def select_project(self):
        directory = os.path.abspath(os.path.join('..', 'Projects'))
        filename, _ = QFileDialog.getOpenFileName(self, 'Open Project',
                                                  directory, '*.gwt ; *.what')

        if filename:
            self.projectfile = filename
            self.load_project(filename)

    def load_project(self, filename):
        if not osp.exists(filename):
            self.__projet = None
            msg = """
                  <p>
                    <b>Failed to load the project.</b><br><br>
                    The project file<br>%s<br>
                    does not exist.<br><br>
                    Please open an existing project or create a new one.
                  </p>
                """ % osp.abspath(filename)
            QMessageBox.warning(self, 'Warning', msg, QMessageBox.Ok)
            return False

        try:
            self.__projet = projet = ProjetReader(filename)
        except Exception:
            self.__projet = None
            msg = """
                  <p>
                    <b>Failed to load the project.</b><br><br>
                    The project file<br>%s<br>
                    is not valid.<br><br>
                    Please open a valid project or create a new one.
                  </p>
                """ % osp.abspath(filename)
            QMessageBox.warning(self, 'Warning', msg, QMessageBox.Ok)
            return False
        else:
            wldir = os.path.join(projet.dirname, "Water Levels")
            init_waterlvl_measures(wldir)
            self.project_display.setText(projet.name)
            self.project_display.adjustSize()
            self.currentProjetChanged.emit(projet)
            return True

    def close_projet(self):
        """Closes the currently opened hdf5 project file."""
        if self.__projet is not None:
            self.__projet.close_projet()

    def show_newproject_dialog(self):
        self.new_projet_dialog.reset_UI()
        self.new_projet_dialog.show()
예제 #22
0
class WatsonTableModel(QAbstractTableModel):

    HEADER = [
        'start', 'end', 'duration', 'project', 'tags', 'comment', 'id', ''
    ]
    COLUMNS = {
        'start': 0,
        'end': 1,
        'duration': 2,
        'project': 3,
        'tags': 4,
        'comment': 5,
        'id': 6,
        'icons': 7
    }
    EDIT_COLUMNS = [
        COLUMNS['start'], COLUMNS['end'], COLUMNS['project'],
        COLUMNS['comment'], COLUMNS['tags']
    ]
    sig_btn_delrow_clicked = QSignal(QModelIndex)
    sig_model_changed = QSignal()
    sig_total_seconds_changed = QSignal(float)

    def __init__(self, client):
        super(WatsonTableModel, self).__init__()
        self.client = client

        self.dataChanged.connect(self.model_changed)
        self.rowsInserted.connect(self.model_changed)
        self.modelReset.connect(self.model_changed)
        self.rowsRemoved.connect(self.model_changed)

    def model_changed(self):
        """Emit a signal whenever the model is changed."""
        self.sig_model_changed.emit()

    def rowCount(self, parent=QModelIndex()):
        """Qt method override. Return the number of row of the table."""
        return len(self.client.frames)

    def columnCount(self, parent=QModelIndex()):
        """Qt method override. Return the number of column of the table."""
        return len(self.HEADER)

    def data(self, index, role=Qt.DisplayRole):
        """Qt method override."""
        frames = self.client.frames
        if role == Qt.DisplayRole:
            if index.column() == self.COLUMNS['start']:
                return frames[index.row()][0].format('YYYY-MM-DD HH:mm')
            elif index.column() == self.COLUMNS['end']:
                return frames[index.row()][1].format('YYYY-MM-DD HH:mm')
            elif index.column() == self.COLUMNS['duration']:
                total_seconds = (frames[index.row()][1] -
                                 frames[index.row()][0]).total_seconds()
                return strftime("%Hh %Mmin", gmtime(total_seconds))
            elif index.column() == self.COLUMNS['project']:
                return str(frames[index.row()].project)
            elif index.column() == self.COLUMNS['comment']:
                msg = frames[index.row()].message
                return '' if msg is None else msg
            elif index.column() == self.COLUMNS['id']:
                return frames[index.row()].id[:7]
            elif index.column() == self.COLUMNS['tags']:
                return list_to_str(frames[index.row()].tags)
            else:
                return ''
        elif role == Qt.ToolTipRole:
            if index.column() == self.COLUMNS['comment']:
                msg = frames[index.row()].message
                return '' if msg is None else msg
            elif index.column() == self.COLUMNS['id']:
                return frames[index.row()].id
            elif index.column() == self.COLUMNS['icons']:
                return "Delete frame"
            elif index.column() == self.COLUMNS['project']:
                return frames[index.row()].project
            elif index.column() == self.COLUMNS['tags']:
                return list_to_str(frames[index.row()].tags)
        elif role == Qt.BackgroundRole:
            return colors.get_qcolor('base')
        elif role == Qt.TextAlignmentRole:
            if index.column() == self.COLUMNS['comment']:
                return Qt.AlignLeft | Qt.AlignVCenter
            if index.column() == self.COLUMNS['tags']:
                return Qt.AlignLeft | Qt.AlignVCenter
            else:
                return Qt.AlignCenter
        else:
            return QVariant()

    def headerData(self, section, orientation, role):
        """Qt method override."""
        if role == Qt.DisplayRole and orientation == Qt.Horizontal:
            return self.HEADER[section]
        if role == Qt.DisplayRole and orientation == Qt.Vertical:
            return section
        else:
            return QVariant()

    def flags(self, index):
        """Qt method override."""
        if index.column() in self.EDIT_COLUMNS:
            return Qt.ItemIsEnabled | Qt.ItemIsEditable | Qt.ItemIsSelectable
        else:
            return Qt.ItemIsEnabled | Qt.ItemIsSelectable

    # ---- Utils

    @property
    def projects(self):
        return self.client.projects

    def get_frame_from_index(self, index):
        """Return the frame stored at the row of index."""
        return self.client.frames[index.row()]

    def get_project_from_index(self, index):
        """Return the project of the frame corresponding to the model index."""
        return self.client.frames[index.row()].project

    def get_tags_from_index(self, index):
        """Return a list of tags for the frame from a table index."""
        return self.client.frames[index.row()].tags

    def get_frameid_from_index(self, index):
        """Return the frame id from a table index."""
        return self.client.frames[index.row()].id

    def get_start_datetime_range(self, index):
        """
        Return the range in which the start time of the frame located at
        index can be moved without creating any conflict.
        """
        frames = self.client.frames
        lmin = (frames[index.row() -
                       1].stop if index.row() > 0 else local_arrow_from_str(
                           '1980-01-01 00:00:00', 'YYYY-MM-DD HH:mm:ss'))
        lmax = frames[index.row()].stop
        return lmin, lmax

    def get_stop_datetime_range(self, index):
        """
        Return the range in which the stop time of the frame located at
        index can be moved without creating any conflict.
        """
        frames = self.client.frames
        lmin = frames[index.row()].start
        lmax = (arrow.now() if index.row() == (len(frames) -
                                               1) else frames[index.row() +
                                                              1].start)
        return lmin, lmax

    # ---- Watson handlers

    def emit_btn_delrow_clicked(self, index):
        """
        Send a signal with the model index where the button to delete an
        activity has been clicked.
        """
        self.sig_btn_delrow_clicked.emit(index)

    def editFrame(self,
                  index,
                  start=None,
                  stop=None,
                  project=None,
                  message=None,
                  tags=None):
        """
        Edit Frame stored at index in the model from the provided
        arguments
        """
        edit_frame_at(self.client, index.row(), start, stop, project, message,
                      tags)
        self.client.save()
        self.dataChanged.emit(index, index)

    def editDateTime(self, index, date_time):
        """Edit the start or stop field in the frame stored at index."""
        date_time = local_arrow_from_str(date_time, 'YYYY-MM-DD HH:mm:ss')
        if index.column() == self.COLUMNS['start']:
            span = self.get_start_datetime_range(index)
            self.editFrame(index,
                           start=contraint_arrow_to_span(date_time, span))
        elif index.column() == self.COLUMNS['end']:
            span = self.get_stop_datetime_range(index)
            self.editFrame(index,
                           stop=contraint_arrow_to_span(date_time, span))
예제 #23
0
class SmartSpinBox(QDoubleSpinBox):
    """
    A spinbox that can act as a QSpinBox or QDoubleSpinBox that stores its
    value in an internal variable so that there is no loss in precision when
    the value of the spinbox is set programatically. In addition, the
    previous value of the spinbox is stored internally.

    The signal that is emitted when the value of the spinbox changes is also
    smarter than the one implemented in the QDoubleSpinBox. The signal also
    send the previous value in addition to the new value.

    Finally, it is allowed to enter values that are above or below the range
    of the spinbox when editing the value in the line edit. The value will
    be corrected to the maximum or minimum value once the editing is finished.
    """
    sig_value_changed = QSignal(float, float)

    def __init__(self,
                 val=0,
                 dec=0,
                 step=1,
                 units=None,
                 parent=None,
                 show_btns=True):
        super(SmartSpinBox, self).__init__(parent)
        if show_btns is False:
            self.setButtonSymbols(QDoubleSpinBox.NoButtons)
        self.setAlignment(Qt.AlignCenter)
        self.setKeyboardTracking(False)
        self.setAccelerated(True)

        self.__current_value = val
        self.__previous_value = 0
        self.setRange(0, 100)
        self.setDecimals(dec)
        self.setValue(val)
        if step is not None:
            self.setSingleStep(step)
        else:
            self.setSingleStep(10**-dec)
        if units is not None:
            self.setSuffix(units)

        self.editingFinished.connect(self.editValue)

    def keyPressEvent(self, event):
        """
        Qt method overrides to block certain key events when we want the
        spinbox to act as a QSpinBox instead of a QDoubleSpinBox.
        """
        if (event.key() in [Qt.Key_Comma, Qt.Key_Period]
                and self.decimals() == 0):
            event.accept()
        elif event.key() == Qt.Key_Minus and self.__min_value >= 0:
            event.accept()
        else:
            super(SmartSpinBox, self).keyPressEvent(event)

    def editValue(self):
        """
        Ensure that the value that was entered by editing the value of the
        spin box is within the range of values of the spinbox.
        """
        self.setValue(super(SmartSpinBox, self).value())

    def stepBy(self, n):
        """
        Qt method overrides to ensure the value remains within the
        range of values of the spinbox.
        """
        new_value = self.value() + n * self.singleStep()
        self.setValue(new_value)

    def value(self):
        """
        Qt method override that returns the value stocked in the internal
        variable instead of the one displayed in the UI.
        """
        return self.__current_value

    def previousValue(self):
        """
        Returns the previous value of the spinbox.
        """
        return self.__previous_value

    def setValue(self, new_value):
        """Qt method override to save the value in an internal variable."""
        new_value = max(min(new_value, self.__max_value), self.__min_value)
        self.blockSignals(True)
        super(SmartSpinBox, self).setValue(new_value)
        self.blockSignals(False)
        if new_value != self.__current_value:
            self.__previous_value = self.__current_value
            self.__current_value = new_value
            self.sig_value_changed.emit(self.__current_value,
                                        self.__previous_value)

    def setValueSilently(self, x):
        """
        Sets the value of the spinbox silently.
        """
        self.blockSignals(True)
        self.setValue(x)
        self.blockSignals(False)

    def setDecimals(self, x):
        """Qt method override to force a reset of the displayed range."""
        super(SmartSpinBox, self).setDecimals(x)
        self.setRange(self.__min_value, self.__max_value)

    def setRange(self, xmin, xmax):
        """Qt method override to save the range in internal variables."""
        if xmin > xmax:
            raise ValueError("xmin must be <= xmax")
        self.__max_value = xmax
        self.__min_value = xmin

        # Set the range of the spinbox so that its width is adjusted
        # correctly :
        lenght_int = int(np.ceil(np.log10(max(abs(xmax), abs(xmin))))) + 1
        max_edit = float('9' * lenght_int + '.' + '9' * self.decimals())
        super(SmartSpinBox, self).setRange(-max_edit, max_edit)

        self.setValue(super(SmartSpinBox, self).value())
예제 #24
0
class FormatedWatsonTableView(BasicWatsonTableView):
    """
    A BasicWatsonTableView formatted to look good when put in a scrollarea
    in a vertical stack of tables.
    """
    sig_focused_in = QSignal(object)
    _hovered_row = None

    def __init__(self, source_model, parent=None):
        super(FormatedWatsonTableView, self).__init__(source_model, parent)
        self.setup()
        self.update_table_height()
        self.entered.connect(self.itemEnterEvent)

    def setup(self):
        """Setup the table view with the provided arguments."""
        self.setAlternatingRowColors(False)
        self.setShowGrid(False)
        self.setFrameShape(QFrame.NoFrame)
        self.setWordWrap(False)

        self.setMouseTracking(True)
        self.setSelectionBehavior(self.SelectRows)
        self.setSelectionMode(self.SingleSelection)
        self.set_selected(False)

        self.horizontalHeader().hide()
        self.verticalHeader().hide()

        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        self.proxy_model.sig_sourcemodel_changed.connect(
            self.update_table_height)

    def update_table_height(self):
        """
        Update the height of the table to fit all the data, so that there is
        no need for a vertical scrollbar.
        """
        self.setFixedHeight(self.get_min_height())

    def get_min_height(self):
        """Calculate the height of the table content."""
        h = 2 * self.frameWidth()
        for i in range(self.model().get_accepted_row_count()):
            h += self.rowHeight(i)
        return h

    def set_date_span(self, date_span):
        """
        Method override to update table height when setting the date span.
        """
        super(FormatedWatsonTableView, self).set_date_span(date_span)
        self.update_table_height()

    def set_project_filters(self, project_filters):
        """
        Method override to update table height when setting the
        project filters.
        """
        super().set_project_filters(project_filters)
        self.update_table_height()

    def set_tag_filters(self, tag_filters):
        """
        Method override to update table height when setting the tag filters.
        """
        super().set_tag_filters(tag_filters)
        self.update_table_height()

    # ---- Row selection

    def set_selected(self, value):
        self.is_selected = bool(value)
        self.viewport().update()

    def get_selected_row(self):
        """
        Return the index of the selected row if there is one and return
        None otherwise.
        """
        selected_rows = self.selectionModel().selectedRows()
        if self.is_selected and len(selected_rows) > 0:
            return selected_rows[0].row()
        else:
            return None

    def get_selected_frame_index(self):
        """
        Return the index of the frame corresponding to the selected row if
        there is one, else return None.
        """
        if self.is_selected:
            selected_row = self.selectionModel().selectedRows()
            if len(selected_row) > 0:
                return self.proxy_model.mapToSource(selected_row[0]).row()
        return None

    # ---- Mouse hovered

    def set_hovered_row(self, row):
        if self._hovered_row != row:
            self._hovered_row = row
            self.viewport().update()

    def itemEnterEvent(self, index):
        self.set_hovered_row(index.row())

    def leaveEvent(self, event):
        super(FormatedWatsonTableView, self).leaveEvent(event)
        self.set_hovered_row(None)

    def focusOutEvent(self, event):
        super(FormatedWatsonTableView, self).focusOutEvent(event)
        self.set_hovered_row(None)
예제 #25
0
class NewDatasetDialog(QDialog):
    """
    A dialog window where water level and weather datasets can be imported
    into the project.
    """

    ConsoleSignal = QSignal(str)
    sig_new_dataset_imported = QSignal(str, object)

    DATATYPES = ['water level', 'daily weather']

    def __init__(self, datatype, parent=None, projet=None):
        super(NewDatasetDialog, self).__init__(parent)

        if datatype.lower() not in self.DATATYPES:
            raise ValueError("datatype value must be :", self.DATATYPES)
        self._datatype = datatype.lower()

        self.setWindowTitle('Import Dataset: %s' % datatype.title())
        self.setWindowIcon(icons.get_icon('master'))
        self.setWindowFlags(Qt.Window | Qt.CustomizeWindowHint
                            | Qt.WindowCloseButtonHint)

        self.set_projet(projet)
        self.workdir = os.path.dirname(os.getcwd())
        self._dataset = None

        self.__initUI__()
        if datatype == 'water level':
            warning = ('<i>Warning : Water levels must be in meter below '
                       'ground surface (mbgs)</i>')
            self.layout().addWidget(QLabel(warning), 4, 0)

    def __initUI__(self):

        # ---- Select Dataset

        self.directory = QLineEdit()
        self.directory.setReadOnly(True)
        self.directory.setMinimumWidth(400)

        self.btn_browse = QToolButtonSmall(icons.get_icon('openFile'))
        self.btn_browse.setToolTip('Select a datafile...')
        self.btn_browse.clicked.connect(self.select_dataset)

        url_i = "https://gwhat.readthedocs.io/en/latest/manage_data.html"
        msg = ("<font color=red size=2><i>"
               "The %s data file is not formatted correctly.<br>"
               "Please consult the <a href=\"%s\">documentation</a>"
               " for detailed information<br>"
               "on how to format your input data files correctly."
               "</i></font>") % (self._datatype.capitalize(), url_i)
        self._error_lbl = QLabel(msg)
        self._error_lbl.setVisible(False)
        self._error_lbl.setOpenExternalLinks(True)

        # Select Dataset Layout

        grp_dset = QGridLayout()
        row = 0
        text = "Select a valid %s datafile :" % self._datatype.lower()
        grp_dset.addWidget(QLabel(text), row, 0, 1, 3)
        row += 1
        grp_dset.addWidget(QLabel("File name :"), row, 0)
        grp_dset.addWidget(self.directory, row, 1)
        grp_dset.addWidget(self.btn_browse, row, 3)
        row += 1
        grp_dset.addWidget(self._error_lbl, row, 1, 1, 3)

        grp_dset.setContentsMargins(0, 0, 0, 15)
        grp_dset.setColumnStretch(1, 100)
        grp_dset.setVerticalSpacing(15)

        # ----- Station Info Groupbox

        self._stn_name = QLineEdit()
        self._stn_name.setAlignment(Qt.AlignCenter)

        self._sid = QLineEdit()
        self._sid.setAlignment(Qt.AlignCenter)

        self._lat = myqt.QDoubleSpinBox(0, 3, 0.1, ' °')
        self._lat.setRange(-180, 180)

        self._lon = myqt.QDoubleSpinBox(0, 3, 0.1, ' °')
        self._lon.setRange(-180, 180)

        self._alt = myqt.QDoubleSpinBox(0, 3, 0.1, ' m')
        self._alt.setRange(-9999, 9999)

        self._prov = QLineEdit()
        self._prov.setAlignment(Qt.AlignCenter)

        # Info Groubox Layout

        self.grp_info = QGroupBox("Dataset info :")
        self.grp_info.setEnabled(False)
        self.grp_info.setLayout(QGridLayout())
        self.grp_info.layout().setColumnStretch(2, 100)
        self.grp_info.layout().setSpacing(10)

        if self._datatype == 'water level':
            labels = [
                'Well name :', 'Well ID :', 'Latitude :', 'Longitude :',
                'Altitude :', 'Province :'
            ]
        else:
            labels = [
                'Station name :', 'Station ID :', 'Latitude :', 'Longitude :',
                'Altitude :', 'Location :'
            ]
        widgets = [
            self._stn_name, self._sid, self._lat, self._lon, self._alt,
            self._prov
        ]
        for label, widget in zip(labels, widgets):
            self._add_info_field(label, widget)

        # ----- Toolbar

        self._dset_name = QLineEdit()
        self._dset_name.setEnabled(False)

        self.btn_ok = QPushButton('Import')
        self.btn_ok.setMinimumWidth(100)
        self.btn_ok.setEnabled(False)
        self.btn_ok.clicked.connect(self.accept_dataset)

        btn_cancel = QPushButton('Cancel')
        btn_cancel.setMinimumWidth(100)
        btn_cancel.clicked.connect(self.close)

        # Tool layout

        toolbar = QGridLayout()

        toolbar.addWidget(QLabel('Dataset name :'), 0, 0)
        toolbar.addWidget(self._dset_name, 0, 1)
        toolbar.addWidget(self.btn_ok, 0, 3)
        toolbar.addWidget(btn_cancel, 0, 4)

        toolbar.setSpacing(10)
        toolbar.setColumnStretch(2, 100)
        toolbar.setContentsMargins(0, 15, 0, 0)  # (L, T, R, B)

        # ---- Main Layout

        layout = QGridLayout(self)

        layout.addLayout(grp_dset, 0, 0)
        layout.addWidget(self.grp_info, 1, 0)
        layout.addLayout(toolbar, 2, 0)

        layout.setRowMinimumHeight(3, 15)
        layout.setRowStretch(10, 100)
        layout.setColumnStretch(0, 100)

    def _add_info_field(self, label, widget):
        """Add a new field to the Station Info group box."""
        layout = self.grp_info.layout()
        row = layout.rowCount()
        layout.addWidget(QLabel(label), row, 0)
        layout.addWidget(widget, row, 1)

    # ---- Properties

    @property
    def projet(self):
        return self._projet

    def set_projet(self, projet):
        if projet is None:
            self._projet = None
        else:
            self._projet = projet
            self.workdir = os.path.dirname(projet.filename)

    @property
    def name(self):
        """Name that will be use to reference the dataset in the project."""
        return self._dset_name.text()

    @property
    def station_name(self):
        """Common name of the climate or piezomatric station."""
        return self._stn_name.text()

    @property
    def station_id(self):
        """Unique identifier of the climate or piezomatric station."""
        return self._sid.text()

    @property
    def province(self):
        """Province where the station is located."""
        return self._prov.text()

    @property
    def latitude(self):
        """Latitude in decimal degree of the station location."""
        return self._lat.value()

    @property
    def longitude(self):
        """Longitude in decimal degree of the station location."""
        return self._lon.value()

    @property
    def altitude(self):
        """Elevation of the station in meters above see level."""
        return self._alt.value()

    # ---- Dataset Handlers

    def select_dataset(self):
        """Opens a dialog to select a single datafile."""

        if self._datatype == 'water level':
            exts = '(*.csv;*.xls;*.xlsx)'
        elif self._datatype == 'daily weather':
            exts = '(*.csv;*.out;*.xls;*.xlsx)'
        filename, _ = QFileDialog.getOpenFileName(
            self, 'Select a %s data file' % self._datatype, self.workdir, exts)

        for i in range(5):
            QCoreApplication.processEvents()

        if filename:
            self.workdir = os.path.dirname(filename)
            self.load_dataset(filename)

    def load_dataset(self, filename):
        """Load the dataset and display the information in the UI."""
        if not osp.exists(filename):
            print('Path does not exist. Cannot open %s.' % filename)
            return

        # Load the Data :

        QApplication.setOverrideCursor(Qt.WaitCursor)
        self.ConsoleSignal.emit("<font color=black>Loading %s data...</font>" %
                                self._datatype)
        for i in range(5):
            QCoreApplication.processEvents()

        try:
            if self._datatype == 'water level':
                self._dataset = WLDataFrame(filename)
            elif self._datatype == 'daily weather':
                self._dataset = WXDataFrame(filename)
        except Exception as e:
            print(e)
            self._dataset = None

        self.update_gui(filename)
        QApplication.restoreOverrideCursor()

    def update_gui(self, filename=None):
        """
        Display the values stored in the dataset. Disable the UI and show
        an error message if the dataset is not valid.
        """
        if filename is not None:
            self.directory.setText(filename)
        else:
            self.directory.clear()

        if self._dataset is None:
            self._dset_name.clear()
            self._stn_name.clear()
            self._prov.clear()
            self._lat.setValue(0)
            self._lon.setValue(0)
            self._alt.setValue(0)
            self._sid.clear()
        else:
            if self._datatype == 'water level':
                self._prov.setText(self._dataset['Province'])
                self._lat.setValue(self._dataset['Latitude'])
                self._lon.setValue(self._dataset['Longitude'])
                self._alt.setValue(self._dataset['Elevation'])
                self._stn_name.setText(self._dataset['Well'])
                self._sid.setText(self._dataset['Well ID'])
                dsetname = self._dataset['Well']
            elif self._datatype == 'daily weather':
                self._prov.setText(self._dataset.metadata['Location'])
                self._lat.setValue(self._dataset.metadata['Latitude'])
                self._lon.setValue(self._dataset.metadata['Longitude'])
                self._alt.setValue(self._dataset.metadata['Elevation'])
                self._stn_name.setText(self._dataset.metadata['Station Name'])
                self._sid.setText(self._dataset.metadata['Station ID'])
                dsetname = self._dataset.metadata['Station Name']
            # We replace the invalid characters to avoid problems when
            # saving the dataset to the hdf5 format.
            for char in INVALID_CHARS:
                dsetname = dsetname.replace(char, '_')
            self._dset_name.setText(dsetname)

        self._error_lbl.setVisible(self._dataset is None
                                   and self.directory.text() != '')
        self.btn_ok.setEnabled(self._dataset is not None)
        self.grp_info.setEnabled(self._dataset is not None)
        self._dset_name.setEnabled(self._dataset is not None)

    def accept_dataset(self):
        """Accept and emit the dataset."""
        if not is_dsetname_valid(self.name):
            msg = ('''
                   <p>Please enter a valid name for the dataset.</p>
                   <p>A dataset name must be at least one charater long
                   and can't contain any of the following special
                   characters:</p>
                   <center>\\ / : * ? " < > |</center>
                   ''')
            btn = QMessageBox.Ok
            QMessageBox.warning(self, 'Save dataset', msg, btn)
            return

        if self._datatype == 'water level':
            is_dsetname_exists = self.name in self.projet.wldsets
            del_dset = self.projet.del_wldset
        elif self._datatype == 'daily weather':
            is_dsetname_exists = self.name in self.projet.wxdsets
            del_dset = self.projet.del_wxdset

        if is_dsetname_exists:
            msg = ('The dataset <i>%s</i> already exists.'
                   ' Do you want to replace the existing dataset?'
                   ' All data will be lost.') % self.name
            btn = QMessageBox.Yes | QMessageBox.No
            reply = QMessageBox.question(self, 'Save dataset', msg, btn)
            if reply == QMessageBox.No:
                return
            else:
                del_dset(self.name)

        # Update dataset attributes from UI and emit dataset.
        if self._datatype == 'water level':
            self._dataset['Well'] = self.station_name
            self._dataset['Well ID'] = self.station_id
            self._dataset['Province'] = self.province
            self._dataset['Latitude'] = self.latitude
            self._dataset['Longitude'] = self.longitude
            self._dataset['Elevation'] = self.altitude
        elif self._datatype == 'daily weather':
            self._dataset.metadata['Station Name'] = self.station_name
            self._dataset.metadata['Station ID'] = self.station_id
            self._dataset.metadata['Location'] = self.province
            self._dataset.metadata['Latitude'] = self.latitude
            self._dataset.metadata['Longitude'] = self.longitude
            self._dataset.metadata['Elevation'] = self.altitude
        self.hide()
        self.sig_new_dataset_imported.emit(self.name, self._dataset)
        self.close()

    # ---- Display Handlers

    def close(self):
        """Qt method override."""
        super(NewDatasetDialog, self).close()
        self._dataset = None
        self.directory.clear()
        self.update_gui()
예제 #26
0
class RechgEvalWorker(QObject):

    sig_glue_progress = QSignal(float)
    sig_glue_finished = QSignal(object)

    def __init__(self):
        super(RechgEvalWorker, self).__init__()
        self.wxdset = None
        self.ETP, self.PTOT, self.TAVG = [], [], []

        self.wldset = None
        self.A, self.B = None, None
        self.twlvl = []
        self.wlobs = []

        self.TMELT = 0
        self.CM = 4
        self.deltat = 0

        self.Sy = (0, 1)
        self.Cro = (0, 1)
        self.RASmax = (0, 150)

        self.glue_pardist_res = 'fine'

    @property
    def language(self):
        return self.__language

    @language.setter
    def language(self, x):
        if x.lower() in ['french', 'english']:
            self.__language = x
        else:
            raise NameError('Language must be either French or English.')

    @property
    def CM(self):
        return self.__CM

    @CM.setter
    def CM(self, x):
        if x > 0:
            self.__CM = x
        else:
            raise ValueError('CM must be greater than 0.')

    @property
    def TMELT(self):
        return self.__TMELT

    @TMELT.setter
    def TMELT(self, x):
        self.__TMELT = x

    def load_data(self, wxdset, wldset):
        # Setup weather data.

        self.wxdset = wxdset
        self.ETP = self.wxdset['PET']
        self.PTOT = self.wxdset['Ptot']
        self.TAVG = self.wxdset['Tavg']
        self.tweatr = self.wxdset['Time'] + self.deltat
        # We introduce a time lag here to take into account the travel time
        # through the unsaturated zone.

        # Setup water level data.

        self.wldset = wldset
        self.A, self.B = wldset['mrc/params']
        self.twlvl, self.wlobs = self.make_data_daily(wldset['Time'],
                                                      wldset['WL'])

        if not self.A and not self.B:
            error = ("Groundwater recharge cannot be computed because a"
                     " master recession curve (MRC) must be defined first.")
            return error

        # Clip the observed water level time series to the weather data.
        self.twlvl, self.wlobs = clip_time_series(
            self.tweatr, self.twlvl, self.wlobs)

        if len(self.twlvl) == 0:
            # The wldset and wxdset are not mutually exclusive.
            error = ("Groundwater recharge cannot be computed because the"
                     " water level and weather datasets are mutually"
                     " exclusive in time.")
            return error
        else:
            return None

    def make_data_daily(self, t, h):
        """
        Convert a given time series to a daily basis. Only the last water level
        measurements made on a given day is kept in the daily time series.
        If there is no measurement at all for a given day, the default nan
        value is kept instead in the daily time series.
        """
        argsort = np.argsort(t)
        t = np.floor(t[argsort])
        h = h[argsort]

        td = np.arange(np.min(t), np.max(t)+1, 1).astype(int)
        hd = np.ones(len(td)) * np.nan
        for i in range(len(td)):
            indx = np.where(t == td[i])[0]
            if len(indx) > 0:
                hd[i] = h[indx[-1]]

        return td, hd

    def produce_params_combinations(self):
        """
        Produce a set of parameter combinations (RASmax + Cro) from the ranges
        provided by the user using a flat distribution.
        """
        if self.glue_pardist_res == 'rough':
            U_RAS = np.arange(self.RASmax[0], self.RASmax[1]+1, 5)
        elif self.glue_pardist_res == 'fine':
            U_RAS = np.arange(self.RASmax[0], self.RASmax[1]+1, 1)
        U_Cro = np.arange(self.Cro[0], self.Cro[1]+0.01, 0.01)

        return U_RAS, U_Cro

    def eval_recharge(self):
        """
        Produce a set of behavioural models that all represent the observed
        data equiprobably and evaluate the water budget with GLUE for diffrent
        GLUE uncertainty limits.
        """

        U_RAS, U_Cro = self.produce_params_combinations()

        # Find the indexes to align the water level with the weather data
        # daily time series.

        ts = np.where(self.twlvl[0] == self.tweatr)[0][0]
        te = np.where(self.twlvl[-1] == self.tweatr)[0][0]

        # ---- Produce realizations

        set_RMSE = []

        set_Sy = []
        set_RASmax = []
        set_Cru = []

        sets_waterlevels = []
        set_recharge = []
        set_runoff = []
        set_evapo = []

        Sy0 = np.mean(self.Sy)
        time_start = time.clock()
        N = sum(1 for p in product(U_Cro, U_RAS))
        self.sig_glue_progress.emit(0)
        for it, (cro, rasmax) in enumerate(product(U_Cro, U_RAS)):
            rechg, ru, etr, ras, pacc = self.surf_water_budget(cro, rasmax)
            SyOpt, RMSE, wlvlest = self.optimize_specific_yield(
                    Sy0, self.wlobs*1000, rechg[ts:te])
            Sy0 = SyOpt

            if SyOpt >= min(self.Sy) and SyOpt <= max(self.Sy):
                set_RMSE.append(RMSE)
                set_recharge.append(rechg)
                sets_waterlevels.append(wlvlest)
                set_Sy.append(SyOpt)
                set_RASmax.append(rasmax)
                set_Cru.append(cro)
                set_evapo.append(etr)
                set_runoff.append(ru)

            self.sig_glue_progress.emit((it+1)/N*100)
            print(('Cru = %0.3f ; RASmax = %0.0f mm ; Sy = %0.4f ; ' +
                   'RMSE = %0.1f') % (cro, rasmax, SyOpt, RMSE))

        print("GLUE computed in : %0.1f s" % (time.clock()-time_start))
        self._print_model_params_summary(set_Sy, set_Cru, set_RASmax)

        # ---- Format results

        glue_rawdata = {}
        glue_rawdata['count'] = len(set_RMSE)
        glue_rawdata['RMSE'] = set_RMSE
        glue_rawdata['params'] = {'Sy': set_Sy,
                                  'RASmax': set_RASmax,
                                  'Cru': set_Cru,
                                  'tmelt': self.TMELT,
                                  'CM': self.CM,
                                  'deltat': self.deltat}
        glue_rawdata['ranges'] = {'Sy': self.Sy,
                                  'Cro': self.Cro,
                                  'RASmax': self.RASmax}

        glue_rawdata['water levels'] = {}
        glue_rawdata['water levels']['time'] = self.twlvl
        glue_rawdata['water levels']['observed'] = self.wlobs

        glue_rawdata['Weather'] = {'Tmax': self.wxdset['Tmax'],
                                   'Tmin': self.wxdset['Tmin'],
                                   'Tavg': self.wxdset['Tavg'],
                                   'Ptot': self.wxdset['Ptot'],
                                   'Rain': self.wxdset['Rain'],
                                   'PET': self.wxdset['PET']}

        # Save the water levels simulated with the mrc, as well as and values
        # of the parameters that characterized this mrc.
        glue_rawdata['mrc'] = {}
        glue_rawdata['mrc']['params'] = self.wldset['mrc/params']
        glue_rawdata['mrc']['time'] = self.wldset['mrc/time']
        glue_rawdata['mrc']['levels'] = self.wldset['mrc/recess']

        # Store the models output that will need to be processed with GLUE.

        glue_rawdata['hydrograph'] = sets_waterlevels
        glue_rawdata['recharge'] = set_recharge
        glue_rawdata['etr'] = set_evapo
        glue_rawdata['ru'] = set_runoff
        glue_rawdata['Time'] = self.wxdset['Time']
        glue_rawdata['Year'] = self.wxdset['Year']
        glue_rawdata['Month'] = self.wxdset['Month']
        glue_rawdata['Day'] = self.wxdset['Day']

        # Save infos about the piezometric station.

        keys = ['Well', 'Well ID', 'Province', 'Latitude', 'Longitude',
                'Elevation', 'Municipality']
        glue_rawdata['wlinfo'] = {k: self.wldset[k] for k in keys}

        # Save infos about the weather station.

        keys = ['Station Name', 'Climate Identifier', 'Province', 'Latitude',
                'Longitude', 'Elevation']
        glue_rawdata['wxinfo'] = {k: self.wxdset[k] for k in keys}

        # Calcul GLUE from the set of behavioural model and send the results
        # with a signal so that it can be handled on the UI side.

        if glue_rawdata['count'] > 0:
            glue_dataf = GLUEDataFrame(glue_rawdata)
            # self._save_glue_to_npy(glue_rawdata)
        else:
            glue_dataf = None
        self.sig_glue_finished.emit(glue_dataf)

        return glue_dataf

    def _print_model_params_summary(self, set_Sy, set_Cru, set_RASmax):
        """
        Print a summary of the range of parameter values that were used to
        produce the set of behavioural models.
        """
        print('-'*78)
        if len(set_Sy) > 0:
            print('-'*78)
            print('%d behavioural models were produced' % len(set_Sy))
            range_sy = (np.min(set_Sy), np.max(set_Sy))
            print('range Sy = %0.3f to %0.3f' % range_sy)
            range_rasmax = (np.min(set_RASmax), np.max(set_RASmax))
            print('range RASmax = %d to %d' % range_rasmax)
            range_cru = (np.min(set_Cru), np.max(set_Cru))
            print('range Cru = %0.3f to %0.3f' % range_cru)
            print('-'*78)
        else:
            print("The number of behavioural model produced is 0.")

    def _save_glue_to_npy(self, glue_rawdata):
        """Save the last computed glue results in a numpy npy file."""
        if not osp.exists(osp.dirname(__file__)):
            os.makedirs(osp.dirname(__file__))
        filename = osp.join(osp.dirname(__file__), 'glue_rawdata.npy')
        np.save(filename, glue_rawdata)

    def optimize_specific_yield(self, Sy0, wlobs, rechg):
        """
        Find the optimal value of Sy that minimizes the RMSE between the
        observed and predicted ground-water hydrographs. The observed water
        level (wlobs) and simulated recharge (rechg) time series must be
        in mm and be properly align in time.
        """
        nonan_indx = np.where(~np.isnan(wlobs))

        # ---- Gauss-Newton

        tolmax = 0.001
        Sy = Sy0
        dSy = 0.01

        wlpre = self.calc_hydrograph(rechg, Sy)
        RMSE = calcul_rmse(wlobs[nonan_indx], wlpre[nonan_indx])

        it = 0
        while 1:
            it += 1
            if it > 100:
                print('Not converging.')
                break

            # Calculating Jacobian (X) Numerically.
            wlvl = self.calc_hydrograph(rechg, Sy * (1+dSy))
            X = Xt = (wlvl[nonan_indx] - wlpre[nonan_indx])/(Sy*dSy)

            # Solving Linear System.
            dh = wlobs[nonan_indx] - wlpre[nonan_indx]
            XtX = np.dot(Xt, X)
            Xtdh = np.dot(Xt, dh)

            dr = np.linalg.tensorsolve(XtX, Xtdh, axes=None)

            # Storing old parameter values.
            Syold = np.copy(Sy)
            RMSEold = np.copy(RMSE)

            # Loop for Damping (to prevent overshoot)
            while 1:
                # Calculating new paramter values.
                Sy = Syold + dr

                # Solving for new parameter values.
                wlpre = self.calc_hydrograph(rechg, Sy)
                RMSE = calcul_rmse(wlobs[nonan_indx], wlpre[nonan_indx])

                # Checking overshoot.
                if (RMSE - RMSEold) > 0.1:
                    dr = dr * 0.5
                else:
                    break

            # Checking tolerance.
            tol = np.abs(Sy - Syold)
            if tol < tolmax:
                return Sy, RMSE, wlpre

    def surf_water_budget(self, CRU, RASmax):
        """
        Compute recharge with a daily soil surface moisture balance model.

        CRU = Surface runoff coefficient
        RASmax = Maximum readily available storage in mm
        ETP = Dailty potential evapotranspiration in mm
        PTOT = Daily total precipitation in mm
        TAVG = Daily average air temperature in deg. C.
        CM = Daily melt coefficient
        TMELT = Temperature treshold for snowmelt

        rechg = Daily groundwater recharge in mm
        etr = Daily real evapotranspiration in mm
        ru = Daily surface runoff in mm
        ras = Daily readily available storage in mm
        pacc = Daily accumulated precipitation on the ground surface in mm
        """
        rechg, ru, etr, ras, pacc = calcul_surf_water_budget(
                self.ETP, self.PTOT, self.TAVG, self.TMELT,
                self.CM,  CRU, RASmax)

        return rechg, ru, etr, ras, pacc

    def calc_hydrograph(self, RECHG, Sy, nscheme='forward'):
        """
        This is a forward numerical explicit scheme for generating the
        synthetic well hydrograph.

        This is a backward explicit scheme to produce a synthetic well
        hydrograph. The water level starts at the last days in the observed
        water level time series and generate the hydrograph by going backward
        in time. This is very usefull when one which to produce water level
        for the period of time before water level measurements are
        available.

        Parameters
        ----------
        Wlpre: Predicted Water Level (mm)
        Sy: Specific Yield
        RECHG: Groundwater Recharge (mm)
        WLobs: Observed Water Level (mm)

        A, B: MRC Parameters, where: Recess(m/d) = -A * h + B
        nscheme: Option are "forward" or "downdward" depending if the
                 hydrograph is being built forward in time or backward.
                 Default is "forward".
        """

        # TODO: It should also be possible to do a Crank-Nicholson on this.
        # I should check this out.

        A, B = self.A, self.B
        wlobs = self.wlobs*1000
        if nscheme == 'backward':
            wlpre = np.zeros(len(RECHG)+1) * np.nan
            wlpre[0] = wlobs[-1]
            for i in reversed(range(len(RECHG))):
                RECESS = (B - A * wlpre[i] / 1000.) * 1000
                RECESS = max(RECESS, 0)

                wlpre[i] = wlpre[i+1] + (RECHG[i] / Sy) - RECESS
        elif nscheme == 'forward':
            wlpre = calc_hydrograph_forward(RECHG, wlobs, Sy, self.A, self.B)
        else:
            wlpre = []

        return wlpre

    @staticmethod
    def mrc2rechg(t, hobs, A, B, z, Sy):

        """
        Calculate groundwater recharge from the Master Recession Curve (MRC)
        equation defined by the parameters A and B, the water level time series
        in mbgs (t and ho) and the soil column description (z and Sy), using
        the water-level fluctuation principle.

        INPUTS
        ------
        {1D array} t : Time in days
        {1D array} hobs = Observed water level in mbgs
        {float}    A = Model parameter of the MRC
        {float}    B = Model parameter of the MRC
        {1D array} z = Depth of the soil layer limits
        {1D array} Sy = Specific yield for each soil layer
        {1D array} indx = Time index defining the periods over which recharge
                          is to be computed. Odd index numbers are for the
                          beginning of periods while even index numbers are for
                          the end of periods.

        OUTPUTS
        -------
        {1D array} RECHG = Groundwater recharge time series in m

        Note: This is documented in logbook #11, p.23.
        """

        # ---- Check Data Integrity ----

        if np.min(hobs) < 0:
            print('Water level rise above ground surface.' +
                  ' Please check your data.')
            return

        dz = np.diff(z)  # Tickness of soil layer

        dt = np.diff(t)
        RECHG = np.zeros(len(dt))

        # !Do not forget it is mbgs. Everything is upside down!

        for i in range(len(dt)):

            # Calculate projected water level at i+1

            LUMP1 = 1 - A * dt[i] / 2
            LUMP2 = B * dt[i]
            LUMP3 = (1 + A * dt[i] / 2) ** -1

            hp = (LUMP1 * hobs[i] + LUMP2) * LUMP3

            # Calculate resulting recharge over dt (See logbook #11, p.23)

            hup = min(hp, hobs[i+1])
            hlo = max(hp, hobs[i+1])

            iup = np.where(hup >= z)[0][-1]
            ilo = np.where(hlo >= z)[0][-1]

            RECHG[i] = np.sum(dz[iup:ilo+1] * Sy[iup:ilo+1])
            RECHG[i] -= (z[ilo+1] - hlo) * Sy[ilo]
            RECHG[i] -= (hup - z[iup]) * Sy[iup]

            # RECHG[i] will be positive in most cases. In theory, it should
            # always be positive, but error in the MRC and noise in the data
            # can cause hp to be above ho in some cases.

            RECHG[i] *= np.sign(hp - hobs[i+1])

        return RECHG
예제 #27
0
class ActivityOverviewWidget(QWidget):
    """A widget to show and edit activities logged with Watson."""
    sig_add_activity = QSignal(int, arrow.Arrow, arrow.Arrow)
    sig_del_activity = QSignal(int)
    sig_load_settings = QSignal(object)

    def __init__(self, model, parent=None):
        super(ActivityOverviewWidget, self).__init__(parent)
        self.setWindowIcon(icons.get_icon('master'))
        self.setWindowTitle("Activity Overview")

        self.model = model
        self.model.sig_btn_delrow_clicked.connect(self.del_activity)

        self.setup(model)
        self.filter_btn.tags_menu.setup_menu_items()
        self.filter_btn.projects_menu.setup_menu_items()
        self.date_span_changed()

    def setup(self, model):
        """Setup the widget with the provided arguments."""
        self.table_widg = WatsonMultiTableWidget(model, parent=self)
        self.toolbar = self.setup_toolbar()

        # ---- Setup the layout

        layout = QGridLayout(self)
        layout.addWidget(self.toolbar)
        layout.addWidget(self.table_widg)

    def setup_toolbar(self):
        """Setup the toolbar of the widget."""
        self.date_range_nav = DateRangeNavigator()
        self.date_range_nav.sig_date_span_changed.connect(
            self.date_span_changed)

        self.add_act_above_btn = QToolButtonBase('insert_above', 'small')
        self.add_act_above_btn.setToolTip(
            "<b>Add Activity Above</b><br><br>"
            "Add a new activity directly above the currently selected"
            " activity. If no activity is selected, the new activity will"
            " be added on the first day of the week.")
        self.add_act_above_btn.clicked.connect(
            lambda: self.add_new_activity('above'))

        self.add_act_below_btn = QToolButtonBase('insert_below', 'small')
        self.add_act_below_btn.setToolTip(
            "<b>Add Activity Below</b><br><br>"
            "Add a new activity directly below the currently selected"
            " activity. If no activity is selected, the new activity will"
            " be added on the last day of the week.")
        self.add_act_below_btn.clicked.connect(
            lambda: self.add_new_activity('below'))

        self.btn_load_row_settings = QToolButtonBase('copy_over', 'small')
        self.btn_load_row_settings.setToolTip(
            "<b>Setup Mainwindow from Activity</b><br><br>"
            "Set the project, tags, and comment in the mainwindow"
            " to the values of the currently selected activity.")
        self.btn_load_row_settings.clicked.connect(
            lambda: self.sig_load_settings.emit(self.table_widg.selectedFrame(
            )))

        self.filter_btn = FilterButton(self.model.client)
        self.filter_btn.setToolTip(
            "<b>Setup Activity Filters</b><br><br>"
            "Set filters to show activities only for selected "
            "tags and projects in the overview table.")
        self.filter_btn.sig_projects_checkstate_changed.connect(
            self.table_widg.set_project_filters)
        self.filter_btn.sig_tags_checkstate_changed.connect(
            self.table_widg.set_tag_filters)

        # Setup the layout.

        toolbar = ToolBarWidget()
        toolbar.setSpacing(1)

        toolbar.addWidget(self.date_range_nav)
        toolbar.addStretch(100)
        toolbar.addWidget(self.btn_load_row_settings)
        toolbar.addWidget(self.add_act_above_btn)
        toolbar.addWidget(self.add_act_below_btn)
        toolbar.addWidget(self.filter_btn)

        return toolbar

    def date_span_changed(self):
        """Handle when the range of the date range navigator widget change."""
        self.table_widg.set_date_span(self.date_range_nav.current)

    def show(self):
        """Qt method override to restore the window when minimized."""
        self.table_widg.scrollarea.widget().hide()
        # We show and hide the table_widg scrollarea to avoid flickering.

        self.setWindowState((self.windowState()
                             & ~(Qt.WindowMinimized | Qt.WindowFullScreen))
                            | Qt.WindowActive)
        self.setAttribute(Qt.WA_Resized, True)
        # Setting the Qt.WA_Resized attribute to True is required or else the
        # size of the wigdet will not be updated correctly when restoring the
        # window from a maximized state and the layout won't be expanded
        # correctly to the full size of the widget.
        # See Issue #58 and PR #67.

        super(ActivityOverviewWidget, self).show()
        self.raise_()
        self.setFocus()

        self.table_widg.scrollarea.widget().show()
        # We show and hide the table_widg scrollarea to avoid flickering.

    def add_new_activity(self, where):
        """
        Send a signal containing the index, the start time, and stop time of
        the new activity that needs to be added to Watson's frames.
        The QWatson mainwindow is in charge of actually adding the activity
        to Watson's frames. The argument 'where' indicates wether the new
        activity is to be added above or below the selected row.
        """
        index, time = self.table_widg.get_new_activity_index_and_time(where)
        self.sig_add_activity.emit(index, time, time)

    def del_activity(self, index):
        """
        Ask for confirmation to delete a row and delete or not the row from
        the model according the answer.
        """
        frame = self.model.client.frames[index.row()]
        ans = QMessageBox.question(self,
                                   'Delete frame',
                                   "Do you want to delete frame %s?" %
                                   frame.id,
                                   defaultButton=QMessageBox.No)
        if ans == QMessageBox.Yes:
            self.sig_del_activity.emit(index.row())

            # If the activity that was deleted was selected and was the
            # last activity in the WatsonTableWidget, we need to clear the
            # focus of the WatsonMultiTableWidget.
            table = self.table_widg.last_focused_table
            if table is not None and table.rowCount() == 0:
                self.table_widg.clear_focused_table()
예제 #28
0
class DataGapfillManager(TaskManagerBase):
    sig_task_progress = QSignal(int)
    sig_status_message = QSignal(str)

    def __init__(self):
        super().__init__()
        worker = DataGapfillWorker()
        self.set_worker(worker)
        worker.sig_task_progress.connect(self.sig_task_progress.emit)
        worker.sig_status_message.connect(self.sig_status_message.emit)

    def count(self):
        """
        Return the number of datasets that are currently loaded in the
        gapfill data worker.
        """
        return self.worker().wxdatasets.count()

    def get_station_names(self):
        """
        Return the list of station names for which data are loaded in memory.
        """
        return self.worker().wxdatasets.station_names

    def get_station_ids(self):
        """
        Return the list of station IDs for which data are loaded in memory.
        """
        return self.worker().wxdatasets.station_ids

    def set_workdir(self, workdir):
        self.worker().inputDir = workdir

    def set_target_station(self,
                           station_id,
                           callback=None,
                           postpone_exec=False):
        """
        Set the target station to the station corresponding
        to the specified station id.

        Setting the target station also trigger the recalculation of the
        correlation coefficients with the neighboring stations.
        """
        self.add_task('set_target_station',
                      callback=callback,
                      station_id=station_id)
        if not postpone_exec:
            self.run_tasks()

    def load_data(self,
                  force_reload=False,
                  callback=None,
                  postpone_exec=False):
        """Read the csv files in the input data directory folder."""
        self.add_task('load_data',
                      force_reload=force_reload,
                      callback=callback)
        if not postpone_exec:
            self.run_tasks()

    def gapfill_data(self,
                     time_start,
                     time_end,
                     max_neighbors,
                     hdist_limit,
                     vdist_limit,
                     regression_mode,
                     callback=None,
                     postpone_exec=False):
        """Gapfill the data of the target station."""
        self.add_task('gapfill_data',
                      callback=callback,
                      time_start=time_start,
                      time_end=time_end,
                      max_neighbors=max_neighbors,
                      hdist_limit=hdist_limit,
                      vdist_limit=vdist_limit,
                      regression_mode=regression_mode)
        if not postpone_exec:
            self.run_tasks()
예제 #29
0
class PageSetupWin(QWidget):

    newPageSetupSent = QSignal(bool)

    def __init__(self, parent=None):
        super(PageSetupWin, self).__init__(parent)

        self.setWindowTitle('Page and Figure Setup')
        self.setWindowIcon(icons.get_icon('master'))
        self.setWindowFlags(Qt.Window | Qt.CustomizeWindowHint
                            | Qt.WindowCloseButtonHint)

        # ---- Default Values ----

        self.pageSize = (11, 7)
        self.isLegend = True
        self.isGraphTitle = True
        self.isTrendLine = False
        self.is_meteo_on = True
        self.is_glue_wl_on = False
        self.is_mrc_wl_on = False
        self.va_ratio = 0.2
        self.figframe_lw = 0

        self.__initUI__()

    def __initUI__(self):

        # ---- Toolbar ----

        toolbar_widget = QWidget()

        self.btn_apply = btn_apply = QPushButton('Apply')
        btn_apply.clicked.connect(self.btn_apply_isClicked)
        self.btn_cancel = btn_cancel = QPushButton('Cancel')
        btn_cancel.clicked.connect(self.close)
        self.btn_OK = btn_OK = QPushButton('OK')
        btn_OK.clicked.connect(self.btn_OK_isClicked)

        toolbar_layout = QGridLayout()
        toolbar_layout.addWidget(btn_OK, 0, 1)
        toolbar_layout.addWidget(btn_cancel, 0, 2)
        toolbar_layout.addWidget(btn_apply, 0, 3)
        toolbar_layout.setColumnStretch(0, 100)

        toolbar_widget.setLayout(toolbar_layout)

        # ---- Main Layout

        main_layout = QGridLayout()
        main_layout.addWidget(self._setup_figure_layout_grpbox(), 0, 0)
        main_layout.addWidget(self._setup_element_visibility_grpbox(), 1, 0)
        main_layout.setRowStretch(2, 100)
        main_layout.setRowMinimumHeight(2, 15)
        main_layout.addWidget(toolbar_widget, 3, 0)

        self.setLayout(main_layout)

    def _setup_figure_layout_grpbox(self):
        """
        Setup a groupbox containing various widget to control the layout
        of the figure.
        """
        self.fwidth = QDoubleSpinBox()
        self.fwidth.setSingleStep(0.05)
        self.fwidth.setMinimum(5.)
        self.fwidth.setValue(self.pageSize[0])
        self.fwidth.setSuffix('  in')
        self.fwidth.setAlignment(Qt.AlignCenter)
        self.fwidth.label = "Figure Width"

        self.fheight = QDoubleSpinBox()
        self.fheight.setSingleStep(0.05)
        self.fheight.setMinimum(5.)
        self.fheight.setValue(self.pageSize[1])
        self.fheight.setSuffix('  in')
        self.fheight.setAlignment(Qt.AlignCenter)
        self.fheight.label = "Figure Heigh"

        self.va_ratio_spinBox = QDoubleSpinBox()
        self.va_ratio_spinBox.setSingleStep(0.01)
        self.va_ratio_spinBox.setMinimum(0.1)
        self.va_ratio_spinBox.setMaximum(0.95)
        self.va_ratio_spinBox.setValue(self.va_ratio)
        self.va_ratio_spinBox.setAlignment(Qt.AlignCenter)
        self.va_ratio_spinBox.label = "Top/Bottom Axes Ratio"

        self.fframe_lw_widg = QDoubleSpinBox()
        self.fframe_lw_widg.setSingleStep(0.1)
        self.fframe_lw_widg.setDecimals(1)
        self.fframe_lw_widg.setMinimum(0)
        self.fframe_lw_widg.setMaximum(99.9)
        self.fframe_lw_widg.setSuffix('  pt')
        self.fframe_lw_widg.setAlignment(Qt.AlignCenter)
        self.fframe_lw_widg.label = "Frame Thickness"
        self.fframe_lw_widg.setValue(self.figframe_lw)

        # Setup the layout of the groupbox.

        grpbox = QGroupBox("Figure Size :")
        layout = QGridLayout(grpbox)
        widgets = [
            self.fwidth, self.fheight, self.va_ratio_spinBox,
            self.fframe_lw_widg
        ]
        for row, widget in enumerate(widgets):
            layout.addWidget(QLabel("%s :" % widget.label), row, 0)
            layout.addWidget(widget, row, 2)

        layout.setColumnStretch(1, 100)
        layout.setContentsMargins(10, 10, 10, 10)
        return grpbox

    def _setup_element_visibility_grpbox(self):
        """
        Setup a groupbox containing all the options to set the visibility of
        various elements of the graph.
        """
        # Legend

        self.legend_on = OnOffToggleWidget('Legend', True)
        self.title_on = OnOffToggleWidget('Figure Title', True)
        self.wltrend_on = OnOffToggleWidget('Water Level Trend', False)
        self.meteo_on = OnOffToggleWidget('Weather Data', True)
        self.glue_wl_on = OnOffToggleWidget('GLUE Water Levels', False)
        self.mrc_wl_on = OnOffToggleWidget('MRC Water Levels', False)

        grpbox = QGroupBox("Graph Components Visibility :")
        layout = QGridLayout(grpbox)
        for i, widget in enumerate([
                self.legend_on, self.title_on, self.wltrend_on, self.meteo_on,
                self.glue_wl_on, self.mrc_wl_on
        ]):
            layout.addWidget(widget, i, 0)
        layout.setContentsMargins(10, 10, 10, 10)

        return grpbox

    # ---- Handlers

    def btn_OK_isClicked(self):
        """Apply the selected settings and close the window."""
        self.btn_apply_isClicked()
        self.close()

    def btn_apply_isClicked(self):
        """Apply the selected settings and emit a signal."""
        self.pageSize = (self.fwidth.value(), self.fheight.value())
        self.isLegend = self.legend_on.value()
        self.isGraphTitle = self.title_on.value()
        self.isTrendLine = self.wltrend_on.value()
        self.is_meteo_on = self.meteo_on.value()
        self.is_glue_wl_on = self.glue_wl_on.value()
        self.is_mrc_wl_on = self.mrc_wl_on.value()
        self.va_ratio = self.va_ratio_spinBox.value()
        self.figframe_lw = self.fframe_lw_widg.value()

        self.newPageSetupSent.emit(True)

    def closeEvent(self, event):
        """Qt method override."""
        super(PageSetupWin, self).closeEvent(event)

        # ---- Refresh UI ----

        # If cancel or X is clicked, the parameters will be reset to
        # the values they had the last time "Accept" button was
        # clicked.

        self.fwidth.setValue(self.pageSize[0])
        self.fheight.setValue(self.pageSize[1])
        self.va_ratio_spinBox.setValue(self.va_ratio)
        self.fframe_lw_widg.setValue(self.figframe_lw)

        self.legend_on.set_value(self.isLegend)
        self.title_on.set_value(self.isGraphTitle)
        self.wltrend_on.set_value(self.isTrendLine)
        self.meteo_on.set_value(self.is_meteo_on)
        self.glue_wl_on.set_value(self.is_glue_wl_on)
        self.mrc_wl_on.set_value(self.is_mrc_wl_on)

    def show(self):
        """Qt method override."""
        super(PageSetupWin, self).show()
        self.activateWindow()
        self.raise_()

        qr = self.frameGeometry()
        if self.parentWidget():
            parent = self.parentWidget()

            wp = parent.frameGeometry().width()
            hp = parent.frameGeometry().height()
            cp = parent.mapToGlobal(QPoint(wp / 2, hp / 2))
        else:
            cp = QDesktopWidget().availableGeometry().center()

        qr.moveCenter(cp)
        self.move(qr.topLeft())
        self.setMinimumSize(self.size())
        self.setFixedSize(self.size())
예제 #30
0
class WatsonTableWidget(QWidget):
    """
    A widget that contains a formatted table view and a custom title bar
    that shows the date span and the time count of all the activities listed
    in the table.
    """
    sig_tableview_focused_in = QSignal(object)
    sig_tableview_cleared = QSignal(object)

    def __init__(self, model, parent=None):
        super(WatsonTableWidget, self).__init__(parent)
        self.view = FormatedWatsonTableView(model)
        titlebar = self.setup_titlebar()

        layout = QGridLayout(self)
        layout.addWidget(titlebar, 0, 0)
        layout.addWidget(self.view, 1, 0)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(0)

        self.view.proxy_model.sig_total_seconds_changed.connect(
            self.setup_timecount)
        self.view.sig_focused_in.connect(
            lambda: self.sig_tableview_focused_in.emit(self))

    def setup_titlebar(self):
        """Setup the titlebar of the table."""
        font = QLabel().font()
        font.setBold(True)

        self.title = QLabel()
        self.title.setMargin(5)
        self.title.setFont(font)

        self.timecount = QLabel()
        self.timecount.setMargin(5)
        self.timecount.setFont(font)

        titlebar = ColoredFrame(color='grey')
        titlebar_layout = QHBoxLayout(titlebar)
        titlebar_layout.setContentsMargins(0, 0, 0, 0)

        titlebar_layout.addWidget(self.title)
        titlebar_layout.addStretch(100)
        titlebar_layout.addWidget(self.timecount)

        return titlebar

    @property
    def date_span(self):
        """Return the arrow span of the filter proxy model."""
        return self.view.proxy_model.date_span

    def rowCount(self):
        """Return the number of activity shown in the table."""
        return self.view.proxy_model.get_accepted_row_count()

    def set_date_span(self, date_span):
        """Set the date span in the table and title."""
        self.view.set_date_span(date_span)
        self.title.setText(arrowspan_to_str(date_span))

    def set_project_filters(self, project_filters):
        """Set the project filters of the table."""
        self.view.set_project_filters(project_filters)

    def set_tag_filters(self, tag_filters):
        """Set the tag filters of the table."""
        self.view.set_tag_filters(tag_filters)

    def setup_timecount(self, total_seconds):
        """
        Setup the time count for the activities of the table in the titlebar.
        """
        self.timecount.setText(total_seconds_to_hour_min(total_seconds))

    def get_selected_row(self):
        """Return the index of the selected row in the view."""
        return self.view.get_selected_row()

    def get_selected_frame_index(self):
        """
        Return the index of the frame corresponding to the selected row if
        there is one, else return None.
        """
        return self.view.get_selected_frame_index()