#!/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('\n')
f.write('\n')
f.write(' \n')
f.write(f' \n')
f.write(' \n')
for w in wpts:
f.write(f' \n')
f.write(f' {escape(w["name"])}\n')
# OsmAnd favorites group name (most important)
f.write(f' {escape(w["group"])}\n')
if w["desc"] is not None:
f.write(f' {escape(w["desc"])}\n')
if w["time"] is not None:
f.write(f' \n')
# Extra hints (safe if ignored)
f.write(' \n')
f.write(f' {escape(w["group"])}\n')
f.write(f' \n')
f.write(' \n')
f.write(' \n')
f.write('\n')
print(f"Wrote {len(wpts)} favorites to {out_path} with groups like '{prefix}/'")
if __name__ == "__main__":
main()