Files
TUGAS-SIG/gis_maps.html
T
superbia-17 2b42e822eb first commit
2026-06-06 23:34:20 +07:00

288 lines
13 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>GIS Pontianak - Integrated SPBU Management</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css"/>
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500;600&display=swap" rel="stylesheet"/>
<style>
:root {
--bg: #0d1117; --surface: #161b22; --border: #30363d;
--accent: #00d4aa; --accent2: #ff6b35; --text: #e6edf3;
--muted: #8b949e; --danger: #f85149; --panel-w: 340px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family:'DM Sans',sans-serif; background:var(--bg); color:var(--text); height:100vh; display:flex; flex-direction:column; overflow:hidden; }
/* ── HEADER ── */
header { height:52px; background:var(--surface); border-bottom:1px solid var(--border); display:flex; align-items:center; padding:0 16px; gap:10px; z-index:1000; flex-shrink:0; }
.logo { font-family:'Space Mono',monospace; font-size:15px; font-weight:700; color:var(--accent); letter-spacing:-.5px; }
.logo span { color:var(--accent2); }
.badge { background:rgba(0,212,170,.12); color:var(--accent); font-size:11px; font-weight:600; padding:3px 8px; border-radius:4px; border:1px solid rgba(0,212,170,.3); }
/* Search Bar */
.search-wrap { position: relative; flex: 1; max-width: 320px; }
#search-input { width:100%; background:var(--bg); border:1px solid var(--border); border-radius:8px; padding:7px 34px; font-size:13px; color:var(--text); outline:none; }
#search-dropdown { display:none; position:absolute; top:calc(100% + 6px); left:0; right:0; background:var(--surface); border:1px solid var(--border); border-radius:10px; z-index:9000; max-height:380px; overflow-y:auto; }
#search-dropdown.open { display:block; }
/* Toolbar */
.draw-toolbar { display:flex; align-items:center; gap:6px; margin-left:auto; }
.tool-btn { display:flex; align-items:center; gap:5px; padding:6px 11px; border-radius:6px; border:1px solid var(--border); background:var(--bg); color:var(--muted); font-size:12px; font-weight:600; cursor:pointer; transition:all .2s; }
.tool-btn:hover { border-color:var(--accent); color:var(--accent); }
.tool-btn.active { border-color:var(--accent2); color:#fff; background:var(--accent2); }
/* ── MAIN LAYOUT ── */
.main { flex:1; display:flex; overflow:hidden; }
#map { flex:1; }
/* Panel & Tabs */
.panel { width:var(--panel-w); background:var(--surface); border-left:1px solid var(--border); display:flex; flex-direction:column; flex-shrink:0; }
.panel-tabs { display:flex; border-bottom:1px solid var(--border); }
.tab-btn { flex:1; padding:10px; font-size:11px; font-weight:700; text-transform:uppercase; background:none; border:none; color:var(--muted); cursor:pointer; border-bottom:2px solid transparent; }
.tab-btn.active { color:var(--accent); border-bottom-color:var(--accent); }
.tab-pane { display:none; flex:1; flex-direction:column; overflow:hidden; }
.tab-pane.active { display:flex; }
.records-list { flex:1; overflow-y:auto; padding:10px; display:flex; flex-direction:column; gap:7px; }
.record-card { background:var(--bg); border:1px solid var(--border); border-radius:8px; padding:11px 13px; cursor:pointer; position:relative; }
.rc-name { font-weight:600; font-size:13px; color:var(--text); }
.rc-sub { font-size:11px; color:var(--muted); margin-top:2px; font-family:'Space Mono',monospace; }
/* Modal */
.modal-overlay { display:none; position:fixed; inset:0; background:rgba(0,0,0,.6); z-index:9999; align-items:center; justify-content:center; backdrop-filter:blur(4px); }
.modal-overlay.active { display:flex; }
.modal { background:var(--surface); border:1px solid var(--border); border-radius:14px; width:400px; padding:20px; }
.field { margin-bottom: 15px; }
.field label { display:block; font-size:11px; color:var(--muted); margin-bottom:5px; }
.field input, .field select { width:100%; background:var(--bg); border:1px solid var(--border); padding:9px; color:#fff; border-radius:7px; outline:none; }
/* Drawing Hint */
#draw-hint { display:none; position:fixed; bottom:60px; left:50%; transform:translateX(-50%); background:var(--surface); border:1px solid var(--border); border-radius:10px; padding:10px 20px; z-index:5000; }
#draw-hint.visible { display:flex; align-items:center; gap:10px; }
/* Leaflet Control Custom */
.leaflet-control-layers { background: var(--surface) !important; color: var(--text) !important; border: 1px solid var(--border) !important; border-radius: 8px !important; }
#toast { position:fixed; bottom:24px; left:50%; transform:translateX(-50%); background:var(--accent); color:#0d1117; padding:10px 22px; border-radius:8px; opacity:0; transition:0.3s; z-index:10000; font-weight:700; }
#toast.show { opacity:1; }
</style>
</head>
<body>
<header>
<div class="logo">GIS<span>.</span>PTK</div>
<span class="badge">Pontianak</span>
<div class="search-wrap">
<input type="text" id="search-input" placeholder="Cari lokasi, jalan, atau kavling..."/>
<div id="search-dropdown"></div>
</div>
<div class="draw-toolbar">
<button class="tool-btn active" id="tool-point">📍 Titik</button>
<button class="tool-btn" id="tool-polyline">〰️ Jalan</button>
<button class="tool-btn" id="tool-polygon">⬡ Kavling</button>
</div>
</header>
<div class="main">
<div id="map"></div>
<aside class="panel">
<div class="panel-tabs">
<button class="tab-btn active" data-tab="tab-points">📍 Titik</button>
<button class="tab-btn" data-tab="tab-roads">〰️ Jalan</button>
<button class="tab-btn" data-tab="tab-kavling">⬡ Kavling</button>
</div>
<div class="tab-pane active" id="tab-points">
<div class="records-list" id="records-list"></div>
</div>
<div class="tab-pane" id="tab-roads">
<div class="records-list" id="roads-list"></div>
</div>
<div class="tab-pane" id="tab-kavling">
<div class="records-list" id="kavling-list"></div>
</div>
</aside>
</div>
<div class="modal-overlay" id="modal-point">
<div class="modal">
<h3 style="margin-bottom:15px">📍 Tambah SPBU</h3>
<div class="field"><label>Nama Lokasi</label><input type="text" id="pt-name"/></div>
<div class="field"><label>Telepon</label><input type="tel" id="pt-phone"/></div>
<div class="field">
<label>Buka 24 Jam?</label>
<select id="pt-is24h">
<option value="1">Ya, 24 Jam</option>
<option value="0">Tidak</option>
</select>
</div>
<button class="tool-btn active" id="pt-save" style="width:100%; justify-content:center; padding:12px">Simpan</button>
</div>
</div>
<div id="draw-hint">
<span id="draw-hint-text">Klik peta untuk menggambar</span>
<button class="tool-btn active" id="dh-finish">Selesai</button>
<button class="tool-btn" id="dh-cancel">Batal</button>
</div>
<div id="toast"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
<script>
// --- CONFIG & STATE ---
const PONTIANAK = [-0.0264, 109.3425];
const API_POINT = 'api/locations.php';
const API_ROAD = 'api/roads.php';
const API_KAVLING = 'api/kavlings.php';
let currentMode = 'point';
let drawingPoints = [];
let previewLayer = null;
let pendingLatLng = null;
const markerMap = {}, roadMap = {}, kavlingMap = {};
let searchCache = { points:[], roads:[], kavlings:[] };
// --- INITIALIZE MAP & LAYER GROUPS ---
const map = L.map('map').setView(PONTIANAK, 14);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
// LAYER GROUPS (TUGAS)
const layerSpbu24 = L.layerGroup().addTo(map);
const layerSpbuReguler = L.layerGroup().addTo(map);
const overlayMaps = {
"SPBU Buka 24 Jam": layerSpbu24,
"SPBU Tidak Buka 24 Jam": layerSpbuReguler
};
L.control.layers(null, overlayMaps, { collapsed: false }).addTo(map);
// --- API HELPERS ---
const api = {
async get(url) { const r=await fetch(url); const j=await r.json(); return j.data; },
async post(url,body) { const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}); return (await r.json()).data; },
async del(url) { await fetch(url,{method:'DELETE'}); }
};
// --- RESTORED DRAWING TOOLS LOGIC ---
document.getElementById('tool-point').onclick = () => setMode('point');
document.getElementById('tool-polyline').onclick = () => setMode('polyline');
document.getElementById('tool-polygon').onclick = () => setMode('polygon');
function setMode(mode) {
currentMode = mode;
drawingPoints = [];
if(previewLayer) map.removeLayer(previewLayer);
document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active'));
document.getElementById('tool-' + mode).classList.add('active');
document.getElementById('draw-hint').classList.toggle('visible', mode !== 'point');
}
map.on('click', (e) => {
if(currentMode === 'point') {
pendingLatLng = e.latlng;
document.getElementById('modal-point').classList.add('active');
} else {
drawingPoints.push(e.latlng);
updatePreview();
}
});
function updatePreview() {
if(previewLayer) map.removeLayer(previewLayer);
if(drawingPoints.length < 2) return;
previewLayer = currentMode === 'polyline' ? L.polyline(drawingPoints, {color: '#ff6b35'}) : L.polygon(drawingPoints, {color: '#a371f7'});
previewLayer.addTo(map);
}
// --- RESTORED SEARCH LOGIC ---
document.getElementById('search-input').oninput = (e) => {
const q = e.target.value.toLowerCase();
const drop = document.getElementById('search-dropdown');
if(!q) { drop.classList.remove('open'); return; }
let html = '';
const filtered = searchCache.points.filter(p => p.name.toLowerCase().includes(q));
filtered.forEach(p => {
html += `<div style="padding:10px; cursor:pointer; border-bottom:1px solid var(--border)" onclick="focusLoc(${p.lat}, ${p.lng}, ${p.id})">📍 ${p.name}</div>`;
});
drop.innerHTML = html || '<div style="padding:10px">Tidak ditemukan</div>';
drop.classList.add('open');
};
window.focusLoc = (lat, lng, id) => {
map.flyTo([lat, lng], 17);
if(markerMap[id]) markerMap[id].openPopup();
document.getElementById('search-dropdown').classList.remove('open');
};
// --- REFRESH LISTS & LAYER GROUPS ---
function placeMarker(rec) {
const is24 = rec.is24h == 1;
const color = is24 ? '#56d364' : '#ff7b72';
const icon = L.divIcon({ html: `<div style="background:${color}; width:12px; height:12px; border-radius:50%; border:2px solid #fff;"></div>`, className: '', iconSize:[12,12]});
const marker = L.marker([rec.lat, rec.lng], {icon}).bindPopup(`<b>${rec.name}</b><br>${is24 ? '✅ 24 Jam' : '❌ Reguler'}`);
// Logic Layer Group
if(is24) marker.addTo(layerSpbu24); else marker.addTo(layerSpbuReguler);
markerMap[rec.id] = marker;
}
async function refreshAll() {
const pts = await api.get(API_POINT);
searchCache.points = pts;
layerSpbu24.clearLayers();
layerSpbuReguler.clearLayers();
const list = document.getElementById('records-list');
list.innerHTML = '';
pts.forEach(p => {
placeMarker(p);
list.innerHTML += `<div class="record-card" onclick="map.panTo([${p.lat},${p.lng}])">
<div class="rc-name">${p.name}</div>
<div class="rc-sub">${p.is24h == 1 ? 'Buka 24 Jam' : 'Reguler'}</div>
<button style="position:absolute; top:8px; right:8px; background:none; border:none; color:var(--danger); cursor:pointer" onclick="event.stopPropagation(); deletePt(${p.id})">🗑</button>
</div>`;
});
}
window.deletePt = async (id) => {
await api.del(API_POINT + '?id=' + id);
if(markerMap[id]) {
layerSpbu24.removeLayer(markerMap[id]);
layerSpbuReguler.removeLayer(markerMap[id]);
}
refreshAll();
};
document.getElementById('pt-save').onclick = async () => {
const name = document.getElementById('pt-name').value;
const is24h = document.getElementById('pt-is24h').value;
if(!name) return;
await api.post(API_POINT, {name, is24h: parseInt(is24h), lat: pendingLatLng.lat, lng: pendingLatLng.lng});
document.getElementById('modal-point').classList.remove('active');
refreshAll();
showToast('Lokasi disimpan');
};
// --- TABS LOGIC ---
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.onclick = () => {
document.querySelectorAll('.tab-btn, .tab-pane').forEach(el => el.classList.remove('active'));
btn.classList.add('active');
document.getElementById(btn.dataset.tab).classList.add('active');
};
});
function showToast(m) { const t=document.getElementById('toast'); t.textContent=m; t.classList.add('show'); setTimeout(()=>t.classList.remove('show'),2000); }
refreshAll();
</script>
</body>
</html>