Files
webgis/tugas-spbu/index.html
T
2026-06-11 17:43:01 +07:00

778 lines
35 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<title>WebGIS</title>
<meta charset="utf-8">
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Leaflet Draw CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
<!-- Leaflet Draw JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<!-- GeometryUtil (FIXED & WORKING) -->
<script src="https://unpkg.com/leaflet-geometryutil"></script>
<style>
/* Simple modal styles */
.modal {position:fixed;left:0;top:0;right:0;bottom:0;background:rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;z-index:9999}
.modal.hidden{display:none}
.modal-content{background:#fff;padding:18px;border-radius:8px;min-width:320px;max-width:480px;box-shadow:0 6px 18px rgba(0,0,0,0.2)}
.modal-content h3{margin:0 0 10px 0;font-size:18px}
.modal-content label{display:block;margin-top:8px;font-size:13px}
.modal-content input[type=text], .modal-content select{width:100%;padding:8px;margin-top:4px;border:1px solid #ddd;border-radius:4px}
.modal-actions{margin-top:12px;text-align:right}
.modal-actions button{margin-left:8px;padding:8px 12px;border-radius:4px;border:0;cursor:pointer}
.btn-primary{background:#1976d2;color:white}
.btn-secondary{background:#e0e0e0}
</style>
</head>
<body>
<!-- Modal for adding items -->
<div id="modal" class="modal hidden" role="dialog" aria-hidden="true">
<div class="modal-content">
<h3 id="modal-title">Form</h3>
<form id="modal-form">
<div id="modal-fields"></div>
<div class="modal-actions">
<button type="button" id="modal-cancel" class="btn-secondary">Batal</button>
<button type="submit" id="modal-submit" class="btn-primary">Simpan</button>
</div>
</form>
</div>
</div>
<div id="auth-controls" style="position: absolute; top: 10px; right: 10px; z-index: 1000; background: white; padding: 10px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; gap: 8px; align-items: center; font-family: sans-serif;">
<span id="api-key-status" style="font-size: 13px; font-weight: bold; color: #666;">Guest Mode</span>
<button id="btn-api-key" onclick="promptApiKey()" style="padding: 6px 12px; border-radius: 4px; border: none; background: #1976d2; color: white; cursor: pointer; font-weight: bold; font-size: 12px; transition: all 0.2s;">🔑 Set API Key</button>
</div>
<div id="map" style="height:100vh;"></div>
<script>
let isDrawing = false;
let pendingLayer = null;
let pendingType = null;
function escapeHtml(str) {
if (!str) return '';
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
}
function updateApiKeyStatus() {
const key = localStorage.getItem('spbu_api_key');
const statusEl = document.getElementById('api-key-status');
const btnEl = document.getElementById('btn-api-key');
if (statusEl && btnEl) {
if (key) {
statusEl.textContent = 'Admin Mode';
statusEl.style.color = '#2e7d32';
btnEl.textContent = '🔑 Ubah API Key';
btnEl.style.background = '#2e7d32';
} else {
statusEl.textContent = 'Guest Mode';
statusEl.style.color = '#666';
btnEl.textContent = '🔑 Set API Key';
btnEl.style.background = '#1976d2';
}
}
}
function promptApiKey() {
const currentKey = localStorage.getItem('spbu_api_key') || '';
const newKey = prompt('Masukkan API Key untuk otentikasi Admin SPBU:', currentKey);
if (newKey !== null) {
const trimmed = newKey.trim();
if (trimmed) {
localStorage.setItem('spbu_api_key', trimmed);
} else {
localStorage.removeItem('spbu_api_key');
}
updateApiKeyStatus();
}
}
function handleApiResponse(res, callback) {
if (res.status === 401) {
alert('Akses Ditolak: API Key tidak valid atau kosong. Silakan atur API Key terlebih dahulu.');
promptApiKey();
} else {
res.json().then(data => {
if (data && data.success === false) {
alert('Error: ' + (data.error || 'Terjadi kesalahan'));
} else if (callback) {
callback(data);
}
}).catch(() => {
if (res.ok && callback) {
callback();
} else {
alert('Terjadi kesalahan koneksi server');
}
});
}
}
function buildHeaders(){
const headers = {'Content-Type':'application/json'};
const key = localStorage.getItem('spbu_api_key');
if(key){ headers['X-API-KEY'] = key; }
return headers;
}
const map = L.map('map').setView([-0.0552, 109.3500], 13);
const drawnItems = new L.FeatureGroup();
map.addLayer(drawnItems);
const drawControl = new L.Control.Draw({
draw: {
polyline: true,
polygon: true,
marker: true,
rectangle: false,
circle: false,
circlemarker: false
},
edit: {
featureGroup: drawnItems,
edit: true,
remove: true
}
});
map.addControl(drawControl);
map.on(L.Draw.Event.CREATED, function (e) {
const layer = e.layer;
if (e.layerType === 'marker') {
// opening lokasi modal for marker creation
pendingType = 'lokasi';
const ll = layer.getLatLng();
// store the created layer so we can remove it from drawnItems after save
pendingLayer = layer;
openModal('Tambah Lokasi', `
<label>Nama <input name="nama" type="text" required></label>
<label>Jenis <select name="jenis"><option value="rumah">Rumah</option><option value="masjid">Masjid</option><option value="spbu">SPBU</option></select></label>
<label>No Telp <input name="no_telp" type="text"></label>
<label><input name="buka_24_jam" type="checkbox"> Buka 24 Jam</label>
<label>Koordinat <input name="coords" type="text" readonly value="${ll.lat.toFixed(6)}, ${ll.lng.toFixed(6)}"></label>
<label>Alamat <input name="alamat" type="text"></label>
`, handleModalSubmit);
// try to reverse-geocode and prefill the alamat field
reverseGeocode(ll.lat, ll.lng).then(addr=>{
try{ const f = document.querySelector('#modal-fields input[name="alamat"]'); if(f && addr) f.value = addr; }catch(e){}
}).catch(()=>{});
}
if (e.layerType === 'polyline') {
handlePolyline(layer);
}
if (e.layerType === 'polygon') {
handlePolygon(layer);
}
drawnItems.addLayer(layer);
});
map.on('draw:drawstart', function () {
isDrawing = true;
});
map.on('draw:drawstop', function () {
isDrawing = false;
});
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
// let markers = [];
// ====== LAYER GROUPS & CONTROL ======
const spbu24Group = L.layerGroup().addTo(map);
const spbuNon24Group = L.layerGroup().addTo(map);
const rumahGroup = L.layerGroup().addTo(map);
const masjidGroup = L.layerGroup().addTo(map);
const jalanGroup = L.layerGroup().addTo(map);
const tanahGroup = L.layerGroup().addTo(map);
const lokasiGroup = L.layerGroup(); // fallback group (not added by default)
const rumahMarkers = []; // store rumah circleMarkers for radius checks
L.control.layers(null, {
"SPBU Buka 24 Jam": spbu24Group,
"SPBU Tidak 24 Jam": spbuNon24Group,
"Rumah": rumahGroup,
"Masjid": masjidGroup,
"Jalan": jalanGroup,
"Tanah": tanahGroup
}, { collapsed: false }).addTo(map);
// Using the built-in Leaflet.Draw toolbar (marker, polyline, polygon).
function tanahStatusFull(code){
return code === 'SHM' ? 'Sertifikat Hak Milik (SHM)'
: code === 'HGB' ? 'Sertifikat Hak Guna Bangunan (HGB)'
: code === 'HGU' ? 'Sertifikat Hak Guna Usaha (HGU)'
: code === 'HP' ? 'Sertifikat Hak Pakai (HP)'
: code || '';
}
function loadData() {
// clear previous markers from groups
spbu24Group.clearLayers();
spbuNon24Group.clearLayers();
// hide any active masjid radius when reloading data
try{ hideMasjidRadius(); }catch(e){}
fetch('src/api/get_lokasi.php')
.then(res => res.json())
.then(data => {
// clear previous lokasi markers/groups
lokasiGroup.clearLayers(); rumahGroup.clearLayers(); masjidGroup.clearLayers(); rumahMarkers.length = 0;
data.forEach(item => {
const statusHtml = parseInt(item.buka_24_jam) === 1
? "<span style='color:green'>Buka 24 Jam</span>"
: "<span style='color:red'>Tidak 24 Jam</span>";
const safeNama = escapeHtml(item.nama);
const safeAlamat = escapeHtml(item.alamat);
const jsEscapedNama = safeNama.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
const jsEscapedAlamat = safeAlamat.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
const jenis = (item.jenis||'').toLowerCase();
let marker;
if(jenis === 'rumah' || jenis === 'masjid'){
// use circleMarker so we can change color dynamically
const color = jenis === 'masjid' ? '#1976d2' : '#d32f2f';
marker = L.circleMarker([item.latitude, item.longitude], {radius:8, color:color, fillColor:color, fillOpacity:1});
marker.bindPopup(`
<b>${safeNama}</b><br>
${statusHtml}<br>
<div style="font-size:12px;color:#444;margin-top:6px">${safeAlamat}</div>
<br>
<button onclick="hapus('${item.id}')">Hapus</button>
<button onclick="edit('${item.id}','${jsEscapedNama}','${item.no_telp}',${item.buka_24_jam}, '${jsEscapedAlamat}', '${jenis}')">Edit</button>
`);
// store rumah markers for radius checks
if(jenis === 'rumah') { rumahMarkers.push(marker); rumahGroup.addLayer(marker); }
if(jenis === 'masjid') { masjidGroup.addLayer(marker); }
} else {
// default: spbu or other — normal marker
marker = L.marker([item.latitude, item.longitude]);
marker.bindPopup(`
<b>${safeNama}</b><br>
${statusHtml}<br>
<div style="font-size:12px;color:#444;margin-top:6px">${safeAlamat}</div>
<br>
<button onclick="hapus('${item.id}')">Hapus</button>
<button onclick="edit('${item.id}','${jsEscapedNama}','${item.no_telp}',${item.buka_24_jam}, '${jsEscapedAlamat}', '${jenis}')">Edit</button>
`);
}
// attach jenis data for future reference
marker._jenis = jenis;
marker._data = item;
// add marker to appropriate group if not already added
if(jenis !== 'rumah' && jenis !== 'masjid'){
// SPBU or other — add to spbu groups by buka_24_jam
if(parseInt(item.buka_24_jam) === 1) spbu24Group.addLayer(marker);
else spbuNon24Group.addLayer(marker);
}
// Also add to lokasiGroup as general index (optional)
lokasiGroup.addLayer(marker);
});
});
}
// MASJID RADIUS CONTROL
let activeMasjid = null;
let masjidCircle = null;
let masjidControlEl = null;
function showMasjidRadius(masjidMarker){
// ensure only one active
hideMasjidRadius();
activeMasjid = masjidMarker;
const center = masjidMarker.getLatLng();
const defaultRadius = 200; // meters (default changed to 200m)
masjidCircle = L.circle(center, {radius: defaultRadius, color: '#1976d2', fillOpacity: 0.08}).addTo(map);
// create control UI
masjidControlEl = document.createElement('div');
masjidControlEl.id = 'masjid-radius-control';
masjidControlEl.style.position='absolute';
// place at bottom-left so it doesn't overlap layer controls / zoom
masjidControlEl.style.left='10px'; masjidControlEl.style.bottom='80px';
masjidControlEl.style.background='white'; masjidControlEl.style.padding='8px'; masjidControlEl.style.borderRadius='6px'; masjidControlEl.style.boxShadow='0 2px 6px rgba(0,0,0,0.2)';
masjidControlEl.innerHTML = `Radius: <span id="masjid-radius-value">${defaultRadius}</span> m<br><input id="masjid-radius-slider" type="range" min="50" max="3000" step="10" value="${defaultRadius}" style="width:200px"> <button id="masjid-radius-close">Close</button>`;
// append to the map container and ensure it's above map layers
try{
const mapContainer = map.getContainer && map.getContainer();
if(mapContainer){ mapContainer.appendChild(masjidControlEl); }
else { document.body.appendChild(masjidControlEl); }
}catch(e){ document.body.appendChild(masjidControlEl); }
masjidControlEl.style.zIndex = 10000;
masjidControlEl.style.pointerEvents = 'auto';
// prevent map dragging/interaction while user is interacting with the slider
const sliderEl = document.getElementById('masjid-radius-slider');
if(sliderEl){
const disableMapDrag = ()=>{ try{ if(map.dragging && map.dragging.enabled()) map.dragging.disable(); }catch(e){} };
const enableMapDrag = ()=>{ try{ if(map.dragging && !map.dragging.enabled()) map.dragging.enable(); }catch(e){} };
const startDrag = (ev)=>{ disableMapDrag(); sliderEl._dragging = true; };
const endDrag = (ev)=>{ sliderEl._dragging = false; enableMapDrag(); };
sliderEl.addEventListener('pointerdown', startDrag);
sliderEl.addEventListener('pointerup', endDrag);
sliderEl.addEventListener('pointercancel', endDrag);
// mouse/touch fallback
sliderEl.addEventListener('mousedown', startDrag);
document.addEventListener('mouseup', endDrag);
sliderEl.addEventListener('touchstart', startDrag, {passive:true});
document.addEventListener('touchend', endDrag);
// actual slider input handling (keep default behavior so slider remains draggable)
sliderEl.addEventListener('input', function(ev){
const r = parseInt(ev.target.value,10);
try{ masjidCircle.setRadius(r); }catch(e){}
const valEl = document.getElementById('masjid-radius-value'); if(valEl) valEl.innerText = r;
updateHomeColors(center, r);
});
}
document.getElementById('masjid-radius-close').addEventListener('click', function(){ hideMasjidRadius(); });
updateHomeColors(center, defaultRadius);
}
function hideMasjidRadius(){
if(masjidCircle){ map.removeLayer(masjidCircle); masjidCircle = null; }
if(masjidControlEl){ masjidControlEl.remove(); masjidControlEl = null; }
activeMasjid = null;
// reset rumah markers to default red
rumahMarkers.forEach(m=>{ try{ m.setStyle({color:'#d32f2f', fillColor:'#d32f2f'}); }catch(e){} });
}
function updateHomeColors(centerLatLng, radiusMeters){
rumahMarkers.forEach(m=>{
try{
const dist = m.getLatLng().distanceTo(centerLatLng);
if(dist <= radiusMeters){
m.setStyle({color:'#2e7d32', fillColor:'#2e7d32'}); // green inside
} else {
m.setStyle({color:'#d32f2f', fillColor:'#d32f2f'}); // red outside
}
}catch(e){ }
});
}
// add delegated click handler: when popup for masjid opens, show button to toggle radius
map.on('popupopen', function(e){
try{
const layer = e.popup._source;
if(layer && layer._jenis === 'masjid'){
const container = e.popup.getElement();
if(container && !container.querySelector('.show-masjid-radius')){
const btn = document.createElement('button'); btn.textContent='Show Radius'; btn.className='show-masjid-radius';
btn.style.marginLeft='6px';
btn.onclick = function(){ showMasjidRadius(layer); };
const content = container.querySelector('.leaflet-popup-content');
if(content) content.appendChild(btn);
}
}
}catch(e){}
});
function loadJalan(){
jalanGroup.clearLayers();
fetch('src/api/get_jalan.php')
.then(res=>res.json())
.then(data=>{
data.forEach(j=>{
let geo = null;
try {
geo = (typeof j.geom === 'string') ? JSON.parse(j.geom) : j.geom;
} catch(err) {
try {
// try double-encoded JSON
geo = JSON.parse(JSON.parse(j.geom));
} catch(err2){
console.error('Failed to parse jalan.geom for id', j.id, err2);
// fallback: show a marker at map center with raw info so user sees the record
const safeNama = escapeHtml(j.nama);
const m = L.marker(map.getCenter()).bindPopup(`<b>${safeNama}</b><br>Invalid geometry<br><pre style="max-height:120px;overflow:auto">${escapeHtml(String(j.geom)).slice(0,400)}</pre>`);
jalanGroup.addLayer(m);
map.panTo(m.getLatLng());
m.openPopup();
return; // skip adding geojson
}
}
const pj = parseFloat(j.panjang) || 0;
const safeNama = escapeHtml(j.nama);
const safeStatus = escapeHtml(j.status);
const jsEscapedNama = safeNama.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
const popup = `<b>${safeNama}</b><br>${safeStatus}<br>${pj.toFixed(2)} m<br><br>
<button onclick="hapusJalan('${j.id}')">Hapus</button>
<button onclick="editJalan('${j.id}','${jsEscapedNama}', '${safeStatus}', ${pj})">Edit</button>`;
console.log('loadJalan: id=', j.id, 'geom=', geo);
const gj = L.geoJSON(geo,{
style:{color:
j.status=="Nasional"?"red":
j.status=="Provinsi"?"blue":"green"}
});
try{
gj.eachLayer(function(layer){
layer._fid = j.id;
layer.bindPopup(popup);
jalanGroup.addLayer(layer);
try{
if(layer.getBounds){
const c = layer.getBounds().getCenter();
if(!map.getBounds().contains(c)){
map.panTo(c);
layer.openPopup && layer.openPopup();
}
} else if(layer.getLatLng){
const p = layer.getLatLng();
if(!map.getBounds().contains(p)){
map.panTo(p);
layer.openPopup && layer.openPopup();
}
}
}catch(e){console.warn('pan check failed', e)}
});
} catch(e){
console.error('Error adding jalan geo layer', j.id, e);
const safeErrNama = escapeHtml(j.nama);
const m = L.marker(map.getCenter()).bindPopup(`<b>${safeErrNama}</b><br>Geo add error<br><pre style="max-height:120px;overflow:auto">${escapeHtml(String(j.geom)).slice(0,400)}</pre>`);
jalanGroup.addLayer(m);
map.panTo(m.getLatLng());
m.openPopup();
}
});
});
}
function loadTanah(){
tanahGroup.clearLayers();
fetch('src/api/get_tanah.php')
.then(res=>res.json())
.then(data=>{
data.forEach(t=>{
let geo = null;
try {
geo = (typeof t.geom === 'string') ? JSON.parse(t.geom) : t.geom;
} catch(err) {
try {
geo = JSON.parse(JSON.parse(t.geom));
} catch(err2){
console.error('Failed to parse tanah.geom for id', t.id, err2);
const safeNama = escapeHtml(t.nama);
const m = L.marker(map.getCenter()).bindPopup(`<b>${safeNama}</b><br>Invalid geometry<br><pre style="max-height:120px;overflow:auto">${escapeHtml(String(t.geom)).slice(0,400)}</pre>`);
tanahGroup.addLayer(m);
map.panTo(m.getLatLng());
m.openPopup();
return;
}
}
const luasNum = parseFloat(t.luas) || 0;
const safeNama = escapeHtml(t.nama);
const safeStatus = escapeHtml(t.status);
const jsEscapedNama = safeNama.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
const popup = `<b>${safeNama}</b><br>${tanahStatusFull(t.status)}<br>${luasNum.toFixed(2)} m²<br><br>
<button onclick="hapusTanah('${t.id}')">Hapus</button>
<button onclick="editTanah('${t.id}','${jsEscapedNama}', '${safeStatus}', ${luasNum})">Edit</button>`;
console.log('loadTanah: id=', t.id, 'geom=', geo);
const gj = L.geoJSON(geo,{
style:{color:
t.status=="SHM"?"green":
t.status=="HGB"?"blue":
t.status=="HGU"?"orange":"purple"}
});
try{
gj.eachLayer(function(layer){
layer._fid = t.id;
layer.bindPopup(popup);
tanahGroup.addLayer(layer);
try{
if(layer.getBounds){
const c = layer.getBounds().getCenter();
if(!map.getBounds().contains(c)){
map.panTo(c);
layer.openPopup && layer.openPopup();
}
} else if(layer.getLatLng){
const p = layer.getLatLng();
if(!map.getBounds().contains(p)){
map.panTo(p);
layer.openPopup && layer.openPopup();
}
}
}catch(e){console.warn('pan check failed', e)}
});
} catch(e){
console.error('Error adding tanah geo layer', t.id, e);
const safeErrNama = escapeHtml(t.nama);
const m = L.marker(map.getCenter()).bindPopup(`<b>${safeErrNama}</b><br>Geo add error<br><pre style="max-height:120px;overflow:auto">${escapeHtml(String(t.geom)).slice(0,400)}</pre>`);
tanahGroup.addLayer(m);
map.panTo(m.getLatLng());
m.openPopup();
}
});
});
}
// ================= DELETE =================
function hapus(id){
fetch('src/api/hapus_lokasi.php',{
method: 'POST', headers: buildHeaders(),
body: JSON.stringify({id: id})
}).then(res => handleApiResponse(res, loadData));
}
function hapusJalan(id){
fetch('src/api/hapus_jalan.php',{
method: 'POST', headers: buildHeaders(),
body: JSON.stringify({id: id})
}).then(res => handleApiResponse(res, loadJalan));
}
function hapusTanah(id){
fetch('src/api/hapus_tanah.php',{
method: 'POST', headers: buildHeaders(),
body: JSON.stringify({id: id})
}).then(res => handleApiResponse(res, loadTanah));
}
// ================= UPDATE =================
function edit(id,nama,telp,buka,alamat,jenis){
openModal('Edit Lokasi', `
<input type="hidden" name="id" value="${id}">
<label>Nama <input name="nama" type="text" required value="${(nama||'').replace(/"/g,'&quot;')}"></label>
<label>Jenis <select name="jenis"><option value="rumah">Rumah</option><option value="masjid">Masjid</option><option value="spbu">SPBU</option></select></label>
<label>No Telp <input name="no_telp" type="text" value="${(telp||'').replace(/"/g,'&quot;')}"></label>
<label><input name="buka_24_jam" type="checkbox" ${buka? 'checked' : ''}> Buka 24 Jam</label>
<label>Alamat <input name="alamat" type="text" value="${(alamat||'').replace(/"/g,'&quot;')}"></label>
`, function(values){
fetch('src/api/update_lokasi.php',{
method:'POST',headers: buildHeaders(),
body:JSON.stringify({id:id,nama:values.nama, jenis: values.jenis || 'rumah', no_telp:values.no_telp||'',buka_24_jam:values.buka_24_jam?1:0, alamat: values.alamat || ''})
}).then(res => handleApiResponse(res, loadData));
});
// set current jenis selection after modal fields inserted
setTimeout(()=>{ const sel=document.querySelector('#modal-fields select[name="jenis"]'); if(sel) sel.value = (jenis||'rumah'); },0);
}
function editJalan(id,nama,status,panjang){
openModal('Edit Jalan', `
<input type="hidden" name="id" value="${id}">
<label>Nama <input name="nama" type="text" required value="${(nama||'').replace(/"/g,'&quot;')}"></label>
<label>Status <select name="status"><option value="Nasional">Nasional</option><option value="Provinsi">Provinsi</option><option value="Kabupaten">Kabupaten</option></select></label>
<label>Panjang (m) <input name="panjang" type="text" readonly value="${parseFloat(panjang).toFixed(2)}"></label>
`, function(values){
fetch('src/api/update_jalan.php',{
method:'POST',headers: buildHeaders(),
body:JSON.stringify({id:id,nama:values.nama,status:values.status,panjang:parseFloat(values.panjang)||0})
}).then(res => handleApiResponse(res, loadJalan));
});
}
function editTanah(id,nama,status,luas){
const fields = `
<input type="hidden" name="id" value="${id}">
<label>Nama Pemilik <input name="nama" type="text" required value="${(nama||'').replace(/"/g,'&quot;')}"></label>
<label>Status <select name="status">
<option value="SHM">Sertifikat Hak Milik (SHM)</option>
<option value="HGB">Sertifikat Hak Guna Bangunan (HGB)</option>
<option value="HGU">Sertifikat Hak Guna Usaha (HGU)</option>
<option value="HP">Sertifikat Hak Pakai (HP)</option>
</select></label>
<label>Luas (m²) <input name="luas" type="text" readonly value="${parseFloat(luas).toFixed(2)}"></label>
`;
const cb = function(values){
fetch('src/api/update_tanah.php',{
method:'POST',headers: buildHeaders(),
body:JSON.stringify({id:id,nama:values.nama,status:values.status,luas:parseFloat(values.luas)||0})
}).then(res => handleApiResponse(res, loadTanah));
};
openModal('Edit Tanah', fields, cb);
// set the select value after modal fields are inserted (avoid embedding closing script tag inside templates)
setTimeout(()=>{ const sel=document.querySelector('#modal-fields select[name="status"]'); if(sel) sel.value = status; },0);
}
// Using Draw's marker tool to create 'lokasi' (handled in draw:created)
function handlePolyline(layer) {
// open modal to collect jalan info, store layer pending until user submits
pendingType = 'jalan';
pendingLayer = layer;
let latlngs = layer.getLatLngs();
let panjang = 0;
for(let i=0;i<latlngs.length-1;i++){
panjang += latlngs[i].distanceTo(latlngs[i+1]);
}
openModal('Tambah Jalan', `
<label>Nama <input name="nama" type="text" required></label>
<label>Status <select name="status"><option value="Nasional">Nasional</option><option value="Provinsi">Provinsi</option><option value="Kabupaten">Kabupaten</option></select></label>
<label>Panjang (m) <input name="panjang" type="text" readonly value="${panjang.toFixed(2)}"></label>
`, handleModalSubmit);
}
function handlePolygon(layer) {
// open modal to collect tanah info
pendingType = 'tanah';
pendingLayer = layer;
const latlngs = layer.getLatLngs()[0];
const luas = L.GeometryUtil.geodesicArea(latlngs);
openModal('Tambah Tanah', `
<label>Nama Pemilik <input name="nama" type="text" required></label>
<label>Status <select name="status">
<option value="SHM">Sertifikat Hak Milik (SHM)</option>
<option value="HGB">Sertifikat Hak Guna Bangunan (HGB)</option>
<option value="HGU">Sertifikat Hak Guna Usaha (HGU)</option>
<option value="HP">Sertifikat Hak Pakai (HP)</option>
</select></label>
<label>Luas (m²) <input name="luas" type="text" readonly value="${luas.toFixed(2)}"></label>
`, handleModalSubmit);
}
// Reverse geocode helper using Nominatim (OpenStreetMap). Returns a Promise<string> of the display address.
function reverseGeocode(lat, lng){
const url = `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${encodeURIComponent(lat)}&lon=${encodeURIComponent(lng)}`;
return fetch(url, { headers: { 'Accept': 'application/json' } })
.then(res => res.ok ? res.json() : Promise.reject('nores'))
.then(data => (data && data.display_name) ? data.display_name : '')
.catch(err => {
console.warn('reverseGeocode failed', err);
return '';
});
}
// Modal helpers
function openModal(title, fieldsHtml, submitHandler){
document.getElementById('modal-title').textContent = title;
document.getElementById('modal-fields').innerHTML = fieldsHtml;
const modal = document.getElementById('modal');
modal.classList.remove('hidden');
modal.setAttribute('aria-hidden','false');
const form = document.getElementById('modal-form');
form.onsubmit = function(ev){
ev.preventDefault();
const fd = new FormData(form);
const obj = {};
for(const [k,v] of fd.entries()){
obj[k] = v;
}
const bukaCheckbox = form.querySelector('input[name="buka_24_jam"]');
if(bukaCheckbox) obj['buka_24_jam'] = bukaCheckbox.checked?1:0;
submitHandler(obj);
closeModal();
};
document.getElementById('modal-cancel').onclick = function(){ closeModal(); pendingLayer=null; pendingType=null; };
}
function closeModal(){
try{ if(document.activeElement && typeof document.activeElement.blur === 'function') document.activeElement.blur(); }catch(e){}
const modal = document.getElementById('modal');
modal.classList.add('hidden');
modal.setAttribute('aria-hidden','true');
document.getElementById('modal-fields').innerHTML = '';
}
function handleModalSubmit(values){
if(pendingType === 'lokasi'){
const latlng = (typeof pendingLayer.getLatLng === 'function') ? pendingLayer.getLatLng() : pendingLayer;
fetch('src/api/tambah_lokasi.php',{
method:'POST',headers: buildHeaders(),
body:JSON.stringify({
nama: values.nama,
jenis: values.jenis || 'rumah',
no_telp: values.no_telp || '',
buka_24_jam: values.buka_24_jam?1:0,
alamat: values.alamat || '',
latitude: latlng.lat,
longitude: latlng.lng
})
}).then(res => handleApiResponse(res, () => {
if(pendingLayer && drawnItems.hasLayer && drawnItems.hasLayer(pendingLayer)) drawnItems.removeLayer(pendingLayer);
loadData();
pendingLayer=null;
pendingType=null;
}));
} else if(pendingType === 'jalan'){
const geojson = pendingLayer.toGeoJSON();
const panjang = parseFloat((document.querySelector('input[name="panjang"]').value)||0);
const status = values.status || 'Kabupaten';
const nama = values.nama || '';
let warna = status==='Nasional'?'red':(status==='Provinsi'?'blue':'green');
pendingLayer.setStyle({color:warna});
fetch('src/api/tambah_jalan.php',{
method:'POST',headers: buildHeaders(),
body:JSON.stringify({nama:nama,status:status,panjang:panjang,geom:geojson.geometry})
}).then(res => handleApiResponse(res, (resp) => {
const newId = resp.id || null;
if(drawnItems.hasLayer(pendingLayer)) drawnItems.removeLayer(pendingLayer);
pendingLayer._fid = newId;
const safeNama = escapeHtml(nama);
const jsEscapedNama = safeNama.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
pendingLayer.bindPopup(`<b>${safeNama}</b><br>${status}<br>Panjang: ${panjang.toFixed(2)} m<br><br>` +
`<button onclick="hapusJalan('${newId}')">Hapus</button>` +
`<button onclick="editJalan('${newId}','${jsEscapedNama}', '${status}', ${panjang})">Edit</button>`);
jalanGroup.addLayer(pendingLayer);
pendingLayer=null; pendingType=null;
}));
} else if(pendingType === 'tanah'){
const geojson = pendingLayer.toGeoJSON();
const luas = parseFloat((document.querySelector('input[name="luas"]').value)||0);
const status = values.status || 'SHM';
const nama = values.nama || '';
let warna = status==='SHM'?'green':(status==='HGB'?'blue':(status==='HGU'?'orange':'purple'));
pendingLayer.setStyle({color:warna});
fetch('src/api/tambah_tanah.php',{
method:'POST',headers: buildHeaders(),
body:JSON.stringify({nama:nama,status:status,luas:luas,geom:geojson.geometry})
}).then(res => handleApiResponse(res, (resp) => {
const newId = resp.id || null;
if(drawnItems.hasLayer(pendingLayer)) drawnItems.removeLayer(pendingLayer);
pendingLayer._fid = newId;
const safeT = escapeHtml(nama);
const jsEscapedT = safeT.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
pendingLayer.bindPopup(`<b>${safeT}</b><br>${status}<br>Luas: ${luas.toFixed(2)} m²<br><br>` +
`<button onclick="hapusTanah('${newId}')">Hapus</button>` +
`<button onclick="editTanah('${newId}','${jsEscapedT}', '${status}', ${luas})">Edit</button>`);
tanahGroup.addLayer(pendingLayer);
pendingLayer=null; pendingType=null;
}));
}
}
// ================= INIT =================
updateApiKeyStatus();
loadData();
loadJalan();
loadTanah();
</script>
</body>
</html>