Initial commit untuk UAS SIG

This commit is contained in:
z0rayy
2026-06-08 21:16:23 +07:00
commit c90ce9aedc
39 changed files with 7105 additions and 0 deletions
+403
View File
@@ -0,0 +1,403 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Peta SPBU Publik — WebGIS</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--clr-bg: #0f172a; --clr-surface: #1e293b; --clr-surface-light: #334155;
--clr-border: #334155; --clr-text: #f1f5f9; --clr-muted: #94a3b8;
--clr-primary: #3b82f6; --clr-primary-dark: #2563eb;
--clr-success: #22c55e; --clr-danger: #ef4444;
--radius: 10px; --shadow: 0 4px 24px rgba(0,0,0,.45);
--transition: .2s ease;
--sidebar-width: 260px; --sidebar-collapsed: 70px;
}
html, body { height: 100%; font-family: 'Inter', sans-serif; background: var(--clr-bg); color: var(--clr-text); overflow: hidden; }
/* ---- Layout ---- */
.app-wrapper { display: flex; height: 100vh; }
/* ---- Sidebar ---- */
.sidebar {
width: var(--sidebar-width);
background: linear-gradient(135deg, #1a2a42 0%, #0f172a 100%);
border-right: 1px solid var(--clr-border);
display: flex; flex-direction: column;
transition: width var(--transition); z-index: 100;
box-shadow: 2px 0 8px rgba(0,0,0,.3); overflow: hidden;
}
.sidebar.collapsed { width: var(--sidebar-collapsed); }
.sidebar-header {
padding: 20px; border-bottom: 1px solid var(--clr-border);
display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;
}
.sidebar-logo { display: flex; align-items: center; gap: 10px; flex: 1; overflow: hidden; }
.sidebar-logo-icon {
width: 40px; height: 40px; flex-shrink: 0;
background: linear-gradient(135deg, var(--clr-primary), #8b5cf6);
border-radius: 8px; display: flex; align-items: center; justify-content: center;
font-size: 14px; font-weight: 700; box-shadow: 0 0 12px rgba(59,130,246,.4);
}
.sidebar-logo-text { overflow: hidden; }
.sidebar-logo-text h2 { font-size: 14px; font-weight: 700; white-space: nowrap; }
.sidebar-logo-text p { font-size: 11px; color: var(--clr-muted); white-space: nowrap; }
.sidebar.collapsed .sidebar-logo-text { display: none; }
.sidebar-toggle { background: transparent; border: none; color: var(--clr-muted); cursor: pointer; font-size: 20px; padding: 4px; flex-shrink: 0; transition: color var(--transition); }
.sidebar-toggle:hover { color: var(--clr-text); }
.sidebar-nav { flex: 1; overflow-y: auto; padding: 16px 0; }
.sidebar-section { padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,.05); }
.sidebar-section:last-child { border-bottom: none; }
.sidebar-section-title { padding: 10px 16px; font-size: 10px; font-weight: 700; color: var(--clr-muted); text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; overflow: hidden; }
.sidebar.collapsed .sidebar-section-title { opacity: 0; }
.sidebar-menu { display: flex; flex-direction: column; }
.sidebar-item {
display: flex; align-items: center; gap: 12px; padding: 13px 16px;
color: var(--clr-muted); cursor: pointer; text-decoration: none;
transition: all var(--transition); border-left: 3px solid transparent;
}
.sidebar-item:hover { color: var(--clr-text); background: rgba(255,255,255,.05); border-left-color: var(--clr-primary); }
.sidebar-item.active { color: var(--clr-primary); background: rgba(59,130,246,.1); border-left-color: var(--clr-primary); }
.sidebar-item-icon { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; }
.sidebar-item-text { flex: 1; font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; }
.sidebar.collapsed .sidebar-item-text { display: none; }
/* ---- Main ---- */
.main-wrapper { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
header {
flex-shrink: 0; height: 60px;
background: linear-gradient(135deg, #1e3a5f 0%, #0f172a 100%);
border-bottom: 1px solid var(--clr-border);
display: flex; align-items: center; justify-content: space-between;
padding: 0 20px; box-shadow: 0 2px 12px rgba(0,0,0,.5);
}
.header-title h1 { font-size: 18px; font-weight: 700; }
.header-breadcrumb { font-size: 13px; color: var(--clr-muted); margin-top: 2px; }
.content-area { flex: 1; position: relative; overflow: hidden; }
/* ---- Map ---- */
#map { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; }
/* ---- Controls Panel ---- */
.controls-panel {
position: absolute; top: 16px; right: 16px;
background: rgba(30, 41, 59, 0.95); backdrop-filter: blur(8px);
border: 1px solid var(--clr-border); border-radius: var(--radius);
padding: 16px; z-index: 1000; width: 280px;
box-shadow: var(--shadow);
}
.controls-panel h3 { font-size: 13px; font-weight: 700; color: var(--clr-text); margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
.ctrl-input {
width: 100%; padding: 9px 12px; margin-bottom: 10px;
background: var(--clr-bg); border: 1px solid var(--clr-border);
border-radius: 6px; color: var(--clr-text); font-family: inherit; font-size: 13px; outline: none;
}
.ctrl-input:focus { border-color: var(--clr-primary); }
.ctrl-btn {
display: block; width: 100%; padding: 10px; margin-bottom: 8px;
background: var(--clr-primary); color: white; border: none; border-radius: 6px;
font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer; transition: 0.2s;
}
.ctrl-btn:hover { background: var(--clr-primary-dark); }
.ctrl-btn:disabled { background: var(--clr-surface-light); cursor: not-allowed; color: var(--clr-muted); }
.info-box {
display: none; background: rgba(59,130,246,.1); border: 1px solid rgba(59,130,246,.3);
border-radius: 6px; padding: 10px; margin-top: 8px; font-size: 13px;
}
.info-box.active { display: block; }
.info-box strong { color: var(--clr-primary); }
/* ---- Search Panel (Left) ---- */
.search-panel {
position: absolute; top: 16px; left: 60px; /* offset for leaflet zoom controls */
background: rgba(30, 41, 59, 0.95); backdrop-filter: blur(8px);
border: 1px solid var(--clr-border); border-radius: var(--radius);
padding: 12px; z-index: 1000; width: 300px;
box-shadow: var(--shadow); display: flex; flex-direction: column;
}
.search-panel h3 { font-size: 13px; font-weight: 700; color: var(--clr-text); margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; display: flex; align-items: center; gap: 8px; }
.search-input-wrap { position: relative; }
.search-input-wrap .ctrl-input { margin-bottom: 0; padding-right: 30px; }
.search-results {
max-height: 250px; overflow-y: auto; margin-top: 8px;
display: none; flex-direction: column; gap: 4px;
}
.search-results.active { display: flex; }
.search-result-item {
padding: 8px 10px; background: rgba(255,255,255,0.05); border-radius: 6px;
cursor: pointer; transition: 0.2s; border: 1px solid transparent;
}
.search-result-item:hover { background: rgba(59,130,246,0.1); border-color: rgba(59,130,246,0.3); }
.search-result-name { font-size: 13px; font-weight: 600; color: #f1f5f9; display: block; margin-bottom: 2px; }
.search-result-desc { font-size: 11px; color: #94a3b8; display: block; }
/* ---- Legend Panel ---- */
.legend-panel {
position: absolute; bottom: 16px; right: 16px;
background: rgba(30, 41, 59, 0.95); backdrop-filter: blur(8px);
border: 1px solid var(--clr-border); border-radius: var(--radius);
padding: 14px; z-index: 1000; width: 200px; font-size: 13px;
box-shadow: var(--shadow);
}
.legend-panel h3 { font-size: 11px; font-weight: 700; color: var(--clr-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; }
.legend-row { display: flex; align-items: center; gap: 10px; margin-bottom: 7px; color: var(--clr-text); font-size: 12px; }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
</style>
</head>
<body>
<div class="app-wrapper">
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="sidebar-logo">
<div class="sidebar-logo-icon">WG</div>
<div class="sidebar-logo-text">
<h2>WebGIS</h2>
<p>SPBU System</p>
</div>
</div>
<button class="sidebar-toggle" id="sidebar-toggle"></button>
</div>
<nav class="sidebar-nav">
<div class="sidebar-section">
<div class="sidebar-section-title">Navigasi</div>
<div class="sidebar-menu">
<div class="sidebar-item active">
<div class="sidebar-item-icon">🗺</div>
<div class="sidebar-item-text">Peta SPBU</div>
</div>
<a href="index.html" class="sidebar-item">
<div class="sidebar-item-icon">🏠</div>
<div class="sidebar-item-text">Beranda</div>
</a>
</div>
</div>
<div class="sidebar-section">
</div>
</nav>
</aside>
<!-- Main -->
<div class="main-wrapper">
<header>
<div class="header-title">
<h1>Peta Persebaran SPBU</h1>
<div class="header-breadcrumb">Peta Publik — Data dikelola oleh Administrator</div>
</div>
<div style="display:flex; align-items:center; gap:10px;">
<span id="data-count" style="font-size:13px; color:#94a3b8; background:rgba(255,255,255,.06); border:1px solid #334155; padding:5px 12px; border-radius:7px;">Memuat...</span>
<button id="btn-refresh" title="Refresh data terbaru" style="background:rgba(59,130,246,.15); border:1px solid rgba(59,130,246,.3); color:#60a5fa; border-radius:7px; padding:6px 12px; cursor:pointer; font-size:13px; font-family:inherit;">🔄 Refresh</button>
</div>
</header>
<div class="content-area">
<div id="map"></div>
<!-- Search Panel (Left) -->
<div class="search-panel">
<h3><i class="fa-solid fa-magnifying-glass" style="color:var(--clr-primary)"></i> Cari SPBU</h3>
<div class="search-input-wrap">
<input id="search-spbu" class="ctrl-input" type="text" placeholder="Ketik nama atau no SPBU..." autocomplete="off">
</div>
<div id="search-results" class="search-results"></div>
</div>
<div class="controls-panel">
<h3>📍 Lokasi & Rute</h3>
<button id="btn-location" class="ctrl-btn">📍 Dapatkan Lokasi Saya</button>
<button id="btn-nearest" class="ctrl-btn" disabled>🎯 Cari SPBU Terdekat</button>
<div id="info-box" class="info-box">
<strong>SPBU Terdekat:</strong><br>
<span id="nearest-name">-</span><br>
<strong>Jarak:</strong> <span id="nearest-distance">-</span> km
</div>
</div>
<div class="legend-panel">
<h3>Legenda</h3>
<div class="legend-row"><div class="legend-dot" style="background:#22c55e;"></div> SPBU 24 Jam</div>
<div class="legend-row"><div class="legend-dot" style="background:#f59e0b;"></div> SPBU Tidak 24 Jam</div>
<div class="legend-row"><div class="legend-dot" style="background:#ef4444;"></div> Lokasi Anda</div>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js"></script>
<script>
// Sidebar toggle
document.getElementById('sidebar-toggle').addEventListener('click', () => {
document.getElementById('sidebar').classList.toggle('collapsed');
});
// Init Map
const map = L.map('map').setView([-0.026, 109.342], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors', maxZoom: 19
}).addTo(map);
const baseMaps = {
'Standard': L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }),
'Dark': L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { maxZoom: 19 })
};
L.control.layers(baseMaps).addTo(map);
let spbuData = [], spbuLayer = L.layerGroup().addTo(map);
let userMarker = null, routeLine = null, userLocation = null;
const icon24 = L.circleMarker ? null : null; // will use circleMarker
async function loadSPBU(silent = false) {
try {
const res = await fetch('backend/read.php');
const result = await res.json();
// Fix: read.php returns {success: true, data:[...]}, not {status:'success'}
if (result.success && result.data) {
spbuData = result.data;
renderSPBU(document.getElementById('search-spbu').value);
if (!silent) {
document.getElementById('data-count').textContent = spbuData.length + ' SPBU';
}
}
} catch (err) { console.error('Error loading SPBU:', err); }
}
function renderSPBU(filterText = '', autoZoom = false) {
spbuLayer.clearLayers();
const bounds = [];
spbuData.forEach(spbu => {
if (filterText && !spbu.nama.toLowerCase().includes(filterText.toLowerCase()) && !spbu.no_spbu.toLowerCase().includes(filterText.toLowerCase())) return;
const lat = parseFloat(spbu.latitude), lng = parseFloat(spbu.longitude);
if (isNaN(lat) || isNaN(lng)) return;
const color = spbu.status === '24jam' ? '#22c55e' : '#f59e0b';
const marker = L.circleMarker([lat, lng], { radius: 7, fillColor: color, color: '#fff', weight: 2, opacity: 1, fillOpacity: 0.9 })
.bindPopup('<strong>' + spbu.nama + '</strong><br>No: ' + spbu.no_spbu + '<br>Status: ' + (spbu.status === '24jam' ? '✅ 24 Jam' : '⏰ Tidak 24 Jam'))
.addTo(spbuLayer);
bounds.push([lat, lng]);
});
if (autoZoom && filterText && bounds.length > 0) {
if (bounds.length === 1) {
map.flyTo(bounds[0], 16);
} else {
map.fitBounds(bounds, { padding: [50, 50] });
}
}
}
document.getElementById('search-spbu').addEventListener('input', e => {
const filterText = e.target.value.toLowerCase();
const resultsDiv = document.getElementById('search-results');
if (!filterText) {
resultsDiv.classList.remove('active');
renderSPBU(''); // render all, no zoom
return;
}
// Filter data for list
const matched = spbuData.filter(spbu =>
spbu.nama.toLowerCase().includes(filterText) ||
spbu.no_spbu.toLowerCase().includes(filterText)
);
if (matched.length > 0) {
resultsDiv.innerHTML = matched.map(m => `
<div class="search-result-item" onclick="window.selectSPBU(${m.id})">
<span class="search-result-name">${m.nama}</span>
<span class="search-result-desc">No: ${m.no_spbu}${m.status === '24jam' ? '24 Jam' : 'Tidak 24 Jam'}</span>
</div>
`).join('');
} else {
resultsDiv.innerHTML = '<div style="padding:8px 10px; color:#94a3b8; font-size:12px;">Tidak ditemukan.</div>';
}
resultsDiv.classList.add('active');
// Filter map markers, but don't auto-zoom on every keystroke
renderSPBU(filterText, false);
});
window.selectSPBU = function(id) {
const spbu = spbuData.find(s => s.id == id);
if (!spbu) return;
// Set input value to name
document.getElementById('search-spbu').value = spbu.nama;
document.getElementById('search-results').classList.remove('active');
// Render and zoom to specific SPBU
renderSPBU(spbu.nama.toLowerCase(), true);
};
const userIcon = 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]
});
document.getElementById('btn-location').addEventListener('click', () => {
if (!navigator.geolocation) return alert('Geolocation tidak didukung browser Anda.');
const btn = document.getElementById('btn-location');
btn.textContent = '⏳ Mencari lokasi...';
btn.disabled = true;
navigator.geolocation.getCurrentPosition(pos => {
const lat = pos.coords.latitude, lng = pos.coords.longitude;
userLocation = [lng, lat];
if (userMarker) map.removeLayer(userMarker);
userMarker = L.marker([lat, lng], { icon: userIcon }).bindPopup('📍 Lokasi Anda').addTo(map);
map.flyTo([lat, lng], 14);
btn.textContent = '✅ Lokasi Diperbarui';
btn.disabled = false;
document.getElementById('btn-nearest').disabled = false;
}, err => {
alert('Gagal mendapatkan lokasi: ' + err.message);
btn.textContent = '📍 Dapatkan Lokasi Saya';
btn.disabled = false;
});
});
document.getElementById('btn-nearest').addEventListener('click', () => {
if (!userLocation || !spbuData.length) return;
let nearestSPBU = null, minDist = Infinity;
const userPt = turf.point(userLocation);
spbuData.forEach(spbu => {
const lat = parseFloat(spbu.latitude), lng = parseFloat(spbu.longitude);
if (isNaN(lat) || isNaN(lng)) return;
const dist = turf.distance(userPt, turf.point([lng, lat]), { units: 'kilometers' });
if (dist < minDist) { minDist = dist; nearestSPBU = spbu; }
});
if (!nearestSPBU) return;
if (routeLine) map.removeLayer(routeLine);
const lat = parseFloat(nearestSPBU.latitude), lng = parseFloat(nearestSPBU.longitude);
routeLine = L.polyline([[userLocation[1], userLocation[0]], [lat, lng]], {
color: '#ef4444', weight: 3, dashArray: '8, 8', opacity: 0.85
}).addTo(map);
map.fitBounds(routeLine.getBounds(), { padding: [60, 60] });
document.getElementById('nearest-name').textContent = nearestSPBU.nama + ' (No. ' + nearestSPBU.no_spbu + ')';
document.getElementById('nearest-distance').textContent = minDist.toFixed(2);
document.getElementById('info-box').classList.add('active');
});
document.getElementById('btn-refresh').addEventListener('click', () => {
const btn = document.getElementById('btn-refresh');
btn.textContent = 'Memuat...';
loadSPBU().then(() => {
btn.textContent = '🔄 Refresh';
});
});
// Auto-refresh every 30 seconds
setInterval(() => {
loadSPBU(true); // silent refresh
}, 30000);
loadSPBU();
</script>
</body>
</html>