Merge branch 'dps_sim_graph' of github.com:pyfa-org/Pyfa into dps_sim_graph

This commit is contained in:
DarkPhoenix
2019-07-05 00:40:30 +03:00
41 changed files with 2725 additions and 1831 deletions

View File

@@ -1,76 +0,0 @@
# ===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
import math
from abc import ABCMeta, abstractmethod
class Graph(metaclass=ABCMeta):
def __init__(self):
self._cache = {}
@abstractmethod
def getPlotPoints(self, fit, extraData, xRange, xAmount):
raise NotImplementedError
def getYForX(self, fit, extraData, x):
raise NotImplementedError
def _xIter(self, fit, extraData, xRange, xAmount):
rangeLow, rangeHigh = self._limitXRange(xRange, fit, extraData)
# Amount is amount of ranges between points here, not amount of points
step = (rangeHigh - rangeLow) / xAmount
if step == 0:
yield xRange[0]
else:
current = rangeLow
# Take extra half step to make sure end of range is always included
# despite any possible float errors
while current <= (rangeHigh + step / 2):
yield current
current += step
def _limitXRange(self, xRange, fit, extraData):
rangeLow, rangeHigh = sorted(xRange)
limitLow, limitHigh = self._getXLimits(fit, extraData)
rangeLow = max(limitLow, rangeLow)
rangeHigh = min(limitHigh, rangeHigh)
return rangeLow, rangeHigh
def _getXLimits(self, fit, extraData):
return -math.inf, math.inf
def clearCache(self, key=None):
if key is None:
self._cache.clear()
elif key in self._cache:
del self._cache[key]
class SmoothGraph(Graph, metaclass=ABCMeta):
def getPlotPoints(self, fit, extraData, xRange, xAmount):
xs = []
ys = []
for x in self._xIter(fit, extraData, xRange, xAmount):
xs.append(x)
ys.append(self.getYForX(fit, extraData, x))
return xs, ys

View File

@@ -1,15 +0,0 @@
import math
from eos.graph import SmoothGraph
class FitCapAmountVsTimeGraph(SmoothGraph):
def getYForX(self, fit, extraData, time):
if time < 0:
return 0
maxCap = fit.ship.getModifiedItemAttr('capacitorCapacity')
regenTime = fit.ship.getModifiedItemAttr('rechargeRate') / 1000
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
cap = maxCap * (1 + math.exp(5 * -time / regenTime) * -1) ** 2
return cap

View File

@@ -1,14 +0,0 @@
import math
from eos.graph import SmoothGraph
class FitCapRegenVsCapPercGraph(SmoothGraph):
def getYForX(self, fit, extraData, perc):
maxCap = fit.ship.getModifiedItemAttr('capacitorCapacity')
regenTime = fit.ship.getModifiedItemAttr('rechargeRate') / 1000
currentCap = maxCap * perc / 100
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate
regen = 10 * maxCap / regenTime * (math.sqrt(currentCap / maxCap) - currentCap / maxCap)
return regen

View File

@@ -1,17 +0,0 @@
import math
from eos.graph import SmoothGraph
class FitDistanceVsTimeGraph(SmoothGraph):
def getYForX(self, fit, extraData, time):
maxSpeed = fit.ship.getModifiedItemAttr('maxVelocity')
mass = fit.ship.getModifiedItemAttr('mass')
agility = fit.ship.getModifiedItemAttr('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,151 +0,0 @@
# ===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from eos.graph import Graph
from eos.utils.spoolSupport import SpoolType, SpoolOptions
from gui.utils.numberFormatter import roundToPrec
class FitDmgVsTimeGraph(Graph):
def getPlotPoints(self, fit, extraData, xRange, xAmount):
# We deliberately ignore xAmount here to build graph which will reflect
# all steps of building up the damage
minX, maxX = self._limitXRange(xRange, fit, extraData)
if fit.ID not in self._cache:
self.__generateCache(fit, maxX)
currentY = None
xs = []
ys = []
cache = self._cache[fit.ID]
for time in sorted(cache):
prevY = currentY
currentX = time / 1000
currentY = roundToPrec(cache[time], 6)
if currentX < minX:
continue
# First set of data points
if not xs:
# Start at exactly requested time, at last known value
initialY = prevY or 0
xs.append(minX)
ys.append(initialY)
# If current time is bigger then starting, extend plot to that time with old value
if currentX > minX:
xs.append(currentX)
ys.append(initialY)
# If new value is different, extend it with new point to the new value
if currentY != prevY:
xs.append(currentX)
ys.append(currentY)
continue
# Last data point
if currentX >= maxX:
xs.append(maxX)
ys.append(prevY)
break
# Anything in-between
if currentY != prevY:
if prevY is not None:
xs.append(currentX)
ys.append(prevY)
xs.append(currentX)
ys.append(currentY)
return xs, ys
def getYForX(self, fit, extraData, x):
time = x * 1000
cache = self._cache[fit.ID]
closestTime = max((t for t in cache if t <= time), default=None)
if closestTime is None:
return 0
return roundToPrec(cache[closestTime], 6)
def _getXLimits(self, fit, extraData):
return 0, 2500
def __generateCache(self, fit, maxTime):
cache = self._cache[fit.ID] = {}
def addDmg(addedTime, addedDmg):
if addedDmg == 0:
return
if addedTime not in cache:
prevTime = max((t for t in cache if t < addedTime), default=None)
if prevTime is None:
cache[addedTime] = 0
else:
cache[addedTime] = cache[prevTime]
for time in (t for t in cache if t >= addedTime):
cache[time] += addedDmg
# We'll handle calculations in milliseconds
maxTime = maxTime * 1000
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 cycleTime, inactiveTime in cycleParams.iterCycles():
volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True))
for volleyTime, volley in volleyParams.items():
addDmg(currentTime + volleyTime, volley.total)
if inactiveTime == 0:
nonstopCycles += 1
else:
nonstopCycles = 0
if currentTime > maxTime:
break
currentTime += cycleTime + inactiveTime
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 cycleTime, inactiveTime in cycleParams.iterCycles():
for volleyTime, volley in volleyParams.items():
addDmg(currentTime + volleyTime, volley.total)
if currentTime > maxTime:
break
currentTime += cycleTime + inactiveTime
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 cycleTime, inactiveTime in abilityCycleParams.iterCycles():
for volleyTime, volley in abilityVolleyParams.items():
addDmg(currentTime + volleyTime, volley.total)
if currentTime > maxTime:
break
currentTime += cycleTime + inactiveTime

View File

