Files
D1041231071_UAS-Tugas_WebGI…/webgis_app/public_assets/js/peta-user.js
T
2026-06-13 00:31:36 +07:00

399 lines
18 KiB
JavaScript
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.
/* =========================================================
user-map.js — Read-only WebGIS Map (user mode)
========================================================= */
const API = APP_BASE + '/api';
const STAT_API = APP_BASE + '/api/statistik.php';
const state = {
map: null,
layers: {
spbu: null, jalan: null, kavling: null,
kawasan: null, rumah: null, warga: null,
choropleth: null, blankSpot: null, spbuRoute: null
}
};
const COLORS = {
spbu: '#F59E0B',
spbu24: '#10B981',
jalan: '#3B82F6',
kavling: '#8B5CF6',
kawasan: '#EF4444',
rumah: '#A855F7',
warga: '#EF4444',
blankSpot: '#F97316'
};
function makeIcon(emoji, bgColor, size = 34) {
return L.divIcon({
className: 'div-icon-wrap',
html: `<div class="marker-pin" style="background:${bgColor};width:${size}px;height:${size}px;"><span>${emoji}</span></div>`,
iconSize: [size, size],
iconAnchor: [size / 2, size],
popupAnchor: [0, -size]
});
}
const ICONS = {
spbu24: makeIcon('⛽', COLORS.spbu24),
spbu: makeIcon('⛽', COLORS.spbu),
rumah: makeIcon('🕌', COLORS.rumah), // default (Islam)
warga: makeIcon('👤', COLORS.warga),
blankSpot: makeIcon('⚠️', COLORS.blankSpot, 36)
};
/* ── Per-religion icon colors & emojis ── */
const AGAMA_EMOJI = {
'Islam': '🕌',
'Kristen': '⛪',
'Katolik': '⛪',
'Hindu': '🛕',
'Buddha': '☯️',
'Konghucu': '🏯'
};
const AGAMA_COLOR = {
'Islam': '#6D28D9',
'Kristen': '#2563EB',
'Katolik': '#1D4ED8',
'Hindu': '#D97706',
'Buddha': '#F59E0B',
'Konghucu': '#DC2626'
};
function getRumahIcon(agama) {
const emoji = AGAMA_EMOJI[agama] || '🛐';
const color = AGAMA_COLOR[agama] || COLORS.rumah;
return makeIcon(emoji, color);
}
function toast(msg, type = 'success') {
const c = document.getElementById('toastContainer');
const t = document.createElement('div');
t.className = `toast ${type}`;
t.innerHTML = `<i class="fas fa-${type === 'success' ? 'check-circle' : 'info-circle'}" style="margin-right:8px;"></i>${msg}`;
c.appendChild(t);
setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 400); }, 3000);
}
function initMap() {
state.map = L.map('user-map', { zoomControl: false })
.setView([-0.0583, 109.3448], 13);
L.control.zoom({ position: 'bottomright' }).addTo(state.map);
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
const tileUrl = isDark
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
const tileLayer = L.tileLayer(tileUrl, {
attribution: '© OpenStreetMap, © CARTO',
subdomains: 'abcd',
maxZoom: 20
}).addTo(state.map);
// Dynamic theme updater for tile layer
const observer = new MutationObserver(() => {
const dark = document.documentElement.getAttribute('data-theme') === 'dark';
tileLayer.setUrl(dark
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'
);
});
observer.observe(document.documentElement, { attributes: true });
}
async function fetchLayer(path) {
const r = await fetch(`${API}/${path}`);
const j = await r.json();
return j.status === 'success' ? j.data : { type: 'FeatureCollection', features: [] };
}
function formatKm(value) {
return Number(value || 0).toLocaleString('id-ID', {
minimumFractionDigits: 0,
maximumFractionDigits: 2
});
}
function bindReadOnlyPopup(layer, type, props) {
let html = `<div class="popup-title">${props.nama || props.nama_pemilik || props.nama_kk || props.nama_kawasan || 'Tanpa Nama'}</div>`;
if (type === 'spbu') {
html += `<div class="popup-row">24 Jam: <strong style="color:${props.buka_24_jam ? '#10B981' : '#EF4444'}">${props.buka_24_jam ? 'Ya' : 'Tidak'}</strong></div>`;
if (props.deskripsi) html += `<div class="popup-row">${props.deskripsi}</div>`;
html += `<div class="popup-row">Rating: <strong>${props.avg_rating > 0 ? '⭐ ' + props.avg_rating + ' (' + props.total_ulasan + ' ulasan)' : 'Belum ada ulasan'}</strong></div>`;
html += `<button class="btn btn-primary btn-sm" style="margin-top:10px;width:100%;" onclick="bukaReview('spbu', ${props.id}, '${props.nama}')">Beri Ulasan</button>`;
} else if (type === 'jalan') {
html += `<div class="popup-row">Jenis: <strong>${props.jenis_jalan || '-'}</strong></div>`;
} else if (type === 'kavling') {
html += `<div class="popup-row">Status: <strong>${props.status_kepemilikan || '-'}</strong></div>`;
html += `<div class="popup-row">Luas: <strong>${props.luas ? props.luas + ' m²' : '-'}</strong></div>`;
} else if (type === 'rumah') {
html += `<div class="popup-row">Agama: <strong>${props.agama || '-'}</strong></div>`;
html += `<div class="popup-row">Radius Bantuan: <strong>${formatKm(props.radius_bantuan_km)} km</strong></div>`;
html += `<div class="popup-row">Rating: <strong>${props.avg_rating > 0 ? '⭐ ' + props.avg_rating + ' (' + props.total_ulasan + ' ulasan)' : 'Belum ada ulasan'}</strong></div>`;
html += `<button class="btn btn-primary btn-sm" style="margin-top:10px;width:100%;" onclick="bukaReview('rumah_ibadah', ${props.id}, '${props.nama}')">Beri Ulasan</button>`;
} else if (type === 'warga') {
html += `<div class="popup-row">Penghasilan: <strong>Rp ${Number(props.penghasilan || 0).toLocaleString('id-ID')}</strong></div>`;
html += `<div class="popup-row">Tanggungan: <strong>${props.jumlah_tanggungan || 0} org</strong></div>`;
}
layer.bindPopup(html);
}
// Ulasan Modal Logic
window.bukaReview = function(tipe, id, nama) {
let m = document.getElementById('modalReview');
if(!m) {
m = document.createElement('div');
m.id = 'modalReview';
m.className = 'modal-overlay';
m.innerHTML = `
<div class="modal">
<div class="modal-header">
<span class="modal-title">⭐ Beri Ulasan</span>
<button class="modal-close" onclick="document.getElementById('modalReview').style.display='none'">×</button>
</div>
<div style="padding:15px;">
<h4 id="revNama" style="margin-top:0; color:var(--text-primary);"></h4>
<input type="hidden" id="revTipe"><input type="hidden" id="revId">
<div class="form-group">
<label class="form-label">Rating (1-5)</label>
<select id="revRating" class="form-control">
<option value="5">⭐⭐⭐⭐⭐ Sangat Baik</option>
<option value="4">⭐⭐⭐⭐ Baik</option>
<option value="3">⭐⭐⭐ Cukup</option>
<option value="2">⭐⭐ Buruk</option>
<option value="1">⭐ Sangat Buruk</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Komentar</label>
<textarea id="revKomen" class="form-control" rows="3" placeholder="Tuliskan pengalaman Anda..."></textarea>
</div>
<button class="btn btn-primary" style="width:100%" onclick="submitReview()">Kirim Ulasan</button>
</div>
</div>`;
document.body.appendChild(m);
}
document.getElementById('revTipe').value = tipe;
document.getElementById('revId').value = id;
document.getElementById('revNama').innerText = "Mengulas: " + nama;
document.getElementById('revRating').value = "5";
document.getElementById('revKomen').value = "";
m.style.display = 'flex';
};
window.submitReview = async function() {
const tipe = document.getElementById('revTipe').value;
const id = document.getElementById('revId').value;
const rating = document.getElementById('revRating').value;
const komen = document.getElementById('revKomen').value;
const res = await fetch(APP_BASE + '/api/ulasan.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fasilitas_tipe: tipe, fasilitas_id: id, rating: rating, komentar: komen })
});
const j = await res.json();
if(j.status === 'success') {
toast('Ulasan berhasil dikirim!');
document.getElementById('modalReview').style.display = 'none';
loadAll(); // reload data
} else {
toast(j.message, 'error');
}
};
function renderSpbu(data) {
if (state.layers.spbu) state.map.removeLayer(state.layers.spbu);
state.layers.spbu = L.geoJSON(data, {
pointToLayer: (f, ll) => L.marker(ll, { icon: f.properties.buka_24_jam ? ICONS.spbu24 : ICONS.spbu }),
onEachFeature: (f, l) => bindReadOnlyPopup(l, 'spbu', f.properties)
});
if (document.getElementById('ly-spbu')?.checked) state.layers.spbu.addTo(state.map);
}
function renderJalan(data) {
if (state.layers.jalan) state.map.removeLayer(state.layers.jalan);
state.layers.jalan = L.geoJSON(data, {
style: { color: COLORS.jalan, weight: 5, opacity: 0.85 },
onEachFeature: (f, l) => bindReadOnlyPopup(l, 'jalan', f.properties)
});
if (document.getElementById('ly-jalan')?.checked) state.layers.jalan.addTo(state.map);
}
function renderKavling(data) {
if (state.layers.kavling) state.map.removeLayer(state.layers.kavling);
state.layers.kavling = L.geoJSON(data, {
style: { color: COLORS.kavling, fillColor: COLORS.kavling, fillOpacity: 0.3, weight: 2 },
onEachFeature: (f, l) => bindReadOnlyPopup(l, 'kavling', f.properties)
});
if (document.getElementById('ly-kavling')?.checked) state.layers.kavling.addTo(state.map);
}
function renderKawasan(data) {
if (state.layers.kawasan) state.map.removeLayer(state.layers.kawasan);
state.layers.kawasan = L.geoJSON(data, {
style: { color: COLORS.kawasan, fillColor: COLORS.kawasan, fillOpacity: 0.25, weight: 2, dashArray: '6 4' },
onEachFeature: (f, l) => bindReadOnlyPopup(l, 'kawasan', f.properties)
});
if (document.getElementById('ly-kawasan')?.checked) state.layers.kawasan.addTo(state.map);
}
function renderRumah(data) {
if (state.layers.rumah) state.map.removeLayer(state.layers.rumah);
state.layers.rumah = L.featureGroup();
(data.features || []).forEach((f) => {
if (!f.geometry || f.geometry.type !== 'Point') return;
const props = f.properties || {};
const agama = props.agama || 'Islam';
const agamaColor = AGAMA_COLOR[agama] || COLORS.rumah;
const ll = L.latLng(f.geometry.coordinates[1], f.geometry.coordinates[0]);
const circle = L.circle(ll, {
radius: Number(props.radius_bantuan_meter || 1000),
color: agamaColor,
fillColor: agamaColor,
fillOpacity: 0.10,
opacity: 0.60,
weight: 2
});
const marker = L.marker(ll, { icon: getRumahIcon(agama) });
bindReadOnlyPopup(circle, 'rumah', props);
bindReadOnlyPopup(marker, 'rumah', props);
state.layers.rumah.addLayer(circle);
state.layers.rumah.addLayer(marker);
});
if (document.getElementById('ly-rumah')?.checked) state.layers.rumah.addTo(state.map);
}
function renderWarga(data) {
if (state.layers.warga) state.map.removeLayer(state.layers.warga);
state.layers.warga = L.geoJSON(data, {
pointToLayer: (f, ll) => L.marker(ll, { icon: ICONS.warga }),
onEachFeature: (f, l) => bindReadOnlyPopup(l, 'warga', f.properties)
});
if (document.getElementById('ly-warga')?.checked) state.layers.warga.addTo(state.map);
}
function renderChoropleth(data) {
if (state.layers.choropleth) state.map.removeLayer(state.layers.choropleth);
state.layers.choropleth = L.geoJSON(data, {
style: (f) => {
const c = f.properties.jumlah_warga;
let color = '#10B981';
if (c > 0) color = '#F59E0B';
if (c > 3) color = '#EF4444';
return { color, fillColor: color, fillOpacity: 0.35, weight: 3 };
},
onEachFeature: (f, l) => {
l.bindPopup(`<div class="popup-title">${f.properties.nama_kawasan}</div>
<div class="popup-row">Jumlah Warga: <strong>${f.properties.jumlah_warga}</strong></div>
<div class="popup-row">Total Tanggungan: <strong>${f.properties.total_tanggungan}</strong></div>`);
}
});
if (document.getElementById('ly-choropleth')?.checked) state.layers.choropleth.addTo(state.map);
}
function renderBlankSpot(data) {
if (state.layers.blankSpot) state.map.removeLayer(state.layers.blankSpot);
state.layers.blankSpot = L.geoJSON(data, {
pointToLayer: (f, ll) => L.marker(ll, { icon: ICONS.blankSpot }),
onEachFeature: (f, l) => {
l.bindPopup(`<div class="popup-title">⚠️ ${f.properties.nama_kk}</div>
<div class="popup-row">Jarak ke RI terdekat: <strong>${formatKm(f.properties.jarak_km)} km</strong></div>
<div class="popup-row">Radius bantuan: <strong>${formatKm(f.properties.radius_km)} km</strong></div>
<div class="popup-row">Di luar radius: <strong>${formatKm(f.properties.selisih_km)} km</strong></div>
<div class="popup-row">Rumah ibadah terdekat: <strong>${f.properties.rumah_ibadah_terdekat || '-'}</strong></div>
<div class="popup-row" style="color:#F97316">Blank spot di luar radius bantuan</div>`);
}
});
if (document.getElementById('ly-blankspot')?.checked) state.layers.blankSpot.addTo(state.map);
}
function bindLayerToggles() {
const map = {
'ly-spbu': 'spbu', 'ly-jalan': 'jalan', 'ly-kavling': 'kavling',
'ly-kawasan': 'kawasan', 'ly-rumah': 'rumah', 'ly-warga': 'warga',
'ly-choropleth': 'choropleth', 'ly-blankspot': 'blankSpot'
};
Object.entries(map).forEach(([id, key]) => {
document.getElementById(id)?.addEventListener('change', (e) => {
const layer = state.layers[key];
if (!layer) return;
if (e.target.checked) state.map.addLayer(layer);
else state.map.removeLayer(layer);
});
});
}
async function findNearestSpbu() {
const center = state.map.getCenter();
document.getElementById('spbuPanelResult').innerHTML = '<div class="spinner"></div> Mencari...';
document.getElementById('spbuPanel').style.display = 'block';
try {
const r = await fetch(`${API}/spbu_terdekat.php?lat=${center.lat}&lng=${center.lng}`);
const j = await r.json();
if (j.status === 'success' && j.data) {
const p = j.data.properties;
const coords = j.data.geometry.coordinates;
document.getElementById('spbuPanelResult').innerHTML = `<div class="spbu-result">
<div class="spbu-result-name">⛽ ${p.nama}</div>
<div class="spbu-result-meta">${p.buka_24_jam ? '🟢 24 Jam' : '🟡 Terbatas'}</div>
<div class="spbu-result-distance">${p.jarak_km} km</div>
</div>`;
if (state.layers.spbuRoute) state.map.removeLayer(state.layers.spbuRoute);
state.layers.spbuRoute = L.marker([coords[1], coords[0]], { icon: ICONS.spbu }).addTo(state.map)
.bindPopup(`<b>${p.nama}</b><br>${p.jarak_km} km dari pusat peta`).openPopup();
} else {
document.getElementById('spbuPanelResult').innerHTML = `<div class="spbu-result">${j.message || 'Tidak ada SPBU'}</div>`;
}
} catch (e) {
document.getElementById('spbuPanelResult').innerHTML = `<div class="spbu-result">Error: ${e.message}</div>`;
}
}
async function loadAll() {
document.getElementById('loadingOverlay').style.display = 'flex';
try {
const [spbu, jalan, kavling, kawasan, rumah, warga, stat, blank] = await Promise.all([
fetchLayer('spbu.php'),
fetchLayer('jalan.php'),
fetchLayer('kavling.php'),
fetchLayer('kawasan_kumuh.php'),
fetchLayer('rumah_ibadah.php'),
fetchLayer('warga_miskin.php'),
fetch(STAT_API).then(r => r.json()).then(j => j.data).catch(() => null),
fetchLayer('blank_spot.php')
]);
renderSpbu(spbu); renderJalan(jalan); renderKavling(kavling);
renderKawasan(kawasan); renderRumah(rumah); renderWarga(warga);
if (stat && stat.choropleth) renderChoropleth(stat.choropleth);
if (blank) renderBlankSpot(blank);
if (stat && stat.counts) {
document.getElementById('qs-spbu').textContent = stat.counts.total_spbu;
document.getElementById('qs-jalan').textContent = stat.counts.total_jalan;
document.getElementById('qs-kavling').textContent = stat.counts.total_kavling;
document.getElementById('qs-warga').textContent = stat.counts.total_warga_miskin;
}
} catch (e) {
toast('Gagal load data: ' + e.message, 'error');
} finally {
document.getElementById('loadingOverlay').style.display = 'none';
}
}
document.addEventListener('DOMContentLoaded', () => {
initMap();
loadAll();
bindLayerToggles();
document.getElementById('btn-find-spbu')?.addEventListener('click', findNearestSpbu);
document.getElementById('btn-close-spbu')?.addEventListener('click', () => {
document.getElementById('spbuPanel').style.display = 'none';
if (state.layers.spbuRoute) { state.map.removeLayer(state.layers.spbuRoute); state.layers.spbuRoute = null; }
});
});