Permalink
Switch branches/tags
Nothing to show
Find file
Fetching contributors…
Cannot retrieve contributors at this time
720 lines (595 sloc) 28.4 KB
"""
Copyright 2013 Štěpán Bahník
This file is part of Carousel Maze Manager.
Carousel Maze Manager 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.
Carousel Maze Manager 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 Carousel Maze Manager. If not, see <http://www.gnu.org/licenses/>.
"""
from tkinter import *
from tkinter import ttk
from math import degrees, atan2, floor, ceil
from optionget import optionGet
import mode as m
def getGraphTypes():
"returns types of graphs used in Processor class as a list (with their respective methods)"
types = [["Angle", "AngleGraph(self)"],
["Distance from center", "DistanceFromCenterGraph(self)"],
["Speed", "SpeedGraph(self)"]
]
if m.mode == "OF":
types[1] = ["Proximity to side", "DistanceFromCenterGraph(self)"]
elif m.mode == "MWM":
types.insert(2, ["Distance from platform", "DistanceFromPlatformGraph(self)"])
elif m.mode == "RA":
types.insert(2, ["Distance from robot", "DistanceFromRobotGraph(self)"])
return types
class Graphs(Canvas):
"parent class for all 'wide' graphs in Explore page"
def __init__(self, parent, width = 600, height = 120, **kwargs):
super().__init__(parent)
self["width"] = width
self["height"] = height
self["background"] = "white"
self.height = height
self.width = width
self.drawnParameter = None
self.parent = parent
def changedTime(self, newTime):
"changes position of a time measure on a graph"
x = (newTime - self.minTime) * self.width / (self.maxTime - self.minTime)
if x < 2:
x = 2
self.coords("timeMeasure", (x, 0, x, self.height))
def CM_loaded(self, CM, minTime, maxTime, initTime):
"basic method called when a file is loaded"
# time measure
self.create_line((2, 0, 2, self.height), fill = "red", tags = "timeMeasure")
# maximum time in miliseconds
if maxTime == "max":
self.maxTime = CM.data[-1][1]
else:
self.maxTime = maxTime
if minTime == "min":
self.minTime = CM.data[0][1]
else:
self.minTime = minTime
# set time measure
self.changedTime(initTime)
self.drawParameter(cm = CM, parameter = self.drawnParameter)
def drawParameter(self, cm, parameter, purpose = "graph"):
"computes selected parameter to be drawn on top of the graph"
if self.drawnParameter and purpose == "graph":
self.delete("parameter")
self.drawnParameter = parameter
if parameter == "periodicity":
periodicity = cm.getPeriodicity(forGraph = True, time = self.maxTime / 60000,
startTime = self.minTime / 60000,
minSpeed = optionGet('MinSpeedPeriodicity',
10, ['int', 'float']),
skip = optionGet('SkipPeriodicity', 12, ['int']),
smooth = optionGet('SmoothPeriodicity', 2, ['int']),
minTime = optionGet('MinTimePeriodicity', 9,
['int', 'float', 'list']))
return self.drawPeriods(periodicity)
elif parameter == "immobility":
immobility = cm.getMaxTimeOfImmobility(forGraph = True,
time = self.maxTime / 60000,
startTime = self.minTime / 60000,
minSpeed = optionGet('MinSpeedMaxTimeImmobility',
10, ['int', 'float']),
skip = optionGet('SkipMaxTimeImmobility',
12, 'int'),
smooth = optionGet('SmoothMaxTimeImmobility',
2, 'int'))
return self.drawPeriods(immobility)
elif parameter == "mobility":
immobility = cm.getMaxTimeOfImmobility(forGraph = True, time = self.maxTime / 60000,
startTime = self.minTime / 60000,
minSpeed = optionGet('MinSpeedPercentMobility',
5, ['int', 'float']),
skip = optionGet('SkipPercentMobility', 12,
'int'),
smooth = optionGet('SmoothPercentMobility', 2,
'int'))
mobility = []
t0 = self.minTime
for times in immobility:
t1 = times[0]
mobility.append((t0, t1))
t0 = times[1]
return self.drawPeriods(mobility)
elif parameter == "thigmotaxis":
percentSize = optionGet("ThigmotaxisPercentSize", 20, ["int", "float"])
start = cm.findStart(self.minTime / 60000)
periods = []
outside = False
t0 = self.minTime
border = cm.radius * (1 - (percentSize / 100))
if m.mode == "OF":
x0, x1 = cm.radius - cm.centerX, cm.centerX + cm.radius
y0, y1 = cm.radius - cm.centerY, cm.centerY + cm.radius
def distance(line):
return cm.radius - min([line[2] - x0, x1 - line[2], line[3] - y0, y1 - line[3]])
else:
def distance(line):
x, y = line[cm.indices]
return ((x - cm.centerX)**2 + (y - cm.centerY)**2)**0.5
for content in cm.data[start:]:
if content[1] <= self.maxTime:
if distance(content) >= border:
if not outside:
t0 = content[1]
outside = True
elif outside:
outside = False
periods.append((t0, content[1]))
else:
break
if outside:
periods.append((t0, self.maxTime))
return self.drawPeriods(periods)
elif parameter == "shocks":
shocks = []
prev = 0
for content in cm.data:
if content[5] != 2 and prev != 2:
continue
elif content[5] != 2 and prev == 2:
if content[5] != 5:
prev = content[5]
elif content[5] == 2 and prev == 2:
continue
elif content[5] == 2 and prev != 2:
shocks.append(content[1])
prev = 2
return self.drawTimes(shocks)
elif parameter == "entrances":
entrances = []
prev = 0
for content in cm.data:
if content[5] != 2 and prev != 2:
continue
elif content[5] != 2 and prev == 2:
if content[5] == 0:
prev = 0
elif content[5] == 2 and prev == 2:
continue
elif content[5] == 2 and prev != 2:
entrances.append(content[1])
prev = 2
return self.drawTimes(entrances)
elif parameter == "passes":
passes = []
prev = 0
for content in cm.data:
if content[5] == 0 and prev != 2:
continue
elif content[5] == 0 and prev == 2:
if content[5] == 0:
prev = 0
elif content[5] > 0 and content[5] != 5 and prev == 2:
continue
elif content[5] > 0 and content[5] != 5 and prev != 2:
passes.append(content[1])
prev = 2
return self.drawTimes(passes)
elif parameter == "bad points":
if cm.interpolated:
sortd = sorted(cm.interpolated)
wrongs = []
prev = sortd[0]
start = sortd[0]
for wrong in sortd[1:]:
if wrong != prev + 1:
wrongs.append((start, prev))
start = wrong
prev = wrong
wrongs.append((start, prev))
bps = [(cm.data[wrong[0] - 1][1], cm.data[wrong[1] - 1][1]) for wrong in wrongs]
return self.drawPeriods(bps)
elif parameter == "strategies":
rows = optionGet('rowsStrategies', 25, 'int')
minSpeed = optionGet('minSpeedStrategies', 10, ['int', 'float'])
minAngle = optionGet('minAngleStrategies', 15, ['int', 'float'])
borderPercentSize = optionGet('borderPercentSizeStrategies', 20, ['int', 'float'])
strategies = cm.getStrategies(time = self.maxTime / 60000,
startTime = self.minTime / 60000,
rows = rows, minSpeed = minSpeed, minAngle = minAngle,
borderPercentSize = borderPercentSize, summary = False)
colors = {"counterclockwise": "green",
"clockwise": "dodger blue",
"no_reaction": "deep pink",
"immobile": "white",
"reaction_counterclockwise": "red",
"reaction_clockwise": "goldenrod1",
"center": "wheat4"}
if purpose == "graph":
for strategy, periods in strategies.items():
self.drawPeriods(periods, color = colors[strategy], width = 240)
self.lower("parameter")
else:
for strategy, periods in strategies.items():
if strategy == "immobile":
continue
self.drawPeriods(periods, color = colors[strategy].replace(" ", "").rstrip("14"),
width = 240, toReturn = False)
return self.returnPeriods()
def drawPeriods(self, periods, color = "red", width = 3):
"draws selected parameter on top of the graph"
if not periods:
return
timeSpread = (self.maxTime - self.minTime)
for period in periods:
if period[0] > self.minTime and period[1] < self.maxTime:
begin = period[0]
end = period[1]
elif self.minTime < period[1] < self.maxTime:
begin = self.minTime
end = period[1]
elif self.minTime < period[0] < self.maxTime:
begin = period[0]
end = self.maxTime
else:
continue
self.create_line(((begin - self.minTime) * self.width / timeSpread,
0.03 * self.height,
(end - self.minTime) * self.width / timeSpread,
0.03 * self.height),
fill = color, width = width, tags = "parameter")
def drawTimes(self, times):
"draws selected parameter on top of the graph"
if not times:
return
timeSpread = (self.maxTime - self.minTime)
for time in times:
if self.minTime < time < self.maxTime:
x = (time - self.minTime) * self.width / timeSpread
self.create_line((x, 0.01 * self.height, x, 0.07 * self.height),
fill = "red", width = 1, tags = "parameter")
def drawGraph(self, maxY, valueList):
"""draws lines on a canvas based on maxY and valueList parameters
maxY parameter sets maximum value at y-axis
valueList parameter must be a list containing successive values depicted in the graph
"""
maxX = len(valueList)
for i in range(maxX - 1):
x0 = (i / (maxX - 1)) * self.width
x1 = ((i + 1) / (maxX - 1)) * self.width
y0 = (1 - (valueList[i] / maxY)) * self.height
y1 = (1 - (valueList[i + 1] / maxY)) * self.height
self.create_line((x0, y0, x1, y1))
self.lift("timeMeasure")
class SvgGraph():
"represents graph to be saved in .svg file"
def __init__(self, parent, cm, width = 600, height = 120):
self.height = height
self.width = width
self.drawnParameter = None
self.parent = parent
self.periodText = ""
self.__class__.__bases__ = (self.__class__.__bases__[1], self.__class__.__bases__[0])
def __del__(self):
self.__class__.__bases__ = (self.__class__.__bases__[1], self.__class__.__bases__[0])
def saveGraph(self, cm):
"returns information about graph for saving in .svg file"
self.maxTime = eval(self.parent.timeFrame.timeVar.get()) * 60000
self.minTime = eval(self.parent.timeFrame.startTimeVar.get()) * 60000
self.compute(cm)
self.writeFurtherText()
return self.points, self.maxY, self.furtherText
def drawPeriods(self, periods, color = "red", width = 8, toReturn = True):
"draws selected parameter on top of the graph"
if not periods:
return ""
timeSpread = (self.maxTime - self.minTime)
text = ""
line = '<line x1="{0}" y1="{1}" x2="{2}" y2="{1}" stroke="{3}" stroke-width="{4}"/>\n'
for period in periods:
if period[0] > self.minTime and period[1] < self.maxTime:
begin = period[0]
end = period[1]
elif self.minTime < period[1] < self.maxTime:
begin = self.minTime
end = period[1]
elif self.minTime < period[0] < self.maxTime:
begin = period[0]
end = self.maxTime
else:
continue
text += line.format((begin - self.minTime) * self.width / timeSpread,
(width/480) * self.height,
(end - self.minTime) * self.width / timeSpread,
color, width/2)
self.periodText += text
if toReturn:
return self.periodText
def returnPeriods(self):
return self.periodText
def drawTimes(self, times):
"draws selected parameter on top of the graph"
if not times:
return ""
timeSpread = (self.maxTime - self.minTime)
text = ""
for time in times:
if self.minTime < time < self.maxTime:
x = (time - self.minTime) * self.width / timeSpread
text += '<line x1="{0}" y1="0" x2="{0}" y2="5" stroke="red"/>\n'.format(x)
return text
class SpeedGraph(Graphs, SvgGraph):
"graph depicting speed during the session"
def __init__(self, parent, cm = None, purpose = "graph", width = 600):
self.primaryParent = Graphs if purpose == "graph" else SvgGraph
self.primaryParent.__init__(self, parent, cm = cm, width = width)
def writeFurtherText(self):
"makes text for svg file representing horizontal lines for every 10cm/s"
self.furtherText = ""
for y in range(1, floor(self.maxY / 10)):
text = '<line stroke="lightgray" stroke-width="0.5" ' +\
'x1="0" y1="{0}" x2="{1}" y2="{0}"/>\n'.format(y * 10 * 120 / self.maxY,
self.width)
self.furtherText += text
def compute(self, cm, skip = 12, smooth = 2):
"""computes speeds
parameter skip controls how many lines should be skipped when computing speed
parameter smooth controls how many speed data points should be averaged
e.g. when skip = 12 and smooth = 2, speed is computed as an average of two speeds
computed from lines separated by 11 lines
"""
indices = cm.indices
resolution = cm.trackerResolution
# saving speed between every 'skip' data point ... in centimeters per second
start = cm.findStart(self.minTime / 60000)
self.speed = []
x0, y0 = cm.data[start][indices]
t0 = cm.data[start][1]
for line in cm.data[(start + skip)::skip]:
x1, y1 = line[indices]
t1 = line[1]
speed = ((((x1 - x0)**2 + (y1 - y0)**2)**0.5) / resolution) / ((t1 - t0) / 1000)
self.speed.append(speed)
if t1 >= self.maxTime:
break
x0, y0, t0 = x1, y1, t1
# averaging speed across 'smooth' speed data points
self.points = []
avgSpeed = 0
for smoothCounter, point in enumerate(self.speed, 1):
avgSpeed += point
if smoothCounter % smooth == 0:
self.points.append((avgSpeed / smooth))
avgSpeed = 0
else:
if avgSpeed != 0:
self.points.append((avgSpeed / (smoothCounter % smooth)))
# computing maximum speed depicted on y-axis
self.maxY = ceil(max(self.points) / 10) * 10
def CM_loaded(self, cm, initTime = 0, minTime = 0, maxTime = "max"):
"""creates graph when CM file is loaded
parameter initTime is the time of the player when the graph is initialized
"""
super().CM_loaded(cm, minTime, maxTime, initTime)
self.compute(cm)
for y in range(1, floor(self.maxY / 10)):
self.create_line((0, y*10 * self.height / self.maxY, self.width,
y*10 * self.height / self.maxY), fill = "gray86")
self.drawGraph(maxY = self.maxY, valueList = self.points)
def addYticks(self):
at = []
labels = []
for i in range((self.maxY // 20) + 1):
at.append(i*20 / self.maxY)
labels.append(str(i*20))
return at, labels
def getYlabel(self):
return "Speed [m/s]"
class DistanceFromCenterGraph(Graphs, SvgGraph):
"graph depicting distance from center of arena during the session"
def __init__(self, parent, cm = None, purpose = "graph", width = 600):
self.primaryParent = Graphs if purpose == "graph" else SvgGraph
self.primaryParent.__init__(self, parent, cm = cm, width = width)
def writeFurtherText(self):
"makes text for svg file containing info about line representing border of the arena"
y = ((self.maxY - self.radius) / self.maxY) * 120
self.furtherText = '<line stroke="gray" stroke-width="0.5" x1="0" ' +\
'y1="{0}" x2="{1}" y2="{0}"/>\n'.format(y, self.width)
def compute(self, cm, smooth = 10):
"""computes distances from center of the graph
parameter smooth controls how many data points should be averaged
"""
start = cm.findStart(self.minTime / 60000)
self.radius = cm.radius
self.resolution = cm.trackerResolution
Cx, Cy = cm.centerX, cm.centerY
if m.mode == "RA":
dists = [((line[7] - Cx)**2 + (line[8] - Cy)**2)**0.5 for line in cm.data[start:] if
line[1] <= self.maxTime]
elif m.mode != "OF":
dists = [((line[2] - Cx)**2 + (line[3] - Cy)**2)**0.5 for line in cm.data[start:] if
line[1] <= self.maxTime]
else:
r = self.radius
lb, tb, rb, bb = Cx - r, Cy + r, Cx + r, Cy - r
dists = [r - min([line[2]-lb, rb-line[2], line[3]-bb, tb-line[3]]) for line in
cm.data[start:] if line[1] <= self.maxTime]
self.maxY = self.radius + 10
self.points = dists
def CM_loaded(self, cm, initTime = 0, minTime = 0, maxTime = "max"):
"""creates graph when CM file is loaded
parameter initTime is the time of the player when the graph is initialized
"""
super().CM_loaded(cm, minTime, maxTime, initTime)
self.compute(cm)
self.create_line((0, 10, self.width, 10), fill = "grey")
self.drawGraph(maxY = self.maxY, valueList = self.points)
def addYticks(self):
at = []
labels = []
maxYcm = self.maxY / self.resolution
for i in range((int(maxYcm) // 20) + 1):
at.append(i*20 / maxYcm)
labels.append(str(i*20))
return at, labels
def getYlabel(self):
return "Distance [cm]"
class DistanceFromPlatformGraph(Graphs, SvgGraph):
"graph depicting distance from platform during the MWM session"
def __init__(self, parent, cm = None, purpose = "graph", width = 600):
self.primaryParent = Graphs if purpose == "graph" else SvgGraph
self.primaryParent.__init__(self, parent, cm = cm, width = width)
def writeFurtherText(self):
"makes text for svg file containing info about line representing border of the platform"
self.furtherText = '<line stroke="gray" stroke-width="0.5" x1="0" ' +\
'y1="{0}" x2="{1}" y2="{0}"/>\n'.format(self.platformRadius, self.width)
def compute(self, cm, smooth = 10):
"""computes distances from center of the graph
parameter smooth controls how many data points should be averaged
"""
start = cm.findStart(self.minTime / 60000)
Cx, Cy = cm.centerX, cm.centerY
Px, Py = cm.platformX, cm.platformY
self.radius = cm.platformRadius
self.points = [((line[2] - Px)**2 + (line[3] - Py)**2)**0.5 for line in cm.data[start:] if
line[1] <= self.maxTime]
self.maxY = cm.radius + ((Px - Cx)**2 + (Py - Cy)**2)**0.5
def CM_loaded(self, cm, initTime = 0, minTime = 0, maxTime = "max"):
"""creates graph when CM file is loaded
parameter initTime is the time of the player when the graph is initialized
"""
super().CM_loaded(cm, minTime, maxTime, initTime)
self.compute(cm)
y = self.height * (1 - self.radius/self.maxY)
color = "grey" if m.mode == "MWM" else "red"
self.create_line((0, y, self.width, y), fill = color)
self.drawGraph(maxY = self.maxY, valueList = self.points)
class DistanceFromRobotGraph(DistanceFromPlatformGraph):
"graph depicting distance from the robot during the RA session"
def __init__(self, parent, cm = None, purpose = "graph", width = 600):
self.primaryParent = Graphs if purpose == "graph" else SvgGraph
self.primaryParent.__init__(self, parent, cm = cm, width = width)
def compute(self, cm, smooth = 10):
start = cm.findStart(self.minTime / 60000)
self.radius = cm.sectorRadius
self.points = [((line[7] - line[2])**2 + (line[8] - line[3])**2)**0.5
for line in cm.data[start:] if line[1] <= self.maxTime]
self.maxY = cm.radius * 2
class AngleGraph(Graphs, SvgGraph):
"graph depicting angle relative to the center of shock zone during the session"
def __init__(self, parent, cm = None, purpose = "graph", width = 600):
self.primaryParent = Graphs if purpose == "graph" else SvgGraph
self.primaryParent.__init__(self, parent, cm = cm, width = width)
self.maxTime = eval(self.parent.timeFrame.timeVar.get()) * 60000
self.minTime = eval(self.parent.timeFrame.startTimeVar.get()) * 60000
def compute(self, cm):
"computes angles of position relative to center"
start = cm.findStart(self.minTime / 60000)
Cx, Cy = cm.centerX, cm.centerY
CA = cm.centerAngle
# saving angles to a list
self.angles = []
nxt = False
prev = 180
self.crosses = 0
for line in cm.data[start:]:
if line[1] > self.maxTime:
break
else:
if m.mode == "RA":
angle = (degrees(atan2(line[8] - line[3],
line[2] - line[7] + 0.000001)) + 540) % 360
else:
angle = (degrees(atan2(Cy - line[3], line[2] - Cx + 0.000001)) + 720 - CA) % 360
if prev > 270 and angle < 90:
self.angles.append(360)
self.angles.append(0)
self.crosses += 1
elif prev < 90 and angle > 270:
self.angles.append(0)
self.angles.append(360)
self.crosses += 1
else:
self.angles.append(angle)
prev = angle
def CM_loaded(self, cm, initTime = 0, minTime = 0, maxTime = "max"):
"""creates graph when CM file is loaded
parameter initTime is the time of the player when the graph is initialized
"""
super().CM_loaded(cm, minTime, maxTime, initTime)
self.compute(cm)
# drawing lines representing the sector
if "CM" in m.mode:
wid = cm.width
y1 = ((360 - (wid / 2)) / 360) * self.height
y2 = ((wid / 2) / 360) * self.height
self.create_line((0, y1, self.width, y1), fill = "red")
self.create_line((0, y2, self.width, y2), fill = "red")
# drawing the graph
maxX = len(self.angles) - self.crosses
i = 0
prev = self.angles[0]
for angle in self.angles[1:]:
x0 = (i / (maxX - 1)) * self.width
x1 = ((i + 1) / (maxX - 1)) * self.width
y0 = (1 - (prev / 360)) * self.height
y1 = (1 - (angle / 360)) * self.height
if (prev == 0 and angle == 360) or (prev == 360 and angle == 0):
prev = angle
continue
prev = angle
self.create_line((x0, y0, x1, y1))
i += 1
self.lift("timeMeasure")
def saveGraph(self, cm):
"returns information about graph for saving to .svg file"
self.compute(cm)
wid = cm.width
size = (self.width, self.height)
maxX = len(self.angles) - self.crosses
text = ""
for i in (360 - (wid / 2), wid / 2):
text += '<line stroke="red" stroke-width="0.5" ' +\
'x1="0" y1="{1}" x2="{0}" y2="{1}"/>\n'.format(size[0], i * size[1] / 360)
points = []
width = size[0]
height = size[1]
i = 0
prev = self.angles[0]
x0 = (i / (maxX - 1)) * width
y0 = (1 - (prev / 360)) * height
points.append((x0, y0))
for angle in self.angles[1:]:
x0 = (i / (maxX - 1)) * width
x1 = ((i + 1) / (maxX - 1)) * width
y0 = (1 - (prev / 360)) * height
y1 = (1 - (angle / 360)) * height
if (prev == 0 and angle == 360) or (prev == 360 and angle == 0):
prev = angle
text += self._addLine(points)
points = []
points.append((x1, y1))
continue
prev = angle
points.append((x1, y1))
i += 1
text += self._addLine(points)
return None, None, text
def _addLine(self, points):
"returns line in a svg format joining points given as an argument"
text = '<polyline points="'
for pair in points:
text += ",".join(map(str, pair)) + " "
text += '" style = "fill:none;stroke:black"/>\n'
return text
def addYticks(self):
at = [0, 0.5, 1]
labels = ["0", "180", "360"]
return at, labels
def getYlabel(self):
return "Angle [deg]"