diff --git a/.travis.yml b/.travis.yml index ee6c1aa..3866824 100644 --- a/.travis.yml +++ b/.travis.yml @@ -48,7 +48,7 @@ install: - conda create -y -c anaconda --name $PYN python=$PY `head -n1 requirements_conda.txt` - source activate $PYN - python ./service/installreq.py - - python setup.py install + - python setup.py develop # Builds Complete - virtualenv --version - easy_install --version diff --git a/src/rrpam_wds/gui/dialogs.py b/src/rrpam_wds/gui/dialogs.py index c70eefb..c9d218c 100644 --- a/src/rrpam_wds/gui/dialogs.py +++ b/src/rrpam_wds/gui/dialogs.py @@ -1,5 +1,6 @@ from rrpam_wds.gui import set_pyqt_api # isort:skip # NOQA +import math import random import sys @@ -13,6 +14,7 @@ from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar from matplotlib.figure import Figure from numpy import arange +from numpy import array from numpy import interp from numpy import linspace from numpy import pi @@ -42,7 +44,6 @@ CONF.set("plot", "selection/distance", 10.0) # todo: This has to be saved as project's setting file (CONF.save provides that facility) - STYLE = style_generator() @@ -60,16 +61,22 @@ class CurveDialogWithClosable(CurveDialog): """ def __init__(self, *args, **kwargs): - super(CurveDialogWithClosable, self).__init__(*args, **kwargs) + kwargs_ = dict(kwargs) + del kwargs_["mainwindow"] + super(CurveDialogWithClosable, self).__init__(*args, **kwargs_) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) self._can_be_closed = True self.get_plot().set_antialiasing(True) self.add_tools() + self.get_plot().SIG_ITEM_SELECTION_CHANGED.connect(kwargs['mainwindow'].selected_holder) + self.get_plot().SIG_ITEM_REMOVED.connect(self.__item_removed) + self.myplotitems = {} def set_all_private(self): """" Set all current items in the plot private""" [x.set_private(True) for x in self.get_plot().get_items()] - def set_scale(self, axes_limits=None): + def set_axes_limits(self, axes_limits=None): """Sets axes limits axes_limits should be a list with four float values [x0,x1,y0,y1] """ self.get_plot().PREFERRED_AXES_LIMITS = axes_limits # now autoscale @@ -85,6 +92,7 @@ def setClosable(self, closable=True): def closeEvent(self, evnt): if self._can_be_closed: super(CurveDialogWithClosable, self).closeEvent(evnt) + else: evnt.ignore() self.setWindowState(QtCore.Qt.WindowMinimized) @@ -95,47 +103,104 @@ def keyPressEvent(self, e): else: pass + def plot_item(self, id_, data, title="Point", icon="pipe.png"): + raise NotImplemented + + def add_plot_item_to_record(self, id_, item): + """All the plot items register here by calling this method. See also: remove_plot_item_from_record""" + self.myplotitems[id_] = item + + def remove_plot_item_from_record(self, id_): + """When removing a plot item it should be notified to this function. See also: add_plot_item_to_record """ + del self.myplotitems[id_] + + def __item_removed(self, goner): + tmplist = dict(self.myplotitems) + for id_, item in tmplist.items(): + if goner in item: + # first remove related items. + others = [x for x in item if x != goner] + for i in others: + try: + self.get_plot().del_item(i) + except: + pass + try: + self.remove_plot_item_from_record(id_) + except: + pass + class RiskMatrix(CurveDialogWithClosable): SCALE = 10. - def __init__(self, name="Risk Matrix", parent=None, options={}, axes_limits=[0, 15000, 0, 100]): + def __init__(self, name="Risk Matrix", mainwindow=None, parent=None, + units=units["EURO"], options={}, axes_limits=[0, 15000, 0, 100]): if("xlabel" not in options): - options['xlabel'] = "Consequence ($)" + options['xlabel'] = "Consequence (%s)" % (units) if("ylabel" not in options): options['ylabel'] = "Proabability(-)" if("gridparam" not in options): options['gridparam'] = make.gridparam() super(RiskMatrix, self).__init__(edit=False, - icon="guiqwt.svg", + icon="risk.svg", toolbar=True, options=options, parent=parent, panels=None, - wintitle=name) - self.set_scale(axes_limits) + wintitle=name, + mainwindow=mainwindow) + self.setClosable(False) + _axes_limits = axes_limits[0], axes_limits[1] * 1.1, axes_limits[2], axes_limits[3] * 1.1 + self.set_axes_limits(_axes_limits) + + l = make.legend("TR") + self.get_plot().add_item(l) + self.set_all_private() + + def get_ellipse_xaxis(self, consequence, probability): + l = self.get_plot().PREFERRED_AXES_LIMITS + SCALE = self.get_scale(consequence, probability, l) + return consequence - SCALE, probability + SCALE,\ + consequence + SCALE, probability - SCALE + + def get_scale(self, consequence, probability, l): + SCALE = self.SCALE * math.pow(consequence * probability, .25) / math.pow((l[1] * l[3]), .25) + return SCALE + + def get_proper_axis_limits(self): + return - def plot_item(self, consequence, probability, title="Point"): + def plot_item(self, id_, data, title="Point", icon="pipe.png"): global STYLE - ci = make.ellipse(consequence - self.SCALE, probability - self.SCALE, - consequence + self.SCALE, probability + self.SCALE, - title=title) + consequence, probability = data + + ci = make.ellipse(*self.get_ellipse_xaxis(consequence, probability), + title=title) ci.shapeparam._DataSet__icon = u.get_icon('Risk') ci.shapeparam._DataSet__title = title param = ci.shapeparam param.fill.color = QColor('red') + param.sel_fill.color = QColor('purple') + param.sel_fill.alpha = .7 + param.sel_symbol.Marker = "NoSymbol" + param.sel_symbol.Color = QColor('red') update_style_attr('-r', param) param.update_shape(ci) + ci.id_ = id_ # add the ide to the item before plotting. self.get_plot().add_item(ci) - self.get_plot().add_item(make.legend("TR")) - ci.plot().replot() + # now add a label with title + # ci + # la = make.label(title, ci.get_center(), (0, 0), "C") + self.add_plot_item_to_record(id_, [ci]) class NetworkMap(CurveDialogWithClosable): - def __init__(self, name, nodes=None, links=None, parent=None, options={}): + def __init__(self, name="Network Map", mainwindow=None, + nodes=None, links=None, parent=None, options={}): pass if("xlabel" not in options): options['xlabel'] = "X (distance units)" @@ -145,11 +210,15 @@ def __init__(self, name, nodes=None, links=None, parent=None, options={}): gridparam = make.gridparam() super(NetworkMap, self).__init__(edit=False, - icon="guiqwt.svg", + icon="network.svg", toolbar=True, options=dict(gridparam=gridparam), parent=parent, - panels=None) + wintitle=name, + panels=None, + mainwindow=mainwindow) + self.setClosable(False) + # legend = make.legend("TR") # self.get_plot().add_item(legend) self.set_all_private() @@ -177,19 +246,28 @@ def interp_curve(self, x, y): def draw_links(self, links): for link in links: pts = [(link.start.x, link.start.y)] + link.vertices + [(link.end.x, link.end.y)] - x = [n[0] for n in pts] - y = [n[1] for n in pts] - x_, y_ = self.interp_curve(x, y) - cu = make.curve(x_, y_, title=u.get_title(link)) - cu.curveparam._DataSet__icon = u.get_icon(link) - cu.curveparam._DataSet__title = u.get_title(link) - self.get_plot().add_item(cu) - - # create a label for the node and add it to the plot - l = int(len(x_) / 2.0) - la = make.label(link.id, (x_[l], y_[l]), (0, 0), "C") - la.set_private(True) - self.get_plot().add_item(la) + title = u.get_title(link) + icon = u.get_icon(link) + id_ = link.id + self.plot_item(id_, pts, title, icon) + + def plot_item(self, id_, data, title, icon="pipe.png"): + x = [n[0] for n in data] + y = [n[1] for n in data] + x_, y_ = self.interp_curve(x, y) + cu = make.curve(x_, y_, title=title) + cu.curveparam._DataSet__icon = icon + cu.curveparam._DataSet__title = title + cu.id_ = id_ # add the ide to the item before plotting. + self.get_plot().add_item(cu) + + # create a label for the node and add it to the plot + l = int(len(x_) / 2.0) + la = make.label(id_, (x_[l], y_[l]), (0, 0), "C") + la.set_private(True) + self.get_plot().add_item(la) + + self.add_plot_item_to_record(id_, [cu, la]) def draw_nodes(self, nodes): @@ -213,24 +291,27 @@ def draw_nodes(self, nodes): class optimalTimeGraph(CurveDialogWithClosable): - def __init__(self, name, year, damagecost, renewalcost, - units=units["EURO"], parent=None, options={}): + def __init__(self, name="Whole life cost", mainwindow=None, year=None, + damagecost=None, renewalcost=None, units=units["EURO"], + parent=None, options={}): + self.mainwindow = mainwindow if("xlabel" not in options): options['xlabel'] = "Time(years)" if("ylabel" not in options): options['ylabel'] = "Cost (%s)" % (units) - # if("wintitle" not in options): - # options['wintitle'] = "Costs against time" - self.curvesets = [] super(optimalTimeGraph, self).__init__(edit=False, - icon="guiqwt.svg", + icon="wlc.svg", toolbar=True, options=options, parent=parent, - panels=None) + panels=None, + wintitle=name, + mainwindow=mainwindow) + if (isinstance(self.mainwindow, MainWindow)): + self.mainwindow.optimaltimegraphs[id(self)] = self legend = make.legend("TR") self.get_plot().add_item(legend) if(year is None or damagecost is None or renewalcost is None): @@ -238,7 +319,26 @@ def __init__(self, name, year, damagecost, renewalcost, else: self.plotCurveSet(name, year, damagecost, renewalcost) - def plotCurveSet(self, name, year, damagecost, renewalcost): + def closeEvent(self, evnt): + if (not isinstance(self.mainwindow, MainWindow)): + _can_be_closed = True + elif (len(self.mainwindow.optimaltimegraphs) > 1): + _can_be_closed = True + del(self.mainwindow.optimaltimegraphs[id(self)]) + else: + _can_be_closed = False + + if _can_be_closed: + super(optimalTimeGraph, self).closeEvent(evnt) + else: + evnt.ignore() + self.setWindowState(QtCore.Qt.WindowMinimized) + + def plot_item(self, id_, data, title, icon="pipe.png"): + year, damagecost, renewalcost = data + self.add_plot_item_to_record(id_, self.plotCurveSet(title, year, damagecost, renewalcost)) + + def plotCurveSet(self, id_, year, damagecost, renewalcost): c = curve_colors[len(self.curvesets) % len(curve_colors)] dc = make.curve( year, damagecost, title="Damage Cost", color=c, linestyle="DashLine", @@ -248,6 +348,7 @@ def plotCurveSet(self, name, year, damagecost, renewalcost): markeredgecolor=None, shade=None, curvestyle=None, baseline=None, xaxis="bottom", yaxis="left") + dc.id_ = id_ # add the ide to the item before plotting. self.get_plot().add_item(dc) rc = make.curve( year, renewalcost, title="Renewal Cost", color=c, linestyle="DotLine", @@ -257,74 +358,130 @@ def plotCurveSet(self, name, year, damagecost, renewalcost): markeredgecolor=None, shade=None, curvestyle=None, baseline=None, xaxis="bottom", yaxis="left") + rc.id_ = id_ # add the ide to the item before plotting. self.get_plot().add_item(rc) tc = make.curve( - year, damagecost + renewalcost, title="Total Cost", color=c, linestyle=None, + year, array(damagecost) + array(renewalcost), title="Total Cost", color=c, linestyle=None, linewidth=5, marker=None, markersize=None, markerfacecolor=None, markeredgecolor=None, shade=None, curvestyle="Lines", baseline=None, xaxis="bottom", yaxis="left") + tc.id_ = id_ # add the ide to the item before plotting. self.get_plot().add_item(tc) - self.curvesets.append([name, dc, tc, rc]) + self.curvesets.append([id_, dc, tc, rc]) + return [dc, tc, rc] class MainWindow(QMainWindow): """The maion 'container' of the application. This is a multi-document interface where all other windows live in.""" - count = 0 + + class emptyclass: + pass + menuitems = emptyclass + menuitems.new_wlc = "New WLC window" + menuitems.cascade = "Cascade" + menuitems.tiled = "Tiled" + + update_selected_items = True def __init__(self, parent=None): super(MainWindow, self).__init__(parent) + self.optimaltimegraphs = {} self.mdi = QMdiArea() self.setCentralWidget(self.mdi) self.setMenu() - # self.new_window(closable=False) + self.standard_windows() + + def standard_windows(self): + self.add_networkmap() + self.add_riskmatrix() + self.add_optimaltimegraph() + + def selected_holder(self, widget): + """ When mocking remember do not patch slots. This is a slot. So, instead patch the function this calls below. """ + if(self.update_selected_items): + self.update_all_plots_with_selection(widget) + + def update_all_plots_with_selection(self, widget): + print("selection changed!") + # firt get all subplots + subplots = [x.get_plot() for x in self.optimaltimegraphs.values()] + subplots.append(self.riskmatrix.get_plot()) + subplots.append(self.networkmap.get_plot()) + # OK, now remove the plot represented by the argument 'widget' + subplots = filter(lambda a: a != widget, subplots) + # now select the selections of 'widget' in them. + selected_ids = [x.id_ for x in widget.get_selected_items()] + for p in subplots: + # first switch off responding to selections + self.update_selected_items = False + # find corressponding items + targets = [x for x in p.get_items() if getattr(x, 'id_', None) in selected_ids] + # now update + p.select_some_items(targets) + # don't forget to reset + self.update_selected_items = True + + def add_optimaltimegraph(self): + wlc = optimalTimeGraph(mainwindow=self) + self.mdi.addSubWindow(wlc) + wlc.show() + + def add_riskmatrix(self): + if(not any([x for x in self.mdi.subWindowList() if isinstance(x.widget(), RiskMatrix)])): + self.riskmatrix = RiskMatrix(mainwindow=self) + self.mdi.addSubWindow(self.riskmatrix) + self.riskmatrix.show() + + def add_networkmap(self): + if(not any([x for x in self.mdi.subWindowList() if isinstance(x.widget(), NetworkMap)])): + self.networkmap = NetworkMap("Network Map", mainwindow=self) + self.mdi.addSubWindow(self.networkmap) + self.networkmap.show() def setMenu(self): bar = self.menuBar() file = bar.addMenu("File") - file.addAction("New guiqwt") - file.addAction("New matplotlib") + file.addAction(self.menuitems.new_wlc) file.triggered[QAction].connect(self.windowaction) file2 = bar.addMenu("View") - file2.addAction("cascade") - file2.addAction("Tiled") + file2.addAction(self.menuitems.cascade) + file2.addAction(self.menuitems.tiled) file2.triggered[QAction].connect(self.windowaction) self.setWindowTitle("MDI demo") def windowaction(self, q): print("triggered") - if q.text() == "New guiqwt": - MainWindow.count = MainWindow.count + 1 - - self.new_window() + if q.text() == self.menuitems.new_wlc: + self.add_optimaltimegraph() if q.text() == "New matplotlib": - MainWindow.count = MainWindow.count + 1 + # MainWindow.count = MainWindow.count + 1 self.new_matplotlib_window() - if q.text() == "cascade": + if q.text() == self.menuitems.cascade: self.mdi.cascadeSubWindows() - if q.text() == "Tiled": + if q.text() == self.menuitems.tiled: self.mdi.tileSubWindows() def addSubWindow(self, *args, **kwargs): self.mdi.addSubWindow(*args, **kwargs) - def new_window(self, closable=True): + def new_window(self, closable=True, mainwindow=None): win = CurveDialogWithClosable( - edit=False, toolbar=True, wintitle="CurveDialog test", + edit=False, toolbar=True, wintitle="CurveDialog test", mainwindow=mainwindow, options=dict(title="Title", xlabel="xlabel", ylabel="ylabel")) win.setClosable(closable) self.plot_some_junk(win) - win.setWindowTitle("subwindow" + str(MainWindow.count)) + # win.setWindowTitle("subwindow" + str(MainWindow.count)) self.mdi.addSubWindow(win) win.show() return win @@ -334,7 +491,7 @@ def new_matplotlib_window(self, closable=True): # win.setClosable(closable) # self.plot_some_junk(win) - win.setWindowTitle("subwindow" + str(MainWindow.count)) + # win.setWindowTitle("subwindow" + str(MainWindow.count)) self.mdi.addSubWindow(win) win.show() diff --git a/src/rrpam_wds/gui/images/network.svg b/src/rrpam_wds/gui/images/network.svg new file mode 100644 index 0000000..f32e5fd --- /dev/null +++ b/src/rrpam_wds/gui/images/network.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/rrpam_wds/gui/images/risk.svg b/src/rrpam_wds/gui/images/risk.svg new file mode 100644 index 0000000..733fbdf --- /dev/null +++ b/src/rrpam_wds/gui/images/risk.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/rrpam_wds/gui/images/wlc.svg b/src/rrpam_wds/gui/images/wlc.svg new file mode 100644 index 0000000..9be0ede --- /dev/null +++ b/src/rrpam_wds/gui/images/wlc.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/rrpam_wds/gui/monkey_patch_guiqwt_guidata.py b/src/rrpam_wds/gui/monkey_patch_guiqwt_guidata.py index 4463a15..b8d7b77 100644 --- a/src/rrpam_wds/gui/monkey_patch_guiqwt_guidata.py +++ b/src/rrpam_wds/gui/monkey_patch_guiqwt_guidata.py @@ -13,6 +13,30 @@ def _patch_all(): _patch_item_list() _patch_curve_do_autoscale() _patch_curveitem_hit_test() + _patch_curveplot___del__() + + +def _patch_curveplot___del__(): + """1. Close a subwindow within qmidarea. 2. Close the application + It crashes with the message: + Traceback (most recent call last): + File "/home/user/miniconda3/envs/py34/lib/python3.5/site-packages/guiqwt/curve.py", line 1404, in __del__ + canvas.removeEventFilter(self.filter) +RuntimeError: wrapped C/C++ object of type QwtPlotCanvas has been deleted + + Currently I assume this is a bug in the curve.__del__ routine, not an issure of my implementation. + So, just adding a try: except clause + """ + orig___del__ = CurvePlot.__del__ + + def custom__del__(self): + try: + orig___del__(self) + except: + print("Better to fix me later.") + + # now monkey patch + CurvePlot.__del__ = custom__del__ def _patch_curveitem_hit_test(): diff --git a/src/rrpam_wds/gui/utils.py b/src/rrpam_wds/gui/utils.py index f01353c..f351062 100644 --- a/src/rrpam_wds/gui/utils.py +++ b/src/rrpam_wds/gui/utils.py @@ -19,4 +19,4 @@ def _get_type(epanet_network_item): if(epanet_network_item == "Risk"): return "R:", 'link.png' - return "None", None + return "None", "curve.png" diff --git a/tests/test_closable_graph.py b/tests/test_closable_graph.py index 0ccbd93..297263c 100755 --- a/tests/test_closable_graph.py +++ b/tests/test_closable_graph.py @@ -38,9 +38,9 @@ def tearDown(self): def test_closable_graph_can_be_closed_by_user(self): dummytitle = uniquestring() title = uniquestring() - self.dummy = CurveDialogWithClosable(wintitle=dummytitle) + self.dummy = CurveDialogWithClosable(wintitle=dummytitle, mainwindow=self.aw) self.aw.addSubWindow(self.dummy) - self.graph = CurveDialogWithClosable(wintitle=title) + self.graph = CurveDialogWithClosable(wintitle=title, mainwindow=self.aw) self.aw.addSubWindow(self.graph) self.assertNotEqual(self.graph.windowTitle, self.dummy.windowTitle) self.assertEqual(self.aw.mdi.subWindowList()[-1].windowTitle(), title) @@ -51,9 +51,9 @@ def test_closable_graph_can_be_closed_by_user(self): def test_closable_graph_closable_false_minized(self): dummytitle = uniquestring() title = uniquestring() - self.dummy = CurveDialogWithClosable(wintitle=dummytitle) + self.dummy = CurveDialogWithClosable(wintitle=dummytitle, mainwindow=self.aw) self.aw.addSubWindow(self.dummy) - self.graph = CurveDialogWithClosable(wintitle=title) + self.graph = CurveDialogWithClosable(wintitle=title, mainwindow=self.aw) self.graph.setClosable(False) self.aw.addSubWindow(self.graph) self.assertNotEqual(self.graph.windowTitle, self.dummy.windowTitle) @@ -66,9 +66,9 @@ def test_closable_graph_closable_false_minized(self): def test_presseing_esc_does_not_close_or_clear_the_closable_graph(self): dummytitle = uniquestring() title = uniquestring() - self.dummy = CurveDialogWithClosable(wintitle=dummytitle) + self.dummy = CurveDialogWithClosable(wintitle=dummytitle, mainwindow=self.aw) self.aw.addSubWindow(self.dummy) - self.graph = CurveDialogWithClosable(wintitle=title) + self.graph = CurveDialogWithClosable(wintitle=title, mainwindow=self.aw) self.graph.setClosable(False) self.aw.addSubWindow(self.graph) self.aw.show() diff --git a/tests/test_keeping_records_of_adding_removing_plot_items.py b/tests/test_keeping_records_of_adding_removing_plot_items.py new file mode 100644 index 0000000..009e11b --- /dev/null +++ b/tests/test_keeping_records_of_adding_removing_plot_items.py @@ -0,0 +1,144 @@ +from rrpam_wds.gui import set_pyqt_api # isort:skip # NOQA +import os +import sys +import time +import unittest + +import mock +import numpy as np +import pytest +from guiqwt import tests +from guiqwt.curve import CurveItem +from guiqwt.plot import CurveDialog +from guiqwt.shapes import EllipseShape +from numpy.testing import assert_array_almost_equal +from PyQt5.QtCore import Qt +from PyQt5.QtCore import QTimer +from PyQt5.QtTest import QTest +from PyQt5.QtWidgets import QApplication +from PyQt5.QtWidgets import QMainWindow +from PyQt5.QtWidgets import QMdiArea + +from rrpam_wds.gui.dialogs import CurveDialogWithClosable +from rrpam_wds.gui.dialogs import MainWindow +from rrpam_wds.gui.dialogs import NetworkMap +from rrpam_wds.gui.dialogs import RiskMatrix +from rrpam_wds.gui.dialogs import optimalTimeGraph + + +class test_keeping_records(unittest.TestCase): + start = 0 + stop = 0 + + def setUp(self): + global start + self.app = QApplication(sys.argv) + start = time.time() + self.aw = MainWindow() + self.aw.setWindowTitle("Records tests") + + pass + + def tearDown(self): + global stop + stop = time.time() + print("\ncalculation took %0.2f seconds." % (stop - start)) + self.aw = None + pass + + def runTest(self): + """ otherwise python 2.7 returns an error + ValueError: no such test method in : runTest""" + pass + + def test_riskmatrix_report_adding_to_the_register(self): + rm = self.aw.riskmatrix + + with mock.patch.object(rm, 'add_plot_item_to_record', autospec=True) as mock_add_plot_item_to_record: + self.assertFalse(mock_add_plot_item_to_record.called) + rm.plot_item("fox", [5000, 50], title="fox") + self.assertTrue(mock_add_plot_item_to_record.called) + + def test_riskmatrix_report_removing_to_the_register(self): + rm = self.aw.riskmatrix + + with mock.patch.object(rm, 'remove_plot_item_from_record', autospec=True) as mock_remove_plot_item_from_record: + self.assertFalse(mock_remove_plot_item_from_record.called) + rm.plot_item("fox", [5000, 50], title="fox") + self.assertFalse(mock_remove_plot_item_from_record.called) + risk_item = [x for x in rm.get_plot().get_items() if isinstance(x, EllipseShape)][0] + rm.get_plot().del_item(risk_item) + self.assertTrue(mock_remove_plot_item_from_record.called) + + def test_networkmap_report_adding_to_the_register(self): + otg = self.aw.networkmap + + with mock.patch.object(otg, 'add_plot_item_to_record', autospec=True) as mock_add_plot_item_to_record: + self.assertFalse(mock_add_plot_item_to_record.called) + otg.plot_item("fox", [(5, 10), (8, 3), (24, 1)], "fox") + self.assertTrue(mock_add_plot_item_to_record.called) + + def test_networkmap_report_removing_to_the_register(self): + otg = self.aw.networkmap + + with mock.patch.object(otg, 'remove_plot_item_from_record', autospec=True) as mock_remove_plot_item_from_record: + self.assertFalse(mock_remove_plot_item_from_record.called) + otg.plot_item("fox", [(5, 10), (8, 3), (24, 1)], "fox") + self.assertFalse(mock_remove_plot_item_from_record.called) + nw_item = [x for x in otg.get_plot().get_items() if isinstance(x, CurveItem)][0] + otg.get_plot().del_item(nw_item) + self.assertTrue(mock_remove_plot_item_from_record.called) + + def test_optimaltimegraph_report_adding_to_the_register(self): + otg = list(self.aw.optimaltimegraphs.values())[0] + + with mock.patch.object(otg, 'add_plot_item_to_record', autospec=True) as mock_add_plot_item_to_record: + self.assertFalse(mock_add_plot_item_to_record.called) + otg.plot_item( + "fox", [[1997, 1998, 2005, 2008], [5, 10, 25, 95], [100, 50, 25, 12]], "fox") + self.assertTrue(mock_add_plot_item_to_record.called) + + def test_optimaltimegraph_report_removing_to_the_register(self): + otg = list(self.aw.optimaltimegraphs.values())[0] + + with mock.patch.object(otg, 'remove_plot_item_from_record', autospec=True) as mock_remove_plot_item_from_record: + self.assertFalse(mock_remove_plot_item_from_record.called) + otg.plot_item( + "fox", [[1997, 1998, 2005, 2008], [5, 10, 25, 95], [100, 50, 25, 12]], "fox") + self.assertFalse(mock_remove_plot_item_from_record.called) + nw_item = [x for x in otg.get_plot().get_items() if isinstance(x, CurveItem)][0] + otg.get_plot().del_item(nw_item) + self.assertTrue(mock_remove_plot_item_from_record.called) + + def test_deleting_a_curve_in_optimaltimegraph_removes_all_three_curves(self): + # for the heck of it add another optimal time graph + self.aw.add_optimaltimegraph() + l = list(self.aw.optimaltimegraphs.values()) + otg1 = l[0] + otg2 = l[1] + self.assertEqual(len(otg1.myplotitems), 0) + otg1.plot_item("fox", [[1997, 1998, 2005, 2008], [5, 10, 25, 95], [100, 50, 25, 12]], "fox") + otg2.plot_item("fox", [[1997, 1998, 2005, 2008], [5, 10, 25, 95], [100, 50, 25, 12]], "fox") + self.assertEqual(len(otg1.myplotitems), 1) + self.assertEqual(len(otg2.myplotitems), 1) + otg_item = [x for x in otg1.get_plot().get_items() if isinstance(x, CurveItem)][0] + otg1.get_plot().del_item(otg_item) + self.assertEqual(len(otg1.myplotitems), 0) + otg_item = [x for x in otg2.get_plot().get_items() if isinstance(x, CurveItem)][ + 2] # pick a different curve from the set! + otg2.get_plot().del_item(otg_item) + self.assertEqual(len(otg2.myplotitems), 0) + + +def drive(test=True): # pragma: no cover + if(test): + unittest.main(verbosity=2) + else: + ot = test_keeping_records() + ot.setUp() + ot.test_deleting_a_curve_in_optimaltimegraph_removes_all_three_curves() + ot.aw.show() + sys.exit(ot.app.exec_()) + +if __name__ == '__main__': # pragma: no cover + drive(test=False) diff --git a/tests/test_main_window.py b/tests/test_main_window.py new file mode 100644 index 0000000..c870df6 --- /dev/null +++ b/tests/test_main_window.py @@ -0,0 +1,121 @@ +from rrpam_wds.gui import set_pyqt_api # isort:skip # NOQA +import os +import sys +import time +import unittest + +import numpy as np +import pytest +from guiqwt import tests +from guiqwt.plot import CurveDialog +from numpy.testing import assert_array_almost_equal +from PyQt5.QtCore import Qt +from PyQt5.QtCore import QTimer +from PyQt5.QtTest import QTest +from PyQt5.QtWidgets import QApplication +from PyQt5.QtWidgets import QMainWindow +from PyQt5.QtWidgets import QMdiArea + +from rrpam_wds.gui.dialogs import CurveDialogWithClosable +from rrpam_wds.gui.dialogs import MainWindow +from rrpam_wds.gui.dialogs import NetworkMap +from rrpam_wds.gui.dialogs import RiskMatrix +from rrpam_wds.gui.dialogs import optimalTimeGraph + + +class test_main_window(unittest.TestCase): + start = 0 + stop = 0 + + def setUp(self): + global start + self.app = QApplication(sys.argv) + start = time.time() + self.aw = MainWindow() + self.aw.setWindowTitle("Testing main window") + + pass + + def tearDown(self): + global stop + stop = time.time() + print("\ncalculation took %0.2f seconds." % (stop - start)) + self.aw = None + pass + + def runTest(self): + """ otherwise python 2.7 returns an error + ValueError: no such test method in : runTest""" + pass + + def test_mainwindow_is_derived_from_QMainWindow(self): + """optimalTimeGraph should be derived from CurveDialogWithClosable class""" + self.assertIsInstance(self.aw, QMainWindow) + + def test_mainwindow_has_a_mdi_Area(self): + self.assertIsInstance(self.aw.mdi, QMdiArea) + + def test_mainwindow_always_has_a_network_map_and_risk_matrix_one_each(self): + list1 = self.aw.mdi.subWindowList() + self.assertTrue(len([x for x in list1 if (isinstance(x.widget(), RiskMatrix))]), 1) + self.assertTrue(len([x for x in list1 if (isinstance(x.widget(), NetworkMap))]), 1) + + def test_main_window_will_not_add_more_than_one_network_map_or_risk_matrix(self): + self.aw.standard_windows() # Adds riskmatrix, network map and a optimaltimegraph + list1 = self.aw.mdi.subWindowList() # should have above three + self.aw.add_riskmatrix() # try adding a rm + self.aw.add_networkmap() # try adding a nm + self.aw.standard_windows() # this will add only an extra optimaltimegraph + list2 = self.aw.mdi.subWindowList() + list1 = [x for x in list1 if not isinstance(x.widget(), optimalTimeGraph)] + list2 = [x for x in list2 if not isinstance(x.widget(), optimalTimeGraph)] + self.assertEqual(list1, list2) + + def test_attempting_to_close_will_minimize_network_map_and_risk_matrix_and_the_last_optimal_time_graph( + self): + + list1 = self.aw.mdi.subWindowList() + self.assertFalse(self.aw.riskmatrix.isMinimized()) + self.assertFalse(self.aw.networkmap.isMinimized()) + self.close_all_windows() + list2 = self.aw.mdi.subWindowList() + self.assertEqual(list1, list2) + self.assertTrue(self.aw.networkmap.isMinimized()) + self.assertTrue(self.aw.networkmap.isMinimized()) + + def close_all_windows(self): + for w in self.aw.mdi.subWindowList(): + w.close() + + def test_multiple_optimal_time_graphs_can_be_added(self): + list1 = self.aw.mdi.subWindowList() + self.assertTrue(len([x for x in list1 if (isinstance(x.widget(), optimalTimeGraph))]), 1) + self.aw.add_optimaltimegraph() + self.assertEqual(len(list1) + 1, len(self.aw.mdi.subWindowList())) + + def test_last_remaining_optimal_time_graph_will_not_be_deleted_but_minimized(self): + list1 = self.aw.mdi.subWindowList() + self.aw.add_optimaltimegraph() # now we have two + self.aw.add_optimaltimegraph() # now we have + self.close_all_windows() + list2 = self.aw.mdi.subWindowList() + # print(list1) + # print(list2) + self.assertEqual(len(list1), len(list2)) + + for w in self.aw.mdi.subWindowList(): + self.assertTrue(w.isMinimized()) + + +def drive(test=True): # pragma: no cover + if(test): + unittest.main(verbosity=2) + else: + ot = test_main_window() + ot.setUp() + ot.test_last_remaining_optimal_time_graph_will_not_be_deleted_but_minimized() + # ot.aw.show() + # sys.exit(ot.app.exec_()) + +if __name__ == '__main__': # pragma: no cover + drive(test=False) diff --git a/tests/test_network_map.py b/tests/test_network_map.py index f3b9043..7d0b720 100644 --- a/tests/test_network_map.py +++ b/tests/test_network_map.py @@ -47,7 +47,7 @@ def runTest(self): def test_NetworkMap_is_derived_from_CurveDialogWithClosable(self): """NetworkMaph should be derived from CurveDialogWithClosable class""" - nwm = NetworkMap("foo", None, None, parent=self.aw) + nwm = NetworkMap(name="foo", nodes=None, links=None, parent=self.aw, mainwindow=self.aw) self.assertIsInstance(nwm, CurveDialogWithClosable) self.aw.addSubWindow(nwm) @@ -65,7 +65,7 @@ def draw_a_network(self, network=ex.networks[0]): e1 = hs.pdd_service(network, coords=True, adfcalc=False) nodes = e1.nodes.values() links = e1.links.values() - nwm = NetworkMap("foo", nodes, links, parent=self.aw) + nwm = NetworkMap(name="foo", nodes=nodes, links=links, parent=self.aw, mainwindow=self.aw) self.aw.addSubWindow(nwm) self.aw.show() @@ -82,11 +82,11 @@ def test_NetworkMap_scales_to_fit_network_on_creation(self): plot = nwm.get_plot() _xmin, _xmax = plot.get_axis_limits("bottom") _ymin, _ymax = plot.get_axis_limits("left") - delta=20. - self.assertAlmostEqual(xmin-delta, _xmin, delta=delta) - self.assertAlmostEqual(xmax+delta, _xmax, delta=delta) - self.assertAlmostEqual(ymin-delta, _ymin, delta=delta) - self.assertAlmostEqual(ymax+delta, _ymax, delta=delta) + delta = 20. + self.assertAlmostEqual(xmin - delta, _xmin, delta=delta) + self.assertAlmostEqual(xmax + delta, _xmax, delta=delta) + self.assertAlmostEqual(ymin - delta, _ymin, delta=delta) + self.assertAlmostEqual(ymax + delta, _ymax, delta=delta) pass def test_NetworkMap_has_correct_number_of_link_representations(self): diff --git a/tests/test_optimal_time_graph.py b/tests/test_optimal_time_graph.py index 0894cda..7ff6493 100644 --- a/tests/test_optimal_time_graph.py +++ b/tests/test_optimal_time_graph.py @@ -44,7 +44,8 @@ def runTest(self): def test_optimalTimeGraph_is_derived_from_CurveDialogWithClosable(self): """optimalTimeGraph should be derived from CurveDialogWithClosable class""" - tg1 = optimalTimeGraph("set1", None, None, None, parent=self.aw) + tg1 = optimalTimeGraph(name="set1", damagecost=None, renewalcost=None, + year=None, parent=self.aw, mainwindow=self.aw) self.assertIsInstance(tg1, CurveDialogWithClosable) self.aw.addSubWindow(tg1) @@ -54,7 +55,8 @@ def test_optimalTimeGraph_creates_right_three_curves(self): damagecost = year**2.1 renewalcost = (100 - year)**1.9 tg1 = optimalTimeGraph( - "set1", year, damagecost, renewalcost, parent=self.aw) + "set1", year=year, damagecost=damagecost, renewalcost=renewalcost, + parent=self.aw, mainwindow=self.aw) self.aw.addSubWindow(tg1) it = [x for x in tg1.get_plot().get_items() if ( isinstance(x, CurveItem))] @@ -80,7 +82,7 @@ def drive(test=True): # pragma: no cover else: ot = test_optimal_time_graph() ot.setUp() - ot.test_optimalTimeGraph_is_derived_from_CurveDialogWithClosable() + ot.test_optimalTimeGraph_creates_right_three_curves() ot.aw.show() sys.exit(ot.app.exec_()) diff --git a/tests/test_pytest_mainwindow.py b/tests/test_pytest_mainwindow.py new file mode 100644 index 0000000..a6b4dcf --- /dev/null +++ b/tests/test_pytest_mainwindow.py @@ -0,0 +1,44 @@ +import subprocess +import sys + +from PyQt5.QtCore import QTimer +from PyQt5.QtWidgets import QApplication + +from rrpam_wds.gui.dialogs import CurveDialogWithClosable +from rrpam_wds.gui.dialogs import MainWindow +from rrpam_wds.gui.dialogs import NetworkMap +from rrpam_wds.gui.dialogs import RiskMatrix +from rrpam_wds.gui.dialogs import optimalTimeGraph + +# def test_closing_a_optimal_time_graph_window_does_not_cause_error_at_exit(): + #""" See notes at: monkey_patch_guiqwt_guidata._patch_curveplot___del__ + + # At the moment this test is useless. + + #""" + # print("Running: ",[sys.executable,__file__]) + # p = subprocess.Popen([sys.executable,__file__], + # stdout=subprocess.PIPE, + # stderr=subprocess.PIPE, + # stdin=subprocess.PIPE) + # output = p.communicate() + # print("out:", output[0]) + # print("err:", output[1]) + + # assert output[1]==b"" + # assert output[0]==b"" + # assert False + +if __name__ == "__main__": + app = QApplication(sys.argv) + aw = MainWindow() + aw.add_optimaltimegraph() + + for w in aw.mdi.subWindowList(): + w.close() + # following should not raise exceptions + aw.show() + aw.deleteLater() + QTimer.singleShot(5000, app.quit) # Make the application quit just after start + print("Boo") + val = app.exec_() diff --git a/tests/test_reset_zoom_tool.py b/tests/test_reset_zoom_tool.py index 0e7b34c..be143cb 100644 --- a/tests/test_reset_zoom_tool.py +++ b/tests/test_reset_zoom_tool.py @@ -29,7 +29,7 @@ def setUp(self): start = time.time() self.aw = MainWindow() self.aw.setWindowTitle("Testing optimal time graph") - self.win = self.aw.new_window() + self.win = self.aw.new_window(mainwindow=self.aw) pass def tearDown(self): diff --git a/tests/test_risk_matrix.py b/tests/test_risk_matrix.py index 2c1a595..026eda4 100644 --- a/tests/test_risk_matrix.py +++ b/tests/test_risk_matrix.py @@ -31,7 +31,7 @@ def setUp(self): start = time.time() self.aw = MainWindow() self.aw.setWindowTitle("Testing risk matrix") - self.rm = RiskMatrix() + self.rm = RiskMatrix(mainwindow=self.aw) self.aw.addSubWindow(self.rm) self.rm.show() pass @@ -54,11 +54,13 @@ def test_Risk_Map_is_derived_from_CurveDialogWithClosable(self): def test_plot_item_will_create_a_circle(self): pts0 = [x for x in self.rm.get_plot().get_items() if isinstance(x, EllipseShape)] - self.rm.plot_item(5000.0, 50, title="foo") - self.rm.plot_item(1000.0, 20, title="bar") - self.rm.plot_item(8000.0, 70, title="bax") + self.rm.plot_item("foo", [5000.0, 50], title="foo") + self.rm.plot_item("bar", [1000.0, 20], title="bar") + self.rm.plot_item("bax", [8000.0, 70], title="bax") + self.rm.plot_item("bax", [15000.0, 100], title="bax") pts1 = [x for x in self.rm.get_plot().get_items() if isinstance(x, EllipseShape)] - self.assertEqual(len(pts1), len(pts0) + 3) + self.assertEqual(len(pts1), len(pts0) + 4) + self.assertEqual(pts1[1].get_xdiameter(), self.rm.get_ellipse_xaxis(1000., 20.)) def drive(test=True): # pragma: no cover diff --git a/tests/test_signalling_among_plots.py b/tests/test_signalling_among_plots.py new file mode 100644 index 0000000..762b10b --- /dev/null +++ b/tests/test_signalling_among_plots.py @@ -0,0 +1,150 @@ +from rrpam_wds.gui import set_pyqt_api # isort:skip # NOQA +import os +import sys +import time +import unittest + +import mock +import numpy as np +import pytest +from guiqwt import tests +from guiqwt.curve import CurveItem +from guiqwt.plot import CurveDialog +from guiqwt.shapes import EllipseShape +from numpy.testing import assert_array_almost_equal +from PyQt5.QtCore import Qt +from PyQt5.QtCore import QTimer +from PyQt5.QtTest import QTest +from PyQt5.QtWidgets import QApplication +from PyQt5.QtWidgets import QMainWindow +from PyQt5.QtWidgets import QMdiArea + +from rrpam_wds.gui.dialogs import CurveDialogWithClosable +from rrpam_wds.gui.dialogs import MainWindow +from rrpam_wds.gui.dialogs import NetworkMap +from rrpam_wds.gui.dialogs import RiskMatrix +from rrpam_wds.gui.dialogs import optimalTimeGraph + + +class test_main_window(unittest.TestCase): + start = 0 + stop = 0 + + def setUp(self): + global start + self.app = QApplication(sys.argv) + start = time.time() + self.aw = MainWindow() + self.aw.setWindowTitle("Signalling tests") + + pass + + def tearDown(self): + global stop + stop = time.time() + print("\ncalculation took %0.2f seconds." % (stop - start)) + self.aw = None + pass + + def runTest(self): + """ otherwise python 2.7 returns an error + ValueError: no such test method in : runTest""" + pass + + def test_every_plot_triggers_selection_update_function_in_the_main_window(self): + nm, rm, ot = self.plot_a_random_dataset() + with mock.patch.object(self.aw, 'update_all_plots_with_selection', autospec=True) as mock_update_all_plots_with_selection: + self.assertFalse(mock_update_all_plots_with_selection.called) + risk_item = [x for x in rm.get_plot().get_items() if isinstance(x, EllipseShape)][0] + rm.get_plot().select_item(risk_item) + self.assertTrue(mock_update_all_plots_with_selection.called) + mock_update_all_plots_with_selection.reset_mock() + self.assertFalse(mock_update_all_plots_with_selection.called) + link_item = [x for x in nm.get_plot().get_items() if isinstance(x, CurveItem)][0] + nm.get_plot().select_item(link_item) + mock_update_all_plots_with_selection.assert_called_with(nm.get_plot()) + + mock_update_all_plots_with_selection.reset_mock() + self.assertFalse(mock_update_all_plots_with_selection.called) + ot_item = [x for x in ot.get_plot().get_items() if isinstance(x, CurveItem)][2] + ot.get_plot().select_item(ot_item) + mock_update_all_plots_with_selection.assert_called_with(ot.get_plot()) + + def test_selecting_items_in_riskmatrix_plot_update_slections_to_match_in_other_plots(self): + nm, rm, ot = self.plot_a_random_dataset() + + risk_item = [x for x in rm.get_plot().get_items() if isinstance(x, EllipseShape)][5] + # ^ lets select something that is also represented in the current optimaltimegraph (see plot_a_random_dataset) + self.assertFalse(nm.get_plot().get_selected_items()) + rm.get_plot().select_item(risk_item) + self.assertEqual(nm.get_plot().get_selected_items()[0].id_, risk_item.id_) + self.assertEqual(ot.get_plot().get_selected_items()[0].id_, risk_item.id_) + + def test_selecting_items_in_networkmap_plot_update_slections_to_match_in_other_plots(self): + nm, rm, ot = self.plot_a_random_dataset() + + network_item = [x for x in nm.get_plot().get_items() if isinstance(x, CurveItem)][2] + # ^ now this curve is not represented in the optimaltimegraph (see plot_a_random_dataset) + + nm.get_plot().select_item(network_item) + self.assertEqual(rm.get_plot().get_selected_items()[0].id_, network_item.id_) + + def test_selecting_items_in_optimaltimegraph_plot_update_slections_to_match_in_other_plots(self): + nm, rm, ot = self.plot_a_random_dataset() + + ot_item = [x for x in ot.get_plot().get_items() if isinstance(x, CurveItem)][4] + ot.get_plot().select_item(ot_item) + self.assertEqual(nm.get_plot().get_selected_items()[0].id_, ot_item.id_) + self.assertEqual(rm.get_plot().get_selected_items()[0].id_, ot_item.id_) + + def plot_a_random_dataset(self): + import string + import random + import math + rm = self.aw.riskmatrix + nm = self.aw.networkmap + ot = list(self.aw.optimaltimegraphs.values())[0] + + def id_generator(size=3, chars=string.ascii_letters + " " + string.digits): + return ''.join(random.choice(chars) for _ in range(size)) + + def plot_link(x1, y1, x2, y2, id): + link = mock.MagicMock() + link.start.x = x1 + link.start.y = y1 + link.end.x = x2 + link.end.y = y2 + link.vertices = [] + link.id = id + nm.draw_links([link]) + + N = 10 + x1 = np.random.rand(N) * 500 + y1 = np.random.rand(N) * 1000 + x2 = np.random.rand(N) * 500 + y2 = np.random.rand(N) * 1000 + ids = [id_generator(4) for x in range(N)] + + for i in range(N): + plot_link(x1[i], y1[i], x2[i], y2[i], ids[i]) + rm.plot_item(ids[i], [random.random() * 15000, random.random() * 100], title=ids[i]) + for i in range(5, 8): + years = [1997, 1998, 1999, 2005, 2008] + val = random.random() / 10. + ot.plotCurveSet(ids[i], years, [1000 * math.exp(val * (x - years[0])) + for x in years], [1000 - 1000 * math.exp(-val * 2 * (x - years[0])) for x in years]) + return nm, rm, ot + + +def drive(test=True): # pragma: no cover + if(test): + unittest.main(verbosity=2) + else: + ot = test_main_window() + ot.setUp() + ot.test_selecting_items_in_riskmatrix_plot_update_slections_to_match_in_other_plots() + ot.aw.show() + sys.exit(ot.app.exec_()) + +if __name__ == '__main__': # pragma: no cover + drive(test=False)