Atlas: Deploy-Buendel vor Staustufen-Sprint

- Logistik: Musik-Player Playlist-Modus (5 Gruppen, 20 Tracks)
- Heli: End-Screen auf .ggs-endscreen, Mission-Bilder, Voice-Lines, Briefing-Audio
- Logistik: Layout-Tausch Auftraege+Fahrzeuge links, Karte rechts
- Logistik: Tier-1-Staedte ausgebaut, Auto-Timescale-Badge
- Logistik: Roadnet/Railnet Dijkstra-Routing, Balance-Updates
- Atlas: 5 Atlas-Inbox-Nachrichten (Cards-Pattern, DALL-E-Key, Musik-Playlists)
- Status-Updates Heli + Logistik

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 01:05:13 +02:00
parent f811437a35
commit 79ac9dad84
773 changed files with 17482 additions and 541 deletions
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
"""
Reichert lg-railnet.json mit OSRM-Polylines an. Nutzt das Auto-Profil als
Approximation fuer Bahnstrecken — die echten Bahnkorridore folgen oft
den Autobahnen (Wien-Muenchen via Salzburg, Hamburg-Rotterdam via Bremen-
Osnabrueck etc.), das ist auf Europa-Zoom visuell sauberer als Luftlinie.
Aufruf:
python App/sims/logistik/scripts/enrich-railnet-osrm.py
"""
import json
import math
import ssl
import time
import urllib.request
from pathlib import Path
RAILNET = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-railnet.json"
LOCATIONS = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-locations.json"
TOLERANCE_DEG = 0.001
SSL_CTX = ssl.create_default_context()
SSL_CTX.check_hostname = False
SSL_CTX.verify_mode = ssl.CERT_NONE
OSRM = "https://router.project-osrm.org/route/v1/driving/{coords}?overview=full&geometries=geojson"
def perp(pt, a, b):
dx, dy = b[0]-a[0], b[1]-a[1]
if dx == 0 and dy == 0:
return math.hypot(pt[0]-a[0], pt[1]-a[1])
t = max(0.0, min(1.0, ((pt[0]-a[0])*dx + (pt[1]-a[1])*dy)/(dx*dx+dy*dy)))
return math.hypot(pt[0]-(a[0]+t*dx), pt[1]-(a[1]+t*dy))
def rdp(points, eps):
if len(points) < 3:
return list(points)
md, idx = 0.0, 0
for i in range(1, len(points)-1):
d = perp(points[i], points[0], points[-1])
if d > md:
md, idx = d, i
if md <= eps:
return [points[0], points[-1]]
return rdp(points[:idx+1], eps)[:-1] + rdp(points[idx:], eps)
def fetch(waypoints):
coords = ";".join(f"{w[1]},{w[0]}" for w in waypoints)
with urllib.request.urlopen(OSRM.format(coords=coords), timeout=30, context=SSL_CTX) as r:
data = json.load(r)
route = data["routes"][0]
poly = [[round(c[1], 5), round(c[0], 5)] for c in route["geometry"]["coordinates"]]
return poly, route["distance"]/1000.0
def main():
net = json.loads(RAILNET.read_text(encoding="utf-8"))
locs = {l["id"]: l for l in json.loads(LOCATIONS.read_text(encoding="utf-8"))}
nodes = {n["id"]: n for n in net["nodes"]}
for i, edge in enumerate(net["edges"], 1):
fl = locs[nodes[edge["from"]]["locationId"]]
tl = locs[nodes[edge["to"]]["locationId"]]
start = [fl["lat"], fl["lon"]]
end = [tl["lat"], tl["lon"]]
print(f"[{i}/{len(net['edges'])}] {edge['id']}: ", end="", flush=True)
try:
poly, km = fetch([start, end])
poly = rdp(poly, TOLERANCE_DEG)
edge["polyline"] = poly
edge["distanceKmOsrm"] = round(km, 1)
print(f"{km:.0f} km, {len(poly)} Punkte")
except Exception as e:
print(f"FEHLER: {e}")
time.sleep(1.1)
RAILNET.write_text(
json.dumps(net, ensure_ascii=False, separators=(",", ":")),
encoding="utf-8",
)
sz = RAILNET.stat().st_size / 1024
print(f"\nFertig: {sz:.1f} KB")
if __name__ == "__main__":
main()
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""
Reichert App/assets/data/lg-roadnet.json an: ersetzt die hand-kuratierte
Polyline jeder Kante durch die echte OSRM-Routengeometrie entlang echter
Autobahnen. Die bisherigen Wegpunkte dienen als Stuetzstellen, damit OSRM
den gewuenschten Korridor waehlt (z.B. Brenner statt Mont Blanc).
Nutzt den freien OSRM-Demo-Server (router.project-osrm.org). Nur fuer
Dev-Zeit — die fertige Polyline landet im JSON, die Produktion ruft
OSRM nie auf (DSGVO, Rate-Limit, Verfuegbarkeit).
Aufruf:
python App/sims/logistik/scripts/enrich-roadnet-osrm.py
"""
import json
import ssl
import urllib.request
import urllib.parse
import time
import sys
from pathlib import Path
# SSL-Cert-Verify aus — Windows-Python findet den CA-Store oft nicht. Das
# ist fuer ein Dev-Tool (einmaliger Run, Ergebnis landet im JSON) akzeptabel.
# Produktiv wird OSRM nie aufgerufen.
SSL_CTX = ssl.create_default_context()
SSL_CTX.check_hostname = False
SSL_CTX.verify_mode = ssl.CERT_NONE
ROADNET = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-roadnet.json"
OSRM = "https://router.project-osrm.org/route/v1/driving/{coords}?overview=full&geometries=geojson"
def fetch_route(waypoints):
# waypoints = [[lat, lon], ...] -> OSRM erwartet "lon,lat;lon,lat;..."
coords = ";".join(f"{w[1]},{w[0]}" for w in waypoints)
url = OSRM.format(coords=coords)
with urllib.request.urlopen(url, timeout=30, context=SSL_CTX) as resp:
data = json.load(resp)
if data.get("code") != "Ok" or not data.get("routes"):
raise RuntimeError(f"OSRM-Fehler: {data.get('code')} / {data.get('message')}")
route = data["routes"][0]
geo = route["geometry"]["coordinates"] # [[lon, lat], ...]
polyline = [[round(c[1], 5), round(c[0], 5)] for c in geo]
distance_km = route["distance"] / 1000.0
return polyline, distance_km
def main():
net = json.loads(ROADNET.read_text(encoding="utf-8"))
total = len(net["edges"])
for i, edge in enumerate(net["edges"], 1):
print(f"[{i}/{total}] {edge['id']}: ", end="", flush=True)
waypoints = edge.get("polyline") or []
if len(waypoints) < 2:
print("skip (keine Wegpunkte)")
continue
try:
poly, dist_km = fetch_route(waypoints)
except Exception as e:
print(f"FEHLER — {e}")
continue
edge["polyline"] = poly
# OSRM-Distanz in die Kante schreiben als Diagnose, Original beibehalten
edge["distanceKmOsrm"] = round(dist_km, 1)
print(f"{len(poly)} Punkte, {dist_km:.0f} km (hand: {edge.get('distanceKm')})")
time.sleep(1.1) # OSRM-Demo: ~1 req/s, nicht mehr
ROADNET.write_text(
json.dumps(net, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(f"\nFertig — {ROADNET} aktualisiert.")
if __name__ == "__main__":
main()
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""
Erweitert lg-roadnet.json um die wichtigsten Hafen-Knoten + Edges. So
zeichnen Routes zu Häfen echte Polylines statt Luftlinie.
Neu: hamburg_hafen, rotterdam_hafen, triest_hafen, genua_hafen,
goeteborg_hafen, gdansk_hafen — jeweils mit Edge zur naechsten Stadt
und ggf. direkt zu wichtigen Hub-Staedten.
"""
import json
import math
import ssl
import time
import urllib.request
from pathlib import Path
ROADNET = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-roadnet.json"
LOCATIONS = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-locations.json"
TOLERANCE_DEG = 0.001
SSL_CTX = ssl.create_default_context()
SSL_CTX.check_hostname = False
SSL_CTX.verify_mode = ssl.CERT_NONE
OSRM = "https://router.project-osrm.org/route/v1/driving/{coords}?overview=full&geometries=geojson"
def perp(pt, a, b):
dx, dy = b[0]-a[0], b[1]-a[1]
if dx == 0 and dy == 0:
return math.hypot(pt[0]-a[0], pt[1]-a[1])
t = max(0.0, min(1.0, ((pt[0]-a[0])*dx + (pt[1]-a[1])*dy)/(dx*dx+dy*dy)))
return math.hypot(pt[0]-(a[0]+t*dx), pt[1]-(a[1]+t*dy))
def rdp(points, eps):
if len(points) < 3:
return list(points)
md, idx = 0.0, 0
for i in range(1, len(points)-1):
d = perp(points[i], points[0], points[-1])
if d > md:
md, idx = d, i
if md <= eps:
return [points[0], points[-1]]
return rdp(points[:idx+1], eps)[:-1] + rdp(points[idx:], eps)
def fetch(waypoints):
coords = ";".join(f"{w[1]},{w[0]}" for w in waypoints)
with urllib.request.urlopen(OSRM.format(coords=coords), timeout=30, context=SSL_CTX) as r:
data = json.load(r)
route = data["routes"][0]
poly = [[round(c[1], 5), round(c[0], 5)] for c in route["geometry"]["coordinates"]]
return poly, route["distance"]/1000.0
def main():
net = json.loads(ROADNET.read_text(encoding="utf-8"))
locs = {l["id"]: l for l in json.loads(LOCATIONS.read_text(encoding="utf-8"))}
existing_node_ids = {n["id"] for n in net["nodes"]}
existing_edge_ids = {e["id"] for e in net["edges"]}
NEW_NODES = ['hamburg_hafen', 'rotterdam_hafen', 'triest_hafen',
'genua_hafen', 'goeteborg_hafen', 'gdansk_hafen']
for nid in NEW_NODES:
if nid in existing_node_ids:
continue
loc = locs[nid]
net["nodes"].append({"id": nid, "locationId": nid, "name": loc["name"]})
# Edges: jeweils Stadt -> Hafen + ev. weitere
NEW_EDGES = [
('hamburg-hamburg_hafen', 'hamburg', 'hamburg_hafen', 6, 'A7-Anbindung'),
('rotterdam-rotterdam_hafen', 'rotterdam', 'rotterdam_hafen', 30, 'A15-Maasvlakte'),
('mailand-genua_hafen', 'mailand', 'genua_hafen', 145, 'A7'),
('muenchen-genua_hafen', 'muenchen', 'genua_hafen', 640, 'A22 + A7'),
('wien-triest_hafen', 'wien', 'triest_hafen', 480, 'A2 + A1 + A4 SLO'),
('muenchen-triest_hafen', 'muenchen', 'triest_hafen', 500, 'A8 + A23'),
('hamburg-goeteborg_hafen', 'hamburg', 'goeteborg_hafen', 500, 'A7 + Faehre Kiel-Goeteborg'),
('warschau-gdansk_hafen', 'warschau', 'gdansk_hafen', 340, 'S7'),
]
for eid, fnode, tnode, dist, code in NEW_EDGES:
if eid in existing_edge_ids:
print(f"skip {eid}")
continue
fl = locs[fnode]
tl = locs[tnode]
start = [fl["lat"], fl["lon"]]
end = [tl["lat"], tl["lon"]]
print(f"-> {eid}: ", end="", flush=True)
try:
poly, km = fetch([start, end])
poly = rdp(poly, TOLERANCE_DEG)
net["edges"].append({
"id": eid, "from": fnode, "to": tnode,
"distanceKm": dist, "routeCode": code,
"polyline": poly,
"distanceKmOsrm": round(km, 1),
})
print(f"{km:.0f} km, {len(poly)} pts")
except Exception as e:
print(f"FEHLER: {e}")
time.sleep(1.1)
ROADNET.write_text(
json.dumps(net, ensure_ascii=False, separators=(",", ":")),
encoding="utf-8",
)
sz = ROADNET.stat().st_size / 1024
print(f"\nFertig: {len(net['edges'])} Kanten total, {sz:.1f} KB")
if __name__ == "__main__":
main()
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
"""
Erweitert lg-roadnet.json um innsbruck und graz als Knoten. Neue Kanten:
- muenchen-innsbruck (A8/A93, ~170 km)
- innsbruck-salzburg (A12/A10 ueber Loferer Hochtal, ~190 km)
- innsbruck-mailand (A13+A22 Brenner, ~390 km)
- wien-graz (A2 Sued, ~200 km)
- salzburg-graz (A10+A9 via Tauerntunnel+Liezen, ~280 km)
Polylines per OSRM holen + Douglas-Peucker mit ~110 m Toleranz.
"""
import json
import math
import ssl
import time
import urllib.request
from pathlib import Path
ROADNET = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-roadnet.json"
LOCATIONS = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-locations.json"
TOLERANCE_DEG = 0.001
SSL_CTX = ssl.create_default_context()
SSL_CTX.check_hostname = False
SSL_CTX.verify_mode = ssl.CERT_NONE
OSRM = "https://router.project-osrm.org/route/v1/driving/{coords}?overview=full&geometries=geojson"
def perp_dist(pt, a, b):
dx, dy = b[0] - a[0], b[1] - a[1]
if dx == 0 and dy == 0:
return math.hypot(pt[0] - a[0], pt[1] - a[1])
t = max(0.0, min(1.0, ((pt[0] - a[0]) * dx + (pt[1] - a[1]) * dy) / (dx*dx + dy*dy)))
return math.hypot(pt[0] - (a[0] + t*dx), pt[1] - (a[1] + t*dy))
def rdp(points, eps):
if len(points) < 3:
return list(points)
max_d, idx = 0.0, 0
for i in range(1, len(points) - 1):
d = perp_dist(points[i], points[0], points[-1])
if d > max_d:
max_d, idx = d, i
if max_d <= eps:
return [points[0], points[-1]]
return rdp(points[:idx+1], eps)[:-1] + rdp(points[idx:], eps)
def fetch(waypoints):
coords = ";".join(f"{w[1]},{w[0]}" for w in waypoints)
with urllib.request.urlopen(OSRM.format(coords=coords), timeout=30, context=SSL_CTX) as r:
data = json.load(r)
route = data["routes"][0]
poly = [[round(c[1], 5), round(c[0], 5)] for c in route["geometry"]["coordinates"]]
return poly, route["distance"] / 1000.0
def main():
net = json.loads(ROADNET.read_text(encoding="utf-8"))
locs = {l["id"]: l for l in json.loads(LOCATIONS.read_text(encoding="utf-8"))}
# Neue Knoten
existing_ids = {n["id"] for n in net["nodes"]}
for nid in ("innsbruck", "graz"):
if nid in existing_ids:
continue
loc = locs[nid]
net["nodes"].append({"id": nid, "locationId": nid, "name": loc["name"]})
# Neue Kanten
NEW_EDGES = [
("muenchen-innsbruck", "muenchen", "innsbruck", 170, "A8/A93"),
("innsbruck-salzburg", "innsbruck", "salzburg", 190, "A12/A93/A10"),
("innsbruck-mailand", "innsbruck", "mailand", 390, "A13/A22 Brenner"),
("wien-graz", "wien", "graz", 200, "A2 Sued"),
("salzburg-graz", "salzburg", "graz", 280, "A10/A9 Tauern+Liezen"),
]
existing_edge_ids = {e["id"] for e in net["edges"]}
for eid, fnode, tnode, dist, code in NEW_EDGES:
if eid in existing_edge_ids:
print(f"skip {eid} (schon da)")
continue
fl = locs[net["nodes"][[n["id"] for n in net["nodes"]].index(fnode)]["locationId"]]
tl = locs[net["nodes"][[n["id"] for n in net["nodes"]].index(tnode)]["locationId"]]
start = [fl["lat"], fl["lon"]]
end = [tl["lat"], tl["lon"]]
print(f"-> {eid}: OSRM [{start}] -> [{end}] ...", flush=True)
try:
poly, osrm_km = fetch([start, end])
poly = rdp(poly, TOLERANCE_DEG)
net["edges"].append({
"id": eid, "from": fnode, "to": tnode,
"distanceKm": dist, "routeCode": code,
"polyline": poly,
"distanceKmOsrm": round(osrm_km, 1),
})
print(f" {osrm_km:.0f} km real, {len(poly)} Punkte")
except Exception as e:
print(f" FEHLER: {e}")
time.sleep(1.1)
ROADNET.write_text(
json.dumps(net, ensure_ascii=False, separators=(",", ":")),
encoding="utf-8",
)
sz = ROADNET.stat().st_size / 1024
print(f"\nFertig: {len(net['edges'])} Kanten, {sz:.1f} KB")
if __name__ == "__main__":
main()
@@ -0,0 +1,144 @@
#!/usr/bin/env bash
# Generiert die Logistik-Stadt-Bilder via DALL-E 3.
# Stil: Flat Scandinavian panoramic, an Heli-Pattern angelehnt (visuelle
# Kontinuitaet zwischen den Simulationen). Ohne Helikopter / Bergrettung —
# stattdessen ruhige, wiedererkennbare Stadt- und Landschafts-Panoramen.
#
# Voraussetzung:
# - OPENAI_API_KEY als Env-Variable (aus App/.env.local)
# - curl + python (fuer JSON-Handling; jq nicht verfuegbar auf Git Bash / Windows)
#
# Aufruf:
# set -a; source App/.env.local; set +a
# App/sims/logistik/scripts/generate-city-images.sh # alle
# App/sims/logistik/scripts/generate-city-images.sh wien salzburg # Auswahl
# FORCE=1 ... # ueberschreibt bestehende
#
# Kosten: ~0.04 EUR pro Bild (DALL-E 3 standard, 1792x1024).
set -e
: "${OPENAI_API_KEY:?Env-Variable OPENAI_API_KEY muss gesetzt sein}"
cd "$(dirname "$0")/../assets/cities"
PROMPT_BASE="Flat Scandinavian illustration, wide panoramic landscape filling the entire 16:9 frame edge-to-edge, NOT a circular composition, painted style with clean vector shapes, dark forest green (#1f4b37) and sage green (#4a7c4e) tones, yellow-mustard (#e8c547) accents, beige (#e8e4d8) buildings and valleys, white highlights. Recognizable geographic landmarks and city silhouettes. No text, no watermark, no logos, no borders, no frame, no people close-up."
declare -A PROMPTS=(
# Test-Set (5 Staedte) — DACH + Italien, sofort brauchbar
[wien]="$PROMPT_BASE Scene: Vienna cityscape with St. Stephen's Cathedral tower clearly visible in the center, Danube river curving on one side, Ferris wheel of Prater in the distance, Ringstrasse boulevard with historic buildings, Kahlenberg hill on the horizon."
[salzburg]="$PROMPT_BASE Scene: Salzburg old town panorama with Hohensalzburg fortress on the hill dominating the skyline, Salzach river flowing through, Baroque church domes and towers (Salzburger Dom), surrounding Alpine foothills, narrow historic streets."
[muenchen]="$PROMPT_BASE Scene: Munich skyline with the twin onion domes of Frauenkirche (Liebfrauenkirche) clearly visible, Rathaus tower, Alps on the southern horizon, Isar river, flat Bavarian plain, English Garden greenery."
[hamburg]="$PROMPT_BASE Scene: Hamburg harbour and city with the Elbphilharmonie's wave-shaped roof on the Elbe river, harbour cranes and container stacks in the distance, Michel church tower (St. Michaelis) with dark copper dome, Alster lakes reflecting the sky."
[mailand]="$PROMPT_BASE Scene: Milan cityscape with the Duomo cathedral's intricate spires and white marble facade in the center, Galleria Vittorio Emanuele II dome beside it, Castello Sforzesco walls, Po Valley flat landscape around, distant Alps on the northern horizon."
# Erweiterung (12 weitere) — fuer spaetere Runs, wenn Pattern gefaellt
[berlin]="$PROMPT_BASE Scene: Berlin skyline with Brandenburger Tor gate in foreground, TV tower (Fernsehturm) with its characteristic sphere in middle distance, Reichstag dome, Spree river curving through, flat North German plain."
[paris]="$PROMPT_BASE Scene: Paris panorama with Eiffel Tower prominent on the left, Sacre-Coeur basilica white dome on Montmartre hill, Seine river with bridges, Notre-Dame cathedral towers, Haussmann boulevards, uniform rooftop silhouette."
[rotterdam]="$PROMPT_BASE Scene: Rotterdam port skyline with the cable-stayed Erasmus Bridge, cube houses (Kubuswoningen), container terminal cranes in distance, Nieuwe Maas river, flat polder landscape."
[rom]="$PROMPT_BASE Scene: Rome cityscape with Colosseum amphitheatre on the left, St. Peter's Basilica dome (Vatican) on the right, Tiber river curving through, cypress trees and umbrella pines, seven hills, warm ochre rooftops."
[london]="$PROMPT_BASE Scene: London Thames panorama with Big Ben and Westminster Palace on one side, Tower Bridge suspension cables on the other, London Eye wheel, St. Paul's dome, modern skyscrapers (Gherkin, Shard) in background."
[amsterdam]="$PROMPT_BASE Scene: Amsterdam canal belt with narrow gabled merchant houses in rows, bicycles parked on bridges, canal boats, Westerkerk church tower, tulip fields visible on horizon, windmill in the distance."
[bruessel]="$PROMPT_BASE Scene: Brussels cityscape with Atomium spheres prominent, Grand-Place gothic guild halls, Palace of Justice dome, EU Parliament modern complex, flat Flemish landscape."
[prag]="$PROMPT_BASE Scene: Prague panorama with Prague Castle and St. Vitus Cathedral on the hill, Charles Bridge with its stone towers crossing the Vltava river, red-tiled rooftops of Old Town, Bohemian rolling hills."
[budapest]="$PROMPT_BASE Scene: Budapest with Parliament building's neo-gothic facade on the Danube, Chain Bridge with its lions, Buda Castle on hilltop, Fisherman's Bastion white turrets, Pannonian plain stretching east."
[kopenhagen]="$PROMPT_BASE Scene: Copenhagen with the Little Mermaid statue on rocks by the waterfront, Nyhavn colourful canal houses, spires of Christiansborg and Rosenborg Castle, Oresund bridge arching over to Sweden in the distance."
[stockholm]="$PROMPT_BASE Scene: Stockholm archipelago city on 14 islands, Gamla Stan old town with red-orange buildings, Royal Palace, Riddarholmen church spire, sailboats in the harbour, pine-covered rocky islands."
[warschau]="$PROMPT_BASE Scene: Warsaw skyline with Palace of Culture and Science (Stalinist skyscraper) prominent, rebuilt Old Town market square with coloured townhouses, Vistula river, flat Mazovian plain."
[madrid]="$PROMPT_BASE Scene: Madrid cityscape with Puerta de Alcalá triumphal arch, Gran Via avenue with early-20th-century buildings, Royal Palace with white facade, Plaza Mayor, dry Castilian meseta plateau around."
[athen]="$PROMPT_BASE Scene: Athens with the Acropolis hill crowned by the Parthenon marble temple, Lycabettus hill with white chapel on top, whitewashed Plaka district below, Aegean sea glimpsed on the horizon, port of Piraeus in distance with container cranes."
[oslo]="$PROMPT_BASE Scene: Oslo fjord panorama with modern white Opera House like a sloping iceberg at the waterfront, sailing boats on blue fjord, Akershus Fortress on a hilltop, pine-covered hills around, northern light over the water."
[helsinki]="$PROMPT_BASE Scene: Helsinki harbour with Helsinki Cathedral's white facade and green dome on the hill above Senate Square, market square with colourful stalls, Baltic Sea ferries, pine islands in the archipelago beyond."
[dublin]="$PROMPT_BASE Scene: Dublin with the Ha'penny Bridge arching over the River Liffey, Georgian terraced houses with colourful doors, Trinity College campanile, Dublin Bay with seagulls, green Irish countryside on horizon."
[lissabon]="$PROMPT_BASE Scene: Lisbon with yellow tram #28 on a steep cobblestone street, Belém Tower limestone fortress by the Tagus river, 25 de Abril suspension bridge in the distance, red-tiled rooftops cascading down hills, Atlantic Ocean."
[bern]="$PROMPT_BASE Scene: Bern old town on a peninsula in the Aare river, sandstone buildings with red-tiled roofs, Zytglogge clocktower, distant Bernese Alps with snowy Eiger/Mönch/Jungfrau peaks, green river bend."
[hamburg_hafen]="$PROMPT_BASE Scene: Port of Hamburg with massive container terminal, rows of blue and orange shipping containers stacked high, gantry cranes loading a container ship, Köhlbrandbrücke cable-stayed bridge, Elbe river industrial waterfront."
[rotterdam_hafen]="$PROMPT_BASE Scene: Port of Rotterdam Maasvlakte container terminal, huge container ships at deep-water berths, automated gantry cranes, container stacks, wind turbines on the seawall, North Sea waves beyond."
# Tier 2 (Grossstaedte)
[frankfurt]="$PROMPT_BASE Scene: Frankfurt am Main skyline with cluster of glass high-rises of the banking district (Mainhattan), Main river with bridges, half-timbered Roemer town hall at Paulskirche, Frankfurt airport control tower in distant haze."
[koeln]="$PROMPT_BASE Scene: Cologne cityscape dominated by the twin black-gothic spires of the Koelner Dom cathedral, Hohenzollern railway bridge crossing the Rhine river, old town with gabled houses in foreground, Rhine barges on the water."
[stuttgart]="$PROMPT_BASE Scene: Stuttgart valley city with Mercedes-Benz Museum curved modern building, vineyards climbing the surrounding hills (typical), TV tower Fernsehturm on Bopser hill, Swabian alb mountains on horizon."
[leipzig]="$PROMPT_BASE Scene: Leipzig with the Voelkerschlachtdenkmal obelisk monument prominent, historic Hauptbahnhof station facade, modern Gewandhaus concert hall, flat Saxon plain stretching, tree-lined canals."
[nuernberg]="$PROMPT_BASE Scene: Nuremberg with the Imperial Castle on a sandstone hill crowning the city, half-timbered medieval old town below, covered wooden Henkerbruecke bridge over the Pegnitz river, red-tiled rooftops."
[innsbruck]="$PROMPT_BASE Scene: Innsbruck in Inn valley with the Nordkette mountain range rising steeply behind, Goldenes Dachl gold-roofed medieval balcony, Bergisel ski jump tower modern, Inn river, alpine meadows."
[graz]="$PROMPT_BASE Scene: Graz Austria. ON TOP of the green forested Schlossberg hill stands the iconic Grazer UHRTURM clocktower (Renaissance, NOT a church, NOT a steeple): a STOUT SQUARE STANDALONE TOWER made of beige-cream stone, with a very wide WOODEN ROOFED GALLERY-BALCONY wrapping around all four sides at the top, crowned by a steep RED-SHINGLE PYRAMID ROOF, and each side shows one LARGE ROUND BLACK-AND-WHITE CLOCK FACE. The tower is wider at the gallery than at the base. NO crosses, NO bell tower, NO spire. Below the hill: red-tiled medieval old town along the Mur river, the dark glass blob-shaped Kunsthaus 'friendly alien' museum on the river bank, distant rolling Styrian vineyards."
[zuerich]="$PROMPT_BASE Scene: Zurich lake with snow-capped Alps reflected in calm water, Grossmuenster twin romanesque towers and Fraumuenster spire in old town, Bahnhofstrasse, green hillsides, steamboats on the lake."
[turin]="$PROMPT_BASE Scene: Turin grid-plan city with the Mole Antonelliana tall spire tower prominent, Palazzo Reale royal palace, snow-capped western Alps forming the backdrop, Po river, broad boulevards."
[neapel]="$PROMPT_BASE Scene: Naples bay with Mount Vesuvius volcano looming behind the city, Castel dell'Ovo fortress on a small island, colorful stacked waterfront houses, fishing boats, blue Tyrrhenian sea, pines."
[lyon]="$PROMPT_BASE Scene: Lyon peninsula between Rhone and Saone rivers, Notre-Dame de Fourviere white basilica on the hill, traboule alleys in old town, red-tiled rooftops, green Beaujolais hills in distance."
[marseille]="$PROMPT_BASE Scene: Marseille Vieux-Port old harbor full of fishing boats, Notre-Dame de la Garde golden statue basilica on hilltop, Chateau d'If island fortress, Mediterranean calanques limestone cliffs, Provence sun."
[barcelona]="$PROMPT_BASE Scene: Barcelona with Sagrada Familia towers with intricate spires and mosaics rising above the Eixample grid, Park Gueell tiled terraces in foreground, Mediterranean beach, Montjuic hill with castle."
[valencia]="$PROMPT_BASE Scene: Valencia with the City of Arts and Sciences futuristic white Calatrava complex (Hemispheric eye-shape), orange groves surrounding the city, Turia riverbed turned into a park, Mediterranean coast."
[porto]="$PROMPT_BASE Scene: Porto cascading down steep hills to the Douro river, Ribeira district colorful tile-fronted houses stacked, Ponte Luis I double-deck iron bridge, port wine cellars across in Vila Nova de Gaia."
[manchester]="$PROMPT_BASE Scene: Manchester with red-brick Victorian industrial warehouses converted to lofts, Beetham Tower needle-thin glass skyscraper, tram lines, canals with narrowboats, Pennines hills on horizon."
[krakau]="$PROMPT_BASE Scene: Krakow Wawel Royal Castle on a limestone hill above the Vistula river, Main Market Square with Cloth Hall Sukiennice gothic arcade, medieval town walls, distant snowy Tatra mountains."
[thessaloniki]="$PROMPT_BASE Scene: Thessaloniki waterfront with the White Tower fortress at the harbor, Byzantine city ramparts on a hill, Mount Olympus distant across the Thermaic Gulf, port cranes, Aegean Sea blue."
# Tier 3 (Haefen und Terminals)
[antwerpen_hafen]="$PROMPT_BASE Scene: Port of Antwerp on the Scheldt river, massive container terminal with thousands of colorful containers, oil refineries and chemical storage tanks in the distance, gantry cranes, inland barges, flat Flemish landscape."
[bremerhaven_hafen]="$PROMPT_BASE Scene: Port of Bremerhaven car terminal with thousands of new cars in long rows awaiting export, giant RoRo ship with ramp lowered to dock, container cranes, Weser river estuary, North Sea on horizon."
[duisburg_terminal]="$PROMPT_BASE Scene: Duisburg inland harbor at the Rhine-Ruhr confluence, container stacks, railway tracks for trans-Eurasian freight trains, a Chinese cargo train in the foreground with logos, industrial Ruhr backdrop."
[basel_terminal]="$PROMPT_BASE Scene: Rhine river harbor of Basel Switzerland, container terminal with gantry cranes, river barges pushing upstream, Three-Country-Corner monument, Alps distant on horizon, industrial Rhine-port infrastructure."
[marseille_hafen]="$PROMPT_BASE Scene: Port of Marseille with massive container ships at berth, oil tankers, Mediterranean sea sparkling, Notre-Dame de la Garde distant on hill behind, pine trees, industrial harbor cranes and warehouses."
[barcelona_hafen]="$PROMPT_BASE Scene: Port of Barcelona container terminal with cruise ships and cargo freighters side by side, gantry cranes, Mediterranean sea, Montjuic cable car line above, Columbus statue distant, industrial-urban edge."
[genua_hafen]="$PROMPT_BASE Scene: Port of Genoa Italy with colorful old-town houses stacked on the hillside above the docks, container terminal, Lanterna old lighthouse tower (oldest working in Mediterranean), Italian Riviera mountains."
[triest_hafen]="$PROMPT_BASE Scene: Port of Trieste with Piazza Unita d'Italia waterfront plaza, container terminal, Adriatic sea, Karst limestone hills behind, passage ships from the Balkans, Slovenian border hills in distance."
[piraeus_hafen]="$PROMPT_BASE Scene: Port of Piraeus Greece with massive COSCO container terminal, Chinese cargo ships at berth, cruise ships, Saronic Gulf islands in distance, bright Aegean sunlight, industrial port infrastructure."
[gdansk_hafen]="$PROMPT_BASE Scene: Port of Gdansk on the Baltic Sea, container terminal with gantry cranes, historic Solidarity shipyards with gantry structures, coal loading conveyors, red-brick Hanseatic old town tower visible in background."
[goeteborg_hafen]="$PROMPT_BASE Scene: Port of Gothenburg Sweden, deep-water container terminal with a massive container ship at berth, gantry cranes, industrial waterfront, North Sea, Scandinavian pine forest hills on the far shore."
)
TARGETS=("$@")
if [ ${#TARGETS[@]} -eq 0 ]; then
# Default: alle definierten Staedte + Haefen. Bereits generierte Bilder
# werden uebersprungen (FORCE=1 zum Ueberschreiben).
TARGETS=(
# Tier 1: Hauptstaedte und Kern-Haefen
wien salzburg muenchen hamburg hamburg_hafen rotterdam rotterdam_hafen
paris mailand berlin warschau madrid kopenhagen rom london
bruessel amsterdam prag budapest athen stockholm oslo helsinki
dublin lissabon bern
# Tier 2: Grossstaedte
frankfurt koeln stuttgart leipzig nuernberg innsbruck graz zuerich
turin neapel lyon marseille barcelona valencia porto manchester
krakau thessaloniki
# Tier 3: Haefen und Terminals
antwerpen_hafen bremerhaven_hafen duisburg_terminal basel_terminal
marseille_hafen barcelona_hafen genua_hafen triest_hafen
piraeus_hafen gdansk_hafen goeteborg_hafen
)
fi
for id in "${TARGETS[@]}"; do
prompt="${PROMPTS[$id]}"
if [ -z "$prompt" ]; then
echo "x $id: kein Prompt definiert, ueberspringe"
continue
fi
if [ -f "${id}.png" ] && [ -z "$FORCE" ]; then
echo "ok ${id}.png schon vorhanden (FORCE=1 zum Ueberschreiben)"
continue
fi
echo "-> ${id}: generiere ..."
payload=$(PROMPT="$prompt" python -c 'import json,os; print(json.dumps({"model":"dall-e-3","prompt":os.environ["PROMPT"],"n":1,"size":"1792x1024","quality":"standard"}))')
resp=$(curl -s https://api.openai.com/v1/images/generations \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d "$payload")
url=$(echo "$resp" | python -c 'import sys,json
d=json.load(sys.stdin)
print(d.get("data",[{}])[0].get("url",""))' 2>/dev/null)
if [ -z "$url" ]; then
err=$(echo "$resp" | python -c 'import sys,json
try: print(json.load(sys.stdin).get("error",{}).get("message","?"))
except: print("unparseable response")' 2>/dev/null)
echo "x ${id}: Fehler - $err"
continue
fi
curl -s "$url" -o "${id}.png"
echo "ok ${id}.png gespeichert ($(du -h "${id}.png" | cut -f1))"
sleep 2
done
echo
echo "Fertig. Optional auf 1024x576 verkleinern:"
echo " for f in *.png; do magick \"\$f\" -resize 1024x576 -strip \"\$f\"; done"
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Level-Splash-Bilder fuer Logistik. Gleicher Stil wie die Stadt-Bilder
# (Flat Scandinavian Illustration), aber breitere Szene pro Level.
#
# Aufruf:
# set -a; source App/.env.local; set +a
# App/sims/logistik/scripts/generate-splash-images.sh
set -e
: "${OPENAI_API_KEY:?Env-Variable OPENAI_API_KEY muss gesetzt sein}"
mkdir -p "$(dirname "$0")/../assets/splash"
cd "$(dirname "$0")/../assets/splash"
PROMPT_BASE="Flat Scandinavian illustration, wide panoramic landscape filling the entire 16:9 frame edge-to-edge, painted style with clean vector shapes, dark forest green (#1f4b37) and sage green (#4a7c4e) tones, yellow-mustard (#e8c547) accents, beige (#e8e4d8) buildings and valleys, white highlights. No text, no watermark, no logos, no borders, no frame."
declare -A PROMPTS=(
[level-1]="$PROMPT_BASE Scene: Wide panoramic view over central Europe (the Alpine DACH region). A small red delivery truck driving on a winding Alpine highway between forested mountains, Austrian valleys with small villages and church towers in the distance, snow-capped Alps dominating the background. Blue sky with a warm sunrise glow. The feel is a friendly 'starting out' first-day atmosphere, wide horizons, adventurous but manageable."
[level-2]="$PROMPT_BASE Scene: Panoramic central European logistics hub view with multiple trucks on parallel autobahn lanes, a freight train crossing a viaduct in the foreground, container terminals with stacks of colourful containers in the middle distance, mixed industrial-rural landscape, wider geographic spread with glimpses of Hamburg harbour on the left, Alps on the right. Feeling of scaling up and coordinating multiple vehicles."
[level-3]="$PROMPT_BASE Scene: Grand European panorama showing the full continent scale with major port cities Rotterdam and Hamburg on the North Sea, container ships, freight trains crossing continental Europe on multiple rail corridors, trucks on transnational highways, the Alps with Brenner pass, and southern ports Marseille and Trieste visible. Overcast with dramatic sky, feeling of complex multi-modal logistics empire."
)
TARGETS=("$@")
if [ ${#TARGETS[@]} -eq 0 ]; then TARGETS=(level-1 level-2 level-3); fi
for id in "${TARGETS[@]}"; do
prompt="${PROMPTS[$id]}"
if [ -z "$prompt" ]; then echo "x $id: kein Prompt definiert"; continue; fi
if [ -f "${id}.png" ] && [ -z "$FORCE" ]; then
echo "ok ${id}.png schon vorhanden (FORCE=1 zum Ueberschreiben)"
continue
fi
echo "-> ${id}: generiere ..."
payload=$(PROMPT="$prompt" python -c 'import json,os; print(json.dumps({"model":"dall-e-3","prompt":os.environ["PROMPT"],"n":1,"size":"1792x1024","quality":"standard"}))')
resp=$(curl -s https://api.openai.com/v1/images/generations \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-H "Content-Type: application/json" \
-d "$payload")
url=$(echo "$resp" | python -c 'import sys,json
d=json.load(sys.stdin)
print(d.get("data",[{}])[0].get("url",""))' 2>/dev/null)
if [ -z "$url" ]; then
err=$(echo "$resp" | python -c 'import sys,json
try: print(json.load(sys.stdin).get("error",{}).get("message","?"))
except: print("unparseable")' 2>/dev/null)
echo "x ${id}: Fehler - $err"
continue
fi
curl -s "$url" -o "${id}.png"
echo "ok ${id}.png ($(du -h "${id}.png" | cut -f1))"
sleep 2
done
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""
Generiert alle Voice-Line-MP3s via OpenAI TTS API.
Liest App/sims/logistik/assets/audio/voice-lines.json und legt pro Eintrag
zwei Dateien an: <id>.mp3 (std) und <id>_easy.mp3 (easy).
Aufruf:
set -a; source App/.env.local; set +a
python App/sims/logistik/scripts/generate-voice-lines.py
Vorhandene Dateien werden uebersprungen (FORCE=1 zum Ueberschreiben).
"""
import json
import os
import ssl
import sys
import time
import urllib.request
from pathlib import Path
SCRIPT_DIR = Path(__file__).resolve().parent
LINES_JSON = SCRIPT_DIR.parent / "assets" / "audio" / "voice-lines.json"
OUT_DIR = SCRIPT_DIR.parent / "assets" / "audio" / "voice" / "de"
API_KEY = os.environ.get("OPENAI_API_KEY")
if not API_KEY:
print("FEHLER: OPENAI_API_KEY muss als Env-Variable gesetzt sein.", file=sys.stderr)
sys.exit(1)
FORCE = bool(os.environ.get("FORCE"))
SSL_CTX = ssl.create_default_context()
SSL_CTX.check_hostname = False
SSL_CTX.verify_mode = ssl.CERT_NONE
def tts(text, voice="nova", model="tts-1", speed=1.0):
"""Ruft OpenAI TTS auf, gibt mp3-bytes zurueck."""
payload = json.dumps({
"model": model,
"voice": voice,
"input": text,
"speed": speed,
"response_format": "mp3",
}).encode("utf-8")
req = urllib.request.Request(
"https://api.openai.com/v1/audio/speech",
data=payload,
headers={
"Authorization": "Bearer " + API_KEY,
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(req, timeout=60, context=SSL_CTX) as resp:
return resp.read()
def main():
OUT_DIR.mkdir(parents=True, exist_ok=True)
data = json.loads(LINES_JSON.read_text(encoding="utf-8"))
voice = data.get("_voice", "nova")
model = data.get("_model", "tts-1")
lines = data["lines"]
count_new = 0
count_skip = 0
total_chars = 0
for entry in lines:
lid = entry["id"]
for variant, text_key, speed in (("", "std", 1.0), ("_easy", "easy", 0.88)):
text = entry.get(text_key)
if not text:
continue
fname = f"{lid}{variant}.mp3"
out_path = OUT_DIR / fname
if out_path.exists() and not FORCE:
count_skip += 1
print(f"ok {fname} (schon da)")
continue
total_chars += len(text)
print(f"-> {fname}: {len(text)} Zeichen, speed={speed} ...", end="", flush=True)
try:
mp3 = tts(text, voice=voice, model=model, speed=speed)
except Exception as e:
print(f" FEHLER: {e}")
continue
out_path.write_bytes(mp3)
size_kb = len(mp3) / 1024
print(f" {size_kb:.0f} KB")
count_new += 1
time.sleep(0.35) # sanftes Rate-Limit
# tts-1 Kosten: $15 / 1M Zeichen (2024)
cost_eur = total_chars / 1_000_000 * 15 * 0.92 # USD -> EUR approx
print(f"\nFertig: {count_new} neu, {count_skip} uebersprungen.")
print(f"Zeichen neu erzeugt: {total_chars} (~{cost_eur:.3f} EUR)")
if __name__ == "__main__":
main()
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""
Baut die Polylines in lg-roadnet.json neu — diesmal OHNE Zwischen-
wegpunkte. Grund: hand-kuratierte Zwischenpunkte lagen oft auf Stadt-
Koordinaten (Linz, Wels, Amstetten). OSRM snappt solche Punkte auf die
naechste Strasse = meist Innenstadt-Strasse, nicht Autobahn, und routet
entsprechend hin und zurueck. Das erzeugt die sichtbaren Zacken.
Loesung: nur [start, ende] an OSRM schicken. OSRM waehlt automatisch die
zeitkuerzeste Route (= Autobahn zwischen Grossstaedten). Fuer Berlin-
Kopenhagen behalten wir Rostock als Via, da OSRM die Faehre nicht
kennt und sonst die Ostsee umrundet.
Nach dem OSRM-Fetch direkt Douglas-Peucker-Vereinfachung (~110 m).
"""
import json
import math
import ssl
import time
import urllib.request
from pathlib import Path
ROADNET = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-roadnet.json"
LOCATIONS = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-locations.json"
TOLERANCE_DEG = 0.001
SSL_CTX = ssl.create_default_context()
SSL_CTX.check_hostname = False
SSL_CTX.verify_mode = ssl.CERT_NONE
OSRM = "https://router.project-osrm.org/route/v1/driving/{coords}?overview=full&geometries=geojson"
def perpendicular_distance(pt, a, b):
dx = b[0] - a[0]
dy = b[1] - a[1]
if dx == 0 and dy == 0:
return math.hypot(pt[0] - a[0], pt[1] - a[1])
t = ((pt[0] - a[0]) * dx + (pt[1] - a[1]) * dy) / (dx * dx + dy * dy)
t = max(0.0, min(1.0, t))
return math.hypot(pt[0] - (a[0] + t * dx), pt[1] - (a[1] + t * dy))
def rdp(points, epsilon):
if len(points) < 3:
return list(points)
max_d = 0.0
idx = 0
for i in range(1, len(points) - 1):
d = perpendicular_distance(points[i], points[0], points[-1])
if d > max_d:
max_d, idx = d, i
if max_d <= epsilon:
return [points[0], points[-1]]
left = rdp(points[:idx + 1], epsilon)
right = rdp(points[idx:], epsilon)
return left[:-1] + right
def fetch_osrm(waypoints):
coords = ";".join(f"{w[1]},{w[0]}" for w in waypoints)
url = OSRM.format(coords=coords)
with urllib.request.urlopen(url, timeout=30, context=SSL_CTX) as resp:
data = json.load(resp)
route = data["routes"][0]
poly = [[round(c[1], 5), round(c[0], 5)] for c in route["geometry"]["coordinates"]]
return poly, route["distance"] / 1000.0
def main():
net = json.loads(ROADNET.read_text(encoding="utf-8"))
locs = {l["id"]: l for l in json.loads(LOCATIONS.read_text(encoding="utf-8"))}
nodes = {n["id"]: n for n in net["nodes"]}
# Sonderfall Berlin-Kopenhagen (Faehre) — wird separat behandelt
FERRY_EDGE = "berlin-kopenhagen"
for idx, edge in enumerate(net["edges"], 1):
name = edge["id"]
from_n = nodes[edge["from"]]
to_n = nodes[edge["to"]]
from_l = locs[from_n["locationId"]]
to_l = locs[to_n["locationId"]]
start = [from_l["lat"], from_l["lon"]]
end = [to_l["lat"], to_l["lon"]]
print(f"[{idx}/{len(net['edges'])}] {name}: ", end="", flush=True)
try:
if name == FERRY_EDGE:
# Berlin -> Rostock (Land), Rostock-Gedser (Faehre direkt),
# Gedser -> Kopenhagen (Land). Distanz = Land-Summen + 48 km Faehre.
rostock = [54.09, 12.14]
gedser = [54.58, 11.93]
poly1, d1 = fetch_osrm([start, rostock])
time.sleep(1.1)
poly3, d3 = fetch_osrm([gedser, end])
ferry_poly = [rostock, [54.33, 12.03], gedser]
full = poly1 + ferry_poly[1:] + poly3[1:]
total = d1 + 48 + d3
print(f"Faehre-Route {d1:.0f}+48+{d3:.0f}={total:.0f} km", end="")
else:
full, total = fetch_osrm([start, end])
print(f"OSRM-Route {total:.0f} km", end="")
# Vereinfachen
simplified = rdp(full, TOLERANCE_DEG)
edge["polyline"] = simplified
edge["distanceKmOsrm"] = round(total, 1)
print(f" -> {len(simplified)} Punkte")
except Exception as e:
print(f" FEHLER: {e}")
continue
time.sleep(1.1)
ROADNET.write_text(
json.dumps(net, ensure_ascii=False, separators=(",", ":")),
encoding="utf-8",
)
size = ROADNET.stat().st_size / 1024
print(f"\nFertig: {size:.1f} KB")
if __name__ == "__main__":
main()
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""
Vereinfacht die OSRM-Polylines in lg-roadnet.json via Douglas-Peucker
(~100 m Toleranz). Reduziert die Dateigroesse von ~9 MB auf ~1 MB, ohne
dass die Kurvenfuehrung auf Europa-Zoomstufen sichtbar leidet.
Dazu: Berlin-Kopenhagen manuell reparieren — OSRM Auto-Profil kennt die
Fähre Rostock-Gedser nicht und umrundet die Ostsee (1027 km). Wir setzen
die Route auf Berlin->Rostock (Land, OSRM) + Rostock-Gedser (Fähre, direkt)
+ Gedser->Kopenhagen (Land, OSRM), Distanz = 450 km analog Realfahrt.
"""
import json
import math
import ssl
import time
import urllib.request
from pathlib import Path
ROADNET = Path(__file__).resolve().parents[3] / "assets" / "data" / "lg-roadnet.json"
TOLERANCE_DEG = 0.001 # ~110 m — glatt genug auf Europa-Zoom 4-8
OSRM = "https://router.project-osrm.org/route/v1/driving/{coords}?overview=full&geometries=geojson"
SSL_CTX = ssl.create_default_context()
SSL_CTX.check_hostname = False
SSL_CTX.verify_mode = ssl.CERT_NONE
def perpendicular_distance(pt, a, b):
"""Abstand pt zu Gerade a-b (in lat/lon-Einheiten, nicht metrisch, ok fuer RDP)."""
dx = b[0] - a[0]
dy = b[1] - a[1]
if dx == 0 and dy == 0:
return math.hypot(pt[0] - a[0], pt[1] - a[1])
t = ((pt[0] - a[0]) * dx + (pt[1] - a[1]) * dy) / (dx * dx + dy * dy)
t = max(0.0, min(1.0, t))
proj_x = a[0] + t * dx
proj_y = a[1] + t * dy
return math.hypot(pt[0] - proj_x, pt[1] - proj_y)
def rdp(points, epsilon):
"""Douglas-Peucker-Vereinfachung."""
if len(points) < 3:
return list(points)
# Max-deviation Punkt finden
max_d = 0.0
idx = 0
for i in range(1, len(points) - 1):
d = perpendicular_distance(points[i], points[0], points[-1])
if d > max_d:
max_d = d
idx = i
if max_d <= epsilon:
return [points[0], points[-1]]
left = rdp(points[:idx + 1], epsilon)
right = rdp(points[idx:], epsilon)
return left[:-1] + right
def fetch_route(waypoints):
coords = ";".join(f"{w[1]},{w[0]}" for w in waypoints)
url = OSRM.format(coords=coords)
with urllib.request.urlopen(url, timeout=30, context=SSL_CTX) as resp:
data = json.load(resp)
route = data["routes"][0]
geo = route["geometry"]["coordinates"]
polyline = [[round(c[1], 5), round(c[0], 5)] for c in geo]
distance_km = route["distance"] / 1000.0
return polyline, distance_km
def main():
net = json.loads(ROADNET.read_text(encoding="utf-8"))
# 1) Berlin-Kopenhagen (Faehre Rostock-Gedser) neu bauen
print("Berlin-Kopenhagen (Faehre rekonstruieren):")
rostock = [54.09, 12.14]
gedser = [54.58, 11.93]
berlin = [52.52, 13.405]
kph = [55.6761, 12.5683]
try:
poly1, d1 = fetch_route([berlin, rostock])
time.sleep(1.1)
poly3, d3 = fetch_route([gedser, kph])
# Faehren-Segment: gerade Linie mit zwei Zwischenpunkten fuer Animation
poly2 = [rostock, [54.33, 12.03], gedser]
combined = poly1 + poly2[1:] + poly3[1:]
# Land-Strecke + Faehre-Distanz (Rostock-Gedser ca. 48 km Luftlinie ueber Ostsee)
total_km = d1 + 48 + d3
print(f" Berlin->Rostock {d1:.0f} km, Faehre 48 km, Gedser->KPH {d3:.0f} km = {total_km:.0f} km")
for edge in net["edges"]:
if edge["id"] == "berlin-kopenhagen":
edge["polyline"] = combined
edge["distanceKmOsrm"] = round(total_km, 1)
break
except Exception as e:
print(f" FEHLER: {e} — behalte bisherige Polyline.")
# 2) Alle Polylines vereinfachen
print(f"\nDouglas-Peucker mit Tolerance {TOLERANCE_DEG}° (~{int(TOLERANCE_DEG*111*1000)} m):")
for edge in net["edges"]:
before = len(edge.get("polyline") or [])
if before < 3:
continue
simplified = rdp(edge["polyline"], TOLERANCE_DEG)
edge["polyline"] = simplified
after = len(simplified)
ratio = after / before * 100
print(f" {edge['id']}: {before} -> {after} Punkte ({ratio:.1f}%)")
ROADNET.write_text(
json.dumps(net, ensure_ascii=False, separators=(",", ":"),
indent=None),
encoding="utf-8",
)
new_size = ROADNET.stat().st_size / 1024 / 1024
print(f"\nFertig — {new_size:.2f} MB")
if __name__ == "__main__":
main()
+39
View File
@@ -0,0 +1,39 @@
#!/usr/bin/env node
// Testet calculateRoute-Ergebnis fuer Innsbruck->Mailand und Salzburg->Innsbruck.
// Laedt Engine, Seeds aus lg-roadnet.json + lg-locations.json und simuliert.
const fs = require('fs');
const path = require('path');
// UMD-Engine laden
const engineSrc = fs.readFileSync(path.join(__dirname, '..', 'engine.js'), 'utf-8');
const ctx = { window: {}, module: { exports: {} } };
ctx.exports = ctx.module.exports;
new Function('module','exports','window','self', engineSrc)(ctx.module, ctx.exports, ctx.window, ctx.window);
const L = ctx.module.exports.LogistikEngine || ctx.window.LogistikEngine || ctx.module.exports;
const locations = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', '..', 'assets', 'data', 'lg-locations.json')));
const roadnet = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', '..', 'assets', 'data', 'lg-roadnet.json')));
const railnet = { nodes: [], edges: [] };
const game = L.createGame(1);
game.worldState = game.worldState || {};
game.worldState.locations = locations;
game.worldState.roadnet = roadnet;
game.worldState.railnet = railnet;
game.worldState.routesOsm = [];
const TESTS = [
['innsbruck', 'mailand'],
['mailand', 'innsbruck'],
['salzburg', 'innsbruck'],
['innsbruck', 'salzburg'],
['wien', 'innsbruck'],
];
for (const [o, t] of TESTS) {
const r = L.calculateRoute(game, o, t, 'TRUCK_SMALL');
const geom = r.geometry || [];
const segs = (r.segments || []).map(s => `${s.startLat.toFixed(2)},${s.startLon.toFixed(2)}${s.endLat.toFixed(2)},${s.endLon.toFixed(2)}`).join(' · ');
console.log(`${o}${t}: provider=${r.provider} dist=${r.totalDistanceKm.toFixed(0)}km geom=${geom.length} pts, start=[${geom[0] && geom[0].map(n => n.toFixed(2)).join(',')}] end=[${geom[geom.length-1] && geom[geom.length-1].map(n => n.toFixed(2)).join(',')}]`);
console.log(` Segmente: ${segs}`);
}