Im Beitrag #64 habe ich gezeigt, wie ilitransformer Objekte aus einem INTERLIS-Modell in ein anderes überführt. Das Beispiel war absichtlich überschaubar: Quellobjekt lesen, Attribute zuweisen, Zielobjekt schreiben. Ein paar Funktionen wie trim() oder upper() genügten, um das Prinzip zu erklären.

Diesmal geht es um eine ganz andere Liga: die Transformation des alten Datenmodells der amtlichen Vermessung DM01 ins neue DMAV – und wieder zurück.

Das ist nicht einfach die grössere Ausgabe derselben Übung. DM01 und DMAV unterscheiden sich nicht nur bei Klassen- und Attributnamen. Es sind unterschiedliche Sprachversionen, unterschiedliche Transferformate. Andere Beziehungen, andere Identifikatoren usw.

Inhaltlich ändert sich beim Übergang vom DM01 ins DMAV sehr wenig bis nichts. Man könnte also auch annehmen, dass entsprechend die Transformation schnell erledigt ist. Denkste. Alleine die schiere Grösse des Modells / der Modelle ist überwältigend. Es gibt viele Tabellen / Klassen mit jeweils viele Attributen und anfassen muss man jedes mindestens einmal. Es gibt also viel zu tun. Egal mit was man transformiert, initial aufwändig ist es immer.

Und eines kann man vorwegnehmen: Ob ilitransformer, SQL, FME, XSLT, Apache Hop, QGIS-Modeler. Es bleibt immer das Gleiche: Man muss die «Sprache» kennen. Sei es wirklich eine Programmiersprache oder eine Software mit einem GUI. Ob die DSL von ilitransformer wirklich geeignet ist, einfachen und verständlichen Code zu schreiben, wird sich zeigen. Der Vorteil liegt aber auf der Hand: Man kann die Konfigurationsdateien versionieren und die Sprache kann geändert/verbessert werden.

Plus: eine Transformation in diesem Umfang ist initial aufwändig. Mir hat hier die Korrelationstabelle und ein LLM geholfen. Ich hätte nie im Leben das alles von Hand schreiben wollen.

Zwei Modellwelten

Etwas vereinfacht sieht die Ausgangslage so aus:

DM01 DMAV

INTERLIS 1, normalerweise ITF

INTERLIS 2.4, XTF

ein grosses Modell mit vielen Topics

stärker modularisierte Teilmodelle

viele eigenständige Tabellen und Referenzen

häufiger Strukturen, BAG OF und explizite Assoziationen

vorhandene ITF-Objektidentifikatoren

stabile, modellübergreifend verwendbare OIDs erforderlich

Schon der erste Punkt hat Folgen. Ein XTF-Writer kann nicht einfach durch einen ITF-Writer ersetzt werden. INTERLIS-1-AREA-Geometrien benötigen im ITF zusätzliche Geometrie-Hilfstabellen und eine konsistente Topologie. Auch die Reihenfolge von Objekten und Referenzen ist nicht beliebig.

Noch wichtiger ist die Struktur. Was im DM01 als eigene Tabelle vorkommt, kann im DMAV eine eingebettete Struktur sein. In der Gegenrichtung muss aus einer Struktur wieder ein Objekt mit Identität und Referenz entstehen. Aus «ein Objekt wird ein Objekt» wird schnell «mehrere Quellobjekte werden zu einem Zielobjekt» – oder umgekehrt.

Vier Bausteine im Mapping

Die folgenden Beispiele zeigen vier typische Aufgaben: Werte übernehmen, Werte umrechnen, andere Objekte suchen und Strukturen abbilden. Dafür enthält die DSL Ausdrücke wie div, enumMap, lookupOptional, coalesce und refEquals.

1. Werte übernehmen und umrechnen

Ein Lagefixpunkt der Kategorie 3 lässt sich zum Teil direkt abbilden. Andere Werte müssen umgerechnet, übersetzt oder aus einer weiteren Tabelle gesucht werden:

assign {
  Nummer = p.Nummer;
  Lagegenauigkeit = div(p.LageGen, 100.0);
  IstLagezuverlaessig = enumMap(p.LageZuv, Zuverlaessigkeit_DM01_DMAV);
  SymbolOri = lookupOptional('DM01AVCH24LV95D.FixpunkteKategorie3.LFP3Symbol',
    'LFP3Symbol_von', oid(p), 'Ori');
  LFPArt = #LFP3;
  AktiverUnterhalt = true;
}

Nummer wird direkt übernommen. div teilt die Lagegenauigkeit durch 100. enumMap übersetzt den Wert anhand der folgenden Zuordnung:

