Files
C_UAS_WEBGIS_AndrieWijaya_D…/01/modules/jalan.js
T
2026-06-11 18:14:21 +07:00

433 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 `
<div class="info-popup">
<div class="info-popup-header">
<div class="info-popup-icon" style="background:${color}22;">🛣️</div>
<div>
<div class="info-popup-name">${escapeHTML(row.nama_jalan)}</div>
<div class="info-popup-id">#${row.id}</div>
</div>
</div>
<div class="info-row">
<div class="info-row-icon">🏷️</div>
<div>
<div class="info-row-label">Status Jalan</div>
<div class="info-row-value">
<span class="status-badge-inline"
style="background:${color}22;color:${color};border:1px solid ${color}55;">
Jalan ${escapeHTML(row.status_jalan)}
</span>
</div>
</div>
</div>
<div class="info-row">
<div class="info-row-icon">📏</div>
<div>
<div class="info-row-label">Panjang Jalan</div>
<div class="info-row-value" style="font-family:var(--font-mono);">${panjang}</div>
</div>
</div>
<button class="btn-hapus" onclick="window._jalanHapus(${row.id}, this)">🗑 Hapus Jalan</button>
</div>
`;
}
// ── 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('<b> Lanjut</b> · <kbd>Enter</kbd> / 2×klik selesai<br>Panjang: ' + txt)
.addTo(map);
} else {
liveTooltip.setLatLng(currentPoint);
liveTooltip.setContent('<b> Lanjut</b> · <kbd>Enter</kbd> / 2×klik selesai<br>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(`
<div class="form-popup">
<div class="form-popup-header">
<div class="form-popup-icon blue">🛣️</div>
<div>
<div class="form-popup-title">Tambah Data Jalan</div>
<div class="form-popup-coords">${points.length} titik · ${totalMeter} m</div>
</div>
</div>
<div class="form-group">
<label>Nama Jalan *</label>
<input type="text" id="fj_nama" placeholder="cth. Jl. Ahmad Yani" maxlength="100" autocomplete="off">
</div>
<div class="form-group">
<label>Status Jalan *</label>
<select id="fj_status">
<option value="Nasional">Jalan Nasional</option>
<option value="Provinsi">Jalan Provinsi</option>
<option value="Kabupaten" selected>Jalan Kabupaten</option>
</select>
</div>
<div class="form-group">
<label>Panjang Jalan (otomatis)</label>
<div class="input-readonly">${totalMeter} meter</div>
</div>
<input type="hidden" id="fj_geojson" value='${escapeHTML(geojson)}'>
<input type="hidden" id="fj_panjang" value="${totalMeter}">
<button class="btn-save" id="btnSimpanJalan" onclick="window._jalanSimpan()">💾 Simpan Jalan</button>
<div class="form-status" id="jalanStatus"></div>
</div>
`)
.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
}
};
})();