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