1832 lines
72 KiB
HTML
1832 lines
72 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>SPBU Map</title>
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
<link rel="stylesheet" href="https://unpkg.com/@geoman-io/leaflet-geoman-free@2.15.0/dist/leaflet-geoman.css" />
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<script src="https://unpkg.com/@geoman-io/leaflet-geoman-free@2.15.0/dist/leaflet-geoman.min.js"></script>
|
|
<script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
|
|
<!-- Google Fonts for premium typography -->
|
|
<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=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
|
|
<style>
|
|
:root {
|
|
--primary: #3b82f6;
|
|
--primary-hover: #2563eb;
|
|
--success: #10b981;
|
|
--success-hover: #059669;
|
|
--danger: #ef4444;
|
|
--danger-hover: #dc2626;
|
|
--warning: #f59e0b;
|
|
--info: #06b6d4;
|
|
|
|
--bg-main: #f8fafc;
|
|
--bg-card: #ffffff;
|
|
--border-color: #e2e8f0;
|
|
--text-primary: #0f172a;
|
|
--text-secondary: #64748b;
|
|
|
|
--font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.08);
|
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
|
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
padding: 0;
|
|
font-family: var(--font-family);
|
|
background-color: var(--bg-main);
|
|
color: var(--text-primary);
|
|
overflow: hidden;
|
|
}
|
|
|
|
#app {
|
|
display: flex;
|
|
height: 100vh;
|
|
width: 100vw;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* 320px Fixed Sidebar Data Hub */
|
|
#sidebar {
|
|
width: 320px;
|
|
min-width: 320px;
|
|
max-width: 320px;
|
|
background: #ffffff;
|
|
border-right: 1px solid var(--border-color);
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
box-sizing: border-box;
|
|
z-index: 10;
|
|
}
|
|
|
|
.sidebar-header {
|
|
padding: 24px 20px 16px 20px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
.sidebar-header h1 {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
margin: 0 0 6px 0;
|
|
letter-spacing: -0.025em;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.sidebar-header p {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
margin: 0;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
/* Modern Tabs Component */
|
|
.tab-headers {
|
|
display: flex;
|
|
background: #f1f5f9;
|
|
padding: 4px;
|
|
margin: 16px 20px;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--border-color);
|
|
gap: 2px;
|
|
}
|
|
|
|
.tab-btn {
|
|
flex: 1;
|
|
text-align: center;
|
|
padding: 8px 6px;
|
|
font-family: var(--font-family);
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
border-radius: 6px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.tab-btn:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.tab-btn.active {
|
|
background: #ffffff;
|
|
color: var(--text-primary);
|
|
box-shadow: var(--shadow-sm);
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Tab Content List Hub */
|
|
.sidebar-content {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 0 20px 20px 20px;
|
|
}
|
|
|
|
.tab-pane {
|
|
display: none;
|
|
}
|
|
|
|
.tab-pane.active {
|
|
display: block;
|
|
}
|
|
|
|
.side-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.side-list li {
|
|
margin: 0 0 8px 0;
|
|
}
|
|
|
|
/* Compact Cards (Typography Hierarchical) */
|
|
.side-item {
|
|
width: 100%;
|
|
text-align: left;
|
|
border: 1px solid transparent;
|
|
background: #ffffff;
|
|
border-radius: 8px;
|
|
padding: 10px 12px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
transition: var(--transition);
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.02);
|
|
font-family: var(--font-family);
|
|
}
|
|
|
|
.side-item:hover {
|
|
background: #f8fafc;
|
|
border-color: var(--border-color);
|
|
transform: translateY(-1px);
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.status-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.side-item .text-wrap {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
overflow: hidden;
|
|
flex: 1;
|
|
}
|
|
|
|
.side-item .item-title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.side-item .item-desc {
|
|
font-size: 11px;
|
|
color: var(--text-secondary);
|
|
white-space: nowrap;
|
|
text-overflow: ellipsis;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Map Canvas */
|
|
#map-container {
|
|
position: relative;
|
|
flex: 1;
|
|
height: 100%;
|
|
}
|
|
|
|
#map {
|
|
height: 100%;
|
|
width: 100%;
|
|
}
|
|
|
|
/* Floating Toolbars over Map */
|
|
.floating-toolbar {
|
|
position: absolute;
|
|
background: #ffffff;
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
box-shadow: var(--shadow-md);
|
|
z-index: 1000;
|
|
padding: 4px;
|
|
display: flex;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
/* Top-Right Drawing Toolbar */
|
|
#drawing-toolbar {
|
|
top: 20px;
|
|
right: 20px;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.toolbar-btn {
|
|
width: 36px;
|
|
height: 36px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--text-secondary);
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
position: relative;
|
|
}
|
|
|
|
.toolbar-btn:hover {
|
|
background: #f1f5f9;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* Tooltip style */
|
|
.toolbar-btn::after {
|
|
content: attr(data-tooltip);
|
|
position: absolute;
|
|
right: 48px;
|
|
top: 50%;
|
|
transform: translateY(-50%) translateX(5px);
|
|
background: #0f172a;
|
|
color: #ffffff;
|
|
padding: 4px 8px;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
border-radius: 4px;
|
|
white-space: nowrap;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: var(--transition);
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.toolbar-btn:hover::after {
|
|
opacity: 1;
|
|
transform: translateY(-50%) translateX(0);
|
|
}
|
|
|
|
.toolbar-btn.active {
|
|
background: #eff6ff;
|
|
color: var(--primary);
|
|
border: 1px solid #bfdbfe;
|
|
}
|
|
|
|
.toolbar-btn.danger-btn {
|
|
color: var(--danger);
|
|
}
|
|
|
|
.toolbar-btn.danger-btn:hover {
|
|
background: #fee2e2;
|
|
color: var(--danger-hover);
|
|
}
|
|
|
|
/* Bottom-Right Controls Toolbar */
|
|
#controls-toolbar {
|
|
bottom: 20px;
|
|
right: 20px;
|
|
gap: 4px;
|
|
}
|
|
|
|
.control-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 6px 12px;
|
|
font-family: var(--font-family);
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
border: 1px solid transparent;
|
|
background: transparent;
|
|
color: var(--text-secondary);
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.control-btn:hover {
|
|
background: #f1f5f9;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.control-btn.active {
|
|
background: #eff6ff;
|
|
color: var(--primary);
|
|
border-color: #bfdbfe;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Floating Status Pill */
|
|
#status-pill {
|
|
position: absolute;
|
|
top: 20px;
|
|
left: 50%;
|
|
transform: translate(-50%, -10px);
|
|
background: #0f172a;
|
|
color: #ffffff;
|
|
padding: 8px 16px;
|
|
border-radius: 30px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
box-shadow: var(--shadow-lg);
|
|
z-index: 1000;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
#status-pill.active {
|
|
opacity: 1;
|
|
transform: translate(-50%, 0);
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.status-pulse {
|
|
width: 8px;
|
|
height: 8px;
|
|
background-color: var(--success);
|
|
border-radius: 50%;
|
|
animation: pulse-ring 1.5s infinite;
|
|
}
|
|
|
|
@keyframes pulse-ring {
|
|
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7); }
|
|
70% { transform: scale(1); box-shadow: 0 0 0 6px rgba(16, 185, 129, 0); }
|
|
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(16, 185, 129, 0); }
|
|
}
|
|
|
|
/* Modernized Modals & Dialogs */
|
|
#modalOverlay {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(15, 23, 42, 0.4);
|
|
backdrop-filter: blur(4px);
|
|
z-index: 1500;
|
|
opacity: 0;
|
|
transition: opacity 0.25s ease;
|
|
}
|
|
|
|
#modalOverlay.active {
|
|
display: block;
|
|
opacity: 1;
|
|
}
|
|
|
|
#editModal, #shapeModal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%) scale(0.95);
|
|
z-index: 2000;
|
|
background: #ffffff;
|
|
padding: 24px;
|
|
border-radius: 12px;
|
|
box-shadow: var(--shadow-lg);
|
|
width: 320px;
|
|
border: 1px solid var(--border-color);
|
|
opacity: 0;
|
|
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
|
font-family: var(--font-family);
|
|
}
|
|
|
|
#editModal.active, #shapeModal.active {
|
|
display: block;
|
|
opacity: 1;
|
|
transform: translate(-50%, -50%) scale(1);
|
|
}
|
|
|
|
#editModal h3, #shapeModal h3 {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
margin: 0 0 16px 0;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
width: 100%;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.form-group label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
input[type="text"], select {
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 6px;
|
|
padding: 8px 12px;
|
|
font-size: 13px;
|
|
font-family: var(--font-family);
|
|
color: var(--text-primary);
|
|
background: #ffffff;
|
|
outline: none;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
input[type="text"]:focus, select:focus {
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
|
|
}
|
|
|
|
input[readonly] {
|
|
background-color: #f8fafc;
|
|
color: var(--text-secondary);
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.form-info {
|
|
font-size: 11px;
|
|
color: var(--text-secondary);
|
|
line-height: 1.4;
|
|
margin-top: -4px;
|
|
}
|
|
|
|
/* Modal Button Group */
|
|
.btn-group {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.modal-btn {
|
|
font-family: var(--font-family);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--border-color);
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
background: #ffffff;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.modal-btn:hover {
|
|
background: #f1f5f9;
|
|
}
|
|
|
|
.modal-btn.primary {
|
|
background: var(--primary);
|
|
color: #ffffff;
|
|
border-color: var(--primary);
|
|
}
|
|
|
|
.modal-btn.primary:hover {
|
|
background: var(--primary-hover);
|
|
}
|
|
|
|
.modal-btn.danger {
|
|
background: #fee2e2;
|
|
color: var(--danger-hover);
|
|
border-color: transparent;
|
|
}
|
|
|
|
.modal-btn.danger:hover {
|
|
background: #fecaca;
|
|
}
|
|
|
|
/* Leaflet Popup overrides for modern feel */
|
|
.leaflet-popup-content-wrapper {
|
|
border-radius: 12px !important;
|
|
box-shadow: var(--shadow-lg) !important;
|
|
border: 1px solid var(--border-color);
|
|
padding: 4px;
|
|
font-family: var(--font-family);
|
|
}
|
|
|
|
.leaflet-popup-content {
|
|
margin: 12px 14px !important;
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.leaflet-popup-content b {
|
|
font-size: 14px;
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
display: block;
|
|
margin-bottom: 4px;
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
.leaflet-popup-content hr {
|
|
border: 0;
|
|
border-top: 1px solid var(--border-color);
|
|
margin: 8px 0;
|
|
}
|
|
|
|
/* Leaflet Popup Action buttons styling */
|
|
.leaflet-popup-content .btn-action {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-family: var(--font-family);
|
|
padding: 5px 10px;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
border-radius: 4px;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
margin-right: 4px;
|
|
}
|
|
|
|
.leaflet-popup-content .btn-edit {
|
|
background: #eff6ff;
|
|
color: var(--primary);
|
|
}
|
|
|
|
.leaflet-popup-content .btn-edit:hover {
|
|
background: #dbeafe;
|
|
}
|
|
|
|
.leaflet-popup-content .btn-delete {
|
|
background: #fee2e2;
|
|
color: var(--danger);
|
|
}
|
|
|
|
.leaflet-popup-content .btn-delete:hover {
|
|
background: #fecaca;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app">
|
|
<!-- 320px Data Management Sidebar -->
|
|
<aside id="sidebar">
|
|
<div class="sidebar-header">
|
|
<h1>GIS Workspace</h1>
|
|
<p>Kelola data spasial secara dinamis di peta. Pilih tab untuk melihat daftar data saat ini.</p>
|
|
</div>
|
|
|
|
<!-- Tabbed Navigation System -->
|
|
<div class="tab-headers">
|
|
<button class="tab-btn active" onclick="switchTab('spbu-pane', this)">SPBU (<span id="spbuCount">0</span>)</button>
|
|
<button class="tab-btn" onclick="switchTab('jalan-pane', this)">Jalan (<span id="jalanCount">0</span>)</button>
|
|
<button class="tab-btn" onclick="switchTab('parsil-pane', this)">Parsil (<span id="parsilCount">0</span>)</button>
|
|
</div>
|
|
|
|
<!-- Scrollable lists -->
|
|
<div class="sidebar-content">
|
|
<!-- SPBU List Tab Pane -->
|
|
<div class="tab-pane active" id="spbu-pane">
|
|
<ul class="side-list" id="spbuList"></ul>
|
|
</div>
|
|
|
|
<!-- Jalan List Tab Pane -->
|
|
<div class="tab-pane" id="jalan-pane">
|
|
<ul class="side-list" id="jalanList"></ul>
|
|
</div>
|
|
|
|
<!-- Parsil List Tab Pane -->
|
|
<div class="tab-pane" id="parsil-pane">
|
|
<ul class="side-list" id="parsilList"></ul>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Expansive Interactive Map Canvas -->
|
|
<div id="map-container">
|
|
<!-- Sleek Top-Center Floating Status Pill -->
|
|
<div id="status-pill">
|
|
<span class="status-pulse"></span>
|
|
<span id="toolStatusText">Status: Siap.</span>
|
|
</div>
|
|
|
|
<!-- Top-Right Spatial Drawing Floating Toolbar -->
|
|
<div class="floating-toolbar" id="drawing-toolbar">
|
|
<button class="toolbar-btn" id="btnAddSpbu" type="button" data-tooltip="Tambah SPBU (Marker)">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/></svg>
|
|
</button>
|
|
<button class="toolbar-btn" id="btnAddJalan" type="button" data-tooltip="Tambah Jalan (Polyline)">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m4 20 4-4 8 4 4-16"/></svg>
|
|
</button>
|
|
<button class="toolbar-btn" id="btnAddParsil" type="button" data-tooltip="Tambah Parsil (Polygon)">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 14 8 4l13 3-3 12-10 2Z"/></svg>
|
|
</button>
|
|
<button class="toolbar-btn" id="btnAddParsilCircle" type="button" data-tooltip="Parsil Lingkaran">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/></svg>
|
|
</button>
|
|
<button class="toolbar-btn" id="btnAddParsilEllipse" type="button" data-tooltip="Parsil Ellipse">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2"/></svg>
|
|
</button>
|
|
<button class="toolbar-btn danger-btn" id="btnStopTool" type="button" data-tooltip="Batal / Stop Tool">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Bottom-Right Spatial Toggle Modes Floating Toolbar -->
|
|
<div class="floating-toolbar" id="controls-toolbar">
|
|
<button class="control-btn" id="btnEnableEdit" type="button">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>
|
|
Edit Vertices
|
|
</button>
|
|
<button class="control-btn" id="btnEnableDrag" type="button">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 9-3-3-3 3"/><path d="M12 6v12"/><path d="m9 15 3 3 3-3"/><path d="m15 12 3-3 3 3"/><path d="M18 12H6"/><path d="m9 12-3-3-3 3"/></svg>
|
|
Drag
|
|
</button>
|
|
<button class="control-btn" id="btnDisableModes" type="button">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m4.9 4.9 14.2 14.2"/></svg>
|
|
Lock Modes
|
|
</button>
|
|
<button class="control-btn" id="btnRefreshAll" type="button">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/><path d="M16 16h5v5"/></svg>
|
|
Refresh
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Map Target -->
|
|
<div id="map"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dialog overlay -->
|
|
<div id="modalOverlay" onclick="closeModal()"></div>
|
|
|
|
<!-- Sleek SPBU Edit Modal -->
|
|
<div id="editModal">
|
|
<h3>Edit SPBU</h3>
|
|
<form id="editForm" onsubmit="updateData(event)">
|
|
<input type="hidden" id="editId">
|
|
<div class="form-group">
|
|
<label for="editNama">Nama SPBU</label>
|
|
<input type="text" id="editNama" required autocomplete="off">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="editNomor">Nomor SPBU</label>
|
|
<input type="text" id="editNomor" required autocomplete="off">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="editLat">Latitude</label>
|
|
<input type="text" id="editLat" readonly>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="editLng">Longitude</label>
|
|
<input type="text" id="editLng" readonly>
|
|
</div>
|
|
<div class="form-info">
|
|
Latitude & Longitude are read-only. Gunakan Drag Mode di toolbar kanan bawah untuk memindahkan marker di peta.
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="editStatus">Status Operasional</label>
|
|
<select id="editStatus" required>
|
|
<option value="Buka 24 Jam">Buka 24 Jam</option>
|
|
<option value="Tidak">Tidak</option>
|
|
</select>
|
|
</div>
|
|
<div class="btn-group">
|
|
<button type="button" class="modal-btn" onclick="closeModal()">Batal</button>
|
|
<button type="submit" class="modal-btn primary">Simpan</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Sleek Shape (Jalan / Parsil) Dialog Modal -->
|
|
<div id="shapeModal">
|
|
<h3 id="shapeModalTitle"></h3>
|
|
<form id="shapeForm" onsubmit="submitShape(event)">
|
|
<input type="hidden" id="shapeAction" value="create">
|
|
<input type="hidden" id="shapeKind" value="">
|
|
<input type="hidden" id="shapeId" value="">
|
|
|
|
<div class="form-group">
|
|
<label id="shapeNameLabel" for="shapeName"></label>
|
|
<input type="text" id="shapeName" required autocomplete="off">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label id="shapeStatusLabel" for="shapeStatus"></label>
|
|
<select id="shapeStatus" required></select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label id="shapeMetricLabel" for="shapeMetric"></label>
|
|
<input type="text" id="shapeMetric" readonly>
|
|
</div>
|
|
<div class="form-info">
|
|
Nilai kalkulasi di atas dihitung otomatis oleh peta dari hasil geometri Anda.
|
|
</div>
|
|
|
|
<div class="btn-group">
|
|
<button type="button" class="modal-btn" onclick="closeModal()">Batal</button>
|
|
<button type="button" id="shapeDeleteBtn" class="modal-btn danger" onclick="deleteShape()" style="display:none;">Hapus</button>
|
|
<button type="submit" id="shapeSubmitBtn" class="modal-btn primary">Simpan</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<script>
|
|
// Simple and robust modular tab switcher globally defined before other code
|
|
window.switchTab = function(paneId, btn) {
|
|
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
|
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
document.getElementById(paneId).classList.add('active');
|
|
btn.classList.add('active');
|
|
};
|
|
</script>
|
|
|
|
|
|
<script>
|
|
const map = L.map('map').setView([-0.055, 109.34], 13);
|
|
|
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
maxZoom: 19,
|
|
attribution: '© OpenStreetMap'
|
|
}).addTo(map);
|
|
|
|
const popup = L.popup();
|
|
|
|
// Define Green and Red Icons
|
|
const greenIcon = new L.Icon({
|
|
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
|
|
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
|
|
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
|
|
});
|
|
|
|
const redIcon = new L.Icon({
|
|
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
|
|
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
|
|
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
|
|
});
|
|
|
|
const markersLayer = L.layerGroup().addTo(map);
|
|
const roadsLayer = L.layerGroup().addTo(map);
|
|
const parcelsLayer = L.layerGroup().addTo(map);
|
|
|
|
// Sidebar state
|
|
const spbuMarkersById = new Map();
|
|
const roadLayersById = new Map();
|
|
const parcelLayersById = new Map();
|
|
let lastSpbuRows = [];
|
|
let lastRoadRows = [];
|
|
let lastParcelRows = [];
|
|
|
|
function setToolStatus(text) {
|
|
const el = document.getElementById('toolStatusText');
|
|
const pill = document.getElementById('status-pill');
|
|
if (el) el.textContent = 'Status: ' + text;
|
|
if (pill) {
|
|
// Show pill only when tool status is not "Siap." or general feedback
|
|
if (text && text !== 'Siap.' && text !== 'Mode dimatikan.' && text !== 'Data diperbarui.' && text !== 'Gagal refresh (cek console).') {
|
|
pill.classList.add('active');
|
|
} else {
|
|
pill.classList.remove('active');
|
|
}
|
|
}
|
|
}
|
|
|
|
function safeDisableAllTools() {
|
|
try { map.pm.disableDraw(); } catch {}
|
|
try { map.pm.disableGlobalEditMode(); } catch {}
|
|
try { map.pm.disableGlobalDragMode(); } catch {}
|
|
|
|
// Remove active classes on toolbar buttons
|
|
document.querySelectorAll('.toolbar-btn').forEach(btn => btn.classList.remove('active'));
|
|
document.querySelectorAll('.control-btn').forEach(btn => btn.classList.remove('active'));
|
|
}
|
|
|
|
function renderSidebarLists() {
|
|
// Update Tab Counts
|
|
const spbuCountEl = document.getElementById('spbuCount');
|
|
const jalanCountEl = document.getElementById('jalanCount');
|
|
const parsilCountEl = document.getElementById('parsilCount');
|
|
if (spbuCountEl) spbuCountEl.textContent = String(lastSpbuRows.length);
|
|
if (jalanCountEl) jalanCountEl.textContent = String(lastRoadRows.length);
|
|
if (parsilCountEl) parsilCountEl.textContent = String(lastParcelRows.length);
|
|
|
|
// Lists
|
|
const spbuList = document.getElementById('spbuList');
|
|
const jalanList = document.getElementById('jalanList');
|
|
const parsilList = document.getElementById('parsilList');
|
|
|
|
const mkItem = (label, sublabel, status, kind, onClick) => {
|
|
const li = document.createElement('li');
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'side-item';
|
|
|
|
// Color-coded status dot indicator
|
|
const dot = document.createElement('span');
|
|
dot.className = 'status-dot';
|
|
|
|
let color = '#64748b'; // default slate
|
|
if (kind === 'spbu') {
|
|
color = (status === 'Buka 24 Jam') ? '#10b981' : '#ef4444';
|
|
} else if (kind === 'jalan') {
|
|
if (status === 'Jalan Nasional') color = '#ef4444';
|
|
else if (status === 'Jalan Provinsi') color = '#10b981';
|
|
else if (status === 'Jalan Kabupaten') color = '#3b82f6';
|
|
} else if (kind === 'parsil') {
|
|
if (status === 'SHM') color = '#f59e0b';
|
|
else if (status === 'HGB') color = '#8b5cf6';
|
|
else if (status === 'HGU') color = '#f97316';
|
|
else if (status === 'HP') color = '#6b7280';
|
|
}
|
|
dot.style.backgroundColor = color;
|
|
btn.appendChild(dot);
|
|
|
|
const textWrap = document.createElement('div');
|
|
textWrap.className = 'text-wrap';
|
|
|
|
const titleSpan = document.createElement('span');
|
|
titleSpan.className = 'item-title';
|
|
titleSpan.textContent = label;
|
|
textWrap.appendChild(titleSpan);
|
|
|
|
if (sublabel) {
|
|
const descSpan = document.createElement('span');
|
|
descSpan.className = 'item-desc';
|
|
descSpan.textContent = sublabel;
|
|
textWrap.appendChild(descSpan);
|
|
}
|
|
|
|
btn.appendChild(textWrap);
|
|
btn.addEventListener('click', onClick);
|
|
li.appendChild(btn);
|
|
return li;
|
|
};
|
|
|
|
spbuList.innerHTML = '';
|
|
lastSpbuRows.slice(0, 50).forEach(spbu => {
|
|
spbuList.appendChild(mkItem(
|
|
spbu.nama || ('SPBU #' + spbu.id),
|
|
'Nomor: ' + (spbu.nomor ?? '-') + ' • ' + (spbu.status ?? '-'),
|
|
spbu.status,
|
|
'spbu',
|
|
() => {
|
|
const marker = spbuMarkersById.get(spbu.id);
|
|
if (!marker) return;
|
|
map.setView(marker.getLatLng(), Math.max(map.getZoom(), 16));
|
|
marker.openPopup();
|
|
}
|
|
));
|
|
});
|
|
|
|
jalanList.innerHTML = '';
|
|
lastRoadRows.slice(0, 50).forEach(row => {
|
|
jalanList.appendChild(mkItem(
|
|
row.nama_jalan || ('Jalan #' + row.id),
|
|
(row.status_jalan ?? '-') + ' • ' + Number(row.panjang_meter || 0).toFixed(2) + ' m',
|
|
row.status_jalan,
|
|
'jalan',
|
|
() => {
|
|
const layer = roadLayersById.get(row.id);
|
|
if (!layer) return;
|
|
try { map.fitBounds(layer.getBounds(), { padding: [20, 20] }); } catch {}
|
|
try { layer.openPopup(); } catch {}
|
|
}
|
|
));
|
|
});
|
|
|
|
parsilList.innerHTML = '';
|
|
lastParcelRows.slice(0, 50).forEach(row => {
|
|
parsilList.appendChild(mkItem(
|
|
row.nama_pemilik || ('Parsil #' + row.id),
|
|
(row.status_kepemilikan ?? '-') + ' • ' + Number(row.luas_m2 || 0).toFixed(2) + ' m²',
|
|
row.status_kepemilikan,
|
|
'parsil',
|
|
() => {
|
|
const layer = parcelLayersById.get(row.id);
|
|
if (!layer) return;
|
|
try { map.fitBounds(layer.getBounds(), { padding: [20, 20] }); } catch {}
|
|
try { layer.openPopup(); } catch {}
|
|
}
|
|
));
|
|
});
|
|
}
|
|
|
|
let isDrawing = false;
|
|
let activeShapeContext = null; // { action, kind, layer, id, saved }
|
|
let activeSpbuMarker = null; // marker instance when editing SPBU
|
|
let activeSpbuCreateMarker = null; // temp marker created from Draw Marker until saved
|
|
|
|
const SHAPE_STATUS_OPTIONS = {
|
|
jalan: ['Jalan Nasional', 'Jalan Provinsi', 'Jalan Kabupaten'],
|
|
parsil: ['SHM', 'HGB', 'HGU', 'HP']
|
|
};
|
|
|
|
function roadColor(status) {
|
|
if (status === 'Jalan Nasional') return '#FF0000';
|
|
if (status === 'Jalan Provinsi') return '#00FF00';
|
|
if (status === 'Jalan Kabupaten') return '#0000FF';
|
|
return '#666666';
|
|
}
|
|
|
|
function parcelFillColor(status) {
|
|
if (status === 'SHM') return '#FFD700';
|
|
if (status === 'HGB') return '#800080';
|
|
if (status === 'HGU') return '#FFA500';
|
|
if (status === 'HP') return '#808080';
|
|
return '#666666';
|
|
}
|
|
|
|
function applyShapeStyle(layer, kind, status) {
|
|
if (!layer || typeof layer.setStyle !== 'function') return;
|
|
if (kind === 'jalan') {
|
|
layer.setStyle({ color: roadColor(status), weight: 4 });
|
|
} else {
|
|
layer.setStyle({ color: '#333333', weight: 2, fillColor: parcelFillColor(status), fillOpacity: 0.45 });
|
|
}
|
|
}
|
|
|
|
function calcPolylineLengthMeters(latlngs) {
|
|
if (!Array.isArray(latlngs) || latlngs.length < 2) return 0;
|
|
let total = 0;
|
|
for (let i = 1; i < latlngs.length; i++) {
|
|
total += map.distance(latlngs[i - 1], latlngs[i]);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
function calcPolygonAreaM2(layer) {
|
|
try {
|
|
const gj = layer.toGeoJSON();
|
|
return turf.area(gj);
|
|
} catch (e) {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
function updateShapeMetricFromLayer(layer, kind) {
|
|
let value = 0;
|
|
if (kind === 'jalan') {
|
|
value = calcPolylineLengthMeters(layer.getLatLngs());
|
|
} else {
|
|
value = calcPolygonAreaM2(layer);
|
|
}
|
|
document.getElementById('shapeMetric').value = value.toFixed(2);
|
|
return value;
|
|
}
|
|
|
|
function generateEllipseLatLngs(centerLatLng, radiusXMeters, radiusYMeters, steps = 72) {
|
|
const lat0 = centerLatLng.lat;
|
|
const lng0 = centerLatLng.lng;
|
|
const lat0Rad = lat0 * Math.PI / 180;
|
|
const R = 6378137; // WGS84 (meters)
|
|
|
|
const points = [];
|
|
for (let i = 0; i < steps; i++) {
|
|
const t = (i / steps) * (Math.PI * 2);
|
|
const dx = radiusXMeters * Math.cos(t);
|
|
const dy = radiusYMeters * Math.sin(t);
|
|
|
|
const dLat = (dy / R) * (180 / Math.PI);
|
|
const dLng = (dx / (R * Math.cos(lat0Rad))) * (180 / Math.PI);
|
|
points.push([lat0 + dLat, lng0 + dLng]);
|
|
}
|
|
return points;
|
|
}
|
|
|
|
function setShapeStatusOptions(kind, selected) {
|
|
const select = document.getElementById('shapeStatus');
|
|
select.innerHTML = '';
|
|
(SHAPE_STATUS_OPTIONS[kind] || []).forEach(val => {
|
|
const opt = document.createElement('option');
|
|
opt.value = val;
|
|
opt.textContent = val;
|
|
select.appendChild(opt);
|
|
});
|
|
if (selected) select.value = selected;
|
|
}
|
|
|
|
function isShapeModalOpen() {
|
|
return document.getElementById('shapeModal').style.display === 'block';
|
|
}
|
|
|
|
function wireSpbuMarkerEvents(marker) {
|
|
marker.on('dragend', function() {
|
|
const spbu = marker.__spbu;
|
|
if (!spbu || !spbu.id) return;
|
|
|
|
const ll = marker.getLatLng();
|
|
spbu.Latitude = ll.lat.toFixed(8);
|
|
spbu.Longitude = ll.lng.toFixed(8);
|
|
|
|
if (activeSpbuMarker === marker && document.getElementById('editModal').style.display === 'block') {
|
|
document.getElementById('editLat').value = spbu.Latitude;
|
|
document.getElementById('editLng').value = spbu.Longitude;
|
|
}
|
|
|
|
let nama = spbu.nama;
|
|
let nomor = spbu.nomor;
|
|
let status = spbu.status;
|
|
|
|
// If we're editing this marker, prefer the live values from the modal
|
|
if (activeSpbuMarker === marker && document.getElementById('editModal').style.display === 'block') {
|
|
nama = document.getElementById('editNama').value;
|
|
nomor = document.getElementById('editNomor').value;
|
|
status = document.getElementById('editStatus').value;
|
|
}
|
|
|
|
spbu.nama = nama;
|
|
spbu.nomor = nomor;
|
|
spbu.status = status;
|
|
|
|
const formData = new FormData();
|
|
formData.append('id', spbu.id);
|
|
formData.append('nama', nama);
|
|
formData.append('nomor', nomor);
|
|
formData.append('status', status);
|
|
formData.append('latitude', spbu.Latitude);
|
|
formData.append('longitude', spbu.Longitude);
|
|
|
|
fetch('update_spbu.php', { method: 'POST', body: formData })
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.status !== 'success') {
|
|
console.error('Failed to update SPBU location:', data);
|
|
}
|
|
})
|
|
.catch(err => console.error('Error updating SPBU location:', err));
|
|
});
|
|
}
|
|
|
|
function wireRoadLayerEvents(layer) {
|
|
const save = () => {
|
|
if (isShapeModalOpen() && activeShapeContext && activeShapeContext.layer === layer) return;
|
|
const meta = layer.__shape;
|
|
if (!meta || !meta.id) return;
|
|
|
|
const length = calcPolylineLengthMeters(layer.getLatLngs());
|
|
const geometry = layer.toGeoJSON().geometry;
|
|
|
|
const formData = new FormData();
|
|
formData.append('id', meta.id);
|
|
formData.append('nama_jalan', meta.nama);
|
|
formData.append('status_jalan', meta.status);
|
|
formData.append('panjang_meter', length);
|
|
formData.append('koordinat', JSON.stringify(geometry));
|
|
|
|
fetch('update_jalan.php', { method: 'POST', body: formData })
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.status !== 'success') {
|
|
console.error('Failed to update jalan geometry:', data);
|
|
}
|
|
})
|
|
.catch(err => console.error('Error updating jalan:', err));
|
|
};
|
|
|
|
layer.on('pm:update', save);
|
|
layer.on('pm:dragend', save);
|
|
}
|
|
|
|
function wireParcelLayerEvents(layer) {
|
|
const save = () => {
|
|
if (isShapeModalOpen() && activeShapeContext && activeShapeContext.layer === layer) return;
|
|
const meta = layer.__shape;
|
|
if (!meta || !meta.id) return;
|
|
|
|
const area = calcPolygonAreaM2(layer);
|
|
const geometry = layer.toGeoJSON().geometry;
|
|
|
|
const formData = new FormData();
|
|
formData.append('id', meta.id);
|
|
formData.append('nama_pemilik', meta.nama);
|
|
formData.append('status_kepemilikan', meta.status);
|
|
formData.append('luas_m2', area);
|
|
formData.append('koordinat', JSON.stringify(geometry));
|
|
|
|
fetch('update_parsil.php', { method: 'POST', body: formData })
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.status !== 'success') {
|
|
console.error('Failed to update parsil geometry:', data);
|
|
}
|
|
})
|
|
.catch(err => console.error('Error updating parsil:', err));
|
|
};
|
|
|
|
layer.on('pm:update', save);
|
|
layer.on('pm:dragend', save);
|
|
}
|
|
|
|
function refreshMarkers() {
|
|
return fetch('get_spbu.php')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
markersLayer.clearLayers();
|
|
spbuMarkersById.clear();
|
|
lastSpbuRows = Array.isArray(data) ? data : [];
|
|
|
|
lastSpbuRows.forEach(spbu => {
|
|
const markerIcon = (spbu.status === 'Buka 24 Jam') ? greenIcon : redIcon;
|
|
const lat = spbu.Latitude;
|
|
const lng = spbu.Longitude;
|
|
|
|
const popupContent = `
|
|
<b>${spbu.nama}</b><br>
|
|
Nomor: ${spbu.nomor}<br>
|
|
Buka: ${spbu.status}<br>
|
|
Latitude: ${lat}<br>
|
|
Longitude: ${lng}<br>
|
|
<hr style="margin: 8px 0;">
|
|
<button type="button" class="btn-action btn-edit js-edit">Edit</button>
|
|
<button type="button" class="btn-action btn-delete js-delete">Delete</button>
|
|
`;
|
|
|
|
const marker = L.marker([lat, lng], { icon: markerIcon });
|
|
marker.__spbu = spbu;
|
|
wireSpbuMarkerEvents(marker);
|
|
marker.bindPopup(popupContent);
|
|
marker.addTo(markersLayer);
|
|
|
|
if (spbu && spbu.id != null) spbuMarkersById.set(spbu.id, marker);
|
|
});
|
|
|
|
renderSidebarLists();
|
|
})
|
|
.catch(error => console.error('Error fetching data:', error));
|
|
}
|
|
|
|
function refreshRoads() {
|
|
return fetch('get_jalan.php')
|
|
.then(r => r.json())
|
|
.then(rows => {
|
|
roadsLayer.clearLayers();
|
|
roadLayersById.clear();
|
|
lastRoadRows = Array.isArray(rows) ? rows : [];
|
|
lastRoadRows.forEach(row => {
|
|
let geom;
|
|
try {
|
|
geom = JSON.parse(row.koordinat);
|
|
} catch {
|
|
return;
|
|
}
|
|
const gj = L.geoJSON(geom, {
|
|
style: { color: roadColor(row.status_jalan), weight: 4 }
|
|
});
|
|
|
|
let firstLayer = null;
|
|
gj.eachLayer(l => {
|
|
if (!firstLayer) firstLayer = l;
|
|
l.__shape = {
|
|
kind: 'jalan',
|
|
id: row.id,
|
|
nama: row.nama_jalan,
|
|
status: row.status_jalan
|
|
};
|
|
l.bindPopup('<div>Loading...</div>');
|
|
wireRoadLayerEvents(l);
|
|
});
|
|
gj.addTo(roadsLayer);
|
|
|
|
if (row && row.id != null && firstLayer) roadLayersById.set(row.id, firstLayer);
|
|
});
|
|
|
|
renderSidebarLists();
|
|
})
|
|
.catch(err => console.error('Error fetching jalan:', err));
|
|
}
|
|
|
|
function refreshParcels() {
|
|
return fetch('get_parsil.php')
|
|
.then(r => r.json())
|
|
.then(rows => {
|
|
parcelsLayer.clearLayers();
|
|
parcelLayersById.clear();
|
|
lastParcelRows = Array.isArray(rows) ? rows : [];
|
|
lastParcelRows.forEach(row => {
|
|
let geom;
|
|
try {
|
|
geom = JSON.parse(row.koordinat);
|
|
} catch {
|
|
return;
|
|
}
|
|
const gj = L.geoJSON(geom, {
|
|
style: { color: '#333333', weight: 2, fillColor: parcelFillColor(row.status_kepemilikan), fillOpacity: 0.45 }
|
|
});
|
|
|
|
let firstLayer = null;
|
|
gj.eachLayer(l => {
|
|
if (!firstLayer) firstLayer = l;
|
|
l.__shape = {
|
|
kind: 'parsil',
|
|
id: row.id,
|
|
nama: row.nama_pemilik,
|
|
status: row.status_kepemilikan
|
|
};
|
|
l.bindPopup('<div>Loading...</div>');
|
|
wireParcelLayerEvents(l);
|
|
});
|
|
gj.addTo(parcelsLayer);
|
|
|
|
if (row && row.id != null && firstLayer) parcelLayersById.set(row.id, firstLayer);
|
|
});
|
|
|
|
renderSidebarLists();
|
|
})
|
|
.catch(err => console.error('Error fetching parsil:', err));
|
|
}
|
|
|
|
function deleteShapeById(kind, id) {
|
|
if (!confirm('Are you sure you want to delete this shape?')) return;
|
|
|
|
const formData = new FormData();
|
|
formData.append('id', id);
|
|
|
|
const url = (kind === 'jalan') ? 'delete_jalan.php' : 'delete_parsil.php';
|
|
fetch(url, { method: 'POST', body: formData })
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.status !== 'success') {
|
|
console.error('Delete failed:', data);
|
|
return;
|
|
}
|
|
map.closePopup();
|
|
return (kind === 'jalan') ? refreshRoads() : refreshParcels();
|
|
})
|
|
.catch(err => console.error('Error deleting shape:', err));
|
|
}
|
|
|
|
// Wire popup buttons (avoids inline onclick bugs)
|
|
map.on('popupopen', function(e) {
|
|
const source = e.popup._source;
|
|
if (!source) return;
|
|
|
|
const el = e.popup.getElement();
|
|
if (!el) return;
|
|
|
|
// SPBU markers
|
|
const spbu = source.__spbu;
|
|
if (spbu) {
|
|
const editBtn = el.querySelector('.js-edit');
|
|
const deleteBtn = el.querySelector('.js-delete');
|
|
|
|
if (editBtn) {
|
|
editBtn.addEventListener('click', function(ev) {
|
|
ev.preventDefault();
|
|
openModal(spbu, source);
|
|
});
|
|
}
|
|
if (deleteBtn) {
|
|
deleteBtn.addEventListener('click', function(ev) {
|
|
ev.preventDefault();
|
|
deleteData(spbu.id);
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Roads/Parcels
|
|
const meta = source.__shape;
|
|
if (!meta || !meta.kind || !meta.id) return;
|
|
|
|
const kind = meta.kind;
|
|
const metric = (kind === 'jalan')
|
|
? calcPolylineLengthMeters(source.getLatLngs())
|
|
: calcPolygonAreaM2(source);
|
|
const metricLabel = (kind === 'jalan') ? 'Panjang (m)' : 'Luas (m²)';
|
|
const statusLabel = (kind === 'jalan') ? 'Status Jalan' : 'Status Kepemilikan';
|
|
|
|
e.popup.setContent(`
|
|
<b>${meta.nama}</b><br>
|
|
${statusLabel}: ${meta.status}<br>
|
|
${metricLabel}: ${metric.toFixed(2)}<br>
|
|
<hr style="margin: 8px 0;">
|
|
<button type="button" class="btn-action btn-edit js-shape-edit">Edit</button>
|
|
<button type="button" class="btn-action btn-delete js-shape-delete">Delete</button>
|
|
`);
|
|
|
|
const el2 = e.popup.getElement();
|
|
if (!el2) return;
|
|
|
|
const editBtn2 = el2.querySelector('.js-shape-edit');
|
|
const deleteBtn2 = el2.querySelector('.js-shape-delete');
|
|
|
|
if (editBtn2) {
|
|
editBtn2.addEventListener('click', function(ev) {
|
|
ev.preventDefault();
|
|
openShapeModal({ action: 'update', kind, layer: source, id: meta.id });
|
|
});
|
|
}
|
|
if (deleteBtn2) {
|
|
deleteBtn2.addEventListener('click', function(ev) {
|
|
ev.preventDefault();
|
|
deleteShapeById(kind, meta.id);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Leaflet-Geoman controls are disabled/hidden as we use our custom top-right & bottom-right floating toolbars
|
|
map.pm.addControls({
|
|
drawMarker: false,
|
|
drawCircle: false,
|
|
drawPolyline: false,
|
|
drawRectangle: false,
|
|
drawPolygon: false,
|
|
editMode: false,
|
|
dragMode: false,
|
|
cutPolygon: false,
|
|
removalMode: false,
|
|
rotateMode: false,
|
|
oneBlock: false
|
|
});
|
|
|
|
// Make snapping less aggressive (default snapDistance is 20px)
|
|
// - snapSegment=false prevents snapping to the middle of lines (common cause of “always snaps onto polyline”)
|
|
// - snapVertex=true keeps vertex-to-vertex snapping available
|
|
map.pm.setGlobalOptions({
|
|
snappable: true,
|
|
snapDistance: 8,
|
|
snapMiddle: false,
|
|
snapSegment: false,
|
|
snapVertex: true
|
|
});
|
|
map.pm.applyGlobalOptions();
|
|
|
|
// If the "Add SPBU" popup is closed without saving, remove the temp marker.
|
|
map.on('popupclose', function(e) {
|
|
if (!e || e.popup !== popup) return;
|
|
if (!activeSpbuCreateMarker) return;
|
|
try { map.removeLayer(activeSpbuCreateMarker); } catch {}
|
|
try { markersLayer.removeLayer(activeSpbuCreateMarker); } catch {}
|
|
activeSpbuCreateMarker = null;
|
|
});
|
|
|
|
map.on('pm:drawstart', () => {
|
|
isDrawing = true;
|
|
map.closePopup();
|
|
});
|
|
map.on('pm:drawend', () => {
|
|
isDrawing = false;
|
|
});
|
|
|
|
map.on('pm:create', (e) => {
|
|
const { layer, shape } = e;
|
|
const isCircle = (shape === 'Circle') || (layer instanceof L.Circle);
|
|
const isRectangle = (shape === 'Rectangle') || (layer instanceof L.Rectangle);
|
|
const isLine = (shape === 'Line') || (layer instanceof L.Polyline && !(layer instanceof L.Polygon));
|
|
const isPolygon = (shape === 'Polygon') || (layer instanceof L.Polygon && !(layer instanceof L.Rectangle));
|
|
const isMarker = (shape === 'Marker') || (layer instanceof L.Marker);
|
|
|
|
if (!isLine && !isPolygon && !isMarker && !isCircle && !isRectangle) return;
|
|
|
|
map.closePopup();
|
|
|
|
if (isLine) {
|
|
roadsLayer.addLayer(layer);
|
|
openShapeModal({ action: 'create', kind: 'jalan', layer, id: null });
|
|
}
|
|
if (isPolygon) {
|
|
parcelsLayer.addLayer(layer);
|
|
openShapeModal({ action: 'create', kind: 'parsil', layer, id: null });
|
|
}
|
|
if (isCircle) {
|
|
// Convert circle -> polygon (stored as Polygon GeoJSON)
|
|
const center = layer.getLatLng();
|
|
const r = layer.getRadius();
|
|
const ring = generateEllipseLatLngs(center, r, r, 72);
|
|
|
|
const poly = L.polygon(ring);
|
|
parcelsLayer.addLayer(poly);
|
|
try { map.removeLayer(layer); } catch {}
|
|
|
|
openShapeModal({ action: 'create', kind: 'parsil', layer: poly, id: null });
|
|
}
|
|
if (isRectangle) {
|
|
// Draw rectangle to define an ellipse, then convert -> polygon ellipse
|
|
const bounds = layer.getBounds();
|
|
const center = bounds.getCenter();
|
|
|
|
const west = bounds.getWest();
|
|
const east = bounds.getEast();
|
|
const north = bounds.getNorth();
|
|
const south = bounds.getSouth();
|
|
|
|
const widthMeters = map.distance([center.lat, west], [center.lat, east]);
|
|
const heightMeters = map.distance([south, center.lng], [north, center.lng]);
|
|
|
|
const rx = Math.max(0, widthMeters / 2);
|
|
const ry = Math.max(0, heightMeters / 2);
|
|
const ring = generateEllipseLatLngs(center, rx, ry, 72);
|
|
|
|
const poly = L.polygon(ring);
|
|
parcelsLayer.addLayer(poly);
|
|
try { map.removeLayer(layer); } catch {}
|
|
|
|
openShapeModal({ action: 'create', kind: 'parsil', layer: poly, id: null });
|
|
}
|
|
if (isMarker) {
|
|
// Geoman Draw Marker == Add SPBU workflow
|
|
activeSpbuCreateMarker = layer;
|
|
const ll = layer.getLatLng();
|
|
const lat = ll.lat.toFixed(8);
|
|
const lng = ll.lng.toFixed(8);
|
|
|
|
const formHTML = `
|
|
<form id="spbuForm" onsubmit="saveData(event)">
|
|
<h4 style="margin: 0 0 6px 0;">Tambah SPBU</h4>
|
|
<div style="font-size:12px; color:#6c757d; margin-bottom:8px;">Isi data berikut, lalu klik Save.</div>
|
|
|
|
<label style="font-size:12px; color:#495057;">Nama SPBU</label>
|
|
<input type="text" id="nama" required>
|
|
|
|
<label style="font-size:12px; color:#495057;">Nomor SPBU</label>
|
|
<input type="text" id="nomor" required>
|
|
|
|
<label style="font-size:12px; color:#495057;">Status</label>
|
|
<select id="status" required>
|
|
<option value="Buka 24 Jam">Buka 24 Jam</option>
|
|
<option value="Tidak">Tidak</option>
|
|
</select>
|
|
<div>Latitude: ${lat}</div>
|
|
<div>Longitude: ${lng}</div>
|
|
<input type="hidden" id="lat" value="${lat}">
|
|
<input type="hidden" id="lng" value="${lng}">
|
|
<button type="submit" style="background:#28a745; color:white; border:none; padding:5px;">Save</button>
|
|
</form>
|
|
`;
|
|
|
|
popup.setLatLng(ll).setContent(formHTML).openOn(map);
|
|
|
|
// Single-click add behavior (disable draw after placing one marker)
|
|
try { map.pm.disableDraw('Marker'); } catch {}
|
|
}
|
|
});
|
|
|
|
map.on('pm:edit', (e) => {
|
|
if (!activeShapeContext || !activeShapeContext.layer) return;
|
|
if (e && e.layer && e.layer !== activeShapeContext.layer) return;
|
|
updateShapeMetricFromLayer(activeShapeContext.layer, activeShapeContext.kind);
|
|
});
|
|
|
|
map.on('pm:update', (e) => {
|
|
if (!activeShapeContext || !activeShapeContext.layer) return;
|
|
if (e && e.layer && e.layer !== activeShapeContext.layer) return;
|
|
updateShapeMetricFromLayer(activeShapeContext.layer, activeShapeContext.kind);
|
|
});
|
|
|
|
// Save (create)
|
|
function saveData(event) {
|
|
event.preventDefault();
|
|
|
|
const formData = new FormData();
|
|
formData.append('nama', document.getElementById('nama').value);
|
|
formData.append('nomor', document.getElementById('nomor').value);
|
|
formData.append('status', document.getElementById('status').value);
|
|
formData.append('latitude', document.getElementById('lat').value);
|
|
formData.append('longitude', document.getElementById('lng').value);
|
|
|
|
fetch('save_spbu.php', { method: 'POST', body: formData })
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
map.closePopup();
|
|
if (activeSpbuCreateMarker) {
|
|
try { map.removeLayer(activeSpbuCreateMarker); } catch {}
|
|
try { markersLayer.removeLayer(activeSpbuCreateMarker); } catch {}
|
|
activeSpbuCreateMarker = null;
|
|
}
|
|
return refreshMarkers();
|
|
}
|
|
console.error('Save failed:', data);
|
|
})
|
|
.catch(error => console.error('Error:', error));
|
|
}
|
|
|
|
function setActiveDrawButton(btnId) {
|
|
document.querySelectorAll('.toolbar-btn').forEach(btn => btn.classList.remove('active'));
|
|
if (btnId) {
|
|
const activeBtn = document.getElementById(btnId);
|
|
if (activeBtn) activeBtn.classList.add('active');
|
|
}
|
|
}
|
|
|
|
function setActiveControlButton(btnId) {
|
|
document.querySelectorAll('.control-btn').forEach(btn => btn.classList.remove('active'));
|
|
if (btnId) {
|
|
const activeBtn = document.getElementById(btnId);
|
|
if (activeBtn) activeBtn.classList.add('active');
|
|
}
|
|
}
|
|
|
|
// Custom Toolbar button wiring
|
|
document.getElementById('btnAddSpbu').addEventListener('click', () => {
|
|
safeDisableAllTools();
|
|
setActiveDrawButton('btnAddSpbu');
|
|
try { map.pm.enableDraw('Marker'); } catch {}
|
|
setToolStatus('Tambah SPBU: klik di peta untuk meletakkan marker.');
|
|
});
|
|
document.getElementById('btnAddJalan').addEventListener('click', () => {
|
|
safeDisableAllTools();
|
|
setActiveDrawButton('btnAddJalan');
|
|
try { map.pm.enableDraw('Line'); } catch {}
|
|
setToolStatus('Tambah Jalan: klik beberapa titik, lalu double-click untuk selesai.');
|
|
});
|
|
document.getElementById('btnAddParsil').addEventListener('click', () => {
|
|
safeDisableAllTools();
|
|
setActiveDrawButton('btnAddParsil');
|
|
try { map.pm.enableDraw('Polygon'); } catch {}
|
|
setToolStatus('Tambah Parsil: klik beberapa titik, lalu click titik awal untuk selesai.');
|
|
});
|
|
document.getElementById('btnAddParsilCircle').addEventListener('click', () => {
|
|
safeDisableAllTools();
|
|
setActiveDrawButton('btnAddParsilCircle');
|
|
try { map.pm.enableDraw('Circle'); } catch {}
|
|
setToolStatus('Parsil Lingkaran: klik-drag untuk ukuran. Akan disimpan sebagai polygon.');
|
|
});
|
|
document.getElementById('btnAddParsilEllipse').addEventListener('click', () => {
|
|
safeDisableAllTools();
|
|
setActiveDrawButton('btnAddParsilEllipse');
|
|
try { map.pm.enableDraw('Rectangle'); } catch {}
|
|
setToolStatus('Parsil Ellipse: gambar rectangle, akan dikonversi jadi ellipse polygon.');
|
|
});
|
|
document.getElementById('btnStopTool').addEventListener('click', () => {
|
|
safeDisableAllTools();
|
|
setActiveDrawButton(null);
|
|
setToolStatus('Siap.');
|
|
});
|
|
document.getElementById('btnEnableEdit').addEventListener('click', () => {
|
|
safeDisableAllTools();
|
|
setActiveControlButton('btnEnableEdit');
|
|
try { map.pm.enableGlobalEditMode(); } catch {}
|
|
setToolStatus('Edit Mode aktif: klik shape lalu edit vertex.');
|
|
});
|
|
document.getElementById('btnEnableDrag').addEventListener('click', () => {
|
|
safeDisableAllTools();
|
|
setActiveControlButton('btnEnableDrag');
|
|
try { map.pm.enableGlobalDragMode(); } catch {}
|
|
setToolStatus('Drag Mode aktif: geser marker/shape untuk pindah lokasi.');
|
|
});
|
|
document.getElementById('btnDisableModes').addEventListener('click', () => {
|
|
safeDisableAllTools();
|
|
setActiveControlButton('btnDisableModes');
|
|
setToolStatus('Mode dimatikan.');
|
|
});
|
|
document.getElementById('btnRefreshAll').addEventListener('click', () => {
|
|
// Refresh doesn't hold an active mode state on buttons permanently
|
|
Promise.all([refreshMarkers(), refreshRoads(), refreshParcels()])
|
|
.then(() => setToolStatus('Data diperbarui.'))
|
|
.catch(() => setToolStatus('Gagal refresh (cek console).'));
|
|
});
|
|
|
|
// Delete
|
|
function deleteData(id) {
|
|
if (!confirm("Are you sure you want to delete this SPBU?")) return;
|
|
|
|
const formData = new FormData();
|
|
formData.append('id', id);
|
|
|
|
fetch('delete_spbu.php', { method: 'POST', body: formData })
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
map.closePopup();
|
|
return refreshMarkers();
|
|
}
|
|
console.error('Delete failed:', data);
|
|
})
|
|
.catch(error => console.error('Error:', error));
|
|
}
|
|
|
|
// Modal Controllers
|
|
function openModal(spbu, marker) {
|
|
activeSpbuMarker = marker || null;
|
|
document.getElementById('editId').value = spbu.id;
|
|
document.getElementById('editNama').value = spbu.nama;
|
|
document.getElementById('editNomor').value = spbu.nomor;
|
|
document.getElementById('editStatus').value = spbu.status;
|
|
document.getElementById('editLat').value = spbu.Latitude;
|
|
document.getElementById('editLng').value = spbu.Longitude;
|
|
|
|
if (activeSpbuMarker && activeSpbuMarker.dragging) {
|
|
try { activeSpbuMarker.dragging.enable(); } catch {}
|
|
}
|
|
|
|
document.getElementById('modalOverlay').classList.add('active');
|
|
document.getElementById('editModal').classList.add('active');
|
|
}
|
|
|
|
function closeModal() {
|
|
document.getElementById('modalOverlay').classList.remove('active');
|
|
document.getElementById('editModal').classList.remove('active');
|
|
|
|
if (activeSpbuMarker && activeSpbuMarker.dragging) {
|
|
try { activeSpbuMarker.dragging.disable(); } catch {}
|
|
}
|
|
activeSpbuMarker = null;
|
|
|
|
const shapeModal = document.getElementById('shapeModal');
|
|
if (shapeModal && shapeModal.classList.contains('active')) {
|
|
shapeModal.classList.remove('active');
|
|
|
|
if (activeShapeContext && activeShapeContext.layer) {
|
|
try { activeShapeContext.layer.pm && activeShapeContext.layer.pm.disable(); } catch {}
|
|
try { activeShapeContext.layer.pm && activeShapeContext.layer.pm.disableLayerDrag && activeShapeContext.layer.pm.disableLayerDrag(); } catch {}
|
|
if (activeShapeContext.action === 'create' && !activeShapeContext.saved) {
|
|
// User cancelled a newly drawn shape: remove it.
|
|
if (activeShapeContext.kind === 'jalan') roadsLayer.removeLayer(activeShapeContext.layer);
|
|
if (activeShapeContext.kind === 'parsil') parcelsLayer.removeLayer(activeShapeContext.layer);
|
|
}
|
|
}
|
|
activeShapeContext = null;
|
|
}
|
|
}
|
|
|
|
function openShapeModal({ action, kind, layer, id }) {
|
|
// Close SPBU modal if open
|
|
document.getElementById('editModal').classList.remove('active');
|
|
|
|
if (activeSpbuMarker && activeSpbuMarker.dragging) {
|
|
try { activeSpbuMarker.dragging.disable(); } catch {}
|
|
}
|
|
activeSpbuMarker = null;
|
|
|
|
activeShapeContext = { action, kind, layer, id, saved: action === 'update' };
|
|
|
|
document.getElementById('shapeAction').value = action;
|
|
document.getElementById('shapeKind').value = kind;
|
|
document.getElementById('shapeId').value = id ?? '';
|
|
|
|
document.getElementById('shapeModalTitle').textContent = (kind === 'jalan')
|
|
? (action === 'create' ? 'Tambah Jalan' : 'Edit Jalan')
|
|
: (action === 'create' ? 'Tambah Parsil' : 'Edit Parsil');
|
|
|
|
document.getElementById('shapeNameLabel').textContent = (kind === 'jalan') ? 'Nama Jalan' : 'Nama Pemilik';
|
|
document.getElementById('shapeStatusLabel').textContent = (kind === 'jalan') ? 'Status Jalan' : 'Status Kepemilikan';
|
|
document.getElementById('shapeMetricLabel').textContent = (kind === 'jalan') ? 'Panjang (meter)' : 'Luas (m²)';
|
|
|
|
// Prefill fields from layer metadata if available
|
|
const currentName = layer.__shape && layer.__shape.nama ? layer.__shape.nama : '';
|
|
const currentStatus = layer.__shape && layer.__shape.status ? layer.__shape.status : (SHAPE_STATUS_OPTIONS[kind][0] || '');
|
|
|
|
document.getElementById('shapeName').value = currentName;
|
|
setShapeStatusOptions(kind, currentStatus);
|
|
|
|
// Metric is auto calculated from geometry
|
|
updateShapeMetricFromLayer(layer, kind);
|
|
|
|
// Update style live when status changes
|
|
const statusEl = document.getElementById('shapeStatus');
|
|
statusEl.onchange = () => applyShapeStyle(layer, kind, statusEl.value);
|
|
applyShapeStyle(layer, kind, statusEl.value);
|
|
|
|
// Enable vertex editing while modal is open
|
|
try { layer.pm && layer.pm.enable(); } catch {}
|
|
try { layer.pm && layer.pm.enableLayerDrag && layer.pm.enableLayerDrag(); } catch {}
|
|
|
|
// Delete visible only for update
|
|
document.getElementById('shapeDeleteBtn').style.display = (action === 'update') ? 'inline-block' : 'none';
|
|
document.getElementById('shapeSubmitBtn').textContent = (action === 'update') ? 'Update' : 'Save';
|
|
|
|
document.getElementById('modalOverlay').classList.add('active');
|
|
document.getElementById('shapeModal').classList.add('active');
|
|
}
|
|
|
|
function submitShape(event) {
|
|
event.preventDefault();
|
|
if (!activeShapeContext || !activeShapeContext.layer) return;
|
|
|
|
const action = document.getElementById('shapeAction').value;
|
|
const kind = document.getElementById('shapeKind').value;
|
|
const id = document.getElementById('shapeId').value;
|
|
const name = document.getElementById('shapeName').value;
|
|
const status = document.getElementById('shapeStatus').value;
|
|
|
|
const metric = updateShapeMetricFromLayer(activeShapeContext.layer, kind);
|
|
const geometry = activeShapeContext.layer.toGeoJSON().geometry;
|
|
|
|
const formData = new FormData();
|
|
if (kind === 'jalan') {
|
|
formData.append('nama_jalan', name);
|
|
formData.append('status_jalan', status);
|
|
formData.append('panjang_meter', metric);
|
|
} else {
|
|
formData.append('nama_pemilik', name);
|
|
formData.append('status_kepemilikan', status);
|
|
formData.append('luas_m2', metric);
|
|
}
|
|
formData.append('koordinat', JSON.stringify(geometry));
|
|
if (action === 'update') formData.append('id', id);
|
|
|
|
const url = (kind === 'jalan')
|
|
? (action === 'update' ? 'update_jalan.php' : 'save_jalan.php')
|
|
: (action === 'update' ? 'update_parsil.php' : 'save_parsil.php');
|
|
|
|
fetch(url, { method: 'POST', body: formData })
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.status !== 'success') {
|
|
console.error('Shape save/update failed:', data);
|
|
return;
|
|
}
|
|
|
|
// persist metadata locally so immediate re-open works without refresh
|
|
activeShapeContext.layer.__shape = {
|
|
kind,
|
|
id: action === 'update' ? parseInt(id, 10) : (activeShapeContext.layer.__shape && activeShapeContext.layer.__shape.id) || null,
|
|
nama: name,
|
|
status
|
|
};
|
|
applyShapeStyle(activeShapeContext.layer, kind, status);
|
|
|
|
activeShapeContext.saved = true;
|
|
|
|
closeModal();
|
|
return (kind === 'jalan') ? refreshRoads() : refreshParcels();
|
|
})
|
|
.catch(err => console.error('Error submitting shape:', err));
|
|
}
|
|
|
|
function deleteShape() {
|
|
if (!activeShapeContext || !activeShapeContext.layer) return;
|
|
if (activeShapeContext.action !== 'update') return;
|
|
if (!confirm('Are you sure you want to delete this shape?')) return;
|
|
|
|
const kind = activeShapeContext.kind;
|
|
const id = document.getElementById('shapeId').value;
|
|
|
|
const formData = new FormData();
|
|
formData.append('id', id);
|
|
|
|
const url = (kind === 'jalan') ? 'delete_jalan.php' : 'delete_parsil.php';
|
|
fetch(url, { method: 'POST', body: formData })
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.status !== 'success') {
|
|
console.error('Delete failed:', data);
|
|
return;
|
|
}
|
|
closeModal();
|
|
return (kind === 'jalan') ? refreshRoads() : refreshParcels();
|
|
})
|
|
.catch(err => console.error('Error deleting shape:', err));
|
|
}
|
|
|
|
// Update
|
|
function updateData(event) {
|
|
event.preventDefault();
|
|
|
|
// If user dragged the marker, reflect latest location
|
|
if (activeSpbuMarker && activeSpbuMarker.__spbu) {
|
|
document.getElementById('editLat').value = activeSpbuMarker.__spbu.Latitude;
|
|
document.getElementById('editLng').value = activeSpbuMarker.__spbu.Longitude;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('id', document.getElementById('editId').value);
|
|
formData.append('nama', document.getElementById('editNama').value);
|
|
formData.append('nomor', document.getElementById('editNomor').value);
|
|
formData.append('status', document.getElementById('editStatus').value);
|
|
formData.append('latitude', document.getElementById('editLat').value);
|
|
formData.append('longitude', document.getElementById('editLng').value);
|
|
|
|
fetch('update_spbu.php', { method: 'POST', body: formData })
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.status === 'success') {
|
|
closeModal();
|
|
return refreshMarkers();
|
|
}
|
|
console.error('Update failed:', data);
|
|
})
|
|
.catch(error => console.error('Error:', error));
|
|
}
|
|
|
|
// Initial load
|
|
Promise.all([refreshMarkers(), refreshRoads(), refreshParcels()])
|
|
.then(() => setToolStatus('Siap.'));
|
|
</script>
|
|
</body>
|
|
</html> |