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

View File

@@ -1,7 +0,0 @@
# noinspection PyUnresolvedReferences
from gui.builtinGraphs import ( # noqa: E402,F401
fitDamageStats,
fitShieldRegen,
fitCapRegen,
fitMobility,
fitWarpTime)

View File

@@ -1,26 +0,0 @@
# =============================================================================
# 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 .cache import FitDataCache
from .defs import XDef, YDef, VectorDef, Input
from .getter import PointGetter, SmoothPointGetter
from .graph import FitGraph
# noinspection PyUnresolvedReferences
from gui.builtinGraphs import *

View File

@@ -1,31 +0,0 @@
# =============================================================================
# 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/>.
# =============================================================================
class FitDataCache:
def __init__(self):
self._data = {}
def clearForFit(self, fitID):
if fitID in self._data:
del self._data[fitID]
def clearAll(self):
self._data.clear()

View File

@@ -1,40 +0,0 @@
# =============================================================================
# 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
YDef = namedtuple('YDef', ('handle', 'unit', 'label'))
XDef = namedtuple('XDef', ('handle', 'unit', 'label', 'mainInput'))
VectorDef = namedtuple('VectorDef', ('lengthHandle', 'lengthUnit', 'angleHandle', 'angleUnit', 'label'))
class Input:
def __init__(self, handle, unit, label, iconID, defaultValue, defaultRange, mainOnly=False, mainTooltip=None, secondaryTooltip=None):
self.handle = handle
self.unit = unit
self.label = label
self.iconID = iconID
self.defaultValue = defaultValue
self.defaultRange = defaultRange
self.mainOnly = mainOnly
self.mainTooltip = mainTooltip
self.secondaryTooltip = secondaryTooltip

View File

@@ -1,97 +0,0 @@
# =============================================================================
# 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
from abc import ABCMeta, abstractmethod
class PointGetter(metaclass=ABCMeta):
def __init__(self, graph):
self.graph = graph
@abstractmethod
def getRange(self, xRange, miscParams, fit, tgt):
raise NotImplementedError
@abstractmethod
def getPoint(self, x, miscParams, fit, tgt):
raise NotImplementedError
class SmoothPointGetter(PointGetter, metaclass=ABCMeta):
def __init__(self, graph, baseResolution=50, extraDepth=2):
super().__init__(graph)
self._baseResolution = baseResolution
self._extraDepth = extraDepth
def getRange(self, xRange, miscParams, fit, tgt):
xs = []
ys = []
commonData = self._getCommonData(miscParams=miscParams, fit=fit, tgt=tgt)
def addExtraPoints(x1, y1, x2, y2, depth):
if depth <= 0 or y1 == y2:
return
newX = (x1 + x2) / 2
newY = self._calculatePoint(x=newX, miscParams=miscParams, fit=fit, tgt=tgt, commonData=commonData)
addExtraPoints(x1=prevX, y1=prevY, x2=newX, y2=newY, depth=depth - 1)
xs.append(newX)
ys.append(newY)
addExtraPoints(x1=newX, y1=newY, x2=x2, y2=y2, depth=depth - 1)
prevX = None
prevY = None
# Go through X points defined by our resolution setting
for x in self._xIterLinear(xRange):
y = self._calculatePoint(x=x, miscParams=miscParams, fit=fit, tgt=tgt, commonData=commonData)
if prevX is not None and prevY is not None:
# And if Y values of adjacent data points are not equal, add extra points
# depending on extra depth setting
addExtraPoints(x1=prevX, y1=prevY, x2=x, y2=y, depth=self._extraDepth)
prevX = x
prevY = y
xs.append(x)
ys.append(y)
return xs, ys
def getPoint(self, x, miscParams, fit, tgt):
commonData = self._getCommonData(miscParams=miscParams, fit=fit, tgt=tgt)
return self._calculatePoint(x=x, miscParams=miscParams, fit=fit, tgt=tgt, commonData=commonData)
def _xIterLinear(self, xRange):
xLow = min(xRange)
xHigh = max(xRange)
# Resolution defines amount of ranges between points here,
# not amount of points
step = (xHigh - xLow) / self._baseResolution
if step == 0 or math.isnan(step):
yield xLow
else:
for i in range(self._baseResolution + 1):
yield xLow + step * i
def _getCommonData(self, miscParams, fit, tgt):
return {}
@abstractmethod
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
raise NotImplementedError

View File

@@ -1,230 +0,0 @@
# =============================================================================
# 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
from abc import ABCMeta, abstractmethod
from collections import OrderedDict
from eos.saveddata.fit import Fit
from eos.saveddata.targetProfile import TargetProfile
from eos.utils.float import floatUnerr
from service.const import GraphCacheCleanupReason
class FitGraph(metaclass=ABCMeta):
# UI stuff
views = []
viewMap = {}
@classmethod
def register(cls):
FitGraph.views.append(cls)
FitGraph.viewMap[cls.internalName] = cls
def __init__(self):
# Format: {(fit ID, target type, target ID): data}
self._plotCache = {}
@property
@abstractmethod
def name(self):
raise NotImplementedError
@property
@abstractmethod
def internalName(self):
raise NotImplementedError
@property
@abstractmethod
def yDefs(self):
raise NotImplementedError
@property
def yDefMap(self):
return OrderedDict(((y.handle, y.unit), y) for y in self.yDefs)
@property
@abstractmethod
def xDefs(self):
raise NotImplementedError
@property
def xDefMap(self):
return OrderedDict(((x.handle, x.unit), x) for x in self.xDefs)
@property
def inputs(self):
raise NotImplementedError
@property
def inputMap(self):
return OrderedDict(((i.handle, i.unit), i) for i in self.inputs)
@property
def srcExtraCols(self):
return ()
@property
def tgtExtraCols(self):
return ()
srcVectorDef = None
tgtVectorDef = None
hasTargets = False
def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt=None):
if isinstance(tgt, Fit):
tgtType = 'fit'
elif isinstance(tgt, TargetProfile):
tgtType = 'profile'
else:
tgtType = None
cacheKey = (fit.ID, tgtType, getattr(tgt, 'ID', None))
try:
plotData = self._plotCache[cacheKey][(ySpec, xSpec)]
except KeyError:
plotData = self._calcPlotPoints(mainInput, miscInputs, xSpec, ySpec, fit, tgt)
self._plotCache.setdefault(cacheKey, {})[(ySpec, xSpec)] = plotData
return plotData
def clearCache(self, reason, extraData=None):
plotKeysToClear = set()
# If fit changed - clear plots which concern this fit
if reason in (GraphCacheCleanupReason.fitChanged, GraphCacheCleanupReason.fitRemoved):
for cacheKey in self._plotCache:
cacheFitID, cacheTgtType, cacheTgtID = cacheKey
if extraData == cacheFitID:
plotKeysToClear.add(cacheKey)
elif cacheTgtType == 'fit' and extraData == cacheTgtID:
plotKeysToClear.add(cacheKey)
# Same for profile
elif reason in (GraphCacheCleanupReason.profileChanged, GraphCacheCleanupReason.profileRemoved):
for cacheKey in self._plotCache:
cacheFitID, cacheTgtType, cacheTgtID = cacheKey
if cacheTgtType == 'profile' and extraData == cacheTgtID:
plotKeysToClear.add(cacheKey)
# Wipe out whole plot cache otherwise
else:
for cacheKey in self._plotCache:
plotKeysToClear.add(cacheKey)
# Do actual cleanup
for cacheKey in plotKeysToClear:
del self._plotCache[cacheKey]
# Process any internal caches graphs might have
self._clearInternalCache(reason, extraData)
def _clearInternalCache(self, reason, extraData):
return
# Calculation stuff
def _calcPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt):
mainParamRange, miscParams = self._normalizeInputs(mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt)
mainParamRange, miscParams = self._limitParams(mainParamRange=mainParamRange, miscParams=miscParams, fit=fit, tgt=tgt)
xs, ys = self._getPoints(xRange=mainParamRange[1], miscParams=miscParams, xSpec=xSpec, ySpec=ySpec, fit=fit, tgt=tgt)
ys = self._denormalizeValues(ys, ySpec, fit, tgt)
# Sometimes x denormalizer may fail (e.g. during conversion of 0 ship speed to %).
# If both inputs and outputs are in %, do some extra processing to at least have
# proper graph which shows that fit has the same value over whole specified
# relative parameter range
try:
xs = self._denormalizeValues(xs, xSpec, fit, tgt)
except ZeroDivisionError:
if mainInput.unit == xSpec.unit == '%' and len(set(floatUnerr(y) for y in ys)) == 1:
xs = [min(mainInput.value), max(mainInput.value)]
ys = [ys[0], ys[0]]
else:
raise
else:
# Same for NaN which means we tried to denormalize infinity values, which might be the
# case for the ideal target profile with infinite signature radius
if mainInput.unit == xSpec.unit == '%' and all(math.isnan(x) for x in xs):
xs = [min(mainInput.value), max(mainInput.value)]
ys = [ys[0], ys[0]]
return xs, ys
_normalizers = {}
def _normalizeInputs(self, mainInput, miscInputs, fit, tgt):
key = (mainInput.handle, mainInput.unit)
if key in self._normalizers:
normalizer = self._normalizers[key]
mainParamRange = (mainInput.handle, tuple(normalizer(v, fit, tgt) for v in mainInput.value))
else:
mainParamRange = (mainInput.handle, mainInput.value)
miscParams = []
for miscInput in miscInputs:
key = (miscInput.handle, miscInput.unit)
if key in self._normalizers:
normalizer = self._normalizers[key]
miscParam = (miscInput.handle, normalizer(miscInput.value, fit, tgt))
else:
miscParam = (miscInput.handle, miscInput.value)
miscParams.append(miscParam)
return mainParamRange, miscParams
_limiters = {}
def _limitParams(self, mainParamRange, miscParams, fit, tgt):
def limitToRange(val, limitRange):
if val is None:
return None
val = max(val, min(limitRange))
val = min(val, max(limitRange))
return val
mainHandle, mainValue = mainParamRange
if mainHandle in self._limiters:
limiter = self._limiters[mainHandle]
newMainParamRange = (mainHandle, tuple(limitToRange(v, limiter(fit, tgt)) for v in mainValue))
else:
newMainParamRange = mainParamRange
newMiscParams = []
for miscParam in miscParams:
miscHandle, miscValue = miscParam
if miscHandle in self._limiters:
limiter = self._limiters[miscHandle]
newMiscParam = (miscHandle, limitToRange(miscValue, limiter(fit, tgt)))
newMiscParams.append(newMiscParam)
else:
newMiscParams.append(miscParam)
return newMainParamRange, newMiscParams
_getters = {}
def _getPoints(self, xRange, miscParams, xSpec, ySpec, fit, tgt):
try:
getterClass = self._getters[(xSpec.handle, ySpec.handle)]
except KeyError:
return [], []
else:
getter = getterClass(graph=self)
return getter.getRange(xRange=xRange, miscParams=miscParams, fit=fit, tgt=tgt)
_denormalizers = {}
def _denormalizeValues(self, values, axisSpec, fit, tgt):
key = (axisSpec.handle, axisSpec.unit)
if key in self._denormalizers:
denormalizer = self._denormalizers[key]
values = [denormalizer(v, fit, tgt) for v in values]
return values

