"""Jump-graph routing between systems/planets, built from FIO's /systemstars (698 systems, 3D coordinates + jump connections). Not in PRUNplanner's own source (it doesn't model travel) - this is the project's own addition. Edge weight = straight-line distance between connected systems (ly); PrUn's own FTL fuel/time scale with this, not raw jump count, so Dijkstra on distance gives the right "cheapest" route, with jump count reported alongside for reference.""" import heapq import math from . import fio def _dist(a, b): return math.dist((a["PositionX"], a["PositionY"], a["PositionZ"]), (b["PositionX"], b["PositionY"], b["PositionZ"])) def load_graph(): """SystemId -> {name, natural_id, pos, neighbors: {SystemId: distance_ly}}""" systems = {s["SystemId"]: s for s in fio.systems()} g = {} for sid, s in systems.items(): neighbors = {} for c in s.get("Connections", []): other = systems.get(c["ConnectingId"]) if other: neighbors[c["ConnectingId"]] = _dist(s, other) g[sid] = {"name": s["Name"], "natural_id": s["NaturalId"], "pos": s, "neighbors": neighbors} return g def resolve_system(query: str) -> str: """Planet natural id (e.g. ZV-759c), CX station code (ANT), system natural id (ZV-759), or a name -> SystemId.""" q = query.strip().upper() for p in fio.planets_full(): if p.get("PlanetNaturalId", "").upper() == q or (p.get("PlanetName") or "").upper() == q: return p["SystemId"] for st in fio.stations(): if st["NaturalId"].upper() == q or st["Name"].upper() == q: return st["SystemId"] for s in fio.systems(): if s["NaturalId"].upper() == q or s["Name"].upper() == q: return s["SystemId"] raise ValueError(f"no planet, station, or system matches {query!r}") def shortest_path(src_system_id: str, dst_system_id: str, graph=None): """Dijkstra by straight-line ly distance. Returns dict: jumps, distance_ly, path (list of system natural ids).""" g = graph or load_graph() if src_system_id not in g or dst_system_id not in g: raise ValueError("unknown system id") dist = {src_system_id: 0.0} prev = {} pq = [(0.0, src_system_id)] seen = set() while pq: d, u = heapq.heappop(pq) if u in seen: continue seen.add(u) if u == dst_system_id: break for v, w in g[u]["neighbors"].items(): nd = d + w if nd < dist.get(v, float("inf")): dist[v] = nd prev[v] = u heapq.heappush(pq, (nd, v)) if dst_system_id not in dist: return None path = [dst_system_id] while path[-1] != src_system_id: path.append(prev[path[-1]]) path.reverse() return { "jumps": len(path) - 1, "distance_ly": dist[dst_system_id], "path": [g[sid]["natural_id"] for sid in path], }