Frankreich, Niederlande, Belgien als weitere Quellen (noch im Test)

- Quellen fr (SNCF), nl (OVapi), be (GTFS.be), Kacheln für alle 5 Länder
- Landesgrenzen für 5 Länder (borders.json), Gitter-Cache für die Prüfung
- Echtzeit-Abgleich auch über Haltestellen-ID (FR, NL), über den Bahnhof
  bei Gleiswechsel
- SNCF: Gattung aus der Trip-ID, Zugnummer aus dem Headsign; NL-Linien gekürzt
- calendar.txt optional (SNCF hat nur calendar_dates)
- OSM je Land zugeschnitten vor pfaedle; beim Zusammenführen je Objekt
  nur die neueste Version (time-filter)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X48pE4bzjHej8w9HyZXCjb
This commit is contained in:
Claude
2026-10-22 07:06:47 +02:00
parent 76a8fc0806
commit d339da5da2
12 changed files with 203 additions and 68 deletions
+51 -11
View File
@@ -1,12 +1,22 @@
"""Landesgrenze der Schweiz (aus OpenStreetMap, auf ~400 m vereinfacht, mit den Exklaven Büsingen
und Campione als Löcher). Damit zeigt jeder Fahrplan seine Züge nur im eigenen Land, sonst wären
grenzüberschreitende Züge doppelt auf der Karte."""
"""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
_B = json.loads((Path(__file__).with_name("ch_border.json")).read_text())
_BBOX = (min(x for x, _ in _B["outer"]), min(y for _, y in _B["outer"]),
max(x for x, _ in _B["outer"]), max(y for _, y in _B["outer"]))
_RAW = json.loads((Path(__file__).with_name("borders.json")).read_text())
ORDER = ["ch", "de", "fr", "be", "nl"] # 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:
@@ -17,11 +27,41 @@ def _in_ring(x: float, y: float, ring) -> bool:
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:
if not (_BBOX[0] <= lon <= _BBOX[2] and _BBOX[1] <= lat <= _BBOX[3]):
return False
return _in_ring(lon, lat, _B["outer"]) and not any(_in_ring(lon, lat, h) for h in _B["holes"])
return _country(lon, lat) == "ch"
# Welche Züge ein Fahrplan zeigen darf, wenn mehrere Quellen aktiv sind
SHOW = {"ch": in_switzerland, "de": lambda lon, lat: not in_switzerland(lon, lat)}
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