479 lines
19 KiB
HTML
479 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Peta SPBU</title>
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<style>
|
|
:root {
|
|
--bg: #0b1220;
|
|
--panel: rgba(255, 255, 255, 0.95);
|
|
--text: #0f172a;
|
|
--muted: #64748b;
|
|
--border: rgba(15, 23, 42, 0.12);
|
|
--primary: #2563eb;
|
|
--danger: #dc3545;
|
|
--warning: #ffc107;
|
|
--success: #16a34a;
|
|
}
|
|
|
|
body { margin: 0; padding: 0; font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; }
|
|
#map { height: 100vh; width: 100vw; }
|
|
form { display: flex; flex-direction: column; gap: 8px; width: 220px; }
|
|
|
|
input, select {
|
|
padding: 8px 10px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
font: inherit;
|
|
}
|
|
input:focus, select:focus {
|
|
outline: 2px solid rgba(37, 99, 235, 0.25);
|
|
outline-offset: 1px;
|
|
}
|
|
|
|
.leaflet-popup-content-wrapper { border-radius: 14px; }
|
|
.leaflet-popup-content { margin: 12px 14px; }
|
|
.leaflet-control-layers {
|
|
border-radius: 12px;
|
|
border: 1px solid var(--border);
|
|
box-shadow: 0 10px 30px rgba(2, 6, 23, 0.10);
|
|
}
|
|
|
|
.toolbar {
|
|
position: fixed;
|
|
top: 12px;
|
|
left: 12px;
|
|
z-index: 2500;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 10px;
|
|
border-radius: 12px;
|
|
border: 1px solid var(--border);
|
|
background: var(--panel);
|
|
box-shadow: 0 10px 30px rgba(2, 6, 23, 0.18);
|
|
backdrop-filter: blur(8px);
|
|
}
|
|
|
|
.toolbar-title {
|
|
font-weight: 700;
|
|
color: var(--text);
|
|
margin-right: 6px;
|
|
letter-spacing: 0.2px;
|
|
user-select: none;
|
|
}
|
|
|
|
.hint {
|
|
position: fixed;
|
|
top: 62px;
|
|
left: 12px;
|
|
z-index: 2500;
|
|
padding: 10px 12px;
|
|
border-radius: 12px;
|
|
border: 1px solid var(--border);
|
|
background: var(--panel);
|
|
color: var(--muted);
|
|
font-size: 13px;
|
|
box-shadow: 0 10px 30px rgba(2, 6, 23, 0.10);
|
|
}
|
|
|
|
.btn {
|
|
appearance: none;
|
|
border: 1px solid var(--border);
|
|
background: white;
|
|
color: var(--text);
|
|
font-weight: 600;
|
|
padding: 8px 10px;
|
|
border-radius: 10px;
|
|
cursor: pointer;
|
|
line-height: 1;
|
|
}
|
|
.btn:hover { filter: brightness(0.98); }
|
|
.btn:active { transform: translateY(1px); }
|
|
.btn-primary { background: var(--primary); color: white; border-color: rgba(37, 99, 235, 0.25); }
|
|
.btn-ghost { background: white; }
|
|
.btn-toggle[aria-pressed="true"] { background: #0f172a; color: white; border-color: rgba(15, 23, 42, 0.25); }
|
|
|
|
/* Modal Styles */
|
|
#editModal {
|
|
display: none; position: fixed; top: 50%; left: 50%;
|
|
transform: translate(-50%, -50%); z-index: 2000;
|
|
background: white; padding: 20px; border-radius: 8px;
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.3); width: 260px;
|
|
}
|
|
#modalOverlay {
|
|
display: none; position: fixed; top: 0; left: 0;
|
|
width: 100%; height: 100%; background: rgba(0,0,0,0.5);
|
|
z-index: 1500;
|
|
}
|
|
.btn-group { display: flex; justify-content: space-between; margin-top: 10px; }
|
|
.btn-action { padding: 6px 10px; border: none; cursor: pointer; border-radius: 8px; font-weight: 600; }
|
|
.btn-edit { background: var(--warning); }
|
|
.btn-delete { background: var(--danger); color: white; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="toolbar" role="region" aria-label="Kontrol peta">
|
|
<div class="toolbar-title">Peta SPBU</div>
|
|
<button id="btnAdd" type="button" class="btn btn-primary">Tambah titik baru</button>
|
|
<button id="btnMove" type="button" class="btn btn-ghost btn-toggle" aria-pressed="false">Aktifkan pindah</button>
|
|
</div>
|
|
<div id="hint" class="hint" hidden>Klik pada peta untuk menambahkan titik baru.</div>
|
|
|
|
<div id="map"></div>
|
|
|
|
<div id="modalOverlay" onclick="closeModal()"></div>
|
|
|
|
<div id="editModal">
|
|
<h3 style="margin-top:0;">Ubah SPBU</h3>
|
|
<form id="editForm" onsubmit="updateData(event)">
|
|
<input type="hidden" id="editId">
|
|
<input type="text" id="editNama" required>
|
|
<input type="text" id="editNomor" required>
|
|
<input type="text" id="editLat" readonly>
|
|
<input type="text" id="editLng" readonly>
|
|
<div style="font-size:12px; color:#6c757d; margin-top:-6px;">
|
|
Lintang & bujur akan berubah saat titik dipindah (mode Pindah aktif).
|
|
</div>
|
|
<select id="editStatus" required>
|
|
<option value="Buka 24 Jam">Buka 24 Jam</option>
|
|
<option value="Tidak">Tidak</option>
|
|
</select>
|
|
<div class="btn-group">
|
|
<button type="button" class="btn-action" onclick="closeModal()">Batal</button>
|
|
<button type="submit" class="btn-action" style="background:#28a745; color:white;">Simpan</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<script>
|
|
const map = L.map('map').setView([-0.055, 109.34], 13);
|
|
|
|
// Base map layers
|
|
const baseOSM = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
maxZoom: 19,
|
|
attribution: '© OpenStreetMap'
|
|
});
|
|
|
|
const baseOSMHOT = L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
|
|
maxZoom: 19,
|
|
subdomains: 'abc',
|
|
attribution: '© OpenStreetMap contributors, Tiles style by Humanitarian OpenStreetMap Team hosted by OpenStreetMap France'
|
|
});
|
|
|
|
const baseTopo = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', {
|
|
maxZoom: 17,
|
|
subdomains: 'abc',
|
|
attribution: 'Map data: © OpenStreetMap contributors, SRTM | Map style: © OpenTopoMap (CC-BY-SA)'
|
|
});
|
|
|
|
// Default base layer
|
|
baseOSM.addTo(map);
|
|
|
|
const popup = L.popup();
|
|
|
|
// Define Green and Red Icons
|
|
const greenIcon = new L.Icon({
|
|
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
|
|
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
|
|
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
|
|
});
|
|
|
|
const redIcon = new L.Icon({
|
|
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
|
|
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
|
|
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
|
|
});
|
|
|
|
// Layers for filtering via Leaflet Layers Control
|
|
const layer24 = L.layerGroup().addTo(map);
|
|
const layerNot = L.layerGroup().addTo(map);
|
|
|
|
const baseLayers = {
|
|
'Peta Jalan (OSM)': baseOSM,
|
|
'Peta Jalan (HOT)': baseOSMHOT,
|
|
'Peta Topografi': baseTopo
|
|
};
|
|
|
|
const overlayLayers = {
|
|
'Buka 24 Jam': layer24,
|
|
'Tidak': layerNot
|
|
};
|
|
|
|
L.control.layers(baseLayers, overlayLayers, { collapsed: false }).addTo(map);
|
|
|
|
let isAddMode = false;
|
|
let isMoveMode = false;
|
|
|
|
function setAddMode(enabled) {
|
|
isAddMode = enabled;
|
|
document.getElementById('hint').hidden = !enabled;
|
|
const btn = document.getElementById('btnAdd');
|
|
btn.textContent = enabled ? 'Klik peta untuk menaruh…' : 'Tambah titik baru';
|
|
}
|
|
|
|
function setMoveMode(enabled) {
|
|
isMoveMode = enabled;
|
|
const btn = document.getElementById('btnMove');
|
|
btn.setAttribute('aria-pressed', enabled ? 'true' : 'false');
|
|
btn.textContent = enabled ? 'Matikan pindah' : 'Aktifkan pindah';
|
|
|
|
const toggle = (marker) => {
|
|
if (!marker || !marker.dragging) return;
|
|
if (enabled) marker.dragging.enable();
|
|
else marker.dragging.disable();
|
|
};
|
|
|
|
layer24.eachLayer(toggle);
|
|
layerNot.eachLayer(toggle);
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? '')
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''');
|
|
}
|
|
|
|
function buildPopupContent(spbu) {
|
|
return `
|
|
<b>${escapeHtml(spbu.nama)}</b><br>
|
|
Nomor: ${escapeHtml(spbu.nomor)}<br>
|
|
Status: ${escapeHtml(spbu.status)}<br>
|
|
Lintang: ${escapeHtml(spbu.Latitude)}<br>
|
|
Bujur: ${escapeHtml(spbu.Longitude)}<br>
|
|
<hr style="margin: 8px 0;">
|
|
<button type="button" class="btn-action btn-edit js-edit">Ubah</button>
|
|
<button type="button" class="btn-action btn-delete js-delete">Hapus</button>
|
|
`;
|
|
}
|
|
|
|
function updateCoordinates(id, latitude, longitude) {
|
|
const formData = new FormData();
|
|
formData.append('id', id);
|
|
formData.append('latitude', latitude);
|
|
formData.append('longitude', longitude);
|
|
|
|
return fetch('update_spbu.php', { method: 'POST', body: formData })
|
|
.then(r => r.json());
|
|
}
|
|
|
|
function refreshMarkers() {
|
|
return fetch('get_spbu.php')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
map.closePopup();
|
|
layer24.clearLayers();
|
|
layerNot.clearLayers();
|
|
|
|
const rows = Array.isArray(data) ? data : [];
|
|
rows.forEach(spbu => {
|
|
const is24 = spbu.status === 'Buka 24 Jam';
|
|
const markerIcon = is24 ? greenIcon : redIcon;
|
|
const lat = parseFloat(spbu.Latitude);
|
|
const lng = parseFloat(spbu.Longitude);
|
|
|
|
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
|
|
|
|
const popupContent = buildPopupContent(spbu);
|
|
|
|
const marker = L.marker([lat, lng], { icon: markerIcon, draggable: true });
|
|
if (!isMoveMode && marker.dragging) marker.dragging.disable();
|
|
marker.__spbu = spbu;
|
|
marker.__oldLatLng = null;
|
|
marker.bindPopup(popupContent);
|
|
|
|
marker.on('dragstart', function(ev) {
|
|
const m = ev.target;
|
|
m.closePopup();
|
|
m.__oldLatLng = m.getLatLng();
|
|
});
|
|
|
|
marker.on('dragend', function(ev) {
|
|
const m = ev.target;
|
|
const oldLatLng = m.__oldLatLng;
|
|
const current = m.getLatLng();
|
|
const newLat = current.lat.toFixed(8);
|
|
const newLng = current.lng.toFixed(8);
|
|
|
|
const spbuRef = m.__spbu;
|
|
updateCoordinates(spbuRef.id, newLat, newLng)
|
|
.then(res => {
|
|
if (res && res.status === 'success') {
|
|
spbuRef.Latitude = newLat;
|
|
spbuRef.Longitude = newLng;
|
|
m.setPopupContent(buildPopupContent(spbuRef));
|
|
return;
|
|
}
|
|
if (oldLatLng) m.setLatLng(oldLatLng);
|
|
console.error('Move failed:', res);
|
|
})
|
|
.catch(err => {
|
|
if (oldLatLng) m.setLatLng(oldLatLng);
|
|
console.error('Move error:', err);
|
|
});
|
|
});
|
|
|
|
marker.addTo(is24 ? layer24 : layerNot);
|
|
});
|
|
})
|
|
.catch(error => console.error('Error fetching data:', error));
|
|
}
|
|
|
|
// Wire popup buttons (avoids inline onclick bugs)
|
|
map.on('popupopen', function(e) {
|
|
const source = e.popup._source;
|
|
const spbu = source && source.__spbu;
|
|
if (!spbu) return;
|
|
|
|
const el = e.popup.getElement();
|
|
if (!el) return;
|
|
|
|
const editBtn = el.querySelector('.js-edit');
|
|
const deleteBtn = el.querySelector('.js-delete');
|
|
|
|
if (editBtn) {
|
|
editBtn.addEventListener('click', function(ev) {
|
|
ev.preventDefault();
|
|
openModal(spbu);
|
|
});
|
|
}
|
|
if (deleteBtn) {
|
|
deleteBtn.addEventListener('click', function(ev) {
|
|
ev.preventDefault();
|
|
deleteData(spbu.id);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Toolbar
|
|
document.getElementById('btnAdd').addEventListener('click', function() {
|
|
setAddMode(!isAddMode);
|
|
});
|
|
|
|
document.getElementById('btnMove').addEventListener('click', function() {
|
|
setMoveMode(!isMoveMode);
|
|
});
|
|
|
|
// Handle Map Click (add SPBU only when Create mode enabled)
|
|
map.on('click', function(e) {
|
|
if (!isAddMode) return;
|
|
|
|
const lat = e.latlng.lat.toFixed(8);
|
|
const lng = e.latlng.lng.toFixed(8);
|
|
|
|
const formHTML = `
|
|
<form id="spbuForm" onsubmit="saveData(event)">
|
|
<h4 style="margin: 0 0 10px 0;">Tambah SPBU</h4>
|
|
<input type="text" id="nama" placeholder="Nama SPBU" required>
|
|
<input type="text" id="nomor" placeholder="Nomor SPBU" required>
|
|
<select id="status" required>
|
|
<option value="Buka 24 Jam">Buka 24 Jam</option>
|
|
<option value="Tidak">Tidak</option>
|
|
</select>
|
|
<div>Lintang: ${lat}</div>
|
|
<div>Bujur: ${lng}</div>
|
|
<input type="hidden" id="lat" value="${lat}">
|
|
<input type="hidden" id="lng" value="${lng}">
|
|
<button type="submit" style="background:#28a745; color:white; border:none; padding:5px;">Simpan</button>
|
|
</form>
|
|
`;
|
|
|
|
popup.setLatLng(e.latlng).setContent(formHTML).openOn(map);
|
|
|
|
// One-shot add: after placing the form, leave add mode.
|
|
setAddMode(false);
|
|
});
|
|
|
|
// Save (create)
|
|
function saveData(event) {
|
|
event.preventDefault();
|
|
|
|
const formData = new FormData();
|
|
formData.append('nama', document.getElementById('nama').value);
|
|
formData.append('nomor', document.getElementById('nomor').value);
|
|
formData.append('status', document.getElementById('status').value);
|
|
formData.append('latitude', document.getElementById('lat').value);
|
|
formData.append('longitude', document.getElementById('lng').value);
|
|
|
|
fetch('save_spbu.php', { method: 'POST', body: formData })
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
map.closePopup();
|
|
return refreshMarkers();
|
|
}
|
|
console.error('Save failed:', data);
|
|
})
|
|
.catch(error => console.error('Error:', error));
|
|
}
|
|
|
|
// Delete
|
|
function deleteData(id) {
|
|
if (!confirm("Yakin ingin menghapus SPBU ini?")) return;
|
|
|
|
const formData = new FormData();
|
|
formData.append('id', id);
|
|
|
|
fetch('delete_spbu.php', { method: 'POST', body: formData })
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
map.closePopup();
|
|
return refreshMarkers();
|
|
}
|
|
console.error('Delete failed:', data);
|
|
})
|
|
.catch(error => console.error('Error:', error));
|
|
}
|
|
|
|
// Modal
|
|
function openModal(spbu) {
|
|
document.getElementById('editId').value = spbu.id;
|
|
document.getElementById('editNama').value = spbu.nama;
|
|
document.getElementById('editNomor').value = spbu.nomor;
|
|
document.getElementById('editStatus').value = spbu.status;
|
|
document.getElementById('editLat').value = spbu.Latitude;
|
|
document.getElementById('editLng').value = spbu.Longitude;
|
|
|
|
document.getElementById('modalOverlay').style.display = 'block';
|
|
document.getElementById('editModal').style.display = 'block';
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('modalOverlay').style.display = 'none';
|
|
document.getElementById('editModal').style.display = 'none';
|
|
}
|
|
|
|
// Update
|
|
function updateData(event) {
|
|
event.preventDefault();
|
|
|
|
const formData = new FormData();
|
|
formData.append('id', document.getElementById('editId').value);
|
|
formData.append('nama', document.getElementById('editNama').value);
|
|
formData.append('nomor', document.getElementById('editNomor').value);
|
|
formData.append('status', document.getElementById('editStatus').value);
|
|
|
|
fetch('update_spbu.php', { method: 'POST', body: formData })
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
closeModal();
|
|
return refreshMarkers();
|
|
}
|
|
console.error('Update failed:', data);
|
|
})
|
|
.catch(error => console.error('Error:', error));
|
|
}
|
|
|
|
// Initial load
|
|
refreshMarkers();
|
|
</script>
|
|
</body>
|
|
</html> |