1573 lines
54 KiB
JavaScript
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: '© <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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <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 © Esri — 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: © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, <a href="http://viewfinderpanoramas.org">SRTM</a> | Map style: © <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: '© 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>×</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
|
|
});
|