// Map.js - FTTH Planner Map Functionality let map; let markers = {}; let routes = {}; let isRoutingMode = false; let routingFromItem = null; let currentRoutes = []; // 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 lebih kecil container.style.display = 'flex'; container.style.alignItems = 'center'; container.style.padding = '4px 8px'; container.style.gap = '6px'; container.style.minWidth = '160px'; container.style.width = '28vw'; container.style.maxWidth = '280px'; container.style.boxSizing = 'border-box'; // Buat icon pencarian var icon = L.DomUtil.create('span', '', container); icon.innerHTML = '🔍'; icon.style.fontSize = '14px'; // Buat input 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"; // 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'; 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'; // kasih jarak 50px biar gak nabrak tombol fullscreen searchEl.style.background = 'white'; searchEl.style.borderRadius = '4px'; searchEl.style.boxShadow = '0 2px 6px rgba(0,0,0,0.3)'; 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'; switch(itemType) { case 'OLT': case '1': iconClass = 'fas fa-server'; break; case 'Tiang Tumpu': case '2': iconClass = 'fas fa-tower-broadcast'; break; case 'ODP': case '3': iconClass = 'fas fa-project-diagram'; break; case 'ODC': case '4': iconClass = 'fas fa-network-wired'; break; case 'Pelanggan': case '5': iconClass = 'fas fa-home'; break; } // Default RX Power dulu let rxPower = "Memuat..."; return L.divIcon({ className: 'custom-div-icon', html: `
${rxPower}
`, iconSize: [40, 50], iconAnchor: [20, 40], popupAnchor: [0, -25] }); } // 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() : ''; let html = `
${item.name}
`; // Ambil RX Power setelah popup ada di DOM // Lebih baik menggunakan requestAnimationFrame untuk memastikan DOM sudah diupdate return html; } // Get item icon based on type (moved to bottom for global export) // 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() { $.ajax({ url: 'api/items.php', method: 'GET', success: function(response) { if (response.success) { response.data.forEach(function(item) { addMarkerToMap(item); }); updateStatistics(); } }, error: function() { showNotification('Error loading items', 'error'); } }); } // Add marker to map function addMarkerToMap(item) { let itemTypeColors = { 'OLT': '#FF6B6B', 'Tiang Tumpu': '#4ECDC4', 'ODP': '#45B7D1', 'ODC': '#96CEB4', 'Pelanggan': '#FFA500' }; 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: true, itemType: item.item_type_name, itemId: item.id, itemData: item }).addTo(map); marker.bindPopup(createPopupContent(item)); // Add drag event marker.on('dragend', function(e) { let newPos = e.target.getLatLng(); updateItemPosition(item.id, newPos.lat, newPos.lng); }); // Add click event for routing mode marker.on('click', function(e) { if (isRoutingMode) { handleRoutingClick(item); e.originalEvent.stopPropagation(); } }); markers[item.id] = marker; } // 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'); } // Handle routing click function handleRoutingClick(toItem) { if (routingFromItem && routingFromItem !== toItem.id) { createRoute(routingFromItem, toItem.id); exitRoutingMode(); } } // Exit routing mode function exitRoutingMode() { isRoutingMode = false; routingFromItem = 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: '#ffc107', 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: '#ffc107', 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'); } }); } // 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); let dashArray = route.status === 'installed' ? null : '10, 5'; let routeLine = L.polyline(coordinates, { color: color, weight: 4, opacity: 0.8, dashArray: dashArray }).addTo(map); 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)}

`); routes[route.id] = routeLine; } }); } } }); } // Get route color based on status function getRouteColor(status) { switch(status) { case 'installed': return '#28a745'; case 'planned': return '#ffc107'; case 'maintenance': return '#dc3545'; default: return '#6c757d'; } } // Add map legend function addMapLegend() { let legend = L.control({position: 'bottomleft'}); legend.onAdd = function(map) { let div = L.DomUtil.create('div', 'map-legend'); div.innerHTML = `
Legend
OLT
Tiang Tumpu
ODP
ODC
Pelanggan

━━━ Terpasang
┅┅┅ Perencanaan
┅┅┅ Maintenance
`; return div; }; legend.addTo(map); } //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-server'; 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-home'; 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 });