From f1384074b56a3f3eb04d08dbe926dbdd20d3474c Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 17 May 2019 14:48:42 +0300 Subject: [PATCH] Rework internal graph interfaces --- eos/graph/__init__.py | 120 ++++++-------------------- eos/graph/fitCapAmountTime.py | 27 +++--- gui/builtinGraphs/__init__.py | 18 ++-- gui/builtinGraphs/fitCapAmountTime.py | 62 +++---------- gui/graph.py | 47 ++++++++-- gui/graphFrame.py | 56 ++++++------ 6 files changed, 127 insertions(+), 203 deletions(-) diff --git a/eos/graph/__init__.py b/eos/graph/__init__.py index a5fd6c72b..77a348e88 100644 --- a/eos/graph/__init__.py +++ b/eos/graph/__init__.py @@ -17,107 +17,35 @@ # along with eos. If not, see . # =============================================================================== -import itertools + +from abc import ABCMeta, abstractmethod -class Graph: +class Graph(metaclass=ABCMeta): - def __init__(self, fit, function, data=None): - self.fit = fit - self.data = {} - if data is not None: - for name, d in data.items(): - self.setData(Data(name, d)) + def __init__(self): + self.cache = {} - self.function = function + @abstractmethod + def getPlotPoints(self, fit, extraData, xRange, xAmount): + raise NotImplementedError - def clearData(self): - self.data.clear() + def getYForX(self, fit, extraData, x): + raise NotImplementedError - def setData(self, data): - self.data[data.name] = data - - def getIterator(self): - pointNames = [] - pointIterators = [] - for data in self.data.values(): - pointNames.append(data.name) - pointIterators.append(data) - - return self._iterator(pointNames, pointIterators) - - def _iterator(self, pointNames, pointIterators): - for pointValues in itertools.product(*pointIterators): - point = {} - for i in range(len(pointValues)): - point[pointNames[i]] = pointValues[i] - yield point, self.function(point) - - -class Data: - - def __init__(self, name, dataString, step=None): - self.name = name - self.step = step - self.data = self.parseString(dataString) - - def parseString(self, dataString): - if not isinstance(dataString, str): - return Constant(dataString), - - dataList = [] - for data in dataString.split(";"): - if isinstance(data, str) and "-" in data: - # Dealing with a range - dataList.append(Range(data, self.step)) - else: - dataList.append(Constant(data)) - - return dataList - - def __iter__(self): - for data in self.data: - for value in data: - yield value - - def isConstant(self): - return len(self.data) == 1 and self.data[0].isConstant() - - -class Constant: - - def __init__(self, const): - if isinstance(const, str): - self.value = None if const == "" else float(const) + def _xIter(self, xRange, xAmount): + rangeStart, rangeEnd = sorted(xRange) + # Amount is amount of ranges between points here, not amount of points + step = (rangeEnd - rangeStart) / xAmount + if step == 0: + yield xRange[0] else: - self.value = const + current = rangeStart + # Take extra half step to make sure end of range is always included + # despite any possible float errors + while current <= (rangeEnd + step / 2): + yield current + current += step - def __iter__(self): - yield self.value - - @staticmethod - def isConstant(): - return True - - -class Range: - - def __init__(self, string, step): - start, end = string.split("-") - self.start = float(start) - self.end = float(end) - self.step = step - - def __iter__(self): - current = start = self.start - end = self.end - step = self.step or (end - start) / 200 - i = 0 - while current < end: - current = start + i * step - i += 1 - yield current - - @staticmethod - def isConstant(): - return False + def clearCache(self, fitID): + self.cache.clear() diff --git a/eos/graph/fitCapAmountTime.py b/eos/graph/fitCapAmountTime.py index c8dcf91d9..d8542c81d 100644 --- a/eos/graph/fitCapAmountTime.py +++ b/eos/graph/fitCapAmountTime.py @@ -1,24 +1,27 @@ import math -from logbook import Logger from eos.graph import Graph -pyfalog = Logger(__name__) - - class FitCapAmountTimeGraph(Graph): - defaults = {"time": 0} + def getPlotPoints(self, fit, extraData, xRange, xAmount): + xs = [] + ys = [] + for x in self._xIter(xRange, xAmount): + xs.append(x) + ys.append(self.calcCap(fit, x)) + return xs, {'capAmount': ys} - def __init__(self, fit, data=None): - Graph.__init__(self, fit, self.calcAmount, data if data is not None else self.defaults) - self.fit = fit + def getYForX(self, fit, extraData, x): + return {'capAmount': self.calcCap(fit, x)} - def calcAmount(self, data): - time = data["time"] - maxCap = self.fit.ship.getModifiedItemAttr('capacitorCapacity') - regenTime = self.fit.ship.getModifiedItemAttr('rechargeRate') / 1000 + @staticmethod + def calcCap(fit, time): + if time < 0: + return 0 + maxCap = fit.ship.getModifiedItemAttr('capacitorCapacity') + regenTime = fit.ship.getModifiedItemAttr('rechargeRate') / 1000 # https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate cap = maxCap * (1 + math.exp(5 * -time / regenTime) * -1) ** 2 return cap diff --git a/gui/builtinGraphs/__init__.py b/gui/builtinGraphs/__init__.py index ee4e01011..4b81c5914 100644 --- a/gui/builtinGraphs/__init__.py +++ b/gui/builtinGraphs/__init__.py @@ -1,13 +1,13 @@ # noinspection PyUnresolvedReferences from gui.builtinGraphs import ( # noqa: E402,F401 - fitDpsRange, - fitDpsTime, - fitDmgTime, - fitShieldRegenAmount, - fitShieldAmountTime, - fitCapRegenAmount, + # fitDpsRange, + # fitDpsTime, + # fitDmgTime, + # fitShieldRegenAmount, + # fitShieldAmountTime, + # fitCapRegenAmount, fitCapAmountTime, - fitSpeedTime, - fitDistanceTime, - fitWarpTimeDistance + # fitSpeedTime, + # fitDistanceTime, + # fitWarpTimeDistance ) diff --git a/gui/builtinGraphs/fitCapAmountTime.py b/gui/builtinGraphs/fitCapAmountTime.py index 60a0e9d48..42ce9f945 100644 --- a/gui/builtinGraphs/fitCapAmountTime.py +++ b/gui/builtinGraphs/fitCapAmountTime.py @@ -17,65 +17,31 @@ # along with pyfa. If not, see . # ============================================================================= + import gui.mainFrame -from eos.graph import Data -from eos.graph.fitCapAmountTime import FitCapAmountTimeGraph as EosFitCapAmountTimeGraph -from gui.bitmap_loader import BitmapLoader -from gui.graph import Graph -from service.attribute import Attribute +from eos.graph.fitCapAmountTime import FitCapAmountTimeGraph as EosGraph +from gui.graph import Graph, XDef, YDef class FitCapAmountTimeGraph(Graph): - propertyLabelMap = {"time": "Time (seconds)"} - - defaults = EosFitCapAmountTimeGraph.defaults.copy() + name = 'Cap Amount vs Time' def __init__(self): - Graph.__init__(self) - self.defaults["time"] = "0-300" - self.name = "Cap Amount vs Time" - self.eosGraph = None + self.eosGraph = EosGraph() self.mainFrame = gui.mainFrame.MainFrame.getInstance() - def getFields(self): - return self.defaults + @property + def xDef(self): + return XDef(handle='time', inputDefault='0-300', inputLabel='Time (seconds)', inputIconID=1392, axisLabel='Time, s') - def getLabels(self): - return self.propertyLabelMap + @property + def yDefs(self): + return [YDef(handle='capAmount', switchLabel='Cap amount', axisLabel='Cap amount, GJ')] - def getIcons(self): - iconFile = Attribute.getInstance().getAttributeInfo('duration').iconID - bitmap = BitmapLoader.getBitmap(iconFile, "icons") - return {"time": bitmap} - - def getPoints(self, fit, fields): - eosGraph = getattr(self, "eosGraph", None) - if eosGraph is None or eosGraph.fit != fit: - eosGraph = self.eosGraph = EosFitCapAmountTimeGraph(fit) - - eosGraph.clearData() - variable = None - for fieldName, value in fields.items(): - d = Data(fieldName, value) - if not d.isConstant(): - if variable is None: - variable = fieldName - else: - # We can't handle more then one variable atm, OOPS FUCK OUT - return False, "Can only handle 1 variable" - - eosGraph.setData(d) - - if variable is None: - return False, "No variable" - - x = [] - y = [] - for point, val in eosGraph.getIterator(): - x.append(point[variable]) - y.append(val) - return x, y + def getPlotPoints(self, fit, extraData, xRange, xAmount): + xRange = self.parseRange(xRange) + return self.eosGraph.getPlotPoints(fit, extraData, xRange, xAmount) FitCapAmountTimeGraph.register() diff --git a/gui/graph.py b/gui/graph.py index f54e79767..3d02915b8 100644 --- a/gui/graph.py +++ b/gui/graph.py @@ -18,26 +18,59 @@ # ============================================================================= -class Graph(object): +import re +from abc import ABCMeta, abstractmethod +from collections import namedtuple + + +class Graph(metaclass=ABCMeta): + views = [] + yTypes = None @classmethod def register(cls): Graph.views.append(cls) - def __init__(self): - self.name = "" + @property + @abstractmethod + def name(self): + raise NotImplementedError - def getFields(self, fit, fields): - raise NotImplementedError() + @property + @abstractmethod + def xDef(self): + raise NotImplementedError - def getIcons(self): - return None + @property + def extraInputs(self): + return () + + @property + @abstractmethod + def yDefs(self): + raise NotImplementedError @property def redrawOnEffectiveChange(self): return False + def parseRange(self, string): + m = re.match('\s*(?P\d+(\.\d+)?)\s*(-\s*(?P\d+(\.\d+)?))?', string) + if m is None: + return (0, 0) + first = m.group('first') + second = m.group('second') + if not second: + return (0, float(first)) + else: + return (float(first), float(second)) + + +XDef = namedtuple('XDef', ('handle', 'inputDefault', 'inputLabel', 'inputIconID', 'axisLabel')) +YDef = namedtuple('YDef', ('handle', 'switchLabel', 'axisLabel')) +ExtraInput = namedtuple('ExtraInput', ('handle', 'inputDefault', 'inputLabel', 'inputIconID')) + # noinspection PyUnresolvedReferences from gui.builtinGraphs import * diff --git a/gui/graphFrame.py b/gui/graphFrame.py index fe8b79fbe..3ccb482cf 100644 --- a/gui/graphFrame.py +++ b/gui/graphFrame.py @@ -19,6 +19,7 @@ import os import traceback +from itertools import chain # noinspection PyPackageRequirements import wx @@ -230,50 +231,42 @@ class GraphFrame(wx.Frame): def getValues(self): values = {} - for fieldName, field in self.fields.items(): - values[fieldName] = field.GetValue() + for fieldHandle, field in self.fields.items(): + values[fieldHandle] = field.GetValue() return values def select(self, index): view = self.getView() - icons = view.getIcons() - labels = view.getLabels() sizer = self.gridSizer sizer.Clear() self.gridPanel.DestroyChildren() self.fields.clear() # Setup textboxes - for field, defaultVal in view.getFields().items(): - + for fieldDef in (view.xDef, *view.extraInputs): textBox = wx.TextCtrl(self.gridPanel, wx.ID_ANY, style=0) - self.fields[field] = textBox + self.fields[fieldDef.handle] = textBox textBox.Bind(wx.EVT_TEXT, self.onFieldChanged) sizer.Add(textBox, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 3) - if defaultVal is not None: - if not isinstance(defaultVal, str): - defaultVal = ("%f" % defaultVal).rstrip("0") - if defaultVal[-1:] == ".": - defaultVal += "0" + if fieldDef.inputDefault is not None: + inputDefault = fieldDef.inputDefault + if not isinstance(inputDefault, str): + inputDefault = ("%f" % inputDefault).rstrip("0") + if inputDefault[-1:] == ".": + inputDefault += "0" - textBox.ChangeValue(defaultVal) + textBox.ChangeValue(inputDefault) imgLabelSizer = wx.BoxSizer(wx.HORIZONTAL) - if icons: - icon = icons.get(field) + if fieldDef.inputIconID: + icon = BitmapLoader.getBitmap(fieldDef.inputIconID, "icons") if icon is not None: static = wx.StaticBitmap(self.gridPanel) static.SetBitmap(icon) imgLabelSizer.Add(static, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 1) - if labels: - label = labels.get(field) - label = label if label is not None else field - else: - label = field - - imgLabelSizer.Add(wx.StaticText(self.gridPanel, wx.ID_ANY, label), 0, + imgLabelSizer.Add(wx.StaticText(self.gridPanel, wx.ID_ANY, fieldDef.inputLabel), 0, wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 3) sizer.Add(imgLabelSizer, 0, wx.ALIGN_CENTER_VERTICAL) sizer.Layout() @@ -299,19 +292,20 @@ class GraphFrame(wx.Frame): min_y = 0 max_y = 0 + + xRange = values[view.xDef.handle] + extraInputs = {i.handle: values[i.handle] for i in view.extraInputs} + chosenY = view.yDefs[0].handle + for fit in self.fits: try: - success, status = view.getPoints(fit, values) - if not success: - # TODO: Add a pwetty statys bar to report errors with - self.SetStatusText(status) - return + xs, ys = view.getPlotPoints(fit, extraInputs, xRange, 100) + ys = ys[chosenY] - x, y = success, status - min_y = min(min_y, min(y, default=0)) - max_y = max(max_y, max(y, default=0)) + min_y = min(min_y, min(ys, default=0)) + max_y = max(max_y, max(ys, default=0)) - self.subplot.plot(x, y) + self.subplot.plot(xs, ys) legend.append('{} ({})'.format(fit.name, fit.ship.item.getShortName())) except Exception as ex: pyfalog.warning("Invalid values in '{0}'", fit.name)