enum Zuverlaessigkeit_DM01_DMAV {
  "ja" => true;
  "nein" => false;
}

lookupOptional sucht die Symbolorientierung in der separaten Tabelle LFP3Symbol. Ist dort kein passendes Objekt vorhanden, bleibt der Wert optional.

LFPArt = #LFP3 und AktiverUnterhalt = true sind dagegen feste Werte des Mappings. Sie werden nicht aus p gelesen.

Auch eine Beschriftung kann als Struktur abgebildet werden:

bag Textposition {
  from pos in dm01 class
    "DM01AVCH24LV95D.FixpunkteKategorie3.LFP3Pos"
    where refEquals(pos.LFP3Pos_von, p);
  assign {
    Position = pos.Pos;
    Orientierung = coalesce(pos.Ori, 100.0);
  }
}

Für jedes passende LFP3Pos-Objekt wird eine Textposition erzeugt. refEquals beschränkt die Auswahl auf Positionen des aktuellen Punkts. Fehlt die Orientierung, verwendet coalesce den Wert 100.0.

Nicht jeder Wert des Zielmodells hat im Quellmodell ein Gegenstück. Das Mapping kann solche Verluste ausdrücklich festhalten:

loss {
  sourcePath p.Schutzart;
  reasonCode "DMAV_ONLY";
  description "DM01 LFP3 hat kein Attribut für Schutzart.";
  when defined(p.Schutzart);
}

Die Regel meldet Schutzart nur dann als Verlust, wenn der Wert definiert ist. Wie der spätere Lossiness-Report genau aussieht, ist in diesem Ausschnitt nicht zu sehen.

2. Joins und Referenzen

Bei einem Grenzpunkt wird zusätzlich die passende Nachführung gelesen. Die Regel verbindet dafür zwei Quellklassen:

