408 lines
17 KiB
JavaScript
408 lines
17 KiB
JavaScript
/* Input mode: draw jalan / kavling / rusak on map */
|
|
var drawMode = null, drawing = false, points = [], previewLayer = null, tempMarkers = [];
|
|
var pendingKoord = null, pendingUkuran = 0, currentMode = null;
|
|
var pendingRusakLatLng = null, fotoBase64 = '';
|
|
|
|
function setMode(m) {
|
|
if (drawing) cancelDraw();
|
|
drawMode = m;
|
|
currentMode = m;
|
|
|
|
var bj = document.getElementById('btn-jalan');
|
|
var bp = document.getElementById('btn-parsil');
|
|
var br = document.getElementById('btn-rusak');
|
|
if (bj) bj.className = m === 'jalan' ? 'active-jalan' : '';
|
|
if (bp) bp.className = m === 'parsil' ? 'active-parsil' : '';
|
|
if (br) br.className = m === 'rusak' ? 'active-rusak' : '';
|
|
|
|
var actions = document.getElementById('draw-actions');
|
|
var hint = document.getElementById('hint');
|
|
|
|
if (m === 'rusak') {
|
|
drawing = false;
|
|
points = [];
|
|
if (hint) hint.textContent = 'Klik titik di peta untuk menandai jalan rusak';
|
|
map.getContainer().style.cursor = 'crosshair';
|
|
if (actions) actions.style.display = 'flex';
|
|
['undo-btn', 'finish-btn'].forEach(function (id) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.style.display = 'none';
|
|
});
|
|
var cancel = document.getElementById('cancel-btn');
|
|
if (cancel) cancel.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
drawing = true;
|
|
points = [];
|
|
if (actions) actions.style.display = 'flex';
|
|
['undo-btn', 'finish-btn', 'cancel-btn'].forEach(function (id) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.style.display = 'block';
|
|
});
|
|
if (hint) {
|
|
hint.textContent = m === 'jalan'
|
|
? 'Klik peta untuk menambah titik jalan — minimal 2 titik'
|
|
: 'Klik peta untuk menggambar area kavling — minimal 3 titik';
|
|
}
|
|
map.getContainer().style.cursor = 'crosshair';
|
|
}
|
|
|
|
map.on('click', function (e) {
|
|
if (currentMode === 'rusak') {
|
|
pendingRusakLatLng = e.latlng;
|
|
openRusakModal(e.latlng);
|
|
return;
|
|
}
|
|
if (!drawing) return;
|
|
points.push(e.latlng);
|
|
var dot = L.circleMarker(e.latlng, {
|
|
radius: 5,
|
|
color: drawMode === 'jalan' ? '#2563eb' : '#7c3aed',
|
|
fillColor: '#fff',
|
|
fillOpacity: 1,
|
|
weight: 2.5
|
|
}).addTo(map);
|
|
tempMarkers.push(dot);
|
|
redrawPreview();
|
|
});
|
|
|
|
function redrawPreview() {
|
|
if (previewLayer) map.removeLayer(previewLayer);
|
|
if (points.length < 2) return;
|
|
var opts = { color: drawMode === 'jalan' ? '#2563eb' : '#7c3aed', weight: 2.5, dashArray: '7,5', opacity: 0.85 };
|
|
previewLayer = drawMode === 'jalan'
|
|
? L.polyline(points, opts).addTo(map)
|
|
: L.polygon(points, Object.assign({}, opts, { fillOpacity: 0.1 })).addTo(map);
|
|
}
|
|
|
|
function undoLast() {
|
|
if (!points.length) return;
|
|
points.pop();
|
|
var last = tempMarkers.pop();
|
|
if (last) map.removeLayer(last);
|
|
redrawPreview();
|
|
}
|
|
|
|
function cancelDraw() {
|
|
drawing = false;
|
|
points = [];
|
|
if (previewLayer) { map.removeLayer(previewLayer); previewLayer = null; }
|
|
tempMarkers.forEach(function (m) { map.removeLayer(m); });
|
|
tempMarkers = [];
|
|
map.getContainer().style.cursor = '';
|
|
var actions = document.getElementById('draw-actions');
|
|
if (actions) actions.style.display = 'none';
|
|
['btn-jalan', 'btn-parsil', 'btn-rusak'].forEach(function (id) {
|
|
var el = document.getElementById(id);
|
|
if (el) el.className = '';
|
|
});
|
|
var hint = document.getElementById('hint');
|
|
if (hint) hint.textContent = 'Pilih mode input di atas';
|
|
currentMode = null;
|
|
drawMode = null;
|
|
pendingRusakLatLng = null;
|
|
}
|
|
|
|
function finishShape() {
|
|
if (drawMode === 'jalan' && points.length < 2) { showToast('Minimal 2 titik untuk jalan!', 'error'); return; }
|
|
if (drawMode === 'parsil' && points.length < 3) { showToast('Minimal 3 titik untuk kavling!', 'error'); return; }
|
|
drawing = false;
|
|
map.getContainer().style.cursor = '';
|
|
|
|
var ukuran = 0;
|
|
if (drawMode === 'jalan') {
|
|
for (var i = 0; i < points.length - 1; i++) ukuran += points[i].distanceTo(points[i + 1]);
|
|
ukuran = Math.round(ukuran * 10) / 10;
|
|
document.getElementById('inp-ukuran').value = ukuran.toFixed(1) + ' meter';
|
|
document.getElementById('form-title').textContent = 'Simpan Data Jalan';
|
|
document.getElementById('form-sub').textContent = 'Lengkapi informasi ruas jalan';
|
|
document.getElementById('form-icon').textContent = '🛣';
|
|
document.getElementById('form-icon').className = 'modal-icon jalan';
|
|
document.getElementById('label-nama').textContent = 'Nama Jalan';
|
|
document.getElementById('label-status').textContent = 'Status Jalan';
|
|
document.getElementById('label-ukuran').innerHTML = 'Panjang Jalan <span class="readonly-tag">Otomatis</span>';
|
|
document.getElementById('inp-nama').placeholder = 'Contoh: Jl. Ahmad Yani';
|
|
document.getElementById('inp-status').innerHTML =
|
|
'<option value="Jalan Nasional">Jalan Nasional</option>' +
|
|
'<option value="Jalan Provinsi">Jalan Provinsi</option>' +
|
|
'<option value="Jalan Kabupaten">Jalan Kabupaten</option>';
|
|
document.getElementById('parsil-extra-fields').style.display = 'none';
|
|
} else {
|
|
ukuran = calcArea(points);
|
|
ukuran = Math.round(ukuran * 10) / 10;
|
|
document.getElementById('inp-ukuran').value = ukuran.toFixed(1) + ' m²';
|
|
document.getElementById('form-title').textContent = 'Simpan Data Kavling';
|
|
document.getElementById('form-sub').textContent = 'Lengkapi informasi kavling tanah';
|
|
document.getElementById('form-icon').textContent = '🏘';
|
|
document.getElementById('form-icon').className = 'modal-icon parsil';
|
|
document.getElementById('label-nama').textContent = 'Nama / Nomor Parsil';
|
|
document.getElementById('label-status').textContent = 'Status Kepemilikan';
|
|
document.getElementById('label-ukuran').innerHTML = 'Luas Area <span class="readonly-tag">Otomatis</span>';
|
|
document.getElementById('inp-nama').placeholder = 'Contoh: Parsil A-01';
|
|
document.getElementById('inp-status').innerHTML =
|
|
'<option value="SHM">SHM — Sertifikat Hak Milik</option>' +
|
|
'<option value="HGB">HGB — Hak Guna Bangunan</option>' +
|
|
'<option value="HGU">HGU — Hak Guna Usaha</option>' +
|
|
'<option value="HP">HP — Hak Pakai</option>';
|
|
document.getElementById('parsil-extra-fields').style.display = 'block';
|
|
document.getElementById('inp-pemilik').value = '';
|
|
document.getElementById('inp-nosert').value = '';
|
|
document.getElementById('inp-keterangan').value = '';
|
|
}
|
|
|
|
pendingKoord = points.map(function (p) { return [p.lat, p.lng]; });
|
|
pendingUkuran = ukuran;
|
|
document.getElementById('inp-nama').value = '';
|
|
document.getElementById('form-modal').classList.add('active');
|
|
setTimeout(function () { document.getElementById('inp-nama').focus(); }, 100);
|
|
}
|
|
|
|
function calcArea(latlngs) {
|
|
var R = 6371000, n = latlngs.length, area = 0;
|
|
if (n < 3) return 0;
|
|
for (var i = 0; i < n; i++) {
|
|
var j = (i + 1) % n;
|
|
var xi = latlngs[i].lng * Math.PI / 180 * R * Math.cos(latlngs[i].lat * Math.PI / 180);
|
|
var yi = latlngs[i].lat * Math.PI / 180 * R;
|
|
var xj = latlngs[j].lng * Math.PI / 180 * R * Math.cos(latlngs[j].lat * Math.PI / 180);
|
|
var yj = latlngs[j].lat * Math.PI / 180 * R;
|
|
area += xi * yj - xj * yi;
|
|
}
|
|
return Math.abs(area / 2);
|
|
}
|
|
|
|
function saveData() {
|
|
var nama = document.getElementById('inp-nama').value.trim();
|
|
var status = document.getElementById('inp-status').value;
|
|
if (!nama) { showToast('Nama tidak boleh kosong!', 'error'); return; }
|
|
|
|
var btn = document.querySelector('#form-modal .btn-primary');
|
|
btn.textContent = 'Menyimpan…';
|
|
btn.disabled = true;
|
|
|
|
var params = {
|
|
action: currentMode === 'jalan' ? 'save_jalan' : 'save_parsil',
|
|
nama: nama,
|
|
status: status,
|
|
ukuran: pendingUkuran,
|
|
koordinat: JSON.stringify(pendingKoord)
|
|
};
|
|
if (currentMode === 'parsil') {
|
|
params.pemilik = document.getElementById('inp-pemilik').value.trim();
|
|
params.nosert = document.getElementById('inp-nosert').value.trim();
|
|
params.keterangan = document.getElementById('inp-keterangan').value.trim();
|
|
}
|
|
|
|
fetch('api.php?' + new URLSearchParams(params))
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (res) {
|
|
btn.textContent = 'Simpan Data';
|
|
btn.disabled = false;
|
|
if (res.ok) {
|
|
showToast('Data berhasil disimpan!', 'success');
|
|
closeForm();
|
|
var id = res.id;
|
|
var ll = pendingKoord.map(function (p) { return [p[0], p[1]]; });
|
|
if (currentMode === 'jalan') {
|
|
var layer = L.polyline(ll, { color: WARNA_JALAN[status] || '#999', weight: 5, opacity: 0.9 }).addTo(map);
|
|
layer.bindPopup(makeJalanPopup(id, nama, status, pendingUkuran.toFixed(1)), { maxWidth: 280 });
|
|
layer.on('click', function () { lnavHighlightFromMap('jalan', id); });
|
|
jalanLayers[id] = layer;
|
|
jalanData[id] = { id: id, nama: nama, status: status, pjg: pendingUkuran.toFixed(1), ll: ll };
|
|
} else {
|
|
var opt = WARNA_PARSIL[status] || { color: '#999', fillColor: '#999', fillOpacity: 0.2, weight: 2 };
|
|
var layer2 = L.polygon(ll, opt).addTo(map);
|
|
layer2.bindPopup(makeParsilPopup(id, nama, params.pemilik || '', params.nosert || '', params.keterangan || '', status, pendingUkuran.toFixed(1)), { maxWidth: 280 });
|
|
layer2.on('click', function () { lnavHighlightFromMap('parsil', id); });
|
|
parsilLayers[id] = layer2;
|
|
parsilData[id] = { id: id, nama: nama, pemilik: params.pemilik || '', nosert: params.nosert || '', ket: params.keterangan || '', status: status, luas: pendingUkuran.toFixed(1), ll: ll };
|
|
}
|
|
if (res.tj !== undefined) updateStats(res);
|
|
cancelDraw();
|
|
} else {
|
|
showToast('Gagal: ' + (res.msg || 'unknown'), 'error');
|
|
}
|
|
})
|
|
.catch(function () {
|
|
btn.textContent = 'Simpan Data';
|
|
btn.disabled = false;
|
|
showToast('Koneksi gagal!', 'error');
|
|
});
|
|
}
|
|
|
|
function closeForm() {
|
|
document.getElementById('form-modal').classList.remove('active');
|
|
}
|
|
|
|
document.getElementById('form-modal').addEventListener('click', function (e) {
|
|
if (e.target === this) closeForm();
|
|
});
|
|
|
|
function openRusakModal(latlng) {
|
|
var d = new Date();
|
|
var today = d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
|
|
document.getElementById('rusak-tanggal').value = today;
|
|
document.getElementById('rusak-koordinat').value = latlng.lat.toFixed(6) + ', ' + latlng.lng.toFixed(6);
|
|
document.getElementById('rusak-sub').textContent = latlng.lat.toFixed(5) + ', ' + latlng.lng.toFixed(5);
|
|
document.getElementById('rusak-pelapor').value = '';
|
|
document.getElementById('rusak-nama-jalan').value = 'Mencari nama jalan…';
|
|
document.getElementById('rusak-ket').value = '';
|
|
clearFoto();
|
|
|
|
fetch('https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=' + latlng.lat + '&lon=' + latlng.lng + '&zoom=17&accept-language=id')
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (geo) {
|
|
var road = geo.address && (geo.address.road || geo.address.residential || geo.address.path || geo.address.pedestrian || geo.address.footway || '');
|
|
document.getElementById('rusak-nama-jalan').value = road || '(Jalan tidak teridentifikasi)';
|
|
})
|
|
.catch(function () {
|
|
document.getElementById('rusak-nama-jalan').value = '(Gagal deteksi nama jalan)';
|
|
});
|
|
|
|
var badge = document.getElementById('gps-badge');
|
|
badge.style.display = 'inline-flex';
|
|
badge.className = 'gps-badge loading';
|
|
badge.textContent = 'Mengambil GPS…';
|
|
if (navigator.geolocation) {
|
|
navigator.geolocation.getCurrentPosition(function (pos) {
|
|
badge.className = 'gps-badge';
|
|
badge.textContent = 'GPS: ±' + Math.round(pos.coords.accuracy) + 'm akurasi';
|
|
}, function () {
|
|
badge.className = 'gps-badge';
|
|
badge.textContent = 'Koordinat dari klik peta';
|
|
}, { timeout: 5000 });
|
|
} else {
|
|
badge.className = 'gps-badge';
|
|
badge.textContent = 'Koordinat dari klik peta';
|
|
}
|
|
|
|
document.getElementById('rusak-modal').classList.add('active');
|
|
setTimeout(function () { document.getElementById('rusak-pelapor').focus(); }, 100);
|
|
}
|
|
|
|
function closeRusakModal() {
|
|
document.getElementById('rusak-modal').classList.remove('active');
|
|
fotoBase64 = '';
|
|
pendingRusakLatLng = null;
|
|
}
|
|
|
|
document.getElementById('rusak-modal').addEventListener('click', function (e) {
|
|
if (e.target === this) closeRusakModal();
|
|
});
|
|
|
|
function handleFoto(input) {
|
|
var file = input.files[0];
|
|
if (!file) return;
|
|
if (file.size > 5 * 1024 * 1024) { showToast('Foto maks 5 MB!', 'error'); return; }
|
|
|
|
EXIF.getData(file, function() {
|
|
var lat = EXIF.getTag(this, "GPSLatitude");
|
|
var lon = EXIF.getTag(this, "GPSLongitude");
|
|
var latRef = EXIF.getTag(this, "GPSLatitudeRef") || "N";
|
|
var lonRef = EXIF.getTag(this, "GPSLongitudeRef") || "E";
|
|
if (lat && lon) {
|
|
function parseDms(dms) {
|
|
if (!dms || dms.length < 3) return null;
|
|
var d = (typeof dms[0] === 'object' && dms[0].denominator) ? dms[0].numerator / dms[0].denominator : Number(dms[0]);
|
|
var m = (typeof dms[1] === 'object' && dms[1].denominator) ? dms[1].numerator / dms[1].denominator : Number(dms[1]);
|
|
var s = (typeof dms[2] === 'object' && dms[2].denominator) ? dms[2].numerator / dms[2].denominator : Number(dms[2]);
|
|
return d + (m / 60) + (s / 3600);
|
|
}
|
|
|
|
var latDec = parseDms(lat);
|
|
var lonDec = parseDms(lon);
|
|
|
|
if (latDec !== null && lonDec !== null && !isNaN(latDec) && !isNaN(lonDec)) {
|
|
if (latRef === "S") latDec = -latDec;
|
|
if (lonRef === "W") lonDec = -lonDec;
|
|
pendingRusakLatLng = {lat: latDec, lng: lonDec};
|
|
document.getElementById('rusak-koordinat').value = latDec.toFixed(6) + ', ' + lonDec.toFixed(6);
|
|
showToast('Geotagging terdeteksi!', 'success');
|
|
|
|
var badge = document.getElementById('gps-badge');
|
|
if(badge) {
|
|
badge.style.display = 'inline-flex';
|
|
badge.className = 'gps-badge';
|
|
badge.textContent = 'GPS dari Foto';
|
|
}
|
|
|
|
fetch('https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=' + latDec + '&lon=' + lonDec + '&zoom=17&accept-language=id')
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (geo) {
|
|
var road = geo.address && (geo.address.road || geo.address.residential || geo.address.path || geo.address.pedestrian || geo.address.footway || '');
|
|
document.getElementById('rusak-nama-jalan').value = road || geo.display_name || '(Jalan tidak teridentifikasi)';
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
var reader = new FileReader();
|
|
reader.onload = function (e) {
|
|
fotoBase64 = e.target.result;
|
|
var preview = document.getElementById('photo-preview');
|
|
preview.src = fotoBase64;
|
|
preview.style.display = 'block';
|
|
document.getElementById('foto-clear-btn').style.display = 'flex';
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
|
|
function clearFoto() {
|
|
fotoBase64 = '';
|
|
document.getElementById('photo-preview').style.display = 'none';
|
|
document.getElementById('photo-preview').src = '';
|
|
document.getElementById('foto-clear-btn').style.display = 'none';
|
|
document.getElementById('foto-file').value = '';
|
|
document.getElementById('foto-cam').value = '';
|
|
}
|
|
|
|
function saveRusak() {
|
|
if (!pendingRusakLatLng) { showToast('Pilih titik di peta dulu!', 'error'); return; }
|
|
var pelapor = document.getElementById('rusak-pelapor').value.trim() || 'Anonym';
|
|
var namaJalan = document.getElementById('rusak-nama-jalan').value.trim();
|
|
var tanggal = document.getElementById('rusak-tanggal').value;
|
|
var ket = document.getElementById('rusak-ket').value.trim();
|
|
var lat = pendingRusakLatLng.lat;
|
|
var lng = pendingRusakLatLng.lng;
|
|
|
|
var btn = document.getElementById('rusak-save-btn');
|
|
btn.textContent = 'Menyimpan…';
|
|
btn.disabled = true;
|
|
|
|
var formData = new FormData();
|
|
formData.append('action', 'save_rusak');
|
|
formData.append('pelapor', pelapor);
|
|
formData.append('nama_jalan', namaJalan);
|
|
formData.append('tanggal', tanggal);
|
|
formData.append('lat', lat);
|
|
formData.append('lng', lng);
|
|
formData.append('keterangan', ket);
|
|
formData.append('foto', fotoBase64);
|
|
|
|
fetch('api.php', { method: 'POST', body: formData })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (res) {
|
|
btn.textContent = 'Simpan Titik';
|
|
btn.disabled = false;
|
|
if (res.ok) {
|
|
showToast('Titik jalan rusak disimpan!', 'success');
|
|
closeRusakModal();
|
|
var id = res.id;
|
|
var nama = res.nama || pelapor;
|
|
var layer = makeRusakLayer(id, nama, tanggal, lat, lng, ket, fotoBase64, 'recent', false);
|
|
rusakLayers[id] = layer;
|
|
rusakData[id] = { id: id, nama: nama, tgl: tanggal, lat: lat, lng: lng, ket: ket, foto: fotoBase64, age: 'recent', recurring: false };
|
|
if (res.tr !== undefined) updateStats(res);
|
|
cancelDraw();
|
|
} else {
|
|
showToast('Gagal: ' + (res.msg || 'unknown'), 'error');
|
|
}
|
|
})
|
|
.catch(function () {
|
|
btn.textContent = 'Simpan Titik';
|
|
btn.disabled = false;
|
|
showToast('Koneksi gagal!', 'error');
|
|
});
|
|
}
|