// 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: '© OpenStreetMap contributors', maxZoom: 19 }), "CartoDB Positron": L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap contributors © CARTO', maxZoom: 20 }), "CartoDB Dark": L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { attribution: '© OpenStreetMap contributors © CARTO', 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: © OpenStreetMap contributors, SRTM | Map style: © OpenTopoMap (CC-BY-SA)', 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 = ''; 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 = ' 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: `
`, 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 = `
Pelanggan Terhubung: Memuat...
`; } let html = `
${item.name}
`; 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 += `Pelanggan Terhubung:`; if (pelangganList.length > 0) { html += ""; } else { html += "Tidak ada pelanggan"; } // Node Terhubung html += `
Terhubung KE:`; if (nodeList.length > 0) { html += ""; } else { html += "Tidak ada"; } } // ========================= // 🔹 Untuk ODC else if (isODC) { // ODP Terhubung html += `ODP Terhubung:`; let odpList = nodeList.filter(c => c.type === 'odp'); if (odpList.length > 0) { html += ""; } else { html += "Tidak ada ODP"; } // Terhubung (OLT/ODC/TIANG lain) html += `
Terhubung KE:`; let otherNodes = nodeList.filter(c => c.type !== 'odp'); if (otherNodes.length > 0) { html += ""; } else { html += "Tidak ada"; } } // ========================= // 🔹 Untuk OLT else if (isOLT) { console.log("🔍 ConnectedClients (OLT):", connectedClients); console.log("🔍 NodeList (OLT):", nodeList); html += `Node Terhubung:`; if (nodeList.length > 0) { html += ""; } else { console.warn("⚠️ Tidak ada nodeList untuk OLT:", currentItem); html += "Tidak ada node terhubung"; } } // 🔹 Render ke halaman $(`#connected-clients-${itemId}`).html(html); } else { $(`#connected-clients-${itemId}`).html("Tidak ada yang terhubung"); } }, error: function() { $(`#connected-clients-${itemId}`).html("Gagal memuat"); } }); } // 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(` Redaman: ${rx} dBm
${status.label} (${status.batas}) `); } else { $(`#redaman-${itemId}`).html(` Redaman: Tidak tersedia `); } }, error: function() { $(`#redaman-${itemId}`).html(` Redaman: Gagal memuat `); } }); } 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(` Voltage: ${v} V
${status.label} (${status.batas}) `); } else { $(`#voltage-${itemId}`).html(` Voltage: Tidak tersedia `); } }, error: function() { $(`#voltage-${itemId}`).html(` Voltage: Gagal memuat `); } }); } 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( `ONU Status: ${label}` ); $(`#last-update-${itemId}`).html( `Last Update: ${lastUpdate}` ); } else { $(`#onu-status-${itemId}`).html( `ONU Status: Tidak tersedia` ); $(`#last-update-${itemId}`).html( `Last Update: -` ); } }, error: function() { $(`#onu-status-${itemId}`).html( `ONU Status: Gagal` ); $(`#last-update-${itemId}`).html( `Last Update: Gagal` ); } }); } // 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(`
Route Kabel

Jarak: ${(route.summary.totalDistance / 1000).toFixed(2)} km

Tipe Kabel: Fiber Optic

Jumlah Core: 24

Status: Perencanaan

`); 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(`
Route Kabel (Direct)

Jarak: ${(distance / 1000).toFixed(2)} km

Tipe Kabel: Fiber Optic

Jumlah Core: 24

Status: Perencanaan

`); 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(`
Route Kabel Manual

Jarak: ${(distance / 1000).toFixed(2)} km

Tipe Kabel: Fiber Optic

Jumlah Core: 24

Status: Perencanaan

`); // 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(`
Route Kabel

Jarak: ${(route.distance / 1000).toFixed(2)} km

Tipe Kabel: ${route.cable_type}

Jumlah Core: ${route.core_count}

Status: ${getStatusText(route.status)}

`); // 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 = ` `; $('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 = ''; 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 });