View File

@@ -1 +0,0 @@
import gui.builtinGraphs.fitCapRegen.graph # noqa: E402,F401

View File

@@ -1,93 +0,0 @@
# =============================================================================
# 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
from gui.builtinGraphs.base import SmoothPointGetter
class Time2CapAmountGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
return {
'maxCapAmount': fit.ship.getModifiedItemAttr('capacitorCapacity'),
'capRegenTime': fit.ship.getModifiedItemAttr('rechargeRate') / 1000}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
time = x
capAmount = calculateCapAmount(
maxCapAmount=commonData['maxCapAmount'],
capRegenTime=commonData['capRegenTime'],
time=time)
return capAmount
class Time2CapRegenGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
return {
'maxCapAmount': fit.ship.getModifiedItemAttr('capacitorCapacity'),
'capRegenTime': fit.ship.getModifiedItemAttr('rechargeRate') / 1000}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
time = x
capAmount = calculateCapAmount(
maxCapAmount=commonData['maxCapAmount'],
capRegenTime=commonData['capRegenTime'],
time=time)
capRegen = calculateCapRegen(
maxCapAmount=commonData['maxCapAmount'],
capRegenTime=commonData['capRegenTime'],
currentCapAmount=capAmount)
return capRegen
# Useless, but valid combination of x and y
class CapAmount2CapAmountGetter(SmoothPointGetter):
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
capAmount = x
return capAmount
class CapAmount2CapRegenGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
return {
'maxCapAmount': fit.ship.getModifiedItemAttr('capacitorCapacity'),
'capRegenTime': fit.ship.getModifiedItemAttr('rechargeRate') / 1000}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
capAmount = x
capRegen = calculateCapRegen(
maxCapAmount=commonData['maxCapAmount'],
capRegenTime=commonData['capRegenTime'],
currentCapAmount=capAmount)
return capRegen
def calculateCapAmount(maxCapAmount, capRegenTime, time):
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
return maxCapAmount * (1 + math.exp(5 * -time / capRegenTime) * -1) ** 2
def calculateCapRegen(maxCapAmount, capRegenTime, currentCapAmount):
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
return 10 * maxCapAmount / capRegenTime * (math.sqrt(currentCapAmount / maxCapAmount) - currentCapAmount / maxCapAmount)

View File

@@ -1,56 +0,0 @@
# =============================================================================
# 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 gui.builtinGraphs.base import FitGraph, XDef, YDef, Input
from .getter import Time2CapAmountGetter, Time2CapRegenGetter, CapAmount2CapAmountGetter, CapAmount2CapRegenGetter
class FitCapRegenGraph(FitGraph):
# UI stuff
internalName = 'capRegenGraph'
name = 'Capacitor Regeneration'
xDefs = [
XDef(handle='time', unit='s', label='Time', mainInput=('time', 's')),
XDef(handle='capAmount', unit='GJ', label='Cap amount', mainInput=('capAmount', '%')),
XDef(handle='capAmount', unit='%', label='Cap amount', mainInput=('capAmount', '%'))]
yDefs = [
YDef(handle='capAmount', unit='GJ', label='Cap amount'),
YDef(handle='capRegen', unit='GJ/s', label='Cap regen')]
inputs = [
Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=120, defaultRange=(0, 300), mainOnly=True),
Input(handle='capAmount', unit='%', label='Cap amount', iconID=1668, defaultValue=25, defaultRange=(0, 100), mainOnly=True)]
srcExtraCols = ('CapAmount', 'CapTime')
# Calculation stuff
_normalizers = {
('capAmount', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('capacitorCapacity')}
_limiters = {
'capAmount': lambda fit, tgt: (0, fit.ship.getModifiedItemAttr('capacitorCapacity'))}
_getters = {
('time', 'capAmount'): Time2CapAmountGetter,
('time', 'capRegen'): Time2CapRegenGetter,
('capAmount', 'capAmount'): CapAmount2CapAmountGetter,
('capAmount', 'capRegen'): CapAmount2CapRegenGetter}
_denormalizers = {
('capAmount', '%'): lambda v, fit, tgt: v * 100 / fit.ship.getModifiedItemAttr('capacitorCapacity')}
FitCapRegenGraph.register()

View File

@@ -1 +0,0 @@
import gui.builtinGraphs.fitDamageStats.graph # noqa: E402,F401

View File

@@ -1,22 +0,0 @@
# =============================================================================
# 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 .projected import ProjectedDataCache
from .time import TimeCache

View File

@@ -1,131 +0,0 @@
# =============================================================================
# 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
from gui.builtinGraphs.base import FitDataCache
from eos.const import FittingModuleState
from eos.modifiedAttributeDict import getResistanceAttrID
ModProjData = namedtuple('ModProjData', ('boost', 'optimal', 'falloff', 'stackingGroup', 'resAttrID'))
MobileProjData = namedtuple('MobileProjData', ('boost', 'optimal', 'falloff', 'stackingGroup', 'resAttrID', 'speed', 'radius'))
class ProjectedDataCache(FitDataCache):
def getProjModData(self, fit):
try:
projectedData = self._data[fit.ID]['modules']
except KeyError:
# Format of items for both: (boost strength, optimal, falloff, stacking group, resistance attr ID)
webMods = []
tpMods = []
projectedData = self._data.setdefault(fit.ID, {})['modules'] = (webMods, tpMods)
for mod in fit.modules:
if mod.state <= FittingModuleState.ONLINE:
continue
for webEffectName in ('remoteWebifierFalloff', 'structureModuleEffectStasisWebifier'):
if webEffectName in mod.item.effects:
webMods.append(ModProjData(
mod.getModifiedItemAttr('speedFactor'),
mod.maxRange or 0,
mod.falloff or 0,
'default',
getResistanceAttrID(modifyingItem=mod, effect=mod.item.effects[webEffectName])))
if 'doomsdayAOEWeb' in mod.item.effects:
webMods.append(ModProjData(
mod.getModifiedItemAttr('speedFactor'),
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - fit.ship.getModifiedItemAttr('radius')),
mod.falloff or 0,
'default',
getResistanceAttrID(modifyingItem=mod, effect=mod.item.effects['doomsdayAOEWeb'])))
for tpEffectName in ('remoteTargetPaintFalloff', 'structureModuleEffectTargetPainter'):
if tpEffectName in mod.item.effects:
tpMods.append(ModProjData(
mod.getModifiedItemAttr('signatureRadiusBonus'),
mod.maxRange or 0,
mod.falloff or 0,
'default',
getResistanceAttrID(modifyingItem=mod, effect=mod.item.effects[tpEffectName])))
if 'doomsdayAOEPaint' in mod.item.effects:
tpMods.append(ModProjData(
mod.getModifiedItemAttr('signatureRadiusBonus'),
max(0, (mod.maxRange or 0) + mod.getModifiedItemAttr('doomsdayAOERange') - fit.ship.getModifiedItemAttr('radius')),
mod.falloff or 0,
'default',
getResistanceAttrID(modifyingItem=mod, effect=mod.item.effects['doomsdayAOEPaint'])))
return projectedData
def getProjDroneData(self, fit):
try:
projectedData = self._data[fit.ID]['drones']
except KeyError:
# Format of items for both: (boost strength, optimal, falloff, stacking group, resistance attr ID, drone speed, drone radius)
webDrones = []
tpDrones = []
projectedData = self._data.setdefault(fit.ID, {})['drones'] = (webDrones, tpDrones)
for drone in fit.drones:
if drone.amountActive <= 0:
continue
if 'remoteWebifierEntity' in drone.item.effects:
webDrones.extend(drone.amountActive * (MobileProjData(
drone.getModifiedItemAttr('speedFactor'),
drone.maxRange or 0,
drone.falloff or 0,
'default',
getResistanceAttrID(modifyingItem=drone, effect=drone.item.effects['remoteWebifierEntity']),
drone.getModifiedItemAttr('maxVelocity'),
drone.getModifiedItemAttr('radius')),))
if 'remoteTargetPaintEntity' in drone.item.effects:
tpDrones.extend(drone.amountActive * (MobileProjData(
drone.getModifiedItemAttr('signatureRadiusBonus'),
drone.maxRange or 0,
drone.falloff or 0,
'default',
getResistanceAttrID(modifyingItem=drone, effect=drone.item.effects['remoteTargetPaintEntity']),
drone.getModifiedItemAttr('maxVelocity'),
drone.getModifiedItemAttr('radius')),))
return projectedData
def getProjFighterData(self, fit):
try:
projectedData = self._data[fit.ID]['fighters']
except KeyError:
# Format of items for both: (boost strength, optimal, falloff, stacking group, resistance attr ID, fighter speed, fighter radius)
webFighters = []
tpFighters = []
projectedData = self._data.setdefault(fit.ID, {})['fighters'] = (webFighters, tpFighters)
for fighter in fit.fighters:
if not fighter.active:
continue
for ability in fighter.abilities:
if not ability.active:
continue
if ability.effect.name == 'fighterAbilityStasisWebifier':
webFighters.append(MobileProjData(
fighter.getModifiedItemAttr('fighterAbilityStasisWebifierSpeedPenalty') * fighter.amountActive,
fighter.getModifiedItemAttr('fighterAbilityStasisWebifierOptimalRange'),
fighter.getModifiedItemAttr('fighterAbilityStasisWebifierFalloffRange'),
'default',
getResistanceAttrID(modifyingItem=fighter, effect=fighter.item.effects['fighterAbilityStasisWebifier']),
fighter.getModifiedItemAttr('maxVelocity'),
fighter.getModifiedItemAttr('radius')))
return projectedData

View File

