Initial commit of unified WebGIS projects
This commit is contained in:
@@ -0,0 +1,778 @@
|
||||
<!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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
||||
}
|
||||
|
||||
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,'"')}"></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,'"')}"></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,'"')}"></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,'"')}"></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,'"')}"></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>
|
||||
Reference in New Issue
Block a user