From 85e5839f998a36a2e9dd592001553e3475c7e834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Atakan=20Do=C4=9Fan=20=C3=96zban?= <68283731+atakanozban@users.noreply.github.com> Date: Thu, 23 Oct 2025 16:34:45 +0300 Subject: [PATCH] Add files via upload --- interpolate_gpx.py | 111 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 interpolate_gpx.py diff --git a/interpolate_gpx.py b/interpolate_gpx.py new file mode 100644 index 0000000..4a93c64 --- /dev/null +++ b/interpolate_gpx.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +import sys +import xml.etree.ElementTree as ET +from datetime import datetime, timedelta, timezone +from copy import deepcopy + +NS = {"gpx": "http://www.topografix.com/GPX/1/1"} +ET.register_namespace("", NS["gpx"]) + +def parse_time(t: str) -> datetime: + # GPX ISO8601 times end with Z (UTC) + return datetime.fromisoformat(t.replace("Z", "+00:00")).astimezone(timezone.utc) + +def fmt_time(dt: datetime) -> str: + return dt.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + +def get_ele(tp): + el = tp.find("gpx:ele", NS) + if el is not None and el.text not in (None, ""): + try: + return float(el.text) + except ValueError: + return None + return None + +def set_ele(tp, value): + if value is None: + return + el = tp.find("gpx:ele", NS) + if el is None: + el = ET.SubElement(tp, f"{{{NS['gpx']}}}ele") + el.text = f"{value:.2f}" + +def interpolate_segment(seg): + """Return a NEW with gaps >1s filled at 1s steps (linear lat/lon/ele).""" + pts = seg.findall("gpx:trkpt", NS) + if len(pts) < 2: + return deepcopy(seg) + + new_seg = ET.Element(f"{{{NS['gpx']}}}trkseg") + inserted_total = 0 + gaps = 0 + + for i in range(len(pts) - 1): + cur, nxt = pts[i], pts[i + 1] + new_seg.append(deepcopy(cur)) + + t1_el = cur.find("gpx:time", NS) + t2_el = nxt.find("gpx:time", NS) + if t1_el is None or t2_el is None or not t1_el.text or not t2_el.text: + continue + + t1, t2 = parse_time(t1_el.text), parse_time(t2_el.text) + dt = int((t2 - t1).total_seconds()) + if dt <= 1: + continue + + gaps += 1 + lat1, lon1 = float(cur.get("lat")), float(cur.get("lon")) + lat2, lon2 = float(nxt.get("lat")), float(nxt.get("lon")) + ele1, ele2 = get_ele(cur), get_ele(nxt) + + # Fill each missing second with linear interpolation + for s in range(1, dt): + alpha = s / dt + lat = lat1 + (lat2 - lat1) * alpha + lon = lon1 + (lon2 - lon1) * alpha + ele = None if (ele1 is None or ele2 is None) else (ele1 + (ele2 - ele1) * alpha) + + new_tp = ET.Element(f"{{{NS['gpx']}}}trkpt", attrib={ + "lat": f"{lat:.7f}", + "lon": f"{lon:.7f}", + }) + if ele is not None: + ele_el = ET.SubElement(new_tp, f"{{{NS['gpx']}}}ele") + ele_el.text = f"{ele:.2f}" + time_el = ET.SubElement(new_tp, f"{{{NS['gpx']}}}time") + time_el.text = fmt_time(t1 + timedelta(seconds=s)) + + new_seg.append(new_tp) + inserted_total += 1 + + # append last original point + new_seg.append(deepcopy(pts[-1])) + return new_seg + +def main(in_path, out_path): + tree = ET.parse(in_path) + root = tree.getroot() + + # keep original structure, only replace each trkseg content with interpolated version + changed = False + for trk in root.findall(".//gpx:trk", NS): + for seg in trk.findall("gpx:trkseg", NS): + new_seg = interpolate_segment(seg) + if new_seg is not None: + trk.remove(seg) + trk.append(new_seg) + changed = True + + if not changed: + print("No found or no changes made.", file=sys.stderr) + + tree.write(out_path, encoding="utf-8", xml_declaration=True) + print(f"Interpolated GPX saved to: {out_path}") + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: python interpolate_gpx.py input.gpx output.gpx", file=sys.stderr) + sys.exit(1) + main(sys.argv[1], sys.argv[2])