Files
API_Karte_QGISDemo/tools/headless_render_live.py
KAIandClaude Opus 5.5 c84bd6e378 Sichtpruefung der Verkehrszeichen mit echten Daten, ohne Zugangsschluessel
tools/headless_render_live.py rendert ein Verfahren in QGIS 4.2 offscreen: Daten aus dem
oeffentlichen /v2/maps/widmung/<vkz>, zusatz als Text wie beim OAPIF-Provider, Stil aus dem
Stil-Erzeuger des Managers (derselbe Code wie auf dem Server), Grafiken und Umschreiben
ueber zeichen_katalog. Heraus kommen Bilder bei 1:2000, 1:500 je Schild, 1:5000 und 1:20000.

Entstanden am 23.09.2026: die Fragezeichen unter den Schildern und ein gedrehtes Schild,
das aufrecht stand, sah man erst im echten Bild, nicht im Quelltext des Stils. Wichtig
fuer solche Laeufe: @map_scale gibt es nur mit dem Ausdruckskontext der Karteneinstellungen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 06:44:48 +02:00

162 lines
7.0 KiB
Python

"""Ein Verfahren in QGIS rendern — echte Daten, Stil wie vom Server, Plugin-Code fuer die Grafiken.
Sichtpruefung der Verkehrszeichen ohne Zugangsschluessel. Entstanden am 23.09.2026, als
unter den Schildern Fragezeichen standen und ein gedrehtes Schild aufrecht blieb — beides
sah man erst im echten Bild, nicht im Quelltext des Stils.
Aufruf (VKZ als zweites Argument, Vorgabe 22017; Bilder landen als <ziel>_*.png):
APP=/Applications/QGIS-final-4_2_0.app; LIB="$APP/Contents/Resources/python3.12"
QT_QPA_PLATFORM=offscreen PROJ_DATA="$APP/Contents/Resources/qgis/proj" \\
PYTHONPATH="$LIB:$LIB/lib-dynload:$LIB/site-packages" \\
"$APP/Contents/MacOS/python3.12" tools/headless_render_live.py /tmp/vln 22017
Braucht den Klon des Managers fuer den Stil-Erzeuger (Umgebungsvariable VLN_MANAGER_REPO,
Vorgabe ~/GIT/VLNManager) — derselbe Code, der auf dem Server laeuft.
Weg wie im Plugin, nur ohne Anmeldung:
* Daten: GET /v2/maps/widmung/22017 (oeffentlich) — dieselben Objekte wie die OGC-Sammlung.
* `zusatz` als TEXT, so wie der OAPIF-Provider es fuehrt (die Queryables melden "string").
* Stil: require.widmung_stil.qml() — derselbe Code, der auf S1 laeuft (Checkout = Repo).
* Grafiken: vln_karten.zeichen_katalog (sichere_svgs, pfad_registrieren, stil_lokalisieren).
"""
import json
import os
import sys
import urllib.request
sys.path.insert(0, os.environ.get("VLN_MANAGER_REPO", os.path.expanduser("~/GIT/VLNManager")))
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from qgis.core import QgsApplication
app = QgsApplication([], True)
QgsApplication.setPrefixPath("/Applications/QGIS-final-4_2_0.app/Contents/MacOS", True)
QgsApplication.setPluginPath("/Applications/QGIS-final-4_2_0.app/Contents/PlugIns/qgis")
app.initQgis()
from qgis.PyQt.QtCore import QMetaType, QSize
from qgis.PyQt.QtGui import QColor, QImage, QPainter
from qgis.PyQt.QtNetwork import QNetworkProxyFactory
from qgis.core import (QgsCoordinateReferenceSystem, QgsExpressionContext,
QgsExpressionContextUtils, QgsFeature, QgsField, QgsGeometry,
QgsMapRendererCustomPainterJob, QgsMapSettings, QgsPointXY,
QgsRectangle, QgsVectorLayer)
QNetworkProxyFactory.setUseSystemConfiguration(False)
from require import widmung_stil as W
from vln_karten import zeichen_katalog as zk
from vln_karten.api_client import KartenApiClient
API = "https://api.flurneuordnung-sachsen.de"
ZIEL = sys.argv[1]
VKZ = sys.argv[2] if len(sys.argv) > 2 else "22017"
fc = json.load(urllib.request.urlopen(API + "/v2/maps/widmung/" + VKZ, timeout=30))
zeichen = [f for f in fc["features"] if (f.get("properties") or {}).get("typ") == "verkehrszeichen"]
linien = [f for f in fc["features"] if (f.get("properties") or {}).get("typ") == "widmung"]
print("Live-Daten: %d Zeichen, %d Widmungslinien" % (len(zeichen), len(linien)))
def punkt_layer():
layer = QgsVectorLayer("Point?crs=EPSG:25833", "Zeichen " + VKZ, "memory")
dp = layer.dataProvider()
dp.addAttributes([QgsField("vz", QMetaType.Type.QString),
QgsField("zusatz", QMetaType.Type.QString),
QgsField("drehung", QMetaType.Type.Double),
QgsField("groesse", QMetaType.Type.Int),
QgsField("vz_status", QMetaType.Type.QString)])
layer.updateFields()
neu = []
for f in zeichen:
p = f["properties"]
x, y = f["geometry"]["coordinates"][:2]
q = QgsFeature(layer.fields())
q.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(x, y)))
# So fuehrt der OAPIF-Provider das Feld: als Text.
q.setAttributes([str(p.get("vz") or ""), json.dumps(p.get("zusatz") or []),
float(p.get("drehung") or 0), int(p.get("groesse") or 2),
str(p.get("vz_status") or "")])
neu.append(q)
print(" Zeichen %-4s zusatz=%-14s drehung=%s" % (p.get("vz"), json.dumps(p.get("zusatz")),
p.get("drehung")))
dp.addFeatures(neu)
layer.updateExtents()
return layer
def linien_layer():
layer = QgsVectorLayer("LineString?crs=EPSG:25833", "Widmung " + VKZ, "memory")
dp = layer.dataProvider()
dp.addAttributes([QgsField("strassenklasse", QMetaType.Type.QString),
QgsField("widmung_status", QMetaType.Type.QString),
QgsField("neue_klasse", QMetaType.Type.QString)])
layer.updateFields()
neu = []
for f in linien:
p = f["properties"]
q = QgsFeature(layer.fields())
q.setGeometry(QgsGeometry.fromPolylineXY(
[QgsPointXY(x, y) for x, y, *_ in f["geometry"]["coordinates"]]))
q.setAttributes([str(p.get("strassenklasse") or ""), str(p.get("widmung_status") or ""),
str(p.get("neue_klasse") or "")])
neu.append(q)
dp.addFeatures(neu)
layer.updateExtents()
return layer
zeichen_layer = punkt_layer()
widmung_layer = linien_layer()
# Plugin-Weg: Grafiken holen, Suchpfad eintragen, Stil auf die lokale Ablage umschreiben.
client = KartenApiClient()
codes = zk.codes_im_layer(zeichen_layer)
zk.pfad_registrieren()
geholt, fehlend = zk.sichere_svgs(client, codes)
print("Codes im Layer: %s | geholt %d | fehlend %s" % (sorted(codes), geholt, fehlend))
for layer, familie in ((zeichen_layer, "punkt"), (widmung_layer, "linie")):
qml = W.qml(familie, API + "/v2/zeichen/svg/", layer.name())
qml, ersetzt = zk.stil_lokalisieren(qml, API)
pfad = "/tmp/vln_live_%s.qml" % familie
with open(pfad, "w", encoding="utf-8") as fh:
fh.write(qml)
meldung, ok = layer.loadNamedStyle(pfad)
print("Stil %-6s geladen=%s lokalisiert=%s" % (familie, ok, ersetzt))
def rendern(massstab, breite, hoehe, datei, mitte=None):
mitte = mitte or zeichen_layer.extent().center()
m_je_px = massstab / 3779.5276
b, h = breite * m_je_px / 2, hoehe * m_je_px / 2
e = QgsMapSettings()
e.setLayers([zeichen_layer, widmung_layer])
e.setBackgroundColor(QColor(255, 255, 255))
e.setOutputSize(QSize(breite, hoehe))
e.setOutputDpi(96)
e.setDestinationCrs(QgsCoordinateReferenceSystem("EPSG:25833"))
e.setExtent(QgsRectangle(mitte.x() - b, mitte.y() - h, mitte.x() + b, mitte.y() + h))
ctx = QgsExpressionContext()
ctx.appendScope(QgsExpressionContextUtils.globalScope())
ctx.appendScope(QgsExpressionContextUtils.mapSettingsScope(e))
e.setExpressionContext(ctx)
bild = QImage(e.outputSize(), QImage.Format.Format_ARGB32)
bild.fill(QColor(255, 255, 255))
maler = QPainter(bild)
auftrag = QgsMapRendererCustomPainterJob(e, maler)
auftrag.start()
auftrag.waitForFinished()
maler.end()
bild.save(datei)
print(" 1:%-6d -> %s" % (round(e.scale()), datei))
print("Bilder:")
rendern(2000, 520, 700, ZIEL + "_beide.png")
for f in zeichen_layer.getFeatures():
rendern(500, 360, 360, ZIEL + "_%s.png" % f["vz"], f.geometry().asPoint())
rendern(5000, 500, 300, ZIEL + "_marke.png")
rendern(20000, 500, 300, ZIEL + "_punkt.png")
app.exitQgis()