/* 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 Otomatis'; document.getElementById('inp-nama').placeholder = 'Contoh: Jl. Ahmad Yani'; document.getElementById('inp-status').innerHTML = '' + '' + ''; 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 Otomatis'; document.getElementById('inp-nama').placeholder = 'Contoh: Parsil A-01'; document.getElementById('inp-status').innerHTML = '' + '' + '' + ''; 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('api.php?action=reverse&lat=' + latlng.lat + '&lon=' + latlng.lng) .then(function (r) { return r.json(); }) .then(function (geo) { var a = geo.address || {}; var road = a.road || a.residential || a.path || a.pedestrian || a.footway || a.track || a.cycleway || a.neighbourhood || a.hamlet || a.village || a.suburb || ''; document.getElementById('rusak-nama-jalan').value = road || geo.display_name || '(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('api.php?action=reverse&lat=' + latDec + '&lon=' + lonDec) .then(function (r) { return r.json(); }) .then(function (geo) { var a = geo.address || {}; var road = a.road || a.residential || a.path || a.pedestrian || a.footway || a.track || a.cycleway || a.neighbourhood || a.hamlet || a.village || a.suburb || ''; document.getElementById('rusak-nama-jalan').value = road || geo.display_name || '(Jalan tidak teridentifikasi)'; }); } } }); var reader = new FileReader(); reader.onload = function (e) { // Kompres & resize foto agar payload kecil (hindari post_max_size & batas kolom DB) var img = new Image(); img.onload = function () { var maxDim = 1280; var w = img.width, h = img.height; if (w > h && w > maxDim) { h = Math.round(h * maxDim / w); w = maxDim; } else if (h >= w && h > maxDim) { w = Math.round(w * maxDim / h); h = maxDim; } var canvas = document.createElement('canvas'); canvas.width = w; canvas.height = h; canvas.getContext('2d').drawImage(img, 0, 0, w, h); fotoBase64 = canvas.toDataURL('image/jpeg', 0.7); var preview = document.getElementById('photo-preview'); preview.src = fotoBase64; preview.style.display = 'block'; document.getElementById('foto-clear-btn').style.display = 'flex'; }; img.onerror = function () { // Fallback bila gambar tak bisa di-decode (mis. format tak didukung canvas) 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'; }; img.src = e.target.result; }; 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.text().then(function (txt) { var res; try { res = JSON.parse(txt); } catch (e) { // Respons bukan JSON: tampilkan status & potongan isi supaya jelas penyebabnya var info = txt.trim() ? txt.trim().slice(0, 200) : '(respons kosong)'; throw new Error('HTTP ' + r.status + ' — ' + info); } return res; }); }) .then(function (res) { btn.textContent = 'Simpan Titik'; btn.disabled = false; if (res.ok) { showToast('Titik jalan rusak disimpan!', 'success'); var savedFoto = fotoBase64; // simpan dulu: closeRusakModal() me-reset fotoBase64 closeRusakModal(); var id = res.id; var nama = res.nama || pelapor; var layer = makeRusakLayer(id, nama, tanggal, lat, lng, ket, savedFoto, 'recent', false); rusakLayers[id] = layer; rusakData[id] = { id: id, nama: nama, tgl: tanggal, lat: lat, lng: lng, ket: ket, foto: savedFoto, age: 'recent', recurring: false }; if (res.tr !== undefined) updateStats(res); cancelDraw(); } else { showToast('Gagal: ' + (res.msg || 'unknown'), 'error'); } }) .catch(function (err) { btn.textContent = 'Simpan Titik'; btn.disabled = false; showToast('Gagal simpan: ' + (err && err.message ? err.message : 'koneksi gagal'), 'error'); console.error('saveRusak error:', err); }); }