Files
UAS_WebGIS_MuthiaNurainiAst…/parsiljalan/index.php
T
2026-06-15 21:53:17 +07:00

1416 lines
104 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
$mode = (isset($_GET['mode']) && $_GET['mode'] === 'input') ? 'input' : 'view';
if (isset($_GET['action']) || isset($_POST['action'])) { include 'api.php'; exit; }
include 'db2.php';
require_once 'includes/stats.php';
$s = getStats($conn);
?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Sistem Informasi Infrastruktur Jalan — Pontianak</title>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/jalan.css"/>
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css"/>
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<style>
:root{
--blue:#2563eb;--blue-dk:#1d4ed8;--blue-lt:#eff6ff;
--green:#16a34a;--red:#dc2626;--violet:#7c3aed;
--border:#e5e7eb;--text:#111827;--muted:#6b7280;
--radius:12px;--tb-h:62px;--sp-w:340px;
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;font-family:'DM Sans',sans-serif;color:var(--text);overflow:hidden}
body{display:flex}
#content-area{display:flex;flex:1;overflow:hidden;min-width:0}
#map-wrap{flex:1;position:relative;min-width:0}
#map{height:100%;width:100%}
/* sidebar - light theme */
#sidebar{width:320px;min-width:320px;background:#fff;border-right:1px solid #e2e8f0;display:flex;flex-direction:column;z-index:200;box-shadow: 2px 0 8px rgba(0,0,0,.04);}
.sb-head{padding:18px 16px 14px;border-bottom:1px solid #e2e8f0;background:#ffffff;color:#0f172a}
.sb-title h1{font-size:14px;font-weight:700;color:#0f172a;line-height:1.2}
.sb-title p{font-size:10.5px;color:#94a3b8;margin-top:2px}
.sb-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-bottom:4px}
.sb-stat{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:8px 6px;text-align:center}
.sb-stat b{display:block;font-size:16px;font-weight:800;font-family:'DM Mono',monospace;color:#1e293b;line-height:1}
.sb-stat span{font-size:8.5px;color:#94a3b8;text-transform:uppercase;margin-top:4px;display:block;font-weight:600;letter-spacing:.3px}
.sb-stat:nth-child(1) b{color:#1d4ed8}
.sb-stat:nth-child(2) b{color:#ca8a04}
.sb-stat:nth-child(3) b{color:#dc2626}
.sb-nav{display:flex;padding:10px 12px 0;gap:4px;border-bottom:1px solid #e2e8f0}
.sb-nav a{flex:1;padding:8px;text-align:center;font-size:12px;font-weight:600;color:#475569;text-decoration:none;border-radius:7px 7px 0 0;border:1px solid transparent;transition:all .15s}
.sb-nav a:hover{color:#0f172a;background:#f8fafc}
.sb-nav a.active{color:#1d4ed8;background:#fff;border-color:#e2e8f0;margin-bottom:-1px;font-weight:700}
.sb-section-lbl {
font-size: 9.5px; font-weight: 700; color: #94a3b8;
text-transform: uppercase; letter-spacing: .8px; padding: 10px 8px 4px;
}
.sb-item {
display: flex; align-items: center; gap: 9px; padding: 8px 10px;
border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 500;
color: #475569; transition: all .15s; border: 1px solid transparent; text-decoration: none;
}
.sb-item:hover { background: #f1f5f9; color: #0f172a; }
.sb-item.active { background: #eff6ff; color: #1d4ed8; border-color: #bfdbfe; font-weight: 600; }
.sb-item .ico { font-size: 16px; width: 20px; text-align: center; flex-shrink: 0; display: flex; align-items: center; justify-content: center; }
.lnav-search{padding:12px;border-bottom:1px solid #e2e8f0}
.lnav-search input{width:100%;padding:9px 12px 9px 34px;border:1px solid #e2e8f0;border-radius:8px;font-size:13px;font-family:inherit;outline:none;background:#f8fafc url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' fill='%2394a3b8' viewBox='0 0 16 16'%3E%3Cpath d='M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85zM6.5 12a5.5 5.5 0 1 1 0-11 5.5 5.5 0 0 1 0 11z'/%3E%3C/svg%3E") no-repeat 11px center}
.lnav-search input:focus{border-color:#1d4ed8;background-color:#fff}
.lnav-filters{padding:10px 12px;border-bottom:1px solid #e2e8f0;display:flex;flex-direction:column;gap:10px;flex-shrink:0;max-height:220px;overflow-y:auto}
.lnav-filters::-webkit-scrollbar{width:3px}
.lnav-filters::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:3px}
.filter-block{display:flex;flex-direction:column;gap:6px}
.filter-label{font-size:10px;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em}
.filter-seg{display:grid;grid-template-columns:repeat(4,1fr);gap:4px}
.filter-seg-btn{
padding:7px 4px;border-radius:7px;font-size:11px;font-weight:600;cursor:pointer;
border:1px solid #e2e8f0;background:#f8fafc;color:#64748b;font-family:inherit;
transition:all .12s;text-align:center;
}
.filter-seg-btn:hover{background:#fff;border-color:#cbd5e1}
.filter-seg-btn.active{background:#1d4ed8;border-color:#1d4ed8;color:#fff}
.filter-chips{display:flex;flex-wrap:wrap;gap:5px}
.filter-chip{
display:inline-flex;align-items:center;gap:5px;padding:5px 9px;border-radius:7px;
font-size:11px;font-weight:600;cursor:pointer;border:1px solid #e2e8f0;
background:#fff;color:#475569;font-family:inherit;transition:all .12s;
}
.filter-chip:hover{background:#f8fafc}
.filter-chip.active{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}
.filter-chip .dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.lnav-panjang{display:flex;align-items:center;gap:6px}
.lnav-panjang span{font-size:11px;color:#94a3b8;white-space:nowrap}
.lnav-panjang input{width:68px;padding:5px 8px;border:1px solid #e2e8f0;border-radius:6px;font-size:12px;font-family:inherit;outline:none;background:#fff}
.lnav-panjang input:focus{border-color:#1d4ed8}
.lnav-view-body{display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden}
.lnav-list{flex:1;overflow-y:auto;padding:4px 0;min-height:0}
.lnav-list::-webkit-scrollbar{width:3px}
.lnav-list::-webkit-scrollbar-thumb{background:rgba(255,255,255,.12);border-radius:3px}
.lnav-item{display:flex;align-items:center;gap:9px;padding:10px 14px;cursor:pointer;border-bottom:1px solid #f1f5f9;transition:background .12s;border-left:3px solid transparent}
.lnav-item:hover{background:#f8fafc}
.lnav-item.active{background:#eff6ff;border-left-color:#1d4ed8}
.lnav-item-icon{font-size:14px;flex-shrink:0;width:20px;text-align:center}
.lnav-item-body{flex:1;min-width:0}
.lnav-item-name{font-size:13px;font-weight:600;color:#0f172a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.lnav-item-meta{font-size:11px;color:#94a3b8;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.lnav-item-badge{font-size:9.5px;font-weight:700;padding:2px 6px;border-radius:10px;white-space:nowrap;flex-shrink:0}
.lnav-badge-jalan{background:rgba(37,99,235,.25);color:#93c5fd}
.lnav-badge-parsil{background:rgba(124,58,237,.25);color:#c4b5fd}
.lnav-badge-rusak{background:rgba(249,115,22,.25);color:#fdba74}
.lnav-empty{padding:24px 14px;text-align:center;color:#94a3b8;font-size:13px}
.lnav-count{padding:6px 14px;font-size:11px;color:#94a3b8;border-bottom:1px solid #f1f5f9;font-weight:600}
.input-tools{padding:16px;display:flex;flex-direction:column;gap:12px;flex:1;overflow-y:auto}
.input-tools button.active-jalan{border-color:#1d4ed8 !important;background:#eff6ff !important;box-shadow:0 0 0 2px rgba(29,78,216,0.15) !important}
.input-tools button.active-parsil{border-color:#7c3aed !important;background:#f5f3ff !important;box-shadow:0 0 0 2px rgba(124,58,237,0.15) !important}
.input-tools button.active-rusak{border-color:#ea580c !important;background:#fff7ed !important;box-shadow:0 0 0 2px rgba(234,88,12,0.15) !important}
.draw-actions{display:flex;gap:6px;margin-top:8px}
.draw-actions button{flex:1;padding:8px;border-radius:7px;border:1px solid #cbd5e1;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit}
#topbar{
position:relative;z-index:1000;flex-shrink:0;
height:var(--tb-h);
background:#ffffff;
backdrop-filter:blur(20px) saturate(1.4);
-webkit-backdrop-filter:blur(20px) saturate(1.4);
border-bottom:1px solid #e2e8f0;
padding:0 16px;
display:flex;align-items:center;gap:9px;overflow-x:auto;
}
#topbar::-webkit-scrollbar{height:0}
.brand{display:flex;align-items:center;gap:9px;flex-shrink:0}
.brand-icon{width:34px;height:34px;border-radius:9px;background:linear-gradient(135deg,#3b82f6,#8b5cf6);display:flex;align-items:center;justify-content:center;font-size:16px;box-shadow:0 2px 10px rgba(99,102,241,.4)}
.brand-text{font-size:13.5px;font-weight:800;color:#1e293b;letter-spacing:-.4px}
.brand-sub{font-size:10px;color:#64748b;font-weight:500;margin-top:1px}
.sep{width:1px;height:24px;background:#e2e8f0;flex-shrink:0}
.search-wrap{position:relative;flex:1;max-width:290px;min-width:140px}
.search-ico{position:absolute;left:10px;top:50%;transform:translateY(-50%);width:14px;height:14px;pointer-events:none;opacity:.5}
#search-input{
width:100%;padding:7px 34px 7px 30px;
background:#f8fafc;border:1.5px solid #e2e8f0;
border-radius:9px;color:#1e293b;font-size:13px;font-family:'Inter',sans-serif;outline:none;transition:all .18s;
}
#search-input::placeholder{color:#94a3b8}
#search-input:focus{background:#ffffff;border-color:rgba(99,102,241,.65);box-shadow:0 0 0 3px rgba(99,102,241,.15)}
.search-filter-btn{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;color:rgba(148,163,184,.6);font-size:14px;padding:2px;transition:color .15s}
.search-filter-btn:hover{color:#a5b4fc}
.search-filter-btn.active{color:#818cf8}
#search-drop{
display:none;
position:fixed;
top:var(--tb-h);
width:380px;
background:#1e293b;
border:1px solid rgba(255,255,255,.12);
border-radius:0 0 12px 12px;
overflow:hidden;
box-shadow:0 10px 40px rgba(0,0,0,.55);
z-index:5000;
max-height:430px;
overflow-y:auto;
}
#search-drop.open{display:block}
#search-drop::-webkit-scrollbar{width:4px}
#search-drop::-webkit-scrollbar-thumb{background:rgba(255,255,255,.15);border-radius:4px}
.sf-panel{padding:10px 12px;background:rgba(255,255,255,.04);border-bottom:1px solid rgba(255,255,255,.08)}
.sf-row{display:flex;gap:5px;flex-wrap:wrap;margin-bottom:6px}
.sf-row:last-child{margin-bottom:0}
.sf-chip{padding:3px 10px;border-radius:20px;font-size:11px;font-weight:600;cursor:pointer;border:1.5px solid rgba(255,255,255,.12);background:rgba(255,255,255,.05);color:rgba(148,163,184,.8);transition:all .14s;white-space:nowrap}
.sf-chip:hover{background:rgba(255,255,255,.1);color:#cbd5e1}
.sf-chip.active{background:rgba(99,102,241,.25);border-color:#818cf8;color:#a5b4fc}
.sf-chip.jalan-nas.active{background:rgba(220,38,38,.2);border-color:#f87171;color:#fca5a5}
.sf-chip.jalan-pro.active{background:rgba(245,158,11,.2);border-color:#fbbf24;color:#fcd34d}
.sf-chip.jalan-kab.active{background:rgba(22,163,74,.2);border-color:#4ade80;color:#86efac}
.sf-chip.parsil-shm.active{background:rgba(245,158,11,.2);border-color:#fbbf24;color:#fcd34d}
.sf-chip.parsil-hgb.active{background:rgba(37,99,235,.2);border-color:#60a5fa;color:#93c5fd}
.sf-chip.parsil-hgu.active{background:rgba(22,163,74,.2);border-color:#4ade80;color:#86efac}
.sf-chip.parsil-hp.active{background:rgba(220,38,38,.2);border-color:#f87171;color:#fca5a5}
.sf-chip.rusak.active{background:rgba(249,115,22,.2);border-color:#fb923c;color:#fdba74}
.sf-range{display:flex;align-items:center;gap:6px;margin-top:4px}
.sf-range-label{font-size:11px;color:rgba(148,163,184,.65);white-space:nowrap}
.sf-range-input{width:80px;padding:4px 8px;background:rgba(255,255,255,.07);border:1.5px solid rgba(255,255,255,.1);border-radius:7px;color:#f1f5f9;font-size:12px;font-family:'Inter',sans-serif;outline:none}
.sf-range-input:focus{border-color:rgba(99,102,241,.6)}
.sr-item{display:flex;align-items:center;gap:9px;padding:9px 13px;cursor:pointer;border-bottom:1px solid rgba(255,255,255,.05);transition:background .12s}
.sr-item:last-child{border-bottom:none}
.sr-item:hover{background:rgba(255,255,255,.08)}
.sr-icon{font-size:15px;flex-shrink:0}
.sr-txt .sr-name{font-size:13px;font-weight:600;color:#f1f5f9}
.sr-txt .sr-meta{font-size:11px;color:rgba(148,163,184,.7);margin-top:1px}
.sr-badge{margin-left:auto;padding:2px 7px;border-radius:20px;font-size:10px;font-weight:700;white-space:nowrap;flex-shrink:0}
.sr-badge.jalan{background:rgba(37,99,235,.25);color:#93c5fd}
.sr-badge.parsil{background:rgba(124,58,237,.25);color:#c4b5fd}
.sr-badge.rusak{background:rgba(249,115,22,.25);color:#fdba74}
.sr-empty{padding:16px;text-align:center;color:rgba(148,163,184,.6);font-size:13px}
.stat-section{display:flex;align-items:center;gap:4px;flex-shrink:0}
.stat-label{font-size:9px;font-weight:700;color:rgba(148,163,184,.65);text-transform:uppercase;letter-spacing:.8px;white-space:nowrap;margin-right:1px}
.stat-pill{display:flex;align-items:center;gap:4px;padding:4px 9px;border-radius:20px;font-size:11px;font-weight:600;white-space:nowrap;border:1px solid;flex-shrink:0;transition:transform .1s;cursor:default}
.stat-pill:hover{transform:translateY(-1px)}
.pill-total{background:#f8fafc;border-color:#cbd5e1;color:#334155}
.pill-nas{background:#fef2f2;border-color:#fca5a5;color:#dc2626}
.pill-pro{background:#fffbeb;border-color:#fcd34d;color:#b45309}
.pill-kab{background:#f0fdf4;border-color:#86efac;color:#15803d}
.pill-shm{background:#fffbeb;border-color:#fcd34d;color:#b45309}
.pill-hgb{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}
.pill-hgu{background:#f0fdf4;border-color:#86efac;color:#15803d}
.pill-hp{background:#fef2f2;border-color:#fca5a5;color:#dc2626}
.pill-rusak{background:#fff7ed;border-color:#fdba74;color:#c2410c}
.pill-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
.pill-num{font-size:12.5px;font-weight:800}
.nav-link{display:flex;align-items:center;gap:6px;padding:6px 12px;border-radius:8px;border:1.5px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);color:#94a3b8;font-size:12.5px;font-weight:600;font-family:'Inter',sans-serif;text-decoration:none;transition:all .15s;white-space:nowrap;flex-shrink:0;margin-left:auto}
.nav-link:hover{background:rgba(255,255,255,.1);color:#cbd5e1}
#legend{
position:absolute;bottom:16px;left:16px;z-index:900;
background:#fff;border:1px solid #e2e8f0;border-radius:10px;
padding:10px 12px;box-shadow:0 4px 16px rgba(15,23,42,.1);
min-width:150px;max-width:180px;font-size:11px;
}
.leg-title{font-size:9px;font-weight:700;color:#94a3b8;text-transform:uppercase;letter-spacing:.06em;margin-bottom:5px}
.leg-title:not(:first-child){margin-top:8px}
.leg-row{display:flex;align-items:center;gap:7px;margin-bottom:4px;font-size:11px;font-weight:500;color:#475569}
.leg-line{width:20px;height:3px;border-radius:2px;flex-shrink:0}
.leg-swatch{width:10px;height:10px;border-radius:2px;flex-shrink:0;border:1.5px solid}
.leg-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;border:2px solid}
.leaflet-popup-content-wrapper{border-radius:14px!important;box-shadow:0 8px 36px rgba(0,0,0,.2)!important;border:1px solid var(--border)!important;padding:0!important;overflow:hidden!important}
.leaflet-popup-content{margin:0!important}
.leaflet-popup-tip{background:white!important}
.pw{font-family:'Inter',sans-serif;min-width:210px}
.pw-head{padding:13px 15px 9px}
.pw-title{font-size:13px;font-weight:700;color:#111827;margin-bottom:4px}
.pw-badge{display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:600;margin-bottom:7px}
.pw-row{font-size:12px;color:#6b7280;margin-bottom:3px}
.pw-row b{color:#111827}
.pw-foot{display:flex;border-top:1px solid #f0f0f0}
.pw-btn{flex:1;padding:9px 0;border:none;background:white;font-size:12px;font-weight:600;font-family:'Inter',sans-serif;cursor:pointer;transition:background .12s;color:#374151}
.pw-btn:not(:last-child){border-right:1px solid #f0f0f0}
.pw-btn:hover{background:#f9fafb}
.pw-btn.edit{color:#2563eb}.pw-btn.edit:hover{background:#eff6ff}
.pw-btn.danger{color:#dc2626}.pw-btn.danger:hover{background:#fef2f2}
#side-panel{display:none;position:fixed;inset:0;z-index:9999;background:rgba(15,23,42,0.4);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);align-items:center;justify-content:center;padding:20px}
#side-panel.open{display:flex}
.sp-card{background:#ffffff;border-radius:12px;box-shadow:0 20px 25px -5px rgba(0,0,0,0.1),0 10px 10px -5px rgba(0,0,0,0.04);width:100%;max-width:420px;max-height:90vh;display:flex;flex-direction:column;overflow:hidden;border:1px solid #e2e8f0;animation:spFadeIn 0.2s ease-out}
@keyframes spFadeIn{from{opacity:0;transform:scale(0.95)}to{opacity:1;transform:scale(1)}}
.geo-btn-mini {background:#f8fafc;color:#64748b;border:1.5px solid #e2e8f0;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;cursor:pointer;transition:all 0.15s;display:inline-flex;align-items:center;gap:4px;font-family:'Inter',sans-serif}
.geo-btn-mini:hover {background:#f1f5f9;color:#1e293b;border-color:#cbd5e1}
.geo-btn-mini.active {background:rgba(29,78,216,0.1);border-color:#1d4ed8;color:#1d4ed8}
.sp-head{padding:18px 20px 14px;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;gap:12px;flex-shrink:0}
.sp-icon{width:40px;height:40px;border-radius:11px;display:flex;align-items:center;justify-content:center;font-size:19px;flex-shrink:0}
.sp-icon.jalan{background:linear-gradient(135deg,rgba(37,99,235,.35),rgba(37,99,235,.1));border:1px solid rgba(59,130,246,.3)}
.sp-icon.parsil{background:linear-gradient(135deg,rgba(124,58,237,.35),rgba(124,58,237,.1));border:1px solid rgba(139,92,246,.3)}
.sp-icon.rusak{background:linear-gradient(135deg,rgba(249,115,22,.35),rgba(249,115,22,.1));border:1px solid rgba(251,146,60,.3)}
.sp-title{font-size:16px;font-weight:800;color:#1e293b;letter-spacing:-.3px}
.sp-sub{font-size:11.5px;color:#64748b;margin-top:2px}
.sp-close{margin-left:auto;width:30px;height:30px;border-radius:8px;background:#f1f5f9;border:1px solid #e2e8f0;color:#94a3b8;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s;flex-shrink:0}
.sp-close:hover{background:#e2e8f0;color:#1e293b}
.sp-tabs{display:flex;border-bottom:1px solid #e2e8f0;flex-shrink:0}
.sp-tab{flex:1;padding:11px;text-align:center;font-size:12.5px;font-weight:700;color:#94a3b8;cursor:pointer;transition:all .15s;border-bottom:2px solid transparent;margin-bottom:-1px}
.sp-tab:hover{color:#94a3b8}
.sp-tab.active{color:#818cf8;border-color:#818cf8}
.sp-body{flex:1;overflow-y:auto;padding:18px 20px}
.sp-body::-webkit-scrollbar{width:4px}
.sp-body::-webkit-scrollbar-thumb{background:rgba(255,255,255,.12);border-radius:4px}
.sp-field{margin-bottom:14px}
.sp-label{display:block;font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.3px;margin-bottom:6px}
.sp-input{width:100%;padding:9px 12px;background:#f8fafc;border:1.5px solid #e2e8f0;border-radius:9px;font-size:13.5px;font-family:'Inter',sans-serif;color:#1e293b;outline:none;transition:border-color .15s,box-shadow .15s,background .15s}
.sp-input::placeholder{color:#94a3b8}
.sp-input:focus{border-color:rgba(99,102,241,.7);background:#ffffff;box-shadow:0 0 0 3px rgba(99,102,241,.18)}
.sp-input[readonly]{background:#f1f5f9;color:#64748b;cursor:default}
.sp-input[readonly]:focus{border-color:#e2e8f0;box-shadow:none}
.sp-input option{background:#ffffff;color:#1e293b}
.sp-row{display:grid;grid-template-columns:1fr 1fr;gap:11px}
.sp-auto{float:right;font-size:10px;font-weight:700;color:#4ade80;background:rgba(74,222,128,.15);padding:1px 7px;border-radius:20px;text-transform:none;letter-spacing:0}
.geo-hint{background:rgba(99,102,241,.12);border:1px solid rgba(99,102,241,.25);border-radius:10px;padding:12px 14px;margin-bottom:14px;font-size:12px;color:#a5b4fc;line-height:1.6}
.geo-hint strong{color:#c7d2fe;display:block;margin-bottom:4px;font-size:12.5px}
.geo-toolbar{display:flex;flex-direction:column;gap:8px;margin-bottom:16px}
.geo-btn{display:flex;align-items:center;gap:9px;padding:10px 14px;border-radius:10px;background:#f8fafc;border:1.5px solid #e2e8f0;color:#64748b;font-size:13px;font-weight:600;font-family:'Inter',sans-serif;cursor:pointer;transition:all .15s;text-align:left;width:100%}
.geo-btn:hover{background:#f1f5f9;color:#1e293b;border-color:#cbd5e1}
.geo-btn.active{background:rgba(99,102,241,.2);border-color:#818cf8;color:#a5b4fc}
.geo-btn-icon{font-size:17px;flex-shrink:0}
.geo-btn-title{font-size:13px;font-weight:700}
.geo-btn-desc{font-size:11px;color:#94a3b8;margin-top:1px}
.geo-note{font-size:11.5px;color:#64748b;line-height:1.7;padding:2px}
.sp-foot{padding:14px 20px;border-top:1px solid #e2e8f0;display:flex;gap:9px;flex-shrink:0}
.sp-save{flex:1;background:linear-gradient(135deg,#3b82f6,#6366f1);color:white;border:none;padding:11px;border-radius:10px;cursor:pointer;font-size:13.5px;font-weight:700;font-family:'Inter',sans-serif;transition:all .15s;box-shadow:0 4px 14px rgba(99,102,241,.35)}
.sp-save:hover{box-shadow:0 6px 20px rgba(99,102,241,.5);transform:translateY(-1px)}
.sp-save:active{transform:none}
.sp-save:disabled{opacity:.5;cursor:default;transform:none;box-shadow:none}
.sp-cancel{padding:11px 18px;border-radius:10px;background:#f1f5f9;color:#64748b;border:1.5px solid #e2e8f0;cursor:pointer;font-size:13.5px;font-weight:600;font-family:'Inter',sans-serif;transition:all .15s}
.sp-cancel:hover{background:#e2e8f0;color:#1e293b}
#sp-foto-preview{width:100%;max-height:140px;object-fit:cover;border-radius:8px;margin-top:6px;display:none}
.photo-btns{display:flex;gap:8px;margin-top:8px}
.photo-btn{flex:1;padding:7px;border-radius:8px;border:1.5px solid #e2e8f0;background:#f8fafc;color:#64748b;font-size:11.5px;font-weight:600;font-family:'Inter',sans-serif;cursor:pointer;transition:all .15s;display:flex;align-items:center;justify-content:center;gap:5px}
.photo-btn:hover{background:#f1f5f9;color:#1e293b}
.leaflet-control-zoom{border:none!important;border-radius:10px!important;overflow:hidden;box-shadow:0 4px 16px rgba(0,0,0,.2)!important}
.leaflet-control-zoom a{background:#ffffff!important;color:#64748b!important;border:none!important;width:33px!important;height:33px!important;line-height:33px!important;font-size:16px!important;transition:background .12s,color .12s!important}
.leaflet-control-zoom a:hover{background:#f1f5f9!important;color:#1e293b!important}
.leaflet-control-zoom-in{border-bottom:1px solid #e2e8f0!important}
/* Input modals — light theme */
.modal-overlay{display:none;position:fixed;z-index:9999;inset:0;background:rgba(15,23,42,.45);backdrop-filter:blur(4px);justify-content:center;align-items:center;padding:16px}
.modal-overlay.active{display:flex}
.modal-card{background:#fff;border:1px solid #e2e8f0;border-radius:14px;padding:22px 22px 18px;min-width:min(420px,94vw);max-width:480px;max-height:90vh;overflow-y:auto;box-shadow:0 20px 50px rgba(15,23,42,.18);animation:mIn .2s ease}
@keyframes mIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
.modal-header{display:flex;align-items:center;gap:12px;margin-bottom:18px;padding-bottom:14px;border-bottom:1px solid #f1f5f9}
.modal-icon{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}
.modal-icon.jalan{background:#eff6ff;border:1px solid #bfdbfe}
.modal-icon.parsil{background:#f5f3ff;border:1px solid #ddd6fe}
.modal-icon.rusak{background:#fff7ed;border:1px solid #fed7aa}
.modal-title{font-size:16px;font-weight:700;color:#0f172a}
.modal-sub{font-size:12px;color:#64748b;margin-top:2px}
.field-group{margin-bottom:13px}
.field-label{display:block;font-size:11px;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.35px;margin-bottom:5px}
.field-input{width:100%;padding:10px 12px;border:1px solid #e2e8f0;border-radius:8px;font-size:14px;font-family:inherit;color:#0f172a;outline:none;background:#fff;transition:border-color .15s,box-shadow .15s}
.field-input::placeholder{color:#94a3b8}
.field-input:focus{border-color:#1d4ed8;box-shadow:0 0 0 3px rgba(29,78,216,.12)}
.field-input[readonly]{background:#f8fafc;color:#64748b}
.field-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.readonly-tag{float:right;font-size:10px;font-weight:600;color:#15803d;background:#f0fdf4;padding:2px 7px;border-radius:20px;text-transform:none;letter-spacing:0}
.modal-footer{display:flex;gap:8px;margin-top:18px}
.btn-primary{flex:1;background:#1d4ed8;color:#fff;border:none;padding:11px;border-radius:8px;cursor:pointer;font-size:14px;font-weight:600;font-family:inherit;transition:background .15s}
.btn-primary:hover{background:#1e40af}
.btn-primary:disabled{opacity:.55;cursor:default}
.btn-ghost{flex:1;background:#fff;color:#64748b;border:1px solid #e2e8f0;padding:11px;border-radius:8px;cursor:pointer;font-size:14px;font-weight:600;font-family:inherit}
.btn-ghost:hover{background:#f8fafc}
.photo-upload-area{border:2px dashed #e2e8f0;border-radius:10px;padding:14px;text-align:center;cursor:pointer;background:#f8fafc;transition:border-color .15s}
.photo-upload-area:hover{border-color:#93c5fd;background:#eff6ff}
.photo-upload-area .pu-icon{font-size:22px;margin-bottom:4px}
.photo-upload-area .pu-text{font-size:12px;color:#475569;font-weight:500}
.photo-upload-area .pu-sub{font-size:11px;color:#94a3b8;margin-top:2px}
#photo-preview{width:100%;max-height:150px;object-fit:cover;border-radius:8px;margin-top:8px;display:none}
.gps-badge{display:inline-flex;align-items:center;gap:4px;padding:3px 9px;border-radius:20px;font-size:11px;font-weight:600;background:#f0fdf4;color:#15803d;border:1px solid #bbf7d0;margin-top:5px}
.gps-badge.loading{background:#fffbeb;color:#b45309;border-color:#fde68a}
</style>
<script src="https://cdn.jsdelivr.net/npm/exif-js"></script>
</head>
<body>
<div id="content-area">
<aside id="sidebar">
<div class="sb-head">
<a href="index.php" style="display:flex; align-items:center; gap:10px; text-decoration:none; color:inherit">
<div class="sb-logo" style="width:38px; height:38px; border-radius:10px; background:linear-gradient(135deg, #1d4ed8, #60a5fa); display:flex; align-items:center; justify-content:center; flex-shrink:0; box-shadow:0 2px 8px rgba(29,78,216,.25)">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="#fff" stroke-width="2"><path d="M5 3v18M19 3v18M12 3v3M12 9v3M12 15v3"/></svg>
</div>
<div class="sb-title"><h1>Infrastruktur Jalan</h1><p>Sistem Informasi Geografis</p></div>
</a>
<!-- 2x2 Stats Grid -->
<div class="sb-stats" style="display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:12px">
<div class="sb-stat" style="background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; padding:9px 10px; text-align:center">
<div class="sb-stat-n" style="font-size:20px; font-weight:800; font-family:'DM Mono',monospace; line-height:1; color:#1d4ed8" id="s-tj"><?=$s['tj']?></div>
<div class="sb-stat-l" style="font-size:9.5px; color:#94a3b8; margin-top:3px; font-weight:600; text-transform:uppercase; letter-spacing:.4px">Jalan</div>
</div>
<div class="sb-stat" style="background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; padding:9px 10px; text-align:center">
<div class="sb-stat-n" style="font-size:20px; font-weight:800; font-family:'DM Mono',monospace; line-height:1; color:#ca8a04" id="s-tp"><?=$s['tp']?></div>
<div class="sb-stat-l" style="font-size:9.5px; color:#94a3b8; margin-top:3px; font-weight:600; text-transform:uppercase; letter-spacing:.4px">Kavling</div>
</div>
<div class="sb-stat" style="background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; padding:9px 10px; text-align:center">
<div class="sb-stat-n" style="font-size:20px; font-weight:800; font-family:'DM Mono',monospace; line-height:1; color:#dc2626" id="s-tr"><?=$s['tr']?></div>
<div class="sb-stat-l" style="font-size:9.5px; color:#94a3b8; margin-top:3px; font-weight:600; text-transform:uppercase; letter-spacing:.4px">Titik Rusak</div>
</div>
<div class="sb-stat" style="background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; padding:9px 10px; text-align:center">
<div class="sb-stat-n" style="font-size:20px; font-weight:800; font-family:'DM Mono',monospace; line-height:1; color:#7c3aed" id="s-total"><?=$s['tj'] + $s['tp'] + $s['tr']?></div>
<div class="sb-stat-l" style="font-size:9.5px; color:#94a3b8; margin-top:3px; font-weight:600; text-transform:uppercase; letter-spacing:.4px">Total Data</div>
</div>
</div>
</div>
<!-- Navigation Sections -->
<div class="sb-nav-container" style="padding:8px; display:flex; flex-direction:column; gap:2px; border-bottom:1px solid #e2e8f0">
<div class="sb-section-lbl">Aksi Admin</div>
<a href="index.php?mode=input" class="sb-item <?= $mode==='input'?'active':'' ?>">
<span class="ico"><svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg></span>
<span>Input Data Baru</span>
</a>
</div>
<?php if ($mode === 'view'): ?>
<div class="lnav-view-body" style="flex:1; display:flex; flex-direction:column; min-height:0; overflow:hidden">
<div class="lnav-search" style="padding:12px; border-bottom:1px solid #e2e8f0">
<input type="text" id="lnav-input" placeholder="Cari jalan, kavling, atau titik rusak…" autocomplete="off" style="width:100%; padding:9px 12px 9px 34px; border:1px solid #e2e8f0; border-radius:8px; font-size:13px; font-family:inherit; outline:none; background:#f8fafc url('data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'14\' height=\'14\' fill=\'%2394a3b8\' viewBox=\'0 0 16 16\'%3E%3Cpath d=\'M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85zM6.5 12a5.5 5.5 0 1 1 0-11 5.5 5.5 0 0 1 0 11z\'/%3E%3C/svg%3E') no-repeat 11px center">
</div>
<div class="lnav-filters">
<div class="filter-block">
<span class="filter-label">Jenis Data</span>
<div class="filter-seg">
<button type="button" class="filter-seg-btn active" data-group="tipe" data-val="semua" onclick="lnavChip(this)">Semua</button>
<button type="button" class="filter-seg-btn" data-group="tipe" data-val="jalan" onclick="lnavChip(this)">Jalan</button>
<button type="button" class="filter-seg-btn" data-group="tipe" data-val="parsil" onclick="lnavChip(this)">Kavling</button>
<button type="button" class="filter-seg-btn" data-group="tipe" data-val="rusak" onclick="lnavChip(this)">Rusak</button>
</div>
</div>
<div class="filter-block" id="lnav-jalan-block">
<span class="filter-label">Klasifikasi Jalan</span>
<div class="filter-chips" id="lnav-jalan-chips">
<button type="button" class="filter-chip active" data-group="status" data-val="" onclick="lnavChip(this)">Semua</button>
<button type="button" class="filter-chip" data-group="status" data-val="Jalan Nasional" onclick="lnavChip(this)"><span class="dot" style="background:#e63946"></span>Nasional</button>
<button type="button" class="filter-chip" data-group="status" data-val="Jalan Provinsi" onclick="lnavChip(this)"><span class="dot" style="background:#f59e0b"></span>Provinsi</button>
<button type="button" class="filter-chip" data-group="status" data-val="Jalan Kabupaten" onclick="lnavChip(this)"><span class="dot" style="background:#16a34a"></span>Kabupaten</button>
</div>
</div>
<div class="filter-block" id="lnav-parsil-block" style="display:none">
<span class="filter-label">Status Kepemilikan</span>
<div class="filter-chips" id="lnav-parsil-chips">
<button type="button" class="filter-chip active" data-group="status" data-val="" onclick="lnavChip(this)">Semua</button>
<button type="button" class="filter-chip" data-group="status" data-val="SHM" onclick="lnavChip(this)"><span class="dot" style="background:#f59e0b"></span>SHM</button>
<button type="button" class="filter-chip" data-group="status" data-val="HGB" onclick="lnavChip(this)"><span class="dot" style="background:#2563eb"></span>HGB</button>
<button type="button" class="filter-chip" data-group="status" data-val="HGU" onclick="lnavChip(this)"><span class="dot" style="background:#16a34a"></span>HGU</button>
<button type="button" class="filter-chip" data-group="status" data-val="HP" onclick="lnavChip(this)"><span class="dot" style="background:#dc2626"></span>HP</button>
</div>
</div>
<div class="filter-block" id="lnav-panjang-row">
<span class="filter-label">Panjang Jalan (meter)</span>
<div class="lnav-panjang">
<input type="number" id="lnav-pmin" placeholder="Min" min="0">
<span>s/d</span>
<input type="number" id="lnav-pmax" placeholder="Maks" min="0">
<span>m</span>
</div>
</div>
</div>
<div class="lnav-count" id="lnav-count">Memuat…</div>
<div class="lnav-list" id="lnav-list"></div>
</div>
<?php else: ?>
<div class="input-tools" style="padding: 16px; display: flex; flex-direction: column; gap: 12px; flex: 1; overflow-y: auto;">
<a href="index.php" style="display:flex; align-items:center; gap:8px; padding:10px 12px; background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; font-size:12.5px; font-weight:600; color:#475569; text-decoration:none; transition:all 0.15s;" onmouseover="this.style.background='#eff6ff'; this.style.borderColor='#bfdbfe'; this.style.color='#1d4ed8'" onmouseout="this.style.background='#f8fafc'; this.style.borderColor='#e2e8f0'; this.style.color='#475569'">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink:0"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
Kembali ke Data
</a>
<div style="background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 8px; padding: 10px; font-size: 12px; color: #1e40af; line-height: 1.5; display: flex; gap: 8px; align-items: flex-start;">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" style="margin-top:2px; flex-shrink:0"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
<span>Pilih jenis data di bawah, lalu klik/gambar di peta sebelah kanan.</span>
</div>
<div style="display:flex; flex-direction:column; gap:8px">
<button type="button" id="btn-jalan" onclick="setMode('jalan')" style="display:flex; align-items:center; gap:12px; width:100%; padding:12px; background:#fff; border:1px solid #e2e8f0; border-radius:8px; font-size:13px; font-weight:600; color:#334155; text-align:left; cursor:pointer; transition:all 0.2s; box-shadow:0 1px 2px rgba(0,0,0,0.03)">
<span style="display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:6px; background:#eff6ff; color:#1d4ed8"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 3v18M19 3v18M12 3v3M12 9v3M12 15v3"/></svg></span>
<span>Tambah Jalan</span>
</button>
<button type="button" id="btn-parsil" onclick="setMode('parsil')" style="display:flex; align-items:center; gap:12px; width:100%; padding:12px; background:#fff; border:1px solid #e2e8f0; border-radius:8px; font-size:13px; font-weight:600; color:#334155; text-align:left; cursor:pointer; transition:all 0.2s; box-shadow:0 1px 2px rgba(0,0,0,0.03)">
<span style="display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:6px; background:#f5f3ff; color:#7c3aed"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg></span>
<span>Tambah Kavling</span>
</button>
<button type="button" id="btn-rusak" onclick="setMode('rusak')" style="display:flex; align-items:center; gap:12px; width:100%; padding:12px; background:#fff; border:1px solid #e2e8f0; border-radius:8px; font-size:13px; font-weight:600; color:#334155; text-align:left; cursor:pointer; transition:all 0.2s; box-shadow:0 1px 2px rgba(0,0,0,0.03)">
<span style="display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:6px; background:#fff7ed; color:#ea580c"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span>
<span>Titik Jalan Rusak</span>
</button>
</div>
<div class="draw-actions" id="draw-actions" style="display:none; flex-direction:column; gap:8px; background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; padding:12px; margin-top:4px">
<p style="font-size:11px; font-weight:600; color:#64748b; margin-bottom:4px; text-transform:uppercase; letter-spacing:0.5px">Kontrol Menggambar</p>
<div style="display:flex; gap:6px">
<button type="button" id="undo-btn" onclick="undoLast()" style="flex:1; padding:8px; background:#fff; border:1px solid #cbd5e1; border-radius:6px; font-size:12px; font-weight:600; color:#475569; cursor:pointer; display:flex; align-items:center; justify-content:center; gap:4px">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 7v6h6M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"/></svg> Undo
</button>
<button type="button" id="finish-btn" onclick="finishShape()" style="flex:1.2; padding:8px; background:#10b981; border:1px solid #059669; border-radius:6px; font-size:12px; font-weight:600; color:#fff; cursor:pointer; display:flex; align-items:center; justify-content:center; gap:4px; box-shadow:0 2px 4px rgba(16,185,129,0.2)">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg> Selesai
</button>
<button type="button" id="cancel-btn" onclick="cancelDraw()" style="flex:1; padding:8px; background:#ef4444; border:1px solid #dc2626; border-radius:6px; font-size:12px; font-weight:600; color:#fff; cursor:pointer; display:flex; align-items:center; justify-content:center; gap:4px; box-shadow:0 2px 4px rgba(239,68,68,0.2)">
Batal
</button>
</div>
</div>
<p id="hint" style="font-size:12px; text-align:center; color:#94a3b8; font-style:italic; margin-top:8px">Pilih mode input di atas untuk mulai</p>
</div>
<?php endif; ?>
<div class="sb-foot" style="padding: 12px; border-top: 1px solid #e2e8f0; margin-top: auto; flex-shrink: 0; display: flex; flex-direction: column; gap: 8px; background: #fff;">
<a href="../index.php" class="btn-portal" style="display: flex; align-items: center; justify-content: center; gap: 6px; padding: 10px; background: #eff6ff; color: #1d4ed8; border: 1px solid #bfdbfe; border-radius: 8px; font-weight: 600; text-decoration: none; font-size: 12px; transition: all 0.15s;">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink:0"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
Kembali ke Portal
</a>
</div>
</aside>
<!-- ── Peta ── -->
<div id="map-wrap" style="display:flex; flex-direction:column; flex:1; position:relative; min-width:0; height:100%">
<!-- Topbar -->
<div id="topbar" style="position:relative; z-index:1000; height:56px; background:#fff; border-bottom:1px solid #e2e8f0; display:flex; align-items:center; justify-content:space-between; padding:0 16px; flex-shrink:0">
<div style="display:flex; align-items:center; gap:8px">
<button type="button" id="sb-toggle-jt" onclick="document.getElementById('sidebar').style.display=document.getElementById('sidebar').style.display==='none'?'flex':'none';setTimeout(function(){map.invalidateSize()},300)" style="background:none;border:none;cursor:pointer;color:#64748b;padding:4px;display:flex;align-items:center"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg></button>
<div style="font-size:15px; font-weight:700; color:#1e293b">Peta GIS <span style="font-size:13px; font-weight:500; color:#64748b; margin-left:4px">Jalan &amp; Kavling Tanah</span></div>
</div>
<div style="display:flex; gap:8px">
<?php if ($mode === 'view'): ?>
<a href="index.php?mode=input" style="display:flex; align-items:center; gap:6px; padding:8px 14px; background:#1d4ed8; color:#fff; border-radius:8px; font-size:12px; font-weight:600; text-decoration:none; box-shadow:0 2px 4px rgba(29,78,216,0.15)">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink:0"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
Input Baru
</a>
<?php else: ?>
<a href="index.php" style="display:flex; align-items:center; gap:6px; padding:8px 14px; border:1px solid #cbd5e1; color:#475569; border-radius:8px; font-size:12px; font-weight:600; text-decoration:none; background:#fff">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink:0"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg>
Lihat Peta
</a>
<?php endif; ?>
</div>
</div>
<div id="map" style="flex:1; min-height:0; width:100%; position:relative"></div>
<!-- Map controls (top-right overlay) -->
<div id="map-controls" style="position: absolute; top: 68px; right: 12px; z-index: 900; display: flex; flex-direction: column; gap: 8px; width: 220px;">
<!-- Legend Card -->
<div class="ctrl-card" style="background:#fff; border:1px solid #e2e8f0; border-radius:10px; padding:12px; box-shadow:0 4px 16px rgba(15,23,42,0.08);">
<h4 style="font-size:10px; font-weight:700; color:#94a3b8; text-transform:uppercase; letter-spacing:.5px; margin-bottom:8px; margin-top:0"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle;margin-right:4px"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>Legenda</h4>
<div style="font-size:9.5px; font-weight:700; color:#94a3b8; text-transform:uppercase; margin-bottom:5px">Jalan</div>
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:4px"><div class="leg-line" style="width:20px; height:3px; border-radius:2px; background:#e63946"></div>Jalan Nasional</div>
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:4px"><div class="leg-line" style="width:20px; height:3px; border-radius:2px; background:#f59e0b"></div>Jalan Provinsi</div>
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:6px"><div class="leg-line" style="width:20px; height:3px; border-radius:2px; background:#16a34a"></div>Jalan Kabupaten</div>
<div style="font-size:9.5px; font-weight:700; color:#94a3b8; text-transform:uppercase; margin-bottom:5px; margin-top:6px">Kavling</div>
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:4px"><div class="leg-swatch" style="width:10px; height:10px; border-radius:2px; border:1.5px solid #f59e0b; background:rgba(245,158,11,0.22)"></div>SHM</div>
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:4px"><div class="leg-swatch" style="width:10px; height:10px; border-radius:2px; border:1.5px solid #2563eb; background:rgba(37,99,235,0.15)"></div>HGB</div>
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:4px"><div class="leg-swatch" style="width:10px; height:10px; border-radius:2px; border:1.5px solid #16a34a; background:rgba(22,163,74,0.15)"></div>HGU</div>
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:6px"><div class="leg-swatch" style="width:10px; height:10px; border-radius:2px; border:1.5px solid #dc2626; background:rgba(220,38,38,0.15)"></div>HP</div>
<div style="font-size:9.5px; font-weight:700; color:#94a3b8; text-transform:uppercase; margin-bottom:5px; margin-top:6px">Jalan Rusak</div>
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:4px"><div class="leg-dot" style="width:8px; height:8px; border-radius:50%; border:2px solid #f97316; background:#f97316"></div>&lt; 1 bulan</div>
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:4px"><div class="leg-dot" style="width:8px; height:8px; border-radius:50%; border:2px solid #ca8a04; background:#facc15"></div>3-12 bulan</div>
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:4px"><div class="leg-dot" style="width:8px; height:8px; border-radius:50%; border:2px solid #64748b; background:#94a3b8"></div>&gt; 1 tahun</div>
<div class="legend-row" style="display:flex; align-items:center; gap:7px; font-size:11px; color:#475569; margin-bottom:4px"><div class="leg-dot" style="width:8px; height:8px; border-radius:50%; border:2px solid #991b1b; background:#ef4444"></div>⚡ Berulang rusak</div>
</div>
<!-- Filter/Display Card -->
<div class="ctrl-card" style="background:#fff; border:1px solid #e2e8f0; border-radius:10px; padding:12px; box-shadow:0 4px 16px rgba(15,23,42,0.08);">
<h4 style="font-size:10px; font-weight:700; color:#94a3b8; text-transform:uppercase; letter-spacing:.5px; margin-bottom:8px; margin-top:0"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle;margin-right:4px"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>Tampilkan</h4>
<label style="display:flex; align-items:center; gap:6px; font-size:12px; cursor:pointer; margin-bottom:5px; color:#475569">
<input type="checkbox" id="show-jalan" checked onchange="toggleLayerGroup('jalan')"> Ruas Jalan
</label>
<label style="display:flex; align-items:center; gap:6px; font-size:12px; cursor:pointer; margin-bottom:5px; color:#475569">
<input type="checkbox" id="show-parsil" checked onchange="toggleLayerGroup('parsil')"> Kavling Tanah
</label>
<label style="display:flex; align-items:center; gap:6px; font-size:12px; cursor:pointer; color:#475569">
<input type="checkbox" id="show-rusak" checked onchange="toggleLayerGroup('rusak')"> Jalan Rusak
</label>
</div>
<!-- Base Layer Toggle -->
<div class="ctrl-card" style="background:#fff; border:1px solid #e2e8f0; border-radius:10px; padding:12px; box-shadow:0 4px 16px rgba(15,23,42,0.08);">
<h4 style="font-size:10px; font-weight:700; color:#94a3b8; text-transform:uppercase; letter-spacing:.5px; margin-bottom:8px; margin-top:0"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:middle;margin-right:4px"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>Peta Dasar</h4>
<div style="display:flex; gap:5px">
<button type="button" id="jt-base-map" style="flex:1;padding:6px;border:1px solid #1d4ed8;border-radius:7px;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;background:#1d4ed8;color:#fff" onclick="if(map.hasLayer(baseSat))map.removeLayer(baseSat);if(!map.hasLayer(baseMap))baseMap.addTo(map);this.style.background='#1d4ed8';this.style.color='#fff';this.style.borderColor='#1d4ed8';document.getElementById('jt-base-sat').style.background='#fff';document.getElementById('jt-base-sat').style.color='#475569';document.getElementById('jt-base-sat').style.borderColor='#e2e8f0'">Peta</button>
<button type="button" id="jt-base-sat" style="flex:1;padding:6px;border:1px solid #e2e8f0;border-radius:7px;font-size:11px;font-weight:600;cursor:pointer;font-family:inherit;background:#fff;color:#475569" onclick="if(map.hasLayer(baseMap))map.removeLayer(baseMap);if(!map.hasLayer(baseSat))baseSat.addTo(map);this.style.background='#1d4ed8';this.style.color='#fff';this.style.borderColor='#1d4ed8';document.getElementById('jt-base-map').style.background='#fff';document.getElementById('jt-base-map').style.color='#475569';document.getElementById('jt-base-map').style.borderColor='#e2e8f0'">Satelit</button>
</div>
</div>
</div>
</div>
</div>
<!-- SIDE PANEL -->
<div id="side-panel">
<div class="sp-card">
<div class="sp-head">
<div class="sp-icon" id="sp-icon">🛣</div>
<div><div class="sp-title" id="sp-title">Edit Data</div><div class="sp-sub" id="sp-sub">—</div></div>
<button class="sp-close" onclick="closePanel()">✕</button>
</div>
<div class="sp-tabs" id="sp-tabs">
<div class="sp-tab active" id="tab-data" onclick="switchTab('data')">📋 Data</div>
<div class="sp-tab" id="tab-geo" onclick="switchTab('geo')">📐 Geometri</div>
</div>
<div class="sp-body" id="pane-data">
<input type="hidden" id="sp-id">
<input type="hidden" id="sp-type">
<div class="sp-field">
<label class="sp-label" id="sp-lbl-nama">Nama</label>
<input class="sp-input" type="text" id="sp-nama" placeholder="Masukkan nama…">
</div>
<div id="sp-parsil-extra" style="display:none">
<div class="sp-row">
<div class="sp-field"><label class="sp-label">Nama Pemilik</label><input class="sp-input" type="text" id="sp-pemilik" placeholder="Nama pemilik…"></div>
<div class="sp-field"><label class="sp-label">No. Sertifikat</label><input class="sp-input" type="text" id="sp-nosert" placeholder="No. sertifikat…"></div>
</div>
<div class="sp-field"><label class="sp-label">Keterangan</label><textarea class="sp-input" id="sp-ket" rows="2" placeholder="Keterangan (opsional)…" style="resize:vertical"></textarea></div>
</div>
<div id="sp-rusak-extra" style="display:none">
<div class="sp-row" style="grid-template-columns:1fr 1fr; gap:11px">
<div class="sp-field"><label class="sp-label">Tanggal</label><input class="sp-input" type="date" id="sp-tanggal"></div>
<div class="sp-field">
<label class="sp-label">Koordinat</label>
<div style="display:flex; gap:6px">
<input class="sp-input" type="text" id="sp-koordinat-rusak" readonly style="flex:1; min-width:0">
<button type="button" onclick="startPointDrag()" class="photo-btn" style="background:#eff6ff; color:#1d4ed8; border-color:#bfdbfe; margin-top:0; padding:8px 10px; font-size:11px; white-space:nowrap; flex-shrink:0">📍 Geser</button>
</div>
</div>
</div>
<div class="sp-field"><label class="sp-label">Keterangan</label><textarea class="sp-input" id="sp-ket-rusak" rows="2" style="resize:vertical"></textarea></div>
<div class="sp-field">
<label class="sp-label">Foto</label>
<img id="sp-foto-preview" src="" alt="foto">
<div class="photo-btns">
<button class="photo-btn" onclick="document.getElementById('sp-foto-file').click()">🖼 Ganti Foto</button>
<button class="photo-btn" id="sp-foto-clear" onclick="spClearFoto()" style="display:none">🗑 Hapus</button>
</div>
<input type="file" id="sp-foto-file" accept="image/*" style="display:none" onchange="spHandleFoto(this)">
</div>
</div>
<div id="sp-status-wrap" class="sp-field">
<label class="sp-label" id="sp-lbl-status">Status</label>
<select class="sp-input" id="sp-status"></select>
</div>
<div id="sp-ukuran-wrap" class="sp-field">
<label class="sp-label">Ukuran <span class="sp-auto">Otomatis</span></label>
<input class="sp-input" type="text" id="sp-ukuran" readonly>
</div>
</div>
<div class="sp-body" id="pane-geo" style="display:none">
<div class="geo-hint"><strong>🖱 Edit Geometri</strong>Pilih mode lalu interaksi dengan shape di peta. Klik lagi untuk nonaktifkan.</div>
<div class="geo-toolbar">
<button class="geo-btn" id="geo-btn-vertex" onclick="setGeoMode('vertex')"><span class="geo-btn-icon">🔵</span><div><div class="geo-btn-title">Drag Titik</div><div class="geo-btn-desc">Geser titik individual untuk ubah bentuk</div></div></button>
<button class="geo-btn" id="geo-btn-move" onclick="setGeoMode('move')"><span class="geo-btn-icon">✋</span><div><div class="geo-btn-title">Geser Seluruh Shape</div><div class="geo-btn-desc">Klik-tahan peta untuk pindahkan keseluruhan</div></div></button>
<button class="geo-btn" id="geo-btn-add" onclick="setGeoMode('add')"><span class="geo-btn-icon"></span><div><div class="geo-btn-title">Tambah Titik</div><div class="geo-btn-desc">Klik titik hijau di tengah segmen untuk sisipkan</div></div></button>
<button class="geo-btn" id="geo-btn-del" onclick="setGeoMode('del')"><span class="geo-btn-icon">🗑</span><div><div class="geo-btn-title">Hapus Titik</div><div class="geo-btn-desc">Klik titik merah untuk menghapusnya</div></div></button>
</div>
<div class="geo-note">Perubahan disimpan saat klik <b style="color:#a5b4fc">💾 Simpan</b>.</div>
</div>
<div class="sp-foot">
<button class="sp-save" id="sp-save-btn" onclick="submitEdit()">💾 Simpan</button>
<button class="sp-cancel" onclick="closePanel()">Batal</button>
</div>
</div>
</div>
<!-- FLOATING GEO EDIT BAR -->
<div id="geo-edit-bar" style="display:none; position:fixed; top:24px; left:50%; transform:translateX(-50%); z-index:99999; background:#ffffff; padding:12px 20px; border-radius:12px; box-shadow:0 10px 25px rgba(0,0,0,0.15); border:1px solid #e2e8f0; align-items:center; gap:16px; font-family:'Inter',sans-serif;">
<div style="font-size:13px; font-weight:700; color:#1e293b; display:flex; align-items:center; gap:6px; white-space:nowrap">
<span id="geo-edit-title-icon">📐</span>
<span id="geo-edit-title-text">Edit Geometri</span>
</div>
<div style="width:1px; height:24px; background:#e2e8f0" id="geo-edit-divider-1"></div>
<div style="display:flex; gap:6px" id="geo-edit-modes">
<button type="button" onclick="setGeoMode('vertex')" class="geo-btn-mini active" id="gbm-vertex">🔵 Drag Titik</button>
<button type="button" onclick="setGeoMode('move')" class="geo-btn-mini" id="gbm-move">✋ Geser Shape</button>
<button type="button" onclick="setGeoMode('add')" class="geo-btn-mini" id="gbm-add"> Tambah</button>
<button type="button" onclick="setGeoMode('del')" class="geo-btn-mini" id="gbm-del">🗑️ Hapus</button>
</div>
<div style="width:1px; height:24px; background:#e2e8f0"></div>
<div style="display:flex; gap:8px">
<button type="button" onclick="exitFloatingGeoEdit(true)" style="background:#1d4ed8; color:white; border:none; padding:8px 16px; border-radius:8px; font-size:12px; font-weight:700; cursor:pointer; transition:all 0.15s">Selesai</button>
<button type="button" onclick="exitFloatingGeoEdit(false)" style="background:#f1f5f9; color:#475569; border:1px solid #cbd5e1; padding:8px 16px; border-radius:8px; font-size:12px; font-weight:600; cursor:pointer; transition:all 0.15s">Batal</button>
</div>
</div>
<!-- Modal simpan jalan / kavling -->
<div class="modal-overlay" id="form-modal">
<div class="modal-card">
<div class="modal-header">
<div class="modal-icon" id="form-icon">🛣</div>
<div>
<div class="modal-title" id="form-title">Simpan Data</div>
<div class="modal-sub" id="form-sub">Lengkapi informasi di bawah ini</div>
</div>
</div>
<div class="field-group">
<label class="field-label" id="label-nama">Nama</label>
<input class="field-input" type="text" id="inp-nama" placeholder="Masukkan nama…">
</div>
<div id="parsil-extra-fields" style="display:none">
<div class="field-row">
<div class="field-group">
<label class="field-label">Nama Pemilik</label>
<input class="field-input" type="text" id="inp-pemilik" placeholder="Nama pemilik…">
</div>
<div class="field-group">
<label class="field-label">Nomor Sertifikat</label>
<input class="field-input" type="text" id="inp-nosert" placeholder="No. sertifikat…">
</div>
</div>
<div class="field-group">
<label class="field-label">Keterangan</label>
<textarea class="field-input" id="inp-keterangan" rows="2" placeholder="Keterangan tambahan (opsional)…" style="resize:vertical"></textarea>
</div>
</div>
<div class="field-group">
<label class="field-label" id="label-status">Status</label>
<select class="field-input" id="inp-status"></select>
</div>
<div class="field-group">
<label class="field-label" id="label-ukuran">Ukuran <span class="readonly-tag">Otomatis</span></label>
<input class="field-input" type="text" id="inp-ukuran" readonly>
</div>
<div class="modal-footer">
<button class="btn-primary" type="button" onclick="saveData()">Simpan Data</button>
<button class="btn-ghost" type="button" onclick="closeForm()">Batal</button>
</div>
</div>
</div>
<!-- Modal jalan rusak -->
<div class="modal-overlay" id="rusak-modal">
<div class="modal-card">
<div class="modal-header">
<div class="modal-icon rusak">⚠</div>
<div>
<div class="modal-title">Input Jalan Rusak</div>
<div class="modal-sub" id="rusak-sub">Titik kerusakan jalan</div>
</div>
</div>
<div class="field-group">
<label class="field-label">Nama Pelapor <span style="font-size:10px;color:#94a3b8;text-transform:none;letter-spacing:0">(kosongkan = Anonym)</span></label>
<input class="field-input" type="text" id="rusak-pelapor" placeholder="Nama pelapor (opsional)…">
</div>
<div class="field-group">
<label class="field-label">Nama Jalan <span class="readonly-tag">Otomatis</span></label>
<input class="field-input" type="text" id="rusak-nama-jalan" readonly placeholder="Terdeteksi dari lokasi titik…">
</div>
<div class="field-row">
<div class="field-group">
<label class="field-label">Tanggal <span class="readonly-tag">Otomatis</span></label>
<input class="field-input" type="date" id="rusak-tanggal">
</div>
<div class="field-group">
<label class="field-label">Koordinat</label>
<input class="field-input" type="text" id="rusak-koordinat" readonly>
<div id="gps-badge" class="gps-badge" style="display:none">📍 GPS aktif</div>
</div>
</div>
<div class="field-group">
<label class="field-label">Keterangan</label>
<textarea class="field-input" id="rusak-ket" rows="2" placeholder="Deskripsi kerusakan (opsional)…" style="resize:vertical"></textarea>
</div>
<div class="field-group">
<label class="field-label">Foto Dokumentasi</label>
<div class="photo-upload-area" onclick="document.getElementById('foto-cam').click()">
<div class="pu-icon">📷</div>
<div class="pu-text">Klik untuk ambil foto</div>
<div class="pu-sub">JPG / PNG · maks 5 MB</div>
</div>
<div class="photo-btns">
<button class="photo-btn" type="button" onclick="document.getElementById('foto-file').click()">🖼 Galeri</button>
<button class="photo-btn" type="button" onclick="document.getElementById('foto-cam').click()">📷 Kamera</button>
<button class="photo-btn" type="button" id="foto-clear-btn" onclick="clearFoto()" style="display:none">🗑 Hapus</button>
</div>
<input type="file" id="foto-file" accept="image/*" style="display:none" onchange="handleFoto(this)">
<input type="file" id="foto-cam" accept="image/*" capture="environment" style="display:none" onchange="handleFoto(this)">
<img id="photo-preview" src="" alt="preview">
</div>
<div class="modal-footer">
<button class="btn-primary" type="button" id="rusak-save-btn" onclick="saveRusak()">Simpan Titik</button>
<button class="btn-ghost" type="button" onclick="closeRusakModal()">Batal</button>
</div>
</div>
</div>
<?php include 'includes/confirm-modal.php'; ?>
<script>
/* ══ MAP ══════════════════════════════════════════════ */
var map=L.map('map',{zoomControl:false,maxZoom:22}).setView([-0.0263,109.3425],13);
L.control.zoom({position:'bottomright'}).addTo(map);
var baseMap=L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{attribution:'© <a href="https://www.openstreetmap.org/">OSM</a>',maxZoom:22,maxNativeZoom:19}).addTo(map);
var baseSat=L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',{attribution:'© Esri',maxZoom:22,maxNativeZoom:19});
map.createPane('rusakPane');
map.getPane('rusakPane').style.zIndex=450;
var jalanLayers={},parsilLayers={},rusakLayers={};
var jalanData={},parsilData={},rusakData={};
var WARNA_JALAN={'Jalan Nasional':'#e63946','Jalan Provinsi':'#f59e0b','Jalan Kabupaten':'#16a34a'};
var WARNA_PARSIL={'SHM':{color:'#f59e0b',fillColor:'#f59e0b',fillOpacity:.22,weight:2},'HGB':{color:'#2563eb',fillColor:'#2563eb',fillOpacity:.18,weight:2},'HGU':{color:'#16a34a',fillColor:'#16a34a',fillOpacity:.18,weight:2},'HP':{color:'#dc2626',fillColor:'#dc2626',fillOpacity:.18,weight:2}};
function parseCoords(raw){
try{var g=typeof raw==='string'?JSON.parse(raw):raw;
if(g&&g.type==='LineString')return g.coordinates.map(function(c){return[c[1],c[0]];});
if(g&&g.type==='Polygon') return g.coordinates[0].map(function(c){return[c[1],c[0]];});
if(Array.isArray(g)) return g.map(function(p){return[p[0],p[1]];});
}catch(e){}return[];
}
function updateStats(d){
['tj','tp','tr'].forEach(function(k){var e=document.getElementById('s-'+k);if(e&&d[k]!==undefined)e.textContent=d[k];});
var tj = parseInt(document.getElementById('s-tj').textContent)||0;
var tp = parseInt(document.getElementById('s-tp').textContent)||0;
var tr = parseInt(document.getElementById('s-tr').textContent)||0;
var tot = document.getElementById('s-total');
if (tot) tot.textContent = tj + tp + tr;
}
function toggleLayerGroup(type) {
var show = document.getElementById('show-' + type).checked;
var layers = type === 'jalan' ? jalanLayers : (type === 'parsil' ? parsilLayers : rusakLayers);
Object.values(layers).forEach(function(l) {
if (show) {
map.addLayer(l);
} else {
map.removeLayer(l);
}
});
}
function h(s){return(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
/* ══ POPUP BUILDERS ─── defined BEFORE PHP data loops ══ */
function badgeJ(st){var c={'Jalan Nasional':'#fef2f2;color:#dc2626','Jalan Provinsi':'#fffbeb;color:#b45309','Jalan Kabupaten':'#f0fdf4;color:#15803d'};return'<span style="display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:600;margin-bottom:7px;background:'+(c[st]||'#f3f4f6;color:#374151')+'">'+st+'</span>';}
function badgeP(st){var c={'SHM':'#fffbeb;color:#b45309','HGB':'#eff6ff;color:#1d4ed8','HGU':'#f0fdf4;color:#15803d','HP':'#fef2f2;color:#dc2626'};return'<span style="display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:600;margin-bottom:7px;background:'+(c[st]||'#f3f4f6;color:#374151')+'">'+st+'</span>';}
function makeJalanPopup(id,nama,status,pjg){
var titleHtml = '<div style="display:flex;align-items:flex-start;gap:8px;margin-bottom:8px">' +
'<span style="background:#eff6ff;color:#2563eb;border-radius:6px;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg></span>' +
'<div style="font-size:13px;font-weight:700;color:#1e293b;line-height:1.3">' + h(nama) + '</div>' +
'</div>';
var lenRow = '<div style="display:flex;align-items:center;gap:6px;font-size:11.5px;color:#64748b;margin-bottom:4px">' +
'<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="color:#94a3b8"><line x1="5" y1="9" x2="19" y2="9"/><line x1="5" y1="15" x2="19" y2="15"/><line x1="9" y1="5" x2="9" y2="19"/><line x1="15" y1="5" x2="15" y2="19"/></svg>' +
'<span>Panjang: <b>' + pjg + ' m</b></span>' +
'</div>';
return '<div class="pw" style="font-family:\'Inter\',sans-serif;padding:0"><div class="pw-head" style="padding:14px 14px 10px">' + titleHtml + badgeJ(status) + lenRow + '</div><div class="pw-foot" style="display:flex;border-top:1px solid #f1f5f9;background:#f8fafc"><button class="pw-btn edit" onclick="openEdit(\'jalan\','+id+')" style="flex:1;padding:10px 0;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;gap:4px;color:#2563eb;background:transparent;border:none;cursor:pointer"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg> Edit</button><button class="pw-btn danger" onclick="hapusJalan('+id+')" style="flex:1;padding:10px 0;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;gap:4px;color:#dc2626;background:transparent;border:none;cursor:pointer;border-left:1px solid #e2e8f0"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg> Hapus</button></div></div>';
}
function makeParsilPopup(id,nama,pemilik,nosert,ket,status,luas){
var titleHtml = '<div style="display:flex;align-items:flex-start;gap:8px;margin-bottom:8px">' +
'<span style="background:#f0fdf4;color:#16a34a;border-radius:6px;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg></span>' +
'<div style="font-size:13px;font-weight:700;color:#1e293b;line-height:1.3">' + h(nama) + '</div>' +
'</div>';
var areaRow = '<div style="display:flex;align-items:center;gap:6px;font-size:11.5px;color:#64748b;margin-bottom:4px">' +
'<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="color:#94a3b8"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/></svg>' +
'<span>Luas: <b>' + luas + ' m²</b></span>' +
'</div>';
var extra=(pemilik?'<div style="font-size:11.5px;color:#64748b;margin-bottom:3px">Pemilik: <b>'+h(pemilik)+'</b></div>':'')+(nosert?'<div style="font-size:11.5px;color:#64748b;margin-bottom:3px">No.Sert: <b>'+h(nosert)+'</b></div>':'')+(ket?'<div style="margin-top:6px;padding:6px 8px;background:#f8fafc;border-radius:6px;font-size:11px;color:#475569;border-left:2px solid #cbd5e1;line-height:1.4">'+h(ket)+'</div>':'');
return '<div class="pw" style="font-family:\'Inter\',sans-serif;padding:0"><div class="pw-head" style="padding:14px 14px 10px">' + titleHtml + badgeP(status) + areaRow + extra + '</div><div class="pw-foot" style="display:flex;border-top:1px solid #f1f5f9;background:#f8fafc"><button class="pw-btn edit" onclick="openEdit(\'parsil\','+id+')" style="flex:1;padding:10px 0;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;gap:4px;color:#2563eb;background:transparent;border:none;cursor:pointer"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg> Edit</button><button class="pw-btn danger" onclick="hapusParsil('+id+')" style="flex:1;padding:10px 0;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;gap:4px;color:#dc2626;background:transparent;border:none;cursor:pointer;border-left:1px solid #e2e8f0"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg> Hapus</button></div></div>';
}
function makeRusakPopup(id,nama,tgl,lat,lng,ket,foto,ageClass,recurring){
var fHtml=foto?'<div style="position:relative;margin-top:10px;border-radius:8px;overflow:hidden;border:1px solid #e2e8f0;box-shadow:0 2px 6px rgba(0,0,0,0.06)"><img src="'+foto+'" style="width:100%;display:block;max-height:120px;object-fit:cover;transition:transform 0.2s" onmouseover="this.style.transform=\'scale(1.03)\'" onmouseout="this.style.transform=\'scale(1)\'"></div>':'';
var ageBadge=recurring?'<span style="display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:12px;font-size:9.5px;font-weight:700;background:#fef2f2;color:#dc2626;border:1px solid #fee2e2;margin-bottom:6px"><svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="3" style="flex-shrink:0"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg> Berulang Rusak</span><br>':'';
var titleHtml = '<div style="display:flex;align-items:flex-start;gap:8px;margin-bottom:8px">' +
'<span style="background:#fee2e2;color:#ef4444;border-radius:6px;width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg></span>' +
'<div style="font-size:13px;font-weight:700;color:#1e293b;line-height:1.3">' + h(nama) + '</div>' +
'</div>';
var dateRow = '<div style="display:flex;align-items:center;gap:6px;font-size:11.5px;color:#64748b;margin-bottom:4px">' +
'<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="color:#94a3b8"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>' +
'<span>' + h(tgl) + '</span>' +
'</div>';
var coordRow = '<div style="display:flex;align-items:center;gap:6px;font-size:11.5px;color:#64748b;margin-bottom:4px">' +
'<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" style="color:#94a3b8"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>' +
'<span style="font-family:monospace;letter-spacing:-0.2px">' + parseFloat(lat).toFixed(6) + ', ' + parseFloat(lng).toFixed(6) + '</span>' +
'</div>';
var ketRow = ket ? '<div style="margin-top:6px;padding:6px 8px;background:#f8fafc;border-radius:6px;font-size:11px;color:#475569;border-left:2px solid #cbd5e1;line-height:1.4">' + h(ket) + '</div>' : '';
return '<div class="pw" style="font-family:\'Inter\',sans-serif;padding:0"><div class="pw-head" style="padding:14px 14px 10px">' + titleHtml + ageBadge + dateRow + coordRow + ketRow + fHtml + '</div><div class="pw-foot" style="display:flex;border-top:1px solid #f1f5f9;background:#f8fafc"><button class="pw-btn edit" onclick="openEdit(\'rusak\','+id+')" style="flex:1;padding:10px 0;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;gap:4px;color:#2563eb;background:transparent;border:none;cursor:pointer"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg> Edit</button><button class="pw-btn danger" onclick="hapusRusak('+id+')" style="flex:1;padding:10px 0;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;gap:4px;color:#dc2626;background:transparent;border:none;cursor:pointer;border-left:1px solid #e2e8f0"><svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg> Hapus</button></div></div>';
}
function getRusakIcon(ageClass,recurring){
var colors={recent:'#f97316',medium:'#facc15',old:'#94a3b8'};
var borders={recent:'white',medium:'#92400e',old:'#334155'};
var bg=recurring?'#ef4444':(colors[ageClass]||colors.recent);
var bd=recurring?'white':(borders[ageClass]||'white');
var sz=recurring?18:15;
var shadow=recurring?'0 0 0 3px rgba(239,68,68,.35),0 2px 8px rgba(0,0,0,.4)':'0 2px 8px rgba(0,0,0,.3)';
return L.divIcon({
html:'<div style="width:'+sz+'px;height:'+sz+'px;border-radius:50%;background:'+bg+';border:2.5px solid '+bd+';box-shadow:'+shadow+'"></div>',
className:'',iconAnchor:[Math.round(sz/2),Math.round(sz/2)]
});
}
function makeRusakLayer(id,nama,tgl,lat,lng,ket,foto,ageClass,recurring){
ageClass=ageClass||'recent';recurring=recurring||false;
var icon=getRusakIcon(ageClass,recurring);
var layer=L.marker([lat,lng],{icon:icon,pane:'rusakPane'}).addTo(map);
layer.bindPopup(makeRusakPopup(id,nama,tgl,lat,lng,ket,foto,ageClass,recurring),{maxWidth:250});
layer.on('click',function(){lnavHighlightFromMap('rusak',id);});
return layer;
}
/* ══ LOAD DATA FROM PHP ═══════════════════════════════ */
<?php
$jln=$conn->query("SELECT * FROM jalan");
while($r=$jln->fetch_assoc()):
$id=$r['id'];$nama=addslashes($r['nama_jalan']);$status=addslashes($r['status_jalan']);
$pjg=number_format($r['panjang_meter'],1);$coords=addslashes($r['koordinat']);
?>
(function(){
var id=<?=$id?>,nama='<?=$nama?>',status='<?=$status?>',pjg='<?=$pjg?>';
var ll=parseCoords('<?=$coords?>');
var layer=L.polyline(ll,{color:WARNA_JALAN[status]||'#999',weight:5,opacity:.9}).addTo(map);
layer.bindPopup(makeJalanPopup(id,nama,status,pjg),{maxWidth:280});
layer.on('click',function(){lnavHighlightFromMap('jalan',id);});
jalanLayers[id]=layer; jalanData[id]={id:id,nama:nama,status:status,pjg:pjg,ll:ll};
})();
<?php endwhile;?>
<?php
$prs=$conn->query("SELECT * FROM parsil");
while($r=$prs->fetch_assoc()):
$id=$r['id'];$nama=addslashes($r['nama_parsil']);$pemilik=addslashes($r['nama_pemilik']);
$nosert=addslashes($r['nomor_sertifikat']);$ket=addslashes($r['keterangan']);
$status=addslashes($r['status_kepemilikan']);$luas=number_format($r['luas_m2'],1);$coords=addslashes($r['koordinat']);
?>
(function(){
var id=<?=$id?>,nama='<?=$nama?>',pemilik='<?=$pemilik?>',nosert='<?=$nosert?>',ket='<?=$ket?>',status='<?=$status?>',luas='<?=$luas?>';
var ll=parseCoords('<?=$coords?>');
var opt=WARNA_PARSIL[status]||{color:'#999',fillColor:'#999',fillOpacity:.2,weight:2};
var layer=L.polygon(ll,opt).addTo(map);
layer.bindPopup(makeParsilPopup(id,nama,pemilik,nosert,ket,status,luas),{maxWidth:280});
layer.on('click',function(){lnavHighlightFromMap('parsil',id);});
parsilLayers[id]=layer; parsilData[id]={id:id,nama:nama,pemilik:pemilik,nosert:nosert,ket:ket,status:status,luas:luas,ll:ll};
})();
<?php endwhile;?>
<?php
$repeatMapS=[];
$rRepeatS=$conn->query("SELECT ROUND(latitude,4) as la, ROUND(longitude,4) as lo, COUNT(*) as cnt FROM jalan_rusak GROUP BY la,lo HAVING cnt>2");
while($rr2=$rRepeatS->fetch_assoc()) $repeatMapS[round($rr2['la'],4).','.round($rr2['lo'],4)]=(int)$rr2['cnt'];
$rsk=$conn->query("SELECT * FROM jalan_rusak ORDER BY tanggal DESC");
while($r=$rsk->fetch_assoc()):
$id=$r['id'];$nama=addslashes($r['nama']);$tgl=addslashes($r['tanggal']);
$ket=addslashes($r['keterangan']??'');$lat=(float)$r['latitude'];$lng=(float)$r['longitude'];
$foto=addslashes($r['foto']??'');
$key=round($lat,4).','.round($lng,4);
$recurring=isset($repeatMapS[$key])?'true':'false';
$tglDate=new DateTime($r['tanggal']);
$now=new DateTime();
$diffDays=$now->diff($tglDate)->days;
if($diffDays<=30) $ageClass='recent';
elseif($diffDays<=365) $ageClass='medium';
else $ageClass='old';
?>
(function(){
var id=<?=$id?>,nama='<?=$nama?>',tgl='<?=$tgl?>',lat=<?=$lat?>,lng=<?=$lng?>,ket='<?=$ket?>',foto='<?=$foto?>';
var layer=makeRusakLayer(id,nama,tgl,lat,lng,ket,foto,'<?=$ageClass?>',<?=$recurring?>);
rusakLayers[id]=layer; rusakData[id]={id:id,nama:nama,tgl:tgl,lat:lat,lng:lng,ket:ket,foto:foto,age:'<?=$ageClass?>',recurring:<?=$recurring?>};
})();
<?php endwhile;?>
/* ══ DELETE ═══════════════════════════════════════════ */
function hapusJalan(id){showConfirm({title: 'Hapus Jalan?', message: 'Hapus permanen data jalan ini?', confirmText: 'Ya, Hapus', danger: true}).then(ok=>{if(!ok)return;fetch('api.php?action=delete_jalan&id='+id).then(function(r){return r.json();}).then(function(d){if(jalanLayers[id]){map.removeLayer(jalanLayers[id]);delete jalanLayers[id];delete jalanData[id];}updateStats(d);if(typeof lnavRender==='function')lnavRender();showToast('Data jalan dihapus.','success');});});}
function hapusParsil(id){showConfirm({title: 'Hapus Kavling?', message: 'Hapus permanen data kavling ini?', confirmText: 'Ya, Hapus', danger: true}).then(ok=>{if(!ok)return;fetch('api.php?action=delete_parsil&id='+id).then(function(r){return r.json();}).then(function(d){if(parsilLayers[id]){map.removeLayer(parsilLayers[id]);delete parsilLayers[id];delete parsilData[id];}updateStats(d);if(typeof lnavRender==='function')lnavRender();showToast('Data kavling dihapus.','success');});});}
function hapusRusak(id){showConfirm({title: 'Hapus Jalan Rusak?', message: 'Hapus permanen titik jalan rusak ini?', confirmText: 'Ya, Hapus', danger: true}).then(ok=>{if(!ok)return;fetch('api.php?action=delete_rusak&id='+id).then(function(r){return r.json();}).then(function(d){if(rusakLayers[id]){map.removeLayer(rusakLayers[id]);delete rusakLayers[id];delete rusakData[id];}updateStats(d);if(typeof lnavRender==='function')lnavRender();showToast('Titik jalan rusak dihapus.','success');});});}
/* ══ SIDE PANEL ═══════════════════════════════════════ */
var activeId=null,activeType=null,activeTab='data';
var spFotoBase64='';
function el(id){return document.getElementById(id);}
function openEdit(type,id){
map.closePopup();activeId=id;activeType=type;
if(type==='jalan'){
var d=jalanData[id];
el('sp-icon').textContent='🛣';el('sp-icon').className='sp-icon jalan';
el('sp-title').textContent='Edit Jalan';el('sp-sub').textContent=d.nama;
el('sp-lbl-nama').textContent='Nama Jalan';el('sp-lbl-status').textContent='Status Jalan';
el('sp-nama').value=d.nama;el('sp-ukuran').value=d.pjg+' m';
el('sp-parsil-extra').style.display='none';el('sp-rusak-extra').style.display='none';
el('sp-status-wrap').style.display='';el('sp-ukuran-wrap').style.display='';
var sel=el('sp-status');sel.innerHTML='<option>Jalan Nasional</option><option>Jalan Provinsi</option><option>Jalan Kabupaten</option>';sel.value=d.status;
editLL=d.ll.map(function(p){return[p[0],p[1]];});
originalLL=d.ll.map(function(p){return[p[0],p[1]];});
el('tab-geo').style.display='';
} else if(type==='parsil'){
var d=parsilData[id];
el('sp-icon').textContent='🏘';el('sp-icon').className='sp-icon parsil';
el('sp-title').textContent='Edit Kavling';el('sp-sub').textContent=d.nama;
el('sp-lbl-nama').textContent='Nama / No. Parsil';el('sp-lbl-status').textContent='Status Kepemilikan';
el('sp-nama').value=d.nama;el('sp-pemilik').value=d.pemilik;el('sp-nosert').value=d.nosert;el('sp-ket').value=d.ket;
el('sp-ukuran').value=d.luas+' m²';
el('sp-parsil-extra').style.display='block';el('sp-rusak-extra').style.display='none';
el('sp-status-wrap').style.display='';el('sp-ukuran-wrap').style.display='';
var sel=el('sp-status');sel.innerHTML='<option value="SHM">SHM</option><option value="HGB">HGB</option><option value="HGU">HGU</option><option value="HP">HP</option>';sel.value=d.status;
editLL=d.ll.map(function(p){return[p[0],p[1]];});
originalLL=d.ll.map(function(p){return[p[0],p[1]];});
el('tab-geo').style.display='';
} else {
var d=rusakData[id];
el('sp-icon').textContent='⚠';el('sp-icon').className='sp-icon rusak';
el('sp-title').textContent='Edit Jalan Rusak';el('sp-sub').textContent=d.nama;
el('sp-lbl-nama').textContent='Nama / Deskripsi';
el('sp-nama').value=d.nama;
el('sp-tanggal').value=d.tgl;
el('sp-koordinat-rusak').value=d.lat.toFixed(6)+', '+d.lng.toFixed(6);
el('sp-ket-rusak').value=d.ket;
el('sp-parsil-extra').style.display='none';el('sp-rusak-extra').style.display='block';
el('sp-status-wrap').style.display='none';el('sp-ukuran-wrap').style.display='none';
el('tab-geo').style.display='none';
originalLat=d.lat;
originalLng=d.lng;
spFotoBase64=d.foto||'';
if(d.foto){el('sp-foto-preview').src=d.foto;el('sp-foto-preview').style.display='block';el('sp-foto-clear').style.display='flex';}
else{el('sp-foto-preview').style.display='none';el('sp-foto-clear').style.display='none';}
}
switchTab('data');
el('side-panel').classList.add('open');
setTimeout(function(){el('sp-nama').focus();},200);
}
function closePanel(){clearGeo();el('side-panel').classList.remove('open');activeId=null;activeType=null;}
function switchTab(tab){
if (tab === 'geo') {
el('side-panel').classList.remove('open');
el('geo-edit-bar').style.display = 'flex';
el('geo-edit-modes').style.display = 'flex';
el('geo-edit-title-text').textContent = 'Edit Geometri: ' + el('sp-sub').textContent;
setGeoMode('vertex');
initGeo();
} else {
activeTab=tab;
el('tab-data').className='sp-tab active';
el('tab-geo').className ='sp-tab';
el('pane-data').style.display='';
el('pane-geo').style.display ='none';
clearGeo();
}
}
function exitGeoEdit(saveChanges){
if (saveChanges) {
if (activeType === 'jalan') {
var totalDist = 0;
for (var i = 0; i < editLL.length - 1; i++) {
var p1 = L.latLng(editLL[i]), p2 = L.latLng(editLL[i+1]);
totalDist += p1.distanceTo(p2);
}
el('sp-ukuran').value = totalDist.toFixed(1) + ' m';
} else if (activeType === 'parsil') {
el('sp-ukuran').value = 'Diperbarui setelah disimpan';
}
showToast('Geometri diperbarui sementara di peta. Klik Simpan untuk menyimpan ke database.', 'success');
} else {
editLL = originalLL.map(function(p){return [p[0], p[1]];});
updateGhost();
}
clearGeo();
el('geo-edit-bar').style.display = 'none';
el('side-panel').classList.add('open');
activeTab='data';
el('tab-data').className='sp-tab active';
el('tab-geo').className ='sp-tab';
el('pane-data').style.display='';
el('pane-geo').style.display ='none';
}
function startPointDrag() {
el('side-panel').classList.remove('open');
var marker = rusakLayers[activeId];
if (marker) {
marker.dragging.enable();
marker.bindTooltip("Seret saya ke lokasi baru!", {permanent:true, direction:'top'}).openTooltip();
var bar = el('geo-edit-bar');
bar.style.display = 'flex';
el('geo-edit-modes').style.display = 'none';
el('geo-edit-title-text').textContent = 'Geser Penanda Jalan Rusak';
}
}
function exitPointDrag(save) {
var marker = rusakLayers[activeId];
if (marker) {
marker.dragging.disable();
marker.unbindTooltip();
if (save) {
var newPos = marker.getLatLng();
rusakData[activeId].lat = newPos.lat;
rusakData[activeId].lng = newPos.lng;
el('sp-koordinat-rusak').value = newPos.lat.toFixed(6) + ', ' + newPos.lng.toFixed(6);
showToast('Lokasi baru tersimpan sementara.', 'success');
} else {
marker.setLatLng([originalLat, originalLng]);
rusakData[activeId].lat = originalLat;
rusakData[activeId].lng = originalLng;
}
}
el('geo-edit-bar').style.display = 'none';
el('side-panel').classList.add('open');
}
function exitFloatingGeoEdit(save) {
if (activeType === 'rusak') {
exitPointDrag(save);
} else {
exitGeoEdit(save);
}
}
function spHandleFoto(input){var f=input.files[0];if(!f)return;if(f.size>5*1024*1024){showToast('Foto maks 5MB!','error');return;}var r=new FileReader();r.onload=function(e){var img=new Image();img.onload=function(){var maxDim=1280,w=img.width,h=img.height;if(w>h&&w>maxDim){h=Math.round(h*maxDim/w);w=maxDim;}else if(h>=w&&h>maxDim){w=Math.round(w*maxDim/h);h=maxDim;}var c=document.createElement('canvas');c.width=w;c.height=h;c.getContext('2d').drawImage(img,0,0,w,h);spFotoBase64=c.toDataURL('image/jpeg',0.7);el('sp-foto-preview').src=spFotoBase64;el('sp-foto-preview').style.display='block';el('sp-foto-clear').style.display='flex';};img.onerror=function(){spFotoBase64=e.target.result;el('sp-foto-preview').src=spFotoBase64;el('sp-foto-preview').style.display='block';el('sp-foto-clear').style.display='flex';};img.src=e.target.result;};r.readAsDataURL(f);}
function spClearFoto(){spFotoBase64='';el('sp-foto-preview').style.display='none';el('sp-foto-file').value='';el('sp-foto-clear').style.display='none';}
/* ══ GEOMETRY EDIT ═══════════════════════════════════ */
var editLL=[],originalLL=[],originalLat=null,originalLng=null,ghostLayer=null,vtxMarkers=[],midMarkers=[],geoMode=null;
var moveActive=false,movePrev=null;
function getColor(){var st=el('sp-status').value;return activeType==='jalan'?WARNA_JALAN[st]||'#6366f1':(WARNA_PARSIL[st]||{color:'#6366f1'}).color;}
function initGeo(){
clearGeo();
var col=getColor();
if (activeType === 'jalan' && jalanLayers[activeId]) {
map.removeLayer(jalanLayers[activeId]);
} else if (activeType === 'parsil' && parsilLayers[activeId]) {
map.removeLayer(parsilLayers[activeId]);
}
ghostLayer=(activeType==='jalan'?L.polyline:L.polygon)(editLL,{color:col,fillColor:col,fillOpacity:.08,weight:3,dashArray:'8,5',interactive:false}).addTo(map);
}
function clearGeo(){
if(ghostLayer){map.removeLayer(ghostLayer);ghostLayer=null;}
vtxMarkers.forEach(function(m){map.removeLayer(m);});vtxMarkers=[];
midMarkers.forEach(function(m){map.removeLayer(m);});midMarkers=[];
stopMove();
map.off('click', onMapClickForAdd);
setGeoBtn(null);
geoMode=null;
map.dragging.enable();
if (activeType === 'jalan' && jalanLayers[activeId]) {
jalanLayers[activeId].addTo(map);
jalanLayers[activeId].setLatLngs(editLL);
} else if (activeType === 'parsil' && parsilLayers[activeId]) {
parsilLayers[activeId].addTo(map);
parsilLayers[activeId].setLatLngs(editLL);
}
}
function updateGhost(){if(ghostLayer)ghostLayer.setLatLngs(editLL);}
function onMapClickForAdd(e) {
if (geoMode === 'add') {
editLL.push([e.latlng.lat, e.latlng.lng]);
updateGhost();
renderVtx();
}
}
function renderVtx(){
vtxMarkers.forEach(function(m){map.removeLayer(m);});vtxMarkers=[];
midMarkers.forEach(function(m){map.removeLayer(m);});midMarkers=[];
editLL.forEach(function(ll,i){
var isDel=geoMode==='del';
var m=L.circleMarker([ll[0],ll[1]],{radius:isDel?7:6,color:isDel?'#ef4444':'#6366f1',fillColor:isDel?'#fca5a5':'white',fillOpacity:1,weight:2.5}).addTo(map);
if(geoMode==='vertex') enableDrag(m,i);
if(isDel)(function(idx){m.on('click',function(e){if(e.originalEvent)L.DomEvent.stopPropagation(e.originalEvent);var min=activeType==='jalan'?2:3;if(editLL.length<=min){showToast('Titik sudah minimal!','error');return;}editLL.splice(idx,1);updateGhost();renderVtx();});})(i);
vtxMarkers.push(m);
});
if(geoMode==='add'){var n=editLL.length;var loop=activeType==='parsil';for(var i=0;i<(loop?n:n-1);i++)(function(i){var j=(i+1)%n,mlat=(editLL[i][0]+editLL[j][0])/2,mlng=(editLL[i][1]+editLL[j][1])/2;var mm=L.circleMarker([mlat,mlng],{radius:5,color:'#10b981',fillColor:'#d1fae5',fillOpacity:1,weight:2}).addTo(map);mm.on('click',function(e){if(e.originalEvent)L.DomEvent.stopPropagation(e.originalEvent);editLL.splice(i+1,0,[mlat,mlng]);updateGhost();renderVtx();});midMarkers.push(mm);})(i);}
}
function enableDrag(marker,idx){
marker.on('mousedown',function(e){if(e.originalEvent)L.DomEvent.stopPropagation(e.originalEvent);map.dragging.disable();
var onM=function(ev){editLL[idx]=[ev.latlng.lat,ev.latlng.lng];marker.setLatLng(ev.latlng);updateGhost();};
var onU=function(){map.dragging.enable();map.off('mousemove',onM);map.off('mouseup',onU);};
map.on('mousemove',onM);map.on('mouseup',onU);});
}
function startMove(){map.dragging.disable();map._mh=function(e){moveActive=true;movePrev=e.latlng;var oM=function(ev){if(!moveActive||!movePrev)return;var dl=ev.latlng.lat-movePrev.lat,dg=ev.latlng.lng-movePrev.lng;editLL=editLL.map(function(p){return[p[0]+dl,p[1]+dg];});movePrev=ev.latlng;updateGhost();vtxMarkers.forEach(function(m,i){m.setLatLng([editLL[i][0],editLL[i][1]]);});};var oU=function(){moveActive=false;movePrev=null;map.off('mousemove',oM);map.off('mouseup',oU);};map.on('mousemove',oM);map.on('mouseup',oU);};map.on('mousedown',map._mh);map.getContainer().style.cursor='grab';}
function stopMove(){if(map._mh){map.off('mousedown',map._mh);map._mh=null;}map.dragging.enable();map.getContainer().style.cursor='';}
function setGeoMode(m){
if(geoMode===m){
geoMode=null;
setGeoBtn(null);
vtxMarkers.forEach(function(mk){map.removeLayer(mk);});
vtxMarkers=[];
midMarkers.forEach(function(mk){map.removeLayer(mk);});
midMarkers=[];
stopMove();
map.off('click', onMapClickForAdd);
return;
}
stopMove();
map.off('click', onMapClickForAdd);
geoMode=m;
setGeoBtn(m);
renderVtx();
if(m==='move')startMove();
if(m==='add'){
map.on('click', onMapClickForAdd);
}
}
function setGeoBtn(m){['vertex','move','add','del'].forEach(function(k){var b=el('geo-btn-'+k);if(b)b.className='geo-btn'+(m===k?' active':''); var bm=el('gbm-'+k); if(bm)bm.className='geo-btn-mini'+(m===k?' active':'');});}
/* ══ SUBMIT EDIT ══════════════════════════════════════ */
function submitEdit(){
if(!activeId)return;
var nama=el('sp-nama').value.trim();
if(!nama){showToast('Nama tidak boleh kosong!','error');return;}
var btn=el('sp-save-btn');btn.textContent='⏳ Menyimpan…';btn.disabled=true;
if(activeType==='rusak'){
var fd=new FormData();
fd.append('action','edit_rusak');fd.append('id',activeId);
fd.append('nama',nama);fd.append('tanggal',el('sp-tanggal').value);
fd.append('keterangan',el('sp-ket-rusak').value.trim());
fd.append('foto',spFotoBase64);
fd.append('latitude',rusakData[activeId].lat);
fd.append('longitude',rusakData[activeId].lng);
fetch('api.php?action=edit_rusak&id='+activeId,{method:'POST',body:fd}).then(function(r){return r.json();}).then(function(d){
btn.textContent='💾 Simpan';btn.disabled=false;
if(d.error){showToast('Error: '+d.error,'error');return;}
updateStats(d);
var rd=rusakData[activeId];
rd.nama=nama;rd.tgl=el('sp-tanggal').value;rd.ket=el('sp-ket-rusak').value.trim();rd.foto=spFotoBase64;
rusakLayers[activeId].setPopupContent(makeRusakPopup(activeId,rd.nama,rd.tgl,rd.lat,rd.lng,rd.ket,rd.foto,rd.age||'recent',rd.recurring||false));
el('sp-sub').textContent=nama;
closePanel(); // Close modal on success!
showToast('Data jalan rusak diperbarui!','success');
}).catch(function(){btn.textContent='💾 Simpan';btn.disabled=false;showToast('Koneksi gagal!','error');});
return;
}
var status=el('sp-status').value;
var coords=JSON.stringify(editLL);
var url='api.php?action=edit_'+(activeType==='jalan'?'jalan':'parsil')+'&id='+activeId+'&nama='+encodeURIComponent(nama)+'&status='+encodeURIComponent(status)+'&koordinat='+encodeURIComponent(coords);
if(activeType==='parsil'){url+='&pemilik='+encodeURIComponent(el('sp-pemilik').value.trim())+'&nosert='+encodeURIComponent(el('sp-nosert').value.trim())+'&keterangan='+encodeURIComponent(el('sp-ket').value.trim());}
fetch(url).then(function(r){return r.json();}).then(function(d){
btn.textContent='💾 Simpan';btn.disabled=false;
if(d.error){showToast('Error: '+d.error,'error');return;}
updateStats(d);
if(activeType==='jalan'){
var np=d.panjang!=null?parseFloat(d.panjang).toFixed(1):el('sp-ukuran').value.replace(/[^0-9.]/g,'');
jalanLayers[activeId].setLatLngs(editLL);jalanLayers[activeId].setStyle({color:WARNA_JALAN[status]||'#999'});
jalanLayers[activeId].setPopupContent(makeJalanPopup(activeId,nama,status,np));
jalanData[activeId]=Object.assign(jalanData[activeId],{nama:nama,status:status,pjg:np,ll:editLL.map(function(p){return[p[0],p[1]];})});
el('sp-ukuran').value=np+' m';
} else {
var pm=el('sp-pemilik').value.trim(),pn=el('sp-nosert').value.trim(),pk=el('sp-ket').value.trim();
var nl=d.luas!=null?parseFloat(d.luas).toFixed(1):el('sp-ukuran').value.replace(/[^0-9.]/g,'');
parsilLayers[activeId].setLatLngs(editLL);parsilLayers[activeId].setStyle(WARNA_PARSIL[status]||{});
parsilLayers[activeId].setPopupContent(makeParsilPopup(activeId,nama,pm,pn,pk,status,nl));
parsilData[activeId]=Object.assign(parsilData[activeId],{nama:nama,status:status,pemilik:pm,nosert:pn,ket:pk,luas:nl,ll:editLL.map(function(p){return[p[0],p[1]];})});
el('sp-ukuran').value=nl+' m²';
}
el('sp-sub').textContent=nama;if(activeTab==='geo')updateGhost();
showToast('Data berhasil disimpan!','success');
}).catch(function(){btn.textContent='💾 Simpan';btn.disabled=false;showToast('Koneksi gagal!','error');});
}
/* ══ TOAST ════════════════════════════════════════════ */
function showToast(msg,type){
var old=document.getElementById('__t');if(old)old.remove();
var t=document.createElement('div');t.id='__t';t.textContent=msg;
t.style.cssText='position:fixed;bottom:28px;left:50%;transform:translateX(-50%);z-index:99999;padding:10px 22px;border-radius:24px;font-size:13px;font-weight:600;font-family:Inter,sans-serif;box-shadow:0 4px 20px rgba(0,0,0,.14);transition:opacity .3s;white-space:nowrap;'+(type==='success'?'background:#f0fdf4;color:#15803d;border:1px solid #86efac;':'background:#fef2f2;color:#dc2626;border:1px solid #fca5a5;');
document.body.appendChild(t);
setTimeout(function(){t.style.opacity='0';setTimeout(function(){t.remove();},300);},2800);
}
/* ══ LEFT NAV BAR ════════════════════════════════════ */
var lnavTipe='semua', lnavStatus='', lnavPmin=0, lnavPmax=0;
var lnavSearchTimer=null;
var lnavActiveId=null, lnavActiveType=null;
// Semua data dikumpulkan setelah load (dipopulate oleh PHP loop di atas)
// jalanData, parsilData, rusakData sudah tersedia
function lnavResetStatusChips(){
lnavStatus='';
document.querySelectorAll('.filter-chip[data-group="status"]').forEach(function(c){c.classList.remove('active');});
document.querySelectorAll('.filter-chip[data-group="status"][data-val=""]').forEach(function(c){c.classList.add('active');});
}
function lnavUpdateFilterPanels(){
var t=lnavTipe;
var jalanBlock=document.getElementById('lnav-jalan-block');
var parsilBlock=document.getElementById('lnav-parsil-block');
var panjangRow=document.getElementById('lnav-panjang-row');
if(jalanBlock) jalanBlock.style.display=(t==='semua'||t==='jalan')?'flex':'none';
if(parsilBlock) parsilBlock.style.display=(t==='semua'||t==='parsil')?'flex':'none';
if(panjangRow) panjangRow.style.display=(t==='parsil'||t==='rusak')?'none':'flex';
}
function lnavChip(el){
var g=el.getAttribute('data-group'), v=el.getAttribute('data-val');
if(g==='tipe'){
document.querySelectorAll('.filter-seg-btn[data-group="tipe"]').forEach(function(c){c.classList.remove('active');});
el.classList.add('active');
lnavTipe=v;
lnavResetStatusChips();
lnavUpdateFilterPanels();
}
if(g==='status'){
document.querySelectorAll('.filter-chip[data-group="status"]').forEach(function(c){c.classList.remove('active');});
el.classList.add('active');
lnavStatus=v;
}
lnavRender();
}
lnavUpdateFilterPanels();
// Close side panel when clicking outside the sp-card
var spEl = document.getElementById('side-panel');
if (spEl) {
spEl.addEventListener('click', function(e) {
if (e.target === this) {
closePanel();
}
});
}
var lnavInputEl = document.getElementById('lnav-input');
if (lnavInputEl) {
lnavInputEl.addEventListener('input', function(){
clearTimeout(lnavSearchTimer);
lnavSearchTimer=setTimeout(lnavRender, 200);
});
}
var lnavPminEl = document.getElementById('lnav-pmin');
var lnavPmaxEl = document.getElementById('lnav-pmax');
if (lnavPminEl) lnavPminEl.addEventListener('input', function(){clearTimeout(lnavSearchTimer);lnavSearchTimer=setTimeout(lnavRender,300);});
if (lnavPmaxEl) lnavPmaxEl.addEventListener('input', function(){clearTimeout(lnavSearchTimer);lnavSearchTimer=setTimeout(lnavRender,300);});
function lnavRender(){
var inputEl=document.getElementById('lnav-input');
if(!inputEl)return;
var q=inputEl.value.trim().toLowerCase();
lnavPmin=parseFloat(document.getElementById('lnav-pmin').value)||0;
lnavPmax=parseFloat(document.getElementById('lnav-pmax').value)||0;
var results=[];
var jalanStatuses=['Jalan Nasional','Jalan Provinsi','Jalan Kabupaten'];
var parsilStatuses=['SHM','HGB','HGU','HP'];
// ── Jalan ──
if(lnavTipe==='semua'||lnavTipe==='jalan'){
Object.values(jalanData).forEach(function(d){
if(lnavStatus && parsilStatuses.indexOf(lnavStatus)>=0) return;
if(lnavStatus && d.status!==lnavStatus) return;
if(lnavPmin>0 && parseFloat(d.pjg)<lnavPmin) return;
if(lnavPmax>0 && parseFloat(d.pjg)>lnavPmax) return;
if(q && d.nama.toLowerCase().indexOf(q)<0) return;
results.push({type:'jalan',id:d.id,icon:'🛣',name:d.nama,meta:d.status+' · '+parseFloat(d.pjg).toFixed(0)+' m',badge:'Jalan',badgeCls:'lnav-badge-jalan'});
});
}
// ── Parsil ──
if(lnavTipe==='semua'||lnavTipe==='parsil'){
Object.values(parsilData).forEach(function(d){
if(lnavStatus && jalanStatuses.indexOf(lnavStatus)>=0) return;
if(lnavStatus && d.status!==lnavStatus) return;
if(q && d.nama.toLowerCase().indexOf(q)<0 && (!d.pemilik||d.pemilik.toLowerCase().indexOf(q)<0)) return;
results.push({type:'parsil',id:d.id,icon:'🏘',name:d.nama,meta:d.status+(d.pemilik?' · '+d.pemilik:''),badge:'Kavling',badgeCls:'lnav-badge-parsil'});
});
}
// ── Rusak ──
if(lnavTipe==='semua'||lnavTipe==='rusak'){
Object.values(rusakData).forEach(function(d){
if(q && d.nama.toLowerCase().indexOf(q)<0) return;
results.push({type:'rusak',id:d.id,icon:'⚠',name:d.nama,meta:'📅 '+d.tgl,badge:'Rusak',badgeCls:'lnav-badge-rusak'});
});
}
// Sort: jalan asc nama, parsil asc nama, rusak desc tgl
results.sort(function(a,b){
if(a.type!==b.type) return a.type<b.type?-1:1;
return a.name.localeCompare(b.name,'id');
});
// Count
var countEl=document.getElementById('lnav-count');
countEl.textContent=results.length+' data ditemukan';
var list=document.getElementById('lnav-list');
if(results.length===0){
list.innerHTML='<div class="lnav-empty">😕 Tidak ada data<br>yang sesuai filter</div>';
return;
}
list.innerHTML=results.map(function(r){
var isActive=(lnavActiveId===r.id && lnavActiveType===r.type);
return '<div class="lnav-item'+(isActive?' active':'')+'" data-id="'+r.id+'" data-type="'+r.type+'" onclick="lnavFlyTo(this)">'
+'<span class="lnav-item-icon">'+r.icon+'</span>'
+'<div class="lnav-item-body">'
+'<div class="lnav-item-name">'+h(r.name)+'</div>'
+'<div class="lnav-item-meta">'+h(r.meta)+'</div>'
+'</div>'
+'<span class="lnav-item-badge '+r.badgeCls+'">'+r.badge+'</span>'
+'</div>';
}).join('');
}
function lnavFlyTo(el){
var type = el.getAttribute('data-type');
var id = parseInt(el.getAttribute('data-id'), 10);
document.querySelectorAll('.lnav-item').forEach(function(i){i.classList.remove('active');});
el.classList.add('active');
lnavActiveId=id; lnavActiveType=type;
try{
if(type==='jalan'){
var d=jalanData[id]; if(!d)return;
var ll=d.ll; if(!ll||!ll.length)return;
map.flyToBounds(L.polyline(ll).getBounds().pad(.35),{maxZoom:17,duration:.9});
setTimeout(function(){if(jalanLayers[id])jalanLayers[id].openPopup();},1000);
} else if(type==='parsil'){
var d=parsilData[id]; if(!d)return;
var ll=d.ll; if(!ll||!ll.length)return;
map.flyToBounds(L.polygon(ll).getBounds().pad(.35),{maxZoom:18,duration:.9});
setTimeout(function(){if(parsilLayers[id])parsilLayers[id].openPopup();},1000);
} else if(type==='rusak'){
var d=rusakData[id]; if(!d)return;
map.flyTo([d.lat,d.lng],17,{duration:.9});
setTimeout(function(){if(rusakLayers[id])rusakLayers[id].openPopup();},1000);
}
}catch(e){console.error('lnavFlyTo error:',e);}
}
// Highlight sidebar saat klik layer di peta
function lnavHighlightFromMap(type, id){
if(!document.getElementById('lnav-list'))return;
lnavActiveId=id; lnavActiveType=type;
document.querySelectorAll('.lnav-item').forEach(function(i){
var match=(parseInt(i.getAttribute('data-id'))===id && i.getAttribute('data-type')===type);
i.classList.toggle('active', match);
if(match) i.scrollIntoView({behavior:'smooth',block:'nearest'});
});
}
if (document.getElementById('lnav-list')) {
setTimeout(function(){ lnavRender(); }, 100);
}
</script>
<?php if ($mode === 'input'): ?>
<script src="assets/jalan-input.js?v=<?= @filemtime(__DIR__ . '/assets/jalan-input.js') ?: time() ?>"></script>
<?php endif; ?>
</body>
</html>