rule grenzpunkt-gueltig {
  target dmav class "DMAV_Grundstuecke_V1_1.Grundstuecke.Grenzpunkt";
  source gp from dm01 class "DM01AVCH24LV95D.Liegenschaften.Grenzpunkt";
  source nf from dm01 class "DM01AVCH24LV95D.Liegenschaften.LSNachfuehrung";
  where if(notDefined(nf.Gueltigkeit), true, nf.Gueltigkeit != #projektiert);
  join inner gp to nf on eq(gp.Entstehung, nf);
  identity gp.Identifikator;

  assign {
    Nummer = gp.Identifikator;
    Lagegenauigkeit = div(gp.LageGen, 100.0);
  }

  ref Entstehung {
    association "Entstehung_Grenzpunkt";
    role "Entstehung";
    required;
    target rule gs-nachfuehrung-gueltig sourceRef gp.Entstehung;
  }
}

Der where-Ausdruck lässt nur Nachführungen zu, die nicht als projektiert markiert sind. join inner verlangt eine passende Nachführung. identity legt die Identität des Zielobjekts fest.

Der ref-Block beschreibt die Beziehung zur Nachführung. sourceRef gp.Entstehung verwendet dafür die Referenz aus dem Quellobjekt; required macht die Zielreferenz obligatorisch.

Manchmal gibt es in der Quelle aber keine konkrete Referenz. Dann kann ein Profil wie folgt aussehen:

rule gemeinde-to-dmav {
  target dmav class "DMAV_HoheitsgrenzenAV_V1_0.HoheitsgrenzenAV.Gemeinde";
  source gm from dm01 class "DM01AVCH24LV95D.Gemeindegrenzen.Gemeinde";
  identity gm.BFSNr;

  assign {
    Name = truncate(gm.Name, 60);
    BFSNummer = gm.BFSNr;
    Fiktiv = false;
  }

  ref Entstehung {
    required;
    target rule hhg-nachfuehrung-gueltig sourceRef #first;
  }
}

sourceRef #first bedeutet hier nicht, dass fachlich einfach der erste Kandidat gewählt wird. Gibt es genau ein von hhg-nachfuehrung-gueltig erzeugtes Zielobjekt, kann die Referenz aufgelöst werden. Gibt es mehrere, bleibt die Beziehung mehrdeutig:

Darum ist das Topic hoheitsgrenzen momentan bei mir ausgeschlossen:

`transform: ambiguous #first references ...; multiple target candidates are found.`

Die Information, welcher Kandidat richtig wäre, fehlt in der Quelle. Das lässt sich nicht durch eine weitere Attributzuweisung lösen.

3. Strukturen und Bags

Im DMAV ist Objektnummer eine Struktur. Für die Rückrichtung muss daraus wieder ein DM01-Objekt werden:

bag Objektnummer {
  from gn in dmav class
    "DMAV_Bodenbedeckung_V1_1.Bodenbedeckung.Objektnummer";
  structure "DM01AVCH24LV95D.Bodenbedeckung.Gebaeudenummer";
  mode expand;
  parentRef role "Gebaeudenummer_von" parent b;
  assign {
    Nummer = gn.Nummer;
    GWR_EGID = b.EGID;
  }
}

mode expand erzeugt aus jedem passenden Objektnummer-Element ein DM01-Objekt. parentRef verbindet es mit dem Elternobjekt b. Das ist der umgekehrte Weg zu einem einfachen Attribut-Mapping: Aus einer Struktur wird wieder ein Objekt mit eigener Referenz.

Das Elternobjekt b stammt aus dem umgebenden Mapping und ist in diesem gekürzten Ausschnitt nicht zu sehen.

Der YAML-Überbau: der Dirigent

Ein YAML-Manifest beschreibt den gemeinsamen Lauf: Quelldatei, Zielmodell, Ausgabe und die einzubindenden .ilimap-Dateien. Ein gekürzter Ausschnitt sieht so aus:

datasetSlug: so-2546
direction: dm01-to-dmav
failPolicy: strict

source:
  pathHint: ./source/2546.ch.so.agi.av.dm01_ch.itf
  model: DM01AVCH24LV95D
  format: itf

output:
  model: DMAVTYM_Alles_V1_1
  format: xtf
  fileName: dmav-so-2546.xtf

mapping:
  oidStrategy: deterministicUuid
  basketStrategy: byTopic

topics:
  include:
    - id: bb
      mapping: profiles/dm01-to-dmav/1.1/bb.ilimap
    - id: lfp3
      mapping: profiles/dm01-to-dmav/1.1/lfp3.ilimap
  exclude:
    - id: hoheitsgrenzen
      reason: "ambiguous #first reference"

topics.include bindet einzelne Mappings ein; topics.exclude kann ein Thema mit einer Begründung ausschliessen. oidStrategy und basketStrategy legen globale Strategien für den Lauf fest. Weitere Felder des vollständigen Manifests sind in diesem Ausschnitt nicht zu sehen.

Was heute funktioniert – und was noch nicht

Ob meine Mappings wirklich funktionieren, müssten Transformationen mit echten Daten zeigen.

Der Lauf SO‑2549 (Kammersrohr) von DM01 nach DMAV ist erfolgreich:

  • 1’860 Quellobjekte gelesen

  • 985 durch die Profilregeln gefiltert

  • 937 Zielobjekte erzeugt und geschrieben

  • 1’846 Join-Lookups

  • keine Transformationsfehler und keine Transformationswarnungen

  • Ausgabe durch ilivalidator als gültig beurteilt; der Validator meldete eine Warnung

Das ist ein erfolgreicher Lauf mit echten Daten. Die Ausgabe wurde von ilivalidator als gültig beurteilt; der Validator meldete dabei eine Warnung.

In der Gegenrichtung scheitert die Validierung dagegen an einer Liegenschaft:

failed to build polygons of
DM01AVCH24LV95D.Liegenschaften.Liegenschaft.Geometrie
no area-ref to polygon of lines 18, 21, 19

Die AREA-Topologie im erzeugten INTERLIS-1-Transfer ist für diesen Fall noch nicht gültig. Wegen failPolicy: strict aus dem YAML-Beispiel wird das Ergebnis nicht als erfolgreicher Output übernommen. Zusätzlich erzeugt der Lauf zwei Zielobjekte weniger als erwartet. Die Ursache ist noch offen.

Die Beispiele zeigen damit auch die wichtigsten Grenzen:

  • sourceRef #first kann mehrdeutig sein.

  • Ein Zielwert kann ohne Gegenstück im Quellmodell verloren gehen; dafür gibt es die loss-Regel.

  • Ein Mapping kann Daten erzeugen, die nicht aus einem einzelnen Quellattribut stammen, zum Beispiel durch feste Werte wie LFPArt = #LFP3.

Es gibt funktionierende Teil- und Full Runs, aber noch keine pauschale Garantie für einen verlustfreien, validator-gültigen Roundtrip über alle Themen.

Validierung ist deshalb Teil der Transformation und nicht nur eine Kontrolle am Schluss. Champions League ist nicht die Anzahl Mappingzeilen, sondern der kontrollierte Umgang mit den Stellen, an denen zwei Modelle nicht dasselbe wissen.

Links: