Reorganize graph folder structure

This commit is contained in:
DarkPhoenix
2019-08-03 17:23:34 +03:00
parent d2b71d97d2
commit d213e94860
43 changed files with 175 additions and 61 deletions

21
graphs/gui/__init__.py Normal file
View File

@@ -0,0 +1,21 @@
# =============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
from .frame import GraphFrame

341
graphs/gui/frame.py Normal file
View File

@@ -0,0 +1,341 @@
# =============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
import itertools
import os
import traceback
# noinspection PyPackageRequirements
import wx
from logbook import Logger
import gui.display
import gui.globalEvents as GE
import gui.mainFrame
from eos.saveddata.fit import Fit
from eos.saveddata.targetProfile import TargetProfile
from graphs.data.base import FitGraph
from gui.bitmap_loader import BitmapLoader
from service.const import GraphCacheCleanupReason
from service.settings import GraphSettings
from .panel import GraphControlPanel
pyfalog = Logger(__name__)
try:
import matplotlib as mpl
mpl_version = int(mpl.__version__[0]) or -1
if mpl_version >= 2:
mpl.use('wxagg')
graphFrame_enabled = True
else:
graphFrame_enabled = False
from matplotlib.patches import Patch
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as Canvas
from matplotlib.figure import Figure
except ImportError as e:
pyfalog.warning('Matplotlib failed to import. Likely missing or incompatible version.')
graphFrame_enabled = False
except Exception:
# We can get exceptions deep within matplotlib. Catch those. See GH #1046
tb = traceback.format_exc()
pyfalog.critical('Exception when importing Matplotlib. Continuing without importing.')
pyfalog.critical(tb)
graphFrame_enabled = False
class GraphFrame(wx.Frame):
def __init__(self, parent, style=wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE | wx.FRAME_FLOAT_ON_PARENT):
global graphFrame_enabled
if not graphFrame_enabled:
pyfalog.warning('Matplotlib is not enabled. Skipping initialization.')
return
wx.Frame.__init__(self, parent, title='pyfa: Graph Generator', style=style, size=(520, 390))
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.SetIcon(wx.Icon(BitmapLoader.getBitmap('graphs_small', 'gui')))
# Remove matplotlib font cache, see #234
try:
cache_dir = mpl._get_cachedir()
except:
cache_dir = os.path.expanduser(os.path.join('~', '.matplotlib'))
cache_file = os.path.join(cache_dir, 'fontList.cache')
if os.access(cache_dir, os.W_OK | os.X_OK) and os.path.isfile(cache_file):
os.remove(cache_file)
mainSizer = wx.BoxSizer(wx.VERTICAL)
# Layout - graph selector
self.graphSelection = wx.Choice(self, wx.ID_ANY, style=0)
self.graphSelection.Bind(wx.EVT_CHOICE, self.OnGraphSwitched)
mainSizer.Add(self.graphSelection, 0, wx.EXPAND)
# Layout - plot area
self.figure = Figure(figsize=(5, 3), tight_layout={'pad': 1.08})
rgbtuple = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE).Get()
clr = [c / 255. for c in rgbtuple]
self.figure.set_facecolor(clr)
self.figure.set_edgecolor(clr)
self.canvas = Canvas(self, -1, self.figure)
self.canvas.SetBackgroundColour(wx.Colour(*rgbtuple))
self.subplot = self.figure.add_subplot(111)
self.subplot.grid(True)
mainSizer.Add(self.canvas, 1, wx.EXPAND)
mainSizer.Add(wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0, wx.EXPAND)
# Layout - graph control panel
self.ctrlPanel = GraphControlPanel(self, self)
mainSizer.Add(self.ctrlPanel, 0, wx.EXPAND | wx.ALL, 0)
self.SetSizer(mainSizer)
# Setup - graph selector
for view in FitGraph.views:
self.graphSelection.Append(view.name, view())
self.graphSelection.SetSelection(0)
self.ctrlPanel.updateControls(layout=False)
# Event bindings - local events
self.Bind(wx.EVT_CLOSE, self.closeEvent)
self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent)
# Event bindings - external events
self.mainFrame.Bind(GE.FIT_RENAMED, self.OnFitRenamed)
self.mainFrame.Bind(GE.FIT_CHANGED, self.OnFitChanged)
self.mainFrame.Bind(GE.FIT_REMOVED, self.OnFitRemoved)
self.mainFrame.Bind(GE.TARGET_PROFILE_RENAMED, self.OnProfileRenamed)
self.mainFrame.Bind(GE.TARGET_PROFILE_CHANGED, self.OnProfileChanged)
self.mainFrame.Bind(GE.TARGET_PROFILE_REMOVED, self.OnProfileRemoved)
self.mainFrame.Bind(GE.GRAPH_OPTION_CHANGED, self.OnGraphOptionChanged)
self.Layout()
self.UpdateWindowSize()
self.draw()
def UpdateWindowSize(self):
curW, curH = self.GetSize()
bestW, bestH = self.GetBestSize()
newW = max(curW, bestW)
newH = max(curH, bestH)
if newW > curW or newH > curH:
newSize = wx.Size(newW, newH)
self.SetSize(newSize)
self.SetMinSize(newSize)
def closeEvent(self, event):
self.closeWindow()
event.Skip()
def kbEvent(self, event):
keycode = event.GetKeyCode()
mstate = wx.GetMouseState()
if keycode == wx.WXK_ESCAPE and mstate.GetModifiers() == wx.MOD_NONE:
self.closeWindow()
return
event.Skip()
# Fit events
def OnFitRenamed(self, event):
event.Skip()
self.ctrlPanel.OnFitRenamed(event)
self.draw()
def OnFitChanged(self, event):
event.Skip()
for fitID in event.fitIDs:
self.clearCache(reason=GraphCacheCleanupReason.fitChanged, extraData=fitID)
self.ctrlPanel.OnFitChanged(event)
self.draw()
def OnFitRemoved(self, event):
event.Skip()
self.clearCache(reason=GraphCacheCleanupReason.fitRemoved, extraData=event.fitID)
self.ctrlPanel.OnFitRemoved(event)
self.draw()
# Target profile events
def OnProfileRenamed(self, event):
event.Skip()
self.ctrlPanel.OnProfileRenamed(event)
self.draw()
def OnProfileChanged(self, event):
event.Skip()
self.clearCache(reason=GraphCacheCleanupReason.profileChanged, extraData=event.profileID)
self.ctrlPanel.OnProfileChanged(event)
self.draw()
def OnProfileRemoved(self, event):
event.Skip()
self.clearCache(reason=GraphCacheCleanupReason.profileRemoved, extraData=event.profileID)
self.ctrlPanel.OnProfileRemoved(event)
self.draw()
def OnGraphOptionChanged(self, event):
event.Skip()
self.clearCache(reason=GraphCacheCleanupReason.optionChanged)
self.draw()
def OnGraphSwitched(self, event):
view = self.getView()
GraphSettings.getInstance().set('selectedGraph', view.internalName)
self.clearCache(reason=GraphCacheCleanupReason.graphSwitched)
self.ctrlPanel.updateControls()
self.draw()
event.Skip()
def closeWindow(self):
self.mainFrame.Unbind(GE.FIT_RENAMED, handler=self.OnFitRenamed)
self.mainFrame.Unbind(GE.FIT_CHANGED, handler=self.OnFitChanged)
self.mainFrame.Unbind(GE.FIT_REMOVED, handler=self.OnFitRemoved)
self.mainFrame.Unbind(GE.TARGET_PROFILE_RENAMED, handler=self.OnProfileRenamed)
self.mainFrame.Unbind(GE.TARGET_PROFILE_CHANGED, handler=self.OnProfileChanged)
self.mainFrame.Unbind(GE.TARGET_PROFILE_REMOVED, handler=self.OnProfileRemoved)
self.mainFrame.Unbind(GE.GRAPH_OPTION_CHANGED, handler=self.OnGraphOptionChanged)
self.Destroy()
def getView(self):
return self.graphSelection.GetClientData(self.graphSelection.GetSelection())
def clearCache(self, reason, extraData=None):
self.getView().clearCache(reason, extraData)
def draw(self):
global mpl_version
# Eee #1430. draw() is not being unbound properly when the window closes.
# This is an easy fix, but not a proper solution
if not self:
pyfalog.warning('GraphFrame handled event, however GraphFrame no longer exists. Ignoring event')
return
self.subplot.clear()
self.subplot.grid(True)
legend = []
min_y = 0 if self.ctrlPanel.showY0 else None
max_y = 0 if self.ctrlPanel.showY0 else None
chosenX = self.ctrlPanel.xType
chosenY = self.ctrlPanel.yType
self.subplot.set(xlabel=self.ctrlPanel.formatLabel(chosenX), ylabel=self.ctrlPanel.formatLabel(chosenY))
mainInput, miscInputs = self.ctrlPanel.getValues()
view = self.getView()
fits = self.ctrlPanel.fits
if view.hasTargets:
targets = self.ctrlPanel.targets
iterList = tuple(itertools.product(fits, targets))
else:
iterList = tuple((f, None) for f in fits)
for fit, target in iterList:
try:
xs, ys = view.getPlotPoints(mainInput, miscInputs, chosenX, chosenY, fit, target)
# Figure out min and max Y
min_y_this = min(ys, default=None)
if min_y is None:
min_y = min_y_this
elif min_y_this is not None:
min_y = min(min_y, min_y_this)
max_y_this = max(ys, default=None)
if max_y is None:
max_y = max_y_this
elif max_y_this is not None:
max_y = max(max_y, max_y_this)
if len(xs) == 1 and len(ys) == 1:
self.subplot.plot(xs, ys, '.')
else:
self.subplot.plot(xs, ys)
if target is None:
legend.append(self.getObjName(fit))
else:
legend.append('{} vs {}'.format(self.getObjName(fit), self.getObjName(target)))
except Exception as ex:
pyfalog.warning('Invalid values in "{0}"', fit.name)
self.canvas.draw()
self.Refresh()
return
# Special case for when we do not show Y = 0 and have no fits
if min_y is None:
min_y = 0
if max_y is None:
max_y = 0
# Extend range a little for some visual space
y_range = max_y - min_y
min_y -= y_range * 0.05
max_y += y_range * 0.05
if min_y == max_y:
min_y -= min_y * 0.05
max_y += min_y * 0.05
if min_y == max_y:
min_y -= 5
max_y += 5
self.subplot.set_ylim(bottom=min_y, top=max_y)
legend2 = []
legend_colors = {
0: 'blue',
1: 'orange',
2: 'green',
3: 'red',
4: 'purple',
5: 'brown',
6: 'pink',
7: 'grey',
}
for i, i_name in enumerate(legend):
try:
selected_color = legend_colors[i]
except:
selected_color = None
legend2.append(Patch(color=selected_color, label=i_name), )
if len(legend2) > 0 and self.ctrlPanel.showLegend:
leg = self.subplot.legend(handles=legend2)
for t in leg.get_texts():
t.set_fontsize('small')
for l in leg.get_lines():
l.set_linewidth(1)
self.canvas.draw()
self.Refresh()
@staticmethod
def getObjName(thing):
if isinstance(thing, Fit):
return '{} ({})'.format(thing.name, thing.ship.item.getShortName())
elif isinstance(thing, TargetProfile):
return thing.name
return ''

