Initial commit

This commit is contained in:
miaaurl
2026-06-11 16:40:31 +07:00
commit c321f2ba18
18 changed files with 6877 additions and 0 deletions

138
jalan_tanah/api/jalan.php Normal file
View File

@@ -0,0 +1,138 @@
<?php
// api/jalan.php — CRUD Data Jalan (GeoJSON LineString)
require_once '../config/database.php';
setCorsHeaders();
$method = $_SERVER['REQUEST_METHOD'];
$conn = getConnection();
switch ($method) {
case 'GET':
if (isset($_GET['id'])) {
$id = intval($_GET['id']);
$stmt = $conn->prepare("SELECT id, nama_jalan, status_jalan, panjang_meter, keterangan, geom, created_at, updated_at FROM data_jalan WHERE id = ?");
$stmt->bind_param("i", $id);
$stmt->execute();
$row = $stmt->get_result()->fetch_assoc();
if (!$row) { http_response_code(404); echo json_encode(['status'=>'error','message'=>'Data tidak ditemukan']); break; }
$row['koordinat'] = parseGeoJsonToLatLng($row['geom']);
unset($row['geom']);
echo json_encode(['status'=>'success','data'=>$row]);
} else {
// Filter support: ?status=Nasional&min_panjang=100&max_panjang=5000&q=nama
$where = ['1=1'];
$params = [];
$types = '';
if (!empty($_GET['status'])) {
$where[] = 'status_jalan = ?';
$params[] = $_GET['status'];
$types .= 's';
}
if (!empty($_GET['min_panjang'])) {
$where[] = 'panjang_meter >= ?';
$params[] = (float)$_GET['min_panjang'];
$types .= 'd';
}
if (!empty($_GET['max_panjang'])) {
$where[] = 'panjang_meter <= ?';
$params[] = (float)$_GET['max_panjang'];
$types .= 'd';
}
if (!empty($_GET['q'])) {
$where[] = 'nama_jalan LIKE ?';
$params[] = '%' . $_GET['q'] . '%';
$types .= 's';
}
$sql = 'SELECT id, nama_jalan, status_jalan, panjang_meter, keterangan, geom, created_at FROM data_jalan WHERE ' . implode(' AND ', $where) . ' ORDER BY created_at DESC';
$stmt = $conn->prepare($sql);
if ($params) $stmt->bind_param($types, ...$params);
$stmt->execute();
$rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
$data = array_map(function($row) {
$row['koordinat'] = parseGeoJsonToLatLng($row['geom']);
unset($row['geom']);
return $row;
}, $rows);
echo json_encode(['status'=>'success','data'=>$data,'total'=>count($data)]);
}
break;
case 'POST':
$input = getJsonInput();
if (!$input) { http_response_code(400); echo json_encode(['status'=>'error','message'=>'Input tidak valid']); break; }
$nama = trim($input['nama_jalan'] ?? '');
$status = trim($input['status_jalan'] ?? '');
$panjang = (float)($input['panjang_meter'] ?? 0);
$ket = trim($input['keterangan'] ?? '');
$coords = $input['koordinat'] ?? [];
if (!$nama || !$status || count($coords) < 2) {
http_response_code(400);
echo json_encode(['status'=>'error','message'=>'Nama, status, dan minimal 2 koordinat wajib diisi']);
break;
}
if (!in_array($status, ['Nasional','Provinsi','Kabupaten'])) {
http_response_code(400); echo json_encode(['status'=>'error','message'=>'Status tidak valid']); break;
}
$geom = buildGeoJsonLine($coords);
$stmt = $conn->prepare("INSERT INTO data_jalan (nama_jalan,status_jalan,panjang_meter,keterangan,geom) VALUES (?,?,?,?,?)");
$stmt->bind_param("ssdss", $nama, $status, $panjang, $ket, $geom);
if ($stmt->execute()) {
echo json_encode(['status'=>'success','message'=>'Data jalan berhasil ditambahkan','id'=>$conn->insert_id]);
} else {
http_response_code(500); echo json_encode(['status'=>'error','message'=>'Gagal menyimpan: '.$conn->error]);
}
break;
case 'PUT':
$input = getJsonInput();
$id = intval($_GET['id'] ?? 0);
if (!$id || !$input) { http_response_code(400); echo json_encode(['status'=>'error','message'=>'ID dan input wajib ada']); break; }
$nama = trim($input['nama_jalan'] ?? '');
$status = trim($input['status_jalan'] ?? '');
$panjang = (float)($input['panjang_meter'] ?? 0);
$ket = trim($input['keterangan'] ?? '');
$coords = $input['koordinat'] ?? [];
if (!$nama || !$status) { http_response_code(400); echo json_encode(['status'=>'error','message'=>'Nama dan status wajib diisi']); break; }
$geom = buildGeoJsonLine($coords);
$stmt = $conn->prepare("UPDATE data_jalan SET nama_jalan=?,status_jalan=?,panjang_meter=?,keterangan=?,geom=? WHERE id=?");
$stmt->bind_param("ssdssi", $nama, $status, $panjang, $ket, $geom, $id);
if ($stmt->execute()) {
echo json_encode(['status'=>'success','message'=>'Data jalan berhasil diperbarui']);
} else {
http_response_code(500); echo json_encode(['status'=>'error','message'=>'Gagal memperbarui: '.$conn->error]);
}
break;
case 'DELETE':
$id = intval($_GET['id'] ?? 0);
if (!$id) { http_response_code(400); echo json_encode(['status'=>'error','message'=>'ID tidak valid']); break; }
$stmt = $conn->prepare("DELETE FROM data_jalan WHERE id=?");
$stmt->bind_param("i", $id);
if ($stmt->execute() && $stmt->affected_rows > 0) {
echo json_encode(['status'=>'success','message'=>'Data jalan berhasil dihapus']);
} else {
http_response_code(404); echo json_encode(['status'=>'error','message'=>'Data tidak ditemukan']);
}
break;
default:
http_response_code(405);
echo json_encode(['status'=>'error','message'=>'Method tidak diizinkan']);
}
$conn->close();

