// modules/jalan.js — Logika Pertemuan 2: Manajemen Data Jalan (Polyline) (function () { const layerGroup = L.layerGroup(); let allLayers = {}; // { id: polylineObject } let active = false; // ── Warna per status jalan ──────────────────────── const JALAN_COLORS = { 'Nasional': '#ef4444', 'Provinsi': '#f97316', 'Kabupaten': '#eab308' }; function getColor(status) { return JALAN_COLORS[status] || '#888'; } // ── Render polyline ─────────────────────────────── function addPolylineToMap(row) { let coords; try { const gj = typeof row.geojson === 'string' ? JSON.parse(row.geojson) : row.geojson; // GeoJSON coordinates: [lng, lat] → Leaflet: [lat, lng] coords = gj.coordinates.map(c => [c[1], c[0]]); } catch (e) { console.error('GeoJSON parse error (jalan id=' + row.id + ')', e); return; } const poly = L.polyline(coords, { color: getColor(row.status_jalan), weight: 5, opacity: 0.85, pane: 'jalanPane' }); poly._jalanId = row.id; poly._jalanNama = row.nama_jalan; poly._jalanStatus = row.status_jalan; allLayers[row.id] = poly; poly.bindPopup(buildInfoPopup(row), { maxWidth: 300 }); layerGroup.addLayer(poly); } function buildInfoPopup(row) { const color = getColor(row.status_jalan); const panjang = row.panjang_meter ? parseFloat(row.panjang_meter) >= 1000 ? (parseFloat(row.panjang_meter) / 1000).toFixed(2) + ' km' : parseFloat(row.panjang_meter).toFixed(1) + ' m' : '—'; return `
🛣️
${escapeHTML(row.nama_jalan)}
#${row.id}
🏷️
Status Jalan
Jalan ${escapeHTML(row.status_jalan)}
📏
Panjang Jalan
${panjang}
`; } // ── Load data ─────────────────────────────────────────────────── function loadData() { layerGroup.clearLayers(); allLayers = {}; fetch('api/jalan/ambil.php?_=' + Date.now(), { cache: 'no-store' }) .then(r => r.json()) .then(j => { if (j.status !== 'success') throw new Error(j.message); updateCount(j.total, '🛣️'); j.data.forEach(addPolylineToMap); refreshJalanList(j.data); }) .catch(err => { showToast('Gagal memuat data jalan.', 'error'); console.error(err); }); } // ── Refresh panel list Data Jalan ────────────────────────────────── function refreshJalanList(data) { if (typeof window.dlRefreshList !== 'function') return; const COLORS = { 'Nasional': '#ef4444', 'Provinsi': '#f97316', 'Kabupaten': '#eab308' }; const items = (data || []).map(row => ({ id: row.id, name: row.nama_jalan, badge: row.status_jalan, badgeColor: COLORS[row.status_jalan] || '#888', dotColor: COLORS[row.status_jalan] || '#888' })); window.dlRefreshList('Jalan', items); } // ── Hapus jalan ─────────────────────────────────── window._jalanHapus = function (id, btnEl) { showDeleteConfirm('Yakin ingin menghapus data jalan ini?').then(confirmed => { if (!confirmed) return; btnEl.disabled = true; btnEl.textContent = '⏳ Menghapus...'; const fd = new FormData(); fd.append('id', id); fetch('api/jalan/hapus.php', { method: 'POST', body: fd }) .then(r => r.json()) .then(j => { if (j.status === 'success') { map.closePopup(); if (allLayers[id]) { layerGroup.removeLayer(allLayers[id]); delete allLayers[id]; } updateCount(Object.keys(allLayers).length, '🛣️'); const COLORS = { 'Nasional': '#ef4444', 'Provinsi': '#f97316', 'Kabupaten': '#eab308' }; const remaining = Object.values(allLayers).map(l => ({ id: l._jalanId, name: l._jalanNama, badge: l._jalanStatus, badgeColor: COLORS[l._jalanStatus] || '#888', dotColor: COLORS[l._jalanStatus] || '#888' })); if (typeof window.dlRefreshList === 'function') window.dlRefreshList('Jalan', remaining); showToast('Data jalan berhasil dihapus.'); } else throw new Error(j.message); }) .catch(err => { showToast(err.message || 'Gagal menghapus.', 'error'); btnEl.disabled = false; btnEl.textContent = '🗑 Hapus Jalan'; }); }); }; // ── Drawing: gambar polyline baru ──────────────── let drawingPoints = []; let drawingLayer = null; // polyline permanen per point click let liveLineLayer = null; // polyline preview kursor ke ujung terakhir let liveTooltip = null; // tooltip info ukuran let drawingMarkers = []; // titik-titik sementara let isDrawing = false; let lastClickAt = 0; function startDrawing() { isDrawing = true; drawingPoints = []; lastClickAt = 0; document.getElementById('drawHint').classList.add('visible'); map.doubleClickZoom.disable(); } function stopDrawing(save) { isDrawing = false; document.getElementById('drawHint').classList.remove('visible'); map.doubleClickZoom.enable(); // Bersihkan preview if (drawingLayer) { map.removeLayer(drawingLayer); drawingLayer = null; } if (liveLineLayer) { map.removeLayer(liveLineLayer); liveLineLayer = null; } if (liveTooltip) { map.removeLayer(liveTooltip); liveTooltip = null; } drawingMarkers.forEach(m => map.removeLayer(m)); drawingMarkers = []; if (save && drawingPoints.length >= 2) { showFormJalan(drawingPoints.slice()); } drawingPoints = []; if (typeof window._resetActiveTool === 'function') window._resetActiveTool(); } function finishDrawing() { if (!isDrawing) return; if (drawingPoints.length >= 2) { stopDrawing(true); } else { showToast('Minimal 2 titik untuk membuat jalan (Polyline)', 'error'); } } function onMapClick(e) { if (currentMode !== 2 || currentSubMode !== 'jalan') return; if (!isDrawing) return; const now = Date.now(); // Deteksi double-click: klik kedua dalam 350ms → finalisasi tanpa menambah titik if (now - lastClickAt < 350) { lastClickAt = 0; finishDrawing(); return; } lastClickAt = now; const latlng = e.latlng; drawingPoints.push(latlng); // Titik visual const m = L.circleMarker(latlng, { radius: 5, color: '#388bfd', fillColor: '#388bfd', fillOpacity: 1, pane: 'drawPane' }); m.addTo(map); drawingMarkers.push(m); // Preview polyline if (drawingLayer) map.removeLayer(drawingLayer); if (drawingPoints.length >= 2) { drawingLayer = L.polyline(drawingPoints, { color: '#388bfd', weight: 4, opacity: .9, pane: 'drawPane' }).addTo(map); } } function onMapDoubleClick(e) { if (currentMode !== 2 || currentSubMode !== 'jalan' || !isDrawing) return; if (e.originalEvent) L.DomEvent.stop(e.originalEvent); finishDrawing(); } function onMouseMove(e) { if (currentMode !== 2 || currentSubMode !== 'jalan' || !isDrawing) return; if (drawingPoints.length === 0) { if (liveTooltip) { map.removeLayer(liveTooltip); liveTooltip = null; } return; } const lastPoint = drawingPoints[drawingPoints.length - 1]; const currentPoint = e.latlng; // Gambar garis tipis (preview) dari titik terakhir ke cursor if (liveLineLayer) map.removeLayer(liveLineLayer); liveLineLayer = L.polyline([lastPoint, currentPoint], { color: '#79c0ff', weight: 2, dashArray: '5,6', opacity: 0.6, pane: 'drawPane' }).addTo(map); // Hitung total panjang sampai titik ini let totalMeter = 0; for (let i = 0; i < drawingPoints.length - 1; i++) { totalMeter += drawingPoints[i].distanceTo(drawingPoints[i + 1]); } totalMeter += lastPoint.distanceTo(currentPoint); const txt = (totalMeter >= 1000) ? (totalMeter / 1000).toFixed(2) + ' km' : totalMeter.toFixed(1) + ' m'; // Tampilkan tooltip berjalan if (!liveTooltip) { liveTooltip = L.tooltip({ permanent: true, className: 'live-tooltip', direction: 'auto', offset: [15, 15], pane: 'drawPane' }) .setLatLng(currentPoint) .setContent('➕ Lanjut · Enter / 2×klik selesai
Panjang: ' + txt) .addTo(map); } else { liveTooltip.setLatLng(currentPoint); liveTooltip.setContent('➕ Lanjut · Enter / 2×klik selesai
Panjang: ' + txt); } } function onKeyDown(e) { if (currentMode !== 2 || currentSubMode !== 'jalan' || !isDrawing) return; if (e.key === 'Enter' || e.keyCode === 13) { e.preventDefault(); finishDrawing(); } if (e.key === 'Escape') stopDrawing(false); } // ── Form popup setelah gambar selesai ───────────── function showFormJalan(points) { // Hitung panjang via Leaflet let totalMeter = 0; for (let i = 0; i < points.length - 1; i++) { totalMeter += points[i].distanceTo(points[i + 1]); } totalMeter = totalMeter.toFixed(2); // Build GeoJSON LineString const geojson = JSON.stringify({ type: 'LineString', coordinates: points.map(p => [p.lng, p.lat]) }); // Popup di titik tengah const midIdx = Math.floor(points.length / 2); const midPt = points[midIdx]; L.popup({ maxWidth: 340, closeOnClick: false, closeButton: true }) .setLatLng(midPt) .setContent(`
🛣️
Tambah Data Jalan
${points.length} titik · ${totalMeter} m
${totalMeter} meter
`) .openOn(map); setTimeout(() => { const el = document.getElementById('fj_nama'); if (el) el.focus(); }, 200); // Kalau popup ditutup tanpa simpan: hapus preview map.once('popupclose', () => { // preview sudah di-clear di stopDrawing }); } // ── Simpan jalan ────────────────────────────────── window._jalanSimpan = function () { const nama = document.getElementById('fj_nama').value.trim(); const status = document.getElementById('fj_status').value; const geojson = document.getElementById('fj_geojson').value; const panjang = document.getElementById('fj_panjang').value; const statusEl = document.getElementById('jalanStatus'); const btn = document.getElementById('btnSimpanJalan'); if (!nama) { statusEl.textContent = '⚠ Nama jalan wajib diisi.'; statusEl.className = 'form-status error'; document.getElementById('fj_nama').focus(); return; } btn.disabled = true; btn.textContent = '⏳ Menyimpan...'; const fd = new FormData(); fd.append('nama_jalan', nama); fd.append('status_jalan', status); fd.append('geojson', geojson); fd.append('panjang_meter', panjang); fetch('api/jalan/simpan.php', { method: 'POST', body: fd }) .then(r => r.json()) .then(j => { if (j.status === 'success') { map.closePopup(); addPolylineToMap(j.data); updateCount(Object.keys(allLayers).length, '🛣️'); const COLORS = { 'Nasional': '#ef4444', 'Provinsi': '#f97316', 'Kabupaten': '#eab308' }; const allItems = Object.values(allLayers).map(l => ({ id: l._jalanId, name: l._jalanNama, badge: l._jalanStatus, badgeColor: COLORS[l._jalanStatus] || '#888', dotColor: COLORS[l._jalanStatus] || '#888' })); if (typeof window.dlRefreshList === 'function') window.dlRefreshList('Jalan', allItems); showToast(`"${escapeHTML(nama)}" berhasil disimpan!`); } else throw new Error(j.message); }) .catch(err => { statusEl.textContent = '✕ ' + err.message; statusEl.className = 'form-status error'; btn.disabled = false; btn.textContent = '💾 Simpan Jalan'; }); }; // ── Init / cleanup ─────────────────────────────────────────── window.initJalan = function () { if (!active) { active = true; map.on('click', onMapClick); map.on('dblclick', onMapDoubleClick); map.on('mousemove', onMouseMove); document.addEventListener('keydown', onKeyDown); } // Daftarkan fungsi focus untuk panel list window._dlFocusFns['Jalan'] = function(id) { const layer = allLayers[id]; if (!layer) return; map.fitBounds(layer.getBounds(), { padding: [40, 40], animate: true }); setTimeout(() => layer.openPopup(layer.getBounds().getCenter()), 400); }; layerGroup.addTo(map); loadData(); }; // Ekspos ke global agar bisa dipanggil dari index.php window._jalanStartDraw = startDrawing; window._jalanFinishDraw = finishDrawing; window._jalanStopDraw = function() { stopDrawing(false); }; // Filter Toggle window._jalanToggleLayer = function(visible) { if (visible) { map.addLayer(layerGroup); } else { map.removeLayer(layerGroup); stopDrawing(false); // Batalkan mode draw jika layer disembunyikan } }; })();