Initial commit
This commit is contained in:
@@ -0,0 +1,479 @@
|
||||
<!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>
|
||||
Reference in New Issue
Block a user