363
jalan_tanah/api/laporan.php Normal file
View File

@@ -0,0 +1,363 @@
<?php
// api/laporan.php — CRUD + Analitik Laporan Jalan Rusak
require_once '../config/database.php';
setCorsHeaders();
$method = $_SERVER['REQUEST_METHOD'];
$conn = getConnection();
$action = $_GET['action'] ?? '';
// ---- Endpoint analitik terpisah ----
if ($method === 'GET' && $action === 'analytics') {
handleAnalytics($conn);
$conn->close();
exit;
}
switch ($method) {
// ==========================================
// GET — list dengan filter
// ==========================================
case 'GET':
if (isset($_GET['id'])) {
$id = intval($_GET['id']);
$stmt = $conn->prepare("SELECT * FROM laporan_jalan_rusak WHERE id = ?");
$stmt->bind_param("i", $id);
$stmt->execute();
$row = $stmt->get_result()->fetch_assoc();
if (!$row) { http_response_code(404); echo json_encode(['status'=>'error','message'=>'Tidak ditemukan']); break; }
$row = formatLaporanRow($row);
echo json_encode(['status'=>'success','data'=>$row]);
} else {
$where = ['1=1'];
$params = [];
$types = '';
// Filter: bulan terakhir (default), atau rentang tahun
if (!empty($_GET['bulan_terakhir'])) {
$bulan = intval($_GET['bulan_terakhir']);
$where[] = 'tanggal_input >= DATE_SUB(NOW(), INTERVAL ? MONTH)';
$params[] = $bulan;
$types .= 'i';
} elseif (!empty($_GET['tahun_terakhir'])) {
$tahun = intval($_GET['tahun_terakhir']);
$where[] = 'tanggal_input >= DATE_SUB(NOW(), INTERVAL ? YEAR)';
$params[] = $tahun;
$types .= 'i';
}
if (!empty($_GET['status'])) {
$where[] = 'status = ?';
$params[] = $_GET['status'];
$types .= 's';
}
if (!empty($_GET['nama_jalan'])) {
$where[] = 'nama_jalan LIKE ?';
$params[] = '%'.$_GET['nama_jalan'].'%';
$types .= 's';
}
if (!empty($_GET['q'])) {
$where[] = '(nama_jalan LIKE ? OR deskripsi LIKE ? OR nama_pelapor LIKE ?)';
$params[] = '%'.$_GET['q'].'%';
$params[] = '%'.$_GET['q'].'%';
$params[] = '%'.$_GET['q'].'%';
$types .= 'sss';
}
$sql = 'SELECT * FROM laporan_jalan_rusak WHERE ' . implode(' AND ', $where) . ' ORDER BY tanggal_input DESC';
$stmt = $conn->prepare($sql);
if ($params) $stmt->bind_param($types, ...$params);
$stmt->execute();
$rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
$data = array_map('formatLaporanRow', $rows);
// Hitung cluster (radius 50m, min 3 laporan → urgent)
$clusters = buildClusters($data, 50, 3);
echo json_encode(['status'=>'success','data'=>$data,'total'=>count($data),'clusters'=>$clusters]);
}
break;
// ==========================================
// POST — Tambah laporan (multipart/form-data)
// ==========================================
case 'POST':
$nama_jalan = trim($_POST['nama_jalan'] ?? '');
$deskripsi = trim($_POST['deskripsi'] ?? '');
$nama_pelapor= trim($_POST['nama_pelapor']?? '');
$lat = (float)($_POST['lat'] ?? 0);
$lng = (float)($_POST['lng'] ?? 0);
if (!$nama_jalan || !$lat || !$lng) {
http_response_code(400);
echo json_encode(['status'=>'error','message'=>'Nama jalan dan koordinat wajib diisi']);
break;
}
$foto_path = null;
$foto_lat = null;
$foto_lng = null;
$foto_datetime = null;
// Handle upload foto
if (!empty($_FILES['foto']) && $_FILES['foto']['error'] === UPLOAD_ERR_OK) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['foto']['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, ALLOWED_TYPES)) {
http_response_code(400);
echo json_encode(['status'=>'error','message'=>'Format foto tidak didukung (JPG/PNG/WebP)']);
break;
}
if ($_FILES['foto']['size'] > MAX_FILE_SIZE) {
http_response_code(400);
echo json_encode(['status'=>'error','message'=>'Ukuran foto melebihi batas 8MB']);
break;
}
// Baca EXIF sebelum pindah file
$exifData = @exif_read_data($_FILES['foto']['tmp_name']);
if ($exifData) {
// Ekstrak koordinat GPS dari EXIF
$gpsCoords = extractExifGps($exifData);
if ($gpsCoords) {
$foto_lat = $gpsCoords['lat'];
$foto_lng = $gpsCoords['lng'];
}
// Ekstrak datetime
$dt = $exifData['DateTimeOriginal'] ?? $exifData['DateTime'] ?? null;
if ($dt) {
$foto_datetime = date('Y-m-d H:i:s', strtotime(str_replace(':', '-', substr($dt, 0, 10)) . substr($dt, 10)));
}
}
// Jika GPS tidak ada di EXIF, pakai koordinat yang dikirim user
if (!$foto_lat) $foto_lat = $lat;
if (!$foto_lng) $foto_lng = $lng;
if (!is_dir(UPLOAD_DIR)) mkdir(UPLOAD_DIR, 0755, true);
$ext = match($mime) { 'image/png' => 'png', 'image/webp' => 'webp', default => 'jpg' };
$filename = 'laporan_' . date('Ymd_His') . '_' . uniqid() . '.' . $ext;
$destPath = UPLOAD_DIR . $filename;
if (!move_uploaded_file($_FILES['foto']['tmp_name'], $destPath)) {
http_response_code(500);
echo json_encode(['status'=>'error','message'=>'Gagal menyimpan foto']);
break;
}
$foto_path = UPLOAD_URL . $filename;
}
// Koordinat point: prioritaskan GPS foto, fallback ke input user
$pointLat = $foto_lat ?? $lat;
$pointLng = $foto_lng ?? $lng;
$geom = buildGeoJsonPoint($pointLat, $pointLng);
$stmt = $conn->prepare(
"INSERT INTO laporan_jalan_rusak (nama_pelapor,nama_jalan,deskripsi,foto_path,foto_lat,foto_lng,foto_datetime,geom)
VALUES (?,?,?,?,?,?,?,?)"
);
$npVal = $nama_pelapor ?: null;
$stmt->bind_param("ssssddss", $npVal, $nama_jalan, $deskripsi, $foto_path, $foto_lat, $foto_lng, $foto_datetime, $geom);
if ($stmt->execute()) {
echo json_encode(['status'=>'success','message'=>'Laporan berhasil dikirim','id'=>$conn->insert_id]);
} else {
http_response_code(500);
echo json_encode(['status'=>'error','message'=>'Gagal menyimpan: '.$conn->error]);
}
break;
// ==========================================
// PUT — Update status laporan
// ==========================================
case 'PUT':
$input = getJsonInput();
$id = intval($_GET['id'] ?? 0);
if (!$id || !$input) { http_response_code(400); echo json_encode(['status'=>'error','message'=>'ID dan input wajib ada']); break; }
$status = trim($input['status'] ?? '');
if (!in_array($status, ['pending','verified','resolved'])) {
http_response_code(400); echo json_encode(['status'=>'error','message'=>'Status tidak valid']); break;
}
$stmt = $conn->prepare("UPDATE laporan_jalan_rusak SET status=? WHERE id=?");
$stmt->bind_param("si", $status, $id);
if ($stmt->execute()) {
echo json_encode(['status'=>'success','message'=>'Status laporan diperbarui']);
} else {
http_response_code(500); echo json_encode(['status'=>'error','message'=>$conn->error]);
}
break;
// ==========================================
// DELETE
// ==========================================
case 'DELETE':
$id = intval($_GET['id'] ?? 0);
if (!$id) { http_response_code(400); echo json_encode(['status'=>'error','message'=>'ID tidak valid']); break; }
// Hapus file foto juga
$stmt = $conn->prepare("SELECT foto_path FROM laporan_jalan_rusak WHERE id=?");
$stmt->bind_param("i", $id);
$stmt->execute();
$row = $stmt->get_result()->fetch_assoc();
if ($row && $row['foto_path']) {
$fullPath = __DIR__ . '/../' . $row['foto_path'];
if (file_exists($fullPath)) @unlink($fullPath);
}
$stmt = $conn->prepare("DELETE FROM laporan_jalan_rusak WHERE id=?");
$stmt->bind_param("i", $id);
if ($stmt->execute() && $stmt->affected_rows > 0) {
echo json_encode(['status'=>'success','message'=>'Laporan berhasil dihapus']);
} else {
http_response_code(404); echo json_encode(['status'=>'error','message'=>'Data tidak ditemukan']);
}
break;
default:
http_response_code(405);
echo json_encode(['status'=>'error','message'=>'Method tidak diizinkan']);
}
$conn->close();
// ============================================
// HELPERS
// ============================================
function formatLaporanRow(array $row): array {
$geom = json_decode($row['geom'] ?? '{}', true);
$row['lat'] = $geom['coordinates'][1] ?? null;
$row['lng'] = $geom['coordinates'][0] ?? null;
unset($row['geom']);
return $row;
}
function extractExifGps(array $exif): ?array {
if (empty($exif['GPSLatitude']) || empty($exif['GPSLongitude'])) return null;
$lat = gpsToDecimal($exif['GPSLatitude'], $exif['GPSLatitudeRef'] ?? 'N');
$lng = gpsToDecimal($exif['GPSLongitude'], $exif['GPSLongitudeRef'] ?? 'E');
return ['lat' => $lat, 'lng' => $lng];
}
function gpsToDecimal(array $parts, string $ref): float {
$deg = evalFraction($parts[0]);
$min = evalFraction($parts[1]);
$sec = evalFraction($parts[2]);
$dec = $deg + ($min / 60) + ($sec / 3600);
return in_array($ref, ['S','W']) ? -$dec : $dec;
}
function evalFraction(string $frac): float {
if (strpos($frac, '/') === false) return (float)$frac;
[$n, $d] = explode('/', $frac);
return $d ? (float)$n / (float)$d : 0;
}
/**
* Hitung jarak Haversine antara dua titik (meter)
*/
function haversine(float $lat1, float $lng1, float $lat2, float $lng2): float {
$R = 6371000;
$p1 = deg2rad($lat1); $p2 = deg2rad($lat2);
$dp = deg2rad($lat2 - $lat1);
$dl = deg2rad($lng2 - $lng1);
$a = sin($dp/2)**2 + cos($p1)*cos($p2)*sin($dl/2)**2;
return $R * 2 * atan2(sqrt($a), sqrt(1-$a));
}
/**
* Bangun cluster sederhana: greedy radius clustering
* Laporan dalam radius $radiusM meter dianggap satu cluster.
* Cluster dengan >= $minCount laporan ditandai urgent.
*/
function buildClusters(array $laporan, float $radiusM, int $minCount): array {
$clusters = [];
$assigned = [];
foreach ($laporan as $i => $l) {
if (isset($assigned[$i])) continue;
if ($l['lat'] === null) continue;
$cluster = [$i];
$assigned[$i] = true;
foreach ($laporan as $j => $m) {
if ($j === $i || isset($assigned[$j])) continue;
if ($m['lat'] === null) continue;
if (haversine($l['lat'], $l['lng'], $m['lat'], $m['lng']) <= $radiusM) {
$cluster[] = $j;
$assigned[$j] = true;
}
}
if (count($cluster) >= $minCount) {
// Centroid
$cLat = array_sum(array_map(fn($k) => $laporan[$k]['lat'], $cluster)) / count($cluster);
$cLng = array_sum(array_map(fn($k) => $laporan[$k]['lng'], $cluster)) / count($cluster);
$ids = array_map(fn($k) => $laporan[$k]['id'], $cluster);
$jalanNames = array_unique(array_map(fn($k) => $laporan[$k]['nama_jalan'], $cluster));
$clusters[] = [
'lat' => $cLat,
'lng' => $cLng,
'count' => count($cluster),
'ids' => $ids,
'nama_jalan' => implode(', ', $jalanNames),
'urgent' => count($cluster) >= $minCount,
];
}
}
return $clusters;
}
/**
* Analitik: frekuensi kerusakan per jalan, per periode
*/
function handleAnalytics(mysqli $conn): void {
$tahun = intval($_GET['tahun'] ?? 1);
if (!in_array($tahun, [1,2,3,5,10])) $tahun = 1;
// Frekuensi per jalan
$stmt = $conn->prepare(
"SELECT nama_jalan,
COUNT(*) AS total,
SUM(CASE WHEN status='resolved' THEN 1 ELSE 0 END) AS resolved,
SUM(CASE WHEN status='verified' THEN 1 ELSE 0 END) AS verified,
SUM(CASE WHEN status='pending' THEN 1 ELSE 0 END) AS pending,
MIN(tanggal_input) AS pertama,
MAX(tanggal_input) AS terakhir
FROM laporan_jalan_rusak
WHERE tanggal_input >= DATE_SUB(NOW(), INTERVAL ? YEAR)
GROUP BY nama_jalan
ORDER BY total DESC"
);
$stmt->bind_param("i", $tahun);
$stmt->execute();
$perJalan = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
// Tren per bulan (12 bulan terakhir)
$tren = $conn->query(
"SELECT DATE_FORMAT(tanggal_input,'%Y-%m') AS bulan, COUNT(*) AS total
FROM laporan_jalan_rusak
WHERE tanggal_input >= DATE_SUB(NOW(), INTERVAL 12 MONTH)
GROUP BY bulan ORDER BY bulan ASC"
)->fetch_all(MYSQLI_ASSOC);
// Sering rusak: jalan dengan total >= 3 dalam rentang waktu
$seringRusak = array_filter($perJalan, fn($r) => (int)$r['total'] >= 3);
echo json_encode([
'status' => 'success',
'per_jalan' => $perJalan,
'tren_bulanan' => $tren,
'sering_rusak' => array_values($seringRusak),
'rentang_tahun'=> $tahun,
]);
}

