Files
2026-06-09 19:39:33 +07:00

570 lines
24 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="WebGIS SPBU Layer Groups & Layers Control">
<title>WebGIS SPBU Layer Groups</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="style.css">
</head>
<body>
<header id="header-bar">
<div>
<h1>⛽ WebGIS SPBU Pontianak</h1>
<p>Layer Groups &amp; Layers Control Pertemuan 2</p>
</div>
<div id="stats-bar">
<span class="stat-badge green" id="count-24jam">🟢 24 Jam: 0</span>
<span class="stat-badge orange" id="count-tidak24jam">🔴 Tidak 24 Jam: 0</span>
</div>
</header>
<div id="map"></div>
<div class="fab-container">
<button class="fab-btn" id="fab-tambah" title="Tambah SPBU" onclick="aktifkanModeTambah()"></button>
</div>
<div id="toast"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<script>
// Gunakan path relatif sederhana — lebih stabil dari URL constructor
const API_SPBU = 'api_spbu.php';
// ============================================================
// INISIALISASI PETA (mengikuti pola Leaflet Layers Control)
// https://leafletjs.com/examples/layers-control/
// ============================================================
const map = L.map('map').setView([-0.0553, 109.3495], 13);
const osmLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
maxZoom: 19
});
const satelliteLayer = L.tileLayer(
'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
{ attribution: 'Tiles &copy; Esri', maxZoom: 19 }
);
osmLayer.addTo(map);
// ============================================================
// LAYER GROUPS 2 grup SPBU
// ============================================================
const layer24Jam = L.layerGroup();
const layerTidak24Jam = L.layerGroup();
// Kedua layer tampil secara default saat app dimuat
layer24Jam.addTo(map);
layerTidak24Jam.addTo(map);
// ============================================================
// BASE MAPS & OVERLAY MAPS
// ============================================================
const baseMaps = {
'🗺️ OpenStreetMap': osmLayer,
'🛰️ Citra Satelit': satelliteLayer
};
const overlayMaps = {
'🟢 SPBU Buka 24 Jam': layer24Jam,
'🔴 SPBU Tidak 24 Jam': layerTidak24Jam
};
L.control.layers(baseMaps, overlayMaps, {
collapsed: false,
position: 'topright'
}).addTo(map);
// ============================================================
// IKON MARKER warna berbeda per grup
// ============================================================
function ikonSpbu(buka24Jam) {
const is24Jam = buka24Jam === 'Ya';
const color = is24Jam ? '#10b981' : '#f97316';
const border = is24Jam ? '#065f46' : '#9a3412';
const emoji = '⛽';
return L.divIcon({
className: '',
html: `<div style="width:32px;height:32px;border-radius:50% 50% 50% 0;background:${color};transform:rotate(-45deg);border:3px solid ${border};box-shadow:0 3px 10px rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center;">
<span style="transform:rotate(45deg);font-size:14px;line-height:1">${emoji}</span>
</div>`,
iconSize: [32, 32],
iconAnchor: [16, 32],
popupAnchor: [0, -34]
});
}
// ============================================================
// POPUP INFO
// ============================================================
function statusLabel(buka24Jam) {
return buka24Jam === 'Ya' ? 'Buka 24 Jam' : 'Tidak Buka 24 Jam';
}
function escHtml(str) {
return String(str ?? '')
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function popupInfoSpbu(d) {
const is24Jam = d.buka_24_jam === 'Ya';
const statusClass = is24Jam ? 'status-24jam' : 'status-tidak24jam';
const statusText = statusLabel(d.buka_24_jam);
const titleColor = is24Jam ? '#10b981' : '#f97316';
return `<div class="gis-popup">
<div class="popup-title" style="border-bottom-color:${titleColor}">⛽ ${escHtml(d.nama_spbu)}</div>
<div class="info-row">
<span class="info-label">Alamat</span>
<span class="info-value" style="font-size:11px">${escHtml(d.alamat)}</span>
</div>
<div class="info-row">
<span class="info-label">Status</span>
<span class="info-value"><span class="status-badge ${statusClass}">${statusText}</span></span>
</div>
<div class="info-row">
<span class="info-label">WhatsApp</span>
<span class="info-value">${escHtml(d.no_wa)}</span>
</div>
<div class="info-row">
<span class="info-label">Koordinat</span>
<span class="info-value" style="font-size:10px">${parseFloat(d.latitude).toFixed(5)}, ${parseFloat(d.longitude).toFixed(5)}</span>
</div>
<div class="popup-actions">
<button type="button" class="popup-btn popup-btn-edit btn-edit-spbu" data-id="${d.id}" onclick="event.stopPropagation(); bukaFormEditSpbu(${d.id}); return false;">✏️ Edit</button>
<button type="button" class="popup-btn popup-btn-delete btn-delete-spbu" data-id="${d.id}" onclick="event.stopPropagation(); konfirmasiHapusSpbu(${d.id}); return false;">🗑️ Hapus</button>
</div>
</div>`;
}
function formSpbuHtml({ title, prefix, lat, lng, d = {} }) {
const buka24 = d.buka_24_jam === 'Ya' ? 'Ya' : 'Tidak';
return `<div class="gis-popup">
<div class="popup-title">${title}</div>
<div class="popup-form">
<label>Nama SPBU</label>
<input type="text" id="${prefix}-nama" placeholder="Contoh: SPBU Jl. Ahmad Yani" value="${escHtml(d.nama_spbu || '')}">
<label>Alamat</label>
<input type="text" id="${prefix}-alamat" placeholder="Alamat lengkap" value="${escHtml(d.alamat || '')}">
<label>WhatsApp</label>
<input type="text" id="${prefix}-wa" placeholder="08xx-xxxx-xxxx" value="${escHtml(d.no_wa || '')}">
<label>Status Operasional</label>
<select id="${prefix}-24jam">
<option value="Ya"${buka24 === 'Ya' ? ' selected' : ''}>🟢 Buka 24 Jam</option>
<option value="Tidak"${buka24 === 'Tidak' ? ' selected' : ''}>🔴 Tidak Buka 24 Jam</option>
</select>
<label>Koordinat</label>
<input type="text" id="${prefix}-koord" readonly value="${lat.toFixed(6)}, ${lng.toFixed(6)}" style="font-size:10px">
</div>
<div class="popup-actions">
<button class="popup-btn popup-btn-save" onclick="${prefix === 'add' ? `simpanSpbu(${lat},${lng})` : `updateSpbu(${d.id})`}">💾 Simpan</button>
<button class="popup-btn popup-btn-cancel" onclick="map.closePopup()">✖ Batal</button>
</div>
</div>`;
}
// ============================================================
// RENDER MARKER KE LAYER GROUP YANG SESUAI
// ============================================================
const spbuMarkers = {};
const spbuDataCache = {};
let modeTambah = false;
let dbReady = false;
function getAllSpbuData() {
return Object.values(spbuDataCache);
}
function normalizeId(id) {
return Number(id);
}
function parseJsonResponse(text) {
const clean = (text || '').replace(/^\uFEFF/, '').trim();
if (!clean) return null;
try {
return JSON.parse(clean);
} catch (e) {
throw new Error('Respons server bukan JSON valid');
}
}
async function apiSend(method, id, body) {
const hasId = id !== null && id !== undefined && id !== '';
const baseUrl = hasId ? `${API_SPBU}?id=${normalizeId(id)}` : API_SPBU;
const fetchOpts = {
method,
headers: { 'Content-Type': 'application/json' }
};
if (body !== undefined) {
fetchOpts.body = JSON.stringify(body);
}
let res = await fetch(baseUrl, fetchOpts);
if ((method === 'DELETE' || method === 'PUT') && (res.status === 405 || res.status === 501)) {
res = await fetch(`${baseUrl}&_method=${method}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...(body || {}), _method: method, id: normalizeId(id) })
});
}
const data = parseJsonResponse(await res.text());
if (!res.ok) {
throw new Error((data && data.error) ? data.error : `Server error (${res.status})`);
}
return data;
}
async function apiGetAll() {
const res = await fetch(API_SPBU);
const data = parseJsonResponse(await res.text());
if (!res.ok) {
throw new Error((data && data.error) ? data.error : `Server error (${res.status})`);
}
if (!Array.isArray(data)) {
throw new Error('Data SPBU harus berupa array JSON');
}
return data;
}
function renderSpbu(d) {
const id = normalizeId(d.id);
d.id = id;
spbuDataCache[id] = d;
const targetLayer = d.buka_24_jam === 'Ya' ? layer24Jam : layerTidak24Jam;
if (spbuMarkers[id]) {
const oldMarker = spbuMarkers[id];
layer24Jam.removeLayer(oldMarker);
layerTidak24Jam.removeLayer(oldMarker);
}
const marker = L.marker([d.latitude, d.longitude], {
icon: ikonSpbu(d.buka_24_jam),
draggable: true
});
marker._data = d;
marker._targetLayer = targetLayer;
marker._spbuId = id;
marker.bindPopup(() => popupInfoSpbu(spbuDataCache[id] || d));
marker.on('click', () => marker.openPopup());
marker.on('dragend', function () {
const pos = marker.getLatLng();
const data = spbuDataCache[id];
if (!data) return;
data.latitude = pos.lat;
data.longitude = pos.lng;
spbuDataCache[id] = data;
if (!dbReady) {
showToast('📍 Posisi diperbarui (mode offline)', 'info');
marker.getPopup().setContent(popupInfoSpbu(data));
return;
}
apiSend('PUT', id, { latitude: pos.lat, longitude: pos.lng })
.then(() => {
showToast('📍 Posisi SPBU diperbarui', 'success');
marker.getPopup().setContent(popupInfoSpbu(data));
})
.catch(err => showToast('❌ ' + err.message, 'error'));
});
targetLayer.addLayer(marker);
spbuMarkers[id] = marker;
updateCounts(getAllSpbuData());
}
function removeSpbuFromMap(id) {
const numId = normalizeId(id);
const marker = spbuMarkers[numId];
if (marker) {
layer24Jam.removeLayer(marker);
layerTidak24Jam.removeLayer(marker);
marker.remove();
delete spbuMarkers[numId];
}
delete spbuDataCache[numId];
updateCounts(getAllSpbuData());
}
function updateCounts(data) {
const count24 = data.filter(d => d.buka_24_jam === 'Ya').length;
const countTidak = data.filter(d => d.buka_24_jam === 'Tidak').length;
document.getElementById('count-24jam').textContent = `🟢 24 Jam: ${count24}`;
document.getElementById('count-tidak24jam').textContent = `🔴 Tidak 24 Jam: ${countTidak}`;
}
function showToast(msg, type = 'info') {
const container = document.getElementById('toast');
const toast = document.createElement('div');
toast.className = `toast-item toast-${type}`;
toast.textContent = msg;
container.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(30px)';
toast.style.transition = 'all 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// ============================================================
// MODE TAMBAH SPBU (FAB + klik peta)
// ============================================================
function aktifkanModeTambah() {
modeTambah = !modeTambah;
const fab = document.getElementById('fab-tambah');
if (modeTambah) {
fab.classList.add('active');
fab.textContent = '✖';
showToast('📍 Klik lokasi di peta untuk menambah SPBU baru', 'info');
} else {
fab.classList.remove('active');
fab.textContent = '';
}
}
window.aktifkanModeTambah = aktifkanModeTambah;
map.on('click', function (e) {
if (!modeTambah) return;
modeTambah = false;
document.getElementById('fab-tambah').classList.remove('active');
document.getElementById('fab-tambah').textContent = '';
tampilkanFormTambahSpbu(e.latlng.lat, e.latlng.lng);
});
function tampilkanFormTambahSpbu(lat, lng) {
L.popup({ closeButton: true, maxWidth: 300 })
.setLatLng([lat, lng])
.setContent(formSpbuHtml({ title: ' Tambah SPBU Baru', prefix: 'add', lat, lng }))
.openOn(map);
}
function simpanSpbu(lat, lng) {
const nama = document.getElementById('add-nama').value.trim();
const alamat = document.getElementById('add-alamat').value.trim();
const no_wa = document.getElementById('add-wa').value.trim();
const buka_24_jam = document.getElementById('add-24jam').value;
if (!nama || !alamat || !no_wa) {
showToast('⚠️ Nama, alamat, dan WhatsApp wajib diisi!', 'error');
return;
}
if (!dbReady) {
showToast('❌ Database belum tersedia. Import database.sql terlebih dahulu.', 'error');
return;
}
apiSend('POST', null, { nama_spbu: nama, alamat, no_wa, buka_24_jam, latitude: lat, longitude: lng })
.then(res => {
map.closePopup();
renderSpbu({
id: normalizeId(res.id),
nama_spbu: res.nama_spbu,
alamat: res.alamat,
no_wa: res.no_wa,
buka_24_jam: res.buka_24_jam,
latitude: res.latitude,
longitude: res.longitude
});
showToast('✅ SPBU berhasil ditambahkan', 'success');
})
.catch(err => showToast('❌ ' + err.message, 'error'));
}
window.simpanSpbu = simpanSpbu;
function bukaFormEditSpbu(id) {
const d = spbuDataCache[normalizeId(id)];
if (!d) return;
map.closePopup();
L.popup({ closeButton: true, maxWidth: 300 })
.setLatLng([d.latitude, d.longitude])
.setContent(formSpbuHtml({
title: '✏️ Edit SPBU',
prefix: 'edit',
lat: d.latitude,
lng: d.longitude,
d
}))
.openOn(map);
}
window.bukaFormEditSpbu = bukaFormEditSpbu;
function updateSpbu(id) {
const numId = normalizeId(id);
const nama = document.getElementById('edit-nama').value.trim();
const alamat = document.getElementById('edit-alamat').value.trim();
const no_wa = document.getElementById('edit-wa').value.trim();
const buka_24_jam = document.getElementById('edit-24jam').value;
const old = spbuDataCache[numId];
if (!old) return;
if (!nama || !alamat || !no_wa) {
showToast('⚠️ Nama, alamat, dan WhatsApp wajib diisi!', 'error');
return;
}
const payload = {
nama_spbu: nama,
alamat,
no_wa,
buka_24_jam,
latitude: old.latitude,
longitude: old.longitude
};
if (!dbReady) {
map.closePopup();
renderSpbu({ ...payload, id: numId });
showToast('✅ Data diperbarui (mode offline)', 'success');
return;
}
apiSend('PUT', numId, payload)
.then(() => {
map.closePopup();
renderSpbu({ ...payload, id: numId });
showToast('✅ SPBU berhasil diperbarui', 'success');
})
.catch(err => showToast('❌ ' + err.message, 'error'));
}
window.updateSpbu = updateSpbu;
// Tampilkan konfirmasi inline di dalam popup (confirm() diblokir di Leaflet popup)
function konfirmasiHapusSpbu(id) {
const numId = normalizeId(id);
const d = spbuDataCache[numId];
if (!d) { showToast('❌ Data SPBU tidak ditemukan', 'error'); return; }
const nama = d.nama_spbu;
const konfirmHtml = `
<div class="gis-popup">
<div class="popup-title" style="color:#ef4444">🗑️ Hapus SPBU?</div>
<p style="margin:8px 0 16px;font-size:12px;color:#94a3b8">"${escHtml(nama)}" akan dihapus permanen.</p>
<div class="popup-actions">
<button class="popup-btn" onclick="map.closePopup()" style="background:rgba(255,255,255,0.08);color:#94a3b8">Batal</button>
<button class="popup-btn popup-btn-delete" onclick="hapusSpbu(${numId})">🗑️ Ya, Hapus</button>
</div>
</div>`;
const lyr = spbuMarkers[numId];
if (lyr) {
lyr.getPopup().setContent(konfirmHtml);
} else {
// fallback: langsung hapus tanpa konfirmasi popup
hapusSpbu(numId);
}
}
window.konfirmasiHapusSpbu = konfirmasiHapusSpbu;
async function hapusSpbu(id) {
const numId = normalizeId(id);
const d = spbuDataCache[numId];
if (!d) {
showToast('❌ Data SPBU tidak ditemukan', 'error');
return;
}
if (!dbReady) {
map.closePopup();
removeSpbuFromMap(numId);
showToast('🗑️ Data dihapus (mode offline)', 'success');
return;
}
try {
await apiSend('DELETE', numId);
map.closePopup();
removeSpbuFromMap(numId);
showToast('🗑️ SPBU berhasil dihapus', 'success');
} catch (err) {
showToast('❌ ' + err.message, 'error');
}
}
window.hapusSpbu = hapusSpbu;
// Data fallback jika database belum diimport
const SAMPLE_DATA = [
{ id: 1, nama_spbu: 'SPBU Jl. Ahmad Yani', alamat: 'Jl. Ahmad Yani No. 12, Pontianak', no_wa: '0812-1111-0001', buka_24_jam: 'Ya', latitude: -0.0400, longitude: 109.3200 },
{ id: 2, nama_spbu: 'SPBU Jl. Gajah Mada', alamat: 'Jl. Gajah Mada No. 45, Pontianak', no_wa: '0812-1111-0002', buka_24_jam: 'Ya', latitude: -0.0520, longitude: 109.3450 },
{ id: 3, nama_spbu: 'SPBU Jl. Diponegoro', alamat: 'Jl. Diponegoro No. 88, Pontianak', no_wa: '0812-1111-0003', buka_24_jam: 'Tidak', latitude: -0.0610, longitude: 109.3600 },
{ id: 4, nama_spbu: 'SPBU Jl. Sutan Syahrir', alamat: 'Jl. Sutan Syahrir No. 5, Pontianak', no_wa: '0812-1111-0004', buka_24_jam: 'Tidak', latitude: -0.0480, longitude: 109.3700 },
{ id: 5, nama_spbu: 'SPBU Jl. Kom. Yos Sudarso', alamat: 'Jl. Kom. Yos Sudarso No. 22, Pontianak', no_wa: '0812-1111-0005', buka_24_jam: 'Ya', latitude: -0.0350, longitude: 109.3550 }
];
function loadSpbuData(data, fitBounds = true) {
layer24Jam.clearLayers();
layerTidak24Jam.clearLayers();
Object.keys(spbuMarkers).forEach(k => delete spbuMarkers[k]);
Object.keys(spbuDataCache).forEach(k => delete spbuDataCache[k]);
data.forEach(d => renderSpbu(d));
if (fitBounds && data.length > 0) {
const bounds = L.latLngBounds(data.map(d => [d.latitude, d.longitude]));
map.fitBounds(bounds, { padding: [50, 50], maxZoom: 14 });
}
}
// ============================================================
// MUAT DATA DARI API (dengan retry jika gagal)
// ============================================================
async function muatDataSpbu() {
const MAX_RETRY = 3;
for (let attempt = 1; attempt <= MAX_RETRY; attempt++) {
try {
const data = await apiGetAll();
dbReady = true;
loadSpbuData(data);
showToast(data.length > 0
? `✅ ${data.length} SPBU dimuat dari database`
: '📭 Database kosong — klik untuk tambah SPBU', 'success');
return; // sukses, keluar
} catch (err) {
console.warn(`API SPBU gagal (percobaan ${attempt}/${MAX_RETRY}):`, err);
if (attempt < MAX_RETRY) {
await new Promise(r => setTimeout(r, 800 * attempt)); // tunggu sebentar
} else {
// Semua retry gagal — mode offline
dbReady = false;
loadSpbuData(SAMPLE_DATA);
const pesan = err.message || 'Tidak bisa terhubung ke server';
showToast(`⚠️ Mode offline: ${pesan}`, 'error');
console.error('API SPBU gagal setelah semua retry:', err);
}
}
}
}
// Tunggu page load penuh sebelum fetch ke API
if (document.readyState === 'complete') {
muatDataSpbu();
} else {
window.addEventListener('load', muatDataSpbu);
}
</script>
</body>
</html>