@@ -1,254 +0,0 @@
# =============================================================================
# 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 copy import copy
from eos.utils.float import floatUnerr
from eos.utils.spoolSupport import SpoolType, SpoolOptions
from eos.utils.stats import DmgTypes
from gui.builtinGraphs.base import FitDataCache
class TimeCache(FitDataCache):
# Whole data getters
def getDpsData(self, fit):
"""Return DPS data in {time: {key: dps}} format."""
return self._data[fit.ID]['finalDps']
def getVolleyData(self, fit):
"""Return volley data in {time: {key: volley}} format."""
return self._data[fit.ID]['finalVolley']
def getDmgData(self, fit):
"""Return inflicted damage data in {time: {key: damage}} format."""
return self._data[fit.ID]['finalDmg']
# Specific data point getters
def getDpsDataPoint(self, fit, time):
"""Get DPS data by specified time in {key: dps} format."""
return self._getDataPoint(fit, time, self.getDpsData)
def getVolleyDataPoint(self, fit, time):
"""Get volley data by specified time in {key: volley} format."""
return self._getDataPoint(fit, time, self.getVolleyData)
def getDmgDataPoint(self, fit, time):
"""Get inflicted damage data by specified time in {key: dmg} format."""
return self._getDataPoint(fit, time, self.getDmgData)
# Preparation functions
def prepareDpsData(self, fit, maxTime):
self._prepareDpsVolleyData(fit, maxTime)
def prepareVolleyData(self, fit, maxTime):
self._prepareDpsVolleyData(fit, maxTime)
def prepareDmgData(self, fit, maxTime):
# Time is none means that time parameter has to be ignored,
# we do not need cache for that
if maxTime is None:
return
self._generateInternalForm(fit, maxTime)
fitCache = self._data[fit.ID]
# Final cache has been generated already, don't do anything
if 'finalDmg' in fitCache:
return
intCache = fitCache['internalDmg']
changesByTime = {}
for key, dmgMap in intCache.items():
for time in dmgMap:
changesByTime.setdefault(time, []).append(key)
# Here we convert cache to following format:
# {time: {key: damage done by key at this time}}
finalCache = fitCache['finalDmg'] = {}
timeDmgData = {}
for time in sorted(changesByTime):
timeDmgData = copy(timeDmgData)
for key in changesByTime[time]:
keyDmg = intCache[key][time]
if key in timeDmgData:
timeDmgData[key] = timeDmgData[key] + keyDmg
else:
timeDmgData[key] = keyDmg
finalCache[time] = timeDmgData
# We do not need internal cache once we have final
del fitCache['internalDmg']
# Private stuff
def _prepareDpsVolleyData(self, fit, maxTime):
# Time is none means that time parameter has to be ignored,
# we do not need cache for that
if maxTime is None:
return True
self._generateInternalForm(fit, maxTime)
fitCache = self._data[fit.ID]
# Final cache has been generated already, don't do anything
if 'finalDps' in fitCache and 'finalVolley' in fitCache:
return
# Convert cache from segments with assigned values into points
# which are located at times when dps/volley values change
pointCache = {}
for key, dmgList in fitCache['internalDpsVolley'].items():
pointData = pointCache[key] = {}
prevDps = None
prevVolley = None
prevTimeEnd = None
for timeStart, timeEnd, dps, volley in dmgList:
# First item
if not pointData:
pointData[timeStart] = (dps, volley)
# Gap between items
elif floatUnerr(prevTimeEnd) < floatUnerr(timeStart):
pointData[prevTimeEnd] = (DmgTypes(0, 0, 0, 0), DmgTypes(0, 0, 0, 0))
pointData[timeStart] = (dps, volley)
# Changed value
elif dps != prevDps or volley != prevVolley:
pointData[timeStart] = (dps, volley)
prevDps = dps
prevVolley = volley
prevTimeEnd = timeEnd
# We have data in another form, do not need old one any longer
del fitCache['internalDpsVolley']
changesByTime = {}
for key, dmgMap in pointCache.items():
for time in dmgMap:
changesByTime.setdefault(time, []).append(key)
# Here we convert cache to following format:
# {time: {key: (dps, volley}}
finalDpsCache = fitCache['finalDps'] = {}
finalVolleyCache = fitCache['finalVolley'] = {}
timeDpsData = {}
timeVolleyData = {}
for time in sorted(changesByTime):
timeDpsData = copy(timeDpsData)
timeVolleyData = copy(timeVolleyData)
for key in changesByTime[time]:
dps, volley = pointCache[key][time]
timeDpsData[key] = dps
timeVolleyData[key] = volley
finalDpsCache[time] = timeDpsData
finalVolleyCache[time] = timeVolleyData
def _generateInternalForm(self, fit, maxTime):
if self._isTimeCacheValid(fit, maxTime):
return
fitCache = self._data[fit.ID] = {'maxTime': maxTime}
intCacheDpsVolley = fitCache['internalDpsVolley'] = {}
intCacheDmg = fitCache['internalDmg'] = {}
def addDpsVolley(ddKey, addedTimeStart, addedTimeFinish, addedVolleys):
if not addedVolleys:
return
volleySum = sum(addedVolleys, DmgTypes(0, 0, 0, 0))
if volleySum.total > 0:
addedDps = volleySum / (addedTimeFinish - addedTimeStart)
# We can take "just best" volley, no matter target resistances, because all
# known items have the same damage type ratio throughout their cycle - and
# applying resistances doesn't change final outcome
bestVolley = max(addedVolleys, key=lambda v: v.total)
ddCacheDps = intCacheDpsVolley.setdefault(ddKey, [])
ddCacheDps.append((addedTimeStart, addedTimeFinish, addedDps, bestVolley))
def addDmg(ddKey, addedTime, addedDmg):
if addedDmg.total == 0:
return
intCacheDmg.setdefault(ddKey, {})[addedTime] = addedDmg
# Modules
for mod in fit.modules:
if not mod.isDealingDamage():
continue
cycleParams = mod.getCycleParameters(reloadOverride=True)
if cycleParams is None:
continue
currentTime = 0
nonstopCycles = 0
for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles():
cycleVolleys = []
volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True))
for volleyTimeMs, volley in volleyParams.items():
cycleVolleys.append(volley)
addDmg(mod, currentTime + volleyTimeMs / 1000, volley)
addDpsVolley(mod, currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys)
if inactiveTimeMs > 0:
nonstopCycles = 0
else:
nonstopCycles += 1
if currentTime > maxTime:
break
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
# Drones
for drone in fit.drones:
if not drone.isDealingDamage():
continue
cycleParams = drone.getCycleParameters(reloadOverride=True)
if cycleParams is None:
continue
currentTime = 0
volleyParams = drone.getVolleyParameters()
for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles():
cycleVolleys = []
for volleyTimeMs, volley in volleyParams.items():
cycleVolleys.append(volley)
addDmg(drone, currentTime + volleyTimeMs / 1000, volley)
addDpsVolley(drone, currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys)
if currentTime > maxTime:
break
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
# Fighters
for fighter in fit.fighters:
if not fighter.isDealingDamage():
continue
cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True)
if cycleParams is None:
continue
volleyParams = fighter.getVolleyParametersPerEffect()
for effectID, abilityCycleParams in cycleParams.items():
if effectID not in volleyParams:
continue
currentTime = 0
abilityVolleyParams = volleyParams[effectID]
for cycleTimeMs, inactiveTimeMs in abilityCycleParams.iterCycles():
cycleVolleys = []
for volleyTimeMs, volley in abilityVolleyParams.items():
cycleVolleys.append(volley)
addDmg((fighter, effectID), currentTime + volleyTimeMs / 1000, volley)
addDpsVolley((fighter, effectID), currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys)
if currentTime > maxTime:
break
currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000
def _isTimeCacheValid(self, fit, maxTime):
try:
cacheMaxTime = self._data[fit.ID]['maxTime']
except KeyError:
return False
return maxTime <= cacheMaxTime
def _getDataPoint(self, fit, time, dataFunc):
data = dataFunc(fit)
timesBefore = [t for t in data if floatUnerr(t) <= floatUnerr(time)]
try:
time = max(timesBefore)
except ValueError:
return {}
else:
return data[time]

View File

@@ -1,18 +0,0 @@
# =============================================================================
# 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/>.
# =============================================================================

View File