144
jalan_tanah/api/parsil.php Normal file
View File

@@ -0,0 +1,144 @@
<?php
// api/parsil.php — CRUD Data Parsil Tanah (GeoJSON Polygon)
require_once '../config/database.php';
setCorsHeaders();
$method = $_SERVER['REQUEST_METHOD'];
$conn = getConnection();
switch ($method) {
case 'GET':
if (isset($_GET['id'])) {
$id = intval($_GET['id']);
$stmt = $conn->prepare("SELECT id, nama_parsil, pemilik, status_sertifikat, nomor_sertifikat, luas_meter2, keterangan, geom, created_at FROM data_parsil WHERE id = ?");
$stmt->bind_param("i", $id);
$stmt->execute();
$row = $stmt->get_result()->fetch_assoc();
if (!$row) { http_response_code(404); echo json_encode(['status'=>'error','message'=>'Data tidak ditemukan']); break; }
$row['koordinat'] = parseGeoJsonToLatLng($row['geom']);
unset($row['geom']);
echo json_encode(['status'=>'success','data'=>$row]);
} else {
$where = ['1=1'];
$params = [];
$types = '';
if (!empty($_GET['status'])) {
$where[] = 'status_sertifikat = ?';
$params[] = $_GET['status'];
$types .= 's';
}
if (!empty($_GET['min_luas'])) {
$where[] = 'luas_meter2 >= ?';
$params[] = (float)$_GET['min_luas'];
$types .= 'd';
}
if (!empty($_GET['max_luas'])) {
$where[] = 'luas_meter2 <= ?';
$params[] = (float)$_GET['max_luas'];
$types .= 'd';
}
if (!empty($_GET['q'])) {
$where[] = '(nama_parsil LIKE ? OR pemilik LIKE ?)';
$params[] = '%'.$_GET['q'].'%';
$params[] = '%'.$_GET['q'].'%';
$types .= 'ss';
}
$sql = 'SELECT id, nama_parsil, pemilik, status_sertifikat, nomor_sertifikat, luas_meter2, keterangan, geom, created_at FROM data_parsil WHERE ' . implode(' AND ', $where) . ' ORDER BY created_at DESC';
$stmt = $conn->prepare($sql);
if ($params) $stmt->bind_param($types, ...$params);
$stmt->execute();
$rows = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
$data = array_map(function($row) {
$row['koordinat'] = parseGeoJsonToLatLng($row['geom']);
unset($row['geom']);
return $row;
}, $rows);
echo json_encode(['status'=>'success','data'=>$data,'total'=>count($data)]);
}
break;
case 'POST':
$input = getJsonInput();
if (!$input) { http_response_code(400); echo json_encode(['status'=>'error','message'=>'Input tidak valid']); break; }
$nama = trim($input['nama_parsil'] ?? '');
$pemilik = trim($input['pemilik'] ?? '');
$status = trim($input['status_sertifikat'] ?? '');
$nosert = trim($input['nomor_sertifikat'] ?? '');
$luas = (float)($input['luas_meter2'] ?? 0);
$ket = trim($input['keterangan'] ?? '');
$coords = $input['koordinat'] ?? [];
if (!$nama || !$pemilik || !$status || count($coords) < 3) {
http_response_code(400);
echo json_encode(['status'=>'error','message'=>'Nama, pemilik, status, dan minimal 3 koordinat wajib diisi']);
break;
}
if (!in_array($status, ['SHM','HGB','HGU','HP'])) {
http_response_code(400); echo json_encode(['status'=>'error','message'=>'Status tidak valid']); break;
}
$geom = buildGeoJsonPolygon($coords);
$stmt = $conn->prepare("INSERT INTO data_parsil (nama_parsil,pemilik,status_sertifikat,nomor_sertifikat,luas_meter2,keterangan,geom) VALUES (?,?,?,?,?,?,?)");
$stmt->bind_param("ssssdss", $nama, $pemilik, $status, $nosert, $luas, $ket, $geom);
if ($stmt->execute()) {
echo json_encode(['status'=>'success','message'=>'Data parsil berhasil ditambahkan','id'=>$conn->insert_id]);
} else {
http_response_code(500); echo json_encode(['status'=>'error','message'=>'Gagal menyimpan: '.$conn->error]);
}
break;
case 'PUT':
$input = getJsonInput();
$id = intval($_GET['id'] ?? 0);
if (!$id || !$input) { http_response_code(400); echo json_encode(['status'=>'error','message'=>'ID dan input wajib ada']); break; }
$nama = trim($input['nama_parsil'] ?? '');
$pemilik = trim($input['pemilik'] ?? '');
$status = trim($input['status_sertifikat'] ?? '');
$nosert = trim($input['nomor_sertifikat'] ?? '');
$luas = (float)($input['luas_meter2'] ?? 0);
$ket = trim($input['keterangan'] ?? '');
$coords = $input['koordinat'] ?? [];
if (!$nama || !$pemilik || !$status) {
http_response_code(400); echo json_encode(['status'=>'error','message'=>'Nama, pemilik, dan status wajib diisi']); break;
}
$geom = buildGeoJsonPolygon($coords);
$stmt = $conn->prepare("UPDATE data_parsil SET nama_parsil=?,pemilik=?,status_sertifikat=?,nomor_sertifikat=?,luas_meter2=?,keterangan=?,geom=? WHERE id=?");
$stmt->bind_param("ssssdssi", $nama, $pemilik, $status, $nosert, $luas, $ket, $geom, $id);
if ($stmt->execute()) {
echo json_encode(['status'=>'success','message'=>'Data parsil berhasil diperbarui']);
} else {
http_response_code(500); echo json_encode(['status'=>'error','message'=>'Gagal memperbarui: '.$conn->error]);
}
break;
case 'DELETE':
$id = intval($_GET['id'] ?? 0);
if (!$id) { http_response_code(400); echo json_encode(['status'=>'error','message'=>'ID tidak valid']); break; }
$stmt = $conn->prepare("DELETE FROM data_parsil WHERE id=?");
$stmt->bind_param("i", $id);
if ($stmt->execute() && $stmt->affected_rows > 0) {
echo json_encode(['status'=>'success','message'=>'Data parsil berhasil dihapus']);
} else {
http_response_code(404); echo json_encode(['status'=>'error','message'=>'Data tidak ditemukan']);
}
break;
default:
http_response_code(405);
echo json_encode(['status'=>'error','message'=>'Method tidak diizinkan']);
}
$conn->close();

