bwscheddebrock_trassenplaner/index.html

3141 lines
176 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TrassenPlaner Pro</title>
<!-- Libraries (CDN via jsDelivr) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6.5.0/turf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/shpjs@latest/dist/shp.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lucide@latest/dist/umd/lucide.js"></script>
<script src="https://cdn.jsdelivr.net/npm/leaflet-editable@1.2.0/src/Leaflet.Editable.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/proj4@2.11.0/dist/proj4.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js"></script>
<style>
:root {
--primary: #cca300;
/* Dark Yellow */
--primary-hover: #b38f00;
--bg-glass: rgba(255, 253, 235, 0.85);
/* Warm base */
--border-glass: rgba(204, 163, 0, 0.2);
--sidebar-width: 340px;
--panel-width: 310px;
--success: #299500;
/* CMYK 75 10 100 35 */
--danger: #ef4444;
--warning: #66E659;
/* CMYK 60 10 65 0 */
--info: #8CE6D9;
/* CMYK 45 10 15 0 */
--gray: #64748b;
--corporate-teal: #004b50;
/* CMYK 100 25 25 40 - Adjusted to Dark Blue */
--light-green: #CCFFCC;
/* CMYK 20 0 20 0 */
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
height: 100vh;
overflow: hidden;
display: flex;
background: #0f172a;
}
/* Layout Structure */
#sidebar {
width: var(--sidebar-width);
height: 100%;
background: var(--corporate-teal);
border-right: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
z-index: 1000;
transition: transform 0.3s ease;
color: white;
}
#map-container {
flex: 1;
position: relative;
}
#map {
width: 100%;
height: 100%;
}
#right-panel {
position: absolute;
top: 20px;
right: 20px;
width: var(--panel-width);
background: var(--bg-glass);
backdrop-filter: blur(16px);
border-radius: 16px;
border: 1px solid var(--border-glass);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
z-index: 1000;
padding: 20px;
}
/* Sidebar CRM Elements */
.sidebar-header {
padding: 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-content {
flex: 1;
overflow-y: auto;
padding: 12px 16px;
}
.search-box {
width: 100%;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: rgba(0, 0, 0, 0.2);
color: white;
margin-bottom: 20px;
outline: none;
transition: all 0.2s;
}
.search-box::placeholder {
color: rgba(255, 255, 255, 0.5);
}
.search-box:focus {
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
border-color: var(--info);
}
.status-select {
margin-top: 10px;
width: 100%;
padding: 8px;
border-radius: 8px;
font-size: 12px;
background: rgba(0, 0, 0, 0.2);
color: white;
border: 1px solid rgba(255, 255, 255, 0.1);
outline: none;
}
.owner-card {
background: white;
border-radius: 10px;
padding: 10px 12px;
margin-bottom: 8px;
border: 1px solid rgba(204, 163, 0, 0.1);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
color: #333;
/* Base text color for cards */
}
.owner-card .owner-name {
color: var(--corporate-teal);
font-weight: 700;
margin-bottom: 4px;
}
.owner-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 20px -8px rgba(204, 163, 0, 0.2);
border-color: var(--info);
}
/* Variant Stats */
.stat-card {
background: var(--bg-glass);
padding: 16px;
border-radius: 12px;
margin-top: 10px;
font-size: 14px;
border: 1px solid var(--border-glass);
}
.stat-row {
display: flex;
justify-content: space-between;
margin-bottom: 6px;
padding-bottom: 4px;
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
}
.drilling-stat {
color: #000000;
font-weight: 700;
}
/* Actions */
.btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
border: none;
transition: all 0.2s;
font-size: 14px;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary) 0%, var(--corporate-teal) 100%);
color: white;
border: none;
box-shadow: 0 4px 10px rgba(0, 115, 115, 0.3);
}
.btn-primary:hover {
transform: translateY(-1px);
box-shadow: 0 6px 14px rgba(0, 115, 115, 0.4);
filter: brightness(1.1);
}
.btn-outline {
background: white;
border: 1.5px solid var(--primary);
color: var(--primary);
}
.btn-outline:hover {
background: var(--light-green);
border-color: var(--success);
color: var(--success);
}
/* Options Menu Styles */
.options-menu {
position: absolute;
bottom: 75px;
left: 20px;
right: 20px;
background: #ffffff;
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
display: none;
flex-direction: column;
padding: 10px;
gap: 5px;
z-index: 1001;
border: 1px solid rgba(255, 255, 255, 0.2);
animation: slideUp 0.2s ease-out;
}
@keyframes slideUp {
from {
transform: translateY(10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.options-menu.active {
display: flex;
}
.options-menu .btn {
justify-content: flex-start;
padding: 8px 12px;
font-size: 13px;
color: #333;
border: none;
background: transparent;
}
.options-menu .btn:hover {
background: rgba(0, 75, 80, 0.05);
color: var(--corporate-teal);
}
.options-menu hr {
border: none;
border-top: 1px solid rgba(0, 0, 0, 0.05);
margin: 5px 0;
}
/* Required Plots Panel */
#required-plots-container {
margin-top: 20px;
max-height: 300px;
overflow-y: auto;
border-top: 1px solid rgba(0, 0, 0, 0.05);
padding-top: 16px;
}
.plot-card {
background: white;
border-radius: 10px;
padding: 12px;
margin-bottom: 8px;
border: 1px solid rgba(204, 163, 0, 0.1);
font-size: 12px;
display: flex;
align-items: center;
gap: 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.03);
transition: all 0.2s;
}
.plot-card:hover {
border-color: var(--primary);
background: var(--light-green);
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
border: 1px solid rgba(0, 0, 0, 0.1);
}
.plot-info {
flex: 1;
}
.plot-owner {
font-weight: 600;
color: #334155;
margin-bottom: 2px;
}
.plot-details {
color: #64748b;
}
.btn-outline:hover {
background: #f8fafc;
border-color: var(--primary);
color: var(--primary);
}
/* Dropzone Overlay */
#dropzone {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(37, 99, 235, 0.1);
border: 4px dashed var(--primary);
z-index: 5000;
display: none;
justify-content: center;
align-items: center;
font-size: 24px;
color: var(--primary);
font-weight: 700;
pointer-events: none;
}
.status-badge {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
margin-right: 8px;
}
/* Marker & Icon Custom Styles */
.anlage-icon {
background: white;
border: 2px solid var(--corporate-teal);
border-radius: 50%;
width: 32px !important;
height: 32px !important;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
color: var(--corporate-teal);
}
.anlage-icon svg {
width: 20px;
height: 20px;
}
.infrastructure-icon {
background: white;
border: 2px solid var(--corporate-teal);
border-radius: 8px;
width: 32px !important;
height: 32px !important;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
color: var(--corporate-teal);
}
.infrastructure-icon svg {
width: 20px;
height: 20px;
}
.nvp-icon {
background: #000;
border: 2px solid white;
border-radius: 4px;
width: 24px;
height: 24px;
}
/* Editing Marker Styles */
.leaflet-vertex-icon {
background-color: #fff !important;
border: 2px solid var(--corporate-teal) !important;
border-radius: 50% !important;
width: 12px !important;
height: 12px !important;
margin-left: -6px !important;
margin-top: -6px !important;
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8), 0 2px 8px rgba(0, 0, 0, 0.3) !important;
cursor: move !important;
}
.leaflet-middle-marker {
background-color: rgba(255, 255, 255, 0.9) !important;
border: 2px dashed var(--primary) !important;
border-radius: 50% !important;
width: 12px !important;
height: 12px !important;
margin-left: -6px !important;
margin-top: -6px !important;
opacity: 0.8 !important;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2) !important;
cursor: cell !important;
}
.segment-label {
display: none;
/* Hidden by default */
background: rgba(255, 255, 255, 0.9);
border: 1px solid #333;
border-radius: 4px;
padding: 1px 4px;
font-size: 11px;
font-weight: 700;
white-space: nowrap;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
align-items: center;
justify-content: center;
transform: translateY(-5px);
text-shadow: 0 0 2px white;
}
.map-zoom-mid .segment-label,
.map-zoom-mid .drilling-segment-label {
display: flex !important;
}
.drilling-segment-label {
display: none !important;
}
/* Tooltips for owners */
.owner-label {
display: none;
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
font-size: 10px;
color: #333;
text-align: center;
line-height: 1.2;
padding: 2px 4px;
border-radius: 4px;
white-space: nowrap;
}
/* Only show tooltips when map container has map-zoom-high class */
.map-zoom-high .owner-label {
display: block;
}
/* Panel Collapse CSS */
#sidebar {
transition: margin-left 0.3s ease;
}
#sidebar.collapsed {
margin-left: calc(-1 * var(--sidebar-width));
}
#right-panel {
transition: transform 0.3s ease;
}
#right-panel.collapsed {
transform: translateX(calc(100% + 40px));
}
.panel-toggle-btn {
position: absolute;
background: white;
border: 1px solid #ccc;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
cursor: pointer;
z-index: 2000;
padding: 8px 4px;
display: flex;
align-items: center;
justify-content: center;
color: var(--corporate-teal);
transition: left 0.3s ease, right 0.3s ease;
}
.sidebar-collapse-btn {
left: var(--sidebar-width);
top: 50%;
transform: translateY(-50%);
border-radius: 0 8px 8px 0;
border-left: none;
}
#sidebar.collapsed ~ .sidebar-collapse-btn {
left: 0;
}
.right-panel-collapse-btn {
right: calc(var(--panel-width) + 20px);
top: 40px;
border-radius: 8px 0 0 8px;
border-right: none;
transition: right 0.3s ease;
}
#right-panel.collapsed ~ .right-panel-collapse-btn {
right: 0;
}
.right-panel-collapse-btn-hidden {
}
#db-status-indicator {
position: fixed;
bottom: 25px;
right: 25px;
z-index: 10001;
padding: 10px 20px;
background: #004b50;
color: white;
border-radius: 30px;
font-size: 13px;
display: flex;
align-items: center;
gap: 10px;
opacity: 0;
visibility: hidden;
transform: translateY(10px);
transition: all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
pointer-events: none;
border: 1px solid rgba(255,255,255,0.1);
}
#db-status-indicator.active {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.variant-map-label div {
transform: translate(-50%, -50%);
pointer-events: none;
}
</style>
</head>
<body>
<div id="dropzone">Shapefiles hier ablegen...</div>
<!-- Note Modal -->
<div id="note-modal" style="display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: white; padding: 20px; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); z-index: 9999; flex-direction: column; width: 320px;">
<div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e2e8f0; padding-bottom: 10px; margin-bottom: 10px;">
<h4 style="margin: 0; color: var(--corporate-teal); display: flex; align-items: center; gap: 8px;"><i data-lucide="file-edit" style="width: 18px;"></i> Notiz bearbeiten</h4>
<button id="note-modal-close" style="background: none; border: none; cursor: pointer; color: #64748b; padding: 4px; border-radius: 4px;"><i data-lucide="x" style="width: 18px;"></i></button>
</div>
<textarea id="note-modal-text" placeholder="Notiz hier eingeben..." style="width: 100%; height: 120px; padding: 10px; border: 1px solid #cbd5e1; border-radius: 8px; resize: vertical; font-family: inherit; font-size: 13px;"></textarea>
<button id="note-modal-save" class="btn btn-primary" style="align-self: flex-end; margin-top: 15px;">Speichern</button>
</div>
<aside id="sidebar">
<div class="sidebar-header">
<img src="Logos/20201202-ENWELO-Logo-4c_ohne_Claim.png" alt="ENWELO"
style="width: 100%; max-width: 180px; margin-bottom: 12px;">
<div style="display: flex; flex-direction: column; margin-bottom: 12px;">
<h1
style="font-size: 18px; display: flex; align-items: center; gap: 8px; color: white; margin: 0; font-weight: 800; letter-spacing: -0.5px;">
<i data-lucide="map-pin"></i> TrassenPlaner
</h1>
<span
style="font-size: 10px; color: var(--info); opacity: 0.9; margin-left: 30px; margin-top: -2px; font-weight: 600;">v1.0.5</span>
</div>
<div id="quick-toggles" style="padding: 0; border-top: 1px solid rgba(255, 255, 255, 0.1); padding-top: 12px; margin-top: 8px;">
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer; color: white; font-size: 12px; font-weight: 600; padding: 4px 0;">
<input type="checkbox" id="sidebar-toggle-owner-status" checked onchange="document.getElementById('toggle-owner-status').checked = this.checked; toggleLayer('ownerStatus', this.checked); document.getElementById('status-legend').style.display = this.checked ? 'block' : 'none';" style="width: 14px; height: 14px; cursor: pointer;">
<span style="display: flex; align-items: center; gap: 6px;">
<i data-lucide="shield-check" style="width: 16px; color: var(--info);"></i> Sicherungsstand aktiv
</span>
</label>
</div>
<div id="owner-search-container" style="margin-top: 8px;">
<input type="text" class="search-box" id="owner-search" placeholder="Eigentümer suchen..."
style="margin-bottom: 0; padding: 8px 12px; border-radius: 8px; background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.2); color: white; font-size: 13px;">
</div>
</div>
<div class="sidebar-content">
<div id="owner-list">
<div style="text-align: center; color: #888; margin-top: 40px;">
<i data-lucide="upload-cloud" style="width: 48px; height: 48px; opacity: 0.5;"></i>
<p>Shapefiles laden...</p>
</div>
</div>
</div>
<div
style="padding: 20px; border-top: 1px solid rgba(255, 255, 255, 0.1); display: flex; flex-direction: column; gap: 10px; position: relative;">
<!-- Legend Container -->
<details id="status-legend" style="margin: 0 0 10px 0; padding: 12px; background: rgba(0,0,0,0.25); border-radius: 12px; border: 1px solid rgba(255,255,255,0.05); display: none;" open>
<summary style="cursor: pointer; list-style: none; outline: none; margin: 0; color: white; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; opacity: 0.7; display: flex; align-items: center; gap: 8px;">
<i data-lucide="info" style="width: 14px;"></i> Legende Sicherungsstand
<i data-lucide="chevron-down" style="width: 12px; margin-left: auto;"></i>
</summary>
<div id="legend-items" style="display: flex; flex-direction: column; gap: 6px; margin-top: 10px;">
<!-- Legend items will be injected here -->
</div>
</details>
<!-- New Options Menu -->
<div id="options-popup" class="options-menu">
<div id="layer-status" style="padding: 8px; font-size: 11px; margin-bottom: 5px; color: #333;">
<div style="font-weight: bold; margin-bottom: 8px; color: var(--corporate-teal);"><i data-lucide="layers" style="width: 14px; display: inline-block; vertical-align: middle;"></i> Kartenebenen</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer;"><input type="checkbox" id="toggle-owners" checked onchange="toggleLayer('owners', this.checked)"> Flurstück-Grenzen</label>
<span id="status-owners" style="color: var(--danger); font-weight: 600;">Fehlt</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; padding-left: 18px;">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer;"><input type="checkbox" id="toggle-owner-status" checked onchange="toggleLayer('ownerStatus', this.checked)"> Sicherungsstand</label>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; padding-left: 18px;">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer;"><input type="checkbox" id="toggle-owner-color" onchange="toggleLayer('ownerColor', this.checked)"> Farbe: Eigentümer</label>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer;"><input type="checkbox" id="toggle-usage" onchange="toggleLayer('usage', this.checked)"> Nutzung-Daten</label>
<span id="status-usage" style="color: var(--danger); font-weight: 600;">Fehlt</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer;"><input type="checkbox" id="toggle-wea" checked onchange="toggleLayer('wea', this.checked)"> WEA-Standorte</label>
<span id="status-wea" style="color: var(--danger); font-weight: 600;">Fehlt</span>
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer;"><input type="checkbox" id="toggle-infrastructure" checked onchange="toggleLayer('infrastructure', this.checked)"> Infrastruktur (UW)</label>
<span id="status-infrastructure" style="color: var(--danger); font-weight: 600;">Fehlt</span>
</div>
</div>
<hr>
<button class="btn" id="btn-sync-folder">
<i data-lucide="link" style="width: 16px;"></i> Ordner synchronisieren
</button>
<hr>
<button class="btn" onclick="document.getElementById('file-input').click()">
<i data-lucide="file-plus" style="width: 16px;"></i> Dateien wählen
</button>
<hr>
<button class="btn" id="btn-export">
<i data-lucide="download" style="width: 16px;"></i> GDB Export (.zip)
</button>
<button class="btn" id="btn-import">
<i data-lucide="upload" style="width: 16px;"></i> Projekt-Import (.json)
</button>
<hr>
<div style="padding: 8px; font-size: 11px; margin-bottom: 5px; color: #333;">
<div style="font-weight: bold; margin-bottom: 8px; color: var(--corporate-teal);"><i data-lucide="wrench" style="width: 14px; display: inline-block; vertical-align: middle;"></i> Werkzeuge</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<button class="btn btn-outline" id="btn-draw" style="justify-content: center; padding: 6px; font-size: 11px; border-color: #ddd;">
<i data-lucide="plus" style="width: 14px;"></i> Zeichnen
</button>
<button class="btn btn-outline" id="btn-measure" style="justify-content: center; padding: 6px; font-size: 11px; border-color: #ddd;">
<i data-lucide="ruler" style="width: 14px;"></i> Messen
</button>
</div>
</div>
<hr>
<button class="btn" id="btn-pdf-export-alt">
<i data-lucide="file-text" style="width: 16px;"></i> Als PDF exportieren
</button>
</div>
<button class="btn btn-outline" id="btn-toggle-options"
style="width: 100%; border-color: rgba(255,255,255,0.3); color: white; background: rgba(255,255,255,0.05);">
<i data-lucide="settings"></i> Optionen
</button>
<input type="file" id="file-input" multiple style="display: none;">
</div>
</aside>
<button id="sidebar-toggle-btn" class="panel-toggle-btn sidebar-collapse-btn" onclick="document.getElementById('sidebar').classList.toggle('collapsed'); this.querySelector('i').setAttribute('data-lucide', document.getElementById('sidebar').classList.contains('collapsed') ? 'chevron-right' : 'chevron-left'); lucide.createIcons({root: this});">
<i data-lucide="chevron-left"></i>
</button>
<main id="map-container">
<div id="map"></div>
<div id="right-panel">
<h3 style="font-size: 16px; margin-bottom: 16px; display: flex; align-items: center; gap: 8px;">
<i data-lucide="activity"></i> Varianten
</h3>
<div id="variant-controls">
<!-- Variants will be injected here -->
</div>
<div id="required-plots-container" style="margin-top: 20px;">
<!-- Required plots will be injected here -->
</div>
</div>
</div>
<button id="right-panel-toggle-btn" class="panel-toggle-btn right-panel-collapse-btn" onclick="document.getElementById('right-panel').classList.toggle('collapsed'); this.querySelector('i').setAttribute('data-lucide', document.getElementById('right-panel').classList.contains('collapsed') ? 'chevron-left' : 'chevron-right'); lucide.createIcons({root: this});">
<i data-lucide="chevron-right"></i>
</button>
</main>
<script>
const API_BASE = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'
? "http://localhost:3000/api"
: window.location.origin + "/api";
const ENWELO_LOGO_BASE64 = `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`;
// --- Core Application State ---
const STATUS_CONFIG = {
"Ablehnung": "#FF0000",
"Erwartet Negativ": "#FFA500",
"Unentschlossen": "#FFFF00",
"Unbekannt": "transparent",
"Erwartet Positiv": "#90EE90",
"Zusage (mündlich)": "#008000",
"Vertraglich gesichert": "#006400",
"In der Projektgesellschaft": "#EE82EE"
};
const STATUS_DESCRIPTIONS = {
"Ablehnung": "Der Eigentümer lehnt die Trassenführung strikt ab.",
"Erwartet Negativ": "Erste Signale oder Tendenzen deuten auf eine Ablehnung hin.",
"Unentschlossen": "Rückmeldung ist noch offen oder der Eigentümer zögert.",
"Unbekannt": "Bisher kein Kontakt erfolgt; Status ist völlig offen.",
"Erwartet Positiv": "Eine grundsätzliche Bereitschaft zur Zustimmung wird erwartet.",
"Zusage (mündlich)": "Klare mündliche Zustimmung liegt vor, der schriftliche Vertrag ist noch offen.",
"Vertraglich gesichert": "Der Vertrag liegt unterschrieben vor.",
"In der Projektgesellschaft": "Grundstückseigentümer ist in der Projektgesellschaft"
};
const STATUS_ORDER = [
"Ablehnung", "Erwartet Negativ", "Unentschlossen", "Unbekannt",
"Erwartet Positiv", "Zusage (mündlich)", "Vertraglich gesichert",
"In der Projektgesellschaft"
];
try {
proj4.defs("EPSG:25832", "+proj=utm +zone=32 +ellps=GRS80 +units=m +no_defs");
} catch (e) {
console.error("Proj4 definition failed:", e);
}
const state = {
owners: [],
usage: [],
wea: [],
highlightedOwners: {},
variants: [
{ id: 1, name: "Variante A", color: "#cca300", routes: [], active: true, visible: true, stats: { total: 0, drilling: 0, open: 0, muffen: 0 } },
{ id: 2, name: "Variante B", color: "#cca300", routes: [], active: false, visible: true, stats: { total: 0, drilling: 0, open: 0, muffen: 0 } },
{ id: 3, name: "Variante C", color: "#cca300", routes: [], active: false, visible: true, stats: { total: 0, drilling: 0, open: 0, muffen: 0 } },
{ id: 4, name: "Variante D", color: "#cca300", routes: [], active: false, visible: true, stats: { total: 0, drilling: 0, open: 0, muffen: 0 } },
{ id: 5, name: "Variante E", color: "#cca300", routes: [], active: false, visible: true, stats: { total: 0, drilling: 0, open: 0, muffen: 0 } },
{ id: 6, name: "Variante F", color: "#cca300", routes: [], active: false, visible: true, stats: { total: 0, drilling: 0, open: 0, muffen: 0 } },
{ id: 7, name: "Variante G", color: "#cca300", routes: [], active: false, visible: true, stats: { total: 0, drilling: 0, open: 0, muffen: 0 } },
{ id: 8, name: "Variante H", color: "#cca300", routes: [], active: false, visible: true, stats: { total: 0, drilling: 0, open: 0, muffen: 0 } }
],
visibleLayers: {
owners: true,
ownerStatus: true,
ownerColor: false,
usage: false,
wea: true,
infrastructure: true
},
directoryHandle: null,
measurementLines: [],
isDrawing: false,
isMeasuring: false
};
// --- Modal & Highlight Helpers ---
window.currentNoteOwnerKey = null;
window.openNoteModal = function(ownerKey, currentNote) {
window.currentNoteOwnerKey = ownerKey;
document.getElementById('note-modal-text').value = currentNote || '';
document.getElementById('note-modal').style.display = 'flex';
};
window.toggleHighlightOwner = function(ownerKey) {
if (!state.highlightedOwners) state.highlightedOwners = {};
state.highlightedOwners[ownerKey] = !state.highlightedOwners[ownerKey];
updateOwnerLayer();
renderOwnerList(document.getElementById('owner-search').value);
};
// --- Table Download Generator ---
window.downloadVariantTable = function(id) {
const v = state.variants.find(varnt => varnt.id === id);
if (!v || !v.routes || v.routes.length < 2 || !state.owners || !state.owners.features) {
alert("Nicht genug Daten vorhanden (Trasse / Flurstücke fehlen).");
return;
}
const { jsPDF } = window.jspdf;
const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
if (ENWELO_LOGO_BASE64) {
try {
doc.addImage("data:image/png;base64," + ENWELO_LOGO_BASE64, "PNG", 14, 10, 45, 17.5);
} catch(e) { }
}
doc.setFont("helvetica", "bold");
doc.setFontSize(14);
doc.setTextColor(0, 75, 80);
doc.text(`Betroffene Flurstücke - ${v.name}`, 14, 30);
doc.setFont("helvetica", "normal");
doc.setFontSize(9);
const footerText = "Dieses Dokument wurde vom Auftragnehmer erstellt und aus dem Planungstool automatisch generiert.";
const latLngs = getFlattenedCoords(v.routes);
const lineStr = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
const lineBbox = turf.bbox(lineStr);
const tableData = [];
state.owners.features.forEach(f => {
try {
const obsBbox = turf.bbox(f);
if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] ||
lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return;
if (turf.booleanIntersects(lineStr, f)) {
const p = f.properties;
let intersectLength = 0;
try {
const split = turf.lineSplit(lineStr, f);
if (split.features.length > 0) {
split.features.forEach(seg => {
const mid = turf.along(seg, turf.length(seg)/2, {units: 'kilometers'});
if (turf.booleanPointInPolygon(mid, f)) {
intersectLength += turf.length(seg, {units: 'meters'});
}
});
} else {
const mid = turf.along(lineStr, turf.length(lineStr)/2, {units: 'kilometers'});
if (turf.booleanPointInPolygon(mid, f)) {
intersectLength = turf.length(lineStr, {units: 'meters'});
}
}
} catch(e) { }
tableData.push([
`${p.Vorname || ''} ${p.Nachname || ''}`.trim() || 'Unbekannt',
p.status || 'Unbekannt',
p.Flurstueck || p.Flurstück || p.flurstueck || p.FLST_NR || p.FLST || p.flst || p.NUMMER || p.FlSt || '-',
p.Flur || p.FLUR || p.flur || p.FL || p.fl || '-',
p.Gemarkung || p.GEMARKUNG || '-',
intersectLength > 0 ? (intersectLength.toFixed(1) + ' m') : 'Betroffen'
]);
}
} catch(e) {}
});
const uniqueMap = new Map();
tableData.forEach(row => {
const key = row[0]+row[2]+row[3]+row[4];
if (!uniqueMap.has(key)) uniqueMap.set(key, row);
});
const finalData = Array.from(uniqueMap.values());
doc.autoTable({
startY: 38,
head: [['Name', 'Sicherungsstand', 'Flurstück', 'Flur', 'Gemarkung', 'Trassenlänge (ca.)']],
body: finalData,
styles: { fontSize: 9 },
headStyles: { fillColor: [0, 75, 80] },
margin: { bottom: 20 },
didDrawPage: function(data) {
doc.setFontSize(8);
doc.setTextColor(150);
doc.text(footerText, 14, doc.internal.pageSize.height - 10);
}
});
doc.save(`Flurstuecke_${v.name.replace(/\s+/g, '_')}.pdf`);
};
window.addEventListener('DOMContentLoaded', () => {
const btnClose = document.getElementById('note-modal-close');
const btnSave = document.getElementById('note-modal-save');
if (btnClose) {
btnClose.addEventListener('click', () => {
document.getElementById('note-modal').style.display = 'none';
});
}
if (btnSave) {
btnSave.addEventListener('click', () => {
if (!window.currentNoteOwnerKey) return;
const newNote = document.getElementById('note-modal-text').value;
const parts = window.currentNoteOwnerKey.split('|');
const pNachname = parts[0];
const pVorname = parts[1];
const pStr = parts[2];
const pOrt = parts[3];
// Update all matching features in state
if (state.owners && state.owners.features) {
state.owners.features.forEach(f => {
const p = f.properties;
if (p.Nachname == pNachname && (p.Vorname || '') == (pVorname || '') &&
(p.Str_HNr || p.STR || '') == pStr && (p.Ort || p.ORT || '') == pOrt) {
p.notiz = newNote;
}
});
}
document.getElementById('note-modal').style.display = 'none';
renderOwnerList(document.getElementById('owner-search').value);
updateOwnerLayer();
if (state.owners && state.owners.features) {
state.owners.features.forEach(f => {
const p = f.properties;
if (p.Nachname == pNachname && (p.Vorname || '') == (pVorname || '') &&
(p.Str_HNr || p.STR || '') == pStr && (p.Ort || p.ORT || '') == pOrt) {
saveOwner(f);
}
});
}
const activeV = state.variants.find(v => v.active);
if (activeV) updateRequiredPlots(activeV);
if (activeV) saveVariantToDB(activeV);
});
}
});
// --- Helpers ---
function getStatusColor(s) {
if (!s) return STATUS_CONFIG["Unbekannt"];
const raw = s.toLowerCase().trim();
// Re-indexing logic for database strings (handles both old and new labels)
if (raw === 'ablehnung' || raw.includes("status 01")) return STATUS_CONFIG["Ablehnung"];
if (raw === 'erwartet negativ' || raw.includes("status 02")) return STATUS_CONFIG["Erwartet Negativ"];
if (raw === 'unentschlossen' || raw.includes("status 03")) return STATUS_CONFIG["Unentschlossen"];
if (raw.includes("projektgesellschaft") || (raw.includes("status 04") && !raw.includes("unbekannt")) || raw.includes("status 09"))
return STATUS_CONFIG["In der Projektgesellschaft"];
if (raw === 'unbekannt' || raw.includes("status 04") || raw.includes("status 05")) return STATUS_CONFIG["Unbekannt"];
if (raw === 'erwartet positiv' || raw.includes("status 05") || raw.includes("status 07")) return STATUS_CONFIG["Erwartet Positiv"];
if (raw.includes("zusage") || raw.includes("status 06") || raw.includes("status 08")) return STATUS_CONFIG["Zusage (mündlich)"];
if (raw.includes("vertraglich gesichert") || raw.includes("status 07") || (raw.includes("status 09") && !raw.includes("projekt")))
return STATUS_CONFIG["Vertraglich gesichert"];
return STATUS_CONFIG["Unbekannt"];
}
function getStatusKey(s) {
if (!s) return "Unbekannt";
const color = getStatusColor(s);
const entry = Object.entries(STATUS_CONFIG).find(([k, v]) => v === color);
return entry ? entry[0] : "Unbekannt";
}
function getUsageColor(type) {
const t = (type || '').toLowerCase();
if (t.includes('gehölz') || t.includes('wald')) return '#22c55e'; // Green
if (t.includes('gewässer') || t.includes('wasser')) return '#3b82f6'; // Blue
if (t.includes('bahn') || t.includes('straße') || t.includes('baufläche') || t.includes('weg') || t.includes('verkehr')) return '#ef4444'; // Red
return '#f59e0b'; // Default orange/warning
}
function usageStyle(f) {
const color = getUsageColor(f.properties.nutzart || f.properties.NUTZART || '');
return {
color: color,
weight: 2,
fillOpacity: 0.4,
fillColor: color
};
}
const ownerColors = new Map();
function getOwnerRandomColor(ownerKey) {
if (!ownerKey) return '#cbd5e1';
if (ownerColors.has(ownerKey)) return ownerColors.get(ownerKey);
// Generate a random but stable color (hsl)
let hash = 0;
for (let i = 0; i < ownerKey.length; i++) {
hash = ownerKey.charCodeAt(i) + ((hash << 5) - hash);
}
const h = Math.abs(hash % 360);
const s = 60 + Math.abs(hash % 30); // 60-90% saturation
const l = 40 + Math.abs(hash % 20); // 40-60% lightness
const color = `hsl(${h}, ${s}%, ${l}%)`;
ownerColors.set(ownerKey, color);
return color;
}
// --- Map Initialization ---
const map = L.map('map', {
zoomControl: false,
editable: true,
preferCanvas: true, // Crucial for html2canvas to capture vector layers correctly
maxZoom: 22
}).setView([52.2291, 7.5501], 14);
// Manage visibility of detailed owner labels based on zoom
function updateMapZoomClass() {
const zoom = map.getZoom();
const container = map.getContainer();
// Level 1: Cable Dimensions (Bemaßung)
if (zoom >= 17) {
container.classList.add('map-zoom-mid');
} else {
container.classList.remove('map-zoom-mid');
}
// Level 2: Owner Labels (Flurstücksbeschriftung)
if (zoom >= 17) {
container.classList.add('map-zoom-high');
} else {
container.classList.remove('map-zoom-high');
}
}
map.on('zoomend', updateMapZoomClass);
updateMapZoomClass(); // initialize
// Robust initialization for Leaflet.Editable
function ensureEditable() {
if (!map.editTools) {
if (window.L && L.Editable) {
map.editTools = new L.Editable(map, {
lineOptions: { color: '#cca300', weight: 6 },
vertexOptions: { color: '#cca300', radius: 5 },
middleMarkerOptions: { color: '#cca300', opacity: 0.5, radius: 3 },
drawingLineOptions: { color: '#cca300', weight: 6 },
drawingVertexOptions: { color: '#cca300', radius: 5 }
});
console.log("Leaflet.Editable manually initialized");
} else {
console.error("Leaflet.Editable library not found!");
}
}
// Create panes for routes to ensure proper layering
if (!map.getPane('trassenPane')) {
const trassenPane = map.createPane('trassenPane');
trassenPane.style.zIndex = 550; // Base route
}
if (!map.getPane('drillingPane')) {
const drillingPane = map.createPane('drillingPane');
drillingPane.style.zIndex = 560; // Drilling (over route)
}
if (!map.getPane('labelPane')) {
const labelPane = map.createPane('labelPane');
labelPane.style.zIndex = 570; // Labels and Markers (top)
}
if (!map.getPane('ownerStatusPane')) {
const p = map.createPane('ownerStatusPane');
p.style.zIndex = 410; // Below routes (550)
}
if (!map.getPane('ownerOutlinePane')) {
const p = map.createPane('ownerOutlinePane');
p.style.zIndex = 420; // Boundaires above status but below routes
}
}
ensureEditable();
const osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap',
maxZoom: 22,
maxNativeZoom: 19
});
const satelliteLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
maxZoom: 22,
maxNativeZoom: 18
});
// Add default layer
satelliteLayer.addTo(map);
// Layer Control
const baseMaps = {
"Luftbild (Satellit)": satelliteLayer,
"OpenStreetMap": osmLayer
};
L.control.layers(baseMaps, null, { position: 'topleft' }).addTo(map);
L.control.zoom({ position: 'bottomright' }).addTo(map);
// Global editing events for real-time synchronization
map.on('editable:vertex:drag editable:drawing:move editable:vertex:dragend editable:vertex:deleted editable:vertex:new editable:drawing:end editable:editing editable:drag', (e) => {
try {
// Determine which layer is being edited (support for direct layer drag and vertex drag)
const layer = e.layer || (e.vertex ? (e.vertex.editor ? e.vertex.editor.feature : e.vertex.polyline) : null);
if (!layer || !layer.getLatLngs) return;
const variant = state.variants.find(v => routeLayers[v.id] === layer);
if (variant) {
const liveCoords = getFlattenedCoords(layer.getLatLngs());
const tempPt = (e.type === 'editable:drawing:move') ? e.latlng : null;
const isContinuous = ['editable:vertex:drag', 'editable:drawing:move', 'editable:drag', 'editable:editing'].includes(e.type);
// Always make sure variant.routes is up to date during dragging to allow calculateStats and labels to function properly
if (isContinuous) {
renderSegmentLabels(variant, liveCoords, tempPt);
} else {
variant.routes = liveCoords;
calculateStats(variant);
updateVariantStatsUI(variant);
renderVariants();
if (variant.active) updateRequiredPlots(variant);
const _activeV = state.variants.find(v => v.active); if (_activeV) saveVariantToDB(routeLayers[_activeV.id] || _activeV);
}
}
} catch (err) {
console.error("Sync error:", err);
}
});
// --- Layer Groups ---
const layers = {
owners: L.geoJSON(null, {
style: (f) => {
const p = f.properties;
const mergedOwners = p._mergedOwners || [p];
const isHighlighted = mergedOwners.some(mp => {
const key = `${mp.Nachname}|${mp.Vorname}|${mp.Str_HNr || mp.STR}|${mp.Ort || mp.ORT}`;
return state.highlightedOwners && state.highlightedOwners[key];
});
if (isHighlighted) {
return { color: '#ffee00', weight: 6, opacity: 1, fillColor: '#ffee00', fillOpacity: 0.3 };
}
return {
color: '#000000',
weight: 2.5,
opacity: 1,
fillColor: '#ffffff',
fillOpacity: 0.01,
pane: 'ownerOutlinePane'
};
},
onEachFeature: (f, layer) => {
const p = f.properties;
// Handle pre-merged multiple owners (from updateOwnerLayer)
const mergedOwners = p._mergedOwners || [p];
layer.bindPopup(() => {
return mergedOwners.map(mp => {
return `<b>${mp.Vorname || ''} ${mp.Nachname || ''}</b><br>${mp.Str_HNr || mp.STR || ''}<br>${mp.PLZ || ''} ${mp.Ort || mp.ORT || ''}<br>Status: <b>${mp.status || 'Unbekannt'}</b>`;
}).join('<hr style="margin: 8px 0; border: none; border-top: 1px solid #ccc;">');
});
// Add text showing who it belongs to
// Join multiple names with " und "
const names = mergedOwners.map(mp => `${mp.Vorname || ''} ${mp.Nachname || ''}`.trim()).filter(n => n);
// If more than two, maybe just show "Name u. a." or list them all. We will list up to 2, then "u. a.".
let nameDisplay = '';
if (names.length === 1) nameDisplay = names[0];
else if (names.length === 2) nameDisplay = names.join(' und ');
else if (names.length > 2) nameDisplay = names[0] + ' u. a.';
// Label collection
let labelParts = [];
if (nameDisplay) labelParts.push(`<b>${nameDisplay}</b>`);
// Abbreviated Flur and Flurstueck
const flur = p.Flur || p.FLUR || p.flur || p.FL || p.fl || '';
const flst = p.Flurstueck || p.Flurstück || p.flurstueck || p.FLST_NR || p.FLST || p.flst || p.NUMMER || p.FlSt || '';
let flurFlstText = '';
if (flur) flurFlstText += `Fl. ${flur}`;
if (flst) {
if (flurFlstText) flurFlstText += `, FlSt ${flst}`;
else flurFlstText += `FlSt ${flst}`;
}
if (flurFlstText) labelParts.push(flurFlstText);
if (labelParts.length > 0) {
layer.bindTooltip(labelParts.join('<br>'), {
permanent: true,
direction: 'center',
className: 'owner-label',
interactive: false
});
}
}
}).addTo(map),
ownerColor: L.geoJSON(null, {
style: (f) => {
const properties = f.properties._mergedOwners && f.properties._mergedOwners.length > 0 ? f.properties._mergedOwners[0] : f.properties;
const name = (properties.Nachname || '').trim();
const street = (properties.Str_HNr || properties.STR || '').trim();
const key = `${name}-${street}`;
return {
stroke: false,
fillColor: getOwnerRandomColor(key),
fillOpacity: 0.7
};
},
interactive: false
}).addTo(map),
ownerStatus: L.geoJSON(null, {
style: (f) => {
const properties = f.properties._mergedOwners && f.properties._mergedOwners.length > 0 ? f.properties._mergedOwners[0] : f.properties;
const status = (properties.status || 'Unbekannt').trim();
const color = getStatusColor(status);
const isTransparent = color === 'transparent';
return {
stroke: false,
fillColor: color,
fillOpacity: isTransparent ? 0.0 : 0.6,
pane: 'ownerStatusPane'
};
},
interactive: false
}).addTo(map),
usage: L.geoJSON(null, {
style: usageStyle,
onEachFeature: (f, layer) => {
if (f.properties && (f.properties.nutzart || f.properties.NUTZART)) {
layer.bindPopup(`Nutzung: <b>${f.properties.nutzart || f.properties.NUTZART}</b>`);
}
}
}).addTo(map),
wea: L.geoJSON(null, {
pointToLayer: (feature, latlng) => {
return L.marker(latlng, {
icon: L.divIcon({
className: 'anlage-icon',
html: '<i data-lucide="fan"></i>',
iconSize: [32, 32],
iconAnchor: [16, 16],
popupAnchor: [0, -16]
})
});
},
onEachFeature: (f, layer) => {
const name = f.properties.Name || f.properties.NAME || f.properties.bezeichnun || 'WEA';
layer.bindPopup(`<b>${name}</b><br>Windenergieanlage`);
// Immediately initialize lucide icon for this marker
layer.on('add', function () {
const el = layer.getElement();
if (el && window.lucide) {
lucide.createIcons({ root: el });
}
});
}
}).addTo(map),
infrastructure: L.geoJSON(null, {
pointToLayer: (feature, latlng) => {
return L.marker(latlng, {
icon: L.divIcon({
className: 'infrastructure-icon',
html: '<i data-lucide="building-2"></i>',
iconSize: [32, 32],
iconAnchor: [16, 16],
popupAnchor: [0, -16]
})
});
},
onEachFeature: (f, layer) => {
const name = f.properties.name || f.properties.type || 'Infrastruktur';
layer.bindPopup(`<b>${name}</b>`);
layer.on('add', function () {
const el = layer.getElement();
if (el && window.lucide) {
lucide.createIcons({ root: el });
}
});
}
}).addTo(map)
};
// --- File Selection Implementation ---
document.getElementById('file-input').addEventListener('change', async (e) => {
const files = Array.from(e.target.files);
await processFileList(files);
});
// --- Drag & Drop Implementation ---
const dropzone = document.getElementById('dropzone');
window.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.style.display = 'flex'; });
window.addEventListener('dragleave', (e) => { if (e.relatedTarget === null) dropzone.style.display = 'none'; });
window.addEventListener('drop', async (e) => {
e.preventDefault();
dropzone.style.display = 'none';
const files = Array.from(e.dataTransfer.files);
await processFileList(files);
});
async function processFileList(files) {
console.log("Processing files:", files.map(f => f.name));
const fileGroups = {};
for (let file of files) {
const parts = file.name.split('.');
const ext = parts.pop().toLowerCase();
const base = parts.join('.').toLowerCase();
if (!fileGroups[base]) fileGroups[base] = {};
fileGroups[base][ext] = file;
}
for (let base in fileGroups) {
if (fileGroups[base].shp && fileGroups[base].dbf) {
await processShapefileGroup(base, fileGroups[base]);
}
}
}
async function processShapefileGroup(name, group) {
console.log("Loading Shapefile Group:", name);
// Handle both File objects (drag-drop) and ArrayBuffers (folder sync)
const shpBuffer = group.shp instanceof ArrayBuffer ? group.shp : await group.shp.arrayBuffer();
const dbfBuffer = group.dbf instanceof ArrayBuffer ? group.dbf : await group.dbf.arrayBuffer();
try {
let geojson = await shp.combine([
shp.parseShp(shpBuffer),
shp.parseDbf(dbfBuffer)
]);
// Check for UTM32 and transform if necessary
// Easting in Germany is usually > 200,000
const firstFeat = geojson.features[0];
if (firstFeat && firstFeat.geometry) {
let coord = firstFeat.geometry.coordinates;
while (Array.isArray(coord[0])) coord = coord[0];
const testVal = coord[0]; // Assuming x is first
if (testVal > 180 || testVal < -180) {
console.log("Large coordinates detected (" + testVal + "), transforming from UTM32 to WGS84...");
geojson = transformGeoJSON(geojson, "EPSG:25832", "EPSG:4326");
}
}
const lowerName = name.toLowerCase();
if (lowerName.includes('eigentuemer') || lowerName.includes('eigentümer')) {
state.owners = geojson;
state.owners.features.forEach((f, i) => f.id = f.id || i);
updateOwnerLayer();
renderOwnerList();
// Zoom to owner data if available
if (layers.owners.getBounds().isValid()) {
map.fitBounds(layers.owners.getBounds(), { padding: [20, 20] });
}
} else if (lowerName.includes('nutzung')) {
state.usage = geojson;
cachedObstacles = null; // Reset cache for drilling logic
updateUsageLayer();
} else if (lowerName.includes('wea')) {
state.wea = geojson;
updateWEALayer();
}
} catch (err) {
console.error("Fehler beim Kombinieren der Shapefile-Daten:", err);
}
}
function transformGeoJSON(geojson, from, to) {
const transformed = JSON.parse(JSON.stringify(geojson));
transformed.features.forEach(feature => {
if (!feature.geometry) return;
const transformPoint = (c) => proj4(from, to, c);
if (feature.geometry.type === 'Point') {
feature.geometry.coordinates = transformPoint(feature.geometry.coordinates);
} else if (feature.geometry.type === 'LineString' || feature.geometry.type === 'MultiPoint') {
feature.geometry.coordinates = feature.geometry.coordinates.map(transformPoint);
} else if (feature.geometry.type === 'Polygon' || feature.geometry.type === 'MultiLineString') {
feature.geometry.coordinates = feature.geometry.coordinates.map(ring => ring.map(transformPoint));
} else if (feature.geometry.type === 'MultiPolygon') {
feature.geometry.coordinates = feature.geometry.coordinates.map(poly => poly.map(ring => ring.map(transformPoint)));
}
});
return transformed;
}
function updateOwnerLayer() {
if (!state.owners || !state.owners.features) return;
const statusEl = document.getElementById('status-owners');
if (statusEl) {
statusEl.innerText = `${state.owners.features.length} Objekte`;
statusEl.style.color = 'var(--success)';
}
layers.owners.clearLayers();
if (layers.ownerStatus) layers.ownerStatus.clearLayers();
if (layers.ownerColor) layers.ownerColor.clearLayers();
const processedFeatures = [];
// Group features logic...
const groupedFeaturesMap = new Map();
state.owners.features.forEach(f => {
const p = f.properties;
const flur = p.Flur || p.FLUR || '';
const flst = p.Flurstueck || p.Flurstück || p.flurstueck || p.FLST_NR || '';
const key = `${flur}-${flst}`;
if (flur && flst) {
if (groupedFeaturesMap.has(key)) {
groupedFeaturesMap.get(key).properties._mergedOwners.push(p);
} else {
const featureCopy = JSON.parse(JSON.stringify(f));
featureCopy.properties._mergedOwners = [p];
groupedFeaturesMap.set(key, featureCopy);
processedFeatures.push(featureCopy);
}
} else {
const featureCopy = JSON.parse(JSON.stringify(f));
featureCopy.properties._mergedOwners = [p];
processedFeatures.push(featureCopy);
}
});
if (state.visibleLayers.owners) {
layers.owners.addData({ type: "FeatureCollection", features: processedFeatures });
}
if (state.visibleLayers.ownerStatus && layers.ownerStatus) {
layers.ownerStatus.addData({ type: "FeatureCollection", features: processedFeatures });
}
if (state.visibleLayers.ownerColor && layers.ownerColor) {
layers.ownerColor.addData({ type: "FeatureCollection", features: processedFeatures });
}
// WICHTIG: Liste der Eigentümer in der Seitenleiste initialisieren!
renderOwnerList();
}
window.toggleLayer = (layerKey, visible) => {
state.visibleLayers[layerKey] = visible;
if (layerKey === 'owners' || layerKey === 'ownerStatus' || layerKey === 'ownerColor') updateOwnerLayer();
if (layerKey === 'usage') updateUsageLayer();
if (layerKey === 'wea') updateWEALayer();
if (layerKey === 'infrastructure') updateInfrastructureLayer();
};
function updateWEALayer() {
if (!state.wea || !state.wea.features) return;
const statusEl = document.getElementById('status-wea');
if (statusEl) {
statusEl.innerText = `${state.wea.features.length} WEA`;
statusEl.style.color = 'var(--success)';
}
layers.wea.clearLayers();
if (state.visibleLayers.wea) {
layers.wea.addData(state.wea);
}
if (Object.keys(layers.wea._layers).length > 0 && state.visibleLayers.wea) {
map.fitBounds(layers.wea.getBounds(), { padding: [50, 50] });
}
}
function updateInfrastructureLayer() {
if (!state.infrastructure || !state.infrastructure.features) return;
const statusEl = document.getElementById('status-infrastructure');
if (statusEl) {
statusEl.innerText = `${state.infrastructure.features.length} Objekte`;
statusEl.style.color = 'var(--success)';
}
layers.infrastructure.clearLayers();
if (state.visibleLayers.infrastructure) {
layers.infrastructure.addData(state.infrastructure);
}
}
function varStyle(name) {
const val = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
return val || '#94a3b8';
}
function updateUsageLayer() {
if (!state.usage || !state.usage.features) return;
const statusEl = document.getElementById('status-usage');
// Filtering: Expanded list and more robust matching
const requestedKeywords = [
'gehölz', 'wald', 'forst', 'bahn', 'verkehr', 'straße', 'baufläche', 'wohnbau',
'weg', 'gewässer', 'wasser', 'grünland', 'landwirtsch', 'acker'
];
const filteredFeatures = state.usage.features.filter(f => {
const props = f.properties;
const rawType = props.nutzart || props.NUTZART || props.bezeichnung || props.BEZEICHNUN || props.nutzungsart || '';
const type = rawType.toLowerCase();
return requestedKeywords.some(kw => type.includes(kw));
});
console.log(`Nutzung-Layer: ${filteredFeatures.length} von ${state.usage.features.length} Objekten nach Filterung.`);
if (statusEl) {
statusEl.innerText = `${filteredFeatures.length} Objekte`;
statusEl.style.color = filteredFeatures.length > 0 ? 'var(--success)' : 'var(--warning)';
}
layers.usage.clearLayers();
if (state.visibleLayers.usage) {
layers.usage.addData({
type: "FeatureCollection",
features: filteredFeatures
});
if (layers.usage.getBounds().isValid() && filteredFeatures.length > 0) {
console.log("Zooming to Usage layer...");
// map.fitBounds(layers.usage.getBounds()); // Optional: Auto-zoom to usage
}
}
}
function zoomToPlot(props) {
if (!layers.owners) return;
const targetFlur = props.Flur || props.FLUR || props.flur || props.FL || props.fl || '';
const targetFlst = props.Flurstueck || props.Flurstück || props.flurstueck || props.FLST_NR || props.FLST || props.flst || props.NUMMER || props.FlSt || '';
let found = false;
layers.owners.eachLayer(l => {
if (found) return;
const p = l.feature.properties;
const lFlur = p.Flur || p.FLUR || p.flur || p.FL || p.fl || '';
const lFlst = p.Flurstueck || p.Flurstück || p.flurstueck || p.FLST_NR || p.FLST || p.flst || p.NUMMER || p.FlSt || '';
if (lFlur == targetFlur && lFlst == targetFlst) {
map.fitBounds(l.getBounds(), { padding: [100, 100], maxZoom: 18 });
l.openPopup();
found = true;
}
});
}
function renderOwnerList(filter = '') {
const list = document.getElementById('owner-list');
list.innerHTML = '';
// Group by unique owner to avoid duplicates
const uniqueOwners = new Map();
(state.owners.features || []).forEach(f => {
const p = f.properties;
const searchStr = `${p.Vorname || ''} ${p.Nachname || ''}`.toLowerCase();
if (searchStr.includes(filter.toLowerCase())) {
// Unique key: Name + Address
const key = `${p.Nachname}|${p.Vorname}|${p.Str_HNr || p.STR}|${p.Ort || p.ORT}`;
if (!uniqueOwners.has(key)) {
uniqueOwners.set(key, f);
}
}
});
uniqueOwners.forEach((f, key) => {
const currentNote = f.properties.notiz || '';
const isHighlighted = state.highlightedOwners && state.highlightedOwners[key];
const card = document.createElement('div');
card.className = 'owner-card';
if (isHighlighted) {
card.style.borderColor = '#ffee00';
card.style.borderWidth = '2px';
card.style.boxShadow = '0 0 10px rgba(255, 238, 0, 0.4)';
}
card.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div>
<div class="owner-name">${f.properties.Vorname || ''} ${f.properties.Nachname || ''}</div>
<div style="font-size: 12px; color: #64748b;">${f.properties.Str_HNr || f.properties.STR || ''}, ${f.properties.PLZ || ''} ${f.properties.Ort || f.properties.ORT || ''}</div>
</div>
<div style="display: flex; gap: 4px;">
<button class="btn btn-icon ${isHighlighted ? 'active-highlight' : ''}" title="Flächen auf Karte markieren" onclick="event.stopPropagation(); toggleHighlightOwner('${key.replace(/'/g, "\\'")}')" style="padding: 4px; border: none; background: ${isHighlighted ? '#ffee00' : 'rgba(0,0,0,0.05)'}; border-radius: 6px; cursor: pointer;">
<i data-lucide="search" style="width: 16px; height: 16px; color: ${isHighlighted ? '#000' : '#333'};"></i>
</button>
<button class="btn btn-icon" title="Notiz bearbeiten" onclick="event.stopPropagation(); openNoteModal('${key.replace(/'/g, "\\'")}', \`${currentNote.replace(/`/g, '\\`')}\`)" style="padding: 4px; border: none; background: rgba(0,0,0,0.05); border-radius: 6px; cursor: pointer;">
<i data-lucide="file-edit" style="width: 16px; height: 16px; color: #333;"></i>
</button>
</div>
</div>
${currentNote ? `<div style="font-size: 11px; margin-top: 6px; padding: 6px; background: rgba(0,75,80,0.05); border-radius: 4px; border-left: 2px solid var(--corporate-teal); color: #333; font-style: italic;">${currentNote.replace(/\n/g, '<br>')}</div>` : ''}
<select class="status-select" data-id="${f.id}" style="margin-top: 8px;">
${STATUS_ORDER.map(s => {
const current = getStatusKey(f.properties.status || 'Unbekannt');
return `<option value="${s}" ${current === s ? 'selected' : ''}>${s}</option>`;
}).join('')}
</select>
`;
card.addEventListener('click', (e) => {
if (e.target.classList.contains('status-select')) return;
zoomToPlot(f.properties);
});
list.appendChild(card);
});
lucide.createIcons({ root: list });
document.querySelectorAll('.status-select').forEach(sel => {
sel.addEventListener('change', (e) => {
const id = e.target.dataset.id;
const status = e.target.value;
const feature = state.owners.features.find(feat => feat.id == id);
if (feature) {
const name = feature.properties.Nachname;
const street = feature.properties.Str_HNr || feature.properties.STR;
state.owners.features.forEach(f => {
if (f.properties.Nachname === name && (f.properties.Str_HNr === street || f.properties.STR === street)) {
f.properties.status = status;
saveOwner(f);
}
});
updateOwnerLayer();
}
});
});
}
function renderLegend() {
const container = document.getElementById('legend-items');
if (!container) return;
container.innerHTML = '';
STATUS_ORDER.forEach(label => {
const color = STATUS_CONFIG[label];
const item = document.createElement('div');
item.style.display = 'flex';
item.style.alignItems = 'flex-start';
item.style.gap = '10px';
item.style.fontSize = '12px';
item.style.color = 'white';
const desc = STATUS_DESCRIPTIONS[label] || "";
const isUnknown = color === 'transparent';
const dotColor = isUnknown ? 'transparent' : color;
const dotBorder = isUnknown ? '1px dashed rgba(255,255,255,0.5)' : '1px solid rgba(255,255,255,0.2)';
item.innerHTML = `
<div style="width: 14px; height: 14px; border-radius: 50%; background: ${dotColor}; border: ${dotBorder}; margin-top: 2px; flex-shrink: 0;"></div>
<div style="display: flex; flex-direction: column; gap: 2px;">
<span style="font-weight: 600;">${label}</span>
<span style="font-size: 10px; opacity: 0.7; line-height: 1.2;">${desc}</span>
</div>
`;
container.appendChild(item);
});
// Sync legend visibility with initial state
const isVisible = document.getElementById('sidebar-toggle-owner-status')?.checked;
document.getElementById('status-legend').style.display = isVisible ? 'block' : 'none';
}
document.getElementById('owner-search').addEventListener('input', (e) => {
renderOwnerList(e.target.value);
});
// --- Route Drawing Logic ---
const routeLayers = {};
const drillingLayers = {};
const labelLayers = {};
function updateRouteLayers() {
state.variants.forEach(v => {
// Ensure layer exists
if (!routeLayers[v.id]) {
routeLayers[v.id] = L.polyline(v.routes, {
color: '#cca300',
weight: 6,
opacity: 1,
editable: true,
pane: 'trassenPane',
lineOptions: { color: '#cca300', weight: 6 },
vertexOptions: { color: '#cca300', radius: 5 },
middleMarkerOptions: { color: '#cca300', opacity: 0.5, radius: 3 }
});
drillingLayers[v.id] = L.featureGroup({ interactive: false, pane: 'drillingPane' });
labelLayers[v.id] = L.featureGroup({ interactive: false, pane: 'labelPane' });
if (v.visible) {
routeLayers[v.id].addTo(map);
drillingLayers[v.id].addTo(map);
labelLayers[v.id].addTo(map);
}
// Allow vertex insertion by clicking the polyline itself
routeLayers[v.id].on('click', handleVertexInsertion);
}
const layer = routeLayers[v.id];
const labelLayer = labelLayers[v.id];
// Sync visibility
if (v.visible) {
if (!map.hasLayer(layer)) {
layer.addTo(map);
drillingLayers[v.id].addTo(map);
labelLayers[v.id].addTo(map);
}
// Z-Index: Active variant to front
if (v.active) {
layer.bringToFront();
if (drillingLayers[v.id]) drillingLayers[v.id].bringToFront();
if (labelLayers[v.id]) labelLayers[v.id].bringToFront();
}
// Sync geometry if not actively being dragged/edited
if (!v.active) {
try {
layer.setLatLngs(v.routes);
} catch (e) {
console.warn("Geometrie-Sync Warnung:", e);
}
}
// Visuelle Unterscheidung: Aktive vs. Inaktive Varianten
if (v.active) {
layer.setStyle({ opacity: 1, weight: 6, color: '#cca300' });
} else {
layer.setStyle({ opacity: 0.3, weight: 4, color: '#cca300' });
}
// Labels aktualisieren (Name in der Mitte der Linie auf weißem Hintergrund)
labelLayer.clearLayers();
if (v.routes && v.routes.length > 1) {
// Find geometric middle of the polyline
const points = layer.getLatLngs();
let totalDist = 0;
const distances = [];
for (let i = 0; i < points.length - 1; i++) {
const d = map.distance(points[i], points[i+1]);
distances.push(d);
totalDist += d;
}
let currentDist = 0;
let midPos = points[0];
for (let i = 0; i < distances.length; i++) {
if (currentDist + distances[i] >= totalDist / 2) {
// Midpoint is on this segment
const ratio = (totalDist / 2 - currentDist) / distances[i];
const lat = points[i].lat + (points[i+1].lat - points[i].lat) * ratio;
const lng = points[i].lng + (points[i+1].lng - points[i].lng) * ratio;
midPos = L.latLng(lat, lng);
break;
}
currentDist += distances[i];
}
L.marker(midPos, {
pane: 'labelPane',
icon: L.divIcon({
className: 'variant-map-label',
html: `<div style="background: white; border: 1px solid #cca300; color: #444; padding: 2px 8px; border-radius: 4px; font-weight: bold; font-size: 11px; box-shadow: 0 1px 4px rgba(0,0,0,0.2); white-space: nowrap; opacity: ${v.active ? 1 : 0.4}; transition: opacity 0.3s;">${v.name}</div>`,
iconSize: [0, 0],
iconAnchor: [0, 0]
}),
interactive: false
}).addTo(labelLayer);
}
// Sync edit state
if (layer.enableEdit) {
if (v.active) {
layer.enableEdit();
} else {
layer.disableEdit();
}
}
calculateStats(v);
} else {
if (map.hasLayer(layer)) map.removeLayer(layer);
if (map.hasLayer(drillingLayers[v.id])) map.removeLayer(drillingLayers[v.id]);
if (map.hasLayer(labelLayers[v.id])) map.removeLayer(labelLayers[v.id]);
}
});
}
document.getElementById('btn-draw').addEventListener('click', () => {
state.isDrawing = !state.isDrawing;
state.isMeasuring = false;
if (state.isDrawing) {
const activeV = state.variants.find(v => v.active);
if (activeV && routeLayers[activeV.id]) {
// Start/continue editing
if (!map.editTools) {
alert("Das Zeichen-Werkzeug konnte nicht geladen werden. Bitte prüfen Sie Ihre Internetverbindung oder nutzen Sie einen anderen Browser (Chrome/Edge).");
state.isDrawing = false;
updateToolCursors();
return;
}
if (activeV.routes.length === 0) {
map.editTools.startPolyline();
} else if (routeLayers[activeV.id].editor) {
routeLayers[activeV.id].editor.continueForward();
} else {
routeLayers[activeV.id].enableEdit().continueForward();
}
}
} else {
if (map.editTools) map.editTools.stopDrawing();
}
updateToolCursors();
updateRouteLayers();
renderVariants();
});
function updateToolCursors() {
const container = map.getContainer();
if (state.isDrawing) container.style.cursor = 'crosshair';
else if (state.isMeasuring) container.style.cursor = 'help';
else container.style.cursor = '';
document.getElementById('btn-draw').className = state.isDrawing ? 'btn btn-primary' : 'btn btn-outline';
document.getElementById('btn-measure').className = state.isMeasuring ? 'btn btn-primary' : 'btn btn-outline';
}
map.on('click', (e) => {
if (state.isMeasuring) {
addMeasurementPoint(e.latlng);
}
});
// Capture changes (drawing, dragging, inserting, deleting)
map.on('editable:drawing:clicked editable:drawing:move editable:drawing:end editable:created editable:vertex:drag editable:vertex:dragend editable:vertex:deleted editable:vertex:inserted', (e) => {
const activeV = state.variants.find(v => v.active);
if (!activeV) return;
// Enforce color on everything newly created or edited
if (e.layer && e.layer.setStyle) {
e.layer.setStyle({ color: '#cca300', weight: 6 });
}
// If a new layer was created (via startPolyline or startNewPath), merge it into our existing layer
if (e.type === 'editable:created' && activeV && routeLayers[activeV.id] && e.layer !== routeLayers[activeV.id]) {
const current = getNestedCoords(routeLayers[activeV.id].getLatLngs());
const addition = e.layer.getLatLngs();
const merged = [...current, addition];
routeLayers[activeV.id].setLatLngs(merged);
activeV.routes = merged;
map.removeLayer(e.layer); // Clean up temp layer
routeLayers[activeV.id].enableEdit();
} else if (routeLayers[activeV.id] && e.layer === routeLayers[activeV.id]) {
activeV.routes = e.layer.getLatLngs();
}
// Live updates for labels, drillings and stats
calculateStats(activeV);
updateVariantStatsUI(activeV);
if (e.type.includes('end') || e.type === 'editable:created') {
renderVariants();
const _activeV = state.variants.find(v => v.active); if (_activeV) saveVariantToDB(_activeV);
}
});
function getFlattenedCoords(raw) {
if (!raw) return [];
// If it's a flat array of LatLngs
if (raw.length > 0 && (raw[0].lat !== undefined || (Array.isArray(raw[0]) && typeof raw[0][0] === 'number'))) {
return raw.map(ll => L.latLng(ll));
}
// If it's nested
const flattened = [];
raw.forEach(section => {
if (Array.isArray(section)) {
section.forEach(ll => flattened.push(L.latLng(ll)));
}
});
return flattened;
}
function getNestedCoords(raw) {
if (!raw) return [];
// Check if it's already nested
if (raw.length > 0 && Array.isArray(raw[0]) && raw[0].length > 0 && (raw[0][0].lat !== undefined || Array.isArray(raw[0][0]))) {
return raw.map(section => section.map(ll => L.latLng(ll)));
}
// If it's flat, wrap it
if (raw.length > 0) return [raw.map(ll => L.latLng(ll))];
return [];
}
function renderSegmentLabels(variant, coordsInput = null, tempPt = null) {
const vLabel = labelLayers[variant.id];
if (!vLabel) return;
if (!variant.visible) { vLabel.clearLayers(); return; }
const nested = coordsInput ? [coordsInput] : getNestedCoords(variant.routes);
const currentMarkers = vLabel.getLayers();
let markerIdx = 0;
nested.forEach((latLngs, sectionIdx) => {
const drawLatLngs = [...latLngs];
// Only add temp point to the very LAST section if we are actively drawing
if (tempPt && sectionIdx === nested.length - 1) drawLatLngs.push(L.latLng(tempPt));
if (drawLatLngs.length < 2) return;
for (let i = 0; i < drawLatLngs.length - 1; i++) {
const p1 = drawLatLngs[i];
const p2 = drawLatLngs[i + 1];
const dist = map.distance(p1, p2);
const mid = L.latLng((p1.lat + p2.lat) / 2, (p1.lng + p2.lng) / 2);
const labelText = `${dist.toFixed(0)}m`;
if (markerIdx < currentMarkers.length) {
const m = currentMarkers[markerIdx];
m.setLatLng(mid);
const el = m.getElement();
if (el) {
const span = el.querySelector('.segment-label-content');
if (span) span.textContent = labelText;
}
} else {
L.marker(mid, {
interactive: false,
pane: 'labelPane',
icon: L.divIcon({
className: 'segment-label',
html: `<span class="segment-label-content">${labelText}</span>`,
iconSize: [46, 20],
iconAnchor: [23, 10]
})
}).addTo(vLabel);
}
markerIdx++;
}
});
while (markerIdx < currentMarkers.length) {
vLabel.removeLayer(currentMarkers[markerIdx++]);
}
}
// Cache for obstacles to avoid repeated filtering/lowercase during drag
let cachedObstacles = null;
function calculateStats(variant) {
if (!variant) return;
const vDrill = drillingLayers[variant.id];
if (vDrill) vDrill.clearLayers();
const nested = getNestedCoords(variant.routes);
variant.stats.total = 0;
variant.stats.drilling = 0;
variant.stats.muffen = 0;
variant.stats.hasTooLongDrilling = false;
variant.drillingSegments = [];
if (nested.length === 0) return;
renderSegmentLabels(variant);
// Pre-filter obstacles once
if (!cachedObstacles && state.usage.features) {
const keywords = ['bahn', 'gewässer', 'wasser', 'straße', 'verkehr', 'gehölz', 'baufläche', 'wald', 'forst', 'hecke', 'weg', 'pfad', 'graben', 'bach', 'fluss'];
cachedObstacles = state.usage.features.filter(f => {
const type = (f.properties.nutzart || f.properties.NUTZART || f.properties.Nutzart || '').toLowerCase();
return keywords.some(k => type.includes(k));
});
}
const currentObstacles = cachedObstacles || [];
nested.forEach(latLngs => {
if (latLngs.length < 2) return;
const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
const sectionLength = turf.length(line, { units: 'meters' });
variant.stats.total += sectionLength;
const startPoint = turf.point([latLngs[0].lng, latLngs[0].lat]);
const drillingRanges = [];
if (currentObstacles.length > 0) {
const lineBbox = turf.bbox(line);
currentObstacles.forEach(obs => {
try {
const obsBbox = turf.bbox(obs);
if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] || lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return;
if (turf.booleanIntersects(line, obs)) {
const intersect = turf.lineIntersect(line, obs);
let distances = [0, sectionLength];
intersect.features.forEach(f => {
distances.push(turf.length(turf.lineSlice(startPoint, f, line), { units: 'meters' }));
});
distances = [...new Set(distances)].sort((a, b) => a - b);
for (let i = 0; i < distances.length - 1; i++) {
const dStart = distances[i];
const dEnd = distances[i + 1];
const midPt = turf.along(line, (dStart + dEnd) / 2 / 1000, { units: 'kilometers' });
if (turf.booleanPointInPolygon(midPt, obs)) {
drillingRanges.push([Math.max(0, dStart - 20), Math.min(sectionLength, dEnd + 20)]);
}
}
}
} catch (e) {}
});
}
let mergedRanges = [];
if (drillingRanges.length > 0) {
drillingRanges.sort((a, b) => a[0] - b[0]);
let cur = drillingRanges[0];
for (let i = 1; i < drillingRanges.length; i++) {
if (drillingRanges[i][0] <= cur[1]) cur[1] = Math.max(cur[1], drillingRanges[i][1]);
else { mergedRanges.push(cur); cur = drillingRanges[i]; }
}
mergedRanges.push(cur);
}
variant.stats.drilling += mergedRanges.reduce((sum, r) => sum + (r[1] - r[0]), 0);
variant.stats.muffen += mergedRanges.length * 2;
mergedRanges.forEach(range => {
const lengthM = range[1] - range[0];
if (lengthM > 180) variant.stats.hasTooLongDrilling = true;
const s = turf.along(line, range[0] / 1000, { units: 'kilometers' });
const e = turf.along(line, range[1] / 1000, { units: 'kilometers' });
variant.drillingSegments.push({
path: [[s.geometry.coordinates[1], s.geometry.coordinates[0]], [e.geometry.coordinates[1], e.geometry.coordinates[0]]],
length: lengthM,
muffen: [[s.geometry.coordinates[1], s.geometry.coordinates[0]], [e.geometry.coordinates[1], e.geometry.coordinates[0]]]
});
});
});
variant.stats.open = Math.max(0, variant.stats.total - variant.stats.drilling);
if (vDrill) {
const drillingOpacity = variant.active ? 1 : 0.3;
variant.drillingSegments.forEach(seg => {
L.polyline(seg.path, {
color: '#000000',
weight: 8,
opacity: drillingOpacity,
interactive: false,
pane: 'drillingPane'
}).addTo(vDrill);
// Individual drilling label (number only)
const midLat = (seg.path[0][0] + seg.path[1][0]) / 2;
const midLng = (seg.path[0][1] + seg.path[1][1]) / 2;
L.marker([midLat, midLng], {
interactive: false,
pane: 'labelPane',
icon: L.divIcon({
className: 'drilling-segment-label',
html: `<div style="background: #000000; color: white; padding: 1px 6px; border-radius: 4px; font-size: 10px; font-weight: bold; white-space: nowrap; box-shadow: 0 2px 4px rgba(0,0,0,0.2); opacity: ${drillingOpacity};">${seg.length.toFixed(0)}m</div>`,
iconSize: [50, 20],
iconAnchor: [25, 25]
})
}).addTo(vDrill);
// Add Muffen markers
seg.muffen.forEach(mpos => {
L.circleMarker(mpos, {
radius: 5,
color: '#000000',
weight: 2,
fillOpacity: drillingOpacity,
opacity: drillingOpacity,
fillColor: '#ffffff',
pane: 'drillingPane',
interactive: false
}).addTo(vDrill);
});
});
// ONLY update the sidebar UI/list if this is the active variant
if (variant.active) {
updateRequiredPlots(variant);
}
}
}
function updateRequiredPlots(variant) {
const container = document.getElementById('required-plots-container');
if (!container) return;
container.innerHTML = '<h4 style="font-size: 13px; margin-bottom: 10px; color: #64748b; display: flex; align-items: center; gap: 8px;"><i data-lucide="layers" style="width: 14px;"></i> Benötigte Flurstücke</h4>';
const latLngs = getFlattenedCoords(variant ? variant.routes : []);
if (!variant || latLngs.length < 2 || !state.owners || !state.owners.features || state.owners.features.length === 0) {
container.innerHTML += '<p style="font-size: 11px; color: #94a3b8; text-align: center; margin-top: 20px;">Keine Daten oder Trasse vorhanden</p>';
return;
}
try {
const nested = getNestedCoords(variant.routes);
if (nested.length === 0) return;
const lines = nested.filter(s => s.length >= 2).map(s => turf.lineString(s.map(ll => [ll.lng, ll.lat])));
if (lines.length === 0) return;
const multiLine = lines.length === 1 ? lines[0] : turf.multiLineString(lines.map(l => l.geometry.coordinates));
const lineBbox = turf.bbox(multiLine);
const intersectingPlots = state.owners.features.filter(f => {
try {
const obsBbox = turf.bbox(f);
if (lineBbox[0] > obsBbox[2] || lineBbox[2] < obsBbox[0] ||
lineBbox[1] > obsBbox[3] || lineBbox[3] < obsBbox[1]) return false;
return turf.booleanIntersects(multiLine, f);
} catch (e) { return false; }
});
if (intersectingPlots.length === 0) {
container.innerHTML += '<p style="font-size: 11px; color: #94a3b8; text-align: center; margin-top: 20px;">Keine Überschneidungen gefunden</p>';
} else {
// Group by Flur/Flurstück to avoid duplicates if owners are split across features
const seen = new Set();
intersectingPlots.forEach(f => {
const p = f.properties;
const flur = p.Flur || p.FLUR || p.flur || p.FL || p.fl || '';
const flst = p.Flurstueck || p.Flurstück || p.flurstueck || p.FLST_NR || p.FLST || p.flst || p.NUMMER || p.FlSt || '';
const gem = p.Gemarkung || p.GEMARKUNG || '';
const ownerKey = `${p.Vorname}-${p.Nachname}`;
const key = `${gem}-${flur}-${flst}-${ownerKey}`;
if (seen.has(key)) return;
seen.add(key);
const statusColor = getStatusColor(p.status || 'Unbekannt');
const name = `${p.Vorname || ''} ${p.Nachname || ''}`.trim() || 'Unbekannter Eigentümer';
const currentNote = p.notiz || '';
const noteOwnerKey = `${p.Nachname}|${p.Vorname}|${p.Str_HNr || p.STR}|${p.Ort || p.ORT}`;
const card = document.createElement('div');
card.className = 'plot-card';
card.style.cursor = 'pointer';
card.innerHTML = `
<div class="status-dot" style="background-color: ${statusColor};" title="${p.status || 'Unbekannt'}"></div>
<div class="plot-info">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div class="plot-owner">${name}</div>
<button class="btn btn-icon" title="Notiz bearbeiten" onclick="event.stopPropagation(); openNoteModal('${noteOwnerKey.replace(/'/g, "\\'")}', \`${currentNote.replace(/`/g, '\\`')}\`)" style="padding: 2px; border: none; background: rgba(0,0,0,0.05); border-radius: 4px; cursor: pointer;">
<i data-lucide="file-edit" style="width: 14px; height: 14px; color: #333;"></i>
</button>
</div>
<div class="plot-details">${gem ? gem + ' | ' : ''}Fl. ${flur}, FlSt ${flst}</div>
${currentNote ? `<div style="font-size: 10px; margin-top: 4px; color: #64748b; font-style: italic; background: rgba(0,0,0,0.02); padding: 4px; border-radius: 4px; border-left: 2px solid var(--corporate-teal);"><i data-lucide="info" style="width: 10px; height: 10px; display: inline-block; margin-right: 2px; vertical-align: middle;"></i>${currentNote}</div>` : ''}
</div>
`;
card.addEventListener('click', () => zoomToPlot(p));
container.appendChild(card);
});
}
} catch (err) {
console.error("Error updating required plots:", err);
}
lucide.createIcons({ root: container });
}
// --- Variant Management UI ---
window.startNewPath = (id) => {
if (map.editTools) {
state.isDrawing = true;
state.isMeasuring = false;
updateToolCursors();
map.editTools.startPolyline();
}
};
function renderVariants() {
const container = document.getElementById('variant-controls');
if (!container) return;
// Critical Safeguard: Ensure variants is always an array
if (!Array.isArray(state.variants)) state.variants = [];
container.innerHTML = '';
if (state.variants.length > 0 && !state.variants.find(v => v.active)) {
state.variants[0].active = true;
}
// Render Tabs
const tabContainer = document.createElement('div');
tabContainer.style.display = 'flex';
tabContainer.style.gap = '6px';
tabContainer.style.marginBottom = '12px';
tabContainer.style.overflowX = 'auto';
tabContainer.style.paddingBottom = '4px';
state.variants.forEach(v => {
const btn = document.createElement('button');
const vName = v.name || "Variante";
btn.innerText = vName.replace('Variante ', ''); // Kurzer Name 'A', 'B' usw.
btn.title = v.name;
btn.style.padding = '6px 14px';
btn.style.border = 'none';
btn.style.borderRadius = '6px';
btn.style.cursor = 'pointer';
btn.style.fontWeight = 'bold';
btn.style.flexShrink = '0';
if (v.active) {
btn.style.background = 'var(--primary)';
btn.style.color = 'white';
btn.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
} else {
btn.style.background = '#e2e8f0';
btn.style.color = '#475569';
}
btn.onclick = () => setActiveVariant(v.id);
tabContainer.appendChild(btn);
});
container.appendChild(tabContainer);
// Render Active Variant Content
const v = state.variants.find(v => v.active);
if (v) {
const div = document.createElement('div');
div.id = `variant-card-${v.id}`;
div.style.padding = '12px';
div.style.background = 'white';
div.style.borderRadius = '10px';
div.style.border = '1px solid #e2e8f0';
div.innerHTML = `
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; border-bottom: 1px solid #e2e8f0; padding-bottom: 8px;">
<span style="font-weight: bold; color: #cca300; font-size: 15px; cursor: pointer;" onclick="renameVariant(${v.id})" title="Klicken zum Umbenennen">
${v.name || 'Variante'}
</span>
<div style="display: flex; gap: 10px; align-items: center;">
<label style="display: flex; align-items: center; gap: 2px; font-size: 12px; cursor: pointer;" title="Auf Karte ein-/ausblenden">
<input type="checkbox" ${v.visible ? 'checked' : ''} onclick="toggleVariantVisibility(${v.id}, this.checked)">
<i data-lucide="eye" style="width: 14px;"></i>
</label>
<i data-lucide="plus-circle" class="action-icon" title="Weiteren Abschnitt hinzufügen" style="cursor: pointer; width: 15px; color: #10b981;" onclick="startNewPath(${v.id})"></i>
<i data-lucide="table" class="action-icon" title="Tabelle herunterladen" style="cursor: pointer; width: 15px; color: var(--corporate-teal);" onclick="downloadVariantTable(${v.id})"></i>
<i data-lucide="copy" class="action-icon" title="Duplizieren" style="cursor: pointer; width: 15px; color: #64748b;" onclick="duplicateVariant(${v.id})"></i>
<i data-lucide="trash-2" class="action-icon" title="Inhalt löschen" style="cursor: pointer; width: 15px; color: var(--danger);" onclick="clearVariant(${v.id})"></i>
</div>
</div>
<div class="stat-card" style="padding: 0; margin: 0; background: transparent;">
<div class="stat-row"><span>Gesamtlänge:</span> <span class="stat-total-val" style="font-weight: 600;">${(v.stats?.total || 0).toFixed(0)} m</span></div>
<div class="stat-row"><span>Offenbauweise:</span> <span class="stat-open-val" style="font-weight: 600;">${(v.stats?.open || 0).toFixed(0)} m</span></div>
<div class="stat-row drilling-stat"><span>Bohrungsanteil:</span> <span class="stat-drilling-val">${(v.stats?.drilling || 0).toFixed(0)} m</span></div>
<div class="stat-row muffen-stat" style="color: #8b5cf6; font-weight: 600;"><span>Anzahl Muffen:</span> <span class="stat-muffen-val">${v.stats?.muffen || 0}</span></div>
</div>
${v.stats?.hasTooLongDrilling ?
'<div style="background: rgba(239,68,68,0.1); border: 1px solid var(--danger); color: var(--danger); padding: 8px; border-radius: 8px; font-size: 11px; margin-top: 10px; display: flex; align-items: center; gap: 6px;">' +
'<i data-lucide="alert-triangle" style="width: 14px;"></i>' +
'<span>Achtung: Bohrung > 180m</span>' +
'</div>' : ''}
`;
container.appendChild(div);
}
lucide.createIcons();
}
function updateVariantStatsUI(variant) {
const card = document.getElementById(`variant-card-${variant.id}`);
if (card) {
const totalSpan = card.querySelector('.stat-total-val');
const openSpan = card.querySelector('.stat-open-val');
const drillingSpan = card.querySelector('.stat-drilling-val');
const muffenSpan = card.querySelector('.stat-muffen-val');
if (totalSpan) totalSpan.textContent = `${variant.stats.total.toFixed(0)} m`;
if (openSpan) openSpan.textContent = `${(variant.stats.open || 0).toFixed(0)} m`;
if (drillingSpan) drillingSpan.textContent = `${variant.stats.drilling.toFixed(0)} m`;
if (muffenSpan) muffenSpan.textContent = variant.stats.muffen || 0;
}
}
window.toggleVariantVisibility = (id, visible) => {
const v = state.variants.find(varnt => varnt.id === id);
if (v) {
v.visible = visible;
updateRouteLayers();
}
};
window.setActiveVariant = (id) => {
state.variants.forEach(v => v.active = (v.id === id));
renderVariants();
updateToolCursors();
updateRouteLayers();
const activeV = state.variants.find(v => v.id === id);
if (activeV) updateRequiredPlots(activeV);
};
window.renameVariant = (id) => {
const v = state.variants.find(v => v.id === id);
if (!v) return;
const newName = prompt("Neuer Name für die Variante:", v.name);
if (newName && newName.trim()) {
v.name = newName.trim();
renderVariants();
if (activeV) saveVariantToDB(activeV);
}
};
window.duplicateVariant = (id) => {
const original = state.variants.find(v => v.id === id);
if (!original) return;
const copy = JSON.parse(JSON.stringify(original));
copy.id = Date.now();
copy.name = (copy.name || "Variante") + " (Kopie)";
copy.active = true;
copy.visible = true;
state.variants.forEach(v => v.active = false);
state.variants.push(copy);
renderVariants();
updateRouteLayers();
const activeV = state.variants.find(v => v.active);
if (activeV) updateRequiredPlots(activeV);
if (activeV) saveVariantToDB(activeV);
};
window.clearVariant = (id) => {
const v = state.variants.find(v => v.id === id);
if (!v) return;
if (!confirm(`Möchten Sie alle gezeichneten Linien in '${v.name}' wirklich löschen?`)) return;
// Reset geometry and stats
v.routes = [];
v.stats = { total: 0, drilling: 0, open: 0, muffen: 0 };
if (routeLayers[id]) {
routeLayers[id].setLatLngs([]);
if (routeLayers[id].editor) routeLayers[id].editor.reset();
}
if (drillingLayers[id]) drillingLayers[id].clearLayers();
if (labelLayers[id]) labelLayers[id].clearLayers();
renderVariants();
updateRouteLayers();
if (v.active) updateRequiredPlots(v);
const _activeV = state.variants.find(v => v.active); if (_activeV) saveVariantToDB(_activeV);
};
// Note: deleteVariant is now effectively disabled to keep the slots fixed
window.deleteVariant = (id) => {
console.log("Delete disabled. Use clearVariant instead.");
window.clearVariant(id);
};
// Initial rendering deferred to end of script or window.load
function initApp() {
// Ensure we have at least our default variants if something went wrong
if (!state.variants || state.variants.length === 0) {
state.variants = [
{ id: 1, name: "Variante A", color: "#cca300", routes: [], active: true, visible: true, stats: { total: 0, drilling: 0, open: 0, muffen: 0 } },
{ id: 2, name: "Variante B", color: "#cca300", routes: [], active: false, visible: true, stats: { total: 0, drilling: 0, open: 0, muffen: 0 } },
{ id: 3, name: "Variante C", color: "#cca300", routes: [], active: false, visible: true, stats: { total: 0, drilling: 0, open: 0, muffen: 0 } },
{ id: 4, name: "Variante D", color: "#cca300", routes: [], active: false, visible: true, stats: { total: 0, drilling: 0, open: 0, muffen: 0 } },
{ id: 5, name: "Variante E", color: "#cca300", routes: [], active: false, visible: true, stats: { total: 0, drilling: 0, open: 0, muffen: 0 } },
{ id: 6, name: "Variante F", color: "#cca300", routes: [], active: false, visible: true, stats: { total: 0, drilling: 0, open: 0, muffen: 0 } },
{ id: 7, name: "Variante G", color: "#cca300", routes: [], active: false, visible: true, stats: { total: 0, drilling: 0, open: 0, muffen: 0 } },
{ id: 8, name: "Variante H", color: "#cca300", routes: [], active: false, visible: true, stats: { total: 0, drilling: 0, open: 0, muffen: 0 } }
];
}
// Force update colors to the new standard on startup
state.variants.forEach(v => v.color = '#cca300');
// Ensure at least one is active
if (!state.variants.find(v => v.active)) {
state.variants[0].active = true;
}
renderVariants();
updateRouteLayers();
updateOwnerLayer();
updateUsageLayer();
updateWEALayer();
const activeV = state.variants.find(v => v.active);
if (activeV) updateRequiredPlots(activeV);
// Sync Legend visibility with checkbox state
const toggleLegend = document.getElementById('sidebar-toggle-owner-status');
const legendContainer = document.getElementById('status-legend');
if (toggleLegend && legendContainer) {
legendContainer.style.display = toggleLegend.checked ? 'block' : 'none';
}
}
// --- Ad-hoc Measurement Tool ---
let measureLayer = L.featureGroup().addTo(map);
let activeMeasure = null;
let tempLine = null;
document.getElementById('btn-measure').addEventListener('click', () => {
state.isMeasuring = !state.isMeasuring;
state.isDrawing = false;
updateToolCursors();
if (!state.isMeasuring && tempLine) {
map.removeLayer(tempLine);
tempLine = null;
}
});
function addMeasurementPoint(latlng) {
if (!activeMeasure) {
activeMeasure = L.polyline([latlng], { color: '#333', weight: 3 }).addTo(measureLayer);
tempLine = L.polyline([latlng, latlng], { color: '#999', weight: 2, dashArray: '5, 5' }).addTo(map);
map.on('mousemove', onMeasureMove);
} else {
activeMeasure.addLatLng(latlng);
tempLine.setLatLngs([latlng, latlng]);
}
}
function onMeasureMove(e) {
if (activeMeasure && tempLine) {
const pts = activeMeasure.getLatLngs();
const last = pts[pts.length - 1];
tempLine.setLatLngs([last, e.latlng]);
// Live distance tooltip
const dist = map.distance(last, e.latlng);
tempLine.bindTooltip(`${dist.toFixed(1)} m`, { sticky: true, offset: [15, 0] }).openTooltip();
}
}
map.on('contextmenu', () => {
if (state.isMeasuring && activeMeasure) {
const pts = activeMeasure.getLatLngs();
const totalDist = calculatePolylineDistance(pts);
// Add marker with distance and "X" to delete
const lastPt = pts[pts.length - 1];
const marker = L.marker(lastPt, {
icon: L.divIcon({
className: 'measure-label',
html: `<div style="background: white; padding: 2px 8px; border-radius: 10px; border: 1px solid #333; white-space: nowrap;">
${totalDist.toFixed(1)} m <span style="color:red; cursor:pointer;" onclick="clearMeasure(this)">×</span>
</div>`,
iconSize: [100, 20]
})
}).addTo(measureLayer);
// Link marker to polyline for deletion
marker.poly = activeMeasure;
activeMeasure = null;
if (tempLine) tempLine.remove();
map.off('mousemove', onMeasureMove);
}
});
function calculatePolylineDistance(pts) {
let d = 0;
for (let i = 0; i < pts.length - 1; i++) {
d += map.distance(pts[i], pts[i + 1]);
}
return d;
}
window.clearMeasure = (el) => {
measureLayer.clearLayers();
};
// Delete vertex on double-click
map.on('editable:vertex:dblclick', (e) => {
e.vertex.delete();
});
// Global Map Click for generous vertex insertion ("Click anywhere near the line")
const handleVertexInsertion = (e) => {
if (state.isMeasuring) return;
// VERY IMPORTANT: Don't interfere if we are actively drawing a path
if (map.editTools && map.editTools.drawing()) return;
const variant = state.variants.find(v => v.active);
if (!variant) return;
const layer = routeLayers[variant.id];
if (!layer) return;
// If the variant is empty, we don't return here anymore,
// but we only attempt insertion if we have at least one segment (2 points).
const latlngs = getFlattenedCoords(layer.getLatLngs());
// If drawing and clicking far away, we let startPolyline/continueForward handle it
if (latlngs.length < 2) return;
if (!layer.editor) {
layer.enableEdit();
}
let minIdx = -1;
let minDist = Infinity;
const pt = map.latLngToLayerPoint(e.latlng);
for (let i = 0; i < latlngs.length - 1; i++) {
const p1 = map.latLngToLayerPoint(latlngs[i]);
const p2 = map.latLngToLayerPoint(latlngs[i + 1]);
const dist = L.LineUtil.pointToSegmentDistance(pt, p1, p2);
if (dist < minDist) {
minDist = dist;
minIdx = i;
}
}
// CRITICAL FIX: Check if we are clicking directly on or very near an existing vertex
// If we are closer than 10 pixels to any vertex, we assume the user wants
// to move the point, not insert a new one.
const isNearVertex = latlngs.some(ll => map.latLngToLayerPoint(ll).distanceTo(pt) < 12);
if (isNearVertex) return;
// High tolerance hit-testing: 35 pixels (approx. the width of a finger/mouse inaccuracy)
const tolerance = state.isDrawing ? 15 : 35;
if (minIdx !== -1 && minDist < tolerance) {
latlngs.splice(minIdx + 1, 0, e.latlng);
layer.setLatLngs(latlngs);
if (layer.editor && layer.editor.reset) {
layer.editor.reset();
}
// Force synchronization
variant.routes = getFlattenedCoords(layer.getLatLngs());
calculateStats(variant);
updateVariantStatsUI(variant);
renderVariants();
const _activeV = state.variants.find(v => v.active); if (_activeV) saveVariantToDB(_activeV);
}
};
map.on('click', handleVertexInsertion);
layers.owners.on('click', handleVertexInsertion);
layers.usage.on('click', handleVertexInsertion);
// --- Local Folder Persistence (File System Access API) ---
const DB_NAME = "TrassenPlanerSync";
const STORE_NAME = "directoryHandles";
async function openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, 1);
request.onupgradeneeded = () => request.result.createObjectStore(STORE_NAME);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function saveHandle(handle) {
const db = await openDB();
const tx = db.transaction(STORE_NAME, "readwrite");
tx.objectStore(STORE_NAME).put(handle, "projectRoot");
return new Promise(r => tx.oncomplete = r);
}
async function getHandle() {
const db = await openDB();
return new Promise((resolve) => {
const request = db.transaction(STORE_NAME).objectStore(STORE_NAME).get("projectRoot");
request.onsuccess = () => resolve(request.result);
request.onerror = () => resolve(null);
});
}
document.getElementById('btn-sync-folder').addEventListener('click', async () => {
try {
if (!window.showDirectoryPicker) {
alert("Ihr Browser unterstützt keinen direkten Ordner-Zugriff. Bitte nutzen Sie Chrome oder Edge.");
return;
}
if (state.directoryHandle) {
// Check if already connected or just needs permission
const status = await state.directoryHandle.queryPermission({ mode: 'readwrite' });
if (status === 'granted') {
// Already works, so assume the user wants to SWITCH folders if they click again
if (!confirm("Ordner ist bereits synchronisiert. Möchten Sie einen ANDEREN Ordner wählen?")) {
return;
}
} else {
// Just needs permission restore
const newStatus = await state.directoryHandle.requestPermission({ mode: 'readwrite' });
if (newStatus === 'granted') {
updateSyncUI('connected');
await loadFromDatabase();
return;
}
}
}
// New picker (either first time or switching)
state.directoryHandle = await window.showDirectoryPicker();
await saveHandle(state.directoryHandle);
updateSyncUI('connected');
await loadFromDatabase();
} catch (err) {
console.log("Folder selection cancelled/failed", err);
}
});
function updateSyncUI(status) {
const btn = document.getElementById('btn-sync-folder');
if (status === 'connected') {
btn.innerHTML = '<i data-lucide="refresh-cw" style="width: 16px;"></i> Synchronisiert';
btn.style.color = 'var(--success)';
} else if (status === 'restorable') {
btn.innerHTML = '<i data-lucide="lock" style="width: 16px;"></i> Zugriff erlauben';
btn.style.color = 'var(--primary)';
} else {
btn.innerHTML = '<i data-lucide="link" style="width: 16px;"></i> Ordner synchronisieren';
btn.style.color = '';
}
lucide.createIcons({ root: btn });
}
let isLoading = false;
async function saveOwner(feature) {
const indicator = document.getElementById('db-status-indicator');
if (indicator) {
indicator.innerHTML = '<i data-lucide="cloud-cog" class="spin" style="width:16px;"></i> <span>Speichert...</span>';
lucide.createIcons({root: indicator});
indicator.classList.add('active');
}
try {
await fetch(`${API_BASE}/owners/${feature.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
status: feature.properties.status,
notiz: feature.properties.notiz
})
});
if (indicator) {
indicator.innerHTML = '<i data-lucide="check-circle" style="width:16px; color:#66E659;"></i> <span>Gespeichert</span>';
lucide.createIcons({root: indicator});
setTimeout(() => indicator.classList.remove('active'), 2000);
}
} catch (err) { console.error("DB Save failed:", err); }
}
async function saveVariantToDB(variantDef) {
try {
// Identify the true state variant regardless of what parameter was passed
const v = variantDef.id ? variantDef : state.variants.find(vx => routeLayers[vx.id] === variantDef);
if (!v) {
console.warn("Save skipped: Could not map to a valid variant.");
return;
}
const name = v.name || "Neue Trasse";
let rawRoutes = v.routes || [];
// Fallback attempt if routes is somehow empty but layer exists
if (rawRoutes.length === 0 && routeLayers[v.id] && typeof routeLayers[v.id].getLatLngs === 'function') {
rawRoutes = routeLayers[v.id].getLatLngs();
}
if (!rawRoutes || rawRoutes.length === 0) {
console.warn("Save skipped: No route data available.");
return;
}
// Leaflet-Punkte in GeoJSON-Format umwandeln [lng, lat]
let geoJsonCoords = [];
const isMulti = routeLayers[v.id] && Array.isArray(rawRoutes[0]);
if (isMulti || Array.isArray(rawRoutes[0])) {
geoJsonCoords = rawRoutes.map(line => line.map(p => [p.lng, p.lat]));
} else {
geoJsonCoords = rawRoutes.map(p => [p.lng, p.lat]);
}
const geomType = (isMulti || Array.isArray(rawRoutes[0])) ? "MultiLineString" : "LineString";
const payload = { id: v.id, geometry: {
type: geoJsonCoords.length ? geomType : "LineString",
coordinates: geoJsonCoords
},
properties: {
name: name,
Variante: name.replace('Variante ', '')
}
};
console.log("Sende Daten an Server:", payload);
// Verwende die absolute API_BASE URL, um CORS-Fehler bei lokalen Datei-Aufrufen zu vermeiden
const response = await fetch(`${API_BASE}/variants`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error('Server-Fehler: ' + errorText);
}
console.log("Speichern in Datenbank erfolgreich!");
} catch (err) {
console.error("DB Save failed:", err.message);
// alert("Fehler beim Speichern: " + err.message); // Commented to prevent spam during map moves
}
}
async function loadFromDatabase() {
if (isLoading) return;
isLoading = true;
try {
console.log("Starte Daten-Abruf vom Server...");
const sanitize = (gj) => {
if (!gj || !gj.features) return { type: "FeatureCollection", features: [] };
return {
...gj,
features: gj.features.filter(f => f && f.geometry && f.geometry.coordinates)
};
};
// 1. Eigentümer / Flurstücke
const ownersRes = await fetch(`${API_BASE}/owners`);
if (ownersRes.ok) {
let geojson = sanitize(await ownersRes.json());
// Transformation falls die DB UTM-Koordinaten liefert (z.B. > 1000)
if (geojson.features.length > 0) {
const firstCoord = geojson.features[0].geometry?.coordinates?.[0]?.[0]?.[0] || 0;
if (Math.abs(firstCoord) > 1000) {
console.log("Erkannte UTM-Koordinaten in DB, transformiere...");
geojson = transformGeoJSON(geojson, "EPSG:25832", "EPSG:4326");
}
}
state.owners = geojson;
updateOwnerLayer();
if (layers.owners.getBounds().isValid()) {
map.fitBounds(layers.owners.getBounds());
}
}
// 2. Varianten
const variantsRes = await fetch(`${API_BASE}/variants`);
if (variantsRes.ok) {
const variants = await variantsRes.json();
if (variants && variants.length > 0) {
variants.forEach(sv => {
const local = state.variants.find(lv => lv.id === sv.id);
if (local) {
local.routes = sv.routes || [];
local.name = sv.name || local.name;
}
});
updateRouteLayers();
}
}
// 3. Nutzungen
const usageRes = await fetch(`${API_BASE}/usage`);
if (usageRes.ok) {
state.usage = sanitize(await usageRes.json());
updateUsageLayer();
}
// 4. WEA
const weaRes = await fetch(`${API_BASE}/wea`);
if (weaRes.ok) {
state.wea = sanitize(await weaRes.json());
updateWEALayer();
}
// 5. Infratruktur
const infraRes = await fetch(`${API_BASE}/infrastructure`);
if (infraRes.ok) {
state.infrastructure = sanitize(await infraRes.json());
updateInfrastructureLayer();
}
console.log("Daten erfolgreich vom Server geladen.");
} catch (err) {
console.error("Fehler beim Laden der Server-Daten:", err);
} finally {
isLoading = false;
renderVariants();
}
}
let isSaving = false;
async function saveToFolder() {
if (isSaving || !state.directoryHandle) return;
isSaving = true;
try {
const geoHandle = await state.directoryHandle.getDirectoryHandle('Geodaten', { create: true });
const saveFile = async (name, data) => {
const handle = await geoHandle.getFileHandle(name, { create: true });
const writable = await handle.createWritable();
await writable.write(JSON.stringify(data, null, 2));
await writable.close();
};
// 1. Project State
const saveData = JSON.parse(JSON.stringify(state, (key, value) => {
if (key === 'directoryHandle' || key.startsWith('_')) return undefined;
return value;
}));
await saveFile('project.json', saveData);
// 2. Layer Data
if (state.owners?.features?.length > 0) await saveFile('eigentuemer.geojson', state.owners);
if (state.usage?.features?.length > 0) await saveFile('nutzung.geojson', state.usage);
if (state.wea?.features?.length > 0) await saveFile('wea.geojson', state.wea);
console.log("Deep-Save completed to Geodaten/");
} catch (err) {
console.error("Save failed:", err);
// Non-intrusive log instead of alert for background sync
} finally {
isSaving = false;
}
}
// --- PDF Export Implementation ---
async function runPdfExport(btnId) {
const btn = document.getElementById(btnId);
const originalText = btn.innerHTML;
btn.innerHTML = '<i class="spin" data-lucide="refresh-cw"></i> Exportiere...';
lucide.createIcons({ root: btn });
try {
const { jsPDF } = window.jspdf;
const doc = new jsPDF({
orientation: 'landscape',
unit: 'mm',
format: 'a4'
});
const pageWidth = doc.internal.pageSize.getWidth();
const pageHeight = doc.internal.pageSize.getHeight();
const margin = 10;
// 1. Capture Map
const mapContainer = document.getElementById('map');
await new Promise(r => setTimeout(r, 500));
const controls = document.querySelectorAll('.leaflet-control-container, .leaflet-control-zoom, .leaflet-control-layers');
controls.forEach(c => c.style.display = 'none');
const canvas = await html2canvas(mapContainer, {
useCORS: true,
scale: 2,
backgroundColor: '#ffffff',
ignoreElements: (el) => el.classList.contains('leaflet-control-container') || el.classList.contains('leaflet-control-zoom') || el.classList.contains('leaflet-control-layers')
});
controls.forEach(c => c.style.display = '');
const imgData = canvas.toDataURL('image/jpeg', 0.95);
const mapW = pageWidth - (margin * 2);
const mapH = pageHeight - (margin * 2) - 30;
doc.addImage(imgData, 'JPEG', margin, margin + 20, mapW, mapH);
// 2. Header
const logo = document.querySelector('.sidebar-header img');
if (logo && logo.complete && logo.naturalWidth > 0) {
try {
const logoCanvas = document.createElement('canvas');
logoCanvas.width = logo.naturalWidth;
logoCanvas.height = logo.naturalHeight;
const ctx = logoCanvas.getContext('2d');
ctx.drawImage(logo, 0, 0);
const logoData = logoCanvas.toDataURL('image/png');
const lWidth = 45;
const lHeight = lWidth * (logo.naturalHeight / logo.naturalWidth);
doc.addImage(logoData, 'PNG', margin, margin, lWidth, lHeight);
} catch (e) { }
}
doc.setFont("helvetica", "bold");
doc.setFontSize(18);
doc.setTextColor(0, 75, 80);
doc.text("Projekt: Trassenplanung BW Scheddebrock", margin + 50, margin + 10);
doc.setFontSize(10);
doc.setFont("helvetica", "normal");
doc.setTextColor(100, 100, 100);
doc.text(`Datum: ${new Date().toLocaleDateString('de-DE')} | Projektstand: v1.0.4`, margin + 50, margin + 16);
// 3. North Arrow
const nx = pageWidth - margin - 15;
const ny = margin + 12;
doc.setDrawColor(0, 0, 0);
doc.line(nx, ny, nx, ny + 12);
doc.line(nx, ny, nx - 4, ny + 4);
doc.line(nx, ny, nx + 4, ny + 4);
doc.text("N", nx - 1.5, ny - 3);
// 4. Legend
const legendX = margin + 5;
const legendY = pageHeight - margin - 35;
doc.setFillColor(255, 255, 255);
doc.rect(legendX, legendY, 65, 35, 'F');
doc.setDrawColor(200, 200, 200);
doc.rect(legendX, legendY, 65, 35, 'S');
doc.setFontSize(9);
doc.setFont("helvetica", "bold");
doc.setTextColor(0, 0, 0);
doc.text("Legende", legendX + 5, legendY + 7);
let ly = legendY + 13;
doc.setFont("helvetica", "normal");
doc.setFontSize(8);
doc.setFillColor(41, 149, 0);
doc.rect(legendX + 5, ly - 3, 4, 4, 'F');
doc.text("Vertraglich gesichert", legendX + 12, ly);
ly += 5;
doc.setFillColor(102, 230, 89);
doc.rect(legendX + 5, ly - 3, 4, 4, 'F');
doc.text("Zustimmung liegt vor", legendX + 12, ly);
ly += 5;
doc.setDrawColor(204, 163, 0);
doc.line(legendX + 5, ly - 1, legendX + 9, ly - 1);
doc.text("Geplante Trasse", legendX + 12, ly);
ly += 5;
doc.setDrawColor(0, 0, 0);
doc.setLineDash([1, 1], 0);
doc.line(legendX + 5, ly - 1, legendX + 9, ly - 1);
doc.setLineDash([], 0);
doc.text("HDD-Bohrung", legendX + 12, ly);
doc.save(`Trassenplan_Export_${new Date().toISOString().split('T')[0]}.pdf`);
} catch (err) {
console.error("PDF Export failed:", err);
alert("PDF Export fehlgeschlagen.");
} finally {
btn.innerHTML = originalText;
lucide.createIcons({ root: btn });
}
}
// Attach listeners to both possible PDF buttons (one in footer, one in options)
const pdfBtn = document.getElementById('btn-pdf-export');
if (pdfBtn) pdfBtn.addEventListener('click', () => runPdfExport('btn-pdf-export'));
const pdfBtnAlt = document.getElementById('btn-pdf-export-alt');
if (pdfBtnAlt) pdfBtnAlt.addEventListener('click', () => runPdfExport('btn-pdf-export-alt'));
document.getElementById('btn-export').addEventListener('click', async () => {
try {
const zip = new JSZip();
const saveData = JSON.parse(JSON.stringify(state, (key, value) => {
if (key === 'directoryHandle' || key.startsWith('_')) return undefined;
return value;
}));
zip.file("project.json", JSON.stringify(saveData));
zip.file("eigentuemer.geojson", JSON.stringify(state.owners));
zip.file("nutzung.geojson", JSON.stringify(state.usage));
zip.file("wea.geojson", JSON.stringify(state.wea));
state.variants.forEach(v => {
if (v.routes.length > 1) {
const line = turf.lineString(v.routes.map(ll => [ll.lng, ll.lat]));
zip.file(`trasse_${v.name.replace(/\s+/g, '_')}.geojson`, JSON.stringify(line));
}
});
const content = await zip.generateAsync({ type: "blob" });
const url = window.URL.createObjectURL(content);
const a = document.createElement("a");
a.href = url;
a.download = `TrassenPlaner_Export_${new Date().toISOString().split('T')[0]}.zip`;
document.body.appendChild(a);
a.click();
setTimeout(() => { window.URL.revokeObjectURL(url); a.remove(); }, 100);
} catch (err) {
console.error("Export failed:", err);
alert("Export fehlgeschlagen: " + err.message);
}
});
// Options Popup Logic
const toggleBtn = document.getElementById('btn-toggle-options');
const optionsPopup = document.getElementById('options-popup');
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
optionsPopup.classList.toggle('active');
lucide.createIcons({ root: optionsPopup });
});
// Close when clicking outside
document.addEventListener('click', (e) => {
if (!optionsPopup.contains(e.target) && e.target !== toggleBtn) {
optionsPopup.classList.remove('active');
}
});
// Link the second PDF button to the original function
// (Handled above by runPdfExport function refactoring)
document.getElementById('btn-import').addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file'; input.accept = '.json';
input.onchange = e => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = readerEvent => {
try {
const imported = JSON.parse(readerEvent.target.result);
if (imported.variants) {
const merged = [...state.variants];
imported.variants.forEach(v => {
let idx = merged.findIndex(m => m.id === v.id);
if (idx === -1 && v.name && v.name.startsWith("Variante ")) {
idx = merged.findIndex(m => m.name === v.name);
}
if (idx !== -1) {
merged[idx] = { ...merged[idx], ...v };
} else {
merged.push(v);
}
});
state.variants = merged.sort((a, b) => {
const nameA = (a.name || "").toUpperCase();
const nameB = (b.name || "").toUpperCase();
if (nameA < nameB) return -1;
if (nameA > nameB) return 1;
return 0;
});
delete imported.variants;
}
Object.assign(state, imported);
// Force update colors to the new standard
if (state.variants) state.variants.forEach(v => v.color = '#cca300');
updateOwnerLayer(); updateUsageLayer(); updateWEALayer(); updateRouteLayers();
renderOwnerList(); renderVariants();
} catch (err) {
console.error("Import error:", err);
alert("Import fehlgeschlagen: " + err.message);
}
}
reader.readAsText(file, 'UTF-8');
}
input.click();
});
window.addEventListener('load', async () => {
try {
initApp();
if (window.lucide) lucide.createIcons();
// 1. Immer die Daten aus der Datenbank laden!
await loadFromDatabase();
renderLegend();
// 2. Optionales lokales Verzeichnis wiederherstellen (für Legacy Export)
const handle = await getHandle();
if (handle) {
state.directoryHandle = handle;
const status = await handle.queryPermission({ mode: 'readwrite' });
if (status === 'granted') {
updateSyncUI('connected');
} else {
updateSyncUI('restorable');
console.log("Handle found, but needs permission restoration.");
}
}
} catch (err) {
console.error("Startup failed:", err);
}
});
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
state.isDrawing = false; state.isMeasuring = false;
if (activeMeasure) { activeMeasure.remove(); activeMeasure = null; }
if (tempLine) { tempLine.remove(); tempLine = null; }
updateToolCursors(); renderVariants();
}
});
</script>
<div id="db-status-indicator"><i data-lucide="cloud-cog" class="spin" style="width:16px;"></i> <span>Speichert...</span></div>
</body>
</html>