Files
WebGIS_Jalan-JalanRusak/assets/js/app.js
T
2026-06-11 02:42:37 +07:00

506 lines
21 KiB
JavaScript

// 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: '<div style="background:white; border:2px solid #333; border-radius:50%; width:24px; height:24px; display:flex; align-items:center; justify-content:center; cursor:grab; box-shadow:0 2px 4px rgba(0,0,0,0.3);"><i class="fa-solid fa-arrows-up-down-left-right" style="color:#333; font-size:12px;"></i></div>',
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 += `<option value="${val}">${label}</option>`;
});
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 = `<img src="${item.foto_url}" style="max-height:150px; border-radius:5px;">`;
} 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 += `<option value="${val}">${label}</option>`;
});
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(`<b>${item.nama_jalan}</b><br>${displayStatus}<br>${item.panjang} m`);
} else if (type === 'parsil') {
layer.bindTooltip(`<b>${item.nama_pemilik}</b><br>${displayStatus}<br>${item.luas} m²`);
} else {
layer.bindTooltip(`<div style="text-align:center;"><b>${item.keterangan}</b><br><img src="${item.foto_url}" style="width:120px; height:auto; margin-top:5px; border-radius:5px;"><br>Laporkan: Titik Rusak</div>`, {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 = `<tr><td colspan="5" style="text-align:center; color:#9ca3af;">Data kosong atau tidak ditemukan.</td></tr>`;
return;
}
filteredList.forEach((item, index) => {
const name = currentTableTab === 'titik' ? item.keterangan : (currentTableTab === 'jalan' ? item.nama_jalan : item.nama_pemilik);
const metric = currentTableTab === 'titik' ? `<img src="${item.foto_url}" style="height:30px; border-radius:3px;">` : (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 = `
<td>${index + 1}</td>
<td style="font-weight:500;">${name}</td>
<td><span class="status-badge" style="color:${color}; background:${bg};">${displayStatus}</span></td>
<td>${metric}</td>
<td>
<button class="btn-sm-del" onclick="event.stopPropagation(); deleteData('${currentTableTab}', ${item.id})">
<i class="fa-solid fa-trash"></i> Hapus
</button>
</td>
`;
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 = `<img src="${evt.target.result}" style="max-height:150px; border-radius:5px;">`;
}
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();