@@ -1,372 +0,0 @@
# =============================================================================
# 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
from functools import lru_cache
from eos.const import FittingHardpoint
from eos.saveddata.fit import Fit
from eos.utils.float import floatUnerr
from gui.builtinGraphs.fitDamageStats.helper import getTgtRadius
from service.const import GraphDpsDroneMode
from service.settings import GraphSettings
def getApplicationPerKey(fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius):
applicationMap = {}
for mod in fit.modules:
if not mod.isDealingDamage():
continue
if mod.hardpoint == FittingHardpoint.TURRET:
applicationMap[mod] = getTurretMult(
mod=mod,
fit=fit,
tgt=tgt,
atkSpeed=atkSpeed,
atkAngle=atkAngle,
distance=distance,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtSigRadius=tgtSigRadius)
elif mod.hardpoint == FittingHardpoint.MISSILE:
applicationMap[mod] = getLauncherMult(
mod=mod,
fit=fit,
distance=distance,
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
elif mod.item.group.name in ('Smart Bomb', 'Structure Area Denial Module'):
applicationMap[mod] = getSmartbombMult(
mod=mod,
distance=distance)
elif mod.item.group.name == 'Missile Launcher Bomb':
applicationMap[mod] = getBombMult(
mod=mod,
fit=fit,
tgt=tgt,
distance=distance,
tgtSigRadius=tgtSigRadius)
elif mod.item.group.name == 'Structure Guided Bomb Launcher':
applicationMap[mod] = getGuidedBombMult(
mod=mod,
fit=fit,
distance=distance,
tgtSigRadius=tgtSigRadius)
elif mod.item.group.name in ('Super Weapon', 'Structure Doomsday Weapon'):
applicationMap[mod] = getDoomsdayMult(
mod=mod,
tgt=tgt,
distance=distance,
tgtSigRadius=tgtSigRadius)
for drone in fit.drones:
if not drone.isDealingDamage():
continue
applicationMap[drone] = getDroneMult(
drone=drone,
fit=fit,
tgt=tgt,
atkSpeed=atkSpeed,
atkAngle=atkAngle,
distance=distance,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtSigRadius=tgtSigRadius)
for fighter in fit.fighters:
if not fighter.isDealingDamage():
continue
for ability in fighter.abilities:
if not ability.dealsDamage or not ability.active:
continue
applicationMap[(fighter, ability.effectID)] = getFighterAbilityMult(
fighter=fighter,
ability=ability,
fit=fit,
distance=distance,
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
# Ensure consistent results - round off a little to avoid float errors
for k, v in applicationMap.items():
applicationMap[k] = floatUnerr(v)
return applicationMap
# Item application multiplier calculation
def getTurretMult(mod, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius):
cth = _calcTurretChanceToHit(
atkSpeed=atkSpeed,
atkAngle=atkAngle,
atkRadius=fit.ship.getModifiedItemAttr('radius'),
atkOptimalRange=mod.maxRange,
atkFalloffRange=mod.falloff,
atkTracking=mod.getModifiedItemAttr('trackingSpeed'),
atkOptimalSigRadius=mod.getModifiedItemAttr('optimalSigRadius'),
distance=distance,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtRadius=getTgtRadius(tgt),
tgtSigRadius=tgtSigRadius)
mult = _calcTurretMult(cth)
return mult
def getLauncherMult(mod, fit, distance, tgtSpeed, tgtSigRadius):
modRange = mod.maxRange
if modRange is None:
return 0
if distance is not None and distance + fit.ship.getModifiedItemAttr('radius') > modRange:
return 0
mult = _calcMissileFactor(
atkEr=mod.getModifiedChargeAttr('aoeCloudSize'),
atkEv=mod.getModifiedChargeAttr('aoeVelocity'),
atkDrf=mod.getModifiedChargeAttr('aoeDamageReductionFactor'),
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
return mult
def getSmartbombMult(mod, distance):
modRange = mod.maxRange
if modRange is None:
return 0
if distance is not None and distance > modRange:
return 0
return 1
def getDoomsdayMult(mod, tgt, distance, tgtSigRadius):
modRange = mod.maxRange
# Single-target DDs have no range limit
if distance is not None and modRange and distance > modRange:
return 0
# Single-target titan DDs are vs capitals only
if {'superWeaponAmarr', 'superWeaponCaldari', 'superWeaponGallente', 'superWeaponMinmatar'}.intersection(mod.item.effects):
# Disallow only against subcap fits, allow against cap fits and tgt profiles
if isinstance(tgt, Fit) and not tgt.ship.item.requiresSkill('Capital Ships'):
return 0
damageSig = mod.getModifiedItemAttr('doomsdayDamageRadius') or mod.getModifiedItemAttr('signatureRadius')
if not damageSig:
return 1
return min(1, tgtSigRadius / damageSig)
def getBombMult(mod, fit, tgt, distance, tgtSigRadius):
modRange = mod.maxRange
if modRange is None:
return 0
blastRadius = mod.getModifiedChargeAttr('explosionRange')
atkRadius = fit.ship.getModifiedItemAttr('radius')
tgtRadius = getTgtRadius(tgt)
# Bomb starts in the center of the ship
# Also here we assume that it affects target as long as blast
# touches its surface, not center - I did not check this
if distance is not None and distance < max(0, modRange - atkRadius - tgtRadius - blastRadius):
return 0
if distance is not None and distance > max(0, modRange - atkRadius + tgtRadius + blastRadius):
return 0
return _calcBombFactor(
atkEr=mod.getModifiedChargeAttr('aoeCloudSize'),
tgtSigRadius=tgtSigRadius)
def getGuidedBombMult(mod, fit, distance, tgtSigRadius):
modRange = mod.maxRange
if modRange is None:
return 0
if distance is not None and distance > modRange - fit.ship.getModifiedItemAttr('radius'):
return 0
eR = mod.getModifiedChargeAttr('aoeCloudSize')
if eR == 0:
return 1
else:
return min(1, tgtSigRadius / eR)
def getDroneMult(drone, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius):
if distance is not None and distance > fit.extraAttributes['droneControlRange']:
return 0
droneSpeed = drone.getModifiedItemAttr('maxVelocity')
# Hard to simulate drone behavior, so assume chance to hit is 1 for mobile drones
# which catch up with target
droneOpt = GraphSettings.getInstance().get('mobileDroneMode')
if (
droneSpeed > 1 and (
(droneOpt == GraphDpsDroneMode.auto and droneSpeed >= tgtSpeed) or
droneOpt == GraphDpsDroneMode.followTarget)
):
cth = 1
# Otherwise put the drone into center of the ship, move it at its max speed or ship's speed
# (whichever is lower) towards direction of attacking ship and see how well it projects
else:
droneRadius = drone.getModifiedItemAttr('radius')
if distance is None:
cthDistance = None
else:
# As distance is ship surface to ship surface, we adjust it according
# to attacker fit's radiuses to have drone surface to ship surface distance
cthDistance = distance + fit.ship.getModifiedItemAttr('radius') - droneRadius
cth = _calcTurretChanceToHit(
atkSpeed=min(atkSpeed, droneSpeed),
atkAngle=atkAngle,
atkRadius=droneRadius,
atkOptimalRange=drone.maxRange,
atkFalloffRange=drone.falloff,
atkTracking=drone.getModifiedItemAttr('trackingSpeed'),
atkOptimalSigRadius=drone.getModifiedItemAttr('optimalSigRadius'),
distance=cthDistance,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtRadius=getTgtRadius(tgt),
tgtSigRadius=tgtSigRadius)
mult = _calcTurretMult(cth)
return mult
def getFighterAbilityMult(fighter, ability, fit, distance, tgtSpeed, tgtSigRadius):
fighterSpeed = fighter.getModifiedItemAttr('maxVelocity')
attrPrefix = ability.attrPrefix
# It's bomb attack
if attrPrefix == 'fighterAbilityLaunchBomb':
# Just assume we can land bomb anywhere
return _calcBombFactor(
atkEr=fighter.getModifiedChargeAttr('aoeCloudSize'),
tgtSigRadius=tgtSigRadius)
droneOpt = GraphSettings.getInstance().get('mobileDroneMode')
# It's regular missile-based attack
if (droneOpt == GraphDpsDroneMode.auto and fighterSpeed >= tgtSpeed) or droneOpt == GraphDpsDroneMode.followTarget:
rangeFactor = 1
# Same as with drones, if fighters are slower - put them to center of
# the ship and see how they apply
else:
if distance is None:
rangeFactorDistance = None
else:
rangeFactorDistance = distance + fit.ship.getModifiedItemAttr('radius') - fighter.getModifiedItemAttr('radius')
rangeFactor = _calcRangeFactor(
atkOptimalRange=fighter.getModifiedItemAttr('{}RangeOptimal'.format(attrPrefix)) or fighter.getModifiedItemAttr('{}Range'.format(attrPrefix)),
atkFalloffRange=fighter.getModifiedItemAttr('{}RangeFalloff'.format(attrPrefix)),
distance=rangeFactorDistance)
drf = fighter.getModifiedItemAttr('{}ReductionFactor'.format(attrPrefix), None)
if drf is None:
drf = fighter.getModifiedItemAttr('{}DamageReductionFactor'.format(attrPrefix))
drs = fighter.getModifiedItemAttr('{}ReductionSensitivity'.format(attrPrefix), None)
if drs is None:
drs = fighter.getModifiedItemAttr('{}DamageReductionSensitivity'.format(attrPrefix))
missileFactor = _calcMissileFactor(
atkEr=fighter.getModifiedItemAttr('{}ExplosionRadius'.format(attrPrefix)),
atkEv=fighter.getModifiedItemAttr('{}ExplosionVelocity'.format(attrPrefix)),
atkDrf=_calcAggregatedDrf(reductionFactor=drf, reductionSensitivity=drs),
tgtSpeed=tgtSpeed,
tgtSigRadius=tgtSigRadius)
mult = rangeFactor * missileFactor
return mult
# Turret-specific math
@lru_cache(maxsize=50)
def _calcTurretMult(chanceToHit):
"""Calculate damage multiplier for turret-based weapons."""
# https://wiki.eveuniversity.org/Turret_mechanics#Damage
wreckingChance = min(chanceToHit, 0.01)
wreckingPart = wreckingChance * 3
normalChance = chanceToHit - wreckingChance
if normalChance > 0:
avgDamageMult = (0.01 + chanceToHit) / 2 + 0.49
normalPart = normalChance * avgDamageMult
else:
normalPart = 0
totalMult = normalPart + wreckingPart
return totalMult
@lru_cache(maxsize=1000)
def _calcTurretChanceToHit(
atkSpeed, atkAngle, atkRadius, atkOptimalRange, atkFalloffRange, atkTracking, atkOptimalSigRadius,
distance, tgtSpeed, tgtAngle, tgtRadius, tgtSigRadius
):
"""Calculate chance to hit for turret-based weapons."""
# https://wiki.eveuniversity.org/Turret_mechanics#Hit_Math
angularSpeed = _calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius)
rangeFactor = _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance)
trackingFactor = _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius)
cth = rangeFactor * trackingFactor
return cth
def _calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius):
"""Calculate angular speed based on mobility parameters of two ships."""
if distance is None:
return 0
atkAngle = atkAngle * math.pi / 180
tgtAngle = tgtAngle * math.pi / 180
ctcDistance = atkRadius + distance + tgtRadius
# Target is to the right of the attacker, so transversal is projection onto Y axis
transSpeed = abs(atkSpeed * math.sin(atkAngle) - tgtSpeed * math.sin(tgtAngle))
if ctcDistance == 0:
return 0 if transSpeed == 0 else math.inf
else:
return transSpeed / ctcDistance
def _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius):
"""Calculate tracking chance to hit component."""
return 0.5 ** (((angularSpeed * atkOptimalSigRadius) / (atkTracking * tgtSigRadius)) ** 2)
# Missile-specific math
@lru_cache(maxsize=200)
def _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius):
"""Missile application."""
factors = [1]
# "Slow" part
if atkEr > 0:
factors.append(tgtSigRadius / atkEr)
# "Fast" part
if tgtSpeed > 0:
factors.append(((atkEv * tgtSigRadius) / (atkEr * tgtSpeed)) ** atkDrf)
totalMult = min(factors)
return totalMult
def _calcAggregatedDrf(reductionFactor, reductionSensitivity):
"""
Sometimes DRF is specified as 2 separate numbers,
here we combine them into generic form.
"""
return math.log(reductionFactor) / math.log(reductionSensitivity)
# Generic math
def _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance):
"""Range strength/chance factor, applicable to guns, ewar, RRs, etc."""
if distance is None:
return 1
if atkFalloffRange > 0:
return 0.5 ** ((max(0, distance - atkOptimalRange) / atkFalloffRange) ** 2)
elif distance <= atkOptimalRange:
return 1
else:
return 0
def _calcBombFactor(atkEr, tgtSigRadius):
if atkEr == 0:
return 1
else:
return min(1, tgtSigRadius / atkEr)

View File

