from collections import namedtuple
from datetime import datetime, timedelta
from xml.etree import ElementTree as ET
import gpxpy
from . import parse
GPSData = namedtuple("GPSData",
[
"description",
"timestamp",
"precision",
"fix",
"latitude",
"longitude",
"altitude",
"speed_2d",
"speed_3d",
"units",
"npoints"
])
[docs]
def parse_gps_block(gps_block):
"""Turn GPS data blocks into `GPSData` objects
Supports both GPS5 (Hero 5-10) and GPS9 (Hero 11+) streams
Parameters
----------
gps_block: list of KVLItem
A list of KVLItem corresponding to a GPS data block.
Returns
-------
gps_data: GPSData
A GPSData object holding the GPS information of a block.
"""
block_dict = {
s.key: s for s in gps_block
}
# Hero 11-13 support: use GPS9 if available (10Hz), fall back to GPS5
gps_key = "GPS9" if "GPS9" in block_dict else "GPS5"
if gps_key == "GPS9":
# GPS9: complex structure with 9 fields (Hero 11+)
# Extract first 5 fields (lat, lon, alt, speed_2d, speed_3d) for compatibility
gps_values = block_dict["GPS9"].value
# Handle both array and single-value cases
if hasattr(gps_values, 'shape') and len(gps_values.shape) > 1:
# Multi-sample case: use first 5 columns
gps_data = gps_values[:, :5] * 1.0 / block_dict["SCAL"].value[:5]
else:
# Single sample case
gps_data = gps_values[:5] * 1.0 / block_dict["SCAL"].value[:5]
else:
# GPS5: traditional 5-field structure (Hero 5-10)
gps_data = block_dict["GPS5"].value * 1.0 / block_dict["SCAL"].value
latitude, longitude, altitude, speed_2d, speed_3d = gps_data.T
return GPSData(
description=block_dict["STNM"].value,
timestamp=block_dict["GPSU"].value,
precision=block_dict["GPSP"].value / 100.,
fix=block_dict["GPSF"].value,
latitude=latitude,
longitude=longitude,
altitude=altitude,
speed_2d=speed_2d,
speed_3d=speed_3d,
units=block_dict["UNIT"].value,
npoints=len(gps_data)
)
FIX_TYPE = {
0: "none",
2: "2d",
3: "3d"
}
def _make_speed_extensions(gps_data, i):
speed_2d = ET.Element("speed_2d")
value = ET.SubElement(speed_2d, "value")
value.text = "%g" % gps_data.speed_2d[i]
unit = ET.SubElement(speed_2d, "unit")
unit.text = "m/s"
speed_3d = ET.Element("speed_3d")
value = ET.SubElement(speed_3d, "value")
value.text = "%g" % gps_data.speed_3d[i]
unit = ET.SubElement(speed_3d, "unit")
unit.text = "m/s"
return [speed_2d, speed_3d]
[docs]
def make_pgx_segment(gps_blocks, first_only=False, speeds_as_extensions=True):
"""Convert a list of GPSData objects into a GPX track segment.
Parameters
----------
gps_blocks: list of GPSData
A list of GPSData objects
first_only: bool, optional (default=False)
If True use only the first GPS entry of each data block.
speeds_as_extensions: bool, optional (default=True)
If True, include 2d and 3d speed values as exentensions of
the GPX trackpoints. This is especially useful when saving
to GPX 1.1 format.
Returns
-------
gpx_segment: gpxpy.gpx.GPXTrackSegment
A gpx track segment.
"""
track_segment = gpxpy.gpx.GPXTrackSegment()
dt = timedelta(seconds=1.0 / 18.)
for gps_data in gps_blocks:
time = datetime.strptime(gps_data.timestamp, "%Y-%m-%d %H:%M:%S.%f")
# Reference says the frequency is about 18 Hz and other GPS data about 1Hz
stop = 1 if first_only else gps_data.npoints
for i in range(stop):
tp = gpxpy.gpx.GPXTrackPoint(
latitude=gps_data.latitude[i],
longitude=gps_data.longitude[i],
elevation=gps_data.altitude[i],
speed=gps_data.speed_3d[i],
position_dilution=gps_data.precision,
time=time + i * dt,
symbol="Square",
)
tp.type_of_gpx_fix = FIX_TYPE[gps_data.fix]
if speeds_as_extensions:
for e in _make_speed_extensions(gps_data, 0):
tp.extensions.append(e)
track_segment.points.append(tp)
return track_segment