795 lines
35 KiB
HTML
795 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="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() {
|
|
let key = localStorage.getItem('spbu_api_key');
|
|
if (!key) {
|
|
key = '8f3b7e6a9c4d2f1a6b8c9d0e4f7a1b2c';
|
|
localStorage.setItem('spbu_api_key', key);
|
|
}
|
|
const statusEl = document.getElementById('api-key-status');
|
|
const btnEl = document.getElementById('btn-api-key');
|
|
if (statusEl && btnEl) {
|
|
statusEl.textContent = 'Admin Mode';
|
|
statusEl.style.color = '#2e7d32';
|
|
btnEl.textContent = '🔑 Ubah API Key';
|
|
btnEl.style.background = '#2e7d32';
|
|
}
|
|
}
|
|
|
|
function promptApiKey() {
|
|
const currentKey = localStorage.getItem('spbu_api_key') || '8f3b7e6a9c4d2f1a6b8c9d0e4f7a1b2c';
|
|
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.setItem('spbu_api_key', '8f3b7e6a9c4d2f1a6b8c9d0e4f7a1b2c');
|
|
}
|
|
updateApiKeyStatus();
|
|
}
|
|
}
|
|
|
|
function updateBukaVisibility() {
|
|
const selectEl = document.querySelector('#modal-fields select[name="jenis"]');
|
|
const container = document.getElementById('buka-24-jam-container');
|
|
if (selectEl && container) {
|
|
if (selectEl.value === 'rumah') {
|
|
container.style.display = 'none';
|
|
const checkbox = container.querySelector('input[name="buka_24_jam"]');
|
|
if (checkbox) checkbox.checked = false;
|
|
} else {
|
|
container.style.display = 'block';
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleApiResponse(res, callback) {
|
|
if (res.status === 401) {
|
|
alert('Akses Ditolak: API Key tidak valid atau kosong.');
|
|
} else {
|
|
res.json().then(data => {
|
|
if (data && data.success === false) {
|
|
alert('Error: ' + (data.error || 'Terjadi kesalahan'));
|
|
} else if (callback) {
|
|
callback(data);
|
|
}
|
|
}).catch(err => {
|
|
console.error('Failed to parse API JSON response', err);
|
|
alert('Gagal memproses data: Response server tidak valid (Kemungkinan masalah koneksi database).');
|
|
});
|
|
}
|
|
}
|
|
|
|
function buildHeaders(){
|
|
const headers = {'Content-Type':'application/json'};
|
|
const key = localStorage.getItem('spbu_api_key') || '8f3b7e6a9c4d2f1a6b8c9d0e4f7a1b2c';
|
|
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 id="buka-24-jam-container"><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);
|
|
|
|
// Setup dynamic visibility for 24 hours checkbox
|
|
const selectEl = document.querySelector('#modal-fields select[name="jenis"]');
|
|
if (selectEl) {
|
|
selectEl.addEventListener('change', updateBukaVisibility);
|
|
updateBukaVisibility();
|
|
}
|
|
|
|
// 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 id="buka-24-jam-container"><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 and setup listener
|
|
setTimeout(()=>{
|
|
const sel=document.querySelector('#modal-fields select[name="jenis"]');
|
|
if(sel) {
|
|
sel.value = (jenis||'rumah');
|
|
sel.addEventListener('change', updateBukaVisibility);
|
|
updateBukaVisibility();
|
|
}
|
|
},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> |