324
graphs/gui/lists.py Normal file
View File

@@ -0,0 +1,324 @@
# =============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
# noinspection PyPackageRequirements
import wx
import gui.display
from eos.saveddata.targetProfile import TargetProfile
from gui.contextMenu import ContextMenu
from service.const import GraphCacheCleanupReason
from service.fit import Fit
class BaseList(gui.display.Display):
DEFAULT_COLS = (
'Base Icon',
'Base Name')
def __init__(self, graphFrame, parent):
super().__init__(parent)
self.graphFrame = graphFrame
self.fits = []
self.hoveredRow = None
self.hoveredColumn = None
self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent)
self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick)
self.Bind(wx.EVT_MOTION, self.OnMouseMove)
self.Bind(wx.EVT_LEAVE_WINDOW, self.OnLeaveWindow)
def refreshExtraColumns(self, extraColSpecs):
baseColNames = set()
for baseColName in self.DEFAULT_COLS:
if ":" in baseColName:
baseColName = baseColName.split(":", 1)[0]
baseColNames.add(baseColName)
columnsToRemove = set()
for col in self.activeColumns:
if col.name not in baseColNames:
columnsToRemove.add(col)
for col in columnsToRemove:
self.removeColumn(col)
for colSpec in extraColSpecs:
self.appendColumnBySpec(colSpec)
self.refreshView()
def handleDrag(self, type, fitID):
if type == 'fit':
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit not in self.fits:
self.fits.append(fit)
self.updateView()
self.graphFrame.draw()
def kbEvent(self, event):
keycode = event.GetKeyCode()
mstate = wx.GetMouseState()
if keycode == 65 and mstate.GetModifiers() == wx.MOD_CONTROL:
self.selectAll()
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and mstate.GetModifiers() == wx.MOD_NONE:
self.removeListItems(self.getSelectedListItems())
event.Skip()
def OnLeftDClick(self, event):
row, _ = self.HitTest(event.Position)
item = self.getListItem(row)
if item is None:
return
self.removeListItems([item])
def OnMouseMove(self, event):
row, _, col = self.HitTestSubItem(event.Position)
if row != self.hoveredRow or col != self.hoveredColumn:
if self.ToolTip is not None:
self.SetToolTip(None)
else:
self.hoveredRow = row
self.hoveredColumn = col
if row != -1 and col != -1 and col < self.ColumnCount:
item = self.getListItem(row)
if item is None:
return
tooltip = self.activeColumns[col].getToolTip(item)
if tooltip:
self.SetToolTip(tooltip)
else:
self.SetToolTip(None)
else:
self.SetToolTip(self.defaultTTText)
event.Skip()
def OnLeaveWindow(self, event):
self.SetToolTip(None)
self.hoveredRow = None
self.hoveredColumn = None
event.Skip()
# Fit events
def OnFitRenamed(self, event):
if event.fitID in [f.ID for f in self.fits]:
self.updateView()
def OnFitChanged(self, event):
if set(event.fitIDs).union(f.ID for f in self.fits):
self.updateView()
def OnFitRemoved(self, event):
fit = next((f for f in self.fits if f.ID == event.fitID), None)
if fit is not None:
self.fits.remove(fit)
self.updateView()
@property
def defaultTTText(self):
raise NotImplementedError
def refreshView(self):
raise NotImplementedError
def updateView(self):
raise NotImplementedError
def getListItem(self, row):
raise NotImplementedError
def removeListItems(self, items):
raise NotImplementedError
def getSelectedListItems(self):
items = []
for row in self.getSelectedRows():
item = self.getListItem(row)
if item is None:
continue
items.append(item)
return items
# Context menu handlers
def addFit(self, fit):
if fit is None:
return
if fit in self.fits:
return
self.fits.append(fit)
self.updateView()
self.graphFrame.draw()
def getExistingFitIDs(self):
return [f.ID for f in self.fits]
def addFitsByIDs(self, fitIDs):
sFit = Fit.getInstance()
for fitID in fitIDs:
fit = sFit.getFit(fitID)
if fit is not None:
self.fits.append(fit)
self.updateView()
self.graphFrame.draw()
class FitList(BaseList):
def __init__(self, graphFrame, parent):
super().__init__(graphFrame, parent)
self.Bind(wx.EVT_CONTEXT_MENU, self.spawnMenu)
fit = Fit.getInstance().getFit(self.graphFrame.mainFrame.getActiveFit())
if fit is not None:
self.fits.append(fit)
self.updateView()
def refreshView(self):
self.refresh(self.fits)
def updateView(self):
self.update(self.fits)
def spawnMenu(self, event):
selection = self.getSelectedListItems()
clickedPos = self.getRowByAbs(event.Position)
mainItem = self.getListItem(clickedPos)
sourceContext = 'graphFitList'
itemContext = None if mainItem is None else 'Fit'
menu = ContextMenu.getMenu(self, mainItem, selection, (sourceContext, itemContext))
if menu:
self.PopupMenu(menu)
def getListItem(self, row):
if row == -1:
return None
try:
return self.fits[row]
except IndexError:
return None
def removeListItems(self, items):
toRemove = [i for i in items if i in self.fits]
if not toRemove:
return
for fit in toRemove:
self.fits.remove(fit)
self.updateView()
for fit in toRemove:
self.graphFrame.clearCache(reason=GraphCacheCleanupReason.fitRemoved, extraData=fit.ID)
self.graphFrame.draw()
@property
def defaultTTText(self):
return 'Drag a fit into this list to graph it'
class TargetList(BaseList):
def __init__(self, graphFrame, parent):
super().__init__(graphFrame, parent)
self.Bind(wx.EVT_CONTEXT_MENU, self.spawnMenu)
self.profiles = []
self.profiles.append(TargetProfile.getIdeal())
self.updateView()
def refreshView(self):
self.refresh(self.targets)
def updateView(self):
self.update(self.targets)
def spawnMenu(self, event):
selection = self.getSelectedListItems()
clickedPos = self.getRowByAbs(event.Position)
mainItem = self.getListItem(clickedPos)
sourceContext = 'graphTgtList'
itemContext = None if mainItem is None else 'Target'
menu = ContextMenu.getMenu(self, mainItem, selection, (sourceContext, itemContext))
if menu:
self.PopupMenu(menu)
def getListItem(self, row):
if row == -1:
return None
numFits = len(self.fits)
numProfiles = len(self.profiles)
if (numFits + numProfiles) == 0:
return None
if row < numFits:
return self.fits[row]
else:
return self.profiles[row - numFits]
def removeListItems(self, items):
fitsToRemove = [i for i in items if i in self.fits]
profilesToRemove = [i for i in items if i in self.profiles]
if not fitsToRemove and not profilesToRemove:
return
for fit in fitsToRemove:
self.fits.remove(fit)
for profile in profilesToRemove:
self.profiles.remove(profile)
self.updateView()
for fit in fitsToRemove:
self.graphFrame.clearCache(reason=GraphCacheCleanupReason.fitRemoved, extraData=fit.ID)
for profile in profilesToRemove:
self.graphFrame.clearCache(reason=GraphCacheCleanupReason.profileRemoved, extraData=profile.ID)
self.graphFrame.draw()
# Target profile events
def OnProfileRenamed(self, event):
if event.profileID in [tp.ID for tp in self.profiles]:
self.updateView()
def OnProfileChanged(self, event):
if event.profileID in [tp.ID for tp in self.profiles]:
self.updateView()
def OnProfileRemoved(self, event):
profile = next((tp for tp in self.profiles if tp.ID == event.profileID), None)
if profile is not None:
self.profiles.remove(profile)
self.updateView()
@property
def targets(self):
return self.fits + self.profiles
@property
def defaultTTText(self):
return 'Drag a fit into this list to have your fits graphed against it'
# Context menu handlers
def addProfile(self, profile):
if profile is None:
return
if profile in self.profiles:
return
self.profiles.append(profile)
self.updateView()
self.graphFrame.draw()

