From 8b2cfa29edbb99b09df2bc6d3bb109c431082d0f Mon Sep 17 00:00:00 2001 From: Stefan Liebl Date: Tue, 28 Apr 2026 21:53:48 +0200 Subject: [PATCH] Add nextcloud_favorites_to_osmand_prefixed_groups.py --- ...oud_favorites_to_osmand_prefixed_groups.py | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100755 nextcloud_favorites_to_osmand_prefixed_groups.py diff --git a/nextcloud_favorites_to_osmand_prefixed_groups.py b/nextcloud_favorites_to_osmand_prefixed_groups.py new file mode 100755 index 0000000..7c084ad --- /dev/null +++ b/nextcloud_favorites_to_osmand_prefixed_groups.py @@ -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('\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() \ No newline at end of file