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()

Share post

Related Posts

Comments

comments powered by Disqus