diff --git a/examples/radolan_viewer.py b/examples/radolan_viewer.py index 42e3889..08d4055 100644 --- a/examples/radolan_viewer.py +++ b/examples/radolan_viewer.py @@ -5,9 +5,9 @@ # ----------------------------------------------------------------------------- #!/usr/bin/env python -import sys - from wradvis import gui +import matplotlib +matplotlib.use('Qt5Agg') if __name__ == '__main__': - gui.start(sys) + gui.start() diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2417ea9 --- /dev/null +++ b/setup.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python +# Copyright (c) 2016-2018, wradlib developers. +# Distributed under the MIT License. See LICENSE.txt for more info. + + +def setup_package(): + + from setuptools import setup, find_packages + + metadata = dict( + name='wradvis', + version='0.1.0', + packages=find_packages(), + entry_points={ + 'gui_scripts': [ + 'wradvis = wradvis.gui:start' + ] + }, + ) + + setup(**metadata) + + +if __name__ == '__main__': + setup_package() diff --git a/wradvis/__init__.py b/wradvis/__init__.py index db6504e..291401e 100644 --- a/wradvis/__init__.py +++ b/wradvis/__init__.py @@ -1,8 +1,5 @@ -# -*- coding: utf-8 -*- -# ----------------------------------------------------------------------------- -# Copyright (c) 2016, wradlib Development Team. All Rights Reserved. -# Distributed under the MIT License. See LICENSE.txt for more info. -# ----------------------------------------------------------------------------- #!/usr/bin/env python +# Copyright (c) 2016-2018, wradlib developers. +# Distributed under the MIT License. See LICENSE.txt for more info. from . import gui diff --git a/wradvis/config.py b/wradvis/config.py index 06f0e61..bff84cf 100644 --- a/wradvis/config.py +++ b/wradvis/config.py @@ -1,9 +1,6 @@ -# -*- coding: utf-8 -*- -# ----------------------------------------------------------------------------- -# Copyright (c) 2016, wradlib Development Team. All Rights Reserved. -# Distributed under the MIT License. See LICENSE.txt for more info. -# ----------------------------------------------------------------------------- #!/usr/bin/env python +# Copyright (c) 2016-2018, wradlib developers. +# Distributed under the MIT License. See LICENSE.txt for more info. """ """ diff --git a/wradvis/glcanvas.py b/wradvis/glcanvas.py index 4217b60..b33f574 100644 --- a/wradvis/glcanvas.py +++ b/wradvis/glcanvas.py @@ -1,27 +1,183 @@ -# -*- coding: utf-8 -*- -# ----------------------------------------------------------------------------- -# Copyright (c) 2016, wradlib Development Team. All Rights Reserved. -# Distributed under the MIT License. See LICENSE.txt for more info. -# ----------------------------------------------------------------------------- #!/usr/bin/env python +# Copyright (c) 2016-2018, wradlib developers. +# Distributed under the MIT License. See LICENSE.txt for more info. import numpy as np -from PyQt4 import QtGui, QtCore +from PyQt5 import QtCore +from PyQt5.QtWidgets import (QSplitter, QWidget, QHBoxLayout) from vispy.scene import SceneCanvas from vispy.util.event import EventEmitter from vispy.visuals.transforms import STTransform, MatrixTransform, PolarTransform from vispy.scene.cameras import PanZoomCamera -from vispy.scene.visuals import Image, ColorBar, Markers, Text +from vispy.scene.visuals import Image, ColorBar, Markers, Text, Line, InfiniteLine +from vispy.scene.widgets import Label, AxisWidget from vispy.geometry import Rect +from vispy.color import Color from wradvis import utils from wradvis.config import conf -class ColorbarCanvas(SceneCanvas): +class GlCanvas(SceneCanvas): + def __init__(self, vrow=0, vcol=0, **kwargs): + super(GlCanvas, self).__init__(**kwargs) + + self.unfreeze() + self.grid = self.central_widget.add_grid(margin=10) + self.grid.spacing = 0 + self.view = self.grid.add_view(row=vrow, col=vcol, + border_color='white') + + self.transitem = None + self.cursor = None + + self._mouse_position = None + self._mouse_press_position = (0, 0) + self._cursor_position = None + self._cursor_press_position = (0, 0) + + self.mouse_double_clicked = EventEmitter(source=self, + type="mouse_double_clicked") + self.mouse_moved = EventEmitter(source=self, type="mouse_moved") + self.mouse_pressed = EventEmitter(source=self, type="mouse_pressed") + self.key_pressed = EventEmitter(source=self, type="key_pressed") + + self.freeze() + + def on_mouse_double_click(self, event): + point = self.scene.node_transform(self.transitem).map(event.pos)[:2] + self._mouse_position = point + # emit signal + self.mouse_double_clicked(event) + + def on_key_press(self, event): + self.key_pressed(event) + + def on_mouse_move(self, event): + point = self.scene.node_transform(self.transitem).map(event.pos)[:2] + cursor = self.scene.node_transform(self.cursor).map(event.pos)[:2] + self._mouse_position = point + self._cursor_position = cursor + self.update_cursor() + self.mouse_moved(event) + + def on_mouse_press(self, event): + point = self.scene.node_transform(self.transitem).map(event.pos)[:2] + cursor = self.scene.node_transform(self.cursor).map(event.pos)[:2] + self._mouse_press_position = point + self._cursor_press_position = cursor + self.update_select_cursor() + self.mouse_pressed(event) + + def add_cursor(self): + # cursor lines + self.vline = InfiniteLine(parent=self.view.scene, + color=Color("blue").RGBA) + self.vline.transform = STTransform( + translate=(0, 0, -10)) + self.hline = InfiniteLine(parent=self.view.scene, + color=Color("blue").RGBA, + vertical=False) + self.hline.transform = STTransform( + translate=(0, 0, -10)) + self.vline.visible = False + self.hline.visible = False + + self.cursor = self.hline + + # pick lines + self.vpline = InfiniteLine(parent=self.view.scene, + color=Color("red").RGBA) + self.vpline.transform = STTransform( + translate=(0, 0, -5)) + self.hpline = InfiniteLine(parent=self.view.scene, + color=Color("red").RGBA, + vertical=False) + self.hpline.transform = STTransform( + translate=(0, 0, -5)) + self.vpline.visible = False + self.hpline.visible = False + + def update_cursor(self): + pos = self._cursor_position + self.vline.set_data(pos=pos[0]) + self.hline.set_data(pos=pos[1]) + # needed to work, set_data doesn't update by itself + self.update() + + def update_select_cursor(self): + pos = self._cursor_press_position + self.vpline.set_data(pos[0]) + self.hpline.set_data(pos[1]) + self.update() + + +class AxisCanvas(GlCanvas): + def __init__(self, **kwargs): + super(AxisCanvas, self).__init__(**kwargs) + + self.size = 450, 200 + self.unfreeze() + + self.pl_title = Label("Time Graph", color='white') + self.pl_title.height_max = 25 + self.grid.add_widget(self.pl_title, row=0, col=0, col_span=3) + + self.yaxis = AxisWidget(orientation='left') + self.yaxis.width_max = 25 + self.grid.add_widget(self.yaxis, row=1, col=1) + + self.ylabel = Label('Units', rotation=-90, color='white') + self.ylabel.width_max = 25 + self.grid.add_widget(self.ylabel, row=1, col=0) + + self.xaxis = AxisWidget(orientation='bottom') + self.xaxis.height_max = 25 + self.grid.add_widget(self.xaxis, row=2, col=2) + + self.xlabel = Label('Time', color='white') + self.xlabel.height_max = 25 + self.grid.add_widget(self.xlabel, row=3, col=0, col_span=3) + + self.right_padding = self.grid.add_widget(row=0, col=3, row_span=3) + self.right_padding.width_max = 30 + + self.add_cursor() + + self.cam = PanZoomCamera(name="PanZoom", + #rect=Rect(0, 0, 900, 900), + #aspect=1, + parent=self.view.scene) + # data line + self.plot = Line(parent=self.view.scene) + self.plot.transform = STTransform( + translate=(0, 0, -2.5)) + + self.transitem = self.plot + + # cursors + self.low_line = InfiniteLine(parent=self.view.scene, color=Color("blue").RGBA) + self.low_line.transform = STTransform( + translate=(0, 0, -2.5)) + self.high_line = InfiniteLine(parent=self.view.scene, color=Color("blue").RGBA) + self.high_line.transform = STTransform( + translate=(0, 0, -2.5)) + self.cur_line = InfiniteLine(parent=self.view.scene, + color=Color("red").RGBA) + self.cur_line.transform = STTransform( + translate=(0, 0, -2.5)) + + self.view.camera = self.cam + + self.xaxis.link_view(self.view) + self.yaxis.link_view(self.view) + + self.freeze() + +class ColorbarCanvas(GlCanvas): def __init__(self, **kwargs): super(ColorbarCanvas, self).__init__(keys='interactive', **kwargs) @@ -31,11 +187,10 @@ def __init__(self, **kwargs): # unfreeze needed to add more elements self.unfreeze() - # add grid central widget - self.grid = self.central_widget.add_grid() + self.events.mouse_move.block() + self.events.mouse_press.block() + self.events.mouse_double_click.block() - # add view to grid - self.view = self.grid.add_view(row=0, col=0) self.view.border_color = (0.5, 0.5, 0.5, 1) # initialize colormap, we take cubehelix for now @@ -65,8 +220,7 @@ def __init__(self, **kwargs): self.freeze() -class RadolanCanvas(SceneCanvas): - +class RadolanCanvas(GlCanvas): def __init__(self, **kwargs): super(RadolanCanvas, self).__init__(keys='interactive', **kwargs) @@ -76,17 +230,6 @@ def __init__(self, **kwargs): # unfreeze needed to add more elements self.unfreeze() - # add grid central widget - self.grid = self.central_widget.add_grid() - - # add view to grid - self.view = self.grid.add_view(row=0, col=0) - self.view.border_color = (0.5, 0.5, 0.5, 1) - - # add signal emitters - self.mouse_moved = EventEmitter(source=self, type="mouse_moved") - self.key_pressed = EventEmitter(source=self, type="key_pressed") - # block double clicks self.events.mouse_double_click.block() @@ -107,6 +250,8 @@ def __init__(self, **kwargs): clim=(0,50), parent=self.view.scene) + self.transitem = self.image + self.images.append(self.image) # add transform to Image @@ -119,6 +264,9 @@ def __init__(self, **kwargs): # create cities (Markers and Text Visuals self.create_cities() + # cursor lines + self.add_cursor() + # create PanZoomCamera self.cam = PanZoomCamera(name="PanZoom", rect=Rect(0, 0, 900, 900), @@ -127,7 +275,6 @@ def __init__(self, **kwargs): self.view.camera = self.cam - self._mouse_position = None self.freeze() # print FPS to console, vispy SceneCanvas internal function self.measure_fps() @@ -183,13 +330,9 @@ def create_cities(self): self.text.append(t) i += 1 - def on_mouse_move(self, event): - point = self.scene.node_transform(self.image).map(event.pos)[:2] - self._mouse_position = point - # emit signal - self.mouse_moved(event) - def on_mouse_press(self, event): + super(RadolanCanvas, self).on_mouse_press(event) + self.view.interactive = False for v in self.visuals_at(event.pos, radius=30): @@ -207,9 +350,6 @@ def on_mouse_press(self, event): self.view.interactive = True - def on_key_press(self, event): - self.key_pressed(event) - class PTransform(PolarTransform): glsl_imap = """ @@ -221,6 +361,16 @@ class PTransform(PolarTransform): } """ + def imap(self, coords): + coords = np.array(coords) + ret = np.empty(coords.shape, coords.dtype) + ret[..., 0] = np.rad2deg(np.arctan2(coords[..., 0], + coords[..., 1]) + np.pi) + ret[..., 1] = (coords[..., 0] ** 2 + coords[..., 1] ** 2) ** 0.5 + for i in range(2, coords.shape[-1]): + ret[..., i] = coords[..., i] + return ret + class PolarImage(Image): def __init__(self, source=None, **kwargs): @@ -250,26 +400,20 @@ def __init__(self, source=None, **kwargs): # the translation moves the image to centere the ppi rot = MatrixTransform() rot.rotate(180, (0, 0, 1)) - self.transform = (STTransform(translate=(self.range+xoff, self.range+yoff, 0)) * + self.transform = (STTransform(translate=(self.range+xoff, + self.range+yoff, 0)) * rot * PTransform()) self.freeze() -class DXCanvas(SceneCanvas): +class DXCanvas(GlCanvas): def __init__(self, **kwargs): super(DXCanvas, self).__init__(keys='interactive', **kwargs) self.size = 450, 450 self.unfreeze() - # add grid central widget - self.grid = self.central_widget.add_grid() - - # add view to grid - self.view = self.grid.add_view(row=0, col=0) - self.view.border_color = (0.5, 0.5, 0.5, 1) - # This is hardcoded now, but maybe handled as the data source changes self.img_data = np.zeros((360, 128)) @@ -288,11 +432,12 @@ def __init__(self, **kwargs): clim=(-32.5, 95), parent=self.view.scene) + self.transitem = self.image + self.images.append(self.image) - # add signal emitters - self.mouse_moved = EventEmitter(source=self, type="mouse_moved") - self.key_pressed = EventEmitter(source=self, type="key_pressed") + # cursor lines + self.add_cursor() # block double clicks self.events.mouse_double_click.block() @@ -306,24 +451,9 @@ def __init__(self, **kwargs): self.view.camera = self.cam - self._mouse_position = None - self.freeze() self.measure_fps() - def on_mouse_move(self, event): - tr = self.scene.node_transform(self.image) - point = tr.map(event.pos)[:2] - # todo: we should actually move this into PTransform in the future - point[0] += np.pi - point[0] = np.rad2deg(point[0]) - self._mouse_position = point - # emit signal - self.mouse_moved(event) - - def on_key_press(self, event): - self.key_pressed(event) - def add_image(self, radar): # this adds an image to the images list image = PolarImage(source=radar, @@ -336,7 +466,7 @@ def add_image(self, radar): self.images.append(image) -class RadolanWidget(QtGui.QWidget): +class RadolanWidget(QWidget): def __init__(self, parent=None): super(RadolanWidget, self).__init__(parent) self.parent = parent @@ -357,7 +487,7 @@ def __init__(self, parent=None): self.swapper['R'] = self.rcanvas.native self.swapper['P'] = self.pcanvas.native - self.splitter = QtGui.QSplitter(QtCore.Qt.Horizontal) + self.splitter = QSplitter(QtCore.Qt.Horizontal) self.splitter.addWidget(self.swapper['R']) self.splitter.addWidget(self.swapper['P']) self.swapper['P'].hide() @@ -367,10 +497,14 @@ def __init__(self, parent=None): self.splitter.setStretchFactor(0, 1) self.splitter.setStretchFactor(1, 1) self.splitter.setStretchFactor(2, 0) - self.hbl = QtGui.QHBoxLayout() + self.hbl = QHBoxLayout() self.hbl.addWidget(self.splitter) self.setLayout(self.hbl) + def connect_signals(self): + self.parent.mediabox.signal_time_slider_changed.connect(self.set_time) + self.parent.mousebox.signal_toggle_Cursor.connect(self.toggle_cursor) + def set_canvas(self, type): if type == 'DX': self.canvas = self.pcanvas @@ -381,6 +515,15 @@ def set_canvas(self, type): self.swapper['R'].show() self.swapper['P'].hide() + def set_time(self, pos): + # now this sets same data to all images + # we would need to do the data loading + # via objects (maybe radar-object from above) + # and use + for im in self.canvas.images: + im.set_data(self.parent.props.mem.variables['data'][pos][:]) + self.canvas.update() + def set_data(self, data): # now this sets same data to all images # we would need to do the data loading @@ -393,3 +536,65 @@ def set_data(self, data): def set_clim(self, clim): self.canvas.image.clim = clim self.cbar.cbar.clim = clim + + def toggle_cursor(self, state): + self.canvas.hline.visible = state + self.canvas.vline.visible = state + self.canvas.hpline.visible = state + self.canvas.vpline.visible = state + self.canvas.update() + + +class RadolanLineWidget(QWidget): + + #signal_mouse_double_clicked = QtCore.pyqtSignal(int, name='mouseDblClicked') + + def __init__(self, parent=None): + super(RadolanLineWidget, self).__init__(parent) + self.parent = parent + self.canvas = AxisCanvas(vrow=1, vcol=2) + self.canvas.create_native() + self.canvas.native.setParent(self) + self.hbl = QHBoxLayout() + self.hbl.addWidget(self.canvas.native) + self.setLayout(self.hbl) + + def sizeHint(self): + return QtCore.QSize(650, 200) + + def connect_signals(self): + self.parent.parent.iwidget.rcanvas.mouse_pressed.connect(self.set_line) + self.parent.parent.iwidget.pcanvas.mouse_pressed.connect(self.set_line) + self.parent.parent.mediabox.signal_time_properties_changed.connect(self.set_time_limits) + #self.canvas.mouse_double_clicked(self.mouse_double_clicked) + + def set_line(self, event): + pos = self.parent.parent.iwidget.canvas._mouse_press_position + + if self.parent.props.mem.variables['data'].source in ['DX']: + y = self.parent.props.mem.variables['data'][:, int(pos[0]), int(pos[1])] + else: + y = self.parent.props.mem.variables['data'][:, int(pos[1]), int(pos[0])] + x = np.arange(len(y)) + try: + self.plot.parent = None + except: + pass + self.plot = Line(np.squeeze(np.dstack((x, y))), parent=self.canvas.view.scene) + self.plot.transform = STTransform( + translate=(0, 0, -2.5)) + self.set_time_limits() + + def set_time_limits(self): + low = self.parent.parent.mediabox.range.low() + high = self.parent.parent.mediabox.range.high() + cur = self.parent.parent.mediabox.time_slider.value() + + self.canvas.low_line.set_data(low) + self.canvas.high_line.set_data(high) + self.canvas.cur_line.set_data(cur) + self.canvas.cam.set_range(margin=0.) + + #def mouse_double_clicked(self): + # self.signal_mouse_double_clicked.emit(self.canvas._mouse_position) + diff --git a/wradvis/gui.py b/wradvis/gui.py index a5df65e..e12c8c0 100644 --- a/wradvis/gui.py +++ b/wradvis/gui.py @@ -1,22 +1,26 @@ -# -*- coding: utf-8 -*- -# ----------------------------------------------------------------------------- -# Copyright (c) 2016, wradlib Development Team. All Rights Reserved. -# Distributed under the MIT License. See LICENSE.txt for more info. -# ----------------------------------------------------------------------------- #!/usr/bin/env python +# Copyright (c) 2016-2018, wradlib developers. +# Distributed under the MIT License. See LICENSE.txt for more info. + +import sys -from PyQt4 import QtGui, QtCore +from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5.QtWidgets import (QMainWindow, QApplication, QSplitter, QAction, + QDockWidget, QSizePolicy) import vispy +import matplotlib +matplotlib.use('Qt5Agg') # other wradvis imports from wradvis.glcanvas import RadolanWidget from wradvis.mplcanvas import MplWidget -from wradvis.properties import Properties, MediaBox, SourceBox, MouseBox +from wradvis.properties import Properties, MediaBox, SourceBox, \ + MouseBox, GraphBox from wradvis import utils from wradvis.config import conf -class MainWindow(QtGui.QMainWindow): +class MainWindow(QMainWindow): def __init__(self, parent=None): super(MainWindow, self).__init__(parent) @@ -28,7 +32,6 @@ def __init__(self, parent=None): self._need_canvas_refresh = False self.timer = QtCore.QTimer() - self.timer.timeout.connect(self.reload) # initialize RadolanCanvas self.rwidget = RadolanWidget(self) @@ -49,7 +52,7 @@ def __init__(self, parent=None): self.props = Properties(self) # add Horizontal Splitter and the three widgets - self.splitter = QtGui.QSplitter(QtCore.Qt.Horizontal) + self.splitter = QSplitter(QtCore.Qt.Horizontal) self.splitter.addWidget(self.swapper[0]) self.splitter.addWidget(self.swapper[1]) self.swapper[1].hide() @@ -61,69 +64,86 @@ def __init__(self, parent=None): self.connect_signals() - # finish init self.props.update_props() def connect_signals(self): self.mediabox.signal_playpause_changed.connect(self.start_stop) - self.mediabox.signal_time_slider_changed.connect(self.slider_changed) self.mediabox.signal_speed_changed.connect(self.speed) - self.props.signal_props_changed.connect(self.slider_changed) + self.mediabox.connect_signals() + self.rwidget.connect_signals() + self.graphbox.connect_signals() def createActions(self): # Set directory - self.setDataDir = QtGui.QAction("&Set directory", self, - statusTip='Set directory', - triggered=self.props.set_datadir) + self.setDataDir = QAction("&Set directory", self, + statusTip='Set directory', + triggered=self.props.set_datadir) # Open project (configuration) - self.openConf = QtGui.QAction("&Open project", self, - shortcut="Ctrl+O", - statusTip='Open project', - triggered=self.props.open_conf) + self.openConf = QAction("&Open project", self) + self.openConf.setShortcut("Ctrl+O") + self.openConf.setStatusTip('Open project') + self.openConf.triggered.connect(self.props.open_conf) # Save project (configuration) - self.saveConf = QtGui.QAction("&Save project", self, + self.saveConf = QAction("&Save project", self, shortcut="Ctrl+S", statusTip='Save project', triggered=self.props.save_conf) + # Load netcdf (data file) + self.loadNC = QAction("&Load NetCDF", self, + shortcut="Ctrl+L", + statusTip='Load netCDF data file', + triggered=self.props.load_data) + + # Save netcdf (data file) + self.saveNC = QAction("Save &NetCDF", self, + shortcut="Ctrl+N", + statusTip='Save data file as netCDF', + triggered=self.props.save_data) + def createMenus(self): self.fileMenu = self.menuBar().addMenu("&File") self.fileMenu.addAction(self.setDataDir) self.fileMenu.addAction(self.openConf) self.fileMenu.addAction(self.saveConf) + self.fileMenu.addAction(self.loadNC) + self.fileMenu.addAction(self.saveNC) self.toolsMenu = self.menuBar().addMenu('&Tools') self.helpMenu = self.menuBar().addMenu('&Help') def createDockWindows(self): - dock = QtGui.QDockWidget("Radar Source Data", self) + dock = QDockWidget("Radar Source Data", self) dock.setAllowedAreas(QtCore.Qt.RightDockWidgetArea) self.sourcebox = SourceBox(self) dock.setWidget(self.sourcebox) self.addDockWidget(QtCore.Qt.RightDockWidgetArea, dock) self.toolsMenu.addAction(dock.toggleViewAction()) - dock = QtGui.QDockWidget("Media Handling", self) + dock = QDockWidget("Media Handling", self) dock.setAllowedAreas(QtCore.Qt.RightDockWidgetArea) self.mediabox = MediaBox(self) dock.setWidget(self.mediabox) self.addDockWidget(QtCore.Qt.RightDockWidgetArea, dock) self.toolsMenu.addAction(dock.toggleViewAction()) - dock = QtGui.QDockWidget("Mouse Interaction", self) + dock = QDockWidget("Mouse Interaction", self) dock.setAllowedAreas(QtCore.Qt.RightDockWidgetArea) self.mousebox = MouseBox(self) dock.setWidget(self.mousebox) self.addDockWidget(QtCore.Qt.RightDockWidgetArea, dock) self.toolsMenu.addAction(dock.toggleViewAction()) - def reload(self): - if self.mediabox.time_slider.value() >= self.mediabox.range.high(): - self.mediabox.time_slider.setValue(self.mediabox.range.low()) - else: - self.mediabox.time_slider.setValue(self.mediabox.time_slider.value() + 1) + dock = QDockWidget("Time Graphs", self) + dock.setAllowedAreas(QtCore.Qt.BottomDockWidgetArea) + size_pol = (QSizePolicy.MinimumExpanding, + QSizePolicy.MinimumExpanding) + self.graphbox = GraphBox(self, size_pol=size_pol) + dock.setWidget(self.graphbox) + self.addDockWidget(QtCore.Qt.BottomDockWidgetArea, dock) + self.toolsMenu.addAction(dock.toggleViewAction()) def start_stop(self): if self.timer.isActive(): @@ -131,46 +151,32 @@ def start_stop(self): else: self.timer.start() - def speed(self): - self.timer.setInterval(self.mediabox.speed.value()) - - def slider_changed(self, pos): - try: - # Todo: switching happens here, - # but we should just use a common reading function, where the - # underlying wradlib function is exchanged on switching - # format - if self.props.product == 'DX': - self.data, _ = utils.read_dx( - self.props.filelist[pos]) - - else: - self.data, _ = utils.read_radolan(self.props.filelist[pos]) - if self.props.product == 'RX': - self.data = (self.data / 2) - 32.5 - except IndexError: - print("Could not read any data.") - else: - self.iwidget.set_data(self.data) + def speed(self, value): + self.timer.setInterval(value) def keyPressEvent(self, event): if isinstance(event, QtGui.QKeyEvent): text = event.text() else: text = event.text - if text == 'c': - self.swapper = self.swapper[::-1] - self.iwidget = self.swapper[0] - self.swapper[0].show() - self.swapper[0].setFocus() - self.swapper[1].hide() - - -def start(arg): - appQt = QtGui.QApplication(arg.argv) + print(event) + # Todo: fully implement MPLCanvas + #if text == 'c': + # self.swapper = self.swapper[::-1] + # self.iwidget = self.swapper[0] + # self.swapper[0].show() + # self.swapper[0].setFocus() + # self.swapper[1].hide() + + +def start(args=None): + if args is None: + args = sys.argv[1:] + appQt = QApplication(args) win = MainWindow() win.show() appQt.exec_() + if __name__ == '__main__': - print('wradview: Calling module as main...') + start() diff --git a/wradvis/mplcanvas.py b/wradvis/mplcanvas.py index fcbcb70..b7ef892 100644 --- a/wradvis/mplcanvas.py +++ b/wradvis/mplcanvas.py @@ -1,18 +1,18 @@ -# -*- coding: utf-8 -*- -# ----------------------------------------------------------------------------- -# Copyright (c) 2016, wradlib Development Team. All Rights Reserved. -# Distributed under the MIT License. See LICENSE.txt for more info. -# ----------------------------------------------------------------------------- #!/usr/bin/env python +# Copyright (c) 2016-2018, wradlib developers. +# Distributed under the MIT License. See LICENSE.txt for more info. import numpy as np import matplotlib -matplotlib.use('Qt4Agg') +matplotlib.use('Qt5Agg') + +from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5.QtWidgets import (QMainWindow, QApplication, QSplitter, QAction, + QDockWidget, QSizePolicy, QWidget, QHBoxLayout) -from PyQt4 import QtGui, QtCore -from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure from matplotlib.cm import get_cmap from mpl_toolkits.axes_grid1 import make_axes_locatable @@ -34,8 +34,8 @@ def __init__(self):#, parent, props): # we define the widget as expandable FigureCanvas.setSizePolicy(self, - QtGui.QSizePolicy.Expanding, - QtGui.QSizePolicy.Expanding) + QSizePolicy.Expanding, + QSizePolicy.Expanding) # notify the system of updated policy FigureCanvas.updateGeometry(self) @@ -55,6 +55,7 @@ def __init__(self):#, parent, props): self.ax.set_xlim([grid[..., 0].min(), grid[..., 0].max()]) self.ax.set_ylim([grid[..., 1].min(), grid[..., 1].max()]) self._mouse_position = None + self._mouse_press_position = (0, 0) self.create_cities() @@ -101,14 +102,14 @@ def on_key_press(self, event): def on_move(self, event): if event.inaxes: ax = event.inaxes # the axes instance - print('data coords %f %f' % (event.xdata, event.ydata)) + #print('data coords %f %f' % (event.xdata, event.ydata)) self._mouse_position = np.array([event.xdata, event.ydata]) self.mouse_moved.emit(event) -class MplWidget(QtGui.QWidget): +class MplWidget(QWidget): def __init__(self): - QtGui.QWidget.__init__(self) + QWidget.__init__(self) self.rcanvas = MplCanvas() self.pcanvas = MplCanvas() @@ -129,7 +130,7 @@ def __init__(self): #self.splitter.setStretchFactor(0, 1) #self.splitter.setStretchFactor(1, 1) #self.splitter.setStretchFactor(2, 0) - self.hbl = QtGui.QHBoxLayout() + self.hbl = QHBoxLayout() self.hbl.addWidget(self.swapper['R']) self.hbl.addWidget(self.swapper['P']) self.setLayout(self.hbl) diff --git a/wradvis/properties.py b/wradvis/properties.py index 09bf87a..63c223b 100644 --- a/wradvis/properties.py +++ b/wradvis/properties.py @@ -1,9 +1,6 @@ -# -*- coding: utf-8 -*- -# ----------------------------------------------------------------------------- -# Copyright (c) 2016, wradlib Development Team. All Rights Reserved. -# Distributed under the MIT License. See LICENSE.txt for more info. -# ----------------------------------------------------------------------------- #!/usr/bin/env python +# Copyright (c) 2016-2018, wradlib developers. +# Distributed under the MIT License. See LICENSE.txt for more info. """ """ @@ -11,15 +8,24 @@ import os import glob from datetime import datetime as dt +import numpy as np +from urllib.request import urlopen + +from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5.QtWidgets import (QMainWindow, QApplication, QLabel, QWidget, + QSpinBox, QComboBox, QCheckBox, QFrame, QSlider, + QGridLayout, QVBoxLayout, QToolButton, + QPushButton, QStyle, QFileDialog, QSizePolicy, + QStyleOptionSlider) +from PyQt5.QtGui import QFontMetrics, QPainter, QPalette -from PyQt4 import QtGui, QtCore -from PyQt4.QtGui import QLabel, QFontMetrics, QPainter from wradvis import utils from wradvis.config import conf +from wradvis.glcanvas import RadolanLineWidget -class TimeSlider(QtGui.QSlider): +class TimeSlider(QSlider): """ This software is OSI Certified Open Source Software. OSI Certified is a certification mark of the Open Source Initiative. @@ -66,8 +72,8 @@ def __init__(self, *args): self._low = self.minimum() self._high = self.maximum() - self.pressed_control = QtGui.QStyle.SC_None - self.hover_control = QtGui.QStyle.SC_None + self.pressed_control = QStyle.SC_None + self.hover_control = QStyle.SC_None self.click_offset = 0 # 0 for the low, 1 for the high, -1 for both @@ -92,39 +98,38 @@ def paintEvent(self, event): # based on # http://qt.gitorious.org/qt/qt/blobs/master/src/gui/widgets/qslider.cpp - painter = QtGui.QPainter(self) - style = QtGui.QApplication.style() + painter = QPainter(self) + style = QApplication.style() for i, value in enumerate([self._low, self._high]): - opt = QtGui.QStyleOptionSlider() + opt = QStyleOptionSlider() self.initStyleOption(opt) # Only draw the groove for the first slider so it doesn't get drawn # on top of the existing ones every time if i == 0: - opt.subControls = QtGui.QStyle.SC_SliderGroove | QtGui.QStyle.SC_SliderHandle + opt.subControls = QStyle.SC_SliderGroove | QStyle.SC_SliderHandle else: - opt.subControls = QtGui.QStyle.SC_SliderHandle + opt.subControls = QStyle.SC_SliderHandle - print(self.tickPosition(), self.NoTicks) if self.tickPosition() != self.NoTicks: - opt.subControls |= QtGui.QStyle.SC_SliderTickmarks + opt.subControls |= QStyle.SC_SliderTickmarks if self.pressed_control: opt.activeSubControls = self.pressed_control - opt.state |= QtGui.QStyle.State_Sunken + opt.state |= QStyle.State_Sunken else: opt.activeSubControls = self.hover_control opt.sliderPosition = value opt.sliderValue = value - style.drawComplexControl(QtGui.QStyle.CC_Slider, opt, painter, self) + style.drawComplexControl(QStyle.CC_Slider, opt, painter, self) def mousePressEvent(self, event): event.accept() - style = QtGui.QApplication.style() + style = QApplication.style() button = event.button() # In a normal slider control, when the user clicks on a point in the @@ -134,7 +139,7 @@ def mousePressEvent(self, event): # slider parts if button: - opt = QtGui.QStyleOptionSlider() + opt = QStyleOptionSlider() self.initStyleOption(opt) self.active_slider = -1 @@ -152,7 +157,7 @@ def mousePressEvent(self, event): break if self.active_slider < 0: - self.pressed_control = QtGui.QStyle.SC_SliderHandle + self.pressed_control = QStyle.SC_SliderHandle self.click_offset = self.__pixelPosToRangeValue(self.__pick(event.pos())) self.triggerAction(self.SliderMove) self.setRepeatAction(self.SliderNoAction) @@ -160,13 +165,13 @@ def mousePressEvent(self, event): event.ignore() def mouseMoveEvent(self, event): - if self.pressed_control != QtGui.QStyle.SC_SliderHandle: + if self.pressed_control != QStyle.SC_SliderHandle: event.ignore() return event.accept() new_pos = self.__pixelPosToRangeValue(self.__pick(event.pos())) - opt = QtGui.QStyleOptionSlider() + opt = QStyleOptionSlider() self.initStyleOption(opt) if self.active_slider < 0: @@ -208,9 +213,9 @@ def __pick(self, pt): def __pixelPosToRangeValue(self, pos): - opt = QtGui.QStyleOptionSlider() + opt = QStyleOptionSlider() self.initStyleOption(opt) - style = QtGui.QApplication.style() + style = QApplication.style() gr = style.subControlRect(style.CC_Slider, opt, style.SC_SliderGroove, self) sr = style.subControlRect(style.CC_Slider, opt, style.SC_SliderHandle, self) @@ -229,7 +234,6 @@ def __pixelPosToRangeValue(self, pos): opt.upsideDown) - class LongLabel(QLabel): def paintEvent( self, event ): painter = QPainter(self) @@ -242,49 +246,80 @@ def paintEvent( self, event ): painter.drawText(self.rect(), self.alignment(), elided) -class DockBox(QtGui.QWidget): - def __init__(self, parent=None): +class DockBox(QWidget): + def __init__(self, parent=None, size_pol=(QSizePolicy.Fixed, + QSizePolicy.Fixed)): super(DockBox, self).__init__(parent) - self.layout = QtGui.QGridLayout() + self.layout = QGridLayout() self.setLayout(self.layout) - self.setSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + self.setSizePolicy(size_pol[0], size_pol[1]) self.props = parent.props +class GraphBox(DockBox): + def __init__(self, parent=None, size_pol=(QSizePolicy.Fixed, + QSizePolicy.Fixed)): + super(GraphBox, self).__init__(parent) + + self.parent = parent + self.graph = RadolanLineWidget(self) + self.layout.addWidget(self.graph, 0, 0) + self.setSizePolicy(size_pol[0], size_pol[1]) + + def connect_signals(self): + self.graph.connect_signals() + + class MouseBox(DockBox): + + signal_toggle_Cursor = QtCore.pyqtSignal(int, name='toggleCursor') + def __init__(self, parent=None): super(MouseBox, self).__init__(parent) self.parent = parent self.r0 = utils.get_radolan_origin() - self.mousePointLabel = QtGui.QLabel("Mouse Position", self) - self.mousePointXYLabel = QtGui.QLabel("XY", self) - self.mousePointLLLabel = QtGui.QLabel("LL", self) - self.mousePointXY = QtGui.QLabel("", self) - self.mousePointLL = QtGui.QLabel("", self) - self.hline2 = QtGui.QFrame() - self.hline2.setFrameShape(QtGui.QFrame.HLine) - self.hline2.setFrameShadow(QtGui.QFrame.Sunken) + self.mousePointLabel = QLabel("Mouse Position", self) + self.mousePointXYLabel = QLabel("XY", self) + self.mousePointLLLabel = QLabel("LL", self) + self.mousePointXY = QLabel("", self) + self.mousePointLL = QLabel("", self) + self.mousePointS = QLabel("", self) + self.curCheckBox = QCheckBox() + self.curCheckBox.stateChanged.connect(self.signal_toggle_Cursor) + + self.hline2 = QFrame() + self.hline2.setFrameShape(QFrame.HLine) + self.hline2.setFrameShadow(QFrame.Sunken) self.layout.addWidget(self.mousePointLabel, 0, 0) self.layout.addWidget(self.mousePointXYLabel, 0, 1) self.layout.addWidget(self.mousePointXY, 0, 2) self.layout.addWidget(self.mousePointLLLabel, 1, 1) self.layout.addWidget(self.mousePointLL, 1, 2) - self.layout.addWidget(self.hline2, 2, 0, 1, 3) + self.layout.addWidget(QLabel("XYsel", self), 2, 1) + self.layout.addWidget(self.mousePointS, 2, 2) + self.layout.addWidget(QLabel("Activate Cursor", self), 3, 0) + self.layout.addWidget(self.curCheckBox, 3, 1) + self.layout.addWidget(self.hline2, 4, 0, 1, 3) # connect to signal self.parent.rwidget.rcanvas.mouse_moved.connect(self.mouse_moved) + self.parent.rwidget.rcanvas.mouse_pressed.connect(self.mouse_moved) self.parent.rwidget.pcanvas.mouse_moved.connect(self.mouse_moved) self.parent.mwidget.rcanvas.mouse_moved.connect(self.mouse_moved) def mouse_moved(self, event): # todo: check if originating from mpl and adapt self.r0 correctly point = self.parent.iwidget.canvas._mouse_position + point1 = self.parent.iwidget.canvas._mouse_press_position self.mousePointXY.setText( "({0:d}, {1:d})".format(int(point[0]), int(point[1]))) + self.mousePointS.setText( + "({0:d}, {1:d})".format(int(point1[0]), int(point1[1]))) + # Todo: move this all to utils and use a generalized # ll-retrieving function if self.parent.props.product != 'DX': @@ -300,22 +335,22 @@ class SourceBox(DockBox): def __init__(self, parent=None): super(SourceBox, self).__init__(parent) - palette = QtGui.QPalette() + palette = QPalette() self.setStyleSheet(""" QMenuBar { font-size:13px; }""") # Horizontal line - self.hline = QtGui.QFrame() - self.hline.setFrameShape(QtGui.QFrame.HLine) - self.hline.setFrameShadow(QtGui.QFrame.Sunken) + self.hline = QFrame() + self.hline.setFrameShape(QFrame.HLine) + self.hline.setFrameShadow(QFrame.Sunken) self.dirname = "None" #conf["dirs"]["time_slider"] self.dirLabel = LongLabel(self.dirname) - self.layout.addWidget(LongLabel("Current time_slider directory"), 0, 0, 1, 7) + self.layout.addWidget(LongLabel("Current data directory"), 0, 0, 1, 7) self.layout.addWidget(self.dirLabel, 1, 0, 1, 7) self.dirLabel.setFixedWidth(200) - palette.setColor(QtGui.QPalette.Foreground, QtCore.Qt.darkGreen) + palette.setColor(QPalette.Foreground, QtCore.Qt.darkGreen) self.dirLabel.setPalette(palette) self.props.props_changed.connect(self.update_label) @@ -328,32 +363,35 @@ def update_label(self): class MediaBox(DockBox): signal_playpause_changed = QtCore.pyqtSignal(name='startstop') - signal_time_slider_changed = QtCore.pyqtSignal(int, name='dataslidervalueChanged') - signal_speed_changed = QtCore.pyqtSignal(name='speedChanged') + signal_time_slider_changed = QtCore.pyqtSignal(int, name='timeChanged') + signal_speed_changed = QtCore.pyqtSignal(int, name='speedChanged') + signal_time_properties_changed = QtCore.pyqtSignal(name='timepropsChanged') def __init__(self, parent=None): super(MediaBox, self).__init__(parent) + self.parent = parent + # Time Slider - self.time_slider = QtGui.QSlider(QtCore.Qt.Horizontal) + self.time_slider = QSlider(QtCore.Qt.Horizontal) self.time_slider.setMinimum(0) self.time_slider.setTickInterval(1) self.time_slider.setSingleStep(1) self.time_slider.valueChanged.connect(self.time_slider_moved) - self.current_date = QtGui.QLabel("1900-01-01") - self.current_time = QtGui.QComboBox() + self.current_date = QLabel("1900-01-01") + self.current_time = QComboBox() self.current_time.currentIndexChanged.connect(self.current_time_changed) # Range Slider self.range = TimeSlider(QtCore.Qt.Horizontal) - self.range_start = QtGui.QComboBox() - self.range_end = QtGui.QComboBox() + self.range_start = QComboBox() + self.range_end = QComboBox() self.range.signal_range_moved.connect(self.range_update) self.range_start.currentIndexChanged.connect(self.range_changed) self.range_end.currentIndexChanged.connect(self.range_changed) # Speed Slider - self.speed = QtGui.QSlider(QtCore.Qt.Horizontal) + self.speed = QSlider(QtCore.Qt.Horizontal) self.speed.setMinimum(0) self.speed.setMaximum(1000) self.speed.setTickInterval(10) @@ -362,57 +400,63 @@ def __init__(self, parent=None): # layout self.createMediaButtons() - self.hline0 = QtGui.QFrame() - self.hline0.setFrameShape(QtGui.QFrame.HLine) - self.hline0.setFrameShadow(QtGui.QFrame.Sunken) - self.hline1 = QtGui.QFrame() - self.hline1.setFrameShape(QtGui.QFrame.HLine) - self.hline1.setFrameShadow(QtGui.QFrame.Sunken) + self.hline0 = QFrame() + self.hline0.setFrameShape(QFrame.HLine) + self.hline0.setFrameShadow(QFrame.Sunken) + self.hline1 = QFrame() + self.hline1.setFrameShape(QFrame.HLine) + self.hline1.setFrameShadow(QFrame.Sunken) self.layout.addWidget(self.hline0, 0, 0, 1, 5) - self.layout.addWidget(QtGui.QLabel("Date"), 1, 0, 1, 1) + self.layout.addWidget(QLabel("Date"), 1, 0, 1, 1) self.layout.addWidget(self.current_date, 1, 1, 1, 2) self.layout.addWidget(self.playPauseButton, 2, 1) self.layout.addWidget(self.rewButton, 2, 2) self.layout.addWidget(self.fwdButton, 2, 3) - self.layout.addWidget(QtGui.QLabel("Start Time"), 3, 1, 1, 1) - self.layout.addWidget(QtGui.QLabel("Current Time"), 3, 2, 1, 1) - self.layout.addWidget(QtGui.QLabel("Stop Time"), 3, 3, 1, 1) + self.layout.addWidget(QLabel("Start Time"), 3, 1, 1, 1) + self.layout.addWidget(QLabel("Current Time"), 3, 2, 1, 1) + self.layout.addWidget(QLabel("Stop Time"), 3, 3, 1, 1) self.layout.addWidget(self.range_start, 4, 1, 1, 1) self.layout.addWidget(self.current_time, 4, 2, 1, 1) self.layout.addWidget(self.range_end, 4, 3, 1, 1) - self.layout.addWidget(QtGui.QLabel("Time"), 5, 0, 1, 1) + self.layout.addWidget(QLabel("Time"), 5, 0, 1, 1) self.layout.addWidget(self.time_slider, 5, 1, 1, 4) - self.layout.addWidget(QtGui.QLabel("Range"), 6, 0, 1, 1) + self.layout.addWidget(QLabel("Range"), 6, 0, 1, 1) self.layout.addWidget(self.range, 6, 1, 1, 4) - self.layout.addWidget(QtGui.QLabel("Speed"), 7, 0, 1, 1) + self.layout.addWidget(QLabel("Speed"), 7, 0, 1, 1) self.layout.addWidget(self.speed, 7, 1, 1, 4) self.layout.addWidget(self.hline1, 8, 0, 1, 5) + + def connect_signals(self): + # Todo: this seems not the correct way of doing this, needs fixing + # there must be a nicer method, than traversing over objects self.props.props_changed.connect(self.update_props) + self.props.parent.timer.timeout.connect(self.seekforward) + self.parent.graphbox.graph.canvas.mouse_double_clicked.connect(self.current_time_changed) def createMediaButtons(self): iconSize = QtCore.QSize(18, 18) - self.playPauseButton = self.createButton(QtGui.QStyle.SP_MediaPlay, + self.playPauseButton = self.createButton(QStyle.SP_MediaPlay, iconSize, "Play", self.playpause) - self.fwdButton = self.createButton(QtGui.QStyle.SP_MediaSeekForward, + self.fwdButton = self.createButton(QStyle.SP_MediaSeekForward, iconSize, "SeekForward", self.seekforward) - self.rewButton = self.createButton(QtGui.QStyle.SP_MediaSeekBackward, + self.rewButton = self.createButton(QStyle.SP_MediaSeekBackward, iconSize, "SeekBackward", self.seekbackward) def createButton(self, style, size, tip, cfunc): - button = QtGui.QToolButton() + button = QToolButton() button.setIcon(self.style().standardIcon(style)) button.setIconSize(size) button.setToolTip(tip) @@ -420,12 +464,20 @@ def createButton(self, style, size, tip, cfunc): return button def speed_changed(self, position): - self.signal_speed_changed.emit() + self.signal_speed_changed.emit(position) + + def time_slider_moved(self, pos): + self.current_time.setCurrentIndex(pos) + + # check if data already read + if self.current_time.currentText() == '--': + self.props.add_data(pos) + time = utils.get_dt(self.props.mem.variables['time'][pos]) + self.range_start.setItemText(pos, time.strftime("%H:%M")) + self.current_time.setItemText(pos, time.strftime("%H:%M")) + self.range_end.setItemText(pos, time.strftime("%H:%M")) - def time_slider_moved(self, position): - self.props.actualFrame = position - self.current_time.setCurrentIndex(position) - self.signal_time_slider_changed.emit(position) + self.signal_time_slider_changed.emit(pos) def seekforward(self): if self.time_slider.value() >= self.range.high(): @@ -443,30 +495,41 @@ def playpause(self): if self.playPauseButton.toolTip() == 'Play': self.playPauseButton.setToolTip("Pause") self.playPauseButton.setIcon( - self.style().standardIcon(QtGui.QStyle.SP_MediaPause)) + self.style().standardIcon(QStyle.SP_MediaPause)) else: self.playPauseButton.setToolTip("Play") self.playPauseButton.setIcon( - self.style().standardIcon(QtGui.QStyle.SP_MediaPlay)) + self.style().standardIcon(QStyle.SP_MediaPlay)) self.signal_playpause_changed.emit() def update_props(self): + + stime = utils.get_dt(self.props.mem.variables['time'][0]) + etime = utils.get_dt(self.props.mem.variables['time'][-1]) + try: + rtime = [utils.get_dt(item).strftime("%H:%M") for item in self.props.mem.variables['time'][1:-1]] + except np.ma.core.MaskError: + rtime = [str(item) for item in self.props.mem.variables['time'][1:-1]] + self.range_start.clear() - self.range_start.addItems( - [item['datetime'].strftime("%H:%M") for item in self.props.cube]) + self.range_start.addItem(stime.strftime("%H:%M")) + self.range_start.addItems(rtime) + self.range_start.addItem(etime.strftime("%H:%M")) self.range_end.clear() - self.range_end.addItems( - [item['datetime'].strftime("%H:%M") for item in self.props.cube]) + self.range_end.addItem(stime.strftime("%H:%M")) + self.range_end.addItems(rtime) + self.range_end.addItem(etime.strftime("%H:%M")) self.current_time.clear() - self.current_time.addItems( - [item['datetime'].strftime("%H:%M") for item in self.props.cube]) - self.time_slider.setMaximum(self.props.frames) + self.current_time.addItem(stime.strftime("%H:%M")) + self.current_time.addItems(rtime) + self.current_time.addItem(etime.strftime("%H:%M")) + self.time_slider.setMaximum(len(rtime) + 1) self.time_slider.setValue(0) - self.current_date.setText(self.props.cube[0]['datetime'].strftime("%Y-%M-%d")) + self.current_date.setText(stime.strftime("%Y-%M-%d")) self.range.setMinimum(0) - self.range.setMaximum(self.props.frames) + self.range.setMaximum(len(rtime) + 1) self.range.setLow(0) - self.range.setHigh(self.props.frames) + self.range.setHigh(len(rtime) + 1) self.range_update(self.range.low(), self.range.high()) def range_update(self, low, high): @@ -476,13 +539,14 @@ def range_update(self, low, high): def range_changed(self): self.range.setLow(self.range_start.currentIndex()) self.range.setHigh(self.range_end.currentIndex()) + self.signal_time_properties_changed.emit() def current_time_changed(self, value): - self.time_slider.blockSignals(True) - self.time_slider.setValue(value) - self.time_slider.blockSignals(False) - self.props.actualFrame = value - self.signal_time_slider_changed.emit(value) + try: + self.time_slider.setValue(value) + except TypeError: + self.time_slider.setValue(round(self.parent.graphbox.graph.canvas._mouse_position[0])) + self.signal_time_properties_changed.emit() # Properties @@ -496,16 +560,17 @@ def __init__(self, parent=None): super(Properties, self).__init__(parent) self.parent = parent - self.update_props() + self.mem = None + #self.update_props() def set_datadir(self): - f = QtGui.QFileDialog.getExistingDirectory(self.parent, + f = QFileDialog.getExistingDirectory(self.parent, "Select a Folder", - "/automount/time_slider/radar/dwd", - QtGui.QFileDialog.ShowDirsOnly) + "/automount/radar/dwd_new", + QFileDialog.ShowDirsOnly) if os.path.isdir(f): - conf["dirs"]["time_slider"] = str(f) + conf["dirs"]["data"] = str(f) try: _ , meta = utils.read_dx(glob.glob(os.path.join(self.dir, "raa0*"))[0]) except ValueError: @@ -514,40 +579,86 @@ def set_datadir(self): self.update_props() def save_conf(self): - name = QtGui.QFileDialog.getSaveFileName(self.parent, 'Save File') + name = QFileDialog.getSaveFileName(self.parent, 'Save File') + name = name[0] with open(name, "w") as f: conf.write(f) def open_conf(self): - name = QtGui.QFileDialog.getOpenFileName(self.parent, 'Open project') + name = QFileDialog.getOpenFileName(self.parent, 'Open project') + name = name[0] with open(name, "r") as f: conf.read_file(f) self.update_props() + def load_data(self): + newfile = QFileDialog.getOpenFileName(self.parent, + 'Save NetCDF File', '', + 'netCDF (*.nc)') + if self.mem is not None: + self.mem.close() + newfile = newfile[0] + self.mem = utils.open_ncdf(newfile) + conf["source"]["product"] = self.mem.variables['data'].source + # activate the correct canvas (grid or polar) + self.product = conf["source"]["product"] + self.parent.iwidget.set_canvas(self.product) + self.signal_props_changed.emit(0) + + def save_data(self): + newfile = QFileDialog.getSaveFileName(self.parent, + 'Save NetCDF File', + '', + 'netCDF (*.nc)') + newfile = newfile[0] + oldfile = os.path.abspath(self.mem.filepath()) + self.mem.close() + os.rename(oldfile, newfile) + self.mem = utils.open_ncdf(newfile) + def update_props(self): self.dir = conf["dirs"]["data"] self.product = conf["source"]["product"] + + # setting reader function according to data type + if self.product == 'DX': + self.rfunc = utils.read_dx + else: + self.rfunc = utils.read_radolan + + # activate the correct canvas (grid or polar) self.parent.iwidget.set_canvas(self.product) + self.clim = (conf.get("vis", "cmin"), conf.get("vis", "cmax")) self.parent.iwidget.set_clim(self.clim) self.loc = conf.get("source", "loc") self.filelist = glob.glob(os.path.join(self.dir, "raa0*{0}*".format(self.loc))) self.frames = len(self.filelist) - 1 - self.actualFrame = 0 - self.cube = self.create_data_cube() + if self.mem is not None: + self.mem.close() + self.mem = self.create_nc_dataset() + #self.mem.filepath() + #print(dir(self.mem))#.disk_format) self.signal_props_changed.emit(0) - def create_data_cube(self): + def create_nc_dataset(self): ''' First attempt to create some time_slider layer Here we just add the metadata dictionaries ''' - cube = [] - for name in self.filelist: - if self.product == 'DX': - _, meta = utils.read_dx(name) - else: - _, meta = utils.read_radolan(name) - cube.append(meta) - return cube + mem = None + + + data, meta = self.rfunc(self.filelist[0]) + + mem = utils.create_ncdf('tmpfile.nc', meta, units='normal') + utils.add_ncdf(mem, data, 0, meta) + data, meta = self.rfunc(self.filelist[-1]) + utils.add_ncdf(mem, data, self.frames, meta) + + return mem + + def add_data(self, pos): + data, meta = self.rfunc(self.filelist[pos]) + utils.add_ncdf(self.mem, data, pos, meta) diff --git a/wradvis/utils.py b/wradvis/utils.py index 4baca26..609f3ef 100644 --- a/wradvis/utils.py +++ b/wradvis/utils.py @@ -1,15 +1,15 @@ -# -*- coding: utf-8 -*- -# ----------------------------------------------------------------------------- -# Copyright (c) 2016, wradlib Development Team. All Rights Reserved. -# Distributed under the MIT License. See LICENSE.txt for more info. -# ----------------------------------------------------------------------------- #!/usr/bin/env python +# Copyright (c) 2016-2018, wradlib developers. +# Distributed under the MIT License. See LICENSE.txt for more info. """ """ import wradlib as wrl import numpy as np +import netCDF4 as nc +import datetime as dt +import pytz from wradvis.config import conf @@ -32,6 +32,7 @@ def radolan_to_wgs84(coords): projection_target=proj_wgs) return ll + def dx_to_wgs84(coords): # currently works only with radar feldberg @@ -48,28 +49,30 @@ def dx_to_wgs84(coords): radius = wrl.georef.get_earth_radius(radar["lat"], proj_radar) - lon, lat, height = wrl.georef.polar2lonlatalt_n(coords[1] * 1000, + lonlatalt = wrl.georef.spherical_to_proj(coords[1] * 1000, coords[0], 0.8, sitecoords, re=radius, - ke=4. / 3.) - - return np.hstack((lon, lat)) + ke=4. / 3. + ) + return lonlatalt[..., 0:2] def get_radolan_grid(): return wrl.georef.get_radolan_grid() + def get_radolan_origin(): return wrl.georef.get_radolan_grid()[0, 0] -def read_radolan(f, missing=0, loaddata=True): - return wrl.io.read_RADOLAN_composite(f, missing=missing, loaddata=loaddata) +def read_radolan(f, missing=-9999, loaddata=True): + return read_RADOLAN_composite(f, missing=missing, loaddata=loaddata) + def read_dx(f, missing=0, loaddata=True): - return wrl.io.readDX(f) + return wrl.io.read_dx(f) def get_cities_coords(): @@ -87,3 +90,410 @@ def get_cities_coords(): return cities +# just for testing purposes, this can be used from wradlib when it is finalized +# and adapted +def read_RADOLAN_composite(f, missing=-9999, loaddata=True): + """Read quantitative radar composite format of the German Weather Service + + The quantitative composite format of the DWD (German Weather Service) was + established in the course of the `RADOLAN project ` + and includes several file types, e.g. RX, RO, RK, RZ, RP, RT, RC, RI, RG, PC, + PG and many, many more. + (see format description on the RADOLAN project homepage :cite:`DWD2009`). + + At the moment, the national RADOLAN composite is a 900 x 900 grid with 1 km + resolution and in polar-stereographic projection. There are other grid resolutions + for different composites (eg. PC, PG) + + **Beware**: This function already evaluates and applies the so-called PR factor which is + specified in the header section of the RADOLAN files. The raw values in an RY file + are in the unit 0.01 mm/5min, while read_RADOLAN_composite returns values + in mm/5min (i. e. factor 100 higher). The factor is also returned as part of + attrs dictionary under keyword "precision". + + Parameters + ---------- + fname : path to the composite file + + missing : value assigned to no-data cells + + Returns + ------- + output : tuple of two items (data, attrs) + - data : numpy array of shape (number of rows, number of columns) + - attrs : dictionary of metadata information from the file header + + """ + + NODATA = missing + mask = 0xFFF # max value integer + + # If a file name is supplied, get a file handle + try: + header = wrl.io.radolan.read_radolan_header(f) + except AttributeError: + f = wrl.io.radolan.get_radolan_filehandle(f) + header = wrl.io.radolan.read_radolan_header(f) + + attrs = wrl.io.radolan.parse_dwd_composite_header(header) + + if not loaddata: + f.close() + return None, attrs + + attrs["nodataflag"] = NODATA + + #if not attrs["radarid"] == "10000": + # warnings.warn("WARNING: You are using function e" + + # "wradlib.io.read_RADOLAN_composit for a non " + + # "composite file.\n " + + # "This might work...but please check the validity " + + # "of the results") + + # read the actual data + indat = wrl.io.read_radolan_binary_array(f, attrs['datasize']) + + # data handling taking different product types into account + # RX, EX, WX 'qualitative', temporal resolution 5min, RVP6-units [dBZ] + if attrs["producttype"] in ["RX", "EX", "WX"]: + #convert to 8bit unsigned integer + arr = np.frombuffer(indat, np.uint8).astype(np.uint8) + # clutter & nodata + cluttermask = np.where(arr == 249)[0] + nodatamask = np.where(arr == 250)[0] + #attrs['cluttermask'] = np.where(arr == 249)[0] + + #arr = np.where(arr >= 249, np.int32(255), arr) + + elif attrs['producttype'] in ["PG", "PC"]: + arr = wrl.io.decode_radolan_runlength_array(indat, attrs) + else: + # convert to 16-bit integers + arr = np.frombuffer(indat, np.uint16).astype(np.uint16) + # evaluate bits 13, 14, 15 and 16 + secondary = np.where(arr & 0x1000)[0] + attrs['secondary'] = np.where(arr & 0x1000)[0] + #attrs['nodata'] = np.where(arr & 0x2000)[0] + nodatamask = np.where(arr & 0x2000)[0] + negative = np.where(arr & 0x4000)[0] + cluttermask = np.where(arr & 0x8000)[0] + #attrs['cluttermask'] = np.where(arr & 0x8000)[0] + + # mask out the last 4 bits + arr = arr & mask + + # consider negative flag if product is RD (differences from adjustment) + if attrs["producttype"] == "RD": + # NOT TESTED, YET + arr[negative] = -arr[negative] + # apply precision factor + # this promotes arr to float if precision is float + #arr = arr * attrs["precision"] + # set nodata value# + #arr[attrs['secondary']] = np.int32(4096) + #arr[nodata] = np.int32(4096)#NODATA + + if nodatamask is not None: + attrs['nodatamask'] = nodatamask + if cluttermask is not None: + attrs['cluttermask'] = cluttermask + #arr[np.where(arr == 2500)[0]] = np.int32(4096) + #arr[np.where(arr == 2490)[0]] = np.int32(4096) + #arr[nodata] = np.int32(0) + #arr[clutter] = np.int32(65535) + # anyway, bring it into right shape + arr = arr.reshape((attrs["nrow"], attrs["ncol"])) + #arr = arr.reshape((attrs["nrow"], attrs["ncol"])) + + return arr, attrs + + +def open_ncdf(filename): + return nc.Dataset(filename, 'r', format='NETCDF4') + + +def get_netcdf_varattrs(attrs, units='original'): + product = attrs['producttype'] + precision = attrs.get('precision', 1) + if product in ['DX', 'RX', 'EX']: + if units == 'original': + scale_factor = None + add_offset = None + unit = 'RVP6' + else: + scale_factor = np.float32(0.5) + add_offset = np.float32(-32.5) + unit = 'dBZ' + + valid_min = np.int32(0) + valid_max = np.int32(255) + missing_value = np.int32(255) + fillvalue = np.int32(255) + vtype = 'u1' + standard_name = 'equivalent_reflectivity_factor' + long_name = 'equivalent_reflectivity_factor' + + elif product in ['RY', 'RZ', 'EY', 'EZ']: + if units == 'original': + scale_factor = None + add_offset = None + unit = '0.01mm 5min-1' + elif units == 'normal': + scale_factor = np.float32(precision * 3600) + add_offset = np.float(0) + unit = 'mm h-1' + else: + scale_factor = np.float32(precision / 1000) + add_offset = np.float(0) + unit = 'm s-1' + + valid_min = np.int32(0) + valid_max = np.int32(4095) + missing_value = np.int32(4096) + fillvalue = np.int32(65535) + vtype = 'u2' + standard_name = 'rainfall_amount' + long_name = 'rainfall_amount' + + elif product in ['RH', 'RB', 'RW', 'RL', 'RU', 'EH', 'EB', 'EW']: + if units == 'original': + scale_factor = None + add_offset = None + unit = '0.1mm h-1' + elif units == 'normal': + scale_factor = np.float32(precision) + add_offset = np.float(0.) + unit = 'mm h-1' + else: + scale_factor = np.float32(precision / (int * 1000)) + add_offset = np.float(0) + unit = 'm s-1' + + valid_min = np.int32(0) + valid_max = np.int32(4095) + missing_value = np.int32(4096) + fillvalue = np.int32(65535) + vtype = 'u2' + standard_name = 'rainfall_amount' + long_name = 'rainfall_amount' + + elif product in ['SQ', 'SH', 'SF']: + scale_factor = np.float32(precision) + add_offset = np.float(0.) + valid_min = np.int32(0) + valid_max = np.int32(4095) + missing_value = np.int32(4096) + fillvalue = np.int32(65535) + vtype = 'u2' + standard_name = 'rainfall_amount' + long_name = 'rainfall_amount' + if int == (360 * 60): + unit = 'mm 6h-1' + elif int == (720 * 60): + unit = 'mm 12h-1' + elif int == (1440 * 60): + unit = 'mm d-1' + + vattr = {'scale_factor': scale_factor, 'add_offset': add_offset, + 'valid_min': valid_min, 'valid_max': valid_max, + 'missing_value': missing_value, 'fillvalue': fillvalue, + 'vtype': vtype, 'standard_name': standard_name, + 'long_name': long_name, 'unit':unit} + + return vattr + + +def create_ncdf(filename, attrs, units='original'): + + product = attrs['producttype'] + if product not in ['DX']: + nx = attrs['nrow'] + ny = attrs['ncol'] + version = attrs['radolanversion'] + else: + nx = attrs['clutter'].shape[0] + ny = attrs['clutter'].shape[1] + version = attrs['version'] + + #precision = attrs['precision'] + + #int = attrs['intervalseconds'] + #nodata = attrs['nodataflag'] + #missing_value = None + + # create NETCDF4 file in memory + id = nc.Dataset(filename, 'w', format='NETCDF4', diskless=True, persist=True) + #id.close() + #id = nc.Dataset(filename, 'a', format='NETCDF4') + + # create dimensions + yid = id.createDimension('y', ny) + xid = id.createDimension('x', nx) + tbid = id.createDimension('nv', 2) + tid = id.createDimension('time', None) + + # create and set the grid x variable that serves as x coordinate + xiid = id.createVariable('x', 'f4', ('x')) + xiid.axis = 'X' + xiid.units = 'km' + xiid.long_name = 'x coordinate of projection' + xiid.standard_name = 'projection_x_coordinate' + + # create and set the grid y variable that serves as y coordinate + yiid = id.createVariable('y', 'f4', ('y')) + yiid.axis = 'Y' + yiid.units = 'km' + yiid.long_name = 'y coordinate of projection' + yiid.standard_name = 'projection_y_coordinate' + + # create time variable + tiid = id.createVariable('time', 'f8', ('time',)) + tiid.axis = 'T' + tiid.units = 'seconds since 1970-01-01 00:00:00' + tiid.standard_name = 'time' + tiid.bounds = 'time_bnds' + + # create time bounds variable + tbiid = id.createVariable('time_bnds', 'f8', ('time', 'nv',)) + + if product not in ['DX']: + # create grid variable that serves as lon coordinate + lonid = id.createVariable('lon', 'f4', ('x', 'y',), zlib=True, complevel=4) + lonid.units = 'degrees_east' + lonid.standard_name = 'longitude' + lonid.long_name = 'longitude coordinate' + + # create grid variable that serves as lat coordinate + latid = id.createVariable('lat', 'f4', ('x', 'y',), zlib=True, complevel=4) + latid.units = 'degrees_north' + latid.standard_name = 'latitude' + latid.long_name = 'latitude coordinate' + + # create projection variable that defines the projection according to CF-Metadata standards + coordid = id.createVariable('polar_stereographic', 'i4', zlib=True, + complevel=2) + coordid.grid_mapping_name = 'polar_stereographic' + coordid.straight_vertical_longitude_from_pole = np.float32(10.) + coordid.latitude_of_projection_origin = np.float32(90.) + coordid.standard_parallel = np.float32(60.) + coordid.false_easting = np.float32(0.) + coordid.false_northing = np.float32(0.) + coordid.earth_model_of_projection = 'spherical' + coordid.earth_radius_of_projection = np.float32(6370.04) + coordid.units = 'km' + coordid.ancillary_data = 'grid_latitude grid_longitude' + coordid.long_name = 'polar_stereographic' + + vattr = get_netcdf_varattrs(attrs, units=units) + + prod = id.createVariable('data', vattr['vtype'], ('time', 'x', 'y',), + fill_value=vattr['fillvalue'], zlib=True, complevel=4, + chunksizes=(1, 32, 32)) + # accept data as unsigned byte without scaling, crucial for writing already packed data + #prod.set_auto_maskandscale(False) + prod.units = vattr['unit'] + prod.standard_name = vattr['standard_name'] + prod.long_name = vattr['long_name'] + + if product not in ['DX']: + prod.grid_mapping = 'polar_stereographic' + prod.coordinates = 'lat lon' + + if vattr['scale_factor']: + prod.scale_factor = vattr['scale_factor'] + if vattr['add_offset']: + prod.add_offset = vattr['add_offset'] + if vattr['valid_min']: + prod.valid_min = vattr['valid_min'] + if vattr['valid_max']: + prod.valid_max = vattr['valid_max'] + if vattr['missing_value']: + prod.missing_value = vattr['missing_value'] + prod.version = 'RADOLAN {0}'.format(version) + prod.source = product + prod.comment = 'NO COMMENT' + + id_str1 = id.createVariable('radars', 'S128', ('time',), zlib=True, + complevel=4) + + # create GLOBAL attributes + if product not in ['DX']: + id.Title = 'RADOLAN {0} Composite'.format(product) + id.History = 'Data transferred from RADOLAN composite format to netcdf using wradvis version 0.1 by wradlib developers' + id.Source = 'DWD C-Band Weather Radar Network, Original RADOLAN Data by Deutscher Wetterdienst' + else: + id.Title = '{0} - radarid: {1}'.format(product, attrs['radarid']) + id.History = 'Data transferred from DX format to netcdf using wradvis version 0.1 by wradlib developers' + id.Source = 'DWD C-Band Weather Radar Network, Original DX Data by Deutscher Wetterdienst' + + id.Institution = 'Data owned by Deutscher Wetterdienst' + id.Conventions = 'CF-1.6 where applicable' + utcnow = dt.datetime.utcnow() + id.Processing_date = utcnow.strftime("%Y-%m-%dT%H:%M:%S") + id.Author = '{0}, {1}'.format('Author', 'wradlib@wradlib.org') + id.Comments = 'blank' + id.License = 'DWD Licenses' + + # fill general variables + if product not in ['DX']: + #ny, nx = attrs['ncol'], attrs['nrow'] + radolan_grid_xy = wrl.georef.get_radolan_grid(nx, ny) + xarr = radolan_grid_xy[0, :, 0] + yarr = radolan_grid_xy[:, 0, 1] + radolan_grid_ll = wrl.georef.get_radolan_grid(nx, ny, wgs84=True) + lons = radolan_grid_ll[..., 0] + lats = radolan_grid_ll[..., 1] + id.variables['lat'][:] = lats + id.variables['lon'][:] = lons + else: + xarr = np.arange(nx) + yarr = np.arange(ny) + + id.variables['x'][:] = xarr + id.variables['x'].valid_min = xarr[0] + id.variables['x'].valid_max = xarr[-1] + id.variables['y'][:] = yarr + id.variables['y'].valid_min = yarr[0] + id.variables['y'].valid_max = yarr[-1] + + + return id + + +def add_ncdf(id, data, time_index, attrs): + + if attrs['producttype'] in ['DX']: + mas = True + else: + mas = False + + # remove clutter, nodata and secondary data from raw files + # wrap with if/else if necessary + cluttermask = attrs.get('cluttermask', None) + nodatamask = attrs.get('nodatamask', None) + if cluttermask is not None: + data.flat[cluttermask] = id.variables[ + 'data'].missing_value + if nodatamask is not None: + data.flat[nodatamask] = id.variables[ + 'data'].missing_value + #if attrs['secondary'] is not None: + # data.flat[attrs['secondary']] = id.variables[ + # attrs['producttype'].lower()].missing_value + + + id.variables['data'].set_auto_maskandscale(mas) + id.variables['data'][time_index, :, :] = data + id.variables['data'].set_auto_maskandscale(~mas) + try: + delta = attrs['datetime'] - dt.datetime.utcfromtimestamp(0) + except TypeError: + delta = attrs['datetime'] - dt.datetime.utcfromtimestamp(0).replace(tzinfo=pytz.UTC) + id.variables['time'][time_index] = delta.total_seconds() + id.variables['time_bnds'][time_index, :] = delta.total_seconds() + #id.variables['time_bnds'][time_index,1] = delta.total_seconds() + attrs['intervalseconds'] + #id.variables['radars'][time_index] = ','.join(attrs['radarlocations']) + + +def get_dt(unix): + return dt.datetime.utcfromtimestamp(unix)