WEB_PROJECT_TA/assets/js/map.js

973 lines
34 KiB
JavaScript

// 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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19
}),
"CartoDB Positron": L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
maxZoom: 20
}),
"CartoDB Dark": L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
maxZoom: 20
}),
"Satellite": L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
attribution: 'Tiles &copy; Esri &mdash; Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community',
maxZoom: 20
}),
"Terrain": L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
attribution: 'Map data: &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, <a href="http://viewfinderpanoramas.org">SRTM</a> | Map style: &copy; <a href="https://opentopomap.org">OpenTopoMap</a> (<a href="https://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA</a>)',
maxZoom: 17
}),
"Google Hybrid": L.tileLayer('https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', {
attribution: '&copy; Google',
maxZoom: 20
})
};
// Add default layer (OpenStreetMap)
tileLayers["OpenStreetMap"].addTo(map);
// Add layer control
L.control.layers(tileLayers, null, {
position: 'topright',
collapsed: false
}).addTo(map);
// Add custom zoom control with home button
const zoomControl = L.control.zoom({
position: 'topleft'
}).addTo(map);
// Add home button to zoom control
const homeControl = L.Control.extend({
options: {
position: 'topleft'
},
onAdd: function(map) {
const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control leaflet-control-custom');
container.style.backgroundColor = 'white';
container.style.backgroundImage = 'none';
container.style.width = '26px';
container.style.height = '26px';
container.style.cursor = 'pointer';
container.innerHTML = '<i class="fas fa-home" 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';
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: `
<div class="custom-marker marker-${itemType.toLowerCase().replace(' ', '')}"
style="background-color: ${color}; width: 30px; height: 30px;
border-radius: 50%; display: flex; flex-direction: column;
align-items: center; justify-content: flex-start; border: 3px solid white;
box-shadow: 0 2px 5px rgba(255, 255, 255, 0); position: relative;">
<div class="rx-power-box"
style="width: 40px; height: 16px; margin-bottom: 4px;
border: 1px solid white; background-color: rgba(0, 0, 0, 1);
color: white; font-size: 10px; text-align: center;
position: absolute; top: -18px; left: 50%; transform: translateX(-50%);">
${rxPower}
</div>
<i class="${iconClass}" style="color: white; font-size: 14px;"></i>
</div>
`,
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 = `
<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">Warna Tube:</span> ${tubeColorName}</div>
${item.core_used ? `<div class="info-row"><span class="info-label">Core Digunakan:</span> ${item.core_used}</div>` : ''}
<div class="info-row"><span class="info-label">Splitter Utama:</span> ${splitterMain}</div>
<div class="info-row"><span class="info-label">Splitter ODP:</span> ${splitterOdp}</div>
<div class="info-row"><span class="info-label">Status:</span>
<span class="badge badge-${getStatusBadgeClass(item.status)}">${getStatusText(item.status)}</span>
</div>
<div class="popup-actions">
<button class="btn btn-info btn-sm" onclick="loadRedamanDetail(${item.id})"> Detail Redaman</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-danger btn-sm" onclick="deleteItem(${item.id})"><i class="fas fa-trash"></i> Hapus</button>
</div>
</div>
`;
// 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(`
<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: '#ffc107',
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');
}
});
}
// 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(`
<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>
`);
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 = `
<h6 style="margin-bottom: 10px; font-weight: bold;">Legend</h6>
<div class="legend-item">
<div class="legend-icon" style="background-color: #FF6B6B;"><i class="fas fa-server" style="color: white; font-size: 10px;"></i></div>
<span>OLT</span>
</div>
<div class="legend-item">
<div class="legend-icon" style="background-color: #4ECDC4;"><i class="fas fa-tower-broadcast" style="color: white; font-size: 10px;"></i></div>
<span>Tiang Tumpu</span>
</div>
<div class="legend-item">
<div class="legend-icon" style="background-color: #45B7D1;"><i class="fas fa-project-diagram" style="color: white; font-size: 10px;"></i></div>
<span>ODP</span>
</div>
<div class="legend-item">
<div class="legend-icon" style="background-color: #96CEB4;"><i class="fas fa-network-wired" style="color: white; font-size: 10px;"></i></div>
<span>ODC</span>
</div>
<div class="legend-item">
<div class="legend-icon" style="background-color: #FFA500;"><i class="fas fa-home" style="color: white; font-size: 10px;"></i></div>
<span>Pelanggan</span>
</div>
<hr style="margin: 10px 0;">
<div style="font-size: 12px;">
<div><span style="border-bottom: 3px solid #28a745; padding-bottom: 1px;">━━━</span> Terpasang</div>
<div><span style="border-bottom: 3px dashed #ffc107; padding-bottom: 1px;">┅┅┅</span> Perencanaan</div>
<div><span style="border-bottom: 3px dashed #dc3545; padding-bottom: 1px;">┅┅┅</span> Maintenance</div>
</div>
`;
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 = `
<div class="alert ${alertClass} alert-dismissible fade show" role="alert" style="position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 300px;">
${message}
<button type="button" class="close" data-dismiss="alert">
<span>&times;</span>
</button>
</div>
`;
$('body').append(notification);
setTimeout(function() {
$('.alert').fadeOut();
}, 5000);
}
// Show routing mode
function showRoutingMode() {
if (isRoutingMode) {
exitRoutingMode();
showNotification('Mode routing dinonaktifkan', 'info');
} else {
isRoutingMode = true;
map.getContainer().style.cursor = 'crosshair';
showNotification('Mode routing aktif. Klik dua item untuk membuat route.', 'info');
}
}
// Zoom to specific bounds
function zoomToItems() {
if (Object.keys(markers).length > 0) {
const group = new L.featureGroup(Object.values(markers));
map.fitBounds(group.getBounds().pad(0.1));
} else {
showNotification('Tidak ada item untuk di-zoom', 'warning');
}
}
// Zoom to specific item type
function zoomToItemType(itemType) {
const filteredMarkers = Object.values(markers).filter(marker => {
return marker.options && marker.options.itemType === itemType;
});
if (filteredMarkers.length > 0) {
const group = new L.featureGroup(filteredMarkers);
map.fitBounds(group.getBounds().pad(0.1));
// Highlight markers of this type temporarily
filteredMarkers.forEach(marker => {
if (marker._icon) {
marker._icon.style.transform += ' scale(1.3)';
marker._icon.style.zIndex = '1000';
setTimeout(() => {
marker._icon.style.transform = marker._icon.style.transform.replace(' scale(1.3)', '');
marker._icon.style.zIndex = '';
}, 2000);
}
});
showNotification(`Menampilkan ${filteredMarkers.length} ${itemType}`, 'success');
} else {
showNotification(`Tidak ada ${itemType} ditemukan`, 'info');
}
}
// Enhanced locate user function
function locateUser() {
if (navigator.geolocation) {
map.locate({
setView: true,
maxZoom: 16,
enableHighAccuracy: true,
timeout: 10000
});
map.on('locationfound', function(e) {
L.circle(e.latlng, e.accuracy).addTo(map)
.bindPopup('Anda berada di sekitar area ini').openPopup();
showNotification('Lokasi berhasil ditemukan', 'success');
});
map.on('locationerror', function(e) {
showNotification('Gagal menemukan lokasi: ' + e.message, 'error');
});
} else {
showNotification('Geolocation tidak didukung browser ini', 'error');
}
}
// Add keyboard shortcuts for zoom
function addKeyboardShortcuts() {
document.addEventListener('keydown', function(e) {
if (e.target.tagName.toLowerCase() === 'input' || e.target.tagName.toLowerCase() === 'textarea') {
return; // Don't interfere with form inputs
}
switch(e.key) {
case '+':
case '=':
map.zoomIn();
break;
case '-':
map.zoomOut();
break;
case 'h':
case 'H':
map.setView([-2.5, 118], 5); // Home to Indonesia
break;
case 'f':
case 'F':
if (map.isFullscreen && map.isFullscreen()) {
map.toggleFullscreen();
} else if (map.toggleFullscreen) {
map.toggleFullscreen();
}
break;
case 'l':
case 'L':
locateUser();
break;
case 'a':
case 'A':
zoomToItems();
break;
}
});
}
// Enhanced map ready function
function onMapReady() {
addKeyboardShortcuts();
// Add help tooltip
const helpControl = L.control({position: 'bottomright'});
helpControl.onAdd = function(map) {
const div = L.DomUtil.create('div', 'leaflet-control-help');
div.innerHTML = '<i class="fas fa-question-circle" title="Shortcuts: +/- zoom, H home, F fullscreen, L locate, A zoom to all"></i>';
div.style.background = 'rgba(255,255,255,0.8)';
div.style.padding = '5px';
div.style.borderRadius = '3px';
div.style.cursor = 'help';
return div;
};
helpControl.addTo(map);
console.log('🎮 Map keyboard shortcuts enabled: +/- zoom, H home, F fullscreen, L locate, A zoom to all');
}
// Helper functions needed by detail modal
function getItemIcon(typeName) {
switch(typeName) {
case 'OLT': return 'fas fa-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
});