1413 lines
67 KiB
PHP
1413 lines
67 KiB
PHP
<!DOCTYPE html>
|
||
<html lang="en" class="dark">
|
||
<head>
|
||
<meta charset="utf-8"/>
|
||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||
<title>WebGIS Dashboard</title>
|
||
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
||
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=Inter:wght@400;500;600&display=swap" rel="stylesheet"/>
|
||
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
|
||
<script id="tailwind-config">
|
||
tailwind.config = {
|
||
darkMode: "class",
|
||
theme: {
|
||
extend: {
|
||
colors: {
|
||
"on-surface": "#dae2fd",
|
||
"outline": "#8d90a2",
|
||
"on-primary-container": "#dfe3ff",
|
||
"inverse-on-surface": "#283044",
|
||
"primary": "#b7c4ff",
|
||
"surface-container": "#171f33",
|
||
"on-tertiary-fixed-variant": "#5a00c6",
|
||
"on-secondary": "#263143",
|
||
"tertiary-fixed": "#eaddff",
|
||
"on-error": "#690005",
|
||
"on-primary-fixed-variant": "#0038b6",
|
||
"primary-fixed": "#dde1ff",
|
||
"on-error-container": "#ffdad6",
|
||
"on-tertiary-fixed": "#25005a",
|
||
"on-primary": "#002682",
|
||
"surface": "#0b1326",
|
||
"secondary-container": "#3e495d",
|
||
"on-background": "#dae2fd",
|
||
"on-surface-variant": "#c3c5d9",
|
||
"on-secondary-fixed": "#111c2d",
|
||
"error-container": "#93000a",
|
||
"inverse-primary": "#004ced",
|
||
"secondary-fixed": "#d8e3fb",
|
||
"on-primary-fixed": "#001452",
|
||
"on-secondary-container": "#aeb9d0",
|
||
"on-tertiary-container": "#ecdfff",
|
||
"surface-container-lowest": "#060e20",
|
||
"surface-container-high": "#222a3d",
|
||
"surface-dim": "#0b1326",
|
||
"secondary": "#bcc7de",
|
||
"on-tertiary": "#3f008e",
|
||
"primary-container": "#0052ff",
|
||
"tertiary": "#d2bbff",
|
||
"surface-variant": "#2d3449",
|
||
"surface-tint": "#b7c4ff",
|
||
"surface-container-low": "#131b2e",
|
||
"surface-bright": "#31394d",
|
||
"background": "#0b1326",
|
||
"on-secondary-fixed-variant": "#3c475a",
|
||
"inverse-surface": "#dae2fd",
|
||
"tertiary-fixed-dim": "#d2bbff",
|
||
"primary-fixed-dim": "#b7c4ff",
|
||
"outline-variant": "#434656",
|
||
"tertiary-container": "#7b39ec",
|
||
"secondary-fixed-dim": "#bcc7de",
|
||
"surface-container-highest": "#2d3449",
|
||
"error": "#ffb4ab"
|
||
},
|
||
borderRadius: {
|
||
DEFAULT: '0.25rem',
|
||
lg: '0.5rem',
|
||
xl: '0.75rem',
|
||
full: '9999px'
|
||
},
|
||
fontFamily: {
|
||
headline: ['Manrope'],
|
||
body: ['Inter'],
|
||
label: ['Inter']
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
<style>
|
||
body { font-family: "Inter", sans-serif; }
|
||
.font-manrope { font-family: "Manrope", sans-serif; }
|
||
.glass { background: rgba(23, 31, 51, 0.7); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); }
|
||
.celestial-shadow { box-shadow: 0 24px 48px -12px rgba(0, 0, 0, 0.5), 0 0 20px rgba(183, 196, 255, 0.05); }
|
||
.map-bg { background-image: url(https://lh3.googleusercontent.com/aida-public/AB6AXuCyPXdDASv9Rbub1TekuMzrRsMuR_hPwbNxnzcHdd8c7BZiH0hZhoGZG98H6dzlhx-72Yh2etKw42boJCR9KDD6cYTbPgRyY1BIRbx_2uEPiPXqYslErPWsHtCtHb1pON_9dWiRPqnOvRGCtb0zBUw5bPhy9qhMnyoqjD-DrT_9bpKKWh3vRT6OfxGKkM6ohEdGE1zpUG56oZhhO0N-LVbSxZq8YLF3sH2pWYyHU9vdcu__gCmEl9oTAzJQF6oQzQj6kb8AyriHeGM); background-size: cover; background-position: center; }
|
||
.tonal-stepping { background: linear-gradient(180deg, rgba(11, 19, 38, 0.4) 0%, rgba(6, 14, 32, 0.4) 100%); }
|
||
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
|
||
.custom-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
||
.custom-scrollbar::-webkit-scrollbar-thumb { background: rgba(141, 144, 162, 0.2); border-radius: 10px; }
|
||
#map { width: 100%; height: 100%; z-index: 10; }
|
||
#map .leaflet-tile-pane { filter: brightness(1.05) contrast(1.08); }
|
||
#map .leaflet-control { z-index: 30; }
|
||
|
||
.popup-modern {
|
||
width: 230px;
|
||
padding: 18px;
|
||
border-radius: 20px;
|
||
|
||
background: linear-gradient(145deg, #1e293b, #0f172a);
|
||
color: #e2e8f0;
|
||
|
||
text-align: center;
|
||
|
||
box-shadow:
|
||
0 20px 40px rgba(0,0,0,0.5),
|
||
inset 0 1px 1px rgba(255,255,255,0.05);
|
||
}
|
||
|
||
.popup-form {
|
||
width: 280px;
|
||
padding: 18px;
|
||
border-radius: 20px;
|
||
|
||
background: linear-gradient(145deg, #1e293b, #0f172a);
|
||
color: #e2e8f0;
|
||
|
||
text-align: left;
|
||
|
||
box-shadow:
|
||
0 20px 40px rgba(0,0,0,0.5),
|
||
inset 0 1px 1px rgba(255,255,255,0.05);
|
||
}
|
||
|
||
/* TITLE */
|
||
.popup-modern h3 {
|
||
font-size: 18px;
|
||
font-weight: 700;
|
||
margin-bottom: 10px;
|
||
text-transform: capitalize;
|
||
}
|
||
|
||
/* TEXT */
|
||
.popup-modern p {
|
||
font-size: 13px;
|
||
color: #cbd5e1;
|
||
margin: 3px 0;
|
||
}
|
||
|
||
/* BUTTON BASE */
|
||
.popup-modern button {
|
||
width: 100%;
|
||
padding: 10px;
|
||
margin-top: 8px;
|
||
border: none;
|
||
border-radius: 12px;
|
||
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
/* BUTTON HOVER */
|
||
.popup-modern button:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 15px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
/* COLORS */
|
||
.btn-edit {
|
||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||
}
|
||
|
||
.btn-lokasi {
|
||
background: linear-gradient(135deg, #9333ea, #7c3aed);
|
||
}
|
||
|
||
.btn-hapus {
|
||
background: linear-gradient(135deg, #ef4444, #dc2626);
|
||
}
|
||
|
||
.leaflet-popup-content-wrapper {
|
||
background: transparent !important;
|
||
box-shadow: none !important;
|
||
padding: 0 !important;
|
||
}
|
||
|
||
.leaflet-popup-tip {
|
||
display: none;
|
||
}
|
||
|
||
.leaflet-popup-content {
|
||
margin: 0 !important;
|
||
}
|
||
|
||
</style>
|
||
<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>
|
||
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw/dist/leaflet.draw.css"/>
|
||
<script src="https://unpkg.com/leaflet-draw/dist/leaflet.draw.js"></script>
|
||
<script src="https://unpkg.com/leaflet-geometryutil"></script>
|
||
</head>
|
||
<body class="bg-surface text-on-surface overflow-hidden">
|
||
<main class="flex h-screen">
|
||
<aside class="w-[340px] h-full bg-[#0b1326]/95 backdrop-blur-2xl border-r border-slate-800/20 celestial-shadow z-40 flex flex-col overflow-y-auto custom-scrollbar">
|
||
<div class="p-6 border-b border-outline-variant/10">
|
||
<div class="flex items-center gap-3 mb-1">
|
||
<span class="text-blue-400 uppercase tracking-widest text-[10px] font-manrope font-bold">Spatial Metrics</span>
|
||
</div>
|
||
<h2 class="text-on-surface font-manrope font-bold text-lg">WEBGIS</h2>
|
||
<p class="text-outline text-[11px] font-medium tracking-wide">LIVE GIS DATA FEED • SYSTEM ACTIVE</p>
|
||
</div>
|
||
<div class="p-5 space-y-6">
|
||
<div class="grid grid-cols-1 gap-3">
|
||
<div class="bg-surface-container-high/40 p-4 rounded-xl border border-outline-variant/5">
|
||
<div class="flex justify-between items-start">
|
||
<div>
|
||
<p class="text-outline text-[10px] uppercase font-bold tracking-wider mb-1">Total SPBU</p>
|
||
<h3 class="text-2xl font-manrope font-extrabold text-on-surface" id="count-spbu">0</h3>
|
||
</div>
|
||
<span class="material-symbols-outlined text-primary text-xl">local_gas_station</span>
|
||
</div>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3">
|
||
<div class="bg-surface-container-high/40 p-4 rounded-xl border border-outline-variant/5">
|
||
<p class="text-outline text-[10px] uppercase font-bold tracking-wider mb-1">Total Jalan</p>
|
||
<h3 class="text-xl font-manrope font-bold text-on-surface" id="count-jalan">0</h3>
|
||
</div>
|
||
<div class="bg-surface-container-high/40 p-4 rounded-xl border border-outline-variant/5">
|
||
<p class="text-outline text-[10px] uppercase font-bold tracking-wider mb-1">Total Parsil</p>
|
||
<h3 class="text-xl font-manrope font-bold text-on-surface" id="count-parsil">0</h3>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="space-y-2">
|
||
<button id="btn-point" class="w-full bg-primary-container text-white py-3 rounded-xl font-manrope font-bold text-sm flex items-center justify-center gap-2 shadow-lg shadow-primary-container/20 hover:scale-[1.02] transition-transform" onclick="setMode('point')">
|
||
<span class="material-symbols-outlined text-sm">add_location_alt</span>
|
||
Tambah Point
|
||
</button>
|
||
<div class="grid grid-cols-2 gap-2">
|
||
<button id="btn-jalan" class="bg-surface-container-highest text-on-surface py-2.5 rounded-xl font-manrope font-bold text-xs flex items-center justify-center gap-2 hover:bg-surface-bright transition-colors" onclick="setMode('jalan')">
|
||
<span class="material-symbols-outlined text-sm">route</span>
|
||
Tambah Jalan
|
||
</button>
|
||
<button id="btn-parsil" class="bg-surface-container-highest text-on-surface py-2.5 rounded-xl font-manrope font-bold text-xs flex items-center justify-center gap-2 hover:bg-surface-bright transition-colors" onclick="setMode('parsil')">
|
||
<span class="material-symbols-outlined text-sm">grid_view</span>
|
||
Tambah Parsil
|
||
</button>
|
||
</div>
|
||
<button id="btn-cancel" class="w-full bg-error-container/20 text-error border border-error/20 py-2.5 rounded-xl font-manrope font-bold text-xs flex items-center justify-center gap-2 hover:bg-error-container/30 transition-colors" onclick="setMode(null)">
|
||
<span class="material-symbols-outlined text-sm">cancel</span>
|
||
Batal Mode
|
||
</button>
|
||
</div>
|
||
<div>
|
||
<h4 class="text-outline text-[10px] uppercase font-bold tracking-widest mb-3 px-1">Recent Assets</h4>
|
||
<div id="recent-assets" class="space-y-2"></div>
|
||
</div>
|
||
<div class="bg-surface-container-lowest/40 rounded-2xl p-4 border border-outline-variant/10">
|
||
<h4 class="text-on-surface font-manrope font-bold text-xs mb-4 flex items-center gap-2">
|
||
<span class="material-symbols-outlined text-sm">legend_toggle</span>
|
||
Map Legend
|
||
</h4>
|
||
<div class="space-y-4">
|
||
<div class="space-y-2">
|
||
<p class="text-[9px] uppercase font-bold text-outline tracking-widest">Petroleum Assets</p>
|
||
<div class="flex items-center gap-3 text-[11px] font-medium text-on-surface-variant">
|
||
<span class="w-2.5 h-2.5 rounded-full bg-primary shadow-[0_0_8px_rgba(183,196,255,0.6)]"></span>
|
||
SPBU 24 Jam
|
||
</div>
|
||
<div class="flex items-center gap-3 text-[11px] font-medium text-on-surface-variant">
|
||
<span class="w-2.5 h-2.5 rounded-full bg-outline"></span>
|
||
SPBU Terbatas
|
||
</div>
|
||
</div>
|
||
<div class="space-y-2">
|
||
<p class="text-[9px] uppercase font-bold text-outline tracking-widest">Infrastructure</p>
|
||
<div class="flex items-center gap-3 text-[11px] font-medium text-on-surface-variant">
|
||
<span class="w-6 h-[2px] bg-secondary-fixed shadow-[0_0_8px_rgba(216,227,251,0.6)]"></span>
|
||
Arteri Utama
|
||
</div>
|
||
<div class="flex items-center gap-3 text-[11px] font-medium text-on-surface-variant">
|
||
<span class="w-6 h-[2px] bg-tertiary shadow-[0_0_8px_rgba(210,187,255,0.6)]"></span>
|
||
Parsil Area
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
<section class="flex-1 relative overflow-hidden">
|
||
<div id="map" class="absolute inset-0 z-10"></div>
|
||
<div id="location-edit-panel" class="hidden absolute bottom-10 right-6 z-50 w-[320px] glass rounded-3xl border border-outline-variant/20 p-4 celestial-shadow shadow-xl">
|
||
<div class="flex items-start justify-between gap-3">
|
||
<div>
|
||
<p class="text-[10px] text-primary uppercase font-bold tracking-widest mb-1">Edit Lokasi Aktif</p>
|
||
<p class="text-[11px] leading-5 text-on-surface-variant">Gunakan tombol Simpan untuk menyimpan perubahan lokasi, atau Batal untuk mengembalikan posisi sebelumnya.</p>
|
||
</div>
|
||
<button class="text-slate-300 hover:text-white" onclick="cancelLocationEdit()">×</button>
|
||
</div>
|
||
<div class="mt-4 grid grid-cols-2 gap-3">
|
||
<button id="location-save-btn" class="bg-primary text-surface rounded-2xl py-3 text-sm font-semibold">Simpan Lokasi</button>
|
||
<button id="location-cancel-btn" class="bg-surface-container-high text-on-surface rounded-2xl py-3 text-sm font-semibold">Batal</button>
|
||
</div>
|
||
</div>
|
||
<div id="toast-container" class="fixed top-6 right-6 z-50 flex flex-col gap-3 pointer-events-none"></div>
|
||
<div class="absolute top-6 left-6 z-40">
|
||
<div class="bg-secondary-container/80 backdrop-blur-md px-4 py-2 rounded-full border border-secondary/30 flex items-center gap-2 celestial-shadow">
|
||
<span class="w-2 h-2 rounded-full bg-secondary-fixed animate-pulse"></span>
|
||
<span class="text-[11px] font-manrope font-bold text-on-secondary-container tracking-wider uppercase" id="mode-label">Mode: Idle</span>
|
||
</div>
|
||
</div>
|
||
<div class="absolute top-6 right-6 z-40">
|
||
<div class="glass p-4 rounded-2xl border border-outline-variant/10 celestial-shadow w-56">
|
||
<h5 class="text-[10px] font-bold text-outline uppercase tracking-widest mb-3">Layer Controls</h5>
|
||
<div class="space-y-3">
|
||
<label class="flex items-center justify-between cursor-pointer group">
|
||
<span class="text-xs font-medium text-on-surface group-hover:text-primary transition-colors">SPBU 24 Jam</span>
|
||
<div class="relative inline-flex items-center">
|
||
<input id="toggle-spbu24" class="sr-only peer" type="checkbox" checked onchange="setLayerVisibility('spbu24', this.checked)"/>
|
||
<div class="w-8 h-4 bg-surface-container-highest rounded-full peer-checked:bg-primary-container transition-colors"></div>
|
||
<div class="absolute left-0.5 top-0.5 w-3 h-3 bg-white rounded-full transition-transform peer-checked:translate-x-4"></div>
|
||
</div>
|
||
</label>
|
||
<label class="flex items-center justify-between cursor-pointer group">
|
||
<span class="text-xs font-medium text-on-surface group-hover:text-primary transition-colors">SPBU Tidak 24 Jam</span>
|
||
<div class="relative inline-flex items-center">
|
||
<input id="toggle-spbuNo24" class="sr-only peer" type="checkbox" checked onchange="setLayerVisibility('spbuNo24', this.checked)"/>
|
||
<div class="w-8 h-4 bg-surface-container-highest rounded-full peer-checked:bg-primary-container transition-colors"></div>
|
||
<div class="absolute left-0.5 top-0.5 w-3 h-3 bg-white rounded-full transition-transform peer-checked:translate-x-4"></div>
|
||
</div>
|
||
</label>
|
||
<label class="flex items-center justify-between cursor-pointer group">
|
||
<span class="text-xs font-medium text-on-surface group-hover:text-primary transition-colors">Show Parsil Borders</span>
|
||
<div class="relative inline-flex items-center">
|
||
<input id="toggle-parsil" class="sr-only peer" type="checkbox" checked onchange="setLayerVisibility('parsil', this.checked)"/>
|
||
<div class="w-8 h-4 bg-surface-container-highest rounded-full peer-checked:bg-primary-container transition-colors"></div>
|
||
<div class="absolute left-0.5 top-0.5 w-3 h-3 bg-white rounded-full transition-transform peer-checked:translate-x-4"></div>
|
||
</div>
|
||
</label>
|
||
<label class="flex items-center justify-between cursor-pointer group">
|
||
<span class="text-xs font-medium text-on-surface group-hover:text-primary transition-colors">Show Kecamatan</span>
|
||
<div class="relative inline-flex items-center">
|
||
<input id="toggle-kecamatan" class="sr-only peer" type="checkbox" checked onchange="setLayerVisibility('kecamatan', this.checked)"/>
|
||
<div class="w-8 h-4 bg-surface-container-highest rounded-full peer-checked:bg-primary-container transition-colors"></div>
|
||
<div class="absolute left-0.5 top-0.5 w-3 h-3 bg-white rounded-full transition-transform peer-checked:translate-x-4"></div>
|
||
</div>
|
||
</label>
|
||
<label class="flex items-center justify-between cursor-pointer group">
|
||
<span class="text-xs font-medium text-on-surface group-hover:text-primary transition-colors">Show Jalan Borders</span>
|
||
<div class="relative inline-flex items-center">
|
||
<input id="toggle-jalan" class="sr-only peer" type="checkbox" checked onchange="setLayerVisibility('jalan', this.checked)"/>
|
||
<div class="w-8 h-4 bg-surface-container-highest rounded-full peer-checked:bg-primary-container transition-colors"></div>
|
||
<div class="absolute left-0.5 top-0.5 w-3 h-3 bg-white rounded-full transition-transform peer-checked:translate-x-4"></div>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="selected-panel" class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-40 hidden">
|
||
<div class="w-0.5 h-16 bg-primary/40 absolute -bottom-16 left-1/2 -translate-x-1/2"></div>
|
||
<div class="glass p-5 rounded-2xl border border-primary/20 celestial-shadow w-[280px]">
|
||
<div class="flex justify-between items-start mb-4">
|
||
<div>
|
||
<span class="text-[9px] font-bold text-primary uppercase tracking-tighter">Selected Asset</span>
|
||
<h3 class="text-sm font-bold text-on-surface font-manrope" id="selected-title">Segment Arteri Gatot 04</h3>
|
||
</div>
|
||
<button class="material-symbols-outlined text-outline hover:text-on-surface text-lg" onclick="hideSelectedPanel()">close</button>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-3 mb-5">
|
||
<div class="bg-surface-container-lowest/50 p-2.5 rounded-xl">
|
||
<p class="text-[8px] text-outline uppercase font-bold">Status</p>
|
||
<p class="text-[11px] font-semibold text-secondary-fixed" id="selected-status">Verified</p>
|
||
</div>
|
||
<div class="bg-surface-container-lowest/50 p-2.5 rounded-xl">
|
||
<p class="text-[8px] text-outline uppercase font-bold">Panjang</p>
|
||
<p class="text-[11px] font-semibold text-on-surface" id="selected-length">1.42 km</p>
|
||
</div>
|
||
</div>
|
||
<div class="flex flex-col gap-2">
|
||
<div class="grid grid-cols-2 gap-2">
|
||
<button class="bg-primary/10 text-primary py-2 rounded-lg text-[11px] font-bold hover:bg-primary/20 transition-colors flex items-center justify-center gap-1" onclick="editSelected()">
|
||
<span class="material-symbols-outlined text-xs">edit</span> Edit Data
|
||
</button>
|
||
<button class="bg-surface-container-highest text-on-surface py-2 rounded-lg text-[11px] font-bold hover:bg-surface-bright transition-colors flex items-center justify-center gap-1" onclick="editLocation()">
|
||
<span class="material-symbols-outlined text-xs">location_searching</span> Lokasi
|
||
</button>
|
||
</div>
|
||
<button class="w-full bg-error-container/10 text-error py-2 rounded-lg text-[11px] font-bold hover:bg-error-container/20 transition-colors flex items-center justify-center gap-1" onclick="deleteSelected()">
|
||
<span class="material-symbols-outlined text-xs">delete</span> Hapus Feature
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="absolute bottom-10 right-10 z-40 flex flex-col gap-2">
|
||
<button class="w-12 h-12 glass rounded-xl flex items-center justify-center text-on-surface hover:text-primary transition-all hover:scale-105 border border-outline-variant/10" onclick="map.locate({setView:true, maxZoom:16})">
|
||
<span class="material-symbols-outlined">my_location</span>
|
||
</button>
|
||
<button class="w-12 h-12 glass rounded-xl flex items-center justify-center text-on-surface hover:text-primary transition-all hover:scale-105 border border-outline-variant/10" onclick="location.reload()">
|
||
<span class="material-symbols-outlined">restart_alt</span>
|
||
</button>
|
||
<div class="flex flex-col rounded-xl glass border border-outline-variant/10 overflow-hidden">
|
||
<button class="w-12 h-12 flex items-center justify-center text-on-surface hover:bg-surface-bright transition-colors" onclick="map.zoomIn()">
|
||
<span class="material-symbols-outlined">add</span>
|
||
</button>
|
||
<button class="w-12 h-12 flex items-center justify-center text-on-surface hover:bg-surface-bright transition-colors" onclick="map.zoomOut()">
|
||
<span class="material-symbols-outlined">remove</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="absolute bottom-4 left-6 z-40">
|
||
<p class="text-[10px] font-mono text-outline/60 tracking-widest bg-surface/40 px-3 py-1 rounded-full backdrop-blur-sm" id="latlng-display">LAT: - • LONG: - • ZOOM: -</p>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
<div class="fixed inset-0 pointer-events-none z-0" style="display:none;">
|
||
<svg class="w-full h-full opacity-30">
|
||
<path d="M 500 400 L 700 550 L 900 450" fill="none" stroke="#b7c4ff" stroke-dasharray="8 4" stroke-width="3"></path>
|
||
<circle cx="700" cy="550" fill="#0052ff" r="6"></circle>
|
||
<circle cx="900" cy="450" fill="#b7c4ff" r="4"></circle>
|
||
</svg>
|
||
</div>
|
||
<script>
|
||
|
||
const layers = {
|
||
spbu24: new L.LayerGroup(),
|
||
spbuNo24: new L.LayerGroup(),
|
||
jalan: new L.LayerGroup(),
|
||
parsil: new L.LayerGroup(),
|
||
kecamatan: new L.LayerGroup()
|
||
};
|
||
const map = L.map('map').setView([-0.02055556, 109.34138889], 13);
|
||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map);
|
||
Object.values(layers).forEach(layer => layer.addTo(map));
|
||
const drawnItems = new L.FeatureGroup();
|
||
map.addLayer(drawnItems);
|
||
let currentDrawer = null;
|
||
let mode = null;
|
||
let selectedFeature = null;
|
||
let selectedType = null;
|
||
let selectedLayer = null;
|
||
let featureLayers = { spbu: {}, jalan: {}, parsil: {}, kecamatan: {} };
|
||
let activeLocationEditType = null;
|
||
let activeLocationEditId = null;
|
||
let spbuData = [];
|
||
let jalanData = [];
|
||
let parsilData = [];
|
||
let kecamatanData = [];
|
||
|
||
function utmToLatLng(easting, northing, zone = 49, southernHemisphere = true) {
|
||
const a = 6378137.0;
|
||
const f = 1 / 298.257223563;
|
||
const k0 = 0.9996;
|
||
const e = Math.sqrt(f * (2 - f));
|
||
const e1 = (1 - Math.sqrt(1 - e * e)) / (1 + Math.sqrt(1 - e * e));
|
||
const x = easting - 500000.0;
|
||
let y = northing;
|
||
if (southernHemisphere) y -= 10000000.0;
|
||
const m = y / k0;
|
||
const mu = m / (a * (1 - e * e / 4 - 3 * Math.pow(e, 4) / 64 - 5 * Math.pow(e, 6) / 256));
|
||
const j1 = 3 * e1 / 2 - 27 * Math.pow(e1, 3) / 32;
|
||
const j2 = 21 * Math.pow(e1, 2) / 16 - 55 * Math.pow(e1, 4) / 32;
|
||
const j3 = 151 * Math.pow(e1, 3) / 96;
|
||
const j4 = 1097 * Math.pow(e1, 4) / 512;
|
||
const fp = mu + j1 * Math.sin(2 * mu) + j2 * Math.sin(4 * mu) + j3 * Math.sin(6 * mu) + j4 * Math.sin(8 * mu);
|
||
const e2 = e * e / (1 - e * e);
|
||
const c1 = e2 * Math.pow(Math.cos(fp), 2);
|
||
const t1 = Math.pow(Math.tan(fp), 2);
|
||
const n1 = a / Math.sqrt(1 - Math.pow(e * Math.sin(fp), 2));
|
||
const r1 = a * (1 - e * e) / Math.pow(1 - Math.pow(e * Math.sin(fp), 2), 1.5);
|
||
const d = x / (n1 * k0);
|
||
const q1 = n1 * Math.tan(fp) / r1;
|
||
const q2 = Math.pow(d, 2) / 2;
|
||
const q3 = (5 + 3 * t1 + 10 * c1 - 4 * Math.pow(c1, 2) - 9 * e2) * Math.pow(d, 4) / 24;
|
||
const q4 = (61 + 90 * t1 + 298 * c1 + 45 * Math.pow(t1, 2) - 252 * e2 - 3 * Math.pow(c1, 2)) * Math.pow(d, 6) / 720;
|
||
const lat = fp - q1 * (q2 - q3 + q4);
|
||
const q5 = d;
|
||
const q6 = (1 + 2 * t1 + c1) * Math.pow(d, 3) / 6;
|
||
const q7 = (5 - 2 * c1 + 28 * t1 - 3 * Math.pow(c1, 2) + 8 * e2 + 24 * Math.pow(t1, 2)) * Math.pow(d, 5) / 120;
|
||
const lon = (q5 - q6 + q7) / Math.cos(fp);
|
||
const lonOrigin = (zone - 1) * 6 - 180 + 3;
|
||
return { lat: lat * 180 / Math.PI, lng: lonOrigin + lon * 180 / Math.PI };
|
||
}
|
||
|
||
function transformUtmCoordinates(coords, zone = 49, southernHemisphere = true) {
|
||
if (typeof coords[0] === 'number' && typeof coords[1] === 'number') {
|
||
const ll = utmToLatLng(coords[0], coords[1], zone, southernHemisphere);
|
||
return [ll.lng, ll.lat];
|
||
}
|
||
return coords.map(coord => transformUtmCoordinates(coord, zone, southernHemisphere));
|
||
}
|
||
|
||
function convertUtmFeature(feature, zone = 49, southernHemisphere = true) {
|
||
return {
|
||
...feature,
|
||
geometry: {
|
||
...feature.geometry,
|
||
coordinates: transformUtmCoordinates(feature.geometry.coordinates, zone, southernHemisphere)
|
||
}
|
||
};
|
||
}
|
||
|
||
function setMode(m) {
|
||
mode = m;
|
||
document.getElementById('mode-label').textContent = m ? `Mode: Tambah ${m === 'point' ? 'Point' : m === 'jalan' ? 'Jalan' : 'Parsil'}` : 'Mode: Idle';
|
||
['btn-point','btn-jalan','btn-parsil','btn-cancel'].forEach(id => {
|
||
const btn = document.getElementById(id);
|
||
btn.classList.toggle('bg-primary-container', id === `btn-${m}` && m !== null);
|
||
btn.classList.toggle('text-white', id === `btn-${m}` && m !== null);
|
||
});
|
||
if (!m) cancelPendingShape(false);
|
||
updateDrawControl();
|
||
}
|
||
function updateDrawControl() {
|
||
if (currentDrawer && currentDrawer.disable) currentDrawer.disable();
|
||
currentDrawer = null;
|
||
if (mode === 'jalan') {
|
||
currentDrawer = new L.Draw.Polyline(map, { shapeOptions: { color:'#ef4444', weight: 5 } });
|
||
currentDrawer.enable();
|
||
}
|
||
if (mode === 'parsil') {
|
||
currentDrawer = new L.Draw.Polygon(map, { allowIntersection:false, showArea:true, shapeOptions: { color:'#d2bbff', fillOpacity: 0.12 } });
|
||
currentDrawer.enable();
|
||
}
|
||
}
|
||
let pendingLayer = null;
|
||
function setLayerVisibility(key, visible) {
|
||
visible ? map.addLayer(layers[key]) : map.removeLayer(layers[key]);
|
||
}
|
||
function cancelPendingShape(clearMode = true) {
|
||
if (pendingLayer) {
|
||
drawnItems.removeLayer(pendingLayer);
|
||
pendingLayer = null;
|
||
}
|
||
if (clearMode) setMode(null);
|
||
}
|
||
function formTambah(lat, lng) {
|
||
return `
|
||
<div class="popup-form">
|
||
<p style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#94a3b8;margin-bottom:12px">Tambah SPBU</p>
|
||
<form id="formTambah" class="space-y-4 text-sm text-on-surface">
|
||
<div>
|
||
<label class="block text-[11px] text-outline uppercase font-bold mb-1">Nama SPBU</label>
|
||
<input name="nama_spbu" required class="w-full rounded-xl border border-outline-variant/20 bg-surface-container-low px-3 py-2 text-on-surface" />
|
||
</div>
|
||
<div>
|
||
<label class="block text-[11px] text-outline uppercase font-bold mb-1">No WA</label>
|
||
<input name="no_wa" class="w-full rounded-xl border border-outline-variant/20 bg-surface-container-low px-3 py-2 text-on-surface" />
|
||
</div>
|
||
<div>
|
||
<label class="block text-[11px] text-outline uppercase font-bold mb-1">Buka 24 Jam</label>
|
||
<select name="buka_24jam" class="w-full rounded-xl border border-outline-variant/20 bg-surface-container-low px-3 py-2 text-on-surface">
|
||
<option>Ya</option>
|
||
<option>Tidak</option>
|
||
</select>
|
||
</div>
|
||
<input type="hidden" name="latitude" value="${lat}" />
|
||
<input type="hidden" name="longitude" value="${lng}" />
|
||
<button type="submit" class="w-full bg-primary text-surface py-3 rounded-xl font-bold">Simpan SPBU</button>
|
||
</form>
|
||
</div>`;
|
||
}
|
||
function showSelectedPanel(type, item) {
|
||
selectedType = type;
|
||
selectedFeature = item;
|
||
selectedLayer = featureLayers[type]?.[item.id] || null;
|
||
document.getElementById('selected-title').textContent = type === 'spbu' ? item.nama_spbu : type === 'jalan' ? item.nama_jalan : item.nama_pemilik;
|
||
document.getElementById('selected-status').textContent = type === 'spbu' ? item.buka_24jam : type === 'jalan' ? item.status_jalan : item.status;
|
||
document.getElementById('selected-length').textContent = type === 'jalan' ? `${item.panjang} m` : type === 'spbu' ? `${item.latitude.toFixed(6)}, ${item.longitude.toFixed(6)}` : `${Math.round(item.luas)} m²`;
|
||
document.getElementById('selected-panel').classList.remove('hidden');
|
||
}
|
||
function hideSelectedPanel() {
|
||
document.getElementById('selected-panel').classList.add('hidden');
|
||
selectedFeature = null;
|
||
selectedType = null;
|
||
}
|
||
function getFeatureItem(type, id) {
|
||
const source = type === 'spbu' ? spbuData : type === 'jalan' ? jalanData : parsilData;
|
||
return source.find(v => Number(v.id) === Number(id)) || null;
|
||
}
|
||
function selectFeature(type, id, layer = null) {
|
||
selectedType = type;
|
||
selectedFeature = getFeatureItem(type, id);
|
||
selectedLayer = layer || featureLayers[type]?.[id] || null;
|
||
}
|
||
function getFeaturePopup(type, item) {
|
||
|
||
if(type === 'spbu'){
|
||
return `
|
||
<div class="popup-modern">
|
||
<h3>${item.nama_spbu}</h3>
|
||
<p class="status">Status: ${item.buka_24jam}</p>
|
||
|
||
<button class="btn-edit" onclick="editFeatureData('spbu', ${item.id})">
|
||
Edit Data
|
||
</button>
|
||
|
||
<button class="btn-lokasi" onclick="editFeatureLocation('spbu', ${item.id})">
|
||
Edit Lokasi
|
||
</button>
|
||
|
||
<button class="btn-hapus" onclick="deleteFeature('spbu', ${item.id})">
|
||
Hapus
|
||
</button>
|
||
</div>`;
|
||
}
|
||
|
||
if(type === 'jalan'){
|
||
return `
|
||
<div class="popup-modern">
|
||
<h3>${item.nama_jalan}</h3>
|
||
<p class="status">Status: ${item.status_jalan}</p>
|
||
<p class="info">Panjang: ${item.panjang} m</p>
|
||
|
||
<button class="btn-edit" onclick="editFeatureData('jalan', ${item.id})">
|
||
Edit Data
|
||
</button>
|
||
|
||
<button class="btn-lokasi" onclick="editFeatureLocation('jalan', ${item.id})">
|
||
Edit Lokasi
|
||
</button>
|
||
|
||
<button class="btn-hapus" onclick="deleteFeature('jalan', ${item.id})">
|
||
Hapus
|
||
</button>
|
||
</div>`;
|
||
}
|
||
|
||
if(type === 'parsil'){
|
||
return `
|
||
<div class="popup-modern">
|
||
<h3>${item.nama_pemilik}</h3>
|
||
<p class="status">Status: ${item.status}</p>
|
||
<p class="info">Luas: ${Math.round(item.luas)} m²</p>
|
||
|
||
<button class="btn-edit" onclick="editFeatureData('parsil', ${item.id})">
|
||
Edit Data
|
||
</button>
|
||
|
||
<button class="btn-lokasi" onclick="editFeatureLocation('parsil', ${item.id})">
|
||
Edit Lokasi
|
||
</button>
|
||
|
||
<button class="btn-hapus" onclick="deleteFeature('parsil', ${item.id})">
|
||
Hapus
|
||
</button>
|
||
</div>`;
|
||
}
|
||
}
|
||
function editFeatureData(type, id) {
|
||
const item = getFeatureItem(type, id);
|
||
if (!item) return;
|
||
if (type === 'spbu') {
|
||
return openSpbuEditPopup(featureLayers.spbu[id], item);
|
||
}
|
||
if (type === 'jalan') {
|
||
return openJalanEditPopup(featureLayers.jalan[id], item);
|
||
}
|
||
if (type === 'parsil') {
|
||
return openParsilEditPopup(featureLayers.parsil[id], item);
|
||
}
|
||
let fd = new FormData();
|
||
fd.append('id', id);
|
||
fd.append('type', type);
|
||
const nama = prompt('Nama Pemilik', item.nama_pemilik);
|
||
if (nama === null) return;
|
||
const status = prompt('Status', item.status);
|
||
if (status === null) return;
|
||
fd.append('nama_pemilik', nama);
|
||
fd.append('status', status);
|
||
fetch('update.php', { method:'POST', body:fd }).then(() => location.reload());
|
||
}
|
||
function openSpbuEditPopup(layer, item) {
|
||
const popupContent = `
|
||
<div class="popup-form">
|
||
<p style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#94a3b8;margin-bottom:12px">Edit SPBU</p>
|
||
<form id="formEditSpbu" class="space-y-4 text-sm text-on-surface">
|
||
<div>
|
||
<label class="block text-[11px] text-outline uppercase font-bold mb-1">Nama SPBU</label>
|
||
<input name="nama_spbu" value="${item.nama_spbu}" required class="w-full rounded-xl border border-outline-variant/20 bg-surface-container-low px-3 py-2 text-on-surface" />
|
||
</div>
|
||
<div>
|
||
<label class="block text-[11px] text-outline uppercase font-bold mb-1">No WA</label>
|
||
<input name="no_wa" value="${item.no_wa || ''}" class="w-full rounded-xl border border-outline-variant/20 bg-surface-container-low px-3 py-2 text-on-surface" />
|
||
</div>
|
||
<div>
|
||
<label class="block text-[11px] text-outline uppercase font-bold mb-1">Buka 24 Jam</label>
|
||
<select name="buka_24jam" class="w-full rounded-xl border border-outline-variant/20 bg-surface-container-low px-3 py-2 text-on-surface">
|
||
<option${item.buka_24jam === 'Ya' ? ' selected' : ''}>Ya</option>
|
||
<option${item.buka_24jam !== 'Ya' ? ' selected' : ''}>Tidak</option>
|
||
</select>
|
||
</div>
|
||
<input type="hidden" name="id" value="${item.id}" />
|
||
<input type="hidden" name="type" value="spbu" />
|
||
<button type="submit" class="w-full bg-primary text-surface py-3 rounded-xl font-bold">Simpan SPBU</button>
|
||
</form>
|
||
</div>`;
|
||
const center = layer && layer.getLatLng ? layer.getLatLng() : map.getCenter();
|
||
if (layer && layer.bindPopup) {
|
||
layer.bindPopup(popupContent, { closeButton: true, minWidth: 280 }).openPopup(center);
|
||
} else {
|
||
L.popup({ minWidth: 280 }).setLatLng(center).setContent(popupContent).openOn(map);
|
||
}
|
||
setTimeout(() => {
|
||
const form = document.getElementById('formEditSpbu');
|
||
if (!form) return;
|
||
form.addEventListener('submit', function(ev) {
|
||
ev.preventDefault();
|
||
fetch('update.php', { method:'POST', body:new FormData(this) }).then(() => location.reload());
|
||
});
|
||
}, 50);
|
||
}
|
||
function openJalanEditPopup(layer, item) {
|
||
const popupContent = `
|
||
<div class="popup-form">
|
||
<p style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#94a3b8;margin-bottom:12px">Edit Jalan</p>
|
||
<form id="formEditJalan" class="space-y-4 text-sm text-on-surface">
|
||
<div>
|
||
<label class="block text-[11px] text-outline uppercase font-bold mb-1">Nama Jalan</label>
|
||
<input name="nama_jalan" value="${item.nama_jalan}" required class="w-full rounded-xl border border-outline-variant/20 bg-surface-container-low px-3 py-2 text-on-surface" />
|
||
</div>
|
||
<div>
|
||
<label class="block text-[11px] text-outline uppercase font-bold mb-1">Status Jalan</label>
|
||
<select name="status_jalan" class="w-full rounded-xl border border-outline-variant/20 bg-surface-container-low px-3 py-2 text-on-surface">
|
||
<option${item.status_jalan === 'Nasional' ? ' selected' : ''}>Nasional</option>
|
||
<option${item.status_jalan === 'Provinsi' ? ' selected' : ''}>Provinsi</option>
|
||
<option${item.status_jalan === 'Kabupaten' ? ' selected' : ''}>Kabupaten</option>
|
||
</select>
|
||
</div>
|
||
<input type="hidden" name="id" value="${item.id}" />
|
||
<input type="hidden" name="type" value="jalan" />
|
||
<button type="submit" class="w-full bg-primary text-surface py-3 rounded-xl font-bold">Simpan Jalan</button>
|
||
</form>
|
||
</div>`;
|
||
const center = layer && layer.getBounds ? layer.getBounds().getCenter() : map.getCenter();
|
||
if (layer && layer.bindPopup) {
|
||
layer.bindPopup(popupContent, { closeButton: true, minWidth: 300 }).openPopup(center);
|
||
} else {
|
||
L.popup({ minWidth: 300 }).setLatLng(center).setContent(popupContent).openOn(map);
|
||
}
|
||
setTimeout(() => {
|
||
const form = document.getElementById('formEditJalan');
|
||
if (!form) return;
|
||
form.addEventListener('submit', function(ev) {
|
||
ev.preventDefault();
|
||
fetch('update.php', { method:'POST', body:new FormData(this) }).then(() => location.reload());
|
||
});
|
||
}, 50);
|
||
}
|
||
function openParsilEditPopup(layer, item) {
|
||
const popupContent = `
|
||
<div class="popup-form">
|
||
<p style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#94a3b8;margin-bottom:12px">Edit Parsil</p>
|
||
<form id="formEditParsil" class="space-y-4 text-sm text-on-surface">
|
||
<div>
|
||
<label class="block text-[11px] text-outline uppercase font-bold mb-1">Nama Pemilik</label>
|
||
<input name="nama_pemilik" value="${item.nama_pemilik}" required class="w-full rounded-xl border border-outline-variant/20 bg-surface-container-low px-3 py-2 text-on-surface" />
|
||
</div>
|
||
<div>
|
||
<label class="block text-[11px] text-outline uppercase font-bold mb-1">Status</label>
|
||
<select name="status" class="w-full rounded-xl border border-outline-variant/20 bg-surface-container-low px-3 py-2 text-on-surface">
|
||
<option${item.status === 'SHM' ? ' selected' : ''}>SHM</option>
|
||
<option${item.status === 'HGB' ? ' selected' : ''}>HGB</option>
|
||
<option${item.status === 'HGU' ? ' selected' : ''}>HGU</option>
|
||
<option${item.status === 'HP' ? ' selected' : ''}>HP</option>
|
||
</select>
|
||
</div>
|
||
<input type="hidden" name="id" value="${item.id}" />
|
||
<input type="hidden" name="type" value="parsil" />
|
||
<button type="submit" class="w-full bg-primary text-surface py-3 rounded-xl font-bold">Simpan Parsil</button>
|
||
</form>
|
||
</div>`;
|
||
const center = layer && layer.getBounds ? layer.getBounds().getCenter() : map.getCenter();
|
||
if (layer && layer.bindPopup) {
|
||
layer.bindPopup(popupContent, { closeButton: true, minWidth: 300 }).openPopup(center);
|
||
} else {
|
||
L.popup({ minWidth: 300 }).setLatLng(center).setContent(popupContent).openOn(map);
|
||
}
|
||
setTimeout(() => {
|
||
const form = document.getElementById('formEditParsil');
|
||
if (!form) return;
|
||
form.addEventListener('submit', function(ev) {
|
||
ev.preventDefault();
|
||
fetch('update.php', { method:'POST', body:new FormData(this) }).then(() => location.reload());
|
||
});
|
||
}, 50);
|
||
}
|
||
function openSpbuLocationEditor(layer, item) {
|
||
if (!layer || !layer.getLatLng) return;
|
||
if (layer.dragging) {
|
||
layer.dragging.enable();
|
||
}
|
||
layer._originalLatLng = layer.getLatLng();
|
||
activeLocationEditType = 'spbu';
|
||
activeLocationEditId = item.id;
|
||
showLocationEditPanel(item.id, 'spbu');
|
||
const popupContent = `
|
||
<div class="space-y-3 text-sm text-on-surface">
|
||
<p class="text-[11px] text-outline uppercase font-bold tracking-wide">Edit Lokasi SPBU</p>
|
||
<p class="text-[11px] text-on-surface-variant">Tahan lalu geser marker ke lokasi baru. Gunakan tombol Simpan Lokasi untuk menyimpan.</p>
|
||
</div>`;
|
||
layer.bindPopup(popupContent, { closeButton: true, minWidth: 280 }).openPopup(layer.getLatLng());
|
||
}
|
||
function openJalanLocationEditor(layer, item) {
|
||
if (!layer || !layer.getLatLngs) return;
|
||
if (!drawnItems.hasLayer(layer)) {
|
||
drawnItems.addLayer(layer);
|
||
}
|
||
layer._originalLatLngs = JSON.parse(JSON.stringify(layer.getLatLngs()));
|
||
if (layer.editing && layer.editing.enable) {
|
||
layer.editing.enable();
|
||
} else if (L.Edit && L.Edit.Poly) {
|
||
layer.editing = new L.Edit.Poly(layer);
|
||
if (layer.editing.enable) layer.editing.enable();
|
||
}
|
||
showLocationEditPanel(item.id, 'jalan');
|
||
showToast('Mode edit lokasi jalan aktif. Vertex putih dapat digeser atau dihapus.', 'info');
|
||
}
|
||
function openParsilLocationEditor(layer, item) {
|
||
if (!layer || !layer.getLatLngs) return;
|
||
if (!drawnItems.hasLayer(layer)) {
|
||
drawnItems.addLayer(layer);
|
||
}
|
||
layer._originalLatLngs = JSON.parse(JSON.stringify(layer.getLatLngs()));
|
||
if (layer.editing && layer.editing.enable) {
|
||
layer.editing.enable();
|
||
} else if (L.Edit && L.Edit.Poly) {
|
||
layer.editing = new L.Edit.Poly(layer);
|
||
if (layer.editing.enable) layer.editing.enable();
|
||
}
|
||
showLocationEditPanel(item.id, 'parsil');
|
||
showToast('Mode edit lokasi parsil aktif. Vertex putih dapat digeser atau dihapus.', 'info');
|
||
}
|
||
function saveJalanLocation(id) {
|
||
const layer = featureLayers.jalan[id];
|
||
if (!layer || !layer.toGeoJSON) return;
|
||
const geo = layer.toGeoJSON().geometry;
|
||
const latlngs = layer.getLatLngs();
|
||
const panjang = Math.round(calcLength(latlngs));
|
||
const fd = new FormData();
|
||
fd.append('id', id);
|
||
fd.append('type', 'jalan');
|
||
fd.append('geom', JSON.stringify(geo));
|
||
fd.append('panjang', panjang);
|
||
if (layer.editing && layer.editing.disable) layer.editing.disable();
|
||
hideLocationEditPanel();
|
||
fetch('update_lokasi.php', { method:'POST', body:fd })
|
||
.then(response => response.text())
|
||
.then(result => {
|
||
if (result.trim() === 'success') {
|
||
showToast('Lokasi jalan berhasil disimpan.', 'success');
|
||
} else {
|
||
showToast('Gagal menyimpan lokasi jalan.', 'error');
|
||
}
|
||
setTimeout(() => location.reload(), 800);
|
||
});
|
||
}
|
||
function cancelJalanLocation(id) {
|
||
const layer = featureLayers.jalan[id];
|
||
if (!layer || !layer.setLatLngs || !layer._originalLatLngs) return;
|
||
layer.setLatLngs(layer._originalLatLngs);
|
||
if (layer.editing && layer.editing.disable) layer.editing.disable();
|
||
hideLocationEditPanel();
|
||
showToast('Perubahan lokasi jalan dibatalkan.', 'warning');
|
||
}
|
||
function saveParsilLocation(id) {
|
||
const layer = featureLayers.parsil[id];
|
||
if (!layer || !layer.toGeoJSON) return;
|
||
const geo = layer.toGeoJSON().geometry;
|
||
const latlngs = layer.getLatLngs()[0];
|
||
const luas = Math.round(L.GeometryUtil.geodesicArea(latlngs));
|
||
const fd = new FormData();
|
||
fd.append('id', id);
|
||
fd.append('type', 'parsil');
|
||
fd.append('geom', JSON.stringify(geo));
|
||
fd.append('luas', luas);
|
||
if (layer.editing && layer.editing.disable) layer.editing.disable();
|
||
hideLocationEditPanel();
|
||
fetch('update_lokasi.php', { method:'POST', body:fd })
|
||
.then(response => response.text())
|
||
.then(result => {
|
||
if (result.trim() === 'success') {
|
||
showToast('Lokasi parsil berhasil disimpan.', 'success');
|
||
} else {
|
||
showToast('Gagal menyimpan lokasi parsil.', 'error');
|
||
}
|
||
setTimeout(() => location.reload(), 800);
|
||
});
|
||
}
|
||
function cancelParsilLocation(id) {
|
||
const layer = featureLayers.parsil[id];
|
||
if (!layer || !layer.setLatLngs || !layer._originalLatLngs) return;
|
||
layer.setLatLngs(layer._originalLatLngs);
|
||
if (layer.editing && layer.editing.disable) layer.editing.disable();
|
||
hideLocationEditPanel();
|
||
showToast('Perubahan lokasi parsil dibatalkan.', 'warning');
|
||
}
|
||
function showLocationEditPanel(id, type) {
|
||
const panel = document.getElementById('location-edit-panel');
|
||
if (!panel) return;
|
||
panel.classList.remove('hidden');
|
||
activeLocationEditType = type;
|
||
activeLocationEditId = id;
|
||
document.getElementById('location-save-btn').onclick = () => {
|
||
if (type === 'spbu') return saveSpbuLocation(id);
|
||
if (type === 'jalan') return saveJalanLocation(id);
|
||
if (type === 'parsil') return saveParsilLocation(id);
|
||
};
|
||
document.getElementById('location-cancel-btn').onclick = () => cancelLocationEdit();
|
||
}
|
||
function hideLocationEditPanel() {
|
||
const panel = document.getElementById('location-edit-panel');
|
||
if (!panel) return;
|
||
panel.classList.add('hidden');
|
||
activeLocationEditType = null;
|
||
activeLocationEditId = null;
|
||
}
|
||
function cancelLocationEdit() {
|
||
if (!activeLocationEditType || !activeLocationEditId) return;
|
||
if (activeLocationEditType === 'spbu') return cancelSpbuLocation(activeLocationEditId);
|
||
if (activeLocationEditType === 'jalan') return cancelJalanLocation(activeLocationEditId);
|
||
if (activeLocationEditType === 'parsil') return cancelParsilLocation(activeLocationEditId);
|
||
}
|
||
function saveSpbuLocation(id) {
|
||
const layer = featureLayers.spbu[id];
|
||
if (!layer || !layer.getLatLng) return;
|
||
const latlng = layer.getLatLng();
|
||
const fd = new FormData();
|
||
fd.append('id', id);
|
||
fd.append('type', 'spbu');
|
||
fd.append('latitude', latlng.lat);
|
||
fd.append('longitude', latlng.lng);
|
||
if (layer.dragging) layer.dragging.disable();
|
||
hideLocationEditPanel();
|
||
fetch('update_lokasi.php', { method:'POST', body:fd })
|
||
.then(response => response.text())
|
||
.then(result => {
|
||
if (result.trim() === 'success') {
|
||
showToast('Lokasi SPBU berhasil disimpan.', 'success');
|
||
} else {
|
||
showToast('Gagal menyimpan lokasi SPBU.', 'error');
|
||
}
|
||
setTimeout(() => location.reload(), 800);
|
||
});
|
||
}
|
||
function cancelSpbuLocation(id) {
|
||
const layer = featureLayers.spbu[id];
|
||
if (!layer || !layer.getLatLng) return;
|
||
if (layer._originalLatLng) {
|
||
layer.setLatLng(layer._originalLatLng);
|
||
}
|
||
if (layer.dragging) layer.dragging.disable();
|
||
layer.closePopup();
|
||
}
|
||
function editFeatureLocation(type, id) {
|
||
const item = getFeatureItem(type, id);
|
||
if (!item) return;
|
||
if (type === 'spbu') {
|
||
return openSpbuLocationEditor(featureLayers.spbu[id], item);
|
||
}
|
||
if (type === 'jalan') {
|
||
return openJalanLocationEditor(featureLayers.jalan[id], item);
|
||
}
|
||
if (type === 'parsil') {
|
||
return openParsilLocationEditor(featureLayers.parsil[id], item);
|
||
}
|
||
let fd = new FormData();
|
||
fd.append('id', id);
|
||
fd.append('type', type);
|
||
const geo = JSON.parse(item.geom);
|
||
let initial = '';
|
||
if (type === 'jalan') {
|
||
initial = geo.coordinates.map(coord => `${coord[1]},${coord[0]}`).join(';');
|
||
} else {
|
||
initial = geo.coordinates[0].map(coord => `${coord[1]},${coord[0]}`).join(';');
|
||
}
|
||
const coordsString = prompt('Ubah koordinat ke format lat,lng;lat,lng;...', initial);
|
||
if (coordsString === null) return;
|
||
const coords = coordsString.split(';').map(pair => pair.trim()).filter(Boolean).map(pair => {
|
||
const [lat, lng] = pair.split(',').map(Number);
|
||
return [lng, lat];
|
||
});
|
||
if (!coords.length) return;
|
||
if (type === 'parsil') {
|
||
const first = coords[0];
|
||
const last = coords[coords.length - 1];
|
||
if (first[0] !== last[0] || first[1] !== last[1]) coords.push(first);
|
||
fd.append('geom', JSON.stringify({ type:'Polygon', coordinates:[coords] }));
|
||
const area = Math.round(L.GeometryUtil.geodesicArea(coords.map(c => L.latLng(c[1], c[0]))));
|
||
fd.append('luas', area);
|
||
} else {
|
||
fd.append('geom', JSON.stringify({ type:'LineString', coordinates:coords }));
|
||
const latlngs = coords.map(c => L.latLng(c[1], c[0]));
|
||
const panjang = Math.round(calcLength(latlngs));
|
||
fd.append('panjang', panjang);
|
||
}
|
||
fetch('update_lokasi.php', { method:'POST', body:fd }).then(() => location.reload());
|
||
}
|
||
function deleteFeature(type, id) {
|
||
if (!confirm('Yakin ingin menghapus fitur ini?')) return;
|
||
if (type === 'spbu') return hapusPoint(id);
|
||
if (type === 'jalan') return hapusJalan(id);
|
||
if (type === 'parsil') return hapusParsil(id);
|
||
}
|
||
function editSelected() {
|
||
if (!selectedFeature || !selectedType) return;
|
||
editFeatureData(selectedType, selectedFeature.id);
|
||
}
|
||
function editLocation() {
|
||
if (!selectedFeature || !selectedType) return;
|
||
editFeatureLocation(selectedType, selectedFeature.id);
|
||
}
|
||
function deleteSelected() {
|
||
if (!selectedFeature || !selectedType) return;
|
||
deleteFeature(selectedType, selectedFeature.id);
|
||
}
|
||
|
||
function calcLength(latlngs) {
|
||
let total = 0;
|
||
for (let i = 0; i < latlngs.length - 1; i++) {
|
||
total += latlngs[i].distanceTo(latlngs[i + 1]);
|
||
}
|
||
return total;
|
||
}
|
||
function openPointPopup(marker, latlng) {
|
||
const popupContent = formTambah(latlng.lat, latlng.lng);
|
||
marker.bindPopup(popupContent, { closeButton: true, minWidth: 280 }).openPopup();
|
||
setTimeout(() => {
|
||
const form = document.getElementById('formTambah');
|
||
if (!form) return;
|
||
form.addEventListener('submit', function(ev) {
|
||
ev.preventDefault();
|
||
fetch('simpan.php', { method:'POST', body:new FormData(this) }).then(() => location.reload());
|
||
});
|
||
}, 50);
|
||
}
|
||
function formJalan(geojson, panjang) {
|
||
return `
|
||
<div class="popup-form">
|
||
<p style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#94a3b8;margin-bottom:12px">Tambah Jalan</p>
|
||
<form id="formJalan" class="space-y-3 text-sm text-on-surface">
|
||
<div>
|
||
<label class="block text-[11px] text-outline uppercase font-bold mb-1">Nama Jalan</label>
|
||
<input name="nama_jalan" required class="w-full rounded-xl border border-outline-variant/20 bg-surface-container-low px-3 py-2 text-on-surface" />
|
||
</div>
|
||
<div>
|
||
<label class="block text-[11px] text-outline uppercase font-bold mb-1">Status Jalan</label>
|
||
<select name="status_jalan" class="w-full rounded-xl border border-outline-variant/20 bg-surface-container-low px-3 py-2 text-on-surface">
|
||
<option>Nasional</option>
|
||
<option>Provinsi</option>
|
||
<option>Kabupaten</option>
|
||
</select>
|
||
</div>
|
||
<div class="grid grid-cols-2 gap-2 text-[11px] text-on-surface-variant">
|
||
<div class="rounded-xl bg-surface-container-low px-3 py-2">
|
||
<p class="font-medium">Panjang</p>
|
||
<p class="mt-1 text-sm">${panjang} m</p>
|
||
</div>
|
||
</div>
|
||
<input type="hidden" name="geom" value='${JSON.stringify(geojson.geometry)}' />
|
||
<input type="hidden" name="panjang" value="${panjang}" />
|
||
<button type="submit" class="w-full bg-primary text-surface py-3 rounded-xl font-bold">Simpan Jalan</button>
|
||
</form>
|
||
</div>`;
|
||
}
|
||
function formParsil(geojson, luas) {
|
||
return `
|
||
<div class="popup-form">
|
||
<p style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#94a3b8;margin-bottom:12px">Tambah Parsil</p>
|
||
<form id="formParsil" class="space-y-3 text-sm text-on-surface">
|
||
<div>
|
||
<label class="block text-[11px] text-outline uppercase font-bold mb-1">Nama Pemilik</label>
|
||
<input name="nama_pemilik" required class="w-full rounded-xl border border-outline-variant/20 bg-surface-container-low px-3 py-2 text-on-surface" />
|
||
</div>
|
||
<div>
|
||
<label class="block text-[11px] text-outline uppercase font-bold mb-1">Status</label>
|
||
<select name="status" class="w-full rounded-xl border border-outline-variant/20 bg-surface-container-low px-3 py-2 text-on-surface">
|
||
<option>SHM</option>
|
||
<option>HGB</option>
|
||
<option>HGU</option>
|
||
<option>HP</option>
|
||
</select>
|
||
</div>
|
||
<div class="rounded-xl bg-surface-container-low px-3 py-2 text-[11px] text-on-surface-variant">
|
||
<p class="font-medium">Luas</p>
|
||
<p class="mt-1 text-sm">${luas} m²</p>
|
||
</div>
|
||
<input type="hidden" name="geom" value='${JSON.stringify(geojson.geometry)}' />
|
||
<input type="hidden" name="luas" value="${luas}" />
|
||
<button type="submit" class="w-full bg-primary text-surface py-3 rounded-xl font-bold">Simpan Parsil</button>
|
||
</form>
|
||
</div>`;
|
||
}
|
||
function openJalanPopup(layer, geojson, panjang) {
|
||
if (currentDrawer && currentDrawer.disable) currentDrawer.disable();
|
||
currentDrawer = null;
|
||
const center = layer.getBounds ? layer.getBounds().getCenter() : layer.getLatLngs()[0];
|
||
const popupContent = formJalan(geojson, panjang);
|
||
layer.bindPopup(popupContent, { closeButton: true, minWidth: 300 }).openPopup(center);
|
||
setTimeout(() => {
|
||
const form = document.getElementById('formJalan');
|
||
if (!form) return;
|
||
form.addEventListener('submit', function(ev) {
|
||
ev.preventDefault();
|
||
fetch('simpan_jalan.php', { method:'POST', body:new FormData(this) }).then(() => location.reload());
|
||
});
|
||
}, 50);
|
||
}
|
||
function openParsilPopup(layer, geojson, luas) {
|
||
const center = layer.getBounds ? layer.getBounds().getCenter() : layer.getLatLngs()[0][0];
|
||
const popupContent = formParsil(geojson, luas);
|
||
layer.bindPopup(popupContent, { closeButton: true, minWidth: 300 }).openPopup(center);
|
||
setTimeout(() => {
|
||
const form = document.getElementById('formParsil');
|
||
if (!form) return;
|
||
form.addEventListener('submit', function(ev) {
|
||
ev.preventDefault();
|
||
fetch('simpan_parsil.php', { method:'POST', body:new FormData(this) }).then(() => location.reload());
|
||
});
|
||
}, 50);
|
||
}
|
||
function openPointPopup(layer, latlng) {
|
||
const popupContent = formTambah(latlng.lat.toFixed(7), latlng.lng.toFixed(7));
|
||
layer.bindPopup(popupContent, { closeButton: true, minWidth: 300 }).openPopup(latlng);
|
||
setTimeout(() => {
|
||
const form = document.getElementById('formTambah');
|
||
if (!form) return;
|
||
form.addEventListener('submit', function(ev) {
|
||
ev.preventDefault();
|
||
const fd = new FormData(this);
|
||
fetch('simpan.php', { method: 'POST', body: fd })
|
||
.then(r => r.text())
|
||
.then(res => {
|
||
if (res.trim() === 'success') {
|
||
map.closePopup();
|
||
cancelPendingShape(false);
|
||
setMode(null);
|
||
loadData();
|
||
showToast('SPBU berhasil ditambahkan!', 'success');
|
||
} else {
|
||
showToast('Gagal menyimpan SPBU. Coba lagi.', 'error');
|
||
}
|
||
})
|
||
.catch(() => showToast('Error koneksi ke server.', 'error'));
|
||
});
|
||
}, 50);
|
||
}
|
||
map.on('click', function(e) {
|
||
if (mode !== 'point') return;
|
||
if (pendingLayer) {
|
||
drawnItems.removeLayer(pendingLayer);
|
||
pendingLayer = null;
|
||
}
|
||
pendingLayer = L.marker(e.latlng).addTo(drawnItems);
|
||
openPointPopup(pendingLayer, e.latlng);
|
||
});
|
||
map.on(L.Draw.Event.CREATED, function(e) {
|
||
const layer = e.layer;
|
||
const geojson = layer.toGeoJSON();
|
||
if (mode === 'jalan' && e.layerType === 'polyline') {
|
||
if (pendingLayer) drawnItems.removeLayer(pendingLayer);
|
||
pendingLayer = layer;
|
||
drawnItems.addLayer(pendingLayer);
|
||
const latlngs = layer.getLatLngs();
|
||
let panjang = 0;
|
||
for (let i = 0; i < latlngs.length - 1; i++) panjang += latlngs[i].distanceTo(latlngs[i+1]);
|
||
openJalanPopup(layer, geojson, Math.round(panjang));
|
||
}
|
||
if (mode === 'parsil' && e.layerType === 'polygon') {
|
||
if (pendingLayer) drawnItems.removeLayer(pendingLayer);
|
||
pendingLayer = layer;
|
||
drawnItems.addLayer(pendingLayer);
|
||
const luas = L.GeometryUtil.geodesicArea(layer.getLatLngs()[0]);
|
||
openParsilPopup(layer, geojson, Math.round(luas));
|
||
}
|
||
});
|
||
function renderList(type, data) {
|
||
const list = document.getElementById(`list-${type}`);
|
||
if (!list) return;
|
||
list.innerHTML = data.map(item => {
|
||
const title = type === 'spbu' ? item.nama_spbu : type === 'jalan' ? item.nama_jalan : item.nama_pemilik;
|
||
return `<div class="group bg-surface-container-low/50 hover:bg-surface-container-high/80 p-3 rounded-xl cursor-pointer transition-all duration-300 border border-transparent hover:border-outline-variant/20" onclick="flyToFeature('${type}', ${item.id})">
|
||
<div class="flex items-center gap-3">
|
||
<div class="w-10 h-10 rounded-lg bg-secondary/10 flex items-center justify-center text-secondary">
|
||
<span class="material-symbols-outlined text-lg">${type === 'spbu' ? 'local_gas_station' : type === 'jalan' ? 'route' : 'grid_view'}</span>
|
||
</div>
|
||
<div class="flex-1">
|
||
<p class="text-sm font-semibold text-on-surface">${title}</p>
|
||
<p class="text-[10px] text-outline">${type === 'spbu' ? (item.buka_24jam === 'Ya' ? 'SPBU 24 Jam' : 'SPBU Tidak 24 Jam') : type === 'jalan' ? item.status_jalan : item.status}</p>
|
||
</div>
|
||
<span class="material-symbols-outlined text-outline">chevron_right</span>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
}
|
||
function flyToFeature(type, id) {
|
||
const source = type === 'spbu' ? spbuData : type === 'jalan' ? jalanData : parsilData;
|
||
const item = source.find(v => Number(v.id) === Number(id));
|
||
if (!item) return;
|
||
const layer = featureLayers[type]?.[id];
|
||
if (type === 'spbu') {
|
||
map.flyTo([item.latitude, item.longitude], 15, { duration: 0.8 });
|
||
} else {
|
||
const geo = JSON.parse(item.geom);
|
||
map.fitBounds(L.geoJSON(geo).getBounds(), { padding: [40, 40] });
|
||
}
|
||
selectFeature(type, id, layer);
|
||
if (layer && layer.openPopup) {
|
||
layer.openPopup();
|
||
}
|
||
}
|
||
function hapusPoint(id) { fetch('hapus.php', { method:'POST', body:new URLSearchParams({id}) }).then(() => location.reload()); }
|
||
function hapusJalan(id) { fetch('hapus_jalan.php', { method:'POST', body:new URLSearchParams({id}) }).then(() => location.reload()); }
|
||
function hapusParsil(id) { fetch('hapus_parsil.php', { method:'POST', body:new URLSearchParams({id}) }).then(() => location.reload()); }
|
||
function showToast(message, type = 'info') {
|
||
const container = document.getElementById('toast-container');
|
||
if (!container) return;
|
||
const colors = {
|
||
info: 'bg-slate-900 text-white',
|
||
success: 'bg-emerald-600 text-white',
|
||
warning: 'bg-amber-500 text-slate-900',
|
||
error: 'bg-red-600 text-white'
|
||
};
|
||
const toast = document.createElement('div');
|
||
toast.className = `pointer-events-auto rounded-2xl px-4 py-3 shadow-2xl shadow-black/20 ${colors[type] || colors.info}`;
|
||
toast.innerHTML = `<div class="text-sm font-medium">${message}</div>`;
|
||
container.appendChild(toast);
|
||
setTimeout(() => {
|
||
toast.remove();
|
||
}, 3000);
|
||
}
|
||
function updateLatLngDisplay() {
|
||
const center = map.getCenter();
|
||
document.getElementById('latlng-display').textContent = `LAT: ${center.lat.toFixed(4)} • LONG: ${center.lng.toFixed(4)} • ZOOM: ${map.getZoom()}z`;
|
||
}
|
||
map.on('moveend', updateLatLngDisplay);
|
||
map.on('zoomend', updateLatLngDisplay);
|
||
|
||
const iconHijau = L.icon({
|
||
iconUrl: 'https://maps.google.com/mapfiles/ms/icons/green-dot.png',
|
||
iconSize: [32, 32],
|
||
iconAnchor: [16, 32]
|
||
});
|
||
|
||
const iconMerah = L.icon({
|
||
iconUrl: 'https://maps.google.com/mapfiles/ms/icons/red-dot.png',
|
||
iconSize: [32, 32],
|
||
iconAnchor: [16, 32]
|
||
});
|
||
|
||
function loadData() {
|
||
// Clear existing SPBU layers
|
||
layers.spbu24.clearLayers();
|
||
layers.spbuNo24.clearLayers();
|
||
featureLayers.spbu = {};
|
||
|
||
Promise.all([
|
||
fetch('ambil_data.php').then(r => r.json()),
|
||
fetch('ambil_jalan.php').then(r => r.json()),
|
||
fetch('ambil_parsil.php').then(r => r.json())
|
||
]).then(([spbu, jalan, parsil]) => {
|
||
spbuData = spbu;
|
||
jalanData = jalan;
|
||
parsilData = parsil;
|
||
const spbu24 = spbu.filter(item => item.buka_24jam === 'Ya');
|
||
const spbuNo24 = spbu.filter(item => item.buka_24jam !== 'Ya');
|
||
document.getElementById('count-spbu').textContent = spbu.length;
|
||
document.getElementById('count-jalan').textContent = jalan.length;
|
||
document.getElementById('count-parsil').textContent = parsil.length;
|
||
|
||
spbu24.forEach(item => {
|
||
const marker = L.marker([parseFloat(item.latitude), parseFloat(item.longitude)], { icon: iconHijau }).addTo(layers.spbu24);
|
||
featureLayers.spbu[item.id] = marker;
|
||
marker.on('click', () => selectFeature('spbu', item.id, marker));
|
||
marker.bindPopup(getFeaturePopup('spbu', item));
|
||
});
|
||
spbuNo24.forEach(item => {
|
||
const marker = L.marker([parseFloat(item.latitude), parseFloat(item.longitude)], { icon: iconMerah }).addTo(layers.spbuNo24);
|
||
featureLayers.spbu[item.id] = marker;
|
||
marker.on('click', () => selectFeature('spbu', item.id, marker));
|
||
marker.bindPopup(getFeaturePopup('spbu', item));
|
||
});
|
||
|
||
renderList('spbu', spbu);
|
||
renderList('jalan', jalan);
|
||
renderList('parsil', parsil);
|
||
const recentItems = [];
|
||
spbu.slice(-2).reverse().forEach(item => recentItems.push({ type:'spbu', id:item.id, title:item.nama_spbu, subtitle:item.buka_24jam === 'Ya' ? 'SPBU 24 Jam' : 'SPBU Tidak 24 Jam'}));
|
||
jalan.slice(-2).reverse().forEach(item => recentItems.push({ type:'jalan', id:item.id, title:item.nama_jalan, subtitle:item.status_jalan }));
|
||
parsil.slice(-1).reverse().forEach(item => recentItems.push({ type:'parsil', id:item.id, title:item.nama_pemilik, subtitle:item.status }));
|
||
document.getElementById('recent-assets').innerHTML = recentItems.map(item => `
|
||
<div class="group bg-surface-container-low/50 hover:bg-surface-container-high/80 p-3 rounded-xl cursor-pointer transition-all duration-300 border border-transparent hover:border-outline-variant/20" onclick="flyToFeature('${item.type}', ${item.id})">
|
||
<div class="flex items-center gap-3">
|
||
<div class="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center text-primary">
|
||
<span class="material-symbols-outlined text-lg">${item.type === 'spbu' ? 'local_gas_station' : item.type === 'jalan' ? 'route' : 'grid_view'}</span>
|
||
</div>
|
||
<div class="flex-1">
|
||
<p class="text-sm font-semibold text-on-surface">${item.title}</p>
|
||
<p class="text-[10px] text-outline">Updated recently • ${item.type === 'spbu' ? 'SPBU' : item.type === 'jalan' ? 'Jalan' : 'Parsil'}</p>
|
||
</div>
|
||
<span class="material-symbols-outlined text-outline group-hover:text-primary transition-colors">chevron_right</span>
|
||
</div>
|
||
</div>`).join('');
|
||
});
|
||
}
|
||
|
||
Promise.all([
|
||
fetch('ambil_data.php').then(r => r.json()),
|
||
fetch('ambil_jalan.php').then(r => r.json()),
|
||
fetch('ambil_parsil.php').then(r => r.json()),
|
||
fetch('Admin_Kecamatan.json').then(r => r.json())
|
||
]).then(([spbu, jalan, parsil, kecamatan]) => {
|
||
spbuData = spbu;
|
||
jalanData = jalan;
|
||
parsilData = parsil;
|
||
const spbu24 = spbu.filter(item => item.buka_24jam === 'Ya');
|
||
const spbuNo24 = spbu.filter(item => item.buka_24jam !== 'Ya');
|
||
document.getElementById('count-spbu').textContent = spbu.length;
|
||
document.getElementById('count-jalan').textContent = jalan.length;
|
||
document.getElementById('count-parsil').textContent = parsil.length;
|
||
const countSpbu24 = document.getElementById('count-spbu24');
|
||
if (countSpbu24) countSpbu24.textContent = spbu24.length;
|
||
|
||
const bounds = L.latLngBounds([]);
|
||
|
||
spbu24.forEach(item => {
|
||
const marker = L.marker(
|
||
[parseFloat(item.latitude), parseFloat(item.longitude)],
|
||
{ icon: iconHijau }
|
||
).addTo(layers.spbu24);
|
||
|
||
featureLayers.spbu[item.id] = marker;
|
||
marker.on('click', () => selectFeature('spbu', item.id, marker));
|
||
marker.bindPopup(getFeaturePopup('spbu', item));
|
||
bounds.extend([parseFloat(item.latitude), parseFloat(item.longitude)]);
|
||
});
|
||
|
||
spbuNo24.forEach(item => {
|
||
const marker = L.marker(
|
||
[parseFloat(item.latitude), parseFloat(item.longitude)],
|
||
{ icon: iconMerah }
|
||
).addTo(layers.spbuNo24);
|
||
|
||
featureLayers.spbu[item.id] = marker;
|
||
marker.on('click', () => selectFeature('spbu', item.id, marker));
|
||
marker.bindPopup(getFeaturePopup('spbu', item));
|
||
bounds.extend([parseFloat(item.latitude), parseFloat(item.longitude)]);
|
||
});
|
||
jalan.forEach(item => {
|
||
const geo = JSON.parse(item.geom);
|
||
const layer = L.geoJSON(geo, {
|
||
style: { color: item.status_jalan === 'Nasional' ? '#ef4444' : item.status_jalan === 'Provinsi' ? '#2563eb' : '#10b981', weight: 5 },
|
||
onEachFeature: function(feature, featureLayer) {
|
||
featureLayers.jalan[item.id] = featureLayer;
|
||
featureLayer.on('click', () => selectFeature('jalan', item.id, featureLayer));
|
||
featureLayer.bindPopup(getFeaturePopup('jalan', item));
|
||
}
|
||
}).addTo(layers.jalan);
|
||
if (layer.getBounds && layer.getBounds().isValid()) bounds.extend(layer.getBounds());
|
||
});
|
||
parsil.forEach(item => {
|
||
const geo = JSON.parse(item.geom);
|
||
const layer = L.geoJSON(geo, {
|
||
style: { color: '#d2bbff', fillOpacity: 0.16, weight: 3 },
|
||
onEachFeature: function(feature, featureLayer) {
|
||
featureLayers.parsil[item.id] = featureLayer;
|
||
featureLayer.on('click', () => selectFeature('parsil', item.id, featureLayer));
|
||
featureLayer.bindPopup(getFeaturePopup('parsil', item));
|
||
}
|
||
}).addTo(layers.parsil);
|
||
if (layer.getBounds && layer.getBounds().isValid()) bounds.extend(layer.getBounds());
|
||
});
|
||
kecamatanData = (kecamatan.features || []).map(f => convertUtmFeature(f, 49, true));
|
||
const kecamatanPalette = ['#f0abfc', '#fb7185', '#facc15', '#4ade80', '#38bdf8', '#fdba74', '#a78bfa'];
|
||
kecamatanData.forEach((feature, index) => {
|
||
const layer = L.geoJSON(feature, {
|
||
style: {
|
||
color: '#1f2937',
|
||
weight: 2,
|
||
fillColor: kecamatanPalette[index % kecamatanPalette.length],
|
||
fillOpacity: 0.24,
|
||
dashArray: '4 4'
|
||
},
|
||
onEachFeature: function(_, featureLayer) {
|
||
featureLayers.kecamatan[index] = featureLayer;
|
||
const label = feature.properties?.Ket || feature.properties?.name || 'Kecamatan';
|
||
featureLayer.bindPopup(`<div class="text-sm font-semibold">${label}</div>`);
|
||
}
|
||
}).addTo(layers.kecamatan);
|
||
if (layer.getBounds && layer.getBounds().isValid()) bounds.extend(layer.getBounds());
|
||
});
|
||
if (bounds.isValid()) {
|
||
map.fitBounds(bounds, { padding: [40, 40] });
|
||
} else {
|
||
map.setView([-0.0284, 109.3350], 13);
|
||
}
|
||
renderList('spbu', spbu);
|
||
renderList('jalan', jalan);
|
||
renderList('parsil', parsil);
|
||
const recentItems = [];
|
||
spbu.slice(-2).reverse().forEach(item => recentItems.push({ type:'spbu', id:item.id, title:item.nama_spbu, subtitle:item.buka_24jam === 'Ya' ? 'SPBU 24 Jam' : 'SPBU Tidak 24 Jam'}));
|
||
jalan.slice(-2).reverse().forEach(item => recentItems.push({ type:'jalan', id:item.id, title:item.nama_jalan, subtitle:item.status_jalan }));
|
||
parsil.slice(-1).reverse().forEach(item => recentItems.push({ type:'parsil', id:item.id, title:item.nama_pemilik, subtitle:item.status }));
|
||
document.getElementById('recent-assets').innerHTML = recentItems.map(item => `
|
||
<div class="group bg-surface-container-low/50 hover:bg-surface-container-high/80 p-3 rounded-xl cursor-pointer transition-all duration-300 border border-transparent hover:border-outline-variant/20" onclick="flyToFeature('${item.type}', ${item.id})">
|
||
<div class="flex items-center gap-3">
|
||
<div class="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center text-primary">
|
||
<span class="material-symbols-outlined text-lg">${item.type === 'spbu' ? 'local_gas_station' : item.type === 'jalan' ? 'route' : 'grid_view'}</span>
|
||
</div>
|
||
<div class="flex-1">
|
||
<p class="text-sm font-semibold text-on-surface">${item.title}</p>
|
||
<p class="text-[10px] text-outline">Updated recently • ${item.type === 'spbu' ? 'SPBU' : item.type === 'jalan' ? 'Jalan' : 'Parsil'}</p>
|
||
</div>
|
||
<span class="material-symbols-outlined text-outline group-hover:text-primary transition-colors">chevron_right</span>
|
||
</div>
|
||
</div>`).join('');
|
||
updateLatLngDisplay();
|
||
});
|
||
setMode(null);
|
||
</script>
|
||
</body>
|
||
</html>
|