fitur penanda jalan rusak

This commit is contained in:
Farha
2026-04-14 10:52:05 +07:00
parent eee1e89cbc
commit aa3fe296c1
6 changed files with 480 additions and 8 deletions
+297 -2
View File
@@ -83,6 +83,9 @@
<button class="icon-btn" id="modePolygonBtn" title="Tambah polygon parsil">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l8 5v10l-8 5-8-5V7z"/></svg>
</button>
<button class="icon-btn" id="modeRoadDamageBtn" title="Lapor jalan rusak">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 12h12M12 6v12M2 12h20M12 2v20"/><circle cx="12" cy="12" r="10"/></svg>
</button>
<button class="icon-btn" id="modeChoroplethBtn" title="Lihat peta choropleth kecamatan">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6l6-2 6 2 6-2v14l-6 2-6-2-6 2z"/><path d="M9 4v14"/><path d="M15 6v14"/></svg>
</button>
@@ -265,6 +268,58 @@
</div>
</div>
<!-- Modal Lapor Jalan Rusak -->
<div id="roadDamageModal" class="fixed inset-0 z-[10002] hidden items-center justify-center bg-slate-900/50 px-4">
<div class="w-full max-w-lg rounded-2xl bg-white p-5 shadow-2xl">
<div class="flex items-start justify-between gap-4 border-b pb-3">
<div>
<h3 class="text-lg font-bold text-slate-800">Lapor Jalan Rusak</h3>
<p class="text-sm text-slate-500">Dokumentasikan kondisi jalan dengan foto untuk membantu perbaikan.</p>
</div>
<button id="closeRoadDamageModalBtn" class="rounded-lg bg-slate-100 px-3 py-2 text-sm font-bold text-slate-700 hover:bg-slate-200">Tutup</button>
</div>
<div class="mt-4 grid gap-3">
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Jenis Kerusakan</label>
<select id="roadDamageType" class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-red-500">
<option value="">Pilih jenis kerusakan</option>
<option value="lubang">Lubang</option>
<option value="retak">Retak</option>
<option value="ambles">Ambles</option>
<option value="bergelombang">Bergelombang</option>
<option value="lainnya">Lainnya</option>
</select>
</div>
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Tingkat Kerusakan</label>
<select id="roadDamageSeverity" class="w-full rounded-lg border border-slate-300 bg-white p-2 text-sm outline-none focus:ring-2 focus:ring-red-500">
<option value="ringan">Ringan</option>
<option value="sedang" selected>Sedang</option>
<option value="berat">Berat</option>
</select>
</div>
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Deskripsi</label>
<textarea id="roadDamageDesc" placeholder="Jelaskan kondisi jalan, lokasi spesifik, dll..." class="w-full rounded-lg border border-slate-300 p-2 text-sm outline-none focus:ring-2 focus:ring-red-500" rows="3"></textarea>
</div>
<div>
<label class="mb-1 block text-[10px] font-bold uppercase tracking-widest text-slate-400">Foto Kondisi (Opsional)</label>
<input id="roadDamageImage" type="file" accept="image/*" class="w-full rounded-lg border border-slate-300 p-2 text-sm">
<div id="imagePreview" class="mt-2 hidden">
<img id="previewImg" src="" alt="Preview" class="max-h-40 rounded-lg">
</div>
</div>
<div class="text-[10px] text-slate-500 bg-slate-50 p-2 rounded">
📍 Koordinat: <span id="roadDamageCoords">-</span>
</div>
</div>
<div class="mt-5 flex flex-wrap gap-2 border-t pt-4">
<button id="saveRoadDamageBtn" class="rounded-lg bg-red-600 px-4 py-2 text-sm font-bold text-white hover:bg-red-700">Simpan Laporan</button>
<button id="cancelRoadDamageBtn" class="rounded-lg bg-slate-100 px-4 py-2 text-sm font-bold text-slate-700 hover:bg-slate-200">Batal</button>
</div>
</div>
</div>
<select id="featureType" class="hidden">
<option value="polyline">polyline</option>
<option value="polygon">polygon</option>
@@ -299,6 +354,9 @@
let featureRecords = [];
let editingFeatureId = null;
let choroplethLayer = null;
let roadDamageLayer = L.featureGroup().addTo(map);
let roadDamageRecords = [];
let pendingRoadDamageCoords = null;
const FEATURE_STATUS_OPTIONS = {
polyline: [
@@ -316,6 +374,40 @@
const CHOROPLETH_GEOJSON_PATH = 'data/pontianak-kecamatan.geojson';
let choroplethGeoJsonData = null;
const DISTRICT_POPULATION_MAP = {
'pontianak barat': 148800,
'pontianak kota': 98500,
'pontianak selatan': 133600,
'pontianak tenggara': 52900,
'pontianak timur': 121500,
'pontianak utara': 129200
};
function normalizeDistrictName(name) {
return String(name || '')
.toLowerCase()
.replace(/\s+/g, ' ')
.trim();
}
function enrichFeaturePopulation(feature) {
const properties = feature.properties || {};
const districtName = properties.name || properties.NAME || properties.Ket || '';
const districtKey = normalizeDistrictName(districtName);
const mappedPopulation = DISTRICT_POPULATION_MAP[districtKey];
if (mappedPopulation == null) {
return feature;
}
return {
...feature,
properties: {
...properties,
population: Number(mappedPopulation)
}
};
}
function getPopulationFill(population) {
if (population > 120000) return '#ef4444';
@@ -418,7 +510,9 @@
function sanitizeChoroplethGeoJson(data) {
return {
...data,
features: data.features.map(sanitizeChoroplethFeature)
features: data.features
.map(sanitizeChoroplethFeature)
.map(enrichFeaturePopulation)
};
}
@@ -765,7 +859,8 @@
point: document.getElementById('modePointBtn'),
polyline: document.getElementById('modePolylineBtn'),
polygon: document.getElementById('modePolygonBtn'),
choropleth: document.getElementById('modeChoroplethBtn')
choropleth: document.getElementById('modeChoroplethBtn'),
roadDamage: document.getElementById('modeRoadDamageBtn')
};
Object.values(modeButtons).forEach(button => button.classList.remove('active'));
@@ -797,6 +892,18 @@
return;
}
if (mode === 'roadDamage') {
hint.innerText = 'Mode laporan jalan rusak aktif: klik peta untuk menandai lokasi kerusakan.';
spatialControls.classList.add('hidden');
runActionBtn.innerText = 'Klik Peta';
runActionBtn.disabled = true;
clearDraftBtn.disabled = true;
updateDrawControl(null);
document.getElementById('modeMeasure').value = '';
loadRoadDamage(true);
return;
}
runActionBtn.disabled = false;
clearDraftBtn.disabled = false;
@@ -818,6 +925,163 @@
setSpatialType(mode);
}
function openRoadDamageModal(lat, lng) {
pendingRoadDamageCoords = { lat, lng };
document.getElementById('roadDamageCoords').innerText = `${lat.toFixed(5)}, ${lng.toFixed(5)}`;
document.getElementById('roadDamageType').value = '';
document.getElementById('roadDamageSeverity').value = 'sedang';
document.getElementById('roadDamageDesc').value = '';
document.getElementById('roadDamageImage').value = '';
document.getElementById('imagePreview').classList.add('hidden');
const modal = document.getElementById('roadDamageModal');
modal.classList.remove('hidden');
modal.classList.add('flex');
suppressAutoReload = true;
}
function closeRoadDamageModal() {
const modal = document.getElementById('roadDamageModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
pendingRoadDamageCoords = null;
suppressAutoReload = !!document.querySelector('[id^="edit-mode-"]:not(.hidden)') || !!editingFeatureId;
}
function saveRoadDamage() {
if (!pendingRoadDamageCoords) {
showMessage('Koordinat tidak ditemukan', 'bg-red-600 text-white');
return;
}
const jenis = document.getElementById('roadDamageType').value;
if (!jenis) {
showMessage('Pilih jenis kerusakan terlebih dahulu', 'bg-red-600 text-white');
return;
}
const severity = document.getElementById('roadDamageSeverity').value;
const deskripsi = document.getElementById('roadDamageDesc').value;
const fileInput = document.getElementById('roadDamageImage');
const formData = new FormData();
formData.append('latitude', pendingRoadDamageCoords.lat);
formData.append('longitude', pendingRoadDamageCoords.lng);
formData.append('jenis_kerusakan', jenis);
formData.append('severity', severity);
formData.append('deskripsi', deskripsi);
if (fileInput.files && fileInput.files[0]) {
formData.append('gambar', fileInput.files[0]);
}
fetch('save_jalan_rusak.php', { method: 'POST', body: formData })
.then(res => parseJsonResponse(res, 'save_jalan_rusak.php'))
.then(data => {
if (data.status === 'success') {
showMessage('✅ Laporan jalan rusak berhasil disimpan', 'bg-green-600 text-white');
closeRoadDamageModal();
loadRoadDamage(true);
} else {
throw new Error(data.message || 'Gagal menyimpan laporan');
}
})
.catch(err => showMessage(err.message || 'Gagal menyimpan laporan', 'bg-red-600 text-white'));
}
function createRoadDamageIcon(severity) {
const colorMap = {
'ringan': '#fbbf24',
'sedang': '#f97316',
'berat': '#dc2626'
};
const color = colorMap[severity] || '#f97316';
return L.divIcon({
className: "custom-marker",
html: `<svg width="32" height="40" viewBox="0 0 32 40" fill="${color}" xmlns="http://www.w3.org/2000/svg">
<path d="M16 0C8.27 0 2 6.27 2 14c0 8 14 26 14 26s14-18 14-26c0-7.73-6.27-14-14-14z"/>
<circle cx="16" cy="14" r="5" fill="white"/>
<text x="16" y="16" text-anchor="middle" dy=".3em" font-size="10" font-weight="bold" fill="${color}">!</text>
</svg>`,
iconSize: [32, 40],
iconAnchor: [16, 40],
popupAnchor: [0, -40]
});
}
function getRoadDamagePopup(record) {
const imageHtml = record.gambar ? `<img src="${record.gambar}" alt="Foto jalan rusak" class="mt-2 w-full rounded-lg max-h-48 object-cover">` : '';
return `
<div class="w-full p-3">
<h4 class="font-bold text-slate-800">Laporan Jalan Rusak</h4>
<p class="mt-1 text-sm text-slate-600"><strong>Jenis:</strong> ${record.jenis_kerusakan}</p>
<p class="text-sm text-slate-600"><strong>Tingkat:</strong>
<span class="px-2 py-1 rounded text-xs font-bold ${
record.severity === 'ringan' ? 'bg-yellow-100 text-yellow-700' :
record.severity === 'sedang' ? 'bg-orange-100 text-orange-700' :
'bg-red-100 text-red-700'
}">${record.severity}</span>
</p>
${record.deskripsi ? `<p class="mt-1 text-sm text-slate-600"><strong>Deskripsi:</strong> ${record.deskripsi}</p>` : ''}
<p class="mt-1 text-xs text-slate-500">📍 ${record.latitude.toFixed(5)}, ${record.longitude.toFixed(5)}</p>
${imageHtml}
<div class="mt-3 flex gap-2 border-t pt-2">
<button onclick="deleteRoadDamage(${record.id})" class="flex-1 rounded bg-red-50 px-2 py-1 text-xs font-bold text-red-700 hover:bg-red-100">Hapus</button>
</div>
</div>
`;
}
function loadRoadDamage(force = false) {
if (!force && (document.hidden || suppressAutoReload)) return;
fetch('get_jalan_rusak.php')
.then(res => {
if (!res.ok) throw new Error('HTTP Error: ' + res.status);
return res.text();
})
.then(text => {
const data = JSON.parse(text);
if (!Array.isArray(data)) {
throw new Error('Format data tidak valid');
}
roadDamageRecords = data;
roadDamageLayer.clearLayers();
data.forEach(record => {
const marker = L.marker([record.latitude, record.longitude], {
icon: createRoadDamageIcon(record.severity)
}).addTo(roadDamageLayer);
marker.bindPopup(getRoadDamagePopup(record));
});
})
.catch(err => {
console.error('Gagal memuat data jalan rusak:', err);
});
}
function deleteRoadDamage(id) {
if (!confirm('Hapus laporan jalan rusak ini?')) return;
const formData = new FormData();
formData.append('id', id);
fetch('delete_jalan_rusak.php', { method: 'POST', body: formData })
.then(res => parseJsonResponse(res, 'delete_jalan_rusak.php'))
.then(data => {
if (data.status === 'success') {
showMessage('🗑️ Laporan berhasil dihapus', 'bg-orange-600 text-white');
loadRoadDamage(true);
} else {
throw new Error(data.message || 'Gagal menghapus laporan');
}
})
.catch(err => showMessage(err.message || 'Gagal menghapus laporan', 'bg-red-600 text-white'));
}
function updateFeatureMeasurePreview() {
const type = document.getElementById('featureType').value;
const layer = pendingShape;
@@ -1646,6 +1910,12 @@
// Klik Peta untuk Input Baru
map.on('click', function(e) {
if (isFeatureDrawing) return;
if (appMode === 'roadDamage') {
openRoadDamageModal(e.latlng.lat, e.latlng.lng);
return;
}
if (appMode !== 'point') return;
const lat = e.latlng.lat;
@@ -1754,6 +2024,7 @@
document.getElementById('modePolylineBtn').addEventListener('click', () => setDataMode('polyline'));
document.getElementById('modePolygonBtn').addEventListener('click', () => setDataMode('polygon'));
document.getElementById('modeChoroplethBtn').addEventListener('click', () => setDataMode('choropleth'));
document.getElementById('modeRoadDamageBtn').addEventListener('click', () => setDataMode('roadDamage'));
document.getElementById('drawStatus').addEventListener('change', () => {
const type = document.getElementById('featureType').value;
document.getElementById('featureStatus').value = document.getElementById('drawStatus').value;
@@ -1831,6 +2102,30 @@
document.getElementById('newFeatureModal').addEventListener('click', (event) => {
if (event.target.id === 'newFeatureModal') closeNewFeatureModal(true);
});
// Road Damage Modal Event Listeners
document.getElementById('saveRoadDamageBtn').addEventListener('click', saveRoadDamage);
document.getElementById('cancelRoadDamageBtn').addEventListener('click', closeRoadDamageModal);
document.getElementById('closeRoadDamageModalBtn').addEventListener('click', closeRoadDamageModal);
document.getElementById('roadDamageImage').addEventListener('change', (event) => {
const file = event.target.files[0];
const preview = document.getElementById('imagePreview');
const previewImg = document.getElementById('previewImg');
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
previewImg.src = e.target.result;
preview.classList.remove('hidden');
};
reader.readAsDataURL(file);
} else {
preview.classList.add('hidden');
}
});
document.getElementById('roadDamageModal').addEventListener('click', (event) => {
if (event.target.id === 'roadDamageModal') closeRoadDamageModal();
});
</script>
</body>
</html>