add feature basemap

This commit is contained in:
Abimanyu Ridho
2026-06-11 17:32:06 +07:00
parent 0f579478f7
commit b482c48d57
8 changed files with 280 additions and 5 deletions
+83
View File
@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebGIS - Peta Referensi Kota Pontianak</title>
<meta name="description" content="Peta referensi batas kecamatan, jaringan jalan, dan sungai besar Kota Pontianak." />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Space+Grotesk:wght@500;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
<link rel="stylesheet" href="css/app.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Turf.js/6.5.0/turf.min.js"></script>
</head>
<body>
<div id="app">
<header id="header">
<div class="brand">
<a href="index.html" style="text-decoration:none; color:inherit"><div class="brand-title">WebGIS Poverty</div></a>
<div class="brand-sub">Sistem Informasi Geografis Berbasis Web</div>
</div>
<div class="header-search" id="header-search">
<div class="search-box">
<span class="search-icon"></span>
<input id="search-input" type="search" autocomplete="off" placeholder="Telusuri Webgis Poverty" aria-label="Cari objek peta" />
</div>
<div class="search-results" id="search-results" aria-label="Hasil pencarian"></div>
</div>
<div class="header-right">
<div class="coord-display" id="coord-display">Lat: - | Lng: -</div>
<div class="status-pill"><span id="status-zoom">Zoom: 13</span></div>
</div>
</header>
<main id="main">
<section id="map-wrap">
<div id="instruction-banner"></div>
<div id="map"></div>
</section>
<aside id="panel">
<div id="panel-head">
<a href="index.html" class="btn btn-ghost btn-sm" style="margin-bottom: 12px; display: inline-flex; align-items: center; gap: 6px; text-decoration: none;">
<span style="font-size: 14px;">&larr;</span> Kembali ke Beranda
</a>
<h2 id="panel-title">Peta Referensi Pontianak</h2>
</div>
<div id="panel-body">
<div class="loading"><span class="spinner"></span>Memuat data...</div>
</div>
</aside>
</main>
</div>
<div id="modal-overlay" aria-hidden="true">
<div id="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div class="modal-head"><h3 id="modal-title">Modal</h3></div>
<div id="modal-body"></div>
<div id="modal-footer"></div>
</div>
</div>
<div id="toast-container"></div>
<script src="js/app-core.js"></script>
<script src="js/feature-kemiskinan.js"></script>
<script src="js/feature-spbu.js"></script>
<script src="js/feature-jalan.js"></script>
<script src="js/feature-persil.js"></script>
<script src="js/feature-basemap.js"></script>
<script>
initPage('basemap');
</script>
</body>
</html>
+6
View File
@@ -46,6 +46,12 @@
<h2 class="feature-title">Bidang Persil Tanah</h2> <h2 class="feature-title">Bidang Persil Tanah</h2>
<p class="feature-desc">Pemetaan bidang tanah bersertifikat lengkap dengan informasi status kepemilikan.</p> <p class="feature-desc">Pemetaan bidang tanah bersertifikat lengkap dengan informasi status kepemilikan.</p>
</a> </a>
<a href="basemap.html" class="feature-card">
<div class="feature-icon">🏙️</div>
<h2 class="feature-title">Peta Referensi Pontianak</h2>
<p class="feature-desc">Batas kecamatan, jaringan jalan, dan sungai besar Kota Pontianak dari data GeoJSON.</p>
</a>
</div> </div>
</main> </main>
+1
View File
@@ -81,6 +81,7 @@
<script src="js/feature-spbu.js"></script> <script src="js/feature-spbu.js"></script>
<script src="js/feature-jalan.js"></script> <script src="js/feature-jalan.js"></script>
<script src="js/feature-persil.js"></script> <script src="js/feature-persil.js"></script>
<script src="js/feature-basemap.js"></script>
<script> <script>
initPage('jalan'); initPage('jalan');
</script> </script>
+19 -5
View File
@@ -35,14 +35,20 @@ const layers = {
persilSHM: L.layerGroup(), persilSHM: L.layerGroup(),
persilHGB: L.layerGroup(), persilHGB: L.layerGroup(),
persilHGU: L.layerGroup(), persilHGU: L.layerGroup(),
persilHP: L.layerGroup() persilHP: L.layerGroup(),
// Layer referensi GeoJSON statis
basemap_kecamatan: L.layerGroup(),
basemap_jalan: L.layerGroup(),
basemap_sungai: L.layerGroup()
}; };
const layerVisible = { const layerVisible = {
rumahIbadah: true, ibadahRadius: true, pendudukMiskin: true, rumahIbadah: true, ibadahRadius: true, pendudukMiskin: true,
spbu24: true, spbuNon24: true, spbu24: true, spbuNon24: true,
jalanNasional: true, jalanProvinsi: true, jalanKabupaten: true, jalanNasional: true, jalanProvinsi: true, jalanKabupaten: true,
persilSHM: true, persilHGB: true, persilHGU: true, persilHP: true persilSHM: true, persilHGB: true, persilHGU: true, persilHP: true,
// Basemap GeoJSON (default tampil semua)
basemap_kecamatan: true, basemap_jalan: true, basemap_sungai: true
}; };
const searchEntries = []; const searchEntries = [];
@@ -320,7 +326,8 @@ function initPage(tab) {
kemiskinan: ['rumahIbadah', 'ibadahRadius', 'pendudukMiskin'], kemiskinan: ['rumahIbadah', 'ibadahRadius', 'pendudukMiskin'],
spbu: ['spbu24', 'spbuNon24'], spbu: ['spbu24', 'spbuNon24'],
jalan: ['jalanNasional', 'jalanProvinsi', 'jalanKabupaten'], jalan: ['jalanNasional', 'jalanProvinsi', 'jalanKabupaten'],
persil: ['persilSHM', 'persilHGB', 'persilHGU', 'persilHP'] persil: ['persilSHM', 'persilHGB', 'persilHGU', 'persilHP'],
basemap: ['basemap_kecamatan', 'basemap_jalan', 'basemap_sungai']
}; };
if (tabLayers[tab]) { if (tabLayers[tab]) {
@@ -329,8 +336,14 @@ function initPage(tab) {
}); });
} }
const titles = { kemiskinan: 'Ibadah & Kemiskinan', spbu: 'SPBU', jalan: 'Data Jalan', persil: 'Persil Tanah' }; const titles = {
document.getElementById('panel-title').textContent = titles[tab]; kemiskinan: 'Ibadah & Kemiskinan',
spbu: 'SPBU',
jalan: 'Data Jalan',
persil: 'Persil Tanah',
basemap: 'Peta Referensi Pontianak'
};
document.getElementById('panel-title').textContent = titles[tab] || tab;
renderPanel(tab); renderPanel(tab);
} }
@@ -351,6 +364,7 @@ async function renderPanel(tab) {
if (tab === 'spbu') await renderSpbuPanel(pb); if (tab === 'spbu') await renderSpbuPanel(pb);
if (tab === 'jalan') await renderJalanPanel(pb); if (tab === 'jalan') await renderJalanPanel(pb);
if (tab === 'persil') await renderPersilPanel(pb); if (tab === 'persil') await renderPersilPanel(pb);
if (tab === 'basemap') await renderBasemapPanel(pb);
} }
function findGeomLayer(type, id) { function findGeomLayer(type, id) {
+168
View File
@@ -0,0 +1,168 @@
// ============================================================
// feature-basemap.js
// Layer referensi statis dari GeoJSON lokal:
// - Batas Kecamatan Kota Pontianak (Admin_Kecamatan)
// - Jaringan Jalan (Jaringan_Jalan)
// - Sungai Besar (Sungai_Besar)
// ============================================================
const basemapCfg = {
kecamatan: {
label: 'Batas Kecamatan',
file: 'assets/map/Admin_Kecamatan.geojson',
color: '#7c3aed',
fillColor: '#7c3aed',
fillOpacity: 0.08,
weight: 2.5,
icon: '🏙️',
popupProp: ['KECAMATAN', 'KELURAHAN', 'KODE_KEC', 'LUAS_HA'],
popupLabel: { KECAMATAN: 'Kecamatan', KELURAHAN: 'Kelurahan', KODE_KEC: 'Kode', LUAS_HA: 'Luas (ha)' }
},
jalan: {
label: 'Jaringan Jalan',
file: 'assets/map/Jaringan_Jalan.geojson',
color: '#d97706',
weight: 1.8,
fillOpacity: 0,
icon: '🛤️',
popupProp: ['NAMOBJ', 'REMARK', 'FCODE'],
popupLabel: { NAMOBJ: 'Nama', REMARK: 'Keterangan', FCODE: 'Kode' }
},
sungai: {
label: 'Sungai Besar',
file: 'assets/map/Sungai_Besar.geojson',
color: '#0ea5e9',
fillColor: '#0ea5e9',
fillOpacity: 0.25,
weight: 1.5,
icon: '🌊',
popupProp: ['NAMOBJ', 'REMARK', 'FCODE'],
popupLabel: { NAMOBJ: 'Nama', REMARK: 'Keterangan', FCODE: 'Kode' }
}
};
// Cache untuk data GeoJSON yang sudah di-fetch agar tidak fetch ulang
const _basemapCache = {};
/**
* Fetch GeoJSON dan buat Leaflet GeoJSON layer dengan style + popup.
*/
async function loadBasemapLayer(key) {
const cfg = basemapCfg[key];
const layerKey = 'basemap_' + key;
// Jika layer sudah ada isinya, tidak perlu reload
let hasLayers = false;
layers[layerKey].eachLayer(() => { hasLayers = true; });
if (hasLayers) return;
try {
// Gunakan cache jika sudah pernah di-fetch
let geojsonData = _basemapCache[key];
if (!geojsonData) {
const resp = await fetch(cfg.file);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
geojsonData = await resp.json();
_basemapCache[key] = geojsonData;
}
const styleOpts = {
color: cfg.color,
weight: cfg.weight,
fillOpacity: cfg.fillOpacity ?? 0,
fillColor: cfg.fillColor || cfg.color,
opacity: 0.9
};
const geoLayer = L.geoJSON(geojsonData, {
style: styleOpts,
pointToLayer: (feature, latlng) =>
L.circleMarker(latlng, { radius: 5, ...styleOpts }),
onEachFeature: (feature, layer) => {
const props = feature.properties || {};
// Cari nama property (case-insensitive fallback)
const findVal = (keys) => {
for (const k of keys) {
const found = Object.keys(props).find(p => p.toUpperCase() === k.toUpperCase());
if (found && props[found]) return props[found];
}
return null;
};
const rows = cfg.popupProp
.map(p => {
const found = Object.keys(props).find(k => k.toUpperCase() === p.toUpperCase());
const val = found ? props[found] : null;
if (!val) return '';
return `<p><b>${cfg.popupLabel[p] || p}:</b> ${val}</p>`;
})
.filter(Boolean)
.join('');
const nameKeys = ['NAMOBJ', 'NAMA', 'NAME', 'KECAMATAN', 'KELURAHAN'];
const title = findVal(nameKeys) || cfg.label;
if (rows) {
layer.bindPopup(`<h4>${cfg.icon} ${esc(title)}</h4>${rows}`, { maxWidth: 280 });
}
}
});
layers[layerKey].addLayer(geoLayer);
} catch (err) {
console.warn(`[basemap] Gagal memuat ${cfg.file}:`, err);
toast(`Gagal memuat layer ${cfg.label}`, 'error');
}
}
/**
* Render panel kontrol untuk layer basemap di sidebar.
* Dipanggil dari renderPanel() di app-core.js jika tab === 'basemap'.
*/
async function renderBasemapPanel(pb) {
pb.innerHTML = `
<div class="layer-section">
<div class="layer-section-title">Layer Referensi Kota Pontianak</div>
${Object.entries(basemapCfg).map(([key, cfg]) => {
const lk = 'basemap_' + key;
const checked = layerVisible[lk];
return `
<div class="layer-item ${checked ? 'checked' : ''}" id="basemap-item-${key}" onclick="toggleBasemapLayer('${key}', this)">
<div class="layer-check">${checked ? '✓' : ''}</div>
<div class="layer-dot" style="background:${cfg.color};${cfg.fillOpacity > 0 ? '' : 'border-radius:2px;height:4px;width:18px;border-radius:2px'}"></div>
<div class="layer-label">${cfg.icon} ${cfg.label}</div>
</div>`;
}).join('')}
</div>
<div class="layer-section">
<div class="layer-section-title">Informasi</div>
<div style="font-size:11px;line-height:1.8;color:var(--text2);padding:10px 12px;background:var(--bg2);border:1px solid var(--border);border-radius:var(--radius)">
Data batas administrasi kecamatan, jaringan jalan, dan sungai besar Kota Pontianak.<br>
Klik objek di peta untuk melihat informasi detail.<br>
Centang/hilang centang untuk tampil/sembunyikan layer.
</div>
</div>`;
// Load semua layer yang aktif
const loadPromises = Object.keys(basemapCfg)
.filter(key => layerVisible['basemap_' + key])
.map(key => loadBasemapLayer(key));
await Promise.all(loadPromises);
}
/**
* Toggle visibilitas layer basemap dan load data jika belum ada.
*/
window.toggleBasemapLayer = async function(key, el) {
const lk = 'basemap_' + key;
layerVisible[lk] = !layerVisible[lk];
el.classList.toggle('checked', layerVisible[lk]);
el.querySelector('.layer-check').textContent = layerVisible[lk] ? '✓' : '';
if (layerVisible[lk]) {
await loadBasemapLayer(key);
map.addLayer(layers[lk]);
} else {
map.removeLayer(layers[lk]);
}
};
+1
View File
@@ -81,6 +81,7 @@
<script src="js/feature-spbu.js"></script> <script src="js/feature-spbu.js"></script>
<script src="js/feature-jalan.js"></script> <script src="js/feature-jalan.js"></script>
<script src="js/feature-persil.js"></script> <script src="js/feature-persil.js"></script>
<script src="js/feature-basemap.js"></script>
<script> <script>
initPage('kemiskinan'); initPage('kemiskinan');
</script> </script>
+1
View File
@@ -81,6 +81,7 @@
<script src="js/feature-spbu.js"></script> <script src="js/feature-spbu.js"></script>
<script src="js/feature-jalan.js"></script> <script src="js/feature-jalan.js"></script>
<script src="js/feature-persil.js"></script> <script src="js/feature-persil.js"></script>
<script src="js/feature-basemap.js"></script>
<script> <script>
initPage('persil'); initPage('persil');
</script> </script>
+1
View File
@@ -81,6 +81,7 @@
<script src="js/feature-spbu.js"></script> <script src="js/feature-spbu.js"></script>
<script src="js/feature-jalan.js"></script> <script src="js/feature-jalan.js"></script>
<script src="js/feature-persil.js"></script> <script src="js/feature-persil.js"></script>
<script src="js/feature-basemap.js"></script>
<script> <script>
initPage('spbu'); initPage('spbu');
</script> </script>