1137 lines
50 KiB
PHP
1137 lines
50 KiB
PHP
<!DOCTYPE html>
|
||
<html lang="id">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>WebGIS Manajemen SPBU</title>
|
||
|
||
<!-- ── Leaflet CSS ─────────────────────────────────────────────────────── -->
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
||
<!-- ── Leaflet Draw CSS ──────────────────────────────────────────────── -->
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
|
||
|
||
<style>
|
||
/* ════════════════════════════════════════════════════════════════════
|
||
GLOBAL RESET & VARIABLES
|
||
════════════════════════════════════════════════════════════════════ */
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
:root {
|
||
--clr-bg: #0f172a;
|
||
--clr-header: #1e293b;
|
||
--clr-accent: #f59e0b;
|
||
--clr-text: #f1f5f9;
|
||
--clr-muted: #94a3b8;
|
||
--clr-border: #334155;
|
||
--clr-card: #1e293b;
|
||
--clr-success: #22c55e;
|
||
--clr-danger: #ef4444;
|
||
--clr-warning: #f97316;
|
||
--clr-info: #3b82f6;
|
||
--header-h: 52px;
|
||
--radius: 8px;
|
||
--font-main: 'Segoe UI', system-ui, sans-serif;
|
||
}
|
||
|
||
html, body {
|
||
height: 100%;
|
||
font-family: var(--font-main);
|
||
background: var(--clr-bg);
|
||
color: var(--clr-text);
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════════════════
|
||
HEADER
|
||
════════════════════════════════════════════════════════════════════ */
|
||
#app-header {
|
||
height: var(--header-h);
|
||
background: var(--clr-header);
|
||
border-bottom: 1px solid var(--clr-border);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 16px;
|
||
gap: 12px;
|
||
position: relative;
|
||
z-index: 1000;
|
||
box-shadow: 0 2px 8px rgba(0,0,0,.4);
|
||
}
|
||
|
||
#app-header .logo {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
#app-header .logo svg { color: var(--clr-accent); flex-shrink: 0; }
|
||
#app-header h1 {
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
letter-spacing: .02em;
|
||
color: var(--clr-text);
|
||
white-space: nowrap;
|
||
}
|
||
#app-header h1 span { color: var(--clr-accent); }
|
||
|
||
/* Keterangan mode */
|
||
.header-hints {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
flex-wrap: nowrap;
|
||
}
|
||
.hint-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: .72rem;
|
||
color: var(--clr-muted);
|
||
white-space: nowrap;
|
||
}
|
||
.hint-item .dot {
|
||
width: 8px; height: 8px;
|
||
border-radius: 50%;
|
||
background: var(--clr-accent);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* Legend warna marker */
|
||
.legend-wrap {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: nowrap;
|
||
}
|
||
.legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: .65rem;
|
||
color: var(--clr-muted);
|
||
}
|
||
.legend-dot {
|
||
width: 10px; height: 10px;
|
||
border-radius: 50%;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════════════════
|
||
PETA
|
||
════════════════════════════════════════════════════════════════════ */
|
||
#map {
|
||
width: 100%;
|
||
height: calc(100vh - var(--header-h));
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════════════════
|
||
POPUP LEAFLET CUSTOM
|
||
════════════════════════════════════════════════════════════════════ */
|
||
.leaflet-popup-content-wrapper {
|
||
background: var(--clr-card) !important;
|
||
color: var(--clr-text) !important;
|
||
border: 1px solid var(--clr-border) !important;
|
||
border-radius: var(--radius) !important;
|
||
box-shadow: 0 8px 32px rgba(0,0,0,.5) !important;
|
||
padding: 0 !important;
|
||
}
|
||
.leaflet-popup-tip { background: var(--clr-card) !important; }
|
||
.leaflet-popup-content { margin: 0 !important; }
|
||
|
||
/* Popup Form & Detail */
|
||
.popup-box {
|
||
min-width: 260px;
|
||
max-width: 320px;
|
||
padding: 16px;
|
||
}
|
||
.popup-box h3 {
|
||
font-size: .9rem;
|
||
font-weight: 700;
|
||
margin-bottom: 12px;
|
||
color: var(--clr-accent);
|
||
border-bottom: 1px solid var(--clr-border);
|
||
padding-bottom: 8px;
|
||
}
|
||
|
||
/* Form fields */
|
||
.form-group { margin-bottom: 10px; }
|
||
.form-group label {
|
||
display: block;
|
||
font-size: .72rem;
|
||
color: var(--clr-muted);
|
||
margin-bottom: 4px;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: .04em;
|
||
}
|
||
.form-group input,
|
||
.form-group select,
|
||
.form-group textarea {
|
||
width: 100%;
|
||
background: #0f172a;
|
||
border: 1px solid var(--clr-border);
|
||
border-radius: 5px;
|
||
padding: 7px 9px;
|
||
color: var(--clr-text);
|
||
font-size: .82rem;
|
||
outline: none;
|
||
transition: border-color .2s;
|
||
}
|
||
.form-group input:focus,
|
||
.form-group select:focus,
|
||
.form-group textarea:focus {
|
||
border-color: var(--clr-accent);
|
||
}
|
||
.form-group textarea { resize: vertical; min-height: 56px; }
|
||
.form-group select option { background: #0f172a; }
|
||
|
||
/* Detail rows */
|
||
.detail-row {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
padding: 5px 0;
|
||
border-bottom: 1px solid rgba(255,255,255,.05);
|
||
font-size: .8rem;
|
||
}
|
||
.detail-row:last-child { border-bottom: none; }
|
||
.detail-label {
|
||
color: var(--clr-muted);
|
||
font-size: .72rem;
|
||
font-weight: 600;
|
||
text-transform: uppercase;
|
||
letter-spacing: .04em;
|
||
white-space: nowrap;
|
||
}
|
||
.detail-value { color: var(--clr-text); text-align: right; word-break: break-all; }
|
||
|
||
/* Status badges */
|
||
.badge {
|
||
display: inline-block;
|
||
padding: 2px 7px;
|
||
border-radius: 20px;
|
||
font-size: .68rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
}
|
||
.badge-ramai { background: rgba(249,115,22,.2); color: #f97316; }
|
||
.badge-tenang { background: rgba(34,197,94,.2); color: #22c55e; }
|
||
.badge-buka { background: rgba(34,197,94,.2); color: #22c55e; }
|
||
.badge-tutup { background: rgba(239,68,68,.2); color: #ef4444; }
|
||
.badge-ya { background: rgba(251,191,36,.2); color: #fbbf24; }
|
||
.badge-tidak { background: rgba(148,163,184,.2); color: #94a3b8; }
|
||
|
||
/* Buttons */
|
||
.btn-row {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-top: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
.btn {
|
||
flex: 1;
|
||
padding: 7px 12px;
|
||
border: none;
|
||
border-radius: 5px;
|
||
font-size: .78rem;
|
||
font-weight: 700;
|
||
cursor: pointer;
|
||
transition: opacity .2s, transform .1s;
|
||
letter-spacing: .03em;
|
||
}
|
||
.btn:hover { opacity: .85; }
|
||
.btn:active { transform: scale(.97); }
|
||
.btn-save { background: var(--clr-accent); color: #0f172a; }
|
||
.btn-edit { background: var(--clr-info); color: #fff; }
|
||
.btn-delete { background: var(--clr-danger); color: #fff; }
|
||
.btn-cancel { background: var(--clr-border); color: var(--clr-text); }
|
||
.btn-back { background: var(--clr-border); color: var(--clr-text); }
|
||
|
||
/* Color swatch preview */
|
||
.color-preview-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.color-preview-row input[type="color"] {
|
||
width: 36px; height: 32px;
|
||
padding: 2px;
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
}
|
||
.color-hex-input { flex: 1; }
|
||
|
||
/* ════════════════════════════════════════════════════════════════════
|
||
TOAST NOTIFICATION
|
||
════════════════════════════════════════════════════════════════════ */
|
||
#toast-container {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
z-index: 99999;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
.toast {
|
||
min-width: 220px;
|
||
max-width: 320px;
|
||
padding: 10px 14px;
|
||
border-radius: var(--radius);
|
||
font-size: .82rem;
|
||
font-weight: 600;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
box-shadow: 0 4px 16px rgba(0,0,0,.4);
|
||
animation: toast-in .3s ease forwards;
|
||
border-left: 4px solid transparent;
|
||
}
|
||
.toast-success { background: #14532d; border-color: var(--clr-success); color: #bbf7d0; }
|
||
.toast-error { background: #450a0a; border-color: var(--clr-danger); color: #fecaca; }
|
||
.toast-info { background: #1e3a5f; border-color: var(--clr-info); color: #bfdbfe; }
|
||
@keyframes toast-in {
|
||
from { opacity: 0; transform: translateX(40px); }
|
||
to { opacity: 1; transform: translateX(0); }
|
||
}
|
||
@keyframes toast-out {
|
||
from { opacity: 1; transform: translateX(0); }
|
||
to { opacity: 0; transform: translateX(40px); }
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════════════════
|
||
LOADING SPINNER
|
||
════════════════════════════════════════════════════════════════════ */
|
||
#loading-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(0,0,0,.45);
|
||
z-index: 88888;
|
||
display: none;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
#loading-overlay.active { display: flex; }
|
||
.spinner {
|
||
width: 44px; height: 44px;
|
||
border: 4px solid rgba(255,255,255,.15);
|
||
border-top-color: var(--clr-accent);
|
||
border-radius: 50%;
|
||
animation: spin .7s linear infinite;
|
||
}
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
|
||
/* ════════════════════════════════════════════════════════════════════
|
||
LEAFLET DRAW TOOLBAR override (agar terlihat di dark mode)
|
||
════════════════════════════════════════════════════════════════════ */
|
||
.leaflet-draw-toolbar a {
|
||
background-color: var(--clr-card) !important;
|
||
border-color: var(--clr-border) !important;
|
||
color: var(--clr-text) !important;
|
||
}
|
||
.leaflet-draw-toolbar a:hover { background-color: var(--clr-border) !important; }
|
||
.leaflet-touch .leaflet-bar a { color: var(--clr-text); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ══ HEADER ════════════════════════════════════════════════════════════════ -->
|
||
<header id="app-header">
|
||
<div class="logo">
|
||
<!-- Fuel pump icon -->
|
||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||
<path d="M3 22V8a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v14"/><path d="M2 22h14"/><path d="M15 4h1a2 2 0 0 1 2 2v3a2 2 0 0 0 2 2h0a2 2 0 0 0 2-2V7.5L18 4"/><line x1="7" y1="14" x2="11" y2="14"/>
|
||
</svg>
|
||
<h1>WebGIS <span>SPBU</span></h1>
|
||
</div>
|
||
|
||
<div class="header-hints">
|
||
<div class="hint-item"><span class="dot"></span>Klik peta → Tambah SPBU</div>
|
||
<div class="hint-item"><span class="dot" style="background:#3b82f6"></span>Toolbar kiri → Gambar Polyline / Polygon</div>
|
||
</div>
|
||
|
||
<div class="legend-wrap">
|
||
<div class="legend-item"><span class="legend-dot" style="background:#22c55e"></span>Tenang+24j</div>
|
||
<div class="legend-item"><span class="legend-dot" style="background:#3b82f6"></span>Tenang</div>
|
||
<div class="legend-item"><span class="legend-dot" style="background:#f97316"></span>Ramai+24j</div>
|
||
<div class="legend-item"><span class="legend-dot" style="background:#ef4444"></span>Ramai</div>
|
||
<div class="legend-item"><span class="legend-dot" style="background:#1e293b;border:2px solid #64748b"></span>Tutup</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- ══ PETA ═══════════════════════════════════════════════════════════════════ -->
|
||
<div id="map"></div>
|
||
|
||
<!-- ══ LOADING OVERLAY ════════════════════════════════════════════════════════ -->
|
||
<div id="loading-overlay"><div class="spinner"></div></div>
|
||
|
||
<!-- ══ TOAST CONTAINER ════════════════════════════════════════════════════════ -->
|
||
<div id="toast-container"></div>
|
||
|
||
<!-- ══ Leaflet JS ──────────────────────────────────────────────────────────── -->
|
||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||
<!-- ══ Leaflet Draw JS ─────────────────────────────────────────────────────── -->
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
||
|
||
<script>
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
INISIALISASI PETA
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
if (typeof L === 'undefined') {
|
||
document.body.innerHTML = '<p style="color:red;padding:20px">Leaflet gagal dimuat. Periksa koneksi internet.</p>';
|
||
throw new Error('Leaflet not loaded');
|
||
}
|
||
|
||
const MAP_LAT = -0.05566422376778593;
|
||
const MAP_LNG = 109.34854622732368;
|
||
const MAP_ZOOM = 17;
|
||
|
||
const map = L.map('map', { zoomControl: true }).setView([MAP_LAT, MAP_LNG], MAP_ZOOM);
|
||
|
||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||
attribution: '© OpenStreetMap contributors',
|
||
maxZoom: 20
|
||
}).addTo(map);
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
LEAFLET DRAW — Toolbar & Drawn Items Layer
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
const drawnItems = new L.FeatureGroup().addTo(map);
|
||
|
||
const drawControl = new L.Control.Draw({
|
||
draw: {
|
||
marker: false, // marker dikelola manual via klik peta
|
||
circle: false,
|
||
rectangle: false,
|
||
circlemarker: false,
|
||
polyline: {
|
||
shapeOptions: { color: '#3b82f6', weight: 4 }
|
||
},
|
||
polygon: {
|
||
shapeOptions: { color: '#22c55e', weight: 2, fillOpacity: 0.25 }
|
||
}
|
||
},
|
||
edit: false // edit shape tidak diperlukan
|
||
});
|
||
map.addControl(drawControl);
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
STORAGE OBJEK (key = id database)
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
const markersMap = {}; // { id: L.marker }
|
||
const polylinesMap = {}; // { id: L.polyline }
|
||
const polygonsMap = {}; // { id: L.polygon }
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
HELPER: LOADING & TOAST
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
function showLoading() { document.getElementById('loading-overlay').classList.add('active'); }
|
||
function hideLoading() { document.getElementById('loading-overlay').classList.remove('active'); }
|
||
|
||
function showToast(msg, type = 'info') {
|
||
const c = document.getElementById('toast-container');
|
||
const el = document.createElement('div');
|
||
el.className = `toast toast-${type}`;
|
||
const icon = type === 'success' ? '✓' : type === 'error' ? '✕' : 'ℹ';
|
||
el.innerHTML = `<span>${icon}</span><span>${msg}</span>`;
|
||
c.appendChild(el);
|
||
setTimeout(() => {
|
||
el.style.animation = 'toast-out .3s ease forwards';
|
||
setTimeout(() => el.remove(), 300);
|
||
}, 3000);
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
HELPER: WARNA MARKER SPBU
|
||
Aturan warna sesuai spesifikasi
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
function getMarkerColor(spbu) {
|
||
const { status_operasional, status_ramai, buka_24_jam } = spbu;
|
||
if (status_operasional === 'tutup') return '#1e293b'; // hitam (dark)
|
||
if (status_ramai === 'ramai' && buka_24_jam === 'ya') return '#f97316'; // orange
|
||
if (status_ramai === 'tidak_ramai'&& buka_24_jam === 'ya') return '#22c55e'; // hijau
|
||
if (status_ramai === 'ramai' && buka_24_jam === 'tidak') return '#ef4444'; // merah
|
||
if (status_ramai === 'tidak_ramai'&& buka_24_jam === 'tidak') return '#3b82f6'; // biru
|
||
return '#94a3b8'; // fallback abu
|
||
}
|
||
|
||
/* Buat SVG inline marker */
|
||
function makeDivIcon(color) {
|
||
const border = color === '#1e293b' ? '#64748b' : color;
|
||
const svg = `
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="36" viewBox="0 0 28 36">
|
||
<path d="M14 0C6.268 0 0 6.268 0 14c0 9.333 14 22 14 22S28 23.333 28 14C28 6.268 21.732 0 14 0z"
|
||
fill="${color}" stroke="${border}" stroke-width="2"/>
|
||
<circle cx="14" cy="14" r="5" fill="white" opacity="0.85"/>
|
||
</svg>`;
|
||
return L.divIcon({
|
||
html: svg,
|
||
className: '',
|
||
iconSize: [28, 36],
|
||
iconAnchor: [14, 36],
|
||
popupAnchor:[0, -36]
|
||
});
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
RENDER MARKER SPBU
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
function renderMarker(spbu) {
|
||
const color = getMarkerColor(spbu);
|
||
const marker = L.marker([spbu.latitude, spbu.longitude], { icon: makeDivIcon(color) }).addTo(map);
|
||
|
||
// Bind popup detail saat klik marker
|
||
marker.on('click', function() {
|
||
const popup = showDetailSpbuPopup(spbu);
|
||
marker.bindPopup(popup, { maxWidth: 340 }).openPopup();
|
||
});
|
||
|
||
markersMap[spbu.id] = marker;
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
RENDER POLYLINE
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
function renderPolyline(data) {
|
||
// koordinat berupa array of [lat, lng]
|
||
const latlngs = data.koordinat.map(k => [k[0], k[1]]);
|
||
const line = L.polyline(latlngs, {
|
||
color: data.warna || '#3b82f6',
|
||
weight: 4,
|
||
opacity: 0.9
|
||
}).addTo(map);
|
||
|
||
line.on('click', function(e) {
|
||
L.DomEvent.stopPropagation(e);
|
||
showDetailPolylinePopup(data, e.latlng);
|
||
});
|
||
|
||
polylinesMap[data.id] = line;
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
RENDER POLYGON
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
function renderPolygon(data) {
|
||
const latlngs = data.koordinat.map(k => [k[0], k[1]]);
|
||
const poly = L.polygon(latlngs, {
|
||
color: data.warna || '#22c55e',
|
||
weight: 2,
|
||
fillColor: data.warna || '#22c55e',
|
||
fillOpacity: 0.25,
|
||
opacity: 0.9
|
||
}).addTo(map);
|
||
|
||
poly.on('click', function(e) {
|
||
L.DomEvent.stopPropagation(e);
|
||
showDetailPolygonPopup(data, e.latlng);
|
||
});
|
||
|
||
polygonsMap[data.id] = poly;
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
LOAD SEMUA DATA DARI DATABASE
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
async function loadData() {
|
||
showLoading();
|
||
try {
|
||
const res = await fetch('ambil_data.php');
|
||
const json = await res.json();
|
||
|
||
if (json.status === 'success' || json.status === 'partial_error') {
|
||
// Bersihkan layer lama
|
||
Object.values(markersMap).forEach(m => map.removeLayer(m));
|
||
Object.keys(markersMap).forEach(k => delete markersMap[k]);
|
||
Object.values(polylinesMap).forEach(m => map.removeLayer(m));
|
||
Object.keys(polylinesMap).forEach(k => delete polylinesMap[k]);
|
||
Object.values(polygonsMap).forEach(m => map.removeLayer(m));
|
||
Object.keys(polygonsMap).forEach(k => delete polygonsMap[k]);
|
||
|
||
// Render ulang
|
||
json.spbu.forEach(renderMarker);
|
||
json.polyline.forEach(renderPolyline);
|
||
json.polygon.forEach(renderPolygon);
|
||
} else {
|
||
showToast('Gagal memuat data dari server.', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Error koneksi: ' + err.message, 'error');
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
HAPUS DATA (SPBU / POLYLINE / POLYGON)
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
async function hapusData(tipe, id) {
|
||
if (!confirm(`Yakin ingin menghapus ${tipe} ini?`)) return;
|
||
showLoading();
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append('tipe', tipe);
|
||
fd.append('id', id);
|
||
const res = await fetch('hapus.php', { method: 'POST', body: fd });
|
||
const json = await res.json();
|
||
if (json.status === 'success') {
|
||
// Hapus dari peta
|
||
if (tipe === 'spbu' && markersMap[id]) {
|
||
map.removeLayer(markersMap[id]);
|
||
delete markersMap[id];
|
||
} else if (tipe === 'polyline' && polylinesMap[id]) {
|
||
map.removeLayer(polylinesMap[id]);
|
||
delete polylinesMap[id];
|
||
} else if (tipe === 'polygon' && polygonsMap[id]) {
|
||
map.removeLayer(polygonsMap[id]);
|
||
delete polygonsMap[id];
|
||
}
|
||
map.closePopup();
|
||
showToast(json.message, 'success');
|
||
} else {
|
||
showToast(json.message || 'Gagal menghapus.', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Error: ' + err.message, 'error');
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
POPUP: TAMBAH SPBU (klik peta)
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
function showTambahSpbuPopup(latlng) {
|
||
const content = `
|
||
<div class="popup-box">
|
||
<h3>➕ Tambah SPBU</h3>
|
||
<div class="form-group">
|
||
<label>Nama SPBU</label>
|
||
<input type="text" id="spbu-nama" placeholder="Contoh: SPBU 64.773.xx" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Status Keramaian</label>
|
||
<select id="spbu-ramai">
|
||
<option value="ramai">Ramai</option>
|
||
<option value="tidak_ramai">Tidak Ramai</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Buka 24 Jam?</label>
|
||
<select id="spbu-24jam">
|
||
<option value="ya">Ya</option>
|
||
<option value="tidak">Tidak</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Status Operasional</label>
|
||
<select id="spbu-ops">
|
||
<option value="buka">Buka</option>
|
||
<option value="tutup">Tutup</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Nomor HP</label>
|
||
<input type="text" id="spbu-hp" placeholder="Contoh: 0812xxxxxxxx" />
|
||
</div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-save" id="btn-simpan-spbu">💾 Simpan</button>
|
||
<button class="btn btn-cancel" onclick="map.closePopup()">Batal</button>
|
||
</div>
|
||
</div>`;
|
||
|
||
const popup = L.popup({ maxWidth: 340, closeButton: true })
|
||
.setLatLng(latlng)
|
||
.setContent(content)
|
||
.openOn(map);
|
||
|
||
// Setelah popup dibuka, pasang event listener tombol simpan
|
||
setTimeout(() => {
|
||
const btn = document.getElementById('btn-simpan-spbu');
|
||
if (!btn) return;
|
||
btn.addEventListener('click', async () => {
|
||
const nama = document.getElementById('spbu-nama').value.trim();
|
||
const ramai = document.getElementById('spbu-ramai').value;
|
||
const jam24 = document.getElementById('spbu-24jam').value;
|
||
const ops = document.getElementById('spbu-ops').value;
|
||
const hp = document.getElementById('spbu-hp').value.trim();
|
||
|
||
if (!nama) { showToast('Nama SPBU wajib diisi.', 'error'); return; }
|
||
|
||
showLoading();
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append('tipe', 'spbu');
|
||
fd.append('nama_spbu', nama);
|
||
fd.append('status_ramai', ramai);
|
||
fd.append('buka_24_jam', jam24);
|
||
fd.append('status_operasional', ops);
|
||
fd.append('nomor_hp', hp);
|
||
fd.append('latitude', latlng.lat);
|
||
fd.append('longitude', latlng.lng);
|
||
|
||
const res = await fetch('tambah.php', { method: 'POST', body: fd });
|
||
const json = await res.json();
|
||
|
||
if (json.status === 'success') {
|
||
map.closePopup();
|
||
renderMarker(json.data);
|
||
showToast('SPBU berhasil ditambahkan!', 'success');
|
||
} else {
|
||
showToast(json.message || 'Gagal menyimpan.', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Error: ' + err.message, 'error');
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
});
|
||
}, 50);
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
POPUP: DETAIL SPBU
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
function showDetailSpbuPopup(spbu) {
|
||
const badgeRamai = spbu.status_ramai === 'ramai'
|
||
? '<span class="badge badge-ramai">Ramai</span>'
|
||
: '<span class="badge badge-tenang">Tidak Ramai</span>';
|
||
const badgeOps = spbu.status_operasional === 'buka'
|
||
? '<span class="badge badge-buka">Buka</span>'
|
||
: '<span class="badge badge-tutup">Tutup</span>';
|
||
const badge24 = spbu.buka_24_jam === 'ya'
|
||
? '<span class="badge badge-ya">Ya</span>'
|
||
: '<span class="badge badge-tidak">Tidak</span>';
|
||
|
||
const content = `
|
||
<div class="popup-box">
|
||
<h3>⛽ ${escapeHtml(spbu.nama_spbu)}</h3>
|
||
<div class="detail-row"><span class="detail-label">Status Ramai</span><span class="detail-value">${badgeRamai}</span></div>
|
||
<div class="detail-row"><span class="detail-label">Buka 24 Jam</span><span class="detail-value">${badge24}</span></div>
|
||
<div class="detail-row"><span class="detail-label">Operasional</span><span class="detail-value">${badgeOps}</span></div>
|
||
<div class="detail-row"><span class="detail-label">Nomor HP</span><span class="detail-value">${escapeHtml(spbu.nomor_hp || '-')}</span></div>
|
||
<div class="detail-row"><span class="detail-label">Latitude</span><span class="detail-value">${parseFloat(spbu.latitude).toFixed(6)}</span></div>
|
||
<div class="detail-row"><span class="detail-label">Longitude</span><span class="detail-value">${parseFloat(spbu.longitude).toFixed(6)}</span></div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-edit" id="btn-edit-spbu-${spbu.id}">✏️ Edit</button>
|
||
<button class="btn btn-delete" id="btn-hapus-spbu-${spbu.id}">🗑️ Hapus</button>
|
||
</div>
|
||
</div>`;
|
||
|
||
setTimeout(() => {
|
||
const btnEdit = document.getElementById(`btn-edit-spbu-${spbu.id}`);
|
||
const btnHapus = document.getElementById(`btn-hapus-spbu-${spbu.id}`);
|
||
if (btnEdit) btnEdit.addEventListener('click', () => showEditSpbuPopup(spbu));
|
||
if (btnHapus) btnHapus.addEventListener('click', () => hapusData('spbu', spbu.id));
|
||
}, 50);
|
||
|
||
return content;
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
POPUP: EDIT SPBU
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
function showEditSpbuPopup(spbu) {
|
||
const content = `
|
||
<div class="popup-box">
|
||
<h3>✏️ Edit SPBU</h3>
|
||
<div class="form-group">
|
||
<label>Nama SPBU</label>
|
||
<input type="text" id="edit-spbu-nama" value="${escapeHtml(spbu.nama_spbu)}" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Status Keramaian</label>
|
||
<select id="edit-spbu-ramai">
|
||
<option value="ramai" ${spbu.status_ramai === 'ramai' ? 'selected' : ''}>Ramai</option>
|
||
<option value="tidak_ramai" ${spbu.status_ramai === 'tidak_ramai' ? 'selected' : ''}>Tidak Ramai</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Buka 24 Jam?</label>
|
||
<select id="edit-spbu-24jam">
|
||
<option value="ya" ${spbu.buka_24_jam === 'ya' ? 'selected' : ''}>Ya</option>
|
||
<option value="tidak" ${spbu.buka_24_jam === 'tidak' ? 'selected' : ''}>Tidak</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Status Operasional</label>
|
||
<select id="edit-spbu-ops">
|
||
<option value="buka" ${spbu.status_operasional === 'buka' ? 'selected' : ''}>Buka</option>
|
||
<option value="tutup" ${spbu.status_operasional === 'tutup' ? 'selected' : ''}>Tutup</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Nomor HP</label>
|
||
<input type="text" id="edit-spbu-hp" value="${escapeHtml(spbu.nomor_hp || '')}" />
|
||
</div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-save" id="btn-update-spbu-${spbu.id}">💾 Simpan</button>
|
||
<button class="btn btn-cancel" id="btn-edit-cancel-${spbu.id}">Batal</button>
|
||
</div>
|
||
</div>`;
|
||
|
||
// Ganti konten popup yang sudah terbuka
|
||
if (markersMap[spbu.id]) {
|
||
markersMap[spbu.id].setPopupContent(content).openPopup();
|
||
}
|
||
|
||
setTimeout(() => {
|
||
const btnUpdate = document.getElementById(`btn-update-spbu-${spbu.id}`);
|
||
const btnCancel = document.getElementById(`btn-edit-cancel-${spbu.id}`);
|
||
|
||
if (btnCancel) {
|
||
btnCancel.addEventListener('click', () => {
|
||
if (markersMap[spbu.id]) {
|
||
markersMap[spbu.id].setPopupContent(showDetailSpbuPopup(spbu)).openPopup();
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnUpdate) {
|
||
btnUpdate.addEventListener('click', async () => {
|
||
const nama = document.getElementById('edit-spbu-nama').value.trim();
|
||
const ramai = document.getElementById('edit-spbu-ramai').value;
|
||
const jam24 = document.getElementById('edit-spbu-24jam').value;
|
||
const ops = document.getElementById('edit-spbu-ops').value;
|
||
const hp = document.getElementById('edit-spbu-hp').value.trim();
|
||
|
||
if (!nama) { showToast('Nama SPBU wajib diisi.', 'error'); return; }
|
||
|
||
showLoading();
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append('tipe', 'spbu');
|
||
fd.append('id', spbu.id);
|
||
fd.append('nama_spbu', nama);
|
||
fd.append('status_ramai', ramai);
|
||
fd.append('buka_24_jam', jam24);
|
||
fd.append('status_operasional', ops);
|
||
fd.append('nomor_hp', hp);
|
||
|
||
const res = await fetch('edit.php', { method: 'POST', body: fd });
|
||
const json = await res.json();
|
||
|
||
if (json.status === 'success') {
|
||
const updatedSpbu = json.data;
|
||
// Update icon warna
|
||
const newColor = getMarkerColor(updatedSpbu);
|
||
markersMap[spbu.id].setIcon(makeDivIcon(newColor));
|
||
// Refresh popup detail dengan data baru
|
||
markersMap[spbu.id].setPopupContent(showDetailSpbuPopup(updatedSpbu)).openPopup();
|
||
showToast('SPBU berhasil diupdate!', 'success');
|
||
} else {
|
||
showToast(json.message || 'Gagal update.', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Error: ' + err.message, 'error');
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
});
|
||
}
|
||
}, 50);
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
POPUP: TAMBAH POLYLINE (setelah selesai menggambar)
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
function showTambahPolylinePopup(latlngs, drawnLayer) {
|
||
const koordinatJSON = JSON.stringify(latlngs.map(ll => [ll.lat, ll.lng]));
|
||
const center = drawnLayer.getBounds().getCenter();
|
||
|
||
const content = `
|
||
<div class="popup-box">
|
||
<h3>📏 Tambah Polyline</h3>
|
||
<div class="form-group">
|
||
<label>Nama Line</label>
|
||
<input type="text" id="line-nama" placeholder="Contoh: Jalur Utama" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Keterangan</label>
|
||
<textarea id="line-ket" placeholder="Deskripsi polyline..."></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Warna</label>
|
||
<div class="color-preview-row">
|
||
<input type="color" id="line-warna-picker" value="#3b82f6" />
|
||
<input type="text" id="line-warna-hex" class="color-hex-input" value="#3b82f6" placeholder="#3b82f6" />
|
||
</div>
|
||
</div>
|
||
<small style="color:var(--clr-muted);font-size:.7rem">${latlngs.length} titik koordinat</small>
|
||
<div class="btn-row">
|
||
<button class="btn btn-save" id="btn-simpan-line">💾 Simpan</button>
|
||
<button class="btn btn-cancel" id="btn-cancel-line">Batal</button>
|
||
</div>
|
||
</div>`;
|
||
|
||
const popup = L.popup({ maxWidth: 340 })
|
||
.setLatLng(center)
|
||
.setContent(content)
|
||
.openOn(map);
|
||
|
||
setTimeout(() => {
|
||
// Sinkron color picker dan hex input
|
||
const picker = document.getElementById('line-warna-picker');
|
||
const hexInp = document.getElementById('line-warna-hex');
|
||
if (picker && hexInp) {
|
||
picker.addEventListener('input', () => { hexInp.value = picker.value; });
|
||
hexInp.addEventListener('input', () => { if (/^#[0-9a-fA-F]{6}$/.test(hexInp.value)) picker.value = hexInp.value; });
|
||
}
|
||
|
||
const btnCancel = document.getElementById('btn-cancel-line');
|
||
if (btnCancel) {
|
||
btnCancel.addEventListener('click', () => {
|
||
map.removeLayer(drawnLayer);
|
||
map.closePopup();
|
||
});
|
||
}
|
||
|
||
const btnSimpan = document.getElementById('btn-simpan-line');
|
||
if (!btnSimpan) return;
|
||
btnSimpan.addEventListener('click', async () => {
|
||
const nama = document.getElementById('line-nama').value.trim();
|
||
const ket = document.getElementById('line-ket').value.trim();
|
||
const warna = document.getElementById('line-warna-hex').value.trim() || '#3b82f6';
|
||
|
||
if (!nama) { showToast('Nama line wajib diisi.', 'error'); return; }
|
||
|
||
showLoading();
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append('tipe', 'polyline');
|
||
fd.append('nama_line', nama);
|
||
fd.append('keterangan', ket);
|
||
fd.append('warna', warna);
|
||
fd.append('koordinat', koordinatJSON);
|
||
|
||
const res = await fetch('tambah.php', { method: 'POST', body: fd });
|
||
const json = await res.json();
|
||
|
||
if (json.status === 'success') {
|
||
map.removeLayer(drawnLayer); // hapus layer sementara dari draw
|
||
map.closePopup();
|
||
// Render polyline dari data yang baru disimpan
|
||
renderPolyline({
|
||
id: json.data.id,
|
||
nama_line: json.data.nama_line,
|
||
keterangan: json.data.keterangan,
|
||
warna: json.data.warna,
|
||
koordinat: JSON.parse(json.data.koordinat)
|
||
});
|
||
showToast('Polyline berhasil disimpan!', 'success');
|
||
} else {
|
||
showToast(json.message || 'Gagal menyimpan.', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Error: ' + err.message, 'error');
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
});
|
||
}, 50);
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
POPUP: DETAIL POLYLINE
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
function showDetailPolylinePopup(data, latlng) {
|
||
const jumlah = Array.isArray(data.koordinat) ? data.koordinat.length : '?';
|
||
const content = `
|
||
<div class="popup-box">
|
||
<h3>📏 ${escapeHtml(data.nama_line)}</h3>
|
||
<div class="detail-row"><span class="detail-label">Keterangan</span><span class="detail-value">${escapeHtml(data.keterangan || '-')}</span></div>
|
||
<div class="detail-row"><span class="detail-label">Warna</span><span class="detail-value"><span style="display:inline-block;width:14px;height:14px;border-radius:50%;background:${escapeHtml(data.warna)};vertical-align:middle;margin-right:4px"></span>${escapeHtml(data.warna)}</span></div>
|
||
<div class="detail-row"><span class="detail-label">Titik Koordinat</span><span class="detail-value">${jumlah} titik</span></div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-delete" id="btn-hapus-line-${data.id}">🗑️ Hapus</button>
|
||
</div>
|
||
</div>`;
|
||
|
||
L.popup({ maxWidth: 320 })
|
||
.setLatLng(latlng)
|
||
.setContent(content)
|
||
.openOn(map);
|
||
|
||
setTimeout(() => {
|
||
const btn = document.getElementById(`btn-hapus-line-${data.id}`);
|
||
if (btn) btn.addEventListener('click', () => hapusData('polyline', data.id));
|
||
}, 50);
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
POPUP: TAMBAH POLYGON (setelah selesai menggambar)
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
function showTambahPolygonPopup(latlngs, drawnLayer) {
|
||
const koordinatJSON = JSON.stringify(latlngs.map(ll => [ll.lat, ll.lng]));
|
||
const center = drawnLayer.getBounds().getCenter();
|
||
|
||
const content = `
|
||
<div class="popup-box">
|
||
<h3>🔷 Tambah Polygon</h3>
|
||
<div class="form-group">
|
||
<label>Nama Polygon</label>
|
||
<input type="text" id="pgon-nama" placeholder="Contoh: Area Layanan A" />
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Keterangan</label>
|
||
<textarea id="pgon-ket" placeholder="Deskripsi area..."></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Warna</label>
|
||
<div class="color-preview-row">
|
||
<input type="color" id="pgon-warna-picker" value="#22c55e" />
|
||
<input type="text" id="pgon-warna-hex" class="color-hex-input" value="#22c55e" placeholder="#22c55e" />
|
||
</div>
|
||
</div>
|
||
<small style="color:var(--clr-muted);font-size:.7rem">${latlngs.length} titik koordinat</small>
|
||
<div class="btn-row">
|
||
<button class="btn btn-save" id="btn-simpan-pgon">💾 Simpan</button>
|
||
<button class="btn btn-cancel" id="btn-cancel-pgon">Batal</button>
|
||
</div>
|
||
</div>`;
|
||
|
||
const popup = L.popup({ maxWidth: 340 })
|
||
.setLatLng(center)
|
||
.setContent(content)
|
||
.openOn(map);
|
||
|
||
setTimeout(() => {
|
||
const picker = document.getElementById('pgon-warna-picker');
|
||
const hexInp = document.getElementById('pgon-warna-hex');
|
||
if (picker && hexInp) {
|
||
picker.addEventListener('input', () => { hexInp.value = picker.value; });
|
||
hexInp.addEventListener('input', () => { if (/^#[0-9a-fA-F]{6}$/.test(hexInp.value)) picker.value = hexInp.value; });
|
||
}
|
||
|
||
const btnCancel = document.getElementById('btn-cancel-pgon');
|
||
if (btnCancel) {
|
||
btnCancel.addEventListener('click', () => {
|
||
map.removeLayer(drawnLayer);
|
||
map.closePopup();
|
||
});
|
||
}
|
||
|
||
const btnSimpan = document.getElementById('btn-simpan-pgon');
|
||
if (!btnSimpan) return;
|
||
btnSimpan.addEventListener('click', async () => {
|
||
const nama = document.getElementById('pgon-nama').value.trim();
|
||
const ket = document.getElementById('pgon-ket').value.trim();
|
||
const warna = document.getElementById('pgon-warna-hex').value.trim() || '#22c55e';
|
||
|
||
if (!nama) { showToast('Nama polygon wajib diisi.', 'error'); return; }
|
||
|
||
showLoading();
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append('tipe', 'polygon');
|
||
fd.append('nama_polygon', nama);
|
||
fd.append('keterangan', ket);
|
||
fd.append('warna', warna);
|
||
fd.append('koordinat', koordinatJSON);
|
||
|
||
const res = await fetch('tambah.php', { method: 'POST', body: fd });
|
||
const json = await res.json();
|
||
|
||
if (json.status === 'success') {
|
||
map.removeLayer(drawnLayer);
|
||
map.closePopup();
|
||
renderPolygon({
|
||
id: json.data.id,
|
||
nama_polygon: json.data.nama_polygon,
|
||
keterangan: json.data.keterangan,
|
||
warna: json.data.warna,
|
||
koordinat: JSON.parse(json.data.koordinat)
|
||
});
|
||
showToast('Polygon berhasil disimpan!', 'success');
|
||
} else {
|
||
showToast(json.message || 'Gagal menyimpan.', 'error');
|
||
}
|
||
} catch (err) {
|
||
showToast('Error: ' + err.message, 'error');
|
||
} finally {
|
||
hideLoading();
|
||
}
|
||
});
|
||
}, 50);
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
POPUP: DETAIL POLYGON
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
function showDetailPolygonPopup(data, latlng) {
|
||
const jumlah = Array.isArray(data.koordinat) ? data.koordinat.length : '?';
|
||
const content = `
|
||
<div class="popup-box">
|
||
<h3>🔷 ${escapeHtml(data.nama_polygon)}</h3>
|
||
<div class="detail-row"><span class="detail-label">Keterangan</span><span class="detail-value">${escapeHtml(data.keterangan || '-')}</span></div>
|
||
<div class="detail-row"><span class="detail-label">Warna</span><span class="detail-value"><span style="display:inline-block;width:14px;height:14px;border-radius:50%;background:${escapeHtml(data.warna)};vertical-align:middle;margin-right:4px"></span>${escapeHtml(data.warna)}</span></div>
|
||
<div class="detail-row"><span class="detail-label">Titik Koordinat</span><span class="detail-value">${jumlah} titik</span></div>
|
||
<div class="btn-row">
|
||
<button class="btn btn-delete" id="btn-hapus-pgon-${data.id}">🗑️ Hapus</button>
|
||
</div>
|
||
</div>`;
|
||
|
||
L.popup({ maxWidth: 320 })
|
||
.setLatLng(latlng)
|
||
.setContent(content)
|
||
.openOn(map);
|
||
|
||
setTimeout(() => {
|
||
const btn = document.getElementById(`btn-hapus-pgon-${data.id}`);
|
||
if (btn) btn.addEventListener('click', () => hapusData('polygon', data.id));
|
||
}, 50);
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
EVENT: Klik peta → tambah SPBU
|
||
(hanya jika bukan sedang dalam mode draw)
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
let isDrawing = false;
|
||
|
||
map.on('draw:drawstart', () => { isDrawing = true; });
|
||
map.on('draw:drawstop', () => { isDrawing = false; });
|
||
|
||
map.on('click', function(e) {
|
||
if (isDrawing) return; // abaikan jika sedang menggambar
|
||
showTambahSpbuPopup(e.latlng);
|
||
});
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
EVENT: Leaflet Draw — setelah shape selesai digambar
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
map.on(L.Draw.Event.CREATED, function(e) {
|
||
const layer = e.layer;
|
||
const type = e.layerType;
|
||
|
||
if (type === 'polyline') {
|
||
const latlngs = layer.getLatLngs();
|
||
// Jangan tambahkan ke drawnItems, biarkan handler yang kelola
|
||
showTambahPolylinePopup(latlngs, layer);
|
||
} else if (type === 'polygon') {
|
||
// Polygon getLatLngs() mengembalikan array of array, ambil ring pertama
|
||
const latlngs = layer.getLatLngs()[0];
|
||
showTambahPolygonPopup(latlngs, layer);
|
||
}
|
||
});
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
HELPER: escape HTML untuk mencegah XSS
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
function escapeHtml(str) {
|
||
if (str === null || str === undefined) return '';
|
||
return String(str)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
/* ═══════════════════════════════════════════════════════════════════════════
|
||
INIT — load semua data saat halaman pertama kali dibuka
|
||
═══════════════════════════════════════════════════════════════════════════ */
|
||
loadData();
|
||
</script>
|
||
</body>
|
||
</html>
|