Add nextcloud_favorites_to_osmand_prefixed_groups.py

This commit is contained in:
Stefan Liebl 2026-04-28 21:53:48 +02:00
parent 674f406fba
commit 8b2cfa29ed

View file

@ -0,0 +1,127 @@
#!/usr/bin/env python3
import json
import sys
from datetime import datetime, timezone
from xml.sax.saxutils import escape
OSMAND_NS = "https://osmand.net"
def epoch_to_iso_z(value):
if value is None:
return None
try:
ts = int(value)
return datetime.fromtimestamp(ts, tz=timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
except Exception:
return None
def norm(s):
if s is None:
return None
s = str(s).strip()
return s if s else None
def main():
if len(sys.argv) not in (3, 4):
print(
"Usage: nextcloud_favorites_to_osmand_prefixed_groups.py .favorites.json out.gpx [prefix]\n"
"Example: nextcloud_favorites_to_osmand_prefixed_groups.py .favorites.json favourites.gpx Norwegen",
file=sys.stderr
)
sys.exit(2)
in_path = sys.argv[1]
out_path = sys.argv[2]
prefix = sys.argv[3] if len(sys.argv) == 4 else "Norwegen"
with open(in_path, "r", encoding="utf-8") as f:
data = json.load(f)
if not isinstance(data, dict) or data.get("type") != "FeatureCollection":
raise SystemExit("Input is not a GeoJSON FeatureCollection")
features = data.get("features", [])
if not isinstance(features, list):
raise SystemExit("GeoJSON 'features' is not a list")
wpts = []
for idx, feat in enumerate(features):
if not isinstance(feat, dict) or feat.get("type") != "Feature":
continue
geom = feat.get("geometry") or {}
if geom.get("type") != "Point":
continue
coords = geom.get("coordinates")
if not (isinstance(coords, list) and len(coords) >= 2):
continue
# GeoJSON order: [lon, lat]
try:
lon = float(coords[0])
lat = float(coords[1])
except Exception:
continue
props = feat.get("properties") or {}
title = norm(props.get("Title")) or f"Favorite {idx+1}"
comment = norm(props.get("Comment"))
category = norm(props.get("Category")) or "Uncategorized"
group = f"{prefix}/{category}"
# Prefer Updated, else Published (epoch seconds)
time_iso = epoch_to_iso_z(props.get("Updated", props.get("Published")))
wpts.append({
"lat": lat,
"lon": lon,
"name": title,
"desc": comment,
"time": time_iso,
"group": group,
"category": category
})
if not wpts:
raise SystemExit("No Point features found to convert")
with open(out_path, "w", encoding="utf-8") as f:
f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
f.write('<gpx version="1.1" creator="nextcloud_favorites_to_osmand_prefixed_groups.py"\n')
f.write(' xmlns="http://www.topografix.com/GPX/1/1"\n')
f.write(' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"\n')
f.write(f' xmlns:osmand="{OSMAND_NS}"\n')
f.write(' xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">\n')
f.write(' <metadata>\n')
f.write(f' <time>{datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00","Z")}</time>\n')
f.write(' </metadata>\n')
for w in wpts:
f.write(f' <wpt lat="{w["lat"]:.8f}" lon="{w["lon"]:.8f}">\n')
f.write(f' <name>{escape(w["name"])}</name>\n')
# OsmAnd favorites group name (most important)
f.write(f' <type>{escape(w["group"])}</type>\n')
if w["desc"] is not None:
f.write(f' <desc>{escape(w["desc"])}</desc>\n')
if w["time"] is not None:
f.write(f' <time>{escape(w["time"])}</time>\n')
# Extra hints (safe if ignored)
f.write(' <extensions>\n')
f.write(f' <osmand:category>{escape(w["group"])}</osmand:category>\n')
f.write(f' <osmand:tag k="nextcloud_category" v="{escape(w["category"])}"/>\n')
f.write(' </extensions>\n')
f.write(' </wpt>\n')
f.write('</gpx>\n')
print(f"Wrote {len(wpts)} favorites to {out_path} with groups like '{prefix}/<Category>'")
if __name__ == "__main__":
main()