- mehrere Quellen je Land (country), Grenzen aus Natural Earth, OSM und Kachelausschnitt erweitert - Fahrpläne glätten: Unterordner im Zip, aufgefüllte Felder, fehlende parent_station, level_id - Gattung aus der Zugnummer (ÖBB 'RJ 658'), Fernverkehr auch über Routentyp 101/102/105 - Echtzeit: Verspätung vor Uhrzeit (Renfe), Fahrten ohne Datum, Key in der Adresse (Trafiklab) - httpx-Logs aus (würden Keys zeigen) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X48pE4bzjHej8w9HyZXCjb
68 lines
2.6 KiB
Python
68 lines
2.6 KiB
Python
"""Landesgrenzen, damit jeder Fahrplan seine Züge nur im eigenen Land zeigt – sonst wären
|
||
grenzüberschreitende Züge doppelt auf der Karte (z. B. ICE Basel–Frankfurt in CH- und DE-Daten).
|
||
|
||
Schweiz aus OpenStreetMap (~400 m genau, mit Büsingen und Campione als Löcher), die übrigen
|
||
Länder aus Natural Earth 1:10 Mio. (gemeinfrei), auf ~400 m vereinfacht."""
|
||
import json
|
||
from pathlib import Path
|
||
|
||
_RAW = json.loads((Path(__file__).with_name("borders.json")).read_text())
|
||
ORDER = ["ch", "de", "fr", "be", "nl", "at", "dk", "es", "no", "pl", "se"] # Schweiz zuerst: genauere Grenze, gewinnt bei Überlappung
|
||
CELL = 0.01 # ~1 km; im Grenzbereich wird je Zelle einmal gerechnet und gemerkt
|
||
|
||
|
||
def _bbox(ring):
|
||
xs, ys = [p[0] for p in ring], [p[1] for p in ring]
|
||
return min(xs), min(ys), max(xs), max(ys)
|
||
|
||
|
||
_POLYS = {code: [(_bbox(p[0]), p) for p in _RAW[code]] for code in ORDER if code in _RAW}
|
||
|
||
|
||
def _in_ring(x: float, y: float, ring) -> bool:
|
||
inside = False
|
||
for (x1, y1), (x2, y2) in zip(ring, ring[1:] + ring[:1]):
|
||
if (y1 > y) != (y2 > y) and x < (x2 - x1) * (y - y1) / (y2 - y1) + x1:
|
||
inside = not inside
|
||
return inside
|
||
|
||
|
||
def _country(x: float, y: float) -> str | None:
|
||
for code, polys in _POLYS.items():
|
||
for (x0, y0, x1, y1), (outer, *holes) in polys:
|
||
if x0 <= x <= x1 and y0 <= y <= y1 and _in_ring(x, y, outer) \
|
||
and not any(_in_ring(x, y, h) for h in holes):
|
||
return code
|
||
return None
|
||
|
||
|
||
_cache: dict = {}
|
||
|
||
|
||
def country(lon: float, lat: float) -> str | None:
|
||
"""Ländercode (ch, de, fr, be, nl) oder None für alle anderen Länder."""
|
||
key = (round(lon / CELL), round(lat / CELL))
|
||
if key not in _cache:
|
||
if len(_cache) > 500_000:
|
||
_cache.clear()
|
||
_cache[key] = _country(key[0] * CELL, key[1] * CELL)
|
||
return _cache[key]
|
||
|
||
|
||
def bbox(code: str, margin: float = 0.6) -> str:
|
||
"""Ausschnitt eines Landes (+ ~50 km Rand) als "x0,y0,x1,y1" für osmium."""
|
||
boxes = [b for b, _ in _POLYS[code]]
|
||
return ",".join(f"{v:.2f}" for v in (min(b[0] for b in boxes) - margin, min(b[1] for b in boxes) - margin,
|
||
max(b[2] for b in boxes) + margin, max(b[3] for b in boxes) + margin))
|
||
|
||
|
||
def in_switzerland(lon: float, lat: float) -> bool:
|
||
return _country(lon, lat) == "ch"
|
||
|
||
|
||
def shows(source: str, enabled, lon: float, lat: float) -> bool:
|
||
"""Zeigt diese Quelle den Zug an dieser Stelle? Im eigenen Land ja, in einem Land mit eigenem
|
||
aktivem Fahrplan nein (dort zeigt ihn dieser). Überall sonst ja."""
|
||
c = country(lon, lat)
|
||
return c is None or c == source or c not in enabled
|