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)
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)
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()
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)
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)
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
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)
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()
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
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))
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()
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)
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
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")
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()
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()
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()
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"> = </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"> Δ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
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))
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
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()
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))
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())
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)
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()
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
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()
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()
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())
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()