@@ -1,197 +0,0 @@
# ===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from math import exp, log, radians, sin, inf
from logbook import Logger
import eos.config
from eos.const import FittingHardpoint, FittingModuleState
from eos.graph import SmoothGraph
from eos.utils.spoolSupport import SpoolType, SpoolOptions
pyfalog = Logger(__name__)
class FitDpsVsRangeGraph(SmoothGraph):
def getYForX(self, fit, extraData, distance):
tgtSpeed = extraData['speed']
tgtSigRad = extraData['signatureRadius'] if extraData['signatureRadius'] is not None else inf
angle = extraData['angle']
tgtSigRadMods = []
tgtSpeedMods = []
total = 0
distance = distance * 1000
for mod in fit.modules:
if not mod.isEmpty and mod.state >= FittingModuleState.ACTIVE:
if "remoteTargetPaintFalloff" in mod.item.effects or "structureModuleEffectTargetPainter" in mod.item.effects:
tgtSigRadMods.append(
1 + (mod.getModifiedItemAttr("signatureRadiusBonus") / 100)
* self.calculateModuleMultiplier(mod, distance))
if "remoteWebifierFalloff" in mod.item.effects or "structureModuleEffectStasisWebifier" in mod.item.effects:
if distance <= mod.getModifiedItemAttr("maxRange"):
tgtSpeedMods.append(1 + (mod.getModifiedItemAttr("speedFactor") / 100))
elif mod.getModifiedItemAttr("falloffEffectiveness") > 0:
# I am affected by falloff
tgtSpeedMods.append(
1 + (mod.getModifiedItemAttr("speedFactor") / 100) *
self.calculateModuleMultiplier(mod, distance))
tgtSpeed = self.penalizeModChain(tgtSpeed, tgtSpeedMods)
tgtSigRad = self.penalizeModChain(tgtSigRad, tgtSigRadMods)
attRad = fit.ship.getModifiedItemAttr('radius', 0)
defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage']
for mod in fit.modules:
dps = mod.getDps(targetResists=fit.targetResists, spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)).total
if mod.hardpoint == FittingHardpoint.TURRET:
if mod.state >= FittingModuleState.ACTIVE:
total += dps * self.calculateTurretMultiplier(fit, mod, distance, angle, tgtSpeed, tgtSigRad)
elif mod.hardpoint == FittingHardpoint.MISSILE:
if mod.state >= FittingModuleState.ACTIVE and mod.maxRange is not None and (mod.maxRange - attRad) >= distance:
total += dps * self.calculateMissileMultiplier(mod, tgtSpeed, tgtSigRad)
if distance <= fit.extraAttributes['droneControlRange']:
for drone in fit.drones:
multiplier = 1 if drone.getModifiedItemAttr('maxVelocity') > 1 else self.calculateTurretMultiplier(
fit, drone, distance, angle, tgtSpeed, tgtSigRad)
dps = drone.getDps(targetResists=fit.targetResists).total
total += dps * multiplier
# this is janky as fuck
for fighter in fit.fighters:
if not fighter.active:
continue
fighterDpsMap = fighter.getDpsPerEffect(targetResists=fit.targetResists)
for ability in fighter.abilities:
if ability.dealsDamage and ability.active:
if ability.effectID not in fighterDpsMap:
continue
multiplier = self.calculateFighterMissileMultiplier(tgtSpeed, tgtSigRad, ability)
dps = fighterDpsMap[ability.effectID].total
total += dps * multiplier
return total
@staticmethod
def calculateMissileMultiplier(mod, tgtSpeed, tgtSigRad):
explosionRadius = mod.getModifiedChargeAttr('aoeCloudSize')
explosionVelocity = mod.getModifiedChargeAttr('aoeVelocity')
damageReductionFactor = mod.getModifiedChargeAttr('aoeDamageReductionFactor')
sigRadiusFactor = tgtSigRad / explosionRadius
if tgtSpeed:
velocityFactor = (explosionVelocity / explosionRadius * tgtSigRad / tgtSpeed) ** damageReductionFactor
else:
velocityFactor = 1
return min(sigRadiusFactor, velocityFactor, 1)
@classmethod
def calculateTurretMultiplier(cls, fit, mod, distance, angle, tgtSpeed, tgtSigRad):
# Source for most of turret calculation info: http://wiki.eveonline.com/en/wiki/Falloff
chanceToHit = cls.calculateTurretChanceToHit(fit, mod, distance, angle, tgtSpeed, tgtSigRad)
if chanceToHit > 0.01:
# AvgDPS = Base Damage * [ ( ChanceToHit^2 + ChanceToHit + 0.0499 ) / 2 ]
multiplier = (chanceToHit ** 2 + chanceToHit + 0.0499) / 2
else:
# All hits are wreckings
multiplier = chanceToHit * 3
dmgScaling = mod.getModifiedItemAttr('turretDamageScalingRadius')
if dmgScaling:
multiplier = min(1, (float(tgtSigRad) / dmgScaling) ** 2)
return multiplier
@staticmethod
def calculateFighterMissileMultiplier(tgtSpeed, tgtSigRad, ability):
prefix = ability.attrPrefix
explosionRadius = ability.fighter.getModifiedItemAttr('{}ExplosionRadius'.format(prefix))
explosionVelocity = ability.fighter.getModifiedItemAttr('{}ExplosionVelocity'.format(prefix))
damageReductionFactor = ability.fighter.getModifiedItemAttr('{}ReductionFactor'.format(prefix), None)
# the following conditionals are because CCP can't keep a decent naming convention, as if fighter implementation
# wasn't already fucked.
if damageReductionFactor is None:
damageReductionFactor = ability.fighter.getModifiedItemAttr('{}DamageReductionFactor'.format(prefix))
damageReductionSensitivity = ability.fighter.getModifiedItemAttr('{}ReductionSensitivity'.format(prefix), None)
if damageReductionSensitivity is None:
damageReductionSensitivity = ability.fighter.getModifiedItemAttr('{}DamageReductionSensitivity'.format(prefix))
sigRadiusFactor = tgtSigRad / explosionRadius
if tgtSpeed:
velocityFactor = (explosionVelocity / explosionRadius * tgtSigRad / tgtSpeed) ** (
log(damageReductionFactor) / log(damageReductionSensitivity))
else:
velocityFactor = 1
return min(sigRadiusFactor, velocityFactor, 1)
@staticmethod
def calculateTurretChanceToHit(fit, mod, distance, angle, tgtSpeed, tgtSigRad):
tracking = mod.getModifiedItemAttr('trackingSpeed')
turretOptimal = mod.maxRange
turretFalloff = mod.falloff
turretSigRes = mod.getModifiedItemAttr('optimalSigRadius')
transversal = sin(radians(angle)) * tgtSpeed
# Angular velocity is calculated using range from ship center to target center.
# We do not know target radius but we know attacker radius
angDistance = distance + fit.ship.getModifiedItemAttr('radius', 0)
if angDistance == 0 and transversal == 0:
angularVelocity = 0
elif angDistance == 0 and transversal != 0:
angularVelocity = inf
else:
angularVelocity = transversal / angDistance
trackingEq = (((angularVelocity / tracking) *
(turretSigRes / tgtSigRad)) ** 2)
rangeEq = ((max(0, distance - turretOptimal)) / turretFalloff) ** 2
return 0.5 ** (trackingEq + rangeEq)
@staticmethod
def calculateModuleMultiplier(mod, distance):
# Simplified formula, we make some assumptions about the module
# This is basically the calculateTurretChanceToHit without tracking values
turretOptimal = mod.maxRange
turretFalloff = mod.falloff
rangeEq = ((max(0, distance - turretOptimal)) / turretFalloff) ** 2
return 0.5 ** rangeEq
@staticmethod
def penalizeModChain(value, mods):
mods.sort(key=lambda v: -abs(v - 1))
try:
for i in range(len(mods)):
bonus = mods[i]
value *= 1 + (bonus - 1) * exp(- i ** 2 / 7.1289)
return value
except Exception as e:
pyfalog.critical('Caught exception when penalizing modifier chain.')
pyfalog.critical(e)
return value

View File

@@ -1,165 +0,0 @@
# ===============================================================================
# Copyright (C) 2010 Diego Duclos
#
# This file is part of eos.
#
# eos is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# eos 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with eos. If not, see <http://www.gnu.org/licenses/>.
# ===============================================================================
from itertools import chain
from eos.graph import Graph
from eos.utils.spoolSupport import SpoolType, SpoolOptions
from gui.utils.numberFormatter import roundToPrec
class FitDpsVsTimeGraph(Graph):
def getPlotPoints(self, fit, extraData, xRange, xAmount):
# We deliberately ignore xAmount here to build graph which will reflect
# all steps of building up the damage
minX, maxX = self._limitXRange(xRange, fit, extraData)
if fit.ID not in self._cache:
self.__generateCache(fit, maxX)
currentY = None
prevY = None
xs = []
ys = []
cache = self._cache[fit.ID]
for time in sorted(cache):
prevY = currentY
currentX = time / 1000
currentY = roundToPrec(cache[time], 6)
if currentX < minX:
continue
# First set of data points
if not xs:
# Start at exactly requested time, at last known value
initialY = prevY or 0
xs.append(minX)
ys.append(initialY)
# If current time is bigger then starting, extend plot to that time with old value
if currentX > minX:
xs.append(currentX)
ys.append(initialY)
# If new value is different, extend it with new point to the new value
if currentY != prevY:
xs.append(currentX)
ys.append(currentY)
continue
# Last data point
if currentX >= maxX:
xs.append(maxX)
ys.append(prevY)
break
# Anything in-between
if currentY != prevY:
if prevY is not None:
xs.append(currentX)
ys.append(prevY)
xs.append(currentX)
ys.append(currentY)
if max(xs) < maxX:
xs.append(maxX)
ys.append(currentY or 0)
return xs, ys
def getYForX(self, fit, extraData, x):
time = x * 1000
cache = self._cache[fit.ID]
closestTime = max((t for t in cache if t <= time), default=None)
if closestTime is None:
return 0
return roundToPrec(cache[closestTime], 6)
def _getXLimits(self, fit, extraData):
return 0, 2500
def __generateCache(self, fit, maxTime):
cache = []
def addDmg(addedTimeStart, addedTimeFinish, addedDmg):
if addedDmg == 0:
return
addedDps = 1000 * addedDmg / (addedTimeFinish - addedTimeStart)
cache.append((addedTimeStart, addedTimeFinish, addedDps))
# We'll handle calculations in milliseconds
maxTime = maxTime * 1000
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 cycleTime, inactiveTime in cycleParams.iterCycles():
cycleDamage = 0
volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True))
for volleyTime, volley in volleyParams.items():
cycleDamage += volley.total
addDmg(currentTime, currentTime + cycleTime, cycleDamage)
currentTime += cycleTime + inactiveTime
if inactiveTime > 0:
nonstopCycles = 0
else:
nonstopCycles += 1
if currentTime > maxTime:
break
for drone in fit.drones:
if not drone.isDealingDamage():
continue
cycleParams = drone.getCycleParameters(reloadOverride=True)
if cycleParams is None:
continue
currentTime = 0
for cycleTime, inactiveTime in cycleParams.iterCycles():
cycleDamage = 0
volleyParams = drone.getVolleyParameters()
for volleyTime, volley in volleyParams.items():
cycleDamage += volley.total
addDmg(currentTime, currentTime + cycleTime, cycleDamage)
currentTime += cycleTime + inactiveTime
if currentTime > maxTime:
break
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
abilityVolleyParams = volleyParams[effectID]
currentTime = 0
for cycleTime, inactiveTime in abilityCycleParams.iterCycles():
cycleDamage = 0
for volleyTime, volley in abilityVolleyParams.items():
cycleDamage += volley.total
addDmg(currentTime, currentTime + cycleTime, cycleDamage)
currentTime += cycleTime + inactiveTime
if currentTime > maxTime:
break
# Post-process cache
finalCache = {}
for time in sorted(set(chain((i[0] for i in cache), (i[1] for i in cache)))):
entries = (e for e in cache if e[0] <= time < e[1])
dps = sum(e[2] for e in entries)
finalCache[time] = dps
self._cache[fit.ID] = finalCache

View File

@@ -1,27 +0,0 @@
import math
from logbook import Logger
from eos.graph import SmoothGraph
pyfalog = Logger(__name__)
class FitShieldAmountVsTimeGraph(SmoothGraph):
def __init__(self):
super().__init__()
import gui.mainFrame
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def getYForX(self, fit, extraData, time):
if time < 0:
return 0
maxShield = fit.ship.getModifiedItemAttr('shieldCapacity')
regenTime = fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate (shield is similar to cap)
shield = maxShield * (1 + math.exp(5 * -time / regenTime) * -1) ** 2
useEhp = self.mainFrame.statsPane.nameViewMap["resistancesViewFull"].showEffective
if fit.damagePattern is not None and useEhp:
shield = fit.damagePattern.effectivify(fit, shield, 'shield')
return shield

View File

@@ -1,22 +0,0 @@
import math
from eos.graph import SmoothGraph
class FitShieldRegenVsShieldPercGraph(SmoothGraph):
def __init__(self):
super().__init__()
import gui.mainFrame
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
def getYForX(self, fit, extraData, perc):
maxShield = fit.ship.getModifiedItemAttr('shieldCapacity')
regenTime = fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000
currentShield = maxShield * perc / 100
# https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate (shield is similar to cap)
regen = 10 * maxShield / regenTime * (math.sqrt(currentShield / maxShield) - currentShield / maxShield)
useEhp = self.mainFrame.statsPane.nameViewMap["resistancesViewFull"].showEffective
if fit.damagePattern is not None and useEhp:
regen = fit.damagePattern.effectivify(fit, regen, 'shield')
return regen

View File

@@ -1,14 +0,0 @@
import math
from eos.graph import SmoothGraph
class FitSpeedVsTimeGraph(SmoothGraph):
def getYForX(self, fit, extraData, time):
maxSpeed = fit.ship.getModifiedItemAttr('maxVelocity')
mass = fit.ship.getModifiedItemAttr('mass')
agility = fit.ship.getModifiedItemAttr('agility')
# https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae
speed = maxSpeed * (1 - math.exp((-time * 1000000) / (agility * mass)))
return speed

View File