@@ -1,138 +0,0 @@
# =============================================================================
# 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
from eos.saveddata.fit import Fit
from eos.utils.float import floatUnerr
from gui.builtinGraphs.fitDamageStats.helper import getTgtMaxVelocity, getTgtSigRadius
from service.const import GraphDpsDroneMode
from service.settings import GraphSettings
from .application import _calcRangeFactor
def getWebbedSpeed(fit, tgt, currentUnwebbedSpeed, webMods, webDrones, webFighters, distance):
# Can slow down non-immune fits and target profiles
if isinstance(tgt, Fit) and tgt.ship.getModifiedItemAttr('disallowOffensiveModifiers'):
return currentUnwebbedSpeed
maxUnwebbedSpeed = getTgtMaxVelocity(tgt)
try:
speedRatio = currentUnwebbedSpeed / maxUnwebbedSpeed
except ZeroDivisionError:
currentWebbedSpeed = 0
else:
appliedMultipliers = {}
# Modules first, they are applied always the same way
for wData in webMods:
appliedBoost = wData.boost * _calcRangeFactor(
atkOptimalRange=wData.optimal,
atkFalloffRange=wData.falloff,
distance=distance)
if appliedBoost:
appliedMultipliers.setdefault(wData.stackingGroup, []).append((1 + appliedBoost / 100, wData.resAttrID))
maxWebbedSpeed = getTgtMaxVelocity(tgt, extraMultipliers=appliedMultipliers)
currentWebbedSpeed = maxWebbedSpeed * speedRatio
# Drones and fighters
mobileWebs = []
mobileWebs.extend(webFighters)
# Drones have range limit
if distance is None or distance <= fit.extraAttributes['droneControlRange']:
mobileWebs.extend(webDrones)
atkRadius = fit.ship.getModifiedItemAttr('radius')
# As mobile webs either follow the target or stick to the attacking ship,
# if target is within mobile web optimal - it can be applied unconditionally
longEnoughMws = [mw for mw in mobileWebs if distance is None or distance <= mw.optimal - atkRadius + mw.radius]
if longEnoughMws:
for mwData in longEnoughMws:
appliedMultipliers.setdefault(mwData.stackingGroup, []).append((1 + mwData.boost / 100, mwData.resAttrID))
mobileWebs.remove(mwData)
maxWebbedSpeed = getTgtMaxVelocity(tgt, extraMultipliers=appliedMultipliers)
currentWebbedSpeed = maxWebbedSpeed * speedRatio
# Apply remaining webs, from fastest to slowest
droneOpt = GraphSettings.getInstance().get('mobileDroneMode')
while mobileWebs:
# Process in batches unified by speed to save up resources
fastestMwSpeed = max(mobileWebs, key=lambda mw: mw.speed).speed
fastestMws = [mw for mw in mobileWebs if mw.speed == fastestMwSpeed]
for mwData in fastestMws:
# Faster than target or set to follow it - apply full slowdown
if (droneOpt == GraphDpsDroneMode.auto and mwData.speed >= currentWebbedSpeed) or droneOpt == GraphDpsDroneMode.followTarget:
appliedMwBoost = mwData.boost
# Otherwise project from the center of the ship
else:
if distance is None:
rangeFactorDistance = None
else:
rangeFactorDistance = distance + atkRadius - mwData.radius
appliedMwBoost = mwData.boost * _calcRangeFactor(
atkOptimalRange=mwData.optimal,
atkFalloffRange=mwData.falloff,
distance=rangeFactorDistance)
appliedMultipliers.setdefault(mwData.stackingGroup, []).append((1 + appliedMwBoost / 100, mwData.resAttrID))
mobileWebs.remove(mwData)
maxWebbedSpeed = getTgtMaxVelocity(tgt, extraMultipliers=appliedMultipliers)
currentWebbedSpeed = maxWebbedSpeed * speedRatio
# Ensure consistent results - round off a little to avoid float errors
return floatUnerr(currentWebbedSpeed)
def getTpMult(fit, tgt, tgtSpeed, tpMods, tpDrones, tpFighters, distance):
# Can blow non-immune fits and target profiles
if isinstance(tgt, Fit) and tgt.ship.getModifiedItemAttr('disallowOffensiveModifiers'):
return 1
untpedSig = getTgtSigRadius(tgt)
# Modules
appliedMultipliers = {}
for tpData in tpMods:
appliedBoost = tpData.boost * _calcRangeFactor(
atkOptimalRange=tpData.optimal,
atkFalloffRange=tpData.falloff,
distance=distance)
if appliedBoost:
appliedMultipliers.setdefault(tpData.stackingGroup, []).append((1 + appliedBoost / 100, tpData.resAttrID))
# Drones and fighters
mobileTps = []
mobileTps.extend(tpFighters)
# Drones have range limit
if distance is None or distance <= fit.extraAttributes['droneControlRange']:
mobileTps.extend(tpDrones)
droneOpt = GraphSettings.getInstance().get('mobileDroneMode')
atkRadius = fit.ship.getModifiedItemAttr('radius')
for mtpData in mobileTps:
# Faster than target or set to follow it - apply full TP
if (droneOpt == GraphDpsDroneMode.auto and mtpData.speed >= tgtSpeed) or droneOpt == GraphDpsDroneMode.followTarget:
appliedMtpBoost = mtpData.boost
# Otherwise project from the center of the ship
else:
if distance is None:
rangeFactorDistance = None
else:
rangeFactorDistance = distance + atkRadius - mtpData.radius
appliedMtpBoost = mtpData.boost * _calcRangeFactor(
atkOptimalRange=mtpData.optimal,
atkFalloffRange=mtpData.falloff,
distance=rangeFactorDistance)
appliedMultipliers.setdefault(mtpData.stackingGroup, []).append((1 + appliedMtpBoost / 100, mtpData.resAttrID))
tpedSig = getTgtSigRadius(tgt, extraMultipliers=appliedMultipliers)
if tpedSig == math.inf and untpedSig == math.inf:
return 1
mult = tpedSig / untpedSig
# Ensure consistent results - round off a little to avoid float errors
return floatUnerr(mult)

View File