391
graphs/gui/panel.py Normal file
View File

@@ -0,0 +1,391 @@
# =============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
from collections import namedtuple
# noinspection PyPackageRequirements
import wx
from gui.bitmap_loader import BitmapLoader
from gui.contextMenu import ContextMenu
from gui.utils.inputs import FloatBox, FloatRangeBox
from service.const import GraphCacheCleanupReason
from service.fit import Fit
from .lists import FitList, TargetList
from .vector import VectorPicker
InputData = namedtuple('InputData', ('handle', 'unit', 'value'))
InputBox = namedtuple('InputBox', ('handle', 'unit', 'textBox', 'icon', 'label'))
class GraphControlPanel(wx.Panel):
def __init__(self, graphFrame, parent):
super().__init__(parent)
self.graphFrame = graphFrame
self._mainInputBox = None
self._miscInputBoxes = []
self._storedRanges = {}
self._storedConsts = {}
mainSizer = wx.BoxSizer(wx.VERTICAL)
optsSizer = wx.BoxSizer(wx.HORIZONTAL)
commonOptsSizer = wx.BoxSizer(wx.VERTICAL)
ySubSelectionSizer = wx.BoxSizer(wx.HORIZONTAL)
yText = wx.StaticText(self, wx.ID_ANY, 'Axis Y:')
ySubSelectionSizer.Add(yText, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.ySubSelection = wx.Choice(self, wx.ID_ANY)
self.ySubSelection.Bind(wx.EVT_CHOICE, self.OnYTypeUpdate)
ySubSelectionSizer.Add(self.ySubSelection, 1, wx.EXPAND | wx.ALL, 0)
commonOptsSizer.Add(ySubSelectionSizer, 0, wx.EXPAND | wx.ALL, 0)
xSubSelectionSizer = wx.BoxSizer(wx.HORIZONTAL)
xText = wx.StaticText(self, wx.ID_ANY, 'Axis X:')
xSubSelectionSizer.Add(xText, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
self.xSubSelection = wx.Choice(self, wx.ID_ANY)
self.xSubSelection.Bind(wx.EVT_CHOICE, self.OnXTypeUpdate)
xSubSelectionSizer.Add(self.xSubSelection, 1, wx.EXPAND | wx.ALL, 0)
commonOptsSizer.Add(xSubSelectionSizer, 0, wx.EXPAND | wx.TOP, 5)
self.showLegendCb = wx.CheckBox(self, wx.ID_ANY, 'Show legend', wx.DefaultPosition, wx.DefaultSize, 0)
self.showLegendCb.SetValue(True)
self.showLegendCb.Bind(wx.EVT_CHECKBOX, self.OnShowLegendChange)
commonOptsSizer.Add(self.showLegendCb, 0, wx.EXPAND | wx.TOP, 5)
self.showY0Cb = wx.CheckBox(self, wx.ID_ANY, 'Always show Y = 0', wx.DefaultPosition, wx.DefaultSize, 0)
self.showY0Cb.SetValue(True)
self.showY0Cb.Bind(wx.EVT_CHECKBOX, self.OnShowY0Change)
commonOptsSizer.Add(self.showY0Cb, 0, wx.EXPAND | wx.TOP, 5)
optsSizer.Add(commonOptsSizer, 0, wx.EXPAND | wx.RIGHT, 10)
graphOptsSizer = wx.BoxSizer(wx.HORIZONTAL)
self.inputsSizer = wx.BoxSizer(wx.VERTICAL)
graphOptsSizer.Add(self.inputsSizer, 1, wx.EXPAND | wx.ALL, 0)
vectorSize = 90 if 'wxGTK' in wx.PlatformInfo else 75
self.srcVectorSizer = wx.BoxSizer(wx.VERTICAL)
self.srcVectorLabel = wx.StaticText(self, wx.ID_ANY, '')
self.srcVectorSizer.Add(self.srcVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL| wx.BOTTOM, 5)
self.srcVector = VectorPicker(self, style=wx.NO_BORDER, size=vectorSize, offset=0)
self.srcVector.Bind(VectorPicker.EVT_VECTOR_CHANGED, self.OnFieldChanged)
self.srcVectorSizer.Add(self.srcVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0)
graphOptsSizer.Add(self.srcVectorSizer, 0, wx.EXPAND | wx.LEFT, 15)
self.tgtVectorSizer = wx.BoxSizer(wx.VERTICAL)
self.tgtVectorLabel = wx.StaticText(self, wx.ID_ANY, '')
self.tgtVectorSizer.Add(self.tgtVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.BOTTOM, 5)
self.tgtVector = VectorPicker(self, style=wx.NO_BORDER, size=vectorSize, offset=0)
self.tgtVector.Bind(VectorPicker.EVT_VECTOR_CHANGED, self.OnFieldChanged)
self.tgtVectorSizer.Add(self.tgtVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0)
graphOptsSizer.Add(self.tgtVectorSizer, 0, wx.EXPAND | wx.LEFT, 10)
optsSizer.Add(graphOptsSizer, 1, wx.EXPAND | wx.ALL, 0)
contextSizer = wx.BoxSizer(wx.VERTICAL)
savedFont = self.GetFont()
contextIconFont = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
contextIconFont.SetPointSize(8)
self.SetFont(contextIconFont)
self.contextIcon = wx.StaticText(self, wx.ID_ANY, '\u2630', size=wx.Size((10, -1)))
self.contextIcon.Bind(wx.EVT_CONTEXT_MENU, self.contextMenuHandler)
self.contextIcon.Bind(wx.EVT_LEFT_UP, self.contextMenuHandler)
self.SetFont(savedFont)
contextSizer.Add(self.contextIcon, 0, wx.EXPAND | wx.ALL, 0)
optsSizer.Add(contextSizer, 0, wx.EXPAND | wx.ALL, 0)
mainSizer.Add(optsSizer, 0, wx.EXPAND | wx.ALL, 10)
srcTgtSizer = wx.BoxSizer(wx.HORIZONTAL)
self.fitList = FitList(graphFrame, self)
self.fitList.SetMinSize((270, -1))
srcTgtSizer.Add(self.fitList, 1, wx.EXPAND | wx.ALL, 0)
self.targetList = TargetList(graphFrame, self)
self.targetList.SetMinSize((270, -1))
srcTgtSizer.Add(self.targetList, 1, wx.EXPAND | wx.LEFT, 10)
mainSizer.Add(srcTgtSizer, 1, wx.EXPAND | wx.LEFT | wx.BOTTOM | wx.RIGHT, 10)
self.SetSizer(mainSizer)
self.inputTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.OnInputTimer, self.inputTimer)
self._setVectorDefaults()
def updateControls(self, layout=True):
if layout:
self.Freeze()
self._clearStoredValues()
view = self.graphFrame.getView()
self.ySubSelection.Clear()
self.xSubSelection.Clear()
for yDef in view.yDefs:
self.ySubSelection.Append(self.formatLabel(yDef), yDef)
self.ySubSelection.SetSelection(0)
self.ySubSelection.Enable(len(view.yDefs) > 1)
for xDef in view.xDefs:
self.xSubSelection.Append(self.formatLabel(xDef), xDef)
self.xSubSelection.SetSelection(0)
self.xSubSelection.Enable(len(view.xDefs) > 1)
# Vectors
self._setVectorDefaults()
if view.srcVectorDef is not None:
self.srcVectorLabel.SetLabel(view.srcVectorDef.label)
self.srcVector.Show(True)
self.srcVectorLabel.Show(True)
else:
self.srcVector.Show(False)
self.srcVectorLabel.Show(False)
if view.tgtVectorDef is not None:
self.tgtVectorLabel.SetLabel(view.tgtVectorDef.label)
self.tgtVector.Show(True)
self.tgtVectorLabel.Show(True)
else:
self.tgtVector.Show(False)
self.tgtVectorLabel.Show(False)
# Source and target list
self.fitList.refreshExtraColumns(view.srcExtraCols)
self.targetList.refreshExtraColumns(view.tgtExtraCols)
self.targetList.Show(view.hasTargets)
# Inputs
self._updateInputs(storeInputs=False)
# Context icon
self.contextIcon.Show(ContextMenu.hasMenu(self, None, None, (view.internalName,)))
if layout:
self.graphFrame.Layout()
self.graphFrame.UpdateWindowSize()
self.Thaw()
def _updateInputs(self, storeInputs=True):
if storeInputs:
self._storeCurrentValues()
# Clean up old inputs
for inputBox in (self._mainInputBox, *self._miscInputBoxes):
if inputBox is None:
continue
for child in (inputBox.textBox, inputBox.icon, inputBox.label):
if child is not None:
child.Destroy()
self.inputsSizer.Clear()
self._mainInputBox = None
self._miscInputBoxes.clear()
# Update vectors
def handleVector(vectorDef, vector, handledHandles, mainInputHandle):
handledHandles.add(vectorDef.lengthHandle)
handledHandles.add(vectorDef.angleHandle)
try:
storedLength = self._storedConsts[(vectorDef.lengthHandle, vectorDef.lengthUnit)]
except KeyError:
pass
else:
vector.SetLength(storedLength / 100)
try:
storedAngle = self._storedConsts[(vectorDef.angleHandle, vectorDef.angleUnit)]
except KeyError:
pass
else:
vector.SetAngle(storedAngle)
vector.SetDirectionOnly(vectorDef.lengthHandle == mainInputHandle)
view = self.graphFrame.getView()
handledHandles = set()
if view.srcVectorDef is not None:
handleVector(view.srcVectorDef, self.srcVector, handledHandles, self.xType.mainInput[0])
if view.tgtVectorDef is not None:
handleVector(view.tgtVectorDef, self.tgtVector, handledHandles, self.xType.mainInput[0])
# Update inputs
def addInputField(inputDef, handledHandles, mainInput=False):
handledHandles.add(inputDef.handle)
fieldSizer = wx.BoxSizer(wx.HORIZONTAL)
tooltipText = (inputDef.mainTooltip if mainInput else inputDef.secondaryTooltip) or ''
if mainInput:
fieldTextBox = FloatRangeBox(self, self._storedRanges.get((inputDef.handle, inputDef.unit), inputDef.defaultRange))
else:
fieldTextBox = FloatBox(self, self._storedConsts.get((inputDef.handle, inputDef.unit), inputDef.defaultValue))
fieldTextBox.Bind(wx.EVT_TEXT, self.OnFieldChanged)
fieldTextBox.SetToolTip(wx.ToolTip(tooltipText))
fieldSizer.Add(fieldTextBox, 0, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5)
fieldIcon = None
if inputDef.iconID is not None:
icon = BitmapLoader.getBitmap(inputDef.iconID, 'icons')
if icon is not None:
fieldIcon = wx.StaticBitmap(self)
fieldIcon.SetBitmap(icon)
fieldIcon.SetToolTip(wx.ToolTip(tooltipText))
fieldSizer.Add(fieldIcon, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 3)
fieldLabel = wx.StaticText(self, wx.ID_ANY, self.formatLabel(inputDef))
fieldLabel.SetToolTip(wx.ToolTip(tooltipText))
fieldSizer.Add(fieldLabel, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0)
self.inputsSizer.Add(fieldSizer, 0, wx.EXPAND | wx.BOTTOM, 5)
# Store info about added input box
inputBox = InputBox(handle=inputDef.handle, unit=inputDef.unit, textBox=fieldTextBox, icon=fieldIcon, label=fieldLabel)
if mainInput:
self._mainInputBox = inputBox
else:
self._miscInputBoxes.append(inputBox)
addInputField(view.inputMap[self.xType.mainInput], handledHandles, mainInput=True)
for inputDef in view.inputs:
if inputDef.mainOnly:
continue
if inputDef.handle in handledHandles:
continue
addInputField(inputDef, handledHandles)
def OnShowLegendChange(self, event):
event.Skip()
self.graphFrame.draw()
def OnShowY0Change(self, event):
event.Skip()
self.graphFrame.draw()
def OnYTypeUpdate(self, event):
event.Skip()
self.graphFrame.draw()
def OnXTypeUpdate(self, event):
event.Skip()
self._updateInputs()
self.graphFrame.Layout()
self.graphFrame.UpdateWindowSize()
self.graphFrame.draw()
def OnFieldChanged(self, event):
event.Skip()
self.inputTimer.Stop()
self.inputTimer.Start(Fit.getInstance().serviceFittingOptions['marketSearchDelay'], True)
def OnInputTimer(self, event):
event.Skip()
self.graphFrame.clearCache(reason=GraphCacheCleanupReason.inputChanged)
self.graphFrame.draw()
def getValues(self):
view = self.graphFrame.getView()
misc = []
processedHandles = set()
def addMiscData(handle, unit, value):
if handle in processedHandles:
return
inputData = InputData(handle=handle, unit=unit, value=value)
misc.append(inputData)
# Main input box
main = InputData(handle=self._mainInputBox.handle, unit=self._mainInputBox.unit, value=self._mainInputBox.textBox.GetValueRange())
processedHandles.add(self._mainInputBox.handle)
# Vectors
srcVectorDef = view.srcVectorDef
if srcVectorDef is not None:
if not self.srcVector.IsDirectionOnly:
addMiscData(handle=srcVectorDef.lengthHandle, unit=srcVectorDef.lengthUnit, value=self.srcVector.GetLength() * 100)
addMiscData(handle=srcVectorDef.angleHandle, unit=srcVectorDef.angleUnit, value=self.srcVector.GetAngle())
tgtVectorDef = view.tgtVectorDef
if tgtVectorDef is not None:
if not self.tgtVector.IsDirectionOnly:
addMiscData(handle=tgtVectorDef.lengthHandle, unit=tgtVectorDef.lengthUnit, value=self.tgtVector.GetLength() * 100)
addMiscData(handle=tgtVectorDef.angleHandle, unit=tgtVectorDef.angleUnit, value=self.tgtVector.GetAngle())
# Other input boxes
for inputBox in self._miscInputBoxes:
addMiscData(handle=inputBox.handle, unit=inputBox.unit, value=inputBox.textBox.GetValueFloat())
return main, misc
@property
def showLegend(self):
return self.showLegendCb.GetValue()
@property
def showY0(self):
return self.showY0Cb.GetValue()
@property
def yType(self):
return self.ySubSelection.GetClientData(self.ySubSelection.GetSelection())
@property
def xType(self):
return self.xSubSelection.GetClientData(self.xSubSelection.GetSelection())
@property
def fits(self):
return self.fitList.fits
@property
def targets(self):
return self.targetList.targets
# Fit events
def OnFitRenamed(self, event):
self.fitList.OnFitRenamed(event)
self.targetList.OnFitRenamed(event)
def OnFitChanged(self, event):
self.fitList.OnFitChanged(event)
self.targetList.OnFitChanged(event)
def OnFitRemoved(self, event):
self.fitList.OnFitRemoved(event)
self.targetList.OnFitRemoved(event)
# Target profile events
def OnProfileRenamed(self, event):
self.targetList.OnProfileRenamed(event)
def OnProfileChanged(self, event):
self.targetList.OnProfileChanged(event)
def OnProfileRemoved(self, event):
self.targetList.OnProfileRemoved(event)
def formatLabel(self, axisDef):
if axisDef.unit is None:
return axisDef.label
return '{}, {}'.format(axisDef.label, axisDef.unit)
def _storeCurrentValues(self):
main, misc = self.getValues()
if main is not None:
self._storedRanges[(main.handle, main.unit)] = main.value
for input in misc:
self._storedConsts[(input.handle, input.unit)] = input.value
def _clearStoredValues(self):
self._storedConsts.clear()
self._storedRanges.clear()
def _setVectorDefaults(self):
self.srcVector.SetValue(length=0, angle=90)
self.tgtVector.SetValue(length=1, angle=90)
def contextMenuHandler(self, event):
viewName = self.graphFrame.getView().internalName
menu = ContextMenu.getMenu(self, None, None, (viewName,))
if menu is not None:
self.PopupMenu(menu)
event.Skip()

242
graphs/gui/vector.py Normal file
View File

@@ -0,0 +1,242 @@
# =============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of pyfa.
#
# pyfa is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyfa is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with pyfa. If not, see <http://www.gnu.org/licenses/>.
# =============================================================================
import math
# noinspection PyPackageRequirements
import wx
from eos.utils.float import floatUnerr
class VectorPicker(wx.Window):
myEVT_VECTOR_CHANGED = wx.NewEventType()
EVT_VECTOR_CHANGED = wx.PyEventBinder(myEVT_VECTOR_CHANGED, 1)
def __init__(self, *args, **kwargs):
self._label = str(kwargs.pop('label', ''))
self._labelpos = int(kwargs.pop('labelpos', 0))
self._offset = float(kwargs.pop('offset', 0))
self._size = max(0, float(kwargs.pop('size', 50)))
self._fontsize = max(1, float(kwargs.pop('fontsize', 8)))
self._directionOnly = kwargs.pop('directionOnly', False)
super().__init__(*args, **kwargs)
self._font = wx.Font(self._fontsize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False)
self._angle = 0
self.__length = 1
self._left = False
self._right = False
self.SetToolTip(wx.ToolTip(self._tooltip))
self.Bind(wx.EVT_PAINT, self.OnPaint)
self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown)
self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel)
@property
def _tooltip(self):
if self._directionOnly:
return 'Click to set angle\nShift-click or right-click to snap to 15% angle'
else:
return 'Click to set angle and velocity\nShift-click or right-click to snap to 15% angle/5% speed increments\nMouse wheel to change velocity only'
@property
def _length(self):
if self._directionOnly:
return 1
else:
return self.__length
@_length.setter
def _length(self, newLength):
self.__length = newLength
def DoGetBestSize(self):
return wx.Size(self._size, self._size)
def AcceptsFocusFromKeyboard(self):
return False
def GetValue(self):
return self._angle, self._length
def GetAngle(self):
return self._angle
def GetLength(self):
return self._length
def SetValue(self, angle=None, length=None):
if angle is not None:
self._angle = min(max(angle, -180), 180)
if length is not None:
self._length = min(max(length, 0), 1)
self.Refresh()
def SetAngle(self, angle):
self.SetValue(angle, None)
def SetLength(self, length):
self.SetValue(None, length)
def OnPaint(self, event):
dc = wx.BufferedPaintDC(self)
self.Draw(dc)
def Draw(self, dc):
width, height = self.GetClientSize()
if not width or not height:
return
dc.SetBackground(wx.Brush(self.GetBackgroundColour(), wx.BRUSHSTYLE_SOLID))
dc.Clear()
dc.SetTextForeground(wx.Colour(0))
dc.SetFont(self._font)
radius = min(width, height) / 2 - 2
dc.SetBrush(wx.WHITE_BRUSH)
dc.DrawCircle(radius + 2, radius + 2, radius)
a = math.radians(self._angle + self._offset)
x = math.cos(a) * radius
y = math.sin(a) * radius
dc.DrawLine(radius + 2, radius + 2, radius + 2 + x * self._length, radius + 2 - y * self._length)
dc.SetBrush(wx.BLACK_BRUSH)
dc.DrawCircle(radius + 2 + x * self._length, radius + 2 - y * self._length, 2)
if self._label:
labelText = self._label
labelTextW, labelTextH = dc.GetTextExtent(labelText)
labelTextX = (radius * 2 + 4 - labelTextW) if (self._labelpos & 1) else 0
labelTextY = (radius * 2 + 4 - labelTextH) if (self._labelpos & 2) else 0
dc.DrawText(labelText, labelTextX, labelTextY)
if not self._directionOnly:
lengthText = '%d%%' % (100 * self._length,)
lengthTextW, lengthTextH = dc.GetTextExtent(lengthText)
lengthTextX = radius + 2 + x / 2 - y / 3 - lengthTextW / 2
lengthTextY = radius + 2 - y / 2 - x / 3 - lengthTextH / 2
dc.DrawText(lengthText, lengthTextX, lengthTextY)
angleText = '%d\u00B0' % (self._angle,)
angleTextW, angleTextH = dc.GetTextExtent(angleText)
angleTextX = radius + 2 - x / 2 - angleTextW / 2
angleTextY = radius + 2 + y / 2 - angleTextH / 2
dc.DrawText(angleText, angleTextX, angleTextY)
def OnEraseBackground(self, event):
pass
def OnLeftDown(self, event):
self._left = True
self.SetToolTip(None)
self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnLeftUp)
if not self._right:
self.Bind(wx.EVT_MOTION, self.OnMotion)
if not self.HasCapture():
self.CaptureMouse()
self.HandleMouseEvent(event)
def OnRightDown(self, event):
self._right = True
self.SetToolTip(None)
self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp)
self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnRightUp)
if not self._left:
self.Bind(wx.EVT_MOTION, self.OnMotion)
if not self.HasCapture():
self.CaptureMouse()
self.HandleMouseEvent(event)
def OnLeftUp(self, event):
self.HandleMouseEvent(event)
self.Unbind(wx.EVT_LEFT_UP, handler=self.OnLeftUp)
self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST, handler=self.OnLeftUp)
self._left = False
if not self._right:
self.Unbind(wx.EVT_MOTION, handler=self.OnMotion)
self.SendChangeEvent()
self.SetToolTip(wx.ToolTip(self._tooltip))
if self.HasCapture():
self.ReleaseMouse()
def OnRightUp(self, event):
self.HandleMouseEvent(event)
self.Unbind(wx.EVT_RIGHT_UP, handler=self.OnRightUp)
self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST, handler=self.OnRightUp)
self._right = False
if not self._left:
self.Unbind(wx.EVT_MOTION, handler=self.OnMotion)
self.SendChangeEvent()
self.SetToolTip(wx.ToolTip(self._tooltip))
if self.HasCapture():
self.ReleaseMouse()
def OnMotion(self, event):
self.HandleMouseEvent(event)
event.Skip()
def OnWheel(self, event):
amount = 0.1 * event.GetWheelRotation() / event.GetWheelDelta()
self._length = floatUnerr(min(max(self._length + amount, 0.0), 1.0))
self.Refresh()
self.SendChangeEvent()
def HandleMouseEvent(self, event):
width, height = self.GetClientSize()
if width and height:
center = min(width, height) / 2
x, y = event.GetPosition()
x = x - center
y = center - y
angle = self._angle
length = min((x ** 2 + y ** 2) ** 0.5 / (center - 2), 1.0)
if length < 0.01:
length = 0
else:
angle = ((math.degrees(math.atan2(y, x)) - self._offset + 180) % 360) - 180
if (self._right and not self._left) or event.ShiftDown():
angle = round(angle / 15.0) * 15.0
# floor() for length to make it easier to hit 0%, can still hit 100% outside the circle
length = math.floor(length / 0.05) * 0.05
if (angle != self._angle) or (length != self._length):
self._angle = angle
self._length = length
self.Refresh()
if (self._right and not self._left) or event.ShiftDown():
self.SendChangeEvent()
def SendChangeEvent(self):
changeEvent = wx.CommandEvent(self.myEVT_VECTOR_CHANGED, self.GetId())
changeEvent._object = self
changeEvent._angle = self._angle
changeEvent._length = self._length
self.GetEventHandler().ProcessEvent(changeEvent)
def SetDirectionOnly(self, val):
if self._directionOnly is val:
return
self._directionOnly = val
self.GetToolTip().SetTip(self._tooltip)
@property
def IsDirectionOnly(self):
return self._directionOnly