@@ -1,98 +0,0 @@
import math
from eos.const import FittingModuleState
from eos.graph import SmoothGraph
AU_METERS = 149597870700
class FitWarpTimeVsDistanceGraph(SmoothGraph):
def __init__(self):
super().__init__()
self.subwarpSpeed = None
def getYForX(self, fit, extraData, distance):
if distance == 0:
return 0
if fit.ID not in self._cache:
self.__generateCache(fit)
maxWarpSpeed = fit.warpSpeed
subwarpSpeed = self._cache[fit.ID]['cleanSubwarpSpeed']
time = calculate_time_in_warp(maxWarpSpeed, subwarpSpeed, distance * AU_METERS)
return time
def _getXLimits(self, fit, extraData):
return 0, fit.maxWarpDistance
def __generateCache(self, fit):
modStates = {}
for mod in fit.modules:
if mod.item is not None and mod.item.group.name in ('Propulsion Module', 'Mass Entanglers', 'Cloaking Device') 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()
self._cache[fit.ID] = {'cleanSubwarpSpeed': fit.ship.getModifiedItemAttr('maxVelocity')}
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()
# 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):
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

@@ -202,6 +202,13 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut):
explosive=volleyValue.explosive * (1 - getattr(targetResists, "explosiveAmount", 0)))
return adjustedVolley
def getVolleyPerEffect(self, targetResists=None):
volleyParams = self.getVolleyParametersPerEffect(targetResists=targetResists)
volleyMap = {}
for effectID, volleyData in volleyParams.items():
volleyMap[effectID] = volleyData[0]
return volleyMap
def getVolley(self, targetResists=None):
volleyParams = self.getVolleyParametersPerEffect(targetResists=targetResists)
em = 0

View File

@@ -18,6 +18,9 @@
# ===============================================================================
from utils.repr import makeReprStr
class DmgTypes:
"""Container for damage data stats."""
@@ -68,3 +71,41 @@ class DmgTypes:
self.explosive += other.explosive
self._calcTotal()
return self
def __mul__(self, mul):
return type(self)(
em=self.em * mul,
thermal=self.thermal * mul,
kinetic=self.kinetic * mul,
explosive=self.explosive * mul)
def __imul__(self, mul):
if mul == 1:
return
self.em *= mul
self.thermal *= mul
self.kinetic *= mul
self.explosive *= mul
self._calcTotal()
return self
def __truediv__(self, div):
return type(self)(
em=self.em / div,
thermal=self.thermal / div,
kinetic=self.kinetic / div,
explosive=self.explosive / div)
def __itruediv__(self, div):
if div == 1:
return
self.em /= div
self.thermal /= div
self.kinetic /= div
self.explosive /= div
self._calcTotal()
return self
def __repr__(self):
spec = ['em', 'thermal', 'kinetic', 'explosive', 'total']
return makeReprStr(self, spec)

View File

@@ -1,11 +1,9 @@
# noinspection PyUnresolvedReferences
from gui.builtinGraphs import ( # noqa: E402,F401
fitDpsVsRange,
fitDmgVsTime,
fitShieldRegenVsShieldPerc,
fitShieldAmountVsTime,
fitCapRegenVsCapPerc,
fitCapAmountVsTime,
fitMobilityVsTime,
fitWarpTimeVsDistance
fitDamageStats,
# fitDmgVsTime,
fitShieldRegen,
fitCapRegen,
fitMobility,
fitWarpTime
)

223
gui/builtinGraphs/base.py Normal file
View File

@@ -0,0 +1,223 @@
# =============================================================================
# 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 abc import ABCMeta, abstractmethod
from collections import OrderedDict, namedtuple
YDef = namedtuple('YDef', ('handle', 'unit', 'label'))
XDef = namedtuple('XDef', ('handle', 'unit', 'label', 'mainInput'))
Input = namedtuple('Input', ('handle', 'unit', 'label', 'iconID', 'defaultValue', 'defaultRange', 'mainOnly'))
VectorDef = namedtuple('VectorDef', ('lengthHandle', 'lengthUnit', 'angleHandle', 'angleUnit', 'label'))
class FitGraph(metaclass=ABCMeta):
# UI stuff
views = []
@classmethod
def register(cls):
FitGraph.views.append(cls)
def __init__(self):
# Format: {(fit ID, target type, target ID): data}
self._plotCache = {}
@property
@abstractmethod
def name(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)
srcVectorDef = None
tgtVectorDef = None
hasTargets = False
def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt=None):
cacheKey = (fit.ID, None, tgt)
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, fitID=None):
# Clear everything
if fitID is None:
self._plotCache.clear()
return
# Clear plot cache
plotKeysToClear = set()
for cacheKey in self._plotCache:
cacheFitID, cacheTgtType, cacheTgtID = cacheKey
if fitID == cacheFitID:
plotKeysToClear.add(cacheKey)
elif fitID == cacheTgtID:
plotKeysToClear.add(cacheKey)
for cacheKey in plotKeysToClear:
del self._plotCache[cacheKey]
self._clearInternalCache(fitID=fitID)
def _clearInternalCache(self, fitID):
return
# Calculation stuff
def _calcPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt):
mainParam, miscParams = self._normalizeParams(mainInput, miscInputs, fit, tgt)
mainParam, miscParams = self._limitParams(mainParam, miscParams, fit, tgt)
xs, ys = self._getPoints(mainParam, miscParams, xSpec, ySpec, fit, tgt)
# Sometimes 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(xs) >= 2:
xs = list(self._iterLinear(mainInput.value, segments=len(xs) - 1))
else:
raise
ys = self._denormalizeValues(ys, ySpec, fit, tgt)
return xs, ys
_normalizers = {}
def _normalizeParams(self, mainInput, miscInputs, fit, tgt):
key = (mainInput.handle, mainInput.unit)
if key in self._normalizers:
normalizer = self._normalizers[key]
newMainInput = (mainInput.handle, tuple(normalizer(v, fit, tgt) for v in mainInput.value))
else:
newMainInput = (mainInput.handle, mainInput.value)
newMiscInputs = []
for miscInput in miscInputs:
key = (miscInput.handle, miscInput.unit)
if key in self._normalizers:
normalizer = self._normalizers[key]
newMiscInput = (miscInput.handle, normalizer(miscInput.value, fit, tgt))
else:
newMiscInput = (miscInput.handle, miscInput.value)
newMiscInputs.append(newMiscInput)
return newMainInput, newMiscInputs
_limiters = {}
def _limitParams(self, mainInput, miscInputs, 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 = mainInput
if mainHandle in self._limiters:
limiter = self._limiters[mainHandle]
newMainInput = (mainHandle, tuple(limitToRange(v, limiter(fit, tgt)) for v in mainValue))
else:
newMainInput = mainInput
newMiscInputs = []
for miscInput in miscInputs:
miscHandle, miscValue = miscInput
if miscHandle in self._limiters:
limiter = self._limiters[miscHandle]
newMiscInput = (miscHandle, limitToRange(miscValue, limiter(fit, tgt)))
newMiscInputs.append(newMiscInput)
else:
newMiscInputs.append(miscInput)
return newMainInput, newMiscInputs
_getters = {}
def _getPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt):
try:
getter = self._getters[(xSpec.handle, ySpec.handle)]
except KeyError:
return [], []
else:
return getter(self, mainInput, miscInputs, fit, 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
def _iterLinear(self, valRange, segments=200):
rangeLow = min(valRange)
rangeHigh = max(valRange)
# Amount is amount of ranges between points here, not amount of points
step = (rangeHigh - rangeLow) / segments
if step == 0:
yield rangeLow
else:
current = rangeLow
# Take extra half step to make sure end of range is always included
# despite any possible float errors
while current <= (rangeHigh + step / 2):
yield current
current += step
class FitDataCache:
def __init__(self):
self._data = {}
def clear(self, fitID):
if fitID is None:
self._data.clear()
elif fitID in self._data:
del self._data[fitID]
# noinspection PyUnresolvedReferences
from gui.builtinGraphs import *

View File

@@ -0,0 +1,109 @@
# =============================================================================
# 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 .base import FitGraph, XDef, YDef, Input
class FitCapRegenGraph(FitGraph):
# UI stuff
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)]
# Calculation stuff
_normalizers = {
('capAmount', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('capacitorCapacity')}
_limiters = {
'capAmount': lambda fit, tgt: (0, fit.ship.getModifiedItemAttr('capacitorCapacity'))}
_denormalizers = {
('capAmount', '%'): lambda v, fit, tgt: v * 100 / fit.ship.getModifiedItemAttr('capacitorCapacity')}
def _time2capAmount(self, mainInput, miscInputs, fit, tgt):
xs = []
ys = []
maxCapAmount = fit.ship.getModifiedItemAttr('capacitorCapacity')
capRegenTime = fit.ship.getModifiedItemAttr('rechargeRate') / 1000
for time in self._iterLinear(mainInput[1]):
currentCapAmount = calculateCapAmount(maxCapAmount=maxCapAmount, capRegenTime=capRegenTime, time=time)
xs.append(time)
ys.append(currentCapAmount)
return xs, ys
def _time2capRegen(self, mainInput, miscInputs, fit, tgt):
xs = []
ys = []
maxCapAmount = fit.ship.getModifiedItemAttr('capacitorCapacity')
capRegenTime = fit.ship.getModifiedItemAttr('rechargeRate') / 1000
for time in self._iterLinear(mainInput[1]):
currentCapAmount = calculateCapAmount(maxCapAmount=maxCapAmount, capRegenTime=capRegenTime, time=time)
currentCapRegen = calculateCapRegen(maxCapAmount=maxCapAmount, capRegenTime=capRegenTime, currentCapAmount=currentCapAmount)
xs.append(time)
ys.append(currentCapRegen)
return xs, ys
def _capAmount2capAmount(self, mainInput, miscInputs, fit, tgt):
# Useless, but valid combination of x and y
xs = []
ys = []
for currentCapAmount in self._iterLinear(mainInput[1]):
xs.append(currentCapAmount)
ys.append(currentCapAmount)
return xs, ys
def _capAmount2capRegen(self, mainInput, miscInputs, fit, tgt):
xs = []
ys = []
maxCapAmount = fit.ship.getModifiedItemAttr('capacitorCapacity')
capRegenTime = fit.ship.getModifiedItemAttr('rechargeRate') / 1000
for currentCapAmount in self._iterLinear(mainInput[1]):
currentCapRegen = calculateCapRegen(maxCapAmount=maxCapAmount, capRegenTime=capRegenTime, currentCapAmount=currentCapAmount)
xs.append(currentCapAmount)
ys.append(currentCapRegen)
return xs, ys
_getters = {
('time', 'capAmount'): _time2capAmount,
('time', 'capRegen'): _time2capRegen,
('capAmount', 'capAmount'): _capAmount2capAmount,
('capAmount', 'capRegen'): _capAmount2capRegen}
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)
FitCapRegenGraph.register()

