1800 lines
71 KiB
HTML
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 · 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: '© 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)} 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> |