Merge branch 'dps_sim_graph' of github.com:pyfa-org/Pyfa into dps_sim_graph
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
223
gui/builtinGraphs/base.py
Normal 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 *
|
||||
109
gui/builtinGraphs/fitCapRegen.py
Normal file
109
gui/builtinGraphs/fitCapRegen.py
Normal 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()
|
||||
@@ -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()
|
||||
1
gui/builtinGraphs/fitDamageStats/__init__.py
Normal file
1
gui/builtinGraphs/fitDamageStats/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
import gui.builtinGraphs.fitDamageStats.graph # noqa: E402,F401
|
||||
248
gui/builtinGraphs/fitDamageStats/calc.py
Normal file
248
gui/builtinGraphs/fitDamageStats/calc.py
Normal 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)
|
||||
407
gui/builtinGraphs/fitDamageStats/graph.py
Normal file
407
gui/builtinGraphs/fitDamageStats/graph.py
Normal 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()
|
||||
254
gui/builtinGraphs/fitDamageStats/timeCache.py
Normal file
254
gui/builtinGraphs/fitDamageStats/timeCache.py
Normal 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]
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
76
gui/builtinGraphs/fitMobility.py
Normal file
76
gui/builtinGraphs/fitMobility.py
Normal 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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
116
gui/builtinGraphs/fitShieldRegen.py
Normal file
116
gui/builtinGraphs/fitShieldRegen.py
Normal 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()
|
||||
@@ -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()
|
||||
157
gui/builtinGraphs/fitWarpTime.py
Normal file
157
gui/builtinGraphs/fitWarpTime.py
Normal 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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
107
gui/graph.py
107
gui/graph.py
@@ -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 *
|
||||
@@ -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)
|
||||
@@ -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
274
gui/graphFrame/frame.py
Normal 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
98
gui/graphFrame/input.py
Normal 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
136
gui/graphFrame/lists.py
Normal 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
325
gui/graphFrame/panel.py
Normal 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
242
gui/graphFrame/vector.py
Normal 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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user