Files
tugas-kuliah-SIG/tugas_akhir_webgis/index.php
T
Wilhelmus Ikchan Dwi Putra c8e75593cb Upload semua tugas SIG
2026-06-13 00:45:23 +07:00

297 lines
15 KiB
PHP

<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EcoGIS Care - Pemetaan Prasejahtera & Rumah Ibadah</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
* { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
body { display: flex; height: 100vh; background-color: #0f172a; overflow: hidden; }
/* Layout Utama - Sidebar Kiri */
#sidebar {
width: 380px;
background: #1e293b;
box-shadow: 4px 0 15px rgba(0,0,0,0.3);
display: flex;
flex-direction: column;
z-index: 1000;
color: #f1f5f9;
border-right: 1px solid #334155;
}
#map-container { flex: 1; position: relative; height: 100%; }
#map { height: 100%; width: 100%; }
/* Komponen Sidebar */
.header { padding: 25px 20px; background: linear-gradient(135deg, #0f172a, #1e293b); border-bottom: 1px solid #334155; }
.header h1 { font-size: 1.4rem; font-weight: 700; color: #f8fafc; margin-bottom: 5px; }
.header p { font-size: 0.85rem; color: #94a3b8; }
/* KPI Dashboard Ringkas */
.kpi-container { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; padding: 15px; background: #0f172a; border-bottom: 1px solid #334155; }
.kpi-box { padding: 12px 8px; border-radius: 8px; text-align: center; color: white; font-weight: bold; }
.kpi-ekstrem { background-color: #e11d48; }
.kpi-miskin { background-color: #ea580c; }
.kpi-rentan { background-color: #d97706; }
.kpi-box span { display: block; font-size: 1.4rem; font-weight: 800; }
.kpi-box label { font-size: 0.65rem; text-transform: uppercase; letter-spacing: 0.5px; opacity: 0.9; }
/* Konten Fitur Tab / List di Kiri */
.content-area { flex: 1; overflow-y: auto; padding: 15px; background: #1e293b; }
.section-title { font-size: 0.9rem; font-weight: 700; color: #cbd5e1; margin: 15px 0 8px 0; text-transform: uppercase; letter-spacing: 0.5px; }
/* Desain Tabel Data */
.data-table-wrapper { max-height: 220px; overflow-y: auto; border: 1px solid #334155; border-radius: 6px; background: #0f172a; margin-bottom: 15px; }
.data-table { width: 100%; border-collapse: collapse; font-size: 0.85rem; text-align: left; }
.data-table th { background: #1e293b; padding: 10px; font-weight: 600; color: #94a3b8; position: sticky; top: 0; border-bottom: 1px solid #334155; }
.data-table td { padding: 10px; border-bottom: 1px solid #334155; color: #cbd5e1; cursor: pointer; }
.data-table tr:hover { background-color: #1e293b; }
/* Badges */
.badge { padding: 3px 6px; border-radius: 4px; font-size: 0.75rem; font-weight: 600; color: white; display: inline-block; }
.badge-ekstrem { background-color: #e11d48; }
.badge-miskin { background-color: #ea580c; }
.badge-rentan { background-color: #d97706; }
/* REVISI 1: FLOATING PANEL AI PROMPT DENGAN OUTPUT KOTAK JAWABAN */
.ai-floating-panel {
position: absolute;
top: 20px;
right: 20px;
z-index: 1100;
width: 360px;
background: rgba(15, 23, 42, 0.95);
backdrop-filter: blur(8px);
padding: 15px;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0,0,0,0.4);
border: 1px solid #334155;
}
.ai-title { font-size: 0.85rem; font-weight: 700; color: #3b82f6; text-transform: uppercase; margin-bottom: 8px; letter-spacing: 0.5px; }
.ai-input-wrapper { display: flex; gap: 8px; margin-bottom: 10px; }
.ai-input { flex: 1; padding: 10px 12px; background: #1e293b; border: 1px solid #475569; border-radius: 8px; font-size: 0.85rem; color: white; outline: none; }
.ai-input:focus { border-color: #3b82f6; }
.ai-btn { padding: 10px 16px; background: #3b82f6; color: white; border: none; border-radius: 8px; font-weight: 600; font-size: 0.85rem; cursor: pointer; }
.ai-btn:hover { background: #2563eb; }
/* Box Jawaban Teks Hasil Prompt AI */
.ai-response-box {
background: #1e293b;
border: 1px solid #334155;
border-radius: 8px;
padding: 10px 12px;
font-size: 0.8rem;
color: #cbd5e1;
max-height: 150px;
overflow-y: auto;
line-height: 1.4;
display: none; /* Muncul hanya saat ada interaksi */
}
.leaflet-top.leaflet-right { top: 220px !important; }
</style>
</head>
<body>
<div id="sidebar">
<div class="header">
<h1>EcoGIS Care</h1>
<p>Sistem Pemetaan Kemiskinan Berbasis Rumah Ibadah</p>
</div>
<div class="kpi-container">
<div class="kpi-box kpi-ekstrem"><span id="count-ekstrem">0</span><label>Ekstrem</label></div>
<div class="kpi-box kpi-miskin"><span id="count-miskin">0</span><label>Miskin</label></div>
<div class="kpi-box kpi-rentan"><span id="count-rentan">0</span><label>Rentan</label></div>
</div>
<div class="content-area">
<div class="section-title">🏠 Data Hunian Prasejahtera</div>
<div class="data-table-wrapper">
<table class="data-table">
<thead>
<tr><th>Nama KK</th><th>Status</th><th>Luas</th></tr>
</thead>
<tbody id="tabel-warga"></tbody>
</table>
</div>
<div class="section-title">📍 Rumah Ibadah (Mitra Posko)</div>
<div class="data-table-wrapper">
<table class="data-table">
<thead>
<tr><th>Nama Tempat Ibadah</th><th>Agama</th></tr>
</thead>
<tbody id="tabel-ibadah"></tbody>
</table>
</div>
</div>
</div>
<div id="map-container">
<div class="ai-floating-panel">
<div class="ai-title">🤖 AI Assistant & Prompt GIS</div>
<div class="ai-input-wrapper">
<input type="text" id="ai-prompt" class="ai-input" placeholder="Ketik: 'Cari Masjid Mujahidin' atau 'Miskin'..." />
<button id="ai-submit" class="ai-btn">Kirim</button>
</div>
<div id="ai-response" class="ai-response-box"></div>
</div>
<div id="map"></div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
const map = L.map('map', { zoomControl: true }).setView([-0.026330, 109.342503], 13);
const googleSatellitePure = L.tileLayer('https://{s}.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', {
maxZoom: 20,
subdomains: ['mt0', 'mt1', 'mt2', 'mt3'],
attribution: '© Google Maps Satellite'
}).addTo(map);
const groupEkstrem = L.layerGroup().addTo(map);
const groupMiskin = L.layerGroup().addTo(map);
const groupRentan = L.layerGroup().addTo(map);
const groupIbadah = L.layerGroup().addTo(map);
L.control.layers(null, {
"<span style='color:#e11d48; font-weight:bold;'>🔴 Miskin Ekstrem</span>": groupEkstrem,
"<span style='color:#ea580c; font-weight:bold;'>🟠 Miskin</span>": groupMiskin,
"<span style='color:#d97706; font-weight:bold;'>🟡 Rentan Miskin</span>": groupRentan,
"<span style='color:#3b82f6; font-weight:bold;'>🔵 Rumah Ibadah</span>": groupIbadah
}, { position: 'topright', collapsed: false }).addTo(map);
let seluruhKoleksiSpasial = [];
let dataAsliWargaGlobal = [];
let currentRadiusLayer = null; // Variabel penampung objek radius aktif
let cEkstrem = 0, cMiskin = 0, cRentan = 0;
function appendTabel(targetId, id, col1, col2, col3 = '', tipe) {
const tbody = document.getElementById(targetId);
const row = document.createElement('tr');
row.innerHTML = tipe === 'warga' ? `<td><b>${col1}</b></td><td>${col2}</td><td><small>${col3}</small></td>` : `<td><b>${col1}</b></td><td><span class="badge" style="background:#3b82f6;">${col2}</span></td>`;
row.addEventListener('click', () => {
const item = seluruhKoleksiSpasial.find(x => x.id == id && x.type === tipe);
if(item && item.layer) {
map.flyTo(item.layer.getLatLng(), 17);
item.layer.openPopup();
if(tipe === 'ibadah') pemicuRadiusRumahIbadah(item.layer.getLatLng(), col1);
}
});
tbody.appendChild(row);
}
// REVISI 2: FUNGSI AKTIVASI INTERAKTIF RADIUS JANGKAUAN 1 KM
function pemicuRadiusRumahIbadah(latlng, namaIbadah) {
if(currentRadiusLayer) map.removeLayer(currentRadiusLayer);
currentRadiusLayer = L.circle(latlng, {
radius: 1000, // Jangkauan radius 1 Kilometer (1000 Meter)
color: '#3b82f6',
fillColor: '#3b82f6',
fillOpacity: 0.15,
weight: 1.5,
dashArray: '5, 5'
}).addTo(map);
}
// Hapus radius jika peta kosong di-klik
map.on('click', () => { if(currentRadiusLayer) { map.removeLayer(currentRadiusLayer); currentRadiusLayer = null; } });
// LOAD DATA BACKEND
fetch('get_poverty_data.php')
.then(response => response.json())
.then(data => {
dataAsliWargaGlobal = data.warga;
// DATA RUMAH IBADAH
data.ibadah.forEach(ib => {
const marker = L.circleMarker([ib.lat, ib.lng], {
radius: 9, fillColor: '#3b82f6', color: '#ffffff', weight: 2, fillOpacity: 1
}).bindPopup(`<b>⛪ Rumah Ibadah: ${ib.nama}</b><br>Kategori: <b>${ib.agama}</b><br><small style="color:#60a5fa;">💡 Klik titik untuk melihat radius 1 KM</small>`);
marker.on('click', (e) => pemicuRadiusRumahIbadah(e.latlng, ib.nama));
marker.addTo(groupIbadah);
seluruhKoleksiSpasial.push({ id: ib.id, name: ib.nama.toLowerCase(), type: 'ibadah', layer: marker, detail: ib });
appendTabel('tabel-ibadah', ib.id, ib.nama, ib.agama, '', 'ibadah');
});
// DATA WARGA MISKIN
data.warga.forEach(w => {
let badgeClass = w.klasifikasi === 'Miskin Ekstrem' ? 'badge-ekstrem' : (w.klasifikasi === 'Miskin' ? 'badge-miskin' : 'badge-rentan');
if(w.klasifikasi === 'Miskin Ekstrem') cEkstrem++; else if(w.klasifikasi === 'Miskin') cMiskin++; else cRentan++;
let warnaBulat = w.klasifikasi === 'Miskin Ekstrem' ? '#e11d48' : (w.klasifikasi === 'Miskin' ? '#ea580c' : '#d97706');
let geoData = (typeof w.geojson === 'string') ? JSON.parse(w.geojson) : w.geojson;
if (geoData && geoData.geometry) {
let koordinatFix = geoData.geometry.type === 'Polygon' ? [geoData.geometry.coordinates[0][0][1], geoData.geometry.coordinates[0][0][0]] : [geoData.geometry.coordinates[1], geoData.geometry.coordinates[0]];
if (koordinatFix) {
const markerWarga = L.circleMarker(koordinatFix, {
radius: 8, fillColor: warnaBulat, color: '#ffffff', weight: 2, fillOpacity: 1
}).bindPopup(`<b>Nama KK: ${w.nama}</b><br>Status: <b>${w.klasifikasi}</b><br>Luas: ${w.luas} m²<br>Solusi: <i>${w.solusi}</i>`);
if (w.klasifikasi === 'Miskin Ekstrem') markerWarga.addTo(groupEkstrem);
else if (w.klasifikasi === 'Miskin') markerWarga.addTo(groupMiskin);
else markerWarga.addTo(groupRentan);
seluruhKoleksiSpasial.push({ id: w.id, name: w.nama.toLowerCase(), type: 'warga', layer: markerWarga, detail: w });
}
}
appendTabel('tabel-warga', w.id, w.nama, `<span class="badge ${badgeClass}">${w.klasifikasi}</span>`, w.luas + " m²", 'warga');
});
document.getElementById('count-ekstrem').innerText = cEkstrem;
document.getElementById('count-miskin').innerText = cMiskin;
document.getElementById('count-rentan').innerText = cRentan;
});
// REVISI 1: ENGINE OUTPUT TEKS RESPONSE PADA PROMPT AI
function prosesPromptAI() {
const query = document.getElementById('ai-prompt').value.toLowerCase().trim();
const resBox = document.getElementById('ai-response');
if(!query) return;
resBox.style.display = "block";
// Kasus A: Jika user mencari berdasarkan filter klasifikasi makro
if(query === 'miskin' || query === 'miskin ekstrem' || query === 'rentan') {
let filterWarga = dataAsliWargaGlobal.filter(x => x.klasifikasi.toLowerCase().includes(query));
resBox.innerHTML = `🤖 <b>Hasil AI:</b> Menemukan <b>${filterWarga.length} data keluarga</b> dengan status <b>${query}</b> di Pontianak. Silakan periksa visualisasi titik warna merah/oranye di peta.`;
return;
}
// Kasus B: Mencari objek atau nama spesifik tunggal
const hasilCari = seluruhKoleksiSpasial.find(item => item.name.includes(query) || query.includes(item.name));
if(hasilCari && hasilCari.layer) {
map.flyTo(hasilCari.layer.getLatLng(), 18, { animate: true, duration: 1.2 });
setTimeout(() => { hasilCari.layer.openPopup(); }, 1200);
if(hasilCari.type === 'warga') {
resBox.innerHTML = `🤖 <b>Profil Spasial Ditemukan:</b><br>🏠 KK: <b>${hasilCari.detail.nama}</b><br>Status: <span style="color:#ea580c">${hasilCari.detail.klasifikasi}</span><br>Rekomendasi Aksi: <i>${hasilCari.detail.solusi}</i>`;
} else {
pemicuRadiusRumahIbadah(hasilCari.layer.getLatLng(), hasilCari.detail.nama);
resBox.innerHTML = `🤖 <b>Mitra Ibadah Ditemukan:</b><br>⛪ Tempat: <b>${hasilCari.detail.nama}</b><br>Kategori: ${hasilCari.detail.agama}. <br>⚡ <i>Radius radar aman 1 KM otomatis diaktifkan di sekeliling bangunan.</i>`;
}
} else {
resBox.innerHTML = `❌ 🤖 <b>AI Message:</b> Maaf, informasi kata kunci "${query}" tidak ditemukan pada indeks spasial database.`;
}
}
document.getElementById('ai-submit').addEventListener('click', prosesPromptAI);
document.getElementById('ai-prompt').addEventListener('keypress', (e) => { if(e.key === 'Enter') prosesPromptAI(); });
</script>
</body>
</html>