1027 lines
43 KiB
PHP
1027 lines
43 KiB
PHP
<?php include 'koneksi.php'; ?>
|
|
<!DOCTYPE html>
|
|
<html lang="id">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title> WEBGIS— Peta Interaktif</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"/>
|
|
<!-- Google Fonts -->
|
|
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet"/>
|
|
|
|
<style>
|
|
/* ─── Reset & Base ─────────────────────────────── */
|
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
:root {
|
|
--c-bg: #0d1117;
|
|
--c-surface: #161b22;
|
|
--c-border: #30363d;
|
|
--c-accent: #58a6ff;
|
|
--c-green: #3fb950;
|
|
--c-red: #f85149;
|
|
--c-orange: #d29922;
|
|
--c-purple: #bc8cff;
|
|
--c-text: #e6edf3;
|
|
--c-muted: #8b949e;
|
|
--radius: 10px;
|
|
--shadow: 0 8px 32px rgba(0,0,0,.5);
|
|
}
|
|
|
|
body {
|
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
|
background: var(--c-bg);
|
|
color: var(--c-text);
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* ─── Layout ────────────────────────────────────── */
|
|
#app { display: flex; height: 100vh; }
|
|
|
|
/* ─── Sidebar ───────────────────────────────────── */
|
|
#sidebar {
|
|
width: 300px;
|
|
min-width: 300px;
|
|
background: var(--c-surface);
|
|
border-right: 1px solid var(--c-border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
z-index: 1000;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.sidebar-header {
|
|
padding: 20px 18px 14px;
|
|
border-bottom: 1px solid var(--c-border);
|
|
background: linear-gradient(135deg, #1a2332 0%, #161b22 100%);
|
|
}
|
|
.sidebar-header h1 {
|
|
font-family: 'Space Mono', monospace;
|
|
font-size: 17px;
|
|
font-weight: 700;
|
|
color: var(--c-accent);
|
|
letter-spacing: -0.5px;
|
|
}
|
|
.sidebar-header p {
|
|
font-size: 11px;
|
|
color: var(--c-muted);
|
|
margin-top: 3px;
|
|
}
|
|
|
|
/* ─── Stats Bar ─────────────────────────────────── */
|
|
.stats-bar {
|
|
display: flex;
|
|
padding: 10px 14px;
|
|
gap: 6px;
|
|
border-bottom: 1px solid var(--c-border);
|
|
background: #0d1117;
|
|
}
|
|
.stat-chip {
|
|
flex: 1;
|
|
text-align: center;
|
|
background: var(--c-surface);
|
|
border: 1px solid var(--c-border);
|
|
border-radius: 6px;
|
|
padding: 6px 4px;
|
|
}
|
|
.stat-chip .val {
|
|
font-family: 'Space Mono', monospace;
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
}
|
|
.stat-chip .lbl {
|
|
font-size: 9px;
|
|
color: var(--c-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
margin-top: 2px;
|
|
}
|
|
.stat-chip.m .val { color: var(--c-green); }
|
|
.stat-chip.l .val { color: var(--c-accent); }
|
|
.stat-chip.p .val { color: var(--c-purple); }
|
|
|
|
/* ─── Data List ─────────────────────────────────── */
|
|
.list-header {
|
|
padding: 10px 14px 6px;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 1px;
|
|
text-transform: uppercase;
|
|
color: var(--c-muted);
|
|
}
|
|
|
|
#data-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 0 8px 8px;
|
|
}
|
|
#data-list::-webkit-scrollbar { width: 4px; }
|
|
#data-list::-webkit-scrollbar-track { background: transparent; }
|
|
#data-list::-webkit-scrollbar-thumb { background: var(--c-border); border-radius: 4px; }
|
|
|
|
.data-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 9px 10px;
|
|
border-radius: 7px;
|
|
border: 1px solid transparent;
|
|
cursor: pointer;
|
|
transition: all .15s ease;
|
|
margin-bottom: 3px;
|
|
}
|
|
.data-item:hover {
|
|
background: rgba(88,166,255,.08);
|
|
border-color: rgba(88,166,255,.2);
|
|
}
|
|
.data-item.active {
|
|
background: rgba(88,166,255,.12);
|
|
border-color: rgba(88,166,255,.35);
|
|
}
|
|
|
|
.item-icon {
|
|
width: 30px; height: 30px;
|
|
border-radius: 7px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 14px;
|
|
flex-shrink: 0;
|
|
}
|
|
.icon-marker { background: rgba(63,185,80,.15); color: var(--c-green); }
|
|
.icon-polyline{ background: rgba(88,166,255,.15); color: var(--c-accent); }
|
|
.icon-polygon { background: rgba(188,140,255,.15); color: var(--c-purple); }
|
|
|
|
.item-info { flex: 1; min-width: 0; }
|
|
.item-name {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.item-meta {
|
|
font-size: 10px;
|
|
color: var(--c-muted);
|
|
margin-top: 1px;
|
|
}
|
|
|
|
.badge-24 {
|
|
font-size: 9px;
|
|
padding: 2px 5px;
|
|
border-radius: 4px;
|
|
font-weight: 700;
|
|
flex-shrink: 0;
|
|
}
|
|
.badge-ya { background: rgba(63,185,80,.2); color: var(--c-green); }
|
|
.badge-tidak{ background: rgba(248,81,73,.15); color: var(--c-red); }
|
|
|
|
/* ─── Map ───────────────────────────────────────── */
|
|
#map {
|
|
flex: 1;
|
|
height: 100%;
|
|
}
|
|
|
|
/* ─── Leaflet Popup Custom ──────────────────────── */
|
|
.leaflet-popup-content-wrapper {
|
|
background: var(--c-surface) !important;
|
|
border: 1px solid var(--c-border) !important;
|
|
border-radius: var(--radius) !important;
|
|
box-shadow: var(--shadow) !important;
|
|
padding: 0 !important;
|
|
color: var(--c-text) !important;
|
|
}
|
|
.leaflet-popup-tip {
|
|
background: var(--c-surface) !important;
|
|
}
|
|
.leaflet-popup-content {
|
|
margin: 0 !important;
|
|
width: auto !important;
|
|
}
|
|
.leaflet-popup-close-button {
|
|
color: var(--c-muted) !important;
|
|
font-size: 18px !important;
|
|
top: 8px !important;
|
|
right: 8px !important;
|
|
}
|
|
.leaflet-popup-close-button:hover { color: var(--c-text) !important; }
|
|
|
|
/* ─── Popup Panels ──────────────────────────────── */
|
|
.pop-panel { width: 240px; }
|
|
|
|
.pop-header {
|
|
padding: 14px 16px 10px;
|
|
border-bottom: 1px solid var(--c-border);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
.pop-header .ph-icon {
|
|
width: 32px; height: 32px;
|
|
border-radius: 8px;
|
|
display: flex; align-items: center; justify-content: center;
|
|
font-size: 16px;
|
|
}
|
|
.pop-header .ph-title {
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
color: var(--c-text);
|
|
}
|
|
.pop-header .ph-sub {
|
|
font-size: 10px;
|
|
color: var(--c-muted);
|
|
margin-top: 1px;
|
|
}
|
|
|
|
.pop-body { padding: 12px 16px; }
|
|
|
|
/* Menu pilihan aksi */
|
|
.menu-choices { display: flex; flex-direction: column; gap: 6px; }
|
|
.menu-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 9px 12px;
|
|
border: 1px solid var(--c-border);
|
|
border-radius: 7px;
|
|
background: transparent;
|
|
color: var(--c-text);
|
|
cursor: pointer;
|
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
transition: all .15s ease;
|
|
width: 100%;
|
|
text-align: left;
|
|
}
|
|
.menu-btn:hover { border-color: var(--c-accent); background: rgba(88,166,255,.08); }
|
|
.menu-btn.m:hover { border-color: var(--c-green); background: rgba(63,185,80,.08); }
|
|
.menu-btn.l:hover { border-color: var(--c-accent); background: rgba(88,166,255,.08); }
|
|
.menu-btn.p:hover { border-color: var(--c-purple); background: rgba(188,140,255,.08); }
|
|
.menu-btn .mb-icon { font-size: 16px; }
|
|
.menu-btn .mb-desc { font-size: 10px; color: var(--c-muted); margin-top: 1px; }
|
|
|
|
/* Form Fields */
|
|
.form-group { margin-bottom: 10px; }
|
|
.form-group label {
|
|
display: block;
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.5px;
|
|
text-transform: uppercase;
|
|
color: var(--c-muted);
|
|
margin-bottom: 4px;
|
|
}
|
|
.form-group input,
|
|
.form-group select {
|
|
width: 100%;
|
|
padding: 7px 10px;
|
|
background: var(--c-bg);
|
|
border: 1px solid var(--c-border);
|
|
border-radius: 6px;
|
|
color: var(--c-text);
|
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
|
font-size: 12px;
|
|
transition: border-color .15s;
|
|
outline: none;
|
|
}
|
|
.form-group input:focus,
|
|
.form-group select:focus { border-color: var(--c-accent); }
|
|
.form-group input::placeholder { color: var(--c-muted); }
|
|
.form-group select option { background: var(--c-surface); }
|
|
|
|
/* Buttons */
|
|
.btn-row { display: flex; gap: 6px; margin-top: 12px; }
|
|
.btn {
|
|
flex: 1;
|
|
padding: 8px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-family: 'Plus Jakarta Sans', sans-serif;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
cursor: pointer;
|
|
transition: opacity .15s, transform .1s;
|
|
}
|
|
.btn:hover { opacity: .88; transform: translateY(-1px); }
|
|
.btn:active { transform: translateY(0); }
|
|
.btn-primary { background: var(--c-accent); color: #0d1117; }
|
|
.btn-success { background: var(--c-green); color: #0d1117; }
|
|
.btn-danger { background: var(--c-red); color: #fff; }
|
|
.btn-muted { background: var(--c-border); color: var(--c-text); }
|
|
|
|
/* Info row di popup detail */
|
|
.info-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 6px 0;
|
|
border-bottom: 1px solid rgba(48,54,61,.7);
|
|
font-size: 12px;
|
|
}
|
|
.info-row:last-child { border-bottom: none; }
|
|
.info-row .ir-label { color: var(--c-muted); font-size: 10px; width: 52px; flex-shrink: 0; }
|
|
.info-row .ir-val { font-weight: 500; }
|
|
|
|
/* Toast Notification */
|
|
#toast {
|
|
position: fixed;
|
|
bottom: 24px;
|
|
left: 50%;
|
|
transform: translateX(-50%) translateY(20px);
|
|
background: var(--c-surface);
|
|
border: 1px solid var(--c-border);
|
|
border-radius: 10px;
|
|
padding: 10px 18px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
box-shadow: var(--shadow);
|
|
z-index: 9999;
|
|
opacity: 0;
|
|
transition: all .25s ease;
|
|
pointer-events: none;
|
|
white-space: nowrap;
|
|
}
|
|
#toast.show {
|
|
opacity: 1;
|
|
transform: translateX(-50%) translateY(0);
|
|
}
|
|
#toast.ok { border-color: var(--c-green); color: var(--c-green); }
|
|
#toast.err { border-color: var(--c-red); color: var(--c-red); }
|
|
#toast.info { border-color: var(--c-accent); color: var(--c-accent); }
|
|
|
|
/* Drawing hint bar */
|
|
#draw-hint {
|
|
position: fixed;
|
|
top: 12px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: var(--c-surface);
|
|
border: 1px solid var(--c-orange);
|
|
border-radius: 8px;
|
|
padding: 8px 18px;
|
|
font-size: 12px;
|
|
color: var(--c-orange);
|
|
font-weight: 600;
|
|
z-index: 9000;
|
|
display: none;
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
/* Leaflet Draw toolbar dark override */
|
|
.leaflet-draw-toolbar a { background-color: var(--c-surface) !important; color: var(--c-text) !important; }
|
|
|
|
/* Empty state */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 30px 20px;
|
|
color: var(--c-muted);
|
|
font-size: 12px;
|
|
}
|
|
.empty-state .es-icon { font-size: 28px; margin-bottom: 6px; }
|
|
|
|
/* Tooltip leaflet */
|
|
.leaflet-tooltip {
|
|
background: var(--c-surface) !important;
|
|
border: 1px solid var(--c-border) !important;
|
|
color: var(--c-text) !important;
|
|
font-family: 'Plus Jakarta Sans', sans-serif !important;
|
|
font-size: 11px !important;
|
|
font-weight: 600 !important;
|
|
padding: 3px 8px !important;
|
|
border-radius: 5px !important;
|
|
box-shadow: none !important;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="app">
|
|
<!-- ═══════════════════ SIDEBAR ═══════════════════ -->
|
|
<aside id="sidebar">
|
|
<div class="sidebar-header">
|
|
<center><h1>WEBGIS</h1></center>
|
|
<center><p>Klik pada peta untuk menambah objek</p></center>
|
|
</div>
|
|
|
|
<div class="stats-bar">
|
|
<div class="stat-chip m">
|
|
<div class="val" id="cnt-marker">0</div>
|
|
<div class="lbl">📍 Titik</div>
|
|
</div>
|
|
<div class="stat-chip l">
|
|
<div class="val" id="cnt-polyline">0</div>
|
|
<div class="lbl">📏 Garis</div>
|
|
</div>
|
|
<div class="stat-chip p">
|
|
<div class="val" id="cnt-polygon">0</div>
|
|
<div class="lbl">⬡ Area</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="list-header">Daftar Objek</div>
|
|
|
|
<div id="data-list">
|
|
<div class="empty-state" id="empty-state">
|
|
<div class="es-icon">🗺️</div>
|
|
<div>Belum ada data</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- ═══════════════════ MAP ═══════════════════════ -->
|
|
<div id="map"></div>
|
|
</div>
|
|
|
|
<!-- Toast Notifikasi -->
|
|
<div id="toast"></div>
|
|
|
|
<!-- Hint saat drawing -->
|
|
<div id="draw-hint" id="draw-hint">
|
|
✏️ <span id="hint-text">Klik pada peta untuk mulai menggambar. Dobel-klik untuk selesai.</span>
|
|
<button onclick="cancelDraw()" style="background:var(--c-red);color:#fff;border:none;border-radius:5px;padding:3px 8px;font-size:11px;cursor:pointer;">Batal</button>
|
|
</div>
|
|
|
|
<!-- ═══════════════ SCRIPTS ════════════════════════════ -->
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
|
|
|
<script>
|
|
// ─────────────────────────────────────────────────────
|
|
// SETUP MAP
|
|
// ─────────────────────────────────────────────────────
|
|
var map = L.map('map', { zoomControl: true }).setView([-0.0263, 109.3425], 13);
|
|
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap',
|
|
maxZoom: 19
|
|
}).addTo(map);
|
|
|
|
// Layer untuk semua objek yang sudah tersimpan
|
|
var drawnLayers = {}; // { id: layer }
|
|
var pendingLayer = null; // layer sementara saat drawing baru
|
|
var activeDrawTool = null; // referensi tool draw yang aktif
|
|
var pendingCoords = null; // koordinat hasil draw sebelum save
|
|
var pendingType = null; // jenis geometri yang sedang di-draw
|
|
|
|
// ─────────────────────────────────────────────────────
|
|
// ICON HELPER
|
|
// ─────────────────────────────────────────────────────
|
|
function makeIcon(color) {
|
|
var colors = { green: 'green', red: 'red', blue: 'blue', orange: 'orange', violet: 'violet' };
|
|
var c = colors[color] || 'blue';
|
|
return L.icon({
|
|
iconUrl: `https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-${c}.png`,
|
|
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
|
|
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
|
|
});
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────
|
|
// TOAST NOTIFICATION
|
|
// ─────────────────────────────────────────────────────
|
|
function toast(msg, type = 'ok', duration = 2500) {
|
|
var el = document.getElementById('toast');
|
|
el.textContent = msg;
|
|
el.className = 'show ' + type;
|
|
clearTimeout(el._t);
|
|
el._t = setTimeout(() => { el.className = ''; }, duration);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────
|
|
// DRAW HINT
|
|
// ─────────────────────────────────────────────────────
|
|
function showHint(msg) {
|
|
document.getElementById('hint-text').textContent = msg;
|
|
document.getElementById('draw-hint').style.display = 'block';
|
|
}
|
|
function hideHint() {
|
|
document.getElementById('draw-hint').style.display = 'none';
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────
|
|
// KLIK PETA → POPUP PILIHAN AKSI
|
|
// ─────────────────────────────────────────────────────
|
|
map.on('click', function(e) {
|
|
// Jangan tampilkan menu kalau sedang drawing
|
|
if (activeDrawTool) return;
|
|
|
|
var html = `
|
|
<div class="pop-panel">
|
|
<div class="pop-header">
|
|
<div class="ph-icon icon-marker">🗺️</div>
|
|
<div>
|
|
<div class="ph-title">Tambah Objek Baru</div>
|
|
<div class="ph-sub">${e.latlng.lat.toFixed(5)}, ${e.latlng.lng.toFixed(5)}</div>
|
|
</div>
|
|
</div>
|
|
<div class="pop-body">
|
|
<div class="menu-choices">
|
|
<button class="menu-btn m" onclick="startDraw('marker', ${e.latlng.lat}, ${e.latlng.lng})">
|
|
<span class="mb-icon">📍</span>
|
|
<div>
|
|
<div>Buat Titik</div>
|
|
<div class="mb-desc">Tandai lokasi tertentu di peta</div>
|
|
</div>
|
|
</button>
|
|
<button class="menu-btn l" onclick="startDraw('polyline')">
|
|
<span class="mb-icon">📏</span>
|
|
<div>
|
|
<div>Buat Garis</div>
|
|
<div class="mb-desc">Gambar jalur atau batas garis</div>
|
|
</div>
|
|
</button>
|
|
<button class="menu-btn p" onclick="startDraw('polygon')">
|
|
<span class="mb-icon">⬡</span>
|
|
<div>
|
|
<div>Buat Area (Polygon)</div>
|
|
<div class="mb-desc">Gambar area dengan bentuk bebas</div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
L.popup().setLatLng(e.latlng).setContent(html).openOn(map);
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────
|
|
// START DRAWING
|
|
// ─────────────────────────────────────────────────────
|
|
function startDraw(type, lat, lng) {
|
|
map.closePopup();
|
|
|
|
if (type === 'marker') {
|
|
// Untuk marker, langsung pakai koordinat dari klik
|
|
pendingType = 'marker';
|
|
pendingCoords = { lat: lat, lng: lng };
|
|
|
|
// Tampilkan marker sementara (preview)
|
|
if (pendingLayer) map.removeLayer(pendingLayer);
|
|
pendingLayer = L.marker([lat, lng], {
|
|
draggable: true,
|
|
icon: makeIcon('blue'),
|
|
opacity: 0.7
|
|
}).addTo(map);
|
|
pendingLayer.on('dragend', function(ev) {
|
|
pendingCoords = { lat: ev.target.getLatLng().lat, lng: ev.target.getLatLng().lng };
|
|
});
|
|
|
|
openFormPopup('marker', [lat, lng]);
|
|
|
|
} else if (type === 'polyline') {
|
|
pendingType = 'polyline';
|
|
activeDrawTool = new L.Draw.Polyline(map, {
|
|
shapeOptions: { color: '#58a6ff', weight: 4, opacity: 0.9 },
|
|
metric: true
|
|
});
|
|
activeDrawTool.enable();
|
|
showHint('Klik untuk menambah titik garis. Dobel-klik untuk selesai.');
|
|
|
|
} else if (type === 'polygon') {
|
|
pendingType = 'polygon';
|
|
activeDrawTool = new L.Draw.Polygon(map, {
|
|
shapeOptions: { color: '#bc8cff', fillOpacity: 0.25, weight: 3 },
|
|
showArea: true
|
|
});
|
|
activeDrawTool.enable();
|
|
showHint('Klik untuk menambah sudut area. Klik titik awal atau dobel-klik untuk menutup area.');
|
|
}
|
|
}
|
|
|
|
function cancelDraw() {
|
|
if (activeDrawTool) { activeDrawTool.disable(); activeDrawTool = null; }
|
|
if (pendingLayer) { map.removeLayer(pendingLayer); pendingLayer = null; }
|
|
pendingCoords = null; pendingType = null;
|
|
hideHint();
|
|
toast('Drawing dibatalkan', 'info');
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────
|
|
// EVENT: SELESAI MENGGAMBAR (polyline / polygon)
|
|
// ─────────────────────────────────────────────────────
|
|
map.on(L.Draw.Event.CREATED, function(e) {
|
|
activeDrawTool = null;
|
|
hideHint();
|
|
|
|
var layer = e.layer;
|
|
var type = e.layerType === 'polyline' ? 'polyline' : 'polygon';
|
|
|
|
// Simpan koordinat
|
|
var rawCoords = layer.getLatLngs();
|
|
pendingCoords = rawCoords;
|
|
pendingType = type;
|
|
|
|
// Tampilkan layer preview
|
|
if (pendingLayer) map.removeLayer(pendingLayer);
|
|
pendingLayer = layer;
|
|
map.addLayer(pendingLayer);
|
|
|
|
// Hitung titik tengah untuk popup form
|
|
var center = layer.getBounds ? layer.getBounds().getCenter() : rawCoords[0];
|
|
openFormPopup(type, center);
|
|
});
|
|
|
|
// ─────────────────────────────────────────────────────
|
|
// POPUP FORM INPUT (CREATE)
|
|
// ─────────────────────────────────────────────────────
|
|
function openFormPopup(type, latlng) {
|
|
var icons = { marker: '📍', polyline: '📏', polygon: '⬡' };
|
|
var labels = { marker: 'Titik', polyline: 'Garis', polygon: 'Area' };
|
|
var iconClass = { marker: 'icon-marker', polyline: 'icon-polyline', polygon: 'icon-polygon' };
|
|
|
|
var html = `
|
|
<div class="pop-panel">
|
|
<div class="pop-header">
|
|
<div class="ph-icon ${iconClass[type]}">${icons[type]}</div>
|
|
<div>
|
|
<div class="ph-title">Tambah ${labels[type]}</div>
|
|
<div class="ph-sub">Isi detail objek baru</div>
|
|
</div>
|
|
</div>
|
|
<div class="pop-body">
|
|
<div class="form-group">
|
|
<label>Nama Objek *</label>
|
|
<input type="text" id="f-nama" placeholder="Contoh: SPBU Pontianak" autocomplete="off"/>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Buka 24 Jam?</label>
|
|
<select id="f-jam">
|
|
<option value="Ya">Ya</option>
|
|
<option value="Tidak" selected>Tidak</option>
|
|
</select>
|
|
</div>
|
|
<div class="btn-row">
|
|
<button class="btn btn-muted" onclick="cancelDraw()">Batal</button>
|
|
<button class="btn btn-success" onclick="simpanData()">Simpan</button>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
L.popup({ closeButton: true, closeOnClick: false })
|
|
.setLatLng(latlng)
|
|
.setContent(html)
|
|
.openOn(map);
|
|
|
|
// Fokus ke input nama
|
|
setTimeout(() => { var el = document.getElementById('f-nama'); if(el) el.focus(); }, 100);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────
|
|
// SIMPAN DATA (CREATE via AJAX)
|
|
// ─────────────────────────────────────────────────────
|
|
function simpanData() {
|
|
var nama = (document.getElementById('f-nama')?.value || '').trim();
|
|
var wa = (document.getElementById('f-wa')?.value || '').trim();
|
|
var jam = document.getElementById('f-jam')?.value || 'Tidak';
|
|
|
|
if (!nama) { toast('⚠️ Nama wajib diisi!', 'err'); return; }
|
|
if (!pendingCoords || !pendingType) { toast('⚠️ Objek belum digambar!', 'err'); return; }
|
|
|
|
var fd = new FormData();
|
|
fd.append('aksi', 'simpan');
|
|
fd.append('nama', nama);
|
|
fd.append('wa', wa);
|
|
fd.append('jam', jam);
|
|
fd.append('jenis', pendingType);
|
|
fd.append('koordinat', JSON.stringify(pendingCoords));
|
|
|
|
fetch('proses.php', { method: 'POST', body: fd })
|
|
.then(r => r.json())
|
|
.then(res => {
|
|
if (res.status === 'ok') {
|
|
map.closePopup();
|
|
if (pendingLayer) map.removeLayer(pendingLayer);
|
|
pendingLayer = null; pendingCoords = null; pendingType = null;
|
|
toast('✅ ' + res.message, 'ok');
|
|
loadData(); // Reload data tanpa refresh halaman
|
|
} else {
|
|
toast('❌ ' + res.message, 'err');
|
|
}
|
|
})
|
|
.catch(() => toast('❌ Gagal terhubung ke server', 'err'));
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────
|
|
// LOAD DATA (READ)
|
|
// ─────────────────────────────────────────────────────
|
|
function loadData() {
|
|
// Hapus semua layer lama
|
|
Object.values(drawnLayers).forEach(l => map.removeLayer(l));
|
|
drawnLayers = {};
|
|
|
|
fetch('proses.php?aksi=baca')
|
|
.then(r => r.json())
|
|
.then(res => {
|
|
if (res.status !== 'ok') return;
|
|
var data = res.data;
|
|
|
|
// Update stats
|
|
document.getElementById('cnt-marker').textContent = data.filter(d => d.jenis_geometri === 'marker').length;
|
|
document.getElementById('cnt-polyline').textContent = data.filter(d => d.jenis_geometri === 'polyline').length;
|
|
document.getElementById('cnt-polygon').textContent = data.filter(d => d.jenis_geometri === 'polygon').length;
|
|
|
|
// Update sidebar list
|
|
var listEl = document.getElementById('data-list');
|
|
var empEl = document.getElementById('empty-state');
|
|
listEl.innerHTML = '';
|
|
|
|
if (data.length === 0) {
|
|
listEl.appendChild(empEl);
|
|
return;
|
|
}
|
|
|
|
data.forEach(row => {
|
|
addToMap(row);
|
|
addToSidebar(row, listEl);
|
|
});
|
|
})
|
|
.catch(() => toast('❌ Gagal memuat data', 'err'));
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────
|
|
// TAMBAH LAYER KE PETA
|
|
// ─────────────────────────────────────────────────────
|
|
function addToMap(row) {
|
|
var id = row.id;
|
|
var type = row.jenis_geometri;
|
|
var coords = JSON.parse(row.koordinat);
|
|
var color = (row.buka_24jam === 'Ya') ? 'green' : 'red';
|
|
var layer;
|
|
|
|
if (type === 'marker') {
|
|
layer = L.marker([coords.lat, coords.lng], {
|
|
draggable: true,
|
|
icon: makeIcon(color)
|
|
});
|
|
|
|
// Update koordinat saat di-drag
|
|
layer.on('dragend', function(e) {
|
|
var newCoord = e.target.getLatLng();
|
|
var fd = new FormData();
|
|
fd.append('aksi', 'update_koord');
|
|
fd.append('id', id);
|
|
fd.append('koordinat', JSON.stringify({ lat: newCoord.lat, lng: newCoord.lng }));
|
|
fetch('proses.php', { method: 'POST', body: fd })
|
|
.then(r => r.json())
|
|
.then(res => toast(res.status === 'ok' ? '📍 Posisi diperbarui' : '❌ ' + res.message, res.status === 'ok' ? 'ok' : 'err'));
|
|
});
|
|
|
|
} else if (type === 'polyline') {
|
|
layer = L.polyline(coords, { color: '#58a6ff', weight: 4, opacity: 0.9 });
|
|
} else if (type === 'polygon') {
|
|
layer = L.polygon(coords, { color: '#bc8cff', fillOpacity: 0.25, weight: 3 });
|
|
}
|
|
|
|
if (!layer) return;
|
|
|
|
layer.bindTooltip(row.nama_objek, { permanent: false, direction: 'top' });
|
|
layer.bindPopup(buildDetailPopup(row));
|
|
layer.addTo(map);
|
|
|
|
drawnLayers[id] = layer;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────
|
|
// BUILD POPUP DETAIL
|
|
// ─────────────────────────────────────────────────────
|
|
function buildDetailPopup(row) {
|
|
var icons = { marker: '📍', polyline: '📏', polygon: '⬡' };
|
|
var cls = { marker: 'icon-marker', polyline: 'icon-polyline', polygon: 'icon-polygon' };
|
|
var labels = { marker: 'Titik', polyline: 'Garis', polygon: 'Area' };
|
|
var badge = row.buka_24jam === 'Ya'
|
|
? '<span class="badge-24 badge-ya">24 Jam</span>'
|
|
: '<span class="badge-24 badge-tidak">Tutup</span>';
|
|
|
|
return `
|
|
<div class="pop-panel">
|
|
<div class="pop-header">
|
|
<div class="ph-icon ${cls[row.jenis_geometri]}">${icons[row.jenis_geometri]}</div>
|
|
<div>
|
|
<div class="ph-title">${escHtml(row.nama_objek)}</div>
|
|
<div class="ph-sub">${labels[row.jenis_geometri]} · ${badge}</div>
|
|
</div>
|
|
</div>
|
|
<div class="pop-body">
|
|
<div class="info-row">
|
|
<span class="ir-label">WA</span>
|
|
<span class="ir-val">${row.no_wa ? escHtml(row.no_wa) : '<em style="color:var(--c-muted)">—</em>'}</span>
|
|
</div>
|
|
<div class="info-row">
|
|
<span class="ir-label">24 Jam</span>
|
|
<span class="ir-val">${row.buka_24jam}</span>
|
|
</div>
|
|
<div class="btn-row" style="margin-top:10px;">
|
|
<button class="btn btn-primary" onclick="openEditForm(${row.id})">✏️ Edit</button>
|
|
<button class="btn btn-danger" onclick="hapusData(${row.id})">🗑️ Hapus</button>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────
|
|
// TAMBAH ITEM KE SIDEBAR
|
|
// ─────────────────────────────────────────────────────
|
|
function addToSidebar(row, listEl) {
|
|
var icons = { marker: '📍', polyline: '📏', polygon: '⬡' };
|
|
var cls = { marker: 'icon-marker', polyline: 'icon-polyline', polygon: 'icon-polygon' };
|
|
var labels = { marker: 'Titik', polyline: 'Garis', polygon: 'Area' };
|
|
|
|
var item = document.createElement('div');
|
|
item.className = 'data-item';
|
|
item.dataset.id = row.id;
|
|
item.innerHTML = `
|
|
<div class="item-icon ${cls[row.jenis_geometri]}">${icons[row.jenis_geometri]}</div>
|
|
<div class="item-info">
|
|
<div class="item-name">${escHtml(row.nama_objek)}</div>
|
|
<div class="item-meta">${labels[row.jenis_geometri]}${row.no_wa ? ' · ' + escHtml(row.no_wa) : ''}</div>
|
|
</div>
|
|
<span class="badge-24 ${row.buka_24jam === 'Ya' ? 'badge-ya' : 'badge-tidak'}">${row.buka_24jam === 'Ya' ? '24J' : '—'}</span>`;
|
|
|
|
item.addEventListener('click', function() {
|
|
// Highlight active
|
|
document.querySelectorAll('.data-item').forEach(el => el.classList.remove('active'));
|
|
item.classList.add('active');
|
|
|
|
var layer = drawnLayers[row.id];
|
|
if (!layer) return;
|
|
|
|
// Fly ke lokasi
|
|
if (layer.getBounds) {
|
|
map.flyToBounds(layer.getBounds(), { padding: [60, 60], duration: 0.6 });
|
|
} else if (layer.getLatLng) {
|
|
map.flyTo(layer.getLatLng(), 16, { duration: 0.6 });
|
|
}
|
|
|
|
// Buka popup
|
|
setTimeout(() => layer.openPopup(), 700);
|
|
});
|
|
|
|
listEl.appendChild(item);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────
|
|
// EDIT FORM (UPDATE)
|
|
// ─────────────────────────────────────────────────────
|
|
function openEditForm(id) {
|
|
fetch(`proses.php?aksi=detail&id=${id}`)
|
|
.then(r => r.json())
|
|
.then(res => {
|
|
if (res.status !== 'ok') { toast('❌ Data tidak ditemukan', 'err'); return; }
|
|
var row = res.data;
|
|
var layer = drawnLayers[id];
|
|
if (!layer) return;
|
|
|
|
var coords = JSON.parse(row.koordinat);
|
|
var center = layer.getBounds ? layer.getBounds().getCenter()
|
|
: (layer.getLatLng ? layer.getLatLng() : [coords.lat, coords.lng]);
|
|
|
|
var html = `
|
|
<div class="pop-panel">
|
|
<div class="pop-header">
|
|
<div class="ph-icon icon-marker">✏️</div>
|
|
<div>
|
|
<div class="ph-title">Edit Objek</div>
|
|
<div class="ph-sub">ID: ${row.id}</div>
|
|
</div>
|
|
</div>
|
|
<div class="pop-body">
|
|
<div class="form-group">
|
|
<label>Nama Objek *</label>
|
|
<input type="text" id="e-nama" value="${escHtml(row.nama_objek)}"/>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>No. WhatsApp</label>
|
|
<input type="text" id="e-wa" value="${escHtml(row.no_wa || '')}"/>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Buka 24 Jam?</label>
|
|
<select id="e-jam">
|
|
<option value="Ya" ${row.buka_24jam === 'Ya' ? 'selected' : ''}>Ya</option>
|
|
<option value="Tidak" ${row.buka_24jam === 'Tidak' ? 'selected' : ''}>Tidak</option>
|
|
</select>
|
|
</div>
|
|
<div class="btn-row">
|
|
<button class="btn btn-muted" onclick="map.closePopup()">Batal</button>
|
|
<button class="btn btn-primary" onclick="updateData(${id})">Simpan</button>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
L.popup({ closeButton: true, closeOnClick: false })
|
|
.setLatLng(center)
|
|
.setContent(html)
|
|
.openOn(map);
|
|
|
|
setTimeout(() => { var el = document.getElementById('e-nama'); if(el) el.focus(); }, 100);
|
|
})
|
|
.catch(() => toast('❌ Gagal memuat data', 'err'));
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────
|
|
// UPDATE DATA
|
|
// ─────────────────────────────────────────────────────
|
|
function updateData(id) {
|
|
var nama = (document.getElementById('e-nama')?.value || '').trim();
|
|
var wa = (document.getElementById('e-wa')?.value || '').trim();
|
|
var jam = document.getElementById('e-jam')?.value || 'Tidak';
|
|
|
|
if (!nama) { toast('⚠️ Nama wajib diisi!', 'err'); return; }
|
|
|
|
var fd = new FormData();
|
|
fd.append('aksi', 'update');
|
|
fd.append('id', id);
|
|
fd.append('nama', nama);
|
|
fd.append('wa', wa);
|
|
fd.append('jam', jam);
|
|
|
|
fetch('proses.php', { method: 'POST', body: fd })
|
|
.then(r => r.json())
|
|
.then(res => {
|
|
if (res.status === 'ok') {
|
|
map.closePopup();
|
|
toast('✅ ' + res.message, 'ok');
|
|
loadData();
|
|
} else {
|
|
toast('❌ ' + res.message, 'err');
|
|
}
|
|
})
|
|
.catch(() => toast('❌ Gagal terhubung ke server', 'err'));
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────
|
|
// HAPUS DATA (DELETE)
|
|
// ─────────────────────────────────────────────────────
|
|
function hapusData(id) {
|
|
// Konfirmasi custom tanpa menggunakan native confirm()
|
|
var layer = drawnLayers[id];
|
|
if (!layer) return;
|
|
|
|
var center = layer.getBounds ? layer.getBounds().getCenter()
|
|
: (layer.getLatLng ? layer.getLatLng() : map.getCenter());
|
|
|
|
var html = `
|
|
<div class="pop-panel">
|
|
<div class="pop-header">
|
|
<div class="ph-icon" style="background:rgba(248,81,73,.15);color:var(--c-red);">🗑️</div>
|
|
<div>
|
|
<div class="ph-title" style="color:var(--c-red)">Konfirmasi Hapus</div>
|
|
<div class="ph-sub">Tindakan ini tidak bisa dibatalkan</div>
|
|
</div>
|
|
</div>
|
|
<div class="pop-body">
|
|
<p style="font-size:12px;color:var(--c-muted);margin-bottom:12px;">Apakah kamu yakin ingin menghapus objek ini?</p>
|
|
<div class="btn-row">
|
|
<button class="btn btn-muted" onclick="map.closePopup()">Batal</button>
|
|
<button class="btn btn-danger" onclick="konfirmasiHapus(${id})">Ya, Hapus</button>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
L.popup({ closeButton: true, closeOnClick: false })
|
|
.setLatLng(center)
|
|
.setContent(html)
|
|
.openOn(map);
|
|
}
|
|
|
|
function konfirmasiHapus(id) {
|
|
var fd = new FormData();
|
|
fd.append('aksi', 'hapus');
|
|
fd.append('id', id);
|
|
|
|
fetch('proses.php', { method: 'POST', body: fd })
|
|
.then(r => r.json())
|
|
.then(res => {
|
|
if (res.status === 'ok') {
|
|
map.closePopup();
|
|
toast('🗑️ ' + res.message, 'ok');
|
|
loadData();
|
|
} else {
|
|
toast('❌ ' + res.message, 'err');
|
|
}
|
|
})
|
|
.catch(() => toast('❌ Gagal terhubung ke server', 'err'));
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────
|
|
// UTIL: Escape HTML
|
|
// ─────────────────────────────────────────────────────
|
|
function escHtml(str) {
|
|
if (!str) return '';
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────
|
|
// INIT
|
|
// ─────────────────────────────────────────────────────
|
|
loadData();
|
|
</script>
|
|
</body>
|
|
</html>
|