View File

@@ -0,0 +1,108 @@
<?php
// ============================================
// config/database.php
// ============================================
define('DB_HOST', 'localhost');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_NAME', 'db_jalan');
// Path untuk upload foto laporan (relatif dari root project)
define('UPLOAD_DIR', __DIR__ . '/../uploads/laporan/');
define('UPLOAD_URL', 'uploads/laporan/');
define('MAX_FILE_SIZE', 8 * 1024 * 1024); // 8MB
define('ALLOWED_TYPES', ['image/jpeg', 'image/png', 'image/webp']);
function getConnection(): mysqli {
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS);
if ($conn->connect_error) {
http_response_code(500);
die(json_encode(['status' => 'error', 'message' => 'Koneksi database gagal: ' . $conn->connect_error]));
}
// Check if database exists
$dbCheck = $conn->query("SHOW DATABASES LIKE '" . DB_NAME . "'");
$dbExists = ($dbCheck && $dbCheck->num_rows > 0);
if (!$dbExists) {
$conn->query("CREATE DATABASE IF NOT EXISTS " . DB_NAME . " CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
$conn->select_db(DB_NAME);
// Load db_jalan.sql to seed tables
$sqlPath = __DIR__ . '/../database/db_jalan.sql';
if (file_exists($sqlPath)) {
$sqlContent = file_get_contents($sqlPath);
// Remove USE command from raw SQL to avoid conflicts
$sqlContent = preg_replace('/USE\s+[a-zA-Z0-9_]+;/i', '', $sqlContent);
$conn->multi_query($sqlContent);
// Clear multi-query results to prevent synch error
do {
if ($res = $conn->store_result()) {
$res->free();
}
} while ($conn->next_result());
}
} else {
$conn->select_db(DB_NAME);
}
$conn->set_charset('utf8mb4');
return $conn;
}
// CORS & JSON headers — panggil sekali dari tiap API entry point
function setCorsHeaders(): void {
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
}
// Helper: baca JSON body
function getJsonInput(): ?array {
$raw = file_get_contents('php://input');
if (!$raw) return null;
$data = json_decode($raw, true);
return (json_last_error() === JSON_ERROR_NONE) ? $data : null;
}
// Helper: validasi & encode GeoJSON dari array koordinat [lat,lng]
// Frontend tetap kirim [lat,lng] (Leaflet-style), kita konversi ke GeoJSON [lng,lat]
function buildGeoJsonLine(array $coords): string {
$gjCoords = array_map(fn($p) => [(float)$p[1], (float)$p[0]], $coords);
return json_encode(['type' => 'LineString', 'coordinates' => $gjCoords]);
}
function buildGeoJsonPolygon(array $coords): string {
$gjCoords = array_map(fn($p) => [(float)$p[1], (float)$p[0]], $coords);
// tutup ring jika belum
if ($gjCoords[0] !== end($gjCoords)) $gjCoords[] = $gjCoords[0];
return json_encode(['type' => 'Polygon', 'coordinates' => [$gjCoords]]);
}
function buildGeoJsonPoint(float $lat, float $lng): string {
return json_encode(['type' => 'Point', 'coordinates' => [$lng, $lat]]);
}
// Helper: parse GeoJSON → array [lat,lng] (untuk dikirim ke frontend/Leaflet)
function parseGeoJsonToLatLng(?string $geomStr): array {
if (!$geomStr) return [];
$geom = json_decode($geomStr, true);
if (!$geom) return [];
switch ($geom['type'] ?? '') {
case 'LineString':
return array_map(fn($c) => [$c[1], $c[0]], $geom['coordinates']);
case 'Polygon':
return array_map(fn($c) => [$c[1], $c[0]], $geom['coordinates'][0]);
case 'Point':
return [$geom['coordinates'][1], $geom['coordinates'][0]];
default:
return [];
}
}

