Files
UAS_WEBGIS_CindyDyraIslamay…/01SPBU/index.html
T

1800 lines
71 KiB
HTML

<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS - Manajemen Data Point SPBU dengan Layer Control</title>
<!-- Leaflet CSS & JS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Leaflet Draw & Edit -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<!-- Leaflet Extra Markers -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet-extra-markers/1.2.0/css/leaflet.extra-markers.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet-extra-markers/1.2.0/js/leaflet.extra-markers.min.js"></script>
<!-- Turf.js untuk kalkulasi -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/Turf.js/6.5.0/turf.min.js"></script>
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
/* ===== CSS VARIABLES ===== */
:root {
--bg: #f0f2f5;
--surface: #ffffff;
--surface-2: #f7f8fa;
--border: #e4e7ec;
--border-subtle: #f0f2f5;
--text-primary: #111827;
--text-secondary: #6b7280;
--text-muted: #9ca3af;
--accent: #2563eb;
--accent-light: #eff6ff;
--accent-hover: #1d4ed8;
--spbu: #059669;
--spbu-light: #ecfdf5;
--spbu-alt: #dc2626;
--jalan: #d97706;
--jalan-light: #fffbeb;
--parsil: #7c3aed;
--parsil-light: #f5f3ff;
--danger: #dc2626;
--danger-light: #fef2f2;
--success: #059669;
--warning: #d97706;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04);
--shadow-md: 0 4px 12px rgba(0,0,0,0.08), 0 2px 6px rgba(0,0,0,0.04);
--shadow-lg: 0 10px 30px rgba(0,0,0,0.1), 0 4px 12px rgba(0,0,0,0.06);
--radius: 10px;
--radius-sm: 6px;
--radius-lg: 14px;
}
/* ===== RESET ===== */
*, *::before, *::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg);
color: var(--text-primary);
font-size: 13px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/* ===== MAP ===== */
#map {
height: 100vh;
width: 100%;
}
/* ===== SIDEBAR ===== */
.sidebar {
position: absolute;
top: 16px;
left: 16px;
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
z-index: 1000;
width: 320px;
max-height: calc(100vh - 32px);
overflow: hidden;
display: flex;
flex-direction: column;
border: 1px solid var(--border);
}
/* Sidebar Header */
.sidebar-header {
padding: 16px 18px 14px;
border-bottom: 1px solid var(--border-subtle);
flex-shrink: 0;
}
.sidebar-title {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.sidebar-title .logo-icon {
width: 30px;
height: 30px;
background: var(--accent);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
}
.sidebar-title h3 {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
line-height: 1.2;
}
.sidebar-title span {
font-size: 11px;
color: var(--text-muted);
font-weight: 400;
}
/* ===== MODE SELECTOR ===== */
.mode-selector {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
background: var(--surface-2);
border-radius: var(--radius-sm);
padding: 3px;
}
.mode-btn {
padding: 7px 4px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 10.5px;
font-weight: 500;
font-family: 'DM Sans', sans-serif;
transition: all 0.18s ease;
background: transparent;
color: var(--text-secondary);
display: flex;
flex-direction: column;
align-items: center;
gap: 3px;
line-height: 1;
}
.mode-btn i {
font-size: 13px;
}
.mode-btn:hover {
background: var(--surface);
color: var(--text-primary);
}
.mode-btn.active {
background: var(--surface);
color: var(--text-primary);
box-shadow: var(--shadow-sm);
font-weight: 600;
}
.mode-btn.active.navigate { color: var(--accent); }
.mode-btn.active.point { color: var(--spbu); }
.mode-btn.active.jalan { color: var(--jalan); }
.mode-btn.active.parsil { color: var(--parsil); }
/* ===== SIDEBAR BODY ===== */
.sidebar-body {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 12px 14px;
gap: 10px;
}
/* ===== SEARCH BOX ===== */
.search-wrapper {
position: relative;
}
.search-wrapper i {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
font-size: 12px;
}
.search-box {
width: 100%;
padding: 8px 10px 8px 30px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-size: 12.5px;
font-family: 'DM Sans', sans-serif;
background: var(--surface-2);
color: var(--text-primary);
transition: all 0.15s ease;
outline: none;
}
.search-box::placeholder { color: var(--text-muted); }
.search-box:focus {
border-color: var(--accent);
background: var(--surface);
box-shadow: 0 0 0 3px rgba(37,99,235,0.08);
}
/* ===== FILTER TABS ===== */
.filter-tabs {
display: flex;
gap: 4px;
}
.filter-btn {
flex: 1;
padding: 5px 6px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 11px;
font-weight: 500;
font-family: 'DM Sans', sans-serif;
transition: all 0.15s ease;
background: transparent;
color: var(--text-secondary);
white-space: nowrap;
}
.filter-btn:hover {
border-color: var(--text-muted);
color: var(--text-primary);
}
.filter-btn.active {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.filter-btn[data-filter="spbu"].active { background: var(--spbu); border-color: var(--spbu); }
.filter-btn[data-filter="jalan"].active { background: var(--jalan); border-color: var(--jalan); }
.filter-btn[data-filter="parsil"].active { background: var(--parsil); border-color: var(--parsil); }
/* ===== DATA HEADER ===== */
.data-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.data-header h4 {
color: var(--text-secondary);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.refresh-btn {
background: transparent;
color: var(--accent);
border: 1px solid var(--border);
padding: 4px 10px;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 11px;
font-weight: 500;
font-family: 'DM Sans', sans-serif;
transition: all 0.15s;
display: flex;
align-items: center;
gap: 4px;
}
.refresh-btn:hover {
background: var(--accent-light);
border-color: var(--accent);
}
/* ===== DATA LIST ===== */
.data-list {
flex: 1;
overflow-y: auto;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface-2);
min-height: 180px;
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
.data-list::-webkit-scrollbar { width: 4px; }
.data-list::-webkit-scrollbar-track { background: transparent; }
.data-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
.data-item {
padding: 10px 12px;
border-bottom: 1px solid var(--border-subtle);
background: var(--surface);
transition: background 0.12s;
cursor: default;
}
.data-item:first-child { border-radius: var(--radius) var(--radius) 0 0; }
.data-item:last-child { border-bottom: none; border-radius: 0 0 var(--radius) var(--radius); }
.data-item:only-child { border-radius: var(--radius); }
.data-item:hover { background: #fafbfc; }
.data-item-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.data-item-title {
font-weight: 600;
font-size: 12.5px;
color: var(--text-primary);
cursor: pointer;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.12s;
}
.data-item-title:hover { color: var(--accent); }
.data-item-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.action-btn {
width: 26px;
height: 26px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 11px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
background: var(--surface);
}
.action-btn.edit {
color: var(--jalan);
}
.action-btn.edit:hover {
background: var(--jalan-light);
border-color: var(--jalan);
}
.action-btn.delete {
color: var(--danger);
}
.action-btn.delete:hover {
background: var(--danger-light);
border-color: var(--danger);
}
.data-item-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
}
.data-item-badge {
display: inline-flex;
align-items: center;
padding: 2px 7px;
border-radius: 20px;
font-size: 9.5px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.badge-spbu { background: var(--spbu-light); color: var(--spbu); }
.badge-jalan { background: var(--jalan-light); color: var(--jalan); }
.badge-parsil { background: var(--parsil-light); color: var(--parsil); }
.data-item-info {
font-size: 11px;
color: var(--text-secondary);
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.data-item-id {
font-size: 10px;
color: var(--text-muted);
font-family: 'DM Mono', monospace;
flex-shrink: 0;
}
/* ===== DRAWING CONTROLS ===== */
.drawing-controls {
padding: 10px 12px;
background: var(--jalan-light);
border-radius: var(--radius-sm);
border: 1px solid rgba(217,119,6,0.2);
}
.drawing-hint {
font-size: 11.5px;
color: var(--jalan);
margin-bottom: 8px;
line-height: 1.5;
}
.drawing-hint strong { font-weight: 600; }
.drawing-actions {
display: flex;
gap: 6px;
}
.btn {
flex: 1;
padding: 7px 10px;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 12px;
font-weight: 500;
font-family: 'DM Sans', sans-serif;
transition: all 0.15s;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
}
.btn:hover { filter: brightness(0.95); }
.btn-warning {
background: var(--jalan-light);
color: var(--jalan);
border: 1px solid rgba(217,119,6,0.3);
}
.btn-warning:hover { background: rgba(217,119,6,0.15); }
.btn-danger {
background: var(--danger-light);
color: var(--danger);
border: 1px solid rgba(220,38,38,0.2);
}
.btn-danger:hover { background: rgba(220,38,38,0.15); }
/* ===== SIDEBAR FOOTER ===== */
.sidebar-footer {
padding: 8px 14px;
border-top: 1px solid var(--border-subtle);
flex-shrink: 0;
}
.info-footer {
font-size: 10.5px;
color: var(--text-muted);
text-align: center;
}
/* ===== LEGEND ===== */
.legend {
position: absolute;
bottom: 20px;
right: 20px;
background: var(--surface);
padding: 14px 16px;
border-radius: var(--radius);
box-shadow: var(--shadow-md);
z-index: 1000;
font-size: 11.5px;
min-width: 200px;
border: 1px solid var(--border);
}
.legend-title {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text-secondary);
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border-subtle);
}
.legend-section {
margin-bottom: 8px;
}
.legend-section-label {
font-size: 10px;
font-weight: 600;
color: var(--text-muted);
letter-spacing: 0.05em;
text-transform: uppercase;
margin-bottom: 5px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
color: var(--text-secondary);
}
.legend-item:last-child { margin-bottom: 0; }
.legend-dot {
width: 10px;
height: 10px;
border-radius: 50%;
flex-shrink: 0;
}
.legend-line {
width: 18px;
height: 3px;
border-radius: 2px;
flex-shrink: 0;
}
.legend-poly {
width: 14px;
height: 10px;
border-radius: 2px;
flex-shrink: 0;
border-width: 2px;
border-style: solid;
}
/* ===== INFO PANEL ===== */
.info-panel {
position: absolute;
bottom: 20px;
left: 352px;
background: var(--surface);
padding: 8px 14px;
border-radius: var(--radius-sm);
box-shadow: var(--shadow-sm);
z-index: 1000;
font-size: 12px;
color: var(--text-secondary);
border: 1px solid var(--border);
}
.info-panel strong { color: var(--text-primary); font-weight: 600; }
/* ===== MEASUREMENT DISPLAY ===== */
.measurement-display {
position: absolute;
top: 16px;
right: 16px;
background: var(--surface);
padding: 8px 14px;
border-radius: var(--radius-sm);
box-shadow: var(--shadow-sm);
z-index: 1000;
font-size: 13px;
font-weight: 600;
font-family: 'DM Mono', monospace;
color: var(--text-primary);
border: 1px solid var(--border);
}
/* ===== EDIT INDICATOR ===== */
.edit-indicator {
position: absolute;
top: 16px;
right: 200px;
background: var(--spbu);
color: white;
padding: 6px 12px;
border-radius: var(--radius-sm);
font-size: 12px;
font-weight: 500;
z-index: 1000;
display: none;
}
/* ===== LOADING ===== */
.loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--surface);
padding: 24px 32px;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
z-index: 2000;
display: none;
text-align: center;
border: 1px solid var(--border);
}
.loading i {
color: var(--accent);
font-size: 22px;
}
.loading p {
margin-top: 10px;
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
}
/* ===== MODAL ===== */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(17, 24, 39, 0.4);
backdrop-filter: blur(3px);
z-index: 3000;
display: none;
align-items: center;
justify-content: center;
}
.modal-content {
background: var(--surface);
padding: 28px;
border-radius: var(--radius-lg);
max-width: 400px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: var(--shadow-lg);
border: 1px solid var(--border);
}
.modal-content h3 {
margin-bottom: 20px;
color: var(--text-primary);
font-size: 16px;
font-weight: 700;
padding-bottom: 14px;
border-bottom: 1px solid var(--border);
}
.form-group {
margin-bottom: 14px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
font-size: 12px;
color: var(--text-secondary);
letter-spacing: 0.03em;
}
.form-group input,
.form-group select {
width: 100%;
padding: 9px 12px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-size: 13px;
font-family: 'DM Sans', sans-serif;
color: var(--text-primary);
background: var(--surface-2);
outline: none;
transition: all 0.15s;
}
.form-group input:focus,
.form-group select:focus {
border-color: var(--accent);
background: var(--surface);
box-shadow: 0 0 0 3px rgba(37,99,235,0.08);
}
.modal-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
}
.modal-buttons button {
flex: 1;
padding: 10px;
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
font-size: 13px;
font-weight: 600;
font-family: 'DM Sans', sans-serif;
transition: all 0.15s;
}
.modal-buttons button[type="submit"] {
background: var(--accent);
color: white;
}
.modal-buttons button[type="submit"]:hover { background: var(--accent-hover); }
.modal-buttons button[type="button"] {
background: var(--surface-2);
color: var(--text-secondary);
border: 1px solid var(--border);
}
.modal-buttons button[type="button"]:hover {
background: var(--danger-light);
color: var(--danger);
border-color: var(--danger);
}
/* ===== POPUP FORM ===== */
.popup-form {
min-width: 240px;
font-family: 'DM Sans', sans-serif;
}
.popup-form h4 {
font-size: 14px;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 12px;
}
.popup-form input,
.popup-form select {
width: 100%;
padding: 8px 10px;
margin: 4px 0 10px;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-size: 12.5px;
font-family: 'DM Sans', sans-serif;
color: var(--text-primary);
background: var(--surface-2);
outline: none;
transition: border-color 0.15s;
}
.popup-form input:focus,
.popup-form select:focus {
border-color: var(--accent);
}
.coord-info {
font-size: 10px;
color: var(--text-muted);
margin-top: 4px;
font-family: 'DM Mono', monospace;
line-height: 1.6;
}
/* ===== EMPTY STATE ===== */
.empty-state {
padding: 36px 20px;
text-align: center;
color: var(--text-muted);
}
.empty-state i {
font-size: 26px;
margin-bottom: 10px;
display: block;
opacity: 0.4;
}
.empty-state p {
font-size: 12.5px;
}
/* ===== NOTIFICATION ===== */
.notification {
position: fixed;
top: 20px;
right: 20px;
background: var(--success);
color: white;
padding: 11px 18px;
border-radius: var(--radius-sm);
box-shadow: var(--shadow-md);
z-index: 4000;
display: none;
font-size: 13px;
font-weight: 500;
animation: slideIn 0.25s cubic-bezier(0.34, 1.2, 0.64, 1);
max-width: 300px;
}
.notification.error { background: var(--danger); }
.notification.warning { background: var(--warning); }
@keyframes slideIn {
from { transform: translateX(110%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
/* ===== LEAFLET OVERRIDES ===== */
.leaflet-control-layers {
border-radius: var(--radius) !important;
box-shadow: var(--shadow-md) !important;
border: 1px solid var(--border) !important;
font-family: 'DM Sans', sans-serif !important;
font-size: 12.5px !important;
}
.leaflet-control-layers-base label,
.leaflet-control-layers-overlays label {
font-weight: 500;
cursor: pointer;
color: var(--text-primary);
}
.leaflet-control-zoom {
border-radius: var(--radius-sm) !important;
border: 1px solid var(--border) !important;
box-shadow: var(--shadow-sm) !important;
}
.leaflet-control-zoom a {
color: var(--text-secondary) !important;
font-size: 16px !important;
border-radius: inherit !important;
transition: all 0.15s !important;
}
.leaflet-control-zoom a:hover {
background: var(--accent-light) !important;
color: var(--accent) !important;
}
.leaflet-popup-content-wrapper {
border-radius: var(--radius) !important;
box-shadow: var(--shadow-lg) !important;
border: 1px solid var(--border);
font-family: 'DM Sans', sans-serif !important;
}
.leaflet-popup-tip-container { display: none; }
/* ===== POINT LABEL ===== */
.point-label div {
background: var(--surface) !important;
border: 1.5px solid var(--text-muted) !important;
font-family: 'DM Mono', monospace !important;
}
</style>
</head>
<body>
<div id="map"></div>
<!-- Notification -->
<div id="notification" class="notification"></div>
<!-- Modal Edit -->
<div id="editModal" class="modal-overlay">
<div class="modal-content">
<h3 id="modalTitle">Edit Data</h3>
<form id="editForm">
<div id="formFields"></div>
<div class="modal-buttons">
<button type="submit">Simpan</button>
<button type="button" onclick="closeEditModal()">Batal</button>
</div>
</form>
</div>
</div>
<!-- Sidebar -->
<div class="sidebar">
<!-- Header -->
<div class="sidebar-header">
<div class="sidebar-title">
<div class="logo-icon">🗺️</div>
<div>
<h3>WebGIS Pontianak</h3>
<span>Manajemen Data Spasial</span>
</div>
</div>
<div class="mode-selector">
<button class="mode-btn navigate active" data-mode="navigate">
<i class="fas fa-mouse-pointer"></i>
Navigasi
</button>
<button class="mode-btn point" data-mode="point">
<i class="fas fa-map-pin"></i>
SPBU
</button>
<button class="mode-btn jalan" data-mode="jalan">
<i class="fas fa-road"></i>
Jalan
</button>
<button class="mode-btn parsil" data-mode="parsil">
<i class="fas fa-vector-square"></i>
Parsil
</button>
</div>
</div>
<!-- Body -->
<div class="sidebar-body">
<!-- Search -->
<div class="search-wrapper">
<i class="fas fa-search"></i>
<input type="text" class="search-box" id="searchBox" placeholder="Cari data...">
</div>
<!-- Filter Tabs -->
<div class="filter-tabs">
<button class="filter-btn active" data-filter="all">Semua</button>
<button class="filter-btn" data-filter="spbu">SPBU</button>
<button class="filter-btn" data-filter="jalan">Jalan</button>
<button class="filter-btn" data-filter="parsil">Parsil</button>
</div>
<!-- Data List Header -->
<div class="data-header">
<h4>Data Tersimpan</h4>
<button class="refresh-btn" onclick="refreshAllData()">
<i class="fas fa-sync-alt"></i> Refresh
</button>
</div>
<!-- Data List -->
<div class="data-list" id="dataList">
<div class="empty-state">
<i class="fas fa-circle-notch fa-spin"></i>
<p>Memuat data...</p>
</div>
</div>
<!-- Drawing Controls -->
<div id="drawingControls" class="drawing-controls" style="display: none;">
<div class="drawing-hint" id="drawingHint"></div>
<div class="drawing-actions">
<button class="btn btn-warning" id="undoBtn">
<i class="fas fa-undo"></i> Undo
</button>
<button class="btn btn-danger" id="cancelBtn">
<i class="fas fa-times"></i> Batal
</button>
</div>
</div>
</div>
<!-- Footer -->
<div class="sidebar-footer">
<div class="info-footer">Klik nama untuk zoom &nbsp;·&nbsp; Drag marker untuk pindah posisi</div>
</div>
</div>
<!-- Legend -->
<div class="legend">
<div class="legend-title">Legenda</div>
<div class="legend-section">
<div class="legend-section-label">SPBU</div>
<div class="legend-item">
<div class="legend-dot" style="background:#059669;"></div>
<span>24 Jam</span>
</div>
<div class="legend-item">
<div class="legend-dot" style="background:#dc2626;"></div>
<span>Tidak 24 Jam</span>
</div>
</div>
<div class="legend-section">
<div class="legend-section-label">Jalan</div>
<div class="legend-item">
<div class="legend-line" style="background:#FF0000;"></div>
<span>Nasional</span>
</div>
<div class="legend-item">
<div class="legend-line" style="background:#0000FF;"></div>
<span>Provinsi</span>
</div>
<div class="legend-item">
<div class="legend-line" style="background:#008000;"></div>
<span>Kabupaten</span>
</div>
</div>
<div class="legend-section">
<div class="legend-section-label">Status Sertifikat</div>
<div class="legend-item">
<div class="legend-poly" style="background:rgba(255,0,0,0.2); border-color:#FF0000;"></div>
<span>Sertifikat Hak Milik (SHM)</span>
</div>
<div class="legend-item">
<div class="legend-poly" style="background:rgba(0,0,255,0.2); border-color:#0000FF;"></div>
<span>Sertifikat Hak Guna Bangunan (HGB)</span>
</div>
<div class="legend-item">
<div class="legend-poly" style="background:rgba(255,165,0,0.2); border-color:#FFA500;"></div>
<span>Sertifikat Hak Guna Usaha (HGU)</span>
</div>
<div class="legend-item">
<div class="legend-poly" style="background:rgba(128,0,128,0.2); border-color:#800080;"></div>
<span>Sertifikat Hak Pakai (HP)</span>
</div>
</div>
</div>
<!-- Info Panel -->
<div class="info-panel" id="infoPanel">
Mode: <strong>Navigasi</strong> — Klik layer untuk lihat info
</div>
<!-- Measurement Display -->
<div class="measurement-display" id="measurementDisplay" style="display: none;">
<span id="measurementValue"></span>
</div>
<!-- Edit Indicator -->
<div class="edit-indicator" id="editIndicator">
Sedang mengedit posisi...
</div>
<!-- Loading -->
<div class="loading" id="loading">
<i class="fas fa-circle-notch fa-spin"></i>
<p>Memproses...</p>
</div>
<script>
// ==================== INISIALISASI PETA ====================
const map = L.map('map').setView([-0.0556, 109.3485], 13);
// Base layer (Tile Layer)
const baseLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors'
}).addTo(map);
// ==================== LAYER GROUPS UNTUK SPBU ====================
let spbu24hLayer = L.layerGroup();
let spbuNon24hLayer = L.layerGroup();
let jalanLayer = L.layerGroup();
let parsilLayer = L.layerGroup();
// ==================== LAYERS CONTROL ====================
const baseMaps = {
"OpenStreetMap": baseLayer
};
const overlayMaps = {
"SPBU 24 Jam": spbu24hLayer,
"SPBU (Tidak 24 Jam)": spbuNon24hLayer,
"Jalan": jalanLayer,
"Parsil Tanah": parsilLayer
};
spbu24hLayer.addTo(map);
spbuNon24hLayer.addTo(map);
jalanLayer.addTo(map);
parsilLayer.addTo(map);
const layerControl = L.control.layers(baseMaps, overlayMaps, {
collapsed: true,
position: 'topright'
}).addTo(map);
let editGroup = new L.FeatureGroup().addTo(map);
// ==================== VARIABEL GLOBAL ====================
let currentMode = 'navigate';
let isDrawing = false;
let drawingPoints = [];
let drawingLayer = null;
let tempLine = null;
let currentFilter = 'all';
let searchTerm = '';
let currentEditData = null;
let isLoading = false;
let allData = { spbu: [], jalan: [], parsil: [] };
let layerDataMap = new Map();
const jalanColors = {
'Nasional': '#FF0000',
'Provinsi': '#0000FF',
'Kabupaten': '#008000'
};
const parsilColors = {
'SHM': { color: '#FF0000', fillOpacity: 0.3 },
'HGB': { color: '#0000FF', fillOpacity: 0.3 },
'HGU': { color: '#FFA500', fillOpacity: 0.3 },
'HP': { color: '#800080', fillOpacity: 0.3 }
};
// ==================== FUNGSI MODAL EDIT ====================
window.closeEditModal = function() {
document.getElementById('editModal').style.display = 'none';
currentEditData = null;
};
window.openEditModal = function(type, id) {
let data = null;
if (type === 'spbu') data = allData.spbu.find(d => d.id == id);
else if (type === 'jalan') data = allData.jalan.find(d => d.id == id);
else if (type === 'parsil') data = allData.parsil.find(d => d.id == id);
if (!data) { showNotification('Data tidak ditemukan!', 'error'); loadAllData(); return; }
currentEditData = { type, id: data.id, data };
const modal = document.getElementById('editModal');
const modalTitle = document.getElementById('modalTitle');
const formFields = document.getElementById('formFields');
const typeNames = { 'spbu': 'SPBU', 'jalan': 'Jalan', 'parsil': 'Parsil' };
modalTitle.textContent = `Edit ${typeNames[type]}${data.nama}`;
let html = '';
if (type === 'spbu') {
html = `
<div class="form-group">
<label>Nama SPBU</label>
<input type="text" id="edit_nama" value="${data.nama || ''}" required>
</div>
<div class="form-group">
<label>Nomor WA</label>
<input type="text" id="edit_nomor" value="${data.nomor || ''}" required>
</div>
<div class="form-group">
<label>Kategori</label>
<select id="edit_kategori">
<option value="24_jam" ${data.kategori === '24_jam' ? 'selected' : ''}>24 Jam</option>
<option value="tidak" ${data.kategori === 'tidak' ? 'selected' : ''}>Tidak 24 Jam</option>
</select>
</div>
`;
} else if (type === 'jalan') {
html = `
<div class="form-group">
<label>Nama Jalan</label>
<input type="text" id="edit_nama" value="${data.nama || ''}" required>
</div>
<div class="form-group">
<label>Status Jalan</label>
<select id="edit_status">
<option value="Nasional" ${data.status === 'Nasional' ? 'selected' : ''}>Nasional</option>
<option value="Provinsi" ${data.status === 'Provinsi' ? 'selected' : ''}>Provinsi</option>
<option value="Kabupaten" ${data.status === 'Kabupaten' ? 'selected' : ''}>Kabupaten</option>
</select>
</div>
`;
} else if (type === 'parsil') {
html = `
<div class="form-group">
<label>Nama Parsil</label>
<input type="text" id="edit_nama" value="${data.nama || ''}" required>
</div>
<div class="form-group">
<label>Status Sertifikat</label>
<select id="edit_status">
<option value="SHM" ${data.status === 'SHM' ? 'selected' : ''}>Sertifikat Hak Milik (SHM)</option>
<option value="HGB" ${data.status === 'HGB' ? 'selected' : ''}>Sertifikat Hak Guna Bangunan (HGB)</option>
<option value="HGU" ${data.status === 'HGU' ? 'selected' : ''}>Sertifikat Hak Guna Usaha (HGU)</option>
<option value="HP" ${data.status === 'HP' ? 'selected' : ''}>Sertifikat Hak Pakai (HP)</option>
</select>
</div>
`;
}
formFields.innerHTML = html;
modal.style.display = 'flex';
};
document.getElementById('editForm').addEventListener('submit', function(e) {
e.preventDefault();
if (!currentEditData) { closeEditModal(); return; }
const { type, id } = currentEditData;
showLoading();
if (type === 'spbu') {
const nama = document.getElementById('edit_nama').value;
const nomor = document.getElementById('edit_nomor').value;
const kategori = document.getElementById('edit_kategori').value;
if (!nama || !nomor) { showNotification('Semua field harus diisi!', 'warning'); hideLoading(); return; }
const formData = new FormData();
formData.append('id', id); formData.append('nama', nama);
formData.append('nomor', nomor); formData.append('kategori', kategori);
fetch('SPBU/update_spbu_info.php', { method: 'POST', body: formData })
.then(res => res.json()).then(result => {
hideLoading();
if (result.status === 'success') { showNotification('Data SPBU berhasil diupdate!'); closeEditModal(); loadAllData(); }
else showNotification('Gagal: ' + result.message, 'error');
}).catch(error => { hideLoading(); showNotification('Error: ' + error.message, 'error'); });
} else if (type === 'jalan') {
const nama = document.getElementById('edit_nama').value;
const status = document.getElementById('edit_status').value;
if (!nama) { showNotification('Nama harus diisi!', 'warning'); hideLoading(); return; }
const formData = new FormData();
formData.append('id', id); formData.append('nama', nama); formData.append('status', status);
fetch('Jalan/update_jalan.php', { method: 'POST', body: formData })
.then(res => res.json()).then(result => {
hideLoading();
if (result.status === 'success') { showNotification('Data Jalan berhasil diupdate!'); closeEditModal(); loadAllData(); }
else showNotification('Gagal: ' + result.message, 'error');
}).catch(error => { hideLoading(); showNotification('Error: ' + error.message, 'error'); });
} else if (type === 'parsil') {
const nama = document.getElementById('edit_nama').value;
const status = document.getElementById('edit_status').value;
if (!nama) { showNotification('Nama harus diisi!', 'warning'); hideLoading(); return; }
const formData = new FormData();
formData.append('id', id); formData.append('nama', nama); formData.append('status', status);
fetch('Parsil/update_parsil.php', { method: 'POST', body: formData })
.then(res => res.json()).then(result => {
hideLoading();
if (result.status === 'success') { showNotification('Data Parsil berhasil diupdate!'); closeEditModal(); loadAllData(); }
else showNotification('Gagal: ' + result.message, 'error');
}).catch(error => { hideLoading(); showNotification('Error: ' + error.message, 'error'); });
}
});
document.getElementById('editModal').addEventListener('click', function(e) {
if (e.target === this) closeEditModal();
});
// ==================== FUNGSI NOTIFIKASI ====================
function showNotification(message, type = 'success') {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.className = 'notification';
if (type === 'error') notification.classList.add('error');
else if (type === 'warning') notification.classList.add('warning');
notification.style.display = 'block';
setTimeout(() => { notification.style.display = 'none'; }, 3000);
}
function showLoading() { if (!isLoading) { isLoading = true; document.getElementById('loading').style.display = 'block'; } }
function hideLoading() { isLoading = false; document.getElementById('loading').style.display = 'none'; }
// ==================== MODE SWITCHING ====================
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.mode-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
currentMode = this.dataset.mode;
stopDrawing();
disableEditMode();
const drawControls = document.getElementById('drawingControls');
const mapEl = document.getElementById('map');
if (currentMode === 'navigate') {
drawControls.style.display = 'none';
mapEl.style.cursor = 'grab';
document.getElementById('infoPanel').innerHTML = 'Mode: <strong>Navigasi</strong> — Klik layer untuk lihat info';
} else if (currentMode === 'point') {
drawControls.style.display = 'none';
mapEl.style.cursor = 'crosshair';
document.getElementById('infoPanel').innerHTML = 'Mode: <strong>SPBU</strong> — Klik peta untuk menambah SPBU';
} else if (currentMode === 'jalan') {
drawControls.style.display = 'block';
mapEl.style.cursor = 'crosshair';
document.getElementById('drawingHint').innerHTML = 'Klik untuk tambah titik. <strong>Double-click</strong> untuk selesai.';
document.getElementById('infoPanel').innerHTML = 'Mode: <strong>Jalan</strong> — Gambar polyline';
startDrawingMode();
} else if (currentMode === 'parsil') {
drawControls.style.display = 'block';
mapEl.style.cursor = 'crosshair';
document.getElementById('drawingHint').innerHTML = 'Klik untuk tambah titik. <strong>Double-click</strong> atau klik titik awal untuk selesai.';
document.getElementById('infoPanel').innerHTML = 'Mode: <strong>Parsil Tanah</strong> — Gambar polygon';
startDrawingMode();
}
});
});
function enableEditMode() {
editGroup.clearLayers();
spbu24hLayer.eachLayer(layer => { if (layer instanceof L.Marker && layer.dragging) layer.dragging.enable(); });
spbuNon24hLayer.eachLayer(layer => { if (layer instanceof L.Marker && layer.dragging) layer.dragging.enable(); });
jalanLayer.eachLayer(layer => { if (layer instanceof L.Polyline || layer instanceof L.Polygon) editGroup.addLayer(layer); });
parsilLayer.eachLayer(layer => { if (layer instanceof L.Polygon) editGroup.addLayer(layer); });
editGroup.eachLayer(layer => {
try {
if (layer.editing) layer.editing.enable();
else if (layer instanceof L.Polyline || layer instanceof L.Polygon) {
if (L.Edit && L.Edit.Poly) { layer.editing = new L.Edit.Poly(layer); layer.editing.enable(); }
}
} catch (err) {}
});
}
function disableEditMode() {
editGroup.eachLayer(layer => {
try {
if (layer.editing) layer.editing.disable();
if (layer instanceof L.Marker && layer.dragging) layer.dragging.disable();
} catch (err) {}
});
editGroup.clearLayers();
}
// ==================== DRAWING FUNCTIONS ====================
function startDrawingMode() {
isDrawing = true; drawingPoints = [];
if (drawingLayer) map.removeLayer(drawingLayer);
drawingLayer = L.layerGroup().addTo(map);
document.getElementById('measurementDisplay').style.display = 'block';
document.getElementById('measurementValue').innerHTML = '';
}
map.on('click', function(e) {
if (currentMode === 'point' && !isDrawing) { showSPBUForm(e.latlng); return; }
if (!isDrawing) return;
const lat = e.latlng.lat, lng = e.latlng.lng;
if (currentMode === 'parsil' && drawingPoints.length >= 3) {
const firstPoint = drawingPoints[0];
const dist = map.distance([lat, lng], firstPoint);
if (dist < 20) { finishDrawing(); return; }
}
drawingPoints.push([lat, lng]);
L.circleMarker([lat, lng], { radius: 5, color: currentMode === 'jalan' ? '#d97706' : '#7c3aed', fillColor: currentMode === 'jalan' ? '#d97706' : '#7c3aed', fillOpacity: 1, weight: 2 }).addTo(drawingLayer);
L.marker([lat, lng], { icon: L.divIcon({ className: 'point-label', html: `<div style="background:white; border-radius:50%; width:18px; height:18px; display:flex; align-items:center; justify-content:center; font-size:10px; font-weight:bold; border:1.5px solid #6b7280;">${drawingPoints.length}</div>`, iconSize: [18,18], iconAnchor: [9,9] }) }).addTo(drawingLayer);
updateTempLine();
updateMeasurement();
});
map.on('dblclick', function(e) {
if (!isDrawing) return;
if (currentMode === 'jalan' && drawingPoints.length >= 2) finishDrawing();
else if (currentMode === 'parsil' && drawingPoints.length >= 3) finishDrawing();
});
function updateTempLine() {
if (drawingPoints.length < 2) return;
if (tempLine) drawingLayer.removeLayer(tempLine);
if (currentMode === 'jalan') {
tempLine = L.polyline(drawingPoints, { color: '#d97706', weight: 3, opacity: 0.8 }).addTo(drawingLayer);
} else if (currentMode === 'parsil') {
tempLine = L.polygon([...drawingPoints, drawingPoints[0]], { color: '#7c3aed', weight: 2, fillColor: '#7c3aed', fillOpacity: 0.2, opacity: 0.8 }).addTo(drawingLayer);
}
}
function updateMeasurement() {
let value = 0, unit = '';
try {
if (currentMode === 'jalan' && drawingPoints.length >= 2) {
const coords = drawingPoints.map(p => [p[1], p[0]]);
value = turf.length(turf.lineString(coords), {units: 'meters'});
unit = value >= 1000 ? 'km' : 'm';
if (value >= 1000) value /= 1000;
} else if (currentMode === 'parsil' && drawingPoints.length >= 3) {
const coords = drawingPoints.map(p => [p[1], p[0]]);
coords.push([coords[0][0], coords[0][1]]);
value = turf.area(turf.polygon([coords]));
unit = value >= 10000 ? 'ha' : 'm²';
if (value >= 10000) value /= 10000;
}
if (value > 0) {
document.getElementById('measurementValue').innerHTML = `${currentMode === 'jalan' ? 'Panjang' : 'Luas'}: ${value.toFixed(2)} ${unit}`;
}
} catch (error) {}
}
function finishDrawing() {
if (currentMode === 'jalan' && drawingPoints.length < 2) { showNotification('Minimal 2 titik!', 'warning'); return; }
if (currentMode === 'parsil' && drawingPoints.length < 3) { showNotification('Minimal 3 titik!', 'warning'); return; }
const bounds = L.latLngBounds(drawingPoints);
showSaveForm(bounds.getCenter());
}
function stopDrawing() {
isDrawing = false; drawingPoints = [];
if (drawingLayer) { map.removeLayer(drawingLayer); drawingLayer = null; }
tempLine = null;
document.getElementById('measurementDisplay').style.display = 'none';
}
// ==================== FORM SAVE ====================
function showSPBUForm(latlng) {
const popup = L.popup().setLatLng(latlng).setContent(`
<div class="popup-form">
<h4>Tambah SPBU</h4>
<label style="font-size:11px;font-weight:600;color:#6b7280;">Nama SPBU</label>
<input type="text" id="spbu_nama" placeholder="Nama SPBU" required>
<label style="font-size:11px;font-weight:600;color:#6b7280;">Nomor WA</label>
<input type="text" id="spbu_nomor" placeholder="Nomor WA" required>
<label style="font-size:11px;font-weight:600;color:#6b7280;">Kategori</label>
<select id="spbu_kategori">
<option value="24_jam">24 Jam</option>
<option value="tidak">Tidak 24 Jam</option>
</select>
<div class="coord-info">Lat: ${latlng.lat.toFixed(6)}&nbsp;&nbsp;Lng: ${latlng.lng.toFixed(6)}</div>
<div style="margin-top:12px; display:flex; gap:6px;">
<button id="spbu_save" style="background:#2563eb; color:white; padding:8px 12px; border:none; border-radius:6px; cursor:pointer; flex:1; font-family:'DM Sans',sans-serif; font-size:12px; font-weight:600;">Simpan</button>
<button id="spbu_cancel" style="background:#f3f4f6; color:#6b7280; padding:8px 12px; border:1px solid #e4e7ec; border-radius:6px; cursor:pointer; flex:1; font-family:'DM Sans',sans-serif; font-size:12px; font-weight:600;">Batal</button>
</div>
</div>
`).openOn(map);
setTimeout(() => {
const saveBtn = document.getElementById('spbu_save');
const cancelBtn = document.getElementById('spbu_cancel');
if (saveBtn) {
saveBtn.onclick = () => {
const nama = document.getElementById('spbu_nama').value;
const nomor = document.getElementById('spbu_nomor').value;
const kategori = document.getElementById('spbu_kategori').value;
if (!nama || !nomor) { showNotification('Lengkapi data!', 'warning'); return; }
showLoading();
const formData = new FormData();
formData.append('nama', nama); formData.append('nomor', nomor);
formData.append('kategori', kategori); formData.append('latitude', latlng.lat); formData.append('longitude', latlng.lng);
fetch('SPBU/simpan.php', { method: 'POST', body: formData })
.then(res => res.json()).then(r => {
hideLoading();
if (r.status === 'success') { showNotification('SPBU berhasil disimpan!'); map.closePopup(); loadAllData(); }
else showNotification('Gagal: ' + r.message, 'error');
}).catch(() => hideLoading());
};
}
if (cancelBtn) cancelBtn.onclick = () => map.closePopup();
}, 100);
}
function showSaveForm(center) {
let html = `<div class="popup-form"><h4>Simpan ${currentMode === 'jalan' ? 'Jalan' : 'Parsil'}</h4>
<label style="font-size:11px;font-weight:600;color:#6b7280;">Nama</label>
<input type="text" id="save_nama" placeholder="Nama" required>
<label style="font-size:11px;font-weight:600;color:#6b7280;">${currentMode === 'jalan' ? 'Status Jalan' : 'Status Sertifikat'}</label>
<select id="save_status">`;
if (currentMode === 'jalan') {
html += `<option value="Nasional">Nasional</option><option value="Provinsi">Provinsi</option><option value="Kabupaten">Kabupaten</option>`;
} else {
html += `<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>`;
}
html += `</select><div style="margin-top:12px; display:flex; gap:6px;">
<button id="save_btn" style="background:#2563eb; color:white; padding:8px 12px; border:none; border-radius:6px; cursor:pointer; flex:1; font-family:'DM Sans',sans-serif; font-size:12px; font-weight:600;">Simpan</button>
<button id="save_cancel" style="background:#f3f4f6; color:#6b7280; padding:8px 12px; border:1px solid #e4e7ec; border-radius:6px; cursor:pointer; flex:1; font-family:'DM Sans',sans-serif; font-size:12px; font-weight:600;">Batal</button>
</div></div>`;
const popup = L.popup().setLatLng(center).setContent(html).openOn(map);
setTimeout(() => {
const saveBtn = document.getElementById('save_btn');
const cancelBtn = document.getElementById('save_cancel');
if (saveBtn) {
saveBtn.onclick = () => {
const nama = document.getElementById('save_nama').value;
const status = document.getElementById('save_status').value;
if (!nama) { showNotification('Nama harus diisi!', 'warning'); return; }
saveToServer(nama, status);
map.closePopup();
};
}
if (cancelBtn) cancelBtn.onclick = () => map.closePopup();
}, 100);
}
function saveToServer(nama, status) {
showLoading();
const coords = drawingPoints.map(p => [p[1], p[0]]);
let geojson, measurement;
if (currentMode === 'jalan') {
measurement = turf.length(turf.lineString(coords), {units: 'meters'});
geojson = { type: "Feature", geometry: { type: "LineString", coordinates: coords }, properties: {} };
} else {
const closed = [...coords, coords[0]];
measurement = turf.area(turf.polygon([closed]));
geojson = { type: "Feature", geometry: { type: "Polygon", coordinates: [closed] }, properties: {} };
}
const url = currentMode === 'jalan' ? 'Jalan/simpan_jalan.php' : 'Parsil/simpan_parsil.php';
const formData = new FormData();
formData.append('nama', nama); formData.append('status', status);
formData.append('geojson', JSON.stringify(geojson));
formData.append(currentMode === 'jalan' ? 'panjang' : 'luas', measurement);
fetch(url, { method: 'POST', body: formData })
.then(res => res.json()).then(r => {
hideLoading();
if (r.status === 'success') { showNotification('Data berhasil disimpan!'); stopDrawing(); loadAllData(); document.querySelector('.mode-btn.navigate').click(); }
else showNotification('Gagal: ' + r.message, 'error');
}).catch(() => hideLoading());
}
// ==================== FUNGSI HAPUS DATA ====================
window.hapusData = function(type, id) {
if (!confirm('Yakin ingin menghapus data ini?')) return;
const urlMap = { spbu: 'SPBU/hapus.php', jalan: 'Jalan/hapus_jalan.php', parsil: 'Parsil/hapus_parsil.php' };
const url = urlMap[type];
if (!url) { showNotification('Tipe data tidak dikenal!', 'error'); return; }
showLoading();
const formData = new FormData();
formData.append('id', id);
fetch(url, { method: 'POST', body: formData })
.then(res => res.json()).then(data => {
hideLoading();
if (data.status === 'success') { showNotification(data.message); if (map._popup) map.closePopup(); loadAllData(); }
else showNotification('Gagal: ' + data.message, 'error');
}).catch(() => hideLoading());
};
// ==================== LOAD DATA ====================
function loadAllData() {
if (isLoading) return Promise.resolve(allData);
showLoading();
return Promise.all([
fetch('SPBU/ambil.php').then(r => r.json()).catch(() => []),
fetch('Jalan/ambil_jalan.php').then(r => r.json()).catch(() => []),
fetch('Parsil/ambil_parsil.php').then(r => r.json()).catch(() => [])
]).then(([spbu, jalan, parsil]) => {
allData = { spbu, jalan, parsil };
renderSPBU(spbu); renderJalan(jalan); renderParsil(parsil);
renderDataList(); enableEditMode(); hideLoading();
return allData;
}).catch(err => {
hideLoading(); showNotification('Gagal memuat data!', 'error');
return allData;
});
}
// ==================== RENDER FUNCTIONS ====================
function renderSPBU(data) {
spbu24hLayer.clearLayers();
spbuNon24hLayer.clearLayers();
data.forEach(d => {
// Icon lebih cantik dengan ExtraMarkers
const marker = L.marker([d.latitude, d.longitude], {
icon: L.ExtraMarkers.icon({
icon: 'fa-gas-pump',
markerColor: d.kategori === '24_jam' ? 'green' : 'red',
shape: 'circle',
prefix: 'fa',
iconColor: 'white'
}),
draggable: true
});
marker.on('dragend', function(e) {
const pos = marker.getLatLng();
const formData = new FormData();
formData.append('id', d.id); formData.append('latitude', pos.lat); formData.append('longitude', pos.lng);
fetch('update_lokasi.php', { method: 'POST', body: formData }).then(() => loadAllData());
});
// Popup dengan latitude & longitude di bawah kategori
marker.bindPopup(`
<div style="min-width:220px; font-family:'DM Sans',sans-serif;">
<div style="font-size:15px; font-weight:700; color:#111827; margin-bottom:10px; padding-bottom:8px; border-bottom:1px solid #f0f2f5;">⛽ ${d.nama}</div>
<div style="font-size:12px; color:#6b7280; margin-bottom:4px;"><strong style="color:#374151;">WA:</strong> ${d.nomor}</div>
<div style="font-size:12px; color:#6b7280; margin-bottom:8px;"><strong style="color:#374151;">Kategori:</strong> ${d.kategori === '24_jam' ? '<span style="color:#059669;font-weight:600;">24 Jam</span>' : '<span style="color:#dc2626;font-weight:600;">Tidak 24 Jam</span>'}</div>
<div style="font-size:11px; color:#9ca3af; margin-bottom:12px; font-family:'DM Mono',monospace; border-top:1px solid #f0f2f5; padding-top:6px;"><strong>📍 Koordinat:</strong><br>Lat: ${parseFloat(d.latitude).toFixed(6)}<br>Lng: ${parseFloat(d.longitude).toFixed(6)}</div>
<div style="display:flex; gap:6px;">
<button onclick="openEditModal('spbu', ${d.id})" style="background:#fffbeb; color:#d97706; border:1px solid rgba(217,119,6,0.3); padding:7px 10px; border-radius:6px; cursor:pointer; flex:1; font-family:'DM Sans',sans-serif; font-size:12px; font-weight:600;">Edit</button>
<button onclick="hapusData('spbu', ${d.id})" style="background:#fef2f2; color:#dc2626; border:1px solid rgba(220,38,38,0.2); padding:7px 10px; border-radius:6px; cursor:pointer; flex:1; font-family:'DM Sans',sans-serif; font-size:12px; font-weight:600;">Hapus</button>
</div>
</div>
`);
if (d.kategori === '24_jam') marker.addTo(spbu24hLayer);
else marker.addTo(spbuNon24hLayer);
layerDataMap.set(marker, { type: 'spbu', data: d });
});
}
function renderJalan(data) {
jalanLayer.clearLayers();
data.forEach(item => {
try {
const geojson = JSON.parse(item.geojson);
const layer = L.geoJSON(geojson, { style: { color: jalanColors[item.status] || '#FF9800', weight: 4, opacity: 0.8 } }).addTo(jalanLayer);
const panjang = parseFloat(item.panjang);
const panjangText = panjang >= 1000 ? (panjang/1000).toFixed(2) + ' km' : panjang.toFixed(2) + ' m';
// Hitung koordinat polyline (ambil titik tengah untuk tampil, atau semua titik)
let coordsText = '';
if (geojson.geometry && geojson.geometry.coordinates) {
const coords = geojson.geometry.coordinates;
const start = coords[0];
const end = coords[coords.length - 1];
coordsText = `Titik awal: ${start[1].toFixed(6)}, ${start[0].toFixed(6)}<br>Titik akhir: ${end[1].toFixed(6)}, ${end[0].toFixed(6)}`;
}
layer.bindPopup(`
<div style="min-width:220px; font-family:'DM Sans',sans-serif;">
<div style="font-size:15px; font-weight:700; color:#111827; margin-bottom:10px; padding-bottom:8px; border-bottom:1px solid #f0f2f5;">🛣️ ${item.nama}</div>
<div style="font-size:12px; color:#6b7280; margin-bottom:4px;"><strong style="color:#374151;">Status:</strong> ${item.status}</div>
<div style="font-size:12px; color:#6b7280; margin-bottom:8px;"><strong style="color:#374151;">Panjang:</strong> ${panjangText}</div>
<div style="font-size:11px; color:#9ca3af; margin-bottom:12px; font-family:'DM Mono',monospace; border-top:1px solid #f0f2f5; padding-top:6px;"><strong>📍 Koordinat:</strong><br>${coordsText}</div>
<div style="display:flex; gap:6px;">
<button onclick="openEditModal('jalan', ${item.id})" style="background:#fffbeb; color:#d97706; border:1px solid rgba(217,119,6,0.3); padding:7px 10px; border-radius:6px; cursor:pointer; flex:1; font-family:'DM Sans',sans-serif; font-size:12px; font-weight:600;">Edit</button>
<button onclick="hapusData('jalan', ${item.id})" style="background:#fef2f2; color:#dc2626; border:1px solid rgba(220,38,38,0.2); padding:7px 10px; border-radius:6px; cursor:pointer; flex:1; font-family:'DM Sans',sans-serif; font-size:12px; font-weight:600;">Hapus</button>
</div>
</div>
`);
layerDataMap.set(layer, { type: 'jalan', data: item });
} catch (e) {}
});
}
function renderParsil(data) {
parsilLayer.clearLayers();
data.forEach(item => {
try {
const geojson = JSON.parse(item.geojson);
// Normalisasi status untuk menangani data lama yang mungkin tersimpan dengan format panjang
let statusCode = item.status;
if (statusCode.includes('SHM')) statusCode = 'SHM';
else if (statusCode.includes('HGB')) statusCode = 'HGB';
else if (statusCode.includes('HGU')) statusCode = 'HGU';
else if (statusCode.includes('HP')) statusCode = 'HP';
const layer = L.geoJSON(geojson, { style: { color: parsilColors[statusCode]?.color || '#9C27B0', fillOpacity: 0.3, weight: 3 } }).addTo(parsilLayer);
let luas = parseFloat(item.luas);
let luasText = luas >= 10000 ? (luas/10000).toFixed(2) + ' ha' : luas.toFixed(2) + ' m²';
// Hitung koordinat polygon (ambil centroid atau titik awal)
let coordsText = '';
if (geojson.geometry && geojson.geometry.coordinates && geojson.geometry.coordinates[0]) {
const coords = geojson.geometry.coordinates[0];
const center = turf.centerOfMass(geojson);
if (center && center.geometry) {
coordsText = `Pusat: ${center.geometry.coordinates[1].toFixed(6)}, ${center.geometry.coordinates[0].toFixed(6)}`;
} else {
const firstPoint = coords[0];
coordsText = `Titik referensi: ${firstPoint[1].toFixed(6)}, ${firstPoint[0].toFixed(6)}`;
}
}
layer.bindPopup(`
<div style="min-width:220px; font-family:'DM Sans',sans-serif;">
<div style="font-size:15px; font-weight:700; color:#111827; margin-bottom:10px; padding-bottom:8px; border-bottom:1px solid #f0f2f5;">🏠 ${item.nama}</div>
<div style="font-size:12px; color:#6b7280; margin-bottom:4px;"><strong style="color:#374151;">Status:</strong> ${item.status}</div>
<div style="font-size:12px; color:#6b7280; margin-bottom:8px;"><strong style="color:#374151;">Luas:</strong> ${luasText}</div>
<div style="font-size:11px; color:#9ca3af; margin-bottom:12px; font-family:'DM Mono',monospace; border-top:1px solid #f0f2f5; padding-top:6px;"><strong>📍 Koordinat:</strong><br>${coordsText}</div>
<div style="display:flex; gap:6px;">
<button onclick="openEditModal('parsil', ${item.id})" style="background:#fffbeb; color:#d97706; border:1px solid rgba(217,119,6,0.3); padding:7px 10px; border-radius:6px; cursor:pointer; flex:1; font-family:'DM Sans',sans-serif; font-size:12px; font-weight:600;">Edit</button>
<button onclick="hapusData('parsil', ${item.id})" style="background:#fef2f2; color:#dc2626; border:1px solid rgba(220,38,38,0.2); padding:7px 10px; border-radius:6px; cursor:pointer; flex:1; font-family:'DM Sans',sans-serif; font-size:12px; font-weight:600;">Hapus</button>
</div>
</div>
`);
layerDataMap.set(layer, { type: 'parsil', data: item });
} catch (e) {}
});
}
// ==================== DATA LIST ====================
function renderDataList() {
const listEl = document.getElementById('dataList');
let filteredData = [];
if (currentFilter === 'all' || currentFilter === 'spbu') {
allData.spbu.forEach(d => filteredData.push({ type: 'spbu', data: d, title: d.nama, info: d.kategori === '24_jam' ? '24 Jam' : 'Tidak 24 Jam' }));
}
if (currentFilter === 'all' || currentFilter === 'jalan') {
allData.jalan.forEach(d => {
const p = parseFloat(d.panjang);
filteredData.push({ type: 'jalan', data: d, title: d.nama, info: `${d.status} · ${p >= 1000 ? (p/1000).toFixed(2)+'km' : p.toFixed(0)+'m'}` });
});
}
if (currentFilter === 'all' || currentFilter === 'parsil') {
allData.parsil.forEach(d => {
const l = parseFloat(d.luas);
filteredData.push({ type: 'parsil', data: d, title: d.nama, info: `${d.status} · ${l >= 10000 ? (l/10000).toFixed(2)+'ha' : l.toFixed(0)+'m²'}` });
});
}
if (searchTerm) {
filteredData = filteredData.filter(d => d.title.toLowerCase().includes(searchTerm.toLowerCase()));
}
filteredData.sort((a, b) => a.title.localeCompare(b.title));
if (filteredData.length === 0) {
listEl.innerHTML = `<div class="empty-state"><i class="fas fa-search"></i><p>Tidak ada data ditemukan</p></div>`;
return;
}
let html = '';
filteredData.forEach(item => {
const badgeClass = item.type === 'spbu' ? 'badge-spbu' : (item.type === 'jalan' ? 'badge-jalan' : 'badge-parsil');
const typeLabel = item.type === 'spbu' ? 'SPBU' : item.type === 'jalan' ? 'Jalan' : 'Parsil';
html += `
<div class="data-item">
<div class="data-item-header">
<span class="data-item-title" onclick="zoomToLayer('${item.type}', ${item.data.id})">${item.title}</span>
<div class="data-item-actions">
<button class="action-btn edit" onclick="openEditModal('${item.type}', ${item.data.id})" title="Edit"><i class="fas fa-pen"></i></button>
<button class="action-btn delete" onclick="hapusData('${item.type}', ${item.data.id})" title="Hapus"><i class="fas fa-trash"></i></button>
</div>
</div>
<div class="data-item-meta">
<span class="data-item-badge ${badgeClass}">${typeLabel}</span>
<span class="data-item-info">${item.info}</span>
<span class="data-item-id">#${item.data.id}</span>
</div>
</div>
`;
});
listEl.innerHTML = html;
}
window.zoomToLayer = function(type, id) {
if (type === 'spbu') {
let found = false;
spbu24hLayer.eachLayer(layer => {
if (layerDataMap.get(layer)?.data.id === id) { map.setView(layer.getLatLng(), 18); layer.openPopup(); found = true; }
});
if (!found) {
spbuNon24hLayer.eachLayer(layer => {
if (layerDataMap.get(layer)?.data.id === id) { map.setView(layer.getLatLng(), 18); layer.openPopup(); found = true; }
});
}
} else {
const lg = type === 'jalan' ? jalanLayer : parsilLayer;
lg.eachLayer(layer => {
if (layerDataMap.get(layer)?.data.id === id) { map.fitBounds(layer.getBounds(), { padding: [50, 50] }); layer.openPopup(); }
});
}
};
window.refreshAllData = function() {
showNotification('Memuat ulang data...', 'warning');
loadAllData().then(() => showNotification('Data berhasil dimuat!'));
};
// ==================== EVENT LISTENERS ====================
document.getElementById('searchBox').addEventListener('input', e => { searchTerm = e.target.value; renderDataList(); });
document.querySelectorAll('.filter-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
currentFilter = this.dataset.filter;
renderDataList();
});
});
document.getElementById('undoBtn').addEventListener('click', function() {
if (!isDrawing || drawingPoints.length === 0) return;
drawingPoints.pop();
drawingLayer.clearLayers();
drawingPoints.forEach((p, i) => {
L.circleMarker(p, { radius: 5, color: currentMode === 'jalan' ? '#d97706' : '#7c3aed', fillColor: currentMode === 'jalan' ? '#d97706' : '#7c3aed', fillOpacity: 1 }).addTo(drawingLayer);
});
updateTempLine();
updateMeasurement();
});
document.getElementById('cancelBtn').addEventListener('click', function() {
stopDrawing();
document.querySelector('.mode-btn.navigate').click();
});
// ==================== INIT ====================
setTimeout(() => { loadAllData(); }, 100);
</script>
</body>
</html>