GARMINの.fitデータの標高と勾配をQGraphicsViewに表示するカスタムQGraphicsItemを作ってみた
2022/01/22 categories:PyQt5| tags:PyQt5|Python|Garmin|
GARMINの.fitデータをQGraphicsViewに表示する為のカスタムQGraphicsItemを作ってみました。
表示する内容
GARMINの.fitデータを動画に変換するツールrev1に走行ログの標高と勾配を表示するために、QGraphicsItemを作ってみました。表示内容は以下のような内容にします。
- アウトラインがある線の表示
- 線の頂点にアウトラインがある円を表示
- 勾配を文字列で表示
単純な線の表示はQGraphicsPathItemを使うことで実現可能ですが、その線にアウトラインを付けて表示したり、線の頂点に円を表支するとなると一つのアイテムでは表示できないためカスタムアイテムとして実装してみました。
表示例
ソースコード
# -*- coding: utf-8 -*-
import fitdecode
import sys
import numpy as np
from PyQt5 import QtWidgets, QtCore, QtGui
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.resize(700, 200)
self.slider = QtWidgets.QSlider()
self.slider.setOrientation(QtCore.Qt.Horizontal)
self.graphics_view = GraphicsView()
self.setCentralWidget( QtWidgets.QWidget() )
self.centralWidget().setLayout( QtWidgets.QVBoxLayout() )
self.centralWidget().layout().addWidget(self.graphics_view)
self.centralWidget().layout().addWidget(self.slider)
self.scene = QtWidgets.QGraphicsScene(0, 0, 650, 130)
self.graphics_view.setScene(self.scene)
self.fit = Fit('test_fit_data.fit')
self.elevation = Elevation(self.fit, 650, 130)
self.slider.setMaximum(self.fit.timestamp.size-1)
rect = QtWidgets.QGraphicsRectItem(0, 0, 650, 130)
rect.setBrush(QtCore.Qt.green)
self.scene.addItem(rect)
self.scene.addItem(self.elevation)
self.slider.valueChanged.connect(self.elevation.update_item)
class Elevation(QtWidgets.QGraphicsItem):
def __init__(self, fit, x_size, y_size):
super().__init__()
self.fit = fit
self.index = 0
self.y_arr = fit.enhanced_altitude.max() - fit.enhanced_altitude
self.x_arr = fit.distance
self.polygon = QtGui.QPolygonF()
self.ellipse_size = 20
self.ellipse_brush = QtGui.QBrush( QtGui.QColor('#FF0000') )
self.ellipse_pen = QtGui.QPen( QtGui.QBrush( QtGui.QColor('#000000') ), 5 )
self.painter_path = QtGui.QPainterPath()
self.painter_path.addPolygon(self.polygon)
self.outline_pen = QtGui.QPen( QtGui.QBrush( QtGui.QColor('#000000') ), 20 )
self.inline_pen = QtGui.QPen( QtGui.QBrush( QtGui.QColor('#FFFFFF') ), 8 )
self.gradient = QtWidgets.QGraphicsTextItem()
self.change_size(x_size, y_size)
def change_size(self, x_size, y_size):
x_range, y_range = max(self.x_arr) - min(self.x_arr), max(self.y_arr) - min(self.y_arr)
ellipse_r = self.ellipse_size/2 + self.ellipse_pen.width()/2
x_scale = (x_size - 2 * ellipse_r) / x_range
y_scale = (y_size - 2 * ellipse_r) / y_range
x_arr, y_arr = self.x_arr * x_scale, self.y_arr * y_scale
x_arr, y_arr = x_arr - min(x_arr) + ellipse_r, y_arr - min(y_arr) + ellipse_r
self.polygon.clear()
for x, y in zip(x_arr, y_arr):
self.polygon.append( QtCore.QPointF(x, y) )
self.painter_path.clear()
self.painter_path.addPolygon(self.polygon)
def boundingRect(self):
x1 = self.painter_path.boundingRect().topLeft().x()
y1 = self.painter_path.boundingRect().topLeft().y()
x2 = self.painter_path.boundingRect().bottomRight().x()
y2 = self.painter_path.boundingRect().bottomRight().y()
r = self.ellipse_size/2 + self.ellipse_pen.width()
w = x2 - x1 + r * 2
h = y2 - y1 + r * 2
rect = QtCore.QRectF(x1 - r, y1 - r, w, h)
return rect
def paint(self, painter, option, widget=None):
painter.setPen(self.outline_pen)
painter.drawPath(self.painter_path)
painter.setPen(self.inline_pen)
painter.drawPath(self.painter_path)
x, y, s = self.polygon.at(self.index).x(), self.polygon.at(self.index).y(), self.ellipse_size
painter.setBrush(self.ellipse_brush)
painter.setPen(self.ellipse_pen)
painter.drawEllipse(int(x - s/2), int(y - s/2), int(s), int(s))
font = painter.font()
font.setPointSize(12)
painter.setFont(font)
text = 'gradient : {:>4.1f} %'.format( round(self.fit.gradient[self.index], 2) )
painter.drawText(20, 20, text)
text = 'altitude : {:>4.1f} m'.format( round(self.fit.enhanced_altitude[self.index], 2) )
painter.drawText(20, 40, text)
text = 'distance : {:>4.1f} km'.format( round(self.fit.distance[self.index], 2) )
painter.drawText(20, 60, text)
def update_item(self, index):
self.index = index
self.update()
class GraphicsView(QtWidgets.QGraphicsView):
mouseLeftReleased = QtCore.pyqtSignal(QtCore.QRectF)
def __init__(self, parent=None):
super(GraphicsView, self).__init__(parent)
self.numScheduledScalings = 0
self.rectF = QtCore.QRectF(0.0, 0.0, 0.0, 0.0)
self.setBackgroundBrush(QtGui.QColor(210, 210, 210))
def animation_finished(self):
if self.numScheduledScalings > 0:
self.numScheduledScalings -= 1
else:
self.numScheduledScalings += 1
def wheelEvent(self, event):
numDegrees = event.angleDelta().y() / 8
numSteps = numDegrees / 15
self.numScheduledScalings += numSteps
if self.numScheduledScalings * numSteps < 0:
self.numScheduledScalings = numSteps
anim = QtCore.QTimeLine(350, self)
anim.setUpdateInterval(20)
anim.valueChanged.connect(self.scaling_time)
anim.finished.connect(self.animation_finished)
anim.start()
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.MidButton:
self.setDragMode(QtWidgets.QGraphicsView.ScrollHandDrag)
event = QtGui.QMouseEvent(
QtCore.QEvent.GraphicsSceneDragMove,
event.pos(),
QtCore.Qt.MouseButton.LeftButton,
QtCore.Qt.MouseButton.LeftButton,
QtCore.Qt.KeyboardModifier.NoModifier
)
elif event.button() == QtCore.Qt.LeftButton:
self.setDragMode(QtWidgets.QGraphicsView.RubberBandDrag)
point = self.mapToScene( event.pos() )
self.rectF = QtCore.QRectF(point, point)
super().mousePressEvent(event)
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
self.setDragMode(QtWidgets.QGraphicsView.NoDrag)
if event.button() == QtCore.Qt.LeftButton:
p2 = self.mapToScene( event.pos() )
self.rectF.setBottomRight(p2)
self.mouseLeftReleased.emit(self.rectF)
def scaling_time(self, x):
factor = 1.0 + float(self.numScheduledScalings) / 300.0
self.scale(factor, factor)
class Fit():
def __init__(self, filepath):
fit_data = {
'timestamp':[], 'cadence':[], 'temperature':[], 'enhanced_speed':[], 'position_lat':[],
'position_long':[], 'enhanced_altitude':[], 'distance':[], 'heart_rate':[]
}
self.units = {}
for frame in fitdecode.FitReader( filepath, processor=fitdecode.StandardUnitsDataProcessor() ):
if not isinstance(frame, fitdecode.FitDataMessage):
continue
if not frame.name == 'record':
continue
for key in fit_data:
if key[:2] == '__':
continue
if frame.has_field(key):
fit_data[key].append( frame.get_field(key).value )
self.units[key] = frame.get_field(key).units
else:
if len( fit_data[key] ) > 0:
fit_data[key].append( fit_data[key][-1] )
else:
fit_data[key].append( 0 )
self.timestamp = np.array( fit_data['timestamp'] )
self.cadence = np.array( fit_data['cadence'] )
self.temperature = np.array( fit_data['temperature'] )
self.enhanced_speed = np.array( fit_data['enhanced_speed'] )
self.position_lat = np.array( fit_data['position_lat'] )
self.position_long = np.array( fit_data['position_long'] )
self.enhanced_altitude = np.array( fit_data['enhanced_altitude'] )
self.distance = np.array( fit_data['distance'] )
self.heart_rate = np.array( fit_data['heart_rate'] )
if self.units['distance'] == 'km':
d = self.distance * 1000.0
elif self.units['distance'] == 'm':
d = self.distance
d_def = ( np.array( list(d[1:]) + [d[-1]] ) - d )
if self.units['enhanced_altitude'] == 'km':
d = self.enhanced_altitude * 1000.0
elif self.units['enhanced_altitude'] == 'm':
d = self.enhanced_altitude
a = self.enhanced_altitude
a_def = ( np.array( list(a[1:]) + [a[-1]] ) - a )
self.gradient = a_def / d_def * 100
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()