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

460 lines
19 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/parsil.js — Logika Pertemuan 2: Manajemen Parsil Tanah (Polygon)
(function () {
const layerGroup = L.layerGroup();
let allLayers = {}; // { id: polygonObject }
let active = false;
// ── Warna per status kepemilikan ──────────────────
const PARSIL_COLORS = {
'SHM': '#3b82f6',
'HGB': '#8b5cf6',
'HGU': '#ec4899',
'HP': '#14b8a6'
};
function getColor(status) {
return PARSIL_COLORS[status] || '#888';
}
// ── Hitung Luas Polygon (Spherical Area) ──────────
function calculatePolygonArea(latLngs) {
let radius = 6378137; // Earth's radius in meters
let area = 0;
let d2r = Math.PI / 180;
if (latLngs.length > 2) {
for (let i = 0; i < latLngs.length; i++) {
let p1 = latLngs[i];
let p2 = latLngs[(i + 1) % latLngs.length];
area += ((p2.lng - p1.lng) * d2r) * (2 + Math.sin(p1.lat * d2r) + Math.sin(p2.lat * d2r));
}
area = area * radius * radius / 2.0;
}
return Math.abs(area);
}
// ── Render polygon ────────────────────────────────
function addPolygonToMap(row) {
let coords;
try {
const gj = typeof row.geojson === 'string' ? JSON.parse(row.geojson) : row.geojson;
// GeoJSON Polygon coordinates are MultiArray: [[[lng, lat], [lng, lat]...]]
// We just need the exterior ring [0] maps to latlng
let extRing = gj.coordinates[0];
coords = extRing.map(c => [c[1], c[0]]);
} catch (e) {
console.error('GeoJSON parse error (parsil id=' + row.id + ')', e);
return;
}
const poly = L.polygon(coords, {
color: getColor(row.status_kepemilikan),
fillColor: getColor(row.status_kepemilikan),
weight: 2,
fillOpacity: 0.4,
pane: 'parsilPane'
});
poly._parsilId = row.id;
poly._parsilNama = row.nama_parsil;
poly._parsilStatus = row.status_kepemilikan;
allLayers[row.id] = poly;
poly.bindPopup(buildInfoPopup(row), { maxWidth: 300 });
layerGroup.addLayer(poly);
}
function buildInfoPopup(row) {
const color = getColor(row.status_kepemilikan);
const luas = row.luas_m2
? parseFloat(row.luas_m2).toLocaleString('id-ID', { minimumFractionDigits: 1, maximumFractionDigits: 2 }) + ' 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_parsil)}</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 Kepemilikan</div>
<div class="info-row-value">
<span class="status-badge-inline"
style="background:${color}22;color:${color};border:1px solid ${color}55;">
${escapeHTML(row.status_kepemilikan)}
</span>
</div>
</div>
</div>
<div class="info-row">
<div class="info-row-icon">📐</div>
<div>
<div class="info-row-label">Luas Tanah</div>
<div class="info-row-value" style="font-family:var(--font-mono);">${luas}</div>
</div>
</div>
<button class="btn-hapus" onclick="window._parsilHapus(${row.id}, this)">🗑 Hapus Parsil</button>
</div>
`;
}
// ── Load data ───────────────────────────────────────────────────
function loadData() {
layerGroup.clearLayers();
allLayers = {};
fetch('api/parsil/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(addPolygonToMap);
refreshParsilList(j.data);
})
.catch(err => { showToast('Gagal memuat data parsil.', 'error'); console.error(err); });
}
// ── Refresh panel list Parsil Tanah ─────────────────────────────────
function refreshParsilList(data) {
if (typeof window.dlRefreshList !== 'function') return;
const items = (data || []).map(row => ({
id: row.id,
name: row.nama_parsil,
badge: row.status_kepemilikan,
badgeColor: getColor(row.status_kepemilikan),
dotColor: getColor(row.status_kepemilikan)
}));
window.dlRefreshList('Parsil', items);
}
// ── Hapus parsil ──────────────────────────────────
window._parsilHapus = function (id, btnEl) {
showDeleteConfirm('Yakin ingin menghapus data parsil tanah ini?').then(confirmed => {
if (!confirmed) return;
btnEl.disabled = true;
btnEl.textContent = '⏳ Menghapus...';
const fd = new FormData();
fd.append('id', id);
fetch('api/parsil/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 remaining = Object.values(allLayers).map(l => ({
id: l._parsilId, name: l._parsilNama,
badge: l._parsilStatus,
badgeColor: getColor(l._parsilStatus),
dotColor: getColor(l._parsilStatus)
}));
if (typeof window.dlRefreshList === 'function') window.dlRefreshList('Parsil', remaining);
showToast('Data parsil berhasil dihapus.');
} else throw new Error(j.message);
})
.catch(err => {
showToast(err.message || 'Gagal menghapus.', 'error');
btnEl.disabled = false;
btnEl.textContent = '🗑 Hapus Parsil';
});
});
};
// ── Drawing: gambar polygon baru ──────────────────
let drawingPoints = [];
let drawingLayer = null; // polyline/polygon sementara
let liveLineLayer = null; // polyline preview kursor
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 >= 3) {
showFormParsil(drawingPoints.slice());
} else if (save && drawingPoints.length < 3) {
showToast('Minimal 3 titik untuk membuat parsil (Polygon)', 'error');
}
drawingPoints = [];
if (typeof window._resetActiveTool === 'function') window._resetActiveTool();
}
function finishDrawing() {
if (!isDrawing) return;
if (drawingPoints.length >= 3) {
stopDrawing(true);
} else {
showToast('Minimal 3 titik untuk membuat parsil (Polygon)', 'error');
}
}
function onMapClick(e) {
if (currentMode !== 2 || currentSubMode !== 'parsil') 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: '#eab308', fillColor: '#eab308', fillOpacity: 1, pane: 'drawPane' });
m.addTo(map);
drawingMarkers.push(m);
// Preview polygon
if (drawingLayer) map.removeLayer(drawingLayer);
if (drawingPoints.length >= 3) {
drawingLayer = L.polygon(drawingPoints, { color: '#eab308', weight: 2, fillOpacity: 0.3, pane: 'drawPane' }).addTo(map);
} else if (drawingPoints.length === 2) {
drawingLayer = L.polyline(drawingPoints, { color: '#eab308', weight: 2, dashArray: '6,4', opacity: 0.7, pane: 'drawPane' }).addTo(map);
}
}
function onMapDoubleClick(e) {
if (currentMode !== 2 || currentSubMode !== 'parsil' || !isDrawing) return;
if (e.originalEvent) L.DomEvent.stop(e.originalEvent);
finishDrawing();
}
function onMouseMove(e) {
if (currentMode !== 2 || currentSubMode !== 'parsil' || !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 (dan dari kursor ke titik awal jika >= 2 titik)
if (liveLineLayer) map.removeLayer(liveLineLayer);
const previewCoords = [lastPoint, currentPoint];
if (drawingPoints.length >= 2) {
previewCoords.push(drawingPoints[0]); // Garis penutup bayangan
}
liveLineLayer = L.polyline(previewCoords, {
color: '#eab308', weight: 2, dashArray: '5,6', opacity: 0.6, pane: 'drawPane'
}).addTo(map);
// Hitung total luas jika + titik baru ini (minimal 3 titik)
let txt = '';
if (drawingPoints.length >= 2) {
const tempPoints = [...drawingPoints, currentPoint];
let luasArea = calculatePolygonArea(tempPoints);
txt = '<br>Luas: ' + parseFloat(luasArea.toFixed(2)).toLocaleString('id-ID', { minimumFractionDigits: 1, maximumFractionDigits: 2 }) + ' 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' + txt)
.addTo(map);
} else {
liveTooltip.setLatLng(currentPoint);
liveTooltip.setContent('<b> Lanjut</b> · <kbd>Enter</kbd> / 2×klik selesai' + txt);
}
}
function onKeyDown(e) {
if (currentMode !== 2 || currentSubMode !== 'parsil' || !isDrawing) return;
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
finishDrawing();
}
if (e.key === 'Escape') stopDrawing(false);
}
// ── Form popup setelah gambar selesai ─────────────
function showFormParsil(points) {
// Hitung luas area
let luasArea = calculatePolygonArea(points);
luasArea = luasArea.toFixed(2);
let luasStr = parseFloat(luasArea).toLocaleString('id-ID', { minimumFractionDigits: 1, maximumFractionDigits: 2 });
// Build GeoJSON Polygon
let geojsonPoints = points.map(p => [p.lng, p.lat]);
// GeoJSON First and last position must be strictly equivalent
geojsonPoints.push([points[0].lng, points[0].lat]);
const geojson = JSON.stringify({
type: 'Polygon',
coordinates: [geojsonPoints]
});
// Popup di bounds / center polygon
const tempPoly = L.polygon(points);
const centerPt = tempPoly.getBounds().getCenter();
L.popup({ maxWidth: 340, closeOnClick: false, closeButton: true })
.setLatLng(centerPt)
.setContent(`
<div class="form-popup">
<div class="form-popup-header">
<div class="form-popup-icon amber">🏘️</div>
<div>
<div class="form-popup-title">Tambah Data Parsil (Kavling)</div>
<div class="form-popup-coords">${points.length} titik · Luas : ${luasStr} m²</div>
</div>
</div>
<div class="form-group">
<label>Nama/Keterangan Parsil *</label>
<input type="text" id="fp_nama" placeholder="cth. Kavling Blok C" maxlength="100" autocomplete="off">
</div>
<div class="form-group">
<label>Status Kepemilikan *</label>
<select id="fp_status">
<option value="SHM" selected>Sertifikat Hak Milik (SHM)</option>
<option value="HGB">Sertifikat Hak Guna Bangunan (HGB)</option>
<option value="HGU">Sertifikat Hak Guna Usaha (HGU)</option>
<option value="HP">Hak Pakai (HP)</option>
</select>
</div>
<div class="form-group">
<label>Luas Tanah (otomatis)</label>
<div class="input-readonly">${luasStr} m²</div>
</div>
<input type="hidden" id="fp_geojson" value='${escapeHTML(geojson)}'>
<input type="hidden" id="fp_luas" value="${luasArea}">
<button class="btn-save" id="btnSimpanParsil" style="background:var(--c-warn);color:#111" onclick="window._parsilSimpan()">💾 Simpan Parsil</button>
<div class="form-status" id="parsilStatus"></div>
</div>
`)
.openOn(map);
setTimeout(() => { const el = document.getElementById('fp_nama'); if (el) el.focus(); }, 200);
// Kalau popup ditutup tanpa simpan: hapus preview
map.once('popupclose', () => {
// preview sudah di-clear di stopDrawing
});
}
// ── Simpan parsil ─────────────────────────────────
window._parsilSimpan = function () {
const nama = document.getElementById('fp_nama').value.trim();
const status = document.getElementById('fp_status').value;
const geojson = document.getElementById('fp_geojson').value;
const luas = document.getElementById('fp_luas').value;
const statusEl = document.getElementById('parsilStatus');
const btn = document.getElementById('btnSimpanParsil');
if (!nama) {
statusEl.textContent = '⚠ Nama parsil wajib diisi.';
statusEl.className = 'form-status error';
document.getElementById('fp_nama').focus();
return;
}
btn.disabled = true;
btn.textContent = '⏳ Menyimpan...';
const fd = new FormData();
fd.append('nama_parsil', nama);
fd.append('status_kepemilikan', status);
fd.append('geojson', geojson);
fd.append('luas_m2', luas);
fetch('api/parsil/simpan.php', { method: 'POST', body: fd })
.then(r => r.json())
.then(j => {
if (j.status === 'success') {
map.closePopup();
addPolygonToMap(j.data);
updateCount(Object.keys(allLayers).length, '🏘️');
const allItems = Object.values(allLayers).map(l => ({
id: l._parsilId, name: l._parsilNama,
badge: l._parsilStatus,
badgeColor: getColor(l._parsilStatus),
dotColor: getColor(l._parsilStatus)
}));
if (typeof window.dlRefreshList === 'function') window.dlRefreshList('Parsil', 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 Parsil';
});
};
// ── Init / cleanup ────────────────────────────────────────────
window.initParsil = 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['Parsil'] = function(id) {
const layer = allLayers[id];
if (!layer) return;
map.fitBounds(layer.getBounds(), { padding: [60, 60], animate: true });
setTimeout(() => layer.openPopup(layer.getBounds().getCenter()), 400);
};
layerGroup.addTo(map);
loadData();
};
// Ekspos ke global agar bisa dipanggil dari index.php
window._parsilStartDraw = startDrawing;
window._parsilFinishDraw = finishDrawing;
window._parsilStopDraw = function() { stopDrawing(false); };
// Filter Toggle
window._parsilToggleLayer = function(visible) {
if (visible) {
map.addLayer(layerGroup);
} else {
map.removeLayer(layerGroup);
stopDrawing(false); // Batalkan mode draw jika layer disembunyikan
}
};
})();