Initial WebGIS portal project

This commit is contained in:
2026-06-10 19:53:39 +07:00
commit c0aa8e09d5
62 changed files with 9013 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json');
require_once __DIR__ . '/db.php';
$input = json_decode(file_get_contents('php://input'), true);
if (!is_array($input)) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => 'Format request tidak valid.',
]);
exit;
}
$nama = trim((string) ($input['nama'] ?? ''));
$no = trim((string) ($input['no'] ?? ''));
$status24jam = trim((string) ($input['status_24jam'] ?? ''));
$latitude = (float) ($input['latitude'] ?? 0);
$longitude = (float) ($input['longitude'] ?? 0);
if ($nama === '' || $no === '' || $status24jam === '') {
http_response_code(422);
echo json_encode([
'success' => false,
'message' => 'Nama, no, dan status wajib diisi.',
]);
exit;
}
if (!in_array($status24jam, ['24jam', 'tidak'], true)) {
http_response_code(422);
echo json_encode([
'success' => false,
'message' => 'Status harus 24jam atau tidak.',
]);
exit;
}
$stmt = $connection->prepare('INSERT INTO spbu_points (nama, no, status_24jam, latitude, longitude) VALUES (?, ?, ?, ?, ?)');
if (!$stmt) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Gagal menyiapkan query: ' . $connection->error,
]);
exit;
}
$stmt->bind_param('sssdd', $nama, $no, $status24jam, $latitude, $longitude);
if (!$stmt->execute()) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Gagal menyimpan data: ' . $stmt->error,
]);
$stmt->close();
exit;
}
$stmt->close();
echo json_encode([
'success' => true,
'message' => 'Data berhasil disimpan.',
'id' => $connection->insert_id,
]);
+34
View File
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
mysqli_report(MYSQLI_REPORT_OFF);
$host = getenv('SPBU_DB_HOST') ?: getenv('DB_HOST') ?: '127.0.0.1';
$username = getenv('SPBU_DB_USERNAME') ?: getenv('DB_USERNAME') ?: 'root';
$password = getenv('SPBU_DB_PASSWORD') ?: getenv('DB_PASSWORD') ?: '';
$database = getenv('SPBU_DB_DATABASE') ?: 'webgis_spbu';
try {
$connection = new mysqli($host, $username, $password, $database);
} catch (Throwable $exception) {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Koneksi database gagal. Pastikan MySQL XAMPP sedang berjalan.',
'detail' => $exception->getMessage(),
]);
exit;
}
if (!$connection || $connection->connect_errno) {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Koneksi database gagal. Pastikan MySQL XAMPP sedang berjalan.',
]);
exit;
}
$connection->set_charset('utf8mb4');
+42
View File
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
$host = getenv('SPBU_DB_HOST') ?: getenv('DB_HOST') ?: '127.0.0.1';
$database = getenv('SPBU_DB_DATABASE') ?: 'webgis_spbu';
$username = getenv('SPBU_DB_USERNAME') ?: getenv('DB_USERNAME') ?: 'root';
$password = getenv('SPBU_DB_PASSWORD') ?: getenv('DB_PASSWORD') ?: '';
$charset = getenv('DB_CHARSET') ?: 'utf8mb4';
set_exception_handler(static function (Throwable $exception): void {
if (!headers_sent()) {
http_response_code(500);
header('Content-Type: application/json');
}
echo json_encode([
'success' => false,
'message' => 'Terjadi kesalahan server.',
'detail' => $exception->getMessage(),
]);
exit;
});
$dsn = "mysql:host={$host};dbname={$database};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, $username, $password, $options);
} catch (PDOException $exception) {
http_response_code(500);
header('Content-Type: application/json');
echo json_encode([
'success' => false,
'message' => 'Koneksi database gagal: ' . $exception->getMessage(),
]);
exit;
}
+68
View File
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json');
require_once __DIR__ . '/db.php';
$input = json_decode(file_get_contents('php://input'), true);
if (!is_array($input)) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => 'Format request tidak valid.',
]);
exit;
}
$id = (int) ($input['id'] ?? 0);
if ($id <= 0) {
http_response_code(422);
echo json_encode([
'success' => false,
'message' => 'ID titik tidak valid.',
]);
exit;
}
$stmt = $connection->prepare('DELETE FROM spbu_points WHERE id = ?');
if (!$stmt) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Gagal menyiapkan query: ' . $connection->error,
]);
exit;
}
$stmt->bind_param('i', $id);
if (!$stmt->execute()) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Gagal menghapus data: ' . $stmt->error,
]);
$stmt->close();
exit;
}
if ($stmt->affected_rows < 1) {
http_response_code(404);
echo json_encode([
'success' => false,
'message' => 'Data dengan ID tersebut tidak ditemukan.',
]);
$stmt->close();
exit;
}
$stmt->close();
echo json_encode([
'success' => true,
'message' => 'Data berhasil dihapus.',
]);
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db_pdo.php';
require_once __DIR__ . '/spatial_helpers.php';
$input = get_json_input();
$namaPemilik = trim((string) ($input['nama_pemilik'] ?? ''));
$statusKepemilikan = trim((string) ($input['status_kepemilikan'] ?? ''));
$luasM2 = (float) ($input['luas_m2'] ?? 0);
$wkt = sanitize_wkt((string) ($input['wkt'] ?? ''), 'POLYGON');
$allowedStatus = ['SHM', 'HGB', 'HGU', 'HP'];
if ($namaPemilik === '' || !in_array($statusKepemilikan, $allowedStatus, true)) {
json_response(422, [
'success' => false,
'message' => 'Nama pemilik wajib diisi dan status harus SHM/HGB/HGU/HP.',
]);
}
if ($luasM2 <= 0) {
json_response(422, [
'success' => false,
'message' => 'Luas tanah harus lebih dari 0 m2.',
]);
}
$sql = 'INSERT INTO parsil_tanah (nama_pemilik, status_kepemilikan, luas_m2, geom) VALUES (:nama_pemilik, :status_kepemilikan, :luas_m2, ST_GeomFromText(:wkt))';
$stmt = $pdo->prepare($sql);
$stmt->execute([
':nama_pemilik' => $namaPemilik,
':status_kepemilikan' => $statusKepemilikan,
':luas_m2' => $luasM2,
':wkt' => $wkt,
]);
json_response(201, [
'success' => true,
'message' => 'Data parsil tanah berhasil disimpan.',
'id' => (int) $pdo->lastInsertId(),
]);
+30
View File
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db_pdo.php';
require_once __DIR__ . '/spatial_helpers.php';
$input = get_json_input();
$id = (int) ($input['id'] ?? 0);
if ($id <= 0) {
json_response(422, [
'success' => false,
'message' => 'ID parsil tidak valid.',
]);
}
$stmt = $pdo->prepare('DELETE FROM parsil_tanah WHERE id = :id');
$stmt->execute([':id' => $id]);
if ($stmt->rowCount() < 1) {
json_response(404, [
'success' => false,
'message' => 'Data parsil tidak ditemukan.',
]);
}
json_response(200, [
'success' => true,
'message' => 'Data parsil tanah berhasil dihapus.',
]);
+22
View File
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db_pdo.php';
require_once __DIR__ . '/spatial_helpers.php';
$sql = 'SELECT id, nama_pemilik, status_kepemilikan, luas_m2, ST_AsGeoJSON(geom) AS geometry_geojson, created_at, updated_at FROM parsil_tanah ORDER BY id DESC';
$stmt = $pdo->query($sql);
$rows = $stmt->fetchAll();
$data = [];
foreach ($rows as $row) {
$row['id'] = (int) $row['id'];
$row['luas_m2'] = (float) $row['luas_m2'];
$row['geometry_geojson'] = json_decode((string) $row['geometry_geojson'], true);
$data[] = $row;
}
json_response(200, [
'success' => true,
'data' => $data,
]);
+62
View File
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db_pdo.php';
require_once __DIR__ . '/spatial_helpers.php';
$input = get_json_input();
$id = (int) ($input['id'] ?? 0);
$namaPemilik = trim((string) ($input['nama_pemilik'] ?? ''));
$statusKepemilikan = trim((string) ($input['status_kepemilikan'] ?? ''));
$luasM2 = (float) ($input['luas_m2'] ?? 0);
$wkt = sanitize_wkt((string) ($input['wkt'] ?? ''), 'POLYGON');
$allowedStatus = ['SHM', 'HGB', 'HGU', 'HP'];
if ($id <= 0) {
json_response(422, [
'success' => false,
'message' => 'ID parsil tidak valid.',
]);
}
if ($namaPemilik === '' || !in_array($statusKepemilikan, $allowedStatus, true)) {
json_response(422, [
'success' => false,
'message' => 'Nama pemilik wajib diisi dan status harus SHM/HGB/HGU/HP.',
]);
}
if ($luasM2 <= 0) {
json_response(422, [
'success' => false,
'message' => 'Luas tanah harus lebih dari 0 m2.',
]);
}
$sql = 'UPDATE parsil_tanah SET nama_pemilik = :nama_pemilik, status_kepemilikan = :status_kepemilikan, luas_m2 = :luas_m2, geom = ST_GeomFromText(:wkt), updated_at = CURRENT_TIMESTAMP WHERE id = :id';
$stmt = $pdo->prepare($sql);
$stmt->execute([
':id' => $id,
':nama_pemilik' => $namaPemilik,
':status_kepemilikan' => $statusKepemilikan,
':luas_m2' => $luasM2,
':wkt' => $wkt,
]);
if ($stmt->rowCount() < 1) {
$checkStmt = $pdo->prepare('SELECT id FROM parsil_tanah WHERE id = :id LIMIT 1');
$checkStmt->execute([':id' => $id]);
if (!$checkStmt->fetch()) {
json_response(404, [
'success' => false,
'message' => 'Data parsil tidak ditemukan.',
]);
}
}
json_response(200, [
'success' => true,
'message' => 'Data parsil tanah berhasil diperbarui.',
]);
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json');
require_once __DIR__ . '/db.php';
$sql = 'SELECT id, nama, no, status_24jam, latitude, longitude, created_at FROM spbu_points ORDER BY id DESC';
$result = $connection->query($sql);
if (!$result) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Gagal mengambil data: ' . $connection->error,
]);
exit;
}
$points = [];
while ($row = $result->fetch_assoc()) {
$points[] = $row;
}
echo json_encode([
'success' => true,
'data' => $points,
]);
+43
View File
@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db_pdo.php';
require_once __DIR__ . '/spatial_helpers.php';
$input = get_json_input();
$namaJalan = trim((string) ($input['nama_jalan'] ?? ''));
$status = trim((string) ($input['status'] ?? ''));
$panjangMeter = (float) ($input['panjang_meter'] ?? 0);
$wkt = sanitize_wkt((string) ($input['wkt'] ?? ''), 'LINESTRING');
$allowedStatus = ['nasional', 'provinsi', 'kabupaten'];
if ($namaJalan === '' || !in_array($status, $allowedStatus, true)) {
json_response(422, [
'success' => false,
'message' => 'Nama jalan wajib diisi dan status harus nasional/provinsi/kabupaten.',
]);
}
if ($panjangMeter <= 0) {
json_response(422, [
'success' => false,
'message' => 'Panjang jalan harus lebih dari 0 meter.',
]);
}
$sql = 'INSERT INTO jalan (nama_jalan, status, panjang_meter, geom) VALUES (:nama_jalan, :status, :panjang_meter, ST_GeomFromText(:wkt))';
$stmt = $pdo->prepare($sql);
$stmt->execute([
':nama_jalan' => $namaJalan,
':status' => $status,
':panjang_meter' => $panjangMeter,
':wkt' => $wkt,
]);
json_response(201, [
'success' => true,
'message' => 'Data jalan berhasil disimpan.',
'id' => (int) $pdo->lastInsertId(),
]);
+30
View File
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db_pdo.php';
require_once __DIR__ . '/spatial_helpers.php';
$input = get_json_input();
$id = (int) ($input['id'] ?? 0);
if ($id <= 0) {
json_response(422, [
'success' => false,
'message' => 'ID jalan tidak valid.',
]);
}
$stmt = $pdo->prepare('DELETE FROM jalan WHERE id = :id');
$stmt->execute([':id' => $id]);
if ($stmt->rowCount() < 1) {
json_response(404, [
'success' => false,
'message' => 'Data jalan tidak ditemukan.',
]);
}
json_response(200, [
'success' => true,
'message' => 'Data jalan berhasil dihapus.',
]);
+22
View File
@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db_pdo.php';
require_once __DIR__ . '/spatial_helpers.php';
$sql = 'SELECT id, nama_jalan, status, panjang_meter, ST_AsGeoJSON(geom) AS geometry_geojson, created_at, updated_at FROM jalan ORDER BY id DESC';
$stmt = $pdo->query($sql);
$rows = $stmt->fetchAll();
$data = [];
foreach ($rows as $row) {
$row['id'] = (int) $row['id'];
$row['panjang_meter'] = (float) $row['panjang_meter'];
$row['geometry_geojson'] = json_decode((string) $row['geometry_geojson'], true);
$data[] = $row;
}
json_response(200, [
'success' => true,
'data' => $data,
]);
+62
View File
@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db_pdo.php';
require_once __DIR__ . '/spatial_helpers.php';
$input = get_json_input();
$id = (int) ($input['id'] ?? 0);
$namaJalan = trim((string) ($input['nama_jalan'] ?? ''));
$status = trim((string) ($input['status'] ?? ''));
$panjangMeter = (float) ($input['panjang_meter'] ?? 0);
$wkt = sanitize_wkt((string) ($input['wkt'] ?? ''), 'LINESTRING');
$allowedStatus = ['nasional', 'provinsi', 'kabupaten'];
if ($id <= 0) {
json_response(422, [
'success' => false,
'message' => 'ID jalan tidak valid.',
]);
}
if ($namaJalan === '' || !in_array($status, $allowedStatus, true)) {
json_response(422, [
'success' => false,
'message' => 'Nama jalan wajib diisi dan status harus nasional/provinsi/kabupaten.',
]);
}
if ($panjangMeter <= 0) {
json_response(422, [
'success' => false,
'message' => 'Panjang jalan harus lebih dari 0 meter.',
]);
}
$sql = 'UPDATE jalan SET nama_jalan = :nama_jalan, status = :status, panjang_meter = :panjang_meter, geom = ST_GeomFromText(:wkt), updated_at = CURRENT_TIMESTAMP WHERE id = :id';
$stmt = $pdo->prepare($sql);
$stmt->execute([
':id' => $id,
':nama_jalan' => $namaJalan,
':status' => $status,
':panjang_meter' => $panjangMeter,
':wkt' => $wkt,
]);
if ($stmt->rowCount() < 1) {
$checkStmt = $pdo->prepare('SELECT id FROM jalan WHERE id = :id LIMIT 1');
$checkStmt->execute([':id' => $id]);
if (!$checkStmt->fetch()) {
json_response(404, [
'success' => false,
'message' => 'Data jalan tidak ditemukan.',
]);
}
}
json_response(200, [
'success' => true,
'message' => 'Data jalan berhasil diperbarui.',
]);
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
function json_response(int $statusCode, array $payload): void
{
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($payload);
exit;
}
function get_json_input(): array
{
$input = json_decode(file_get_contents('php://input'), true);
if (!is_array($input)) {
json_response(400, [
'success' => false,
'message' => 'Format request tidak valid.',
]);
}
return $input;
}
function sanitize_wkt(string $wkt, string $expectedType): string
{
$trimmed = trim($wkt);
if ($trimmed === '') {
json_response(422, [
'success' => false,
'message' => 'Geometri WKT wajib diisi.',
]);
}
$normalized = strtoupper($trimmed);
if (strpos($normalized, $expectedType . '(') !== 0 && strpos($normalized, $expectedType . ' (') !== 0) {
json_response(422, [
'success' => false,
'message' => 'Tipe geometri tidak sesuai. Diharapkan ' . $expectedType . '.',
]);
}
return $trimmed;
}
+110
View File
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
header('Content-Type: application/json');
require_once __DIR__ . '/db.php';
$input = json_decode(file_get_contents('php://input'), true);
if (!is_array($input)) {
http_response_code(400);
echo json_encode([
'success' => false,
'message' => 'Format request tidak valid.',
]);
exit;
}
$id = (int) ($input['id'] ?? 0);
$latitude = (float) ($input['latitude'] ?? 0);
$longitude = (float) ($input['longitude'] ?? 0);
if ($id <= 0) {
http_response_code(422);
echo json_encode([
'success' => false,
'message' => 'ID titik tidak valid.',
]);
exit;
}
if (!is_finite($latitude) || !is_finite($longitude)) {
http_response_code(422);
echo json_encode([
'success' => false,
'message' => 'Koordinat latitude atau longitude tidak valid.',
]);
exit;
}
$stmt = $connection->prepare('UPDATE spbu_points SET latitude = ?, longitude = ? WHERE id = ?');
if (!$stmt) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Gagal menyiapkan query update: ' . $connection->error,
]);
exit;
}
$stmt->bind_param('ddi', $latitude, $longitude, $id);
if (!$stmt->execute()) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Gagal memperbarui posisi: ' . $stmt->error,
]);
$stmt->close();
exit;
}
if ($stmt->affected_rows < 1) {
$stmt->close();
$checkStmt = $connection->prepare('SELECT id FROM spbu_points WHERE id = ? LIMIT 1');
if (!$checkStmt) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Gagal memverifikasi data titik: ' . $connection->error,
]);
exit;
}
$checkStmt->bind_param('i', $id);
if (!$checkStmt->execute()) {
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Gagal memverifikasi data titik: ' . $checkStmt->error,
]);
$checkStmt->close();
exit;
}
$checkResult = $checkStmt->get_result();
if ($checkResult->num_rows < 1) {
http_response_code(404);
echo json_encode([
'success' => false,
'message' => 'Data dengan ID tersebut tidak ditemukan.',
]);
$checkStmt->close();
exit;
}
$checkStmt->close();
}
$stmt->close();
echo json_encode([
'success' => true,
'message' => 'Posisi titik berhasil diperbarui.',
]);
File diff suppressed because one or more lines are too long
+155
View File
@@ -0,0 +1,155 @@
(function () {
'use strict';
const DEFAULT_SOURCE = 'assets/data/Kecamatan.json';
const DEFAULT_ANCHOR = {
lat: -0.06054796552220232,
lng: 109.34490231930592
};
const DEFAULT_GEO_BOUNDS = {
south: -0.0981948,
west: 109.2741676,
north: 0.0381168,
east: 109.3853475
};
const DEFAULT_CALIBRATION = {
scaleX: 1,
scaleY: 1,
offsetLat: 0,
offsetLng: 0
};
function walkCoordinates(coordinates, callback) {
if (!Array.isArray(coordinates)) {
return;
}
if (coordinates.length >= 2 && typeof coordinates[0] === 'number' && typeof coordinates[1] === 'number') {
callback(coordinates[0], coordinates[1]);
return;
}
coordinates.forEach((child) => walkCoordinates(child, callback));
}
function getCoordinateBounds(data) {
const bounds = {
minX: Infinity,
minY: Infinity,
maxX: -Infinity,
maxY: -Infinity
};
function addGeometry(geometry) {
if (!geometry) {
return;
}
if (geometry.type === 'GeometryCollection') {
(geometry.geometries || []).forEach(addGeometry);
return;
}
walkCoordinates(geometry.coordinates, (x, y) => {
bounds.minX = Math.min(bounds.minX, x);
bounds.minY = Math.min(bounds.minY, y);
bounds.maxX = Math.max(bounds.maxX, x);
bounds.maxY = Math.max(bounds.maxY, y);
});
}
if (data.type === 'FeatureCollection') {
(data.features || []).forEach((feature) => addGeometry(feature.geometry));
} else if (data.type === 'Feature') {
addGeometry(data.geometry);
} else {
addGeometry(data);
}
if (!Number.isFinite(bounds.minX) || !Number.isFinite(bounds.minY)) {
return null;
}
return bounds;
}
function createBoundingBoxTransform(sourceBounds, targetBounds, calibration) {
const sourceWidth = sourceBounds.maxX - sourceBounds.minX;
const sourceHeight = sourceBounds.maxY - sourceBounds.minY;
const targetCenterLng = (targetBounds.west + targetBounds.east) / 2 + calibration.offsetLng;
const targetCenterLat = (targetBounds.south + targetBounds.north) / 2 + calibration.offsetLat;
const targetWidth = (targetBounds.east - targetBounds.west) * calibration.scaleX;
const targetHeight = (targetBounds.north - targetBounds.south) * calibration.scaleY;
return function coordsToLatLng(coordinates) {
const xRatio = ((coordinates[0] - sourceBounds.minX) / sourceWidth) - 0.5;
const yRatio = ((coordinates[1] - sourceBounds.minY) / sourceHeight) - 0.5;
const lng = targetCenterLng + (xRatio * targetWidth);
const lat = targetCenterLat + (yRatio * targetHeight);
return L.latLng(lat, lng);
};
}
window.addKecamatanLayer = async function addKecamatanLayer(map, options) {
const settings = Object.assign({
source: DEFAULT_SOURCE,
anchor: DEFAULT_ANCHOR,
targetBounds: DEFAULT_GEO_BOUNDS,
calibration: DEFAULT_CALIBRATION,
fitBounds: false
}, options || {});
if (!map || typeof L === 'undefined') {
return null;
}
if (!map.getPane('kecamatanPane')) {
map.createPane('kecamatanPane');
map.getPane('kecamatanPane').style.zIndex = 350;
}
const response = await fetch(settings.source, {
headers: { 'Accept': 'application/json' }
});
if (!response.ok) {
throw new Error('Gagal memuat data batas kecamatan.');
}
const data = await response.json();
const coordinateBounds = getCoordinateBounds(data);
if (!coordinateBounds) {
throw new Error('Data batas kecamatan tidak memiliki koordinat valid.');
}
const layer = L.geoJSON(data, {
coordsToLatLng: createBoundingBoxTransform(coordinateBounds, settings.targetBounds, settings.calibration),
pane: 'kecamatanPane',
style: {
color: '#0f766e',
weight: 2,
opacity: 0.9,
fillColor: '#14b8a6',
fillOpacity: 0.08,
dashArray: '6 4'
},
onEachFeature: function (feature, featureLayer) {
const name = feature && feature.properties && (
feature.properties.nama ||
feature.properties.NAMA ||
feature.properties.KECAMATAN ||
feature.properties.kecamatan
);
featureLayer.bindPopup(name ? `Kecamatan: ${name}` : 'Batas Kecamatan');
}
}).addTo(map);
if (settings.fitBounds && layer.getBounds().isValid()) {
map.fitBounds(layer.getBounds().pad(0.12), { maxZoom: 15 });
}
return layer;
};
}());
File diff suppressed because it is too large Load Diff
+622
View File
@@ -0,0 +1,622 @@
function setStatus(element, message, type) {
element.textContent = message;
element.classList.remove('error', 'success');
if (type) {
element.classList.add(type);
}
}
function roadColorByStatus(status) {
if (status === 'nasional') {
return '#dc2626';
}
if (status === 'provinsi') {
return '#2563eb';
}
return '#16a34a';
}
function parcelColorByStatus(status) {
if (status === 'SHM') {
return '#ea580c';
}
if (status === 'HGB') {
return '#0ea5e9';
}
if (status === 'HGU') {
return '#22c55e';
}
return '#a855f7';
}
function calculatePolylineLengthMeters(latlngs) {
let total = 0;
for (let index = 1; index < latlngs.length; index += 1) {
total += latlngs[index - 1].distanceTo(latlngs[index]);
}
return total;
}
function calculatePolygonAreaMeters(latlngs) {
if (window.L && L.GeometryUtil && typeof L.GeometryUtil.geodesicArea === 'function') {
return L.GeometryUtil.geodesicArea(latlngs);
}
const earthRadius = 6378137;
const toRad = (degree) => degree * Math.PI / 180;
let area = 0;
for (let i = 0; i < latlngs.length; i += 1) {
const p1 = latlngs[i];
const p2 = latlngs[(i + 1) % latlngs.length];
area += toRad(p2.lng - p1.lng) * (2 + Math.sin(toRad(p1.lat)) + Math.sin(toRad(p2.lat)));
}
return Math.abs(area * earthRadius * earthRadius / 2);
}
function polylineToWkt(latlngs) {
const coordinates = latlngs.map((point) => `${point.lng} ${point.lat}`).join(', ');
return `LINESTRING(${coordinates})`;
}
function polygonToWkt(latlngs) {
const ring = latlngs.map((point) => `${point.lng} ${point.lat}`);
if (ring[0] !== ring[ring.length - 1]) {
ring.push(ring[0]);
}
return `POLYGON((${ring.join(', ')}))`;
}
async function parseJsonResponse(response) {
const text = await response.text();
try {
return JSON.parse(text);
} catch (error) {
throw new Error('Response server bukan JSON valid.');
}
}
document.addEventListener('DOMContentLoaded', async () => {
const map = L.map('map').setView([-0.0605, 109.3449], 13);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; OpenStreetMap contributors'
}).addTo(map);
if (typeof window.addKecamatanLayer === 'function') {
window.addKecamatanLayer(map, { fitBounds: true }).catch((error) => {
console.warn('Data kecamatan belum termuat:', error);
});
}
const editableLayer = new L.FeatureGroup();
map.addLayer(editableLayer);
const modalBackdrop = document.getElementById('modal-backdrop');
const modalTitle = document.getElementById('modal-title');
const featureForm = document.getElementById('feature-form');
const roadFields = document.getElementById('road-fields');
const parcelFields = document.getElementById('parcel-fields');
const formStatus = document.getElementById('form-status');
const cancelButton = document.getElementById('btn-cancel');
let pendingLayer = null;
let pendingType = null;
let pendingMode = 'create';
function closeModal(shouldRemovePendingLayer) {
modalBackdrop.classList.remove('open');
modalBackdrop.setAttribute('aria-hidden', 'true');
setStatus(formStatus, '', '');
if (shouldRemovePendingLayer && pendingMode === 'create' && pendingLayer) {
editableLayer.removeLayer(pendingLayer);
}
pendingLayer = null;
pendingType = null;
pendingMode = 'create';
featureForm.reset();
}
function openRoadModal(layer, mode, initialData) {
pendingLayer = layer;
pendingType = 'road';
pendingMode = mode;
modalTitle.textContent = mode === 'create' ? 'Tambah Data Jalan' : 'Update Data Jalan';
roadFields.hidden = false;
parcelFields.hidden = true;
const latlngs = layer.getLatLngs();
const lengthMeters = calculatePolylineLengthMeters(latlngs);
document.getElementById('nama_jalan').value = initialData && initialData.nama_jalan ? initialData.nama_jalan : '';
document.getElementById('status_jalan').value = initialData && initialData.status ? initialData.status : 'nasional';
document.getElementById('panjang_meter').value = lengthMeters.toFixed(2);
modalBackdrop.classList.add('open');
modalBackdrop.setAttribute('aria-hidden', 'false');
}
function openParcelModal(layer, mode, initialData) {
pendingLayer = layer;
pendingType = 'parcel';
pendingMode = mode;
modalTitle.textContent = mode === 'create' ? 'Tambah Data Parsil Tanah' : 'Update Data Parsil Tanah';
roadFields.hidden = true;
parcelFields.hidden = false;
const latlngs = layer.getLatLngs()[0];
const areaM2 = calculatePolygonAreaMeters(latlngs);
document.getElementById('nama_pemilik').value = initialData && initialData.nama_pemilik ? initialData.nama_pemilik : '';
document.getElementById('status_kepemilikan').value = initialData && initialData.status_kepemilikan ? initialData.status_kepemilikan : 'SHM';
document.getElementById('luas_m2').value = areaM2.toFixed(2);
modalBackdrop.classList.add('open');
modalBackdrop.setAttribute('aria-hidden', 'false');
}
function attachRoadPopup(layer) {
const props = layer.featureProps;
layer.setStyle({
color: roadColorByStatus(props.status),
weight: 5
});
layer.bindPopup(`
<b>Jalan:</b> ${props.nama_jalan}<br>
<b>Status:</b> ${props.status}<br>
<b>Panjang:</b> ${Number(props.panjang_meter).toFixed(2)} m
<div class="popup-actions">
<button type="button" data-action="edit">Edit</button>
<button type="button" class="btn-danger" data-action="delete">Hapus</button>
</div>
<div class="status" data-role="popup-status"></div>
`);
layer.on('popupopen', (event) => {
const popupElement = event.popup.getElement();
const editButton = popupElement.querySelector('[data-action="edit"]');
const deleteButton = popupElement.querySelector('[data-action="delete"]');
const popupStatus = popupElement.querySelector('[data-role="popup-status"]');
editButton.addEventListener('click', () => {
map.closePopup();
openRoadModal(layer, 'update', layer.featureProps);
});
deleteButton.addEventListener('click', async () => {
if (!window.confirm('Hapus data jalan ini?')) {
return;
}
setStatus(popupStatus, 'Menghapus data...', '');
deleteButton.disabled = true;
try {
const response = await fetch('api/road_delete.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ id: layer.featureId })
});
const result = await parseJsonResponse(response);
if (!response.ok || !result.success) {
throw new Error(result.message || 'Gagal menghapus data jalan.');
}
editableLayer.removeLayer(layer);
map.closePopup();
} catch (error) {
setStatus(popupStatus, error.message || 'Gagal menghapus data.', 'error');
deleteButton.disabled = false;
}
});
});
}
function attachParcelPopup(layer) {
const props = layer.featureProps;
layer.setStyle({
color: parcelColorByStatus(props.status_kepemilikan),
fillColor: parcelColorByStatus(props.status_kepemilikan),
fillOpacity: 0.35,
weight: 3
});
layer.bindPopup(`
<b>Pemilik:</b> ${props.nama_pemilik}<br>
<b>Status:</b> ${props.status_kepemilikan}<br>
<b>Luas:</b> ${Number(props.luas_m2).toFixed(2)}
<div class="popup-actions">
<button type="button" data-action="edit">Edit</button>
<button type="button" class="btn-danger" data-action="delete">Hapus</button>
</div>
<div class="status" data-role="popup-status"></div>
`);
layer.on('popupopen', (event) => {
const popupElement = event.popup.getElement();
const editButton = popupElement.querySelector('[data-action="edit"]');
const deleteButton = popupElement.querySelector('[data-action="delete"]');
const popupStatus = popupElement.querySelector('[data-role="popup-status"]');
editButton.addEventListener('click', () => {
map.closePopup();
openParcelModal(layer, 'update', layer.featureProps);
});
deleteButton.addEventListener('click', async () => {
if (!window.confirm('Hapus data parsil ini?')) {
return;
}
setStatus(popupStatus, 'Menghapus data...', '');
deleteButton.disabled = true;
try {
const response = await fetch('api/parcel_delete.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ id: layer.featureId })
});
const result = await parseJsonResponse(response);
if (!response.ok || !result.success) {
throw new Error(result.message || 'Gagal menghapus data parsil.');
}
editableLayer.removeLayer(layer);
map.closePopup();
} catch (error) {
setStatus(popupStatus, error.message || 'Gagal menghapus data.', 'error');
deleteButton.disabled = false;
}
});
});
}
async function loadRoads() {
const response = await fetch('api/road_read.php', {
headers: {
'Accept': 'application/json'
}
});
const result = await parseJsonResponse(response);
if (!response.ok || !result.success) {
throw new Error(result.message || 'Gagal memuat data jalan.');
}
result.data.forEach((item) => {
if (!item.geometry_geojson) {
return;
}
const layer = L.geoJSON(item.geometry_geojson).getLayers()[0];
if (!layer) {
return;
}
layer.featureType = 'road';
layer.featureId = item.id;
layer.featureProps = {
nama_jalan: item.nama_jalan,
status: item.status,
panjang_meter: item.panjang_meter
};
attachRoadPopup(layer);
editableLayer.addLayer(layer);
});
}
async function loadParcels() {
const response = await fetch('api/parcel_read.php', {
headers: {
'Accept': 'application/json'
}
});
const result = await parseJsonResponse(response);
if (!response.ok || !result.success) {
throw new Error(result.message || 'Gagal memuat data parsil.');
}
result.data.forEach((item) => {
if (!item.geometry_geojson) {
return;
}
const layer = L.geoJSON(item.geometry_geojson).getLayers()[0];
if (!layer) {
return;
}
layer.featureType = 'parcel';
layer.featureId = item.id;
layer.featureProps = {
nama_pemilik: item.nama_pemilik,
status_kepemilikan: item.status_kepemilikan,
luas_m2: item.luas_m2
};
attachParcelPopup(layer);
editableLayer.addLayer(layer);
});
}
const drawControl = new L.Control.Draw({
position: 'topleft',
draw: {
polyline: {
shapeOptions: {
color: '#dc2626',
weight: 5
}
},
polygon: {
allowIntersection: false,
showArea: true,
shapeOptions: {
color: '#ea580c',
fillOpacity: 0.35,
weight: 3
}
},
rectangle: false,
circle: false,
marker: false,
circlemarker: false
},
edit: {
featureGroup: editableLayer,
remove: true
}
});
map.addControl(drawControl);
map.on(L.Draw.Event.CREATED, (event) => {
const layer = event.layer;
const geometryType = event.layerType;
editableLayer.addLayer(layer);
if (geometryType === 'polyline') {
openRoadModal(layer, 'create', null);
return;
}
if (geometryType === 'polygon') {
openParcelModal(layer, 'create', null);
return;
}
editableLayer.removeLayer(layer);
});
map.on(L.Draw.Event.EDITED, async (event) => {
const layers = event.layers.getLayers();
for (const layer of layers) {
try {
if (layer.featureType === 'road') {
const lengthMeters = calculatePolylineLengthMeters(layer.getLatLngs());
const payload = {
id: layer.featureId,
nama_jalan: layer.featureProps.nama_jalan,
status: layer.featureProps.status,
panjang_meter: Number(lengthMeters.toFixed(2)),
wkt: polylineToWkt(layer.getLatLngs())
};
const response = await fetch('api/road_update.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
const result = await parseJsonResponse(response);
if (!response.ok || !result.success) {
throw new Error(result.message || 'Gagal update geometri jalan.');
}
layer.featureProps.panjang_meter = payload.panjang_meter;
attachRoadPopup(layer);
}
if (layer.featureType === 'parcel') {
const areaM2 = calculatePolygonAreaMeters(layer.getLatLngs()[0]);
const payload = {
id: layer.featureId,
nama_pemilik: layer.featureProps.nama_pemilik,
status_kepemilikan: layer.featureProps.status_kepemilikan,
luas_m2: Number(areaM2.toFixed(2)),
wkt: polygonToWkt(layer.getLatLngs()[0])
};
const response = await fetch('api/parcel_update.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
const result = await parseJsonResponse(response);
if (!response.ok || !result.success) {
throw new Error(result.message || 'Gagal update geometri parsil.');
}
layer.featureProps.luas_m2 = payload.luas_m2;
attachParcelPopup(layer);
}
} catch (error) {
window.alert(error.message || 'Gagal menyimpan update geometri.');
}
}
});
map.on(L.Draw.Event.DELETED, async (event) => {
const layers = event.layers.getLayers();
for (const layer of layers) {
if (!layer.featureType || !layer.featureId) {
continue;
}
try {
const endpoint = layer.featureType === 'road' ? 'api/road_delete.php' : 'api/parcel_delete.php';
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({ id: layer.featureId })
});
const result = await parseJsonResponse(response);
if (!response.ok || !result.success) {
throw new Error(result.message || 'Gagal menghapus data.');
}
} catch (error) {
window.alert(error.message || 'Gagal sinkron delete ke database.');
}
}
});
featureForm.addEventListener('submit', async (event) => {
event.preventDefault();
if (!pendingLayer || !pendingType) {
return;
}
setStatus(formStatus, 'Menyimpan data...', '');
try {
if (pendingType === 'road') {
const namaJalan = document.getElementById('nama_jalan').value.trim();
const statusJalan = document.getElementById('status_jalan').value;
const panjangMeter = Number(document.getElementById('panjang_meter').value);
const wkt = polylineToWkt(pendingLayer.getLatLngs());
const endpoint = pendingMode === 'create' ? 'api/road_create.php' : 'api/road_update.php';
const payload = {
nama_jalan: namaJalan,
status: statusJalan,
panjang_meter: panjangMeter,
wkt: wkt
};
if (pendingMode === 'update') {
payload.id = pendingLayer.featureId;
}
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
const result = await parseJsonResponse(response);
if (!response.ok || !result.success) {
throw new Error(result.message || 'Gagal menyimpan data jalan.');
}
pendingLayer.featureType = 'road';
pendingLayer.featureId = pendingMode === 'create' ? result.id : pendingLayer.featureId;
pendingLayer.featureProps = {
nama_jalan: namaJalan,
status: statusJalan,
panjang_meter: panjangMeter
};
attachRoadPopup(pendingLayer);
}
if (pendingType === 'parcel') {
const namaPemilik = document.getElementById('nama_pemilik').value.trim();
const statusKepemilikan = document.getElementById('status_kepemilikan').value;
const luasM2 = Number(document.getElementById('luas_m2').value);
const wkt = polygonToWkt(pendingLayer.getLatLngs()[0]);
const endpoint = pendingMode === 'create' ? 'api/parcel_create.php' : 'api/parcel_update.php';
const payload = {
nama_pemilik: namaPemilik,
status_kepemilikan: statusKepemilikan,
luas_m2: luasM2,
wkt: wkt
};
if (pendingMode === 'update') {
payload.id = pendingLayer.featureId;
}
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(payload)
});
const result = await parseJsonResponse(response);
if (!response.ok || !result.success) {
throw new Error(result.message || 'Gagal menyimpan data parsil.');
}
pendingLayer.featureType = 'parcel';
pendingLayer.featureId = pendingMode === 'create' ? result.id : pendingLayer.featureId;
pendingLayer.featureProps = {
nama_pemilik: namaPemilik,
status_kepemilikan: statusKepemilikan,
luas_m2: luasM2
};
attachParcelPopup(pendingLayer);
}
setStatus(formStatus, 'Data berhasil disimpan.', 'success');
setTimeout(() => closeModal(false), 350);
} catch (error) {
setStatus(formStatus, error.message || 'Gagal menyimpan data.', 'error');
}
});
cancelButton.addEventListener('click', () => closeModal(true));
modalBackdrop.addEventListener('click', (event) => {
if (event.target === modalBackdrop) {
closeModal(true);
}
});
try {
await loadRoads();
await loadParcels();
if (editableLayer.getLayers().length > 0) {
map.fitBounds(editableLayer.getBounds().pad(0.2));
}
} catch (error) {
window.alert(error.message || 'Gagal memuat data spasial.');
}
});
+372
View File
@@ -0,0 +1,372 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebGIS SPBU</title>
<link rel="stylesheet" href="assets/vendor/leaflet/leaflet.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css">
<style>
:root {
--bg-top: #f0f9ff;
--bg-bottom: #e2e8f0;
--panel: rgba(255, 255, 255, 0.92);
--line: #cbd5e1;
--text: #0f172a;
--muted: #334155;
--accent: #16a34a;
--accent-dark: #166534;
}
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
margin: 0;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(180deg, var(--bg-top) 0%, var(--bg-bottom) 100%);
color: var(--text);
overflow: hidden;
}
.app {
height: 100dvh;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
padding: 14px 18px;
border-bottom: 1px solid var(--line);
background: var(--panel);
backdrop-filter: blur(4px);
z-index: 500;
}
.header h1 {
margin: 0 0 6px;
font-size: 20px;
line-height: 1.2;
}
.header p {
margin: 0;
color: var(--muted);
font-size: 14px;
}
.legend {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 8px;
font-size: 12px;
color: #223055;
}
.legend span {
display: inline-flex;
align-items: center;
gap: 6px;
background: #ffffff;
border: 1px solid var(--line);
border-radius: 999px;
padding: 5px 10px;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
}
.map-wrap {
flex: 1;
min-height: 0;
position: relative;
}
#map {
height: 100%;
width: 100%;
background: #dbeafe;
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: #ffffff;
color: var(--text);
}
.leaflet-popup-content {
margin: 12px 14px;
}
.form-popup {
min-width: 260px;
}
.form-popup h3 {
margin: 0 0 12px;
font-size: 16px;
}
.field {
margin-bottom: 10px;
}
.field label {
display: block;
font-size: 12px;
margin-bottom: 5px;
color: var(--muted);
}
.field input,
.field select {
width: 100%;
padding: 9px 10px;
border: 1px solid var(--line);
border-radius: 8px;
background: #ffffff;
color: var(--text);
}
.field input:focus,
.field select:focus {
outline: 2px solid rgba(22, 163, 74, 0.25);
border-color: var(--accent);
}
.actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.actions button {
flex: 1;
padding: 10px 12px;
border: 0;
border-radius: 8px;
cursor: pointer;
font-weight: 700;
}
.btn-save {
background: var(--accent);
color: #f0fdf4;
}
.btn-save:hover {
background: var(--accent-dark);
}
.btn-cancel {
background: #e2e8f0;
color: #1e293b;
}
.btn-delete {
background: #dc2626;
color: #ffffff;
margin-top: 8px;
width: 100%;
}
.btn-delete:hover {
background: #b91c1c;
}
.point-popup .meta {
font-size: 12px;
color: #475569;
margin-top: 8px;
}
.status {
font-size: 12px;
margin-top: 10px;
color: #475569;
}
.status.error {
color: #b91c1c;
}
.status.success {
color: #166534;
}
/* Perkecil vertex saat menggambar line/polygon */
.leaflet-div-icon.leaflet-editing-icon {
width: 8px !important;
height: 8px !important;
margin-left: -4px !important;
margin-top: -4px !important;
border-radius: 50%;
}
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(11, 19, 43, 0.38);
display: none;
align-items: center;
justify-content: center;
padding: 16px;
z-index: 2000;
}
.modal-backdrop.open {
display: flex;
}
.modal {
width: min(460px, 100%);
background: #ffffff;
border-radius: 14px;
border: 1px solid var(--line);
box-shadow: 0 20px 55px rgba(17, 24, 39, 0.28);
padding: 16px;
}
.modal h3 {
margin: 0 0 12px;
}
.field input[readonly] {
background: #eef2ff;
}
.popup-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
.popup-actions button {
flex: 1;
font-size: 12px;
padding: 8px;
border: 0;
border-radius: 8px;
cursor: pointer;
}
.btn-popup-edit {
background: #16a34a;
color: #ffffff;
}
.btn-popup-refine {
background: #f59e0b;
color: #111827;
}
.btn-popup-save {
background: #0f766e;
color: #ffffff;
}
.btn-popup-cancel {
background: #64748b;
color: #ffffff;
}
.btn-popup-delete {
background: #dc2626;
color: #ffffff;
}
@media (max-width: 640px) {
.header h1 {
font-size: 18px;
}
.header p {
font-size: 13px;
}
}
</style>
</head>
<body>
<div class="app">
<header class="header">
<h1>WebGIS SPBU</h1>
<p>Klik peta untuk titik SPBU. Gunakan toolbar kiri peta untuk menggambar jalan dan parsil.</p>
<div class="legend">
<span><i class="dot" style="background:#dc2626"></i>Jalan Nasional</span>
<span><i class="dot" style="background:#2563eb"></i>Jalan Provinsi</span>
<span><i class="dot" style="background:#16a34a"></i>Jalan Kabupaten</span>
</div>
</header>
<div class="map-wrap">
<div id="map"></div>
</div>
</div>
<div id="modal-backdrop" class="modal-backdrop" aria-hidden="true">
<div class="modal">
<h3 id="modal-title">Form Data</h3>
<form id="feature-form">
<div id="road-fields" hidden>
<div class="field">
<label for="nama_jalan">Nama Jalan</label>
<input id="nama_jalan" name="nama_jalan" type="text" required>
</div>
<div class="field">
<label for="status_jalan">Status Jalan</label>
<select id="status_jalan" name="status_jalan" required>
<option value="nasional">Jalan Nasional</option>
<option value="provinsi">Jalan Provinsi</option>
<option value="kabupaten">Jalan Kabupaten</option>
</select>
</div>
<div class="field">
<label for="panjang_meter">Panjang Jalan (meter)</label>
<input id="panjang_meter" name="panjang_meter" type="text" readonly>
</div>
</div>
<div id="parcel-fields" hidden>
<div class="field">
<label for="nama_pemilik">Nama Pemilik</label>
<input id="nama_pemilik" name="nama_pemilik" type="text" required>
</div>
<div class="field">
<label for="status_kepemilikan">Status Kepemilikan</label>
<select id="status_kepemilikan" name="status_kepemilikan" required>
<option value="SHM">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">Sertifikat Hak Pakai (HP)</option>
</select>
</div>
<div class="field">
<label for="luas_m2">Luas Tanah (meter persegi)</label>
<input id="luas_m2" name="luas_m2" type="text" readonly>
</div>
</div>
<div class="actions">
<button type="submit" class="btn-save">Simpan</button>
<button type="button" id="btn-cancel" class="btn-cancel">Batal</button>
</div>
<div id="form-status" class="status"></div>
</form>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
<script src="assets/js/kecamatan-layer.js"></script>
<script src="assets/js/map.js"></script>
</body>
</html>
+296
View File
@@ -0,0 +1,296 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebGIS Spasial Jalan dan Parsil</title>
<link rel="stylesheet" href="assets/vendor/leaflet/leaflet.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.css">
<style>
:root {
--ink: #0b132b;
--sky: #f5f7ff;
--line: #d3d9eb;
--panel: #ffffff;
--accent: #0f766e;
--accent-dark: #115e59;
--danger: #b91c1c;
--muted: #3f4d73;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
height: 100%;
font-family: "Trebuchet MS", "Segoe UI", sans-serif;
color: var(--ink);
background: radial-gradient(circle at top left, #e0f2fe 0%, #f8fafc 45%, #f1f5f9 100%);
}
.app {
height: 100dvh;
display: grid;
grid-template-rows: auto 1fr;
}
.header {
display: flex;
gap: 16px;
justify-content: space-between;
align-items: center;
padding: 12px 18px;
border-bottom: 1px solid var(--line);
background: rgba(255, 255, 255, 0.94);
backdrop-filter: blur(6px);
}
.header h1 {
margin: 0;
font-size: 20px;
}
.header p {
margin: 4px 0 0;
color: var(--muted);
font-size: 13px;
}
.legend {
display: flex;
gap: 10px;
flex-wrap: wrap;
font-size: 12px;
color: #223055;
}
.legend span {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--panel);
border: 1px solid var(--line);
border-radius: 999px;
padding: 6px 10px;
}
.dot {
width: 10px;
height: 10px;
border-radius: 999px;
display: inline-block;
}
.map-wrap {
position: relative;
min-height: 0;
}
#map {
width: 100%;
height: 100%;
}
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(11, 19, 43, 0.38);
display: none;
align-items: center;
justify-content: center;
padding: 16px;
z-index: 2000;
}
.modal-backdrop.open {
display: flex;
}
.modal {
width: min(460px, 100%);
background: var(--panel);
border-radius: 14px;
border: 1px solid var(--line);
box-shadow: 0 20px 55px rgba(17, 24, 39, 0.28);
padding: 16px;
}
.modal h3 {
margin: 0 0 12px;
}
.field {
margin-bottom: 10px;
}
.field label {
display: block;
font-size: 12px;
margin-bottom: 4px;
color: var(--muted);
}
.field input,
.field select {
width: 100%;
padding: 9px 10px;
border-radius: 8px;
border: 1px solid var(--line);
font-size: 14px;
color: var(--ink);
}
.field input[readonly] {
background: #eef2ff;
}
.actions {
display: flex;
gap: 10px;
margin-top: 12px;
}
button {
border: 0;
border-radius: 8px;
padding: 10px 12px;
cursor: pointer;
font-weight: 700;
}
.btn-primary {
flex: 1;
background: var(--accent);
color: #f0fdfa;
}
.btn-primary:hover {
background: var(--accent-dark);
}
.btn-secondary {
background: #e2e8f0;
color: #1e293b;
}
.btn-danger {
background: var(--danger);
color: #fff;
}
.status {
font-size: 12px;
margin-top: 8px;
color: #334155;
min-height: 16px;
}
.status.error {
color: #b91c1c;
}
.status.success {
color: #166534;
}
.popup-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
.popup-actions button {
flex: 1;
font-size: 12px;
padding: 8px;
}
@media (max-width: 700px) {
.header {
flex-direction: column;
align-items: flex-start;
}
}
</style>
</head>
<body>
<div class="app">
<header class="header">
<div>
<h1>WebGIS Manajemen Jalan & Parsil</h1>
<p>Gunakan toolbar kiri peta untuk gambar polyline/polygon, edit, atau hapus geometri.</p>
</div>
<div class="legend">
<span><i class="dot" style="background:#dc2626"></i>Jalan Nasional</span>
<span><i class="dot" style="background:#2563eb"></i>Jalan Provinsi</span>
<span><i class="dot" style="background:#16a34a"></i>Jalan Kabupaten</span>
</div>
</header>
<div class="map-wrap">
<div id="map"></div>
</div>
</div>
<div id="modal-backdrop" class="modal-backdrop" aria-hidden="true">
<div class="modal">
<h3 id="modal-title">Form Data</h3>
<form id="feature-form">
<div id="road-fields" hidden>
<div class="field">
<label for="nama_jalan">Nama Jalan</label>
<input id="nama_jalan" name="nama_jalan" type="text" required>
</div>
<div class="field">
<label for="status_jalan">Status Jalan</label>
<select id="status_jalan" name="status_jalan" required>
<option value="nasional">Jalan Nasional</option>
<option value="provinsi">Jalan Provinsi</option>
<option value="kabupaten">Jalan Kabupaten</option>
</select>
</div>
<div class="field">
<label for="panjang_meter">Panjang Jalan (meter)</label>
<input id="panjang_meter" name="panjang_meter" type="text" readonly>
</div>
</div>
<div id="parcel-fields" hidden>
<div class="field">
<label for="nama_pemilik">Nama Pemilik</label>
<input id="nama_pemilik" name="nama_pemilik" type="text" required>
</div>
<div class="field">
<label for="status_kepemilikan">Status Kepemilikan</label>
<select id="status_kepemilikan" name="status_kepemilikan" required>
<option value="SHM">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">Sertifikat Hak Pakai (HP)</option>
</select>
</div>
<div class="field">
<label for="luas_m2">Luas Tanah (meter persegi)</label>
<input id="luas_m2" name="luas_m2" type="text" readonly>
</div>
</div>
<div class="actions">
<button type="submit" class="btn-primary">Simpan</button>
<button type="button" id="btn-cancel" class="btn-secondary">Batal</button>
</div>
<div id="form-status" class="status"></div>
</form>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<script src="https://unpkg.com/leaflet-draw@1.0.4/dist/leaflet.draw.js"></script>
<script src="assets/js/kecamatan-layer.js"></script>
<script src="assets/js/spatial.js"></script>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
CREATE DATABASE IF NOT EXISTS webgis_spbu;
USE webgis_spbu;
CREATE TABLE IF NOT EXISTS spbu_points (
id INT AUTO_INCREMENT PRIMARY KEY,
nama VARCHAR(150) NOT NULL,
no VARCHAR(50) NOT NULL,
status_24jam ENUM('24jam', 'tidak') NOT NULL,
latitude DECIMAL(10, 7) NOT NULL,
longitude DECIMAL(10, 7) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
+24
View File
@@ -0,0 +1,24 @@
CREATE DATABASE IF NOT EXISTS webgis_spbu;
USE webgis_spbu;
CREATE TABLE IF NOT EXISTS jalan (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_jalan VARCHAR(150) NOT NULL,
status ENUM('nasional', 'provinsi', 'kabupaten') NOT NULL,
panjang_meter DOUBLE NOT NULL,
geom LINESTRING NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
SPATIAL INDEX idx_jalan_geom (geom)
) ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS parsil_tanah (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_pemilik VARCHAR(150) NOT NULL,
status_kepemilikan ENUM('SHM', 'HGB', 'HGU', 'HP') NOT NULL,
luas_m2 DOUBLE NOT NULL,
geom POLYGON NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
SPATIAL INDEX idx_parsil_geom (geom)
) ENGINE=InnoDB;