From 1dbb08a1504257d9ca1413d50ff71f2fb60cd89b Mon Sep 17 00:00:00 2001 From: Stefan Krebs Date: Wed, 11 Feb 2026 13:33:36 +0100 Subject: [PATCH 01/10] Flyt info-kommandogruppe til sit eget modul --- src/fire/cli/info/__init__.py | 9 +++++++++ src/fire/cli/{info.py => info/_info.py} | 9 +-------- 2 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 src/fire/cli/info/__init__.py rename src/fire/cli/{info.py => info/_info.py} (99%) diff --git a/src/fire/cli/info/__init__.py b/src/fire/cli/info/__init__.py new file mode 100644 index 00000000..d1a24d9f --- /dev/null +++ b/src/fire/cli/info/__init__.py @@ -0,0 +1,9 @@ + +import click + +@click.group() +def info(): + """ + Information om objekter i FIRE + """ + pass \ No newline at end of file diff --git a/src/fire/cli/info.py b/src/fire/cli/info/_info.py similarity index 99% rename from src/fire/cli/info.py rename to src/fire/cli/info/_info.py index e9ddac80..112185ba 100644 --- a/src/fire/cli/info.py +++ b/src/fire/cli/info/_info.py @@ -13,6 +13,7 @@ from pyproj.exceptions import CRSError import fire.cli +from fire.cli.info import info from fire.ident import klargør_ident_til_søgning from fire.io.geojson import skriv_sagsrapport_geojson from fire.api.model import ( @@ -37,14 +38,6 @@ DATE_FORMAT = "%d-%m-%Y" -@click.group() -def info(): - """ - Information om objekter i FIRE - """ - pass - - def observation_linje(obs: Observation) -> str: if obs.observationstypeid > 2: return "" From 5e91b14b72a841354150c08329a46e00ebce91a5 Mon Sep 17 00:00:00 2001 From: Stefan Krebs Date: Wed, 11 Feb 2026 13:38:06 +0100 Subject: [PATCH 02/10] Udstil info-kommandoer --- src/fire/cli/info/__init__.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/fire/cli/info/__init__.py b/src/fire/cli/info/__init__.py index d1a24d9f..606ef7fb 100644 --- a/src/fire/cli/info/__init__.py +++ b/src/fire/cli/info/__init__.py @@ -1,9 +1,26 @@ - import click + @click.group() def info(): """ Information om objekter i FIRE """ - pass \ No newline at end of file + pass + + +# Udstil kommandoer +from fire.cli.info._info import ( + punkt, + punktsamling, + srid, + obstype, + infotype, + sag, + sagsevent, +) + +# ... og visse hjælpefunktioner som bruges andre steder +from fire.cli.info._info import ( + punktinforapport, +) From a65cab5f4a22363cd08c463ccc108401972160a0 Mon Sep 17 00:00:00 2001 From: Stefan Krebs Date: Wed, 7 Jan 2026 10:45:44 +0100 Subject: [PATCH 03/10] =?UTF-8?q?Tilf=C3=B8j=20hent=5Fpunkter=5Fmed=5Fflag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fire/api/firedb/hent.py | 45 +++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/fire/api/firedb/hent.py b/src/fire/api/firedb/hent.py index a41b838a..75b559b2 100644 --- a/src/fire/api/firedb/hent.py +++ b/src/fire/api/firedb/hent.py @@ -19,6 +19,7 @@ PunktSamling, PunktInformation, PunktInformationType, + PunktInformationTypeAnvendelse, GeometriObjekt, Grafik, Observation, @@ -145,6 +146,50 @@ def hent_punkter( return result + def hent_punkter_med_flag( + self, infotype: PunktInformationType, inkluder_historiske: bool = False + ) -> list[Punkt]: + """ + Returnerer alle punkter der har en given FLAG-infotype + + Bruges fx til at hente alle 5D-punkterne (NET:5D) eller alle punkter på Færøerne + (REGION:FO). Sættes `inkluder_historiske` til True, så søges der også iblandt + afregistrede attributter. + + Der kan potentielt returneres mange punkter med denne, så brug den klogt. Det + frarådes derfor at søge på de meget almindelige infotyper, som fx. REGION:DK eller + ATTR:højdefikspunkt, da disse vil resultere i ca. 650.000 hhv. 250.000 punkter. + + Hvis intet punkt findes udsendes en NoResultFound exception. + """ + + if not infotype.anvendelse == PunktInformationTypeAnvendelse.FLAG: + raise ValueError(f"Ugyldig infotype, fik {infotype}.") + + query = ( + self.session.query(Punkt) + .options( + joinedload(Punkt.geometriobjekter), + joinedload(Punkt.koordinater), + ) + .join(PunktInformation) + .join(PunktInformationType) + .filter( + PunktInformationType.name == infotype.name, + Punkt._registreringtil == None, # NOQA + ) + ) + + if not inkluder_historiske: + query = query.filter(PunktInformation._registreringtil == None) + + result = query.all() + + if not result: + raise NoResultFound(f"Ingen punkter med attributten {infotype.name} fundet") + + return result + def hent_punkter_fra_uuid_liste(self, uuids: List[str]): """ Hent alle punkter med punkt ID'er matchende listen `uuids`. From 1c827887845054cf15c0a349e804cc592de47061 Mon Sep 17 00:00:00 2001 From: Stefan Krebs Date: Wed, 7 Jan 2026 10:47:20 +0100 Subject: [PATCH 04/10] =?UTF-8?q?Tilf=C3=B8j=20bestem=5Fidenttype=20hj?= =?UTF-8?q?=C3=A6lpefunktion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fire/ident.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/fire/ident.py b/src/fire/ident.py index 48aba885..a0b68225 100644 --- a/src/fire/ident.py +++ b/src/fire/ident.py @@ -6,6 +6,8 @@ import re from typing import Iterable +from fire.api.model.punkttyper import Ident + # Vær mindre pedantisk mht. foranstillede nuller hvis identen er et landsnummer LANDSNUMMERMØNSTER = re.compile("^[0-9]*-[0-9]*-[0-9]*$") @@ -146,6 +148,20 @@ def klargør_ident_til_søgning(ident: str) -> str: return ident +def bestem_identtype(ident: str) -> Ident.IdentType: + if ( + kan_være_landsnummer(ident) or + kan_være_købstadsnummer(ident) or + kan_være_vandstandsbræt(ident) + ): + return Ident.IdentType.LANDSNR + + if kan_være_gnssid(ident): + return Ident.IdentType.GNSS + + if kan_være_gi_nummer(ident): + return Ident.IdentType.GI + def klargør_identer_til_søgning(identer: Iterable[str]): """Klargør flere identer til søgning.""" From 6fae2efaf96f977aec6f123682960b32580ba3f0 Mon Sep 17 00:00:00 2001 From: Stefan Krebs Date: Wed, 7 Jan 2026 10:54:52 +0100 Subject: [PATCH 05/10] Anvend klassen Identtype til at hente identer --- src/fire/api/model/punkttyper.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/fire/api/model/punkttyper.py b/src/fire/api/model/punkttyper.py index 2d134285..cb7b32be 100644 --- a/src/fire/api/model/punkttyper.py +++ b/src/fire/api/model/punkttyper.py @@ -190,7 +190,7 @@ class Punkt(FikspunktregisterObjekt): @reconstructor def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._identer = [] + self._identer: list[Ident] = [] def _populer_identer(self): """ @@ -265,11 +265,9 @@ def tabtgået(self) -> bool: return True return False - def _hent_ident_af_type(self, identtype: str) -> str: - numre = [] - for punktinfo in self.punktinformationer: - if punktinfo.infotype.name == identtype and not punktinfo.registreringtil: - numre.append(punktinfo.tekst) + def _hent_ident_af_type(self, identtype: Ident.IdentType) -> str: + self._populer_identer() + numre = [ident.tekst for ident in self._identer if ident._type == identtype] if numre: return sorted(numre)[0] @@ -291,7 +289,7 @@ def gældende_koordinat(self, srid: str) -> Koordinat: @property def landsnummer(self) -> str: - _landsnummer = self._hent_ident_af_type("IDENT:landsnr") + _landsnummer = self._hent_ident_af_type(Ident.IdentType.LANDSNR) if _landsnummer: return _landsnummer @@ -300,11 +298,11 @@ def landsnummer(self) -> str: @property def gnss_navn(self) -> str: - return self._hent_ident_af_type("IDENT:GNSS") + return self._hent_ident_af_type(Ident.IdentType.GNSS) @property def jessennummer(self) -> str: - return self._hent_ident_af_type("IDENT:jessen") + return self._hent_ident_af_type(Ident.IdentType.JESSEN) def __lt__(self, other: Punkt) -> bool: return self.landsnummer < other.landsnummer From ca3d530cb766a25607a3786027f3f612c58ea668 Mon Sep 17 00:00:00 2001 From: Stefan Krebs Date: Wed, 7 Jan 2026 10:59:20 +0100 Subject: [PATCH 06/10] =?UTF-8?q?Tilf=C3=B8j=20funktion=20til=20print=20af?= =?UTF-8?q?=20flere=20tidsserier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit og andre små rettelser --- src/fire/cli/ts/__init__.py | 109 +++++++++++++++++++++++++++++++++--- 1 file changed, 102 insertions(+), 7 deletions(-) diff --git a/src/fire/cli/ts/__init__.py b/src/fire/cli/ts/__init__.py index ce00a6fe..67f0ddcc 100644 --- a/src/fire/cli/ts/__init__.py +++ b/src/fire/cli/ts/__init__.py @@ -1,4 +1,5 @@ from datetime import datetime +import re import click import pandas as pd @@ -17,6 +18,7 @@ Punkt, PunktSamling, Koordinat, + Srid, ) @@ -28,6 +30,29 @@ def ts(): pass +def bestem_labels(srid: Srid): + """Bestem kolonnenavne til en tabel ud fra en Srid""" + + tid = ["t", "decimalår"] + xyz = ["x", "y", "z"] + sxyz = ["sx", "sy", "sz"] + labels_xyz = [srid.x, srid.y, srid.z] + labels_sxyz = ["sx [mm]", "sy [mm]", "sz [mm]"] + + # Fjern de steder hvor srid.x y eller z er None + indekser = [i for i, lab in enumerate(labels_xyz) if lab is not None] + + xyz = [xyz[i] for i in indekser] + sxyz = [sxyz[i] for i in indekser] + labels_xyz = [labels_xyz[i] for i in indekser] + labels_sxyz = [labels_sxyz[i] for i in indekser] + + parms = tid + xyz + sxyz + labels = tid + labels_xyz + labels_sxyz + + return parms, labels + + def _print_tidsserieoversigt( tidsserier: list[Tidsserie], ) -> None: @@ -54,6 +79,7 @@ def foretrukken_ident(ts: Tidsserie): console = Console() console.print(tabel) + def _udtræk_tidsserie( objekt: str, tidsserieklasse: type[Tidsserie], @@ -80,7 +106,9 @@ def _udtræk_tidsserie( srid_filter = lambda ts: True # Prøv først at søge med objekt som søgestreng på tidsserienavn og filtrer på srid - tidsserier = fire.cli.firedb.hent_tidsserier(objekt, tidsserieklasse=tidsserieklasse) + tidsserier = fire.cli.firedb.hent_tidsserier( + objekt, tidsserieklasse=tidsserieklasse + ) tidsserier = [ts for ts in tidsserier if srid_filter(ts)] # Hvis ingen tidsserier, prøver vi med objekt som ident @@ -91,7 +119,11 @@ def _udtræk_tidsserie( raise SystemExit("Punkt eller tidsserie ikke fundet") else: # Udtræk punktets tidsserier og filtrer på ts-type og srid - tidsserier=[ts for ts in punkt.tidsserier if isinstance(ts, tidsserieklasse) and srid_filter(ts)] + tidsserier = [ + ts + for ts in punkt.tidsserier + if isinstance(ts, tidsserieklasse) and srid_filter(ts) + ] if not tidsserier: raise SystemExit("Fandt ingen tidsserier") @@ -100,7 +132,7 @@ def _udtræk_tidsserie( _print_tidsserieoversigt(tidsserier) # Hvis der kun blev fundet én tidsserie så printer vi den - if len(tidsserier)==1: + if len(tidsserier) == 1: _print_tidsserie(tidsserier[0], parametre_alle, parametre, fil) @@ -110,7 +142,7 @@ def _print_tidsserie( parametre: str, fil: click.Path, ): - """Print en tabel over en tidsserie med de givne parametre """ + """Print en tabel over én tidsserie med de givne parametre""" if parametre.lower() == "alle": parametre = ",".join(parametre_alle.keys()) @@ -124,7 +156,70 @@ def _print_tidsserie( overskrifter.append(p) kolonner.append(tidsserie.__getattribute__(parametre_alle[p])) - tabel = Table(*overskrifter, box=box.SIMPLE) + _print_tabel( + overskrifter, + kolonner, + ) + + if not fil: + return + + _gem_tabel(overskrifter, kolonner, fil) + + +def _print_tidsserier( + tidsserier: list[Tidsserie], + fil: click.Path, +): + """ + Print en tabel over flere tidsserier med de givne parametre + + Alle tidsserierne skal have samme Srid + """ + srid = tidsserier[0].srid + if not all(ts.srid == srid for ts in tidsserier): + fire.cli.print("Fejl: Alle tidsserierne skal have samme Srid", fg="red") + raise SystemExit(1) + + overskrifter = ["Navn", "Srid"] + + # Bestem kolonnenavne ud fra Sriden på første Tidsserie + parametre, labels = bestem_labels(srid) + overskrifter.extend(labels) + + kolonner = [[] for i in range(len(overskrifter))] + navne, srider = zip( + *[ + (ts.navn, (ts.srid.kortnavn or ts.srid.name)) + for ts in tidsserier + for _ in range(len(ts)) + ] + ) + kolonner[0] = navne + kolonner[1] = srider + for ts in tidsserier: + for idx, p in enumerate(parametre, 2): + kolonner[idx].extend(ts.__getattribute__(p)) + + _print_tabel( + overskrifter, + kolonner, + ) + + if not fil: + return + + _gem_tabel(overskrifter, kolonner, fil) + + +def _print_tabel(overskrifter: list, kolonner: list[list]): + + # Erstat "[" med "\\[" så console.Print ikke opfatter det der står inde i [parentesen] + # som et "markup tag", se https://rich.readthedocs.io/en/latest/markup.html# + # Tiltænkt steder hvor kolonnen fx hedder "Kote [m]" eller "sz [mm]" + overskrifter = [re.sub(r"\[(?=.*\])", "\\[", o) for o in overskrifter] + + tabel = Table(*overskrifter, box=box.SIMPLE, header_style="") data = list(zip(*kolonner)) def klargør_celle(input): @@ -134,6 +229,7 @@ def klargør_celle(input): return f"{input:.4f}" if not input: return "" + return str(input) for række in data: tabel.add_row( @@ -143,9 +239,8 @@ def klargør_celle(input): console = Console() console.print(tabel) - if not fil: - raise SystemExit +def _gem_tabel(overskrifter: list, kolonner: list[list], fil: click.Path): data = { overskrift: kolonne for (overskrift, kolonne) in zip(overskrifter, kolonner) } From 37970f2aec60fbf13db1208e7873bcd68288a170 Mon Sep 17 00:00:00 2001 From: Stefan Krebs Date: Tue, 10 Feb 2026 15:37:52 +0100 Subject: [PATCH 07/10] Juster plot af tidsserier --- src/fire/cli/ts/plot_ts.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/fire/cli/ts/plot_ts.py b/src/fire/cli/ts/plot_ts.py index df0ccf25..c56fd241 100644 --- a/src/fire/cli/ts/plot_ts.py +++ b/src/fire/cli/ts/plot_ts.py @@ -46,6 +46,7 @@ def plot_tidsserie( ts: Tidsserie, plot_funktion: Callable, parametre: list = ["n", "e", "u"], + ylabels: list = [], y_enhed: str = "m", ): """ @@ -54,27 +55,25 @@ def plot_tidsserie( Denne funktion håndterer figuropsætningen, og kalder ``plot_funktion``, som forventes at foretage selve plottingen af data. """ - n_parm = min(len(parametre), 3) + # Plot max 3 parametre + parametre = parametre[:3] + n_parm = len(parametre) + + if not ylabels: + ylabels = [TS_PLOTTING_LABELS.get(parm, parm) for parm in parametre] skalafaktor = ENHEDER_SKALAFAKTOR[y_enhed] ax = plt.figure() plt.suptitle(ts.navn) - for i, parm in enumerate(parametre, start=1): - if i > 3: - break + for i, (parm, ylabel) in enumerate(zip(parametre, ylabels), start=1): y = [skalafaktor * yy for yy in getattr(ts, parm)] - try: - label = TS_PLOTTING_LABELS[parm] - except KeyError: - label = parm - ax = plt.subplot(int(f"{n_parm}{1}{i}")) plot_funktion(ts.decimalår, y, y_enhed=y_enhed) - plt.ylabel(f"{label} [{y_enhed}]") + plt.ylabel(f"{ylabel} [{y_enhed}]") plt.grid() # Vis kun xlabel for nederste subplot @@ -330,9 +329,9 @@ def plot_data(x: list, y: list, **kwargs): plt.plot( x, y, - ".", - markersize=4, - color="black", + "o", + markersize=6, + color="blue", ) @@ -452,7 +451,7 @@ def plot_tidsserier( markersize=p[0].get_markersize() * 2, ) - if len(y)<2: + if len(y) < 2: plt.text( x[-1] + 2e-1, y[-1], From d330f09a9de616e954c95d6affe431a92145537e Mon Sep 17 00:00:00 2001 From: Stefan Krebs Date: Tue, 10 Feb 2026 15:42:54 +0100 Subject: [PATCH 08/10] =?UTF-8?q?Tilf=C3=B8j=20ny=20kommando:=20fire=20inf?= =?UTF-8?q?o=20koordinater?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mht. visning af historiske koordinater i tabeller og plots som tidsserier, så anvendes klassen GNSSTidsserie som beholder til koordinaterne. Her skal man passe på ikke at blive fristet til at anvende GNSSTidsserie's mulighed for at vise koordinaterne som n,e,u, da dette kun vil virke hvis tidsseriens oprindelige x,y,z værdier er geocentriske. I denne kontekst kan x,y,z værdierne dog i princippet være hvad som helst. Dette er svagheden ved at vi bruger GNSSTidsserie-klassen som container for de transformerede koordinater. Vi mangler måske en mere generisk Tidsserie-klasse som bare indholder x,y,z- attributter der ikke er begrænset til geocentrisk, som GNSSTidsserie så kan nedarve fra. --- src/fire/cli/info/_koordinater.py | 418 ++++++++++++++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 src/fire/cli/info/_koordinater.py diff --git a/src/fire/cli/info/_koordinater.py b/src/fire/cli/info/_koordinater.py new file mode 100644 index 00000000..839ee52f --- /dev/null +++ b/src/fire/cli/info/_koordinater.py @@ -0,0 +1,418 @@ +import click +from pyproj import CRS, Transformer +from pyproj.exceptions import ProjError +from sqlalchemy.orm.exc import NoResultFound + +import fire.cli +from fire.cli.info import info +from fire.api.model import Punkt, Srid +from fire.api.model.tidsserier import GNSSTidsserie +from fire.cli.ts import ( + _print_tidsserier, +) +from fire.cli.ts.plot_ts import ( + plot_data, + plot_tidsserie, +) +from fire.ident import klargør_identer_til_søgning, bestem_identtype + + +@info.command() +@click.option( + "-s", + "--source", + "sources", + type=str, + default="DVR90", + callback=lambda ctx, param, val: [s.strip() for s in val.split(",")], + help="Vælg koordinat-type der skal udtrækkes. Vælg flere srider, ved at angive dem som en kommasepareret liste.", +) +@click.option( + "-t", + "--target", + type=str, + default="", + help="Vælg koordinatsystem der skal transformeres til.", +) +@click.option( + "-H", + "--historik", + is_flag=True, + default=False, + help="Udskriv også ikke-gældende (historiske) elementer", +) +@click.option( + "-P", + "--plot", + is_flag=True, + default=False, + help="Plot koordinater som tidsserie. Hvis denne er sat, skal ``--historik/-H`` også være sat.", +) +@click.option( + "-f", + "--fil", + required=False, + type=click.Path(), + help="Skriv den udtrukne tidsserie til Excel fil.", +) +@click.argument( + "objekter", + required=True, + type=str, + nargs=-1, +) +@fire.cli.default_options() +def koordinater( + objekter: list[str], + sources: list[str], + target: str, + historik: bool, + plot: bool, + fil: click.Path, + **kwargs, +) -> None: + """ + Udtræk koordinater for ét eller flere punkter + + Med **OBJEKTER** angives en liste af punkter eller fikspunktsnet som skal udtrækkes. + Vælg fx. NET:5D, NET:DMI, eller NET:RTKCONNECT for at udtrække koordinater til hhv. + 5D-punkter, DMIs vandstandsmålere eller RTKConnects referencestationer. Se en fuld + liste over nettene med ``fire info infotype NET``. + + Som standard udtrækkes den gældende DVR90-kote for hvert af de valgte punkter. + + Med parameteren ``--source/-s`` kan angives en alternativ koordinat-type som skal + udtrækkes. Der kan vælges alle srid'er som findes i FIRE, se en liste med ``fire info + srid``. Dog kan der ikke vælges tidsserie-koordinattyper. Hertil skal anvendes ``fire + ts hts`` og ``fire ts gnss``. + + Parameteren ``--target/-t`` bruges til at angive referencerammen som de udtrukne + koordinater skal transformeres til og vises i. Den valgte referenceramme skal kunne + fortolkes af PROJ. + + Der kan vælges flere ``--source`` koordinatsystemer ved at angive dem som en + kommasepareret liste. De vil da alle blive transformeret til det valgte ``--target`` + koordinatsystem. I tilfælde af man ikke har valgt noget ``--target``, så sættes target + til det sidst angivne ``source``-system. + + Programmet gør opmærksom på, hvis transformationerne er af typerne "noop" (no + operation) eller "ballpark". Begge typer angiver, at der reelt ikke foretages en + transformation, og at de resulterende koordinater derfor ikke er nøjagtige. + + Med parameteren ``--historik/-H`` tilvælges historiske koordinater. Dette er fravalgt + som standard, så der derved kun udtrækkes gældende koordinater. + + Hvis historik er tilvalgt, kan man med ``--plot/-P`` desuden få vist de resulterende + tidsserier i et simpelt plot. Ønskes mere avancerede plots og anden analyse, kan + resultaterne gemmes som excel-fil ved at angive et outputfilnavn med ``--fil/-f``. + + + \b **EKSEMPLER** + + Vis gældende DVR90-kote for ALBN, BFYR og CHAK:: + + fire info koordinater ALBN BFYR CHAK + + Vis historiske DVR90-koter for ALBN, BFYR og CHAK:: + + fire info koordinater ALBN BFYR CHAK -H + + Vis historiske DVR90-koter for ALBN, BFYR og CHAK og gem som fil:: + + fire info koordinater ALBN BFYR CHAK -H -f "ABC.xlsx" + + Vis historiske ETRS89-koordinater for ALBN, BFYR og CHAK:: + + fire info koordinater ALBN BFYR CHAK -H -s EPSG:4937 + + Udtræk gældende ETRS89 koordinater for alle GPSNET punkterne og vis dem i UTM32N + + DVR90(2023):: + + fire info koordinater NET:GPSNET -s EPSG:4937 -t EPSG:25832+EPSG:10485 + + Udtræk alle IGb08, IGS14 og IGS20 koordinater for 5D-punkterne, transformer til IGS20 + geografiske koordinater, og plot:: + + fire info koordinater NET:5D -s IGb08,IGS14,IGS20 -t EPSG:10177 -H -P + + For mere info om hvilke transformationer som programmet foretager, kan anvendes + `projinfo` der kaldes med de samme parametre som denne kommando:: + + projinfo -s EPSG:4937 -t EPSG:25832+EPSG:10485 + + """ + if not target: + target = sources[0] + sources, target = oversæt_srid_alias(sources, target) + + transformers = klargør_transformationer(sources, target) + + punkter_identer = håndter_punkter(list(objekter)) + + tidsserier = konstruer_tidsserier(punkter_identer, transformers, historik) + + if not tidsserier: + fire.cli.print( + f"Fejl: Ingen af punkterne har koordinater i det valgte referencesystem.", + fg="red", + ) + raise SystemExit(1) + + _print_tidsserier(tidsserier, fil) + + if not (historik and plot): + return + + # Vi plotter bare tidsseriens x,y,z værdier som de er, (medmindre de er None) da de + # umiddelbart er de eneste vi er sikre på eksisterer. + for ts in tidsserier: + parms = [ + parm + for parm, dim in zip("xyz", [ts.srid.x, ts.srid.y, ts.srid.z]) + if dim is not None + ] + plot_tidsserie(ts, plot_data, parametre=parms, y_enhed="m") + + +def håndter_punkter(objekter: list[str]) -> list[tuple[Punkt, str]]: + """ + Omsæt `objekter` til en liste af punkter. + + `objekter` kan indeholde identer eller navne på "NET"-infotyper, fx. NET:5D. + + Hvert punkt matches med dén ident som ligger tættest på den søgestreng som punktet + blev fundet med, hvilket kan bruges til visning i tabeller, plots etc. + """ + nets = [ + objekter.pop(i) for i, o in enumerate(objekter) if o.upper().startswith("NET:") + ] + identer = objekter + + identer = klargør_identer_til_søgning(identer) + fire.cli.print("Søg i databasen efter punkter til hver ident", fg="yellow") + punkter: list[Punkt] = fire.cli.firedb.hent_punkt_liste( + identer, ignorer_ukendte=False + ) + + # Ingen punkter bliver sprunget over da vi ovenfor har sat ignorer_ukendte=False + # Vi kan derfor antage at punkter og identtyper kommer i samme rækkefølge + identtyper = [bestem_identtype(ident) for ident in identer] + punkter_identer = [ + ( + p, + p._hent_ident_af_type(it) if it is not None else p.ident, + ) # hvis identtypen ikke kunne bestemmes bruger vi bare den almindelige ident + for p, it in zip(punkter, identtyper) + ] + + for net in nets: + infotype = fire.cli.firedb.hent_punktinformationtype(net) + punkter = fire.cli.firedb.hent_punkter_med_flag(infotype) + punkter_identer.extend([(p, p.ident) for p in punkter]) + + punkter_identer = sorted(punkter_identer, key=lambda x: x[1]) + + return punkter_identer + + +def oversæt_srid_alias(sources: list[str], target: str = None) -> tuple[list[str], str]: + """ + Oversæt srid-alias til sridens rigtige navn + + Hvis alias (kortnavn) ikke kan oversættes, returneres inputtet som det blev givet. + """ + srids_med_alias = ( + fire.cli.firedb.session.query(Srid).filter(Srid.kortnavn != None).all() + ) + srids_alias_mapper = {s.kortnavn: s.name for s in srids_med_alias} + + sources = [srids_alias_mapper.get(s, s) for s in sources] + target = srids_alias_mapper.get(target, target) + + return sources, target + + +def klargør_transformationer( + sources: list[str], target: str +) -> dict[Srid, dict[Srid, Transformer]]: + """ + Klargør transformationer fra alle sources til target + + Returnerer en dict-over-dict der mapper alle source-target kombinationer til den + rette transformation, fx.: + { + src_1: { + trg_1: transformation_11 + }, + src_2: { + trg_1: transformation_21, + }, + } + """ + try: + transformers = { + fire.cli.firedb.hent_srid(src := source): { + target: (trans := lav_transformer(source, target)) + } + for source in sources + } + except NoResultFound: + fire.cli.print(f"Fejl: Source-srid '{src}' ikke fundet!", fg="red") + raise SystemExit(1) + + try: + target_srid = fire.cli.firedb.hent_srid(target) + except NoResultFound: + # Konstruér en ny Srid ud fra target_crs + target_srid = Srid_fra_CRS(trans.target_crs, navn=target) + + # Konstruér ny dict med srids som nøgler + transformers = { + src: {target_srid: trans for trg, trans in trg_trans.items()} + for src, trg_trans in transformers.items() + } + return transformers + + +def lav_transformer(s_crs: str | CRS, t_crs: str | CRS) -> Transformer: + """Opret Transformer-objekt""" + if s_crs == t_crs: + return Transformer.from_pipeline("+proj=noop") + + try: + transformer = Transformer.from_crs(crs_from=s_crs, crs_to=t_crs, always_xy=True) + except ProjError as e: + fire.cli.print( + f"Fejl: Kan ikke transformere fra {s_crs} til {t_crs}. Mulig årsag:", + fg="red", + ) + fire.cli.print(e) + raise SystemExit(1) + + if "proj=noop" in transformer.definition: + fire.cli.print( + f'Bemærk: Klargjorde en "noop" transformation fra {s_crs} til {t_crs}', + fg="yellow", + ) + elif "ballpark" in transformer.description.lower(): + fire.cli.print( + f'Bemærk: Klargjorde en "ballpark" transformation fra {s_crs} til {t_crs}', + fg="yellow", + ) + else: + fire.cli.print( + f"Klargjorde transformation fra {s_crs} til {t_crs}.", + ) + + return transformer + +# Her fastsættes dicts til oversættelse af PROJ's interne akse- og enhedsnavne +# til nogle mere kortfattede og danske navne. +danske_akser = { + "Easting": "Easting", + "Northing": "Northing", + "Westing": "Westing", + "Southing": "Southing", + "Geodetic latitude": "Breddegrad", + "Geodetic longitude": "Længdegrad", + "Geocentric X": "X", + "Geocentric Y": "Y", + "Geocentric Z": "Z", + "Ellipsoidal height": "Ellipsoidehøjde", + "Gravity-related height": "Kote", + "Depth": "Dybde", +} +danske_enheder = { + "metre": "[m]", + "degree": "[decimalgrader]", + "degree minute second hemisphere": "[°]", +} + + +def Srid_fra_CRS(crs: CRS, navn: str) -> Srid: + """Oversæt en pyproj.CRS til en Srid""" + + axes = [danske_akser.get(ax.name, ax.name) for ax in crs.axis_info] + units = [danske_enheder.get(ax.unit_name, "") for ax in crs.axis_info] + + akser_enheder = [f"{ax} {un}" for ax, un in zip(axes, units)] + + x, y, z = None, None, None + if len(akser_enheder) == 1: + (z,) = akser_enheder + elif len(akser_enheder) == 2: + x, y = akser_enheder + else: + x, y, z = akser_enheder + + return Srid(name=navn, x=x, y=y, z=z) + + +def konstruer_tidsserier( + punkter_identer: list[tuple[Punkt, str]], + transformers: dict[Srid, dict[Srid, Transformer]], + historik: bool, +) -> list[GNSSTidsserie]: + """ + Konstruér tidsserier til visning i tabeller og plots + + For hvert punkt i listen `punkter_identer` udtrækkes alle koordinater med srid'er + svarende til source_srid-nøglerne givet i `transformers`. + + Koordinaterne transformeres derefter via de givne "transformers", til `target_srid`, + og gemmes i en tidsserie til hvert punkt. + """ + if historik: + historik_filter = lambda k: True + else: + historik_filter = lambda k: k.registreringtil is None + + # Tag target_srid fra første transformation + target_srid = [ + trg for trg_trans in transformers.values() for trg in trg_trans.keys() + ][0] + + # Konstruér tidsserier ud fra punkternes koordinater + tidsserier = [] + for punkt, matchet_ident in punkter_identer: + + tidsserie = GNSSTidsserie( + punkt=punkt, + navn=f"{matchet_ident}", + formål=f"", + srid=target_srid, + ) + + # Hver af punktets koordinater transformeres via dets Srid til det valgte + # output-crs, og tilføjes til tidsserien + koordinater = [] + for k in punkt.koordinater: + if not ( + k.srid in transformers.keys() + and k.fejlmeldt == False + and historik_filter(k) + ): + continue + + # Transformér koordinaterne. Vi ændrer deres x,y,z værdier in-place. Det er + # stadig database-objekter, så man skal IKKE committe noget fra denne session. + # Man vil nok blive reddet af database-triggerne, som forbyder updates, men + # stadig... + k.x, k.y, k.z = transformers[k.srid][target_srid].transform( + k.x or 0.0, k.y or 0.0, k.z or 0.0 + ) + k.transformeret = True + k.srid = target_srid + + koordinater.append(k) + + if not koordinater: + fire.cli.print( + f"Springer {matchet_ident} over. Fandt ingen koordinater.", + ) + continue + + tidsserie.koordinater = sorted(koordinater, key=(lambda k: k.t)) + tidsserier.append(tidsserie) + + return tidsserier From 461a77fd8a896584207bda71950a6e8416553cf8 Mon Sep 17 00:00:00 2001 From: Stefan Krebs Date: Wed, 11 Feb 2026 13:53:40 +0100 Subject: [PATCH 09/10] Udstil info koordinater i info.__init__.py --- src/fire/cli/info/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/fire/cli/info/__init__.py b/src/fire/cli/info/__init__.py index 606ef7fb..2b563c2e 100644 --- a/src/fire/cli/info/__init__.py +++ b/src/fire/cli/info/__init__.py @@ -19,6 +19,9 @@ def info(): sag, sagsevent, ) +from fire.cli.info._koordinater import ( + koordinater +) # ... og visse hjælpefunktioner som bruges andre steder from fire.cli.info._info import ( From 07679bf1773c9957309be1c34bd7e0b794050065 Mon Sep 17 00:00:00 2001 From: Stefan Krebs Date: Wed, 11 Feb 2026 15:53:52 +0100 Subject: [PATCH 10/10] =?UTF-8?q?F=C3=B8j=20info=20koordinater=20til=20dok?= =?UTF-8?q?umentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/apps/info.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/apps/info.rst b/docs/apps/info.rst index 8791d1dc..173ed20a 100644 --- a/docs/apps/info.rst +++ b/docs/apps/info.rst @@ -12,6 +12,10 @@ typer indhold i FIRE databasen. :prog: fire info punkt :nested: full +.. click:: fire.cli.info:koordinater + :prog: fire info koordinater + :nested: full + .. click:: fire.cli.info:sag :prog: fire info sag :nested: full