import argparse
import gpxpy
from geopy.distance import geodesic
from datetime import datetime
from xml.dom.minidom import parseString
def get_time_diff_seconds(t1, t2):
return (t2 - t1).total_seconds() if t1 and t2 else 0
def add_distance_by_speed_with_time(gpx):
total_distance = 0.0
for segment in gpx.tracks[0].segments:
prev = None
for pt in segment.points:
if prev:
dt = get_time_diff_seconds(prev.time, pt.time)
speed = float(pt.extensions.get("speed", 0))
total_distance += speed * dt
pt.extensions['distance'] = round(total_distance, 2)
prev = pt
return gpx
def add_distance_by_gps(gpx):
total_distance = 0.0
for segment in gpx.tracks[0].segments:
prev = None
for pt in segment.points:
if prev:
total_distance += geodesic(
(prev.latitude, prev.longitude), (pt.latitude, pt.longitude)
).meters
pt.extensions['distance'] = round(total_distance, 2)
prev = pt
return gpx
def scale_route_distances(gpx_route, factor):
for seg in gpx_route.tracks[0].segments:
for pt in seg.points:
pt.extensions['distance_scaled'] = round(float(pt.extensions['distance']) * factor, 2)
return gpx_route
def find_surrounding_points(route_points, target_dist):
prev, next = None, None
for pt in route_points:
dist = pt.extensions.get('distance_scaled', 0)
if dist <= target_dist:
prev = pt
elif dist > target_dist:
next = pt
break
return prev, next
def interpolate_coords(p1, p2, target_dist):
d1 = p1.extensions['distance_scaled']
d2 = p2.extensions['distance_scaled']
ratio = (target_dist - d1) / (d2 - d1) if d2 != d1 else 0
lat = p1.latitude + ratio * (p2.latitude - p1.latitude)
lon = p1.longitude + ratio * (p2.longitude - p1.longitude)
return lat, lon
def merge_gps_into_activity(gpx_activity, gpx_route, mode='nearest'):
route_pts = [pt for seg in gpx_route.tracks[0].segments for pt in seg.points]
for seg in gpx_activity.tracks[0].segments:
for pt in seg.points:
d = pt.extensions.get('distance', 0)
prev, nxt = find_surrounding_points(route_pts, d)
if not prev and not nxt:
continue
if mode == "nearest":
if not prev:
chosen = nxt
elif not nxt:
chosen = prev
else:
d_prev = abs(prev.extensions['distance_scaled'] - d)
d_next = abs(nxt.extensions['distance_scaled'] - d)
chosen = prev if d_prev <= d_next else nxt
pt.latitude = chosen.latitude
pt.longitude = chosen.longitude
elif mode == "interpolate" and prev and nxt:
lat, lon = interpolate_coords(prev, nxt, d)
pt.latitude = lat
pt.longitude = lon
return gpx_activity
def write_gpx_with_extensions(gpx, filename):
xml = gpx.to_xml()
dom = parseString(xml)
trkpts = dom.getElementsByTagName("trkpt")
i = 0
for segment in gpx.tracks[0].segments:
for pt in segment.points:
ext_elem = dom.createElement("extensions")
for key, value in pt.extensions.items():
el = dom.createElement(key)
el.appendChild(dom.createTextNode(str(value)))
ext_elem.appendChild(el)
trkpts[i].appendChild(ext_elem)
i += 1
with open(filename, "w", encoding="utf-8") as f:
f.write(dom.toprettyxml(indent=" "))
def main():
parser = argparse.ArgumentParser(description="Merge GPX activity with route GPS.")
parser.add_argument('--activity', required=True, help="Path to activity GPX file")
parser.add_argument('--route', required=True, help="Path to route GPX file")
parser.add_argument('--mode', default="nearest", choices=["nearest", "interpolate"],
help="GPS assignment mode")
args = parser.parse_args()
with open(args.activity, "r") as f1, open(args.route, "r") as f2:
gpx_act = gpxpy.parse(f1)
gpx_rte = gpxpy.parse(f2)
gpx_act = add_distance_by_speed_with_time(gpx_act)
gpx_rte = add_distance_by_gps(gpx_rte)
dist_act = gpx_act.tracks[0].segments[0].points[-1].extensions['distance']
dist_rte = gpx_rte.tracks[0].segments[0].points[-1].extensions['distance']
scale_factor = float(dist_act) / float(dist_rte)
gpx_rte = scale_route_distances(gpx_rte, scale_factor)
gpx_act = merge_gps_into_activity(gpx_act, gpx_rte, mode=args.mode)
write_gpx_with_extensions(gpx_act, "gpx_activity_with_gps.gpx")
write_gpx_with_extensions(gpx_rte, "gpx_route_scaled.gpx")
if __name__ == "__main__":
main()