bwscheddebrock_trassenplaner/index.html

3261 lines
181 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 unpkg) -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
<script src="https://unpkg.com/shpjs@latest/dist/shp.js"></script>
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
<script src="https://unpkg.com/leaflet-editable@1.2.0/src/Leaflet.Editable.js"></script>
<script src="https://unpkg.com/jszip@3.10.1/dist/jszip.min.js"></script>
<script src="https://unpkg.com/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 nested = getNestedCoords(v.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 lineStr = lines.length === 1 ? lines[0] : turf.multiLineString(lines.map(l => l.geometry.coordinates));
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 tempPt = (e.type === 'editable:drawing:move') ? e.latlng : null;
const isContinuous = ['editable:vertex:drag', 'editable:drawing:move', 'editable:drag', 'editable:editing'].includes(e.type);
// Single-path update
if (isContinuous) {
try {
variant.routes = layer.getLatLngs();
renderSegmentLabels(variant, variant.routes, tempPt);
calculateStats(variant);
updateVariantStatsUI(variant);
} catch (err) { console.warn("Live render error:", err); }
} else {
variant.routes = layer.getLatLngs();
calculateStats(variant);
updateVariantStatsUI(variant);
renderVariants();
if (variant.active) updateRequiredPlots(variant);
saveVariantToDB(variant);
}
}
} 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,
middleMarkers: true,
pane: 'trassenPane',
lineOptions: { color: '#cca300', weight: 6 },
vertexOptions: { color: '#cca300', radius: 6 },
middleMarkerOptions: { color: '#cca300', opacity: 0.6, radius: 4 }
}).on('editable:vertex:drag editable:vertex:dragend editable:vertex:new editable:vertex:deleted editable:vertex:inserted', (e) => {
calculateStats(v);
updateVariantStatsUI(v);
updateRouteLabels(v);
});
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 anywhere on the polyline
routeLayers[v.id].on('click', (e) => {
if (!v.active) return;
if (!map.editTools) return;
L.DomEvent.stopPropagation(e);
const layer = routeLayers[v.id];
const point = e.latlng;
const latlngs = layer.getLatLngs();
const vertexIndex = findClosestSegmentIndex(latlngs, point);
if (vertexIndex !== -1) {
latlngs.splice(vertexIndex + 1, 0, point);
layer.setLatLngs(latlngs);
if (layer.editor) layer.editor.refresh();
calculateStats(v);
updateVariantStatsUI(v);
updateRouteLabels(v);
saveVariantToDB(v);
}
});
updateRouteLabels(v);
}
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) {
if (layer.bringToFront) layer.bringToFront();
if (drillingLayers[v.id]) drillingLayers[v.id].bringToFront();
if (labelLayers[v.id]) labelLayers[v.id].bringToFront();
}
// Direct sync for single LineString
const isDrawing = v.active && map.editTools && map.editTools.drawing();
if (!isDrawing) {
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();
const latLngs = (v.routes || []).map(ll => { try { return L.latLng(ll); } catch(e) { return null; } }).filter(ll => !!ll && typeof ll.lat === 'number');
if (latLngs.length >= 2) {
try {
const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
const totalKm = turf.length(line, { units: 'kilometers' });
const midPoint = turf.along(line, totalKm / 2, { units: 'kilometers' });
const midPos = L.latLng(midPoint.geometry.coordinates[1], midPoint.geometry.coordinates[0]);
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);
} catch (err) {
console.warn("Label positioning failed:", err);
}
}
// 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 isClickOnUI(e) {
if (!e || !e.originalEvent) return false;
const sidebar = document.getElementById('sidebar');
const rightPanel = document.getElementById('right-panel');
const target = e.originalEvent.target;
const isMarkerLabel = target.closest && (target.closest('.variant-map-label') || target.closest('.segment-label'));
return ((sidebar && sidebar.contains(target)) || (rightPanel && rightPanel.contains(target))) && !isMarkerLabel;
}
map.on('click', (e) => {
if (isClickOnUI(e)) return;
if (state.isMeasuring) {
addMeasurementPoint(e.latlng);
}
});
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';
}
// 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
if (e.layer && e.layer.setStyle) {
e.layer.setStyle({ color: '#cca300', weight: 6 });
}
// Sync logic for single line
if (activeV && routeLayers[activeV.id] && e.layer === routeLayers[activeV.id]) {
activeV.routes = e.layer.getLatLngs();
}
calculateStats(activeV);
updateVariantStatsUI(activeV);
updateVariantStatsUI(activeV);
if (e.type.includes('end') || e.type === 'editable:created' || e.type.includes('dragend') || e.type === 'editable:vertex:inserted') {
renderVariants();
const _activeV = state.variants.find(v => v.active);
if (_activeV) saveVariantToDB(_activeV);
}
});
// Helper for labels
function updateRouteLabels(variant) {
const labelGroup = labelLayers[variant.id];
if (!labelGroup) return;
labelGroup.clearLayers();
if (variant.routes && variant.routes.length > 1) {
const midIndex = Math.floor(variant.routes.length / 2);
const pos = variant.routes[midIndex];
if (pos) {
L.marker(pos, {
icon: L.divIcon({
className: 'route-label',
html: `<div style="background: white; padding: 2px 8px; border: 2px solid #cca300; border-radius: 10px; font-weight: bold; font-size: 11px; white-space: nowrap; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">${variant.name}</div>`,
iconSize: [80, 20],
iconAnchor: [40, 10]
})
}).addTo(labelGroup);
}
}
}
// Helper to find the best spot to insert a new vertex
function findClosestSegmentIndex(latlngs, point) {
let minDistance = Infinity;
let index = -1;
for (let i = 0; i < latlngs.length - 1; i++) {
const distance = L.LineUtil.closestPointOnSegment(map.latLngToLayerPoint(point), map.latLngToLayerPoint(latlngs[i]), map.latLngToLayerPoint(latlngs[i+1])).distanceTo(map.latLngToLayerPoint(point));
if (distance < minDistance) {
minDistance = distance;
index = i;
}
}
return index;
}
function getFlattenedCoords(raw) {
if (!raw) return [];
// If it's a flat array of LatLngs already
if (raw.length > 0 && raw[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 [[LatLng...], [LatLng...]]
const flattened = [];
if (Array.isArray(raw)) {
raw.forEach(section => {
if (Array.isArray(section)) {
section.forEach(ll => {
if (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 => {
if (!Array.isArray(section)) return [];
return section.map(ll => {
try { return L.latLng(ll); } catch(e) { return null; }
}).filter(ll => !!ll && typeof ll.lat === 'number');
}).filter(s => s.length > 0);
}
// If it's flat, wrap it
if (raw.length > 0) {
const flat = raw.map(ll => {
try { return L.latLng(ll); } catch(e) { return null; }
}).filter(ll => !!ll && typeof ll.lat === 'number');
return flat.length > 0 ? [flat] : [];
}
return [];
}
function renderSegmentLabels(variant, coordsInput = null, tempPt = null) {
const vLabel = labelLayers[variant.id];
if (!vLabel) return;
if (!variant.visible) { vLabel.clearLayers(); return; }
const latLngs = (coordsInput || variant.routes || []).map(ll => {
try { return L.latLng(ll); } catch(e) { return null; }
}).filter(p => !!p && typeof p.lat === 'number');
if (tempPt) latLngs.push(L.latLng(tempPt));
const currentMarkers = vLabel.getLayers();
let markerIdx = 0;
if (latLngs.length < 2) {
vLabel.clearLayers();
return;
}
for (let i = 0; i < latLngs.length - 1; i++) {
const p1 = latLngs[i];
const p2 = latLngs[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 latLngs = (variant.routes || []).map(ll => {
try { return L.latLng(ll); } catch(e) { return null; }
}).filter(p => !!p && typeof p.lat === 'number');
variant.stats.total = 0;
variant.stats.drilling = 0;
variant.stats.muffen = 0;
variant.stats.hasTooLongDrilling = false;
variant.drillingSegments = [];
if (latLngs.length < 2) {
renderSegmentLabels(variant);
return;
}
renderSegmentLabels(variant);
// Pre-filter obstacles
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 || [];
const line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
variant.stats.total = turf.length(line, { units: 'kilometers' }) * 1000;
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, variant.stats.total];
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(variant.stats.total, 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 = (variant.routes || []).map(ll => { try { return L.latLng(ll); } catch(e) { return null; } }).filter(p => !!p && typeof p.lat === 'number');
const hasData = latLngs.length >= 2;
if (!variant || !hasData || !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 line = turf.lineString(latLngs.map(ll => [ll.lng, ll.lat]));
const lineBbox = turf.bbox(line);
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(line, 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) {
const v = state.variants.find(v => v.id == id);
if (v && !v.visible) {
toggleVariantVisibility(v.id, true);
}
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="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 (v) saveVariantToDB(v);
}
};
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 (isClickOnUI(e)) return;
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;
const allPoints = (variant.routes || []).map(ll => L.latLng(ll));
let minIdx = -1;
let minDist = Infinity;
const pt = map.latLngToLayerPoint(e.latlng);
for (let i = 0; i < allPoints.length - 1; i++) {
const p1 = map.latLngToLayerPoint(allPoints[i]);
const p2 = map.latLngToLayerPoint(allPoints[i + 1]);
const dist = L.LineUtil.pointToSegmentDistance(pt, p1, p2);
if (dist < minDist) {
minDist = dist;
minIdx = i;
}
}
const isNearVertex = allPoints.some(ll => map.latLngToLayerPoint(ll).distanceTo(pt) < 12);
if (isNearVertex) return;
// High tolerance hit-testing: 35 pixels
const tolerance = state.isDrawing ? 15 : 35;
if (minIdx !== -1 && minDist < tolerance) {
allPoints.splice(minIdx + 1, 0, e.latlng);
variant.routes = allPoints;
layer.setLatLngs(allPoints);
// FORCE REFRESH of editor markers
if (layer.editor) {
layer.disableEdit();
layer.enableEdit();
}
calculateStats(variant);
updateVariantStatsUI(variant);
renderVariants();
saveVariantToDB(variant);
}
};
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) {
if (!variantDef) return;
const indicator = document.getElementById('db-status-indicator');
if (indicator) indicator.classList.add('active');
try {
// Ensure we have a valid variant object and points
if (!variantDef.id && !variantDef.name) return;
const v = variantDef.id ? variantDef : state.variants.find(vx => vx.id === variantDef.id || vx.name === variantDef.name);
if (!v || !v.routes || v.routes.length < 2) {
if (indicator) indicator.classList.remove('active');
return;
}
// Simple flat coordinates from LineString
const latLngs = (v.routes || []).map(ll => {
try { return L.latLng(ll); } catch(e) { return null; }
}).filter(p => !!p && typeof p.lat === 'number');
if (latLngs.length < 2) {
if (indicator) indicator.classList.remove('active');
return;
}
const payload = {
id: v.id,
geometry: {
type: "LineString",
coordinates: latLngs.map(p => [p.lng, p.lat])
},
properties: {
name: v.name,
Variante: v.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);
}
const result = await response.json();
if (result.success && result.id) {
v.id = result.id; // Sync the database ID back to the state
}
console.log("Speichern in Datenbank erfolgreich!");
} catch (err) {
console.error("DB Save failed:", err.message);
} finally {
if (indicator) indicator.classList.remove('active');
}
}
async function loadFromDatabase() {
const indicator = document.getElementById('db-status-indicator');
if (indicator) indicator.classList.add('active');
console.log("[V3-Sync] Starte Daten-Abruf vom Server...");
const sanitize = (gj) => {
if (!gj || !gj.features) return { type: "FeatureCollection", features: [] };
return gj;
};
// 1. Varianten (JETZT ALS ERSTES)
try {
const res = await fetch(`${API_BASE}/variants`);
if (res.ok) {
const rawData = await res.json();
let features = Array.isArray(rawData) ? rawData.map(item => ({
type: "Feature",
id: item.id,
geometry: {
type: "LineString",
coordinates: (Array.isArray(item.routes[0]) ? item.routes[0] : item.routes)
.map(p => [(p.lng || p[0]), (p.lat || p[1])])
},
properties: { Variante: item.name ? item.name.replace('Variante ', '') : 'A', name: item.name }
})) : (rawData.features || []);
console.log(`[V3-Sync] ${features.length} Trassen geladen.`);
features.forEach(f => {
const p = f.properties || {};
// Robustly find 'Variante' (case-insensitive)
const rawVariante = p.Variante || p.variante || p.VARIANTE || '';
const rawName = p.name || p.Name || p.NAME || '';
const varName = rawVariante ? `Variante ${rawVariante}` : (rawName || "Variante A");
const slot = state.variants.find(lv => lv.name === varName);
if (slot && f.geometry && f.geometry.coordinates) {
// Store the database ID separately so we don't break UI layer references
slot.dbId = f.id || p.fid || p.id || p._id;
// Handle potential nested coordinate arrays
let coords = f.geometry.coordinates;
if (f.geometry.type === "LineString") {
slot.routes = coords.map(c => ({ lat: c[1], lng: c[0] }));
} else if (f.geometry.type === "MultiLineString") {
slot.routes = coords[0].map(c => ({ lat: c[1], lng: c[0] }));
}
// Use the original UI ID to find the map layer
if (routeLayers[slot.id]) {
routeLayers[slot.id].setLatLngs(slot.routes);
}
}
});
state.variants.forEach(v => { calculateStats(v); updateVariantStatsUI(v); });
const varA = state.variants.find(v => v.name === "Variante A");
if (varA) setActiveVariant(varA.id);
renderVariants();
updateRouteLayers();
}
} catch (e) { console.error("[V3-Sync] Variants Error:", e); }
// 2. Eigentümer (SCHWERER LAYER)
try {
const res = await fetch(`${API_BASE}/owners`);
if (res.ok) {
let gj = sanitize(await res.json());
if (gj.features.length > 0) {
const c = gj.features[0].geometry.coordinates;
if (Math.abs(Array.isArray(c[0]) ? c[0][0] : c[0]) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326");
}
state.owners = gj;
updateOwnerLayer();
}
} catch (e) { console.error("[V3-Sync] Owners Error:", e); }
// 3. Nutzungen
try {
const res = await fetch(`${API_BASE}/usage`);
if (res.ok) {
let gj = sanitize(await res.json());
if (gj.features.length > 0) {
const c = gj.features[0].geometry.coordinates;
// Better detect nested multi-polygons
const firstCoord = Array.isArray(c[0]) ? (Array.isArray(c[0][0]) ? c[0][0][0] : c[0][0]) : c[0];
if (Math.abs(firstCoord) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326");
}
state.usage = gj;
cachedObstacles = null; // IMPORTANT: Reset cache to trigger new calculation
updateUsageLayer();
}
} catch (e) { console.error("[V3-Sync] Usage Error:", e); }
// 4. WEA Standorte
try {
const res = await fetch(`${API_BASE}/wea`);
if (res.ok) {
let gj = sanitize(await res.json());
if (gj.features.length > 0) {
const c = gj.features[0].geometry.coordinates;
const firstCoord = Array.isArray(c) ? c[0] : 0;
if (Math.abs(firstCoord) > 1000) gj = transformGeoJSON(gj, "EPSG:25832", "EPSG:4326");
}
state.wea = gj;
updateWEALayer();
}
} catch (e) { console.error("[V3-Sync] WEA Error:", e); }
console.log("[V3-Sync] Daten-Ladevorgang abgeschlossen.");
if (indicator) indicator.classList.remove('active');
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('keydown', (e) => {
if (e.key === 'Escape') {
if (map.editTools) {
if (map.editTools.drawing()) {
map.editTools.stopDrawing();
}
state.isDrawing = false;
state.isMeasuring = false;
updateToolCursors();
renderVariants();
const activeV = state.variants.find(v => v.active);
if (activeV && routeLayers[activeV.id]) {
routeLayers[activeV.id].enableEdit();
saveVariantToDB(activeV);
}
}
}
});
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>