973 lines
34 KiB
JavaScript
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: '© <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-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>×</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
|
|
});
|