// Maps Initialization const map = L.map('map').setView([-0.060586, 109.344989], 16); L.tileLayer('https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', { attribution: '© Google Maps', maxZoom: 20 }).addTo(map); const minimap = L.map('minimap', { zoomControl: false }).setView([-0.060586, 109.344989], 14); L.tileLayer('https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}', { attribution: '© Google Maps', maxZoom: 20 }).addTo(minimap); // Feature Groups const drawnItems = new L.FeatureGroup().addTo(map); const minimapLayer = L.featureGroup().addTo(minimap); // Draw Control on Main Map const drawControl = new L.Control.Draw({ draw: { marker: true, circle: false, circlemarker: false, rectangle: false, polyline: { shapeOptions: { color: '#ffffff', weight: 4 } }, polygon: { allowIntersection: false, showArea: true, shapeOptions: { color: '#ffffff', weight: 2 } } }, edit: { featureGroup: drawnItems, edit: true, remove: false } }); map.addControl(drawControl); // Configuration const JALAN_STATUS = ['Jalan Nasional', 'Jalan Provinsi', 'Jalan Kabupaten']; const PARSIL_STATUS = [ { value: 'SHM', label: 'Sertifikat Hak Milik (SHM)' }, { value: 'HGB', label: 'Sertifikat Hak Guna Bangunan (HGB)' }, { value: 'HGU', label: 'Sertifikat Hak Guna Usaha (HGU)' }, { value: 'HP', label: 'Sertifikat Hak Pakai (HP)' } ]; const COLORS = { 'Jalan Nasional': '#ef4444', 'Jalan Provinsi': '#f97316', 'Jalan Kabupaten': '#eab308', 'SHM': '#22c55e', 'HGB': '#3b82f6', 'HGU': '#a855f7', 'HP': '#06b6d4' }; const BG_COLORS = { 'Jalan Nasional': '#fef2f2', 'Jalan Provinsi': '#fff7ed', 'Jalan Kabupaten': '#fefce8', 'SHM': '#f0fdf4', 'HGB': '#eff6ff', 'HGU': '#faf5ff', 'HP': '#ecfeff' }; const moveIcon = L.divIcon({ html: '
', className: 'custom-move-icon', iconSize: [24, 24], iconAnchor: [12, 12] }); let currentTempLayer = null; let rawData = { jalan: [], parsil: [], titik: [] }; let currentTableTab = 'jalan'; // View Navigation function switchView(view) { document.querySelectorAll('.nav-btn').forEach(btn => btn.classList.remove('active')); document.getElementById(`btn-tab-${view}`).classList.add('active'); document.getElementById('view-input').classList.add('view-hidden'); document.getElementById('view-data').classList.add('view-hidden'); document.getElementById(`view-${view}`).classList.remove('view-hidden'); // Invalidate map sizes because of display:none toggling setTimeout(() => { map.invalidateSize(); minimap.invalidateSize(); if (view === 'data') renderTable(); }, 100); } // Table Tab Navigation function switchTableTab(tab) { document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); document.getElementById(`tab-${tab}`).classList.add('active'); currentTableTab = tab; document.getElementById('th-metric').innerText = tab === 'jalan' ? 'Panjang (m)' : (tab === 'titik' ? 'Foto' : 'Luas (m²)'); document.getElementById('search-input').value = ''; renderTable(); } // Table Search Event document.getElementById('search-input').addEventListener('input', renderTable); // Map Draw Event map.on(L.Draw.Event.CREATED, function (e) { currentTempLayer = e.layer; const type = e.layerType; let geojson = currentTempLayer.toGeoJSON(); const formType = type === 'polyline' ? 'jalan' : (type === 'polygon' ? 'parsil' : 'titik'); document.getElementById('form-type').value = formType; if (geojson.geometry) { document.getElementById('data-geojson').value = JSON.stringify(geojson.geometry); } // Reset data document.getElementById('data-id').value = ''; document.getElementById('data-nama').value = ''; // Setup Modal UI document.getElementById('modal-title').innerText = type === 'polyline' ? 'Input Data Jalan' : (type === 'marker' ? 'Laporkan Titik Kerusakan' : 'Input Data Parsil Tanah'); // Toggle Fields if (type === 'marker') { document.getElementById('data-status').parentElement.style.display = 'none'; document.getElementById('field-foto').style.display = 'block'; document.getElementById('field-metric').style.display = 'none'; document.getElementById('label-nama').innerText = 'Keterangan Kerusakan'; document.getElementById('data-status').removeAttribute('required'); document.getElementById('data-metric').removeAttribute('required'); document.getElementById('foto-preview').innerHTML = ''; } else { document.getElementById('data-status').parentElement.style.display = 'block'; document.getElementById('field-foto').style.display = 'none'; document.getElementById('field-metric').style.display = 'block'; document.getElementById('label-nama').innerText = 'Nama'; document.getElementById('data-status').setAttribute('required', 'true'); document.getElementById('data-metric').setAttribute('required', 'true'); const statusSelect = document.getElementById('data-status'); statusSelect.innerHTML = ''; const options = type === 'polyline' ? JALAN_STATUS : PARSIL_STATUS; options.forEach(opt => { let val = opt.value || opt; let label = opt.label || opt; statusSelect.innerHTML += ``; }); if (type === 'polyline') { const lengthKm = turf.length(geojson, {units: 'meters'}); document.getElementById('data-metric').value = lengthKm.toFixed(2); } else { const areaSqMeters = turf.area(geojson); document.getElementById('data-metric').value = areaSqMeters.toFixed(2); } } document.getElementById('modal-form').style.display = 'flex'; }); // Handle Geometry Drag/Vertex Edit Saves async function saveGeomToDB(layer) { if (!layer.dbId) return; const geojson = layer.toGeoJSON(); const payload = { id: layer.dbId, status: layer.dbItem.status, geojson: JSON.stringify(geojson.geometry) }; if (layer.dbType === 'jalan') { payload.nama_jalan = layer.dbItem.nama_jalan; payload.panjang = turf.length(geojson, {units: 'meters'}).toFixed(2); } else { payload.nama_pemilik = layer.dbItem.nama_pemilik; payload.luas = turf.area(geojson).toFixed(2); } await fetch(`api/${layer.dbType}.php`, { method: 'PUT', body: JSON.stringify(payload) }); } map.on(L.Draw.Event.EDITED, function (e) { const layers = e.layers; layers.eachLayer(saveGeomToDB); setTimeout(loadData, 500); }); // Custom Center Drag Handles for Total Shape Movement let dragHandles = []; map.on('draw:editstart', function() { drawnItems.eachLayer(layer => { if (!layer.getBounds) return; // Skip if no bounds let center = layer.getBounds().getCenter(); let handle = L.marker(center, { icon: moveIcon, draggable: true, zIndexOffset: 2000 }).addTo(map); let startLatLng, initialLatLngs; handle.on('dragstart', () => { startLatLng = handle.getLatLng(); initialLatLngs = layer.getLatLngs(); }); handle.on('drag', (e) => { let currentLatLng = handle.getLatLng(); let dLat = currentLatLng.lat - startLatLng.lat; let dLng = currentLatLng.lng - startLatLng.lng; function shift(ll) { if (Array.isArray(ll)) return ll.map(shift); return new L.LatLng(ll.lat + dLat, ll.lng + dLng); } layer.setLatLngs(shift(initialLatLngs)); }); handle.on('dragend', () => { saveGeomToDB(layer); // Instantly save DB on drop }); dragHandles.push(handle); }); }); map.on('draw:editstop', function() { dragHandles.forEach(h => map.removeLayer(h)); dragHandles = []; setTimeout(loadData, 500); }); // Load and Render Logic async function loadData() { drawnItems.clearLayers(); minimapLayer.clearLayers(); let resJalan = await fetch('api/jalan.php'); rawData.jalan = await resJalan.json(); rawData.jalan.forEach(item => renderToMap(item, 'jalan')); let resParsil = await fetch('api/parsil.php'); rawData.parsil = await resParsil.json(); rawData.parsil.forEach(item => renderToMap(item, 'parsil')); let resTitik = await fetch('api/titik.php'); rawData.titik = await resTitik.json(); rawData.titik.forEach(item => renderToMap(item, 'titik')); // Update Dashboard Stats document.getElementById('stat-jalan').innerText = rawData.jalan.length; document.getElementById('stat-parsil').innerText = rawData.parsil.length; document.getElementById('stat-titik').innerText = rawData.titik.length; if (!document.getElementById('view-data').classList.contains('view-hidden')) { renderTable(); } } function renderToMap(item, type) { try { const geom = JSON.parse(item.geojson); let displayStatus = item.status; if (type === 'parsil') { const found = PARSIL_STATUS.find(p => p.value === item.status); if (found) displayStatus = found.label; } let style = {}; if (type === 'jalan') { style = { color: COLORS[item.status] || '#333', weight: 4 }; } else if (type === 'parsil') { style = { color: COLORS[item.status] || '#333', weight: 2, fillColor: COLORS[item.status] || '#333', fillOpacity: 0.5 }; } const layer = L.geoJSON(geom, { style }).getLayers()[0]; // Internal state for DB update on drag layer.dbId = item.id; layer.dbType = type; layer.dbItem = item; // Click to open edit modal layer.on('click', () => { // Ignore click if Leaflet.Draw edit mode is active (pencil toolbar) if (layer.editing && layer.editing.enabled()) return; currentTempLayer = null; document.getElementById('form-type').value = type; document.getElementById('data-id').value = item.id; document.getElementById('data-geojson').value = item.geojson; document.getElementById('modal-title').innerText = type === 'jalan' ? 'Edit Data Jalan' : 'Edit Data Parsil Tanah'; // Toggle Fields if (type === 'titik') { document.getElementById('data-status').parentElement.style.display = 'none'; document.getElementById('field-foto').style.display = 'block'; document.getElementById('field-metric').style.display = 'none'; document.getElementById('label-nama').innerText = 'Keterangan Kerusakan'; document.getElementById('data-status').removeAttribute('required'); document.getElementById('data-metric').removeAttribute('required'); document.getElementById('data-existing-foto').value = item.foto_url; document.getElementById('foto-preview').innerHTML = ``; } else { document.getElementById('data-status').parentElement.style.display = 'block'; document.getElementById('field-foto').style.display = 'none'; document.getElementById('field-metric').style.display = 'block'; document.getElementById('label-nama').innerText = 'Nama'; const statusSelect = document.getElementById('data-status'); statusSelect.innerHTML = ''; const options = type === 'jalan' ? JALAN_STATUS : PARSIL_STATUS; options.forEach(opt => { let val = opt.value || opt; let label = opt.label || opt; statusSelect.innerHTML += ``; }); statusSelect.value = item.status; } document.getElementById('data-nama').value = type === 'titik' ? item.keterangan : (type === 'jalan' ? item.nama_jalan : item.nama_pemilik); if (type !== 'titik') document.getElementById('data-metric').value = type === 'jalan' ? item.panjang : item.luas; document.getElementById('btn-delete').style.display = 'block'; document.getElementById('modal-form').style.display = 'flex'; }); if (type === 'jalan') { layer.bindTooltip(`${item.nama_jalan}
${displayStatus}
${item.panjang} m`); } else if (type === 'parsil') { layer.bindTooltip(`${item.nama_pemilik}
${displayStatus}
${item.luas} m²`); } else { layer.bindTooltip(`
${item.keterangan}

