Files
Masterprojekt_V3/Netzqualitaet_Genauigkeit.py

580 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import numpy as np
import plotly.graph_objects as go
from scipy.stats import f
import pandas as pd
from IPython.display import HTML
from IPython.display import display
import Berechnungen
import Einheitenumrechnung
class Genauigkeitsmaße:
"""Berechnung von Genauigkeitsmaße zur Bewertung der erreichten Netzqualität.
Die Klasse stellt Methoden zur Verfügung für:
- Berechnung der a-posteriori Standardabweichung der Gewichtseinheit s₀
- Berechnung des Helmertschen Punktfehlers (2D/3D),
- Berechnung der Standardellipse (Helmertschen Fehlerellipse),
- Berechnung der Konfidenzellipse auf Basis eines Konfidenzniveaus (alpha) mit Skalierung über die F-Verteilung,
- Berechnung von Konfidenzellipsen im lokalen ENU-System durch Transformation von Qxx → Qxx_ENU,
inkl. Ausgabe/Export tabellarischer Ergebnisse.
"""
@staticmethod
def berechne_s0apost(v: np.ndarray, P: np.ndarray, r: int, print_ausgabe = True) -> float:
"""
Berechnet die a-posteriori Standardabweichung der Gewichtseinheit s₀.
Die a-posteriori Standardabweichung dient als Qualitätsmaß für die
Ausgleichung nach der Methode der kleinsten Quadrate. Dabei beschreibt
r die Redundanz (Freiheitsgrade).
:param v: Residuenvektor der Beobachtungen.
:type v: numpy.ndarray
:param P: Gewichtsmatrix der Beobachtungen.
:type P: numpy.ndarray
:param r: Redundanz bzw. Anzahl der Freiheitsgrade der Ausgleichung.
:type r: int
:return: a-posteriori Standardabweichung der Gewichtseinheit s₀.
:rtype: float
"""
vTPv_matrix = v.T @ P @ v
vTPv = vTPv_matrix.item()
s0apost = np.sqrt(vTPv / r)
if print_ausgabe:
print(f"s0 a posteriori beträgt: {s0apost:.4f}")
return s0apost
@staticmethod
def helmert_punktfehler(Qxx, s0_apost, unbekannten_liste):
"""
Berechnet den Helmertschen Punktfehler (2D/3D) anhand der Standardabweichungen der Koordinaten der Punkte.
Aus der Kofaktor-Matrix der Unbekannten Qxx werden die Kofaktoren punktweise ausgelesen. Durch Multiplikation
mit der a-posteriori Standardabweichung der Gewichtseinheit s₀ werden die Standardabweichungen je Koordinate
(σx, σy, σz) sowie der Helmertsche Punktfehler σP berechnet:
Die Punktzuordnung erfolgt über die Symbolnamen der Unbekanntenliste (z.B. X1, Y1, Z1).
Die Dimension (2D/3D) wird interaktiv per Eingabe abgefragt. Zusätzlich werden die
Ergebnisse als Tabelle ausgegeben und in eine Excel-Datei exportiert.
:param Qxx: Kofaktor-Matrix der Unbekannten.
:type Qxx: numpy.ndarray
:param s0_apost: a-posteriori Standardabweichung der Gewichtseinheit s₀.
:type s0_apost: float
:param unbekannten_liste: Liste der Unbekannten.
:type unbekannten_liste: list
:return: Tabelle mit Standardabweichungen und Helmertschem Punktfehler je Punkt.
:rtype: pandas.DataFrame
:raises ValueError: Wenn eine ungültige Dimension (nicht 2 oder 3) eingegeben wird.
"""
dim = int(input("Helmertscher Punktfehler (2 = 2D, 3 = 3D): "))
diagQ = np.diag(Qxx)
daten = []
namen_str = [str(sym) for sym in unbekannten_liste]
punkt_ids = []
for n in namen_str:
if n.upper().startswith('X'):
punkt_ids.append(n[1:])
for pid in punkt_ids:
try:
idx_x = next(i for i, n in enumerate(namen_str) if n.upper() == f"X{pid}".upper())
idx_y = next(i for i, n in enumerate(namen_str) if n.upper() == f"Y{pid}".upper())
qx = diagQ[idx_x]
qy = diagQ[idx_y]
qz = 0.0
if dim == 3:
try:
idx_z = next(i for i, n in enumerate(namen_str) if n.upper() == f"Z{pid}".upper())
qz = diagQ[idx_z]
except StopIteration:
qz = 0.0
sx = s0_apost * np.sqrt(qx)
sy = s0_apost * np.sqrt(qy)
sz = s0_apost * np.sqrt(qz) if dim == 3 else 0
sP = s0_apost * np.sqrt(qx + qy + qz)
daten.append([pid, float(sx), float(sy), float(sz), float(sP)])
except:
continue
helmert_punktfehler = pd.DataFrame(daten, columns=["Punkt", "σx", "σy", "σz", f"σP_{dim}D"])
display(HTML(helmert_punktfehler.to_html(index=False)))
helmert_punktfehler.to_excel(r"Netzqualitaet\Standardabweichungen_Helmertscher_Punktfehler.xlsx",index=False)
return helmert_punktfehler
@staticmethod
def standardellipse(Qxx, s0_apost, unbekannten_liste):
"""
Berechnet die Standardellipse (Helmertsche Fehlerellipse) für die Punkte aus Qxx und s₀ a posteriori.
Für jeden Punkt werden aus der Kofaktor-Matrix der Unbekannten Qxx die
Kofaktoren von X und Y ausgelesen (qxx, qyy, qyx).
Daraus werden Standardabweichungen σx, σy sowie die Kovarianz σxy bestimmt und
anschließend die Parameter der Standardellipse berechnet:
- Große und kleine Halbachse der Standardellipse,
- Richtungswinkel θ der großen Halbachse in gon.
Die Punktzuordnung erfolgt über die Symbolnamen der Unbekanntenliste (z.B. X1, Y1).
Zusätzlich werden die Ergebnisse tabellarisch ausgegeben und in eine Excel-Datei expoertiert.
:param Qxx: Kofaktor-Matrix der Unbekannten.
:type Qxx: numpy.ndarray
:param s0_apost: a-posteriori Standardabweichung der Gewichtseinheit s₀.
:type s0_apost: float
:param unbekannten_liste: Liste der Unbekannten.
:type unbekannten_liste: list
:return: Tabelle mit Standardabweichungen und Parametern der Standardellipse je Punkt.
:rtype: pandas.DataFrame
"""
Qxx = np.asarray(Qxx, float)
daten = []
namen_str = [str(sym) for sym in unbekannten_liste]
punkt_ids = []
for n in namen_str:
if n.upper().startswith('X'):
punkt_ids.append(n[1:])
for pid in punkt_ids:
try:
idx_x = next(i for i, n in enumerate(namen_str) if n.upper() == f"X{pid}".upper())
idx_y = next(i for i, n in enumerate(namen_str) if n.upper() == f"Y{pid}".upper())
qxx = Qxx[idx_x, idx_x]
qyy = Qxx[idx_y, idx_y]
qyx = Qxx[idx_y, idx_x]
# Standardabweichungen
sx = s0_apost * np.sqrt(qxx)
sy = s0_apost * np.sqrt(qyy)
sxy = (s0_apost ** 2) * qyx
k = np.sqrt((qxx - qyy) ** 2 + 4 * (qyx ** 2))
# Q_dmax/min = 0.5 * (Qyy + Qxx +/- k)
q_dmax = 0.5 * (qyy + qxx + k)
q_dmin = 0.5 * (qyy + qxx - k)
# Halbachsen
s_max = s0_apost * np.sqrt(q_dmax)
s_min = s0_apost * np.sqrt(q_dmin)
# Richtungswinkel theta in gon:
zaehler = 2 * qyx
nenner = qxx - qyy
t_grund = 0.5 * np.arctan(abs(zaehler) / abs(nenner)) * (200 / np.pi)
# Quadrantenabfrage
if nenner > 0 and qyx > 0: # Qxx - Qyy > 0 und Qyx > 0
t_gon = t_grund # 0 - 50 gon
elif nenner < 0 and qyx > 0: # Qxx - Qyy < 0 und Qyx > 0
t_gon = 100 - t_grund # 50 - 100 gon
elif nenner < 0 and qyx < 0: # Qxx - Qyy < 0 und Qyx < 0
t_gon = 100 + t_grund # 100 - 150 gon
elif nenner > 0 and qyx < 0: # Qxx - Qyy > 0 und Qyx < 0
t_gon = 200 - t_grund # 150 - 200 gon
else:
t_gon = 0.0
daten.append([
pid,
float(sx), float(sy), float(sxy),
float(s_max), float(s_min),
float(t_gon)
])
except:
continue
standardellipse = pd.DataFrame(daten, columns=["Punkt", "σx [m]", "σy [m]", "σxy [m]", "Große Halbachse [m]", "Kleine Halbachse [m]", "θ [gon]"])
standardellipse["σx [m]"] = standardellipse["σx [m]"].astype(float).round(4)
standardellipse["σy [m]"] = standardellipse["σy [m]"].astype(float).round(4)
standardellipse["Große Halbachse [m]"] = standardellipse["Große Halbachse [m]"].astype(float).round(4)
standardellipse["Kleine Halbachse [m]"] = standardellipse["Kleine Halbachse [m]"].astype(float).round(4)
standardellipse["θ [gon]"] = standardellipse["θ [gon]"].astype(float).round(3)
display(HTML(standardellipse.to_html(index=False)))
standardellipse.to_excel(r"Netzqualitaet\Standardellipse.xlsx", index=False)
return standardellipse
@staticmethod
def konfidenzellipse(Qxx, s0_apost, unbekannten_liste, R, ausgabe_erfolgt):
"""
Berechnet die Konfidenzellipse für Punkte aus Qxx und einem Konfidenzniveau.
Auf Basis der Kovarianz-Matrix der Unbekannten Qxx und der a-posteriori
Standardabweichung der Gewichtseinheit s₀ werden für jeden Punkt die Parameter
der Konfidenzellipse berechnet. Das Konfidenzniveau wird mittels einer Eingabe
über alpha gewählt (Standard: 0.05 für 95%).
Die Punktzuordnung erfolgt über die Symbolnamen der Unbekanntenliste (z.B. X1, Y1).
Optional wird die Tabelle ausgegeben und als Excel-Datei exportiert, abhängig von
ausgabe_erfolgt.
:param Qxx: Kofaktor-Matrix der geschätzten Unbekannten.
:type Qxx: numpy.ndarray
:param s0_apost: a-posteriori Standardabweichung der Gewichtseinheit s₀.
:type s0_apost: float
:param unbekannten_liste: Liste der Unbekannten.
:type unbekannten_liste: list
:param R: Redundanz (Freiheitsgrade) für die F-Verteilung.
:type R: int
:param ausgabe_erfolgt: Steuert, ob eine Ausgabe/Dateischreibung erfolgen soll (False = Ausgabe).
:type ausgabe_erfolgt: bool
:return: Tabelle der Konfidenzellipse je Punkt, verwendetes alpha.
:rtype: tuple[pandas.DataFrame, float]
:raises ValueError: Wenn alpha nicht in (0, 1) liegt oder nicht in float umgewandelt werden kann.
"""
alpha_input = input("Konfidenzniveau wählen (z.B. 0.05 für 95%, 0.01 für 99%) [Standard=0.05]: ")
if alpha_input.strip() == "":
alpha = 0.05
else:
alpha = float(alpha_input)
print(f"→ Verwende alpha = {alpha} (Konfidenz = {(1 - alpha) * 100:.1f}%)")
Qxx = np.asarray(Qxx, float)
daten = []
namen_str = [str(sym) for sym in unbekannten_liste]
punkt_ids = [n[1:] for n in namen_str if n.upper().startswith('X')]
# Faktor für Konfidenzellipse (F-Verteilung)
kk = float(np.sqrt(2.0 * f.ppf(1.0 - alpha, 2, R)))
for pid in punkt_ids:
try:
idx_x = next(i for i, n in enumerate(namen_str) if n.upper() == f"X{pid}".upper())
idx_y = next(i for i, n in enumerate(namen_str) if n.upper() == f"Y{pid}".upper())
qxx = Qxx[idx_x, idx_x]
qyy = Qxx[idx_y, idx_y]
qyx = Qxx[idx_y, idx_x]
# Standardabweichungen
sx = s0_apost * np.sqrt(qxx)
sy = s0_apost * np.sqrt(qyy)
sxy = (s0_apost ** 2) * qyx
k = np.sqrt((qxx - qyy) ** 2 + 4 * (qyx ** 2))
# Q_dmax/min = 0.5 * (Qyy + Qxx +/- k)
q_dmax = 0.5 * (qyy + qxx + k)
q_dmin = 0.5 * (qyy + qxx - k)
# Halbachsen der Standardellipse
s_max = s0_apost * np.sqrt(q_dmax)
s_min = s0_apost * np.sqrt(q_dmin)
# Halbachsen der Konfidenzellipse
A_K = kk * s_max
B_K = kk * s_min
# Richtungswinkel theta in gon:
zaehler = 2 * qyx
nenner = qxx - qyy
t_grund = 0.5 * np.arctan(abs(zaehler) / abs(nenner)) * (200 / np.pi)
# Quadrantenabfrage
if nenner > 0 and qyx > 0:
t_gon = t_grund # 0 - 50 gon
elif nenner < 0 and qyx > 0:
t_gon = 100 - t_grund # 50 - 100 gon
elif nenner < 0 and qyx < 0:
t_gon = 100 + t_grund # 100 - 150 gon
elif nenner > 0 and qyx < 0:
t_gon = 200 - t_grund # 150 - 200 gon
else:
t_gon = 0.0
daten.append([
pid,
float(sx), float(sy), float(sxy),
float(A_K), float(B_K),
float(t_gon)
])
except:
continue
konfidenzellipse = pd.DataFrame(daten, columns=["Punkt", "σx [m]", "σy [m]", "σxy [m]", "Große Halbachse [m]", "Kleine Halbachse [m]", "θ [gon]"])
konfidenzellipse["Große Halbachse [m]"] = konfidenzellipse["Große Halbachse [m]"].round(4)
konfidenzellipse["Kleine Halbachse [m]"] = konfidenzellipse["Kleine Halbachse [m]"].round(4)
konfidenzellipse["θ [gon]"] = konfidenzellipse["θ [gon]"].round(3)
if ausgabe_erfolgt == False:
display(HTML(konfidenzellipse.to_html(index=False)))
konfidenzellipse.to_excel(r"Netzqualitaet\Konfidenzellipse.xlsx", index=False)
return konfidenzellipse, alpha
@staticmethod
def konfidenzellipsen_enu(a, b, ausgabe_parameterschaetzung, liste_unbekannte, ausgleichungsergebnis, s0apost, r_gesamt):
"""
Berechnet Konfidenzellipsen im lokalen ENU-System aus einer ins ENU-System transformierten Qxx-Matrix.
Die Funktion transformiert zunächst die Kofaktor-Matrix der Unbekannten Qxx
in ein East-North-Up-System (ENU) bezogen auf den Schwerpunkt der verwendeten
Punkte (B0, L0). Anschließend wird auf Basis der transformierten Matrix die
Konfidenzellipse über die Funktion "konfidenzellipse" bestimmt.
Zum Schluss werden Spaltennamen an die ENU-Notation angepasst, Werte gerundet,
tabellarisch ausgegeben und als Excel-Datei exportiert.
:param a: Große Halbachse a des Referenzellipsoids (z.B. WGS84/GRS80) in Metern.
:type a: float
:param b: Große Halbachse b des Referenzellipsoids (z.B. WGS84/GRS80) in Metern.
:type b: float
:param ausgabe_parameterschaetzung: Dictonary der Ergebnisse der Parameterschätzung, muss "Q_xx" enthalten.
:type ausgabe_parameterschaetzung: dict
:param liste_unbekannte: Liste der Unbekannten.
:type liste_unbekannte: list
:param ausgleichungsergebnis: Dictionary der geschätzten Punktkoordinaten (XYZ) zur ENU-Referenzbildung.
:type ausgleichungsergebnis: dict
:param s0apost: a-posteriori Standardabweichung der Gewichtseinheit s₀.
:type s0apost: float
:param r_gesamt: Redundanz (Freiheitsgrade) für die Konfidenzberechnung.
:type r_gesamt: int
:return: Tabelle der Konfidenzellipse im ENU-System, Rotationsmatrix R0 der ENU-Transformation.
:rtype: tuple[pandas.DataFrame, numpy.ndarray]
:raises KeyError: Wenn ``ausgabe_parameterschaetzung`` keinen Eintrag ``"Q_xx"`` enthält.
"""
berechnungen = Berechnungen.Berechnungen(a, b)
# 1) Qxx ins ENU-System transformieren
Qxx_enu, (B0, L0), R0 = Berechnungen.ENU.transform_Qxx_zu_QxxENU(
Qxx=ausgabe_parameterschaetzung["Q_xx"],
unbekannten_liste= liste_unbekannte,
berechnungen=berechnungen,
dict_xyz= ausgleichungsergebnis,
)
print(
f"ENU-Referenz (Schwerpunkt): B0={Einheitenumrechnung.Einheitenumrechnung.rad_to_gon_Decimal(B0):.8f} rad, L0={Einheitenumrechnung.Einheitenumrechnung.rad_to_gon_Decimal(L0):.8f} rad")
# 2) Konfidenzellipse im ENU-System
Konfidenzellipse_ENU, alpha = Genauigkeitsmaße.konfidenzellipse(
Qxx_enu,
s0apost,
liste_unbekannte,
r_gesamt,
ausgabe_erfolgt = True
)
# 3) Spaltennamen anpassen
Konfidenzellipse_ENU = Konfidenzellipse_ENU.rename(columns={
"σx [m]": "σE [m]",
"σy [m]": "σN [m]",
"σxy [m]": "σEN [m]",
"θ [gon]": "θ_EN [gon]"
})
# 4) Runden und Anzeigen
Konfidenzellipse_ENU["σE [m]"] = Konfidenzellipse_ENU["σE [m]"].round(4)
Konfidenzellipse_ENU["σN [m]"] = Konfidenzellipse_ENU["σN [m]"].round(4)
Konfidenzellipse_ENU["Große Halbachse [m]"] = Konfidenzellipse_ENU["Große Halbachse [m]"].round(4)
Konfidenzellipse_ENU["Kleine Halbachse [m]"] = Konfidenzellipse_ENU["Kleine Halbachse [m]"].round(4)
Konfidenzellipse_ENU["θ_EN [gon]"] = Konfidenzellipse_ENU["θ_EN [gon]"].round(4)
display(HTML(Konfidenzellipse_ENU.to_html(index=False)))
# 5) Export
Konfidenzellipse_ENU.to_excel(r"Netzqualitaet\Konfidenzellipse_ENU.xlsx", index=False)
return Konfidenzellipse_ENU, R0
class Plot:
"""Visualisierung geodätischer Netze und Genauigkeitsmaße.
Die Klasse stellt Methoden zur Verfügung für:
- grafische Darstellung von geodätischen Netzen im lokalen ENU-System,
- Visualisierung von Beobachtungen als Verbindungslinien,
- Darstellung von Konfidenzellipsen,
- interaktive Netzdarstellung mit Plotly inklusive Hover-Informationen,
- Skalierung und Layout-Anpassung zur anschaulichen Präsentation von
Lagegenauigkeiten.
Die Klasse dient ausschließlich der Ergebnisvisualisierung und nimmt keine
numerischen Berechnungen vor.
"""
@staticmethod
def netzplot_ellipsen(
Koord_ENU,
unbekannten_labels,
beobachtungs_labels,
df_konf_ellipsen_enu,
skalierung=1000,
n_ellipse_pts=60,
titel="Netzplot im ENU-System mit Konfidenzellipsen"
):
"""
Erstellt einen Netzplot im ENU-System inklusive Konfidenzellipsen, Netzpunkten und Beobachtungslinien.
Die Funktion visualisiert das geodätische Netz im East-North-Up-System (ENU)
mit Plotly. Dabei werden:
- Beobachtungen als Verbindungslinien zwischen Punkten dargestellt, deren Ansicht aus- und eingeschaltet werden kann,
- Konfidenzellipsen je Punkt (Halbachsen und Richtungswinkel),
- Netzpunkte mit Punkt-ID und Koordinaten im Hover-Text angezeigt.
Die Ellipsen werden zur besseren Sichtbarkeit mit einem Faktor "skalierung" vergrößert. Dieser kann angepasst werden.
Der Richtungswinkel wird in gon erwartet und intern nach Radiant umgerechnet.
:param Koord_ENU: Dictionary der Punktkoordinaten im ENU-System.
:type Koord_ENU: dict
:param unbekannten_labels: Liste der Unbekannten zur Ableitung der Punkt-IDs (z.B. X1, Y1, Z1).
:type unbekannten_labels: list
:param beobachtungs_labels: Liste der Beobachtungen zur Ableitung von Verbindungslinien.
:type beobachtungs_labels: list
:param df_konf_ellipsen_enu: DataFrame mit Konfidenzellipsenparametern je Punkt.
:type df_konf_ellipsen_enu: pandas.DataFrame
:param skalierung: Faktor zur visuellen Vergrößerung der Ellipsen im Plot.
:type skalierung: float
:param n_ellipse_pts: Anzahl der Stützpunkte zur Approximation der Ellipse.
:type n_ellipse_pts: int
:param titel: Titel des Plots.
:type titel: str
:return: None
:rtype: None
:raises ValueError: Wenn weder "θ_EN [gon]" noch "θ [gon]" im DataFrame vorhanden ist.
"""
names = [str(s).strip() for s in unbekannten_labels]
if "θ_EN [gon]" in df_konf_ellipsen_enu.columns:
theta_col = "θ_EN [gon]"
elif "θ [gon]" in df_konf_ellipsen_enu.columns:
theta_col = "θ [gon]"
else:
raise ValueError("Spalte 'θ_EN [gon]' oder 'θ [gon]' fehlt im DataFrame.")
punkt_ids = sorted({nm[1:] for nm in names if nm and nm[0].upper() in ("X", "Y", "Z")})
fig = go.Figure()
# 1) Darstellungen der Beobachtungen
beob_typen = {
'GNSS-Basislinien': {'pattern': 'gnss', 'color': 'rgba(255, 100, 0, 0.4)'},
'Tachymeter-Beob': {'pattern': '', 'color': 'rgba(100, 100, 100, 0.3)'}
}
for typ, info in beob_typen.items():
x_l, y_l = [], []
for bl in beobachtungs_labels:
bl_str = str(bl).lower()
is_typ = ((info['pattern'] in bl_str and info['pattern'] != '') or
(info['pattern'] == '' and 'gnss' not in bl_str and 'niv' not in bl_str))
if not is_typ:
continue
bl_raw = str(bl)
pts = []
for pid in punkt_ids:
if (f"_{pid}" in bl_raw) or bl_raw.startswith(f"{pid}_"):
if pid in Koord_ENU:
pts.append(pid)
if len(pts) >= 2:
p1, p2 = pts[0], pts[1]
x_l.extend([Koord_ENU[p1][0], Koord_ENU[p2][0], None]) # E
y_l.extend([Koord_ENU[p1][1], Koord_ENU[p2][1], None]) # N
if x_l:
fig.add_trace(go.Scatter(x=x_l, y=y_l, mode='lines', name=typ,
line=dict(color=info['color'], width=1)))
# 2) Darstellung der Konfidenzellipsen
t = np.linspace(0, 2 * np.pi, n_ellipse_pts)
first = True
for _, row in df_konf_ellipsen_enu.iterrows():
pid = str(row["Punkt"])
if pid not in Koord_ENU:
continue
a = float(row["Große Halbachse [m]"]) * skalierung
b = float(row["Kleine Halbachse [m]"]) * skalierung
theta = float(row[theta_col]) * np.pi / 200.0 # gon->rad
ex = a * np.cos(t)
ey = b * np.sin(t)
c, s = np.cos(theta), np.sin(theta)
xr = c * ex - s * ey
yr = s * ex + c * ey
E0, N0, _ = Koord_ENU[pid]
fig.add_trace(go.Scatter(
x=E0 + xr, y=N0 + yr,
mode="lines",
line=dict(color="red", width=1.5),
name=f"Ellipsen (×{skalierung})",
legendgroup="Ellipsen",
showlegend=first,
hoverinfo="skip"
))
first = False
# 3) Darstellung der Punkte
xs, ys, texts, hovers = [], [], [], []
for pid in punkt_ids:
if pid not in Koord_ENU:
continue
E, N, U = Koord_ENU[pid]
xs.append(E);
ys.append(N);
texts.append(pid)
hovers.append(f"Punkt {pid}<br>E={E:.4f} m<br>N={N:.4f} m<br>U={U:.4f} m")
fig.add_trace(go.Scatter(
x=xs, y=ys, mode="markers+text",
text=texts, textposition="top center",
marker=dict(size=8, color="black"),
name="Netzpunkte",
hovertext=hovers, hoverinfo="text"
))
fig.update_layout(
title=f"{titel} (Ellipsen ×{skalierung})",
xaxis=dict(title="E [m]", scaleanchor="y", scaleratio=1, showgrid=True, gridcolor="lightgrey"),
yaxis=dict(title="N [m]", showgrid=True, gridcolor="lightgrey"),
width=1100, height=900,
template="plotly_white",
plot_bgcolor="white"
)
fig.add_annotation(
text=f"<b>Maßstab Ellipsen:</b><br>Dargestellte Größe = Konfidenzellipse × {skalierung}",
align='left', showarrow=False, xref='paper', yref='paper', x=0.02, y=0.05,
bgcolor="white", bordercolor="black", borderwidth=1
)
fig.write_image(r"Netzqualitaet\netzplot_ellipsen_volle_ausdehnung.png")
return fig
def plot_speichere_aktuelle_ansicht(plot, dateiname=r"Netzqualitaet\netzplot_ellipsen_zoom_ansicht.png"):
aktuelles_layout = plot.layout
export_plot = go.Figure(plot.data, layout=aktuelles_layout)
export_plot.write_image(dateiname, scale=2)
print(f"✅ Aktuelle Ansicht wurde erfolgreich gespeichert: {dateiname}")
x_bereich = aktuelles_layout.xaxis.range
y_bereich = aktuelles_layout.yaxis.range
if x_bereich and y_bereich:
print(
f"Ausschnitt: E[{x_bereich[0]:.2f} bis {x_bereich[1]:.2f}], N[{y_bereich[0]:.2f} bis {y_bereich[1]:.2f}]")
else:
print("Hinweis: Es wurde die Standardansicht (kein Zoom) gespeichert.")