WEB_PROJECT_TA/assets/js/map.js

1573 lines
54 KiB
JavaScript

// Map.js - FTTH Planner Map Functionality
let map;
let markers = {};
let routes = {};
let isRoutingMode = false;
let routingFromItem = null;
let isManualRouting = false;
let manualFromItem = null;
let currentRoutes = [];
window.items = window.items || [];
// Initialize map
function initMap() {
// Create map with enhanced options
map = L.map('map', {
center: [-7.70298100, 114.01477000], // situbondo, Indonesia
zoom: 11,
minZoom: 5,
maxZoom: 20,
zoomControl: false, // We'll add custom zoom control
fullscreenControl: true,
fullscreenControlOptions: {
position: 'topleft'
}
});
// Buat custom search control
// Buat kontrol pencarian//tombol cari lokasi
var searchMarker = null; // Simpan marker yang sedang aktif
var SearchControl = L.Control.extend({
onAdd: function(map) {
var container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-custom');
// Styling container awal
container.style.display = 'flex';
container.style.alignItems = 'center';
container.style.padding = '4px 8px';
container.style.gap = '6px';
container.style.minWidth = '40px';
container.style.width = 'auto';
container.style.boxSizing = 'border-box';
container.style.background = 'white';
container.style.borderRadius = '4px';
container.style.boxShadow = '0 2px 6px rgba(0,0,0,0.3)';
container.style.cursor = 'pointer';
// Buat icon pencarian
var icon = L.DomUtil.create('span', '', container);
icon.innerHTML = '🔍';
icon.style.fontSize = '16px';
icon.style.cursor = 'pointer';
// Buat input pencarian (disembunyikan awalnya)
var input = L.DomUtil.create('input', '', container);
input.type = "text";
input.placeholder = "Cari lokasi...";
input.style.flex = "1";
input.style.padding = "4px";
input.style.border = "none";
input.style.outline = "none";
input.style.fontSize = "13px";
input.style.display = 'none'; // ← awalnya disembunyikan
// Tombol X untuk clear
var clearBtn = L.DomUtil.create('span', '', container);
clearBtn.innerHTML = '✖';
clearBtn.style.cursor = 'pointer';
clearBtn.style.fontSize = '12px';
clearBtn.style.display = 'none'; // ← awalnya disembunyikan
// Event klik ikon search → toggle tampil/sembunyi
icon.addEventListener('click', function(e) {
e.stopPropagation();
if (input.style.display === 'none') {
// Tampilkan input
input.style.display = 'block';
input.focus();
container.style.minWidth = '180px';
} else {
// Sembunyikan input + clear jika diklik lagi
input.style.display = 'none';
clearBtn.style.display = 'none';
container.style.minWidth = '40px';
input.value = '';
if (searchMarker) {
map.removeLayer(searchMarker);
searchMarker = null;
}
}
});
// Event klik tombol X → reset input
clearBtn.addEventListener('click', function() {
if (searchMarker) {
map.removeLayer(searchMarker);
searchMarker = null;
}
input.value = '';
clearBtn.style.display = 'none';
});
// Event Enter untuk pencarian
L.DomEvent.addListener(input, 'keydown', function(e) {
if (e.key === 'Enter') {
fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(input.value)}`)
.then(res => res.json())
.then(data => {
if (data.length > 0) {
var lat = parseFloat(data[0].lat);
var lon = parseFloat(data[0].lon);
map.setView([lat, lon], 15);
if (searchMarker) {
map.removeLayer(searchMarker);
}
searchMarker = L.marker([lat, lon]).addTo(map).bindPopup(input.value).openPopup();
clearBtn.style.display = 'inline';
}
});
}
});
L.DomEvent.disableClickPropagation(container);
return container;
}
});
// Tambahkan ke peta
map.addControl(new SearchControl());
// Pindahkan ke container utama map
var searchEl = document.querySelector('.leaflet-control-custom');
document.querySelector('.leaflet-container').appendChild(searchEl);
// Posisi kiri atas dengan jarak dari fullscreen
searchEl.style.position = 'absolute';
searchEl.style.top = '10px';
searchEl.style.left = '50px';
searchEl.style.zIndex = 1000;
// Define multiple tile layers
const tileLayers = {
"OpenStreetMap": L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19
}),
"CartoDB Positron": L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
maxZoom: 20
}),
"CartoDB Dark": L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
maxZoom: 20
}),
"Satellite": L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
maxZoom: 20
}),
"Terrain": L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
attribution: 'Map data: &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, <a href="http://viewfinderpanoramas.org">SRTM</a> | Map style: &copy; <a href="https://opentopomap.org">OpenTopoMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)',
maxZoom: 17
}),
"Google Hybrid": L.tileLayer('https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', {
attribution: '&copy; Google',
maxZoom: 20
})
};
// Add default layer (OpenStreetMap)
tileLayers["OpenStreetMap"].addTo(map);
// Add layer control
L.control.layers(tileLayers, null, {
position: 'topright',
collapsed: false
}).addTo(map);
// Add custom zoom control with home button
const zoomControl = L.control.zoom({
position: 'topleft'
}).addTo(map);
// Add home button to zoom control
const homeControl = L.Control.extend({
options: {
position: 'topleft'
},
onAdd: function(map) {
const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-custom');
container.style.backgroundColor = 'white';
container.style.backgroundImage = 'none';
container.style.width = '26px';
container.style.height = '26px';
container.style.cursor = 'pointer';
container.innerHTML = '<i class="fas fa-wifi" style="font-size: 14px; line-height: 26px; text-align: center; width: 26px; display: block;"></i>';
container.title = 'Zoom to Indonesia';
container.onclick = function() {
map.setView([-2.5, 118], 5); // Indonesia overview
};
return container;
}
});
new homeControl().addTo(map);
// Add scale control
L.control.scale({
position: 'bottomright',
metric: true,
imperial: false
}).addTo(map);
// Add coordinates display
const coordsControl = L.control({position: 'bottomleft'});
coordsControl.onAdd = function(map) {
this._div = L.DomUtil.create('div', 'leaflet-control-coords');
this._div.style.background = 'rgba(255,255,255,0.8)';
this._div.style.padding = '5px';
this._div.style.margin = '0';
this._div.style.fontSize = '11px';
this._div.innerHTML = 'Move mouse over map';
return this._div;
};
coordsControl.update = function(lat, lng) {
this._div.innerHTML = `Lat: ${lat.toFixed(6)}, Lng: ${lng.toFixed(6)}`;
};
coordsControl.addTo(map);
// Update coordinates on mouse move
map.on('mousemove', function(e) {
coordsControl.update(e.latlng.lat, e.latlng.lng);
});
// Enhanced zoom behavior
map.on('zoomend', function() {
const zoom = map.getZoom();
if (zoom < 10) {
// Hide detailed markers at low zoom
Object.values(markers).forEach(marker => {
if (marker._icon) {
marker._icon.style.opacity = '0.7';
}
});
} else {
// Show detailed markers at high zoom
Object.values(markers).forEach(marker => {
if (marker._icon) {
marker._icon.style.opacity = '1';
}
});
}
});
// Add map click event for adding new items
map.on('click', function(e) {
if (!isRoutingMode) {
showAddItemModal(e.latlng.lat, e.latlng.lng);
}
});
// Load existing items
loadItems();
loadRoutes();
// Add legend
addMapLegend();
console.log('🗺️ Enhanced map initialized with multiple tile layers and zoom controls');
// Add loading indicator
const mapContainer = document.getElementById('map');
const loadingDiv = document.createElement('div');
loadingDiv.className = 'map-loading';
loadingDiv.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Loading map...';
mapContainer.appendChild(loadingDiv);
// Hide loading indicator after tiles load
map.on('tilesloaded', function() {
if (loadingDiv.parentNode) {
loadingDiv.parentNode.removeChild(loadingDiv);
}
});
}
// Create custom marker icon
// Objek global untuk menyimpan marker berdasarkan SN
// Fungsi buat custom icon
function createCustomIcon(itemType, color) {
let iconClass = 'fas fa-circle'; // Default icon
switch(itemType) {
case 'OLT':
case '1':
iconClass = 'fas fa-cloud'; // Ganti jadi icon cloud
break;
case 'Tiang Tumpu':
case '2':
iconClass = 'fas fa-tower-broadcast'; // Tetap tiang
break;
case 'ODP':
case '3':
iconClass = 'fas fa-boxes-stacked'; // Icon baru untuk ODP
break;
case 'ODC':
case '4':
iconClass = 'fas fa-diagram-project'; // Icon baru untuk ODC
break;
case 'Pelanggan':
case '5':
iconClass = 'fas fa-wifi'; // Ganti jadi icon Wi-Fi
break;
}
return L.divIcon({
className: 'custom-div-icon',
html: `
<div class="custom-marker marker-${itemType.toLowerCase().replace(/\s+/g, '')}"
style="background-color: ${color};
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border: 3px solid white;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);">
<i class="${iconClass}" style="color: white; font-size: 14px;"></i>
</div>
`,
iconSize: [32, 32],
iconAnchor: [16, 16],
popupAnchor: [0, -16]
});
}
// Fungsi update RX Power untuk marker tertentu
// map.js//modifan saya
// pastikan variabel sudah benar
// update RX Power setelah marker dirender
// Create popup content for item
function createPopupContent(item) {
let tubeColorName = item.tube_color_name || 'Tidak ada';
let splitterMain = item.splitter_main_ratio || 'Tidak ada';
let splitterOdp = item.splitter_odp_ratio || 'Tidak ada';
let serialNumber = item.serial_number ? item.serial_number.trim() : '';
// Div untuk pelanggan terhubung hanya untuk ODP/ODC
let connectedClientsHtml = '';
if (item.item_type_name &&
(item.item_type_name.toLowerCase().includes('odp') || item.item_type_name.toLowerCase().includes('odc'))) {
connectedClientsHtml = `
<div class="info-row" id="connected-clients-${item.id}">
<span class="info-label">Pelanggan Terhubung:</span> <em>Memuat...</em>
</div>
`;
}
let html = `
<div>
<h5><i class="${getItemIcon(item.item_type_name)}"></i> ${item.name}</h5>
<div class="popup-info">
<div class="info-row"><span class="info-label">Jenis:</span> ${item.item_type_name}</div>
${item.description ? `<div class="info-row"><span class="info-label">Deskripsi:</span> ${item.description}</div>` : ''}
${item.address ? `<div class="info-row"><span class="info-label">Alamat:</span> ${item.address}</div>` : ''}
<div class="info-row"><span class="info-label">Status:</span>
<span class="badge badge-${getStatusBadgeClass(item.status)}">${getStatusText(item.status)}</span>
</div>
<!-- Redaman hanya untuk item selain ODP, ODC, Tiang Tumpu, OLT -->
${!['odp', 'odc', 'tiang tumpu', 'olt'].includes(item.item_type_name.toLowerCase())
? `
<div class="info-row" id="redaman-${item.id}">
<span class="info-label">Redaman :</span> <em>Memuat...</em>
</div>
<div class="info-row" id="voltage-${item.id}">
<span class="info-label">Voltage :</span> <em>Memuat...</em>
</div>
<div class="info-row" id="onu-status-${item.id}">
<span class="info-label">ONU Status :</span> <em>Memuat...</em>
</div>
<div class="info-row" id="last-update-${item.id}">
<span class="info-label">Last Update :</span> <em>Memuat...</em>
</div>
<div class="info-row">
<span class="info-label">Customer Phone :</span>
<span>${item.customer_phone ? item.customer_phone : '-'}</span>
</div>
`
: ''}
<!-- Pelanggan terhubung hanya untuk ODP & ODC -->
${connectedClientsHtml}
</div>
<div class="popup-actions">
<button class="btn btn-info btn-sm" onclick="loadHistoriRedaman(${item.id})">History Redaman dan Voltage</button>
<button class="btn btn-info btn-sm" onclick="loadHistoriOnu(${item.id})">History ONT</button>
<button class="btn btn-info btn-sm" onclick="showItemDetail(${item.id})"><i class="fas fa-info-circle"></i> Detail</button>
<button class="btn btn-primary btn-sm" onclick="editItem(${item.id})"><i class="fas fa-edit"></i> Edit</button>
<button class="btn btn-success btn-sm" onclick="startRouting(${item.id})"><i class="fas fa-route"></i> Route</button>
<button class="btn btn-primary btn-sm" onclick="startManualRouting(${item.id})" title="Buat Route Manual"><i class="fas fa-pencil-ruler"></i> Manual Route </button>
<button class="btn btn-danger btn-sm" onclick="deleteItem(${item.id})"><i class="fas fa-trash"></i> Hapus</button>
</div>
</div>
`;
setTimeout(() => {
// Load redaman hanya untuk selain ODP
if (!item.item_type_name.toLowerCase().includes('odp')) {
loadRedaman(item.id);
loadVoltage(item.id);
loadOnuStatus(item.id);
}
// Load pelanggan terhubung hanya untuk ODP dan ODC
if (item.item_type_name.toLowerCase().includes('odp') || item.item_type_name.toLowerCase().includes('odc')) {
loadConnectedClientsFromRoutes(item.id);
}
}, 0);
return html;
}
function getRxStatus(rx) {
// null safety
if (rx === null || rx === undefined || isNaN(rx)) {
return { label: 'NO DATA', color: 'gray', batas: '-' };
}
// =========================
// NORMAL RANGE
// -20 s/d -25.99
// =========================
if (rx <= -20 && rx >= -25.99) {
return {
label: 'NORMAL',
color: 'green',
batas: '-20 s/d -25.99 dBm'
};
}
// =========================
// TERLALU KECIL (SIGNAL BAGUS BERLEBIH / OVERPOWER)
// contoh: -19, -10, -5
// =========================
if (rx > -20) {
return {
label: 'CRITICAL (TERLALU KECIL)',
color: 'red',
batas: '> -20 dBm'
};
}
// =========================
// TERLALU BESAR REDAMAN
// contoh: -26, -28, -30
// =========================
if (rx < -25.99) {
return {
label: 'CRITICAL (REDAMAN TINGGI)',
color: 'red',
batas: '< -25.99 dBm'
};
}
}
function getVoltStatus(v) {
if (v === null || v === undefined || isNaN(v)) {
return { label: 'UNKNOWN', color: 'gray', batas: 'No Data' };
}
// =========================
// NORMAL
// 3.20 - 3.29
// =========================
if (v >= 3.20 && v <= 3.29) {
return {
label: 'NORMAL',
color: 'green',
batas: '3.20 - 3.29 V'
};
}
// =========================
// TERLALU TINGGI (OVERVOLT)
// > 3.29
// =========================
if (v > 3.29) {
return {
label: 'CRITICAL (OVER VOLT)',
color: 'red',
batas: '> 3.29 V'
};
}
// =========================
// TERLALU RENDAH (DROP VOLT)
// < 3.20
// =========================
if (v < 3.20) {
return {
label: 'CRITICAL (DROP VOLT)',
color: 'red',
batas: '< 3.20 V'
};
}
}
// Ambil pelanggan yang terhubung melalui routes.php
// revisi client olt/odc/odp Ambil client yang terhubung melalui routes.php
function loadConnectedClientsFromRoutes(itemId) {
$.ajax({
url: 'api/routes.php',
method: 'GET',
dataType: 'json',
success: function(response) {
if (response.success && Array.isArray(response.data)) {
let currentItem = window.items.find(i => i.id == itemId);
let itemType = currentItem && currentItem.item_type_name ? currentItem.item_type_name.toLowerCase() : '';
let isODC = itemType === 'odc';
let isOLT = itemType === 'olt';
let isODP = itemType === 'odp';
// 🔹 Ambil semua client (dua arah)
let connectedClients = response.data
.map(route => {
if (route.to_item_id == itemId) {
return window.items.find(i => i.id == route.from_item_id);
} else if (route.from_item_id == itemId) {
return window.items.find(i => i.id == route.to_item_id);
}
return null;
})
.filter(c => c)
.map(clientItem => ({
id: clientItem.id,
name: clientItem.name,
type: clientItem.item_type_name ? clientItem.item_type_name.toLowerCase() : ''
}));
// 🔹 Pisahkan pelanggan vs node (ODP/ODC/OLT/TIANG)
let pelangganList = [];
let nodeList = [];
connectedClients.forEach(client => {
if (['odc', 'olt', 'odp'].includes(client.type)) {
nodeList.push(client);
} else if (!client.type.includes('tiang tumpu')) {
// hanya masukkan ke pelanggan kalau bukan tiang
pelangganList.push(client);
}
// kalau tiang → dilewati (tidak dimasukkan ke manapun)
});
let html = "";
// =========================
// 🔹 Untuk ODP
if (isODP) {
// Pelanggan Terhubung
html += `<span class="info-label">Pelanggan Terhubung:</span>`;
if (pelangganList.length > 0) {
html += "<ul>";
pelangganList.forEach(client => {
html += `<li id="client-${client.id}">
${client.name} :
<span id="redaman-${client.id}" class="red-blink">Memuat...</span>
</li>`;
// Ambil redaman pelanggan
$.ajax({
url: 'https://10.208.176.251/ftthplanner/api/detail_redaman.php',
method: 'GET',
data: { id: client.id },
success: function(res) {
let el = $(`#redaman-${client.id}`);
if (res.success && res.data && res.data.rx_power && res.data.rx_power.toLowerCase() !== "n/a") {
el.removeClass('red-blink')
.addClass('green-stable')
.text(`${res.data.rx_power}`);
} else {
el.removeClass('green-stable')
.addClass('red-blink')
.text('Tidak tersedia');
}
},
error: function() {
$(`#redaman-${client.id}`)
.removeClass('green-stable')
.addClass('red-blink')
.text('Gagal memuat');
}
});
});
html += "</ul>";
} else {
html += "<em>Tidak ada pelanggan</em>";
}
// Node Terhubung
html += `<br><span class="info-label">Terhubung KE:</span>`;
if (nodeList.length > 0) {
html += "<ul>";
nodeList.forEach(client => {
let label = client.type.toUpperCase();
html += `<li>${client.name} <span class="badge badge-info">${label}</span></li>`;
});
html += "</ul>";
} else {
html += "<em>Tidak ada</em>";
}
}
// =========================
// 🔹 Untuk ODC
else if (isODC) {
// ODP Terhubung
html += `<span class="info-label">ODP Terhubung:</span>`;
let odpList = nodeList.filter(c => c.type === 'odp');
if (odpList.length > 0) {
html += "<ul>";
odpList.forEach(client => {
html += `<li>${client.name} <span class="badge badge-secondary">ODP</span></li>`;
});
html += "</ul>";
} else {
html += "<em>Tidak ada ODP</em>";
}
// Terhubung (OLT/ODC/TIANG lain)
html += `<br><span class="info-label">Terhubung KE:</span>`;
let otherNodes = nodeList.filter(c => c.type !== 'odp');
if (otherNodes.length > 0) {
html += "<ul>";
otherNodes.forEach(client => {
let label = client.type.toUpperCase();
html += `<li>${client.name} <span class="badge badge-primary">${label}</span></li>`;
});
html += "</ul>";
} else {
html += "<em>Tidak ada</em>";
}
}
// =========================
// 🔹 Untuk OLT
else if (isOLT) {
console.log("🔍 ConnectedClients (OLT):", connectedClients);
console.log("🔍 NodeList (OLT):", nodeList);
html += `<span class="info-label">Node Terhubung:</span>`;
if (nodeList.length > 0) {
html += "<ul>";
nodeList.forEach(client => {
console.log(`➡️ Client OLT: id=${client.id}, name=${client.name}, type=${client.type}`);
let label = (client.type === 'odc') ? "ODC" : client.type.toUpperCase();
html += `<li>${client.name} <span class="badge badge-primary">${label}</span></li>`;
});
html += "</ul>";
} else {
console.warn("⚠️ Tidak ada nodeList untuk OLT:", currentItem);
html += "<em>Tidak ada node terhubung</em>";
}
}
// 🔹 Render ke halaman
$(`#connected-clients-${itemId}`).html(html);
} else {
$(`#connected-clients-${itemId}`).html("<em>Tidak ada yang terhubung</em>");
}
},
error: function() {
$(`#connected-clients-${itemId}`).html("<em>Gagal memuat</em>");
}
});
}
// Get item icon based on type (moved to bottom for global export)
function loadRedaman(itemId) {
$.ajax({
url: 'https://10.208.176.251/ftthplanner/api/get_onu_volt.php',
method: 'GET',
data: { id: itemId },
success: function(response) {
if (response.success && response.data) {
let rx = parseFloat(response.data.rx_power);
let status = getRxStatus(rx);
$(`#redaman-${itemId}`).html(`
<span class="info-label">Redaman:</span>
<span style="color:${status.color}; font-weight:bold;">
${rx} dBm
</span>
<br>
<small>${status.label} (${status.batas})</small>
`);
} else {
$(`#redaman-${itemId}`).html(`
<span class="info-label">Redaman:</span>
<em>Tidak tersedia</em>
`);
}
},
error: function() {
$(`#redaman-${itemId}`).html(`
<span class="info-label">Redaman:</span>
<em>Gagal memuat</em>
`);
}
});
}
function loadVoltage(itemId) {
$.ajax({
url: 'https://10.208.176.251/ftthplanner/api/get_onu_volt.php',
method: 'GET',
data: { id: itemId },
success: function(response) {
if (response.success && response.data) {
let v = parseFloat(response.data.voltage);
let status = getVoltStatus(v);
$(`#voltage-${itemId}`).html(`
<span class="info-label">Voltage:</span>
<span style="color:${status.color}; font-weight:bold;">
${v} V
</span>
<br>
<small>${status.label} (${status.batas})</small>
`);
} else {
$(`#voltage-${itemId}`).html(`
<span class="info-label">Voltage:</span>
<em>Tidak tersedia</em>
`);
}
},
error: function() {
$(`#voltage-${itemId}`).html(`
<span class="info-label">Voltage:</span>
<em>Gagal memuat</em>
`);
}
});
}
function loadOnuStatus(itemId) {
$.ajax({
url: 'https://10.208.176.251/ftthplanner/api/get_onu_by_item.php',
method: 'GET',
data: { id: itemId },
success: function(response) {
if (response.success && response.data) {
let status = (response.data.status || 'unknown').toUpperCase();
let lastUpdate = response.data.last_update || '-';
updateMarkerStatus(itemId, status);
let color = 'gray';
let label = status;
// 🔥 LOGIKA STATUS DETAIL
if (status === 'ONLINE') {
color = 'green';
label = 'ONLINE';
}
else if (status === 'POWER FAIL') {
color = 'orange';
label = 'POWER FAIL ⚡';
}
else if (status === 'LOS' || status === 'LOSS' || status === 'LASER OUT') {
color = 'red';
label = 'LOSS / FO CUT 🔥';
}
else {
color = 'gray';
label = status;
}
// tampilkan
$(`#onu-status-${itemId}`).html(
`<span class="info-label">ONU Status:</span>
<b style="color:${color}">${label}</b>`
);
$(`#last-update-${itemId}`).html(
`<span class="info-label">Last Update:</span> ${lastUpdate}`
);
} else {
$(`#onu-status-${itemId}`).html(
`<span class="info-label">ONU Status:</span> <em>Tidak tersedia</em>`
);
$(`#last-update-${itemId}`).html(
`<span class="info-label">Last Update:</span> <em>-</em>`
);
}
},
error: function() {
$(`#onu-status-${itemId}`).html(
`<span class="info-label">ONU Status:</span> <em>Gagal</em>`
);
$(`#last-update-${itemId}`).html(
`<span class="info-label">Last Update:</span> <em>Gagal</em>`
);
}
});
}
// Get status badge class
function getStatusBadgeClass(status) {
switch(status) {
case 'active': return 'success';
case 'inactive': return 'secondary';
case 'maintenance': return 'warning';
default: return 'primary';
}
}
// Get status text
function getStatusText(status) {
switch(status) {
case 'active': return 'Aktif';
case 'inactive': return 'Tidak Aktif';
case 'maintenance': return 'Maintenance';
default: return status;
}
}
// Load items from database
function loadItems(callback) {
$.ajax({
url: 'api/items.php',
method: 'GET',
dataType: 'json',
success: function(response) {
if (response.success && Array.isArray(response.data)) {
window.items = response.data;
response.data.forEach(function(item) {
addMarkerToMap(item);
});
updateStatistics();
if (typeof callback === "function") callback();
}
},
error: function() {
showNotification('Error loading items', 'error');
window.items = [];
if (typeof callback === "function") callback();
}
});
}
// Add marker to map
// Add marker to map
function addMarkerToMap(item) {
let itemTypeColors = {
'OLT': '#FF6B6B',
'Tiang Tumpu': '#00ff55ff',
'ODP': '#e84bd6ff',
'ODC': '#7500a7ff',
'Pelanggan': '#ffbb00ff'
};
let color = itemTypeColors[item.item_type_name] || '#999';
let icon = createCustomIcon(item.item_type_name, color);
let marker = L.marker([item.latitude, item.longitude], {
icon: icon,
draggable: false,
itemType: item.item_type_name,
itemId: item.id,
itemData: item
}).addTo(map);
marker.bindPopup(createPopupContent(item));
// 🔥 INI SUDAH ADA (TIDAK DIUBAH)
marker.on('popupopen', function () {
loadRedaman(item.id);
loadVoltage(item.id);
loadOnuStatus(item.id); // ← nanti update warna di sini
if (!window.items || window.items.length === 0) {
loadItems(function() {
loadConnectedClientsFromRoutes(item.id);
});
} else {
loadConnectedClientsFromRoutes(item.id);
}
});
marker.on('click', function(e) {
if (isRoutingMode) {
handleRoutingClick(item);
e.originalEvent.stopPropagation();
}
});
// 🔥 simpan marker (WAJIB untuk update warna)
markers[item.id] = marker;
}
//function animasi status
function updateMarkerStatus(itemId, status) {
let marker = markers[itemId];
if (!marker) return;
let el = marker._icon;
if (!el) return;
// reset class
el.classList.remove('status-online', 'status-power', 'status-los');
status = (status || '').toUpperCase();
if (status === 'ONLINE') {
el.classList.add('status-online');
}
else if (status === 'POWER FAIL') {
el.classList.add('status-power');
}
else if (status === 'LOS' || status === 'LOSS' || status === 'LASER OUT') {
el.classList.add('status-los');
}
}
// Update item position
function updateItemPosition(itemId, lat, lng) {
// Use FormData with method override for consistency
let formData = new FormData();
formData.append('_method', 'PUT');
formData.append('id', itemId);
formData.append('latitude', lat);
formData.append('longitude', lng);
$.ajax({
url: 'api/items.php',
method: 'POST',
data: formData,
processData: false,
contentType: false,
dataType: 'json',
success: function(response) {
if (response && response.success) {
showNotification('Posisi item berhasil dipindahkan', 'success');
} else {
showNotification(response?.message || 'Error updating position', 'error');
}
},
error: function(xhr, status, error) {
console.error('Position update error:', error, xhr.responseText);
showNotification('Error updating position: ' + error, 'error');
}
});
}
// Start routing mode
function startRouting(itemId) {
isRoutingMode = true;
routingFromItem = itemId;
map.getContainer().style.cursor = 'crosshair';
showNotification('Pilih item tujuan untuk membuat route', 'info');
}
function startManualRouting(itemId) {
isManualRouting = true;
manualFromItem = itemId;
map.getContainer().style.cursor = 'crosshair';
showNotification('Klik item tujuan untuk membuat route manual', 'info');
}
// Handle routing click
function handleRoutingClick(toItem) {
if (routingFromItem && routingFromItem !== toItem.id) {
createRoute(routingFromItem, toItem.id);
exitRoutingMode();
}
}
function handleRoutingClick(toItem) {
// Jika mode manual aktif
if (isManualRouting && manualFromItem && manualFromItem !== toItem.id) {
createManualRoute(manualFromItem, toItem.id);
exitManualRoutingMode();
return;
}
// Mode otomatis (default)
if (routingFromItem && routingFromItem !== toItem.id) {
createRoute(routingFromItem, toItem.id);
exitRoutingMode();
}
}
// Exit routing mode
function exitRoutingMode() {
isRoutingMode = false;
routingFromItem = null;
map.getContainer().style.cursor = '';
}
//manual route by dwijalo
function exitManualRoutingMode() {
isManualRouting = false;
manualFromItem = null;
map.getContainer().style.cursor = '';
}
// Create route between two items
function createRoute(fromItemId, toItemId) {
let fromMarker = markers[fromItemId];
let toMarker = markers[toItemId];
if (!fromMarker || !toMarker) {
showNotification('Marker tidak ditemukan', 'error');
return;
}
let fromPos = fromMarker.getLatLng();
let toPos = toMarker.getLatLng();
console.log('Creating route from', fromPos, 'to', toPos);
// Check if Leaflet Routing Machine is available
if (typeof L.Routing === 'undefined') {
console.log('Leaflet Routing Machine not available, creating simple line');
// Create simple straight line if routing machine not available
createSimpleRoute(fromItemId, toItemId, fromPos, toPos);
return;
}
try {
// Use routing machine to create route following roads
let routing = L.Routing.control({
waypoints: [fromPos, toPos],
routeWhileDragging: false,
show: false,
createMarker: function() { return null; }, // Don't create default markers
addWaypoints: false,
draggableWaypoints: false,
fitSelectedRoutes: false
});
routing.on('routesfound', function(e) {
console.log('Route found:', e.routes[0]);
let route = e.routes[0];
let coordinates = route.coordinates;
// Save route to database
$.ajax({
url: 'api/routes.php',
method: 'POST',
data: {
from_item_id: fromItemId,
to_item_id: toItemId,
route_coordinates: JSON.stringify(coordinates),
distance: route.summary.totalDistance,
cable_type: 'Fiber Optic',
core_count: 24,
status: 'planned'
},
success: function(response) {
console.log('Route save response:', response);
if (response.success) {
// Add route line to map
let routeLine = L.polyline(coordinates, {
color: '#64c5f2ff',
weight: 4,
opacity: 0.8,
dashArray: '10, 5'
}).addTo(map);
// Add popup to route
routeLine.bindPopup(`
<div>
<h6>Route Kabel</h6>
<p><strong>Jarak:</strong> ${(route.summary.totalDistance / 1000).toFixed(2)} km</p>
<p><strong>Tipe Kabel:</strong> Fiber Optic</p>
<p><strong>Jumlah Core:</strong> 24</p>
<p><strong>Status:</strong> Perencanaan</p>
</div>
`);
routes[response.route_id] = routeLine;
showNotification('Route berhasil dibuat', 'success');
// Remove routing control
map.removeControl(routing);
} else {
showNotification(response.message || 'Gagal menyimpan route', 'error');
}
},
error: function(xhr, status, error) {
console.error('Error saving route:', error);
showNotification('Error menyimpan route: ' + error, 'error');
}
});
});
routing.on('routingerror', function(e) {
console.error('Routing error:', e.error);
showNotification('Error routing: ' + e.error.message, 'error');
// Fallback to simple line
createSimpleRoute(fromItemId, toItemId, fromPos, toPos);
});
routing.addTo(map);
} catch (error) {
console.error('Error creating route:', error);
showNotification('Error creating route, using simple line', 'warning');
createSimpleRoute(fromItemId, toItemId, fromPos, toPos);
}
}
// Create simple straight line route (fallback)
function createSimpleRoute(fromItemId, toItemId, fromPos, toPos) {
let coordinates = [[fromPos.lat, fromPos.lng], [toPos.lat, toPos.lng]];
let distance = fromPos.distanceTo(toPos);
$.ajax({
url: 'api/routes.php',
method: 'POST',
data: {
from_item_id: fromItemId,
to_item_id: toItemId,
route_coordinates: JSON.stringify(coordinates),
distance: distance,
cable_type: 'Fiber Optic',
core_count: 24,
status: 'planned'
},
success: function(response) {
if (response.success) {
// Add route line to map
let routeLine = L.polyline(coordinates, {
color: '#64c5f2ff',
weight: 4,
opacity: 0.8,
dashArray: '10, 5'
}).addTo(map);
routeLine.bindPopup(`
<div>
<h6>Route Kabel (Direct)</h6>
<p><strong>Jarak:</strong> ${(distance / 1000).toFixed(2)} km</p>
<p><strong>Tipe Kabel:</strong> Fiber Optic</p>
<p><strong>Jumlah Core:</strong> 24</p>
<p><strong>Status:</strong> Perencanaan</p>
</div>
`);
routes[response.route_id] = routeLine;
showNotification('Route sederhana berhasil dibuat', 'success');
}
},
error: function() {
showNotification('Error menyimpan route', 'error');
}
});
}
//manual route by dwijalo
function createManualRoute(fromItemId, toItemId) {
let fromMarker = markers[fromItemId];
let toMarker = markers[toItemId];
if (!fromMarker || !toMarker) {
showNotification('Marker tidak ditemukan', 'error');
return;
}
let fromPos = fromMarker.getLatLng();
let toPos = toMarker.getLatLng();
// Buat koordinat garis lurus antar dua titik
let coordinates = [[fromPos.lat, fromPos.lng], [toPos.lat, toPos.lng]];
let distance = fromPos.distanceTo(toPos);
// Simpan ke database
$.ajax({
url: 'api/routes.php',
method: 'POST',
data: {
from_item_id: fromItemId,
to_item_id: toItemId,
route_coordinates: JSON.stringify(coordinates),
distance: distance,
cable_type: 'Fiber Optic',
core_count: 24,
status: 'planned'
},
success: function(response) {
if (response.success) {
// Tambahkan kabel dengan animasi berjalan
let routeLine = L.polyline(coordinates, {
color: '#64c5f2ff', // Warna kabel manual
weight: 5,
opacity: 0.9,
dashArray: '15, 10', // Pola strip kabel
className: 'animated-route' // Untuk CSS animasi
}).addTo(map);
// Tambahkan popup detail ke polyline manual
routeLine.bindPopup(`
<div>
<h6>Route Kabel Manual</h6>
<p><strong>Jarak:</strong> ${(distance / 1000).toFixed(2)} km</p>
<p><strong>Tipe Kabel:</strong> Fiber Optic</p>
<p><strong>Jumlah Core:</strong> 24</p>
<p><strong>Status:</strong> Perencanaan</p>
</div>
`);
// Simpan reference route manual di global routes agar bisa dikelola
routes[response.route_id] = routeLine;
showNotification('Route manual berhasil dibuat', 'success');
} else {
showNotification('Gagal membuat route manual', 'error');
}
},
error: function() {
showNotification('Error menyimpan route manual', 'error');
}
});
}
//manual route by dwijalo
function makeRouteBlink(routeLine) {
let visible = true;
setInterval(() => {
if (visible) {
routeLine.setStyle({ opacity: 0.1 });
} else {
routeLine.setStyle({ opacity: 1 });
}
visible = !visible;
}, 500); // 500ms = setengah detik sekali berkedip
}
// Load routes from database
function loadRoutes() {
$.ajax({
url: 'api/routes.php',
method: 'GET',
success: function(response) {
if (response.success) {
response.data.forEach(function(route) {
if (route.route_coordinates) {
let coordinates = JSON.parse(route.route_coordinates);
let color = getRouteColor(route.status);
// Tambahkan dashArray + className untuk animasi
let routeLine = L.polyline(coordinates, {
color: color,
weight: 5,
opacity: 0.9,
dashArray: '15, 10', // Default animasi
className: 'animated-route' // <- Kunci animasi berjalan
}).addTo(map);
// Tambahkan popup informasi kabel
routeLine.bindPopup(`
<div>
<h6>Route Kabel</h6>
<p><strong>Jarak:</strong> ${(route.distance / 1000).toFixed(2)} km</p>
<p><strong>Tipe Kabel:</strong> ${route.cable_type}</p>
<p><strong>Jumlah Core:</strong> ${route.core_count}</p>
<p><strong>Status:</strong> ${getStatusText(route.status)}</p>
</div>
`);
// Simpan route di objek global
routes[route.id] = routeLine;
// Jika kabel offline / loss → bikin blink
if (route.status === 'offline' || route.status === 'loss') {
makeRouteBlink(routeLine);
}
}
});
}
}
});
}
// Get route color based on status
function getRouteColor(status) {
switch(status) {
case 'installed': return '#32beffff';
case 'planned': return '#64c5f2ff';
case 'maintenance': return '#dc3545';
default: return '#6c757d';
}
}
// Add map legend
//buatan saya
// Update statistics
function updateStatistics() {
$.ajax({
url: 'api/statistics.php',
method: 'GET',
success: function(response) {
if (response.success) {
$('#stat-olt').text(response.data.olt || 0);
$('#stat-tiang').text(response.data.tiang_tumpu || 0);
$('#stat-odp').text(response.data.odp || 0);
$('#stat-odc').text(response.data.odc || 0);
$('#stat-pelanggan').text(response.data.pelanggan || 0);
$('#stat-routes').text(response.data.total_routes || 0);
}
}
});
}
// Show notification
function showNotification(message, type) {
let alertClass = 'alert-info';
switch(type) {
case 'success': alertClass = 'alert-success'; break;
case 'error': alertClass = 'alert-danger'; break;
case 'warning': alertClass = 'alert-warning'; break;
}
let notification = `
<div class="alert ${alertClass} alert-dismissible fade show" role="alert" style="position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 300px;">
${message}
<button type="button" class="close" data-dismiss="alert">
<span>&times;</span>
</button>
</div>
`;
$('body').append(notification);
setTimeout(function() {
$('.alert').fadeOut();
}, 5000);
}
// Show routing mode
function showRoutingMode() {
if (isRoutingMode) {
exitRoutingMode();
showNotification('Mode routing dinonaktifkan', 'info');
} else {
isRoutingMode = true;
map.getContainer().style.cursor = 'crosshair';
showNotification('Mode routing aktif. Klik dua item untuk membuat route.', 'info');
}
}
// Zoom to specific bounds
function zoomToItems() {
if (Object.keys(markers).length > 0) {
const group = new L.featureGroup(Object.values(markers));
map.fitBounds(group.getBounds().pad(0.1));
} else {
showNotification('Tidak ada item untuk di-zoom', 'warning');
}
}
// Zoom to specific item type
function zoomToItemType(itemType) {
const filteredMarkers = Object.values(markers).filter(marker => {
return marker.options && marker.options.itemType === itemType;
});
if (filteredMarkers.length > 0) {
const group = new L.featureGroup(filteredMarkers);
map.fitBounds(group.getBounds().pad(0.1));
// Highlight markers of this type temporarily
filteredMarkers.forEach(marker => {
if (marker._icon) {
marker._icon.style.transform += ' scale(1.3)';
marker._icon.style.zIndex = '1000';
setTimeout(() => {
marker._icon.style.transform = marker._icon.style.transform.replace(' scale(1.3)', '');
marker._icon.style.zIndex = '';
}, 2000);
}
});
showNotification(`Menampilkan ${filteredMarkers.length} ${itemType}`, 'success');
} else {
showNotification(`Tidak ada ${itemType} ditemukan`, 'info');
}
}
// Enhanced locate user function
function locateUser() {
if (navigator.geolocation) {
map.locate({
setView: true,
maxZoom: 16,
enableHighAccuracy: true,
timeout: 10000
});
map.on('locationfound', function(e) {
L.circle(e.latlng, e.accuracy).addTo(map)
.bindPopup('Anda berada di sekitar area ini').openPopup();
showNotification('Lokasi berhasil ditemukan', 'success');
});
map.on('locationerror', function(e) {
showNotification('Gagal menemukan lokasi: ' + e.message, 'error');
});
} else {
showNotification('Geolocation tidak didukung browser ini', 'error');
}
}
// Add keyboard shortcuts for zoom
function addKeyboardShortcuts() {
document.addEventListener('keydown', function(e) {
if (e.target.tagName.toLowerCase() === 'input' || e.target.tagName.toLowerCase() === 'textarea') {
return; // Don't interfere with form inputs
}
switch(e.key) {
case '+':
case '=':
map.zoomIn();
break;
case '-':
map.zoomOut();
break;
case 'h':
case 'H':
map.setView([-2.5, 118], 5); // Home to Indonesia
break;
case 'f':
case 'F':
if (map.isFullscreen && map.isFullscreen()) {
map.toggleFullscreen();
} else if (map.toggleFullscreen) {
map.toggleFullscreen();
}
break;
case 'l':
case 'L':
locateUser();
break;
case 'a':
case 'A':
zoomToItems();
break;
}
});
}
// Enhanced map ready function
function onMapReady() {
addKeyboardShortcuts();
// Add help tooltip
const helpControl = L.control({position: 'bottomright'});
helpControl.onAdd = function(map) {
const div = L.DomUtil.create('div', 'leaflet-control-help');
div.innerHTML = '<i class="fas fa-question-circle" title="Shortcuts: +/- zoom, H home, F fullscreen, L locate, A zoom to all"></i>';
div.style.background = 'rgba(255,255,255,0.8)';
div.style.padding = '5px';
div.style.borderRadius = '3px';
div.style.cursor = 'help';
return div;
};
helpControl.addTo(map);
console.log('🎮 Map keyboard shortcuts enabled: +/- zoom, H home, F fullscreen, L locate, A zoom to all');
}
// Helper functions needed by detail modal
function getItemIcon(typeName) {
switch(typeName) {
case 'OLT': return 'fas fa-cloud';
case 'Tiang Tumpu': return 'fas fa-tower-broadcast';
case 'ODP': return 'fas fa-project-diagram';
case 'ODC': return 'fas fa-network-wired';
case 'Pelanggan': return 'fas fa-wifi';
default: return 'fas fa-circle';
}
}
function getStatusBadgeClass(status) {
switch(status) {
case 'active': return 'success';
case 'inactive': return 'secondary';
case 'maintenance': return 'warning';
default: return 'secondary';
}
}
function getStatusText(status) {
switch(status) {
case 'active': return 'Aktif';
case 'inactive': return 'Tidak Aktif';
case 'maintenance': return 'Maintenance';
default: return status || 'Unknown';
}
}
// Export functions to global scope for button access
window.zoomToItems = zoomToItems;
window.zoomToItemType = zoomToItemType;
window.locateUser = locateUser;
window.loadRoutes = loadRoutes;
window.getItemIcon = getItemIcon;
window.getStatusBadgeClass = getStatusBadgeClass;
window.getStatusText = getStatusText;
// Initialize map when document is ready
$(document).ready(function() {
initMap();
setTimeout(onMapReady, 1000); // Wait for map to fully initialize
});