From 40f766ad52b9b8ba4546a55339a9acd76c1c9896 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Mon, 15 May 2023 23:55:34 +0200 Subject: initial version of .fit import --- .mypy.ini | 3 +++ fietsboek/convert.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ fietsboek/views/upload.py | 5 ++++- poetry.lock | 13 ++++++++++++- pyproject.toml | 1 + 5 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 fietsboek/convert.py diff --git a/.mypy.ini b/.mypy.ini index f77b4ba..5c60978 100644 --- a/.mypy.ini +++ b/.mypy.ini @@ -7,6 +7,9 @@ exclude = fietsboek/updater/scripts/.+\.py [mypy-brotli.*] ignore_missing_imports = True +[mypy-fitparse.*] +ignore_missing_imports = True + [mypy-pyramid.*] ignore_missing_imports = True diff --git a/fietsboek/convert.py b/fietsboek/convert.py new file mode 100644 index 0000000..a73883d --- /dev/null +++ b/fietsboek/convert.py @@ -0,0 +1,47 @@ +"""Conversion functions to convert between various recording formats.""" +import fitparse +from gpxpy.gpx import GPX, GPXTrack, GPXTrackPoint, GPXTrackSegment + + +def semicircles_to_deg(circles: int) -> float: + """Convert semicircles coordinate to degree coordinate. + + :param circles: The coordinate value in semicircles. + :return: The coordinate in degrees. + """ + return circles * (180 / 2**31) + + +def from_fit(data: bytes) -> GPX: + """Reads a .fit as GPX data. + + This uses the fitparse_ library under the hood. + + .. _fitparse: https://pypi.org/project/fitparse/ + + :param data: The input bytes. + :return: The converted structure. + """ + fitfile = fitparse.FitFile(data) + points = [] + for record in fitfile.get_messages("record"): + values = record.get_values() + try: + point = GPXTrackPoint( + latitude=semicircles_to_deg(values["position_lat"]), + longitude=semicircles_to_deg(values["position_long"]), + elevation=values["altitude"], + time=values["timestamp"], + ) + except KeyError: + pass + else: + points.append(point) + track = GPXTrack() + track.segments = [GPXTrackSegment(points)] + gpx = GPX() + gpx.tracks = [track] + return gpx + + +__all__ = ["from_fit"] diff --git a/fietsboek/views/upload.py b/fietsboek/views/upload.py index 6fccdba..4fee76a 100644 --- a/fietsboek/views/upload.py +++ b/fietsboek/views/upload.py @@ -9,7 +9,7 @@ from pyramid.response import Response from pyramid.view import view_config from sqlalchemy import select -from .. import actions, models, transformers, util +from .. import actions, convert, models, transformers, util from ..models.track import TrackType, Visibility LOGGER = logging.getLogger(__name__) @@ -52,6 +52,9 @@ def do_upload(request): request.session.flash(request.localizer.translate(_("flash.no_file_selected"))) return HTTPFound(request.route_url("upload")) + if len(gpx) > 11 and gpx[9:12] == b"FIT": + gpx = convert.from_fit(gpx).to_xml().encode("utf-8") + # Before we do anything, we check if we can parse the file. # gpxpy might throw different exceptions, so we simply catch `Exception` # here - if we can't parse it, we don't care too much why at this point. diff --git a/poetry.lock b/poetry.lock index a73ca0a..2ce66c8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -625,6 +625,17 @@ files = [ docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.2.3)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"] +[[package]] +name = "fitparse" +version = "1.2.0" +description = "Python library to parse ANT/Garmin .FIT files" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "fitparse-1.2.0.tar.gz", hash = "sha256:2d691022452dea6dabad13cc6e017ca467fe8a3a895cd3ac67a50a7bb716b4a9"}, +] + [[package]] name = "gpxpy" version = "1.5.0" @@ -2318,4 +2329,4 @@ test = ["zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "24a8b1fc2e405bf9b4e61b85b5bac0890cc5344b15685fa8c3674d4aa7ad390a" +content-hash = "fbb50a44304a40cbcd0a59a5e908dd547b3619db66329b8b17a44d56f069f633" diff --git a/pyproject.toml b/pyproject.toml index dc8d9a0..9ce137c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ termcolor = "^2.1.1" filelock = "^3.8.2" brotli = "^1.0.9" click-option-group = "^0.5.5" +fitparse = "^1.2.0" [tool.poetry.group.docs] optional = true -- cgit v1.2.3 From 5db0d6b2f09beed78035cbcc87a6a537f6735673 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Wed, 31 May 2023 21:16:16 +0200 Subject: fit: skip records that have no position set --- fietsboek/convert.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fietsboek/convert.py b/fietsboek/convert.py index a73883d..bf0664a 100644 --- a/fietsboek/convert.py +++ b/fietsboek/convert.py @@ -2,6 +2,8 @@ import fitparse from gpxpy.gpx import GPX, GPXTrack, GPXTrackPoint, GPXTrackSegment +FIT_RECORD_FIELDS = ["position_lat", "position_long", "altitude", "timestamp"] + def semicircles_to_deg(circles: int) -> float: """Convert semicircles coordinate to degree coordinate. @@ -27,6 +29,8 @@ def from_fit(data: bytes) -> GPX: for record in fitfile.get_messages("record"): values = record.get_values() try: + if any(values[field] is None for field in FIT_RECORD_FIELDS): + continue point = GPXTrackPoint( latitude=semicircles_to_deg(values["position_lat"]), longitude=semicircles_to_deg(values["position_long"]), -- cgit v1.2.3