Files
2026-06-10 00:49:56 +07:00

993 lines
27 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS SPBU — Kota Pontianak</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
font-family: 'Inter', -apple-system, 'Segoe UI', sans-serif;
background-color: #f1f5f9;
}
#map {
margin-left: 300px;
height: 100vh;
width: calc(100% - 300px);
}
/* =========================================
SIDEBAR
========================================= */
#sidebar {
position: absolute;
left: 0; top: 0;
width: 300px;
height: 100%;
background: linear-gradient(180deg, #0f172a 0%, #1e293b 100%);
color: #f8fafc;
z-index: 1000;
padding: 0;
overflow-y: auto;
box-shadow: 4px 0 25px rgba(0,0,0,0.15);
display: flex;
flex-direction: column;
}
/* Scrollbar styling */
#sidebar::-webkit-scrollbar { width: 4px; }
#sidebar::-webkit-scrollbar-track { background: transparent; }
#sidebar::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
#sidebar h2 {
text-align: left;
margin: 0;
padding: 24px 20px 20px;
font-size: 20px;
font-weight: 700;
letter-spacing: -0.3px;
background: linear-gradient(135deg, rgba(6,182,212,0.1) 0%, rgba(139,92,246,0.08) 100%);
border-bottom: 1px solid rgba(255,255,255,0.06);
}
.section {
margin-top: 4px;
padding: 0 20px;
}
.section h3 {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 1.5px;
color: #64748b;
margin-bottom: 8px;
margin-top: 16px;
font-weight: 700;
}
.section h4 {
font-size: 12px;
color: #94a3b8;
margin-bottom: 6px;
margin-top: 14px;
font-weight: 600;
}
/* Search input */
#searchInput {
width: calc(100% - 40px);
margin: 16px 20px 4px;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid #334155;
background: rgba(15, 23, 42, 0.6);
color: #f8fafc;
font-family: inherit;
font-size: 13px;
outline: none;
transition: all 0.3s;
}
#searchInput::placeholder { color: #475569; }
#searchInput:focus {
border-color: #06b6d4;
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.1);
background: rgba(15, 23, 42, 0.8);
}
/* List items */
.list-item {
background: rgba(51, 65, 85, 0.5);
padding: 10px 12px;
margin-bottom: 6px;
border-radius: 10px;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid transparent;
}
.list-item:hover {
background: rgba(71, 85, 105, 0.6);
transform: translateX(4px);
border-color: rgba(6, 182, 212, 0.3);
box-shadow: 0 2px 12px rgba(6, 182, 212, 0.06);
}
.list-item div[style*="font-weight"] { font-size: 13px; }
.list-item small { font-size: 10px; color: #64748b !important; }
/* Sidebar buttons */
#sidebar button {
width: 100%;
padding: 10px;
margin-top: 5px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, #0891b2, #06b6d4);
color: white;
font-weight: 600;
font-family: inherit;
font-size: 13px;
cursor: pointer;
transition: all 0.25s;
box-shadow: 0 2px 8px rgba(6, 182, 212, 0.2);
}
#sidebar button:hover {
background: linear-gradient(135deg, #0e7490, #0891b2);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.3);
}
/* =========================================
POPUP FORM STYLING
========================================= */
.leaflet-popup-content {
font-family: 'Inter', sans-serif !important;
font-size: 13px;
line-height: 1.5;
min-width: 220px;
}
.leaflet-popup-content b {
font-size: 15px;
font-weight: 700;
color: #0f172a;
}
input[type="text"], select {
width: 100%;
padding: 8px 10px;
margin-bottom: 10px;
margin-top: 4px;
border-radius: 8px;
border: 1px solid #e2e8f0;
background: #f8fafc;
font-family: inherit;
font-size: 13px;
color: #0f172a;
outline: none;
transition: all 0.2s;
}
input[type="text"]:focus, select:focus {
border-color: #06b6d4;
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.1);
background: #ffffff;
}
form button[type="submit"] {
background: linear-gradient(135deg, #059669, #10b981);
color: white;
border: none;
padding: 10px 14px;
border-radius: 8px;
cursor: pointer;
width: 100%;
font-family: inherit;
font-size: 13px;
font-weight: 600;
transition: all 0.25s;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.2);
}
form button[type="submit"]:hover {
background: linear-gradient(135deg, #047857, #059669);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
/* Popup edit/delete buttons */
.leaflet-popup-content button {
padding: 6px 14px;
border: none;
border-radius: 6px;
cursor: pointer;
font-family: inherit;
font-size: 12px;
font-weight: 600;
transition: all 0.2s;
margin-right: 4px;
}
.leaflet-editing-icon {
width: 12px;
height: 12px;
}
</style>
</head>
<body>
<div id="sidebar">
<h2>🌍 WebGIS</h2>
<input type="text" id="searchInput" placeholder="Cari layer..." onkeyup="filterList()">
<div class="section">
<h3>📍 Layer</h3>
<h4>⛽ SPBU 24 Jam</h4>
<div id="list24"></div>
<h4>⛽ SPBU Tidak 24 Jam</h4>
<div id="listTidak"></div>
<h4>🛣️ Jalan</h4>
<div id="listJalan"></div>
<h4>🏡 Kavling</h4>
<div id="listKavling"></div>
</div>
<div class="section">
<h3>🎛 Tools</h3>
<button onclick="map.zoomIn()">Zoom In</button>
<button onclick="map.zoomOut()">Zoom Out</button>
</div>
</div>
<div id="map"></div>
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw/dist/leaflet.draw.css"/>
<script src="https://unpkg.com/leaflet-draw/dist/leaflet.draw.js"></script>
<script src="https://unpkg.com/leaflet-geometryutil"></script>
<script>
// =======================
// FUNGSI SEARCH LAYER
// =======================
function filterList() {
let input = document.getElementById('searchInput').value.toLowerCase();
let items = document.querySelectorAll('.list-item');
items.forEach(item => {
let text = item.innerText.toLowerCase();
item.style.display = text.includes(input) ? "block" : "none";
});
}
// =======================
// HAPUS DATA
// =======================
function hapusData(id){
if(confirm("Yakin hapus data?")){
fetch('hapus.php?id=' + id + '&type=spbu')
.then(res => res.text())
.then(res => {
alert("Data dihapus!");
location.reload();
});
}
}
// =======================
// EDIT DATA
// =======================
function editData(id, nama, wa, status){
let formEdit = `
<form id="editForm">
<input type="hidden" name="type" value="spbu">
<input type="hidden" name="id" value="${id}">
Nama SPBU:
<input type="text" name="nama_spbu" value="${nama}">
No WA:
<input type="text" name="no_wa" value="${wa}">
Status:
<select name="status">
<option value="24jam" ${status === '24jam' ? 'selected' : ''}>24 Jam</option>
<option value="tidak" ${status === 'tidak' ? 'selected' : ''}>Tidak</option>
</select>
<button type="submit">Update</button>
</form>
`;
L.popup()
.setLatLng(map.getCenter())
.setContent(formEdit)
.openOn(map);
}
// =======================
// SUBMIT EDIT
// =======================
document.addEventListener('submit', function(e){
if(e.target && e.target.id === 'editForm'){
e.preventDefault();
let formData = new FormData(e.target);
fetch('update.php', {
method: 'POST',
body: formData
})
.then(res => res.text())
.then(res => {
if (res.trim() === 'OK') {
alert("Data berhasil diupdate!");
location.reload();
} else {
alert("Gagal mengupdate data: " + res);
}
})
.catch(err => {
alert("Terjadi kesalahan jaringan: " + err.message);
});
}
});
// =======================
// INIT MAP
// =======================
// Menambahkan zoomControl: false agar tombol +- bawaan yang tertimpa sidebar hilang
const map = L.map('map', { zoomControl: false }).setView([-0.0557, 109.3491], 13);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(map);
// AKTIFKAN DRAW TOOL
var drawnItems = new L.FeatureGroup();
map.addLayer(drawnItems);
var drawControl = new L.Control.Draw({
position: 'topright', // POSISI DRAW TOOL DIUBAH KE KANAN ATAS
draw: {
polygon: true,
polyline: true,
marker: true,
circle: false,
rectangle: false
},
edit: {
featureGroup: drawnItems,
edit: true,
remove: true
}
});
// HITUNG PANJANG & LUAS OTOMATIS
map.addControl(drawControl);
map.on(L.Draw.Event.CREATED, function (e) {
var layer = e.layer;
var type = e.layerType;
// =====================
// MARKER (SPBU)
// =====================
if(type === 'marker'){
var spbuIcon = L.icon({
iconUrl: 'https://maps.google.com/mapfiles/ms/icons/blue-dot.png',
iconSize: [32, 32]
});
layer.setIcon(spbuIcon);
let lat = layer.getLatLng().lat;
let lng = layer.getLatLng().lng;
let form = `
<form id="formData">
<b>Tambah SPBU</b><br><br>
Nama SPBU:
<input type="text" name="nama_spbu" required>
No WhatsApp:
<input type="text" name="no_wa" required>
Status:
<select name="status">
<option value="24jam">Buka 24 Jam</option>
<option value="tidak">Tidak</option>
</select>
<input type="hidden" name="latitude" value="${lat}">
<input type="hidden" name="longitude" value="${lng}">
<button type="submit">Simpan</button>
</form>
`;
layer.bindPopup(form).openPopup();
}
// =====================
// TAMBAH KE MAP
// =====================
drawnItems.addLayer(layer);
let geojson = layer.toGeoJSON();
// =====================
// POLYLINE
// =====================
if(type === 'polyline'){
layer.setStyle({
color: 'blue',
weight: 4
});
let latlngs = layer.getLatLngs();
let panjang = 0;
for(let i=0; i<latlngs.length-1; i++){
panjang += latlngs[i].distanceTo(latlngs[i+1]);
}
layer.bindPopup(`
<form id="formJalan">
Nama Jalan:
<input type="text" name="nama_jalan">
Status:
<select name="status">
<option value="nasional">Nasional</option>
<option value="provinsi">Provinsi</option>
<option value="kabupaten">Kabupaten</option>
</select>
Panjang: ${panjang.toFixed(2)} meter
<input type="hidden" name="panjang" value="${panjang}">
<input type="hidden" name="geom" value='${JSON.stringify(geojson)}'>
<button type="submit">Simpan</button>
</form>
`).openPopup();
}
// =====================
// POLYGON
// =====================
if(type === 'polygon'){
let latlngs = layer.getLatLngs()[0];
let luas = L.GeometryUtil.geodesicArea(latlngs);
layer.myId = null; // sementara
drawnItems.addLayer(layer);
layer.bindPopup(`
<form id="formKavling">
Nama Pemilik:
<input type="text" name="pemilik">
Status:
<select name="status">
<option value="SHM">SHM</option>
<option value="HGB">HGB</option>
<option value="HGU">HGU</option>
<option value="HP">HP</option>
</select>
Luas: ${luas.toFixed(2)}
<input type="hidden" name="luas" value="${luas}">
<input type="hidden" name="geom" value='${JSON.stringify(geojson)}'>
<button type="submit">Simpan</button>
</form>
`).openPopup();
}
});
// WARNA OTOMATIS
// Polyline
function getWarnaJalan(status){
if(status === 'nasional') return 'blue';
if(status === 'provinsi') return 'green';
return 'red';
}
// Polygon
function getWarnaKavling(status){
if(status === 'SHM') return 'green';
if(status === 'HGB') return 'blue';
if(status === 'HGU') return 'orange';
return 'purple';
}
// SIMPAN JALAN
document.addEventListener('submit', function(e){
if(e.target.id === 'formJalan'){
e.preventDefault();
let formData = new FormData(e.target);
fetch('simpan_jalan.php', {
method: 'POST',
body: formData
})
.then(res => res.text())
.then(res => {
if (res.trim() === 'OK') {
alert("Jalan disimpan!");
location.reload();
} else {
alert("Gagal menyimpan jalan: " + res);
}
})
.catch(err => {
alert("Terjadi kesalahan jaringan: " + err.message);
});
}
});
// SIMPAN KAVLING
document.addEventListener('submit', function(e){
if(e.target.id === 'formKavling'){
e.preventDefault();
let formData = new FormData(e.target);
fetch('simpan_kavling.php', {
method: 'POST',
body: formData
})
.then(res => res.text())
.then(res => {
if (res.trim() === 'OK') {
alert("Kavling disimpan!");
location.reload();
} else {
alert("Gagal menyimpan kavling: " + res);
}
})
.catch(err => {
alert("Terjadi kesalahan jaringan: " + err.message);
});
}
});
// =======================
// ICON MARKER
// =======================
const greenIcon = new L.Icon({
iconUrl: 'https://maps.google.com/mapfiles/ms/icons/green-dot.png',
iconSize: [32, 32]
});
const redIcon = new L.Icon({
iconUrl: 'https://maps.google.com/mapfiles/ms/icons/red-dot.png',
iconSize: [32, 32]
});
// =======================
// LAYER GROUP SPBU
// =======================
var spbu24Jam = L.layerGroup();
var spbuTidak24Jam = L.layerGroup();
// Tambahkan ke map default (opsional)
spbu24Jam.addTo(map);
spbuTidak24Jam.addTo(map);
fetch('get_all.php')
.then(res => res.json())
.then(data => {
// =====================
// SPBU (POINT)
// =====================
if(data.spbu) { // Ditambahkan pengecekan agar tidak error jika data kosong
data.spbu.forEach(item => {
let icon = item.status === '24jam' ? greenIcon : redIcon;
let marker = L.marker([item.latitude, item.longitude], {
icon: icon,
draggable: true
});
marker.myId = item.id;
// MASUKKAN KE GROUP SESUAI STATUS
drawnItems.addLayer(marker);
// tetap simpan ke group hanya untuk kontrol layer
if(item.status === '24jam'){
spbu24Jam.addLayer(marker);
} else {
spbuTidak24Jam.addLayer(marker);
}
// tetap tambahkan ke drawnItems jika ingin bisa edit
drawnItems.addLayer(marker);
marker.bindPopup(`
<b>${item.nama_spbu}</b><br>
WA: ${item.no_wa}<br>
Status: ${item.status}<br><br>
<button onclick="editData(${item.id}, '${item.nama_spbu}', '${item.no_wa}', '${item.status}')">Edit</button>
<button onclick="hapusData(${item.id})">Hapus</button>
`);
marker.on('dragend', function(e){
let posisi = e.target.getLatLng();
fetch('update_posisi.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `id=${item.id}&lat=${posisi.lat}&lng=${posisi.lng}`
});
});
tambahKeList(
item.nama_spbu,
[item.latitude, item.longitude],
marker,
item.status,
'spbu'
);
});
}
// =======================
// LAYER CONTROL
// =======================
var overlayMaps = {
"SPBU 24 Jam": spbu24Jam,
"SPBU Tidak 24 Jam": spbuTidak24Jam
};
L.control.layers(null, overlayMaps, {
collapsed: false
}).addTo(map);
// =====================
// KAVLING (POLYGON)
// =====================
if(data.kavling) {
data.kavling.forEach(item => {
try {
let geojson = JSON.parse(item.geom);
let warna = getWarnaKavling(item.status);
let layer = L.geoJSON(geojson, {
style: {
color: warna,
fillOpacity: 0.5
}
});
layer.eachLayer(l => {
l.myId = item.id;
drawnItems.addLayer(l);
l.bindPopup(`
<b>${item.pemilik || 'Tanpa Nama'}</b><br>
Status: ${item.status}<br>
Luas: ${parseFloat(item.luas).toFixed(2)} m²<br><br>
<button onclick="editKavling(${item.id}, '${item.pemilik || ''}', '${item.status}')">Edit</button>
<button onclick="hapusKavling(${item.id})">Hapus</button>
`);
tambahKeList(item.pemilik || 'Kavling', l.getBounds().getCenter(), l, null, 'kavling');
});
} catch(e){
console.log("Geom error kavling ID:", item.id);
}
});
}
// =====================
// JALAN (POLYLINE)
// =====================
if(data.jalan) {
data.jalan.forEach(item => {
try {
let geojson = JSON.parse(item.geom);
let warna = getWarnaJalan(item.status);
let layer = L.geoJSON(geojson, {
style: {
color: warna,
weight: 4
}
});
layer.eachLayer(l => {
l.myId = item.id;
drawnItems.addLayer(l);
l.bindPopup(`
<b>${item.nama_jalan}</b><br>
Status: ${item.status}<br>
Panjang: ${parseFloat(item.panjang).toFixed(2)} meter<br><br>
<button onclick="editJalan(${item.id}, '${item.nama_jalan}', '${item.status}')">Edit</button>
<button onclick="hapusJalan(${item.id})">Hapus</button>
`);
tambahKeList(item.nama_jalan, l.getBounds().getCenter(), l, null, 'jalan');
});
} catch(e){
console.log("Geom error jalan ID:", item.id);
}
});
}
});
// =======================
// SUBMIT FORM
// =======================
document.addEventListener('submit', function(e){
if(e.target && e.target.id === 'formData'){
e.preventDefault();
let formData = new FormData(e.target);
fetch('simpan.php', {
method: 'POST',
body: formData
})
.then(res => res.text())
.then(res => {
if (res.trim() === 'OK') {
alert("Data berhasil disimpan!");
location.reload();
} else {
alert("Gagal menyimpan data: " + res);
}
})
.catch(err => {
alert("Terjadi kesalahan jaringan: " + err.message);
});
}
});
map.on(L.Draw.Event.EDITED, function (e) {
let layers = e.layers;
layers.eachLayer(function(layer){
// =====================
// MARKER (SPBU)
// =====================
if(layer instanceof L.Marker){
let latlng = layer.getLatLng();
fetch('update_posisi.php', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `id=${layer.myId}&lat=${latlng.lat}&lng=${latlng.lng}`
});
}
// =====================
// POLYLINE / POLYGON
// =====================
else {
let geojson = layer.toGeoJSON();
let type = (layer instanceof L.Polygon) ? 'kavling' : 'jalan';
fetch('update_geom.php', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
id: layer.myId,
geom: geojson,
type: type
})
});
}
});
});
function hapusJalan(id){
if(confirm("Yakin hapus jalan?")){
fetch('hapus.php?id=' + id + '&type=jalan')
.then(res => res.text())
.then(res => {
alert("Jalan dihapus!");
location.reload();
});
}
}
function hapusKavling(id){
if(confirm("Yakin hapus kavling?")){
fetch('hapus.php?id=' + id + '&type=kavling')
.then(() => {
alert("Kavling dihapus!");
location.reload();
});
}
}
function editJalan(id, nama, status){
let form = `
<form id="editJalanForm">
<input type="hidden" name="type" value="jalan">
<input type="hidden" name="id" value="${id}">
Nama Jalan:
<input type="text" name="nama_jalan" value="${nama}" required>
Status:
<select name="status">
<option value="nasional" ${status==='nasional'?'selected':''}>Nasional</option>
<option value="provinsi" ${status==='provinsi'?'selected':''}>Provinsi</option>
<option value="kabupaten" ${status==='kabupaten'?'selected':''}>Kabupaten</option>
</select>
<button type="submit">Update</button>
</form>
`;
L.popup()
.setLatLng(map.getCenter())
.setContent(form)
.openOn(map);
}
document.addEventListener('submit', function(e){
// EDIT JALAN
if(e.target.id === 'editJalanForm'){
e.preventDefault();
let formData = new FormData(e.target);
fetch('update.php', {
method: 'POST',
body: formData
})
.then(res => res.text())
.then(res => {
if (res.trim() === 'OK') {
alert("Jalan berhasil diupdate!");
location.reload();
} else {
alert("Gagal mengupdate jalan: " + res);
}
})
.catch(err => {
alert("Terjadi kesalahan jaringan: " + err.message);
});
}
// EDIT KAVLING
if(e.target.id === 'editKavlingForm'){
e.preventDefault();
let formData = new FormData(e.target);
fetch('update.php', {
method: 'POST',
body: formData
})
.then(res => res.text())
.then(res => {
if (res.trim() === 'OK') {
alert("Kavling berhasil diupdate!");
location.reload();
} else {
alert("Gagal mengupdate kavling: " + res);
}
})
.catch(err => {
alert("Terjadi kesalahan jaringan: " + err.message);
});
}
});
function editKavling(id, pemilik, status){
let form = `
<form id="editKavlingForm">
<input type="hidden" name="type" value="kavling">
<input type="hidden" name="id" value="${id}">
Nama Pemilik:
<input type="text" name="pemilik" value="${pemilik}">
Status:
<select name="status">
<option value="SHM" ${status==='SHM'?'selected':''}>SHM</option>
<option value="HGB" ${status==='HGB'?'selected':''}>HGB</option>
<option value="HGU" ${status==='HGU'?'selected':''}>HGU</option>
<option value="HP" ${status==='HP'?'selected':''}>HP</option>
</select>
<button type="submit">Update</button>
</form>
`;
L.popup().setLatLng(map.getCenter()).setContent(form).openOn(map);
}
map.on(L.Draw.Event.DRAWSTART, function(){
console.log("Mulai gambar");
});
map.on(L.Draw.Event.DRAWSTOP, function(){
console.log("Selesai gambar");
});
function tambahKeList(nama, latlng, layer, status, type){
let div;
let icon = "";
if (type === 'jalan') {
div = document.getElementById('listJalan');
icon = "🛣️";
} else if (type === 'kavling') {
div = document.getElementById('listKavling');
icon = "🏡";
} else {
// PILIH CONTAINER BERDASARKAN STATUS
if(status === '24jam'){
div = document.getElementById('list24');
} else {
div = document.getElementById('listTidak');
}
icon = "⛽";
}
let item = document.createElement('div');
item.className = 'list-item';
item.innerHTML = `
<div style="font-weight:bold">${icon} ${nama}</div>
<small style="color:#cbd5f5;">Klik untuk fokus</small>
`;
item.onclick = function(){
map.setView(latlng, 16);
if(layer.getBounds){
map.fitBounds(layer.getBounds());
}
layer.openPopup();
};
div.appendChild(item);
}
</script>
</body>
</html>