This commit is contained in:
ValueOn AG 2025-10-24 21:42:47 +02:00
parent 588e4170ac
commit 15f0e51bd0
14 changed files with 2647 additions and 0 deletions

View file

@ -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<br/>Schweiz]
Kanton[KANTON<br/>z.B. Zürich]
Gemeinde[GEMEINDE<br/>z.B. Zürich Stadt]
Land --> Kanton
Kanton --> Gemeinde
end
subgraph Geo[Geografische Daten]
Parzelle[PARZELLE<br/>Grundstück mit<br/>Bauparametern]
GeoPunkt[GEO_PUNKT<br/>Koordinaten]
Gemeinde --> Parzelle
Parzelle --> GeoPunkt
end
subgraph Core[Kern-Business-Logik]
Projekt[PROJEKT<br/>Bauprojekt]
Dokument[DOKUMENT<br/>Dateien & URLs]
Projekt -.Perimeter.-> Parzelle
Projekt -.Dokumente.-> Dokument
Parzelle -.Dokumente.-> Dokument
end
subgraph Support[Unterstützende Daten]
Kontext[KONTEXT<br/>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

View file

@ -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+

View file

@ -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"]
}
}
}

View file

@ -0,0 +1,45 @@
---
title: Hauptflüsse - Architektur-Planungs-App
---
flowchart TD
Start([Datenmodell Start])
subgraph Admin[Administrative Ebene]
Land[LAND<br/>Schweiz]
Kanton[KANTON<br/>z.B. Zürich]
Gemeinde[GEMEINDE<br/>z.B. Zürich Stadt]
Land --> Kanton
Kanton --> Gemeinde
end
subgraph Geo[Geografische Daten]
Parzelle[PARZELLE<br/>Grundstück mit<br/>Bauparametern]
GeoPunkt[GEO_PUNKT<br/>Koordinaten]
Gemeinde --> Parzelle
Parzelle --> GeoPunkt
end
subgraph Core[Kern-Business-Logik]
Projekt[PROJEKT<br/>Bauprojekt]
Dokument[DOKUMENT<br/>Dateien & URLs]
Projekt -.Perimeter.-> Parzelle
Projekt -.Dokumente.-> Dokument
Parzelle -.Dokumente.-> Dokument
end
subgraph Support[Unterstützende Daten]
Kontext[KONTEXT<br/>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

View file

@ -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
-- ============================================================================

View file

@ -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!")

View file

@ -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")
}

View file

Before

Width:  |  Height:  |  Size: 715 KiB

After

Width:  |  Height:  |  Size: 715 KiB

View file

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

Before

Width:  |  Height:  |  Size: 559 KiB

After

Width:  |  Height:  |  Size: 559 KiB