# World Temperature Anomaly Map # Copyright (C) 2019 Right Rebels import sys from PyQt5 import QtCore, QtGui, QtWidgets from plot import Plotter class MainWindow(QtWidgets.QMainWindow): stop_plot_signal = QtCore.pyqtSignal() def __init__(self, settings): super(MainWindow, self).__init__() self.central_widget = QtWidgets.QWidget(self) self.main_layout = QtWidgets.QVBoxLayout(self.central_widget) self.image_label = QtWidgets.QLabel(self.central_widget) self.status_bar = QtWidgets.QStatusBar(self) self.image_slider = QtWidgets.QSlider(self.central_widget, orientation=QtCore.Qt.Horizontal) self.bottom_layout = QtWidgets.QHBoxLayout() self.move_year_left_button = QtWidgets.QPushButton() self.move_year_right_button = QtWidgets.QPushButton() self.button_layout = QtWidgets.QVBoxLayout() self.plot_button = QtWidgets.QPushButton() self.stop_button = QtWidgets.QPushButton(enabled=False) self.sdate_animate_layout = QtWidgets.QVBoxLayout() self.start_date_layout = QtWidgets.QHBoxLayout() self.start_year = QtWidgets.QSpinBox() self.start_month = QtWidgets.QComboBox() self.animate_button = QtWidgets.QPushButton(enabled=False) self.edate_pref_layout = QtWidgets.QVBoxLayout() self.end_date_layout = QtWidgets.QHBoxLayout() self.end_year = QtWidgets.QSpinBox() self.end_month = QtWidgets.QComboBox() self.preferences_button = QtWidgets.QPushButton() self.animate_timer = QtCore.QTimer() self.image_count = 0 self.settings = settings def setup_ui(self): self.save_default_settings() self.setWindowFlag(QtCore.Qt.MSWindowsFixedSizeDialogHint) self.setCentralWidget(self.central_widget) self.setStatusBar(self.status_bar) self.status_bar.setSizeGripEnabled(False) self.retranslate_ui() self.set_ranges_values() self.connect_signals() self.set_shortcuts() spacer = QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) self.start_date_layout.addWidget(self.start_month) self.start_date_layout.addWidget(self.start_year) self.sdate_animate_layout.addLayout(self.start_date_layout) self.sdate_animate_layout.addWidget(self.animate_button) self.button_layout.addWidget(self.plot_button, alignment=QtCore.Qt.AlignCenter) self.button_layout.addWidget(self.stop_button, alignment=QtCore.Qt.AlignCenter) self.end_date_layout.addWidget(self.end_month) self.end_date_layout.addWidget(self.end_year) self.edate_pref_layout.addLayout(self.end_date_layout) self.edate_pref_layout.addWidget(self.preferences_button) self.bottom_layout.addLayout(self.sdate_animate_layout) self.bottom_layout.addSpacerItem(spacer) self.bottom_layout.addWidget(self.move_year_left_button) self.bottom_layout.addLayout(self.button_layout) self.bottom_layout.addWidget(self.move_year_right_button) self.bottom_layout.addSpacerItem(spacer) self.bottom_layout.addLayout(self.edate_pref_layout) self.main_layout.addWidget(self.image_label, alignment=QtCore.Qt.AlignCenter) self.main_layout.addSpacerItem(spacer) self.main_layout.addWidget(self.image_slider) self.main_layout.addLayout(self.bottom_layout) self.show() self.preferences_button.pressed.connect(self.show_options) self.set_sizes() def set_shortcuts(self): year_move_right = QtWidgets.QShortcut( QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_Right), self) year_move_right.activated.connect(lambda: self.move_slider(self.year_step)) year_move_left = QtWidgets.QShortcut( QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_Left), self) year_move_left.activated.connect(lambda: self.move_slider(-self.year_step)) month_move_right = QtWidgets.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Right), self) month_move_right.activated.connect(lambda: self.move_slider(1)) month_move_left = QtWidgets.QShortcut(QtGui.QKeySequence(QtCore.Qt.Key_Left), self) month_move_left.activated.connect(lambda: self.move_slider(-1)) def set_sizes(self): self.setFixedSize(850, 650) self.image_label.setFixedSize(QtCore.QSize(796, 552)) # set year skip buttons to be square and 5 pixels wider than text in them font = QtGui.QFont() self.move_year_left_button.setFixedWidth( QtGui.QFontMetrics(font).boundingRect(self.move_year_left_button.text()).width() + 5) self.move_year_right_button.setFixedWidth( QtGui.QFontMetrics(font).boundingRect(self.move_year_right_button.text()).width() + 5) self.move_year_left_button.setFixedHeight(self.move_year_left_button.width()) self.move_year_right_button.setFixedHeight(self.move_year_right_button.width()) def set_ranges_values(self): months = ("January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December") self.start_month.addItems(months) self.end_month.addItems(months) self.image_slider.setRange(0, 0) self.image_slider.setValue(0) self.start_year.setRange(1850, 2010) self.end_year.setRange(1980, 2019) self.start_year.setValue(1980) self.end_year.setValue(2010) def connect_signals(self): self.image_slider.valueChanged.connect(self.change_image) # ensure only valid dates can be entered self.start_month.currentIndexChanged.connect(self.date_changed) self.end_year.valueChanged.connect(self.date_changed) self.start_year.valueChanged.connect(self.date_changed) self.animate_button.pressed.connect(self.animate) self.plot_button.pressed.connect(self.plot) self.stop_button.pressed.connect(self.quit_current_tasks) self.move_year_left_button.pressed.connect(lambda: self.move_slider(-self.year_step)) self.move_year_right_button.pressed.connect(lambda: self.move_slider(self.year_step)) def retranslate_ui(self): self.setWindowTitle("World Temperature Anomaly Map") self.plot_button.setText("Plot") self.stop_button.setText("Stop") self.animate_button.setText("Play") self.preferences_button.setText("Preferences") self.move_year_right_button.setText("-->") self.move_year_left_button.setText("<--") self.move_year_left_button.setToolTip("Skip year") self.move_year_right_button.setToolTip("Skip year") def show_options(self): self.preferences_button.setEnabled(False) w = SettingsPop(self.settings, self) w.setup_ui() w.settings_signal.connect(self.refresh_settings) w.close_signal.connect(lambda: self.preferences_button.setEnabled(True)) def save_default_settings(self): if not self.settings.value("Plot step"): self.settings.setValue("Playback FPS", 5) self.settings.setValue("Plot step", 1) self.settings.setValue("Color map", "seismic") self.settings.sync() self.plot_step = self.settings.value("Plot step", type=int) self.play_fps = self.settings.value("Playback FPS", type=int) self.color_map = self.settings.value("Color map") self.year_step = max(int(12 / self.plot_step), 1) def refresh_settings(self, values): self.play_fps = int(values[0]) self.plot_step = int(values[1]) self.color_map = values[2] self.year_step = max(int(12 / self.plot_step), 1) def set_status(self, message): self.status_bar.showMessage(message) def plot(self): self.image_count = 0 QtGui.QPixmapCache.clear() # clear qt image cache self.stop_button.setEnabled(True) self.plot_button.setEnabled(False) self.animate_button.setEnabled(False) # send dates in decimal format to worker start_date = self.start_year.value() + (1 + self.start_month.currentIndex() * 2) / 24 end_date = self.end_year.value() + (1 + self.end_month.currentIndex() * 2) / 24 self.worker = Plotter(start_date, end_date, self.plot_step, self.color_map, self) self.worker.image_increment_signal.connect(self.add_image) self.worker.finished.connect(self.del_worker) self.worker.status_signal.connect(self.set_status) self.worker.start() def del_worker(self): self.worker.quit() self.stop_button.setEnabled(False) self.plot_button.setEnabled(True) self.animate_button.setEnabled(True) def add_image(self): self.image_slider.setMaximum(self.image_count) # move slider to max value if it was at max before if self.image_slider.value() == self.image_slider.maximum() - 1: self.move_slider(1) self.image_count += 1 def move_slider(self, amount: int): """ move image_slider by value""" self.image_slider.setValue(self.image_slider.value() + amount) def change_image(self, index): pixmap = QtGui.QPixmap(f"{Plotter.PLOTS_DIR}plot{index}") self.image_label.setPixmap(pixmap) def date_changed(self): """Ensure only valid dates can be entered if start and end years match, block out months above chosen start month in end months if year is 2019 allow only January-May range""" for item_index in range(0, 12): self.start_month.model().item(item_index).setEnabled(True) self.end_month.model().item(item_index).setEnabled(True) if self.start_year.value() == self.end_year.value(): for item_index in range(0, self.start_month.currentIndex()): self.end_month.model().item(item_index).setEnabled(False) if self.end_month.currentIndex() < self.start_month.currentIndex(): self.end_month.setCurrentIndex(self.start_month.currentIndex()) if self.start_year.value() == 2019: for item_index in range(5, 12): self.start_month.model().item(item_index).setEnabled(False) if self.start_month.currentIndex() > 4: self.start_month.setCurrentIndex(4) if self.end_year.value() == 2019: for item_index in range(5, 12): self.end_month.model().item(item_index).setEnabled(False) if self.end_month.currentIndex() > 4: self.end_month.setCurrentIndex(4) self.start_year.setRange(1850, self.end_year.value()) self.end_year.setRange(self.start_year.value(), 2019) def animate(self): self.image_slider.setValue(1) self.stop_button.setEnabled(True) self.animate_button.setEnabled(False) self.animate_timer.timeout.connect(self.animation) self.animate_timer.start(int(1000 / self.play_fps)) def animation(self): self.move_slider(1) if self.image_slider.value() == self.image_slider.maximum(): self.stop_animation() self.stop_button.setEnabled(False) def stop_animation(self): self.animate_timer.stop() try: self.animate_timer.timeout.disconnect() except TypeError: pass self.animate_button.setEnabled(True) def quit_current_tasks(self): self.stop_plot_signal.emit() self.stop_animation() self.stop_button.setEnabled(False) def closeEvent(self, *args, **kwargs): super(QtWidgets.QMainWindow, self).closeEvent(*args, **kwargs) try: self.worker.clear_plots() except AttributeError: pass class SettingsPop(QtWidgets.QDialog): settings_signal = QtCore.pyqtSignal(tuple) close_signal = QtCore.pyqtSignal() def __init__(self, settings, parent=None): super(SettingsPop, self).__init__(parent) self.main_layout = QtWidgets.QVBoxLayout() self.fps_layout = QtWidgets.QHBoxLayout() self.fps_label = QtWidgets.QLabel() self.fps_spin = QtWidgets.QSpinBox() self.step_layout = QtWidgets.QHBoxLayout() self.step_label = QtWidgets.QLabel() self.step_combo = QtWidgets.QComboBox() self.color_map_layout = QtWidgets.QHBoxLayout() self.color_map = QtWidgets.QPushButton() self.color_map_label = QtWidgets.QLabel() self.settings = settings self.save_button = QtWidgets.QPushButton() self.license_button = QtWidgets.QPushButton() def setup_ui(self): spacer = QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) self.setLayout(self.main_layout) self.fps_layout.addWidget(self.fps_label) self.fps_layout.addSpacerItem(spacer) self.fps_spin.setRange(1, 60) self.fps_spin.setAccelerated(True) self.fps_layout.addWidget(self.fps_spin) self.main_layout.addLayout(self.fps_layout) self.step_layout.addWidget(self.step_label) self.step_combo.addItems(("1", "3", "6", "12", "24")) self.step_layout.addSpacerItem(spacer) self.step_layout.addWidget(self.step_combo) self.main_layout.addLayout(self.step_layout) self.color_map_layout.addWidget(self.color_map) self.color_map_layout.addWidget(self.color_map_label, alignment=QtCore.Qt.AlignRight) self.main_layout.addLayout(self.color_map_layout) self.main_layout.addSpacerItem(spacer) self.main_layout.addWidget(self.save_button, alignment=QtCore.Qt.AlignCenter) self.main_layout.addWidget(self.license_button, alignment=QtCore.Qt.AlignCenter) self.setWindowFlag(QtCore.Qt.WindowContextHelpButtonHint, False) self.color_map.pressed.connect(self.color_map_chooser) self.save_button.pressed.connect(self.save_settings) self.license_button.pressed.connect(self.show_license) self.retranslate_ui() self.grab_settings() self.show() def retranslate_ui(self): self.setWindowTitle("Preferences") self.fps_label.setText("Playback speed [FPS]") self.step_label.setText("Step of plotter") self.color_map.setText("Choose color map") self.save_button.setText("Save preferences") self.license_button.setText("License") def grab_settings(self): self.fps_spin.setValue(self.settings.value("Playback FPS", type=int)) step_value = self.settings.value("Plot step") self.step_combo.setCurrentIndex(("1", "3", "6", "12", "24").index(step_value)) self.color_map_label.setText(self.settings.value("Color map")) def save_settings(self): settings = (self.fps_spin.value(), self.step_combo.currentText(), self.color_map_label.text()) self.settings.setValue("Playback FPS", settings[0]) self.settings.setValue("Plot step", settings[1]) self.settings.setValue("Color map", settings[2]) self.settings.sync() self.settings_signal.emit(settings) self.close() def color_map_chooser(self): w = ColorMapChooser(self.cursor().pos(), self) w.setup_ui() w.choice_signal.connect(self.set_label) def set_label(self, text): self.color_map_label.setText(text) def show_license(self): w = License(self) w.setup() w.close_signal.connect(lambda: self.license_button.setEnabled(True)) self.license_button.setEnabled(False) def closeEvent(self, *args, **kwargs): super(QtWidgets.QDialog, self).closeEvent(*args, **kwargs) self.close_signal.emit() class ColorMapChooser(QtWidgets.QDialog): choice_signal = QtCore.pyqtSignal(str) def __init__(self, pos, parent=None): super(ColorMapChooser, self).__init__(parent) self.main_layout = QtWidgets.QVBoxLayout() self.color_list = QtWidgets.QListWidget() self.start_pos = pos def setup_ui(self): self.main_layout.setContentsMargins(0, 0, 0, 0) self.move(self.start_pos) self.main_layout.addWidget(self.color_list) self.setLayout(self.main_layout) self.color_list.addItems(Plotter.get_color_maps()) self.setWindowFlag(QtCore.Qt.FramelessWindowHint, True) self.setWindowFlags(QtCore.Qt.Popup) self.color_list.itemDoubleClicked.connect(self.send_choice) self.show() def send_choice(self, item): self.choice_signal.emit(item.text()) self.close() class CrashPop(QtWidgets.QDialog): def __init__(self, tb): super(CrashPop, self).__init__() self.label = QtWidgets.QLabel() self.text_browser = QtWidgets.QTextBrowser() self.main_layout = QtWidgets.QVBoxLayout(self) self.traceback = tb def setup(self): self.setFixedSize(400, 250) self.setWindowTitle("Error") self.label.setText("An unexpected error has occured") font = QtGui.QFont() font.setPointSize(12) self.label.setFont(font) for line in self.traceback: self.text_browser.append(line) self.main_layout.addWidget(self.label) self.main_layout.addWidget(self.text_browser) self.setLayout(self.main_layout) self.setWindowFlag(QtCore.Qt.WindowContextHelpButtonHint, False) self.setModal(True) self.show() def closeEvent(self, *args, **kwargs): super(QtWidgets.QDialog, self).closeEvent(*args, **kwargs) sys.exit(-1) class License(QtWidgets.QDialog): close_signal = QtCore.pyqtSignal() def __init__(self, parent=None): super(License, self).__init__(parent) self.main_layout = QtWidgets.QVBoxLayout() self.text = QtWidgets.QTextBrowser() def setup(self): self.text.setText("World Temperature Anomaly Map Copyright (C) 2019 Right Rebels\n" "This program comes with ABSOLUTELY NO WARRANTY.\n" "This is free software, and you are welcome to redistribute it") self.text.append('under certain conditions; <a href="https://www.gnu.org/licenses/">' 'click here</a> for details') self.main_layout.addWidget(self.text) self.text.setOpenExternalLinks(True) self.setLayout(self.main_layout) self.setWindowFlag(QtCore.Qt.WindowContextHelpButtonHint, False) self.show() def closeEvent(self, *args, **kwargs): super(QtWidgets.QDialog, self).closeEvent(*args, **kwargs) self.close_signal.emit()