Files
project-webgis/webgisspbu-parsiltanah/index.php
T
2026-06-11 10:16:53 +07:00

1111 lines
50 KiB
PHP

<!DOCTYPE html>
<html lang="id">
<head>
<base target="_top">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebGIS SPBU & Parsil Tanah</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
<link rel="stylesheet" href="https://unpkg.com/@geoman-io/leaflet-geoman-free@latest/dist/leaflet-geoman.css" />
<script src="https://unpkg.com/@geoman-io/leaflet-geoman-free@latest/dist/leaflet-geoman.min.js"></script>
<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>
<style>
/* ─── DESIGN TOKENS ─── */
:root {
--blue: #2563EB;
--blue-d: #1D4ED8;
--blue-l: #EFF6FF;
--navy: #0F172A;
--green: #10B981;
--green-d: #059669;
--green-l: #ECFDF5;
--red: #EF4444;
--amber: #F59E0B;
--surface: #FFFFFF;
--border: #E2E8F0;
--border-l:#F1F5F9;
--text-2: #475569;
--text-3: #94A3B8;
--r: 16px;
--r2: 12px;
--r3: 8px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; font-family: 'Plus Jakarta Sans', sans-serif; overflow: hidden; background: #f8fafc; }
/* ── NAVBAR ── */
#navbar {
position: fixed;
top: 0; left: 0; right: 0;
height: 56px;
z-index: 1200;
background: rgba(255,255,255,0.92);
backdrop-filter: blur(24px);
border-bottom: 1px solid rgba(226,232,240,0.7);
box-shadow: 0 1px 16px rgba(15,23,42,0.08);
display: flex;
align-items: center;
padding: 0 16px;
}
.nav-brand {
display: flex;
align-items: center;
gap: 9px;
text-decoration: none;
}
.nav-brand-logo {
width: 32px; height: 32px;
border-radius: 9px;
background: linear-gradient(135deg, #10B981, #059669);
display: flex; align-items: center; justify-content: center;
box-shadow: 0 2px 8px rgba(16,185,129,0.35);
}
.nav-brand-text {
font-size: 14px;
font-weight: 800;
color: var(--navy);
letter-spacing: -0.3px;
}
/* ── MAP ── */
#map {
position: fixed;
top: 56px;
left: 0; right: 0; bottom: 0;
z-index: 0;
}
/* ── MODERN POPUP (LEAFLET OVERRIDE) ── */
.leaflet-popup-content-wrapper {
border-radius: var(--r) !important;
border: none !important;
box-shadow: 0 16px 48px rgba(15,23,42,0.2) !important;
font-family: 'Plus Jakarta Sans',sans-serif !important;
padding: 0 !important;
overflow: hidden !important;
}
.leaflet-popup-content {
margin: 0 !important;
width: 260px !important;
display: flex !important;
flex-direction: column !important;
}
.lp-hd {
padding: 14px 16px 10px;
border-bottom: 1px solid var(--border-l);
background: linear-gradient(to bottom, #f8fafc, white);
}
.lp-ti { font-size: 14px; font-weight: 800; color: var(--navy); }
.lp-su { font-size: 11px; color: var(--text-3); font-weight: 500; margin-top: 2px;}
.lp-bd { padding: 14px 16px; font-size: 12px; color: var(--text-2); line-height: 1.6; }
.ff { margin-bottom: 12px; }
.ff label {
display: block; font-size: 10px; font-weight: 700;
color: var(--text-3); text-transform: uppercase;
letter-spacing: 0.05em; margin-bottom: 6px;
}
.ff input, .ff select {
width: 100%; padding: 9px 12px; font-size: 12px; font-family: inherit;
border: 1.5px solid var(--border); border-radius: var(--r3);
color: var(--navy); transition: all 0.2s; background: var(--border-l);
}
.ff input:focus, .ff select:focus {
outline: none; border-color: var(--green);
box-shadow: 0 0 0 3px rgba(16,185,129,0.15); background: #fff;
}
.lp-btns {
padding: 12px 16px 16px;
background: var(--border-l);
border-top: 1px solid var(--border-l);
display: flex; gap: 8px;
}
.btn-sv {
flex: 1; padding: 10px; border-radius: var(--r3); border: none;
font-family: inherit; font-size: 12px; font-weight: 700;
color: white; cursor: pointer; transition: all 0.2s;
display: flex; align-items: center; justify-content: center; gap: 6px;
}
.btn-sv.g { background: var(--green); }
.btn-sv.g:hover { background: var(--green-d); }
.btn-sv.b { background: var(--blue); }
.btn-sv.b:hover { background: var(--blue-d); }
.btn-sv.r { background: var(--red); }
.btn-sv.r:hover { background: #DC2626; }
.btn-sv.w { background: var(--amber); color: #fff;}
.btn-sv.w:hover { background: #D97706; }
.btn-sv:active { transform: scale(0.98); }
.btn-cncl {
padding: 10px 14px; border-radius: var(--r3); border: 1.5px solid var(--border);
background: white; font-family: inherit; font-size: 12px; font-weight: 700;
color: var(--text-2); cursor: pointer; transition: all 0.2s;
}
.btn-cncl:hover { background: var(--border-l); color: var(--navy); }
/* ── MODERN MARKERS ── */
.modern-marker {
position: relative;
width: 30px;
height: 42px;
display: flex;
align-items: flex-start;
justify-content: center;
}
.modern-marker .m-pin {
position: relative;
width: 28px;
height: 28px;
background: var(--m-color);
border-radius: 50% 50% 50% 0;
transform: rotate(-45deg);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(15,23,42,0.3);
border: 2px solid white;
z-index: 2;
transition: transform 0.2s;
}
.modern-marker:hover .m-pin {
transform: rotate(-45deg) scale(1.1);
}
.modern-marker .m-pin svg {
transform: rotate(45deg);
display: block;
}
.modern-marker .m-pulse {
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 12px;
height: 12px;
background: var(--m-color);
border-radius: 50%;
opacity: 0.6;
animation: mPulse 2s infinite ease-out;
z-index: 1;
}
@keyframes mPulse {
0% { transform: translateX(-50%) scale(1); opacity: 0.6; }
100% { transform: translateX(-50%) scale(3); opacity: 0; }
}
/* ── LEAFLET LAYERS CONTROL ── */
.leaflet-control-layers {
border: none !important;
border-radius: 16px !important;
box-shadow: 0 12px 32px rgba(15,23,42,0.12), 0 0 0 1px rgba(226,232,240,0.8) !important;
background: rgba(255,255,255,0.95) !important;
backdrop-filter: blur(16px);
padding: 6px !important;
font-family: 'Plus Jakarta Sans', sans-serif !important;
margin-top: 16px !important;
margin-right: 16px !important;
}
.leaflet-control-layers-list {
margin-bottom: 0 !important;
}
.leaflet-control-layers-overlays label {
display: flex;
align-items: center;
flex-direction: row;
padding: 8px 12px;
margin: 2px 0;
border-radius: 10px;
cursor: pointer;
transition: background 0.2s;
}
.leaflet-control-layers-overlays label:hover {
background: var(--blue-l);
}
.leaflet-control-layers-overlays label > span {
display: flex;
align-items: center;
flex: 1;
}
.leaflet-control-layers-selector {
margin: 0 12px 0 0 !important;
width: 16px;
height: 16px;
cursor: pointer;
accent-color: var(--blue);
flex-shrink: 0;
}
.layer-item {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
font-weight: 600;
color: var(--navy);
margin-left: 2px;
}
/* ── SEMBUNYIKAN TOOLBAR GEOMAN DEFAULT ── */
.leaflet-pm-toolbar { display: none !important; }
/* ── DRAW GUIDE BADGE ── */
#draw-guide {
background: rgba(255,255,255,0.95);
backdrop-filter: blur(16px);
border-radius: 14px;
box-shadow: 0 8px 24px rgba(15,23,42,0.12), 0 0 0 1px rgba(226,232,240,0.8);
padding: 12px 14px;
width: 100%;
margin-top: 4px;
display: none;
}
#draw-guide.visible { display: block; animation: fadeInUp 0.3s ease; }
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.dg-title {
font-size: 11px; font-weight: 800; color: var(--navy);
text-transform: uppercase; letter-spacing: 0.05em;
margin-bottom: 8px;
display: flex; align-items: center; gap: 6px;
}
.dg-step {
display: flex; align-items: flex-start; gap: 8px;
font-size: 11px; color: var(--text-2); font-weight: 500;
margin-bottom: 6px; line-height: 1.4;
}
.dg-step:last-child { margin-bottom: 0; }
.dg-num {
width: 16px; height: 16px; border-radius: 50%;
background: var(--blue); color: white;
font-size: 9px; font-weight: 800;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0; margin-top: 1px;
}
/* ── TOOL PANEL KUSTOM ── */
#tool-panel {
position: fixed;
top: 64px;
left: 16px;
z-index: 1100;
display: flex;
flex-direction: column;
gap: 6px;
}
.tp-section-label {
font-size: 11px;
font-weight: 800;
color: var(--navy);
background: rgba(226,232,240,0.8);
backdrop-filter: blur(8px);
padding: 4px 10px;
border-radius: 6px;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-top: 8px;
align-self: flex-start;
}
.tp-btn {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
border-radius: 12px;
background: rgba(255,255,255,0.95);
backdrop-filter: blur(16px);
box-shadow: 0 4px 16px rgba(15,23,42,0.1), 0 0 0 1px rgba(226,232,240,0.8);
border: none;
cursor: pointer;
text-align: left;
transition: all 0.2s;
width: 100%;
min-width: 180px;
font-family: 'Plus Jakarta Sans', sans-serif;
}
.tp-btn:hover {
box-shadow: 0 8px 24px rgba(15,23,42,0.15), 0 0 0 1px rgba(226,232,240,0.8);
transform: translateX(3px);
}
.tp-btn.active {
background: var(--navy);
box-shadow: 0 8px 24px rgba(15,23,42,0.3);
}
.tp-btn.active .tp-name { color: white; }
.tp-btn.active .tp-desc { color: rgba(255,255,255,0.6); }
.tp-icon {
width: 32px;
height: 32px;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.tp-text { flex: 1; }
.tp-name {
font-size: 12px;
font-weight: 700;
color: var(--navy);
line-height: 1.2;
}
.tp-desc {
font-size: 10px;
color: var(--text-3);
font-weight: 500;
margin-top: 2px;
}
</style>
</head>
<body>
<!-- NAVBAR -->
<nav id="navbar">
<a href="#" class="nav-brand">
<div class="nav-brand-logo">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
</div>
<div class="nav-brand-text">WebGIS SPBU & Parsil Tanah</div>
</a>
</nav>
<div id="map"></div>
<!-- TOOL PANEL KUSTOM -->
<div id="tool-panel">
<div class="tp-section-label">Tambah Data</div>
<button class="tp-btn" id="tool-spbu" onclick="toggleTool('spbu')" title="Klik titik di peta untuk menambahkan SPBU">
<div class="tp-icon" style="background:#EFF6FF;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#2563EB" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/>
</svg>
</div>
<div class="tp-text">
<div class="tp-name">Tambah SPBU</div>
<div class="tp-desc">Klik titik di peta</div>
</div>
</button>
<button class="tp-btn" id="tool-jalan" onclick="toggleTool('jalan')">
<div class="tp-icon" style="background:#ECFDF5;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#10B981" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18M3 12h18M3 18h18"/>
</svg>
</div>
<div class="tp-text">
<div class="tp-name">Gambar Jalan</div>
<div class="tp-desc">Klik titik-titik jalur jalan</div>
</div>
</button>
<button class="tp-btn" id="tool-polygon" onclick="toggleTool('polygon')">
<div class="tp-icon" style="background:#FFFBEB;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="3 6 9 3 15 6 21 3 21 18 15 21 9 18 3 21"/>
</svg>
</div>
<div class="tp-text">
<div class="tp-name">Gambar Parsil</div>
<div class="tp-desc">Klik sudut-sudut bidang tanah</div>
</div>
</button>
<button class="tp-btn" id="tool-rectangle" onclick="toggleTool('rectangle')">
<div class="tp-icon" style="background:#FFF7ED;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#EA580C" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="5" width="18" height="14" rx="2"/>
</svg>
</div>
<div class="tp-text">
<div class="tp-name">Parsil Kotak</div>
<div class="tp-desc">Seret untuk buat persegi panjang</div>
</div>
</button>
<div class="tp-section-label">Edit</div>
<button class="tp-btn" id="tool-edit" onclick="toggleTool('edit')">
<div class="tp-icon" style="background:#F0FDF4;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#16A34A" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
</div>
<div class="tp-text">
<div class="tp-name">Edit Bentuk</div>
<div class="tp-desc">Geser titik sudut layer</div>
</div>
</button>
<button class="tp-btn" id="tool-drag" onclick="toggleTool('drag')">
<div class="tp-icon" style="background:#F5F3FF;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#7C3AED" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="5 9 2 12 5 15"/><polyline points="9 5 12 2 15 5"/>
<polyline points="15 19 12 22 9 19"/><polyline points="19 9 22 12 19 15"/>
<line x1="2" y1="12" x2="22" y2="12"/><line x1="12" y1="2" x2="12" y2="22"/>
</svg>
</div>
<div class="tp-text">
<div class="tp-name">Pindah Layer</div>
<div class="tp-desc">Seret layer ke posisi baru</div>
</div>
</button>
<button class="tp-btn" id="tool-delete" onclick="toggleTool('delete')">
<div class="tp-icon" style="background:#FEF2F2;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#EF4444" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
<path d="M10 11v6"/><path d="M14 11v6"/>
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
</svg>
</div>
<div class="tp-text">
<div class="tp-name">Hapus Layer</div>
<div class="tp-desc">Klik layer untuk menghapus</div>
</div>
</button>
<!-- KOTAK PANDUAN MENGGAMBAR -->
<div id="draw-guide"></div>
</div>
<script>
// ============================================================
// INISIALISASI PETA
// ============================================================
const map = L.map('map', { zoomControl: false }).setView([-0.0548, 109.3494], 13);
L.control.zoom({ position: 'bottomright' }).addTo(map);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; OpenStreetMap'
}).addTo(map);
let popup = L.popup();
let tempLatLng = null;
// ============================================================
// DEKLARASI LAYER
// ============================================================
let layerSPBU_24Jam = L.layerGroup().addTo(map);
let layerSPBU_Reguler = L.layerGroup().addTo(map);
let jalanLayer = L.layerGroup().addTo(map);
let kavlingLayer = L.layerGroup().addTo(map);
let pilihanLayer = {
"<span class='layer-item'><svg width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='#2563EB' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><path d='M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z'></path><circle cx='12' cy='10' r='3'></circle></svg> <span>SPBU Buka 24 Jam</span></span>": layerSPBU_24Jam,
"<span class='layer-item'><svg width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='#EF4444' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><path d='M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z'></path><circle cx='12' cy='10' r='3'></circle></svg> <span>SPBU Tutup Malam</span></span>": layerSPBU_Reguler,
"<span class='layer-item'><svg width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='#10B981' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><path d='M3 6h18M3 12h18M3 18h18'/></svg> <span>Jaringan Jalan</span></span>": jalanLayer,
"<span class='layer-item'><svg width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='#F59E0B' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'><polygon points='3 6 9 3 15 6 21 3 21 18 15 21 9 18 3 21'></polygon><line x1='9' y1='3' x2='9' y2='18'></line><line x1='15' y1='6' x2='15' y2='21'></line></svg> <span>Parsil Tanah</span></span>": kavlingLayer
};
L.control.layers(null, pilihanLayer, { collapsed: false, position: 'topright' }).addTo(map);
// ============================================================
// DEFINISI ICON WARNA
// ============================================================
const customMarkerSVG = (color) => `
<div class="modern-marker" style="--m-color: ${color}">
<div class="m-pulse"></div>
<div class="m-pin">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
</div>
</div>
`;
const iconMerah = L.divIcon({
className: 'custom-div-icon',
html: customMarkerSVG('#EF4444'),
iconSize: [30, 42],
iconAnchor: [15, 42],
popupAnchor: [0, -32]
});
const iconBiru = L.divIcon({
className: 'custom-div-icon',
html: customMarkerSVG('#2563EB'),
iconSize: [30, 42],
iconAnchor: [15, 42],
popupAnchor: [0, -32]
});
// ============================================================
// KLIK PETA - FORM TAMBAH SPBU SAJA
// ============================================================
map.on('click', function(e) {
if (map.pm.globalDrawModeEnabled()) return;
tempLatLng = e.latlng;
let formHTML = `
<div class="lp-hd">
<div class="lp-ti">Tambah SPBU Baru</div>
<div class="lp-su">Input data lokasi stasiun pengisian bahan bakar</div>
</div>
<div class="lp-bd">
<div class="ff">
<label>Nama SPBU</label>
<input type="text" id="nama_spbu" placeholder="Masukkan nama SPBU" required>
</div>
<div class="ff">
<label>Nama Pengelola</label>
<input type="text" id="nama_pengelola" placeholder="Masukkan nama pengelola" required>
</div>
<div class="ff">
<label>No. Whatsapp</label>
<input type="text" id="no_wa" placeholder="Contoh: 0812..." required>
</div>
<div class="ff" style="margin-bottom:0;">
<label>Buka 24 Jam?</label>
<select id="buka_24jam">
<option value="" disabled selected>Pilih status</option>
<option value="Ya">Ya (Buka 24 Jam)</option>
<option value="Tidak">Tidak</option>
</select>
</div>
</div>
<div class="lp-btns">
<button class="btn-sv g" onclick="simpanSPBU()">Simpan Data</button>
</div>
`;
popup.setLatLng(e.latlng).setContent(formHTML).openOn(map);
});
// ============================================================
// BAGIAN SPBU CRUD
// ============================================================
function simpanSPBU() {
let nama = document.getElementById('nama_spbu').value;
let pengelola = document.getElementById('nama_pengelola').value;
let wa = document.getElementById('no_wa').value;
let jam = document.getElementById('buka_24jam').value;
if (!nama || !pengelola || !wa || jam === "") { alert("Harap isi semua data termasuk status 24 jam!"); return; }
let fd = new FormData();
fd.append('nama_spbu', nama); fd.append('nama_pengelola', pengelola); fd.append('no_wa', wa);
fd.append('buka_24jam', jam); fd.append('lat', tempLatLng.lat); fd.append('lng', tempLatLng.lng);
let btn = document.querySelector('.btn-sv.g');
let oldText = btn.innerHTML;
btn.innerHTML = "Menyimpan...";
btn.disabled = true;
fetch('simpan.php', { method: 'POST', body: fd })
.then(res => res.text()).then(data => {
if (data.includes("Sukses")) { map.closePopup(); muatDataSPBU(); }
else { alert("Gagal menyimpan: " + data); btn.innerHTML = oldText; btn.disabled = false;}
}).catch(error => { console.error('Error:', error); btn.innerHTML = oldText; btn.disabled = false; });
}
function muatDataSPBU() {
fetch('tampil.php')
.then(res => res.json())
.then(data => {
layerSPBU_24Jam.clearLayers();
layerSPBU_Reguler.clearLayers();
data.forEach(spbu => {
let is24 = (spbu.buka_24jam.trim() === 'Ya');
let markerHTML = `
<div id="info_${spbu.id}">
<div class="lp-hd">
<div class="lp-ti">${spbu.nama_spbu}</div>
<div class="lp-su">Pengelola: ${spbu.nama_pengelola || '-'}</div>
<div class="lp-su">Kontak: ${spbu.no_wa}</div>
</div>
<div class="lp-bd">
Status: <b>${is24 ? '<span style="color:#10B981;">Buka 24 Jam</span>' : '<span style="color:#EF4444;">Tutup Malam</span>'}</b><br>
<i style="font-size:10px; color:#94A3B8; display:block; margin-top:8px;">Geser pin untuk mengubah lokasi koordinat.</i>
</div>
<div class="lp-btns" style="display:flex; gap:6px;">
<button class="btn-sv w" onclick="bukaFormEdit(${spbu.id})">Edit</button>
<button class="btn-sv r" onclick="hapusSPBU(${spbu.id})">Hapus</button>
</div>
</div>
<div id="form_${spbu.id}" style="display:none;">
<div class="lp-hd">
<div class="lp-ti">Edit SPBU</div>
</div>
<div class="lp-bd">
<div class="ff">
<label>Nama SPBU</label>
<input type="text" id="edit_nama_${spbu.id}" value="${spbu.nama_spbu}">
</div>
<div class="ff">
<label>Nama Pengelola</label>
<input type="text" id="edit_pengelola_${spbu.id}" value="${spbu.nama_pengelola || ''}">
</div>
<div class="ff">
<label>No. Whatsapp</label>
<input type="text" id="edit_wa_${spbu.id}" value="${spbu.no_wa}">
</div>
<div class="ff" style="margin-bottom:0;">
<label>Buka 24 Jam?</label>
<select id="edit_jam_${spbu.id}">
<option value="Ya" ${is24 ? 'selected' : ''}>Ya</option>
<option value="Tidak" ${!is24 ? 'selected' : ''}>Tidak</option>
</select>
</div>
</div>
<div class="lp-btns">
<button class="btn-cncl" onclick="batalEdit(${spbu.id})">Batal</button>
<button class="btn-sv b" onclick="simpanEdit(${spbu.id})">Update</button>
</div>
</div>
`;
let opsiMarker = { draggable: true, icon: is24 ? iconBiru : iconMerah };
let wadahTujuan = is24 ? layerSPBU_24Jam : layerSPBU_Reguler;
let marker = L.marker([spbu.lat, spbu.lng], opsiMarker).addTo(wadahTujuan).bindPopup(markerHTML);
marker.on('dragend', function(e) {
let posisiBaru = e.target.getLatLng();
if (confirm("Simpan posisi koordinat baru untuk SPBU ini?")) {
updateLokasiSPBU(spbu.id, posisiBaru.lat, posisiBaru.lng);
} else { muatDataSPBU(); }
});
});
}).catch(error => console.error('Error:', error));
}
function bukaFormEdit(id) {
document.getElementById(`info_${id}`).style.display = 'none';
document.getElementById(`form_${id}`).style.display = 'block';
}
function batalEdit(id) {
document.getElementById(`info_${id}`).style.display = 'block';
document.getElementById(`form_${id}`).style.display = 'none';
}
function simpanEdit(id) {
let fd = new FormData();
fd.append('id', id);
fd.append('nama_spbu', document.getElementById(`edit_nama_${id}`).value);
fd.append('nama_pengelola', document.getElementById(`edit_pengelola_${id}`).value);
fd.append('no_wa', document.getElementById(`edit_wa_${id}`).value);
fd.append('buka_24jam', document.getElementById(`edit_jam_${id}`).value);
fetch('update.php', { method: 'POST', body: fd })
.then(res => res.text()).then(data => {
if (data.includes("Sukses")) { map.closePopup(); muatDataSPBU(); }
else { alert("Gagal update: " + data); }
}).catch(error => console.error('Error:', error));
}
function hapusSPBU(id) {
if (!confirm("Apakah Anda yakin ingin menghapus SPBU ini?")) return;
let fd = new FormData(); fd.append('id', id);
fetch('hapus.php', { method: 'POST', body: fd })
.then(res => res.text()).then(data => {
if (data.includes("Sukses")) { map.closePopup(); muatDataSPBU(); }
else { alert("Gagal menghapus: " + data); }
}).catch(error => console.error('Error:', error));
}
function updateLokasiSPBU(id, lat, lng) {
let fd = new FormData(); fd.append('id', id); fd.append('lat', lat); fd.append('lng', lng);
fetch('update_lokasi.php', { method: 'POST', body: fd })
.then(res => res.text()).then(data => {
if (!data.includes("Sukses")) { alert("Gagal memperbarui posisi: " + data); muatDataSPBU(); }
}).catch(error => console.error('Error:', error));
}
// ============================================================
// BAGIAN JALAN
// ============================================================
function muatDataJalan() {
fetch('tampil_jalan.php').then(res => res.json()).then(data => {
jalanLayer.clearLayers();
data.forEach(jalan => {
let warnaJalan = '#1E293B'; // navy default
if (jalan.status_jalan === 'Nasional') warnaJalan = '#EF4444';
else if (jalan.status_jalan === 'Provinsi') warnaJalan = '#2563EB';
else if (jalan.status_jalan === 'Kabupaten') warnaJalan = '#10B981';
let geojsonData = JSON.parse(jalan.geojson);
L.geoJSON(geojsonData, {
style: { color: warnaJalan, weight: 5, opacity: 0.8 },
onEachFeature: function (feature, layer) {
let infoPilihan = `
<div id="info_jalan_${jalan.id}">
<div class="lp-hd">
<div class="lp-ti">${jalan.nama_jalan}</div>
</div>
<div class="lp-bd">
Status: <b>${jalan.status_jalan}</b><br>
Panjang: ${jalan.panjang_jalan} m
</div>
<div class="lp-btns" style="display: flex; gap: 6px;">
<button class="btn-sv w" onclick="bukaFormEditJalan(${jalan.id})">Edit</button>
<button class="btn-sv r" onclick="hapusJalan(${jalan.id})">Hapus</button>
</div>
</div>
<div id="form_jalan_${jalan.id}" style="display:none;">
<div class="lp-hd">
<div class="lp-ti">Edit Jaringan Jalan</div>
</div>
<div class="lp-bd">
<div class="ff">
<label>Nama Jalan</label>
<input type="text" id="edit_nama_jalan_${jalan.id}" value="${jalan.nama_jalan}">
</div>
<div class="ff" style="margin-bottom:0;">
<label>Status Jalan</label>
<select id="edit_status_jalan_${jalan.id}">
<option value="Nasional" ${jalan.status_jalan === 'Nasional' ? 'selected' : ''}>Jalan Nasional</option>
<option value="Provinsi" ${jalan.status_jalan === 'Provinsi' ? 'selected' : ''}>Jalan Provinsi</option>
<option value="Kabupaten" ${jalan.status_jalan === 'Kabupaten' ? 'selected' : ''}>Jalan Kabupaten</option>
</select>
</div>
</div>
<div class="lp-btns">
<button class="btn-cncl" onclick="batalEditJalan(${jalan.id})">Batal</button>
<button class="btn-sv b" onclick="simpanEditJalan(${jalan.id})">Update</button>
</div>
</div>
`;
layer.bindPopup(infoPilihan);
}
}).addTo(jalanLayer);
});
}).catch(error => console.error('Error:', error));
}
function hapusJalan(id) {
if (!confirm("Hapus data jaringan jalan ini dari peta?")) return;
let fd = new FormData(); fd.append('id', id);
fetch('hapus_jalan.php', { method: 'POST', body: fd })
.then(res => res.text()).then(data => {
if (data.includes("Sukses")) { map.closePopup(); muatDataJalan(); }
else { alert("Gagal menghapus: " + data); }
}).catch(error => console.error('Error:', error));
}
function bukaFormEditJalan(id) {
document.getElementById(`info_jalan_${id}`).style.display = 'none';
document.getElementById(`form_jalan_${id}`).style.display = 'block';
}
function batalEditJalan(id) {
document.getElementById(`info_jalan_${id}`).style.display = 'block';
document.getElementById(`form_jalan_${id}`).style.display = 'none';
}
function simpanEditJalan(id) {
let namaBaru = document.getElementById(`edit_nama_jalan_${id}`).value;
let statusBaru = document.getElementById(`edit_status_jalan_${id}`).value;
if (!namaBaru) return alert("Nama jalan tidak boleh kosong!");
let fd = new FormData(); fd.append('id', id); fd.append('nama_jalan', namaBaru); fd.append('status_jalan', statusBaru);
fetch('update_jalan.php', { method: 'POST', body: fd })
.then(res => res.text()).then(data => {
if (data.includes("Sukses")) { map.closePopup(); muatDataJalan(); }
else { alert("Gagal update: " + data); }
}).catch(error => console.error('Error:', error));
}
// ============================================================
// BAGIAN KAVLING
// ============================================================
function muatDataKavling() {
fetch('tampil_kavling.php').then(res => res.json()).then(data => {
kavlingLayer.clearLayers();
data.forEach(kavling => {
let warnaKavling = '#94A3B8';
if (kavling.status_kepemilikan === 'SHM') warnaKavling = '#10B981';
else if (kavling.status_kepemilikan === 'HGB') warnaKavling = '#F59E0B';
else if (kavling.status_kepemilikan === 'HGU') warnaKavling = '#EF4444';
else if (kavling.status_kepemilikan === 'HP') warnaKavling = '#2563EB';
let badgeColor = warnaKavling;
let geojsonData = JSON.parse(kavling.geojson);
L.geoJSON(geojsonData, {
style: { color: warnaKavling, fillColor: warnaKavling, weight: 2, fillOpacity: 0.35 },
onEachFeature: function (feature, layer) {
let popupHTML = `
<div id="info_kav_${kavling.id}">
<div class="lp-hd">
<div class="lp-ti">Parsil Tanah</div>
<div class="lp-su" style="margin-top:4px;">
<span style="display:inline-block; background:${badgeColor}22; color:${badgeColor}; border:1px solid ${badgeColor}44; border-radius:20px; padding:2px 10px; font-size:11px; font-weight:700;">${kavling.status_kepemilikan}</span>
</div>
</div>
<div class="lp-bd">
Luas Tanah: <b>${parseFloat(kavling.luas_tanah).toLocaleString('id-ID')} m²</b>
</div>
<div class="lp-btns" style="display:flex; gap:6px;">
<button class="btn-sv w" onclick="bukaFormEditKavling(${kavling.id})">Edit</button>
<button class="btn-sv r" onclick="hapusKavling(${kavling.id})">Hapus</button>
</div>
</div>
<div id="form_kav_${kavling.id}" style="display:none;">
<div class="lp-hd">
<div class="lp-ti">Edit Parsil Tanah</div>
</div>
<div class="lp-bd">
<div class="ff" style="margin-bottom:0;">
<label>Status Kepemilikan</label>
<select id="edit_status_kav_${kavling.id}">
<option value="SHM" ${kavling.status_kepemilikan === 'SHM' ? 'selected' : ''}>Sertifikat Hak Milik (SHM)</option>
<option value="HGB" ${kavling.status_kepemilikan === 'HGB' ? 'selected' : ''}>Hak Guna Bangunan (HGB)</option>
<option value="HGU" ${kavling.status_kepemilikan === 'HGU' ? 'selected' : ''}>Hak Guna Usaha (HGU)</option>
<option value="HP" ${kavling.status_kepemilikan === 'HP' ? 'selected' : ''}>Hak Pakai (HP)</option>
</select>
</div>
</div>
<div class="lp-btns">
<button class="btn-cncl" onclick="batalEditKavling(${kavling.id})">Batal</button>
<button class="btn-sv b" onclick="simpanEditKavling(${kavling.id})">Update</button>
</div>
</div>
`;
layer.bindPopup(popupHTML);
}
}).addTo(kavlingLayer);
});
}).catch(error => console.error('Error:', error));
}
function bukaFormEditKavling(id) {
document.getElementById(`info_kav_${id}`).style.display = 'none';
document.getElementById(`form_kav_${id}`).style.display = 'block';
}
function batalEditKavling(id) {
document.getElementById(`info_kav_${id}`).style.display = 'block';
document.getElementById(`form_kav_${id}`).style.display = 'none';
}
function hapusKavling(id) {
if (!confirm('Hapus data parsil tanah ini dari peta?')) return;
let fd = new FormData(); fd.append('id', id);
fetch('hapus_kavling.php', { method: 'POST', body: fd })
.then(res => res.text()).then(data => {
if (data.includes('Sukses')) { map.closePopup(); muatDataKavling(); }
else { alert('Gagal menghapus: ' + data); }
}).catch(error => console.error('Error:', error));
}
function simpanEditKavling(id) {
let status = document.getElementById(`edit_status_kav_${id}`).value;
let fd = new FormData(); fd.append('id', id); fd.append('status_kepemilikan', status);
fetch('update_kavling.php', { method: 'POST', body: fd })
.then(res => res.text()).then(data => {
if (data.includes('Sukses')) { map.closePopup(); muatDataKavling(); }
else { alert('Gagal update: ' + data); }
}).catch(error => console.error('Error:', error));
}
// ============================================================
// GEOMAN - TOOLBAR & GAMBAR
// ============================================================
map.pm.addControls({
position: 'topleft',
drawMarker: false,
drawPolyline: true,
drawRectangle: true,
drawPolygon: true,
drawCircle: false,
cutLayer: false,
editMode: true,
dragMode: true,
removalMode: true,
});
// Aktifkan bahasa Indonesia untuk tooltip interaktif (petunjuk penggunaan tombol)
map.pm.setLang('id');
// ── FUNGSI TOGGLE TOOL PANEL ──
let activeTool = null;
function setActiveBtn(id) {
document.querySelectorAll('.tp-btn').forEach(b => b.classList.remove('active'));
if (id) document.getElementById('tool-' + id)?.classList.add('active');
}
function deactivateAll() {
map.pm.disableDraw();
map.pm.disableGlobalEditMode();
map.pm.disableGlobalDragMode();
map.pm.disableGlobalRemovalMode();
activeTool = null;
setActiveBtn(null);
guideEl.classList.remove('visible');
}
function toggleTool(tool) {
if (activeTool === tool) { deactivateAll(); return; }
deactivateAll();
activeTool = tool;
setActiveBtn(tool);
if (tool === 'jalan') { map.pm.enableDraw('Line'); showGuide('line'); }
if (tool === 'polygon') { map.pm.enableDraw('Polygon'); showGuide('polygon'); }
if (tool === 'rectangle') { map.pm.enableDraw('Rectangle'); showGuide('polygon'); }
if (tool === 'edit') { map.pm.enableGlobalEditMode(); }
if (tool === 'drag') { map.pm.enableGlobalDragMode(); }
if (tool === 'delete') { map.pm.enableGlobalRemovalMode(); }
if (tool === 'spbu') {
// Mode SPBU cukup dengan klik di peta (sudah default)
}
}
// Sinkronkan tombol panel saat Geoman selesai menggambar
map.on('pm:drawend', deactivateAll);
map.on('pm:create', () => { deactivateAll(); });
// ── PANDUAN MENGGAMBAR (badge bawah-kiri) ──
const guidePolygon = `
<div class="dg-title">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" stroke-width="2.5"><polygon points="3 6 9 3 15 6 21 3 21 18 15 21 9 18 3 21"/></svg>
Cara Tambah Parsil Tanah
</div>
<div class="dg-step"><div class="dg-num">1</div><span>Klik <b>Gambar Parsil / Parsil Kotak</b> di panel kiri</span></div>
<div class="dg-step"><div class="dg-num">2</div><span>Klik di peta untuk membuat titik sudut bidang tanah</span></div>
<div class="dg-step"><div class="dg-num">3</div><span>Double klik atau klik titik awal untuk menutup area</span></div>
<div class="dg-step"><div class="dg-num">4</div><span>Isi status kepemilikan lalu klik <b>Simpan</b></span></div>
`;
const guideLine = `
<div class="dg-title">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#10B981" stroke-width="2.5"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
Cara Tambah Jaringan Jalan
</div>
<div class="dg-step"><div class="dg-num">1</div><span>Klik <b>Gambar Jalan</b> di panel kiri</span></div>
<div class="dg-step"><div class="dg-num">2</div><span>Klik di peta untuk menandai titik-titik jalur jalan</span></div>
<div class="dg-step"><div class="dg-num">3</div><span>Double klik di titik terakhir untuk mengakhiri garis</span></div>
<div class="dg-step"><div class="dg-num">4</div><span>Isi nama & status jalan lalu klik <b>Simpan</b></span></div>
`;
const guideEl = document.getElementById('draw-guide');
function showGuide(type) {
if (type === 'polygon') { guideEl.innerHTML = guidePolygon; guideEl.classList.add('visible'); }
else if (type === 'line') { guideEl.innerHTML = guideLine; guideEl.classList.add('visible'); }
else { guideEl.classList.remove('visible'); }
}
let layerSementara = null;
let geojsonSementara = null;
map.on('pm:create', function(e) {
let layer = e.layer;
let type = e.shape;
layerSementara = layer;
geojsonSementara = layer.toGeoJSON();
if (type === 'Polygon' || type === 'Rectangle') {
let luasTanah = turf.area(geojsonSementara);
let formKavling = `
<div class="lp-hd">
<div class="lp-ti">Data Parsil Tanah</div>
</div>
<div class="lp-bd">
<div style="margin-bottom: 12px;">Luas: <b>${luasTanah.toFixed(2)} m²</b></div>
<div class="ff" style="margin-bottom:0;">
<label>Status Kepemilikan</label>
<select id="status_kavling">
<option value="" disabled selected>Pilih Status...</option>
<option value="SHM">Sertifikat Hak Milik (SHM)</option>
<option value="HGB">Sertifikat Hak Guna Bangunan (HGB)</option>
<option value="HGU">Sertifikat Hak Guna Usaha (HGU)</option>
<option value="HP">Sertifikat Hak Pakai (HP)</option>
</select>
</div>
</div>
<div class="lp-btns">
<button class="btn-cncl" onclick="batalGambar()">Batal</button>
<button class="btn-sv g" onclick="simpanKavling(${luasTanah})">Simpan</button>
</div>
`;
layer.bindPopup(formKavling).openPopup();
} else if (type === 'Line') {
let panjangJalan = turf.length(geojsonSementara, {units: 'kilometers'}) * 1000;
let formJalan = `
<div class="lp-hd">
<div class="lp-ti">Jaringan Jalan</div>
</div>
<div class="lp-bd">
<div style="margin-bottom: 12px;">Panjang: <b>${panjangJalan.toFixed(2)} meter</b></div>
<div class="ff">
<label>Nama Jalan</label>
<input type="text" id="nama_jalan" placeholder="Masukkan Nama Jalan">
</div>
<div class="ff" style="margin-bottom:0;">
<label>Status Jalan</label>
<select id="status_jalan">
<option value="" disabled selected>Pilih Status...</option>
<option value="Nasional">Jalan Nasional</option>
<option value="Provinsi">Jalan Provinsi</option>
<option value="Kabupaten">Jalan Kabupaten</option>
</select>
</div>
</div>
<div class="lp-btns">
<button class="btn-cncl" onclick="batalGambar()">Batal</button>
<button class="btn-sv g" onclick="simpanJalan(${panjangJalan})">Simpan</button>
</div>
`;
layer.bindPopup(formJalan).openPopup();
}
});
function simpanKavling(luas) {
let status = document.getElementById('status_kavling').value;
if (!status) return alert("Pilih status kepemilikan!");
let fd = new FormData();
fd.append('status', status); fd.append('luas', luas.toFixed(2)); fd.append('geojson', JSON.stringify(geojsonSementara));
let btn = event.target;
let oldText = btn.innerHTML;
btn.innerHTML = "Menyimpan...";
btn.disabled = true;
fetch('simpan_kavling.php', { method: 'POST', body: fd })
.then(res => res.text()).then(res => {
if (res.includes("Sukses")) {
map.closePopup(); map.removeLayer(layerSementara); muatDataKavling();
} else { alert("Gagal: " + res); btn.innerHTML = oldText; btn.disabled = false;}
}).catch(error => { console.error('Error AJAX:', error); btn.innerHTML = oldText; btn.disabled = false;});
}
function simpanJalan(panjang) {
let nama = document.getElementById('nama_jalan').value;
let status = document.getElementById('status_jalan').value;
if (!nama || !status) return alert("Isi nama dan status jalan!");
let fd = new FormData();
fd.append('nama', nama); fd.append('status', status);
fd.append('panjang', panjang.toFixed(2)); fd.append('geojson', JSON.stringify(geojsonSementara));
let btn = event.target;
let oldText = btn.innerHTML;
btn.innerHTML = "Menyimpan...";
btn.disabled = true;
fetch('simpan_jalan.php', { method: 'POST', body: fd })
.then(res => res.text()).then(res => {
if (res.includes("Sukses")) {
map.closePopup(); map.removeLayer(layerSementara); muatDataJalan();
} else { alert("Gagal: " + res); btn.innerHTML = oldText; btn.disabled = false;}
}).catch(error => { console.error('Error AJAX:', error); btn.innerHTML = oldText; btn.disabled = false;});
}
function batalGambar() {
map.removeLayer(layerSementara);
map.closePopup();
}
// ============================================================
// INIT LOAD
// ============================================================
muatDataSPBU();
muatDataJalan();
muatDataKavling();
</script>
</body>
</html>