backend: initial commit
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
"""GPX file parser."""
|
||||
|
||||
from datetime import timezone
|
||||
from pathlib import Path
|
||||
|
||||
import gpxpy
|
||||
import gpxpy.gpx
|
||||
|
||||
from bincio.extract.models import DataPoint, ParsedActivity
|
||||
from bincio.extract.parsers.base import BaseParser
|
||||
from bincio.extract.sport import normalise_sport
|
||||
|
||||
# Known GPX extension namespaces
|
||||
_NS_GARMIN = "http://www.garmin.com/xmlschemas/TrackPointExtension/v1"
|
||||
_NS_GARMIN_V2 = "http://www.garmin.com/xmlschemas/TrackPointExtension/v2"
|
||||
|
||||
|
||||
class GpxParser(BaseParser):
|
||||
def parse(self, path: Path, raw_bytes: bytes) -> ParsedActivity:
|
||||
gpx = gpxpy.parse(raw_bytes.decode("utf-8", errors="replace"))
|
||||
|
||||
points: list[DataPoint] = []
|
||||
for track in gpx.tracks:
|
||||
for segment in track.segments:
|
||||
for pt in segment.points:
|
||||
if pt.time is None:
|
||||
continue
|
||||
ts = pt.time
|
||||
if ts.tzinfo is None:
|
||||
ts = ts.replace(tzinfo=timezone.utc)
|
||||
|
||||
dp = DataPoint(
|
||||
timestamp=ts,
|
||||
lat=pt.latitude,
|
||||
lon=pt.longitude,
|
||||
elevation_m=pt.elevation,
|
||||
)
|
||||
_apply_extensions(pt, dp)
|
||||
points.append(dp)
|
||||
|
||||
if not points:
|
||||
raise ValueError(f"No trackpoints found in {path.name}")
|
||||
|
||||
sport = normalise_sport(
|
||||
(gpx.tracks[0].type if gpx.tracks else None) or "cycling"
|
||||
)
|
||||
started_at = points[0].timestamp
|
||||
|
||||
return ParsedActivity(
|
||||
points=points,
|
||||
sport=sport,
|
||||
started_at=started_at,
|
||||
source_file=path.name,
|
||||
source_hash="", # set by factory
|
||||
)
|
||||
|
||||
|
||||
def _apply_extensions(pt: gpxpy.gpx.GPXTrackPoint, dp: DataPoint) -> None:
|
||||
"""Extract HR, cadence, temperature from Garmin TrackPointExtension."""
|
||||
if pt.extensions is None:
|
||||
return
|
||||
for ext in pt.extensions:
|
||||
ns = _strip_ns(ext.tag)
|
||||
if ns == "TrackPointExtension":
|
||||
for child in ext:
|
||||
tag = _strip_ns(child.tag)
|
||||
val = child.text
|
||||
if val is None:
|
||||
continue
|
||||
if tag == "hr":
|
||||
dp.hr_bpm = int(float(val))
|
||||
elif tag == "cad":
|
||||
dp.cadence_rpm = int(float(val))
|
||||
elif tag == "atemp":
|
||||
dp.temperature_c = float(val)
|
||||
elif tag == "speed":
|
||||
dp.speed_kmh = float(val) * 3.6 # m/s → km/h
|
||||
|
||||
|
||||
def _strip_ns(tag: str) -> str:
|
||||
"""'{namespace}localname' → 'localname'."""
|
||||
return tag.split("}")[-1] if "}" in tag else tag
|
||||
Reference in New Issue
Block a user