View File

@@ -1,44 +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 OrderedDict
from eos.graph.fitCapRegenVsCapPerc import FitCapRegenVsCapPercGraph as EosGraph
from gui.graph import Graph, XDef, YDef
class FitCapRegenVsCapPercGraph(Graph):
name = 'Cap Regen vs Cap Amount'
def __init__(self):
super().__init__()
self.eosGraph = EosGraph()
@property
def xDef(self):
return XDef(inputDefault='0-100', inputLabel='Cap amount (percent)', inputIconID=1668, axisLabel='Cap amount, %')
@property
def yDefs(self):
return OrderedDict([('capRegen', YDef(switchLabel='Cap regen', axisLabel='Cap regen, GJ/s', eosGraph='eosGraph'))])
FitCapRegenVsCapPercGraph.register()

View File

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

View File

@@ -0,0 +1,248 @@
# =============================================================================
# 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
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=tgt.ship.getModifiedItemAttr('radius'),
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 + 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 > modRange:
return 0
return 1
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 = tgt.ship.getModifiedItemAttr('radius')
# 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 < max(0, modRange - atkRadius - tgtRadius - blastRadius):
return 0
if 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 > 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 > fit.extraAttributes['droneControlRange']:
return 0
droneSpeed = drone.getModifiedItemAttr('maxVelocity')
# Hard to simulate drone behavior, so assume chance to hit is 1
# when drone is not sentry and is faster than its target
if droneSpeed > 1 and droneSpeed >= tgtSpeed:
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')
cth = _calcTurretChanceToHit(
atkSpeed=min(atkSpeed, droneSpeed),
atkAngle=atkAngle,
atkRadius=droneRadius,
atkOptimalRange=drone.maxRange,
atkFalloffRange=drone.falloff,
atkTracking=drone.getModifiedItemAttr('trackingSpeed'),
atkOptimalSigRadius=drone.getModifiedItemAttr('optimalSigRadius'),
# 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
distance=distance + fit.ship.getModifiedItemAttr('radius') - droneRadius,
tgtSpeed=tgtSpeed,
tgtAngle=tgtAngle,
tgtRadius=tgt.ship.getModifiedItemAttr('radius'),
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)
# It's regular missile-based attack
if fighterSpeed >= tgtSpeed:
rangeFactor = 1
# Same as with drones, if fighters are slower - put them to center of
# the ship and see how they apply
else:
rangeFactor = _calcRangeFactor(
atkOptimalRange=fighter.getModifiedItemAttr('{}RangeOptimal'.format(attrPrefix)),
atkFalloffRange=fighter.getModifiedItemAttr('{}RangeFalloff'.format(attrPrefix)),
distance=distance + fit.ship.getModifiedItemAttr('radius') - fighter.getModifiedItemAttr('radius'))
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
@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."""
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:
angularSpeed = 0 if transSpeed == 0 else math.inf
else:
angularSpeed = transSpeed / ctcDistance
return angularSpeed
def _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius):
"""Calculate tracking chance to hit component."""
return 0.5 ** (((angularSpeed * atkOptimalSigRadius) / (atkTracking * tgtSigRadius)) ** 2)
# Missile-specific
@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
def _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance):
"""Range strength/chance factor, applicable to guns, ewar, RRs, etc."""
return 0.5 ** ((max(0, distance - atkOptimalRange) / atkFalloffRange) ** 2)
def _calcBombFactor(atkEr, tgtSigRadius):
if atkEr == 0:
return 1
else:
return min(1, tgtSigRadius / atkEr)

View File

@@ -0,0 +1,407 @@
# =============================================================================
# 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.const import FittingHardpoint
from eos.utils.spoolSupport import SpoolType, SpoolOptions
from eos.utils.stats import DmgTypes
from gui.builtinGraphs.base import FitGraph, XDef, YDef, Input, VectorDef
from .calc import getTurretMult, getLauncherMult, getDroneMult, getFighterAbilityMult, getSmartbombMult, getBombMult, getGuidedBombMult
from .timeCache import TimeCache
class FitDamageStatsGraph(FitGraph):
def __init__(self):
super().__init__()
self._timeCache = TimeCache()
def _clearInternalCache(self, fitID):
self._timeCache.clear(fitID)
# UI stuff
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), mainOnly=False),
Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=50, defaultRange=(0, 100), mainOnly=False),
Input(handle='tgtSpeed', unit='%', label='Target speed', iconID=1389, defaultValue=100, defaultRange=(0, 100), mainOnly=False),
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
# Calculation stuff
_normalizers = {
('distance', 'km'): lambda v, fit, tgt: v * 1000,
('atkSpeed', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('maxVelocity'),
('tgtSpeed', '%'): lambda v, fit, tgt: v / 100 * tgt.ship.getModifiedItemAttr('maxVelocity'),
('tgtSigRad', '%'): lambda v, fit, tgt: v / 100 * tgt.ship.getModifiedItemAttr('signatureRadius')}
_limiters = {
'time': lambda fit, tgt: (0, 2500)}
_denormalizers = {
('distance', 'km'): lambda v, fit, tgt: v / 1000,
('tgtSpeed', '%'): lambda v, fit, tgt: v * 100 / tgt.ship.getModifiedItemAttr('maxVelocity'),
('tgtSigRad', '%'): lambda v, fit, tgt: v * 100 / tgt.ship.getModifiedItemAttr('signatureRadius')}
def _distance2dps(self, mainInput, miscInputs, fit, tgt):
return self._xDistanceGetter(
mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt,
dmgFunc=self._getDpsPerKey, timeCachePrepFunc=self._timeCache.prepareDpsData)
def _distance2volley(self, mainInput, miscInputs, fit, tgt):
return self._xDistanceGetter(
mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt,
dmgFunc=self._getVolleyPerKey, timeCachePrepFunc=self._timeCache.prepareVolleyData)
def _distance2damage(self, mainInput, miscInputs, fit, tgt):
return self._xDistanceGetter(
mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt,
dmgFunc=self._getDmgPerKey, timeCachePrepFunc=self._timeCache.prepareDmgData)
def _time2dps(self, mainInput, miscInputs, fit, tgt):
return self._xTimeGetter(
mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt,
timeCachePrepFunc=self._timeCache.prepareDpsData, timeCacheGetFunc=self._timeCache.getDpsData)
def _time2volley(self, mainInput, miscInputs, fit, tgt):
return self._xTimeGetter(
mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt,
timeCachePrepFunc=self._timeCache.prepareVolleyData, timeCacheGetFunc=self._timeCache.getVolleyData)
def _time2damage(self, mainInput, miscInputs, fit, tgt):
return self._xTimeGetter(
mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt,
timeCachePrepFunc=self._timeCache.prepareDmgData, timeCacheGetFunc=self._timeCache.getDmgData)
def _tgtSpeed2dps(self, mainInput, miscInputs, fit, tgt):
return self._xTgtSpeedGetter(
mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt,
dmgFunc=self._getDpsPerKey, timeCachePrepFunc=self._timeCache.prepareDpsData)
def _tgtSpeed2volley(self, mainInput, miscInputs, fit, tgt):
return self._xTgtSpeedGetter(
mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt,
dmgFunc=self._getVolleyPerKey, timeCachePrepFunc=self._timeCache.prepareVolleyData)
def _tgtSpeed2damage(self, mainInput, miscInputs, fit, tgt):
return self._xTgtSpeedGetter(
mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt,
dmgFunc=self._getDmgPerKey, timeCachePrepFunc=self._timeCache.prepareDmgData)
def _tgtSigRad2dps(self, mainInput, miscInputs, fit, tgt):
return self._xTgtSigRadiusGetter(
mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt,
dmgFunc=self._getDpsPerKey, timeCachePrepFunc=self._timeCache.prepareDpsData)
def _tgtSigRad2volley(self, mainInput, miscInputs, fit, tgt):
return self._xTgtSigRadiusGetter(
mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt,
dmgFunc=self._getVolleyPerKey, timeCachePrepFunc=self._timeCache.prepareVolleyData)
def _tgtSigRad2damage(self, mainInput, miscInputs, fit, tgt):
return self._xTgtSigRadiusGetter(
mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt,
dmgFunc=self._getDmgPerKey, timeCachePrepFunc=self._timeCache.prepareDmgData)
_getters = {
('distance', 'dps'): _distance2dps,
('distance', 'volley'): _distance2volley,
('distance', 'damage'): _distance2damage,
('time', 'dps'): _time2dps,
('time', 'volley'): _time2volley,
('time', 'damage'): _time2damage,
('tgtSpeed', 'dps'): _tgtSpeed2dps,
('tgtSpeed', 'volley'): _tgtSpeed2volley,
('tgtSpeed', 'damage'): _tgtSpeed2damage,
('tgtSigRad', 'dps'): _tgtSigRad2dps,
('tgtSigRad', 'volley'): _tgtSigRad2volley,
('tgtSigRad', 'damage'): _tgtSigRad2damage}
# Point getter helpers
def _xDistanceGetter(self, mainInput, miscInputs, fit, tgt, dmgFunc, timeCachePrepFunc):
xs = []
ys = []
tgtSigRadius = tgt.ship.getModifiedItemAttr('signatureRadius')
# Process inputs into more convenient form
miscInputMap = dict(miscInputs)
# Get all data we need for all distances into maps/caches
timeCachePrepFunc(fit, miscInputMap['time'])
dmgMap = dmgFunc(fit=fit, time=miscInputMap['time'])
# Go through distances and calculate distance-dependent data
for distance in self._iterLinear(mainInput[1]):
applicationMap = self._getApplicationPerKey(
fit=fit,
tgt=tgt,
atkSpeed=miscInputMap['atkSpeed'],
atkAngle=miscInputMap['atkAngle'],
distance=distance,
tgtSpeed=miscInputMap['tgtSpeed'],
tgtAngle=miscInputMap['tgtAngle'],
tgtSigRadius=tgtSigRadius)
dmg = self._aggregate(dmgMap=dmgMap, applicationMap=applicationMap).total
xs.append(distance)
ys.append(dmg)
return xs, ys
def _xTimeGetter(self, mainInput, miscInputs, fit, tgt, timeCachePrepFunc, timeCacheGetFunc):
xs = []
ys = []
minTime, maxTime = mainInput[1]
tgtSigRadius = tgt.ship.getModifiedItemAttr('signatureRadius')
# Process inputs into more convenient form
miscInputMap = dict(miscInputs)
# Get all data we need for all times into maps/caches
applicationMap = self._getApplicationPerKey(
fit=fit,
tgt=tgt,
atkSpeed=miscInputMap['atkSpeed'],
atkAngle=miscInputMap['atkAngle'],
distance=miscInputMap['distance'],
tgtSpeed=miscInputMap['tgtSpeed'],
tgtAngle=miscInputMap['tgtAngle'],
tgtSigRadius=tgtSigRadius)
timeCachePrepFunc(fit, maxTime)
timeCache = timeCacheGetFunc(fit)
# 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 = self._aggregate(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)
if maxTime > (currentTime or 0):
xs.append(maxTime)
ys.append(currentDmg or 0)
return xs, ys
def _xTgtSpeedGetter(self, mainInput, miscInputs, fit, tgt, dmgFunc, timeCachePrepFunc):
xs = []
ys = []
tgtSigRadius = tgt.ship.getModifiedItemAttr('signatureRadius')
# Process inputs into more convenient form
miscInputMap = dict(miscInputs)
# Get all data we need for all target speeds into maps/caches
timeCachePrepFunc(fit, miscInputMap['time'])
dmgMap = dmgFunc(fit=fit, time=miscInputMap['time'])
# Go through target speeds and calculate distance-dependent data
for tgtSpeed in self._iterLinear(mainInput[1]):
applicationMap = self._getApplicationPerKey(
fit=fit,
tgt=tgt,
atkSpeed=miscInputMap['atkSpeed'],
atkAngle=miscInputMap['atkAngle'],
distance=miscInputMap['distance'],
tgtSpeed=tgtSpeed,
tgtAngle=miscInputMap['tgtAngle'],
tgtSigRadius=tgtSigRadius)
dmg = self._aggregate(dmgMap=dmgMap, applicationMap=applicationMap).total
xs.append(tgtSpeed)
ys.append(dmg)
return xs, ys
def _xTgtSigRadiusGetter(self, mainInput, miscInputs, fit, tgt, dmgFunc, timeCachePrepFunc):
xs = []
ys = []
# Process inputs into more convenient form
miscInputMap = dict(miscInputs)
# Get all data we need for all target speeds into maps/caches
timeCachePrepFunc(fit, miscInputMap['time'])
dmgMap = dmgFunc(fit=fit, time=miscInputMap['time'])
# Go through target speeds and calculate distance-dependent data
for tgtSigRadius in self._iterLinear(mainInput[1]):
applicationMap = self._getApplicationPerKey(
fit=fit,
tgt=tgt,
atkSpeed=miscInputMap['atkSpeed'],
atkAngle=miscInputMap['atkAngle'],
distance=miscInputMap['distance'],
tgtSpeed=miscInputMap['tgtSpeed'],
tgtAngle=miscInputMap['tgtAngle'],
tgtSigRadius=tgtSigRadius)
dmg = self._aggregate(dmgMap=dmgMap, applicationMap=applicationMap).total
xs.append(tgtSigRadius)
ys.append(dmg)
return xs, ys
# Damage data per key getters
def _getDpsPerKey(self, fit, time):
if time is not None:
return self._timeCache.getDpsDataPoint(fit, time)
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 _getVolleyPerKey(self, fit, time):
if time is not None:
return self._timeCache.getVolleyDataPoint(fit, time)
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 _getDmgPerKey(self, fit, time):
# Damage inflicted makes no sense without time specified
if time is None:
raise ValueError
return self._timeCache.getDmgDataPoint(fit, time)
# Application getter
def _getApplicationPerKey(self, 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)
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)
return applicationMap
# Calculate damage from maps
def _aggregate(self, dmgMap, applicationMap):
total = DmgTypes(0, 0, 0, 0)
for key, dmg in dmgMap.items():
total += dmg * applicationMap.get(key, 0)
return total
FitDamageStatsGraph.register()

View File

@@ -0,0 +1,254 @@
# =============================================================================
# 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,48 +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 OrderedDict
from eos.graph.fitDmgVsTime import FitDmgVsTimeGraph as EosGraphDmg
from eos.graph.fitDpsVsTime import FitDpsVsTimeGraph as EosGraphDps
from gui.graph import Graph, XDef, YDef
class FitDmgVsTimeGraph(Graph):
name = 'Damage vs Time'
def __init__(self):
super().__init__()
self.eosGraphDmg = EosGraphDmg()
self.eosGraphDps = EosGraphDps()
@property
def xDef(self):
return XDef(inputDefault='0-80', inputLabel='Time (seconds)', inputIconID=1392, axisLabel='Time, s')
@property
def yDefs(self):
return OrderedDict([
('damage', YDef(switchLabel='Damage inflicted', axisLabel='Damage', eosGraph='eosGraphDmg')),
('dps', YDef(switchLabel='DPS', axisLabel='DPS', eosGraph='eosGraphDps'))])
FitDmgVsTimeGraph.register()

View File

@@ -1,59 +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 OrderedDict
from eos.graph.fitDpsVsRange import FitDpsVsRangeGraph as EosGraph
from gui.graph import Graph, XDef, YDef, ExtraInput
class FitDpsVsRangeGraph(Graph):
name = 'DPS vs Range'
def __init__(self):
super().__init__()
self.eosGraph = EosGraph()
@property
def xDef(self):
return XDef(inputDefault='0-100', inputLabel='Distance to target (km)', inputIconID=1391, axisLabel='Distance to target, km')
@property
def yDefs(self):
return OrderedDict([('dps', YDef(switchLabel='DPS', axisLabel='DPS', eosGraph='eosGraph'))])
@property
def extraInputs(self):
return OrderedDict([
('speed', ExtraInput(inputDefault=0, inputLabel='Target speed (m/s)', inputIconID=1389)),
('signatureRadius', ExtraInput(inputDefault=None, inputLabel='Target signature radius (m)', inputIconID=1390)),
('angle', ExtraInput(inputDefault=0, inputLabel='Target angle (degrees)', inputIconID=1389))])
@property
def hasTargets(self):
return True
@property
def hasVectors(self):
return True
FitDpsVsRangeGraph.register()

View File

@@ -0,0 +1,76 @@
# =============================================================================
# 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 .base import FitGraph, XDef, YDef, Input
class FitMobilityVsTimeGraph(FitGraph):
# UI stuff
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), mainOnly=False)]
# Calculation stuff
_denormalizers = {
('distance', 'km'): lambda v, fit, tgt: v / 1000}
def _time2speed(self, mainInput, miscInputs, fit, tgt):
xs = []
ys = []
maxSpeed = fit.ship.getModifiedItemAttr('maxVelocity')
mass = fit.ship.getModifiedItemAttr('mass')
agility = fit.ship.getModifiedItemAttr('agility')
for time in self._iterLinear(mainInput[1]):
# https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae
speed = maxSpeed * (1 - math.exp((-time * 1000000) / (agility * mass)))
xs.append(time)
ys.append(speed)
return xs, ys
def _time2distance(self, mainInput, miscInputs, fit, tgt):
xs = []
ys = []
maxSpeed = fit.ship.getModifiedItemAttr('maxVelocity')
mass = fit.ship.getModifiedItemAttr('mass')
agility = fit.ship.getModifiedItemAttr('agility')
for time in self._iterLinear(mainInput[1]):
# 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
xs.append(time)
ys.append(distance)
return xs, ys
_getters = {
('time', 'speed'): _time2speed,
('time', 'distance'): _time2distance}
FitMobilityVsTimeGraph.register()

View File

@@ -1,48 +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 OrderedDict
from eos.graph.fitDistanceVsTime import FitDistanceVsTimeGraph as EosGraphDistance
from eos.graph.fitSpeedVsTime import FitSpeedVsTimeGraph as EosGraphSpeed
from gui.graph import Graph, XDef, YDef
class FitMobilityVsTimeGraph(Graph):
name = 'Mobility vs Time'
def __init__(self):
super().__init__()
self.eosGraphSpeed = EosGraphSpeed()
self.eosGraphDistance = EosGraphDistance()
@property
def xDef(self):
return XDef(inputDefault='0-80', inputLabel='Time (seconds)', inputIconID=1392, axisLabel='Time, s')
@property
def yDefs(self):
return OrderedDict([
('speed', YDef(switchLabel='Speed', axisLabel='Speed, m/s', eosGraph='eosGraphSpeed')),
('distance', YDef(switchLabel='Distance', axisLabel='Distance, m', eosGraph='eosGraphDistance'))])
FitMobilityVsTimeGraph.register()

View File

@@ -1,50 +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 OrderedDict
import gui.mainFrame
from eos.graph.fitShieldAmountVsTime import FitShieldAmountVsTimeGraph as EosGraph
from gui.graph import Graph, XDef, YDef
class FitShieldAmountVsTimeGraph(Graph):
name = 'Shield Amount vs Time'
def __init__(self):
super().__init__()
self.eosGraph = EosGraph()
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
@property
def xDef(self):
return XDef(inputDefault='0-300', inputLabel='Time (seconds)', inputIconID=1392, axisLabel='Time, s')
@property
def yDefs(self):
axisLabel = 'Shield amount, {}'.format('EHP' if self.mainFrame.statsPane.nameViewMap["resistancesViewFull"].showEffective else 'HP')
return OrderedDict([('shieldAmount', YDef(switchLabel='Shield amount', axisLabel=axisLabel, eosGraph='eosGraph'))])
def redrawOnEffectiveChange(self):
return True
FitShieldAmountVsTimeGraph.register()

View File

@@ -0,0 +1,116 @@
# =============================================================================
# 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 .base import FitGraph, XDef, YDef, Input
class FitShieldRegenGraph(FitGraph):
# UI stuff
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)]
# Calculation stuff
_normalizers = {
('shieldAmount', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('shieldCapacity')}
_limiters = {
'shieldAmount': lambda fit, tgt: (0, fit.ship.getModifiedItemAttr('shieldCapacity'))}
_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')}
def _time2shieldAmount(self, mainInput, miscInputs, fit, tgt):
xs = []
ys = []
maxShieldAmount = fit.ship.getModifiedItemAttr('shieldCapacity')
shieldRegenTime = fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000
for time in self._iterLinear(mainInput[1]):
currentShieldAmount = calculateShieldAmount(maxShieldAmount=maxShieldAmount, shieldRegenTime=shieldRegenTime, time=time)
xs.append(time)
ys.append(currentShieldAmount)
return xs, ys
def _time2shieldRegen(self, mainInput, miscInputs, fit, tgt):
xs = []
ys = []
maxShieldAmount = fit.ship.getModifiedItemAttr('shieldCapacity')
shieldRegenTime = fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000
for time in self._iterLinear(mainInput[1]):
currentShieldAmount = calculateShieldAmount(maxShieldAmount=maxShieldAmount, shieldRegenTime=shieldRegenTime, time=time)
currentShieldRegen = calculateShieldRegen(maxShieldAmount=maxShieldAmount, shieldRegenTime=shieldRegenTime, currentShieldAmount=currentShieldAmount)
xs.append(time)
ys.append(currentShieldRegen)
return xs, ys
def _shieldAmount2shieldAmount(self, mainInput, miscInputs, fit, tgt):
# Useless, but valid combination of x and y
xs = []
ys = []
for currentShieldAmount in self._iterLinear(mainInput[1]):
xs.append(currentShieldAmount)
ys.append(currentShieldAmount)
return xs, ys
def _shieldAmount2shieldRegen(self, mainInput, miscInputs, fit, tgt):
xs = []
ys = []
maxShieldAmount = fit.ship.getModifiedItemAttr('shieldCapacity')
shieldRegenTime = fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000
for currentShieldAmount in self._iterLinear(mainInput[1]):
currentShieldRegen = calculateShieldRegen(maxShieldAmount=maxShieldAmount, shieldRegenTime=shieldRegenTime, currentShieldAmount=currentShieldAmount)
xs.append(currentShieldAmount)
ys.append(currentShieldRegen)
return xs, ys
_getters = {
('time', 'shieldAmount'): _time2shieldAmount,
('time', 'shieldRegen'): _time2shieldRegen,
('shieldAmount', 'shieldAmount'): _shieldAmount2shieldAmount,
('shieldAmount', 'shieldRegen'): _shieldAmount2shieldRegen}
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)
FitShieldRegenGraph.register()

View File

@@ -1,51 +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 OrderedDict
import gui.mainFrame
from eos.graph.fitShieldRegenVsShieldPerc import FitShieldRegenVsShieldPercGraph as EosGraph
from gui.graph import Graph, XDef, YDef
class FitShieldRegenVsShieldPercGraph(Graph):
name = 'Shield Regen vs Shield Amount'
def __init__(self):
super().__init__()
self.eosGraph = EosGraph()
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
@property
def xDef(self):
return XDef(inputDefault='0-100', inputLabel='Shield amount (percent)', inputIconID=1384, axisLabel='Shield amount, %')
@property
def yDefs(self):
axisLabel = 'Shield regen, {}/s'.format('EHP' if self.mainFrame.statsPane.nameViewMap["resistancesViewFull"].showEffective else 'HP')
return OrderedDict([('shieldRegen', YDef(switchLabel='Shield regen', axisLabel=axisLabel, eosGraph='eosGraph'))])
@property
def redrawOnEffectiveChange(self):
return True
FitShieldRegenVsShieldPercGraph.register()

View File

@@ -0,0 +1,157 @@
# =============================================================================
# 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.const import FittingModuleState
from .base import FitGraph, XDef, YDef, Input, FitDataCache
AU_METERS = 149597870700
class FitWarpTimeGraph(FitGraph):
def __init__(self):
super().__init__()
self._subspeedCache = SubwarpSpeedCache()
def _clearInternalCache(self, fitID):
self._subspeedCache.clear(fitID)
# UI stuff
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), mainOnly=False),
Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=1000, defaultRange=(150, 5000), mainOnly=False)]
# 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)}
_denormalizers = {
('distance', 'AU'): lambda v, fit, tgt: v / AU_METERS,
('distance', 'km'): lambda v, fit, tgt: v / 1000}
def _distance2time(self, mainInput, miscInputs, fit, tgt):
xs = []
ys = []
subwarpSpeed = self._subspeedCache.getSubwarpSpeed(fit)
warpSpeed = fit.warpSpeed
for distance in self._iterLinear(mainInput[1]):
time = calculate_time_in_warp(max_subwarp_speed=subwarpSpeed, max_warp_speed=warpSpeed, warp_dist=distance)
xs.append(distance)
ys.append(time)
return xs, ys
_getters = {
('distance', 'time'): _distance2time}
class SubwarpSpeedCache(FitDataCache):
def getSubwarpSpeed(self, fit):
try:
subwarpSpeed = self._data[fit.ID]
except KeyError:
modStates = {}
for mod in fit.modules:
if mod.item is not None and mod.item.group.name in ('Propulsion Module', 'Mass Entanglers', 'Cloaking Device') 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
# 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
FitWarpTimeGraph.register()

View File

@@ -1,44 +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 OrderedDict
from eos.graph.fitWarpTimeVsDistance import FitWarpTimeVsDistanceGraph as EosGraph
from gui.graph import Graph, XDef, YDef
class FitWarpTimeVsDistanceGraph(Graph):
name = 'Warp Time vs Distance'
def __init__(self):
super().__init__()
self.eosGraph = EosGraph()
@property
def xDef(self):
return XDef(inputDefault='0-50', inputLabel='Distance (AU)', inputIconID=1391, axisLabel='Warp distance, AU')
@property
def yDefs(self):
return OrderedDict([('time', YDef(switchLabel='Warp time', axisLabel='Warp time, s', eosGraph='eosGraph'))])
FitWarpTimeVsDistanceGraph.register()

View File

@@ -27,6 +27,7 @@ import gui.mainFrame
import gui.globalEvents as GE
from gui.utils import fonts
EffectiveHpToggled, EFFECTIVE_HP_TOGGLED = wx.lib.newevent.NewEvent()

View File

@@ -1,107 +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 re
from abc import ABCMeta, abstractmethod
from collections import namedtuple
class Graph(metaclass=ABCMeta):
views = []
yTypes = None
@classmethod
def register(cls):
Graph.views.append(cls)
def __init__(self):
self._cache = {}
@property
@abstractmethod
def name(self):
raise NotImplementedError
@property
@abstractmethod
def xDef(self):
raise NotImplementedError
@property
def extraInputs(self):
return {}
@property
@abstractmethod
def yDefs(self):
raise NotImplementedError
@property
def hasTargets(self):
return False
@property
def hasVectors(self):
return False
@property
def redrawOnEffectiveChange(self):
return False
def getPlotPoints(self, fit, extraData, xRange, xAmount, yType):
try:
plotData = self._cache[fit.ID][yType]
except KeyError:
xRange = self.parseRange(xRange)
extraData = {k: float(v) if v else None for k, v in extraData.items()}
graph = getattr(self, self.yDefs[yType].eosGraph, None)
plotData = graph.getPlotPoints(fit, extraData, xRange, xAmount)
fitCache = self._cache.setdefault(fit.ID, {})
fitCache[yType] = plotData
return plotData
def parseRange(self, string):
m = re.match('\s*(?P<first>\d+(\.\d+)?)\s*(-\s*(?P<second>\d+(\.\d+)?))?', string)
if m is None:
return (0, 0)
first = float(m.group('first'))
second = m.group('second')
second = float(second) if second is not None else 0
low = min(first, second)
high = max(first, second)
return (low, high)
def clearCache(self, key=None):
if key is None:
self._cache.clear()
elif key in self._cache:
del self._cache[key]
for yDef in self.yDefs.values():
getattr(self, yDef.eosGraph).clearCache(key=key)
XDef = namedtuple('XDef', ('inputDefault', 'inputLabel', 'inputIconID', 'axisLabel'))
YDef = namedtuple('YDef', ('switchLabel', 'axisLabel', 'eosGraph'))
ExtraInput = namedtuple('ExtraInput', ('inputDefault', 'inputLabel', 'inputIconID'))
# noinspection PyUnresolvedReferences
from gui.builtinGraphs import *

View File

@@ -1,549 +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 os
import traceback
from itertools import chain
# noinspection PyPackageRequirements
import wx
from logbook import Logger
import gui.display
import gui.globalEvents as GE
import gui.mainFrame
from gui.bitmap_loader import BitmapLoader
from gui.graph import Graph
from service.fit import Fit
pyfalog = Logger(__name__)
try:
import matplotlib as mpl
mpl_version = int(mpl.__version__[0]) or -1
if mpl_version >= 2:
mpl.use('wxagg')
mplImported = True
else:
mplImported = False
from matplotlib.patches import Patch
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as Canvas
from matplotlib.figure import Figure
graphFrame_enabled = True
mplImported = True
except ImportError as e:
pyfalog.warning("Matplotlib failed to import. Likely missing or incompatible version.")
mpl_version = -1
Patch = mpl = Canvas = Figure = None
graphFrame_enabled = False
mplImported = 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)
mpl_version = -1
Patch = mpl = Canvas = Figure = None
graphFrame_enabled = False
mplImported = 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
global mplImported
global mpl_version
self.legendFix = False
if not graphFrame_enabled:
pyfalog.warning("Matplotlib is not enabled. Skipping initialization.")
return
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):
# remove matplotlib font cache, see #234
os.remove(cache_file)
if not mplImported:
mpl.use('wxagg')
graphFrame_enabled = True
if int(mpl.__version__[0]) < 1:
pyfalog.warning("pyfa: Found matplotlib version {} - activating OVER9000 workarounds".format(mpl.__version__))
pyfalog.warning("pyfa: Recommended minimum matplotlib version is 1.0.0")
self.legendFix = True
mplImported = True
wx.Frame.__init__(self, parent, title="pyfa: Graph Generator", style=style, size=(520, 390))
i = wx.Icon(BitmapLoader.getBitmap("graphs_small", "gui"))
self.SetIcon(i)
self.mainFrame = gui.mainFrame.MainFrame.getInstance()
self.CreateStatusBar()
self.mainSizer = wx.BoxSizer(wx.VERTICAL)
self.SetSizer(self.mainSizer)
sFit = Fit.getInstance()
fit = sFit.getFit(self.mainFrame.getActiveFit())
self.fits = [fit] if fit is not None else []
self.fitList = FitList(self)
self.fitList.SetMinSize((270, -1))
self.fitList.fitList.update(self.fits)
self.targets = []
# self.targetList = TargetList(self)
# self.targetList.SetMinSize((270, -1))
# self.targetList.targetList.update(self.targets)
self.graphSelection = wx.Choice(self, wx.ID_ANY, style=0)
self.mainSizer.Add(self.graphSelection, 0, wx.EXPAND)
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)
self.mainSizer.Add(self.canvas, 1, wx.EXPAND)
self.mainSizer.Add(wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0,
wx.EXPAND)
self.graphCtrlPanel = wx.Panel(self)
self.mainSizer.Add(self.graphCtrlPanel, 0, wx.EXPAND | wx.ALL, 0)
self.showY0 = True
self.selectedY = None
self.selectedYRbMap = {}
ctrlPanelSizer = wx.BoxSizer(wx.HORIZONTAL)
viewOptSizer = wx.BoxSizer(wx.VERTICAL)
self.showY0Cb = wx.CheckBox(self.graphCtrlPanel, wx.ID_ANY, "Always show Y = 0", wx.DefaultPosition, wx.DefaultSize, 0)
self.showY0Cb.SetValue(self.showY0)
self.showY0Cb.Bind(wx.EVT_CHECKBOX, self.OnShowY0Update)
viewOptSizer.Add(self.showY0Cb, 0, wx.LEFT | wx.TOP | wx.RIGHT | wx.EXPAND, 5)
self.graphSubselSizer = wx.BoxSizer(wx.VERTICAL)
viewOptSizer.Add(self.graphSubselSizer, 0, wx.ALL | wx.EXPAND, 5)
ctrlPanelSizer.Add(viewOptSizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.BOTTOM, 5)
self.inputsSizer = wx.FlexGridSizer(0, 4, 0, 0)
self.inputsSizer.AddGrowableCol(1)
ctrlPanelSizer.Add(self.inputsSizer, 1, wx.EXPAND | wx.RIGHT | wx.TOP | wx.BOTTOM, 5)
self.graphCtrlPanel.SetSizer(ctrlPanelSizer)
self.drawTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.draw, self.drawTimer)
for view in Graph.views:
view = view()
self.graphSelection.Append(view.name, view)
self.graphSelection.SetSelection(0)
self.fields = {}
self.updateGraphWidgets()
self.sl1 = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL)
self.mainSizer.Add(self.sl1, 0, wx.EXPAND)
fitSizer = wx.BoxSizer(wx.HORIZONTAL)
fitSizer.Add(self.fitList, 1, wx.EXPAND)
#fitSizer.Add(self.targetList, 1, wx.EXPAND)
self.mainSizer.Add(fitSizer, 0, wx.EXPAND)
self.fitList.fitList.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick)
self.fitList.fitList.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu)
self.mainFrame.Bind(GE.FIT_CHANGED, self.OnFitChanged)
self.mainFrame.Bind(GE.FIT_REMOVED, self.OnFitRemoved)
self.Bind(wx.EVT_CLOSE, self.closeEvent)
self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent)
self.Bind(wx.EVT_CHOICE, self.graphChanged)
from gui.builtinStatsViews.resistancesViewFull import EFFECTIVE_HP_TOGGLED # Grr crclar gons
self.mainFrame.Bind(EFFECTIVE_HP_TOGGLED, self.OnEhpToggled)
self.contextMenu = wx.Menu()
removeItem = wx.MenuItem(self.contextMenu, 1, 'Remove Fit')
self.contextMenu.Append(removeItem)
self.contextMenu.Bind(wx.EVT_MENU, self.ContextMenuHandler, removeItem)
self.Fit()
self.SetMinSize(self.GetSize())
def handleDrag(self, type, fitID):
if type == "fit":
self.AppendFitToList(fitID)
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
elif keycode == 65 and mstate.GetModifiers() == wx.MOD_CONTROL:
self.fitList.fitList.selectAll()
elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and mstate.GetModifiers() == wx.MOD_NONE:
self.removeFits(self.getSelectedFits())
event.Skip()
def OnContextMenu(self, event):
if self.getSelectedFits():
self.PopupMenu(self.contextMenu)
def ContextMenuHandler(self, event):
selectedMenuItem = event.GetId()
if selectedMenuItem == 1: # Copy was chosen
fits = self.getSelectedFits()
self.removeFits(fits)
def OnEhpToggled(self, event):
event.Skip()
view = self.getView()
if view.redrawOnEffectiveChange:
view.clearCache()
self.draw()
def OnFitChanged(self, event):
event.Skip()
view = self.getView()
view.clearCache(key=event.fitID)
self.draw()
def OnFitRemoved(self, event):
event.Skip()
fit = next((f for f in self.fits if f.ID == event.fitID), None)
if fit is not None:
self.removeFits([fit])
def graphChanged(self, event):
self.selectedY = None
self.updateGraphWidgets()
event.Skip()
def closeWindow(self):
from gui.builtinStatsViews.resistancesViewFull import EFFECTIVE_HP_TOGGLED # Grr gons
self.fitList.fitList.Unbind(wx.EVT_LEFT_DCLICK, handler=self.OnLeftDClick)
self.mainFrame.Unbind(GE.FIT_CHANGED, handler=self.OnFitChanged)
self.mainFrame.Unbind(GE.FIT_REMOVED, handler=self.OnFitRemoved)
self.mainFrame.Unbind(EFFECTIVE_HP_TOGGLED, handler=self.OnEhpToggled)
self.Destroy()
def getView(self):
return self.graphSelection.GetClientData(self.graphSelection.GetSelection())
def getValues(self):
values = {}
for fieldHandle, field in self.fields.items():
values[fieldHandle] = field.GetValue()
return values
def OnShowY0Update(self, event):
event.Skip()
self.showY0 = self.showY0Cb.GetValue()
self.draw()
def OnYTypeUpdate(self, event):
event.Skip()
obj = event.GetEventObject()
formatName = obj.GetLabel()
self.selectedY = self.selectedYRbMap[formatName]
self.draw()
def updateGraphWidgets(self):
view = self.getView()
view.clearCache()
self.graphSubselSizer.Clear()
self.inputsSizer.Clear()
for child in self.graphCtrlPanel.Children:
if child is not self.showY0Cb:
child.Destroy()
self.fields.clear()
# Setup view options
self.selectedYRbMap.clear()
if len(view.yDefs) > 1:
i = 0
for yAlias, yDef in view.yDefs.items():
if i == 0:
rdo = wx.RadioButton(self.graphCtrlPanel, wx.ID_ANY, yDef.switchLabel, style=wx.RB_GROUP)
else:
rdo = wx.RadioButton(self.graphCtrlPanel, wx.ID_ANY, yDef.switchLabel)
rdo.Bind(wx.EVT_RADIOBUTTON, self.OnYTypeUpdate)
if i == (self.selectedY or 0):
rdo.SetValue(True)
self.graphSubselSizer.Add(rdo, 0, wx.ALL | wx.EXPAND, 0)
self.selectedYRbMap[yDef.switchLabel] = i
i += 1
# Setup inputs
for fieldHandle, fieldDef in (('x', view.xDef), *view.extraInputs.items()):
textBox = wx.TextCtrl(self.graphCtrlPanel, wx.ID_ANY, style=0)
self.fields[fieldHandle] = textBox
textBox.Bind(wx.EVT_TEXT, self.onFieldChanged)
self.inputsSizer.Add(textBox, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 3)
if fieldDef.inputDefault is not None:
inputDefault = fieldDef.inputDefault
if not isinstance(inputDefault, str):
inputDefault = ("%f" % inputDefault).rstrip("0")
if inputDefault[-1:] == ".":
inputDefault += "0"
textBox.ChangeValue(inputDefault)
imgLabelSizer = wx.BoxSizer(wx.HORIZONTAL)
if fieldDef.inputIconID:
icon = BitmapLoader.getBitmap(fieldDef.inputIconID, "icons")
if icon is not None:
static = wx.StaticBitmap(self.graphCtrlPanel)
static.SetBitmap(icon)
imgLabelSizer.Add(static, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 1)
imgLabelSizer.Add(wx.StaticText(self.graphCtrlPanel, wx.ID_ANY, fieldDef.inputLabel), 0,
wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 3)
self.inputsSizer.Add(imgLabelSizer, 0, wx.ALIGN_CENTER_VERTICAL)
self.Layout()
self.draw()
def delayedDraw(self, event=None):
self.drawTimer.Stop()
self.drawTimer.Start(Fit.getInstance().serviceFittingOptions["marketSearchDelay"], True)
def draw(self, event=None):
global mpl_version
if event is not None:
event.Skip()
self.drawTimer.Stop()
# todo: FIX THIS, see #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
values = self.getValues()
view = self.getView()
self.subplot.clear()
self.subplot.grid(True)
legend = []
min_y = 0 if self.showY0 else None
max_y = 0 if self.showY0 else None
xRange = values['x']
extraInputs = {ih: values[ih] for ih in view.extraInputs}
try:
chosenY = [i for i in view.yDefs.keys()][self.selectedY or 0]
except IndexError:
chosenY = [i for i in view.yDefs.keys()][0]
self.subplot.set(xlabel=view.xDef.axisLabel, ylabel=view.yDefs[chosenY].axisLabel)
for fit in self.fits:
try:
xs, ys = view.getPlotPoints(fit, extraInputs, xRange, 100, chosenY)
# 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)
self.subplot.plot(xs, ys)
legend.append('{} ({})'.format(fit.name, fit.ship.item.getShortName()))
except Exception as ex:
pyfalog.warning("Invalid values in '{0}'", fit.name)
self.SetStatusText("Invalid values in '%s'" % fit.name)
self.canvas.draw()
return
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)
if mpl_version < 2:
if self.legendFix and len(legend) > 0:
leg = self.subplot.legend(tuple(legend), "upper right", shadow=False)
for t in leg.get_texts():
t.set_fontsize('small')
for l in leg.get_lines():
l.set_linewidth(1)
elif not self.legendFix and len(legend) > 0:
leg = self.subplot.legend(tuple(legend), "upper right", shadow=False, frameon=False)
for t in leg.get_texts():
t.set_fontsize('small')
for l in leg.get_lines():
l.set_linewidth(1)
elif mpl_version >= 2:
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:
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.SetStatusText("")
self.Refresh()
def onFieldChanged(self, event):
view = self.getView()
view.clearCache()
self.delayedDraw()
def AppendFitToList(self, fitID):
sFit = Fit.getInstance()
fit = sFit.getFit(fitID)
if fit not in self.fits:
self.fits.append(fit)
self.fitList.fitList.update(self.fits)
self.draw()
def OnLeftDClick(self, event):
row, _ = self.fitList.fitList.HitTest(event.Position)
if row != -1:
try:
fit = self.fits[row]
except IndexError:
pass
else:
self.removeFits([fit])
def removeFits(self, fits):
toRemove = [f for f in fits if f in self.fits]
if not toRemove:
return
for fit in toRemove:
self.fits.remove(fit)
self.fitList.fitList.update(self.fits)
view = self.getView()
for fit in fits:
view.clearCache(key=fit.ID)
self.draw()
def getSelectedFits(self):
fits = []
for row in self.fitList.fitList.getSelectedRows():
try:
fit = self.fits[row]
except IndexError:
continue
fits.append(fit)
return fits
class FitList(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.mainSizer = wx.BoxSizer(wx.VERTICAL)
self.SetSizer(self.mainSizer)
self.fitList = FitDisplay(self)
self.mainSizer.Add(self.fitList, 1, wx.EXPAND)
fitToolTip = wx.ToolTip("Drag a fit into this list to graph it")
self.fitList.SetToolTip(fitToolTip)
class FitDisplay(gui.display.Display):
DEFAULT_COLS = ["Base Icon",
"Base Name"]
def __init__(self, parent):
gui.display.Display.__init__(self, parent)
class TargetList(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.mainSizer = wx.BoxSizer(wx.VERTICAL)
self.SetSizer(self.mainSizer)
self.targetList = TargetDisplay(self)
self.mainSizer.Add(self.targetList, 1, wx.EXPAND)
fitToolTip = wx.ToolTip("Drag a fit into this list to graph it")
self.targetList.SetToolTip(fitToolTip)
class TargetDisplay(gui.display.Display):
DEFAULT_COLS = ["Base Icon",
"Base Name"]
def __init__(self, parent):
gui.display.Display.__init__(self, parent)

View File

@@ -18,27 +18,4 @@
# =============================================================================
from collections import OrderedDict
from eos.graph.fitCapAmountVsTime import FitCapAmountVsTimeGraph as EosGraph
from gui.graph import Graph, XDef, YDef
class FitCapAmountVsTimeGraph(Graph):
name = 'Cap Amount vs Time'
def __init__(self):
super().__init__()
self.eosGraph = EosGraph()
@property
def xDef(self):
return XDef(inputDefault='0-300', inputLabel='Time (seconds)', inputIconID=1392, axisLabel='Time, s')
@property
def yDefs(self):
return OrderedDict([('capAmount', YDef(switchLabel='Cap amount', axisLabel='Cap amount, GJ', eosGraph='eosGraph'))])
FitCapAmountVsTimeGraph.register()
from .frame import GraphFrame

274
gui/graphFrame/frame.py Normal file
View File

@@ -0,0 +1,274 @@
# =============================================================================
# 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 gui.bitmap_loader import BitmapLoader
from gui.builtinGraphs.base import FitGraph
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_CHANGED, self.OnFitChanged)
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()
def OnFitChanged(self, event):
event.Skip()
self.getView().clearCache(fitID=event.fitID)
self.draw()
def OnGraphSwitched(self, event):
self.clearCache()
self.ctrlPanel.updateControls()
self.draw()
event.Skip()
def closeWindow(self):
from gui.builtinStatsViews.resistancesViewFull import EFFECTIVE_HP_TOGGLED
self.mainFrame.Unbind(GE.FIT_CHANGED, handler=self.OnFitChanged)
self.ctrlPanel.unbindExternalEvents()
self.Destroy()
def getView(self):
return self.graphSelection.GetClientData(self.graphSelection.GetSelection())
def clearCache(self, fitID=None):
self.getView().clearCache(fitID=fitID)
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)
self.subplot.plot(xs, ys)
if target is None:
legend.append('{} ({})'.format(fit.name, fit.ship.item.getShortName()))
else:
legend.append('{} ({}) vs {} ({})'.format(fit.name, fit.ship.item.getShortName(), target.name, target.ship.item.getShortName()))
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:
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()

98
gui/graphFrame/input.py Normal file
View File

@@ -0,0 +1,98 @@
# =============================================================================
# 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 re
import wx
def valToStr(val):
if val is None:
return ''
if int(val) == val:
val = int(val)
return str(val)
def strToFloat(val):
try:
return float(val)
except ValueError:
return None
class ConstantBox(wx.TextCtrl):
def __init__(self, parent, initial):
super().__init__(parent, wx.ID_ANY, style=0)
self.Bind(wx.EVT_TEXT, self.OnText)
self._storedValue = ''
self.ChangeValue(valToStr(initial))
def ChangeValue(self, value):
self._storedValue = value
super().ChangeValue(value)
def OnText(self, event):
currentValue = self.GetValue()
if currentValue == self._storedValue:
event.Skip()
return
if currentValue == '' or re.match('^\d*\.?\d*$', currentValue):
self._storedValue = currentValue
event.Skip()
else:
self.ChangeValue(self._storedValue)
def GetValueFloat(self):
return strToFloat(self.GetValue())
class RangeBox(wx.TextCtrl):
def __init__(self, parent, initRange):
super().__init__(parent, wx.ID_ANY, style=0)
self.Bind(wx.EVT_TEXT, self.OnText)
self._storedValue = ''
self.ChangeValue('{}-{}'.format(valToStr(min(initRange)), valToStr(max(initRange))))
def ChangeValue(self, value):
self._storedValue = value
super().ChangeValue(value)
def OnText(self, event):
currentValue = self.GetValue()
if currentValue == self._storedValue:
event.Skip()
return
if currentValue == '' or re.match('^\d*\.?\d*-?\d*\.?\d*$', currentValue):
self._storedValue = currentValue
event.Skip()
else:
self.ChangeValue(self._storedValue)
def GetValueRange(self):
parts = self.GetValue().split('-')
if len(parts) == 1:
val = strToFloat(parts[0])
return (val, val)
else:
return (strToFloat(parts[0]), strToFloat(parts[1]))

136
gui/graphFrame/lists.py Normal file
View File

@@ -0,0 +1,136 @@
# =============================================================================
# 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
import gui.globalEvents as GE
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 = []
fitToolTip = wx.ToolTip('Drag a fit into this list to graph it')
self.SetToolTip(fitToolTip)
self.contextMenu = wx.Menu()
removeItem = wx.MenuItem(self.contextMenu, 1, 'Remove Fit')
self.contextMenu.Append(removeItem)
self.contextMenu.Bind(wx.EVT_MENU, self.ContextMenuHandler, removeItem)
self.graphFrame.mainFrame.Bind(GE.FIT_REMOVED, self.OnFitRemoved)
self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick)
self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent)
self.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu)
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.removeFits(self.getSelectedFits())
event.Skip()
def OnLeftDClick(self, event):
row, _ = self.HitTest(event.Position)
if row != -1:
try:
fit = self.fits[row]
except IndexError:
pass
else:
self.removeFits([fit])
def OnContextMenu(self, event):
if self.getSelectedFits():
self.PopupMenu(self.contextMenu)
def ContextMenuHandler(self, event):
selectedMenuItem = event.GetId()
if selectedMenuItem == 1:
fits = self.getSelectedFits()
self.removeFits(fits)
def OnFitRemoved(self, event):
event.Skip()
fit = next((f for f in self.fits if f.ID == event.fitID), None)
if fit is not None:
self.removeFits([fit])
def getSelectedFits(self):
fits = []
for row in self.getSelectedRows():
try:
fit = self.fits[row]
except IndexError:
continue
fits.append(fit)
return fits
def removeFits(self, fits):
toRemove = [f for f in fits if f in self.fits]
if not toRemove:
return
for fit in toRemove:
self.fits.remove(fit)
self.update(self.fits)
for fit in fits:
self.graphFrame.clearCache(fitID=fit.ID)
self.graphFrame.draw()
def unbindExternalEvents(self):
self.graphFrame.mainFrame.Unbind(GE.FIT_REMOVED, handler=self.OnFitRemoved)
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.update(self.fits)
self.graphFrame.draw()
class FitList(BaseList):
def __init__(self, graphFrame, parent):
super().__init__(graphFrame, parent)
fit = Fit.getInstance().getFit(self.graphFrame.mainFrame.getActiveFit())
if fit is not None:
self.fits.append(fit)
self.update(self.fits)
class TargetList(BaseList):
def __init__(self, graphFrame, parent):
super().__init__(graphFrame, parent)
self.update(self.fits)

325
gui/graphFrame/panel.py Normal file
View File

@@ -0,0 +1,325 @@
# =============================================================================
# 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 service.fit import Fit
from .input import ConstantBox, RangeBox
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.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)
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=75, 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=75, 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)
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.drawTimer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.OnDrawTimer, self.drawTimer)
self._setVectorDefaults()
def updateControls(self, layout=True):
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)
# Target list
self.targetList.Show(view.hasTargets)
# Inputs
self._updateInputs(storeInputs=False)
if layout:
self.graphFrame.Layout()
self.graphFrame.UpdateWindowSize()
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)
if mainInput:
fieldTextBox = RangeBox(self, self._storedRanges.get((inputDef.handle, inputDef.unit), inputDef.defaultRange))
else:
fieldTextBox = ConstantBox(self, self._storedConsts.get((inputDef.handle, inputDef.unit), inputDef.defaultValue))
fieldTextBox.Bind(wx.EVT_TEXT, self.OnFieldChanged)
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)
fieldSizer.Add(fieldIcon, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 3)
fieldLabel = wx.StaticText(self, wx.ID_ANY, self.formatLabel(inputDef))
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 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.drawTimer.Stop()
self.drawTimer.Start(Fit.getInstance().serviceFittingOptions['marketSearchDelay'], True)
def OnDrawTimer(self, event):
event.Skip()
self.graphFrame.clearCache()
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 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.fits
def unbindExternalEvents(self):
self.fitList.unbindExternalEvents()
self.targetList.unbindExternalEvents()
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)

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

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

View File

@@ -962,9 +962,9 @@ class MainFrame(wx.Frame):
if not self.graphFrame:
self.graphFrame = GraphFrame(self)
if graphFrame.graphFrame_enabled:
if graphFrame.frame.graphFrame_enabled:
self.graphFrame.Show()
elif graphFrame.graphFrame_enabled:
elif graphFrame.frame.graphFrame_enabled:
self.graphFrame.SetFocus()
def openWXInspectTool(self, event):

View File

@@ -102,7 +102,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.graphFrame_enabled:
if not gui.graphFrame.frame.graphFrame_enabled:
self.Enable(self.graphFrameId, False)
self.ignoreRestrictionItem = fitMenu.Append(self.toggleIgnoreRestrictionID, "Disable Fitting Re&strictions")