diff --git a/mandates/pek/datenmodell/PEK Datenmodell.md b/mandates/pek/datenmodell/PEK Datenmodell.md new file mode 100644 index 0000000..61c2e67 --- /dev/null +++ b/mandates/pek/datenmodell/PEK Datenmodell.md @@ -0,0 +1,558 @@ +# Datenmodell Architektur-Planungs-App + +## Übersicht + +Dieses Datenmodell bildet die Grundlage für eine Schweizer Architektur-Planungs-App zur Verwaltung von Bauprojekten, Parzellen, Dokumenten und regulatorischen Kontextinformationen. + +## Datenfluss-Diagramm + +```mermaid +--- +title: Hauptflüsse - Architektur-Planungs-App +--- +flowchart TD + Start([Datenmodell Start]) + + subgraph Admin[Administrative Ebene] + Land[LAND
Schweiz] + Kanton[KANTON
z.B. Zürich] + Gemeinde[GEMEINDE
z.B. Zürich Stadt] + Land --> Kanton + Kanton --> Gemeinde + end + + subgraph Geo[Geografische Daten] + Parzelle[PARZELLE
Grundstück mit
Bauparametern] + GeoPunkt[GEO_PUNKT
Koordinaten] + Gemeinde --> Parzelle + Parzelle --> GeoPunkt + end + + subgraph Core[Kern-Business-Logik] + Projekt[PROJEKT
Bauprojekt] + Dokument[DOKUMENT
Dateien & URLs] + Projekt -.Perimeter.-> Parzelle + Projekt -.Dokumente.-> Dokument + Parzelle -.Dokumente.-> Dokument + end + + subgraph Support[Unterstützende Daten] + Kontext[KONTEXT
Zusatzinfos] + Projekt --> Kontext + Parzelle --> Kontext + end + + Start --> Admin + + style Land fill:#50C878,stroke:#2D7A4A,stroke-width:2px,color:#fff + style Kanton fill:#50C878,stroke:#2D7A4A,stroke-width:2px,color:#fff + style Gemeinde fill:#50C878,stroke:#2D7A4A,stroke-width:2px,color:#fff + style Parzelle fill:#4A90E2,stroke:#2E5C8A,stroke-width:3px,color:#fff + style Projekt fill:#4A90E2,stroke:#2E5C8A,stroke-width:3px,color:#fff + style Dokument fill:#4A90E2,stroke:#2E5C8A,stroke-width:3px,color:#fff + style GeoPunkt fill:#F5A623,stroke:#C17D11,stroke-width:2px,color:#fff + style Kontext fill:#F5A623,stroke:#C17D11,stroke-width:2px,color:#fff +``` + +--- + +## Alle Datenobjekte als Tabellen + +### Übersichtstabelle + +| Objekt | Typ | Beschreibung | Hauptfelder | +|--------|-----|--------------|-------------| +| **Projekt** | Hauptentität | Bauprojekt mit Status und Perimeter | id, label, statusProzess | +| **Parzelle** | Hauptentität | Grundstück mit Bauparametern | id, label, bauzone, AZ, BZ | +| **Dokument** | Hauptentität | Dateien und URLs mit Versionierung | id, label, typ, format | +| **Land** | Admin | Nationale Ebene | id, label | +| **Kanton** | Admin | Kantonale Ebene mit Baurecht | id, label, Baureglement | +| **Gemeinde** | Admin | Gemeinde-Ebene mit BZO | id, label, plz, BZO | +| **GeoPunkt** | Hilfsobjekt | 3D-Koordinate im LV95 | x, y, z, referenzen | +| **Kontext** | Hilfsobjekt | Flexible Zusatzinformationen | id, thema, inhalt | +| **Tag** | Enum | Dokumentkategorien | - | +| **GeoTag** | Enum | Geopunkt-Kategorien | - | +| **JaNein** | Enum | Drei-wertiger Status | "", "Ja", "Nein" | +| **StatusProzess** | Enum | Projektstatus | 7 Werte | + +--- + +## Zentrale Entitäten + +### 1. Projekt +**Das Kernobjekt, das ein Bauprojekt repräsentiert.** + +| Feld | Datentyp | Pflicht | Beschreibung | +|------|----------|---------|--------------| +| `id` | UUID | ✓ | Eindeutiger Identifier | +| `label` | String | ✓ | Projektbezeichnung | +| `statusProzess` | Array[Enum] | - | Projektstatus: Eingang, Analyse, Studie, Planung, Baurechtsverfahren, Umsetzung, Archiv | +| `perimeter` | Array[Parzelle] | - | Betroffene Parzellen (n:m Beziehung) | +| `dokumenteBauherrschaft` | Array[Dokument] | - | Dokumente vom Bauherrn (n:m Beziehung) | +| `dokumentePlanung` | Array[Dokument] | - | Planungsdokumente (n:m Beziehung) | +| `geoBaulinie` | Array[GeoPunkt] | - | Baulinie als Polygonzug (1:n Beziehung) | +| `kontextInformationen` | Array[Kontext] | - | Projektspezifische Kontextinfos (1:n Beziehung) | + +**Beziehungen:** +- **n:m** zu Parzelle (Perimeter über `projekt_parzelle`) +- **n:m** zu Dokument (Bauherrschaft über `projekt_dokument_bauherrschaft`) +- **n:m** zu Dokument (Planung über `projekt_dokument_planung`) +- **1:n** zu GeoPunkt (Baulinie) +- **1:n** zu Kontext + +--- + +### 2. Parzelle +**Repräsentiert ein Grundstück mit allen baurechtlichen Eigenschaften.** + +#### Grunddaten + +| Feld | Datentyp | Pflicht | Beschreibung | +|------|----------|---------|--------------| +| `id` | UUID | ✓ | Eindeutiger Identifier | +| `label` | String | ✓ | Parzellenbezeichnung | +| `parzellenNummern` | Array[String] | - | Offizielle Parzellennummern | +| `eigentuemerschaaft` | String | - | Eigentümer der Parzelle | +| `strasseNr` | String | - | Straße und Hausnummer | + +#### Geografischer Kontext + +| Feld | Datentyp | Pflicht | Beschreibung | +|------|----------|---------|--------------| +| `kontextLand` | Land | - | Land der Parzelle (n:1 Beziehung) | +| `kontextKanton` | Kanton | - | Kanton der Parzelle (n:1 Beziehung) | +| `kontextGemeinde` | Gemeinde | - | Gemeinde der Parzelle (n:1 Beziehung) | +| `geoUmfang` | Array[GeoPunkt] | - | Parzellengrenze als Polygon | + +#### Nachbarschaft + +| Feld | Datentyp | Pflicht | Beschreibung | +|------|----------|---------|--------------| +| `nachbarEigentuemer` | Array[Parzelle] | - | Selbstreferenz zu angrenzenden Parzellen (n:m Beziehung) | + +#### Schutzzonen + +| Feld | Datentyp | Pflicht | Beschreibung | +|------|----------|---------|--------------| +| `hochwasserschutzzone` | String | - | Hochwasserschutzzone (falls zutreffend) | +| `laermschutzzone` | String | - | Lärmschutzzone | +| `grundwasserschutzzone` | String | - | Grundwasserschutzzone (falls zutreffend) | + +#### Bebauungsparameter + +| Feld | Datentyp | Pflicht | Beschreibung | +|------|----------|---------|--------------| +| `bauzone` | String | - | Bauzonenbezeichnung (z.B. W3, WG2, etc.) | +| `az` | Float | - | Ausnützungsziffer | +| `bz` | Float | - | Bebauungsziffer | +| `vollgeschossZahl` | Integer | - | Anzahl zulässiger Vollgeschosse | +| `anrechenbarDachgeschoss` | Float | - | Anrechenbarer Anteil Dachgeschoss (0.0 - 1.0) | +| `anrechenbarUntergeschoss` | Float | - | Anrechenbarer Anteil Untergeschoss (0.0 - 1.0) | +| `gebaeudehoehe_max` | Float | - | Maximale Gebäudehöhe in Metern | + +#### Abstandsregelungen + +| Feld | Datentyp | Pflicht | Beschreibung | +|------|----------|---------|--------------| +| `regelnGrenzabstand` | String | - | Regelungen zum Grenzabstand | +| `regelnMehrlaengenzuschlag` | String | - | Regelungen zum Mehrlängenzuschlag | +| `regelnMehrhoehenzuschlag` | String | - | Regelungen zum Mehrhöhenzuschlag | + +#### Eigenschaften (Ja/Nein) + +| Feld | Datentyp | Pflicht | Beschreibung | +|------|----------|---------|--------------| +| `parzelleBebaut` | JaNein | - | Ist die Parzelle bebaut? ("", "Ja", "Nein") | +| `parzelleErschlossen` | JaNein | - | Ist die Parzelle erschlossen? ("", "Ja", "Nein") | +| `hanglage` | JaNein | - | Liegt die Parzelle in Hanglage? ("", "Ja", "Nein") | + +#### Weitere Informationen + +| Feld | Datentyp | Pflicht | Beschreibung | +|------|----------|---------|--------------| +| `spezifischeDokumente` | Array[Dokument] | - | Parzellenspezifische Dokumente (n:m Beziehung) | +| `kontextInformationen` | Array[Kontext] | - | Parzellenspezifische Kontextinfos (1:n Beziehung) | + +**Beziehungen:** +- **n:1** zu Land, Kanton, Gemeinde (geografischer Kontext) +- **n:m** zu Parzelle (Nachbarn über `parzelle_nachbar`) +- **n:m** zu Dokument (über `parzelle_dokument`) +- **1:n** zu GeoPunkt (Umfang als Polygon) +- **1:n** zu Kontext + +--- + +### 3. Dokument +**Verwaltet Dateien und URLs mit Versionierung.** + +| Feld | Datentyp | Pflicht | Beschreibung | +|------|----------|---------|--------------| +| `id` | UUID | ✓ | Eindeutiger Identifier | +| `label` | String | ✓ | Dokumentbezeichnung | +| `versionsbezeichnung` | String | - | Versionsnummer oder -bezeichnung (z.B. "v1.0", "Rev. A") | +| `typ` | Enum | ✓ | Art des Dokuments: `Datei` oder `Url` | +| `format` | String | - | Dateiformat (z.B. "PDF", "DWG", "IFC", "URL") | +| `dokumentReferenz` | String | ✓ | Dateipfad oder URL | +| `tags` | Array[Tag] | - | Kategorisierung (siehe Tag-Enum) | + +#### Tag-Enum (Dokumentkategorien) + +| Tag | Beschreibung | +|-----|--------------| +| `Kataster Objekte` | Amtliche Vermessung | +| `Kataster Werkeleitungen` | Leitungskataster | +| `Kataster Belastete Standorte` | Altlasten | +| `Kataster Bäume` | Baumkataster | +| `Zonenplan` | Zonenpläne | +| `Planungs- und Baugesetz (PGB)` | Kantonale Baugesetze | +| `Bau- und Zonenordnung (BZO)` | Gemeinde BZO | +| `Parkplatzverordnung` | Parkplatzregelungen | +| `Eigentümerauskunft` | Grundbuch-Auszüge Eigentümer | +| `Grundbuchauszug` | Vollständige Grundbuch-Auszüge | + +**Beziehungen:** +- **n:m** zu allen Entitäten, die Dokumente referenzieren (Projekt, Parzelle, Land, Kanton, Gemeinde) + +--- + +### 4. Geografische Entitäten + +#### GeoPunkt +**Repräsentiert einen 3D-Punkt im Schweizer Koordinatensystem LV95 (EPSG:2056).** + +| Feld | Datentyp | Pflicht | Beschreibung | +|------|----------|---------|--------------| +| `x` | Float | ✓ | LV95 Ostwert (E), typisch 2'480'000 - 2'840'000 | +| `y` | Float | ✓ | LV95 Nordwert (N), typisch 1'070'000 - 1'300'000 | +| `z` | Float | - | Höhe über Meer in Metern | +| `referenzen` | Array[GeoTag] | - | Kategorisierung des Punktes | + +**Verwendung:** +- Parzellenumfang (Polygon) +- Baulinie (Linienzug) +- Einzelne Referenzpunkte + +#### GeoTag (Enum) + +| Kategorie | Beschreibung | +|-----------|--------------| +| `Referenzpunkt Kat. 1` | Fixpunkt höchster Genauigkeit | +| `Referenzpunkt Kat. 2` | Fixpunkt mittlerer Genauigkeit | +| `Referenzpunkt Kat. 3` | Fixpunkt niedriger Genauigkeit | +| `Geometeraufnahme` | Vom Geometer vermessener Punkt | + +**Koordinatensystem-Beispiel (Zürich Hauptbahnhof):** +- X (Ost): 2'683'140 +- Y (Nord): 1'247'850 +- Z (Höhe): 408 m ü. M. + +--- + +### 5. Administrative Hierarchie + +#### Land + +| Feld | Datentyp | Pflicht | Beschreibung | +|------|----------|---------|--------------| +| `id` | UUID | ✓ | Eindeutiger Identifier | +| `label` | String | ✓ | Landesname (z.B. "Schweiz") | +| `dokumente` | Array[Dokument] | - | Nationale Gesetze (1:n Beziehung) | +| `kontextInformationen` | Array[Kontext] | - | Nationale Kontextinformationen (1:n Beziehung) | + +**Beziehungen:** +- **1:n** zu Kanton + +--- + +#### Kanton + +| Feld | Datentyp | Pflicht | Beschreibung | +|------|----------|---------|--------------| +| `id` | UUID | ✓ | Eindeutiger Identifier | +| `label` | String | ✓ | Kantonsname (z.B. "Zürich") | +| `dokumente` | Array[Dokument] | - | Kantonale Dokumente (1:n Beziehung) | +| `kontextInformationen` | Array[Kontext] | - | Kantonsspezifische Kontextinfos (1:n Beziehung) | +| `baureglementAktuell` | Dokument | - | Aktuelles Baureglement | +| `baureglementRevision` | Dokument | - | Baureglement in Revision | +| `bauverordnungAktuell` | Dokument | - | Aktuelle Bauverordnung | +| `bauverordnungRevision` | Dokument | - | Bauverordnung in Revision | + +**Beziehungen:** +- **n:1** zu Land +- **1:n** zu Gemeinde + +--- + +#### Gemeinde + +| Feld | Datentyp | Pflicht | Beschreibung | +|------|----------|---------|--------------| +| `id` | UUID | ✓ | Eindeutiger Identifier | +| `label` | String | ✓ | Gemeindename (z.B. "Zürich") | +| `plz` | String | - | Postleitzahl | +| `dokumente` | Array[Dokument] | - | Gemeindedokumente (1:n Beziehung) | +| `kontextInformationen` | Array[Kontext] | - | Gemeindespezifische Kontextinfos (1:n Beziehung) | +| `bzoAktuell` | Dokument | - | Aktuelle Bau- und Zonenordnung (BZO) | +| `bzoRevision` | Dokument | - | BZO in Revision | + +**Beziehungen:** +- **n:1** zu Kanton +- **1:n** zu Parzelle + +--- + +### 6. Kontext +**Flexibles System für spezifische Informationen und Hinweise.** + +| Feld | Datentyp | Pflicht | Beschreibung | +|------|----------|---------|--------------| +| `id` | UUID | ✓ | Eindeutiger Identifier | +| `thema` | String | ✓ | Bezeichnung des Themas | +| `inhalt` | String | ✓ | Detaillierte Information (Text) | + +**Polymorphe Beziehung** - Kontext kann gehören zu: +- Projekt (n:1) +- Parzelle (n:1) +- Land (n:1) +- Kanton (n:1) +- Gemeinde (n:1) + +#### Beispielthemen (nicht abschliessend) + +| Themenbereich | Beispiele | +|---------------|-----------| +| **Nutzung** | Vorgaben zur Erdgeschossnutzung (Wohnen erlaubt oder Pflicht für Gewerbe) | +| **Rechte** | Dienstbarkeiten (Wegrechte, Nähebaurechte, etc.) | +| **Parkierung** | Anforderung Parkplätze (Berechnung / Reduktionsfaktoren) | +| **Ausnützung** | Ausnützungsübertragungen | +| **Umwelt** | Schadstoffbelastungen auf Parzellen | +| **Planung** | Aktive Gestaltungspläne | +| **Lärm** | Lärmempfindlichkeitsstufen | +| **Energie** | Mögliche Wärmenutzung (Wärmeverbundnetze; Fernwärme, Anergie) | +| **Natur** | Baumbestand auf privaten Grundstücken | +| **Schutz** | Isos (Ortsbild, Schutzstatus, Denkmalschutz, Weilergebiet, etc.) | +| **Gefahren** | Naturgefahren (z.B. Objektschutzmassnahmen (Hochwasser)) | +| **Revision** | Verweis auf aktuell in oder zukünftig in Revision befindlichen Normen/Gesetze (z.B. Revision PBG mit aktuell negativer Vorwirkung) | + +**Design-Rationale:** +Das Kontext-Objekt ermöglicht flexibles Hinzufügen von projektspezifischen, parzellen-spezifischen oder regionalen Informationen ohne Schemaänderungen. + +--- + +### 7. Hilfsentitäten & Enumerationen + +#### JaNein (Enum) +**Drei-wertiger Zustand für optionale Ja/Nein-Fragen.** + +| Wert | Bedeutung | +|------|-----------| +| `""` (leer) | Unbekannt / Nicht erfasst | +| `"Ja"` | Ja / Zutreffend | +| `"Nein"` | Nein / Nicht zutreffend | + +**Verwendung:** +- `parzelleBebaut`: Ist die Parzelle bebaut? +- `parzelleErschlossen`: Ist die Parzelle erschlossen? +- `hanglage`: Liegt die Parzelle in Hanglage? + +--- + +#### StatusProzess (Enum) +**Projektstatus zur Nachverfolgung des Projektfortschritts.** + +| Wert | Beschreibung | +|------|--------------| +| `Eingang` | Projekt wurde eingereicht | +| `Analyse` | Projekt wird analysiert | +| `Studie` | Machbarkeitsstudie läuft | +| `Planung` | Planungsphase | +| `Baurechtsverfahren` | Baubewilligung läuft | +| `Umsetzung` | Bauprojekt in Umsetzung | +| `Archiv` | Projekt abgeschlossen | + +**Besonderheit:** +Ein Projekt kann mehrere Status gleichzeitig haben (z.B. "Analyse" und "Studie"). + +--- + +#### DokumentTyp (Enum) + +| Wert | Beschreibung | +|------|--------------| +| `Datei` | Physische Datei (PDF, DWG, etc.) | +| `Url` | Externer Link / URL | + +--- + +## Beziehungsdiagramm + +``` +PROJEKT +├── perimeter [1:n] ──────────> PARZELLE +│ ├── dokumenteBauherrschaft [1:n] ──> DOKUMENT +│ ├── dokumentePlanung [1:n] ────────> DOKUMENT +│ ├── geoBaulinie [1:n] ─────────────> GEO_PUNKT +│ └── kontextInformationen [1:n] ───> KONTEXT + +PARZELLE +├── nachbarEigentuemer [n:n] ──> PARZELLE (selbst) +├── kontextLand [n:1] ──────────> LAND +├── kontextKanton [n:1] ────────> KANTON +├── kontextGemeinde [n:1] ──────> GEMEINDE +├── geoUmfang [1:n] ────────────> GEO_PUNKT +├── spezifischeDokumente [1:n] ─> DOKUMENT +└── kontextInformationen [1:n] ─> KONTEXT + +DOKUMENT +└── tags [n:n] ─────────────────> TAG + +GEO_PUNKT +└── referenzen [1:n] ───────────> GEO_TAG + +LAND / KANTON / GEMEINDE +├── dokumente [1:n] ────────────> DOKUMENT +└── kontextInformationen [1:n] ─> KONTEXT +``` + +--- + +## Junction Tables (Many-to-Many Beziehungen) + +**Zwischentabellen zur Auflösung von n:m Beziehungen:** + +| Tabelle | Verbindet | Felder | Beschreibung | +|---------|-----------|--------|--------------| +| `projekt_parzelle` | Projekt ↔ Parzelle | `projekt_id`, `parzelle_id` | Projektperimeter | +| `projekt_dokument_bauherrschaft` | Projekt ↔ Dokument | `projekt_id`, `dokument_id` | Dokumente der Bauherrschaft | +| `projekt_dokument_planung` | Projekt ↔ Dokument | `projekt_id`, `dokument_id` | Planungsdokumente | +| `parzelle_nachbar` | Parzelle ↔ Parzelle | `parzelle_id`, `nachbar_id` | Nachbarschaftsbeziehungen | +| `parzelle_dokument` | Parzelle ↔ Dokument | `parzelle_id`, `dokument_id` | Parzellenspezifische Dokumente | + +**Besonderheiten:** +- `parzelle_nachbar`: Selbstreferenzierende Tabelle mit Constraint `parzelle_id != nachbar_id` +- Alle Junction Tables haben zusammengesetzte Primärschlüssel aus beiden Foreign Keys +- CASCADE DELETE empfohlen für automatische Bereinigung + +--- + +## Implementierungshinweise + +### Datenbank-Design + +#### Empfehlung: Hybrid-Ansatz + +**Relationale Datenbank (PostgreSQL mit PostGIS):** +- Stammdaten: Projekt, Parzelle, Land, Kanton, Gemeinde +- Geografische Daten: GeoPunkt mit PostGIS-Geometrie-Typen +- Strukturierte Queries und Joins + +**Dokumenten-Datenbank oder Blob Storage:** +- Dokumente: S3, Azure Blob Storage oder MinIO +- Metadaten in relationaler DB, Binärdaten extern + +#### Schema-Überlegungen + +**Normalisierung:** +1. Land, Kanton, Gemeinde als separate Tabellen mit Referenzen +2. Dokument als zentrale Tabelle, referenziert von mehreren Entitäten (Polymorphic Associations oder Junction Tables) +3. GeoPunkt entweder embedded (JSON) oder separate Tabelle mit Foreign Keys + +**Denormalisierung für Performance:** +- Häufig abgefragte Parzellendaten können gecached werden +- Gemeinde.plz könnte redundant in Parzelle gespeichert werden + +### Geografische Daten + +**Koordinatensystem:** +Schweizer Landessystem LV95 (EPSG:2056): +- X (Ost): 2'480'000 - 2'840'000 +- Y (Nord): 1'070'000 - 1'300'000 +- Z (Höhe): Meter über Meer + +### Validierung + +**Pflichtfelder:** +- Alle IDs (UUID) +- Alle Labels +- Dokument: typ, dokumentReferenz +- GeoPunkt: x, y (z optional) +- Kontext: thema, inhalt + +**Geschäftslogik-Validierungen:** +- AZ und BZ müssen > 0 sein +- VollgeschossZahl muss ≥ 0 sein +- Geo-Koordinaten müssen in gültigem CH-Bereich liegen +- Parzelle muss mindestens 3 GeoPunkte für gültiges Polygon haben + +### Sicherheit & Zugriffskontrolle + +**Überlegungen:** +- Dokumente: Zugriffskontrolle nach Projekt-/Benutzerrolle +- Eigentümerdaten: DSGVO-konforme Behandlung +- Audit-Log für Änderungen an Bebauungsparametern +- Versionierung von Dokumenten (via versionsbezeichnung) + +### Erweiterbarkeit + +**Flexible Bereiche:** +1. **Kontext-Objekt**: Neue Themen können ohne Schema-Änderung hinzugefügt werden +2. **Tag-System**: Erweiterbar um neue Dokumentkategorien +3. **StatusProzess**: Kann projektspezifisch angepasst werden +4. **GeoTag**: Neue Kategorien für Vermessungspunkte möglich + +**Migration-Strategy:** +- Verwende Datenbank-Migrationen (z.B. Alembic für Python, Flyway für Java) +- Behalte alte Enums bei, füge neue hinzu +- Nutze nullable Felder für neue Eigenschaften + +--- + +## Anwendungsfälle + +### Use Case 1: Neues Projekt anlegen +1. Erstelle Projekt mit Label und Status +2. Füge Parzellen zum Perimeter hinzu +3. Lade Dokumente der Bauherrschaft hoch +4. Verknüpfe Kontext-Informationen + +### Use Case 2: Bebaubarkeit prüfen +1. Lade Parzelle mit allen Eigenschaften +2. Prüfe AZ, BZ, Vollgeschosszahl +3. Berücksichtige Hanglage, Schutzzonen +4. Lade BZO der Gemeinde +5. Prüfe Kontext-Informationen (Dienstbarkeiten, etc.) + +### Use Case 3: Nachbaranalyse +1. Lade Parzelle +2. Folge nachbarEigentuemer-Referenzen +3. Zeige Eigentümer und Bebauung der Nachbarparzellen + +### Use Case 4: Dokumentensuche +1. Suche über Tags (z.B. "Zonenplan") +2. Filtere nach Format (z.B. PDF) +3. Gruppiere nach Projekt/Parzelle/Gemeinde + +### Use Case 5: Revisionen verfolgen +1. Prüfe Gemeinde.bzoRevision +2. Prüfe Kanton.baureglementRevision +3. Erstelle Kontext-Eintrag mit Hinweis auf negative Vorwirkung + +--- + +## Offene Fragen / Zu klären + +1. **Versionierung**: Sollen Änderungen an Parzellen historisiert werden? +2. **Mehrsprachigkeit**: Labels in DE/FR/IT? +3. **Benutzer & Rollen**: Wer darf was bearbeiten? +4. **Workflow-Engine**: Für Statusübergänge und Genehmigungen? +5. **Integration**: Anbindung an amtliche Geodaten (z.B. Swisstopo API)? +6. **Berechnungen**: Sollen Ausnützungsberechnungen automatisiert werden? + +--- + +## Nächste Schritte + +1. **Validierung**: Review mit PEK +2. **Prototyp**: Implementierung der Datenmodell-Klassen +3. **GIS-Integration**: PostGIS aufsetzen, Test-Geodaten importieren +4. **API-Design**: RESTful API (FastAPI) mit OpenAPI-Dokumentation diff --git a/mandates/pek/datenmodell/README.md b/mandates/pek/datenmodell/README.md new file mode 100644 index 0000000..9d59427 --- /dev/null +++ b/mandates/pek/datenmodell/README.md @@ -0,0 +1,409 @@ +# Architektur-Planungs-App - Datenmodell + +## Übersicht + +Dieses Repository enthält das vollständige Datenmodell für eine Schweizer Architektur-Planungs-Applikation zur Verwaltung von Bauprojekten, Parzellen, Dokumenten und regulatorischen Informationen. + +## 📁 Dateien + +### 1. **DATENMODELL_DOKUMENTATION.md** +Umfassende Dokumentation mit: +- Detaillierte Beschreibung aller Entitäten +- Beziehungsdiagramme +- Implementierungshinweise +- Use Cases +- Best Practices +- Offene Fragen + +**Empfohlene Lesereihenfolge: Zuerst diese Datei lesen!** + +### 2. **datenmodell.mermaid** +Visuelles ER-Diagramm zur Darstellung der Entitäten und Beziehungen. + +**Verwendung:** +```bash +# In Visual Studio Code mit Mermaid Extension +# Oder online: https://mermaid.live/ + +# Datei öffnen und als Diagramm anzeigen +``` + +### 3. **datenmodell-schema.json** +JSON Schema Definition im JSON Schema Draft-07 Format. + +**Verwendung:** +- API-Dokumentation mit Swagger/OpenAPI +- Validierung von JSON-Payloads +- Code-Generierung für verschiedene Sprachen + +```bash +# JSON Schema validieren +npm install -g ajv-cli +ajv validate -s datenmodell-schema.json -d beispiel-daten.json +``` + +### 4. **models.py** +Python SQLAlchemy Implementation mit PostGIS-Unterstützung. + +**Verwendung:** +```bash +# Installation +pip install sqlalchemy geoalchemy2 psycopg2-binary --break-system-packages + +# Datenbank erstellen +python models.py + +# In eigener Anwendung verwenden +from models import Projekt, Parzelle, Dokument +``` + +**Tech Stack:** +- Python 3.10+ +- SQLAlchemy 2.0+ +- PostgreSQL 15+ mit PostGIS 3.4+ +- GeoAlchemy2 + +### 5. **schema.prisma** +Prisma Schema für TypeScript/JavaScript Backend. + +**Verwendung:** +```bash +# Installation +npm install prisma @prisma/client + +# Datenbank migrieren +npx prisma migrate dev --name init + +# Prisma Client generieren +npx prisma generate + +# Prisma Studio öffnen +npx prisma studio +``` + +**Tech Stack:** +- Node.js 18+ +- Prisma 5+ +- PostgreSQL 15+ mit PostGIS + +### 6. **migration_001_initial_schema.sql** +SQL-Migrationsskript für direkte PostgreSQL-Verwendung. + +**Verwendung:** +```bash +# PostgreSQL Datenbank erstellen +createdb architektur_app + +# Migration ausführen +psql -d architektur_app -f migration_001_initial_schema.sql + +# Verbinden und testen +psql architektur_app +\dt # Tabellen anzeigen +``` + +## 🚀 Quick Start + +### Option 1: Python mit SQLAlchemy + +```bash +# 1. PostgreSQL mit PostGIS aufsetzen +docker run --name postgis -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgis/postgis:15-3.4 + +# 2. Dependencies installieren +pip install sqlalchemy geoalchemy2 psycopg2-binary --break-system-packages + +# 3. Models verwenden +python models.py +``` + +### Option 2: TypeScript mit Prisma + +```bash +# 1. Projekt initialisieren +npm init -y +npm install prisma @prisma/client + +# 2. Prisma konfigurieren +cp schema.prisma prisma/schema.prisma + +# 3. Database URL setzen +echo "DATABASE_URL=\"postgresql://user:password@localhost:5432/architektur_app\"" > .env + +# 4. Migration ausführen +npx prisma migrate dev --name init +``` + +### Option 3: Direkt mit SQL + +```bash +# 1. Datenbank erstellen +createdb architektur_app + +# 2. Migration ausführen +psql -d architektur_app -f migration_001_initial_schema.sql + +# 3. Daten einfügen und abfragen +psql architektur_app +``` + +## 📊 Datenmodell-Struktur + +### Kern-Entitäten + +``` +PROJEKT (Bauprojekt) +├── Perimeter (Parzellen) +├── Dokumente (Bauherrschaft & Planung) +├── Baulinie (geografisch) +└── Kontext-Informationen + +PARZELLE (Grundstück) +├── Geografischer Kontext (Land, Kanton, Gemeinde) +├── Bauliche Parameter (AZ, BZ, Vollgeschosse, etc.) +├── Schutzzonen +├── Nachbarparzellen +└── Dokumente & Kontext + +DOKUMENT (Datei oder URL) +├── Versionierung +├── Tags +└── Format + +Administrative Hierarchie: +LAND → KANTON → GEMEINDE +``` + +### Geografische Daten + +Das Modell verwendet das **Schweizer Landessystem LV95 (EPSG:2056)**: +- Ostwert (X): 2'480'000 - 2'840'000 +- Nordwert (Y): 1'070'000 - 1'300'000 +- Höhe (Z): Meter über Meer + +## 🗺️ Koordinatensystem + +Alle geografischen Daten verwenden **LV95 (Swiss LV95 / EPSG:2056)**. + +**Beispiel-Koordinaten (Zürich Hauptbahnhof):** +``` +X (Ost): 2'683'140 +Y (Nord): 1'247'850 +Z (Höhe): 408 m ü. M. +``` + +## 🔍 Wichtige Entscheidungen + +### 1. Polymorphe Beziehungen +Das `Kontext`-Objekt kann zu verschiedenen Entitäten gehören (Projekt, Parzelle, Land, Kanton, Gemeinde). Dies ermöglicht flexible Erweiterungen ohne Schema-Änderungen. + +### 2. Array-Felder +- `statusProzess`: Ein Projekt kann mehrere Status gleichzeitig haben +- `tags`: Dokumente können mehrere Tags haben +- `parzellenNummern`: Parzellen können mehrere offizielle Nummern haben + +### 3. Selbstreferenzierende Beziehungen +Parzellen referenzieren sich gegenseitig als Nachbarn (n:m Beziehung). + +### 4. Versionierung +Dokumente haben eine `versionsbezeichnung` für manuelle Versionskontrolle. + +### 5. Drei-wertiger Zustand +`JaNein` Enum erlaubt "", "Ja", "Nein" für unbekannte Zustände. + +## 📋 Anwendungsfälle + +### UC1: Neues Projekt erstellen +```python +# Python Beispiel +projekt = Projekt( + label="Neubau Mehrfamilienhaus", + status_prozess=[StatusProzess.EINGANG] +) +projekt.perimeter.append(parzelle_1) +session.add(projekt) +session.commit() +``` + +### UC2: Bebaubarkeit prüfen +```sql +-- SQL Beispiel +SELECT + p.label, + p.bauzone, + p.az, + p.bz, + p.vollgeschoss_zahl, + p.gebaeudehoehe_max, + ST_Area(p.geo_umfang) as flaeche_m2 +FROM v_parzelle_vollstaendig p +WHERE p.id = 'UUID'; +``` + +### UC3: Nachbaranalyse +```typescript +// TypeScript/Prisma Beispiel +const parzelle = await prisma.parzelle.findUnique({ + where: { id: parzelleId }, + include: { + nachbarEigentuemer_von: { + include: { nachbar: true } + } + } +}); +``` + +## 🎯 Best Practices + +### Geometrie-Handling +```python +# PostGIS: Parzelle mit Polygon erstellen +from geoalchemy2.shape import from_shape +from shapely.geometry import Polygon + +polygon = Polygon([ + (2683140, 1247850), + (2683200, 1247850), + (2683200, 1247900), + (2683140, 1247900), + (2683140, 1247850) +]) + +parzelle.geo_umfang = from_shape(polygon, srid=2056) +``` + +### Kontext-Informationen +```python +# Flexibles Hinzufügen von Kontextinformationen +kontext = Kontext( + thema="Dienstbarkeiten", + inhalt="Wegrecht zugunsten Parzelle 1235 entlang Ostgrenze, eingetragen am 15.03.2020", + parzelle_id=parzelle.id +) +``` + +### Dokumenten-Management +```python +# Dokument mit Tags +dokument = Dokument( + label="Zonenplan Gemeinde Zürich", + versionsbezeichnung="2024-v1", + typ=DokumentTyp.DATEI, + format="PDF", + dokument_referenz="/storage/docs/zonenplan-zh-2024.pdf", + tags=[TagTyp.ZONENPLAN, TagTyp.BZO] +) +``` + +## 🔐 Sicherheit + +### Zu beachten: +- Eigentümerdaten sind personenbezogene Daten (DSGVO/DSG) +- Dokumente benötigen Zugriffskontrolle +- Audit-Logging für Änderungen empfohlen +- Geometriedaten sollten validiert werden + +### Empfohlene Maßnahmen: +```sql +-- Audit-Log Tabelle hinzufügen +CREATE TABLE audit_log ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + table_name VARCHAR(100) NOT NULL, + record_id UUID NOT NULL, + action VARCHAR(20) NOT NULL, + changed_by UUID NOT NULL, + changed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + old_values JSONB, + new_values JSONB +); +``` + +## 📈 Performance-Optimierung + +### Indices sind bereits erstellt für: +- Geografische Suchen (GIST Index auf Geometrien) +- Array-Suchen (GIN Index auf Arrays) +- Foreign Key Joins + +### Zusätzliche Optimierungen: +```sql +-- Materialized View für häufige Reports +CREATE MATERIALIZED VIEW mv_projekt_statistik AS +SELECT + k.label as kanton, + COUNT(DISTINCT pr.id) as anzahl_projekte, + COUNT(DISTINCT pa.id) as anzahl_parzellen, + SUM(ST_Area(pa.geo_umfang)) as gesamtflaeche_m2 +FROM projekt pr +JOIN projekt_parzelle pp ON pr.id = pp.projekt_id +JOIN parzelle pa ON pp.parzelle_id = pa.id +JOIN kanton k ON pa.kanton_id = k.id +GROUP BY k.label; + +-- Refresh periodisch +REFRESH MATERIALIZED VIEW mv_projekt_statistik; +``` + +## 🧪 Testing + +### Unit Tests +```python +# pytest Beispiel +def test_parzelle_creation(): + parzelle = Parzelle( + label="Test Parzelle", + parzellen_nummern=["1234"], + az=1.5, + bz=0.4 + ) + assert parzelle.label == "Test Parzelle" + assert parzelle.az == 1.5 +``` + +### Integration Tests +```typescript +// Jest Beispiel +describe('Projekt API', () => { + it('should create projekt with parzellen', async () => { + const projekt = await createProjekt({ + label: 'Test Projekt', + perimeter: [parzelle1.id, parzelle2.id] + }); + expect(projekt.perimeter).toHaveLength(2); + }); +}); +``` + +## 📚 Weitere Ressourcen + +- [PostGIS Dokumentation](https://postgis.net/docs/) +- [SQLAlchemy ORM](https://docs.sqlalchemy.org/) +- [Prisma Dokumentation](https://www.prisma.io/docs/) +- [Swisstopo - Schweizer Koordinatensysteme](https://www.swisstopo.admin.ch/de/wissen-fakten/geodaesie-vermessung/bezugsrahmen/lokal/lv95.html) + +## 🤝 Nächste Schritte + +1. **Validierung**: Review mit Architekten und Fachexperten +2. **API-Design**: RESTful oder GraphQL API implementieren +3. **Frontend-Prototyp**: Kartenansicht mit Leaflet/MapLibre +4. **GIS-Integration**: Anbindung an Swisstopo-APIs +5. **Workflow-Engine**: Statusübergänge und Genehmigungen +6. **Benutzer-Management**: Rollen und Berechtigungen + +## 📞 Support + +Bei Fragen zum Datenmodell: +- Öffne ein Issue im Repository +- Konsultiere die `DATENMODELL_DOKUMENTATION.md` +- Prüfe die Beispiel-Implementierungen in `models.py` oder `schema.prisma` + +## 📝 License + +[Lizenz hier einfügen] + +--- + +**Version:** 1.0 +**Letzte Aktualisierung:** 2025-10-24 +**Koordinatensystem:** LV95 (EPSG:2056) +**Datenbank:** PostgreSQL 15+ mit PostGIS 3.4+ diff --git a/mandates/pek/datenmodell/datenmodell-schema.json b/mandates/pek/datenmodell/datenmodell-schema.json new file mode 100644 index 0000000..ab27295 --- /dev/null +++ b/mandates/pek/datenmodell/datenmodell-schema.json @@ -0,0 +1,462 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Architektur-Planungs-App Datenmodell", + "version": "1.0", + + "definitions": { + "Projekt": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "Eindeutige Projekt-ID" + }, + "label": { + "type": "string", + "description": "Projektbezeichnung" + }, + "statusProzess": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "Eingang", + "Analyse", + "Studie", + "Planung", + "Baurechtsverfahren", + "Umsetzung", + "Archiv" + ] + }, + "description": "Aktuelle(r) Projektstatus/-stati" + }, + "perimeter": { + "type": "array", + "items": { + "$ref": "#/definitions/Parzelle" + }, + "description": "Parzellen im Projektperimeter" + }, + "dokumenteBauherrschaft": { + "type": "array", + "items": { + "$ref": "#/definitions/Dokument" + }, + "description": "Dokumente der Bauherrschaft" + }, + "dokumentePlanung": { + "type": "array", + "items": { + "$ref": "#/definitions/Dokument" + }, + "description": "Planungsdokumente" + }, + "geoBaulinie": { + "type": "array", + "items": { + "$ref": "#/definitions/GeoPunkt" + }, + "description": "Geografische Punkte der Baulinie" + }, + "kontextInformationen": { + "type": "array", + "items": { + "$ref": "#/definitions/Kontext" + }, + "description": "Kontextuelle Projektinformationen" + } + }, + "required": ["id", "label"] + }, + + "Dokument": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "Eindeutige Dokument-ID" + }, + "label": { + "type": "string", + "description": "Dokumentbezeichnung" + }, + "versionsbezeichnung": { + "type": "string", + "description": "Versionsnummer oder -bezeichnung" + }, + "typ": { + "type": "string", + "enum": ["Datei", "Url"], + "description": "Art des Dokuments" + }, + "format": { + "type": "string", + "description": "Dateiformat (z.B. PDF, DWG, URL)", + "examples": ["PDF", "DWG", "IFC", "DXF", "URL"] + }, + "tags": { + "type": "array", + "items": { + "$ref": "#/definitions/Tag" + }, + "description": "Kategorisierungs-Tags" + }, + "dokumentReferenz": { + "type": "string", + "description": "Pfad oder URL zum Dokument" + } + }, + "required": ["id", "label", "typ", "dokumentReferenz"] + }, + + "Tag": { + "type": "string", + "enum": [ + "Kataster Objekte", + "Kataster Werkeleitungen", + "Kataster Belastete Standorte", + "Kataster Bäume", + "Zonenplan", + "Planungs- und Baugesetz (PGB)", + "Bau- und Zonenordnung (BZO)", + "Parkplatzverordnung", + "Eigentümerauskunft", + "Grundbuchauszug" + ], + "description": "Vordefinierte Dokumentkategorien" + }, + + "Parzelle": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "Eindeutige Parzellen-ID" + }, + "label": { + "type": "string", + "description": "Parzellenbezeichnung" + }, + "parzellenNummern": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Offizielle Parzellennummern" + }, + "eigentuemerschaaft": { + "type": "string", + "description": "Eigentümer der Parzelle" + }, + "nachbarEigentuemer": { + "type": "array", + "items": { + "$ref": "#/definitions/Parzelle" + }, + "description": "Angrenzende Parzellen" + }, + "kontextLand": { + "$ref": "#/definitions/Land", + "description": "Land der Parzelle" + }, + "kontextKanton": { + "$ref": "#/definitions/Kanton", + "description": "Kanton der Parzelle" + }, + "kontextGemeinde": { + "$ref": "#/definitions/Gemeinde", + "description": "Gemeinde der Parzelle" + }, + "kontextInformationen": { + "type": "array", + "items": { + "$ref": "#/definitions/Kontext" + }, + "description": "Parzellenspezifische Kontextinformationen" + }, + "strasseNr": { + "type": "string", + "description": "Straße und Hausnummer" + }, + "geoUmfang": { + "type": "array", + "items": { + "$ref": "#/definitions/GeoPunkt" + }, + "description": "Geografische Umfangspunkte der Parzelle" + }, + "bauzone": { + "type": "string", + "description": "Bauzonenbezeichnung" + }, + "spezifischeDokumente": { + "type": "array", + "items": { + "$ref": "#/definitions/Dokument" + }, + "description": "Parzellenspezifische Dokumente" + }, + "hochwasserschutzzone": { + "type": "string", + "description": "Hochwasserschutzzone (falls zutreffend)" + }, + "laermschutzzone": { + "type": "string", + "description": "Lärmschutzzone" + }, + "grundwasserschutzzone": { + "type": "string", + "description": "Grundwasserschutzzone (falls zutreffend)" + }, + "parzelleBebaut": { + "$ref": "#/definitions/JaNein", + "description": "Ist die Parzelle bebaut?" + }, + "parzelleErschlossen": { + "$ref": "#/definitions/JaNein", + "description": "Ist die Parzelle erschlossen?" + }, + "hanglage": { + "$ref": "#/definitions/JaNein", + "description": "Liegt die Parzelle in Hanglage?" + }, + "az": { + "type": "number", + "description": "Ausnützungsziffer" + }, + "bz": { + "type": "number", + "description": "Bebauungsziffer" + }, + "vollgeschossZahl": { + "type": "integer", + "description": "Anzahl zulässiger Vollgeschosse" + }, + "anrechenbarDachgeschoss": { + "type": "number", + "description": "Anrechenbarer Anteil Dachgeschoss" + }, + "anrechenbarUntergeschoss": { + "type": "number", + "description": "Anrechenbarer Anteil Untergeschoss" + }, + "gebaeudehoehe_max": { + "type": "number", + "description": "Maximale Gebäudehöhe in Metern" + }, + "regelnGrenzabstand": { + "type": "string", + "description": "Regelungen zum Grenzabstand" + }, + "regelnMehrlaengenzuschlag": { + "type": "string", + "description": "Regelungen zum Mehrlängenzuschlag" + }, + "regelnMehrhoehenzuschlag": { + "type": "string", + "description": "Regelungen zum Mehrhöhenzuschlag" + } + }, + "required": ["id", "label"] + }, + + "Gemeinde": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "Eindeutige Gemeinde-ID" + }, + "label": { + "type": "string", + "description": "Gemeindename" + }, + "plz": { + "type": "string", + "description": "Postleitzahl" + }, + "dokumente": { + "type": "array", + "items": { + "$ref": "#/definitions/Dokument" + }, + "description": "Gemeindedokumente" + }, + "kontextInformationen": { + "type": "array", + "items": { + "$ref": "#/definitions/Kontext" + }, + "description": "Gemeindespezifische Kontextinformationen" + }, + "bzoAktuell": { + "$ref": "#/definitions/Dokument", + "description": "Aktuelle Bau- und Zonenordnung" + }, + "bzoRevision": { + "$ref": "#/definitions/Dokument", + "description": "BZO in Revision" + } + }, + "required": ["id", "label"] + }, + + "Kanton": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "Eindeutige Kanton-ID" + }, + "label": { + "type": "string", + "description": "Kantonsname" + }, + "dokumente": { + "type": "array", + "items": { + "$ref": "#/definitions/Dokument" + }, + "description": "Kantonale Dokumente" + }, + "kontextInformationen": { + "type": "array", + "items": { + "$ref": "#/definitions/Kontext" + }, + "description": "Kantonsspezifische Kontextinformationen" + }, + "baureglementAktuell": { + "$ref": "#/definitions/Dokument", + "description": "Aktuelles Baureglement" + }, + "baureglementRevision": { + "$ref": "#/definitions/Dokument", + "description": "Baureglement in Revision" + }, + "bauverordnungAktuell": { + "$ref": "#/definitions/Dokument", + "description": "Aktuelle Bauverordnung" + }, + "bauverordnungRevision": { + "$ref": "#/definitions/Dokument", + "description": "Bauverordnung in Revision" + } + }, + "required": ["id", "label"] + }, + + "Land": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "Eindeutige Land-ID" + }, + "label": { + "type": "string", + "description": "Landesname" + }, + "dokumente": { + "type": "array", + "items": { + "$ref": "#/definitions/Dokument" + }, + "description": "Nationale Dokumente" + }, + "kontextInformationen": { + "type": "array", + "items": { + "$ref": "#/definitions/Kontext" + }, + "description": "Nationale Kontextinformationen" + } + }, + "required": ["id", "label"] + }, + + "GeoPunkt": { + "type": "object", + "properties": { + "x": { + "type": "number", + "description": "X-Koordinate (Ost)" + }, + "y": { + "type": "number", + "description": "Y-Koordinate (Nord)" + }, + "z": { + "type": "number", + "description": "Z-Koordinate (Höhe)" + }, + "referenzen": { + "type": "array", + "items": { + "$ref": "#/definitions/GeoTag" + }, + "description": "Kategorisierung des Geopunkts" + } + }, + "required": ["x", "y"] + }, + + "GeoTag": { + "type": "string", + "enum": [ + "Referenzpunkt Kat. 1", + "Referenzpunkt Kat. 2", + "Referenzpunkt Kat. 3", + "Geometeraufnahme" + ], + "description": "Kategorien für Geopunkte" + }, + + "JaNein": { + "type": "string", + "enum": ["", "Ja", "Nein"], + "description": "Ja/Nein/Leer Wert" + }, + + "Kontext": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid", + "description": "Eindeutige Kontext-ID" + }, + "thema": { + "type": "string", + "description": "Thema der Kontextinformation", + "examples": [ + "Vorgaben zur Erdgeschossnutzung", + "Dienstbarkeiten", + "Anforderung Parkplätze", + "Ausnützungsübertragungen", + "Schadstoffbelastungen auf Parzellen", + "Aktive Gestaltungspläne", + "Lärmempfindlichkeitsstufen", + "Mögliche Wärmenutzung", + "Baumbestand auf privaten Grundstücken", + "Isos (Ortsbild, Schutzstatus, Denkmalschutz, Weilergebiet, etc.)", + "Naturgefahren", + "Verweis auf Revisionen" + ] + }, + "inhalt": { + "type": "string", + "description": "Detaillierter Inhalt der Kontextinformation" + } + }, + "required": ["id", "thema", "inhalt"] + } + } +} diff --git a/mandates/pek/datenmodell/datenmodell.mermaid b/mandates/pek/datenmodell/datenmodell.mermaid new file mode 100644 index 0000000..c3aaf2d --- /dev/null +++ b/mandates/pek/datenmodell/datenmodell.mermaid @@ -0,0 +1,45 @@ +--- +title: Hauptflüsse - Architektur-Planungs-App +--- +flowchart TD + Start([Datenmodell Start]) + + subgraph Admin[Administrative Ebene] + Land[LAND
Schweiz] + Kanton[KANTON
z.B. Zürich] + Gemeinde[GEMEINDE
z.B. Zürich Stadt] + Land --> Kanton + Kanton --> Gemeinde + end + + subgraph Geo[Geografische Daten] + Parzelle[PARZELLE
Grundstück mit
Bauparametern] + GeoPunkt[GEO_PUNKT
Koordinaten] + Gemeinde --> Parzelle + Parzelle --> GeoPunkt + end + + subgraph Core[Kern-Business-Logik] + Projekt[PROJEKT
Bauprojekt] + Dokument[DOKUMENT
Dateien & URLs] + Projekt -.Perimeter.-> Parzelle + Projekt -.Dokumente.-> Dokument + Parzelle -.Dokumente.-> Dokument + end + + subgraph Support[Unterstützende Daten] + Kontext[KONTEXT
Zusatzinfos] + Projekt --> Kontext + Parzelle --> Kontext + end + + Start --> Admin + + style Land fill:#50C878,stroke:#2D7A4A,stroke-width:2px,color:#fff + style Kanton fill:#50C878,stroke:#2D7A4A,stroke-width:2px,color:#fff + style Gemeinde fill:#50C878,stroke:#2D7A4A,stroke-width:2px,color:#fff + style Parzelle fill:#4A90E2,stroke:#2E5C8A,stroke-width:3px,color:#fff + style Projekt fill:#4A90E2,stroke:#2E5C8A,stroke-width:3px,color:#fff + style Dokument fill:#4A90E2,stroke:#2E5C8A,stroke-width:3px,color:#fff + style GeoPunkt fill:#F5A623,stroke:#C17D11,stroke-width:2px,color:#fff + style Kontext fill:#F5A623,stroke:#C17D11,stroke-width:2px,color:#fff \ No newline at end of file diff --git a/mandates/pek/datenmodell/migration_001_initial_schema.sql b/mandates/pek/datenmodell/migration_001_initial_schema.sql new file mode 100644 index 0000000..68090ec --- /dev/null +++ b/mandates/pek/datenmodell/migration_001_initial_schema.sql @@ -0,0 +1,393 @@ +-- ============================================================================ +-- Architektur-Planungs-App Datenbank Schema +-- PostgreSQL 15+ mit PostGIS 3.4+ +-- Schweizer Koordinatensystem: LV95 (EPSG:2056) +-- ============================================================================ + +-- PostGIS Extension aktivieren +CREATE EXTENSION IF NOT EXISTS postgis; + +-- UUID Extension für uuid_generate_v4() +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- ============================================================================ +-- ENUMS +-- ============================================================================ + +CREATE TYPE status_prozess AS ENUM ( + 'Eingang', + 'Analyse', + 'Studie', + 'Planung', + 'Baurechtsverfahren', + 'Umsetzung', + 'Archiv' +); + +CREATE TYPE dokument_typ AS ENUM ( + 'Datei', + 'Url' +); + +CREATE TYPE tag_typ AS ENUM ( + 'Kataster Objekte', + 'Kataster Werkeleitungen', + 'Kataster Belastete Standorte', + 'Kataster Bäume', + 'Zonenplan', + 'Planungs- und Baugesetz (PGB)', + 'Bau- und Zonenordnung (BZO)', + 'Parkplatzverordnung', + 'Eigentümerauskunft', + 'Grundbuchauszug' +); + +CREATE TYPE geo_tag_typ AS ENUM ( + 'Referenzpunkt Kat. 1', + 'Referenzpunkt Kat. 2', + 'Referenzpunkt Kat. 3', + 'Geometeraufnahme' +); + +CREATE TYPE ja_nein AS ENUM ( + '', + 'Ja', + 'Nein' +); + +-- ============================================================================ +-- HAUPTTABELLEN +-- ============================================================================ + +-- Land +CREATE TABLE land ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + label VARCHAR(255) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Kanton +CREATE TABLE kanton ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + label VARCHAR(255) NOT NULL, + baureglement_aktuell_id UUID, + baureglement_revision_id UUID, + bauverordnung_aktuell_id UUID, + bauverordnung_revision_id UUID, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Gemeinde +CREATE TABLE gemeinde ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + label VARCHAR(255) NOT NULL, + plz VARCHAR(10), + bzo_aktuell_id UUID, + bzo_revision_id UUID, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Dokument +CREATE TABLE dokument ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + label VARCHAR(255) NOT NULL, + versionsbezeichnung VARCHAR(100), + typ dokument_typ NOT NULL, + format VARCHAR(50), + dokument_referenz TEXT NOT NULL, + tags tag_typ[], + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Projekt +CREATE TABLE projekt ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + label VARCHAR(255) NOT NULL, + status_prozess status_prozess[], + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Parzelle +CREATE TABLE parzelle ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + label VARCHAR(255) NOT NULL, + parzellen_nummern VARCHAR(50)[], + eigentuemerschaaft VARCHAR(255), + strasse_nr VARCHAR(255), + + -- Geografischer Kontext + land_id UUID REFERENCES land(id), + kanton_id UUID REFERENCES kanton(id), + gemeinde_id UUID REFERENCES gemeinde(id), + + -- Geometrie (PostGIS) + geo_umfang GEOMETRY(POLYGON, 2056), + + -- Bauliche Parameter + bauzone VARCHAR(50), + az DECIMAL(5,2), + bz DECIMAL(5,2), + vollgeschoss_zahl INTEGER, + anrechenbar_dachgeschoss DECIMAL(3,2), + anrechenbar_untergeschoss DECIMAL(3,2), + gebaeudehoehe_max DECIMAL(6,2), + + -- Regelungen + regeln_grenzabstand TEXT, + regeln_mehrlaengenzuschlag TEXT, + regeln_mehrhoehenzuschlag TEXT, + + -- Schutzzonen + hochwasserschutzzone VARCHAR(100), + laermschutzzone VARCHAR(100), + grundwasserschutzzone VARCHAR(100), + + -- Eigenschaften + parzelle_bebaut ja_nein, + parzelle_erschlossen ja_nein, + hanglage ja_nein, + + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- GeoPunkt +CREATE TABLE geo_punkt ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + x DECIMAL(12,3) NOT NULL, -- LV95 Ostwert + y DECIMAL(12,3) NOT NULL, -- LV95 Nordwert + z DECIMAL(8,3), -- Höhe über Meer + referenzen geo_tag_typ[], + projekt_id UUID REFERENCES projekt(id) ON DELETE CASCADE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- Kontext (Polymorphe Beziehung) +CREATE TABLE kontext ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + thema VARCHAR(255) NOT NULL, + inhalt TEXT NOT NULL, + + -- Polymorphe Foreign Keys + projekt_id UUID REFERENCES projekt(id) ON DELETE CASCADE, + parzelle_id UUID REFERENCES parzelle(id) ON DELETE CASCADE, + land_id UUID REFERENCES land(id) ON DELETE CASCADE, + kanton_id UUID REFERENCES kanton(id) ON DELETE CASCADE, + gemeinde_id UUID REFERENCES gemeinde(id) ON DELETE CASCADE, + + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + + -- Constraint: Kontext muss zu genau einer Entität gehören + CONSTRAINT kontext_single_parent CHECK ( + (projekt_id IS NOT NULL)::INTEGER + + (parzelle_id IS NOT NULL)::INTEGER + + (land_id IS NOT NULL)::INTEGER + + (kanton_id IS NOT NULL)::INTEGER + + (gemeinde_id IS NOT NULL)::INTEGER = 1 + ) +); + +-- ============================================================================ +-- JUNCTION TABLES (Many-to-Many Beziehungen) +-- ============================================================================ + +-- Projekt <-> Parzelle +CREATE TABLE projekt_parzelle ( + projekt_id UUID NOT NULL REFERENCES projekt(id) ON DELETE CASCADE, + parzelle_id UUID NOT NULL REFERENCES parzelle(id) ON DELETE CASCADE, + PRIMARY KEY (projekt_id, parzelle_id) +); + +-- Projekt <-> Dokument (Bauherrschaft) +CREATE TABLE projekt_dokument_bauherrschaft ( + projekt_id UUID NOT NULL REFERENCES projekt(id) ON DELETE CASCADE, + dokument_id UUID NOT NULL REFERENCES dokument(id) ON DELETE CASCADE, + PRIMARY KEY (projekt_id, dokument_id) +); + +-- Projekt <-> Dokument (Planung) +CREATE TABLE projekt_dokument_planung ( + projekt_id UUID NOT NULL REFERENCES projekt(id) ON DELETE CASCADE, + dokument_id UUID NOT NULL REFERENCES dokument(id) ON DELETE CASCADE, + PRIMARY KEY (projekt_id, dokument_id) +); + +-- Parzelle <-> Parzelle (Nachbarn) +CREATE TABLE parzelle_nachbar ( + parzelle_id UUID NOT NULL REFERENCES parzelle(id) ON DELETE CASCADE, + nachbar_id UUID NOT NULL REFERENCES parzelle(id) ON DELETE CASCADE, + PRIMARY KEY (parzelle_id, nachbar_id), + CHECK (parzelle_id != nachbar_id) -- Parzelle kann nicht ihr eigener Nachbar sein +); + +-- Parzelle <-> Dokument +CREATE TABLE parzelle_dokument ( + parzelle_id UUID NOT NULL REFERENCES parzelle(id) ON DELETE CASCADE, + dokument_id UUID NOT NULL REFERENCES dokument(id) ON DELETE CASCADE, + PRIMARY KEY (parzelle_id, dokument_id) +); + +-- ============================================================================ +-- FOREIGN KEY CONSTRAINTS (nachträglich für Kanton/Gemeinde Dokumente) +-- ============================================================================ + +ALTER TABLE kanton + ADD CONSTRAINT fk_kanton_baureglement_aktuell + FOREIGN KEY (baureglement_aktuell_id) REFERENCES dokument(id), + ADD CONSTRAINT fk_kanton_baureglement_revision + FOREIGN KEY (baureglement_revision_id) REFERENCES dokument(id), + ADD CONSTRAINT fk_kanton_bauverordnung_aktuell + FOREIGN KEY (bauverordnung_aktuell_id) REFERENCES dokument(id), + ADD CONSTRAINT fk_kanton_bauverordnung_revision + FOREIGN KEY (bauverordnung_revision_id) REFERENCES dokument(id); + +ALTER TABLE gemeinde + ADD CONSTRAINT fk_gemeinde_bzo_aktuell + FOREIGN KEY (bzo_aktuell_id) REFERENCES dokument(id), + ADD CONSTRAINT fk_gemeinde_bzo_revision + FOREIGN KEY (bzo_revision_id) REFERENCES dokument(id); + +-- ============================================================================ +-- INDICES für Performance +-- ============================================================================ + +-- Projekt Indices +CREATE INDEX idx_projekt_status ON projekt USING GIN (status_prozess); + +-- Parzelle Indices +CREATE INDEX idx_parzelle_land ON parzelle(land_id); +CREATE INDEX idx_parzelle_kanton ON parzelle(kanton_id); +CREATE INDEX idx_parzelle_gemeinde ON parzelle(gemeinde_id); +CREATE INDEX idx_parzelle_bauzone ON parzelle(bauzone); +CREATE INDEX idx_parzelle_geo_umfang ON parzelle USING GIST(geo_umfang); + +-- Dokument Indices +CREATE INDEX idx_dokument_typ ON dokument(typ); +CREATE INDEX idx_dokument_tags ON dokument USING GIN (tags); + +-- GeoPunkt Indices +CREATE INDEX idx_geopunkt_projekt ON geo_punkt(projekt_id); +CREATE INDEX idx_geopunkt_referenzen ON geo_punkt USING GIN (referenzen); + +-- Kontext Indices +CREATE INDEX idx_kontext_projekt ON kontext(projekt_id); +CREATE INDEX idx_kontext_parzelle ON kontext(parzelle_id); +CREATE INDEX idx_kontext_land ON kontext(land_id); +CREATE INDEX idx_kontext_kanton ON kontext(kanton_id); +CREATE INDEX idx_kontext_gemeinde ON kontext(gemeinde_id); +CREATE INDEX idx_kontext_thema ON kontext(thema); + +-- ============================================================================ +-- TRIGGER für updated_at +-- ============================================================================ + +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +CREATE TRIGGER update_land_updated_at BEFORE UPDATE ON land + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_kanton_updated_at BEFORE UPDATE ON kanton + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_gemeinde_updated_at BEFORE UPDATE ON gemeinde + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_dokument_updated_at BEFORE UPDATE ON dokument + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_projekt_updated_at BEFORE UPDATE ON projekt + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_parzelle_updated_at BEFORE UPDATE ON parzelle + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_geo_punkt_updated_at BEFORE UPDATE ON geo_punkt + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_kontext_updated_at BEFORE UPDATE ON kontext + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +-- ============================================================================ +-- VIEWS für häufige Abfragen +-- ============================================================================ + +-- View: Parzellen mit vollständigem geografischem Kontext +CREATE VIEW v_parzelle_vollstaendig AS +SELECT + p.*, + l.label as land_name, + k.label as kanton_name, + g.label as gemeinde_name, + g.plz as gemeinde_plz, + ST_AsGeoJSON(p.geo_umfang) as geo_umfang_geojson, + ST_Area(p.geo_umfang) as flaeche_m2 +FROM parzelle p +LEFT JOIN land l ON p.land_id = l.id +LEFT JOIN kanton k ON p.kanton_id = k.id +LEFT JOIN gemeinde g ON p.gemeinde_id = g.id; + +-- View: Projekte mit Perimeter-Information +CREATE VIEW v_projekt_mit_perimeter AS +SELECT + pr.id, + pr.label, + pr.status_prozess, + COUNT(DISTINCT pp.parzelle_id) as anzahl_parzellen, + STRING_AGG(DISTINCT pa.label, ', ') as parzellen_labels +FROM projekt pr +LEFT JOIN projekt_parzelle pp ON pr.id = pp.projekt_id +LEFT JOIN parzelle pa ON pp.parzelle_id = pa.id +GROUP BY pr.id, pr.label, pr.status_prozess; + +-- ============================================================================ +-- BEISPIELDATEN +-- ============================================================================ + +-- Land Schweiz +INSERT INTO land (label) VALUES ('Schweiz'); + +-- Kantone (Beispiele) +INSERT INTO kanton (label) VALUES + ('Zürich'), + ('Bern'), + ('Luzern'); + +-- Gemeinden (Beispiele für Zürich) +INSERT INTO gemeinde (label, plz) VALUES + ('Zürich', '8000'), + ('Winterthur', '8400'), + ('Uster', '8610'); + +-- ============================================================================ +-- KOMMENTARE +-- ============================================================================ + +COMMENT ON TABLE projekt IS 'Bauprojekte mit Status und Perimeter'; +COMMENT ON TABLE parzelle IS 'Grundstücke mit baulichen und rechtlichen Eigenschaften'; +COMMENT ON TABLE dokument IS 'Dokumente und URLs mit Versionierung'; +COMMENT ON TABLE geo_punkt IS '3D-Punkte im LV95-Koordinatensystem'; +COMMENT ON TABLE kontext IS 'Flexible Kontextinformationen für verschiedene Entitäten'; + +COMMENT ON COLUMN parzelle.geo_umfang IS 'Parzellengrenze als PostGIS Polygon im LV95 (EPSG:2056)'; +COMMENT ON COLUMN parzelle.az IS 'Ausnützungsziffer'; +COMMENT ON COLUMN parzelle.bz IS 'Bebauungsziffer'; +COMMENT ON COLUMN geo_punkt.x IS 'LV95 Ostwert (E), typisch 2480000-2840000'; +COMMENT ON COLUMN geo_punkt.y IS 'LV95 Nordwert (N), typisch 1070000-1300000'; +COMMENT ON COLUMN geo_punkt.z IS 'Höhe über Meer in Metern'; + +-- ============================================================================ +-- Ende der Migration +-- ============================================================================ diff --git a/mandates/pek/datenmodell/models.py b/mandates/pek/datenmodell/models.py new file mode 100644 index 0000000..3d5598e --- /dev/null +++ b/mandates/pek/datenmodell/models.py @@ -0,0 +1,457 @@ +""" +SQLAlchemy Datenmodell für Architektur-Planungs-App +Verwendet PostgreSQL mit PostGIS Extension +""" + +from sqlalchemy import ( + Column, String, Integer, Float, Enum as SQLEnum, + ForeignKey, Table, Text, ARRAY +) +from sqlalchemy.dialects.postgresql import UUID, ENUM +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship +from geoalchemy2 import Geometry +import uuid +import enum + +Base = declarative_base() + + +# ============================================================================ +# ENUMS +# ============================================================================ + +class StatusProzess(enum.Enum): + EINGANG = "Eingang" + ANALYSE = "Analyse" + STUDIE = "Studie" + PLANUNG = "Planung" + BAURECHTSVERFAHREN = "Baurechtsverfahren" + UMSETZUNG = "Umsetzung" + ARCHIV = "Archiv" + + +class DokumentTyp(enum.Enum): + DATEI = "Datei" + URL = "Url" + + +class TagTyp(enum.Enum): + KATASTER_OBJEKTE = "Kataster Objekte" + KATASTER_WERKELEITUNGEN = "Kataster Werkeleitungen" + KATASTER_BELASTETE_STANDORTE = "Kataster Belastete Standorte" + KATASTER_BAEUME = "Kataster Bäume" + ZONENPLAN = "Zonenplan" + PGB = "Planungs- und Baugesetz (PGB)" + BZO = "Bau- und Zonenordnung (BZO)" + PARKPLATZVERORDNUNG = "Parkplatzverordnung" + EIGENTUEMER_AUSKUNFT = "Eigentümerauskunft" + GRUNDBUCHAUSZUG = "Grundbuchauszug" + + +class GeoTagTyp(enum.Enum): + REFERENZPUNKT_KAT1 = "Referenzpunkt Kat. 1" + REFERENZPUNKT_KAT2 = "Referenzpunkt Kat. 2" + REFERENZPUNKT_KAT3 = "Referenzpunkt Kat. 3" + GEOMETER_AUFNAHME = "Geometeraufnahme" + + +class JaNein(enum.Enum): + LEER = "" + JA = "Ja" + NEIN = "Nein" + + +# ============================================================================ +# JUNCTION TABLES (Many-to-Many Beziehungen) +# ============================================================================ + +# Projekt <-> Dokument (Bauherrschaft) +projekt_dokumente_bauherrschaft = Table( + 'projekt_dokumente_bauherrschaft', + Base.metadata, + Column('projekt_id', UUID(as_uuid=True), ForeignKey('projekt.id')), + Column('dokument_id', UUID(as_uuid=True), ForeignKey('dokument.id')) +) + +# Projekt <-> Dokument (Planung) +projekt_dokumente_planung = Table( + 'projekt_dokumente_planung', + Base.metadata, + Column('projekt_id', UUID(as_uuid=True), ForeignKey('projekt.id')), + Column('dokument_id', UUID(as_uuid=True), ForeignKey('dokument.id')) +) + +# Projekt <-> Parzelle +projekt_parzelle = Table( + 'projekt_parzelle', + Base.metadata, + Column('projekt_id', UUID(as_uuid=True), ForeignKey('projekt.id')), + Column('parzelle_id', UUID(as_uuid=True), ForeignKey('parzelle.id')) +) + +# Parzelle <-> Parzelle (Nachbarn) +parzelle_nachbar = Table( + 'parzelle_nachbar', + Base.metadata, + Column('parzelle_id', UUID(as_uuid=True), ForeignKey('parzelle.id')), + Column('nachbar_id', UUID(as_uuid=True), ForeignKey('parzelle.id')) +) + +# Parzelle <-> Dokument +parzelle_dokument = Table( + 'parzelle_dokument', + Base.metadata, + Column('parzelle_id', UUID(as_uuid=True), ForeignKey('parzelle.id')), + Column('dokument_id', UUID(as_uuid=True), ForeignKey('dokument.id')) +) + +# Dokument <-> Tag (Many-to-Many, da Tags wiederverwendbar) +dokument_tag = Table( + 'dokument_tag', + Base.metadata, + Column('dokument_id', UUID(as_uuid=True), ForeignKey('dokument.id')), + Column('tag', ENUM(TagTyp, name='tag_typ')) +) + + +# ============================================================================ +# MODELS +# ============================================================================ + +class Projekt(Base): + __tablename__ = 'projekt' + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + label = Column(String, nullable=False) + status_prozess = Column(ARRAY(ENUM(StatusProzess, name='status_prozess'))) + + # Relationships + perimeter = relationship( + 'Parzelle', + secondary=projekt_parzelle, + back_populates='projekte' + ) + + dokumente_bauherrschaft = relationship( + 'Dokument', + secondary=projekt_dokumente_bauherrschaft + ) + + dokumente_planung = relationship( + 'Dokument', + secondary=projekt_dokumente_planung + ) + + geo_baulinie = relationship('GeoPunkt', back_populates='projekt_baulinie') + + kontext_informationen = relationship( + 'Kontext', + foreign_keys='Kontext.projekt_id', + back_populates='projekt' + ) + + +class Parzelle(Base): + __tablename__ = 'parzelle' + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + label = Column(String, nullable=False) + parzellen_nummern = Column(ARRAY(String)) + eigentuemerschaaft = Column(String) + strasse_nr = Column(String) + + # Geografischer Kontext + land_id = Column(UUID(as_uuid=True), ForeignKey('land.id')) + kanton_id = Column(UUID(as_uuid=True), ForeignKey('kanton.id')) + gemeinde_id = Column(UUID(as_uuid=True), ForeignKey('gemeinde.id')) + + # Geometrie (PostGIS) + geo_umfang = Column(Geometry('POLYGON', srid=2056)) # LV95 Koordinatensystem + + # Bauliche Parameter + bauzone = Column(String) + az = Column(Float) # Ausnützungsziffer + bz = Column(Float) # Bebauungsziffer + vollgeschoss_zahl = Column(Integer) + anrechenbar_dachgeschoss = Column(Float) + anrechenbar_untergeschoss = Column(Float) + gebaeudehoehe_max = Column(Float) + + # Regelungen + regeln_grenzabstand = Column(Text) + regeln_mehrlaengenzuschlag = Column(Text) + regeln_mehrhoehenzuschlag = Column(Text) + + # Schutzzonen + hochwasserschutzzone = Column(String) + laermschutzzone = Column(String) + grundwasserschutzzone = Column(String) + + # Eigenschaften + parzelle_bebaut = Column(ENUM(JaNein, name='ja_nein')) + parzelle_erschlossen = Column(ENUM(JaNein, name='ja_nein')) + hanglage = Column(ENUM(JaNein, name='ja_nein')) + + # Relationships + projekte = relationship( + 'Projekt', + secondary=projekt_parzelle, + back_populates='perimeter' + ) + + nachbar_eigentuemer = relationship( + 'Parzelle', + secondary=parzelle_nachbar, + primaryjoin=id == parzelle_nachbar.c.parzelle_id, + secondaryjoin=id == parzelle_nachbar.c.nachbar_id + ) + + kontext_land = relationship('Land', back_populates='parzellen') + kontext_kanton = relationship('Kanton', back_populates='parzellen') + kontext_gemeinde = relationship('Gemeinde', back_populates='parzellen') + + spezifische_dokumente = relationship( + 'Dokument', + secondary=parzelle_dokument + ) + + kontext_informationen = relationship( + 'Kontext', + foreign_keys='Kontext.parzelle_id', + back_populates='parzelle' + ) + + +class Dokument(Base): + __tablename__ = 'dokument' + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + label = Column(String, nullable=False) + versionsbezeichnung = Column(String) + typ = Column(ENUM(DokumentTyp, name='dokument_typ'), nullable=False) + format = Column(String) + dokument_referenz = Column(String, nullable=False) # Pfad oder URL + + # Tags als Array (einfache Variante) + # Alternative: Many-to-Many über Junction Table + tags = Column(ARRAY(ENUM(TagTyp, name='tag_typ'))) + + +class GeoPunkt(Base): + __tablename__ = 'geo_punkt' + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + + # Koordinaten (einzeln gespeichert für Flexibilität) + x = Column(Float, nullable=False) + y = Column(Float, nullable=False) + z = Column(Float) # Optional + + # Alternative: PostGIS Point + # koordinaten = Column(Geometry('POINTZ', srid=2056)) + + # Kategorisierung + referenzen = Column(ARRAY(ENUM(GeoTagTyp, name='geo_tag_typ'))) + + # Foreign Keys (je nach Verwendung) + projekt_id = Column(UUID(as_uuid=True), ForeignKey('projekt.id')) + + # Relationships + projekt_baulinie = relationship('Projekt', back_populates='geo_baulinie') + + +class Kontext(Base): + __tablename__ = 'kontext' + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + thema = Column(String, nullable=False) + inhalt = Column(Text, nullable=False) + + # Polymorphe Beziehung (kann zu verschiedenen Entitäten gehören) + projekt_id = Column(UUID(as_uuid=True), ForeignKey('projekt.id')) + parzelle_id = Column(UUID(as_uuid=True), ForeignKey('parzelle.id')) + land_id = Column(UUID(as_uuid=True), ForeignKey('land.id')) + kanton_id = Column(UUID(as_uuid=True), ForeignKey('kanton.id')) + gemeinde_id = Column(UUID(as_uuid=True), ForeignKey('gemeinde.id')) + + # Relationships + projekt = relationship('Projekt', back_populates='kontext_informationen') + parzelle = relationship('Parzelle', back_populates='kontext_informationen') + land = relationship('Land', back_populates='kontext_informationen') + kanton = relationship('Kanton', back_populates='kontext_informationen') + gemeinde = relationship('Gemeinde', back_populates='kontext_informationen') + + +class Gemeinde(Base): + __tablename__ = 'gemeinde' + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + label = Column(String, nullable=False) + plz = Column(String) + + # BZO Dokumente + bzo_aktuell_id = Column(UUID(as_uuid=True), ForeignKey('dokument.id')) + bzo_revision_id = Column(UUID(as_uuid=True), ForeignKey('dokument.id')) + + # Relationships + parzellen = relationship('Parzelle', back_populates='kontext_gemeinde') + kontext_informationen = relationship( + 'Kontext', + foreign_keys='Kontext.gemeinde_id', + back_populates='gemeinde' + ) + + bzo_aktuell = relationship('Dokument', foreign_keys=[bzo_aktuell_id]) + bzo_revision = relationship('Dokument', foreign_keys=[bzo_revision_id]) + + +class Kanton(Base): + __tablename__ = 'kanton' + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + label = Column(String, nullable=False) + + # Regelwerke + baureglement_aktuell_id = Column(UUID(as_uuid=True), ForeignKey('dokument.id')) + baureglement_revision_id = Column(UUID(as_uuid=True), ForeignKey('dokument.id')) + bauverordnung_aktuell_id = Column(UUID(as_uuid=True), ForeignKey('dokument.id')) + bauverordnung_revision_id = Column(UUID(as_uuid=True), ForeignKey('dokument.id')) + + # Relationships + parzellen = relationship('Parzelle', back_populates='kontext_kanton') + kontext_informationen = relationship( + 'Kontext', + foreign_keys='Kontext.kanton_id', + back_populates='kanton' + ) + + baureglement_aktuell = relationship('Dokument', foreign_keys=[baureglement_aktuell_id]) + baureglement_revision = relationship('Dokument', foreign_keys=[baureglement_revision_id]) + bauverordnung_aktuell = relationship('Dokument', foreign_keys=[bauverordnung_aktuell_id]) + bauverordnung_revision = relationship('Dokument', foreign_keys=[bauverordnung_revision_id]) + + +class Land(Base): + __tablename__ = 'land' + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + label = Column(String, nullable=False) + + # Relationships + parzellen = relationship('Parzelle', back_populates='kontext_land') + kontext_informationen = relationship( + 'Kontext', + foreign_keys='Kontext.land_id', + back_populates='land' + ) + + +# ============================================================================ +# DATENBANK-SETUP +# ============================================================================ + +def create_database_engine(): + """ + Erstellt die Database Engine mit PostGIS Support + """ + from sqlalchemy import create_engine + + # Beispiel Connection String + DATABASE_URL = "postgresql://user:password@localhost:5432/architektur_app" + + engine = create_engine( + DATABASE_URL, + echo=True # SQL Logging für Development + ) + + return engine + + +def init_database(engine): + """ + Initialisiert die Datenbank und erstellt alle Tabellen + """ + # PostGIS Extension aktivieren (manuell oder via SQL) + with engine.connect() as conn: + conn.execute("CREATE EXTENSION IF NOT EXISTS postgis;") + conn.commit() + + # Tabellen erstellen + Base.metadata.create_all(engine) + + +# ============================================================================ +# BEISPIEL USAGE +# ============================================================================ + +if __name__ == "__main__": + from sqlalchemy.orm import sessionmaker + + # Engine erstellen + engine = create_database_engine() + + # Datenbank initialisieren + init_database(engine) + + # Session erstellen + Session = sessionmaker(bind=engine) + session = Session() + + # Beispiel: Schweiz erstellen + schweiz = Land( + label="Schweiz" + ) + session.add(schweiz) + + # Beispiel: Kanton Zürich erstellen + kanton_zh = Kanton( + label="Zürich" + ) + session.add(kanton_zh) + + # Beispiel: Gemeinde Zürich erstellen + gemeinde_zh = Gemeinde( + label="Zürich", + plz="8000" + ) + session.add(gemeinde_zh) + + # Beispiel: Parzelle erstellen + parzelle = Parzelle( + label="Bahnhofstrasse 1", + parzellen_nummern=["1234"], + eigentuemerschaaft="Mustermann AG", + strasse_nr="Bahnhofstrasse 1", + land_id=schweiz.id, + kanton_id=kanton_zh.id, + gemeinde_id=gemeinde_zh.id, + bauzone="W3", + az=1.5, + bz=0.4, + vollgeschoss_zahl=4, + parzelle_bebaut=JaNein.NEIN, + parzelle_erschlossen=JaNein.JA + ) + session.add(parzelle) + + # Beispiel: Projekt erstellen + projekt = Projekt( + label="Neubau Wohnhaus", + status_prozess=[StatusProzess.EINGANG, StatusProzess.ANALYSE] + ) + projekt.perimeter.append(parzelle) + session.add(projekt) + + # Beispiel: Kontext hinzufügen + kontext = Kontext( + thema="Dienstbarkeiten", + inhalt="Wegrecht zugunsten Parzelle 1235 entlang Ostgrenze", + parzelle_id=parzelle.id + ) + session.add(kontext) + + # Speichern + session.commit() + + print("Datenbank erfolgreich initialisiert und Beispieldaten eingefügt!") diff --git a/mandates/pek/datenmodell/schema.prisma b/mandates/pek/datenmodell/schema.prisma new file mode 100644 index 0000000..783257b --- /dev/null +++ b/mandates/pek/datenmodell/schema.prisma @@ -0,0 +1,323 @@ +// Prisma Schema für Architektur-Planungs-App +// Unterstützt PostgreSQL mit PostGIS + +generator client { + provider = "prisma-client-js" + previewFeatures = ["postgresqlExtensions"] +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") + extensions = [postgis] +} + +// ============================================================================ +// ENUMS +// ============================================================================ + +enum StatusProzess { + EINGANG + ANALYSE + STUDIE + PLANUNG + BAURECHTSVERFAHREN + UMSETZUNG + ARCHIV +} + +enum DokumentTyp { + DATEI + URL +} + +enum TagTyp { + KATASTER_OBJEKTE + KATASTER_WERKELEITUNGEN + KATASTER_BELASTETE_STANDORTE + KATASTER_BAEUME + ZONENPLAN + PGB + BZO + PARKPLATZVERORDNUNG + EIGENTUEMER_AUSKUNFT + GRUNDBUCHAUSZUG +} + +enum GeoTagTyp { + REFERENZPUNKT_KAT1 + REFERENZPUNKT_KAT2 + REFERENZPUNKT_KAT3 + GEOMETER_AUFNAHME +} + +enum JaNein { + LEER + JA + NEIN +} + +// ============================================================================ +// MODELS +// ============================================================================ + +model Projekt { + id String @id @default(uuid()) @db.Uuid + label String + statusProzess StatusProzess[] + + // Relationships + perimeter ParzelleInProjekt[] + dokumenteBauherrschaft ProjektDokumentBauherrschaft[] + dokumentePlanung ProjektDokumentPlanung[] + geoBaulinie GeoPunkt[] + kontextInformationen Kontext[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("projekt") +} + +model Parzelle { + id String @id @default(uuid()) @db.Uuid + label String + parzellenNummern String[] + eigentuemerschaaft String? + strasseNr String? + + // Geografischer Kontext + landId String? @db.Uuid + kantonId String? @db.Uuid + gemeindeId String? @db.Uuid + + // Geometrie (als GeoJSON oder WKT gespeichert) + // Für echte PostGIS Integration: Unsupported.("geometry(POLYGON, 2056)") + geoUmfangJson Json? // Alternative: GeoJSON Format + + // Bauliche Parameter + bauzone String? + az Float? + bz Float? + vollgeschossZahl Int? + anrechenbarDachgeschoss Float? + anrechenbarUntergeschoss Float? + gebaeudehoehe_max Float? + + // Regelungen + regelnGrenzabstand String? + regelnMehrlaengenzuschlag String? + regelnMehrhoehenzuschlag String? + + // Schutzzonen + hochwasserschutzzone String? + laermschutzzone String? + grundwasserschutzzone String? + + // Eigenschaften + parzelleBebaut JaNein? + parzelleErschlossen JaNein? + hanglage JaNein? + + // Relationships + projekte ParzelleInProjekt[] + nachbarEigentuemer_von ParzelleNachbar[] @relation("ParzelleNachbarVon") + nachbarEigentuemer_zu ParzelleNachbar[] @relation("ParzelleNachbarZu") + + kontextLand Land? @relation(fields: [landId], references: [id]) + kontextKanton Kanton? @relation(fields: [kantonId], references: [id]) + kontextGemeinde Gemeinde? @relation(fields: [gemeindeId], references: [id]) + + spezifischeDokumente ParzelleDokument[] + kontextInformationen Kontext[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("parzelle") +} + +model Dokument { + id String @id @default(uuid()) @db.Uuid + label String + versionsbezeichnung String? + typ DokumentTyp + format String? + dokumentReferenz String + tags TagTyp[] + + // Relationships + projekteBauherrschaft ProjektDokumentBauherrschaft[] + projektePlanung ProjektDokumentPlanung[] + parzellen ParzelleDokument[] + + // Spezifische Dokumente für Gemeinde/Kanton + gemeindeBzoAktuell Gemeinde[] @relation("GemeindeBZOAktuell") + gemeindeBzoRevision Gemeinde[] @relation("GemeindeBZORevision") + kantonBaureglementAktuell Kanton[] @relation("KantonBaureglementAktuell") + kantonBaureglementRevision Kanton[] @relation("KantonBaureglementRevision") + kantonBauverordnungAktuell Kanton[] @relation("KantonBauverordnungAktuell") + kantonBauverordnungRevision Kanton[] @relation("KantonBauverordnungRevision") + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("dokument") +} + +model GeoPunkt { + id String @id @default(uuid()) @db.Uuid + x Float + y Float + z Float? + referenzen GeoTagTyp[] + + // Foreign Keys + projektId String? @db.Uuid + projekt Projekt? @relation(fields: [projektId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("geo_punkt") +} + +model Kontext { + id String @id @default(uuid()) @db.Uuid + thema String + inhalt String @db.Text + + // Polymorphe Beziehung (kann zu verschiedenen Entitäten gehören) + projektId String? @db.Uuid + parzelleId String? @db.Uuid + landId String? @db.Uuid + kantonId String? @db.Uuid + gemeindeId String? @db.Uuid + + // Relationships + projekt Projekt? @relation(fields: [projektId], references: [id], onDelete: Cascade) + parzelle Parzelle? @relation(fields: [parzelleId], references: [id], onDelete: Cascade) + land Land? @relation(fields: [landId], references: [id], onDelete: Cascade) + kanton Kanton? @relation(fields: [kantonId], references: [id], onDelete: Cascade) + gemeinde Gemeinde? @relation(fields: [gemeindeId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("kontext") +} + +model Gemeinde { + id String @id @default(uuid()) @db.Uuid + label String + plz String? + + // BZO Dokumente + bzoAktuellId String? @db.Uuid + bzoRevisionId String? @db.Uuid + + // Relationships + parzellen Parzelle[] + kontextInformationen Kontext[] + + bzoAktuell Dokument? @relation("GemeindeBZOAktuell", fields: [bzoAktuellId], references: [id]) + bzoRevision Dokument? @relation("GemeindeBZORevision", fields: [bzoRevisionId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("gemeinde") +} + +model Kanton { + id String @id @default(uuid()) @db.Uuid + label String + + // Regelwerke + baureglementAktuellId String? @db.Uuid + baureglementRevisionId String? @db.Uuid + bauverordnungAktuellId String? @db.Uuid + bauverordnungRevisionId String? @db.Uuid + + // Relationships + parzellen Parzelle[] + kontextInformationen Kontext[] + + baureglementAktuell Dokument? @relation("KantonBaureglementAktuell", fields: [baureglementAktuellId], references: [id]) + baureglementRevision Dokument? @relation("KantonBaureglementRevision", fields: [baureglementRevisionId], references: [id]) + bauverordnungAktuell Dokument? @relation("KantonBauverordnungAktuell", fields: [bauverordnungAktuellId], references: [id]) + bauverordnungRevision Dokument? @relation("KantonBauverordnungRevision", fields: [bauverordnungRevisionId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("kanton") +} + +model Land { + id String @id @default(uuid()) @db.Uuid + label String + + // Relationships + parzellen Parzelle[] + kontextInformationen Kontext[] + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("land") +} + +// ============================================================================ +// JUNCTION TABLES (Many-to-Many) +// ============================================================================ + +model ParzelleInProjekt { + projektId String @db.Uuid + parzelleId String @db.Uuid + projekt Projekt @relation(fields: [projektId], references: [id], onDelete: Cascade) + parzelle Parzelle @relation(fields: [parzelleId], references: [id], onDelete: Cascade) + + @@id([projektId, parzelleId]) + @@map("projekt_parzelle") +} + +model ParzelleNachbar { + parzelleId String @db.Uuid + nachbarId String @db.Uuid + parzelle Parzelle @relation("ParzelleNachbarVon", fields: [parzelleId], references: [id], onDelete: Cascade) + nachbar Parzelle @relation("ParzelleNachbarZu", fields: [nachbarId], references: [id], onDelete: Cascade) + + @@id([parzelleId, nachbarId]) + @@map("parzelle_nachbar") +} + +model ProjektDokumentBauherrschaft { + projektId String @db.Uuid + dokumentId String @db.Uuid + projekt Projekt @relation(fields: [projektId], references: [id], onDelete: Cascade) + dokument Dokument @relation(fields: [dokumentId], references: [id], onDelete: Cascade) + + @@id([projektId, dokumentId]) + @@map("projekt_dokument_bauherrschaft") +} + +model ProjektDokumentPlanung { + projektId String @db.Uuid + dokumentId String @db.Uuid + projekt Projekt @relation(fields: [projektId], references: [id], onDelete: Cascade) + dokument Dokument @relation(fields: [dokumentId], references: [id], onDelete: Cascade) + + @@id([projektId, dokumentId]) + @@map("projekt_dokument_planung") +} + +model ParzelleDokument { + parzelleId String @db.Uuid + dokumentId String @db.Uuid + parzelle Parzelle @relation(fields: [parzelleId], references: [id], onDelete: Cascade) + dokument Dokument @relation(fields: [dokumentId], references: [id], onDelete: Cascade) + + @@id([parzelleId, dokumentId]) + @@map("parzelle_dokument") +} diff --git a/mandates/pek/bostich_hole_punch.3ds b/mandates/pek/testing/bostich_hole_punch.3ds similarity index 100% rename from mandates/pek/bostich_hole_punch.3ds rename to mandates/pek/testing/bostich_hole_punch.3ds diff --git a/mandates/pek/create_bostich_3ds.py b/mandates/pek/testing/create_bostich_3ds.py similarity index 100% rename from mandates/pek/create_bostich_3ds.py rename to mandates/pek/testing/create_bostich_3ds.py diff --git a/mandates/pek/create_bostich_3ds_fixed.py b/mandates/pek/testing/create_bostich_3ds_fixed.py similarity index 100% rename from mandates/pek/create_bostich_3ds_fixed.py rename to mandates/pek/testing/create_bostich_3ds_fixed.py diff --git a/mandates/pek/fotos_bostich/20250925_205036.jpg b/mandates/pek/testing/fotos_bostich/20250925_205036.jpg similarity index 100% rename from mandates/pek/fotos_bostich/20250925_205036.jpg rename to mandates/pek/testing/fotos_bostich/20250925_205036.jpg diff --git a/mandates/pek/fotos_bostich/20250925_205044.jpg b/mandates/pek/testing/fotos_bostich/20250925_205044.jpg similarity index 100% rename from mandates/pek/fotos_bostich/20250925_205044.jpg rename to mandates/pek/testing/fotos_bostich/20250925_205044.jpg diff --git a/mandates/pek/fotos_bostich/20250925_205050.jpg b/mandates/pek/testing/fotos_bostich/20250925_205050.jpg similarity index 100% rename from mandates/pek/fotos_bostich/20250925_205050.jpg rename to mandates/pek/testing/fotos_bostich/20250925_205050.jpg diff --git a/mandates/pek/haus_final.dxf b/mandates/pek/testing/haus_final.dxf similarity index 100% rename from mandates/pek/haus_final.dxf rename to mandates/pek/testing/haus_final.dxf