aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.mypy.ini3
-rw-r--r--fietsboek/convert.py47
-rw-r--r--fietsboek/views/upload.py5
-rw-r--r--poetry.lock13
-rw-r--r--pyproject.toml1
5 files changed, 67 insertions, 2 deletions
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
@@ -626,6 +626,17 @@ docs = ["furo (>=2023.3.27)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1
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"
description = "GPX file parser and GPS track manipulation library"
@@ -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