View File

@@ -0,0 +1,118 @@
-- ============================================
-- WebGIS - Manajemen Jalan, Parsil & Laporan Jalan Rusak
-- Database Schema (GeoJSON-based)
-- ============================================
CREATE DATABASE IF NOT EXISTS db_jalan CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE db_jalan;
-- ============================================
-- Tabel Data Jalan (GeoJSON LineString)
-- ============================================
CREATE TABLE IF NOT EXISTS data_jalan (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_jalan VARCHAR(255) NOT NULL,
status_jalan ENUM('Nasional','Provinsi','Kabupaten') NOT NULL,
panjang_meter DOUBLE NOT NULL DEFAULT 0,
keterangan TEXT,
-- GeoJSON LineString: {"type":"LineString","coordinates":[[lng,lat],...]}
geom JSON NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- ============================================
-- Tabel Data Parsil Tanah (GeoJSON Polygon)
-- ============================================
CREATE TABLE IF NOT EXISTS data_parsil (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_parsil VARCHAR(255) NOT NULL,
pemilik VARCHAR(255) NOT NULL,
status_sertifikat ENUM('SHM','HGB','HGU','HP') NOT NULL,
nomor_sertifikat VARCHAR(100),
luas_meter2 DOUBLE NOT NULL DEFAULT 0,
keterangan TEXT,
-- GeoJSON Polygon: {"type":"Polygon","coordinates":[[[lng,lat],...,[lng,lat]]]}
geom JSON NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- ============================================
-- Tabel Laporan Jalan Rusak (GeoJSON Point)
-- ============================================
CREATE TABLE IF NOT EXISTS laporan_jalan_rusak (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_pelapor VARCHAR(255), -- opsional
nama_jalan VARCHAR(255) NOT NULL, -- untuk analisis frekuensi per jalan
deskripsi TEXT,
foto_path VARCHAR(500), -- path relatif file foto
foto_lat DOUBLE, -- dari metadata exif atau manual
foto_lng DOUBLE, -- dari metadata exif atau manual
foto_datetime DATETIME, -- dari metadata exif
-- GeoJSON Point: {"type":"Point","coordinates":[lng,lat]}
geom JSON NOT NULL,
status ENUM('pending','verified','resolved') NOT NULL DEFAULT 'pending',
tanggal_input TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Index untuk query geospasial & analitik
CREATE INDEX idx_laporan_tanggal ON laporan_jalan_rusak(tanggal_input);
CREATE INDEX idx_laporan_nama_jalan ON laporan_jalan_rusak(nama_jalan);
CREATE INDEX idx_laporan_status ON laporan_jalan_rusak(status);
CREATE INDEX idx_jalan_status ON data_jalan(status_jalan);
CREATE INDEX idx_parsil_status ON data_parsil(status_sertifikat);
-- ============================================
-- Sample Data Jalan (GeoJSON)
-- ============================================
INSERT INTO data_jalan (nama_jalan, status_jalan, panjang_meter, keterangan, geom) VALUES
(
'Jalan Trans Kalimantan', 'Nasional', 2500.50,
'Jalan utama lintas Kalimantan',
'{"type":"LineString","coordinates":[[109.3425,0.0263],[109.3500,0.0270],[109.3580,0.0280],[109.3650,0.0290]]}'
),
(
'Jalan Soekarno-Hatta', 'Provinsi', 1800.75,
'Jalan provinsi kawasan kota',
'{"type":"LineString","coordinates":[[109.3300,0.0200],[109.3380,0.0210],[109.3460,0.0225]]}'
),
(
'Jalan Parit Baru', 'Kabupaten', 950.25,
'Jalan kabupaten menuju kawasan industri',
'{"type":"LineString","coordinates":[[109.3200,0.0150],[109.3270,0.0160],[109.3320,0.0175]]}'
);
-- ============================================
-- Sample Data Parsil (GeoJSON)
-- ============================================
INSERT INTO data_parsil (nama_parsil, pemilik, status_sertifikat, nomor_sertifikat, luas_meter2, keterangan, geom) VALUES
(
'Kavling A1', 'Budi Santoso', 'SHM', 'SHM-001/2020', 450.50,
'Kavling perumahan blok A',
'{"type":"Polygon","coordinates":[[[109.3320,0.0230],[109.3330,0.0230],[109.3330,0.0235],[109.3320,0.0235],[109.3320,0.0230]]]}'
),
(
'Kavling B3', 'PT Maju Bersama', 'HGB', 'HGB-023/2019', 1250.00,
'Kavling komersial blok B',
'{"type":"Polygon","coordinates":[[[109.3350,0.0240],[109.3365,0.0240],[109.3365,0.0250],[109.3350,0.0250],[109.3350,0.0240]]]}'
),
(
'Lahan Usaha C1', 'CV Subur Makmur', 'HGU', 'HGU-007/2018', 5800.00,
'Lahan perkebunan',
'{"type":"Polygon","coordinates":[[[109.3270,0.0180],[109.3295,0.0180],[109.3295,0.0195],[109.3270,0.0195],[109.3270,0.0180]]]}'
);
-- ============================================
-- Sample Laporan Jalan Rusak
-- ============================================
INSERT INTO laporan_jalan_rusak (nama_pelapor, nama_jalan, deskripsi, foto_lat, foto_lng, geom, status, tanggal_input) VALUES
('Ahmad Fauzi', 'Jalan Trans Kalimantan', 'Aspal berlubang besar, berbahaya saat hujan', 0.0268, 109.3460, '{"type":"Point","coordinates":[109.3460,0.0268]}', 'pending', NOW() - INTERVAL 5 DAY),
('Siti Rahayu', 'Jalan Trans Kalimantan', 'Jalan retak dan bergelombang', 0.0271, 109.3465, '{"type":"Point","coordinates":[109.3465,0.0271]}', 'pending', NOW() - INTERVAL 8 DAY),
('Budi Hartono', 'Jalan Trans Kalimantan', 'Lubang besar di tengah jalan', 0.0269, 109.3462, '{"type":"Point","coordinates":[109.3462,0.0269]}', 'verified', NOW() - INTERVAL 3 DAY),
('Dewi Lestari', 'Jalan Soekarno-Hatta', 'Marka jalan hilang, genangan air', 0.0215, 109.3385, '{"type":"Point","coordinates":[109.3385,0.0215]}', 'pending', NOW() - INTERVAL 15 DAY),
('Eko Prasetyo', 'Jalan Soekarno-Hatta', 'Bahu jalan rusak', 0.0218, 109.3388, '{"type":"Point","coordinates":[109.3388,0.0218]}', 'pending', NOW() - INTERVAL 20 DAY),
('Fitri Handayani','Jalan Parit Baru', 'Aspal terkelupas', 0.0165, 109.3275, '{"type":"Point","coordinates":[109.3275,0.0165]}', 'resolved', NOW() - INTERVAL 45 DAY),
(NULL, 'Jalan Trans Kalimantan', 'Permukaan jalan sangat rusak parah', 0.0272, 109.3468, '{"type":"Point","coordinates":[109.3468,0.0272]}', 'pending', NOW() - INTERVAL 2 DAY),
('Hendra Wijaya', 'Jalan Trans Kalimantan', 'Lubang dalam di tepi jalan', 0.0266, 109.3458, '{"type":"Point","coordinates":[109.3458,0.0266]}', 'pending', NOW() - INTERVAL 1 DAY);

1455
jalan_tanah/index.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@
# Keep this directory in version control, but ignore contents via .gitignore