First commit

This commit is contained in:
2026-06-09 19:02:22 +07:00
commit c2c0774b3b
36 changed files with 10493 additions and 0 deletions
+562
View File
@@ -0,0 +1,562 @@
<!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 el = document.getElementById('toast');
el.textContent = msg;
el.className = 'show' + (type ? ' ' + type : '');
setTimeout(() => { el.className = ''; }, 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>