Files
thetool/public/js/pages/Pop/fiber.js
Daniel Spitzer 68f29ab995 Poprack
Features:

* Komplettes kabelmanagement auf Rack He Modul Basis
2025-12-02 12:48:51 +01:00

4083 lines
187 KiB
JavaScript

let modalHistory = [];
let currentCableElement;
let cableMenuPopper = null;
let currentCableTrigger;
$(document).ready(function () {
$("body").on("click", ".cable-container", function (e) {
e.stopPropagation();
let parentTd = $(this).closest('td');
currentCableTrigger = parentTd;
let moduleId = parentTd.data('id');
let totalPorts = parentTd.data('ports');
let moduleName = parentTd.data('name');
$('#cableModal #cable-poprackmodule-id').val(moduleId);
$('#cableModal #port-start, #cableModal #port-end').attr('max', totalPorts);
$('#cableModal #cable-name, #cableModal #port-start, #cableModal #port-end').val('');
$('#cableModal #cable-modal-alert').hide();
$('#cableModal .modal-title').text('Kabel auf Panel ' + moduleName);
$('#cableModal').modal('show');
});
$("body").on("click", "#cable-add-button", function () {
if (!currentCableTrigger) return;
let rackid = currentCableTrigger.closest('table').find('th').data('rackid');
let modal = $('#cableModal');
let alert = modal.find('#cable-modal-alert');
let data = {
poprackmodule_id: modal.find('#cable-poprackmodule-id').val(),
cable_name: modal.find('#cable-name').val().trim(),
port_start: parseInt(modal.find('#port-start').val()),
port_end: parseInt(modal.find('#port-end').val()),
fiber_start: parseInt(modal.find('#fiber-start').val()) || null,
fiber_end: parseInt(modal.find('#fiber-end').val()) || null,
description: modal.find('#cable-description').val().trim()
};
if (!data.cable_name || !data.port_start || !data.port_end) {
alert.text('Bitte alle Felder ausfüllen.').show();
return;
}
if (data.port_start > data.port_end) {
alert.text('Start-Port muss kleiner oder gleich dem End-Port sein.').show();
return;
}
if (data.fiber_start && data.fiber_end) {
if (data.fiber_start > data.fiber_end) {
alert.text('Faser-Start muss kleiner oder gleich Faser-Ende sein.').show();
return;
}
const portCount = data.port_end - data.port_start + 1;
const fiberCount = data.fiber_end - data.fiber_start + 1;
if (fiberCount > portCount) {
alert.text('Die Anzahl der Fasern (' + fiberCount + ') darf nicht größer als die Anzahl der Ports (' + portCount + ') sein.').show();
return;
}
}
$.post(linkAddCable, data, function (response) {
if (response.success) {
modal.modal('hide');
let currentSide = currentCableTrigger.closest('tbody').data('side');
updateAllRackViews(rackid, currentSide);
} else {
alert.text(response.error || 'Fehler beim Speichern.').show();
}
}, 'json');
});
function showCableMenuAtEvent(e) {
const $menu = $('#cable-context-menu');
if (!$menu.length) return;
$menu.removeAttr('x-placement');
if ($menu.parent()[0] !== document.body) $menu.detach().appendTo('body');
$menu.css({ display: 'block' }).addClass('show');
const el = $menu[0];
const w = $menu.outerWidth(), h = $menu.outerHeight();
const vw = window.innerWidth, vh = window.innerHeight;
let x = e.clientX, y = e.clientY;
if (x + w > vw) x = vw - w - 6;
if (y + h > vh) y = vh - h - 6;
el.style.setProperty('position', 'fixed', 'important');
el.style.setProperty('left', x + 'px', 'important');
el.style.setProperty('top', y + 'px', 'important');
el.style.setProperty('z-index', '2147483000', 'important');
}
$('body').on('contextmenu click', '.cable-item', function (e) {
if (e.type === 'contextmenu' || e.type === 'click') {
if (e.which !== 1) return;
e.preventDefault();
e.stopPropagation();
currentCableElement = $(this);
$('.dropdown-menu').not('#cable-context-menu').removeClass('show').hide();
showCableMenuAtEvent(e);
requestAnimationFrame(() => {
const el = document.getElementById('cable-context-menu');
});
}
});
function hideCableMenu() {
const $m = $('#cable-context-menu');
const el = $m[0];
if (!$m.length) return;
$m.removeClass('show').hide();
if (el) {
el.style.removeProperty('top');
el.style.removeProperty('left');
el.style.removeProperty('position');
el.style.removeProperty('z-index');
}
}
$('#cable-context-menu')
.off('mousedown.ctx click.ctx contextmenu.ctx')
.on('mousedown.ctx click.ctx contextmenu.ctx', function (e) {
e.stopPropagation();
});
$(document)
.off('mousedown.ctx click.ctx contextmenu.ctx keydown.ctx')
.on('mousedown.ctx click.ctx contextmenu.ctx', function (e) {
if ($(e.target).closest('#cable-context-menu').length) return;
hideCableMenu();
})
.on('keydown.ctx', function (e) {
if (e.key === 'Escape' || e.keyCode === 27) hideCableMenu();
});
$(window)
.off('scroll.ctx resize.ctx')
.on('scroll.ctx resize.ctx', hideCableMenu);
$('#cable-context-menu .dropdown-item').on('click', function (e) {
e.preventDefault();
e.stopPropagation();
hideCableMenu();
const action = $(this).data('action');
const rackid = currentCableElement.closest('table').find('th').data('rackid');
const cableId = currentCableElement.data('cable-id');
const cableName = currentCableElement.data('cable-name');
switch (action) {
case 'view':
const fiberStart = currentCableElement.data('fiber-start');
const fiberEnd = currentCableElement.data('fiber-end');
loadFiberPlanCableDetails(cableName, fiberStart, fiberEnd);
break;
case 'edit': {
const cableId = currentCableElement.data('cable-id');
const cableName = currentCableElement.data('cable-name');
const portStart = currentCableElement.data('port-start');
const portEnd = currentCableElement.data('port-end');
const fiberStart = currentCableElement.data('fiber-start');
const fiberEnd = currentCableElement.data('fiber-end');
const description = currentCableElement.data('description');
$('#edit-cable-id').val(cableId);
$('#edit-cable-name').val(cableName);
$('#edit-port-start').val(portStart);
$('#edit-port-end').val(portEnd);
$('#edit-fiber-start').val(fiberStart);
$('#edit-fiber-end').val(fiberEnd);
$('#edit-cable-description').val(description);
$('#cable-edit-modal-alert').hide();
$('#cableEditModal').modal('show');
break;
}
case 'delete':
if (confirm("Kabel '" + cableName + "' wirklich entfernen?")) {
$.post(linkRemoveCable, { id: cableId }, function (response) {
if (response.success) {
let currentSide = currentCableElement.closest('tbody').data('side');
updateAllRackViews(rackid, currentSide);
} else {
alert('Fehler beim Löschen des Kabels.');
}
}, 'json');
}
break;
}
});
$("body").on("click", "#cable-update-button", function () {
const modal = $('#cableEditModal');
const cableId = modal.find('#edit-cable-id').val();
const rackid = currentCableElement.closest('table').find('th').data('rackid');
const data = {
id: cableId,
cable_name: modal.find('#edit-cable-name').val().trim(),
port_start: parseInt(modal.find('#edit-port-start').val()),
port_end: parseInt(modal.find('#edit-port-end').val()),
fiber_start: parseInt(modal.find('#edit-fiber-start').val()) || null,
fiber_end: parseInt(modal.find('#edit-fiber-end').val()) || null,
description: modal.find('#edit-cable-description').val().trim()
};
if (data.fiber_start && data.fiber_end) {
if (data.fiber_start > data.fiber_end) {
modal.find('#cable-edit-modal-alert').text('Faser-Start muss kleiner oder gleich Faser-Ende sein.').show();
return;
}
const portCount = data.port_end - data.port_start + 1;
const fiberCount = data.fiber_end - data.fiber_start + 1;
if (fiberCount > portCount) {
modal.find('#cable-edit-modal-alert').text('Die Anzahl der Fasern (' + fiberCount + ') darf nicht größer als die Anzahl der Ports (' + portCount + ') sein.').show();
return;
}
}
$.post(linkUpdateCable, data, function (response) {
if (response.success) {
modal.modal('hide');
let currentSide = currentCableElement.closest('tbody').data('side');
updateAllRackViews(rackid, currentSide);
} else {
modal.find('#cable-edit-modal-alert').text(response.error || 'Fehler beim Speichern.').show();
}
}, 'json');
});
function initCableSearch() {
if ($('#cable-search').data('select2')) {
$('#cable-search').select2('destroy');
}
$('#cable-search').empty().append(new Option());
let cableGroups = {};
$('.cable-item').each(function () {
const $cableElement = $(this);
const cableName = $cableElement.data('cable-name');
if (!cableName) return;
let fiberCount = 0;
const fiberStart = parseInt($cableElement.data('fiber-start'));
const fiberEnd = parseInt($cableElement.data('fiber-end'));
if (!isNaN(fiberStart) && !isNaN(fiberEnd)) {
fiberCount = (fiberEnd - fiberStart) + 1;
}
if (!cableGroups[cableName]) {
cableGroups[cableName] = {
totalFibers: 0
};
}
cableGroups[cableName].totalFibers += fiberCount;
});
let cableSearchData = [];
for (const name in cableGroups) {
let label = `Kabel: ${name}`;
if (cableGroups[name].totalFibers > 0) {
label += ` (Gesamt: ${cableGroups[name].totalFibers} Fasern)`;
}
cableSearchData.push({
id: name,
text: label
});
}
$('#cable-search').select2({
data: cableSearchData,
placeholder: "Kabelname suchen...",
allowClear: true,
});
}
$('#cable-search').on('select2:select', function (e) {
const selectedData = e.params.data;
const selectedCableName = selectedData.id;
if (!selectedCableName) return;
const $targetElements = $('.cable-item[data-cable-name="' + selectedCableName + '"]');
if ($targetElements.length === 0) {
return;
}
$('.cable-highlight, .cable-flash').removeClass('cable-highlight cable-flash');
$('.module-highlight').removeClass('module-highlight');
$targetElements.each(function () {
const $el = $(this);
const $parentModule = $el.closest('td');
$el.addClass('cable-highlight cable-flash');
$parentModule.addClass('module-highlight');
});
});
$('#cable-search').on('change', function (e) {
if ($(this).val() === null || $(this).val() === '') {
$('.cable-highlight, .cable-flash').removeClass('cable-highlight cable-flash');
$('.module-highlight').removeClass('module-highlight');
}
});
initCableSearch();
});
function loadFiberPlanCableDetails(cableName, fiberStart = null, fiberEnd = null) {
if (cableName) {
cableName = cableName.split(' ')[0];
}
const modal = $('#fiberPlanCableModal');
const modalBody = $('#fiberPlanCableModalBody');
modalHistory.push({
type: 'cable',
data: cableName
});
modalBody.html(`
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status"></div>
<p class="mt-3">Lade Kabel-Informationen...</p>
</div>
`);
modal.modal('show');
const requestData = {
cable_name: cableName,
network_ids: popNetworkIds // NEU: Network IDs mitsenden
};
if (fiberStart !== null && fiberEnd !== null && fiberStart !== '' && fiberEnd !== '') {
requestData.fiber_start = fiberStart;
requestData.fiber_end = fiberEnd;
}
$.ajax({
url: linkGetCableDetails,
method: 'GET',
data: requestData,
dataType: 'json',
success: function (response) {
if (response.result && response.result.cable && response.result.cable.fiber_list) {
}
if (response.status === 'OK' && response.result.cable) {
const cable = response.result.cable;
const editBtn = $('#modal-edit-cable-btn');
editBtn.attr('data-cable-id', cable.id);
editBtn.attr('data-cable-name', cable.description);
editBtn.show();
renderFiberPlanCableDetails(response.result.cable, fiberStart, fiberEnd);
} else {
$('#modal-edit-cable-btn').hide();
modalBody.html(`
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle"></i>
Kabel "${cableName}" wurde nicht in der Faserplan-Datenbank gefunden.
</div>
`);
}
},
error: function (xhr, status, error) {
modalBody.html(`
<div class="alert alert-danger">
<i class="fa fa-exclamation-circle"></i>
Keine Faserdetails für dieses Kabel vorhanden.
</div>
`);
}
});
}
function renderFiberPlanCableDetails(cable, fiberStart = null, fiberEnd = null) {
const modalBody = $('#fiberPlanCableModalBody');
const usedFibers = cable.fiber_list.filter(f => f.home_id || f.branch_type).length;
const freeFibers = cable.fiber_list.filter(f => !f.home_id && !f.branch_type).length;
let lastCustomer = null;
for (let i = cable.fiber_list.length - 1; i >= 0; i--) {
if (cable.fiber_list[i].home_id) {
lastCustomer = cable.fiber_list[i];
break;
}
}
let lastLocation = null;
for (let i = cable.fiber_list.length - 1; i >= 0; i--) {
const fiber = cable.fiber_list[i];
if (fiber.branch_to_location) {
lastLocation = fiber.branch_to_location;
break;
} else if (fiber.address) {
lastLocation = fiber.address;
break;
}
}
let routeDisplay = '-';
let mapButton = '';
if (cable.cable_route_array && cable.cable_route_array.length > 0) {
routeDisplay = cable.cable_route_array.join(' <i class="fa fa-arrow-right text-primary"></i> ');
if (cable.cable_route_full && cable.cable_route_full.length > 0) {
mapButton = `<button class="btn btn-sm btn-info ms-2 show-cable-route-map"
data-cable-id="${cable.id}"
data-cable-name="${cable.description}"
title="Route auf Karte anzeigen">
<i class="fa fa-map-marked-alt"></i> Karte
</button>`;
}
}
let toggleButton = '';
if (fiberStart && fiberEnd) {
toggleButton = `<button class="btn btn-sm btn-secondary ms-2" id="toggle-fiber-view"
data-cable-name="${cable.description}"
data-fiber-start="${fiberStart}"
data-fiber-end="${fiberEnd}"
data-show-all="false"
title="Alle Fasern anzeigen">
<i class="fa fa-eye"></i> Alle Fasern anzeigen
</button>`;
}
let html = `
<div class="row">
<div class="col-md-12">
<div class="card cable-info-card mb-3">
<div class="card-body">
<h5 class="card-title">
<i class="fa fa-info-circle"></i> Kabel-Informationen
</h5>
<div class="row">
<div class="col-md-6">
<table class="table table-sm table-borderless">
<tr>
<th width="40%">Bezeichnung:</th>
<td><strong>${cable.description || '-'}</strong></td>
</tr>
<tr>
<th>Kabelroute:</th>
<td>
${routeDisplay}
${mapButton}
${toggleButton}
</td>
</tr>
<tr>
<th>Fasern gesamt:</th>
<td>${cable.fibers || '-'}</td>
</tr>
<tr>
<th>Durchmesser:</th>
<td>${cable.diameter ? cable.diameter + ' mm' : '-'}</td>
</tr>
</table>
</div>
<div class="col-md-6">
<table class="table table-sm table-borderless">
<tr>
<th width="40%">Status:</th>
<td>${getStateText(cable.state)}</td>
</tr>
<tr>
<th>Fasern belegt:</th>
<td><span class="badge bg-info">${usedFibers}</span> / ${cable.fibers}</td>
</tr>
<tr>
<th>Fasern frei:</th>
<td><span class="badge bg-success">${freeFibers}</span></td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
`;
html += `
<div class="row">
<div class="col-md-12">
<h6 class="mb-3">
<i class="fa fa-list"></i> Fasern (${cable.fiber_list.length})
</h6>
<div class="table-responsive">
<table id="fiber-details-table" class="table table-sm table-hover table-striped fiber-list-table" style="width: 100%">
<thead class="table-light">
<tr>
<th class="text-center">Stecker</th>
<th class="text-center">Faser</th>
<th class="text-center">Bündel</th>
<th class="text-center">Status</th>
<th class="text-center">Home ID</th>
<th class="text-center">Location</th>
<th class="text-center">Ort</th>
<th class="text-center">Info</th>
<th class="text-center">Aktion</th>
</tr>
</thead>
<tbody>
`;
if (cable.fiber_list.length > 0) {
cable.fiber_list.forEach(fiber => {
const colorStyle = fiber.fiber_color_hex ? `style="background-color: ${fiber.fiber_color_hex}"` : '';
const statusBadge = getFiberStatusBadge(fiber);
const info = getFiberShortInfo(fiber);
const bundleColorStyle = fiber.bundle_color_hex ? `style="background-color: ${fiber.bundle_color_hex}"` : '';
const bundleColorDisplay = fiber.bundle_color_hex ?
`<span class="fiber-color-badge-modal" ${bundleColorStyle} title="${fiber.bundle_color || 'Bündel ' + (fiber.bundle_nr || '')}"></span>
<span class="ms-2 small">${(fiber.bundle_color ? '' : '') || (fiber.bundle_nr ? '' : '-')}</span>` :
'<span class="text-muted">-</span>';
// const hasPath = fiber.branch_type === 'Abzweigkabel' || fiber.home_id;
const hasPath = fiber.home_id && fiber.home_id !== '' || fiber.final_home_id ;
const actionButton = hasPath ?
`<button class="btn btn-sm btn-outline-info view-fiber-path"
data-fiber-id="${fiber.id}"
title="Strecke anzeigen">
<i class="fa fa-route"></i>
</button>` :
'<span class="text-muted">-</span>';
html += `
<tr class="${hasPath ? 'fiber-row-clickable' : ''}" data-fiber-id="${fiber.id}">
<td>${fiber.connector_nr || '-'}</td>
<td><span class="fiber-color-badge-modal" ${colorStyle} title="${fiber.fiber_color || 'N/A'}"></span><span class="ml-1"><strong>${fiber.fiber_nr_cable}</strong></span></td>
<td>${bundleColorDisplay}<span class="ml-1">${fiber.bundle_nr || '-'}</span></td>
<td>${statusBadge}</td>
<td class="text-nowrap small">${fiber.final_home_id || fiber.home_id || '-'}</td>
<td class="small">${fiber.branch_from_location || fiber.location || '-'}</td>
<td class="small text-nowrap">${fiber.final_location || fiber.branch_to_location || fiber.address || '-'}</td>
<td class="small text-muted">${info}</td>
<td class="text-center">${actionButton}</td>
</tr>
`;
});
} else {
html += `
<tr>
<td colspan="8" class="text-center text-muted">
<i>Keine Fasern vorhanden</i>
</td>
</tr>
`;
}
html += `
</tbody>
</table>
</div>
</div>
</div>
`;
modalBody.html(html);
if (cable.fiber_list.length > 0) {
$('#fiber-details-table thead tr').clone(true).addClass('filters').appendTo('#fiber-details-table thead');
$('#fiber-details-table').DataTable({
orderCellsTop: true,
fixedHeader: true,
paging: false,
info: false,
lengthChange: false,
dom: '<"row"<"col-sm-12"f>>t',
order: [[1, 'asc']],
language: {
search: "Suche:",
zeroRecords: "Keine Fasern gefunden",
infoEmpty: "Keine Einträge verfügbar",
searchPlaceholder: "Suchen..."
},
initComplete: function () {
var api = this.api();
api.columns().eq(0).each(function (colIdx) {
var cell = $('.filters th').eq(
$(api.column(colIdx).header()).index()
);
var title = $(cell).text();
if (title !== 'Aktion' && title !== '') {
$(cell).html('<input type="text" class="form-control form-control-sm" placeholder="' + title + '" />');
} else {
$(cell).html('');
}
$('input', cell).off('keyup input').on('keyup input', function (e) {
e.stopPropagation();
$(this).attr('title', $(this).val());
var regexr = '({search})';
api.column(colIdx)
.search(
this.value != ''
? regexr.replace('{search}', '(((' + this.value + ')))')
: '',
this.value != '',
this.value == ''
)
.draw();
});
});
}
});
}
}
function loadFiberPath(fiberId) {
const modal = $('#fiberPlanCableModal');
const modalBody = $('#fiberPlanCableModalBody');
modalHistory.push({
type: 'fiber',
data: fiberId
});
modalBody.html(`
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status"></div>
<p class="mt-3">Lade Faser-Strecke...</p>
</div>
`);
$.ajax({
url: linkGetFiberPath,
method: 'GET',
data: { fiber_id: fiberId },
dataType: 'json',
success: function (response) {
if (response.status === 'OK' && response.result.fiber) {
renderFiberPath(response.result.fiber);
} else {
modalBody.html(`
<div class="alert alert-warning">
<i class="fa fa-exclamation-triangle"></i>
Faser-Details konnten nicht geladen werden.
</div>
`);
}
},
error: function (xhr, status, error) {
modalBody.html(`
<div class="alert alert-danger">
<i class="fa fa-exclamation-circle"></i>
Fehler beim Laden der Faser-Details.
</div>
`);
}
});
}
function goBackInModal() {
modalHistory.pop();
if (modalHistory.length > 0) {
const previousState = modalHistory[modalHistory.length - 1];
modalHistory.pop();
if (previousState.type === 'cable') {
loadFiberPlanCableDetails(previousState.data);
} else if (previousState.type === 'fiber') {
loadFiberPath(previousState.data);
}
} else {
$('#fiberPlanCableModal').modal('hide');
}
}
function renderFiberPath(fiber) {
const modalBody = $('#fiberPlanCableModalBody');
const colorStyle = fiber.fiber_color_hex ? `style="background-color: ${fiber.fiber_color_hex}"` : '';
let html = `
<div class="row">
<div class="col-md-12">
<div class="card cable-info-card mb-3">
<div class="card-body">
<h5 class="card-title">
<i class="fa fa-route"></i> Faser-Strecke
</h5>
<div class="float-end">
<button class="btn btn-sm btn-info me-2 show-fiber-route-map"
data-fiber-id="${fiber.id}"
title="Gesamte Strecke auf Karte anzeigen">
<i class="fa fa-map-marked-alt"></i> Karte
</button>
<button class="btn btn-sm btn-secondary" onclick="goBackInModal()">
<i class="fa fa-arrow-left"></i> Zurück
</button>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h6 class="border-bottom pb-2 mb-3">Start-Faser</h6>
<table class="table table-sm">
<tr>
<th width="40%">Kabel:</th>
<td><strong>${fiber.cable_info ? fiber.cable_info.description : '-'}</strong></td>
</tr>
${fiber.cable_info && fiber.cable_info.cable_route_array && fiber.cable_info.cable_route_array.length > 0 ? `
<tr>
<th>Kabelroute:</th>
<td>${fiber.cable_info.cable_route_array.join(' <i class="fa fa-arrow-right text-primary"></i> ')}</td>
</tr>
` : ''}
<tr>
<th>Faser-Nr:</th>
<td><strong>${fiber.fiber_nr_cable}</strong></td>
</tr>
<tr>
<th>Farbe:</th>
<td>
<span class="fiber-color-badge-modal" ${colorStyle}></span>
</td>
</tr>
<tr>
<th>Stecker:</th>
<td>${fiber.connector_nr || '-'}</td>
</tr>
<tr>
<th>Bündel:</th>
<td>
${fiber.bundle_color ? `
<span class="fiber-color-badge-modal" style="background-color: ${fiber.bundle_color_hex || '#cccccc'}" title="${fiber.bundle_color}"></span>
<span class="ms-2">${fiber.bundle_nr || '-'} (${fiber.bundle_color})</span>
` : (fiber.bundle_nr || '-')}
</td>
</tr>
<tr>
<th>Dropout:</th>
<td class="font-weight-bolder">${fiber.branch_from_location || fiber.location || '-'}</td>
</tr>
</table>
</div>
<div class="col-md-6">
<h6 class="border-bottom pb-2 mb-3">Status</h6>
<table class="table table-sm">
<tr>
<th width="40%">Status:</th>
<td>${getFiberStatusBadge(fiber)}</td>
</tr>
<tr>
<th>Adresse:</th>
<td>${fiber.address || '-'}</td>
</tr>
<tr>
<th>Home-ID:</th>
<td>${fiber.home_id || '-'}</td>
</tr>
<tr>
<th>Name:</th>
<td>${fiber.name || '-'}</td>
</tr>
</table>
</div>
</div>
`;
const hasCustomerData = fiber.customer_cable_type ||
fiber.customer_cable_fiber_nr ||
fiber.customer_connector_type ||
fiber.customer_cable_spec ||
fiber.customer_fiber_range;
if (hasCustomerData) {
}
if (fiber.branch_type === 'Abzweigkabel' && fiber.branch_path) {
html += renderBranchPath(fiber.branch_path, 1);
} else if (hasCustomerData && fiber.address) {
html += `
<div class="row mt-3">
<div class="col-12">
<div class="card border-success">
<div class="card-header bg-opacity-10">
<h6 class="mb-0">
<i class="fa fa-plug"></i> Kunden-Anschluss
</h6>
</div>
<div class="card-body p-3">
<div class="table-responsive mb-2">
<table class="table table-sm table-bordered mb-0">
<thead class="table-light">
<tr>
<th colspan="2">
<i class="fa fa-check-circle text-success"></i>
<strong>Endpunkt erreicht</strong>
</th>
</tr>
</thead>
<tbody class="small">
${fiber.home_id ? `
<tr>
<td class="text-muted" width="30%">Home-ID:</td>
<td><strong>${fiber.home_id}</strong></td>
</tr>
` : ''}
${fiber.address ? `
<tr>
<td class="text-muted">Adresse:</td>
<td>${fiber.address}</td>
</tr>
` : ''}
${fiber.name ? `
<tr>
<td class="text-muted">Name:</td>
<td>${fiber.name}</td>
</tr>
` : ''}
</tbody>
</table>
</div>
${fiber.customer_connector_type || fiber.customer_cable_spec || fiber.customer_fiber_range ? `
<div class="alert alert-success mb-0 py-2">
<h6 class="alert-heading mb-2" style="font-size: 0.9rem;">
<i class="fa fa-info-circle"></i> Anschluss-Details
</h6>
<div style="font-size: 0.85rem;">
${fiber.customer_connector_type ? `<strong>Steckertyp:</strong> ${fiber.customer_connector_type}<br>` : ''}
${fiber.customer_cable_spec ? `<strong>Kabelspezifikation:</strong> ${fiber.customer_cable_spec}<br>` : ''}
${fiber.customer_fiber_range ? `<strong>Faser-Bereich:</strong> ${fiber.customer_fiber_range}` : ''}
</div>
</div>
` : ''}
</div>
</div>
</div>
</div>
`;
}
modalBody.html(html);
$('.show-fiber-route-map').on('click', function () {
const fiberId = $(this).data('fiber-id');
showFiberRouteMap(fiberId);
});
}
function renderBranchPath(branchPath, level) {
if (!branchPath) return '';
let html = `
<div class="row mt-3">
<div class="col-12">
<div class="card border-primary">
<div class="card-header bg-opacity-10">
<h6 class="mb-0">
<i class="fa fa-route text-primary"></i>
Strecken-Verfolgung ${level > 1 ? '- Ebene ' + level : ''}
</h6>
</div>
<div class="card-body p-3">
`;
if (branchPath.error) {
html += `
<div class="alert alert-warning mb-0 py-2">
<i class="fa fa-exclamation-triangle"></i> ${branchPath.error}
</div>
`;
html += `</div></div></div></div>`;
return html;
}
if (branchPath.cable) {
let cableRouteDisplay = '';
if (branchPath.cable.cable_route_array && branchPath.cable.cable_route_array.length > 0) {
cableRouteDisplay = `
<tr>
<td class="text-muted" width="30%">Kabelroute:</td>
<td>${branchPath.cable.cable_route_array.join(' <i class="fa fa-arrow-right text-primary"></i> ')}</td>
</tr>
`;
}
let locationDisplay = '';
if (branchPath.cable.location) {
locationDisplay = `
<tr>
<td class="text-muted" width="30%">Dropout:</td>
<td><strong>${branchPath.cable.location}</strong></td>
</tr>
`;
} else if (branchPath.cable.from_location || branchPath.cable.to_location) {
locationDisplay = `
<tr>
<td class="text-muted" width="30%">Von → Nach:</td>
<td><strong>${branchPath.cable.from_location || '-'}</strong> → <strong>${branchPath.cable.to_location || '-'}</strong></td>
</tr>
`;
}
html += `
<div class="table-responsive mb-2">
<table class="table table-sm table-bordered mb-0">
<thead class="table-light">
<tr>
<th colspan="2" class=" bg-opacity-10">
<i class="fa fa-arrow-right text-primary"></i>
<strong>${branchPath.cable.description}</strong>
</th>
</tr>
</thead>
<tbody class="small">
${cableRouteDisplay}
${locationDisplay}
<tr>
<td class="text-muted">Fasern:</td>
<td>${branchPath.cable.fibers || '-'}</td>
</tr>
</tbody>
</table>
</div>
`;
}
if (branchPath.target_fiber) {
const targetFiber = branchPath.target_fiber;
const colorStyle = targetFiber.fiber_color_hex ? `style="background-color: ${targetFiber.fiber_color_hex}"` : '';
const bundleColorStyle = targetFiber.bundle_color_hex ? `style="background-color: ${targetFiber.bundle_color_hex}"` : '';
html += `
<div class="table-responsive mb-2">
<table class="table table-sm table-bordered mb-0">
<thead class="table-light">
<tr>
<th colspan="2">
<span class="fiber-color-badge-modal" ${colorStyle}></span>
Faser #${targetFiber.fiber_nr_cable}
${targetFiber.connector_nr ? '<span class="badge bg-secondary ms-2">Stecker ' + targetFiber.connector_nr + '</span>' : ''}
</th>
</tr>
</thead>
<tbody class="small">
<tr>
<td class="text-muted" width="30%">Faserfarbe:</td>
<td>
<span class="fiber-color-badge-modal" ${colorStyle}></span>
</td>
</tr>
<tr>
<td class="text-muted">Bündel:</td>
<td>
${targetFiber.bundle_color_hex ? `<span class="fiber-color-badge-modal" ${bundleColorStyle}></span>` : ''}
${targetFiber.bundle_nr || '-'}
</td>
</tr>
${targetFiber.address ? `
<tr>
<td class="text-muted">Adresse:</td>
<td>${targetFiber.address}</td>
</tr>
` : ''}
${targetFiber.home_id ? `
<tr>
<td class="text-muted">Home-ID:</td>
<td><span class="badge bg-success">${targetFiber.home_id}</span></td>
</tr>
` : ''}
</tbody>
</table>
</div>
`;
const hasCustomerData = targetFiber.customer_cable_type ||
targetFiber.customer_cable_fiber_nr ||
targetFiber.customer_connector_type ||
targetFiber.customer_cable_spec ||
targetFiber.customer_fiber_range;
if (hasCustomerData) {
html += `
<div class="row mt-3">
<div class="col-md-12">
<div class="alert alert-success">
<h6 class="alert-heading">
<i class="fa fa-plug"></i> Kunden-Anschluss
</h6>
<div class="row">
<div class="col-md-6">
<table class="table table-sm table-borderless mb-0">
${targetFiber.customer_connector_type ? `
<tr>
<th width="40%">Steckertyp:</th>
<td>${targetFiber.customer_connector_type}</td>
</tr>
` : ''}
${targetFiber.customer_cable_spec ? `
<tr>
<th>Kabelspezifikation:</th>
<td>${targetFiber.customer_cable_spec}</td>
</tr>
` : ''}
${targetFiber.customer_fiber_range ? `
<tr>
<th>Faser-Bereich:</th>
<td>${targetFiber.customer_fiber_range}</td>
</tr>
` : ''}
</table>
</div>
<div class="col-md-6">
<table class="table table-sm table-borderless mb-0">
${targetFiber.customer_cable_type ? `
<tr>
<th width="40%">Kabeltyp:</th>
<td>${targetFiber.customer_cable_type}</td>
</tr>
` : ''}
${targetFiber.customer_cable_fiber_nr ? `
<tr>
<th>Faser-Nr:</th>
<td>${targetFiber.customer_cable_fiber_nr}</td>
</tr>
` : ''}
</table>
</div>
</div>
</div>
</div>
</div>
`;
}
}
if (branchPath.endpoint && branchPath.endpoint.type === 'customer') {
html += `
<div class="alert alert-success mb-0 py-2">
<div class="d-flex align-items-center">
<i class="fa fa-check-circle fa-2x me-3"></i>
<div>
<strong>Endpunkt erreicht</strong><br>
<small>
${branchPath.endpoint.home_id ? '<strong>Home-ID:</strong> ' + branchPath.endpoint.home_id : ''}
${branchPath.endpoint.address ? ' | <strong>Adresse:</strong> ' + branchPath.endpoint.address : ''}
${branchPath.endpoint.name ? ' | <strong>Name:</strong> ' + branchPath.endpoint.name : ''}
</small>
</div>
</div>
</div>
`;
}
html += `</div></div></div></div>`;
if (branchPath.next_branch) {
html += renderBranchPath(branchPath.next_branch, level + 1);
}
return html;
}
function getStateText(state) {
const states = {
10: '<span class="badge bg-warning text-dark">Geplant</span>',
20: '<span class="badge bg-info">In Bau</span>',
30: '<span class="badge bg-success">Produktiv</span>'
};
return states[state] || '<span class="badge bg-secondary">Unbekannt</span>';
}
function getFiberStatusBadge(fiber) {
if (fiber.home_id) {
return '<span class="badge bg-success">Aktiv (Kunde)</span>';
} else if (fiber.branch_type === 'Abzweigkabel') {
return '<span class="badge bg-warning text-dark">Abzweig</span>';
} else {
return '<span class="badge bg-secondary">Frei</span>';
}
}
function getFiberShortInfo(fiber) {
if (fiber.home_id) {
return fiber.address || fiber.home_id;
} else if (fiber.branch_type === 'Abzweigkabel') {
return fiber.branch_cable_nr || 'Abzweig';
}
return 'Frei';
}
$('#fiberPlanCableModal').on('hidden.bs.modal', function () {
modalHistory = [];
});
$(document).on('click', '#modal-edit-cable-btn', function (e) {
e.preventDefault();
e.stopPropagation();
const cableId = $(this).attr('data-cable-id');
const cableName = $(this).attr('data-cable-name');
if (!cableId) {
alert('Fehler: Keine Kabel-ID gefunden!');
return;
}
$('#fiberPlanCableModal').modal('hide');
currentCableId = cableId;
$('#cable-name').text(cableName || 'Unbekanntes Kabel');
$('#excelEditorModal').modal('show');
loadCableData(cableId);
});
$('#fiberPlanCableModal').on('hidden.bs.modal', function () {
modalHistory = [];
$('#modal-edit-cable-btn').hide();
});
function showCableRouteMap(cableId, cableName) {
// ÄNDERUNG: Modal-Body Layout angepasst für Karte (oben) und Schema (unten)
const mapModalHtml = `
<div class="modal fade" id="cableRouteMapModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-fullscreen-map" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fa fa-map-marked-alt"></i>
Kabelroute: ${cableName}
</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body p-0" style="display: flex; flex-direction: column; height: calc(100vh - 120px);">
<div id="cable-route-map" style="flex: 0 0 70%; width: 100%; min-height: 400px;"></div>
<div id="cable-route-schema" style="flex: 0 0 auto; max-height: 30%; width: 100%; overflow-y: auto; border-top: 3px solid #007bff; background: #ffffff; padding: 20px;" class="pt-0"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">
Schließen
</button>
</div>
</div>
</div>
</div>
`;
$('#cableRouteMapModal').remove();
$('body').append(mapModalHtml);
$('#cableRouteMapModal').modal('show');
$.ajax({
url: linkGetCableDetails,
method: 'GET',
data: { cable_name: cableName },
dataType: 'json',
success: function (response) {
if (response.status === 'OK' && response.result.cable) {
initializeCableRouteMap(response.result.cable);
// ÄNDERUNG: Schema rendern aufrufen
renderCableRouteSchema(response.result.cable);
} else {
$('#cable-route-map').html(`
<div class="alert alert-warning m-3">
<i class="fa fa-exclamation-triangle"></i>
Keine Daten für diese Route verfügbar.
</div>
`);
}
},
error: function () {
$('#cable-route-map').html(`
<div class="alert alert-danger m-3">
<i class="fa fa-exclamation-circle"></i>
Fehler beim Laden der Kartendaten.
</div>
`);
}
});
}
function initializeCableRouteMap(cable) {
setTimeout(function () {
const mapContainer = document.getElementById('cable-route-map');
if (!mapContainer) {
return;
}
if (!cable.cable_route_full || cable.cable_route_full.length === 0) {
$('#cable-route-map').html(`
<div class="alert alert-warning m-3">
<i class="fa fa-exclamation-triangle"></i>
Keine GPS-Koordinaten für diese Kabelroute verfügbar.
</div>
`);
return;
}
const validStations = cable.cable_route_full.filter(station =>
station.gps_lat && station.gps_long
);
if (validStations.length === 0) {
$('#cable-route-map').html(`
<div class="alert alert-warning m-3">
<i class="fa fa-exclamation-triangle"></i>
Keine gültigen GPS-Koordinaten gefunden.
</div>
`);
return;
}
let centerLat = 0;
let centerLng = 0;
validStations.forEach(station => {
centerLat += parseFloat(station.gps_lat);
centerLng += parseFloat(station.gps_long);
});
centerLat /= validStations.length;
centerLng /= validStations.length;
const map = L.map('cable-route-map').setView([centerLat, centerLng], 13);
L.tileLayer('https://mapsneu.wien.gv.at/basemap/{id}/normal/google3857/{z}/{y}/{x}.{imgtype}', {
maxZoom: 22,
id: "geolandbasemap",
imgtype: "png"
}).addTo(map);
const fibersByBundle = {};
cable.fiber_list.forEach(fiber => {
const bundleKey = fiber.bundle_nr || 'ohne_buendel';
if (!fibersByBundle[bundleKey]) {
fibersByBundle[bundleKey] = {
fibers: [],
bundle_color: fiber.bundle_color,
bundle_color_hex: fiber.bundle_color_hex,
bundle_nr: fiber.bundle_nr
};
}
fibersByBundle[bundleKey].fibers.push(fiber);
});
const markerGroup = L.featureGroup();
const routePoints = [];
let detailedCoordinates = null;
if (cable.coordinates) {
detailedCoordinates = cable.coordinates;
}
if (detailedCoordinates && typeof detailedCoordinates === 'string') {
try {
detailedCoordinates = JSON.parse(detailedCoordinates);
} catch (e) {
detailedCoordinates = null;
}
}
validStations.forEach((station, index) => {
const lat = parseFloat(station.gps_lat);
const lng = parseFloat(station.gps_long);
routePoints.push([lat, lng]);
let iconColor = '#3388ff';
let iconName = 'circle';
let iconSize = 'm';
if (station.type === 'pop') {
iconColor = '#acf0ab';
iconName = 'village';
iconSize = 'l';
} else if (station.type === 'dispatcher') {
iconColor = '#abbaf0';
iconName = 'home';
iconSize = 'm';
}
const marker = L.marker([lat, lng], {
icon: L.MakiMarkers.icon({
icon: iconName,
color: iconColor,
size: iconSize
})
}).bindPopup(`
<strong>${index + 1}. ${station.name}</strong><br>
<small>Typ: ${station.type === 'pop' ? 'POP' : 'Verteiler'}</small><br>
<small>${lat.toFixed(5)}, ${lng.toFixed(5)}</small>
`);
markerGroup.addLayer(marker);
});
markerGroup.addTo(map);
let routePolyline;
if (detailedCoordinates && Array.isArray(detailedCoordinates) && detailedCoordinates.length > 0) {
const leafletCoords = detailedCoordinates.map(coord => [
parseFloat(coord.gps_lat),
parseFloat(coord.gps_long)
]);
routePolyline = L.polyline(leafletCoords, {
color: '#FF6B6B',
weight: 4,
opacity: 0.7
}).addTo(map);
} else {
routePolyline = L.polyline(routePoints, {
color: '#FF6B6B',
weight: 4,
opacity: 0.7
}).addTo(map);
}
let popupContent = `
<div style="min-width: 300px; max-height: 400px; overflow-y: auto;">
<h6><strong>${cable.description}</strong></h6>
<hr class="my-2">
<div class="mb-2">
<small class="text-muted">Fasern: ${cable.fibers} | Durchmesser: ${cable.diameter}mm</small>
</div>
`;
Object.keys(fibersByBundle).forEach(bundleKey => {
const bundle = fibersByBundle[bundleKey];
const bundleColor = bundle.bundle_color_hex || '#cccccc';
popupContent += `
<div class="border rounded p-2 mb-2" style="background-color: rgba(0,0,0,0.03);">
<div class="d-flex align-items-center mb-1">
<span style="display: inline-block; width: 20px; height: 20px; border-radius: 50%; background-color: ${bundleColor}; border: 2px solid #333; margin-right: 8px;"></span>
<strong>Bündel ${bundle.bundle_nr || '-'}</strong>
${bundle.bundle_color ? `<span class="badge badge-secondary ms-2">${bundle.bundle_color}</span>` : ''}
</div>
<div style="display: flex; flex-wrap: wrap; gap: 4px;">
`;
bundle.fibers.forEach(fiber => {
const fiberColor = fiber.fiber_color_hex || '#cccccc';
const status = fiber.home_id ? 'belegt' : (fiber.branch_type ? 'abzweig' : 'frei');
const statusColor = fiber.home_id ? '#28a745' : (fiber.branch_type ? '#ffc107' : '#6c757d');
popupContent += `
<div style="display: inline-flex; align-items: center; padding: 2px 6px; background: white; border: 1px solid #ddd; border-radius: 3px; font-size: 11px;">
<span style="display: inline-block; width: 12px; height: 12px; border-radius: 50%; background-color: ${fiberColor}; border: 1px solid #666; margin-right: 4px;"></span>
<span style="font-weight: bold;">${fiber.fiber_nr_cable}</span>
${fiber.fiber_nr_bundle ? `<span style="color: #666; margin-left: 2px;">:${fiber.fiber_nr_bundle}</span>` : ''}
<span style="width: 6px; height: 6px; border-radius: 50%; background-color: ${statusColor}; margin-left: 4px;" title="${status}"></span>
</div>
`;
});
popupContent += `
</div>
</div>
`;
});
popupContent += `</div>`;
routePolyline.bindPopup(popupContent, {
maxWidth: 400,
className: 'cable-info-popup'
});
routePolyline.bindTooltip(`${cable.description} (${cable.fibers} Fasern)`, {
permanent: false,
sticky: true,
direction: 'top'
});
if (cable.fiber_list && Array.isArray(cable.fiber_list)) {
cable.fiber_list.forEach(fiber => {
if (fiber.home_id && fiber.address) {
let dropoutStation = null;
if (fiber.location) {
dropoutStation = validStations.find(station =>
station.name === fiber.location
);
}
if (!dropoutStation && validStations.length > 0) {
dropoutStation = validStations[validStations.length - 1];
}
if (dropoutStation) {
const dropoutLat = parseFloat(dropoutStation.gps_lat);
const dropoutLng = parseFloat(dropoutStation.gps_long);
const customerCircle = L.circleMarker([dropoutLat, dropoutLng], {
radius: 8,
fillColor: '#f0abab',
fillOpacity: 0.8,
color: '#000000',
weight: 2,
opacity: 1
}).bindPopup(`
<div style="min-width: 200px;">
<h6><i class="fa fa-home"></i> Hausanschluss</h6>
<hr style="margin: 5px 0;">
<strong>Home-ID:</strong> ${fiber.home_id}<br>
<strong>Adresse:</strong> ${fiber.address}<br>
${fiber.name ? `<strong>Name:</strong> ${fiber.name}<br>` : ''}
<hr style="margin: 5px 0;">
<strong>Faser:</strong> #${fiber.fiber_nr_cable}
${fiber.fiber_color ? ` (${fiber.fiber_color})` : ''}<br>
${fiber.bundle_nr ? `<strong>Bündel:</strong> ${fiber.bundle_nr}` : ''}
${fiber.bundle_color ? ` (${fiber.bundle_color})` : ''}
</div>
`).addTo(map);
markerGroup.addLayer(customerCircle);
}
}
});
}
map.fitBounds(markerGroup.getBounds(), { padding: [50, 50] });
const legend = L.control({ position: 'bottomright' });
legend.onAdd = function (map) {
const div = L.DomUtil.create('div', 'info legend');
div.style.backgroundColor = 'white';
div.style.padding = '10px';
div.style.border = '2px solid #ccc';
div.style.borderRadius = '5px';
div.style.fontSize = '12px';
let html = '<strong>Legende</strong><br>';
html += '<i class="fa fa-circle" style="color: #acf0ab; margin-right: 5px;"></i> POP<br>';
html += '<i class="fa fa-circle" style="color: #abbaf0; margin-right: 5px;"></i> Verteiler<br>';
html += '<hr style="margin: 5px 0;">';
html += '<strong>Status:</strong><br>';
html += '<span style="display: inline-block; width: 10px; height: 10px; border-radius: 50%; background-color: #28a745; margin-right: 5px;"></span> Belegt<br>';
html += '<span style="display: inline-block; width: 10px; height: 10px; border-radius: 50%; background-color: #ffc107; margin-right: 5px;"></span> Abzweig<br>';
html += '<span style="display: inline-block; width: 10px; height: 10px; border-radius: 50%; background-color: #6c757d; margin-right: 5px;"></span> Frei<br>';
div.innerHTML = html;
return div;
};
legend.addTo(map);
}, 300);
}
function renderCableRouteSchema(cable) {
const schemaContainer = $('#cable-route-schema');
if (!cable || !cable.cable_route_full || cable.cable_route_full.length === 0) {
schemaContainer.html('<div class="text-center text-muted">Keine Routen-Informationen verfügbar</div>');
return;
}
let html = '<div class="fiber-path-schema-horizontal">';
html += '<h6 class="p-0"><i class="fa fa-project-diagram"></i> Schematischer Kabelverlauf</h6>';
html += '<div class="schema-flow-horizontal">';
cable.cable_route_full.forEach((station, index) => {
let style = { icon: 'fa-sitemap', cssClass: 'station-distributor', color: '#3388ff', typeLabel: 'Verteiler' };
let badgeClass = 'badge-info';
if (station.type === 'pop') {
style = { icon: 'fa-building', cssClass: 'station-pop', color: '#4caf50', typeLabel: 'POP' };
badgeClass = 'badge-success';
} else if (station.object_type == 2) { // Schacht
style = { icon: 'fa-archive', cssClass: 'station-manhole', color: '#ffa726', typeLabel: 'Schacht' };
badgeClass = 'badge-warning';
}
const popupContent = `
<div style="min-width: 200px;">
<h6><strong>${station.name}</strong></h6>
<span class="badge ${badgeClass}">${style.typeLabel}</span>
<hr style="margin: 8px 0;">
<div style="font-size: 11px;">
<strong>Position:</strong> ${index + 1}<br>
<small class="text-muted">${station.gps_lat ? parseFloat(station.gps_lat).toFixed(5) + ', ' + parseFloat(station.gps_long).toFixed(5) : 'Kein GPS'}</small>
</div>
</div>`;
html += `
<div class="schema-node-horizontal ${style.cssClass} schema-station-clickable"
data-popup-content="${popupContent.replace(/"/g, '&quot;')}"
data-station-name="${station.name}">
<div class="schema-node-icon-horizontal" style="color: ${style.color};">
<i class="fa ${style.icon}"></i>
</div>
<div class="schema-node-label-horizontal">
<strong>${station.name}</strong>
</div>
</div>
`;
if (index < cable.cable_route_full.length - 1) {
html += `
<div class="schema-connection-horizontal">
<div class="schema-line-horizontal" style="background-color: #666; height: 4px;">
<div class="schema-cable-label">
<strong>${cable.description}</strong><br>
</div>
</div>
</div>
`;
}
});
html += '</div></div>';
schemaContainer.html(html);
// Popover Logik aktivieren (wiederverwendet)
$('.schema-station-clickable').off('click').on('click', function (e) {
e.stopPropagation();
const $station = $(this);
const popupContent = $station.attr('data-popup-content').replace(/&quot;/g, '"');
$station.popover('dispose');
$station.popover({
content: popupContent,
html: true,
placement: 'top',
trigger: 'manual',
container: 'body',
template: `
<div class="popover station-schema-popover" role="tooltip">
<div class="arrow"></div>
<h3 class="popover-header"></h3>
<div class="popover-body"></div>
</div>
`
});
$('.schema-station-clickable').not($station).popover('hide');
$station.popover('show');
});
}
function showFiberRouteMap(fiberId) {
const mapModalHtml = `
<div class="modal fade" id="fiberRouteMapModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-fullscreen-map" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fa fa-map-marked-alt"></i>
Komplette Faser-Strecke
</h5>
<button type="button" class="close" data-dismiss="modal">
<span>&times;</span>
</button>
</div>
<div class="modal-body p-0" style="display: flex; flex-direction: column; height: calc(100vh - 120px);">
<!-- ÄNDERUNG: Map nimmt 70% der Höhe -->
<div id="fiber-route-map" style="flex: 0 0 70%; width: 100%; min-height: 400px;"></div>
<!-- ÄNDERUNG: Schema unter der Map mit automatischer Höhe, scrollbar -->
<div id="fiber-route-schema" style="flex: 0 0 auto; max-height: 30%; width: 100%; overflow-y: auto; border-top: 3px solid #007bff; background: #ffffff; padding: 20px;" class="pt-0"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">
Schließen
</button>
</div>
</div>
</div>
</div>
`;
$('#fiberRouteMapModal').remove();
$('body').append(mapModalHtml);
$('#fiberRouteMapModal').modal('show');
$.ajax({
url: linkGetFiberPath,
method: 'GET',
data: { fiber_id: fiberId },
dataType: 'json',
success: function (response) {
if (response.status === 'OK' && response.result.fiber) {
initializeFiberRouteMap(response.result.fiber);
} else {
$('#fiber-route-map').html(`
<div class="alert alert-warning m-3">
<i class="fa fa-exclamation-triangle"></i>
Keine GPS-Daten für diese Strecke verfügbar.
</div>
`);
}
},
error: function () {
$('#fiber-route-map').html(`
<div class="alert alert-danger m-3">
<i class="fa fa-exclamation-circle"></i>
Fehler beim Laden der Kartendaten.
</div>
`);
}
});
}
function findNearestBranchPoint(customerLat, customerLng, allSegments) {
let nearestBranchPoint = null;
let nearestDispatcher = null;
let minBranchDistance = Infinity;
let minDispatcherDistance = Infinity;
allSegments.forEach((segment, segIdx) => {
if (segment.stations && Array.isArray(segment.stations)) {
segment.stations.forEach((station, stationIdx) => {
if (!station.gps_lat || !station.gps_long) return;
const stationLat = parseFloat(station.gps_lat);
const stationLng = parseFloat(station.gps_long);
const distance = Math.sqrt(
Math.pow(customerLat - stationLat, 2) +
Math.pow(customerLng - stationLng, 2)
);
if (station.object_type == 4 || station.object_type === '4') {
if (distance < minBranchDistance) {
minBranchDistance = distance;
nearestBranchPoint = {
lat: stationLat,
lng: stationLng,
name: station.name || 'Abzweigpunkt',
station: station,
type: 'branch_point'
};
}
}
else if (station.type === 'dispatcher' && (station.object_type == 1 || station.object_type == 2 || station.object_type === '1' || station.object_type === '2')) {
if (distance < minDispatcherDistance) {
minDispatcherDistance = distance;
nearestDispatcher = {
lat: stationLat,
lng: stationLng,
name: station.name || 'Verteiler',
station: station,
type: 'dispatcher'
};
}
}
});
}
});
if (nearestBranchPoint) {
return nearestBranchPoint;
}
else if (nearestDispatcher) {
return nearestDispatcher;
}
return null;
}
function getDistanceToPolyline(pointLat, pointLng, polylineCoords) {
let minDistance = Infinity;
for (let i = 0; i < polylineCoords.length - 1; i++) {
const segmentStart = polylineCoords[i];
const segmentEnd = polylineCoords[i + 1];
const distance = pointToSegmentDistance(
pointLat, pointLng,
segmentStart[0], segmentStart[1],
segmentEnd[0], segmentEnd[1]
);
if (distance < minDistance) {
minDistance = distance;
}
}
return minDistance;
}
function pointToSegmentDistance(px, py, x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
if (dx === 0 && dy === 0) {
return Math.sqrt(Math.pow(px - x1, 2) + Math.pow(py - y1, 2));
}
const t = Math.max(0, Math.min(1,
((px - x1) * dx + (py - y1) * dy) / (dx * dx + dy * dy)
));
const nearestX = x1 + t * dx;
const nearestY = y1 + t * dy;
return Math.sqrt(Math.pow(px - nearestX, 2) + Math.pow(py - nearestY, 2));
}
function initializeFiberRouteMap(fiber, isLinework = false) {
setTimeout(function () {
const mapContainer = document.getElementById('fiber-route-map');
const allSegments = [];
const allStations = [];
if (fiber.cable_info && fiber.cable_info.cable_route_full) {
let mainFiberNr = fiber.fiber_nr_cable;
let mainFiberColor = fiber.fiber_color;
let mainFiberColorHex = fiber.fiber_color_hex;
let mainBundleNr = fiber.bundle_nr;
let mainBundleColor = fiber.bundle_color;
let mainBundleColorHex = fiber.bundle_color_hex;
let branchSource = null;
if (fiber.branch_path && fiber.branch_path.source_fiber) {
branchSource = fiber.branch_path.source_fiber;
} else if (fiber.cable_info && fiber.cable_info.branch_path && fiber.cable_info.branch_path.source_fiber) {
branchSource = fiber.cable_info.branch_path.source_fiber;
}
if (isLinework && !branchSource && fiber.branch_path && fiber.branch_path.cable && fiber.cable_info.fibers) {
const firstBranchCableName = fiber.branch_path.cable.description || fiber.branch_path.cable.name;
const targetFiberNrInBranch = fiber.fiber_nr_cable;
if (firstBranchCableName) {
const candidates = fiber.cable_info.fibers.filter(f =>
f.branch_cable_nr === firstBranchCableName ||
(f.branch_path && f.branch_path.cable && f.branch_path.cable.name === firstBranchCableName)
);
let foundSourceFiber = null;
if (candidates.length > 0) {
foundSourceFiber = candidates.find(f =>
f.branch_fiber_nr == targetFiberNrInBranch ||
f.branch_fiber_nr_cable == targetFiberNrInBranch
);
if (!foundSourceFiber) {
// console.log("⚠️ Kein exakter Faser-Match (z.B. auf Branch-Faser " + targetFiberNrInBranch + "), nehme ersten Kandidaten.");
foundSourceFiber = candidates[0];
}
}
if (foundSourceFiber) {
const correctFiberNr = foundSourceFiber.fiber_nr_cable || foundSourceFiber.fiber_nr;
// console.log("🔧 Hauptkabel-Faser gefunden:", correctFiberNr, foundSourceFiber);
branchSource = {
fiber_nr_cable: correctFiberNr,
fiber_color: foundSourceFiber.color_name || foundSourceFiber.fiber_color || foundSourceFiber.color,
fiber_color_hex: foundSourceFiber.color_hex || foundSourceFiber.fiber_color_hex || foundSourceFiber.color,
bundle_nr: foundSourceFiber.bundle_nr || foundSourceFiber.bundle,
bundle_color: foundSourceFiber.bundle_color || foundSourceFiber.bundle_color_name,
bundle_color_hex: foundSourceFiber.bundle_color_hex
};
}
}
}
if (branchSource) {
mainFiberNr = branchSource.fiber_nr_cable;
mainFiberColor = branchSource.fiber_color;
mainFiberColorHex = branchSource.fiber_color_hex;
mainBundleNr = branchSource.bundle_nr;
mainBundleColor = branchSource.bundle_color;
mainBundleColorHex = branchSource.bundle_color_hex;
// console.log("✅ Hauptkabel-Daten erfolgreich korrigiert auf Faser:", mainFiberNr);
} else {
// console.warn("⚠️ Konnte Quell-Faser nicht ermitteln (Fallback auf End-Faser).");
}
allSegments.push({
name: fiber.cable_info.description,
stations: fiber.cable_info.cable_route_full,
color: mainFiberColorHex || '#3388ff',
level: 0,
type: 'main',
fiber_nr: mainFiberNr,
fiber_color: mainFiberColor,
fiber_color_hex: mainFiberColorHex,
bundle_nr: mainBundleNr,
bundle_color: mainBundleColor,
bundle_color_hex: mainBundleColorHex,
location: fiber.location || fiber.cable_info.location,
cable: fiber.cable_info
});
}
if (fiber.cable_info && fiber.cable_info.branch_points && Array.isArray(fiber.cable_info.branch_points) && fiber.cable_info.branch_points.length > 0) {
allSegments.push({
name: 'Abzweigpunkte Netzwerk',
stations: fiber.cable_info.branch_points,
color: '#FF6B6B',
level: 0,
type: 'branch_points',
cable: null
});
} else {
}
function collectBranchRoutes(branchPath, level = 1) {
if (!branchPath || branchPath.error) {
return;
}
if (branchPath.cable && branchPath.cable.cable_route_full) {
const targetFiber = branchPath.target_fiber;
const fiberColor = targetFiber?.fiber_color_hex || getBranchColor(level);
allSegments.push({
name: branchPath.cable.description,
stations: branchPath.cable.cable_route_full,
color: fiberColor,
level: level,
type: 'branch',
location: branchPath.cable.location,
cable: branchPath.cable,
coordinates: branchPath.cable.coordinates,
cable_info: branchPath.cable.cable_info || branchPath.cable,
fiber_nr: targetFiber?.fiber_nr_cable,
fiber_color: targetFiber?.fiber_color,
fiber_color_hex: targetFiber?.fiber_color_hex,
bundle_nr: targetFiber?.bundle_nr,
bundle_color: targetFiber?.bundle_color,
bundle_color_hex: targetFiber?.bundle_color_hex,
target_fiber: targetFiber
});
}
if (branchPath.next_branch) {
collectBranchRoutes(branchPath.next_branch, level + 1);
}
}
if (fiber.branch_path) {
collectBranchRoutes(fiber.branch_path);
}
if (allSegments.length > 1) {
const mainSegment = allSegments.find(seg => seg.type === 'main');
const branchSegments = allSegments.filter(seg => seg.type === 'branch');
if (mainSegment && branchSegments.length > 0) {
branchSegments.forEach(branchSeg => {
if (fiber.cable_info && fiber.cable_info.fibers) {
const branchFiber = fiber.cable_info.fibers.find(f =>
f.branch_type === 'Abzweigkabel' &&
f.branch_cable_nr === branchSeg.name
);
if (branchFiber && branchFiber.branch_from_location) {
branchSeg.branch_from_location_main = branchFiber.branch_from_location;
if (!mainSegment.branch_dropouts) {
mainSegment.branch_dropouts = [];
}
mainSegment.branch_dropouts.push(branchFiber.branch_from_location);
} else {
}
}
});
if (mainSegment.branch_dropouts && mainSegment.branch_dropouts.length > 0) {
}
}
}
if (allSegments.length === 0) {
$('#fiber-route-map').html(`
<div class="alert alert-warning m-3">
<i class="fa fa-exclamation-triangle"></i>
Keine GPS-Koordinaten für diese Faser-Strecke verfügbar.
</div>
`);
return;
}
allSegments.forEach(segment => {
segment.stations.forEach(station => {
if (station.gps_lat && station.gps_long) {
allStations.push(station);
}
});
});
if (allStations.length === 0) {
$('#fiber-route-map').html(`
<div class="alert alert-warning m-3">
<i class="fa fa-exclamation-triangle"></i>
Keine gültigen GPS-Koordinaten gefunden.
</div>
`);
return;
}
let centerLat = 0;
let centerLng = 0;
allStations.forEach(station => {
centerLat += parseFloat(station.gps_lat);
centerLat += parseFloat(station.gps_lat);
centerLng += parseFloat(station.gps_long);
});
centerLat /= allStations.length;
centerLng /= allStations.length;
if (window.fiberRouteMapInstance) {
// console.log('🗑️ Entferne alte Map-Instanz');
window.fiberRouteMapInstance.remove();
window.fiberRouteMapInstance = null;
}
const mapContainerold = document.getElementById('fiber-route-map');
if (mapContainerold) {
mapContainerold.innerHTML = '';
mapContainerold._leaflet_id = null;
if (mapContainerold._leaflet) {
delete mapContainerold._leaflet;
}
}
// console.log('🗺️ Erstelle neue Map-Instanz');
const map = L.map('fiber-route-map').setView([centerLat, centerLng], 13);
window.fiberRouteMapInstance = map;
L.tileLayer('https://mapsneu.wien.gv.at/basemap/{id}/normal/google3857/{z}/{y}/{x}.{imgtype}', {
maxZoom: 22,
id: "geolandbasemap",
imgtype: "png"
}).addTo(map);
window.allBranchPoints = [];
if (fiber.cable_info && fiber.cable_info.branch_points && Array.isArray(fiber.cable_info.branch_points)) {
fiber.cable_info.branch_points.forEach(bp => {
window.allBranchPoints.push({
lat: parseFloat(bp.gps_lat),
lng: parseFloat(bp.gps_long),
name: bp.name || 'Abzweigpunkt',
segment: null,
station: bp
});
});
// console.log(`${window.allBranchPoints.length} Abzweigpunkte aus Backend geladen`);
}
const allBounds = L.featureGroup();
function getBranchConnectionsAtStation(stationName, currentSegment, allSegs) {
const connections = [];
if (currentSegment.type === 'main' && currentSegment.branch_dropouts) {
currentSegment.branch_dropouts.forEach(dropoutLocation => {
if (dropoutLocation === stationName) {
allSegs.forEach(seg => {
if (seg.type === 'branch' && seg.branch_from_location_main === stationName) {
connections.push({
type: 'branch_from_main',
fromCable: currentSegment.name,
fromFiber: currentSegment.fiber_nr,
fromFiberColor: currentSegment.fiber_color,
fromFiberColorHex: currentSegment.color,
fromBundle: currentSegment.bundle_nr,
fromBundleColor: currentSegment.bundle_color,
fromBundleColorHex: currentSegment.bundle_color_hex,
toCable: seg.name,
toFiber: seg.fiber_nr,
toFiberColor: seg.fiber_color,
toFiberColorHex: seg.color,
toBundle: seg.bundle_nr,
toBundleColor: seg.bundle_color,
toBundleColorHex: seg.bundle_color_hex,
level: seg.level
});
}
});
}
});
}
if (currentSegment.type === 'branch' || currentSegment.type === 'main') {
const currentLastStation = currentSegment.stations && currentSegment.stations.length > 0
? currentSegment.stations[currentSegment.stations.length - 1] : null;
allSegs.forEach(seg => {
if (seg.type === 'branch' && seg !== currentSegment) {
const firstStation = seg.stations && seg.stations.length > 0 ? seg.stations[0] : null;
if (seg.level > currentSegment.level && firstStation && firstStation.name === stationName) {
const alreadyAdded = connections.some(c => c.toCable === seg.name);
if (!alreadyAdded) {
connections.push({
type: currentSegment.type === 'main' ? 'branch_from_main' : 'branch_from_branch',
fromCable: currentSegment.name,
fromFiber: currentSegment.fiber_nr,
fromFiberColor: currentSegment.fiber_color,
fromFiberColorHex: currentSegment.color,
fromBundle: currentSegment.bundle_nr,
fromBundleColor: currentSegment.bundle_color,
fromBundleColorHex: currentSegment.bundle_color_hex,
toCable: seg.name,
toFiber: seg.fiber_nr,
toFiberColor: seg.fiber_color,
toFiberColorHex: seg.color,
toBundle: seg.bundle_nr,
toBundleColor: seg.bundle_color,
toBundleColorHex: seg.bundle_color_hex,
level: seg.level
});
}
}
if (seg.level > currentSegment.level &&
currentLastStation && currentLastStation.name === stationName &&
firstStation && firstStation.name === stationName) {
const alreadyAdded = connections.some(c => c.toCable === seg.name);
if (!alreadyAdded) {
connections.push({
type: 'branch_from_branch',
fromCable: currentSegment.name,
fromFiber: currentSegment.fiber_nr,
fromFiberColor: currentSegment.fiber_color,
fromFiberColorHex: currentSegment.color,
fromBundle: currentSegment.bundle_nr,
fromBundleColor: currentSegment.bundle_color,
fromBundleColorHex: currentSegment.bundle_color_hex,
toCable: seg.name,
toFiber: seg.fiber_nr,
toFiberColor: seg.fiber_color,
toFiberColorHex: seg.color,
toBundle: seg.bundle_nr,
toBundleColor: seg.bundle_color,
toBundleColorHex: seg.bundle_color_hex,
level: seg.level
});
}
}
}
});
}
if (connections.length > 0) {
}
return connections;
}
const drawnStations = new Map();
allSegments.forEach((segment, segmentIndex) => {
if (segment.type === 'branch_points') {
return;
}
const validStations = segment.stations.filter(s => s.gps_lat && s.gps_long);
if (validStations.length === 0) {
return;
}
const routePoints = [];
let detailedCoordinates = null;
if (segment.coordinates) {
detailedCoordinates = segment.coordinates;
} else if (segment.type === 'main' && fiber.cable_info && fiber.cable_info.coordinates) {
detailedCoordinates = fiber.cable_info.coordinates;
} else if (segment.cable && segment.cable.coordinates) {
detailedCoordinates = segment.cable.coordinates;
} else if (segment.cable_info && segment.cable_info.coordinates) {
detailedCoordinates = segment.cable_info.coordinates;
}
if (detailedCoordinates && typeof detailedCoordinates === 'string') {
try {
detailedCoordinates = JSON.parse(detailedCoordinates);
} catch (e) {
detailedCoordinates = null;
}
}
validStations.forEach((station, stationIndex) => {
const lat = parseFloat(station.gps_lat);
const lng = parseFloat(station.gps_long);
routePoints.push([lat, lng]);
const branchConnections = getBranchConnectionsAtStation(station.name, segment, allSegments);
const isBranchJunction = branchConnections.length > 0;
let iconColor = '#3388ff';
let iconName = 'circle';
let iconSize = 's';
if (station.type === 'pop') {
iconColor = '#acf0ab';
iconName = 'village';
iconSize = 'l';
} else if (station.type === 'dispatcher') {
if (isBranchJunction) {
iconColor = '#FF6B6B';
iconName = 'home';
iconSize = 'm';
} else if (station.object_type == 2 || station.object_type === '2') {
iconColor = '#ffa726';
iconName = 'home';
iconSize = 'm';
} else {
iconColor = '#abbaf0';
iconName = 'home';
iconSize = 'm';
}
}
let branchConnectionsHtml = '';
if (isBranchJunction) {
branchConnectionsHtml = '<hr style="margin: 8px 0;"><div style=" border: 1px solid #d1dcff; padding: 8px; border-radius: 4px; margin-top: 8px;">';
branchConnectionsHtml += '<h6 style="margin: 0 0 8px 0; color: #000000;"><i class="fa fa-code-branch" style="color:#fff"></i> Faser-Übergänge:</h6>';
branchConnections.forEach((conn, idx) => {
const junctionType = conn.type === 'branch_from_main' ? 'Hauptkabel → Branch' : 'Branch → Sub-Branch';
const fromFiberColorName = conn.fromFiberColor || (conn.fromFiberColorHex ? hexToColorName(conn.fromFiberColorHex) : '');
const fromBundleColorName = conn.fromBundleColor || (conn.fromBundleColorHex ? hexToColorName(conn.fromBundleColorHex) : '');
const toFiberColorName = conn.toFiberColor || (conn.toFiberColorHex ? hexToColorName(conn.toFiberColorHex) : '');
const toBundleColorName = conn.toBundleColor || (conn.toBundleColorHex ? hexToColorName(conn.toBundleColorHex) : '');
branchConnectionsHtml += `
<div style="background: white; padding: 6px; margin-bottom: 6px; border-left: 3px solid #FF6B6B; font-size: 10px;">
<strong style="color: #000000;">${junctionType} (Level ${conn.level})</strong><br>
<div style="margin-top: 4px;">
<strong>Von:</strong> ${conn.fromCable}<br>
<span style="padding-left: 10px;">└─ Faser ${conn.fromFiber || '?'}</span>
${conn.fromFiberColorHex ? ` <span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${conn.fromFiberColorHex}; border: 1px solid #666;"></span> ${fromFiberColorName}` : ''}<br>
${conn.fromBundle ? `<span style="padding-left: 10px;">└─ Bündel ${conn.fromBundle}</span>` : ''}
${conn.fromBundleColorHex ? ` <span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${conn.fromBundleColorHex}; border: 1px solid #666;"></span> ${fromBundleColorName}` : ''}
${conn.fromBundle ? '<br>' : ''}
</div>
<div style="margin-top: 4px; padding-top: 4px; border-top: 1px dashed #ddd;">
<strong>Nach:</strong> ${conn.toCable}<br>
<span style="padding-left: 10px;">└─ Faser ${conn.toFiber || '?'}</span>
${conn.toFiberColorHex ? ` <span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${conn.toFiberColorHex}; border: 1px solid #666;"></span> ${toFiberColorName}` : ''}<br>
${conn.toBundle ? `<span style="padding-left: 10px;">└─ Bündel ${conn.toBundle}</span>` : ''}
${conn.toBundleColorHex ? ` <span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${conn.toBundleColorHex}; border: 1px solid #666;"></span> ${toBundleColorName}` : ''}
${conn.toBundle ? '<br>' : ''}
</div>
</div>
`;
});
branchConnectionsHtml += '</div>';
}
const popupContent = `
<div style="min-width: 250px;">
<h6><strong>${station.name}</strong></h6>
<span class="badge badge-${station.type === 'pop' ? 'success' : (isBranchJunction ? 'danger' : 'info')}">
${station.type === 'pop' ? 'POP' : (isBranchJunction ? 'Branch-Verbindung' : (station.object_type == 2 || station.object_type === '2' ? 'Schacht' : 'Verteiler'))}
</span>
${isBranchJunction ? '<span class="badge badge-warning ml-1">' + branchConnections.length + ' Übergänge</span>' : ''}
<hr style="margin: 8px 0;">
<div style="font-size: 11px;">
<strong>Kabel:</strong> ${segment.name}<br>
${segment.fiber_nr ? `<strong>Faser:</strong> ${segment.fiber_nr}` : ''}
${segment.fiber_color ? ` (${segment.fiber_color})` : ''}<br>
${segment.bundle_nr ? `<strong>Bündel:</strong> ${segment.bundle_nr}` : ''}
${segment.bundle_color ? ` (${segment.bundle_color})` : ''}<br>
<strong>Level:</strong> ${segment.level}<br>
<small class="text-muted">${lat.toFixed(5)}, ${lng.toFixed(5)}</small>
</div>
${branchConnectionsHtml}
</div>
`;
const marker = L.marker([lat, lng], {
icon: L.MakiMarkers.icon({
icon: iconName,
color: iconColor,
size: iconSize
})
}).bindPopup(popupContent);
if (station.type === 'dispatcher') {
const tooltip = marker.bindTooltip(station.name, {
permanent: true,
direction: 'top',
className: 'dispatcher-label',
offset: [0, -25]
});
marker.stationTooltip = tooltip;
marker.stationName = station.name;
}
const stationKey = `${station.name}_${lat.toFixed(6)}_${lng.toFixed(6)}`;
const existingStation = drawnStations.get(stationKey);
if (existingStation) {
if (isBranchJunction && !existingStation.isBranchJunction) {
allBounds.removeLayer(existingStation.marker);
allBounds.addLayer(marker);
drawnStations.set(stationKey, { marker, isBranchJunction });
} else {
}
} else {
allBounds.addLayer(marker);
drawnStations.set(stationKey, { marker, isBranchJunction });
}
if (station.type === 'dispatcher' && station.object_type == 4) {
if (!window.allBranchPoints) {
window.allBranchPoints = [];
}
window.allBranchPoints.push({
lat: lat,
lng: lng,
name: station.name,
segment: segment,
station: station
});
}
});
const lineWeight = segment.type === 'main' ? 6 : 5;
const lineOpacity = 0.9;
const borderWeight = lineWeight + 4;
let coords = routePoints;
if (detailedCoordinates && Array.isArray(detailedCoordinates) && detailedCoordinates.length > 0) {
coords = detailedCoordinates.map(coord => [
parseFloat(coord.gps_lat),
parseFloat(coord.gps_long)
]);
} else {
}
if (coords.length > 100) {
const firstCoord = coords[0];
const lastCoord = coords[coords.length - 1];
const distStartEnd = Math.sqrt(
Math.pow(firstCoord[0] - lastCoord[0], 2) +
Math.pow(firstCoord[1] - lastCoord[1], 2)
);
if (distStartEnd < 0.001) {
let maxDist = 0;
let turningPointIndex = 0;
for (let i = 0; i < coords.length; i++) {
const d = Math.sqrt(
Math.pow(coords[i][0] - firstCoord[0], 2) +
Math.pow(coords[i][1] - firstCoord[1], 2)
);
if (d > maxDist) {
maxDist = d;
turningPointIndex = i;
}
}
if (turningPointIndex > 10 && turningPointIndex < coords.length - 10) {
coords = coords.slice(0, turningPointIndex + 1);
} else {
}
} else {
}
}
if (coords.length >= 2 && validStations.length >= 2) {
const firstCoord = coords[0];
const lastCoord = coords[coords.length - 1];
const popStation = validStations.find(s => s.type === 'pop') || validStations[0];
const popLat = parseFloat(popStation.gps_lat);
const popLng = parseFloat(popStation.gps_long);
const distFirstToPop = Math.sqrt(
Math.pow(firstCoord[0] - popLat, 2) +
Math.pow(firstCoord[1] - popLng, 2)
);
const distLastToPop = Math.sqrt(
Math.pow(lastCoord[0] - popLat, 2) +
Math.pow(lastCoord[1] - popLng, 2)
);
if (distLastToPop < distFirstToPop) {
coords.reverse();
} else {
}
}
if (validStations.length > 0 && coords.length > 0) {
const firstCoord = coords[0];
const firstStation = validStations[0];
const distToPop = Math.sqrt(
Math.pow(firstCoord[0] - parseFloat(firstStation.gps_lat), 2) +
Math.pow(firstCoord[1] - parseFloat(firstStation.gps_long), 2)
);
if (distToPop > 0.001) {
let closestStationIndex = -1;
let minDist = Infinity;
for (let i = 0; i < validStations.length; i++) {
const station = validStations[i];
const dist = Math.sqrt(
Math.pow(firstCoord[0] - parseFloat(station.gps_lat), 2) +
Math.pow(firstCoord[1] - parseFloat(station.gps_long), 2)
);
if (dist < minDist) {
minDist = dist;
closestStationIndex = i;
}
}
const missingCoords = [];
for (let i = 0; i < closestStationIndex; i++) {
const station = validStations[i];
missingCoords.push([
parseFloat(station.gps_lat),
parseFloat(station.gps_long)
]);
}
if (missingCoords.length > 0) {
coords = [...missingCoords, ...coords];
}
} else {
}
}
for (let i = 0; i < Math.min(3, coords.length); i++) {
const coord = coords[i];
}
for (let i = 0; i < Math.min(3, validStations.length); i++) {
const station = validStations[i];
}
if (coords.length > 52) {
for (let i = 50; i <= Math.min(54, coords.length - 1); i++) {
const coord = coords[i];
}
}
const dropoutLocationName = segment.location || fiber.location;
const dropoutStation = validStations.find(s => dropoutLocationName && s.name === dropoutLocationName);
if (dropoutStation) {
}
for (let i = Math.max(0, coords.length - 3); i < coords.length; i++) {
const coord = coords[i];
}
for (let i = Math.max(0, validStations.length - 3); i < validStations.length; i++) {
const station = validStations[i];
}
let tooltipText = `<div style="text-align: left;">
<strong>${segment.name}</strong><br>
${segment.type === 'branch' ? `<small>(Abzweig Ebene ${segment.level})</small><br>` : ''}
${segment.fiber_nr ? `<span style="display: inline-block; width: 12px; height: 12px; border-radius: 50%; background-color: ${segment.color}; border: 1px solid #fff; margin-right: 4px; vertical-align: middle;"></span>Faser: ${segment.fiber_nr}` : ''}
${segment.fiber_color ? ` (${segment.fiber_color})` : ''}<br>
${segment.bundle_nr ? `<span style="display: inline-block; width: 12px; height: 12px; border-radius: 50%; background-color: ${segment.bundle_color_hex || '#ccc'}; border: 1px solid #fff; margin-right: 4px; vertical-align: middle;"></span>Bündel: ${segment.bundle_nr}` : ''}
${segment.bundle_color ? ` (${segment.bundle_color})` : ''}
${segment.location ? `<br><strong>Dropout:</strong> ${segment.location}` : ''}
</div>`;
let shouldSplit = false;
let splitIndex = -1;
let splitPointName = '';
const targetFiber = segment.target_fiber;
const segmentFiber = segment.cable_info || segment.cable || {};
const hasCustomer = fiber.home_id ||
targetFiber?.home_id ||
segmentFiber.home_id ||
fiber.address ||
targetFiber?.address ||
segmentFiber.address ||
fiber.customer_gps ||
targetFiber?.customer_gps ||
segmentFiber.customer_gps;
const hasBranchDropout = segment.type === 'main' && segment.branch_dropouts && segment.branch_dropouts.length > 0;
if (validStations.length > 0 && (hasCustomer || hasBranchDropout)) {
const effectiveFiber = {
location: targetFiber?.location || targetFiber?.branch_from_location || segmentFiber.location || fiber.location,
branch_from_location: targetFiber?.branch_from_location || segmentFiber.branch_from_location,
branch_to_location: targetFiber?.branch_to_location || segmentFiber.branch_to_location,
home_id: targetFiber?.home_id || segmentFiber.home_id || fiber.home_id,
address: targetFiber?.address || segmentFiber.address || fiber.address,
customer_gps: targetFiber?.customer_gps || segmentFiber.customer_gps || fiber.customer_gps,
name: targetFiber?.name || segmentFiber.name || fiber.name,
customer_cable_type: targetFiber?.customer_cable_type || segmentFiber.customer_cable_type || fiber.customer_cable_type,
customer_connector_type: targetFiber?.customer_connector_type || segmentFiber.customer_connector_type || fiber.customer_connector_type,
customer_cable_spec: targetFiber?.customer_cable_spec || segmentFiber.customer_cable_spec || fiber.customer_cable_spec,
customer_fiber_range: targetFiber?.customer_fiber_range || segmentFiber.customer_fiber_range || fiber.customer_fiber_range
};
let dropoutLat = null;
let dropoutLng = null;
let dropoutName = '';
if (effectiveFiber.location) {
const dropoutLocationName = effectiveFiber.location;
let dropoutStation = validStations.find(s => s.name === dropoutLocationName);
if (!dropoutStation) {
for (let seg of allSegments) {
if (seg.type === 'branch_points') continue;
const foundStation = seg.stations?.find(s => s.name === dropoutLocationName);
if (foundStation && foundStation.gps_lat && foundStation.gps_long) {
dropoutStation = foundStation;
break;
}
}
}
if (dropoutStation) {
dropoutLat = parseFloat(dropoutStation.gps_lat);
dropoutLng = parseFloat(dropoutStation.gps_long);
dropoutName = dropoutStation.name;
// console.log("✅ DROPOUT GEFUNDEN:", dropoutStation.name);
// console.log(" GPS:", dropoutLat, dropoutLng);
} else {
// console.log("❌ DROPOUT NICHT GEFUNDEN!");
}
}
if (dropoutLat === null && segment.type === 'main' && segment.branch_dropouts && segment.branch_dropouts.length > 0) {
const firstBranchDropout = segment.branch_dropouts[0];
const branchDropoutStation = validStations.find(s => s.name === firstBranchDropout);
if (branchDropoutStation) {
dropoutLat = parseFloat(branchDropoutStation.gps_lat);
dropoutLng = parseFloat(branchDropoutStation.gps_long);
dropoutName = branchDropoutStation.name;
}
}
if (dropoutLat === null && validStations.length > 0) {
const lastStation = validStations[validStations.length - 1];
dropoutLat = parseFloat(lastStation.gps_lat);
dropoutLng = parseFloat(lastStation.gps_long);
dropoutName = lastStation.name;
}
if (dropoutLat !== null && dropoutLng !== null) {
let dropoutIndex = -1;
let minDist = Infinity;
for (let i = 0; i < coords.length; i++) {
const dist = Math.sqrt(
Math.pow(coords[i][0] - dropoutLat, 2) +
Math.pow(coords[i][1] - dropoutLng, 2)
);
if (dist < minDist) {
minDist = dist;
dropoutIndex = i;
}
}
splitIndex = dropoutIndex;
splitPointName = dropoutName;
if (splitIndex >= 0 && splitIndex < coords.length - 1) {
shouldSplit = true;
}
}
}
if (shouldSplit && segment.type === 'branch' && splitIndex <= 1) {
// console.log("⊘ Branch-Segment: Split am Anfang (Index <= 1) - KEIN Split nötig");
shouldSplit = false;
}
if (shouldSplit) {
const coords2 = coords.slice(splitIndex);
L.polyline(coords2, {
color: '#666666',
weight: borderWeight,
opacity: 0.3,
dashArray: '5, 10'
}).addTo(map);
L.polyline(coords2, {
color: '#999999',
weight: lineWeight,
opacity: 0.3,
dashArray: '5, 10'
}).addTo(map).bindTooltip(`${segment.name}<br><span class="badge badge-secondary">Inaktiv ab ${splitPointName}</span>`, {
permanent: false,
sticky: true,
direction: 'top'
});
const coords1 = coords.slice(0, splitIndex + 1);
let branchDashArray = null;
if (segment.type === 'branch') {
if (segment.branch_level === 1) {
branchDashArray = '15, 10';
} else if (segment.branch_level === 2) {
branchDashArray = '20, 15';
} else {
branchDashArray = '25, 20';
}
}
L.polyline(coords1, {
color: '#333333',
weight: borderWeight,
opacity: 0.8,
dashArray: branchDashArray
}).addTo(map);
const hasDetailedStart = coords.length > 100;
const dashPattern = hasDetailedStart ? null : '10, 5';
L.polyline(coords1, {
color: segment.color,
weight: lineWeight,
opacity: lineOpacity,
dashArray: dashPattern,
zIndex: 1000
}).addTo(map).bindTooltip(tooltipText + '<br><span class="badge badge-success">Aktiv bis ' + splitPointName + '</span>' +
(dashPattern ? '<br><small>Vereinfachte Strecke (nur Stationspunkte)</small>' : ''), {
permanent: false,
sticky: true,
direction: 'top'
});
if (validStations.length >= 2 && coords1.length > 2) {
const stationsBeforeSplit = [];
for (let station of validStations) {
stationsBeforeSplit.push(station);
if (station.name === splitPointName) {
break;
}
}
for (let i = 0; i < stationsBeforeSplit.length - 1; i++) {
const station1 = stationsBeforeSplit[i];
const station2 = stationsBeforeSplit[i + 1];
const lat1 = parseFloat(station1.gps_lat);
const lng1 = parseFloat(station1.gps_long);
const lat2 = parseFloat(station2.gps_lat);
const lng2 = parseFloat(station2.gps_long);
let startIdx = 0;
let minDist1 = Infinity;
for (let j = 0; j < coords1.length; j++) {
const dist = Math.sqrt(
Math.pow(coords1[j][0] - lat1, 2) +
Math.pow(coords1[j][1] - lng1, 2)
);
if (dist < minDist1) {
minDist1 = dist;
startIdx = j;
}
}
let endIdx = coords1.length - 1;
let minDist2 = Infinity;
for (let j = startIdx; j < coords1.length; j++) {
const dist = Math.sqrt(
Math.pow(coords1[j][0] - lat2, 2) +
Math.pow(coords1[j][1] - lng2, 2)
);
if (dist < minDist2) {
minDist2 = dist;
endIdx = j;
}
}
const segmentCoords = coords1.slice(startIdx, endIdx + 1);
if (segmentCoords.length > 0) {
const midIndex = Math.floor(segmentCoords.length / 2);
const midCoord = segmentCoords[midIndex];
const midLat = midCoord[0];
const midLng = midCoord[1];
const cableLabelMarker = L.marker([midLat, midLng], {
icon: L.divIcon({
className: 'cable-label-marker',
html: '',
iconSize: [0, 0]
})
}).addTo(map);
const labelText = `${segment.name}`;
cableLabelMarker.bindTooltip(labelText, {
permanent: true,
direction: 'center',
className: 'cable-segment-label',
offset: [0, 0]
});
cableLabelMarker.cableLabel = true;
cableLabelMarker.cableLabelText = labelText;
allBounds.addLayer(cableLabelMarker);
}
}
}
if (validStations.length > 0) {
const popStation = validStations.find(s => s.type === 'pop') || validStations[0];
const firstDetailedCoord = coords1[2];
if (firstDetailedCoord) {
const popLat = parseFloat(popStation.gps_lat);
const popLng = parseFloat(popStation.gps_long);
const distToPop = Math.sqrt(
Math.pow(firstDetailedCoord[0] - popLat, 2) +
Math.pow(firstDetailedCoord[1] - popLng, 2)
);
if (distToPop > 0.001) {
L.polyline([[popLat, popLng], firstDetailedCoord], {
color: '#FF8C00',
weight: lineWeight,
opacity: 0.6,
dashArray: '15, 10',
zIndex: 999
}).addTo(map).bindTooltip(`${segment.name}<br><span class="badge badge-warning">Vereinfachte Strecke (keine GPS-Daten)</span>`, {
permanent: false,
sticky: true,
direction: 'top'
});
}
}
}
} else {
let branchDashArray = null;
if (segment.type === 'branch') {
if (segment.level === 1) {
branchDashArray = '20, 8';
} else if (segment.level === 2) {
branchDashArray = '10, 10';
} else {
branchDashArray = '25, 20';
}
}
const borderPolyline = L.polyline(coords, {
color: '#333333',
weight: borderWeight,
opacity: 0.8,
dashArray: branchDashArray
}).addTo(map);
const polyline = L.polyline(coords, {
color: segment.color,
weight: lineWeight,
opacity: lineOpacity,
dashArray: branchDashArray
}).addTo(map);
polyline.bindTooltip(tooltipText, {
permanent: false,
sticky: true,
direction: 'top'
});
if (validStations.length >= 2 && coords.length > 2) {
for (let i = 0; i < validStations.length - 1; i++) {
const station1 = validStations[i];
const station2 = validStations[i + 1];
const lat1 = parseFloat(station1.gps_lat);
const lng1 = parseFloat(station1.gps_long);
const lat2 = parseFloat(station2.gps_lat);
const lng2 = parseFloat(station2.gps_long);
let segmentCoords = [];
let startIdx = 0;
let minDist1 = Infinity;
for (let j = 0; j < coords.length; j++) {
const dist = Math.sqrt(
Math.pow(coords[j][0] - lat1, 2) +
Math.pow(coords[j][1] - lng1, 2)
);
if (dist < minDist1) {
minDist1 = dist;
startIdx = j;
}
}
let endIdx = coords.length - 1;
let minDist2 = Infinity;
for (let j = startIdx; j < coords.length; j++) {
const dist = Math.sqrt(
Math.pow(coords[j][0] - lat2, 2) +
Math.pow(coords[j][1] - lng2, 2)
);
if (dist < minDist2) {
minDist2 = dist;
endIdx = j;
}
}
segmentCoords = coords.slice(startIdx, endIdx + 1);
if (segmentCoords.length > 0) {
const midIndex = Math.floor(segmentCoords.length / 2);
const midCoord = segmentCoords[midIndex];
const midLat = midCoord[0];
const midLng = midCoord[1];
const cableLabelMarker = L.marker([midLat, midLng], {
icon: L.divIcon({
className: 'cable-label-marker',
html: '',
iconSize: [0, 0]
})
}).addTo(map);
const labelText = `${segment.name}`;
cableLabelMarker.bindTooltip(labelText, {
permanent: true,
direction: 'center',
className: 'cable-segment-label',
offset: [0, 0]
});
cableLabelMarker.cableLabel = true;
cableLabelMarker.cableLabelText = labelText;
allBounds.addLayer(cableLabelMarker);
}
}
}
}
if (validStations.length > 0) {
const targetFiber = segment.target_fiber;
const segmentFiber = segment.cable_info || segment.cable || {};
const effectiveFiber = {
location: targetFiber?.location || targetFiber?.branch_from_location || segmentFiber.location || fiber.location,
home_id: targetFiber?.home_id || segmentFiber.home_id || fiber.home_id,
address: targetFiber?.address || segmentFiber.address || fiber.address,
customer_gps: targetFiber?.customer_gps || segmentFiber.customer_gps || fiber.customer_gps,
name: targetFiber?.name || segmentFiber.name || fiber.name,
customer_cable_type: targetFiber?.customer_cable_type || segmentFiber.customer_cable_type || fiber.customer_cable_type,
customer_connector_type: targetFiber?.customer_connector_type || segmentFiber.customer_connector_type || fiber.customer_connector_type,
customer_cable_spec: targetFiber?.customer_cable_spec || segmentFiber.customer_cable_spec || fiber.customer_cable_spec,
customer_fiber_range: targetFiber?.customer_fiber_range || segmentFiber.customer_fiber_range || fiber.customer_fiber_range
};
const dropoutLocationName = effectiveFiber.location;
let dropoutStation = validStations.find(s => dropoutLocationName && s.name === dropoutLocationName);
const isLastBranch = segment.type === 'branch' && (
segment.level === Math.max(...allSegments.filter(s => s.type === 'branch').map(s => s.level))
);
if (isLastBranch) {
// console.log(` ⚡ LETZTES Branch-Segment: Verwende LETZTE Station als Dropout für Hausanschluss`);
if (validStations.length > 0) {
dropoutStation = validStations[validStations.length - 1];
// console.log(` ✓ Letzte Station: "${dropoutStation.name}"`);
}
} else if (segment.type === 'branch') {
if (!dropoutStation && validStations.length > 0) {
dropoutStation = validStations[validStations.length - 1];
}
} else {
if (!dropoutStation && splitIndex >= 0 && coords[splitIndex]) {
const splitCoord = coords[splitIndex];
let closestDist = Infinity;
for (let station of validStations) {
const stationLat = parseFloat(station.gps_lat);
const stationLng = parseFloat(station.gps_long);
const dist = Math.sqrt(
Math.pow(splitCoord[0] - stationLat, 2) +
Math.pow(splitCoord[1] - stationLng, 2)
);
if (dist < closestDist) {
closestDist = dist;
dropoutStation = station;
}
}
}
if (!dropoutStation && validStations.length > 0) {
dropoutStation = validStations[validStations.length - 1];
}
}
const dropoutLat = parseFloat(dropoutStation.gps_lat);
const dropoutLng = parseFloat(dropoutStation.gps_long);
let customerGps = null;
let customerInfo = null;
if (effectiveFiber.customer_gps && effectiveFiber.address) {
customerGps = effectiveFiber.customer_gps;
customerInfo = {
address: effectiveFiber.address,
home_id: effectiveFiber.home_id,
name: effectiveFiber.name,
customer_cable_type: effectiveFiber.customer_cable_type,
customer_connector_type: effectiveFiber.customer_connector_type,
customer_cable_spec: effectiveFiber.customer_cable_spec,
customer_fiber_range: effectiveFiber.customer_fiber_range
};
} else if (effectiveFiber.address && effectiveFiber.home_id) {
customerInfo = {
address: effectiveFiber.address,
home_id: effectiveFiber.home_id,
name: effectiveFiber.name,
customer_cable_type: effectiveFiber.customer_cable_type,
customer_connector_type: effectiveFiber.customer_connector_type,
customer_cable_spec: effectiveFiber.customer_cable_spec,
customer_fiber_range: effectiveFiber.customer_fiber_range
};
customerGps = { lat: dropoutLat, lng: dropoutLng, fallback: true };
}
if (customerInfo && customerGps) {
const customerLat = parseFloat(customerGps.lat);
const customerLng = parseFloat(customerGps.lng);
const isFallback = customerGps.fallback || false;
let connectionPoint = {
lat: dropoutLat,
lng: dropoutLng,
name: dropoutStation.name,
type: 'dispatcher'
};
if (!isFallback) {
const nearestBranchPoint = findNearestBranchPoint(customerLat, customerLng, allSegments);
if (nearestBranchPoint) {
const distanceToPolyline = getDistanceToPolyline(nearestBranchPoint.lat, nearestBranchPoint.lng, coords);
if (distanceToPolyline <= 0.0001) {
connectionPoint = nearestBranchPoint;
}
}
}
if (!isFallback && (customerLat !== connectionPoint.lat || customerLng !== connectionPoint.lng)) {
const cableMatch = findCableForCustomerConnection(
customerLat,
customerLng,
window.allCables || []
);
if (cableMatch) {
// console.log('Echtes Kabel für Hausanschluss gefunden:', cableMatch.cable.description);
let cableCoords = cableMatch.cable.coordinates;
if (cableMatch.reverse) {
cableCoords = [...cableCoords].reverse();
}
const latLngs = cableCoords.map(c => [c.gps_lat, c.gps_long]);
L.polyline(latLngs, {
color: '#333333',
weight: 7,
opacity: 0.7,
dashArray: '5, 10'
}).addTo(map);
const customerCableLine = L.polyline(latLngs, {
color: '#FFA500',
weight: 4,
opacity: 0.9,
dashArray: '5, 10',
zIndex: 2000
}).addTo(map).bindTooltip(`Hausanschluss von ${connectionPoint.name}<br>Kabel: ${cableMatch.cable.description}`, {
permanent: false,
sticky: true,
direction: 'top'
});
allBounds.addLayer(customerCableLine);
if (latLngs.length > 0) {
const midIndex = Math.floor(latLngs.length / 2);
const midPoint = latLngs[midIndex];
const customerCableLabelMarker = L.marker(midPoint, {
icon: L.divIcon({
className: 'cable-label-marker',
html: '',
iconSize: [0, 0]
})
}).addTo(map);
const customerCableLabelText = cableMatch.cable.description || 'Hausanschluss';
customerCableLabelMarker.bindTooltip(customerCableLabelText, {
permanent: true,
direction: 'center',
className: 'customer-cable-label',
offset: [0, 0]
});
customerCableLabelMarker.cableLabel = true;
customerCableLabelMarker.cableLabelText = customerCableLabelText;
allBounds.addLayer(customerCableLabelMarker);
}
if (window.allBranchPoints && window.allBranchPoints.length > 0) {
let drawnCount = 0;
window.allBranchPoints.forEach(branchPoint => {
const pointLat = branchPoint.lat;
const pointLng = branchPoint.lng;
let isOnCustomerCable = false;
for (let coord of cableCoords) {
const dist = Math.sqrt(
Math.pow(coord.gps_lat - pointLat, 2) +
Math.pow(coord.gps_long - pointLng, 2)
);
if (dist < 0.00001) {
isOnCustomerCable = true;
break;
}
}
if (isOnCustomerCable) {
const branchRing = L.circleMarker([pointLat, pointLng], {
radius: 8,
fillColor: '#FFD700',
fillOpacity: 0.9,
color: '#FF6B6B',
weight: 3,
opacity: 0.8,
zIndex: 2001
}).bindPopup(`
<div style="min-width: 200px;">
<h6><i class="fa fa-code-branch"></i> <strong>Abzweigpunkt</strong></h6>
<strong>${branchPoint.name}</strong><br>
<span class="badge badge-warning">Auf Kundenstrecke</span>
<hr style="margin: 8px 0;">
<div style="font-size: 11px;">
<strong>Kundenkabel:</strong> ${cableMatch.cable.description}<br>
<strong>Kunde:</strong> ${customerInfo.address || 'Hausanschluss'}<br>
<small class="text-muted">${pointLat.toFixed(5)}, ${pointLng.toFixed(5)}</small>
</div>
</div>
`).addTo(map);
allBounds.addLayer(branchRing);
drawnCount++;
}
});
}
} else {
console.warn('Kein Kabel für Hausanschluss gefunden, nutze direkte Linie:', customerInfo.address);
L.polyline([[connectionPoint.lat, connectionPoint.lng], [customerLat, customerLng]], {
color: '#333333', weight: 7, opacity: 0.7, dashArray: '5, 10'
}).addTo(map);
L.polyline([[connectionPoint.lat, connectionPoint.lng], [customerLat, customerLng]], {
color: '#FFA500',
weight: 4,
opacity: 0.9,
dashArray: '5, 10',
zIndex: 2000
}).addTo(map).bindTooltip(`Hausanschluss von ${connectionPoint.name}`, {
permanent: false, sticky: true, direction: 'top'
});
const midLat = (connectionPoint.lat + customerLat) / 2;
const midLng = (connectionPoint.lng + customerLng) / 2;
const fallbackLabelMarker = L.marker([midLat, midLng], {
icon: L.divIcon({
className: 'cable-label-marker',
html: '',
iconSize: [0, 0]
})
}).addTo(map);
fallbackLabelMarker.bindTooltip('Hausanschluss', {
permanent: true,
direction: 'center',
className: 'customer-cable-label',
offset: [0, 0]
});
fallbackLabelMarker.cableLabel = true;
fallbackLabelMarker.cableLabelText = 'Hausanschluss';
allBounds.addLayer(fallbackLabelMarker);
const connectionMarker = L.circleMarker([connectionPoint.lat, connectionPoint.lng], {
radius: 8, fillColor: '#FFD700', fillOpacity: 0.9, color: '#FF6B6B', weight: 3, opacity: 1
}).bindPopup(`
<div style="min-width: 200px;">
<h6><i class="fa fa-plug"></i> <strong>Verbindungspunkt</strong></h6>
<strong>${connectionPoint.name}</strong><br>
<span class="badge badge-${connectionPoint.type === 'branch_point' ? 'warning' : 'info'}">
${connectionPoint.type === 'branch_point' ? 'Abzweigpunkt' : 'Dispatcher'}
</span>
<hr style="margin: 8px 0;">
<div style="font-size: 11px;">
<strong>Verbunden mit:</strong><br>
${customerInfo.address || 'Hausanschluss'}<br>
${customerInfo.home_id ? `<strong>Home-ID:</strong> ${customerInfo.home_id}` : ''}
</div>
</div>
`).addTo(map);
allBounds.addLayer(connectionMarker);
}
}
const connectionPointType = connectionPoint.type === 'branch_point' ? 'Abzweigpunkt' : 'Verteiler (Dropout)';
const customerPopup = `
<div style="min-width: 250px;">
<h6><i class="fa fa-home"></i> <strong>Kunden-Anschluss</strong></h6>
${isFallback ? '<div class="alert alert-warning py-1 px-2 mb-2" style="font-size: 10px;"><i class="fa fa-exclamation-triangle"></i> Exakte GPS-Position nicht verfügbar</div>' : ''}
<hr style="margin: 8px 0;">
<div style="font-size: 11px;">
<strong>Verbindungspunkt:</strong> ${connectionPoint.name}<br>
<small class="text-muted">(${connectionPointType})</small><br>
<strong>Adresse:</strong> ${customerInfo.address}<br>
${customerInfo.home_id ? `<strong>Home-ID:</strong> ${customerInfo.home_id}<br>` : ''}
${customerInfo.name ? `<strong>Name:</strong> ${customerInfo.name}<br>` : ''}
<hr style="margin: 8px 0;">
<strong>Kabel:</strong> ${segment.name}<br>
Faser: ${segment.fiber_nr} ${segment.fiber_color ? `(${segment.fiber_color})` : ''}
</div>
</div>
`;
const customerMarker = L.marker([customerLat, customerLng], {
icon: L.MakiMarkers.icon({ icon: 'home', color: isFallback ? '#ffd700' : '#f0abab', size: 'm' })
}).bindPopup(customerPopup);
allBounds.addLayer(customerMarker);
} else {
}
}
});
allBounds.addTo(map);
if (allBounds.getLayers().length > 0) {
map.fitBounds(allBounds.getBounds(), { padding: [50, 50] });
}
const MIN_ZOOM_FOR_STATION_LABELS = 15;
const MIN_ZOOM_FOR_CABLE_LABELS = 15;
function updateTooltipVisibility() {
const currentZoom = map.getZoom();
const shouldShowStations = currentZoom >= MIN_ZOOM_FOR_STATION_LABELS;
const shouldShowCables = currentZoom >= MIN_ZOOM_FOR_CABLE_LABELS;
map.eachLayer(function (layer) {
if (layer.stationTooltip && layer.stationName) {
if (shouldShowStations) {
if (!layer.getTooltip()) {
layer.bindTooltip(layer.stationName, {
permanent: true,
direction: 'top',
className: 'dispatcher-label',
offset: [0, -50]
});
}
} else {
layer.unbindTooltip();
}
}
if (layer.cableLabel && layer.cableLabelText) {
if (shouldShowCables) {
if (!layer.getTooltip()) {
const labelClass = layer.cableLabelText.includes('Hausanschluss') ||
layer.options.icon?.options?.className?.includes('customer')
? 'customer-cable-label'
: 'cable-segment-label';
layer.bindTooltip(layer.cableLabelText, {
permanent: true,
direction: 'center',
className: labelClass,
offset: [0, 0]
});
}
} else {
layer.unbindTooltip();
}
}
});
}
updateTooltipVisibility();
map.on('zoomend', updateTooltipVisibility);
const legend = L.control({ position: 'bottomright' });
legend.onAdd = function (map) {
const div = L.DomUtil.create('div', 'info legend');
div.style.backgroundColor = 'white';
div.style.padding = '10px';
div.style.border = '2px solid #ccc';
div.style.borderRadius = '5px';
div.style.fontSize = '12px';
div.style.maxWidth = '250px';
let html = '<strong>Legende</strong><br>';
html += '<div style="margin: 8px 0;">';
html += '<i class="fa fa-circle" style="color: #acf0ab; margin-right: 5px;"></i> POP<br>';
html += '<i class="fa fa-circle" style="color: #abbaf0; margin-right: 5px;"></i> Verteiler<br>';
html += '<i class="fa fa-circle" style="color: #ffa726; margin-right: 5px;"></i> Schacht<br>';
html += '<i class="fa fa-circle" style="color: #f0abab; margin-right: 5px;"></i> Kunden-Haus<br>';
html += '</div>';
html += '<hr style="margin: 8px 0;">';
html += '<strong>Kabel & Fasern:</strong><br>';
allSegments
.filter(seg => seg.type !== 'branch_points')
.forEach((segment, idx) => {
const lineStyle = segment.type === 'branch' ? 'dashed' : 'solid';
const borderStyle = segment.type === 'branch' ? '3px dashed' : '3px solid';
const fiberColorName = segment.fiber_color || (segment.color ? hexToColorName(segment.color) : '');
const bundleColorName = segment.bundle_color || (segment.bundle_color_hex ? hexToColorName(segment.bundle_color_hex) : '');
html += `<div style="margin: 4px 0; display: flex; align-items: center;">
<i style="background: ${segment.color}; width: 20px; height: 3px; display: inline-block; margin-right: 8px; border-top: ${borderStyle} ${segment.color};"></i>
<span style="font-size: 10px; line-height: 1.4;">
<strong>${segment.name}</strong>
${segment.fiber_nr ? `<br><span style="margin-left: 25px;">└─ Faser ${segment.fiber_nr}` : ''}
${fiberColorName ? ` <span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${segment.color}; border: 1px solid #666;"></span> (${fiberColorName})` : ''}
${segment.bundle_nr ? `<br><span style="margin-left: 25px;">└─ Bündel ${segment.bundle_nr}` : ''}
${segment.bundle_color_hex ? ` <span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${segment.bundle_color_hex}; border: 1px solid #666;"></span>` : ''}
${bundleColorName ? ` (${bundleColorName})` : ''}
</span>
</div>`;
});
div.innerHTML = html;
return div;
};
legend.addTo(map);
if (isLinework) {
renderFiberPathSchemaLinework(fiber, allSegments);
} else {
renderFiberPathSchema(fiber, allSegments);
}
}, 300);
}
function renderFiberPathSchema(fiber, allSegments) {
const schemaContainer = $('#fiber-route-schema');
if (!fiber || allSegments.length === 0) {
schemaContainer.html('<div class="text-center text-muted">Keine Daten verfügbar</div>');
return;
}
let html = '<div class="fiber-path-schema-horizontal">';
html += '<h6 class="p-0"><i class="fa fa-project-diagram"></i> Schematische Darstellung des Faserweges</h6>';
html += '<div class="schema-flow-horizontal">';
function getBranchConnectionsAtStation(stationName, currentSegment, allSegs) {
const connections = [];
if (currentSegment.type === 'main' && currentSegment.branch_dropouts) {
currentSegment.branch_dropouts.forEach(dropoutLocation => {
if (dropoutLocation === stationName) {
allSegs.forEach(seg => {
if (seg.type === 'branch' && seg.branch_from_location_main === stationName) {
connections.push({
type: 'branch_from_main',
fromCable: currentSegment.name,
fromFiber: currentSegment.fiber_nr,
fromFiberColor: currentSegment.fiber_color,
fromFiberColorHex: currentSegment.color || currentSegment.fiber_color_hex,
fromBundle: currentSegment.bundle_nr,
fromBundleColor: currentSegment.bundle_color,
fromBundleColorHex: currentSegment.bundle_color_hex,
toCable: seg.name,
toFiber: seg.fiber_nr,
toFiberColor: seg.fiber_color,
toFiberColorHex: seg.color || seg.fiber_color_hex,
toBundle: seg.bundle_nr,
toBundleColor: seg.bundle_color,
toBundleColorHex: seg.bundle_color_hex,
level: seg.level
});
}
});
}
});
}
if (currentSegment.type === 'branch' || currentSegment.type === 'main') {
const currentLastStation = currentSegment.stations && currentSegment.stations.length > 0
? currentSegment.stations[currentSegment.stations.length - 1] : null;
allSegs.forEach(seg => {
if (seg.type === 'branch' && seg !== currentSegment) {
const firstStation = seg.stations && seg.stations.length > 0 ? seg.stations[0] : null;
if (seg.level > currentSegment.level && firstStation && firstStation.name === stationName) {
const alreadyAdded = connections.some(c => c.toCable === seg.name);
if (!alreadyAdded) {
connections.push({
type: currentSegment.type === 'main' ? 'branch_from_main' : 'branch_from_branch',
fromCable: currentSegment.name,
fromFiber: currentSegment.fiber_nr,
fromFiberColor: currentSegment.fiber_color,
fromFiberColorHex: currentSegment.color || currentSegment.fiber_color_hex,
fromBundle: currentSegment.bundle_nr,
fromBundleColor: currentSegment.bundle_color,
fromBundleColorHex: currentSegment.bundle_color_hex,
toCable: seg.name,
toFiber: seg.fiber_nr,
toFiberColor: seg.fiber_color,
toFiberColorHex: seg.color || seg.fiber_color_hex,
toBundle: seg.bundle_nr,
toBundleColor: seg.bundle_color,
toBundleColorHex: seg.bundle_color_hex,
level: seg.level
});
}
}
if (seg.level > currentSegment.level &&
currentLastStation && currentLastStation.name === stationName &&
firstStation && firstStation.name === stationName) {
const alreadyAdded = connections.some(c => c.toCable === seg.name);
if (!alreadyAdded) {
connections.push({
type: 'branch_from_branch',
fromCable: currentSegment.name,
fromFiber: currentSegment.fiber_nr,
fromFiberColor: currentSegment.fiber_color,
fromFiberColorHex: currentSegment.color || currentSegment.fiber_color_hex,
fromBundle: currentSegment.bundle_nr,
fromBundleColor: currentSegment.bundle_color,
fromBundleColorHex: currentSegment.bundle_color_hex,
toCable: seg.name,
toFiber: seg.fiber_nr,
toFiberColor: seg.fiber_color,
toFiberColorHex: seg.color || seg.fiber_color_hex,
toBundle: seg.bundle_nr,
toBundleColor: seg.bundle_color,
toBundleColorHex: seg.bundle_color_hex,
level: seg.level
});
}
}
}
});
}
return connections;
}
function getStationInfo(station, isDropoutPoint, segment, allSegments) {
const objectType = parseInt(station.object_type);
const branchConnections = getBranchConnectionsAtStation(station.name, segment, allSegments);
const isBranchJunction = branchConnections.length > 0;
let icon = 'fa-home';
let colorClass = 'station-default';
let color = '#3388ff';
let label = station.name || 'Station';
let badgeText = 'Station';
let badgeClass = 'badge-secondary';
if (station.type === 'pop') {
icon = 'fa-building';
colorClass = 'station-pop';
color = '#acf0ab';
label = station.name || 'POP';
badgeText = 'POP';
badgeClass = 'badge-success';
} else if (station.type === 'dispatcher') {
if (isBranchJunction) {
icon = 'fa-code-branch';
colorClass = 'station-branch';
color = '#FF6B6B';
label = station.name || 'Branch-Verbindung';
badgeText = 'Branch-Verbindung';
badgeClass = 'badge-danger';
} else if (objectType === 2) {
icon = 'fa-archive';
colorClass = 'station-manhole';
color = '#ffa726';
label = station.name || 'Schacht';
badgeText = 'Schacht';
badgeClass = 'badge-warning';
} else {
icon = 'fa-sitemap';
colorClass = 'station-distributor';
color = '#abbaf0';
label = station.name || 'Verteiler';
badgeText = 'Verteiler';
badgeClass = 'badge-info';
}
}
return {
icon,
colorClass,
label,
color,
badgeText,
badgeClass,
isBranchJunction,
branchConnections
};
}
function getDropoutName(segment, fiber) {
const targetFiber = segment.target_fiber;
const segmentFiber = segment.cable_info || segment.cable || {};
if (segment.type === 'main' && segment.branch_dropouts && segment.branch_dropouts.length > 0) {
return segment.branch_dropouts[0];
}
const effectiveFiber = {
location: targetFiber?.location || targetFiber?.branch_from_location || segmentFiber.location || fiber.location
};
if (effectiveFiber.location) {
return effectiveFiber.location;
}
return null;
}
function getStationsUntilDropout(segment, fiber) {
const validStations = segment.stations.filter(s => s.gps_lat && s.gps_long);
if (validStations.length === 0) {
return [];
}
const dropoutName = getDropoutName(segment, fiber);
if (!dropoutName) {
return validStations;
}
const dropoutIndex = validStations.findIndex(s => s.name === dropoutName);
if (dropoutIndex === -1) {
return validStations;
}
return validStations.slice(0, dropoutIndex + 1);
}
let lastStationName = null;
const segmentChain = allSegments
.filter(s => s.type !== 'branch_points')
.sort((a, b) => {
if (a.type === 'main' && b.type !== 'main') return -1;
if (a.type !== 'main' && b.type === 'main') return 1;
return (a.level || 0) - (b.level || 0);
});
let lastSegmentWithCustomer = null;
for (let i = segmentChain.length - 1; i >= 0; i--) {
const seg = segmentChain[i];
const targetFiber = seg.target_fiber;
if (targetFiber?.home_id || targetFiber?.address || fiber.home_id || fiber.address) {
lastSegmentWithCustomer = seg;
break;
}
}
let currentSegmentName = null;
let previousSegment = null;
segmentChain.forEach((segment, segmentIndex) => {
const stationsToShow = getStationsUntilDropout(segment, fiber);
const dropoutName = getDropoutName(segment, fiber);
// console.log(`Segment ${segment.name} (${segment.type}, level ${segment.level}):`, {
// dropoutName,
// stationsToShow: stationsToShow.map(s => s.name)
// });
if (stationsToShow.length === 0) {
return;
}
stationsToShow.forEach((station, stationIndex) => {
const isSegmentChange = previousSegment !== null && previousSegment.name !== segment.name;
const dropoutName = getDropoutName(segment, fiber);
const isDropoutPoint = dropoutName === station.name;
const isDuplicate = station.name === lastStationName && stationIndex === 0;
if (isDuplicate && isSegmentChange) {
// console.log(`⏭️ Branch beginnt an ${station.name} → Station überspringen, aber ${segment.name}-Linie zeichnen`);
previousSegment = segment;
}
else if (isDuplicate) {
// console.log(`⏭️ Skipping duplicate within segment: ${station.name}`);
return;
}
else {
// console.log(`📍 Drawing station ${station.name}: segment=${segment.name}, isDropout=${isDropoutPoint}`);
const stationInfo = getStationInfo(station, isDropoutPoint, segment, allSegments);
const showTransition = isDropoutPoint && isSegmentChange;
let branchConnectionsHtml = '';
if (stationInfo.isBranchJunction) {
branchConnectionsHtml = '<hr style="margin: 8px 0;"><div style="border: 1px solid #d1dcff; padding: 8px; border-radius: 4px; margin-top: 8px;">';
branchConnectionsHtml += '<h6 style="margin: 0 0 8px 0; color: #000000;"><i class="fa fa-code-branch"></i> Faser-Übergänge:</h6>';
stationInfo.branchConnections.forEach((conn, idx) => {
const junctionType = conn.type === 'branch_from_main' ? 'Hauptkabel → Branch' : 'Branch → Sub-Branch';
const fromFiberColorName = conn.fromFiberColor || (conn.fromFiberColorHex ? hexToColorName(conn.fromFiberColorHex) : '');
const fromBundleColorName = conn.fromBundleColor || (conn.fromBundleColorHex ? hexToColorName(conn.fromBundleColorHex) : '');
const toFiberColorName = conn.toFiberColor || (conn.toFiberColorHex ? hexToColorName(conn.toFiberColorHex) : '');
const toBundleColorName = conn.toBundleColor || (conn.toBundleColorHex ? hexToColorName(conn.toBundleColorHex) : '');
branchConnectionsHtml += `
<div style="background: white; padding: 6px; margin-bottom: 6px; border-left: 3px solid #FF6B6B; font-size: 10px;">
<strong style="color: #000000;">${junctionType} (Level ${conn.level})</strong><br>
<div style="margin-top: 4px;">
<strong>Von:</strong> ${conn.fromCable}<br>
<span style="padding-left: 10px;">└─ Faser ${conn.fromFiber || '?'} ${conn.fromFiberColorHex ? `<span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${conn.fromFiberColorHex}; border: 1px solid #666; margin-left: 4px;"></span> ${fromFiberColorName}` : ''}</span><br>
${conn.fromBundle ? `<span style="padding-left: 10px;">└─ Bündel ${conn.fromBundle}</span>` : ''}
${conn.fromBundleColorHex ? ` <span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${conn.fromBundleColorHex}; border: 1px solid #666;"></span> ${fromBundleColorName}` : ''}
${conn.fromBundle ? '<br>' : ''}
</div>
<div style="margin-top: 4px; padding-top: 4px; border-top: 1px dashed #ddd;">
<strong>Nach:</strong> ${conn.toCable}<br>
<span style="padding-left: 10px;">└─ Faser ${conn.toFiber || '?'} ${conn.toFiberColorHex ? `<span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${conn.toFiberColorHex}; border: 1px solid #666; margin-left: 4px;"></span> ${toFiberColorName}` : ''}</span><br>
${conn.toBundle ? `<span style="padding-left: 10px;">└─ Bündel ${conn.toBundle}</span>` : ''}
${conn.toBundleColorHex ? ` <span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${conn.toBundleColorHex}; border: 1px solid #666;"></span> ${toBundleColorName}` : ''}
${conn.toBundle ? '<br>' : ''}
</div>
</div>
`;
});
branchConnectionsHtml += '</div>';
}
const popupContent = `
<div style="min-width: 250px;">
<h6><strong>${station.name}</strong></h6>
<span class="badge ${stationInfo.badgeClass}">${stationInfo.badgeText}</span>
${stationInfo.isBranchJunction ? '<span class="badge badge-warning ml-1">' + stationInfo.branchConnections.length + ' Übergänge</span>' : ''}
<hr style="margin: 8px 0;">
<div style="font-size: 11px;">
<strong>Kabel:</strong> ${segment.name}<br>
${segment.fiber_nr ? `<strong>F:</strong> ${segment.fiber_nr}` : ''}
${segment.fiber_nr && segment.fiber_color_hex ? ` <span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${segment.fiber_color_hex}; border: 1px solid #666;"></span> (${segment.fiber_color || hexToColorName(segment.fiber_color_hex)})` : ''}<br>
${segment.bundle_nr ? `<strong>B:</strong> ${segment.bundle_nr}` : ''}
${segment.bundle_nr && segment.bundle_color_hex ? ` <span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${segment.bundle_color_hex}; border: 1px solid #666;"></span> (${segment.bundle_color || hexToColorName(segment.bundle_color_hex)})` : ''}<br>
<strong>Level:</strong> ${segment.level}<br>
<small class="text-muted">${parseFloat(station.gps_lat).toFixed(5)}, ${parseFloat(station.gps_long).toFixed(5)}</small>
</div>
${branchConnectionsHtml}
</div>
`;
html += `
<div class="schema-node-horizontal ${stationInfo.colorClass} schema-station-clickable"
data-popup-content="${popupContent.replace(/"/g, '&quot;')}"
data-station-name="${station.name}"
style="cursor: pointer;">
<div class="schema-node-icon-horizontal" style="color: ${stationInfo.color};">
<i class="fa ${stationInfo.icon}" style="color:#fff"></i>
</div>
<div class="schema-node-label-horizontal">
<strong>${stationInfo.label}</strong>
${isDropoutPoint ? '<br><span class="badge badge-warning" style="font-size: 8px;">Dropout</span>' : ''}
${showTransition ? `
<br><div style="margin-top: 6px; padding: 4px 8px; background: #f8f9fa; border-left: 3px solid #dc3545; font-size: 10px; text-align: left;">
<strong style="font-size: 9px; color: #666;">⚡ Faser-Übergang:</strong><br>
<div style="margin-top: 3px; padding-left: 8px;">
<strong>Von:</strong> ${previousSegment.name}<br>
<span style="padding-left: 10px;">└─ Faser ${previousSegment.fiber_nr || '?'} ${(previousSegment.color || previousSegment.fiber_color_hex) ? `<span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${previousSegment.color || previousSegment.fiber_color_hex}; border: 1px solid #666; margin-left: 4px;"></span> ${previousSegment.fiber_color || (previousSegment.fiber_color_hex ? hexToColorName(previousSegment.fiber_color_hex) : '') || (previousSegment.color ? hexToColorName(previousSegment.color) : '')}` : ''}</span><br>
${previousSegment.bundle_nr ? `<span style="padding-left: 10px;">└─ Bündel ${previousSegment.bundle_nr} ${previousSegment.bundle_color_hex ? `<span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${previousSegment.bundle_color_hex}; border: 1px solid #666; margin-left: 4px;"></span> ${previousSegment.bundle_color || (previousSegment.bundle_color_hex ? hexToColorName(previousSegment.bundle_color_hex) : '')}` : ''}</span><br>` : ''}
</div>
<div style="margin-top: 4px; padding-top: 4px; border-top: 1px dashed #ddd; padding-left: 8px;">
<strong>Nach:</strong> ${segment.name}<br>
<span style="padding-left: 10px;">└─ Faser ${segment.fiber_nr || '?'} ${(segment.color || segment.fiber_color_hex) ? `<span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${segment.color || segment.fiber_color_hex}; border: 1px solid #666; margin-left: 4px;"></span> ${segment.fiber_color || (segment.fiber_color_hex ? hexToColorName(segment.fiber_color_hex) : '') || (segment.color ? hexToColorName(segment.color) : '')}` : ''}</span><br>
${segment.bundle_nr ? `<span style="padding-left: 10px;">└─ Bündel ${segment.bundle_nr} ${segment.bundle_color_hex ? `<span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${segment.bundle_color_hex}; border: 1px solid #666; margin-left: 4px;"></span> ${segment.bundle_color || (segment.bundle_color_hex ? hexToColorName(segment.bundle_color_hex) : '')}` : ''}</span><br>` : ''}
</div>
</div>
` : ''}
</div>
</div>
`;
lastStationName = station.name;
previousSegment = segment;
}
const isLastStationInSegment = stationIndex === stationsToShow.length - 1;
const isLastSegment = segmentIndex === segmentChain.length - 1;
let shouldDrawConnection = false;
if (!isLastStationInSegment) {
shouldDrawConnection = true;
} else if (isLastSegment) {
shouldDrawConnection = false;
} else if (isDuplicate && isSegmentChange) {
shouldDrawConnection = true;
}
if (shouldDrawConnection) {
const lineStyle = segment.type === 'branch' ? 'schema-line-dashed-horizontal' : '';
const segmentColor = segment.color || segment.fiber_color_hex || '#FF0000';
const fiberColorName = segment.fiber_color || (segment.fiber_color_hex ? hexToColorName(segment.fiber_color_hex) : '') || (segment.color ? hexToColorName(segment.color) : '');
const bundleColorName = segment.bundle_color || (segment.bundle_color_hex ? hexToColorName(segment.bundle_color_hex) : '');
const fiberColorHex = segment.fiber_color_hex || segment.color || '#FF0000';
const bundleColorHex = segment.bundle_color_hex || '#cccccc';
const labelText = `
<strong>${segment.name}</strong>
${segment.fiber_nr ? `<br><span style="font-size: 12px;"><strong>F:</strong> ${segment.fiber_nr}` : ''}
${fiberColorName ? ` <span class="fiber-color-dot" style="background: ${fiberColorHex}; display: inline-block; width: 8px; height: 8px; border-radius: 50%; border: 1px solid #333; margin-left: 2px; vertical-align: middle;"></span> ${fiberColorName}` : ''}
${segment.fiber_nr ? `</span>` : ''}
${segment.bundle_nr ? `<br><span style="font-size: 12px;"><strong>B:</strong> ${segment.bundle_nr}` : ''}
${bundleColorName ? ` <span class="fiber-color-dot" style="background: ${bundleColorHex}; display: inline-block; width: 8px; height: 8px; border-radius: 50%; border: 1px solid #333; margin-left: 2px; vertical-align: middle;"></span> ${bundleColorName}` : ''}
${segment.bundle_nr ? `</span>` : ''}
`.trim();
html += `
<div class="schema-connection-horizontal">
<div class="schema-line-horizontal ${lineStyle}" style="${segment.type === 'branch' ? `background: repeating-linear-gradient(to right, ${segmentColor}, ${segmentColor} 8px, transparent 8px, transparent 16px); height: 4px;` : `background-color: ${segmentColor};`}">
<div class="schema-cable-label">
${labelText}
</div>
</div>
</div>
`;
}
});
});
if (lastSegmentWithCustomer) {
const targetFiber = lastSegmentWithCustomer.target_fiber;
const customerAddress = targetFiber?.address || fiber.address;
const customerHomeId = targetFiber?.home_id || fiber.home_id;
const customerName = targetFiber?.name || fiber.name;
if (customerHomeId || customerAddress) {
html += `
<div class="schema-connection-horizontal">
<div class="schema-line-horizontal" style="background: repeating-linear-gradient(to right, #FFA500, #FFA500 5px, transparent 5px, transparent 10px); height: 4px;">
<div class="schema-cable-label">
<strong>Hausanschluss</strong>
</div>
</div>
</div>
<div class="schema-node-horizontal station-customer">
<div class="schema-node-icon-horizontal" style="color: #f0abab;">
<i class="fa fa-home"></i>
</div>
<div class="schema-node-label-horizontal">
${customerHomeId ? `<strong>ID: ${customerHomeId}</strong><br>` : ''}
${customerAddress ? `<span class="small">${customerAddress}</span><br>` : ''}
${customerName ? `<span class="small">${customerName}</span>` : ''}
</div>
</div>
`;
}
}
html += '</div>';
html += '</div>';
schemaContainer.html(html);
$('.schema-station-clickable').off('click').on('click', function (e) {
e.stopPropagation();
const $station = $(this);
const popupContent = $station.attr('data-popup-content').replace(/&quot;/g, '"');
const stationName = $station.attr('data-station-name');
$station.popover('dispose');
$station.popover({
content: popupContent,
html: true,
placement: 'top',
trigger: 'manual',
container: 'body',
template: `
<div class="popover station-schema-popover" role="tooltip">
<div class="arrow"></div>
<h3 class="popover-header"></h3>
<div class="popover-body"></div>
</div>
`
});
$('.schema-station-clickable').not($station).popover('hide');
$station.popover('show');
});
$(document).off('click.schema-popup').on('click.schema-popup', function (e) {
if (!$(e.target).closest('.schema-station-clickable, .popover').length) {
$('.schema-station-clickable').popover('hide');
}
});
}
function renderFiberPathSchemaLinework(fiber, allSegments) {
const schemaContainer = $('#fiber-route-schema');
if (!fiber || allSegments.length === 0) {
schemaContainer.html('<div class="text-center text-muted">Keine Daten verfügbar</div>');
return;
}
const sortedSegments = allSegments
.filter(s => s.type !== 'branch_points')
.sort((a, b) => {
if (a.type === 'main') return -1;
if (b.type === 'main') return 1;
return (a.level || 0) - (b.level || 0);
});
let displayNodes = [];
const normalize = (str) => str ? str.toString().toLowerCase().replace(/[^a-z0-9]/g, "") : "";
for (let i = 0; i < sortedSegments.length; i++) {
const segment = sortedSegments[i];
const nextSegment = (i < sortedSegments.length - 1) ? sortedSegments[i + 1] : null;
let stations = segment.stations.filter(s => s.gps_lat && s.gps_long);
if (stations.length === 0) continue;
if (segment.type !== 'main') {
let startNameRaw = null;
if (segment.cable_info && segment.cable_info.from_location) startNameRaw = segment.cable_info.from_location;
else if (segment.from_location) startNameRaw = segment.from_location;
else if (segment.target_fiber && segment.target_fiber.branch_from_location) startNameRaw = segment.target_fiber.branch_from_location;
if (startNameRaw) {
const startIndex = stations.findIndex(s => normalize(s.name) === normalize(startNameRaw));
if (startIndex !== -1) {
stations = stations.slice(startIndex);
}
}
}
let endNameRaw = null;
if (nextSegment) {
if (nextSegment.cable_info && nextSegment.cable_info.from_location) endNameRaw = nextSegment.cable_info.from_location;
else if (nextSegment.from_location) endNameRaw = nextSegment.from_location;
if (!endNameRaw && nextSegment.stations.length > 0) {
endNameRaw = nextSegment.stations[0].name;
}
} else {
endNameRaw = segment.target_fiber?.location || fiber.location;
}
if (endNameRaw) {
const endIndex = stations.findIndex(s => normalize(s.name) === normalize(endNameRaw));
if (endIndex !== -1) {
if (endIndex === 0 && stations.length > 1) {
// console.log(`Schema: Ignoriere Cut bei Index 0 für ${segment.name} (Start=Ende)`);
} else {
stations = stations.slice(0, endIndex + 1);
}
}
}
stations.forEach((station, idx) => {
const isLastInSegment = (idx === stations.length - 1);
const isTransitionPoint = isLastInSegment && (nextSegment !== null);
const segColor = segment.color || segment.fiber_color_hex || '#999';
const lastAddedNode = displayNodes.length > 0 ? displayNodes[displayNodes.length - 1] : null;
const isDuplicate = lastAddedNode && normalize(lastAddedNode.name) === normalize(station.name);
if (isDuplicate) {
if (isTransitionPoint) {
lastAddedNode.isBranchPoint = true;
lastAddedNode.transitionData = {
nextCable: nextSegment.name,
nextFiber: nextSegment.fiber_nr,
nextColor: nextSegment.color || nextSegment.fiber_color_hex,
nextFiberColor: nextSegment.fiber_color,
nextFiberColorHex: nextSegment.fiber_color_hex,
nextBundleNr: nextSegment.bundle_nr,
nextBundleColor: nextSegment.bundle_color,
nextBundleColorHex: nextSegment.bundle_color_hex
};
}
} else {
displayNodes.push({
name: station.name,
type: station.type,
object_type: station.object_type,
lat: parseFloat(station.gps_lat),
lng: parseFloat(station.gps_long),
cable: segment.name,
fiberNr: segment.fiber_nr,
color: segColor,
fiberColor: segment.fiber_color,
fiberColorHex: segment.fiber_color_hex,
bundleNr: segment.bundle_nr,
bundleColor: segment.bundle_color,
bundleColorHex: segment.bundle_color_hex,
level: segment.level,
isBranchPoint: isTransitionPoint,
isDropout: (!nextSegment && isLastInSegment),
transitionData: isTransitionPoint ? {
nextCable: nextSegment.name,
nextFiber: nextSegment.fiber_nr,
nextColor: nextSegment.color || nextSegment.fiber_color_hex,
nextFiberColor: nextSegment.fiber_color,
nextFiberColorHex: nextSegment.fiber_color_hex,
nextBundleNr: nextSegment.bundle_nr,
nextBundleColor: nextSegment.bundle_color,
nextBundleColorHex: nextSegment.bundle_color_hex
} : null
});
}
});
}
let html = '<div class="fiber-path-schema-horizontal">';
html += '<h6 class="p-0"><i class="fa fa-project-diagram"></i> Faserweg (POP → Kunde)</h6>';
html += '<div class="schema-flow-horizontal" id="linework-schema-flow">';
displayNodes.forEach((node, index) => {
const style = getLineworkStationStyle(node);
let badgeClass = 'badge-info';
let badgeText = style.typeLabel;
if (node.isBranchPoint) {
style.cssClass = 'station-branch';
style.icon = 'fa-code-branch';
style.color = '#9c27b0';
badgeClass = 'badge-danger';
badgeText = 'Branch-Verbindung';
} else if (node.type === 'pop') {
badgeClass = 'badge-success';
} else if (node.object_type == 2) {
badgeClass = 'badge-warning';
}
const fiberColorName = node.fiberColor || (node.fiberColorHex ? hexToColorName(node.fiberColorHex) : '');
const bundleColorName = node.bundleColor || (node.bundleColorHex ? hexToColorName(node.bundleColorHex) : '');
let popupContent = `
<div style="min-width: 250px;">
<h6><strong>${node.name}</strong></h6>
<span class="badge ${badgeClass}">${badgeText}</span>
${node.isBranchPoint ? '<span class="badge badge-warning ml-1">1 Übergänge</span>' : ''}
<hr style="margin: 8px 0;">
<div style="font-size: 11px;">
<strong>Kabel:</strong> ${node.cable}<br>
${node.fiberNr ? `<strong>F:</strong> ${node.fiberNr}` : ''}
${node.fiberColorHex ? ` <span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${node.fiberColorHex}; border: 1px solid #666;"></span> (${fiberColorName})` : ''}<br>
${node.bundleNr ? `<strong>B:</strong> ${node.bundleNr}` : ''}
${node.bundleColorHex ? ` <span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${node.bundleColorHex}; border: 1px solid #666;"></span> (${bundleColorName})` : ''}<br>
<strong>Level:</strong> ${node.level || 0}<br>
<small class="text-muted">${(node.lat && node.lng) ? `${node.lat.toFixed(5)}, ${node.lng.toFixed(5)}` : ''}</small>
</div>`;
if (node.transitionData) {
const nextFiberColorName = node.transitionData.nextFiberColor || (node.transitionData.nextFiberColorHex ? hexToColorName(node.transitionData.nextFiberColorHex) : '');
const nextBundleColorName = node.transitionData.nextBundleColor || (node.transitionData.nextBundleColorHex ? hexToColorName(node.transitionData.nextBundleColorHex) : '');
const junctionType = node.level === 0 ? 'Hauptkabel → Branch' : 'Branch → Sub-Branch';
popupContent += `
<hr style="margin: 8px 0;">
<div style="border: 1px solid #d1dcff; padding: 8px; border-radius: 4px; margin-top: 8px;">
<h6 style="margin: 0 0 8px 0; color: #000000; font-size: 12px;"><i class="fa fa-code-branch"></i> Faser-Übergänge:</h6>
<div style="background: white; padding: 6px; margin-bottom: 6px; border-left: 3px solid #FF6B6B; font-size: 10px;">
<strong style="color: #000000;">${junctionType} (Level ${node.level})</strong><br>
<div style="margin-top: 4px;">
<strong>Von:</strong> ${node.cable}<br>
<span style="padding-left: 10px;">└─ Faser ${node.fiberNr || '?'}
${node.fiberColorHex ? `<span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${node.fiberColorHex}; border: 1px solid #666;"></span> ${fiberColorName}` : ''}
</span><br>
<span style="padding-left: 10px;">└─ Bündel ${node.bundleNr || '?'}
${node.bundleColorHex ? `<span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${node.bundleColorHex}; border: 1px solid #666;"></span> ${bundleColorName}` : ''}
</span>
</div>
<div style="margin-top: 4px; padding-top: 4px; border-top: 1px dashed #ddd;">
<strong>Nach:</strong> ${node.transitionData.nextCable}<br>
<span style="padding-left: 10px;">└─ Faser ${node.transitionData.nextFiber || '?'}
${node.transitionData.nextFiberColorHex ? `<span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${node.transitionData.nextFiberColorHex}; border: 1px solid #666;"></span> ${nextFiberColorName}` : ''}
</span><br>
<span style="padding-left: 10px;">└─ Bündel ${node.transitionData.nextBundleNr || '?'}
${node.transitionData.nextBundleColorHex ? `<span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${node.transitionData.nextBundleColorHex}; border: 1px solid #666;"></span> ${nextBundleColorName}` : ''}
</span>
</div>
</div>
</div>`;
}
popupContent += `</div>`;
html += `
<div class="schema-node-horizontal ${style.cssClass} schema-station-clickable"
data-popup-content="${popupContent.replace(/"/g, '&quot;')}"
data-station-name="${node.name}">
<div class="schema-node-icon-horizontal" style="color: ${style.color};">
<i class="fa ${style.icon}"></i>
</div>
<div class="schema-node-label-horizontal">
<strong>${node.name}</strong>
${(node.isDropout || node.isBranchPoint) ? '<span class="badge badge-warning mt-1" style="background-color: #ffc107; color: #000;">Dropout</span>' : ''}
</div>
</div>
`;
if (index < displayNodes.length - 1) {
const nextNode = displayNodes[index + 1];
const isBranchLine = node.isBranchPoint;
let lineColor = '#999';
let lineLabel = '';
const makeLabel = (cable, fNr, fCol, fColHex, bNr, bCol, bColHex) => {
const fName = fCol || (fColHex ? hexToColorName(fColHex) : '');
const bName = bCol || (bColHex ? hexToColorName(bColHex) : '');
return `
<strong>${cable}</strong><br>
<span style="font-size: 9px;">
${fNr ? `F: ${fNr}` : ''} ${fName ? `<span style="display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: ${fColHex || '#000'}; border: 1px solid #333;"></span> ${fName}` : ''}
</span>
${bNr ? `<br><span style="font-size: 9px;">B: ${bNr} ${bName ? `<span style="display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: ${bColHex || '#ccc'}; border: 1px solid #333;"></span> ${bName}` : ''}</span>` : ''}
`;
};
if (isBranchLine && node.transitionData) {
lineColor = node.transitionData.nextColor;
lineLabel = makeLabel(
node.transitionData.nextCable,
node.transitionData.nextFiber,
node.transitionData.nextFiberColor,
node.transitionData.nextFiberColorHex,
node.transitionData.nextBundleNr,
node.transitionData.nextBundleColor,
node.transitionData.nextBundleColorHex
);
} else {
lineColor = nextNode.color;
lineLabel = makeLabel(
nextNode.cable,
nextNode.fiberNr,
nextNode.fiberColor,
nextNode.fiberColorHex,
nextNode.bundleNr,
nextNode.bundleColor,
nextNode.bundleColorHex
);
}
if (!lineColor) lineColor = '#FF0000';
const lineClass = isBranchLine ? 'schema-line-dashed-horizontal' : 'schema-line-horizontal';
const lineStyle = isBranchLine
? `color: ${lineColor};`
: `background-color: ${lineColor}; height: 4px;`;
html += `
<div class="schema-connection-horizontal">
<div class="${lineClass}" style="${lineStyle}">
<div class="schema-cable-label">${lineLabel}</div>
</div>
</div>
`;
}
});
const targetFiber = fiber;
if (targetFiber.home_id || targetFiber.address) {
html += `
<div class="schema-connection-horizontal">
<div class="schema-line-horizontal" style="background: repeating-linear-gradient(to right, #FFA500, #FFA500 5px, transparent 5px, transparent 10px); height: 4px;">
<div class="schema-cable-label"><strong>Hausanschluss</strong></div>
</div>
</div>
<div class="schema-node-horizontal station-customer">
<div class="schema-node-icon-horizontal" style="color: #f0abab; border-color: #f0abab;">
<i class="fa fa-home"></i>
</div>
<div class="schema-node-label-horizontal">
<strong>${targetFiber.home_id || ''}</strong><br>
<span class="small text-muted">${targetFiber.name || 'Kunde'}</span>
</div>
</div>
`;
}
html += '</div></div>';
schemaContainer.html(html);
$('.schema-station-clickable').off('click').on('click', function (e) {
e.stopPropagation();
const $station = $(this);
const popupContent = $station.attr('data-popup-content').replace(/&quot;/g, '"');
$station.popover('dispose');
$station.popover({
content: popupContent,
html: true,
placement: 'top',
trigger: 'manual',
container: 'body',
template: `
<div class="popover station-schema-popover" role="tooltip">
<div class="arrow"></div>
<h3 class="popover-header"></h3>
<div class="popover-body"></div>
</div>
`
});
$('.schema-station-clickable').not($station).popover('hide');
$station.popover('show');
});
$(document).off('click.schema-popup').on('click.schema-popup', function (e) {
if (!$(e.target).closest('.schema-station-clickable, .popover').length) {
$('.schema-station-clickable').popover('hide');
}
});
}
function getLineworkStationStyle(node) {
if (node.type === 'pop') return { icon: 'fa-building', cssClass: 'station-pop', color: '#4caf50', typeLabel: 'POP' };
if (node.object_type == 4) return { icon: 'fa-code-branch', cssClass: 'station-branch', color: '#9c27b0', typeLabel: 'Abzweig' };
if (node.object_type == 2) return { icon: 'fa-archive', cssClass: 'station-manhole', color: '#ffa726', typeLabel: 'Schacht' };
return { icon: 'fa-sitemap', cssClass: 'station-distributor', color: '#3388ff', typeLabel: 'Verteiler' };
}
function getBranchColor(level) {
const colors = [
'#FF6B6B',
'#FFA500',
'#FFD700',
'#9B59B6',
'#E91E63',
];
return colors[(level - 1) % colors.length];
}
function hexToColorName(hex) {
const colorMap = {
'#FF6B6B': 'Rot',
'#FFA500': 'Orange',
'#F79646': 'Orange',
'#FFD700': 'Gold',
'#9B59B6': 'Violett',
'#E91E63': 'Pink',
'#3388ff': 'Blau',
'#FF0000': 'Rot',
'#00FF00': 'Grün',
'#0000FF': 'Blau',
'#FFFF00': 'Gelb',
'#00FFFF': 'Cyan',
'#FF00FF': 'Magenta',
'#800000': 'Dunkelrot',
'#808000': 'Olive',
'#008000': 'Grün',
'#800080': 'Lila',
'#008080': 'Türkis',
'#00B0F0': 'Türkis',
'#000080': 'Marineblau',
'#FFC0CB': 'Rosa',
'#A52A2A': 'Braun',
'#948A54': 'Braun',
'#808080': 'Grau',
'#C0C0C0': 'Grau',
'#000000': 'Schwarz',
'#FFFFFF': 'Weiß',
'#7030A0': 'Violett',
};
const normalizedHex = hex.toUpperCase().startsWith('#') ? hex.toUpperCase() : '#' + hex.toUpperCase();
if (colorMap[normalizedHex]) {
return colorMap[normalizedHex];
}
if (normalizedHex.match(/#FF[0-9A-F]{4}/)) return 'Rot';
if (normalizedHex.match(/#[0-9A-F]{2}FF[0-9A-F]{2}/)) return 'Grün';
if (normalizedHex.match(/#[0-9A-F]{4}FF/)) return 'Blau';
if (normalizedHex.match(/#FFFF[0-9A-F]{2}/)) return 'Gelb';
if (normalizedHex.match(/#FF[0-9A-F]{2}FF/)) return 'Magenta';
if (normalizedHex.match(/#[0-9A-F]{2}FFFF/)) return 'Cyan';
return hex;
}
$(document).on('hidden.bs.modal', '#cableRouteMapModal, #fiberRouteMapModal', function () {
if ($('.modal:visible').length > 0) {
$('body').addClass('modal-open');
}
});
$(document).on('shown.bs.modal', '#cableRouteMapModal, #fiberRouteMapModal', function () {
$('#fiberPlanCableModal').css('overflow', 'hidden');
});
$(document).on('hidden.bs.modal', '#cableRouteMapModal, #fiberRouteMapModal', function () {
$('#fiberPlanCableModal').css('overflow', 'auto');
});
function findTargetFiberAtLevel(branchPath, targetLevel, currentLevel = 1) {
if (!branchPath || branchPath.error) {
return null;
}
if (currentLevel === targetLevel && branchPath.target_fiber) {
return branchPath.target_fiber;
}
if (branchPath.next_branch) {
return findTargetFiberAtLevel(branchPath.next_branch, targetLevel, currentLevel + 1);
}
return null;
}
function findCableForCustomerConnection(customerLat, customerLng, cables) {
const tolerance = 0.00001;
for (let cable of cables) {
if (!cable.coordinates || cable.coordinates.length === 0) continue;
const firstPoint = cable.coordinates[0];
const lastPoint = cable.coordinates[cable.coordinates.length - 1];
if (Math.abs(firstPoint.gps_lat - customerLat) < tolerance &&
Math.abs(firstPoint.gps_long - customerLng) < tolerance) {
return {
cable: cable,
reverse: false
};
}
if (Math.abs(lastPoint.gps_lat - customerLat) < tolerance &&
Math.abs(lastPoint.gps_long - customerLng) < tolerance) {
return {
cable: cable,
reverse: true
};
}
}
return null;
}
$('body').on('click', '#toggle-fiber-view', function () {
const btn = $(this);
const cableName = btn.data('cable-name');
const showAll = btn.data('show-all');
if (showAll === false || showAll === 'false') {
btn.data('show-all', 'true');
btn.html('<i class="fa fa-filter"></i> Gefilterte Ansicht');
btn.attr('title', 'Nur Fasern vom Rack anzeigen');
loadFiberPlanCableDetails(cableName, null, null);
} else {
const fiberStart = btn.data('fiber-start');
const fiberEnd = btn.data('fiber-end');
btn.data('show-all', 'false');
btn.html('<i class="fa fa-eye"></i> Alle Fasern anzeigen');
btn.attr('title', 'Alle Fasern anzeigen');
loadFiberPlanCableDetails(cableName, fiberStart, fiberEnd);
}
});
$(document).ready(function() {
$('body').on('click', '#fiber-details-table .view-fiber-path', function (e) {
e.preventDefault();
e.stopPropagation();
const fiberId = $(this).data('fiber-id');
loadFiberPath(fiberId);
});
$('body').on('click', '#fiber-details-table .fiber-row-clickable', function (e) {
if ($(e.target).closest('button').length || $(e.target).closest('input').length) {
return;
}
const fiberId = $(this).data('fiber-id');
loadFiberPath(fiberId);
});
$('body').on('click', '.load-branch-cable', function (e) {
e.preventDefault();
const branchCableName = $(this).data('cable-name');
loadFiberPlanCableDetails(branchCableName);
});
$('body').on('click', '.show-cable-route-map', function (e) {
const cableId = $(this).data('cable-id');
const cableName = $(this).data('cable-name');
showCableRouteMap(cableId, cableName);
});
});