Laporkan: Titik Rusak
`, {direction: 'top'}); } drawnItems.addLayer(layer); } catch (e) { console.error("GeoJSON parsing err", e); } } // Table Rendering function renderTable() { const list = rawData[currentTableTab]; const tbody = document.getElementById('table-body'); const search = document.getElementById('search-input').value.toLowerCase(); tbody.innerHTML = ''; let filteredList = list.filter(item => { const name = currentTableTab === 'titik' ? item.keterangan : (currentTableTab === 'jalan' ? item.nama_jalan : item.nama_pemilik); return name.toLowerCase().includes(search); }); if (filteredList.length === 0) { tbody.innerHTML = `Data kosong atau tidak ditemukan.`; return; } filteredList.forEach((item, index) => { const name = currentTableTab === 'titik' ? item.keterangan : (currentTableTab === 'jalan' ? item.nama_jalan : item.nama_pemilik); const metric = currentTableTab === 'titik' ? `` : (currentTableTab === 'jalan' ? item.panjang : item.luas); const color = currentTableTab === 'titik' ? '#ef4444' : COLORS[item.status]; const bg = currentTableTab === 'titik' ? '#fef2f2' : BG_COLORS[item.status]; let displayStatus = currentTableTab === 'titik' ? 'Titik Rusak' : item.status; if (currentTableTab === 'parsil') { const found = PARSIL_STATUS.find(p => p.value === item.status); if (found) displayStatus = found.label; } const tr = document.createElement('tr'); tr.onclick = () => showOnMinimap(item, currentTableTab); tr.innerHTML = ` ${index + 1} ${name} ${displayStatus} ${metric} `; tbody.appendChild(tr); }); } function showOnMinimap(item, type) { minimapLayer.clearLayers(); try { const geom = JSON.parse(item.geojson); const style = type === 'jalan' ? { color: COLORS[item.status] || '#333', weight: 4 } : { color: COLORS[item.status] || '#333', weight: 2, fillColor: COLORS[item.status] || '#333', fillOpacity: 0.5 }; const layer = L.geoJSON(geom, { style }); minimapLayer.addLayer(layer); minimap.fitBounds(layer.getBounds(), { padding: [20, 20], maxZoom: 17 }); } catch (e) {} } // Form Submission document.getElementById('main-form').addEventListener('submit', async (e) => { e.preventDefault(); const type = document.getElementById('form-type').value; const dataId = document.getElementById('data-id').value; if (type === 'titik') { const fd = new FormData(); if (dataId) fd.append('id', dataId); fd.append('keterangan', document.getElementById('data-nama').value); fd.append('geojson', document.getElementById('data-geojson').value); fd.append('existing_foto', document.getElementById('data-existing-foto').value); const photoFile = document.getElementById('data-foto').files[0]; if (photoFile) fd.append('foto', photoFile); await fetch(`api/titik.php`, { method: 'POST', body: fd }); } else { let payload = { status: document.getElementById('data-status').value, geojson: document.getElementById('data-geojson').value }; if (type === 'jalan') { payload.nama_jalan = document.getElementById('data-nama').value; payload.panjang = document.getElementById('data-metric').value; } else { payload.nama_pemilik = document.getElementById('data-nama').value; payload.luas = document.getElementById('data-metric').value; } const method = dataId ? 'PUT' : 'POST'; if (dataId) payload.id = dataId; await fetch(`api/${type}.php`, { method: method, body: JSON.stringify(payload) }); } closeModal(); loadData(); }); function deleteData(type, id) { if (!confirm('Hapus data ini dari sistem?')) return; fetch(`api/${type}.php`, { method: 'DELETE', body: JSON.stringify({ id }) }).then(() => loadData()); } function deleteFromModal() { const type = document.getElementById('form-type').value; const id = document.getElementById('data-id').value; if (!id) return; deleteData(type, id); closeModal(); } function closeModal() { document.getElementById('modal-form').style.display = 'none'; document.getElementById('main-form').reset(); document.getElementById('data-id').value = ''; document.getElementById('btn-delete').style.display = 'none'; if (currentTempLayer) { map.removeLayer(currentTempLayer); currentTempLayer = null; } } // EXIF Auto Extractor document.getElementById('auto-upload-exif').addEventListener('change', function(e) { const file = e.target.files[0]; if (!file) return; EXIF.getData(file, function() { const latRef = EXIF.getTag(this, "GPSLatitudeRef"); const lat = EXIF.getTag(this, "GPSLatitude"); const lngRef = EXIF.getTag(this, "GPSLongitudeRef"); const lng = EXIF.getTag(this, "GPSLongitude"); if (lat && lng && latRef && lngRef) { const getDec = (rational) => rational.numerator / rational.denominator; const decimalLat = (getDec(lat[0]) + getDec(lat[1])/60 + getDec(lat[2])/3600) * (latRef === "S" ? -1 : 1); const decimalLng = (getDec(lng[0]) + getDec(lng[1])/60 + getDec(lng[2])/3600) * (lngRef === "W" ? -1 : 1); const geojsonPoint = { type: "Point", coordinates: [decimalLng, decimalLat] }; currentTempLayer = L.marker([decimalLat, decimalLng]); drawnItems.addLayer(currentTempLayer); map.flyTo([decimalLat, decimalLng], 18); document.getElementById('form-type').value = 'titik'; document.getElementById('data-geojson').value = JSON.stringify(geojsonPoint); document.getElementById('data-id').value = ''; document.getElementById('data-nama').value = ''; document.getElementById('modal-title').innerText = 'Laporan Kerusakan via GPS Kamera'; document.getElementById('data-status').parentElement.style.display = 'none'; document.getElementById('field-foto').style.display = 'block'; document.getElementById('field-metric').style.display = 'none'; document.getElementById('label-nama').innerText = 'Keterangan Kerusakan'; document.getElementById('data-status').removeAttribute('required'); document.getElementById('data-metric').removeAttribute('required'); // Transfer file silently to form const dt = new DataTransfer(); dt.items.add(file); document.getElementById('data-foto').files = dt.files; const reader = new FileReader(); reader.onload = function(evt) { document.getElementById('foto-preview').innerHTML = ``; } reader.readAsDataURL(file); document.getElementById('modal-form').style.display = 'flex'; } else { alert('Gagal mendeteksi koordinat: Gambar ini tidak memiliki meta GPS (EXIF). Silakan masukan Pin secara manual di peta.'); document.getElementById('auto-upload-exif').value = ''; } }); }); // Init loadData();