Upload seluruh tugas SIG Point Line dan Polygon
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
<?php
|
||||
// Koneksi database PDO
|
||||
$host = '127.0.0.1';
|
||||
$db = 'webgis_spbu';
|
||||
$user = 'root';
|
||||
$pass = '';
|
||||
$charset = 'utf8mb4';
|
||||
|
||||
$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
|
||||
try {
|
||||
$pdo = new PDO($dsn, $user, $pass, $options);
|
||||
} catch (PDOException $e) {
|
||||
die('Koneksi gagal: ' . htmlspecialchars($e->getMessage()));
|
||||
}
|
||||
|
||||
// Buat tabel jika belum ada
|
||||
$createTableSql = "CREATE TABLE IF NOT EXISTS t_tanah (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_pemilik VARCHAR(255) NOT NULL,
|
||||
status_kepemilikan ENUM('SHM', 'HGB', 'HGU', 'HP') NOT NULL,
|
||||
luas_sqm DECIMAL(10,2) NOT NULL,
|
||||
koordinat_json LONGTEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci";
|
||||
$pdo->exec($createTableSql);
|
||||
|
||||
// Simpan data jika form disubmit
|
||||
$successMessage = '';
|
||||
$errorMessage = '';
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$namaPemilik = trim($_POST['nama_pemilik'] ?? '');
|
||||
$statusKepemilikan = $_POST['status_kepemilikan'] ?? '';
|
||||
$luasSqm = $_POST['luas_sqm'] ?? '';
|
||||
$koordinatJson = $_POST['koordinat_json'] ?? '';
|
||||
|
||||
if ($namaPemilik === '' || $statusKepemilikan === '' || $luasSqm === '' || $koordinatJson === '') {
|
||||
$errorMessage = 'Semua field harus diisi dan polygon harus dibuat sebelum submit.';
|
||||
} else {
|
||||
try {
|
||||
$stmt = $pdo->prepare('INSERT INTO t_tanah (nama_pemilik, status_kepemilikan, luas_sqm, koordinat_json) VALUES (:nama, :status, :luas, :koordinat)');
|
||||
$stmt->execute([
|
||||
':nama' => $namaPemilik,
|
||||
':status' => $statusKepemilikan,
|
||||
':luas' => $luasSqm,
|
||||
':koordinat' => $koordinatJson,
|
||||
]);
|
||||
$successMessage = 'Data tanah berhasil disimpan.';
|
||||
header('Location: ' . $_SERVER['PHP_SELF'] . '?saved=1');
|
||||
exit;
|
||||
} catch (PDOException $e) {
|
||||
$errorMessage = 'Gagal menyimpan data: ' . htmlspecialchars($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ambil semua data polygon
|
||||
$tanahData = [];
|
||||
$stmt = $pdo->query('SELECT id, nama_pemilik, status_kepemilikan, luas_sqm, koordinat_json, created_at FROM t_tanah ORDER BY created_at DESC');
|
||||
while ($row = $stmt->fetch()) {
|
||||
$row['coords'] = json_decode($row['koordinat_json'], true);
|
||||
$tanahData[] = $row;
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Manajemen Parsil Tanah / Kavling (Polygon)</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" crossorigin=""/>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; font-family: Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f4f5f7; color: #111827; }
|
||||
.page { max-width: 1180px; margin: 0 auto; padding: 22px; }
|
||||
.header { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 16px; align-items: center; margin-bottom: 22px; }
|
||||
.header h1 { margin: 0; font-size: clamp(1.75rem, 2vw, 2.5rem); }
|
||||
.header p { margin: 4px 0 0; color: #6b7280; max-width: 720px; }
|
||||
.card { background: #ffffff; border: 1px solid #e5e7eb; border-radius: 24px; box-shadow: 0 20px 60px rgba(15, 23, 42, .06); padding: 24px; margin-bottom: 22px; }
|
||||
.card h2 { margin-top: 0; font-size: 1.25rem; color: #111827; }
|
||||
#map { width: 100%; height: 520px; border-radius: 20px; border: 1px solid #d1d5db; }
|
||||
.grid { display: grid; gap: 20px; }
|
||||
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
label { display: block; margin-bottom: 8px; font-weight: 600; color: #374151; }
|
||||
input[type=text], select, textarea { width: 100%; padding: 12px 14px; border-radius: 14px; border: 1px solid #d1d5db; background: #f9fafb; color: #111827; font-size: 0.98rem; outline: none; transition: border-color .2s ease, box-shadow .2s ease; }
|
||||
input[type=text]:focus, select:focus, textarea:focus { border-color: #2563eb; box-shadow: 0 0 0 3px rgba(59, 130, 246, .15); background: #fff; }
|
||||
.form-group { margin-bottom: 18px; }
|
||||
.btn-primary { display: inline-flex; align-items: center; justify-content: center; padding: 14px 22px; background: #2563eb; color: #fff; border: none; border-radius: 14px; cursor: pointer; font-weight: 700; transition: transform .15s ease, background .15s ease; }
|
||||
.btn-primary:hover { background: #1d4ed8; transform: translateY(-1px); }
|
||||
.note { color: #6b7280; font-size: .95rem; margin-top: -8px; margin-bottom: 16px; }
|
||||
.message { padding: 16px 18px; border-radius: 16px; margin-bottom: 20px; border: 1px solid transparent; }
|
||||
.message.success { background: #ecfdf5; color: #065f46; border-color: #d1fae5; }
|
||||
.message.error { background: #fef2f2; color: #b91c1c; border-color: #fecaca; }
|
||||
.info-list { display: grid; gap: 14px; }
|
||||
.info-card { padding: 18px; border-radius: 18px; border: 1px solid #e5e7eb; background: #fafafa; }
|
||||
.info-card strong { display: block; font-size: 1.05rem; margin-bottom: 10px; }
|
||||
.status-pill { display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; border-radius: 999px; font-size: .92rem; color: #fff; font-weight: 700; }
|
||||
.status-shm { background: #166534; }
|
||||
.status-hgb { background: #b45309; }
|
||||
.status-hgu { background: #5b21b6; }
|
||||
.status-hp { background: #1e40af; }
|
||||
.footer-note { color: #6b7280; font-size: .92rem; }
|
||||
@media (max-width: 900px) { .grid-cols-2 { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="header">
|
||||
<div>
|
||||
<h1>Manajemen Parsil Tanah / Kavling</h1>
|
||||
<p>Tambah, lihat, dan simpan data polygon bidang tanah dengan perhitungan luas otomatis menggunakan Leaflet & PHP PDO.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<?php if (!empty($_GET['saved']) && $_GET['saved'] == 1): ?>
|
||||
<div class="message success">Data tanah berhasil disimpan.</div>
|
||||
<?php endif; ?>
|
||||
<?php if ($errorMessage): ?>
|
||||
<div class="message error"><?= htmlspecialchars($errorMessage) ?></div>
|
||||
<?php endif; ?>
|
||||
<div id="map"></div>
|
||||
<p class="note">Klik beberapa titik pada peta untuk membuat polygon. Tutup polygon dengan klik titik awal lagi untuk menghitung luas otomatis.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Form Tambah Data Parsil</h2>
|
||||
<form method="post" id="tanahForm">
|
||||
<div class="grid grid-cols-2">
|
||||
<div class="form-group">
|
||||
<label for="nama_pemilik">Nama Pemilik</label>
|
||||
<input type="text" id="nama_pemilik" name="nama_pemilik" placeholder="Masukkan nama pemilik" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="status_kepemilikan">Status Kepemilikan</label>
|
||||
<select id="status_kepemilikan" name="status_kepemilikan" required>
|
||||
<option value="">Pilih status kepemilikan</option>
|
||||
<option value="SHM">SHM</option>
|
||||
<option value="HGB">HGB</option>
|
||||
<option value="HGU">HGU</option>
|
||||
<option value="HP">HP</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2">
|
||||
<div class="form-group">
|
||||
<label for="luas_sqm">Luas (m²)</label>
|
||||
<input type="text" id="luas_sqm" name="luas_sqm" readonly placeholder="Luas akan terisi otomatis" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="koordinat_json">Koordinat JSON</label>
|
||||
<textarea id="koordinat_json" name="koordinat_json" rows="3" readonly placeholder="Koordinat akan terisi otomatis" required></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Simpan Parsil</button>
|
||||
</form>
|
||||
<p class="footer-note">Pastikan polygon sudah ditutup sebelum submit. Klik titik awal lagi untuk menyelesaikan polygon.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Daftar Parsil Tertinggi & Detail</h2>
|
||||
<div class="info-list">
|
||||
<?php if (empty($tanahData)): ?>
|
||||
<div class="info-card">Belum ada data parsil. Tambahkan bidang tanah melalui peta dan form di atas.</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($tanahData as $tanah): ?>
|
||||
<div class="info-card">
|
||||
<strong><?= htmlspecialchars($tanah['nama_pemilik']) ?> · <?= htmlspecialchars($tanah['status_kepemilikan']) ?></strong>
|
||||
<div>Luas: <?= number_format($tanah['luas_sqm'], 2, ',', '.') ?> m²</div>
|
||||
<div>Disimpan: <?= htmlspecialchars($tanah['created_at']) ?></div>
|
||||
<div style="margin-top: 10px;"><span class="status-pill status-<?= strtolower($tanah['status_kepemilikan']) ?>"><?= htmlspecialchars($tanah['status_kepemilikan']) ?></span></div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" crossorigin=""></script>
|
||||
<script>
|
||||
const map = L.map('map').setView([0.0225, 109.3425], 12);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors'
|
||||
}).addTo(map);
|
||||
|
||||
const polygonLayerGroup = L.layerGroup().addTo(map);
|
||||
let currentMarkers = [];
|
||||
let currentLine = null;
|
||||
let currentPolygon = null;
|
||||
let currentCoordinates = [];
|
||||
|
||||
const statusColor = {
|
||||
SHM: '#15803d',
|
||||
HGB: '#d97706',
|
||||
HGU: '#7c3aed',
|
||||
HP: '#1d4ed8'
|
||||
};
|
||||
|
||||
const existingData = <?= json_encode($tanahData, JSON_UNESCAPED_UNICODE) ?>;
|
||||
existingData.forEach(item => {
|
||||
if (!Array.isArray(item.coords)) return;
|
||||
const coords = item.coords.map(c => [c.lat, c.lng]);
|
||||
const fillColor = statusColor[item.status_kepemilikan] || '#475569';
|
||||
const poly = L.polygon(coords, {
|
||||
color: fillColor,
|
||||
fillColor: fillColor,
|
||||
fillOpacity: 0.15,
|
||||
weight: 2,
|
||||
opacity: 0.9
|
||||
}).addTo(polygonLayerGroup);
|
||||
poly.bindPopup(`<strong>${escapeHtml(item.nama_pemilik)}</strong><br>${escapeHtml(item.status_kepemilikan)}<br>Luas: ${Number(item.luas_sqm).toLocaleString('id-ID', {minimumFractionDigits:2, maximumFractionDigits:2})} m²`);
|
||||
});
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/\'/g, ''');
|
||||
}
|
||||
|
||||
function updateForm() {
|
||||
const jumlah = currentCoordinates.length;
|
||||
const luasField = document.getElementById('luas_sqm');
|
||||
const koordinatField = document.getElementById('koordinat_json');
|
||||
if (jumlah >= 3) {
|
||||
const area = computeArea(currentCoordinates);
|
||||
luasField.value = area.toFixed(2);
|
||||
koordinatField.value = JSON.stringify(currentCoordinates);
|
||||
} else {
|
||||
luasField.value = '';
|
||||
koordinatField.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function computeArea(latlngs) {
|
||||
const R = 6378137; // radius earth in meters
|
||||
const radians = degrees => degrees * Math.PI / 180;
|
||||
if (latlngs.length < 3) return 0;
|
||||
const lat0 = radians(latlngs.reduce((sum, p) => sum + p.lat, 0) / latlngs.length);
|
||||
const points = latlngs.map(p => ({
|
||||
x: R * radians(p.lng) * Math.cos(lat0),
|
||||
y: R * radians(p.lat)
|
||||
}));
|
||||
let sum = 0;
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const curr = points[i];
|
||||
const next = points[(i + 1) % points.length];
|
||||
sum += curr.x * next.y - next.x * curr.y;
|
||||
}
|
||||
return Math.abs(sum / 2);
|
||||
}
|
||||
|
||||
function redrawCurrentShape() {
|
||||
if (currentLine) {
|
||||
map.removeLayer(currentLine);
|
||||
currentLine = null;
|
||||
}
|
||||
if (currentPolygon) {
|
||||
map.removeLayer(currentPolygon);
|
||||
currentPolygon = null;
|
||||
}
|
||||
currentMarkers.forEach(marker => map.removeLayer(marker));
|
||||
currentMarkers = [];
|
||||
|
||||
if (currentCoordinates.length === 0) return;
|
||||
|
||||
currentCoordinates.forEach(coord => {
|
||||
const marker = L.circleMarker([coord.lat, coord.lng], {
|
||||
radius: 6,
|
||||
fillColor: '#2563eb',
|
||||
color: '#fff',
|
||||
weight: 2,
|
||||
fillOpacity: 1
|
||||
}).addTo(map);
|
||||
currentMarkers.push(marker);
|
||||
});
|
||||
|
||||
if (currentCoordinates.length >= 2) {
|
||||
currentLine = L.polyline(currentCoordinates.map(p => [p.lat, p.lng]), {
|
||||
color: '#2563eb',
|
||||
dashArray: '8,6',
|
||||
weight: 3,
|
||||
opacity: 0.85
|
||||
}).addTo(map);
|
||||
}
|
||||
if (currentCoordinates.length >= 3) {
|
||||
currentPolygon = L.polygon(currentCoordinates.map(p => [p.lat, p.lng]), {
|
||||
color: '#2563eb',
|
||||
fillColor: '#2563eb',
|
||||
fillOpacity: 0.12,
|
||||
weight: 2
|
||||
}).addTo(map);
|
||||
}
|
||||
}
|
||||
|
||||
function tryClosePolygon(clickedLatLng) {
|
||||
if (currentCoordinates.length < 3) return false;
|
||||
const first = currentCoordinates[0];
|
||||
const distance = map.distance([first.lat, first.lng], [clickedLatLng.lat, clickedLatLng.lng]);
|
||||
return distance < 12;
|
||||
}
|
||||
|
||||
map.on('click', event => {
|
||||
const point = { lat: event.latlng.lat, lng: event.latlng.lng };
|
||||
if (tryClosePolygon(event.latlng)) {
|
||||
currentCoordinates.push(currentCoordinates[0]);
|
||||
redrawCurrentShape();
|
||||
updateForm();
|
||||
const lastMarker = currentMarkers[0];
|
||||
if (lastMarker) {
|
||||
lastMarker.setStyle({ fillColor: '#16a34a', color: '#fff' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (currentPolygon) {
|
||||
map.removeLayer(currentPolygon);
|
||||
currentPolygon = null;
|
||||
}
|
||||
currentCoordinates.push(point);
|
||||
redrawCurrentShape();
|
||||
updateForm();
|
||||
});
|
||||
|
||||
document.getElementById('map').addEventListener('contextmenu', event => {
|
||||
event.preventDefault();
|
||||
currentCoordinates = [];
|
||||
redrawCurrentShape();
|
||||
updateForm();
|
||||
});
|
||||
|
||||
const form = document.getElementById('tanahForm');
|
||||
form.addEventListener('submit', event => {
|
||||
if (currentCoordinates.length < 4 || !document.getElementById('luas_sqm').value) {
|
||||
event.preventDefault();
|
||||
alert('Buat polygon tertutup terlebih dahulu sebelum submit. Klik titik awal lagi untuk menutup polygon.');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user