@@ -1,428 +0,0 @@
# =============================================================================
# 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 eos.config
from eos.utils.spoolSupport import SpoolType, SpoolOptions
from eos.utils.stats import DmgTypes
from gui.builtinGraphs.base import PointGetter, SmoothPointGetter
from service.settings import GraphSettings
from .calc.application import getApplicationPerKey
from .calc.projected import getWebbedSpeed, getTpMult
from .helper import getTgtSigRadius
def applyDamage(dmgMap, applicationMap):
total = DmgTypes(0, 0, 0, 0)
for key, dmg in dmgMap.items():
total += dmg * applicationMap.get(key, 0)
return total
# Y mixins
class YDpsMixin:
def _getDamagePerKey(self, fit, time):
# Use data from time cache if time was not specified
if time is not None:
return self._getTimeCacheDataPoint(fit=fit, time=time)
# Compose map ourselves using current fit settings if time is not specified
dpsMap = {}
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
for mod in fit.modules:
if not mod.isDealingDamage():
continue
dpsMap[mod] = mod.getDps(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False))
for drone in fit.drones:
if not drone.isDealingDamage():
continue
dpsMap[drone] = drone.getDps()
for fighter in fit.fighters:
if not fighter.isDealingDamage():
continue
for effectID, effectDps in fighter.getDpsPerEffect().items():
dpsMap[(fighter, effectID)] = effectDps
return dpsMap
def _prepareTimeCache(self, fit, maxTime):
self.graph._timeCache.prepareDpsData(fit=fit, maxTime=maxTime)
def _getTimeCacheData(self, fit):
return self.graph._timeCache.getDpsData(fit=fit)
def _getTimeCacheDataPoint(self, fit, time):
return self.graph._timeCache.getDpsDataPoint(fit=fit, time=time)
class YVolleyMixin:
def _getDamagePerKey(self, fit, time):
# Use data from time cache if time was not specified
if time is not None:
return self._getTimeCacheDataPoint(fit=fit, time=time)
# Compose map ourselves using current fit settings if time is not specified
volleyMap = {}
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
for mod in fit.modules:
if not mod.isDealingDamage():
continue
volleyMap[mod] = mod.getVolley(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False))
for drone in fit.drones:
if not drone.isDealingDamage():
continue
volleyMap[drone] = drone.getVolley()
for fighter in fit.fighters:
if not fighter.isDealingDamage():
continue
for effectID, effectVolley in fighter.getVolleyPerEffect().items():
volleyMap[(fighter, effectID)] = effectVolley
return volleyMap
def _prepareTimeCache(self, fit, maxTime):
self.graph._timeCache.prepareVolleyData(fit=fit, maxTime=maxTime)
def _getTimeCacheData(self, fit):
return self.graph._timeCache.getVolleyData(fit=fit)
def _getTimeCacheDataPoint(self, fit, time):
return self.graph._timeCache.getVolleyDataPoint(fit=fit, time=time)
class YInflictedDamageMixin:
def _getDamagePerKey(self, fit, time):
# Damage inflicted makes no sense without time specified
if time is None:
raise ValueError
return self._getTimeCacheDataPoint(fit=fit, time=time)
def _prepareTimeCache(self, fit, maxTime):
self.graph._timeCache.prepareDmgData(fit=fit, maxTime=maxTime)
def _getTimeCacheData(self, fit):
return self.graph._timeCache.getDmgData(fit=fit)
def _getTimeCacheDataPoint(self, fit, time):
return self.graph._timeCache.getDmgDataPoint(fit=fit, time=time)
# X mixins
class XDistanceMixin(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
# Process params into more convenient form
miscParamMap = dict(miscParams)
# Prepare time cache here because we need to do it only once,
# and this function is called once per point info fetch
self._prepareTimeCache(fit=fit, maxTime=miscParamMap['time'])
return {
'applyProjected': GraphSettings.getInstance().get('applyProjected'),
'miscParamMap': miscParamMap,
'dmgMap': self._getDamagePerKey(fit=fit, time=miscParamMap['time'])}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
distance = x
miscParamMap = commonData['miscParamMap']
tgtSpeed = miscParamMap['tgtSpeed']
tgtSigRadius = getTgtSigRadius(tgt)
if commonData['applyProjected']:
webMods, tpMods = self.graph._projectedCache.getProjModData(fit)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(fit)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(fit)
tgtSpeed = getWebbedSpeed(
fit=fit,
tgt=tgt,
currentUnwebbedSpeed=tgtSpeed,
webMods=webMods,
webDrones=webDrones,
webFighters=webFighters,
distance=distance)
tgtSigRadius = tgtSigRadius * getTpMult(
fit=fit,
tgt=tgt,
tgtSpeed=tgtSpeed,
tpMods=tpMods,
tpDrones=tpDrones,
tpFighters=tpFighters,
distance=distance)
applicationMap = getApplicationPerKey(
fit=fit,
tgt=tgt,
atkSpeed=miscParamMap['atkSpeed'],
atkAngle=miscParamMap['atkAngle'],
distance=distance,
tgtSpeed=tgtSpeed,
tgtAngle=miscParamMap['tgtAngle'],
tgtSigRadius=tgtSigRadius)
y = applyDamage(dmgMap=commonData['dmgMap'], applicationMap=applicationMap).total
return y
class XTimeMixin(PointGetter):
def _prepareApplicationMap(self, miscParams, fit, tgt):
# Process params into more convenient form
miscParamMap = dict(miscParams)
tgtSpeed = miscParamMap['tgtSpeed']
tgtSigRadius = getTgtSigRadius(tgt)
if GraphSettings.getInstance().get('applyProjected'):
webMods, tpMods = self.graph._projectedCache.getProjModData(fit)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(fit)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(fit)
tgtSpeed = getWebbedSpeed(
fit=fit,
tgt=tgt,
currentUnwebbedSpeed=tgtSpeed,
webMods=webMods,
webDrones=webDrones,
webFighters=webFighters,
distance=miscParamMap['distance'])
tgtSigRadius = tgtSigRadius * getTpMult(
fit=fit,
tgt=tgt,
tgtSpeed=tgtSpeed,
tpMods=tpMods,
tpDrones=tpDrones,
tpFighters=tpFighters,
distance=miscParamMap['distance'])
# Get all data we need for all times into maps/caches
applicationMap = getApplicationPerKey(
fit=fit,
tgt=tgt,
atkSpeed=miscParamMap['atkSpeed'],
atkAngle=miscParamMap['atkAngle'],
distance=miscParamMap['distance'],
tgtSpeed=tgtSpeed,
tgtAngle=miscParamMap['tgtAngle'],
tgtSigRadius=tgtSigRadius)
return applicationMap
def getRange(self, xRange, miscParams, fit, tgt):
xs = []
ys = []
minTime, maxTime = xRange
# Prepare time cache and various shared data
self._prepareTimeCache(fit=fit, maxTime=maxTime)
timeCache = self._getTimeCacheData(fit=fit)
applicationMap = self._prepareApplicationMap(miscParams=miscParams, fit=fit, tgt=tgt)
# Custom iteration for time graph to show all data points
currentDmg = None
currentTime = None
for currentTime in sorted(timeCache):
prevDmg = currentDmg
currentDmgData = timeCache[currentTime]
currentDmg = applyDamage(dmgMap=currentDmgData, applicationMap=applicationMap).total
if currentTime < minTime:
continue
# First set of data points
if not xs:
# Start at exactly requested time, at last known value
initialDmg = prevDmg or 0
xs.append(minTime)
ys.append(initialDmg)
# If current time is bigger then starting, extend plot to that time with old value
if currentTime > minTime:
xs.append(currentTime)
ys.append(initialDmg)
# If new value is different, extend it with new point to the new value
if currentDmg != prevDmg:
xs.append(currentTime)
ys.append(currentDmg)
continue
# Last data point
if currentTime >= maxTime:
xs.append(maxTime)
ys.append(prevDmg)
break
# Anything in-between
if currentDmg != prevDmg:
if prevDmg is not None:
xs.append(currentTime)
ys.append(prevDmg)
xs.append(currentTime)
ys.append(currentDmg)
# Special case - there are no damage dealers
if currentDmg is None and currentTime is None:
xs.append(minTime)
ys.append(0)
# Make sure that last data point is always at max time
if maxTime > (currentTime or 0):
xs.append(maxTime)
ys.append(currentDmg or 0)
return xs, ys
def getPoint(self, x, miscParams, fit, tgt):
time = x
# Prepare time cache and various data
self._prepareTimeCache(fit=fit, maxTime=time)
dmgData = self._getTimeCacheDataPoint(fit=fit, time=time)
applicationMap = self._prepareApplicationMap(miscParams=miscParams, fit=fit, tgt=tgt)
y = applyDamage(dmgMap=dmgData, applicationMap=applicationMap).total
return y
class XTgtSpeedMixin(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
# Process params into more convenient form
miscParamMap = dict(miscParams)
# Prepare time cache here because we need to do it only once,
# and this function is called once per point info fetch
self._prepareTimeCache(fit=fit, maxTime=miscParamMap['time'])
return {
'applyProjected': GraphSettings.getInstance().get('applyProjected'),
'miscParamMap': miscParamMap,
'dmgMap': self._getDamagePerKey(fit=fit, time=miscParamMap['time'])}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
tgtSpeed = x
miscParamMap = commonData['miscParamMap']
tgtSigRadius = getTgtSigRadius(tgt)
if commonData['applyProjected']:
webMods, tpMods = self.graph._projectedCache.getProjModData(fit)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(fit)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(fit)
tgtSpeed = getWebbedSpeed(
fit=fit,
tgt=tgt,
currentUnwebbedSpeed=tgtSpeed,
webMods=webMods,
webDrones=webDrones,
webFighters=webFighters,
distance=miscParamMap['distance'])
tgtSigRadius = tgtSigRadius * getTpMult(
fit=fit,
tgt=tgt,
tgtSpeed=tgtSpeed,
tpMods=tpMods,
tpDrones=tpDrones,
tpFighters=tpFighters,
distance=miscParamMap['distance'])
applicationMap = getApplicationPerKey(
fit=fit,
tgt=tgt,
atkSpeed=miscParamMap['atkSpeed'],
atkAngle=miscParamMap['atkAngle'],
distance=miscParamMap['distance'],
tgtSpeed=tgtSpeed,
tgtAngle=miscParamMap['tgtAngle'],
tgtSigRadius=tgtSigRadius)
y = applyDamage(dmgMap=commonData['dmgMap'], applicationMap=applicationMap).total
return y
class XTgtSigRadiusMixin(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
# Process params into more convenient form
miscParamMap = dict(miscParams)
tgtSpeed = miscParamMap['tgtSpeed']
tgtSigMult = 1
if GraphSettings.getInstance().get('applyProjected'):
webMods, tpMods = self.graph._projectedCache.getProjModData(fit)
webDrones, tpDrones = self.graph._projectedCache.getProjDroneData(fit)
webFighters, tpFighters = self.graph._projectedCache.getProjFighterData(fit)
tgtSpeed = getWebbedSpeed(
fit=fit,
tgt=tgt,
currentUnwebbedSpeed=tgtSpeed,
webMods=webMods,
webDrones=webDrones,
webFighters=webFighters,
distance=miscParamMap['distance'])
tgtSigMult = getTpMult(
fit=fit,
tgt=tgt,
tgtSpeed=tgtSpeed,
tpMods=tpMods,
tpDrones=tpDrones,
tpFighters=tpFighters,
distance=miscParamMap['distance'])
# Prepare time cache here because we need to do it only once,
# and this function is called once per point info fetch
self._prepareTimeCache(fit=fit, maxTime=miscParamMap['time'])
return {
'miscParamMap': miscParamMap,
'tgtSpeed': tgtSpeed,
'tgtSigMult': tgtSigMult,
'dmgMap': self._getDamagePerKey(fit=fit, time=miscParamMap['time'])}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
tgtSigRadius = x
miscParamMap = commonData['miscParamMap']
applicationMap = getApplicationPerKey(
fit=fit,
tgt=tgt,
atkSpeed=miscParamMap['atkSpeed'],
atkAngle=miscParamMap['atkAngle'],
distance=miscParamMap['distance'],
tgtSpeed=commonData['tgtSpeed'],
tgtAngle=miscParamMap['tgtAngle'],
tgtSigRadius=tgtSigRadius * commonData['tgtSigMult'])
y = applyDamage(dmgMap=commonData['dmgMap'], applicationMap=applicationMap).total
return y
# Final getters
class Distance2DpsGetter(XDistanceMixin, YDpsMixin):
pass
class Distance2VolleyGetter(XDistanceMixin, YVolleyMixin):
pass
class Distance2InflictedDamageGetter(XDistanceMixin, YInflictedDamageMixin):
pass
class Time2DpsGetter(XTimeMixin, YDpsMixin):
pass
class Time2VolleyGetter(XTimeMixin, YVolleyMixin):
pass
class Time2InflictedDamageGetter(XTimeMixin, YInflictedDamageMixin):
pass
class TgtSpeed2DpsGetter(XTgtSpeedMixin, YDpsMixin):
pass
class TgtSpeed2VolleyGetter(XTgtSpeedMixin, YVolleyMixin):
pass
class TgtSpeed2InflictedDamageGetter(XTgtSpeedMixin, YInflictedDamageMixin):
pass
class TgtSigRadius2DpsGetter(XTgtSigRadiusMixin, YDpsMixin):
pass
class TgtSigRadius2VolleyGetter(XTgtSigRadiusMixin, YVolleyMixin):
pass
class TgtSigRadius2InflictedDamageGetter(XTgtSigRadiusMixin, YInflictedDamageMixin):
pass

View File

@@ -1,104 +0,0 @@
# =============================================================================
# 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 gui.builtinGraphs.base import FitGraph, XDef, YDef, Input, VectorDef
from service.const import GraphCacheCleanupReason
from .getter import (
Distance2DpsGetter, Distance2VolleyGetter, Distance2InflictedDamageGetter,
Time2DpsGetter, Time2VolleyGetter, Time2InflictedDamageGetter,
TgtSpeed2DpsGetter, TgtSpeed2VolleyGetter, TgtSpeed2InflictedDamageGetter,
TgtSigRadius2DpsGetter, TgtSigRadius2VolleyGetter, TgtSigRadius2InflictedDamageGetter)
from .helper import getTgtMaxVelocity, getTgtSigRadius
from .cache import ProjectedDataCache, TimeCache
class FitDamageStatsGraph(FitGraph):
def __init__(self):
super().__init__()
self._timeCache = TimeCache()
self._projectedCache = ProjectedDataCache()
def _clearInternalCache(self, reason, extraData):
# Here, we care only about fit changes and graph changes.
# - Input changes are irrelevant as time cache cares only about
# time input, and it regenerates once time goes beyond cached value
# - Option changes are irrelevant as cache contains "raw" damage
# values which do not rely on any graph options
if reason in (GraphCacheCleanupReason.fitChanged, GraphCacheCleanupReason.fitRemoved):
self._timeCache.clearForFit(extraData)
self._projectedCache.clearForFit(extraData)
elif reason == GraphCacheCleanupReason.graphSwitched:
self._timeCache.clearAll()
self._projectedCache.clearAll()
# UI stuff
internalName = 'dmgStatsGraph'
name = 'Damage Stats'
xDefs = [
XDef(handle='distance', unit='km', label='Distance', mainInput=('distance', 'km')),
XDef(handle='time', unit='s', label='Time', mainInput=('time', 's')),
XDef(handle='tgtSpeed', unit='m/s', label='Target speed', mainInput=('tgtSpeed', '%')),
XDef(handle='tgtSpeed', unit='%', label='Target speed', mainInput=('tgtSpeed', '%')),
XDef(handle='tgtSigRad', unit='m', label='Target signature radius', mainInput=('tgtSigRad', '%')),
XDef(handle='tgtSigRad', unit='%', label='Target signature radius', mainInput=('tgtSigRad', '%'))]
yDefs = [
YDef(handle='dps', unit=None, label='DPS'),
YDef(handle='volley', unit=None, label='Volley'),
YDef(handle='damage', unit=None, label='Damage inflicted')]
inputs = [
Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=None, defaultRange=(0, 80), secondaryTooltip='When set, uses exact attacker\'s damage stats of at a given time\nWhen not set, uses attacker\'s damage stats as shown in stats panel of main window'),
Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=None, defaultRange=(0, 100), mainTooltip='Distance between the attacker and the target, as seen in overview (surface-to-surface)', secondaryTooltip='Distance between the attacker and the target, as seen in overview (surface-to-surface)\nWhen set, places the target that far away from the attacker\nWhen not set, attacker\'s weapons always hit the target'),
Input(handle='tgtSpeed', unit='%', label='Target speed', iconID=1389, defaultValue=100, defaultRange=(0, 100)),
Input(handle='tgtSigRad', unit='%', label='Target signature', iconID=1390, defaultValue=100, defaultRange=(100, 200), mainOnly=True)]
srcVectorDef = VectorDef(lengthHandle='atkSpeed', lengthUnit='%', angleHandle='atkAngle', angleUnit='degrees', label='Attacker')
tgtVectorDef = VectorDef(lengthHandle='tgtSpeed', lengthUnit='%', angleHandle='tgtAngle', angleUnit='degrees', label='Target')
hasTargets = True
srcExtraCols = ('Dps', 'Volley', 'Speed', 'Radius')
tgtExtraCols = ('Speed', 'SigRadius', 'Radius')
# Calculation stuff
_normalizers = {
('distance', 'km'): lambda v, fit, tgt: None if v is None else v * 1000,
('atkSpeed', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('maxVelocity'),
('tgtSpeed', '%'): lambda v, fit, tgt: v / 100 * getTgtMaxVelocity(tgt),
('tgtSigRad', '%'): lambda v, fit, tgt: v / 100 * getTgtSigRadius(tgt)}
_limiters = {
'time': lambda fit, tgt: (0, 2500)}
_getters = {
('distance', 'dps'): Distance2DpsGetter,
('distance', 'volley'): Distance2VolleyGetter,
('distance', 'damage'): Distance2InflictedDamageGetter,
('time', 'dps'): Time2DpsGetter,
('time', 'volley'): Time2VolleyGetter,
('time', 'damage'): Time2InflictedDamageGetter,
('tgtSpeed', 'dps'): TgtSpeed2DpsGetter,
('tgtSpeed', 'volley'): TgtSpeed2VolleyGetter,
('tgtSpeed', 'damage'): TgtSpeed2InflictedDamageGetter,
('tgtSigRad', 'dps'): TgtSigRadius2DpsGetter,
('tgtSigRad', 'volley'): TgtSigRadius2VolleyGetter,
('tgtSigRad', 'damage'): TgtSigRadius2InflictedDamageGetter}
_denormalizers = {
('distance', 'km'): lambda v, fit, tgt: None if v is None else v / 1000,
('tgtSpeed', '%'): lambda v, fit, tgt: v * 100 / getTgtMaxVelocity(tgt),
('tgtSigRad', '%'): lambda v, fit, tgt: v * 100 / getTgtSigRadius(tgt)}
FitDamageStatsGraph.register()

View File

@@ -1,89 +0,0 @@
# =============================================================================
# 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
from eos.saveddata.fit import Fit
from eos.saveddata.targetProfile import TargetProfile
def getTgtMaxVelocity(tgt, extraMultipliers=None):
if isinstance(tgt, Fit):
if extraMultipliers:
maxVelocity = tgt.ship.getModifiedItemAttrWithExtraMods('maxVelocity', extraMultipliers=extraMultipliers)
else:
maxVelocity = tgt.ship.getModifiedItemAttr('maxVelocity')
elif isinstance(tgt, TargetProfile):
maxVelocity = tgt.maxVelocity
if extraMultipliers:
maxVelocity *= _calculateMultiplier(extraMultipliers)
else:
maxVelocity = None
return maxVelocity
def getTgtSigRadius(tgt, extraMultipliers=None):
if isinstance(tgt, Fit):
if extraMultipliers:
sigRadius = tgt.ship.getModifiedItemAttrWithExtraMods('signatureRadius', extraMultipliers=extraMultipliers)
else:
sigRadius = tgt.ship.getModifiedItemAttr('signatureRadius')
elif isinstance(tgt, TargetProfile):
sigRadius = tgt.signatureRadius
if extraMultipliers:
sigRadius *= _calculateMultiplier(extraMultipliers)
else:
sigRadius = None
return sigRadius
def getTgtRadius(tgt):
if isinstance(tgt, Fit):
radius = tgt.ship.getModifiedItemAttr('radius')
elif isinstance(tgt, TargetProfile):
radius = tgt.radius
else:
radius = None
return radius
# Just copy-paste penalization chain calculation code (with some modifications,
# as multipliers arrive in different form) in here to not make actual attribute
# calculations slower than they already are due to extra function calls
def _calculateMultiplier(multipliers):
val = 1
for penalizedMultipliers in multipliers.values():
# A quick explanation of how this works:
# 1: Bonuses and penalties are calculated seperately, so we'll have to filter each of them
l1 = [v[0] for v in penalizedMultipliers if v[0] > 1]
l2 = [v[0] for v in penalizedMultipliers if v[0] < 1]
# 2: The most significant bonuses take the smallest penalty,
# This means we'll have to sort
abssort = lambda _val: -abs(_val - 1)
l1.sort(key=abssort)
l2.sort(key=abssort)
# 3: The first module doesn't get penalized at all
# Any module after the first takes penalties according to:
# 1 + (multiplier - 1) * math.exp(- math.pow(i, 2) / 7.1289)
for l in (l1, l2):
for i in range(len(l)):
bonus = l[i]
val *= 1 + (bonus - 1) * math.exp(- i ** 2 / 7.1289)
return val

View File

@@ -1 +0,0 @@
import gui.builtinGraphs.fitMobility.graph # noqa: E402,F401

View File

@@ -1,62 +0,0 @@
# =============================================================================
# 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
from gui.builtinGraphs.base import SmoothPointGetter
class Time2SpeedGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
return {
'maxSpeed': fit.ship.getModifiedItemAttr('maxVelocity'),
'mass': fit.ship.getModifiedItemAttr('mass'),
'agility': fit.ship.getModifiedItemAttr('agility')}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
time = x
maxSpeed = commonData['maxSpeed']
mass = commonData['mass']
agility = commonData['agility']
# https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae
speed = maxSpeed * (1 - math.exp((-time * 1000000) / (agility * mass)))
return speed
class Time2DistanceGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
return {
'maxSpeed': fit.ship.getModifiedItemAttr('maxVelocity'),
'mass': fit.ship.getModifiedItemAttr('mass'),
'agility': fit.ship.getModifiedItemAttr('agility')}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
time = x
maxSpeed = commonData['maxSpeed']
mass = commonData['mass']
agility = commonData['agility']
# Definite integral of:
# https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae
distance_t = maxSpeed * time + (maxSpeed * agility * mass * math.exp((-time * 1000000) / (agility * mass)) / 1000000)
distance_0 = maxSpeed * 0 + (maxSpeed * agility * mass * math.exp((-0 * 1000000) / (agility * mass)) / 1000000)
distance = distance_t - distance_0
return distance

View File

@@ -1,47 +0,0 @@
# =============================================================================
# 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 gui.builtinGraphs.base import FitGraph, XDef, YDef, Input
from .getter import Time2SpeedGetter, Time2DistanceGetter
class FitMobilityVsTimeGraph(FitGraph):
# UI stuff
internalName = 'mobilityGraph'
name = 'Mobility'
xDefs = [
XDef(handle='time', unit='s', label='Time', mainInput=('time', 's'))]
yDefs = [
YDef(handle='speed', unit='m/s', label='Speed'),
YDef(handle='distance', unit='km', label='Distance')]
inputs = [
Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=10, defaultRange=(0, 30))]
srcExtraCols = ('Speed', 'Agility')
# Calculation stuff
_getters = {
('time', 'speed'): Time2SpeedGetter,
('time', 'distance'): Time2DistanceGetter}
_denormalizers = {
('distance', 'km'): lambda v, fit, tgt: v / 1000}
FitMobilityVsTimeGraph.register()

View File

@@ -1 +0,0 @@
import gui.builtinGraphs.fitShieldRegen.graph # noqa: E402,F401

View File

@@ -1,95 +0,0 @@
# =============================================================================
# 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
from gui.builtinGraphs.base import SmoothPointGetter
class Time2ShieldAmountGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
return {
'maxShieldAmount': fit.ship.getModifiedItemAttr('shieldCapacity'),
'shieldRegenTime': fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
time = x
shieldAmount = calculateShieldAmount(
maxShieldAmount=commonData['maxShieldAmount'],
shieldRegenTime=commonData['shieldRegenTime'],
time=time)
return shieldAmount
class Time2ShieldRegenGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
return {
'maxShieldAmount': fit.ship.getModifiedItemAttr('shieldCapacity'),
'shieldRegenTime': fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
time = x
shieldAmount = calculateShieldAmount(
maxShieldAmount=commonData['maxShieldAmount'],
shieldRegenTime=commonData['shieldRegenTime'],
time=time)
shieldRegen = calculateShieldRegen(
maxShieldAmount=commonData['maxShieldAmount'],
shieldRegenTime=commonData['shieldRegenTime'],
currentShieldAmount=shieldAmount)
return shieldRegen
# Useless, but valid combination of x and y
class ShieldAmount2ShieldAmountGetter(SmoothPointGetter):
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
shieldAmount = x
return shieldAmount
class ShieldAmount2ShieldRegenGetter(SmoothPointGetter):
def _getCommonData(self, miscParams, fit, tgt):
return {
'maxShieldAmount': fit.ship.getModifiedItemAttr('shieldCapacity'),
'shieldRegenTime': fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
shieldAmount = x
shieldRegen = calculateShieldRegen(
maxShieldAmount=commonData['maxShieldAmount'],
shieldRegenTime=commonData['shieldRegenTime'],
currentShieldAmount=shieldAmount)
return shieldRegen
def calculateShieldAmount(maxShieldAmount, shieldRegenTime, time):
# The same formula as for cap
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
return maxShieldAmount * (1 + math.exp(5 * -time / shieldRegenTime) * -1) ** 2
def calculateShieldRegen(maxShieldAmount, shieldRegenTime, currentShieldAmount):
# The same formula as for cap
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
return 10 * maxShieldAmount / shieldRegenTime * (math.sqrt(currentShieldAmount / maxShieldAmount) - currentShieldAmount / maxShieldAmount)

View File

@@ -1,63 +0,0 @@
# =============================================================================
# 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 gui.builtinGraphs.base import FitGraph, XDef, YDef, Input
from .getter import (
Time2ShieldAmountGetter, Time2ShieldRegenGetter,
ShieldAmount2ShieldAmountGetter, ShieldAmount2ShieldRegenGetter)
class FitShieldRegenGraph(FitGraph):
# UI stuff
internalName = 'shieldRegenGraph'
name = 'Shield Regeneration'
xDefs = [
XDef(handle='time', unit='s', label='Time', mainInput=('time', 's')),
XDef(handle='shieldAmount', unit='EHP', label='Shield amount', mainInput=('shieldAmount', '%')),
XDef(handle='shieldAmount', unit='HP', label='Shield amount', mainInput=('shieldAmount', '%')),
XDef(handle='shieldAmount', unit='%', label='Shield amount', mainInput=('shieldAmount', '%'))]
yDefs = [
YDef(handle='shieldAmount', unit='EHP', label='Shield amount'),
YDef(handle='shieldAmount', unit='HP', label='Shield amount'),
YDef(handle='shieldRegen', unit='EHP/s', label='Shield regen'),
YDef(handle='shieldRegen', unit='HP/s', label='Shield regen')]
inputs = [
Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=120, defaultRange=(0, 300), mainOnly=True),
Input(handle='shieldAmount', unit='%', label='Shield amount', iconID=1384, defaultValue=25, defaultRange=(0, 100), mainOnly=True)]
srcExtraCols = ('ShieldAmount', 'ShieldTime')
# Calculation stuff
_normalizers = {
('shieldAmount', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('shieldCapacity')}
_limiters = {
'shieldAmount': lambda fit, tgt: (0, fit.ship.getModifiedItemAttr('shieldCapacity'))}
_getters = {
('time', 'shieldAmount'): Time2ShieldAmountGetter,
('time', 'shieldRegen'): Time2ShieldRegenGetter,
('shieldAmount', 'shieldAmount'): ShieldAmount2ShieldAmountGetter,
('shieldAmount', 'shieldRegen'): ShieldAmount2ShieldRegenGetter}
_denormalizers = {
('shieldAmount', '%'): lambda v, fit, tgt: v * 100 / fit.ship.getModifiedItemAttr('shieldCapacity'),
('shieldAmount', 'EHP'): lambda v, fit, tgt: fit.damagePattern.effectivify(fit, v, 'shield'),
('shieldRegen', 'EHP/s'): lambda v, fit, tgt: fit.damagePattern.effectivify(fit, v, 'shield')}
FitShieldRegenGraph.register()

View File

@@ -1 +0,0 @@
import gui.builtinGraphs.fitWarpTime.graph # noqa: E402,F401

View File

@@ -1,82 +0,0 @@
# =============================================================================
# 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 eos.const import FittingModuleState
from gui.builtinGraphs.base import FitDataCache
class SubwarpSpeedCache(FitDataCache):
def getSubwarpSpeed(self, fit):
try:
subwarpSpeed = self._data[fit.ID]
except KeyError:
modStates = {}
disallowedGroups = (
# Active modules which affect ship speed and cannot be used in warp
'Propulsion Module',
'Mass Entanglers',
'Cloaking Device',
# Those reduce ship speed to 0
'Siege Module',
'Super Weapon',
'Cynosural Field Generator',
'Clone Vat Bay',
'Jump Portal Generator')
for mod in fit.modules:
if mod.item is not None and mod.item.group.name in disallowedGroups and mod.state >= FittingModuleState.ACTIVE:
modStates[mod] = mod.state
mod.state = FittingModuleState.ONLINE
projFitStates = {}
for projFit in fit.projectedFits:
projectionInfo = projFit.getProjectionInfo(fit.ID)
if projectionInfo is not None and projectionInfo.active:
projFitStates[projectionInfo] = projectionInfo.active
projectionInfo.active = False
projModStates = {}
for mod in fit.projectedModules:
if not mod.isExclusiveSystemEffect and mod.state >= FittingModuleState.ACTIVE:
projModStates[mod] = mod.state
mod.state = FittingModuleState.ONLINE
projDroneStates = {}
for drone in fit.projectedDrones:
if drone.amountActive > 0:
projDroneStates[drone] = drone.amountActive
drone.amountActive = 0
projFighterStates = {}
for fighter in fit.projectedFighters:
if fighter.active:
projFighterStates[fighter] = fighter.active
fighter.active = False
fit.calculateModifiedAttributes()
subwarpSpeed = fit.ship.getModifiedItemAttr('maxVelocity')
self._data[fit.ID] = subwarpSpeed
for projInfo, state in projFitStates.items():
projInfo.active = state
for mod, state in modStates.items():
mod.state = state
for mod, state in projModStates.items():
mod.state = state
for drone, amountActive in projDroneStates.items():
drone.amountActive = amountActive
for fighter, state in projFighterStates.items():
fighter.active = state
fit.calculateModifiedAttributes()
return subwarpSpeed

View File

@@ -1,78 +0,0 @@
# =============================================================================
# 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
from gui.builtinGraphs.base import SmoothPointGetter
AU_METERS = 149597870700
class Distance2TimeGetter(SmoothPointGetter):
def __init__(self, graph):
super().__init__(graph, baseResolution=500, extraDepth=0)
def _getCommonData(self, miscParams, fit, tgt):
return {
'subwarpSpeed': self.graph._subspeedCache.getSubwarpSpeed(fit),
'warpSpeed': fit.warpSpeed}
def _calculatePoint(self, x, miscParams, fit, tgt, commonData):
distance = x
time = calculate_time_in_warp(
max_subwarp_speed=commonData['subwarpSpeed'],
max_warp_speed=commonData['warpSpeed'],
warp_dist=distance)
return time
# Taken from https://wiki.eveuniversity.org/Warp_time_calculation#Implementation
# with minor modifications
# Warp speed in AU/s, subwarp speed in m/s, distance in m
def calculate_time_in_warp(max_warp_speed, max_subwarp_speed, warp_dist):
if warp_dist == 0:
return 0
k_accel = max_warp_speed
k_decel = min(max_warp_speed / 3, 2)
warp_dropout_speed = max_subwarp_speed / 2
max_ms_warp_speed = max_warp_speed * AU_METERS
accel_dist = AU_METERS
decel_dist = max_ms_warp_speed / k_decel
minimum_dist = accel_dist + decel_dist
cruise_time = 0
if minimum_dist > warp_dist:
max_ms_warp_speed = warp_dist * k_accel * k_decel / (k_accel + k_decel)
else:
cruise_time = (warp_dist - minimum_dist) / max_ms_warp_speed
accel_time = math.log(max_ms_warp_speed / k_accel) / k_accel
decel_time = math.log(max_ms_warp_speed / warp_dropout_speed) / k_decel
total_time = cruise_time + accel_time + decel_time
return total_time

View File

@@ -1,65 +0,0 @@
# =============================================================================
# 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 service.const import GraphCacheCleanupReason
from gui.builtinGraphs.base import FitGraph, XDef, YDef, Input
from .getter import Distance2TimeGetter, AU_METERS
from .cache import SubwarpSpeedCache
class FitWarpTimeGraph(FitGraph):
def __init__(self):
super().__init__()
self._subspeedCache = SubwarpSpeedCache()
def _clearInternalCache(self, reason, extraData):
if reason in (GraphCacheCleanupReason.fitChanged, GraphCacheCleanupReason.fitRemoved):
self._subspeedCache.clearForFit(extraData)
elif reason == GraphCacheCleanupReason.graphSwitched:
self._subspeedCache.clearAll()
# UI stuff
internalName = 'warpTimeGraph'
name = 'Warp Time'
xDefs = [
XDef(handle='distance', unit='AU', label='Distance', mainInput=('distance', 'AU')),
XDef(handle='distance', unit='km', label='Distance', mainInput=('distance', 'km'))]
yDefs = [
YDef(handle='time', unit='s', label='Warp time')]
inputs = [
Input(handle='distance', unit='AU', label='Distance', iconID=1391, defaultValue=20, defaultRange=(0, 50)),
Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=1000, defaultRange=(150, 5000))]
srcExtraCols = ('WarpSpeed', 'WarpDistance')
# Calculation stuff
_normalizers = {
('distance', 'AU'): lambda v, fit, tgt: v * AU_METERS,
('distance', 'km'): lambda v, fit, tgt: v * 1000}
_limiters = {
'distance': lambda fit, tgt: (0, fit.maxWarpDistance * AU_METERS)}
_getters = {
('distance', 'time'): Distance2TimeGetter}
_denormalizers = {
('distance', 'AU'): lambda v, fit, tgt: v / AU_METERS,
('distance', 'km'): lambda v, fit, tgt: v / 1000}
FitWarpTimeGraph.register()

View File

@@ -1,21 +0,0 @@
# =============================================================================
# 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

View File

@@ -1,341 +0,0 @@
# =============================================================================
# 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 gui.bitmap_loader import BitmapLoader
from gui.builtinGraphs.base import FitGraph
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 ''

View File

@@ -1,324 +0,0 @@
# =============================================================================
# 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()

View File

@@ -1,391 +0,0 @@
# =============================================================================
# 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 service.const import GraphCacheCleanupReason
from service.fit import Fit
from gui.utils.inputs import FloatBox, FloatRangeBox
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()

View File

@@ -1,242 +0,0 @@
# =============================================================================
# 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

View File

@@ -34,11 +34,12 @@ from logbook import Logger
from wx.lib.inspection import InspectionTool
import config
import gui.fitCommands as cmd
import gui.globalEvents as GE
from eos.config import gamedata_date, gamedata_version
# import this to access override setting
from eos.modifiedAttributeDict import ModifiedAttributeDict
from gui import graphFrame
from graphs.gui import GraphFrame, frame as graphFrame
from gui.additionsPane import AdditionsPane
from gui.bitmap_loader import BitmapLoader
from gui.builtinMarketBrowser.events import ItemSelected
@@ -51,16 +52,15 @@ from gui.chrome_tabs import ChromeNotebook
from gui.copySelectDialog import CopySelectDialog
from gui.devTools import DevTools
from gui.esiFittings import EveFittings, ExportToEve, SsoCharacterMgmt
from gui.graphFrame import GraphFrame
from gui.mainMenuBar import MainMenuBar
from gui.marketBrowser import MarketBrowser
from gui.multiSwitch import MultiSwitch
from gui.patternEditor import DmgPatternEditorDlg
from gui.preferenceDialog import PreferenceDialog
from gui.targetProfileEditor import TargetProfileEditorDlg
from gui.setEditor import ImplantSetEditorDlg
from gui.shipBrowser import ShipBrowser
from gui.statsPane import StatsPane
from gui.targetProfileEditor import TargetProfileEditorDlg
from gui.updateDialog import UpdateDialog
from gui.utils.clipboard import fromClipboard
from service.character import Character
@@ -70,7 +70,6 @@ from service.port import IPortUser, Port
from service.price import Price
from service.settings import HTMLExportSettings, SettingsProvider
from service.update import Update
import gui.fitCommands as cmd
pyfalog = Logger(__name__)
@@ -963,9 +962,9 @@ class MainFrame(wx.Frame):
if not self.graphFrame:
self.graphFrame = GraphFrame(self)
if graphFrame.frame.graphFrame_enabled:
if graphFrame.graphFrame_enabled:
self.graphFrame.Show()
elif graphFrame.frame.graphFrame_enabled:
elif graphFrame.graphFrame_enabled:
self.graphFrame.SetFocus()
def openWXInspectTool(self, event):

View File

@@ -23,7 +23,7 @@ import wx
import config
from service.character import Character
from service.fit import Fit
import gui.graphFrame
from graphs.gui import GraphFrame, frame as graphFrame
import gui.globalEvents as GE
from gui.bitmap_loader import BitmapLoader
@@ -101,7 +101,7 @@ class MainMenuBar(wx.MenuBar):
graphFrameItem = wx.MenuItem(fitMenu, self.graphFrameId, "&Graphs\tCTRL+G")
graphFrameItem.SetBitmap(BitmapLoader.getBitmap("graphs_small", "gui"))
fitMenu.Append(graphFrameItem)
if not gui.graphFrame.frame.graphFrame_enabled:
if not graphFrame.graphFrame_enabled:
self.Enable(self.graphFrameId, False)
self.ignoreRestrictionItem = fitMenu.Append(self.toggleIgnoreRestrictionID, "Disable Fitting Re&strictions")