Compare commits
1 Commits
main
..
f6f1575e2d
| Author | SHA1 | Date | |
|---|---|---|---|
| f6f1575e2d |
@@ -1,22 +0,0 @@
|
|||||||
.git/
|
|
||||||
.gitignore
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
MANUAL_BOOK.docx
|
|
||||||
PRESENTASI.pptx
|
|
||||||
NEXT_WORK_SUMMARY.md
|
|
||||||
PROJECT_PROGRESS_SUMMARY.md
|
|
||||||
|
|
||||||
node_modules/
|
|
||||||
vendor/
|
|
||||||
uploads/
|
|
||||||
project_final/uploads/
|
|
||||||
*.log
|
|
||||||
*.tmp
|
|
||||||
database/backups/
|
|
||||||
database/*backup*.sql
|
|
||||||
database/*_backup.sql
|
|
||||||
*.sql.gz
|
|
||||||
*.dump
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
APP_BASE_PATH=/project_final
|
|
||||||
SESSION_SECURE=true
|
|
||||||
CORS_ALLOWED_ORIGIN=
|
|
||||||
|
|
||||||
DB_HOST=mysql
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=webgis_db
|
|
||||||
DB_USERNAME=webgis_user
|
|
||||||
DB_PASSWORD=change_me
|
|
||||||
-55
@@ -1,55 +0,0 @@
|
|||||||
# OS / editor
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
desktop.ini
|
|
||||||
.vscode/
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# Local deliverables / work notes
|
|
||||||
MANUAL_BOOK.docx
|
|
||||||
PRESENTASI.pptx
|
|
||||||
NEXT_WORK_SUMMARY.md
|
|
||||||
PROJECT_PROGRESS_SUMMARY.md
|
|
||||||
|
|
||||||
# Environment / local config
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
project_final/config/db.local.php
|
|
||||||
project_final/config/*.local.php
|
|
||||||
|
|
||||||
# PHP / Composer
|
|
||||||
vendor/
|
|
||||||
|
|
||||||
# Node / frontend packages
|
|
||||||
node_modules/
|
|
||||||
|
|
||||||
# Logs / cache / temp
|
|
||||||
*.log
|
|
||||||
logs/
|
|
||||||
cache/
|
|
||||||
tmp/
|
|
||||||
temp/
|
|
||||||
*.tmp
|
|
||||||
*.bak
|
|
||||||
*.swp
|
|
||||||
|
|
||||||
# Runtime uploads
|
|
||||||
uploads/*
|
|
||||||
project_final/uploads/*
|
|
||||||
!uploads/.gitkeep
|
|
||||||
!project_final/uploads/.gitkeep
|
|
||||||
|
|
||||||
# Generated backups / exports
|
|
||||||
database/backups/
|
|
||||||
database/*backup*.sql
|
|
||||||
database/*_backup.sql
|
|
||||||
*.sql.gz
|
|
||||||
*.dump
|
|
||||||
|
|
||||||
# Test / build output
|
|
||||||
coverage/
|
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
|
|
||||||
akun_testing.txt
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* jalan.php
|
|
||||||
* Tanggung Jawab: Menangani operasi CRUD untuk entitas Jalan (LineString).
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'GET':
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, nama, jenis_jalan, created_at, ST_AsGeoJSON(geom) as geojson FROM jalan");
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama' => $row['nama'],
|
|
||||||
'jenis_jalan' => $row['jenis_jalan'],
|
|
||||||
'created_at' => $row['created_at']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data Jalan berhasil diambil');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal mengambil data Jalan: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
if (!isset($input['nama']) || !isset($input['geometry'])) {
|
|
||||||
sendError('Nama dan geometri wajib diisi');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO jalan (nama, jenis_jalan, geom) VALUES (:nama, :jenis_jalan, ST_GeomFromGeoJSON(:geometry))";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':nama' => $input['nama'],
|
|
||||||
':jenis_jalan' => $input['jenis_jalan'] ?? 'Lokal',
|
|
||||||
':geometry' => json_encode($input['geometry'])
|
|
||||||
]);
|
|
||||||
sendSuccess(['id' => $pdo->lastInsertId()], 'Data Jalan berhasil disimpan', 201);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal menyimpan Jalan: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
if (!$id) {
|
|
||||||
sendError('ID Jalan wajib disertakan');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM jalan WHERE id = :id");
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
sendSuccess(null, 'Data Jalan berhasil dihapus');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal menghapus Jalan: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* kavling.php
|
|
||||||
* Tanggung Jawab: Menangani operasi CRUD untuk entitas Kavling (Polygon).
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'GET':
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, nama_pemilik, luas, created_at, ST_AsGeoJSON(geom) as geojson FROM kavling");
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama' => $row['nama_pemilik'],
|
|
||||||
'nama_pemilik' => $row['nama_pemilik'],
|
|
||||||
'luas' => $row['luas'],
|
|
||||||
'created_at' => $row['created_at']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data Kavling berhasil diambil');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal mengambil data Kavling: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
if (!isset($input['nama_pemilik']) || !isset($input['geometry'])) {
|
|
||||||
sendError('Nama pemilik dan geometri wajib diisi');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO kavling (nama_pemilik, luas, geom) VALUES (:nama_pemilik, :luas, ST_GeomFromGeoJSON(:geometry))";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':nama_pemilik' => $input['nama_pemilik'],
|
|
||||||
':luas' => $input['luas'] ?? 0,
|
|
||||||
':geometry' => json_encode($input['geometry'])
|
|
||||||
]);
|
|
||||||
sendSuccess(['id' => $pdo->lastInsertId()], 'Data Kavling berhasil disimpan', 201);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal menyimpan Kavling: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
if (!$id) {
|
|
||||||
sendError('ID Kavling wajib disertakan');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM kavling WHERE id = :id");
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
sendSuccess(null, 'Data Kavling berhasil dihapus');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal menghapus Kavling: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'GET':
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, nama, deskripsi, buka_24_jam, created_at, ST_AsGeoJSON(geom) as geojson FROM spbu");
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama' => $row['nama'],
|
|
||||||
'deskripsi' => $row['deskripsi'],
|
|
||||||
'buka_24_jam' => (bool)$row['buka_24_jam'],
|
|
||||||
'created_at' => $row['created_at']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data SPBU berhasil diambil');
|
|
||||||
} catch (PDOException $e) { sendError('Gagal: ' . $e->getMessage(), 500); }
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
if (!isset($input['nama']) || !isset($input['geometry'])) sendError('Validasi gagal');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO spbu (nama, deskripsi, buka_24_jam, geom) VALUES (:nama, :deskripsi, :buka_24_jam, ST_GeomFromGeoJSON(:geometry))";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':nama' => $input['nama'],
|
|
||||||
':deskripsi' => $input['deskripsi'] ?? '',
|
|
||||||
':buka_24_jam' => isset($input['buka_24_jam']) && $input['buka_24_jam'] ? 1 : 0,
|
|
||||||
':geometry' => json_encode($input['geometry'])
|
|
||||||
]);
|
|
||||||
sendSuccess(['id' => $pdo->lastInsertId()], 'Disimpan', 201);
|
|
||||||
} catch (PDOException $e) { sendError('Gagal: ' . $e->getMessage(), 500); }
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
if (!$id) sendError('ID dibutuhkan');
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM spbu WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
sendSuccess(null, 'Dihapus');
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Singleton PDO connection.
|
|
||||||
*
|
|
||||||
* Coolify: set DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, and DB_PASSWORD
|
|
||||||
* from the database service credentials.
|
|
||||||
*/
|
|
||||||
class Database {
|
|
||||||
private static $conn = null;
|
|
||||||
|
|
||||||
private static function env(string $key, string $default = ''): string {
|
|
||||||
$value = getenv($key);
|
|
||||||
return ($value === false || $value === '') ? $default : $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getConnection() {
|
|
||||||
if (self::$conn === null) {
|
|
||||||
$dbUrl = self::env('DATABASE_URL');
|
|
||||||
if ($dbUrl) {
|
|
||||||
$parsed = parse_url($dbUrl);
|
|
||||||
$host = $parsed['host'] ?? '127.0.0.1';
|
|
||||||
$port = $parsed['port'] ?? '3306';
|
|
||||||
$username = $parsed['user'] ?? 'root';
|
|
||||||
$password = $parsed['pass'] ?? '';
|
|
||||||
$dbName = ltrim($parsed['path'], '/');
|
|
||||||
} else {
|
|
||||||
$host = self::env('DB_HOST', '127.0.0.1');
|
|
||||||
$port = self::env('DB_PORT', '3306');
|
|
||||||
$dbName = self::env('DB_DATABASE', 'webgis_db');
|
|
||||||
$username = self::env('DB_USERNAME', 'root');
|
|
||||||
$password = self::env('DB_PASSWORD', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
self::$conn = new PDO(
|
|
||||||
"mysql:host={$host};port={$port};dbname={$dbName};charset=utf8mb4",
|
|
||||||
$username,
|
|
||||||
$password
|
|
||||||
);
|
|
||||||
self::$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
||||||
self::$conn->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// Disable strict mode for GROUP BY to prevent 1055 errors in Coolify's MariaDB
|
|
||||||
self::$conn->exec("SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''))");
|
|
||||||
|
|
||||||
// Auto-initialize database if empty
|
|
||||||
$stmt = self::$conn->query("SHOW TABLES LIKE 'users'");
|
|
||||||
if ($stmt->rowCount() == 0) {
|
|
||||||
$sqlFile = __DIR__ . '/../../../database/webgis_db.sql';
|
|
||||||
if (file_exists($sqlFile)) {
|
|
||||||
$sql = file_get_contents($sqlFile);
|
|
||||||
self::$conn->exec($sql);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return self::$conn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* response_helper.php
|
|
||||||
* Tanggung Jawab: Standarisasi format response JSON untuk semua API endpoint.
|
|
||||||
* Mencegah inkonsistensi struktur data yang dikembalikan ke frontend.
|
|
||||||
*/
|
|
||||||
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
|
||||||
// Izinkan CORS (opsional, jika frontend dan backend di port yang beda)
|
|
||||||
$allowedOrigin = getenv('CORS_ALLOWED_ORIGIN');
|
|
||||||
if ($allowedOrigin !== false && $allowedOrigin !== '') {
|
|
||||||
header('Access-Control-Allow-Origin: ' . $allowedOrigin);
|
|
||||||
header('Vary: Origin');
|
|
||||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
|
||||||
header('Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mengirim response sukses
|
|
||||||
* @param mixed $data Data yang ingin dikirim
|
|
||||||
* @param string $message Pesan sukses (opsional)
|
|
||||||
* @param int $statusCode HTTP Status Code (default: 200)
|
|
||||||
*/
|
|
||||||
function sendSuccess($data = null, $message = 'Success', $statusCode = 200) {
|
|
||||||
http_response_code($statusCode);
|
|
||||||
echo json_encode([
|
|
||||||
'status' => 'success',
|
|
||||||
'message' => $message,
|
|
||||||
'data' => $data
|
|
||||||
]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mengirim response error
|
|
||||||
* @param string $message Pesan error
|
|
||||||
* @param int $statusCode HTTP Status Code (default: 400)
|
|
||||||
*/
|
|
||||||
function sendError($message = 'Error', $statusCode = 400) {
|
|
||||||
http_response_code($statusCode);
|
|
||||||
echo json_encode([
|
|
||||||
'status' => 'error',
|
|
||||||
'message' => $message
|
|
||||||
]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
/* base.css
|
|
||||||
* Tanggung Jawab: Reset CSS, definisi variabel warna premium, dan tipografi dasar.
|
|
||||||
*/
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
|
||||||
|
|
||||||
:root {
|
|
||||||
/* Color Palette - Minimalist & Elegant */
|
|
||||||
--primary: #4F46E5;
|
|
||||||
--primary-hover: #4338CA;
|
|
||||||
--secondary: #10B981;
|
|
||||||
--background: #F3F4F6;
|
|
||||||
--surface: #FFFFFF;
|
|
||||||
--surface-glass: rgba(255, 255, 255, 0.85);
|
|
||||||
--text-main: #111827;
|
|
||||||
--text-muted: #6B7280;
|
|
||||||
--border: #E5E7EB;
|
|
||||||
--danger: #EF4444;
|
|
||||||
|
|
||||||
/* Shadows & Effects */
|
|
||||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
||||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
||||||
--radius-md: 12px;
|
|
||||||
--radius-lg: 16px;
|
|
||||||
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
background-color: var(--background);
|
|
||||||
color: var(--text-main);
|
|
||||||
display: flex;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4 {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar minimalis */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: #CBD5E1;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #94A3B8;
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
/* form.css
|
|
||||||
* Tanggung Jawab: Styling komponen form (input, button, select).
|
|
||||||
*/
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-label {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 14px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
transition: var(--transition);
|
|
||||||
background: var(--surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--primary);
|
|
||||||
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-family: inherit;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: var(--transition);
|
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: var(--primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: var(--primary-hover);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background: var(--danger);
|
|
||||||
color: white;
|
|
||||||
width: auto;
|
|
||||||
padding: 6px 12px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger:hover {
|
|
||||||
background: #DC2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Micro-animation for active state */
|
|
||||||
.btn:active {
|
|
||||||
transform: translateY(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form Container Animation */
|
|
||||||
#form-container {
|
|
||||||
background: var(--surface);
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
animation: fadeIn 0.4s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; transform: scale(0.98); }
|
|
||||||
to { opacity: 1; transform: scale(1); }
|
|
||||||
}
|
|
||||||
@@ -1,383 +0,0 @@
|
|||||||
/* ═══════════════════════════════════════════════════
|
|
||||||
WebGIS P01 — main.css | Dark / Light Theme
|
|
||||||
═══════════════════════════════════════════════════ */
|
|
||||||
|
|
||||||
/* ── Design Tokens ── */
|
|
||||||
:root {
|
|
||||||
--ease: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
--transition: all 0.25s var(--ease);
|
|
||||||
--font: 'Inter', sans-serif;
|
|
||||||
--radius: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Dark Theme (default) ── */
|
|
||||||
[data-theme="dark"] {
|
|
||||||
--primary: #818CF8;
|
|
||||||
--primary-deep: #6366F1;
|
|
||||||
--primary-glow: rgba(99,102,241,0.3);
|
|
||||||
--bg-dark: #0A0F1E;
|
|
||||||
--bg-glass: rgba(17,24,39,0.82);
|
|
||||||
--bg-input: rgba(255,255,255,0.05);
|
|
||||||
--border-glass: rgba(255,255,255,0.08);
|
|
||||||
--border-focus: #6366F1;
|
|
||||||
--text-main: #F1F5F9;
|
|
||||||
--text-muted: #94A3B8;
|
|
||||||
--shadow: 0 25px 50px -12px rgba(0,0,0,0.55);
|
|
||||||
--dot-color: rgba(255,255,255,0.025);
|
|
||||||
--btn-spbu: #10B981;
|
|
||||||
--btn-jalan: #F59E0B;
|
|
||||||
--btn-kavling: #3B82F6;
|
|
||||||
--btn-rumah: #8B5CF6;
|
|
||||||
--btn-warga: #EF4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Light Theme ── */
|
|
||||||
[data-theme="light"] {
|
|
||||||
--primary: #4F46E5;
|
|
||||||
--primary-deep: #3730A3;
|
|
||||||
--primary-glow: rgba(79,70,229,0.15);
|
|
||||||
--bg-dark: #F0F4FF;
|
|
||||||
--bg-glass: rgba(255,255,255,0.92);
|
|
||||||
--bg-input: #FFFFFF;
|
|
||||||
--border-glass: #E2E8F0;
|
|
||||||
--border-focus: #4F46E5;
|
|
||||||
--text-main: #0F172A;
|
|
||||||
--text-muted: #64748B;
|
|
||||||
--shadow: 0 25px 50px -12px rgba(79,70,229,0.12);
|
|
||||||
--dot-color: rgba(79,70,229,0.035);
|
|
||||||
--btn-spbu: #059669;
|
|
||||||
--btn-jalan: #D97706;
|
|
||||||
--btn-kavling: #2563EB;
|
|
||||||
--btn-rumah: #7C3AED;
|
|
||||||
--btn-warga: #DC2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Reset ── */
|
|
||||||
*, *::before, *::after { box-sizing: border-box; }
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: var(--font);
|
|
||||||
background: var(--bg-dark);
|
|
||||||
color: var(--text-main);
|
|
||||||
overflow: hidden;
|
|
||||||
transition: background 0.3s var(--ease), color 0.3s var(--ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dot grid background */
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background-image: radial-gradient(circle, var(--dot-color) 1px, transparent 1px);
|
|
||||||
background-size: 28px 28px;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Map ── */
|
|
||||||
#map {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Floating Dock ── */
|
|
||||||
.floating-dock {
|
|
||||||
position: absolute;
|
|
||||||
top: 24px;
|
|
||||||
left: 24px;
|
|
||||||
z-index: 1000;
|
|
||||||
background: var(--bg-glass);
|
|
||||||
backdrop-filter: blur(24px);
|
|
||||||
-webkit-backdrop-filter: blur(24px);
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
width: 320px;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
max-height: calc(100vh - 48px);
|
|
||||||
overflow-y: auto;
|
|
||||||
transition: background 0.3s var(--ease), border-color 0.3s var(--ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating-dock::-webkit-scrollbar { width: 4px; }
|
|
||||||
.floating-dock::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 10px; }
|
|
||||||
|
|
||||||
/* ── Dock Top Bar ── */
|
|
||||||
.dock-topbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-back-portal {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
background: var(--bg-input);
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: var(--transition);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.btn-back-portal:hover {
|
|
||||||
background: var(--primary-deep);
|
|
||||||
border-color: var(--primary-deep);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dock-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-theme-dock {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
background: var(--bg-input);
|
|
||||||
color: var(--text-muted);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
.btn-theme-dock:hover {
|
|
||||||
background: var(--primary-deep);
|
|
||||||
border-color: var(--primary-deep);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Header ── */
|
|
||||||
.header h2 {
|
|
||||||
margin: 0 0 6px 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
background: linear-gradient(135deg, var(--primary), #C084FC);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
.header p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Buttons ── */
|
|
||||||
.menu-group { display: flex; flex-direction: column; gap: 8px; }
|
|
||||||
.menu-title {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action {
|
|
||||||
background: var(--bg-input);
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
color: var(--text-main);
|
|
||||||
padding: 11px 14px;
|
|
||||||
border-radius: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: var(--font);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
transition: var(--transition);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action svg { width: 18px; height: 18px; flex-shrink: 0; }
|
|
||||||
|
|
||||||
.btn-action:hover {
|
|
||||||
background: rgba(255,255,255,0.08);
|
|
||||||
transform: translateX(3px);
|
|
||||||
border-color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] .btn-action:hover {
|
|
||||||
background: #EEF2FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action.active {
|
|
||||||
background: var(--primary-deep);
|
|
||||||
border-color: var(--primary-deep);
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 4px 12px var(--primary-glow);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button color variants */
|
|
||||||
.btn-spbu svg { color: var(--btn-spbu); }
|
|
||||||
.btn-jalan svg { color: var(--btn-jalan); }
|
|
||||||
.btn-kavling svg { color: var(--btn-kavling); }
|
|
||||||
.btn-rumah svg { color: var(--btn-rumah); }
|
|
||||||
.btn-warga svg { color: var(--btn-warga); }
|
|
||||||
|
|
||||||
/* ── Form Panel ── */
|
|
||||||
.form-panel {
|
|
||||||
background: rgba(0,0,0,0.3);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 14px;
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] .form-panel {
|
|
||||||
background: rgba(79,70,229,0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group { display: flex; flex-direction: column; gap: 5px; }
|
|
||||||
.form-group label { font-size: 0.78rem; color: var(--text-muted); font-weight: 500; }
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
background: var(--bg-input);
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
padding: 9px 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: var(--text-main);
|
|
||||||
font-family: var(--font);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
outline: none;
|
|
||||||
transition: border 0.2s;
|
|
||||||
}
|
|
||||||
.form-control:focus { border-color: var(--border-focus); box-shadow: 0 0 0 3px var(--primary-glow); }
|
|
||||||
.form-control::placeholder { color: var(--text-muted); }
|
|
||||||
|
|
||||||
.checkbox-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.checkbox-group input { width: 15px; height: 15px; accent-color: var(--primary); cursor: pointer; }
|
|
||||||
.checkbox-group span { font-size: 0.82rem; color: var(--text-main); }
|
|
||||||
|
|
||||||
.btn-submit {
|
|
||||||
background: var(--primary-deep);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 11px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-family: var(--font);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: var(--transition);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
.btn-submit:hover { filter: brightness(1.1); transform: translateY(-1px); }
|
|
||||||
|
|
||||||
/* ── Leaflet Customizations ── */
|
|
||||||
.leaflet-draw-toolbar { display: none !important; }
|
|
||||||
.leaflet-control-zoom { border: none !important; box-shadow: var(--shadow) !important; }
|
|
||||||
.leaflet-control-zoom a {
|
|
||||||
background: var(--bg-glass) !important;
|
|
||||||
color: var(--text-main) !important;
|
|
||||||
border-color: var(--border-glass) !important;
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
.leaflet-control-zoom a:hover { background: var(--primary-deep) !important; color: white !important; }
|
|
||||||
|
|
||||||
/* Custom Leaflet Layer Control style to match theme */
|
|
||||||
.leaflet-control-layers {
|
|
||||||
background: var(--bg-glass) !important;
|
|
||||||
backdrop-filter: blur(24px) !important;
|
|
||||||
-webkit-backdrop-filter: blur(24px) !important;
|
|
||||||
border: 1px solid var(--border-glass) !important;
|
|
||||||
border-radius: 14px !important;
|
|
||||||
box-shadow: var(--shadow) !important;
|
|
||||||
color: var(--text-main) !important;
|
|
||||||
font-family: var(--font) !important;
|
|
||||||
padding: 10px 14px !important;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
.leaflet-control-layers-expanded {
|
|
||||||
min-width: 140px;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers label {
|
|
||||||
margin-bottom: 6px !important;
|
|
||||||
font-size: 0.85rem !important;
|
|
||||||
cursor: pointer !important;
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
gap: 8px !important;
|
|
||||||
color: var(--text-main) !important;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers label:last-child {
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers input[type="checkbox"] {
|
|
||||||
cursor: pointer !important;
|
|
||||||
width: 15px !important;
|
|
||||||
height: 15px !important;
|
|
||||||
accent-color: var(--primary-deep) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-popup-content-wrapper {
|
|
||||||
background: var(--bg-glass) !important;
|
|
||||||
backdrop-filter: blur(16px) !important;
|
|
||||||
border: 1px solid var(--border-glass) !important;
|
|
||||||
color: var(--text-main) !important;
|
|
||||||
border-radius: 14px !important;
|
|
||||||
box-shadow: var(--shadow) !important;
|
|
||||||
}
|
|
||||||
.leaflet-popup-tip { background: var(--bg-glass) !important; }
|
|
||||||
.leaflet-popup-content { margin: 14px !important; font-family: var(--font); }
|
|
||||||
.leaflet-popup-content h3 { margin: 0 0 8px 0; font-size: 1rem; border-bottom: 1px solid var(--border-glass); padding-bottom: 8px; }
|
|
||||||
.leaflet-popup-content p { margin: 4px 0; font-size: 0.85rem; color: var(--text-muted); }
|
|
||||||
|
|
||||||
.leaflet-popup-content .btn-delete {
|
|
||||||
background: rgba(239,68,68,0.1);
|
|
||||||
color: #EF4444;
|
|
||||||
border: 1px solid rgba(239,68,68,0.3);
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-top: 10px;
|
|
||||||
width: 100%;
|
|
||||||
font-family: var(--font);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
.leaflet-popup-content .btn-delete:hover { background: #EF4444; color: white; }
|
|
||||||
|
|
||||||
/* ── Toast ── */
|
|
||||||
.toast-container {
|
|
||||||
position: fixed; bottom: 24px; right: 24px; z-index: 9999;
|
|
||||||
display: flex; flex-direction: column; gap: 10px;
|
|
||||||
}
|
|
||||||
.toast {
|
|
||||||
background: var(--bg-glass); backdrop-filter: blur(16px);
|
|
||||||
border: 1px solid var(--border-glass); border-left: 4px solid var(--primary-deep);
|
|
||||||
padding: 14px 18px; border-radius: 12px; color: var(--text-main); font-size: 0.875rem;
|
|
||||||
box-shadow: var(--shadow); animation: slideIn 0.3s var(--ease);
|
|
||||||
}
|
|
||||||
.toast.error { border-left-color: #EF4444; }
|
|
||||||
.toast.success { border-left-color: #10B981; }
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
from { transform: translateX(100%); opacity: 0; }
|
|
||||||
to { transform: translateX(0); opacity: 1; }
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
/* map.css
|
|
||||||
* Tanggung Jawab: Styling container peta, kontrol Leaflet, dan pop-up modern.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.map-container {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
background: #E2E8F0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#map {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Customizing Leaflet Controls */
|
|
||||||
.leaflet-control-zoom {
|
|
||||||
border: none !important;
|
|
||||||
box-shadow: var(--shadow-md) !important;
|
|
||||||
border-radius: var(--radius-md) !important;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-control-zoom a {
|
|
||||||
color: var(--text-main) !important;
|
|
||||||
background: var(--surface-glass) !important;
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-control-zoom a:hover {
|
|
||||||
background: var(--surface) !important;
|
|
||||||
color: var(--primary) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sembunyikan Toolbar Default Leaflet Draw karena kita pakai Menu Kiri */
|
|
||||||
.leaflet-draw-toolbar {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom Popup Modern */
|
|
||||||
.leaflet-popup-content-wrapper {
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-popup-content {
|
|
||||||
margin: 14px;
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-title {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-main);
|
|
||||||
margin-bottom: 6px;
|
|
||||||
border-bottom: 2px solid var(--primary);
|
|
||||||
padding-bottom: 4px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-desc {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-action {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
/* sidebar.css
|
|
||||||
* Tanggung Jawab: Styling layout sidebar, navigasi, dan list item.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
width: 350px;
|
|
||||||
background: var(--surface-glass);
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
-webkit-backdrop-filter: blur(12px);
|
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
z-index: 1000;
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header {
|
|
||||||
padding: 24px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-title {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-subtitle {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toast Notification */
|
|
||||||
.toast-container {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 24px;
|
|
||||||
right: 24px;
|
|
||||||
z-index: 9999;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast {
|
|
||||||
background: var(--surface);
|
|
||||||
color: var(--text-main);
|
|
||||||
padding: 12px 20px;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
transform: translateY(20px);
|
|
||||||
opacity: 0;
|
|
||||||
animation: slideIn 0.3s forwards ease-out;
|
|
||||||
border-left: 4px solid var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast.error {
|
|
||||||
border-left-color: var(--danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="id" data-theme="dark">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>WebGIS — Pertemuan 01: Geometri Dasar</title>
|
|
||||||
<meta name="description" content="Modul 01 — Menggambar dan mengelola geometri spasial (titik, garis, poligon) berbasis WebGIS.">
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
|
||||||
<link rel="stylesheet" href="css/main.css">
|
|
||||||
<script>
|
|
||||||
// Global error interceptor to assist in visual debugging
|
|
||||||
window.onerror = function(message, source, lineno, colno, error) {
|
|
||||||
const errorMsg = `JS Error: ${message} (at ${source ? source.split('/').pop() : 'unknown'}:${lineno}:${colno})`;
|
|
||||||
console.error(errorMsg, error);
|
|
||||||
const showInterval = setInterval(() => {
|
|
||||||
const toastContainer = document.getElementById('toast-container');
|
|
||||||
if (toastContainer) {
|
|
||||||
clearInterval(showInterval);
|
|
||||||
const t = document.createElement('div');
|
|
||||||
t.className = 'toast error';
|
|
||||||
t.innerHTML = errorMsg;
|
|
||||||
toastContainer.appendChild(t);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
window.onunhandledrejection = function(event) {
|
|
||||||
const errorMsg = `Promise Error: ${event.reason?.message || event.reason}`;
|
|
||||||
console.error(errorMsg, event.reason);
|
|
||||||
const showInterval = setInterval(() => {
|
|
||||||
const toastContainer = document.getElementById('toast-container');
|
|
||||||
if (toastContainer) {
|
|
||||||
clearInterval(showInterval);
|
|
||||||
const t = document.createElement('div');
|
|
||||||
t.className = 'toast error';
|
|
||||||
t.innerHTML = errorMsg;
|
|
||||||
toastContainer.appendChild(t);
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<!-- Map container -->
|
|
||||||
<div id="map"></div>
|
|
||||||
|
|
||||||
<!-- Floating Dock -->
|
|
||||||
<div class="floating-dock">
|
|
||||||
|
|
||||||
<!-- Dock top bar: back + theme toggle -->
|
|
||||||
<div class="dock-topbar">
|
|
||||||
<a href="../../portal.html" class="btn-back-portal" title="Kembali ke Portal">
|
|
||||||
<i class="fas fa-chevron-left"></i>
|
|
||||||
Portal
|
|
||||||
</a>
|
|
||||||
<div class="dock-controls">
|
|
||||||
<button class="btn-theme-dock" id="themeToggle" title="Toggle tema" aria-label="Toggle dark/light mode">
|
|
||||||
<i class="fas fa-moon" id="themeIcon"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="header">
|
|
||||||
<h2>P01: Geometri Dasar</h2>
|
|
||||||
<p>Sistem Informasi Geografis — CRUD spasial titik, garis, dan area.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Layer Peta (Centang) -->
|
|
||||||
<div class="menu-group">
|
|
||||||
<div class="menu-title">Layer Peta</div>
|
|
||||||
<div class="form-panel" style="gap: 10px;">
|
|
||||||
<label class="checkbox-group">
|
|
||||||
<input type="checkbox" id="layer-spbu" checked>
|
|
||||||
<span>Stasiun SPBU</span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox-group">
|
|
||||||
<input type="checkbox" id="layer-jalan" checked>
|
|
||||||
<span>Jalur Jalan</span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox-group">
|
|
||||||
<input type="checkbox" id="layer-kavling" checked>
|
|
||||||
<span>Kavling Tanah</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Spatial tools -->
|
|
||||||
<div class="menu-group">
|
|
||||||
<div class="menu-title">Alat Spasial</div>
|
|
||||||
|
|
||||||
<button id="btn-draw-spbu" class="btn-action btn-spbu">
|
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.242-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
|
||||||
Tambah SPBU
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button id="btn-draw-jalan" class="btn-action btn-jalan">
|
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path></svg>
|
|
||||||
Tambah Jalan
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button id="btn-draw-kavling" class="btn-action btn-kavling">
|
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg>
|
|
||||||
Tambah Kavling
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Dynamic form container -->
|
|
||||||
<div id="form-container" style="display: none;"></div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Toast -->
|
|
||||||
<div id="toast-container" class="toast-container"></div>
|
|
||||||
|
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
|
||||||
<script>
|
|
||||||
// ── Theme toggle ──
|
|
||||||
const html = document.documentElement;
|
|
||||||
const btn = document.getElementById('themeToggle');
|
|
||||||
const icon = document.getElementById('themeIcon');
|
|
||||||
const saved = localStorage.getItem('portal-theme') || 'dark';
|
|
||||||
setTheme(saved);
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
|
||||||
setTheme(next);
|
|
||||||
localStorage.setItem('portal-theme', next);
|
|
||||||
});
|
|
||||||
function setTheme(t) {
|
|
||||||
html.setAttribute('data-theme', t);
|
|
||||||
icon.className = t === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script type="module" src="js/app.js?v=20260613-9"></script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
import { initMap } from './modules/map.js';
|
|
||||||
import { setupDrawControls } from './modules/draw.js';
|
|
||||||
import { renderForm } from './modules/form.js';
|
|
||||||
import { spbuService, jalanService, kavlingService } from './services/api.service.js';
|
|
||||||
|
|
||||||
let appMap;
|
|
||||||
let drawControl;
|
|
||||||
|
|
||||||
// Global layer object tracker
|
|
||||||
const mapLayers = {
|
|
||||||
spbu: null,
|
|
||||||
jalan: null,
|
|
||||||
kavling: null
|
|
||||||
};
|
|
||||||
|
|
||||||
// Custom SVG Icons Generator
|
|
||||||
export const createIcon = (svgPath, color) => {
|
|
||||||
return L.divIcon({
|
|
||||||
className: 'custom-icon',
|
|
||||||
html: `<div style="background-color: ${color}; width: 34px; height: 34px; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 10px rgba(0,0,0,0.5); border: 2px solid rgba(255,255,255,0.8); color: white;">
|
|
||||||
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">${svgPath}</svg>
|
|
||||||
</div>`,
|
|
||||||
iconSize: [34, 34],
|
|
||||||
iconAnchor: [17, 34],
|
|
||||||
popupAnchor: [0, -34]
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const iconSPBU = (is24h) => createIcon('<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.242-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>', is24h ? '#10B981' : '#EF4444');
|
|
||||||
|
|
||||||
window.showToast = (msg, type='success') => {
|
|
||||||
const t = document.createElement('div');
|
|
||||||
t.className = `toast ${type}`;
|
|
||||||
t.innerHTML = msg;
|
|
||||||
document.getElementById('toast-container').appendChild(t);
|
|
||||||
setTimeout(() => { t.style.transform='translateX(100%)'; t.style.opacity=0; setTimeout(()=>t.remove(),300); }, 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setupCheckboxListeners = () => {
|
|
||||||
const layerConfigs = [
|
|
||||||
{ id: 'layer-spbu', key: 'spbu' },
|
|
||||||
{ id: 'layer-jalan', key: 'jalan' },
|
|
||||||
{ id: 'layer-kavling', key: 'kavling' }
|
|
||||||
];
|
|
||||||
|
|
||||||
layerConfigs.forEach(({ id, key }) => {
|
|
||||||
const checkbox = document.getElementById(id);
|
|
||||||
if (checkbox) {
|
|
||||||
checkbox.addEventListener('change', (e) => {
|
|
||||||
const layer = mapLayers[key];
|
|
||||||
if (layer) {
|
|
||||||
if (e.target.checked) {
|
|
||||||
if (!appMap.hasLayer(layer)) {
|
|
||||||
appMap.addLayer(layer);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (appMap.hasLayer(layer)) {
|
|
||||||
appMap.removeLayer(layer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadSPBUData = async () => {
|
|
||||||
try {
|
|
||||||
const spbu = await spbuService.getAll();
|
|
||||||
mapLayers.spbu = L.geoJSON(spbu, {
|
|
||||||
pointToLayer: (f, latlng) => L.marker(latlng, { icon: iconSPBU(f.properties.buka_24_jam) }),
|
|
||||||
onEachFeature: (f, l) => bindPopup(l, 'spbu', f.properties)
|
|
||||||
});
|
|
||||||
|
|
||||||
const checkbox = document.getElementById('layer-spbu');
|
|
||||||
if (checkbox && checkbox.checked) {
|
|
||||||
mapLayers.spbu.addTo(appMap);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Gagal meload data SPBU:", e);
|
|
||||||
window.showToast("Gagal meload data SPBU: " + e.message, 'error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadJalanData = async () => {
|
|
||||||
try {
|
|
||||||
const jalan = await jalanService.getAll();
|
|
||||||
mapLayers.jalan = L.geoJSON(jalan, {
|
|
||||||
style: { color: '#F59E0B', weight: 4 },
|
|
||||||
onEachFeature: (f, l) => bindPopup(l, 'jalan', f.properties)
|
|
||||||
});
|
|
||||||
|
|
||||||
const checkbox = document.getElementById('layer-jalan');
|
|
||||||
if (checkbox && checkbox.checked) {
|
|
||||||
mapLayers.jalan.addTo(appMap);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Gagal meload data Jalan:", e);
|
|
||||||
window.showToast("Gagal meload data Jalan: " + e.message, 'error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadKavlingData = async () => {
|
|
||||||
try {
|
|
||||||
const kavling = await kavlingService.getAll();
|
|
||||||
mapLayers.kavling = L.geoJSON(kavling, {
|
|
||||||
style: { color: '#3B82F6', weight: 2, fillColor: '#3B82F6', fillOpacity: 0.3 },
|
|
||||||
onEachFeature: (f, l) => bindPopup(l, 'kavling', f.properties)
|
|
||||||
});
|
|
||||||
|
|
||||||
const checkbox = document.getElementById('layer-kavling');
|
|
||||||
if (checkbox && checkbox.checked) {
|
|
||||||
mapLayers.kavling.addTo(appMap);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Gagal meload data Kavling:", e);
|
|
||||||
window.showToast("Gagal meload data Kavling: " + e.message, 'error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadAllData = async () => {
|
|
||||||
await Promise.allSettled([
|
|
||||||
loadSPBUData(),
|
|
||||||
loadJalanData(),
|
|
||||||
loadKavlingData()
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const bindPopup = (layer, type, props) => {
|
|
||||||
const ext = props.buka_24_jam !== undefined ? `<p>Buka 24 Jam: <strong style="color:${props.buka_24_jam?'#10B981':'#EF4444'}">${props.buka_24_jam ? 'Ya' : 'Tidak'}</strong></p>` : '';
|
|
||||||
layer.bindPopup(`
|
|
||||||
<div class="popup-custom">
|
|
||||||
<h3>${props.nama}</h3>
|
|
||||||
<p>${props.deskripsi || ''}</p>
|
|
||||||
${ext}
|
|
||||||
<button class="btn-delete" onclick="window.deleteData('${type}', ${props.id})">Hapus Data</button>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.deleteData = async (type, id) => {
|
|
||||||
if(!confirm('Yakin ingin menghapus?')) return;
|
|
||||||
try {
|
|
||||||
if(type==='spbu') await spbuService.delete(id);
|
|
||||||
if(type==='jalan') await jalanService.delete(id);
|
|
||||||
if(type==='kavling') await kavlingService.delete(id);
|
|
||||||
window.showToast('Data dihapus');
|
|
||||||
setTimeout(()=>location.reload(), 800);
|
|
||||||
} catch(e) { window.showToast('Gagal hapus', 'error'); }
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGeometryCreated = (type, geometry, layer) => {
|
|
||||||
const tempLayer = L.geoJSON(geometry).addTo(appMap);
|
|
||||||
renderForm(type, geometry, async (payload) => {
|
|
||||||
try {
|
|
||||||
if(type==='spbu') await spbuService.create(payload);
|
|
||||||
if(type==='jalan') await jalanService.create(payload);
|
|
||||||
if(type==='kavling') {
|
|
||||||
const kavlingPayload = {
|
|
||||||
nama_pemilik: payload.nama,
|
|
||||||
luas: payload.luas || 0,
|
|
||||||
geometry: payload.geometry
|
|
||||||
};
|
|
||||||
await kavlingService.create(kavlingPayload);
|
|
||||||
}
|
|
||||||
window.showToast('Berhasil disimpan');
|
|
||||||
setTimeout(()=>location.reload(), 800);
|
|
||||||
} catch(e) { window.showToast('Gagal simpan', 'error'); }
|
|
||||||
}, () => {
|
|
||||||
appMap.removeLayer(tempLayer);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = async () => {
|
|
||||||
appMap = initMap('map');
|
|
||||||
setupCheckboxListeners();
|
|
||||||
drawControl = setupDrawControls(appMap, handleGeometryCreated);
|
|
||||||
await loadAllData();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
|
||||||
} else {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/**
|
|
||||||
* config.js
|
|
||||||
* Tanggung Jawab: Menyimpan konstanta dan konfigurasi global aplikasi.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const CONFIG = {
|
|
||||||
BASE_URL: '/01/backend/api',
|
|
||||||
|
|
||||||
// Konstanta tipe fitur
|
|
||||||
FEATURE_TYPES: {
|
|
||||||
SPBU: 'Point',
|
|
||||||
JALAN: 'LineString',
|
|
||||||
KAVLING: 'Polygon'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
export const setupDrawControls = (map, onGeometryCreated) => {
|
|
||||||
const drawControl = new L.Control.Draw({
|
|
||||||
draw: { marker: true, polyline: true, polygon: true, circle: false, circlemarker: false, rectangle: false }
|
|
||||||
});
|
|
||||||
map.addControl(drawControl);
|
|
||||||
|
|
||||||
map.on(L.Draw.Event.CREATED, function (event) {
|
|
||||||
const layer = event.layer;
|
|
||||||
const type = event.layerType;
|
|
||||||
|
|
||||||
// Peta draw default tipe: marker -> spbu, polyline -> jalan, polygon -> kavling
|
|
||||||
let customType = type;
|
|
||||||
if(type === 'marker') customType = 'spbu';
|
|
||||||
if(type === 'polyline') customType = 'jalan';
|
|
||||||
if(type === 'polygon') customType = 'kavling';
|
|
||||||
if(window.currentDrawType) customType = window.currentDrawType; // Untuk P2/P3
|
|
||||||
|
|
||||||
onGeometryCreated(customType, layer.toGeoJSON().geometry, layer);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Custom Menu Handlers
|
|
||||||
const markerDrawer = new L.Draw.Marker(map, drawControl.options.draw.marker);
|
|
||||||
const polylineDrawer = new L.Draw.Polyline(map, drawControl.options.draw.polyline);
|
|
||||||
const polygonDrawer = new L.Draw.Polygon(map, drawControl.options.draw.polygon);
|
|
||||||
|
|
||||||
document.getElementById('btn-draw-spbu')?.addEventListener('click', () => { window.currentDrawType='spbu'; markerDrawer.enable(); });
|
|
||||||
document.getElementById('btn-draw-jalan')?.addEventListener('click', () => { window.currentDrawType='jalan'; polylineDrawer.enable(); });
|
|
||||||
document.getElementById('btn-draw-kavling')?.addEventListener('click', () => { window.currentDrawType='kavling'; polygonDrawer.enable(); });
|
|
||||||
|
|
||||||
return drawControl;
|
|
||||||
};
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
export const renderForm = (type, geometry, onSubmit, onCancel) => {
|
|
||||||
const container = document.getElementById('form-container');
|
|
||||||
container.style.display = 'flex';
|
|
||||||
|
|
||||||
let extraFields = '';
|
|
||||||
if (type === 'spbu') {
|
|
||||||
extraFields = `
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="checkbox-group">
|
|
||||||
<input type="checkbox" id="input-24jam">
|
|
||||||
<span>Buka 24 Jam</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
} else if (type === 'kavling') {
|
|
||||||
extraFields = `
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="number" id="input-luas" class="form-control" placeholder="Luas (m²)" required min="1">
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="form-panel">
|
|
||||||
<h3 style="margin:0 0 10px 0; font-size:1rem; border-bottom:1px solid rgba(255,255,255,0.1); padding-bottom:8px;">Simpan Data (${type.toUpperCase()})</h3>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="text" id="input-nama" class="form-control" placeholder="Nama / Label" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="text" id="input-deskripsi" class="form-control" placeholder="Deskripsi Singkat">
|
|
||||||
</div>
|
|
||||||
${extraFields}
|
|
||||||
<div style="display:flex; gap:10px; margin-top:8px;">
|
|
||||||
<button id="btn-save" class="btn-submit" style="flex:1;">Simpan</button>
|
|
||||||
<button id="btn-cancel" class="btn-submit" style="background:rgba(255,255,255,0.1); color:var(--text-light); flex:1;">Batal</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.getElementById('btn-save').addEventListener('click', () => {
|
|
||||||
const payload = {
|
|
||||||
type,
|
|
||||||
geometry,
|
|
||||||
nama: document.getElementById('input-nama').value,
|
|
||||||
deskripsi: document.getElementById('input-deskripsi').value,
|
|
||||||
};
|
|
||||||
if (type === 'spbu') payload.buka_24_jam = document.getElementById('input-24jam').checked;
|
|
||||||
if (type === 'kavling') payload.luas = document.getElementById('input-luas').value;
|
|
||||||
|
|
||||||
onSubmit(payload);
|
|
||||||
container.style.display = 'none';
|
|
||||||
container.innerHTML = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btn-cancel').addEventListener('click', () => {
|
|
||||||
if(onCancel) onCancel();
|
|
||||||
container.style.display = 'none';
|
|
||||||
container.innerHTML = '';
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/**
|
|
||||||
* map.js
|
|
||||||
* Tanggung Jawab: Inisialisasi peta Leaflet dasar dan base layer.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const initMap = (containerId) => {
|
|
||||||
// Pusat peta default (Universitas Tanjungpura, Pontianak)
|
|
||||||
const map = L.map(containerId, {
|
|
||||||
zoomControl: false
|
|
||||||
}).setView([-0.0583, 109.3448], 15);
|
|
||||||
|
|
||||||
// Pindahkan zoom control ke kanan bawah
|
|
||||||
L.control.zoom({
|
|
||||||
position: 'bottomright'
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
||||||
const tileUrl = isDark
|
|
||||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
|
||||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
|
|
||||||
|
|
||||||
// Tile Layer Premium (CartoDB Dark Matter / Light All)
|
|
||||||
const tileLayer = L.tileLayer(tileUrl, {
|
|
||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
|
||||||
subdomains: 'abcd',
|
|
||||||
maxZoom: 20
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
// Dynamic theme updater for tile layer
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
const dark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
||||||
tileLayer.setUrl(dark
|
|
||||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
|
||||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
observer.observe(document.documentElement, { attributes: true });
|
|
||||||
|
|
||||||
return map;
|
|
||||||
};
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
const getBaseUrl = () => {
|
|
||||||
const segments = window.location.pathname.split('/');
|
|
||||||
const targetIndex = segments.findIndex(seg => ['01', '02', '03', 'versi'].includes(seg));
|
|
||||||
if (targetIndex !== -1) {
|
|
||||||
return segments.slice(0, targetIndex).join('/');
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
export const BASE_URL = getBaseUrl();
|
|
||||||
|
|
||||||
const createService = (endpoint) => ({
|
|
||||||
getAll: async () => {
|
|
||||||
const res = await fetch(`${BASE_URL}/${endpoint}`);
|
|
||||||
const json = await res.json();
|
|
||||||
return json.data;
|
|
||||||
},
|
|
||||||
create: async (data) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/${endpoint}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
return json.data;
|
|
||||||
},
|
|
||||||
delete: async (id) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/${endpoint}?id=${id}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
return json.data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// P01
|
|
||||||
export const spbuService = createService('01/backend/api/spbu.php');
|
|
||||||
export const jalanService = createService('01/backend/api/jalan.php');
|
|
||||||
export const kavlingService = createService('01/backend/api/kavling.php');
|
|
||||||
|
|
||||||
// P02
|
|
||||||
export const rumahIbadahService = createService('02/backend/api/rumah_ibadah.php');
|
|
||||||
export const wargaMiskinService = createService('02/backend/api/warga_miskin.php');
|
|
||||||
export const haversineService = {
|
|
||||||
getDalamRadius: async (id, radius) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/02/backend/api/haversine.php?rumah_ibadah_id=${id}&radius_km=${radius}`);
|
|
||||||
const json = await res.json();
|
|
||||||
return json.data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* jalan.service.js
|
|
||||||
* Tanggung Jawab: Komunikasi HTTP ke endpoint API jalan.php
|
|
||||||
*/
|
|
||||||
import { CONFIG } from '../config.js';
|
|
||||||
|
|
||||||
export const jalanService = {
|
|
||||||
getAll: async () => {
|
|
||||||
const response = await fetch(`${CONFIG.BASE_URL}/jalan.php`);
|
|
||||||
return await response.json();
|
|
||||||
},
|
|
||||||
|
|
||||||
save: async (data) => {
|
|
||||||
const response = await fetch(`${CONFIG.BASE_URL}/jalan.php`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
return await response.json();
|
|
||||||
},
|
|
||||||
|
|
||||||
delete: async (id) => {
|
|
||||||
const response = await fetch(`${CONFIG.BASE_URL}/jalan.php?id=${id}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* kavling.service.js
|
|
||||||
* Tanggung Jawab: Komunikasi HTTP ke endpoint API kavling.php
|
|
||||||
*/
|
|
||||||
import { CONFIG } from '../config.js';
|
|
||||||
|
|
||||||
export const kavlingService = {
|
|
||||||
getAll: async () => {
|
|
||||||
const response = await fetch(`${CONFIG.BASE_URL}/kavling.php`);
|
|
||||||
return await response.json();
|
|
||||||
},
|
|
||||||
|
|
||||||
save: async (data) => {
|
|
||||||
const response = await fetch(`${CONFIG.BASE_URL}/kavling.php`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
return await response.json();
|
|
||||||
},
|
|
||||||
|
|
||||||
delete: async (id) => {
|
|
||||||
const response = await fetch(`${CONFIG.BASE_URL}/kavling.php?id=${id}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* spbu.service.js
|
|
||||||
* Tanggung Jawab: Komunikasi HTTP ke endpoint API spbu.php
|
|
||||||
*/
|
|
||||||
import { CONFIG } from '../config.js';
|
|
||||||
|
|
||||||
export const spbuService = {
|
|
||||||
getAll: async () => {
|
|
||||||
const response = await fetch(`${CONFIG.BASE_URL}/spbu.php`);
|
|
||||||
return await response.json();
|
|
||||||
},
|
|
||||||
|
|
||||||
save: async (data) => {
|
|
||||||
const response = await fetch(`${CONFIG.BASE_URL}/spbu.php`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
return await response.json();
|
|
||||||
},
|
|
||||||
|
|
||||||
delete: async (id) => {
|
|
||||||
const response = await fetch(`${CONFIG.BASE_URL}/spbu.php?id=${id}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* style.util.js
|
|
||||||
* Tanggung Jawab: Mendefinisikan style untuk GeoJSON berdasarkan properti.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const getGeoJsonStyle = (feature) => {
|
|
||||||
// Style default untuk Kavling (Polygon)
|
|
||||||
if (feature.geometry.type === 'Polygon') {
|
|
||||||
return {
|
|
||||||
color: '#10B981', // Secondary color
|
|
||||||
weight: 2,
|
|
||||||
opacity: 0.8,
|
|
||||||
fillColor: '#34D399',
|
|
||||||
fillOpacity: 0.4
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Style default untuk Jalan (LineString)
|
|
||||||
if (feature.geometry.type === 'LineString') {
|
|
||||||
const type = feature.properties.jenis_jalan;
|
|
||||||
let color = '#4F46E5'; // Primary color
|
|
||||||
|
|
||||||
if (type === 'Arteri') color = '#EF4444'; // Danger color
|
|
||||||
else if (type === 'Kolektor') color = '#F59E0B'; // Warning color
|
|
||||||
|
|
||||||
return {
|
|
||||||
color: color,
|
|
||||||
weight: 4,
|
|
||||||
opacity: 0.9
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
/**
|
|
||||||
* ui.util.js
|
|
||||||
* Tanggung Jawab: Manipulasi DOM umum seperti Toast notification.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const showToast = (message, type = 'success') => {
|
|
||||||
const container = document.getElementById('toast-container');
|
|
||||||
if (!container) return;
|
|
||||||
|
|
||||||
const toast = document.createElement('div');
|
|
||||||
toast.className = `toast ${type}`;
|
|
||||||
toast.textContent = message;
|
|
||||||
|
|
||||||
container.appendChild(toast);
|
|
||||||
|
|
||||||
// Hapus toast setelah 3 detik
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.style.opacity = '0';
|
|
||||||
toast.style.transform = 'translateY(20px)';
|
|
||||||
toast.style.transition = 'all 0.3s ease';
|
|
||||||
|
|
||||||
setTimeout(() => toast.remove(), 300);
|
|
||||||
}, 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clearFormContainer = () => {
|
|
||||||
const container = document.getElementById('form-container');
|
|
||||||
if (container) {
|
|
||||||
container.innerHTML = '';
|
|
||||||
container.style.display = 'none';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* jalan.php
|
|
||||||
* Tanggung Jawab: Menangani operasi CRUD untuk entitas Jalan (LineString).
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'GET':
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, nama, jenis_jalan, created_at, ST_AsGeoJSON(geom) as geojson FROM jalan");
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama' => $row['nama'],
|
|
||||||
'jenis_jalan' => $row['jenis_jalan'],
|
|
||||||
'created_at' => $row['created_at']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data Jalan berhasil diambil');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal mengambil data Jalan: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
if (!isset($input['nama']) || !isset($input['geometry'])) {
|
|
||||||
sendError('Nama dan geometri wajib diisi');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO jalan (nama, jenis_jalan, geom) VALUES (:nama, :jenis_jalan, ST_GeomFromGeoJSON(:geometry))";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':nama' => $input['nama'],
|
|
||||||
':jenis_jalan' => $input['jenis_jalan'] ?? 'Lokal',
|
|
||||||
':geometry' => json_encode($input['geometry'])
|
|
||||||
]);
|
|
||||||
sendSuccess(['id' => $pdo->lastInsertId()], 'Data Jalan berhasil disimpan', 201);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal menyimpan Jalan: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
if (!$id) {
|
|
||||||
sendError('ID Jalan wajib disertakan');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM jalan WHERE id = :id");
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
sendSuccess(null, 'Data Jalan berhasil dihapus');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal menghapus Jalan: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* kavling.php
|
|
||||||
* Tanggung Jawab: Menangani operasi CRUD untuk entitas Kavling (Polygon).
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'GET':
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, nama_pemilik, luas, created_at, ST_AsGeoJSON(geom) as geojson FROM kavling");
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama' => $row['nama_pemilik'],
|
|
||||||
'nama_pemilik' => $row['nama_pemilik'],
|
|
||||||
'luas' => $row['luas'],
|
|
||||||
'created_at' => $row['created_at']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data Kavling berhasil diambil');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal mengambil data Kavling: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
if (!isset($input['nama_pemilik']) || !isset($input['geometry'])) {
|
|
||||||
sendError('Nama pemilik dan geometri wajib diisi');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO kavling (nama_pemilik, luas, geom) VALUES (:nama_pemilik, :luas, ST_GeomFromGeoJSON(:geometry))";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':nama_pemilik' => $input['nama_pemilik'],
|
|
||||||
':luas' => $input['luas'] ?? 0,
|
|
||||||
':geometry' => json_encode($input['geometry'])
|
|
||||||
]);
|
|
||||||
sendSuccess(['id' => $pdo->lastInsertId()], 'Data Kavling berhasil disimpan', 201);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal menyimpan Kavling: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
if (!$id) {
|
|
||||||
sendError('ID Kavling wajib disertakan');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM kavling WHERE id = :id");
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
sendSuccess(null, 'Data Kavling berhasil dihapus');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal menghapus Kavling: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* rumah_ibadah.php
|
|
||||||
* Tanggung Jawab: Operasi CRUD Rumah Ibadah + Endpoint analisis jangkauan Haversine.
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
require_once '../utils/geo_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'GET':
|
|
||||||
if (isset($_GET['action']) && $_GET['action'] === 'jangkauan' && isset($_GET['id'])) {
|
|
||||||
// Analisis Jangkauan Haversine
|
|
||||||
$id = $_GET['id'];
|
|
||||||
$radius = isset($_GET['radius']) ? (float) $_GET['radius'] : 1.0; // default 1 km
|
|
||||||
|
|
||||||
// Dapatkan koordinat rumah ibadah ini
|
|
||||||
$stmt = $pdo->prepare("SELECT ST_AsGeoJSON(geom) as geojson FROM rumah_ibadah WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
$rumah_ibadah = $stmt->fetch();
|
|
||||||
|
|
||||||
if (!$rumah_ibadah) sendError('Rumah ibadah tidak ditemukan', 404);
|
|
||||||
|
|
||||||
$geom = json_decode($rumah_ibadah['geojson'], true);
|
|
||||||
$pusatLon = $geom['coordinates'][0];
|
|
||||||
$pusatLat = $geom['coordinates'][1];
|
|
||||||
|
|
||||||
// Panggil geo_helper untuk mendapatkan warga miskin
|
|
||||||
$warga = GeoHelper::getWargaDalamRadius($pdo, $pusatLat, $pusatLon, $radius);
|
|
||||||
|
|
||||||
// Format ke GeoJSON FeatureCollection sesuai kebutuhan frontend
|
|
||||||
$features = [];
|
|
||||||
foreach ($warga as $w) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => $w['geometry'],
|
|
||||||
'properties' => [
|
|
||||||
'id' => $w['id'],
|
|
||||||
'nama' => $w['nama_kk'],
|
|
||||||
'penghasilan' => $w['penghasilan'],
|
|
||||||
'jumlah_tanggungan' => $w['jumlah_tanggungan'],
|
|
||||||
'jarak_km' => $w['jarak_km']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data jangkauan berhasil dihitung');
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// GET Semua Rumah Ibadah (GeoJSON)
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, nama, agama, ST_AsGeoJSON(geom) as geojson FROM rumah_ibadah");
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama' => $row['nama'],
|
|
||||||
'agama' => $row['agama']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data Rumah Ibadah');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
if (!isset($input['nama']) || !isset($input['geometry'])) sendError('Validasi gagal');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO rumah_ibadah (nama, agama, geom) VALUES (:nama, :agama, ST_GeomFromGeoJSON(:geometry))";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':nama' => $input['nama'],
|
|
||||||
':agama' => $input['agama'] ?? 'Islam',
|
|
||||||
':geometry' => json_encode($input['geometry'])
|
|
||||||
]);
|
|
||||||
sendSuccess(['id' => $pdo->lastInsertId()], 'Disimpan', 201);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
if (!$id) sendError('ID dibutuhkan');
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM rumah_ibadah WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
sendSuccess(null, 'Dihapus');
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'GET':
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, nama, deskripsi, buka_24_jam, created_at, ST_AsGeoJSON(geom) as geojson FROM spbu");
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama' => $row['nama'],
|
|
||||||
'deskripsi' => $row['deskripsi'],
|
|
||||||
'buka_24_jam' => (bool)$row['buka_24_jam'],
|
|
||||||
'created_at' => $row['created_at']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data SPBU berhasil diambil');
|
|
||||||
} catch (PDOException $e) { sendError('Gagal: ' . $e->getMessage(), 500); }
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
if (!isset($input['nama']) || !isset($input['geometry'])) sendError('Validasi gagal');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO spbu (nama, deskripsi, buka_24_jam, geom) VALUES (:nama, :deskripsi, :buka_24_jam, ST_GeomFromGeoJSON(:geometry))";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':nama' => $input['nama'],
|
|
||||||
':deskripsi' => $input['deskripsi'] ?? '',
|
|
||||||
':buka_24_jam' => isset($input['buka_24_jam']) && $input['buka_24_jam'] ? 1 : 0,
|
|
||||||
':geometry' => json_encode($input['geometry'])
|
|
||||||
]);
|
|
||||||
sendSuccess(['id' => $pdo->lastInsertId()], 'Disimpan', 201);
|
|
||||||
} catch (PDOException $e) { sendError('Gagal: ' . $e->getMessage(), 500); }
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
if (!$id) sendError('ID dibutuhkan');
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM spbu WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
sendSuccess(null, 'Dihapus');
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* warga_miskin.php
|
|
||||||
* Tanggung Jawab: Operasi CRUD untuk Warga Miskin.
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'GET':
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, nama_kk, penghasilan, jumlah_tanggungan, ST_AsGeoJSON(geom) as geojson FROM warga_miskin");
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama_kk' => $row['nama_kk'],
|
|
||||||
'penghasilan' => $row['penghasilan'],
|
|
||||||
'jumlah_tanggungan' => $row['jumlah_tanggungan']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data Warga Miskin');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
if (!isset($input['nama_kk']) || !isset($input['geometry'])) sendError('Validasi gagal');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO warga_miskin (nama_kk, penghasilan, jumlah_tanggungan, geom) VALUES (:nama_kk, :penghasilan, :jumlah_tanggungan, ST_GeomFromGeoJSON(:geometry))";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':nama_kk' => $input['nama_kk'],
|
|
||||||
':penghasilan' => $input['penghasilan'] ?? 0,
|
|
||||||
':jumlah_tanggungan' => $input['jumlah_tanggungan'] ?? 0,
|
|
||||||
':geometry' => json_encode($input['geometry'])
|
|
||||||
]);
|
|
||||||
sendSuccess(['id' => $pdo->lastInsertId()], 'Disimpan', 201);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
if (!$id) sendError('ID dibutuhkan');
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM warga_miskin WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
sendSuccess(null, 'Dihapus');
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Singleton PDO connection.
|
|
||||||
*
|
|
||||||
* Coolify: set DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, and DB_PASSWORD
|
|
||||||
* from the database service credentials.
|
|
||||||
*/
|
|
||||||
class Database {
|
|
||||||
private static $conn = null;
|
|
||||||
|
|
||||||
private static function env(string $key, string $default = ''): string {
|
|
||||||
$value = getenv($key);
|
|
||||||
return ($value === false || $value === '') ? $default : $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getConnection() {
|
|
||||||
if (self::$conn === null) {
|
|
||||||
$dbUrl = self::env('DATABASE_URL');
|
|
||||||
if ($dbUrl) {
|
|
||||||
$parsed = parse_url($dbUrl);
|
|
||||||
$host = $parsed['host'] ?? '127.0.0.1';
|
|
||||||
$port = $parsed['port'] ?? '3306';
|
|
||||||
$username = $parsed['user'] ?? 'root';
|
|
||||||
$password = $parsed['pass'] ?? '';
|
|
||||||
$dbName = ltrim($parsed['path'], '/');
|
|
||||||
} else {
|
|
||||||
$host = self::env('DB_HOST', '127.0.0.1');
|
|
||||||
$port = self::env('DB_PORT', '3306');
|
|
||||||
$dbName = self::env('DB_DATABASE', 'webgis_db');
|
|
||||||
$username = self::env('DB_USERNAME', 'root');
|
|
||||||
$password = self::env('DB_PASSWORD', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
self::$conn = new PDO(
|
|
||||||
"mysql:host={$host};port={$port};dbname={$dbName};charset=utf8mb4",
|
|
||||||
$username,
|
|
||||||
$password
|
|
||||||
);
|
|
||||||
self::$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
||||||
self::$conn->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// Disable strict mode for GROUP BY to prevent 1055 errors in Coolify's MariaDB
|
|
||||||
self::$conn->exec("SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''))");
|
|
||||||
|
|
||||||
// Auto-initialize database if empty
|
|
||||||
$stmt = self::$conn->query("SHOW TABLES LIKE 'users'");
|
|
||||||
if ($stmt->rowCount() == 0) {
|
|
||||||
$sqlFile = __DIR__ . '/../../../database/webgis_db.sql';
|
|
||||||
if (file_exists($sqlFile)) {
|
|
||||||
$sql = file_get_contents($sqlFile);
|
|
||||||
self::$conn->exec($sql);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return self::$conn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* geo_helper.php
|
|
||||||
* Tanggung Jawab: Menyediakan fungsi matematika spasial manual (seperti Haversine).
|
|
||||||
*/
|
|
||||||
|
|
||||||
class GeoHelper {
|
|
||||||
/**
|
|
||||||
* Menghitung jarak antara dua koordinat geografis menggunakan Haversine Formula.
|
|
||||||
* Mengembalikan jarak dalam satuan Kilometer.
|
|
||||||
*/
|
|
||||||
public static function haversineDistance($lat1, $lon1, $lat2, $lon2) {
|
|
||||||
$earthRadius = 6371; // Jari-jari bumi dalam kilometer
|
|
||||||
|
|
||||||
$dLat = deg2rad($lat2 - $lat1);
|
|
||||||
$dLon = deg2rad($lon2 - $lon1);
|
|
||||||
|
|
||||||
$a = sin($dLat / 2) * sin($dLat / 2) +
|
|
||||||
cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
|
|
||||||
sin($dLon / 2) * sin($dLon / 2);
|
|
||||||
|
|
||||||
$c = 2 * atan2(sqrt($a), sqrt(1 - $a));
|
|
||||||
$distance = $earthRadius * $c;
|
|
||||||
|
|
||||||
return $distance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mencari Warga Miskin dalam radius tertentu dari suatu titik (Rumah Ibadah).
|
|
||||||
*/
|
|
||||||
public static function getWargaDalamRadius($pdo, $pusatLat, $pusatLon, $radiusKm = 1.0) {
|
|
||||||
// Ambil semua data warga miskin
|
|
||||||
$stmt = $pdo->query("SELECT id, nama_kk, penghasilan, jumlah_tanggungan, ST_AsGeoJSON(geom) as geojson FROM warga_miskin");
|
|
||||||
|
|
||||||
$wargaDalamRadius = [];
|
|
||||||
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$geom = json_decode($row['geojson'], true);
|
|
||||||
// GeoJSON Point format: [Longitude, Latitude]
|
|
||||||
$wargaLon = $geom['coordinates'][0];
|
|
||||||
$wargaLat = $geom['coordinates'][1];
|
|
||||||
|
|
||||||
// Hitung jarak dengan Haversine
|
|
||||||
$jarak = self::haversineDistance($pusatLat, $pusatLon, $wargaLat, $wargaLon);
|
|
||||||
|
|
||||||
if ($jarak <= $radiusKm) {
|
|
||||||
$wargaDalamRadius[] = [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama_kk' => $row['nama_kk'],
|
|
||||||
'penghasilan' => $row['penghasilan'],
|
|
||||||
'jumlah_tanggungan' => $row['jumlah_tanggungan'],
|
|
||||||
'jarak_km' => round($jarak, 2),
|
|
||||||
'geometry' => $geom
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Urutkan berdasarkan jarak terdekat
|
|
||||||
usort($wargaDalamRadius, function($a, $b) {
|
|
||||||
return $a['jarak_km'] <=> $b['jarak_km'];
|
|
||||||
});
|
|
||||||
|
|
||||||
return $wargaDalamRadius;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* response_helper.php
|
|
||||||
* Tanggung Jawab: Standarisasi format response JSON.
|
|
||||||
*/
|
|
||||||
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
|
||||||
$allowedOrigin = getenv('CORS_ALLOWED_ORIGIN');
|
|
||||||
if ($allowedOrigin !== false && $allowedOrigin !== '') {
|
|
||||||
header('Access-Control-Allow-Origin: ' . $allowedOrigin);
|
|
||||||
header('Vary: Origin');
|
|
||||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
|
||||||
header('Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit(0); }
|
|
||||||
|
|
||||||
function sendSuccess($data = null, $message = 'Success', $statusCode = 200) {
|
|
||||||
http_response_code($statusCode);
|
|
||||||
echo json_encode(['status' => 'success', 'message' => $message, 'data' => $data]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendError($message = 'Error', $statusCode = 400) {
|
|
||||||
http_response_code($statusCode);
|
|
||||||
echo json_encode(['status' => 'error', 'message' => $message]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
/* base.css
|
|
||||||
* Tanggung Jawab: Reset CSS, definisi variabel warna premium, dan tipografi dasar.
|
|
||||||
*/
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
|
||||||
|
|
||||||
:root {
|
|
||||||
/* Color Palette - Minimalist & Elegant */
|
|
||||||
--primary: #4F46E5;
|
|
||||||
--primary-hover: #4338CA;
|
|
||||||
--secondary: #10B981;
|
|
||||||
--background: #F3F4F6;
|
|
||||||
--surface: #FFFFFF;
|
|
||||||
--surface-glass: rgba(255, 255, 255, 0.85);
|
|
||||||
--text-main: #111827;
|
|
||||||
--text-muted: #6B7280;
|
|
||||||
--border: #E5E7EB;
|
|
||||||
--danger: #EF4444;
|
|
||||||
|
|
||||||
/* Shadows & Effects */
|
|
||||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
||||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
||||||
--radius-md: 12px;
|
|
||||||
--radius-lg: 16px;
|
|
||||||
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
background-color: var(--background);
|
|
||||||
color: var(--text-main);
|
|
||||||
display: flex;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4 {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar minimalis */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: #CBD5E1;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #94A3B8;
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
/* form.css
|
|
||||||
* Tanggung Jawab: Styling komponen form (input, button, select).
|
|
||||||
*/
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-label {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 14px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
transition: var(--transition);
|
|
||||||
background: var(--surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--primary);
|
|
||||||
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-family: inherit;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: var(--transition);
|
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: var(--primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: var(--primary-hover);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background: var(--danger);
|
|
||||||
color: white;
|
|
||||||
width: auto;
|
|
||||||
padding: 6px 12px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger:hover {
|
|
||||||
background: #DC2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Micro-animation for active state */
|
|
||||||
.btn:active {
|
|
||||||
transform: translateY(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form Container Animation */
|
|
||||||
#form-container {
|
|
||||||
background: var(--surface);
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
animation: fadeIn 0.4s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; transform: scale(0.98); }
|
|
||||||
to { opacity: 1; transform: scale(1); }
|
|
||||||
}
|
|
||||||
@@ -1,383 +0,0 @@
|
|||||||
/* ═══════════════════════════════════════════════════
|
|
||||||
WebGIS P01 — main.css | Dark / Light Theme
|
|
||||||
═══════════════════════════════════════════════════ */
|
|
||||||
|
|
||||||
/* ── Design Tokens ── */
|
|
||||||
:root {
|
|
||||||
--ease: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
--transition: all 0.25s var(--ease);
|
|
||||||
--font: 'Inter', sans-serif;
|
|
||||||
--radius: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Dark Theme (default) ── */
|
|
||||||
[data-theme="dark"] {
|
|
||||||
--primary: #818CF8;
|
|
||||||
--primary-deep: #6366F1;
|
|
||||||
--primary-glow: rgba(99,102,241,0.3);
|
|
||||||
--bg-dark: #0A0F1E;
|
|
||||||
--bg-glass: rgba(17,24,39,0.82);
|
|
||||||
--bg-input: rgba(255,255,255,0.05);
|
|
||||||
--border-glass: rgba(255,255,255,0.08);
|
|
||||||
--border-focus: #6366F1;
|
|
||||||
--text-main: #F1F5F9;
|
|
||||||
--text-muted: #94A3B8;
|
|
||||||
--shadow: 0 25px 50px -12px rgba(0,0,0,0.55);
|
|
||||||
--dot-color: rgba(255,255,255,0.025);
|
|
||||||
--btn-spbu: #10B981;
|
|
||||||
--btn-jalan: #F59E0B;
|
|
||||||
--btn-kavling: #3B82F6;
|
|
||||||
--btn-rumah: #8B5CF6;
|
|
||||||
--btn-warga: #EF4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Light Theme ── */
|
|
||||||
[data-theme="light"] {
|
|
||||||
--primary: #4F46E5;
|
|
||||||
--primary-deep: #3730A3;
|
|
||||||
--primary-glow: rgba(79,70,229,0.15);
|
|
||||||
--bg-dark: #F0F4FF;
|
|
||||||
--bg-glass: rgba(255,255,255,0.92);
|
|
||||||
--bg-input: #FFFFFF;
|
|
||||||
--border-glass: #E2E8F0;
|
|
||||||
--border-focus: #4F46E5;
|
|
||||||
--text-main: #0F172A;
|
|
||||||
--text-muted: #64748B;
|
|
||||||
--shadow: 0 25px 50px -12px rgba(79,70,229,0.12);
|
|
||||||
--dot-color: rgba(79,70,229,0.035);
|
|
||||||
--btn-spbu: #059669;
|
|
||||||
--btn-jalan: #D97706;
|
|
||||||
--btn-kavling: #2563EB;
|
|
||||||
--btn-rumah: #7C3AED;
|
|
||||||
--btn-warga: #DC2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Reset ── */
|
|
||||||
*, *::before, *::after { box-sizing: border-box; }
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: var(--font);
|
|
||||||
background: var(--bg-dark);
|
|
||||||
color: var(--text-main);
|
|
||||||
overflow: hidden;
|
|
||||||
transition: background 0.3s var(--ease), color 0.3s var(--ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dot grid background */
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background-image: radial-gradient(circle, var(--dot-color) 1px, transparent 1px);
|
|
||||||
background-size: 28px 28px;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Map ── */
|
|
||||||
#map {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Floating Dock ── */
|
|
||||||
.floating-dock {
|
|
||||||
position: absolute;
|
|
||||||
top: 24px;
|
|
||||||
left: 24px;
|
|
||||||
z-index: 1000;
|
|
||||||
background: var(--bg-glass);
|
|
||||||
backdrop-filter: blur(24px);
|
|
||||||
-webkit-backdrop-filter: blur(24px);
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
width: 320px;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
max-height: calc(100vh - 48px);
|
|
||||||
overflow-y: auto;
|
|
||||||
transition: background 0.3s var(--ease), border-color 0.3s var(--ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating-dock::-webkit-scrollbar { width: 4px; }
|
|
||||||
.floating-dock::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 10px; }
|
|
||||||
|
|
||||||
/* ── Dock Top Bar ── */
|
|
||||||
.dock-topbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-back-portal {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
background: var(--bg-input);
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: var(--transition);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.btn-back-portal:hover {
|
|
||||||
background: var(--primary-deep);
|
|
||||||
border-color: var(--primary-deep);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dock-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-theme-dock {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
background: var(--bg-input);
|
|
||||||
color: var(--text-muted);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
.btn-theme-dock:hover {
|
|
||||||
background: var(--primary-deep);
|
|
||||||
border-color: var(--primary-deep);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Header ── */
|
|
||||||
.header h2 {
|
|
||||||
margin: 0 0 6px 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
background: linear-gradient(135deg, var(--primary), #C084FC);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
.header p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Buttons ── */
|
|
||||||
.menu-group { display: flex; flex-direction: column; gap: 8px; }
|
|
||||||
.menu-title {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action {
|
|
||||||
background: var(--bg-input);
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
color: var(--text-main);
|
|
||||||
padding: 11px 14px;
|
|
||||||
border-radius: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: var(--font);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
transition: var(--transition);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action svg { width: 18px; height: 18px; flex-shrink: 0; }
|
|
||||||
|
|
||||||
.btn-action:hover {
|
|
||||||
background: rgba(255,255,255,0.08);
|
|
||||||
transform: translateX(3px);
|
|
||||||
border-color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] .btn-action:hover {
|
|
||||||
background: #EEF2FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action.active {
|
|
||||||
background: var(--primary-deep);
|
|
||||||
border-color: var(--primary-deep);
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 4px 12px var(--primary-glow);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button color variants */
|
|
||||||
.btn-spbu svg { color: var(--btn-spbu); }
|
|
||||||
.btn-jalan svg { color: var(--btn-jalan); }
|
|
||||||
.btn-kavling svg { color: var(--btn-kavling); }
|
|
||||||
.btn-rumah svg { color: var(--btn-rumah); }
|
|
||||||
.btn-warga svg { color: var(--btn-warga); }
|
|
||||||
|
|
||||||
/* ── Form Panel ── */
|
|
||||||
.form-panel {
|
|
||||||
background: rgba(0,0,0,0.3);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 14px;
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] .form-panel {
|
|
||||||
background: rgba(79,70,229,0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group { display: flex; flex-direction: column; gap: 5px; }
|
|
||||||
.form-group label { font-size: 0.78rem; color: var(--text-muted); font-weight: 500; }
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
background: var(--bg-input);
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
padding: 9px 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: var(--text-main);
|
|
||||||
font-family: var(--font);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
outline: none;
|
|
||||||
transition: border 0.2s;
|
|
||||||
}
|
|
||||||
.form-control:focus { border-color: var(--border-focus); box-shadow: 0 0 0 3px var(--primary-glow); }
|
|
||||||
.form-control::placeholder { color: var(--text-muted); }
|
|
||||||
|
|
||||||
.checkbox-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.checkbox-group input { width: 15px; height: 15px; accent-color: var(--primary); cursor: pointer; }
|
|
||||||
.checkbox-group span { font-size: 0.82rem; color: var(--text-main); }
|
|
||||||
|
|
||||||
.btn-submit {
|
|
||||||
background: var(--primary-deep);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 11px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-family: var(--font);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: var(--transition);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
.btn-submit:hover { filter: brightness(1.1); transform: translateY(-1px); }
|
|
||||||
|
|
||||||
/* ── Leaflet Customizations ── */
|
|
||||||
.leaflet-draw-toolbar { display: none !important; }
|
|
||||||
.leaflet-control-zoom { border: none !important; box-shadow: var(--shadow) !important; }
|
|
||||||
.leaflet-control-zoom a {
|
|
||||||
background: var(--bg-glass) !important;
|
|
||||||
color: var(--text-main) !important;
|
|
||||||
border-color: var(--border-glass) !important;
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
.leaflet-control-zoom a:hover { background: var(--primary-deep) !important; color: white !important; }
|
|
||||||
|
|
||||||
/* Custom Leaflet Layer Control style to match theme */
|
|
||||||
.leaflet-control-layers {
|
|
||||||
background: var(--bg-glass) !important;
|
|
||||||
backdrop-filter: blur(24px) !important;
|
|
||||||
-webkit-backdrop-filter: blur(24px) !important;
|
|
||||||
border: 1px solid var(--border-glass) !important;
|
|
||||||
border-radius: 14px !important;
|
|
||||||
box-shadow: var(--shadow) !important;
|
|
||||||
color: var(--text-main) !important;
|
|
||||||
font-family: var(--font) !important;
|
|
||||||
padding: 10px 14px !important;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
.leaflet-control-layers-expanded {
|
|
||||||
min-width: 140px;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers label {
|
|
||||||
margin-bottom: 6px !important;
|
|
||||||
font-size: 0.85rem !important;
|
|
||||||
cursor: pointer !important;
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
gap: 8px !important;
|
|
||||||
color: var(--text-main) !important;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers label:last-child {
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers input[type="checkbox"] {
|
|
||||||
cursor: pointer !important;
|
|
||||||
width: 15px !important;
|
|
||||||
height: 15px !important;
|
|
||||||
accent-color: var(--primary-deep) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-popup-content-wrapper {
|
|
||||||
background: var(--bg-glass) !important;
|
|
||||||
backdrop-filter: blur(16px) !important;
|
|
||||||
border: 1px solid var(--border-glass) !important;
|
|
||||||
color: var(--text-main) !important;
|
|
||||||
border-radius: 14px !important;
|
|
||||||
box-shadow: var(--shadow) !important;
|
|
||||||
}
|
|
||||||
.leaflet-popup-tip { background: var(--bg-glass) !important; }
|
|
||||||
.leaflet-popup-content { margin: 14px !important; font-family: var(--font); }
|
|
||||||
.leaflet-popup-content h3 { margin: 0 0 8px 0; font-size: 1rem; border-bottom: 1px solid var(--border-glass); padding-bottom: 8px; }
|
|
||||||
.leaflet-popup-content p { margin: 4px 0; font-size: 0.85rem; color: var(--text-muted); }
|
|
||||||
|
|
||||||
.leaflet-popup-content .btn-delete {
|
|
||||||
background: rgba(239,68,68,0.1);
|
|
||||||
color: #EF4444;
|
|
||||||
border: 1px solid rgba(239,68,68,0.3);
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-top: 10px;
|
|
||||||
width: 100%;
|
|
||||||
font-family: var(--font);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
.leaflet-popup-content .btn-delete:hover { background: #EF4444; color: white; }
|
|
||||||
|
|
||||||
/* ── Toast ── */
|
|
||||||
.toast-container {
|
|
||||||
position: fixed; bottom: 24px; right: 24px; z-index: 9999;
|
|
||||||
display: flex; flex-direction: column; gap: 10px;
|
|
||||||
}
|
|
||||||
.toast {
|
|
||||||
background: var(--bg-glass); backdrop-filter: blur(16px);
|
|
||||||
border: 1px solid var(--border-glass); border-left: 4px solid var(--primary-deep);
|
|
||||||
padding: 14px 18px; border-radius: 12px; color: var(--text-main); font-size: 0.875rem;
|
|
||||||
box-shadow: var(--shadow); animation: slideIn 0.3s var(--ease);
|
|
||||||
}
|
|
||||||
.toast.error { border-left-color: #EF4444; }
|
|
||||||
.toast.success { border-left-color: #10B981; }
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
from { transform: translateX(100%); opacity: 0; }
|
|
||||||
to { transform: translateX(0); opacity: 1; }
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
/* map.css
|
|
||||||
* Tanggung Jawab: Styling container peta, kontrol Leaflet, dan pop-up modern.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.map-container {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
background: #E2E8F0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#map {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Customizing Leaflet Controls */
|
|
||||||
.leaflet-control-zoom {
|
|
||||||
border: none !important;
|
|
||||||
box-shadow: var(--shadow-md) !important;
|
|
||||||
border-radius: var(--radius-md) !important;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-control-zoom a {
|
|
||||||
color: var(--text-main) !important;
|
|
||||||
background: var(--surface-glass) !important;
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-control-zoom a:hover {
|
|
||||||
background: var(--surface) !important;
|
|
||||||
color: var(--primary) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom Popup Modern */
|
|
||||||
.leaflet-popup-content-wrapper {
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-popup-content {
|
|
||||||
margin: 14px;
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-title {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-main);
|
|
||||||
margin-bottom: 6px;
|
|
||||||
border-bottom: 2px solid var(--primary);
|
|
||||||
padding-bottom: 4px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-desc {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-action {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
/* sidebar.css
|
|
||||||
* Tanggung Jawab: Styling layout sidebar, navigasi, dan list item.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
width: 350px;
|
|
||||||
background: var(--surface-glass);
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
-webkit-backdrop-filter: blur(12px);
|
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
z-index: 1000;
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header {
|
|
||||||
padding: 24px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-title {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-subtitle {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toast Notification */
|
|
||||||
.toast-container {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 24px;
|
|
||||||
right: 24px;
|
|
||||||
z-index: 9999;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast {
|
|
||||||
background: var(--surface);
|
|
||||||
color: var(--text-main);
|
|
||||||
padding: 12px 20px;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
transform: translateY(20px);
|
|
||||||
opacity: 0;
|
|
||||||
animation: slideIn 0.3s forwards ease-out;
|
|
||||||
border-left: 4px solid var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast.error {
|
|
||||||
border-left-color: var(--danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="id" data-theme="dark">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>WebGIS — Pertemuan 02: Haversine & Tematik</title>
|
|
||||||
<meta name="description" content="Modul 02 — Pemetaan sosial dan analisis radius jarak Haversine berbasis WebGIS.">
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
|
||||||
<link rel="stylesheet" href="css/main.css">
|
|
||||||
<style>
|
|
||||||
.result-item {
|
|
||||||
background: var(--bg-input);
|
|
||||||
padding: 8px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
}
|
|
||||||
.badge {
|
|
||||||
background: var(--primary-deep);
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 0.68rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<!-- Map container -->
|
|
||||||
<div id="map"></div>
|
|
||||||
|
|
||||||
<!-- Floating Dock -->
|
|
||||||
<div class="floating-dock">
|
|
||||||
|
|
||||||
<!-- Dock top bar -->
|
|
||||||
<div class="dock-topbar">
|
|
||||||
<a href="../../portal.html" class="btn-back-portal" title="Kembali ke Portal">
|
|
||||||
<i class="fas fa-chevron-left"></i>
|
|
||||||
Portal
|
|
||||||
</a>
|
|
||||||
<div class="dock-controls">
|
|
||||||
<button class="btn-theme-dock" id="themeToggle" title="Toggle tema" aria-label="Toggle dark/light mode">
|
|
||||||
<i class="fas fa-moon" id="themeIcon"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="header">
|
|
||||||
<h2>P02: Tematik & Jarak</h2>
|
|
||||||
<p>Kumulatif P01 + Analisis radius jarak Haversine.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Layer Peta (Centang) -->
|
|
||||||
<div class="menu-group">
|
|
||||||
<div class="menu-title">Layer Peta</div>
|
|
||||||
<div class="form-panel" style="gap: 10px;">
|
|
||||||
<label class="checkbox-group">
|
|
||||||
<input type="checkbox" id="layer-spbu" checked>
|
|
||||||
<span>Stasiun SPBU</span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox-group">
|
|
||||||
<input type="checkbox" id="layer-jalan" checked>
|
|
||||||
<span>Jalur Jalan</span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox-group">
|
|
||||||
<input type="checkbox" id="layer-kavling" checked>
|
|
||||||
<span>Kavling Tanah</span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox-group">
|
|
||||||
<input type="checkbox" id="layer-rumah" checked>
|
|
||||||
<span>Rumah Ibadah</span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox-group">
|
|
||||||
<input type="checkbox" id="layer-warga" checked>
|
|
||||||
<span>Warga Miskin</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Entitas Dasar (P01) -->
|
|
||||||
<div class="menu-group">
|
|
||||||
<div class="menu-title">Entitas Dasar (P01)</div>
|
|
||||||
<button id="btn-draw-spbu" class="btn-action btn-spbu">
|
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.242-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
|
||||||
Tambah SPBU
|
|
||||||
</button>
|
|
||||||
<button id="btn-draw-jalan" class="btn-action btn-jalan">
|
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path></svg>
|
|
||||||
Tambah Jalan
|
|
||||||
</button>
|
|
||||||
<button id="btn-draw-kavling" class="btn-action btn-kavling">
|
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg>
|
|
||||||
Tambah Kavling
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Entitas Sosial (P02) -->
|
|
||||||
<div class="menu-group" style="margin-top:4px;">
|
|
||||||
<div class="menu-title">Entitas Sosial (P02)</div>
|
|
||||||
<button id="btn-draw-rumah" class="btn-action btn-rumah">
|
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>
|
|
||||||
Tambah Rumah Ibadah
|
|
||||||
</button>
|
|
||||||
<button id="btn-draw-warga" class="btn-action btn-warga">
|
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path></svg>
|
|
||||||
Tambah Warga Miskin
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Analisis Jarak -->
|
|
||||||
<div id="instruction-panel" class="form-panel" style="margin-top:2px;">
|
|
||||||
<h3 style="margin:0 0 4px 0; font-size:0.85rem; color:var(--text-main); font-weight:600;">Analisis Jarak</h3>
|
|
||||||
<p style="font-size:0.78rem; margin:0 0 8px 0; color:var(--text-muted);">Klik [Cek Radius] di popup Rumah Ibadah mana pun.</p>
|
|
||||||
<div id="radius-results" style="max-height:140px; overflow-y:auto; display:flex; flex-direction:column; gap:6px;"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Dynamic form container -->
|
|
||||||
<div id="form-container" style="display: none;"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Toast -->
|
|
||||||
<div id="toast-container" class="toast-container"></div>
|
|
||||||
|
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
|
||||||
<script>
|
|
||||||
// ── Theme toggle ──
|
|
||||||
const html = document.documentElement;
|
|
||||||
const btn = document.getElementById('themeToggle');
|
|
||||||
const icon = document.getElementById('themeIcon');
|
|
||||||
const saved = localStorage.getItem('portal-theme') || 'dark';
|
|
||||||
setTheme(saved);
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
|
||||||
setTheme(next);
|
|
||||||
localStorage.setItem('portal-theme', next);
|
|
||||||
});
|
|
||||||
function setTheme(t) {
|
|
||||||
html.setAttribute('data-theme', t);
|
|
||||||
icon.className = t === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script type="module" src="js/app.js?v=20260613-9"></script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,159 +0,0 @@
|
|||||||
import { initMap } from './modules/map.js';
|
|
||||||
import { setupDrawControls } from './modules/draw.js';
|
|
||||||
import { renderForm } from './modules/form.js';
|
|
||||||
import { spbuService, jalanService, kavlingService, rumahIbadahService, wargaMiskinService, haversineService } from './services/api.service.js';
|
|
||||||
|
|
||||||
let appMap;
|
|
||||||
let drawControl;
|
|
||||||
let currentRadiusCircle = null;
|
|
||||||
let currentHighlightLayer = null;
|
|
||||||
|
|
||||||
export const createIcon = (svgPath, color) => {
|
|
||||||
return L.divIcon({
|
|
||||||
className: 'custom-icon',
|
|
||||||
html: `<div style="background-color: ${color}; width: 34px; height: 34px; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 10px rgba(0,0,0,0.5); border: 2px solid rgba(255,255,255,0.8); color: white;">
|
|
||||||
<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">${svgPath}</svg>
|
|
||||||
</div>`,
|
|
||||||
iconSize: [34, 34], iconAnchor: [17, 34], popupAnchor: [0, -34]
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const iconSPBU = (is24h) => createIcon('<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.242-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>', is24h ? '#10B981' : '#EF4444');
|
|
||||||
const iconRumah = createIcon('<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>', '#8B5CF6');
|
|
||||||
const iconWarga = createIcon('<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', '#EF4444');
|
|
||||||
|
|
||||||
window.showToast = (msg, type='success') => {
|
|
||||||
const t = document.createElement('div');
|
|
||||||
t.className = `toast ${type}`; t.innerHTML = msg;
|
|
||||||
document.getElementById('toast-container').appendChild(t);
|
|
||||||
setTimeout(() => { t.style.transform='translateX(100%)'; t.style.opacity=0; setTimeout(()=>t.remove(),300); }, 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadAllData = async () => {
|
|
||||||
try {
|
|
||||||
const [spbu, jalan, kavling, rumah, warga] = await Promise.all([
|
|
||||||
spbuService.getAll(), jalanService.getAll(), kavlingService.getAll(),
|
|
||||||
rumahIbadahService.getAll(), wargaMiskinService.getAll()
|
|
||||||
]);
|
|
||||||
|
|
||||||
const spbuLayer = L.geoJSON(spbu, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconSPBU(f.properties.buka_24_jam) }), onEachFeature: (f, l) => bindPopup(l, 'spbu', f.properties) }).addTo(appMap);
|
|
||||||
const jalanLayer = L.geoJSON(jalan, { style: { color: '#F59E0B', weight: 4 }, onEachFeature: (f, l) => bindPopup(l, 'jalan', f.properties) }).addTo(appMap);
|
|
||||||
const kavlingLayer = L.geoJSON(kavling, { style: { color: '#3B82F6', weight: 2, fillColor: '#3B82F6', fillOpacity: 0.3 }, onEachFeature: (f, l) => bindPopup(l, 'kavling', f.properties) }).addTo(appMap);
|
|
||||||
const rumahLayer = L.geoJSON(rumah, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconRumah }), onEachFeature: (f, l) => bindPopup(l, 'rumah_ibadah', f.properties, f) }).addTo(appMap);
|
|
||||||
const wargaLayer = L.geoJSON(warga, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconWarga }), onEachFeature: (f, l) => bindPopup(l, 'warga_miskin', f.properties) }).addTo(appMap);
|
|
||||||
|
|
||||||
// Bind checkbox events
|
|
||||||
document.getElementById('layer-spbu').addEventListener('change', (e) => {
|
|
||||||
if (e.target.checked) appMap.addLayer(spbuLayer);
|
|
||||||
else appMap.removeLayer(spbuLayer);
|
|
||||||
});
|
|
||||||
document.getElementById('layer-jalan').addEventListener('change', (e) => {
|
|
||||||
if (e.target.checked) appMap.addLayer(jalanLayer);
|
|
||||||
else appMap.removeLayer(jalanLayer);
|
|
||||||
});
|
|
||||||
document.getElementById('layer-kavling').addEventListener('change', (e) => {
|
|
||||||
if (e.target.checked) appMap.addLayer(kavlingLayer);
|
|
||||||
else appMap.removeLayer(kavlingLayer);
|
|
||||||
});
|
|
||||||
document.getElementById('layer-rumah').addEventListener('change', (e) => {
|
|
||||||
if (e.target.checked) appMap.addLayer(rumahLayer);
|
|
||||||
else appMap.removeLayer(rumahLayer);
|
|
||||||
});
|
|
||||||
document.getElementById('layer-warga').addEventListener('change', (e) => {
|
|
||||||
if (e.target.checked) appMap.addLayer(wargaLayer);
|
|
||||||
else appMap.removeLayer(wargaLayer);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
window.showToast("Gagal meload data: "+e.message, 'error');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const bindPopup = (layer, type, props, feature=null) => {
|
|
||||||
let ext = '';
|
|
||||||
if(type==='spbu') ext = `<p>24 Jam: <strong style="color:${props.buka_24_jam?'#10B981':'#EF4444'}">${props.buka_24_jam?'Ya':'Tidak'}</strong></p>`;
|
|
||||||
if(type==='rumah_ibadah' && feature) ext = `<button class="btn-submit" style="width:100%; margin-top:8px;" onclick="window.checkRadius(${props.id}, ${feature.geometry.coordinates[1]}, ${feature.geometry.coordinates[0]})">Cek Radius 1km</button>`;
|
|
||||||
|
|
||||||
layer.bindPopup(`
|
|
||||||
<div class="popup-custom">
|
|
||||||
<h3>${props.nama}</h3>
|
|
||||||
<p>${props.deskripsi || ''}</p>
|
|
||||||
${ext}
|
|
||||||
<button class="btn-delete" onclick="window.deleteData('${type}', ${props.id})">Hapus</button>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.checkRadius = async (rumahId, lat, lng) => {
|
|
||||||
if (currentRadiusCircle) appMap.removeLayer(currentRadiusCircle);
|
|
||||||
if (currentHighlightLayer) appMap.removeLayer(currentHighlightLayer);
|
|
||||||
|
|
||||||
currentRadiusCircle = L.circle([lat, lng], { radius: 1000, color: '#8B5CF6', fillColor: '#8B5CF6', fillOpacity: 0.15, weight: 2 }).addTo(appMap);
|
|
||||||
appMap.flyTo([lat, lng], 15);
|
|
||||||
window.showToast('Menghitung jarak...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await haversineService.getDalamRadius(rumahId, 1);
|
|
||||||
const container = document.getElementById('radius-results');
|
|
||||||
container.innerHTML = res.features.length ? '' : '<p>Tidak ada warga miskin.</p>';
|
|
||||||
|
|
||||||
res.features.forEach(f => {
|
|
||||||
const d = parseFloat(f.properties.jarak_km).toFixed(2);
|
|
||||||
container.innerHTML += `<div class="result-item"><span>${f.properties.nama}</span><span class="badge">${d} km</span></div>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
currentHighlightLayer = L.geoJSON(res, {
|
|
||||||
pointToLayer: (f, ll) => L.circleMarker(ll, { radius:8, fillColor:'#EF4444', color:'#fff', weight:2, fillOpacity:1 })
|
|
||||||
}).addTo(appMap);
|
|
||||||
|
|
||||||
} catch(e) { window.showToast('Gagal kalkulasi', 'error'); }
|
|
||||||
};
|
|
||||||
|
|
||||||
window.deleteData = async (type, id) => {
|
|
||||||
if(!confirm('Yakin menghapus?')) return;
|
|
||||||
try {
|
|
||||||
if(type==='spbu') await spbuService.delete(id);
|
|
||||||
if(type==='jalan') await jalanService.delete(id);
|
|
||||||
if(type==='kavling') await kavlingService.delete(id);
|
|
||||||
if(type==='rumah_ibadah') await rumahIbadahService.delete(id);
|
|
||||||
if(type==='warga_miskin') await wargaMiskinService.delete(id);
|
|
||||||
window.showToast('Data dihapus'); setTimeout(()=>location.reload(), 800);
|
|
||||||
} catch(e) { window.showToast('Gagal hapus', 'error'); }
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGeometryCreated = (type, geometry, layer) => {
|
|
||||||
const tempLayer = L.geoJSON(geometry).addTo(appMap);
|
|
||||||
renderForm(type, geometry, async (payload) => {
|
|
||||||
try {
|
|
||||||
if(type==='spbu') await spbuService.create(payload);
|
|
||||||
if(type==='jalan') await jalanService.create(payload);
|
|
||||||
if(type==='kavling') {
|
|
||||||
const kavlingPayload = {
|
|
||||||
nama_pemilik: payload.nama,
|
|
||||||
luas: payload.luas || 0,
|
|
||||||
geometry: payload.geometry
|
|
||||||
};
|
|
||||||
await kavlingService.create(kavlingPayload);
|
|
||||||
}
|
|
||||||
if(type==='rumah_ibadah') await rumahIbadahService.create(payload);
|
|
||||||
if(type==='warga_miskin') await wargaMiskinService.create(payload);
|
|
||||||
window.showToast('Tersimpan'); setTimeout(()=>location.reload(), 800);
|
|
||||||
} catch(e) { window.showToast('Gagal simpan', 'error'); }
|
|
||||||
}, () => appMap.removeLayer(tempLayer));
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = async () => {
|
|
||||||
appMap = initMap('map');
|
|
||||||
|
|
||||||
// Bind extra draw events for 02
|
|
||||||
document.getElementById('btn-draw-rumah')?.addEventListener('click', () => { window.currentDrawType='rumah_ibadah'; new L.Draw.Marker(appMap).enable(); });
|
|
||||||
document.getElementById('btn-draw-warga')?.addEventListener('click', () => { window.currentDrawType='warga_miskin'; new L.Draw.Marker(appMap).enable(); });
|
|
||||||
|
|
||||||
drawControl = setupDrawControls(appMap, handleGeometryCreated);
|
|
||||||
await loadAllData();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
|
||||||
} else {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export const CONFIG = {
|
|
||||||
BASE_URL: '/02/backend/api',
|
|
||||||
RADIUS_KM: 1.0 // Radius default 1 km
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
export const setupDrawControls = (map, onGeometryCreated) => {
|
|
||||||
const drawControl = new L.Control.Draw({
|
|
||||||
draw: { marker: true, polyline: true, polygon: true, circle: false, circlemarker: false, rectangle: false }
|
|
||||||
});
|
|
||||||
map.addControl(drawControl);
|
|
||||||
|
|
||||||
map.on(L.Draw.Event.CREATED, function (event) {
|
|
||||||
const layer = event.layer;
|
|
||||||
const type = event.layerType;
|
|
||||||
|
|
||||||
// Peta draw default tipe: marker -> spbu, polyline -> jalan, polygon -> kavling
|
|
||||||
let customType = type;
|
|
||||||
if(type === 'marker') customType = 'spbu';
|
|
||||||
if(type === 'polyline') customType = 'jalan';
|
|
||||||
if(type === 'polygon') customType = 'kavling';
|
|
||||||
if(window.currentDrawType) customType = window.currentDrawType; // Untuk P2/P3
|
|
||||||
|
|
||||||
onGeometryCreated(customType, layer.toGeoJSON().geometry, layer);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Custom Menu Handlers
|
|
||||||
const markerDrawer = new L.Draw.Marker(map, drawControl.options.draw.marker);
|
|
||||||
const polylineDrawer = new L.Draw.Polyline(map, drawControl.options.draw.polyline);
|
|
||||||
const polygonDrawer = new L.Draw.Polygon(map, drawControl.options.draw.polygon);
|
|
||||||
|
|
||||||
document.getElementById('btn-draw-spbu')?.addEventListener('click', () => { window.currentDrawType='spbu'; markerDrawer.enable(); });
|
|
||||||
document.getElementById('btn-draw-jalan')?.addEventListener('click', () => { window.currentDrawType='jalan'; polylineDrawer.enable(); });
|
|
||||||
document.getElementById('btn-draw-kavling')?.addEventListener('click', () => { window.currentDrawType='kavling'; polygonDrawer.enable(); });
|
|
||||||
|
|
||||||
return drawControl;
|
|
||||||
};
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
export const renderForm = (type, geometry, onSubmit, onCancel) => {
|
|
||||||
const container = document.getElementById('form-container');
|
|
||||||
container.style.display = 'flex';
|
|
||||||
|
|
||||||
const extraFields = type === 'spbu' ? `
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="checkbox-group">
|
|
||||||
<input type="checkbox" id="input-24jam">
|
|
||||||
<span>Buka 24 Jam</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
` : '';
|
|
||||||
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="form-panel">
|
|
||||||
<h3 style="margin:0 0 10px 0; font-size:1rem; border-bottom:1px solid rgba(255,255,255,0.1); padding-bottom:8px;">Simpan Data (${type.toUpperCase()})</h3>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="text" id="input-nama" class="form-control" placeholder="Nama / Label" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="text" id="input-deskripsi" class="form-control" placeholder="Deskripsi Singkat">
|
|
||||||
</div>
|
|
||||||
${extraFields}
|
|
||||||
<div style="display:flex; gap:10px; margin-top:8px;">
|
|
||||||
<button id="btn-save" class="btn-submit" style="flex:1;">Simpan</button>
|
|
||||||
<button id="btn-cancel" class="btn-submit" style="background:rgba(255,255,255,0.1); color:var(--text-light); flex:1;">Batal</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.getElementById('btn-save').addEventListener('click', () => {
|
|
||||||
const payload = {
|
|
||||||
type,
|
|
||||||
geometry,
|
|
||||||
nama: document.getElementById('input-nama').value,
|
|
||||||
deskripsi: document.getElementById('input-deskripsi').value,
|
|
||||||
};
|
|
||||||
if (type === 'spbu') payload.buka_24_jam = document.getElementById('input-24jam').checked;
|
|
||||||
|
|
||||||
onSubmit(payload);
|
|
||||||
container.style.display = 'none';
|
|
||||||
container.innerHTML = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btn-cancel').addEventListener('click', () => {
|
|
||||||
if(onCancel) onCancel();
|
|
||||||
container.style.display = 'none';
|
|
||||||
container.innerHTML = '';
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/**
|
|
||||||
* map.js
|
|
||||||
* Tanggung Jawab: Inisialisasi peta Leaflet dasar dan base layer.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const initMap = (containerId) => {
|
|
||||||
// Pusat peta default (Universitas Tanjungpura, Pontianak)
|
|
||||||
const map = L.map(containerId, {
|
|
||||||
zoomControl: false
|
|
||||||
}).setView([-0.0583, 109.3448], 15);
|
|
||||||
|
|
||||||
// Pindahkan zoom control ke kanan bawah
|
|
||||||
L.control.zoom({
|
|
||||||
position: 'bottomright'
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
||||||
const tileUrl = isDark
|
|
||||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
|
||||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
|
|
||||||
|
|
||||||
// Tile Layer Premium (CartoDB Dark Matter / Light All)
|
|
||||||
const tileLayer = L.tileLayer(tileUrl, {
|
|
||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
|
||||||
subdomains: 'abcd',
|
|
||||||
maxZoom: 20
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
// Dynamic theme updater for tile layer
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
const dark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
||||||
tileLayer.setUrl(dark
|
|
||||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
|
||||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
observer.observe(document.documentElement, { attributes: true });
|
|
||||||
|
|
||||||
return map;
|
|
||||||
};
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
const getBaseUrl = () => {
|
|
||||||
const segments = window.location.pathname.split('/');
|
|
||||||
const targetIndex = segments.findIndex(seg => ['01', '02', '03', 'versi'].includes(seg));
|
|
||||||
if (targetIndex !== -1) {
|
|
||||||
return segments.slice(0, targetIndex).join('/');
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
export const BASE_URL = getBaseUrl();
|
|
||||||
|
|
||||||
const createService = (endpoint) => ({
|
|
||||||
getAll: async () => {
|
|
||||||
const res = await fetch(`${BASE_URL}/${endpoint}`);
|
|
||||||
const json = await res.json();
|
|
||||||
return json.data;
|
|
||||||
},
|
|
||||||
create: async (data) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/${endpoint}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
return json.data;
|
|
||||||
},
|
|
||||||
delete: async (id) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/${endpoint}?id=${id}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
return json.data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// P01
|
|
||||||
export const spbuService = createService('01/backend/api/spbu.php');
|
|
||||||
export const jalanService = createService('01/backend/api/jalan.php');
|
|
||||||
export const kavlingService = createService('01/backend/api/kavling.php');
|
|
||||||
|
|
||||||
// P02
|
|
||||||
export const rumahIbadahService = createService('02/backend/api/rumah_ibadah.php');
|
|
||||||
export const wargaMiskinService = createService('02/backend/api/warga_miskin.php');
|
|
||||||
export const haversineService = {
|
|
||||||
getDalamRadius: async (id, radius) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/02/backend/api/rumah_ibadah.php?action=jangkauan&id=${id}&radius=${radius}`);
|
|
||||||
const json = await res.json();
|
|
||||||
return json.data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { CONFIG } from '../config.js';
|
|
||||||
|
|
||||||
export const rumahIbadahService = {
|
|
||||||
getAll: async () => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/rumah_ibadah.php`);
|
|
||||||
return await res.json();
|
|
||||||
},
|
|
||||||
save: async (data) => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/rumah_ibadah.php`, {
|
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
return await res.json();
|
|
||||||
},
|
|
||||||
delete: async (id) => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/rumah_ibadah.php?id=${id}`, { method: 'DELETE' });
|
|
||||||
return await res.json();
|
|
||||||
},
|
|
||||||
getJangkauan: async (id, radius = 1) => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/rumah_ibadah.php?action=jangkauan&id=${id}&radius=${radius}`);
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { CONFIG } from '../config.js';
|
|
||||||
|
|
||||||
export const wargaMiskinService = {
|
|
||||||
getAll: async () => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/warga_miskin.php`);
|
|
||||||
return await res.json();
|
|
||||||
},
|
|
||||||
save: async (data) => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/warga_miskin.php`, {
|
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
return await res.json();
|
|
||||||
},
|
|
||||||
delete: async (id) => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/warga_miskin.php?id=${id}`, { method: 'DELETE' });
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
export const showToast = (message, type = 'success') => {
|
|
||||||
const container = document.getElementById('toast-container');
|
|
||||||
if (!container) return;
|
|
||||||
const toast = document.createElement('div');
|
|
||||||
toast.className = `toast ${type}`;
|
|
||||||
toast.textContent = message;
|
|
||||||
container.appendChild(toast);
|
|
||||||
setTimeout(() => {
|
|
||||||
toast.style.opacity = '0';
|
|
||||||
toast.style.transform = 'translateY(20px)';
|
|
||||||
toast.style.transition = 'all 0.3s ease';
|
|
||||||
setTimeout(() => toast.remove(), 300);
|
|
||||||
}, 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const clearFormContainer = () => {
|
|
||||||
const container = document.getElementById('form-container');
|
|
||||||
if (container) {
|
|
||||||
container.innerHTML = '';
|
|
||||||
container.style.display = 'none';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const renderResultPanel = (wargaList, radiusKm) => {
|
|
||||||
const panel = document.getElementById('result-panel');
|
|
||||||
const list = document.getElementById('result-list');
|
|
||||||
const title = document.getElementById('result-title');
|
|
||||||
|
|
||||||
panel.style.display = 'block';
|
|
||||||
title.innerHTML = `Warga Miskin <br><span style="font-size:0.8rem;color:var(--text-muted);font-weight:normal;">dalam radius ${radiusKm} km</span>`;
|
|
||||||
|
|
||||||
if (wargaList.length === 0) {
|
|
||||||
list.innerHTML = `<p style="font-size:0.85rem;color:var(--text-muted);">Tidak ada data dalam radius ini.</p>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
list.innerHTML = wargaList.map(w => `
|
|
||||||
<div class="warga-item">
|
|
||||||
<div style="display:flex; justify-content:space-between; margin-bottom:4px;">
|
|
||||||
<strong>${w.nama_kk}</strong>
|
|
||||||
<span class="badge-distance">${w.jarak_km} km</span>
|
|
||||||
</div>
|
|
||||||
<div style="color:var(--text-muted);">Rp ${w.penghasilan.toLocaleString('id-ID')} | ${w.jumlah_tanggungan} orang</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const hideResultPanel = () => {
|
|
||||||
document.getElementById('result-panel').style.display = 'none';
|
|
||||||
};
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* jalan.php
|
|
||||||
* Tanggung Jawab: Menangani operasi CRUD untuk entitas Jalan (LineString).
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'GET':
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, nama, jenis_jalan, created_at, ST_AsGeoJSON(geom) as geojson FROM jalan");
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama' => $row['nama'],
|
|
||||||
'jenis_jalan' => $row['jenis_jalan'],
|
|
||||||
'created_at' => $row['created_at']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data Jalan berhasil diambil');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal mengambil data Jalan: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
if (!isset($input['nama']) || !isset($input['geometry'])) {
|
|
||||||
sendError('Nama dan geometri wajib diisi');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO jalan (nama, jenis_jalan, geom) VALUES (:nama, :jenis_jalan, ST_GeomFromGeoJSON(:geometry))";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':nama' => $input['nama'],
|
|
||||||
':jenis_jalan' => $input['jenis_jalan'] ?? 'Lokal',
|
|
||||||
':geometry' => json_encode($input['geometry'])
|
|
||||||
]);
|
|
||||||
sendSuccess(['id' => $pdo->lastInsertId()], 'Data Jalan berhasil disimpan', 201);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal menyimpan Jalan: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
if (!$id) {
|
|
||||||
sendError('ID Jalan wajib disertakan');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM jalan WHERE id = :id");
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
sendSuccess(null, 'Data Jalan berhasil dihapus');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal menghapus Jalan: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* kavling.php
|
|
||||||
* Tanggung Jawab: Menangani operasi CRUD untuk entitas Kavling (Polygon).
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'GET':
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, nama_pemilik, luas, created_at, ST_AsGeoJSON(geom) as geojson FROM kavling");
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama' => $row['nama_pemilik'],
|
|
||||||
'nama_pemilik' => $row['nama_pemilik'],
|
|
||||||
'luas' => $row['luas'],
|
|
||||||
'created_at' => $row['created_at']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data Kavling berhasil diambil');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal mengambil data Kavling: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
if (!isset($input['nama_pemilik']) || !isset($input['geometry'])) {
|
|
||||||
sendError('Nama pemilik dan geometri wajib diisi');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO kavling (nama_pemilik, luas, geom) VALUES (:nama_pemilik, :luas, ST_GeomFromGeoJSON(:geometry))";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':nama_pemilik' => $input['nama_pemilik'],
|
|
||||||
':luas' => $input['luas'] ?? 0,
|
|
||||||
':geometry' => json_encode($input['geometry'])
|
|
||||||
]);
|
|
||||||
sendSuccess(['id' => $pdo->lastInsertId()], 'Data Kavling berhasil disimpan', 201);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal menyimpan Kavling: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
if (!$id) {
|
|
||||||
sendError('ID Kavling wajib disertakan');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM kavling WHERE id = :id");
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
sendSuccess(null, 'Data Kavling berhasil dihapus');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal menghapus Kavling: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* rumah_ibadah.php
|
|
||||||
* Tanggung Jawab: Operasi CRUD Rumah Ibadah + Endpoint analisis jangkauan Haversine.
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
require_once '../utils/geo_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'GET':
|
|
||||||
if (isset($_GET['action']) && $_GET['action'] === 'jangkauan' && isset($_GET['id'])) {
|
|
||||||
// Analisis Jangkauan Haversine
|
|
||||||
$id = $_GET['id'];
|
|
||||||
$radius = isset($_GET['radius']) ? (float) $_GET['radius'] : 1.0; // default 1 km
|
|
||||||
|
|
||||||
// Dapatkan koordinat rumah ibadah ini
|
|
||||||
$stmt = $pdo->prepare("SELECT ST_AsGeoJSON(geom) as geojson FROM rumah_ibadah WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
$rumah_ibadah = $stmt->fetch();
|
|
||||||
|
|
||||||
if (!$rumah_ibadah) sendError('Rumah ibadah tidak ditemukan', 404);
|
|
||||||
|
|
||||||
$geom = json_decode($rumah_ibadah['geojson'], true);
|
|
||||||
$pusatLon = $geom['coordinates'][0];
|
|
||||||
$pusatLat = $geom['coordinates'][1];
|
|
||||||
|
|
||||||
// Panggil geo_helper untuk mendapatkan warga miskin
|
|
||||||
$warga = GeoHelper::getWargaDalamRadius($pdo, $pusatLat, $pusatLon, $radius);
|
|
||||||
|
|
||||||
// Format ke GeoJSON FeatureCollection sesuai kebutuhan frontend
|
|
||||||
$features = [];
|
|
||||||
foreach ($warga as $w) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => $w['geometry'],
|
|
||||||
'properties' => [
|
|
||||||
'id' => $w['id'],
|
|
||||||
'nama' => $w['nama_kk'],
|
|
||||||
'penghasilan' => $w['penghasilan'],
|
|
||||||
'jumlah_tanggungan' => $w['jumlah_tanggungan'],
|
|
||||||
'jarak_km' => $w['jarak_km']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data jangkauan berhasil dihitung');
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// GET Semua Rumah Ibadah (GeoJSON)
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, nama, agama, ST_AsGeoJSON(geom) as geojson FROM rumah_ibadah");
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama' => $row['nama'],
|
|
||||||
'agama' => $row['agama']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data Rumah Ibadah');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
if (!isset($input['nama']) || !isset($input['geometry'])) sendError('Validasi gagal');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO rumah_ibadah (nama, agama, geom) VALUES (:nama, :agama, ST_GeomFromGeoJSON(:geometry))";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':nama' => $input['nama'],
|
|
||||||
':agama' => $input['agama'] ?? 'Islam',
|
|
||||||
':geometry' => json_encode($input['geometry'])
|
|
||||||
]);
|
|
||||||
sendSuccess(['id' => $pdo->lastInsertId()], 'Disimpan', 201);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
if (!$id) sendError('ID dibutuhkan');
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM rumah_ibadah WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
sendSuccess(null, 'Dihapus');
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'GET':
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, nama, deskripsi, buka_24_jam, created_at, ST_AsGeoJSON(geom) as geojson FROM spbu");
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama' => $row['nama'],
|
|
||||||
'deskripsi' => $row['deskripsi'],
|
|
||||||
'buka_24_jam' => (bool)$row['buka_24_jam'],
|
|
||||||
'created_at' => $row['created_at']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data SPBU berhasil diambil');
|
|
||||||
} catch (PDOException $e) { sendError('Gagal: ' . $e->getMessage(), 500); }
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
if (!isset($input['nama']) || !isset($input['geometry'])) sendError('Validasi gagal');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO spbu (nama, deskripsi, buka_24_jam, geom) VALUES (:nama, :deskripsi, :buka_24_jam, ST_GeomFromGeoJSON(:geometry))";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':nama' => $input['nama'],
|
|
||||||
':deskripsi' => $input['deskripsi'] ?? '',
|
|
||||||
':buka_24_jam' => isset($input['buka_24_jam']) && $input['buka_24_jam'] ? 1 : 0,
|
|
||||||
':geometry' => json_encode($input['geometry'])
|
|
||||||
]);
|
|
||||||
sendSuccess(['id' => $pdo->lastInsertId()], 'Disimpan', 201);
|
|
||||||
} catch (PDOException $e) { sendError('Gagal: ' . $e->getMessage(), 500); }
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
if (!$id) sendError('ID dibutuhkan');
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM spbu WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
sendSuccess(null, 'Dihapus');
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* statistik.php
|
|
||||||
* Tanggung Jawab: Menyediakan data analisis spasial menggunakan Point in Polygon (ST_Contains).
|
|
||||||
* Menghitung kepadatan warga miskin dalam setiap kavling.
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
if ($method === 'GET') {
|
|
||||||
try {
|
|
||||||
// Query Spatial Point-in-Polygon
|
|
||||||
// Mengambil Kavling dan menghitung jumlah warga miskin (Point) yang ada di dalamnya
|
|
||||||
$sql = "
|
|
||||||
SELECT
|
|
||||||
k.id,
|
|
||||||
k.nama_pemilik,
|
|
||||||
k.luas,
|
|
||||||
ST_AsGeoJSON(k.geom) as geojson,
|
|
||||||
COUNT(w.id) as jumlah_warga,
|
|
||||||
COALESCE(SUM(w.jumlah_tanggungan), 0) as total_tanggungan
|
|
||||||
FROM kavling k
|
|
||||||
LEFT JOIN warga_miskin w ON ST_Contains(k.geom, w.geom)
|
|
||||||
GROUP BY k.id
|
|
||||||
";
|
|
||||||
|
|
||||||
$stmt = $pdo->query($sql);
|
|
||||||
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$jumlah = (int) $row['jumlah_warga'];
|
|
||||||
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama' => $row['nama_pemilik'],
|
|
||||||
'nama_pemilik' => $row['nama_pemilik'],
|
|
||||||
'luas' => $row['luas'],
|
|
||||||
'jumlah_warga_miskin' => $jumlah,
|
|
||||||
'jumlah_warga' => $jumlah,
|
|
||||||
'total_tanggungan' => (int) $row['total_tanggungan']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data statistik spasial berhasil dihitung');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal menghitung statistik spasial: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* warga_miskin.php
|
|
||||||
* Tanggung Jawab: Operasi CRUD untuk Warga Miskin.
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once __DIR__ . '/../../../webgis_app/core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'GET':
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, nama_kk, penghasilan, jumlah_tanggungan, ST_AsGeoJSON(geom) as geojson FROM warga_miskin");
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama_kk' => $row['nama_kk'],
|
|
||||||
'penghasilan' => $row['penghasilan'],
|
|
||||||
'jumlah_tanggungan' => $row['jumlah_tanggungan']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data Warga Miskin');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
if (!isset($input['nama_kk']) || !isset($input['geometry'])) sendError('Validasi gagal');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO warga_miskin (nama_kk, penghasilan, jumlah_tanggungan, geom) VALUES (:nama_kk, :penghasilan, :jumlah_tanggungan, ST_GeomFromGeoJSON(:geometry))";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':nama_kk' => $input['nama_kk'],
|
|
||||||
':penghasilan' => $input['penghasilan'] ?? 0,
|
|
||||||
':jumlah_tanggungan' => $input['jumlah_tanggungan'] ?? 0,
|
|
||||||
':geometry' => json_encode($input['geometry'])
|
|
||||||
]);
|
|
||||||
sendSuccess(['id' => $pdo->lastInsertId()], 'Disimpan', 201);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
if (!$id) sendError('ID dibutuhkan');
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM warga_miskin WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
sendSuccess(null, 'Dihapus');
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Singleton PDO connection.
|
|
||||||
*
|
|
||||||
* Coolify: set DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, and DB_PASSWORD
|
|
||||||
* from the database service credentials.
|
|
||||||
*/
|
|
||||||
class Database {
|
|
||||||
private static $conn = null;
|
|
||||||
|
|
||||||
private static function env(string $key, string $default = ''): string {
|
|
||||||
$value = getenv($key);
|
|
||||||
return ($value === false || $value === '') ? $default : $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getConnection() {
|
|
||||||
if (self::$conn === null) {
|
|
||||||
$dbUrl = self::env('DATABASE_URL');
|
|
||||||
if ($dbUrl) {
|
|
||||||
$parsed = parse_url($dbUrl);
|
|
||||||
$host = $parsed['host'] ?? '127.0.0.1';
|
|
||||||
$port = $parsed['port'] ?? '3306';
|
|
||||||
$username = $parsed['user'] ?? 'root';
|
|
||||||
$password = $parsed['pass'] ?? '';
|
|
||||||
$dbName = ltrim($parsed['path'], '/');
|
|
||||||
} else {
|
|
||||||
$host = self::env('DB_HOST', '127.0.0.1');
|
|
||||||
$port = self::env('DB_PORT', '3306');
|
|
||||||
$dbName = self::env('DB_DATABASE', 'webgis_db');
|
|
||||||
$username = self::env('DB_USERNAME', 'root');
|
|
||||||
$password = self::env('DB_PASSWORD', '');
|
|
||||||
}
|
|
||||||
|
|
||||||
self::$conn = new PDO(
|
|
||||||
"mysql:host={$host};port={$port};dbname={$dbName};charset=utf8mb4",
|
|
||||||
$username,
|
|
||||||
$password
|
|
||||||
);
|
|
||||||
self::$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
||||||
self::$conn->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
|
||||||
|
|
||||||
// Disable strict mode for GROUP BY to prevent 1055 errors in Coolify's MariaDB
|
|
||||||
self::$conn->exec("SET SESSION sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''))");
|
|
||||||
|
|
||||||
// Auto-initialize database if empty
|
|
||||||
$stmt = self::$conn->query("SHOW TABLES LIKE 'users'");
|
|
||||||
if ($stmt->rowCount() == 0) {
|
|
||||||
$sqlFile = __DIR__ . '/../../../database/webgis_db.sql';
|
|
||||||
if (file_exists($sqlFile)) {
|
|
||||||
$sql = file_get_contents($sqlFile);
|
|
||||||
self::$conn->exec($sql);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return self::$conn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* response_helper.php
|
|
||||||
* Tanggung Jawab: Standarisasi format response JSON.
|
|
||||||
*/
|
|
||||||
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
|
||||||
$allowedOrigin = getenv('CORS_ALLOWED_ORIGIN');
|
|
||||||
if ($allowedOrigin !== false && $allowedOrigin !== '') {
|
|
||||||
header('Access-Control-Allow-Origin: ' . $allowedOrigin);
|
|
||||||
header('Vary: Origin');
|
|
||||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
|
||||||
header('Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit(0); }
|
|
||||||
|
|
||||||
function sendSuccess($data = null, $message = 'Success', $statusCode = 200) {
|
|
||||||
http_response_code($statusCode);
|
|
||||||
echo json_encode(['status' => 'success', 'message' => $message, 'data' => $data]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendError($message = 'Error', $statusCode = 400) {
|
|
||||||
http_response_code($statusCode);
|
|
||||||
echo json_encode(['status' => 'error', 'message' => $message]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
/* base.css
|
|
||||||
* Tanggung Jawab: Reset CSS, definisi variabel warna premium, dan tipografi dasar.
|
|
||||||
*/
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
|
||||||
|
|
||||||
:root {
|
|
||||||
/* Color Palette - Minimalist & Elegant */
|
|
||||||
--primary: #4F46E5;
|
|
||||||
--primary-hover: #4338CA;
|
|
||||||
--secondary: #10B981;
|
|
||||||
--background: #F3F4F6;
|
|
||||||
--surface: #FFFFFF;
|
|
||||||
--surface-glass: rgba(255, 255, 255, 0.85);
|
|
||||||
--text-main: #111827;
|
|
||||||
--text-muted: #6B7280;
|
|
||||||
--border: #E5E7EB;
|
|
||||||
--danger: #EF4444;
|
|
||||||
|
|
||||||
/* Shadows & Effects */
|
|
||||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
||||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
||||||
--radius-md: 12px;
|
|
||||||
--radius-lg: 16px;
|
|
||||||
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
background-color: var(--background);
|
|
||||||
color: var(--text-main);
|
|
||||||
display: flex;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4 {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar minimalis */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: #CBD5E1;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #94A3B8;
|
|
||||||
}
|
|
||||||
@@ -1,383 +0,0 @@
|
|||||||
/* ═══════════════════════════════════════════════════
|
|
||||||
WebGIS P01 — main.css | Dark / Light Theme
|
|
||||||
═══════════════════════════════════════════════════ */
|
|
||||||
|
|
||||||
/* ── Design Tokens ── */
|
|
||||||
:root {
|
|
||||||
--ease: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
--transition: all 0.25s var(--ease);
|
|
||||||
--font: 'Inter', sans-serif;
|
|
||||||
--radius: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Dark Theme (default) ── */
|
|
||||||
[data-theme="dark"] {
|
|
||||||
--primary: #818CF8;
|
|
||||||
--primary-deep: #6366F1;
|
|
||||||
--primary-glow: rgba(99,102,241,0.3);
|
|
||||||
--bg-dark: #0A0F1E;
|
|
||||||
--bg-glass: rgba(17,24,39,0.82);
|
|
||||||
--bg-input: rgba(255,255,255,0.05);
|
|
||||||
--border-glass: rgba(255,255,255,0.08);
|
|
||||||
--border-focus: #6366F1;
|
|
||||||
--text-main: #F1F5F9;
|
|
||||||
--text-muted: #94A3B8;
|
|
||||||
--shadow: 0 25px 50px -12px rgba(0,0,0,0.55);
|
|
||||||
--dot-color: rgba(255,255,255,0.025);
|
|
||||||
--btn-spbu: #10B981;
|
|
||||||
--btn-jalan: #F59E0B;
|
|
||||||
--btn-kavling: #3B82F6;
|
|
||||||
--btn-rumah: #8B5CF6;
|
|
||||||
--btn-warga: #EF4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Light Theme ── */
|
|
||||||
[data-theme="light"] {
|
|
||||||
--primary: #4F46E5;
|
|
||||||
--primary-deep: #3730A3;
|
|
||||||
--primary-glow: rgba(79,70,229,0.15);
|
|
||||||
--bg-dark: #F0F4FF;
|
|
||||||
--bg-glass: rgba(255,255,255,0.92);
|
|
||||||
--bg-input: #FFFFFF;
|
|
||||||
--border-glass: #E2E8F0;
|
|
||||||
--border-focus: #4F46E5;
|
|
||||||
--text-main: #0F172A;
|
|
||||||
--text-muted: #64748B;
|
|
||||||
--shadow: 0 25px 50px -12px rgba(79,70,229,0.12);
|
|
||||||
--dot-color: rgba(79,70,229,0.035);
|
|
||||||
--btn-spbu: #059669;
|
|
||||||
--btn-jalan: #D97706;
|
|
||||||
--btn-kavling: #2563EB;
|
|
||||||
--btn-rumah: #7C3AED;
|
|
||||||
--btn-warga: #DC2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Reset ── */
|
|
||||||
*, *::before, *::after { box-sizing: border-box; }
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: var(--font);
|
|
||||||
background: var(--bg-dark);
|
|
||||||
color: var(--text-main);
|
|
||||||
overflow: hidden;
|
|
||||||
transition: background 0.3s var(--ease), color 0.3s var(--ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dot grid background */
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background-image: radial-gradient(circle, var(--dot-color) 1px, transparent 1px);
|
|
||||||
background-size: 28px 28px;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Map ── */
|
|
||||||
#map {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Floating Dock ── */
|
|
||||||
.floating-dock {
|
|
||||||
position: absolute;
|
|
||||||
top: 24px;
|
|
||||||
left: 24px;
|
|
||||||
z-index: 1000;
|
|
||||||
background: var(--bg-glass);
|
|
||||||
backdrop-filter: blur(24px);
|
|
||||||
-webkit-backdrop-filter: blur(24px);
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
width: 320px;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
max-height: calc(100vh - 48px);
|
|
||||||
overflow-y: auto;
|
|
||||||
transition: background 0.3s var(--ease), border-color 0.3s var(--ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating-dock::-webkit-scrollbar { width: 4px; }
|
|
||||||
.floating-dock::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 10px; }
|
|
||||||
|
|
||||||
/* ── Dock Top Bar ── */
|
|
||||||
.dock-topbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-back-portal {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
background: var(--bg-input);
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: var(--transition);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.btn-back-portal:hover {
|
|
||||||
background: var(--primary-deep);
|
|
||||||
border-color: var(--primary-deep);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dock-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-theme-dock {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
background: var(--bg-input);
|
|
||||||
color: var(--text-muted);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
.btn-theme-dock:hover {
|
|
||||||
background: var(--primary-deep);
|
|
||||||
border-color: var(--primary-deep);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Header ── */
|
|
||||||
.header h2 {
|
|
||||||
margin: 0 0 6px 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
background: linear-gradient(135deg, var(--primary), #C084FC);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
.header p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Buttons ── */
|
|
||||||
.menu-group { display: flex; flex-direction: column; gap: 8px; }
|
|
||||||
.menu-title {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action {
|
|
||||||
background: var(--bg-input);
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
color: var(--text-main);
|
|
||||||
padding: 11px 14px;
|
|
||||||
border-radius: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: var(--font);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
transition: var(--transition);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action svg { width: 18px; height: 18px; flex-shrink: 0; }
|
|
||||||
|
|
||||||
.btn-action:hover {
|
|
||||||
background: rgba(255,255,255,0.08);
|
|
||||||
transform: translateX(3px);
|
|
||||||
border-color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] .btn-action:hover {
|
|
||||||
background: #EEF2FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action.active {
|
|
||||||
background: var(--primary-deep);
|
|
||||||
border-color: var(--primary-deep);
|
|
||||||
color: white;
|
|
||||||
box-shadow: 0 4px 12px var(--primary-glow);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button color variants */
|
|
||||||
.btn-spbu svg { color: var(--btn-spbu); }
|
|
||||||
.btn-jalan svg { color: var(--btn-jalan); }
|
|
||||||
.btn-kavling svg { color: var(--btn-kavling); }
|
|
||||||
.btn-rumah svg { color: var(--btn-rumah); }
|
|
||||||
.btn-warga svg { color: var(--btn-warga); }
|
|
||||||
|
|
||||||
/* ── Form Panel ── */
|
|
||||||
.form-panel {
|
|
||||||
background: rgba(0,0,0,0.3);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 14px;
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] .form-panel {
|
|
||||||
background: rgba(79,70,229,0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group { display: flex; flex-direction: column; gap: 5px; }
|
|
||||||
.form-group label { font-size: 0.78rem; color: var(--text-muted); font-weight: 500; }
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
background: var(--bg-input);
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
padding: 9px 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: var(--text-main);
|
|
||||||
font-family: var(--font);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
outline: none;
|
|
||||||
transition: border 0.2s;
|
|
||||||
}
|
|
||||||
.form-control:focus { border-color: var(--border-focus); box-shadow: 0 0 0 3px var(--primary-glow); }
|
|
||||||
.form-control::placeholder { color: var(--text-muted); }
|
|
||||||
|
|
||||||
.checkbox-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.checkbox-group input { width: 15px; height: 15px; accent-color: var(--primary); cursor: pointer; }
|
|
||||||
.checkbox-group span { font-size: 0.82rem; color: var(--text-main); }
|
|
||||||
|
|
||||||
.btn-submit {
|
|
||||||
background: var(--primary-deep);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 11px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
font-family: var(--font);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: var(--transition);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
.btn-submit:hover { filter: brightness(1.1); transform: translateY(-1px); }
|
|
||||||
|
|
||||||
/* ── Leaflet Customizations ── */
|
|
||||||
.leaflet-draw-toolbar { display: none !important; }
|
|
||||||
.leaflet-control-zoom { border: none !important; box-shadow: var(--shadow) !important; }
|
|
||||||
.leaflet-control-zoom a {
|
|
||||||
background: var(--bg-glass) !important;
|
|
||||||
color: var(--text-main) !important;
|
|
||||||
border-color: var(--border-glass) !important;
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
.leaflet-control-zoom a:hover { background: var(--primary-deep) !important; color: white !important; }
|
|
||||||
|
|
||||||
/* Custom Leaflet Layer Control style to match theme */
|
|
||||||
.leaflet-control-layers {
|
|
||||||
background: var(--bg-glass) !important;
|
|
||||||
backdrop-filter: blur(24px) !important;
|
|
||||||
-webkit-backdrop-filter: blur(24px) !important;
|
|
||||||
border: 1px solid var(--border-glass) !important;
|
|
||||||
border-radius: 14px !important;
|
|
||||||
box-shadow: var(--shadow) !important;
|
|
||||||
color: var(--text-main) !important;
|
|
||||||
font-family: var(--font) !important;
|
|
||||||
padding: 10px 14px !important;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
.leaflet-control-layers-expanded {
|
|
||||||
min-width: 140px;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers label {
|
|
||||||
margin-bottom: 6px !important;
|
|
||||||
font-size: 0.85rem !important;
|
|
||||||
cursor: pointer !important;
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center !important;
|
|
||||||
gap: 8px !important;
|
|
||||||
color: var(--text-main) !important;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers label:last-child {
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
|
||||||
.leaflet-control-layers input[type="checkbox"] {
|
|
||||||
cursor: pointer !important;
|
|
||||||
width: 15px !important;
|
|
||||||
height: 15px !important;
|
|
||||||
accent-color: var(--primary-deep) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-popup-content-wrapper {
|
|
||||||
background: var(--bg-glass) !important;
|
|
||||||
backdrop-filter: blur(16px) !important;
|
|
||||||
border: 1px solid var(--border-glass) !important;
|
|
||||||
color: var(--text-main) !important;
|
|
||||||
border-radius: 14px !important;
|
|
||||||
box-shadow: var(--shadow) !important;
|
|
||||||
}
|
|
||||||
.leaflet-popup-tip { background: var(--bg-glass) !important; }
|
|
||||||
.leaflet-popup-content { margin: 14px !important; font-family: var(--font); }
|
|
||||||
.leaflet-popup-content h3 { margin: 0 0 8px 0; font-size: 1rem; border-bottom: 1px solid var(--border-glass); padding-bottom: 8px; }
|
|
||||||
.leaflet-popup-content p { margin: 4px 0; font-size: 0.85rem; color: var(--text-muted); }
|
|
||||||
|
|
||||||
.leaflet-popup-content .btn-delete {
|
|
||||||
background: rgba(239,68,68,0.1);
|
|
||||||
color: #EF4444;
|
|
||||||
border: 1px solid rgba(239,68,68,0.3);
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-top: 10px;
|
|
||||||
width: 100%;
|
|
||||||
font-family: var(--font);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
.leaflet-popup-content .btn-delete:hover { background: #EF4444; color: white; }
|
|
||||||
|
|
||||||
/* ── Toast ── */
|
|
||||||
.toast-container {
|
|
||||||
position: fixed; bottom: 24px; right: 24px; z-index: 9999;
|
|
||||||
display: flex; flex-direction: column; gap: 10px;
|
|
||||||
}
|
|
||||||
.toast {
|
|
||||||
background: var(--bg-glass); backdrop-filter: blur(16px);
|
|
||||||
border: 1px solid var(--border-glass); border-left: 4px solid var(--primary-deep);
|
|
||||||
padding: 14px 18px; border-radius: 12px; color: var(--text-main); font-size: 0.875rem;
|
|
||||||
box-shadow: var(--shadow); animation: slideIn 0.3s var(--ease);
|
|
||||||
}
|
|
||||||
.toast.error { border-left-color: #EF4444; }
|
|
||||||
.toast.success { border-left-color: #10B981; }
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
from { transform: translateX(100%); opacity: 0; }
|
|
||||||
to { transform: translateX(0); opacity: 1; }
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
/* form.css
|
|
||||||
* Tanggung Jawab: Styling komponen form (input, button, select).
|
|
||||||
*/
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-label {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 14px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
transition: var(--transition);
|
|
||||||
background: var(--surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--primary);
|
|
||||||
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-family: inherit;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: var(--transition);
|
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: var(--primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: var(--primary-hover);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background: var(--danger);
|
|
||||||
color: white;
|
|
||||||
width: auto;
|
|
||||||
padding: 6px 12px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger:hover {
|
|
||||||
background: #DC2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Micro-animation for active state */
|
|
||||||
.btn:active {
|
|
||||||
transform: translateY(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form Container Animation */
|
|
||||||
#form-container {
|
|
||||||
background: var(--surface);
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
animation: fadeIn 0.4s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; transform: scale(0.98); }
|
|
||||||
to { opacity: 1; transform: scale(1); }
|
|
||||||
}
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="id" data-theme="dark">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>WebGIS — Pertemuan 03: Analisis Spasial</title>
|
|
||||||
<meta name="description" content="Modul 03 — Choropleth Map dinamis berbasis Point-in-Polygon berbasis WebGIS.">
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
|
||||||
<link rel="stylesheet" href="css/main.css">
|
|
||||||
<style>
|
|
||||||
.result-item {
|
|
||||||
background: var(--bg-input);
|
|
||||||
padding: 8px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
}
|
|
||||||
.badge {
|
|
||||||
background: var(--primary-deep);
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 0.68rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
.stat-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 0.82rem;
|
|
||||||
}
|
|
||||||
.stat-row strong { font-weight: 700; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<!-- Map container -->
|
|
||||||
<div id="map"></div>
|
|
||||||
|
|
||||||
<!-- Floating Dock -->
|
|
||||||
<div class="floating-dock">
|
|
||||||
|
|
||||||
<!-- Dock top bar -->
|
|
||||||
<div class="dock-topbar">
|
|
||||||
<a href="../../portal.html" class="btn-back-portal" title="Kembali ke Portal">
|
|
||||||
<i class="fas fa-chevron-left"></i>
|
|
||||||
Portal
|
|
||||||
</a>
|
|
||||||
<div class="dock-controls">
|
|
||||||
<button class="btn-theme-dock" id="themeToggle" title="Toggle tema" aria-label="Toggle dark/light mode">
|
|
||||||
<i class="fas fa-moon" id="themeIcon"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="header">
|
|
||||||
<h2>P03: Analisis Spasial</h2>
|
|
||||||
<p>Kumulatif P01 & P02 + Choropleth Map (Point-in-Polygon).</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Layer Peta (Centang) -->
|
|
||||||
<div class="menu-group">
|
|
||||||
<div class="menu-title">Layer Peta</div>
|
|
||||||
<div class="form-panel" style="gap: 10px;">
|
|
||||||
<label class="checkbox-group">
|
|
||||||
<input type="checkbox" id="layer-spbu" checked>
|
|
||||||
<span>Stasiun SPBU</span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox-group">
|
|
||||||
<input type="checkbox" id="layer-jalan" checked>
|
|
||||||
<span>Jalur Jalan</span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox-group">
|
|
||||||
<input type="checkbox" id="layer-rumah" checked>
|
|
||||||
<span>Rumah Ibadah</span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox-group">
|
|
||||||
<input type="checkbox" id="layer-warga" checked>
|
|
||||||
<span>Warga Miskin</span>
|
|
||||||
</label>
|
|
||||||
<label class="checkbox-group">
|
|
||||||
<input type="checkbox" id="layer-choro" checked>
|
|
||||||
<span>Choropleth Kepadatan</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Draw Tools -->
|
|
||||||
<div class="menu-group">
|
|
||||||
<div class="menu-title">Draw Tools</div>
|
|
||||||
<button id="btn-draw-spbu" class="btn-action btn-spbu">
|
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.242-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
|
||||||
Tambah SPBU
|
|
||||||
</button>
|
|
||||||
<button id="btn-draw-jalan" class="btn-action btn-jalan">
|
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path></svg>
|
|
||||||
Tambah Jalan
|
|
||||||
</button>
|
|
||||||
<button id="btn-draw-kavling" class="btn-action btn-kavling">
|
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg>
|
|
||||||
Tambah Kavling
|
|
||||||
</button>
|
|
||||||
<button id="btn-draw-rumah" class="btn-action btn-rumah">
|
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg>
|
|
||||||
Tambah Rumah Ibadah
|
|
||||||
</button>
|
|
||||||
<button id="btn-draw-warga" class="btn-action btn-warga">
|
|
||||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path></svg>
|
|
||||||
Tambah Warga Miskin
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Choropleth stats panel -->
|
|
||||||
<div id="choropleth-panel" class="form-panel" style="margin-top:4px; background:rgba(16,185,129,0.07); border-color:rgba(16,185,129,0.25);">
|
|
||||||
<h3 style="margin:0 0 8px 0; font-size:0.85rem; color:#10B981; font-weight:600;">
|
|
||||||
<i class="fas fa-chart-area" style="margin-right:6px;"></i>Statistik Kavling (Choropleth)
|
|
||||||
</h3>
|
|
||||||
<p style="font-size:0.78rem; margin:0 0 10px 0; color:var(--text-muted);">Pewarnaan otomatis area kavling berdasarkan jumlah warga miskin yang bermukim di atasnya.</p>
|
|
||||||
<div class="stat-row" style="margin-bottom:6px;">
|
|
||||||
<span>Total Penduduk Miskin:</span>
|
|
||||||
<strong id="stat-total-warga" style="color:var(--text-main);">0</strong>
|
|
||||||
</div>
|
|
||||||
<div class="stat-row">
|
|
||||||
<span>Kavling Padat (>2 warga):</span>
|
|
||||||
<strong id="stat-kavling-merah" style="color:#EF4444;">0</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Dynamic form container -->
|
|
||||||
<div id="form-container" style="display: none;"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Toast -->
|
|
||||||
<div id="toast-container" class="toast-container"></div>
|
|
||||||
|
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
|
||||||
<script>
|
|
||||||
// ── Theme toggle ──
|
|
||||||
const html = document.documentElement;
|
|
||||||
const btn = document.getElementById('themeToggle');
|
|
||||||
const icon = document.getElementById('themeIcon');
|
|
||||||
const saved = localStorage.getItem('portal-theme') || 'dark';
|
|
||||||
setTheme(saved);
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
|
||||||
setTheme(next);
|
|
||||||
localStorage.setItem('portal-theme', next);
|
|
||||||
});
|
|
||||||
function setTheme(t) {
|
|
||||||
html.setAttribute('data-theme', t);
|
|
||||||
icon.className = t === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script type="module" src="js/app.js?v=20260613-9"></script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
import { initMap } from './modules/map.js';
|
|
||||||
import { setupDrawControls } from './modules/draw.js';
|
|
||||||
import { renderForm } from './modules/form.js';
|
|
||||||
import { spbuService, jalanService, kavlingService, rumahIbadahService, wargaMiskinService, BASE_URL } from './services/api.service.js';
|
|
||||||
|
|
||||||
let appMap;
|
|
||||||
let drawControl;
|
|
||||||
|
|
||||||
const statistikService = {
|
|
||||||
getChoropleth: async () => {
|
|
||||||
const res = await fetch(`${BASE_URL}/03/backend/api/statistik.php`);
|
|
||||||
const json = await res.json();
|
|
||||||
return json.data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createIcon = (svgPath, color) => L.divIcon({
|
|
||||||
className: 'custom-icon',
|
|
||||||
html: `<div style="background-color: ${color}; width: 34px; height: 34px; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 10px rgba(0,0,0,0.5); border: 2px solid rgba(255,255,255,0.8); color: white;"><svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">${svgPath}</svg></div>`,
|
|
||||||
iconSize: [34, 34], iconAnchor: [17, 34], popupAnchor: [0, -34]
|
|
||||||
});
|
|
||||||
|
|
||||||
const iconSPBU = (is24h) => createIcon('<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.242-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>', is24h ? '#10B981' : '#EF4444');
|
|
||||||
const iconRumah = createIcon('<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>', '#8B5CF6');
|
|
||||||
const iconWarga = createIcon('<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', '#EF4444');
|
|
||||||
|
|
||||||
window.showToast = (msg, type='success') => {
|
|
||||||
const t = document.createElement('div'); t.className = `toast ${type}`; t.innerHTML = msg;
|
|
||||||
document.getElementById('toast-container').appendChild(t);
|
|
||||||
setTimeout(() => { t.style.opacity=0; setTimeout(()=>t.remove(),300); }, 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadAllData = async () => {
|
|
||||||
try {
|
|
||||||
const [spbu, jalan, choro, rumah, warga] = await Promise.all([
|
|
||||||
spbuService.getAll(), jalanService.getAll(), statistikService.getChoropleth(),
|
|
||||||
rumahIbadahService.getAll(), wargaMiskinService.getAll()
|
|
||||||
]);
|
|
||||||
|
|
||||||
const spbuLayer = L.geoJSON(spbu, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconSPBU(f.properties.buka_24_jam) }), onEachFeature: (f, l) => bindPopup(l, 'spbu', f.properties) }).addTo(appMap);
|
|
||||||
const jalanLayer = L.geoJSON(jalan, { style: { color: '#F59E0B', weight: 4 }, onEachFeature: (f, l) => bindPopup(l, 'jalan', f.properties) }).addTo(appMap);
|
|
||||||
const rumahLayer = L.geoJSON(rumah, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconRumah }), onEachFeature: (f, l) => bindPopup(l, 'rumah_ibadah', f.properties) }).addTo(appMap);
|
|
||||||
const wargaLayer = L.geoJSON(warga, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconWarga }), onEachFeature: (f, l) => bindPopup(l, 'warga_miskin', f.properties) }).addTo(appMap);
|
|
||||||
|
|
||||||
let merahCount = 0;
|
|
||||||
const choroLayer = L.geoJSON(choro, {
|
|
||||||
style: (f) => {
|
|
||||||
const count = f.properties.jumlah_warga_miskin;
|
|
||||||
let c = '#10B981'; // Hijau (0)
|
|
||||||
if(count > 0) c = '#F59E0B'; // Kuning (1-2)
|
|
||||||
if(count > 2) { c = '#EF4444'; merahCount++; } // Merah (>2)
|
|
||||||
return { color: c, fillColor: c, fillOpacity: 0.4, weight: 2 };
|
|
||||||
},
|
|
||||||
onEachFeature: (f, l) => bindPopup(l, 'kavling', f.properties)
|
|
||||||
}).addTo(appMap);
|
|
||||||
|
|
||||||
// Bind checkbox events
|
|
||||||
document.getElementById('layer-spbu').addEventListener('change', (e) => {
|
|
||||||
if (e.target.checked) appMap.addLayer(spbuLayer);
|
|
||||||
else appMap.removeLayer(spbuLayer);
|
|
||||||
});
|
|
||||||
document.getElementById('layer-jalan').addEventListener('change', (e) => {
|
|
||||||
if (e.target.checked) appMap.addLayer(jalanLayer);
|
|
||||||
else appMap.removeLayer(jalanLayer);
|
|
||||||
});
|
|
||||||
document.getElementById('layer-rumah').addEventListener('change', (e) => {
|
|
||||||
if (e.target.checked) appMap.addLayer(rumahLayer);
|
|
||||||
else appMap.removeLayer(rumahLayer);
|
|
||||||
});
|
|
||||||
document.getElementById('layer-warga').addEventListener('change', (e) => {
|
|
||||||
if (e.target.checked) appMap.addLayer(wargaLayer);
|
|
||||||
else appMap.removeLayer(wargaLayer);
|
|
||||||
});
|
|
||||||
document.getElementById('layer-choro').addEventListener('change', (e) => {
|
|
||||||
if (e.target.checked) appMap.addLayer(choroLayer);
|
|
||||||
else appMap.removeLayer(choroLayer);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('stat-total-warga').innerText = warga.features.length;
|
|
||||||
document.getElementById('stat-kavling-merah').innerText = merahCount;
|
|
||||||
|
|
||||||
} catch (e) { window.showToast("Gagal meload data: "+e.message, 'error'); }
|
|
||||||
};
|
|
||||||
|
|
||||||
const bindPopup = (layer, type, props) => {
|
|
||||||
let ext = '';
|
|
||||||
if(type==='spbu') ext = `<p>24 Jam: <strong style="color:${props.buka_24_jam?'#10B981':'#EF4444'}">${props.buka_24_jam?'Ya':'Tidak'}</strong></p>`;
|
|
||||||
if(type==='kavling') ext = `<p>Jml Warga Miskin: <strong>${props.jumlah_warga_miskin||0} Jiwa</strong></p>`;
|
|
||||||
|
|
||||||
layer.bindPopup(`
|
|
||||||
<div class="popup-custom">
|
|
||||||
<h3>${props.nama}</h3>
|
|
||||||
<p>${props.deskripsi || ''}</p>
|
|
||||||
${ext}
|
|
||||||
<button class="btn-delete" onclick="window.deleteData('${type}', ${props.id})">Hapus</button>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.deleteData = async (type, id) => {
|
|
||||||
if(!confirm('Yakin menghapus?')) return;
|
|
||||||
try {
|
|
||||||
if(type==='spbu') await spbuService.delete(id);
|
|
||||||
if(type==='jalan') await jalanService.delete(id);
|
|
||||||
if(type==='kavling') await kavlingService.delete(id);
|
|
||||||
if(type==='rumah_ibadah') await rumahIbadahService.delete(id);
|
|
||||||
if(type==='warga_miskin') await wargaMiskinService.delete(id);
|
|
||||||
window.showToast('Data dihapus'); setTimeout(()=>location.reload(), 800);
|
|
||||||
} catch(e) { window.showToast('Gagal hapus', 'error'); }
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGeometryCreated = (type, geometry, layer) => {
|
|
||||||
const tempLayer = L.geoJSON(geometry).addTo(appMap);
|
|
||||||
renderForm(type, geometry, async (payload) => {
|
|
||||||
try {
|
|
||||||
if(type==='spbu') await spbuService.create(payload);
|
|
||||||
if(type==='jalan') await jalanService.create(payload);
|
|
||||||
if(type==='kavling') {
|
|
||||||
const kavlingPayload = {
|
|
||||||
nama_pemilik: payload.nama,
|
|
||||||
luas: payload.luas || 0,
|
|
||||||
geometry: payload.geometry
|
|
||||||
};
|
|
||||||
await kavlingService.create(kavlingPayload);
|
|
||||||
}
|
|
||||||
if(type==='rumah_ibadah') await rumahIbadahService.create(payload);
|
|
||||||
if(type==='warga_miskin') await wargaMiskinService.create(payload);
|
|
||||||
window.showToast('Tersimpan'); setTimeout(()=>location.reload(), 800);
|
|
||||||
} catch(e) { window.showToast('Gagal simpan', 'error'); }
|
|
||||||
}, () => appMap.removeLayer(tempLayer));
|
|
||||||
};
|
|
||||||
|
|
||||||
const init = async () => {
|
|
||||||
appMap = initMap('map');
|
|
||||||
document.getElementById('btn-draw-rumah')?.addEventListener('click', () => { window.currentDrawType='rumah_ibadah'; new L.Draw.Marker(appMap).enable(); });
|
|
||||||
document.getElementById('btn-draw-warga')?.addEventListener('click', () => { window.currentDrawType='warga_miskin'; new L.Draw.Marker(appMap).enable(); });
|
|
||||||
drawControl = setupDrawControls(appMap, handleGeometryCreated);
|
|
||||||
await loadAllData();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
|
||||||
} else {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
export const setupDrawControls = (map, onGeometryCreated) => {
|
|
||||||
const drawControl = new L.Control.Draw({
|
|
||||||
draw: { marker: true, polyline: true, polygon: true, circle: false, circlemarker: false, rectangle: false }
|
|
||||||
});
|
|
||||||
map.addControl(drawControl);
|
|
||||||
|
|
||||||
map.on(L.Draw.Event.CREATED, function (event) {
|
|
||||||
const layer = event.layer;
|
|
||||||
const type = event.layerType;
|
|
||||||
|
|
||||||
// Peta draw default tipe: marker -> spbu, polyline -> jalan, polygon -> kavling
|
|
||||||
let customType = type;
|
|
||||||
if(type === 'marker') customType = 'spbu';
|
|
||||||
if(type === 'polyline') customType = 'jalan';
|
|
||||||
if(type === 'polygon') customType = 'kavling';
|
|
||||||
if(window.currentDrawType) customType = window.currentDrawType; // Untuk P2/P3
|
|
||||||
|
|
||||||
onGeometryCreated(customType, layer.toGeoJSON().geometry, layer);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Custom Menu Handlers
|
|
||||||
const markerDrawer = new L.Draw.Marker(map, drawControl.options.draw.marker);
|
|
||||||
const polylineDrawer = new L.Draw.Polyline(map, drawControl.options.draw.polyline);
|
|
||||||
const polygonDrawer = new L.Draw.Polygon(map, drawControl.options.draw.polygon);
|
|
||||||
|
|
||||||
document.getElementById('btn-draw-spbu')?.addEventListener('click', () => { window.currentDrawType='spbu'; markerDrawer.enable(); });
|
|
||||||
document.getElementById('btn-draw-jalan')?.addEventListener('click', () => { window.currentDrawType='jalan'; polylineDrawer.enable(); });
|
|
||||||
document.getElementById('btn-draw-kavling')?.addEventListener('click', () => { window.currentDrawType='kavling'; polygonDrawer.enable(); });
|
|
||||||
|
|
||||||
return drawControl;
|
|
||||||
};
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
export const renderForm = (type, geometry, onSubmit, onCancel) => {
|
|
||||||
const container = document.getElementById('form-container');
|
|
||||||
container.style.display = 'flex';
|
|
||||||
|
|
||||||
const extraFields = type === 'spbu' ? `
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="checkbox-group">
|
|
||||||
<input type="checkbox" id="input-24jam">
|
|
||||||
<span>Buka 24 Jam</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
` : '';
|
|
||||||
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="form-panel">
|
|
||||||
<h3 style="margin:0 0 10px 0; font-size:1rem; border-bottom:1px solid rgba(255,255,255,0.1); padding-bottom:8px;">Simpan Data (${type.toUpperCase()})</h3>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="text" id="input-nama" class="form-control" placeholder="Nama / Label" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="text" id="input-deskripsi" class="form-control" placeholder="Deskripsi Singkat">
|
|
||||||
</div>
|
|
||||||
${extraFields}
|
|
||||||
<div style="display:flex; gap:10px; margin-top:8px;">
|
|
||||||
<button id="btn-save" class="btn-submit" style="flex:1;">Simpan</button>
|
|
||||||
<button id="btn-cancel" class="btn-submit" style="background:rgba(255,255,255,0.1); color:var(--text-light); flex:1;">Batal</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.getElementById('btn-save').addEventListener('click', () => {
|
|
||||||
const payload = {
|
|
||||||
type,
|
|
||||||
geometry,
|
|
||||||
nama: document.getElementById('input-nama').value,
|
|
||||||
deskripsi: document.getElementById('input-deskripsi').value,
|
|
||||||
};
|
|
||||||
if (type === 'spbu') payload.buka_24_jam = document.getElementById('input-24jam').checked;
|
|
||||||
|
|
||||||
onSubmit(payload);
|
|
||||||
container.style.display = 'none';
|
|
||||||
container.innerHTML = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btn-cancel').addEventListener('click', () => {
|
|
||||||
if(onCancel) onCancel();
|
|
||||||
container.style.display = 'none';
|
|
||||||
container.innerHTML = '';
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
/**
|
|
||||||
* map.js
|
|
||||||
* Tanggung Jawab: Inisialisasi peta Leaflet dasar dan base layer.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const initMap = (containerId) => {
|
|
||||||
// Pusat peta default (Universitas Tanjungpura, Pontianak)
|
|
||||||
const map = L.map(containerId, {
|
|
||||||
zoomControl: false
|
|
||||||
}).setView([-0.0583, 109.3448], 15);
|
|
||||||
|
|
||||||
// Pindahkan zoom control ke kanan bawah
|
|
||||||
L.control.zoom({
|
|
||||||
position: 'bottomright'
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
||||||
const tileUrl = isDark
|
|
||||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
|
||||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png';
|
|
||||||
|
|
||||||
// Tile Layer Premium (CartoDB Dark Matter / Light All)
|
|
||||||
const tileLayer = L.tileLayer(tileUrl, {
|
|
||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
|
||||||
subdomains: 'abcd',
|
|
||||||
maxZoom: 20
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
// Dynamic theme updater for tile layer
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
const dark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
||||||
tileLayer.setUrl(dark
|
|
||||||
? 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png'
|
|
||||||
: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
observer.observe(document.documentElement, { attributes: true });
|
|
||||||
|
|
||||||
return map;
|
|
||||||
};
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
const getBaseUrl = () => {
|
|
||||||
const segments = window.location.pathname.split('/');
|
|
||||||
const targetIndex = segments.findIndex(seg => ['01', '02', '03', 'versi'].includes(seg));
|
|
||||||
if (targetIndex !== -1) {
|
|
||||||
return segments.slice(0, targetIndex).join('/');
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
export const BASE_URL = getBaseUrl();
|
|
||||||
|
|
||||||
const createService = (endpoint) => ({
|
|
||||||
getAll: async () => {
|
|
||||||
const res = await fetch(`${BASE_URL}/${endpoint}`);
|
|
||||||
const json = await res.json();
|
|
||||||
return json.data;
|
|
||||||
},
|
|
||||||
create: async (data) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/${endpoint}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
return json.data;
|
|
||||||
},
|
|
||||||
delete: async (id) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/${endpoint}?id=${id}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
return json.data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// P01
|
|
||||||
export const spbuService = createService('01/backend/api/spbu.php');
|
|
||||||
export const jalanService = createService('01/backend/api/jalan.php');
|
|
||||||
export const kavlingService = createService('01/backend/api/kavling.php');
|
|
||||||
|
|
||||||
// P02
|
|
||||||
export const rumahIbadahService = createService('02/backend/api/rumah_ibadah.php');
|
|
||||||
export const wargaMiskinService = createService('02/backend/api/warga_miskin.php');
|
|
||||||
export const haversineService = {
|
|
||||||
getDalamRadius: async (id, radius) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/02/backend/api/rumah_ibadah.php?action=jangkauan&id=${id}&radius=${radius}`);
|
|
||||||
const json = await res.json();
|
|
||||||
return json.data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { CONFIG } from '../config.js';
|
|
||||||
|
|
||||||
export const rumahIbadahService = {
|
|
||||||
getAll: async () => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/rumah_ibadah.php`);
|
|
||||||
return await res.json();
|
|
||||||
},
|
|
||||||
save: async (data) => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/rumah_ibadah.php`, {
|
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
return await res.json();
|
|
||||||
},
|
|
||||||
delete: async (id) => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/rumah_ibadah.php?id=${id}`, { method: 'DELETE' });
|
|
||||||
return await res.json();
|
|
||||||
},
|
|
||||||
getJangkauan: async (id, radius = 1) => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/rumah_ibadah.php?action=jangkauan&id=${id}&radius=${radius}`);
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export const CONFIG = {
|
|
||||||
BASE_URL: '/03/backend/api'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const statistikService = {
|
|
||||||
getKepadatan: async () => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/statistik.php`);
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { CONFIG } from '../config.js';
|
|
||||||
|
|
||||||
export const wargaMiskinService = {
|
|
||||||
getAll: async () => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/warga_miskin.php`);
|
|
||||||
return await res.json();
|
|
||||||
},
|
|
||||||
save: async (data) => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/warga_miskin.php`, {
|
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
return await res.json();
|
|
||||||
},
|
|
||||||
delete: async (id) => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/warga_miskin.php?id=${id}`, { method: 'DELETE' });
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
/* map.css
|
|
||||||
* Tanggung Jawab: Styling container peta, kontrol Leaflet, dan pop-up modern.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.map-container {
|
|
||||||
flex: 1;
|
|
||||||
position: relative;
|
|
||||||
background: #E2E8F0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#map {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Customizing Leaflet Controls */
|
|
||||||
.leaflet-control-zoom {
|
|
||||||
border: none !important;
|
|
||||||
box-shadow: var(--shadow-md) !important;
|
|
||||||
border-radius: var(--radius-md) !important;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-control-zoom a {
|
|
||||||
color: var(--text-main) !important;
|
|
||||||
background: var(--surface-glass) !important;
|
|
||||||
backdrop-filter: blur(8px);
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-control-zoom a:hover {
|
|
||||||
background: var(--surface) !important;
|
|
||||||
color: var(--primary) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom Popup Modern */
|
|
||||||
.leaflet-popup-content-wrapper {
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
padding: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.leaflet-popup-content {
|
|
||||||
margin: 14px;
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-title {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-main);
|
|
||||||
margin-bottom: 6px;
|
|
||||||
border-bottom: 2px solid var(--primary);
|
|
||||||
padding-bottom: 4px;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-desc {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup-action {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
/* sidebar.css
|
|
||||||
* Tanggung Jawab: Styling layout sidebar, navigasi, dan list item.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
width: 350px;
|
|
||||||
background: var(--surface-glass);
|
|
||||||
backdrop-filter: blur(12px);
|
|
||||||
-webkit-backdrop-filter: blur(12px);
|
|
||||||
border-right: 1px solid var(--border);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
z-index: 1000;
|
|
||||||
box-shadow: var(--shadow-lg);
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header {
|
|
||||||
padding: 24px;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-title {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-subtitle {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toast Notification */
|
|
||||||
.toast-container {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 24px;
|
|
||||||
right: 24px;
|
|
||||||
z-index: 9999;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast {
|
|
||||||
background: var(--surface);
|
|
||||||
color: var(--text-main);
|
|
||||||
padding: 12px 20px;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 500;
|
|
||||||
transform: translateY(20px);
|
|
||||||
opacity: 0;
|
|
||||||
animation: slideIn 0.3s forwards ease-out;
|
|
||||||
border-left: 4px solid var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toast.error {
|
|
||||||
border-left-color: var(--danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes slideIn {
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-22
@@ -1,22 +0,0 @@
|
|||||||
FROM php:8.2-apache
|
|
||||||
|
|
||||||
# Install PHP extensions & enable Apache modules
|
|
||||||
RUN docker-php-ext-install pdo_mysql mysqli \
|
|
||||||
&& a2enmod rewrite headers
|
|
||||||
|
|
||||||
# Copy application files
|
|
||||||
COPY portal.html /var/www/html/index.html
|
|
||||||
COPY portal.html /var/www/html/portal.html
|
|
||||||
COPY favicon.ico /var/www/html/favicon.ico
|
|
||||||
COPY webgis_app/ /var/www/html/webgis_app/
|
|
||||||
COPY db_scripts/ /var/www/html/db_scripts/
|
|
||||||
COPY 01/ /var/www/html/01/
|
|
||||||
COPY 02/ /var/www/html/02/
|
|
||||||
COPY 03/ /var/www/html/03/
|
|
||||||
|
|
||||||
# Set proper permissions
|
|
||||||
RUN chown -R www-data:www-data /var/www/html \
|
|
||||||
&& find /var/www/html -type d -exec chmod 755 {} \; \
|
|
||||||
&& find /var/www/html -type f -exec chmod 644 {} \;
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 naufalzakyramadhan
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
|
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||||
|
portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||||
|
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
|
||||||
|
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
|
||||||
|
USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
@@ -1,240 +1,2 @@
|
|||||||
# 🗺️ WebGIS Smart City Pontianak — Portal & Aplikasi Spasial
|
# D1041231071_UAS-Tugas_WebGISProject
|
||||||
**Tugas Akhir / UAS — Sistem Informasi Geografis (SIG)**
|
|
||||||
|
|
||||||
Selamat datang di repositori **WebGIS Smart City Pontianak**. Repositori ini merupakan wadah proyek akhir mata kuliah Sistem Informasi Geografis yang mengintegrasikan seluruh modul praktikum pembelajaran dari dasar hingga aplikasi final berbasis spasial.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 👤 Identitas Mahasiswa
|
|
||||||
* **Nama**: Naufal Zaky Ramadhan
|
|
||||||
* **NIM**: D1041231071
|
|
||||||
* **Program Studi**: Rekayasa Sistem Komputer
|
|
||||||
* **Mata Kuliah**: Sistem Informasi Geografis (2025/2026)
|
|
||||||
* **Status Proyek**: Produksi / Selesai Deploy (Coolify & XAMPP)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📂 Struktur Repositori
|
|
||||||
|
|
||||||
```text
|
|
||||||
D1041231071_NaufalZakyR_UAS_WebGISProject/
|
|
||||||
├── portal.html # Halaman Hub utama (Directory & Navigator Portal)
|
|
||||||
├── 01/ # Modul Pertemuan 01 - Geometri Spasial Dasar
|
|
||||||
│ ├── backend/ # API lokal Modul 01
|
|
||||||
│ └── frontend/ # Tampilan Leaflet & Leaflet Draw (Geometri Dasar)
|
|
||||||
├── 02/ # Modul Pertemuan 02 - Analisis Proksimitas Jarak
|
|
||||||
│ ├── backend/ # API lokal Modul 02
|
|
||||||
│ └── frontend/ # Tampilan Peta Haversine & Layer Tematik
|
|
||||||
├── 03/ # Modul Pertemuan 03 - Analisis Spasial Lanjut
|
|
||||||
│ ├── backend/ # API lokal Modul 03
|
|
||||||
│ └── frontend/ # Tampilan Peta Choropleth (Point-in-Polygon)
|
|
||||||
├── webgis_app/ # APLIKASI UTAMA (UAS - Integrated Smart City WebGIS)
|
|
||||||
│ ├── core_config/ # Konfigurasi inti (Database, Session, Auth, Env Loader)
|
|
||||||
│ ├── endpoints/ # Handler API data spasial (GeoJSON CRUD & Analisis)
|
|
||||||
│ ├── api/ # API Router / Wrappers
|
|
||||||
│ ├── panel_admin/ # Dashboard & Manajemen Data Spasial Admin
|
|
||||||
│ ├── panel_user/ # Dashboard, Laporan Warga & Peta Interaktif User
|
|
||||||
│ └── public_assets/ # File statis (Aesthetic CSS global, JS Peta, & Ikon)
|
|
||||||
├── db_scripts/ # Script skema SQL & Seed Data awal
|
|
||||||
├── Dockerfile # Konfigurasi containerization Apache + PHP 8.2 untuk Deploy
|
|
||||||
├── .env.example # Contoh konfigurasi environment variable
|
|
||||||
└── README.md # Dokumentasi teknis proyek
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🌐 Fitur Utama Pada Portal (`portal.html`)
|
|
||||||
|
|
||||||
Halaman `portal.html` di root repositori bertindak sebagai **pintu gerbang interaktif (Dashboard Portal Hub)** yang menghubungkan seluruh progres pengerjaan praktikum dari awal semester hingga aplikasi final UAS.
|
|
||||||
|
|
||||||
### Karakteristik & Fitur Portal:
|
|
||||||
* **Rich Modern UI**: Menggunakan skema desain *sleek dark mode* secara default, dengan dukungan *theme toggle* (Dark/Light) yang tersimpan secara lokal menggunakan `localStorage`.
|
|
||||||
* **Micro-Animations**: Efek hover interaktif, kartu portal dinamis dengan glassmorphic gradient, serta ambient glow orbs yang mempercantik estetika antarmuka.
|
|
||||||
* **Milestone Directory**:
|
|
||||||
1. **Modul 01 (Geometri Dasar)**: Pembelajaran cara menggambar dan menyimpan tipe data spasial: Titik/Point (direpresentasikan sebagai SPBU), Garis/LineString (Jalan), dan Poligon/Polygon (Kavling Tanah) menggunakan plugin **Leaflet Draw** ke database MySQL.
|
|
||||||
2. **Modul 02 (Haversine & Tematik)**: Pengenalan analisis jarak melengkung bumi (formula Haversine) secara real-time untuk mendeteksi sebaran Warga Miskin dalam radius bantuan Rumah Ibadah, serta visualisasi tematik sebaran sosial.
|
|
||||||
3. **Modul 03 (Analisis Spasial - Point in Polygon)**: Implementasi algoritma spasial untuk mendeteksi titik Warga Miskin yang berada di dalam koordinat Poligon Kavling secara dinamis guna mewarnai kavling berdasarkan tingkat kepadatan warga (Choropleth Map).
|
|
||||||
4. **Smart City WebGIS App (UAS)**: Integrasi penuh dari modul 01, 02, dan 03 ke dalam sebuah aplikasi web multi-peran dengan fitur pelaporan warga dan ulasan fasilitas kota.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Aplikasi Utama (`webgis_app` / Proyek Final UAS)
|
|
||||||
|
|
||||||
Aplikasi **Smart City WebGIS** merupakan hasil kompilasi dan penyempurnaan dari seluruh modul pembelajaran. Aplikasi ini ditujukan untuk memetakan sarana perkotaan, menganalisis daerah rentan sosial, serta membuka jalur interaksi dua arah antara masyarakat dan pemerintah daerah.
|
|
||||||
|
|
||||||
### 🛠️ Stack Teknologi & Arsitektur Spasial
|
|
||||||
* **Bahasa Pemrograman**: PHP Native (Terstruktur, kompatibel PHP 8.0 - 8.3)
|
|
||||||
* **Peta & Visualisasi**: Leaflet.js, Leaflet.Draw, FontAwesome, & Google Fonts (Inter, Space Grotesk)
|
|
||||||
* **Sistem Database**: MySQL / MariaDB dengan dukungan **Spatial Index** (`SPATIAL INDEX`) dan fungsi spasial OpenGIS (`ST_Contains`, `ST_Distance_Sphere`, `ST_GeomFromText`).
|
|
||||||
* **Security & Auth**: Proteksi Session aman, enkripsi password menggunakan `bcrypt` (`password_verify`), Prepared Statements untuk mencegah SQL Injection, dan middleware batasan hak akses peran.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🛡️ Fitur Bagian Admin (`panel_admin/`)
|
|
||||||
|
|
||||||
Panel khusus administrator perkotaan untuk mengelola seluruh data geospasial secara dinamis.
|
|
||||||
|
|
||||||
#### 1. Dashboard Analitik Spasial
|
|
||||||
* Menampilkan jumlah total objek spasial terdaftar (SPBU, Ruas Jalan, Rumah Ibadah, Kavling, Warga Miskin).
|
|
||||||
* **MySQL Spatial Counters**: Menghitung secara real-time jumlah **Kawasan Rawan** (Kawasan kumuh dengan lebih dari 3 warga miskin di dalamnya) dan jumlah **Blank Spot Bansos** (Warga miskin yang belum tercover bantuan radius rumah ibadah).
|
|
||||||
* Menampilkan daftar entitas terbaru yang baru saja didaftarkan oleh sistem atau dilaporkan oleh warga.
|
|
||||||
|
|
||||||
#### 2. Peta Interaktif Admin & Drawing Tools
|
|
||||||
* Integrasi **Leaflet.Draw** custom. Admin dapat menambah data langsung dengan menggambar di peta:
|
|
||||||
* Menitikkan **SPBU** (Data Point + status operasional 24 jam).
|
|
||||||
* Menitikkan **Rumah Ibadah** (Data Point + radius bantuan sosial).
|
|
||||||
* Menitikkan **Warga Miskin** (Data Point + data ekonomi pendapatan/tanggungan).
|
|
||||||
* Menarik garis **Jalur Jalan** (Data LineString + klasifikasi jalan).
|
|
||||||
* Menggambar area **Kavling Tanah** (Data Polygon + pemilik & luas tanah).
|
|
||||||
* Menggambar **Kawasan Kumuh** (Data Polygon).
|
|
||||||
* **Sinc-Data CRUD**: Klik objek apa saja di peta untuk melihat detail atribut, mengedit properti, mengubah letak koordinat, atau menghapusnya langsung dari peta.
|
|
||||||
|
|
||||||
#### 3. Data Tables Manager
|
|
||||||
* Modul CRUD berbasis tabel interaktif untuk seluruh 8 entitas data.
|
|
||||||
* Fitur pencarian instan tanpa reload, status badge dinamis, serta integrasi input data atribut spasial secara manual.
|
|
||||||
* **Moderasi Laporan Warga**: Admin dapat melihat keluhan lokasi dari warga, melihat titik masalah di peta, dan mengubah status laporan menjadi *Diproses*, *Selesai*, atau *Ditolak*.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 👥 Fitur Bagian Pengguna (`panel_user/`)
|
|
||||||
|
|
||||||
Panel publik yang diperuntukkan bagi warga kota Pontianak untuk memantau kota dan berpartisipasi aktif.
|
|
||||||
|
|
||||||
#### 1. Peta Interaktif User (Read-Only)
|
|
||||||
* Menampilkan seluruh layer spasial perkotaan yang dapat diaktifkan/dinonaktifkan melalui menu kontrol layer di sidebar.
|
|
||||||
* **Legenda Dinamis**: Panduan simbol warna pada peta (seperti SPBU 24 jam berwarna hijau, SPBU terbatas berwarna jingga, dsb).
|
|
||||||
* **Analisis Spasial Publik**: Visualisasi langsung peta Choropleth kepadatan dan sebaran Blank Spot bantuan sosial di wilayah sekitar.
|
|
||||||
|
|
||||||
#### 2. Alat Spasial "SPBU Terdekat" (Proximity Search)
|
|
||||||
* Pengguna dapat menekan tombol **SPBU Terdekat** pada peta.
|
|
||||||
* Sistem akan mendeteksi titik tengah koordinat layar peta pengguna (`map.getCenter()`) dan melakukan query spasial ke backend.
|
|
||||||
* Backend memproses pencarian menggunakan rumus **Haversine** (`ST_Distance_Sphere`) untuk mengurutkan stasiun pengisian bahan bakar terdekat lengkap dengan informasi jarak (meter/kilometer) dan durasi operasionalnya.
|
|
||||||
|
|
||||||
#### 3. Laporan Warga (Citizen Crowdsourcing)
|
|
||||||
* Pengguna dapat mengirimkan pengaduan masalah infrastruktur (misalnya: Jalan Rusak, Banjir, Lampu Jalan Mati, Fasilitas Terbengkalai).
|
|
||||||
* **Location Tagging**: Dilengkapi dengan mini-map Leaflet interaktif di dalam modal. Pengguna cukup mengklik lokasi kerusakan di peta untuk menangkap koordinat Latitude dan Longitude secara presisi.
|
|
||||||
* Pengguna dapat melihat daftar laporan miliknya beserta status perkembangannya (Menunggu, Diproses, Selesai). Laporan yang masih berstatus "Menunggu" dapat dibatalkan/dihapus oleh pengguna.
|
|
||||||
|
|
||||||
#### 4. Ulasan & Rating Fasilitas Kota
|
|
||||||
* Pengguna dapat memberikan rating (skala 1 - 5 bintang) dan ulasan tertulis pada fasilitas publik seperti SPBU dan Rumah Ibadah.
|
|
||||||
* Ulasan ini akan tampil pada popup informasi ketika fasilitas tersebut diklik di peta utama oleh pengguna lain.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 📊 Detail Implementasi Query Spasial MySQL
|
|
||||||
|
|
||||||
Aplikasi ini menggunakan keunggulan MySQL Spatial Geometris untuk efisiensi kalkulasi data peta:
|
|
||||||
|
|
||||||
1. **Deteksi Kawasan Rawan (Kawasan Kumuh Padat Penduduk Miskin)**
|
|
||||||
Menghitung jumlah warga miskin yang posisinya berada di dalam poligon kawasan kumuh menggunakan fungsi `ST_Contains`:
|
|
||||||
```sql
|
|
||||||
SELECT k.id, k.nama_kawasan, COUNT(w.id) as jumlah_warga
|
|
||||||
FROM kawasan_kumuh k
|
|
||||||
LEFT JOIN warga_miskin w ON ST_Contains(k.geom, w.geom)
|
|
||||||
GROUP BY k.id HAVING jumlah_warga > 3;
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Deteksi Blank Spot Bantuan Sosial**
|
|
||||||
Mendeteksi warga miskin yang lokasinya berada di luar radius pelayanan bantuan seluruh rumah ibadah di kota menggunakan `ST_Distance_Sphere`:
|
|
||||||
```sql
|
|
||||||
SELECT w.id, w.nama_kk
|
|
||||||
FROM warga_miskin w
|
|
||||||
WHERE NOT EXISTS (
|
|
||||||
SELECT 1 FROM rumah_ibadah ri
|
|
||||||
WHERE ST_Distance_Sphere(w.geom, ri.geom) <= ri.radius_bantuan_meter
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Pencarian SPBU Terdekat (Proximity Query)**
|
|
||||||
Mencari SPBU dengan radius terdekat dari koordinat input (`:user_lon`, `:user_lat`):
|
|
||||||
```sql
|
|
||||||
SELECT nama, buka_24_jam,
|
|
||||||
ST_Distance_Sphere(geom, ST_GeomFromText('POINT(user_lon user_lat)', 4326)) AS jarak_meter
|
|
||||||
FROM spbu
|
|
||||||
ORDER BY jarak_meter ASC LIMIT 5;
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚙️ Cara Menjalankan Project
|
|
||||||
|
|
||||||
### 💻 A. Menjalankan di XAMPP Lokal (Windows)
|
|
||||||
|
|
||||||
#### 1. Persiapan Direktori Proyek
|
|
||||||
Salin folder proyek ini ke direktori web root XAMPP Anda (biasanya `C:\xampp\htdocs\`). Pastikan nama foldernya sesuai:
|
|
||||||
`C:\xampp\htdocs\D1041231071_NaufalZakyR_UAS_WebGISProject`
|
|
||||||
|
|
||||||
#### 2. Konfigurasi Database
|
|
||||||
1. Aktifkan service **Apache** dan **MySQL** melalui XAMPP Control Panel.
|
|
||||||
2. Buka browser dan akses `http://localhost/phpmyadmin/`.
|
|
||||||
3. Buat database baru bernama `webgis_db`.
|
|
||||||
4. Import file SQL database awal yang terletak di `db_scripts/webgis_naufal_zaky.sql` ke dalam database `webgis_db`.
|
|
||||||
> 💡 **Fitur Migrasi Otomatis**: Alternatif lain, Anda cukup membuat database kosong `webgis_db`. Saat aplikasi pertama kali dijalankan, sistem koneksi PDO akan mendeteksi database kosong dan secara otomatis mengimpor tabel serta data awal tanpa perlu manual phpMyAdmin!
|
|
||||||
|
|
||||||
#### 3. Pengaturan Environment (.env)
|
|
||||||
Buat file bernama `.env` di direktori utama proyek (sejajar dengan `portal.html`) dengan isi sebagai berikut:
|
|
||||||
```env
|
|
||||||
APP_BASE_PATH=/D1041231071_NaufalZakyR_UAS_WebGISProject/webgis_app
|
|
||||||
SESSION_SECURE=false
|
|
||||||
CORS_ALLOWED_ORIGIN=http://localhost
|
|
||||||
|
|
||||||
DB_HOST=127.0.0.1
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=webgis_db
|
|
||||||
DB_USERNAME=root
|
|
||||||
DB_PASSWORD=
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4. Mengakses Aplikasi
|
|
||||||
* Untuk membuka halaman **Portal Proyek**:
|
|
||||||
`http://localhost/D1041231071_NaufalZakyR_UAS_WebGISProject/portal.html`
|
|
||||||
* Untuk langsung menuju **Aplikasi WebGIS Utama**:
|
|
||||||
`http://localhost/D1041231071_NaufalZakyR_UAS_WebGISProject/webgis_app/`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🐳 B. Menjalankan Menggunakan Docker
|
|
||||||
Repositori ini telah dilengkapi dengan `Dockerfile` siap pakai untuk containerization.
|
|
||||||
|
|
||||||
1. Buka terminal di direktori proyek dan jalankan perintah build image:
|
|
||||||
```bash
|
|
||||||
docker build -t webgis-smartcity .
|
|
||||||
```
|
|
||||||
2. Jalankan container dengan menghubungkannya ke container database Anda:
|
|
||||||
```bash
|
|
||||||
docker run -d -p 8080:80 --name webgis-app webgis-smartcity
|
|
||||||
```
|
|
||||||
3. Akses aplikasi di `http://localhost:8080/`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ☁️ C. Deploying to Coolify (Production)
|
|
||||||
Jika Anda men-deploy proyek ini ke cloud server menggunakan platform **Coolify**:
|
|
||||||
1. Hubungkan akun GitHub/GitLab Anda ke Coolify.
|
|
||||||
2. Buat database MySQL / MariaDB resource di panel Coolify.
|
|
||||||
3. Tambahkan aplikasi baru dari repositori Git ini.
|
|
||||||
4. Set buildpack ke **Dockerfile** (Coolify akan mendeteksi `Dockerfile` di root secara otomatis).
|
|
||||||
5. Definisikan **Environment Variables** di panel aplikasi Coolify sesuai konfigurasi database yang dibuat:
|
|
||||||
```env
|
|
||||||
APP_BASE_PATH=
|
|
||||||
SESSION_SECURE=true
|
|
||||||
DB_HOST=mysql-service-name
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=webgis_db
|
|
||||||
DB_USERNAME=coolify_user
|
|
||||||
DB_PASSWORD=coolify_password
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔑 Kredensial Akun Pengujian (Demo)
|
|
||||||
|
|
||||||
Gunakan akun berikut untuk masuk ke sistem di halaman login (`webgis_app/login.php`):
|
|
||||||
|
|
||||||
| Peran (Role) | Username | Password | Deskripsi Akses |
|
|
||||||
| :--- | :--- | :--- | :--- |
|
|
||||||
| **Administrator** | `admin` | `admin123` | Akses penuh edit spasial peta, CRUD tabel, dan moderasi laporan warga. |
|
|
||||||
| **Warga Publik** | `pengguna` | `user123` | Akses view peta interaktif, Proximity SPBU, kirim ulasan, dan input laporan warga. |
|
|
||||||
|
|
||||||
> [!WARNING]
|
|
||||||
> *Demi keamanan data publik, harap segera mengubah kata sandi default atau menghapus akun demo ini melalui menu **Manajemen Pengguna** setelah sistem di-deploy ke server produksi.*
|
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
-- ============================================================
|
|
||||||
-- WebGIS Pontianak - Struktur Database
|
|
||||||
-- ============================================================
|
|
||||||
-- Jalankan file ini terlebih dahulu sebelum menjalankan seed data.
|
|
||||||
|
|
||||||
CREATE DATABASE IF NOT EXISTS webgis_db
|
|
||||||
CHARACTER SET utf8mb4
|
|
||||||
COLLATE utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
USE webgis_db;
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- 1. Users
|
|
||||||
-- ============================================================
|
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
username VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
password VARCHAR(255) NOT NULL,
|
|
||||||
role ENUM('admin', 'user') NOT NULL DEFAULT 'user',
|
|
||||||
nama_lengkap VARCHAR(100) DEFAULT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- 2. Data Spasial
|
|
||||||
-- ============================================================
|
|
||||||
CREATE TABLE IF NOT EXISTS spbu (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
nama VARCHAR(100) NOT NULL,
|
|
||||||
deskripsi TEXT,
|
|
||||||
buka_24_jam TINYINT(1) NOT NULL DEFAULT 0,
|
|
||||||
geom GEOMETRY NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
SPATIAL INDEX idx_spbu_geom (geom)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS rumah_ibadah (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
nama VARCHAR(100) NOT NULL,
|
|
||||||
agama VARCHAR(50) NOT NULL,
|
|
||||||
radius_bantuan_meter INT NOT NULL DEFAULT 1000,
|
|
||||||
geom GEOMETRY NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
SPATIAL INDEX idx_rumah_ibadah_geom (geom)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS jalan (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
nama VARCHAR(100) NOT NULL,
|
|
||||||
jenis_jalan VARCHAR(50) DEFAULT NULL,
|
|
||||||
geom GEOMETRY NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
SPATIAL INDEX idx_jalan_geom (geom)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS kavling (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
nama_pemilik VARCHAR(100) NOT NULL,
|
|
||||||
status_kepemilikan VARCHAR(50) NOT NULL,
|
|
||||||
luas DECIMAL(10,2) DEFAULT NULL,
|
|
||||||
geom GEOMETRY NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
SPATIAL INDEX idx_kavling_geom (geom)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS kawasan_kumuh (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
nama_kawasan VARCHAR(100) NOT NULL,
|
|
||||||
geom GEOMETRY NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
SPATIAL INDEX idx_kawasan_kumuh_geom (geom)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS warga_miskin (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
nama_kk VARCHAR(100) NOT NULL,
|
|
||||||
penghasilan DECIMAL(15,2) NOT NULL,
|
|
||||||
jumlah_tanggungan INT NOT NULL,
|
|
||||||
geom GEOMETRY NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
SPATIAL INDEX idx_warga_miskin_geom (geom)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- 3. Data Interaksi Pengguna
|
|
||||||
-- ============================================================
|
|
||||||
CREATE TABLE IF NOT EXISTS laporan_warga (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
user_id INT NOT NULL,
|
|
||||||
kategori VARCHAR(50) NOT NULL,
|
|
||||||
deskripsi TEXT NOT NULL,
|
|
||||||
geometry POINT NOT NULL,
|
|
||||||
status ENUM('menunggu', 'diproses', 'selesai', 'ditolak') NOT NULL DEFAULT 'menunggu',
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_laporan_user_id (user_id),
|
|
||||||
SPATIAL INDEX idx_laporan_geometry (geometry),
|
|
||||||
CONSTRAINT fk_laporan_user
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
||||||
ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ulasan_fasilitas (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
user_id INT NOT NULL,
|
|
||||||
fasilitas_tipe ENUM('spbu', 'rumah_ibadah') NOT NULL,
|
|
||||||
fasilitas_id INT NOT NULL,
|
|
||||||
rating TINYINT NOT NULL,
|
|
||||||
komentar TEXT,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_ulasan_user_id (user_id),
|
|
||||||
INDEX idx_ulasan_fasilitas (fasilitas_tipe, fasilitas_id),
|
|
||||||
CONSTRAINT fk_ulasan_user
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
CONSTRAINT chk_ulasan_rating CHECK (rating BETWEEN 1 AND 5)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
-- ============================================================
|
|
||||||
-- WebGIS Pontianak - Seed Users
|
|
||||||
-- ============================================================
|
|
||||||
-- Akun demo:
|
|
||||||
-- admin / admin123
|
|
||||||
-- pengguna / user123
|
|
||||||
|
|
||||||
USE webgis_db;
|
|
||||||
|
|
||||||
INSERT INTO users (username, password, role, nama_lengkap) VALUES
|
|
||||||
(
|
|
||||||
'admin',
|
|
||||||
'$2y$12$oRvX1EtLHM/y8XtwlOy./Oi2UVpnHp98GvXqEuLCFimhr0doKus62',
|
|
||||||
'admin',
|
|
||||||
'Administrator WebGIS'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'pengguna',
|
|
||||||
'$2y$12$djJkmhJBRCSCqqpAICIvGuLoVpICrDUS2IVECbZUodgR89VRTqdLW',
|
|
||||||
'user',
|
|
||||||
'Pengguna Publik'
|
|
||||||
)
|
|
||||||
ON DUPLICATE KEY UPDATE
|
|
||||||
password = VALUES(password),
|
|
||||||
role = VALUES(role),
|
|
||||||
nama_lengkap = VALUES(nama_lengkap);
|
|
||||||
@@ -1,305 +0,0 @@
|
|||||||
-- ============================================================
|
|
||||||
-- WebGIS Pontianak - Seed Data Spasial
|
|
||||||
-- ============================================================
|
|
||||||
-- Koordinat menggunakan urutan longitude, latitude.
|
|
||||||
|
|
||||||
USE webgis_db;
|
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 0;
|
|
||||||
|
|
||||||
TRUNCATE TABLE spbu;
|
|
||||||
TRUNCATE TABLE rumah_ibadah;
|
|
||||||
TRUNCATE TABLE jalan;
|
|
||||||
TRUNCATE TABLE kavling;
|
|
||||||
TRUNCATE TABLE kawasan_kumuh;
|
|
||||||
TRUNCATE TABLE warga_miskin;
|
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- 1. SPBU
|
|
||||||
-- ============================================================
|
|
||||||
INSERT INTO spbu (nama, deskripsi, buka_24_jam, geom) VALUES
|
|
||||||
(
|
|
||||||
'SPBU COCO 61.781.01 Ahmad Yani',
|
|
||||||
'Jl. Jend. Ahmad Yani No.3, Pontianak (Dekat Bundaran Digulis Untan)',
|
|
||||||
1,
|
|
||||||
ST_GeomFromText('POINT(109.344381 -0.056461)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.11 Imam Bonjol',
|
|
||||||
'Jl. Imam Bonjol No.30, Pontianak Tenggara',
|
|
||||||
1,
|
|
||||||
ST_GeomFromText('POINT(109.349692 -0.038148)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.06 Kota Baru',
|
|
||||||
'Jl. Prof. Dr. M. Yamin, Kota Baru, Pontianak Selatan',
|
|
||||||
1,
|
|
||||||
ST_GeomFromText('POINT(109.317589 -0.054378)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.01 Adi Sucipto',
|
|
||||||
'Jl. Adi Sucipto, Pontianak Tenggara',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.362241 -0.061730)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.03 KH. Ahmad Dahlan',
|
|
||||||
'Jl. KH. Ahmad Dahlan No.12, Pontianak Kota',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.333830 -0.033621)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.05 Tanjung Pura',
|
|
||||||
'Jl. Tanjung Pura, Pontianak Selatan',
|
|
||||||
1,
|
|
||||||
ST_GeomFromText('POINT(109.336184 -0.026788)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.12 Gusti Hamzah',
|
|
||||||
'Jl. Gusti Hamzah (Pancasila), Pontianak Kota',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.319760 -0.029415)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.14 Husein Hamzah',
|
|
||||||
'Jl. Husein Hamzah (Pal 3), Pontianak Barat',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.294121 -0.027963)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.15 RE Martadinata',
|
|
||||||
'Jl. RE Martadinata, Pontianak Barat',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.314227 -0.016335)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 63.781.02 28 Oktober',
|
|
||||||
'Jl. 28 Oktober, Pontianak Utara',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.366405 0.007621)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.782.01 Khatulistiwa',
|
|
||||||
'Jl. Khatulistiwa, Pontianak Utara',
|
|
||||||
1,
|
|
||||||
ST_GeomFromText('POINT(109.325983 0.035415)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU OSO MT. Haryono',
|
|
||||||
'Jl. Letjen MT Haryono, Pontianak Selatan',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.3367485 -0.0448924)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.18 Danau Sentarum',
|
|
||||||
'Jl. Danau Sentarum, Pontianak Kota',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.311283 -0.046342)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.19 Serdam',
|
|
||||||
'Jl. Sungai Raya Dalam, Pontianak Tenggara',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.358241 -0.076389)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.02 Hasanuddin',
|
|
||||||
'Jl. Hasanuddin, Pontianak Barat',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.321873 -0.024502)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.21 Dr. Wahidin',
|
|
||||||
'Jl. Dr. Wahidin S., Pontianak Kota',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.308210 -0.034502)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.17 Komyos Sudarso',
|
|
||||||
'Jl. Komodor Yos Sudarso, Pontianak Barat',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.299100 -0.019500)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.19 HOS Cokroaminoto',
|
|
||||||
'Jl. HOS Cokroaminoto, Pontianak Kota',
|
|
||||||
1,
|
|
||||||
ST_GeomFromText('POINT(109.337500 -0.031500)')
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- 2. Rumah Ibadah
|
|
||||||
-- ============================================================
|
|
||||||
INSERT INTO rumah_ibadah (nama, agama, radius_bantuan_meter, geom) VALUES
|
|
||||||
(
|
|
||||||
'Masjid Raya Mujahidin',
|
|
||||||
'Islam',
|
|
||||||
1000,
|
|
||||||
ST_GeomFromText('POINT(109.3377 -0.0414)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Masjid Jami Sultan Syarif Abdurrahman',
|
|
||||||
'Islam',
|
|
||||||
1000,
|
|
||||||
ST_GeomFromText('POINT(109.3522 -0.0229)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Gereja Katedral Santo Yosef',
|
|
||||||
'Katolik',
|
|
||||||
1000,
|
|
||||||
ST_GeomFromText('POINT(109.3384 -0.0274)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Gereja Kristen Immanuel (GPIB)',
|
|
||||||
'Kristen',
|
|
||||||
1000,
|
|
||||||
ST_GeomFromText('POINT(109.3375 -0.0292)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Gereja GKKB Pontianak',
|
|
||||||
'Kristen',
|
|
||||||
1000,
|
|
||||||
ST_GeomFromText('POINT(109.3386 -0.0336)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Vihara Bodhisatva Karaniya Metta',
|
|
||||||
'Buddha',
|
|
||||||
1000,
|
|
||||||
ST_GeomFromText('POINT(109.3432 -0.0215)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Maha Vihara Maitreya',
|
|
||||||
'Buddha',
|
|
||||||
1000,
|
|
||||||
ST_GeomFromText('POINT(109.3649 -0.0732)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Pura Giripati Mulawarman',
|
|
||||||
'Hindu',
|
|
||||||
1000,
|
|
||||||
ST_GeomFromText('POINT(109.3802 -0.0663)')
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- 3. Jalan
|
|
||||||
-- ============================================================
|
|
||||||
INSERT INTO jalan (nama, jenis_jalan, geom) VALUES
|
|
||||||
(
|
|
||||||
'Jalan Jenderal Ahmad Yani',
|
|
||||||
'Arteri Primer',
|
|
||||||
ST_GeomFromText('LINESTRING(109.3621 -0.0768, 109.3444 -0.0565, 109.3360 -0.0450, 109.3330 -0.0330)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Jalan Gajah Mada',
|
|
||||||
'Arteri Primer',
|
|
||||||
ST_GeomFromText('LINESTRING(109.3310 -0.0320, 109.3380 -0.0290, 109.3450 -0.0250)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Jalan Tanjung Pura',
|
|
||||||
'Arteri Primer',
|
|
||||||
ST_GeomFromText('LINESTRING(109.3300 -0.0300, 109.3360 -0.0260, 109.3480 -0.0240, 109.3550 -0.0270)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Jalan Imam Bonjol',
|
|
||||||
'Arteri Primer',
|
|
||||||
ST_GeomFromText('LINESTRING(109.3360 -0.0450, 109.3420 -0.0400, 109.3500 -0.0350, 109.3590 -0.0400)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Jalan Adi Sucipto',
|
|
||||||
'Arteri Primer',
|
|
||||||
ST_GeomFromText('LINESTRING(109.3590 -0.0400, 109.3650 -0.0600, 109.3750 -0.0700)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Jalan Khatulistiwa',
|
|
||||||
'Arteri Primer',
|
|
||||||
ST_GeomFromText('LINESTRING(109.3450 0.0050, 109.3350 0.0150, 109.3250 0.0350, 109.3150 0.0550)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Jalan Komodor Yos Sudarso',
|
|
||||||
'Arteri Primer',
|
|
||||||
ST_GeomFromText('LINESTRING(109.3250 -0.0280, 109.3150 -0.0240, 109.3000 -0.0180, 109.2850 -0.0150)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Jalan Prof. M. Yamin',
|
|
||||||
'Arteri Primer',
|
|
||||||
ST_GeomFromText('LINESTRING(109.3245 -0.0487, 109.3170 -0.0540, 109.3050 -0.0620)')
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- 4. Kavling
|
|
||||||
-- ============================================================
|
|
||||||
INSERT INTO kavling (nama_pemilik, status_kepemilikan, luas, geom) VALUES
|
|
||||||
(
|
|
||||||
'PT. Mega Karya',
|
|
||||||
'HGB',
|
|
||||||
25000.00,
|
|
||||||
ST_GeomFromText('POLYGON((109.333 -0.048, 109.338 -0.048, 109.338 -0.053, 109.333 -0.053, 109.333 -0.048))')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Bapak Sudirman',
|
|
||||||
'SHM',
|
|
||||||
1500.00,
|
|
||||||
ST_GeomFromText('POLYGON((109.340 -0.035, 109.341 -0.035, 109.341 -0.036, 109.340 -0.036, 109.340 -0.035))')
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- 5. Kawasan Kumuh
|
|
||||||
-- ============================================================
|
|
||||||
INSERT INTO kawasan_kumuh (nama_kawasan, geom) VALUES
|
|
||||||
(
|
|
||||||
'Kawasan Rawan Bantaran Sungai',
|
|
||||||
ST_GeomFromText('POLYGON((109.360 -0.050, 109.365 -0.050, 109.365 -0.055, 109.360 -0.055, 109.360 -0.050))')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Kawasan Padat Parit Tokaya',
|
|
||||||
ST_GeomFromText('POLYGON((109.342 -0.038, 109.346 -0.038, 109.346 -0.042, 109.342 -0.042, 109.342 -0.038))')
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- 6. Warga Miskin
|
|
||||||
-- ============================================================
|
|
||||||
INSERT INTO warga_miskin (nama_kk, penghasilan, jumlah_tanggungan, geom) VALUES
|
|
||||||
(
|
|
||||||
'Bpk. Budi (Terisolasi)',
|
|
||||||
800000.00,
|
|
||||||
4,
|
|
||||||
ST_GeomFromText('POINT(109.361 -0.051)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Ibu Siti (Terisolasi)',
|
|
||||||
600000.00,
|
|
||||||
2,
|
|
||||||
ST_GeomFromText('POINT(109.362 -0.052)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Bpk. Joko (Terisolasi)',
|
|
||||||
1000000.00,
|
|
||||||
5,
|
|
||||||
ST_GeomFromText('POINT(109.363 -0.053)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Ibu Ani (Terisolasi)',
|
|
||||||
700000.00,
|
|
||||||
3,
|
|
||||||
ST_GeomFromText('POINT(109.364 -0.054)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Bpk. Hasan',
|
|
||||||
1200000.00,
|
|
||||||
3,
|
|
||||||
ST_GeomFromText('POINT(109.334 -0.050)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Mbah Warno',
|
|
||||||
400000.00,
|
|
||||||
1,
|
|
||||||
ST_GeomFromText('POINT(109.336 -0.046)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Pak Junaidi',
|
|
||||||
900000.00,
|
|
||||||
4,
|
|
||||||
ST_GeomFromText('POINT(109.344 -0.040)')
|
|
||||||
);
|
|
||||||
@@ -1,411 +0,0 @@
|
|||||||
-- ============================================================
|
|
||||||
-- WebGIS Pontianak - Database Lengkap
|
|
||||||
-- ============================================================
|
|
||||||
-- Import file ini jika ingin membuat ulang database dari awal.
|
|
||||||
-- Perhatian: tabel lama dengan nama yang sama akan dihapus.
|
|
||||||
|
|
||||||
-- Database selected via connection string
|
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 0;
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS ulasan_fasilitas;
|
|
||||||
DROP TABLE IF EXISTS laporan_warga;
|
|
||||||
DROP TABLE IF EXISTS warga_miskin;
|
|
||||||
DROP TABLE IF EXISTS kawasan_kumuh;
|
|
||||||
DROP TABLE IF EXISTS kavling;
|
|
||||||
DROP TABLE IF EXISTS jalan;
|
|
||||||
DROP TABLE IF EXISTS rumah_ibadah;
|
|
||||||
DROP TABLE IF EXISTS spbu;
|
|
||||||
DROP TABLE IF EXISTS users;
|
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- 1. Users
|
|
||||||
-- ============================================================
|
|
||||||
CREATE TABLE users (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
username VARCHAR(50) NOT NULL UNIQUE,
|
|
||||||
password VARCHAR(255) NOT NULL,
|
|
||||||
role ENUM('admin', 'user') NOT NULL DEFAULT 'user',
|
|
||||||
nama_lengkap VARCHAR(100) DEFAULT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
INSERT INTO users (username, password, role, nama_lengkap) VALUES
|
|
||||||
(
|
|
||||||
'admin',
|
|
||||||
'$2y$12$oRvX1EtLHM/y8XtwlOy./Oi2UVpnHp98GvXqEuLCFimhr0doKus62',
|
|
||||||
'admin',
|
|
||||||
'Administrator WebGIS'
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'pengguna',
|
|
||||||
'$2y$12$djJkmhJBRCSCqqpAICIvGuLoVpICrDUS2IVECbZUodgR89VRTqdLW',
|
|
||||||
'user',
|
|
||||||
'Pengguna Publik'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- 2. Data Spasial
|
|
||||||
-- ============================================================
|
|
||||||
CREATE TABLE spbu (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
nama VARCHAR(100) NOT NULL,
|
|
||||||
deskripsi TEXT,
|
|
||||||
buka_24_jam TINYINT(1) NOT NULL DEFAULT 0,
|
|
||||||
geom GEOMETRY NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
SPATIAL INDEX idx_spbu_geom (geom)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
INSERT INTO spbu (nama, deskripsi, buka_24_jam, geom) VALUES
|
|
||||||
(
|
|
||||||
'SPBU COCO 61.781.01 Ahmad Yani',
|
|
||||||
'Jl. Jend. Ahmad Yani No.3, Pontianak (Dekat Bundaran Digulis Untan)',
|
|
||||||
1,
|
|
||||||
ST_GeomFromText('POINT(109.344381 -0.056461)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.11 Imam Bonjol',
|
|
||||||
'Jl. Imam Bonjol No.30, Pontianak Tenggara',
|
|
||||||
1,
|
|
||||||
ST_GeomFromText('POINT(109.349692 -0.038148)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.06 Kota Baru',
|
|
||||||
'Jl. Prof. Dr. M. Yamin, Kota Baru, Pontianak Selatan',
|
|
||||||
1,
|
|
||||||
ST_GeomFromText('POINT(109.317589 -0.054378)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.01 Adi Sucipto',
|
|
||||||
'Jl. Adi Sucipto, Pontianak Tenggara',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.362241 -0.061730)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.03 KH. Ahmad Dahlan',
|
|
||||||
'Jl. KH. Ahmad Dahlan No.12, Pontianak Kota',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.333830 -0.033621)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.05 Tanjung Pura',
|
|
||||||
'Jl. Tanjung Pura, Pontianak Selatan',
|
|
||||||
1,
|
|
||||||
ST_GeomFromText('POINT(109.336184 -0.026788)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.12 Gusti Hamzah',
|
|
||||||
'Jl. Gusti Hamzah (Pancasila), Pontianak Kota',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.319760 -0.029415)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.14 Husein Hamzah',
|
|
||||||
'Jl. Husein Hamzah (Pal 3), Pontianak Barat',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.294121 -0.027963)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.15 RE Martadinata',
|
|
||||||
'Jl. RE Martadinata, Pontianak Barat',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.314227 -0.016335)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 63.781.02 28 Oktober',
|
|
||||||
'Jl. 28 Oktober, Pontianak Utara',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.366405 0.007621)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.782.01 Khatulistiwa',
|
|
||||||
'Jl. Khatulistiwa, Pontianak Utara',
|
|
||||||
1,
|
|
||||||
ST_GeomFromText('POINT(109.325983 0.035415)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU OSO MT. Haryono',
|
|
||||||
'Jl. Letjen MT Haryono, Pontianak Selatan',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.3367485 -0.0448924)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.18 Danau Sentarum',
|
|
||||||
'Jl. Danau Sentarum, Pontianak Kota',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.311283 -0.046342)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.19 Serdam',
|
|
||||||
'Jl. Sungai Raya Dalam, Pontianak Tenggara',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.358241 -0.076389)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.02 Hasanuddin',
|
|
||||||
'Jl. Hasanuddin, Pontianak Barat',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.321873 -0.024502)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.21 Dr. Wahidin',
|
|
||||||
'Jl. Dr. Wahidin S., Pontianak Kota',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.308210 -0.034502)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.17 Komyos Sudarso',
|
|
||||||
'Jl. Komodor Yos Sudarso, Pontianak Barat',
|
|
||||||
0,
|
|
||||||
ST_GeomFromText('POINT(109.299100 -0.019500)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'SPBU 64.781.19 HOS Cokroaminoto',
|
|
||||||
'Jl. HOS Cokroaminoto, Pontianak Kota',
|
|
||||||
1,
|
|
||||||
ST_GeomFromText('POINT(109.337500 -0.031500)')
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE rumah_ibadah (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
nama VARCHAR(100) NOT NULL,
|
|
||||||
agama VARCHAR(50) NOT NULL,
|
|
||||||
radius_bantuan_meter INT NOT NULL DEFAULT 1000,
|
|
||||||
geom GEOMETRY NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
SPATIAL INDEX idx_rumah_ibadah_geom (geom)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
INSERT INTO rumah_ibadah (nama, agama, radius_bantuan_meter, geom) VALUES
|
|
||||||
(
|
|
||||||
'Masjid Raya Mujahidin',
|
|
||||||
'Islam',
|
|
||||||
1000,
|
|
||||||
ST_GeomFromText('POINT(109.3377 -0.0414)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Masjid Jami Sultan Syarif Abdurrahman',
|
|
||||||
'Islam',
|
|
||||||
1000,
|
|
||||||
ST_GeomFromText('POINT(109.3522 -0.0229)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Gereja Katedral Santo Yosef',
|
|
||||||
'Katolik',
|
|
||||||
1000,
|
|
||||||
ST_GeomFromText('POINT(109.3384 -0.0274)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Gereja Kristen Immanuel (GPIB)',
|
|
||||||
'Kristen',
|
|
||||||
1000,
|
|
||||||
ST_GeomFromText('POINT(109.3375 -0.0292)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Gereja GKKB Pontianak',
|
|
||||||
'Kristen',
|
|
||||||
1000,
|
|
||||||
ST_GeomFromText('POINT(109.3386 -0.0336)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Vihara Bodhisatva Karaniya Metta',
|
|
||||||
'Buddha',
|
|
||||||
1000,
|
|
||||||
ST_GeomFromText('POINT(109.3432 -0.0215)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Maha Vihara Maitreya',
|
|
||||||
'Buddha',
|
|
||||||
1000,
|
|
||||||
ST_GeomFromText('POINT(109.3649 -0.0732)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Pura Giripati Mulawarman',
|
|
||||||
'Hindu',
|
|
||||||
1000,
|
|
||||||
ST_GeomFromText('POINT(109.3802 -0.0663)')
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE jalan (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
nama VARCHAR(100) NOT NULL,
|
|
||||||
jenis_jalan VARCHAR(50) DEFAULT NULL,
|
|
||||||
geom GEOMETRY NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
SPATIAL INDEX idx_jalan_geom (geom)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
INSERT INTO jalan (nama, jenis_jalan, geom) VALUES
|
|
||||||
(
|
|
||||||
'Jalan Jenderal Ahmad Yani',
|
|
||||||
'Arteri Primer',
|
|
||||||
ST_GeomFromText('LINESTRING(109.3621 -0.0768, 109.3444 -0.0565, 109.3360 -0.0450, 109.3330 -0.0330)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Jalan Gajah Mada',
|
|
||||||
'Arteri Primer',
|
|
||||||
ST_GeomFromText('LINESTRING(109.3310 -0.0320, 109.3380 -0.0290, 109.3450 -0.0250)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Jalan Tanjung Pura',
|
|
||||||
'Arteri Primer',
|
|
||||||
ST_GeomFromText('LINESTRING(109.3300 -0.0300, 109.3360 -0.0260, 109.3480 -0.0240, 109.3550 -0.0270)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Jalan Imam Bonjol',
|
|
||||||
'Arteri Primer',
|
|
||||||
ST_GeomFromText('LINESTRING(109.3360 -0.0450, 109.3420 -0.0400, 109.3500 -0.0350, 109.3590 -0.0400)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Jalan Adi Sucipto',
|
|
||||||
'Arteri Primer',
|
|
||||||
ST_GeomFromText('LINESTRING(109.3590 -0.0400, 109.3650 -0.0600, 109.3750 -0.0700)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Jalan Khatulistiwa',
|
|
||||||
'Arteri Primer',
|
|
||||||
ST_GeomFromText('LINESTRING(109.3450 0.0050, 109.3350 0.0150, 109.3250 0.0350, 109.3150 0.0550)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Jalan Komodor Yos Sudarso',
|
|
||||||
'Arteri Primer',
|
|
||||||
ST_GeomFromText('LINESTRING(109.3250 -0.0280, 109.3150 -0.0240, 109.3000 -0.0180, 109.2850 -0.0150)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Jalan Prof. M. Yamin',
|
|
||||||
'Arteri Primer',
|
|
||||||
ST_GeomFromText('LINESTRING(109.3245 -0.0487, 109.3170 -0.0540, 109.3050 -0.0620)')
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE kavling (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
nama_pemilik VARCHAR(100) NOT NULL,
|
|
||||||
status_kepemilikan VARCHAR(50) NOT NULL,
|
|
||||||
luas DECIMAL(10,2) DEFAULT NULL,
|
|
||||||
geom GEOMETRY NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
SPATIAL INDEX idx_kavling_geom (geom)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
INSERT INTO kavling (nama_pemilik, status_kepemilikan, luas, geom) VALUES
|
|
||||||
(
|
|
||||||
'PT. Mega Karya',
|
|
||||||
'HGB',
|
|
||||||
25000.00,
|
|
||||||
ST_GeomFromText('POLYGON((109.333 -0.048, 109.338 -0.048, 109.338 -0.053, 109.333 -0.053, 109.333 -0.048))')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Bapak Sudirman',
|
|
||||||
'SHM',
|
|
||||||
1500.00,
|
|
||||||
ST_GeomFromText('POLYGON((109.340 -0.035, 109.341 -0.035, 109.341 -0.036, 109.340 -0.036, 109.340 -0.035))')
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE kawasan_kumuh (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
nama_kawasan VARCHAR(100) NOT NULL,
|
|
||||||
geom GEOMETRY NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
SPATIAL INDEX idx_kawasan_kumuh_geom (geom)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
INSERT INTO kawasan_kumuh (nama_kawasan, geom) VALUES
|
|
||||||
(
|
|
||||||
'Kawasan Rawan Bantaran Sungai',
|
|
||||||
ST_GeomFromText('POLYGON((109.360 -0.050, 109.365 -0.050, 109.365 -0.055, 109.360 -0.055, 109.360 -0.050))')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Kawasan Padat Parit Tokaya',
|
|
||||||
ST_GeomFromText('POLYGON((109.342 -0.038, 109.346 -0.038, 109.346 -0.042, 109.342 -0.042, 109.342 -0.038))')
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE warga_miskin (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
nama_kk VARCHAR(100) NOT NULL,
|
|
||||||
penghasilan DECIMAL(15,2) NOT NULL,
|
|
||||||
jumlah_tanggungan INT NOT NULL,
|
|
||||||
geom GEOMETRY NOT NULL,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
SPATIAL INDEX idx_warga_miskin_geom (geom)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
INSERT INTO warga_miskin (nama_kk, penghasilan, jumlah_tanggungan, geom) VALUES
|
|
||||||
(
|
|
||||||
'Bpk. Budi (Terisolasi)',
|
|
||||||
800000.00,
|
|
||||||
4,
|
|
||||||
ST_GeomFromText('POINT(109.361 -0.051)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Ibu Siti (Terisolasi)',
|
|
||||||
600000.00,
|
|
||||||
2,
|
|
||||||
ST_GeomFromText('POINT(109.362 -0.052)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Bpk. Joko (Terisolasi)',
|
|
||||||
1000000.00,
|
|
||||||
5,
|
|
||||||
ST_GeomFromText('POINT(109.363 -0.053)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Ibu Ani (Terisolasi)',
|
|
||||||
700000.00,
|
|
||||||
3,
|
|
||||||
ST_GeomFromText('POINT(109.364 -0.054)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Bpk. Hasan',
|
|
||||||
1200000.00,
|
|
||||||
3,
|
|
||||||
ST_GeomFromText('POINT(109.334 -0.050)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Mbah Warno',
|
|
||||||
400000.00,
|
|
||||||
1,
|
|
||||||
ST_GeomFromText('POINT(109.336 -0.046)')
|
|
||||||
),
|
|
||||||
(
|
|
||||||
'Pak Junaidi',
|
|
||||||
900000.00,
|
|
||||||
4,
|
|
||||||
ST_GeomFromText('POINT(109.344 -0.040)')
|
|
||||||
);
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- 3. Data Interaksi Pengguna
|
|
||||||
-- ============================================================
|
|
||||||
CREATE TABLE laporan_warga (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
user_id INT NOT NULL,
|
|
||||||
kategori VARCHAR(50) NOT NULL,
|
|
||||||
deskripsi TEXT NOT NULL,
|
|
||||||
geometry POINT NOT NULL,
|
|
||||||
status ENUM('menunggu', 'diproses', 'selesai', 'ditolak') NOT NULL DEFAULT 'menunggu',
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_laporan_user_id (user_id),
|
|
||||||
SPATIAL INDEX idx_laporan_geometry (geometry),
|
|
||||||
CONSTRAINT fk_laporan_user
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
||||||
ON DELETE CASCADE
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
|
|
||||||
CREATE TABLE ulasan_fasilitas (
|
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
||||||
user_id INT NOT NULL,
|
|
||||||
fasilitas_tipe ENUM('spbu', 'rumah_ibadah') NOT NULL,
|
|
||||||
fasilitas_id INT NOT NULL,
|
|
||||||
rating TINYINT NOT NULL,
|
|
||||||
komentar TEXT,
|
|
||||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
INDEX idx_ulasan_user_id (user_id),
|
|
||||||
INDEX idx_ulasan_fasilitas (fasilitas_tipe, fasilitas_id),
|
|
||||||
CONSTRAINT fk_ulasan_user
|
|
||||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
CONSTRAINT chk_ulasan_rating CHECK (rating BETWEEN 1 AND 5)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
-846
@@ -1,846 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="id" data-theme="dark">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>WebGIS Portal — Sistem Informasi Geografis</title>
|
|
||||||
<meta name="description"
|
|
||||||
content="Portal WebGIS — Direktori modul dan proyek akhir mata kuliah Sistem Informasi Geografis.">
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=Space+Grotesk:wght@400;500;600;700&display=swap"
|
|
||||||
rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
|
||||||
<style>
|
|
||||||
/* ══ DESIGN TOKENS ══ */
|
|
||||||
:root {
|
|
||||||
--font-sans: 'Inter', sans-serif;
|
|
||||||
--font-display: 'Space Grotesk', sans-serif;
|
|
||||||
--ease: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
--transition: all 0.25s var(--ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ══ DARK THEME (default) ══ */
|
|
||||||
[data-theme="dark"] {
|
|
||||||
--bg-page: #0A0F1E;
|
|
||||||
--bg-surface: #111827;
|
|
||||||
--bg-card: #141D2F;
|
|
||||||
--bg-card-hover: #1A2540;
|
|
||||||
--border: rgba(255, 255, 255, 0.07);
|
|
||||||
--border-hover: rgba(99, 102, 241, 0.5);
|
|
||||||
--text-primary: #F1F5F9;
|
|
||||||
--text-secondary: #94A3B8;
|
|
||||||
--text-muted: #64748B;
|
|
||||||
--primary: #6366F1;
|
|
||||||
--primary-glow: rgba(99, 102, 241, 0.25);
|
|
||||||
--accent: #06B6D4;
|
|
||||||
--accent-2: #10B981;
|
|
||||||
--accent-3: #F43F5E;
|
|
||||||
--nav-bg: rgba(10, 15, 30, 0.85);
|
|
||||||
--glass: rgba(17, 24, 39, 0.8);
|
|
||||||
--shine: rgba(255, 255, 255, 0.03);
|
|
||||||
--shadow-card: 0 4px 24px rgba(0, 0, 0, 0.4);
|
|
||||||
--shadow-glow: 0 0 40px rgba(99, 102, 241, 0.15);
|
|
||||||
--gradient-hero: linear-gradient(135deg, #0A0F1E 0%, #111827 50%, #0D1B2E 100%);
|
|
||||||
--dot-color: rgba(255, 255, 255, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ══ LIGHT THEME ══ */
|
|
||||||
[data-theme="light"] {
|
|
||||||
--bg-page: #F0F4FF;
|
|
||||||
--bg-surface: #FFFFFF;
|
|
||||||
--bg-card: #FFFFFF;
|
|
||||||
--bg-card-hover: #F8FAFF;
|
|
||||||
--border: #E2E8F0;
|
|
||||||
--border-hover: #6366F1;
|
|
||||||
--text-primary: #0F172A;
|
|
||||||
--text-secondary: #475569;
|
|
||||||
--text-muted: #94A3B8;
|
|
||||||
--primary: #4F46E5;
|
|
||||||
--primary-glow: rgba(79, 70, 229, 0.12);
|
|
||||||
--accent: #0891B2;
|
|
||||||
--accent-2: #059669;
|
|
||||||
--accent-3: #E11D48;
|
|
||||||
--nav-bg: rgba(240, 244, 255, 0.9);
|
|
||||||
--glass: rgba(255, 255, 255, 0.9);
|
|
||||||
--shine: rgba(255, 255, 255, 0.5);
|
|
||||||
--shadow-card: 0 4px 24px rgba(79, 70, 229, 0.08);
|
|
||||||
--shadow-glow: 0 0 40px rgba(79, 70, 229, 0.1);
|
|
||||||
--gradient-hero: linear-gradient(135deg, #EEF2FF 0%, #F0F4FF 50%, #E8F4FF 100%);
|
|
||||||
--dot-color: rgba(79, 70, 229, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
*,
|
|
||||||
*::before,
|
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: var(--font-sans);
|
|
||||||
background: var(--bg-page);
|
|
||||||
color: var(--text-primary);
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
transition: background 0.3s var(--ease), color 0.3s var(--ease);
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Dot grid background ── */
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background-image: radial-gradient(circle, var(--dot-color) 1px, transparent 1px);
|
|
||||||
background-size: 32px 32px;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Ambient glow orbs ── */
|
|
||||||
.orb {
|
|
||||||
position: fixed;
|
|
||||||
border-radius: 50%;
|
|
||||||
filter: blur(120px);
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
transition: opacity 0.4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.orb-1 {
|
|
||||||
width: 500px;
|
|
||||||
height: 500px;
|
|
||||||
background: var(--primary-glow);
|
|
||||||
top: -100px;
|
|
||||||
left: -100px;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.orb-2 {
|
|
||||||
width: 400px;
|
|
||||||
height: 400px;
|
|
||||||
background: rgba(6, 182, 212, 0.1);
|
|
||||||
bottom: -80px;
|
|
||||||
right: -80px;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Navbar ── */
|
|
||||||
.navbar {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
|
||||||
width: 100%;
|
|
||||||
background: var(--nav-bg);
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
padding: 0 32px;
|
|
||||||
height: 64px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-brand {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-icon {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
background: linear-gradient(135deg, var(--primary), var(--accent));
|
|
||||||
border-radius: 10px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
color: white;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar-right {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-pill {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 4px 14px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Theme toggle ── */
|
|
||||||
.theme-toggle {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
background: var(--bg-card);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-toggle:hover {
|
|
||||||
background: var(--primary);
|
|
||||||
border-color: var(--primary);
|
|
||||||
color: white;
|
|
||||||
transform: rotate(15deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Hero section ── */
|
|
||||||
.hero {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
text-align: center;
|
|
||||||
padding: 80px 24px 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 6px 16px;
|
|
||||||
border-radius: 100px;
|
|
||||||
background: var(--primary-glow);
|
|
||||||
border: 1px solid var(--border-hover);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--primary);
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
animation: fadeSlideDown 0.5s var(--ease) both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero h1 {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: clamp(2.2rem, 5vw, 3.8rem);
|
|
||||||
font-weight: 800;
|
|
||||||
line-height: 1.1;
|
|
||||||
letter-spacing: -0.03em;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
animation: fadeSlideDown 0.5s var(--ease) 0.05s both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero h1 .gradient-text {
|
|
||||||
background: linear-gradient(135deg, var(--primary), var(--accent));
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero p {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
max-width: 560px;
|
|
||||||
margin: 0 auto 48px;
|
|
||||||
line-height: 1.7;
|
|
||||||
animation: fadeSlideDown 0.5s var(--ease) 0.1s both;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Stats bar ── */
|
|
||||||
.stats-bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 32px;
|
|
||||||
margin-bottom: 64px;
|
|
||||||
animation: fadeSlideDown 0.5s var(--ease) 0.15s both;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-item {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: 1.8rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-divider {
|
|
||||||
width: 1px;
|
|
||||||
height: 40px;
|
|
||||||
background: var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Grid ── */
|
|
||||||
.grid-section {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1100px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 24px 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-label {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-muted);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-label::after {
|
|
||||||
content: '';
|
|
||||||
flex: 1;
|
|
||||||
height: 1px;
|
|
||||||
background: var(--border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 20px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Card ── */
|
|
||||||
.card {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 20px;
|
|
||||||
padding: 28px;
|
|
||||||
text-decoration: none;
|
|
||||||
color: inherit;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
box-shadow: var(--shadow-card);
|
|
||||||
transition: var(--transition);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: var(--shine);
|
|
||||||
border-radius: inherit;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
transform: translateY(-6px);
|
|
||||||
border-color: var(--border-hover);
|
|
||||||
box-shadow: var(--shadow-glow), var(--shadow-card);
|
|
||||||
background: var(--bg-card-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover::before {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Number badge */
|
|
||||||
.card-num {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: 4rem;
|
|
||||||
font-weight: 900;
|
|
||||||
color: var(--border);
|
|
||||||
line-height: 1;
|
|
||||||
letter-spacing: -0.05em;
|
|
||||||
transition: var(--transition);
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover .card-num {
|
|
||||||
color: var(--primary-glow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-icon-wrap {
|
|
||||||
width: 52px;
|
|
||||||
height: 52px;
|
|
||||||
border-radius: 14px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover .card-icon-wrap {
|
|
||||||
transform: scale(1.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card themes */
|
|
||||||
.card[data-type="p01"] .card-icon-wrap {
|
|
||||||
background: rgba(99, 102, 241, 0.15);
|
|
||||||
color: #818CF8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card[data-type="p02"] .card-icon-wrap {
|
|
||||||
background: rgba(6, 182, 212, 0.15);
|
|
||||||
color: #22D3EE;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card[data-type="p03"] .card-icon-wrap {
|
|
||||||
background: rgba(16, 185, 129, 0.15);
|
|
||||||
color: #34D399;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card[data-type="uas"] .card-icon-wrap {
|
|
||||||
background: rgba(244, 63, 94, 0.15);
|
|
||||||
color: #FB7185;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-tag {
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
color: var(--text-muted);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
font-family: var(--font-display);
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: 10px;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-desc {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
line-height: 1.6;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-footer {
|
|
||||||
margin-top: 24px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--primary);
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-footer .arrow {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: var(--primary-glow);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover .card-footer {
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover .card-footer .arrow {
|
|
||||||
background: var(--primary);
|
|
||||||
color: white;
|
|
||||||
transform: translateX(3px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Final (UAS) card — full-width special ── */
|
|
||||||
.card-uas {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 32px;
|
|
||||||
background: linear-gradient(135deg, var(--primary) 0%, #4338CA 100%);
|
|
||||||
border-color: transparent;
|
|
||||||
padding: 36px 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas::before {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas:hover {
|
|
||||||
background: var(--bg-card-hover);
|
|
||||||
border-color: var(--border-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas .card-icon-wrap {
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
color: white;
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
font-size: 1.8rem;
|
|
||||||
flex-shrink: 0;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas:hover .card-icon-wrap {
|
|
||||||
background: rgba(244, 63, 94, 0.15);
|
|
||||||
color: #FB7185;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas .card-tag {
|
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas:hover .card-tag {
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas .card-title {
|
|
||||||
color: white;
|
|
||||||
font-size: 1.6rem;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas:hover .card-title {
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas .card-desc {
|
|
||||||
color: rgba(255, 255, 255, 0.8);
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas:hover .card-desc {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas .card-footer {
|
|
||||||
color: white;
|
|
||||||
margin-top: 0;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas:hover .card-footer {
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas .card-num {
|
|
||||||
color: rgba(255, 255, 255, 0.08);
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas:hover .card-num {
|
|
||||||
color: var(--primary-glow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas .card-footer .arrow {
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
color: white;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas:hover .card-footer .arrow {
|
|
||||||
background: var(--primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas-text {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Footer ── */
|
|
||||||
.footer {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
margin-top: auto;
|
|
||||||
padding: 24px 32px;
|
|
||||||
border-top: 1px solid var(--border);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ══ ANIMATIONS ══ */
|
|
||||||
@keyframes fadeSlideDown {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-12px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
animation: fadeSlideUp 0.5s var(--ease) both;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:nth-child(1) {
|
|
||||||
animation-delay: 0.15s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:nth-child(2) {
|
|
||||||
animation-delay: 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:nth-child(3) {
|
|
||||||
animation-delay: 0.25s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:nth-child(4) {
|
|
||||||
animation-delay: 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeSlideUp {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ══ RESPONSIVE ══ */
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.grid {
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas {
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-uas .card-footer {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navbar {
|
|
||||||
padding: 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
padding: 48px 16px 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-bar {
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-divider {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<!-- Ambient orbs -->
|
|
||||||
<div class="orb orb-1"></div>
|
|
||||||
<div class="orb orb-2"></div>
|
|
||||||
|
|
||||||
<!-- Navbar -->
|
|
||||||
<nav class="navbar">
|
|
||||||
<a href="#" class="navbar-brand">
|
|
||||||
<div class="brand-icon"><i class="fas fa-map-marked-alt"></i></div>
|
|
||||||
WebGIS Portal
|
|
||||||
</a>
|
|
||||||
<div class="navbar-right">
|
|
||||||
<span class="nav-pill">SIG 2025/2026</span>
|
|
||||||
<button class="theme-toggle" id="themeToggle" title="Toggle tema" aria-label="Toggle dark/light mode">
|
|
||||||
<i class="fas fa-moon" id="themeIcon"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Hero -->
|
|
||||||
<section class="hero">
|
|
||||||
<div class="hero-badge">
|
|
||||||
<i class="fas fa-satellite-dish"></i>
|
|
||||||
Sistem Informasi Geografis
|
|
||||||
</div>
|
|
||||||
<h1>
|
|
||||||
Direktori Proyek<br>
|
|
||||||
<span class="gradient-text">WebGIS Interaktif</span>
|
|
||||||
</h1>
|
|
||||||
<p>Kumpulan modul pembelajaran dan proyek akhir mata kuliah Sistem Informasi Geografis. Pilih modul untuk
|
|
||||||
memulai eksplorasi.</p>
|
|
||||||
|
|
||||||
<div class="stats-bar">
|
|
||||||
<div class="stat-item">
|
|
||||||
<div class="stat-value">3</div>
|
|
||||||
<div class="stat-label">Modul Praktikum</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-divider"></div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<div class="stat-value">1</div>
|
|
||||||
<div class="stat-label">Proyek Akhir</div>
|
|
||||||
</div>
|
|
||||||
<div class="stat-divider"></div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<div class="stat-value">GIS</div>
|
|
||||||
<div class="stat-label">Teknologi</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Grid -->
|
|
||||||
<div class="grid-section">
|
|
||||||
<div class="section-label">Modul Pembelajaran</div>
|
|
||||||
|
|
||||||
<div class="grid">
|
|
||||||
<!-- Pertemuan 01 -->
|
|
||||||
<a href="01/frontend/" class="card" data-type="p01">
|
|
||||||
<span class="card-num">01</span>
|
|
||||||
<div class="card-icon-wrap">
|
|
||||||
<i class="fas fa-draw-polygon"></i>
|
|
||||||
</div>
|
|
||||||
<div class="card-tag">Pertemuan 01</div>
|
|
||||||
<h2 class="card-title">Geometri Dasar</h2>
|
|
||||||
<p class="card-desc">Menggambar dan mengelola geometri spasial: titik (SPBU), garis (Jalan), dan poligon
|
|
||||||
(Kavling) dengan operasi CRUD.</p>
|
|
||||||
<div class="card-footer">
|
|
||||||
Buka Modul
|
|
||||||
<div class="arrow"><i class="fas fa-arrow-right"></i></div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<!-- Pertemuan 02 -->
|
|
||||||
<a href="02/frontend/" class="card" data-type="p02">
|
|
||||||
<span class="card-num">02</span>
|
|
||||||
<div class="card-icon-wrap">
|
|
||||||
<i class="fas fa-route"></i>
|
|
||||||
</div>
|
|
||||||
<div class="card-tag">Pertemuan 02</div>
|
|
||||||
<h2 class="card-title">Haversine & Tematik</h2>
|
|
||||||
<p class="card-desc">Pemetaan sosial dengan analisis radius jarak Haversine dan visualisasi tematik data
|
|
||||||
kemiskinan penduduk.</p>
|
|
||||||
<div class="card-footer">
|
|
||||||
Buka Modul
|
|
||||||
<div class="arrow"><i class="fas fa-arrow-right"></i></div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<!-- Pertemuan 03 -->
|
|
||||||
<a href="03/frontend/" class="card" data-type="p03">
|
|
||||||
<span class="card-num">03</span>
|
|
||||||
<div class="card-icon-wrap">
|
|
||||||
<i class="fas fa-layer-group"></i>
|
|
||||||
</div>
|
|
||||||
<div class="card-tag">Pertemuan 03</div>
|
|
||||||
<h2 class="card-title">Analisis Spasial</h2>
|
|
||||||
<p class="card-desc">Choropleth Map dinamis berbasis Point-in-Polygon — pewarnaan otomatis area kavling
|
|
||||||
oleh kepadatan warga miskin.</p>
|
|
||||||
<div class="card-footer">
|
|
||||||
Buka Modul
|
|
||||||
<div class="arrow"><i class="fas fa-arrow-right"></i></div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<!-- Final UAS -->
|
|
||||||
<a href="webgis_app/" class="card card-uas" data-type="uas">
|
|
||||||
<span class="card-num">UAS</span>
|
|
||||||
<div class="card-icon-wrap">
|
|
||||||
<i class="fas fa-laptop-code"></i>
|
|
||||||
</div>
|
|
||||||
<div class="card-uas-text">
|
|
||||||
<div class="card-tag">Proyek Akhir — Final UAS</div>
|
|
||||||
<h2 class="card-title">Smart City WebGIS</h2>
|
|
||||||
<p class="card-desc">Sistem komprehensif manajemen kawasan kota dengan dashboard analitik,
|
|
||||||
autentikasi peran, dan visualisasi peta interaktif real-time.</p>
|
|
||||||
<div class="card-footer" style="margin-top:20px;">
|
|
||||||
Buka Aplikasi
|
|
||||||
<div class="arrow"><i class="fas fa-arrow-right"></i></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<footer class="footer">
|
|
||||||
<span>D1041231071 — Naufal Zaky Ramadhan</span>
|
|
||||||
<span>Sistem Informasi Geografis © 2025</span>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// ── Theme toggle ──
|
|
||||||
const html = document.documentElement;
|
|
||||||
const btn = document.getElementById('themeToggle');
|
|
||||||
const icon = document.getElementById('themeIcon');
|
|
||||||
|
|
||||||
// Persist preference
|
|
||||||
const saved = localStorage.getItem('portal-theme') || 'dark';
|
|
||||||
setTheme(saved);
|
|
||||||
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
const next = html.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
|
|
||||||
setTheme(next);
|
|
||||||
localStorage.setItem('portal-theme', next);
|
|
||||||
});
|
|
||||||
|
|
||||||
function setTheme(t) {
|
|
||||||
html.setAttribute('data-theme', t);
|
|
||||||
icon.className = t === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
|
|
||||||
btn.title = t === 'dark' ? 'Beralih ke Mode Terang' : 'Beralih ke Mode Gelap';
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
$host = '203.24.51.230';
|
|
||||||
$port = 33060;
|
|
||||||
$user = 'root';
|
|
||||||
$pass = 'oyMK5S18DMjHtOSVxoQWy2n0YASA5QJuyko00udOuneeijdiNFENgVG2ZMXFL36H';
|
|
||||||
|
|
||||||
$conn = mysqli_connect($host, $user, $pass, '', $port);
|
|
||||||
|
|
||||||
if (!$conn) {
|
|
||||||
die("Connection failed: " . mysqli_connect_error() . "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
$sql = file_get_contents(__DIR__ . '/database/webgis_db.sql');
|
|
||||||
if (mysqli_multi_query($conn, $sql)) {
|
|
||||||
do {
|
|
||||||
if ($res = mysqli_store_result($conn)) {
|
|
||||||
mysqli_free_result($res);
|
|
||||||
}
|
|
||||||
} while (mysqli_more_results($conn) && mysqli_next_result($conn));
|
|
||||||
echo "Database imported successfully!\n";
|
|
||||||
} else {
|
|
||||||
echo "Error importing: " . mysqli_error($conn) . "\n";
|
|
||||||
}
|
|
||||||
mysqli_close($conn);
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* jalan.php
|
|
||||||
* Tanggung Jawab: Menangani operasi CRUD untuk entitas Jalan (LineString).
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once '../core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'GET':
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, nama, jenis_jalan, created_at, ST_AsGeoJSON(geom) as geojson FROM jalan");
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama' => $row['nama'],
|
|
||||||
'jenis_jalan' => $row['jenis_jalan'],
|
|
||||||
'created_at' => $row['created_at']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data Jalan berhasil diambil');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal mengambil data Jalan: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
if (!isset($input['nama']) || !isset($input['geometry'])) {
|
|
||||||
sendError('Nama dan geometri wajib diisi');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO jalan (nama, jenis_jalan, geom) VALUES (:nama, :jenis_jalan, ST_GeomFromGeoJSON(:geometry))";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':nama' => $input['nama'],
|
|
||||||
':jenis_jalan' => $input['jenis_jalan'] ?? 'Lokal',
|
|
||||||
':geometry' => json_encode($input['geometry'])
|
|
||||||
]);
|
|
||||||
sendSuccess(['id' => $pdo->lastInsertId()], 'Data Jalan berhasil disimpan', 201);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal menyimpan Jalan: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
if (!$id) {
|
|
||||||
sendError('ID Jalan wajib disertakan');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM jalan WHERE id = :id");
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
sendSuccess(null, 'Data Jalan berhasil dihapus');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal menghapus Jalan: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* kavling.php
|
|
||||||
* Tanggung Jawab: Menangani operasi CRUD untuk entitas Kavling (Polygon).
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once '../core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'GET':
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, nama_pemilik, luas, created_at, ST_AsGeoJSON(geom) as geojson FROM kavling");
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama_pemilik' => $row['nama_pemilik'],
|
|
||||||
'luas' => $row['luas'],
|
|
||||||
'created_at' => $row['created_at']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data Kavling berhasil diambil');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal mengambil data Kavling: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
if (!isset($input['nama_pemilik']) || !isset($input['geometry'])) {
|
|
||||||
sendError('Nama pemilik dan geometri wajib diisi');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO kavling (nama_pemilik, luas, geom) VALUES (:nama_pemilik, :luas, ST_GeomFromGeoJSON(:geometry))";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':nama_pemilik' => $input['nama_pemilik'],
|
|
||||||
':luas' => $input['luas'] ?? 0,
|
|
||||||
':geometry' => json_encode($input['geometry'])
|
|
||||||
]);
|
|
||||||
sendSuccess(['id' => $pdo->lastInsertId()], 'Data Kavling berhasil disimpan', 201);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal menyimpan Kavling: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
if (!$id) {
|
|
||||||
sendError('ID Kavling wajib disertakan');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM kavling WHERE id = :id");
|
|
||||||
$stmt->execute([':id' => $id]);
|
|
||||||
sendSuccess(null, 'Data Kavling berhasil dihapus');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal menghapus Kavling: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* rumah_ibadah.php
|
|
||||||
* Tanggung Jawab: Operasi CRUD Rumah Ibadah + Endpoint analisis jangkauan Haversine.
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once '../core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
require_once '../utils/geo_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'GET':
|
|
||||||
if (isset($_GET['action']) && $_GET['action'] === 'jangkauan' && isset($_GET['id'])) {
|
|
||||||
// Analisis Jangkauan Haversine
|
|
||||||
$id = $_GET['id'];
|
|
||||||
$radius = isset($_GET['radius']) ? (float) $_GET['radius'] : 1.0; // default 1 km
|
|
||||||
|
|
||||||
// Dapatkan koordinat rumah ibadah ini
|
|
||||||
$stmt = $pdo->prepare("SELECT ST_AsGeoJSON(geom) as geojson FROM rumah_ibadah WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
$rumah_ibadah = $stmt->fetch();
|
|
||||||
|
|
||||||
if (!$rumah_ibadah) sendError('Rumah ibadah tidak ditemukan', 404);
|
|
||||||
|
|
||||||
$geom = json_decode($rumah_ibadah['geojson'], true);
|
|
||||||
$pusatLon = $geom['coordinates'][0];
|
|
||||||
$pusatLat = $geom['coordinates'][1];
|
|
||||||
|
|
||||||
// Panggil geo_helper untuk mendapatkan warga miskin
|
|
||||||
$warga = GeoHelper::getWargaDalamRadius($pdo, $pusatLat, $pusatLon, $radius);
|
|
||||||
sendSuccess($warga, 'Data jangkauan berhasil dihitung');
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// GET Semua Rumah Ibadah (GeoJSON)
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, nama, agama, ST_AsGeoJSON(geom) as geojson FROM rumah_ibadah");
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama' => $row['nama'],
|
|
||||||
'agama' => $row['agama']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data Rumah Ibadah');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
if (!isset($input['nama']) || !isset($input['geometry'])) sendError('Validasi gagal');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO rumah_ibadah (nama, agama, geom) VALUES (:nama, :agama, ST_GeomFromGeoJSON(:geometry))";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':nama' => $input['nama'],
|
|
||||||
':agama' => $input['agama'] ?? 'Islam',
|
|
||||||
':geometry' => json_encode($input['geometry'])
|
|
||||||
]);
|
|
||||||
sendSuccess(['id' => $pdo->lastInsertId()], 'Disimpan', 201);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
if (!$id) sendError('ID dibutuhkan');
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM rumah_ibadah WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
sendSuccess(null, 'Dihapus');
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
<?php
|
|
||||||
require_once '../core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'GET':
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, nama, deskripsi, buka_24_jam, created_at, ST_AsGeoJSON(geom) as geojson FROM spbu");
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama' => $row['nama'],
|
|
||||||
'deskripsi' => $row['deskripsi'],
|
|
||||||
'buka_24_jam' => (bool)$row['buka_24_jam'],
|
|
||||||
'created_at' => $row['created_at']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data SPBU berhasil diambil');
|
|
||||||
} catch (PDOException $e) { sendError('Gagal: ' . $e->getMessage(), 500); }
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
if (!isset($input['nama']) || !isset($input['geometry'])) sendError('Validasi gagal');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO spbu (nama, deskripsi, buka_24_jam, geom) VALUES (:nama, :deskripsi, :buka_24_jam, ST_GeomFromGeoJSON(:geometry))";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':nama' => $input['nama'],
|
|
||||||
':deskripsi' => $input['deskripsi'] ?? '',
|
|
||||||
':buka_24_jam' => isset($input['buka_24_jam']) && $input['buka_24_jam'] ? 1 : 0,
|
|
||||||
':geometry' => json_encode($input['geometry'])
|
|
||||||
]);
|
|
||||||
sendSuccess(['id' => $pdo->lastInsertId()], 'Disimpan', 201);
|
|
||||||
} catch (PDOException $e) { sendError('Gagal: ' . $e->getMessage(), 500); }
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
if (!$id) sendError('ID dibutuhkan');
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM spbu WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
sendSuccess(null, 'Dihapus');
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* statistik.php
|
|
||||||
* Tanggung Jawab: Menyediakan data analisis spasial menggunakan Point in Polygon (ST_Contains).
|
|
||||||
* Menghitung kepadatan warga miskin dalam setiap kavling.
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once '../core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
if ($method === 'GET') {
|
|
||||||
try {
|
|
||||||
// Query Spatial Point-in-Polygon
|
|
||||||
// Mengambil Kavling dan menghitung jumlah warga miskin (Point) yang ada di dalamnya
|
|
||||||
$sql = "
|
|
||||||
SELECT
|
|
||||||
k.id,
|
|
||||||
k.nama_pemilik,
|
|
||||||
k.luas,
|
|
||||||
ST_AsGeoJSON(k.geom) as geojson,
|
|
||||||
COUNT(w.id) as jumlah_warga,
|
|
||||||
COALESCE(SUM(w.jumlah_tanggungan), 0) as total_tanggungan
|
|
||||||
FROM kavling k
|
|
||||||
LEFT JOIN warga_miskin w ON ST_Contains(k.geom, w.geom)
|
|
||||||
GROUP BY k.id
|
|
||||||
";
|
|
||||||
|
|
||||||
$stmt = $pdo->query($sql);
|
|
||||||
|
|
||||||
$features = [];
|
|
||||||
$total_warga = 0;
|
|
||||||
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$jumlah = (int) $row['jumlah_warga'];
|
|
||||||
$total_warga += $jumlah;
|
|
||||||
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama_pemilik' => $row['nama_pemilik'],
|
|
||||||
'luas' => $row['luas'],
|
|
||||||
'jumlah_warga' => $jumlah,
|
|
||||||
'total_tanggungan' => (int) $row['total_tanggungan']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$response = [
|
|
||||||
'statistik' => [
|
|
||||||
'total_kavling' => count($features),
|
|
||||||
'total_warga_terpetakan' => $total_warga
|
|
||||||
],
|
|
||||||
'geojson' => [
|
|
||||||
'type' => 'FeatureCollection',
|
|
||||||
'features' => $features
|
|
||||||
]
|
|
||||||
];
|
|
||||||
|
|
||||||
sendSuccess($response, 'Data statistik spasial berhasil dihitung');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal menghitung statistik spasial: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* warga_miskin.php
|
|
||||||
* Tanggung Jawab: Operasi CRUD untuk Warga Miskin.
|
|
||||||
*/
|
|
||||||
|
|
||||||
require_once '../core_config/database.php';
|
|
||||||
require_once '../utils/response_helper.php';
|
|
||||||
|
|
||||||
$pdo = Database::getConnection();
|
|
||||||
$method = $_SERVER['REQUEST_METHOD'];
|
|
||||||
|
|
||||||
switch ($method) {
|
|
||||||
case 'GET':
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT id, nama_kk, penghasilan, jumlah_tanggungan, ST_AsGeoJSON(geom) as geojson FROM warga_miskin");
|
|
||||||
$features = [];
|
|
||||||
while ($row = $stmt->fetch()) {
|
|
||||||
$features[] = [
|
|
||||||
'type' => 'Feature',
|
|
||||||
'geometry' => json_decode($row['geojson']),
|
|
||||||
'properties' => [
|
|
||||||
'id' => $row['id'],
|
|
||||||
'nama_kk' => $row['nama_kk'],
|
|
||||||
'penghasilan' => $row['penghasilan'],
|
|
||||||
'jumlah_tanggungan' => $row['jumlah_tanggungan']
|
|
||||||
]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
sendSuccess(['type' => 'FeatureCollection', 'features' => $features], 'Data Warga Miskin');
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'POST':
|
|
||||||
$input = json_decode(file_get_contents('php://input'), true);
|
|
||||||
if (!isset($input['nama_kk']) || !isset($input['geometry'])) sendError('Validasi gagal');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sql = "INSERT INTO warga_miskin (nama_kk, penghasilan, jumlah_tanggungan, geom) VALUES (:nama_kk, :penghasilan, :jumlah_tanggungan, ST_GeomFromGeoJSON(:geometry))";
|
|
||||||
$stmt = $pdo->prepare($sql);
|
|
||||||
$stmt->execute([
|
|
||||||
':nama_kk' => $input['nama_kk'],
|
|
||||||
':penghasilan' => $input['penghasilan'] ?? 0,
|
|
||||||
':jumlah_tanggungan' => $input['jumlah_tanggungan'] ?? 0,
|
|
||||||
':geometry' => json_encode($input['geometry'])
|
|
||||||
]);
|
|
||||||
sendSuccess(['id' => $pdo->lastInsertId()], 'Disimpan', 201);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
sendError('Gagal: ' . $e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DELETE':
|
|
||||||
$id = $_GET['id'] ?? null;
|
|
||||||
if (!$id) sendError('ID dibutuhkan');
|
|
||||||
$stmt = $pdo->prepare("DELETE FROM warga_miskin WHERE id = ?");
|
|
||||||
$stmt->execute([$id]);
|
|
||||||
sendSuccess(null, 'Dihapus');
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendError('Method not allowed', 405);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
class Database {
|
|
||||||
private static $conn = null;
|
|
||||||
|
|
||||||
private static function env(string $key, string $default = ''): string {
|
|
||||||
$value = getenv($key);
|
|
||||||
return ($value === false || $value === '') ? $default : $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getConnection() {
|
|
||||||
if (self::$conn === null) {
|
|
||||||
try {
|
|
||||||
$host = self::env('DB_HOST', '127.0.0.1');
|
|
||||||
$port = self::env('DB_PORT', '3306');
|
|
||||||
$dbName = self::env('DB_DATABASE', 'webgis_db');
|
|
||||||
$username = self::env('DB_USERNAME', 'root');
|
|
||||||
$password = self::env('DB_PASSWORD', '');
|
|
||||||
|
|
||||||
self::$conn = new PDO(
|
|
||||||
"mysql:host={$host};port={$port};dbname={$dbName};charset=utf8mb4",
|
|
||||||
$username,
|
|
||||||
$password
|
|
||||||
);
|
|
||||||
self::$conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
||||||
self::$conn->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
|
||||||
} catch (PDOException $exception) {
|
|
||||||
echo "Connection error: " . $exception->getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return self::$conn;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* response_helper.php
|
|
||||||
* Tanggung Jawab: Standarisasi format response JSON.
|
|
||||||
*/
|
|
||||||
|
|
||||||
header('Content-Type: application/json; charset=utf-8');
|
|
||||||
$allowedOrigin = getenv('CORS_ALLOWED_ORIGIN');
|
|
||||||
if ($allowedOrigin !== false && $allowedOrigin !== '') {
|
|
||||||
header('Access-Control-Allow-Origin: ' . $allowedOrigin);
|
|
||||||
header('Vary: Origin');
|
|
||||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
|
||||||
header('Access-Control-Allow-Headers: Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { exit(0); }
|
|
||||||
|
|
||||||
function sendSuccess($data = null, $message = 'Success', $statusCode = 200) {
|
|
||||||
http_response_code($statusCode);
|
|
||||||
echo json_encode(['status' => 'success', 'message' => $message, 'data' => $data]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendError($message = 'Error', $statusCode = 400) {
|
|
||||||
http_response_code($statusCode);
|
|
||||||
echo json_encode(['status' => 'error', 'message' => $message]);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
/* base.css
|
|
||||||
* Tanggung Jawab: Reset CSS, definisi variabel warna premium, dan tipografi dasar.
|
|
||||||
*/
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
|
||||||
|
|
||||||
:root {
|
|
||||||
/* Color Palette - Minimalist & Elegant */
|
|
||||||
--primary: #4F46E5;
|
|
||||||
--primary-hover: #4338CA;
|
|
||||||
--secondary: #10B981;
|
|
||||||
--background: #F3F4F6;
|
|
||||||
--surface: #FFFFFF;
|
|
||||||
--surface-glass: rgba(255, 255, 255, 0.85);
|
|
||||||
--text-main: #111827;
|
|
||||||
--text-muted: #6B7280;
|
|
||||||
--border: #E5E7EB;
|
|
||||||
--danger: #EF4444;
|
|
||||||
|
|
||||||
/* Shadows & Effects */
|
|
||||||
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
||||||
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
||||||
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
||||||
--radius-md: 12px;
|
|
||||||
--radius-lg: 16px;
|
|
||||||
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', sans-serif;
|
|
||||||
background-color: var(--background);
|
|
||||||
color: var(--text-main);
|
|
||||||
display: flex;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4 {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar minimalis */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: #CBD5E1;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #94A3B8;
|
|
||||||
}
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
:root {
|
|
||||||
--primary: #6366F1;
|
|
||||||
--primary-hover: #4F46E5;
|
|
||||||
--bg-dark: #0F172A;
|
|
||||||
--bg-glass: rgba(15, 23, 42, 0.75);
|
|
||||||
--border-glass: rgba(255, 255, 255, 0.1);
|
|
||||||
--text-light: #F8FAFC;
|
|
||||||
--text-muted: #94A3B8;
|
|
||||||
--radius: 20px;
|
|
||||||
--shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
|
||||||
--font: 'Inter', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: var(--font);
|
|
||||||
background: var(--bg-dark);
|
|
||||||
color: var(--text-light);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#map {
|
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Floating Dock */
|
|
||||||
.floating-dock {
|
|
||||||
position: absolute;
|
|
||||||
top: 24px;
|
|
||||||
left: 24px;
|
|
||||||
z-index: 1000;
|
|
||||||
background: var(--bg-glass);
|
|
||||||
backdrop-filter: blur(24px);
|
|
||||||
-webkit-backdrop-filter: blur(24px);
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 24px;
|
|
||||||
width: 340px;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
max-height: calc(100vh - 48px);
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom Scrollbar for Dock */
|
|
||||||
.floating-dock::-webkit-scrollbar { width: 6px; }
|
|
||||||
.floating-dock::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 10px; }
|
|
||||||
|
|
||||||
.header h2 { margin: 0 0 8px 0; font-size: 1.4rem; font-weight: 700; background: linear-gradient(135deg, #818CF8, #C084FC); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
|
|
||||||
.header p { margin: 0; font-size: 0.85rem; color: var(--text-muted); line-height: 1.5; }
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.menu-group { display: flex; flex-direction: column; gap: 10px; }
|
|
||||||
.menu-title { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 1px; color: var(--text-muted); font-weight: 600; margin-bottom: 4px; }
|
|
||||||
|
|
||||||
.btn-action {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
color: var(--text-light);
|
|
||||||
padding: 14px 16px;
|
|
||||||
border-radius: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 500;
|
|
||||||
font-family: var(--font);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action svg { width: 20px; height: 20px; flex-shrink: 0; }
|
|
||||||
|
|
||||||
.btn-action:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
transform: translateX(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-action.active {
|
|
||||||
background: var(--primary);
|
|
||||||
border-color: var(--primary);
|
|
||||||
box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button Variants */
|
|
||||||
.btn-spbu svg { color: #10B981; }
|
|
||||||
.btn-jalan svg { color: #F59E0B; }
|
|
||||||
.btn-kavling svg { color: #3B82F6; }
|
|
||||||
.btn-rumah svg { color: #8B5CF6; }
|
|
||||||
.btn-warga svg { color: #EF4444; }
|
|
||||||
|
|
||||||
/* Form Panel */
|
|
||||||
.form-panel {
|
|
||||||
background: rgba(0, 0, 0, 0.4);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 16px;
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group { display: flex; flex-direction: column; gap: 6px; }
|
|
||||||
.form-group label { font-size: 0.8rem; color: var(--text-muted); }
|
|
||||||
.form-control {
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border: 1px solid var(--border-glass);
|
|
||||||
padding: 10px 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: white;
|
|
||||||
font-family: var(--font);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
outline: none;
|
|
||||||
transition: border 0.2s;
|
|
||||||
}
|
|
||||||
.form-control:focus { border-color: var(--primary); }
|
|
||||||
|
|
||||||
.checkbox-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.checkbox-group input { width: 16px; height: 16px; accent-color: var(--primary); cursor: pointer; }
|
|
||||||
.checkbox-group span { font-size: 0.85rem; color: var(--text-light); }
|
|
||||||
|
|
||||||
.btn-submit {
|
|
||||||
background: var(--primary);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
}
|
|
||||||
.btn-submit:hover { background: var(--primary-hover); }
|
|
||||||
|
|
||||||
/* Leaflet Customizations */
|
|
||||||
.leaflet-draw-toolbar { display: none !important; }
|
|
||||||
.leaflet-control-zoom { border: none !important; box-shadow: var(--shadow) !important; }
|
|
||||||
.leaflet-control-zoom a {
|
|
||||||
background: var(--bg-glass) !important;
|
|
||||||
color: var(--text-light) !important;
|
|
||||||
border-color: var(--border-glass) !important;
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
.leaflet-control-zoom a:hover { background: var(--primary) !important; }
|
|
||||||
|
|
||||||
/* Leaflet Popup Premium */
|
|
||||||
.leaflet-popup-content-wrapper {
|
|
||||||
background: var(--bg-glass) !important;
|
|
||||||
backdrop-filter: blur(16px) !important;
|
|
||||||
border: 1px solid var(--border-glass) !important;
|
|
||||||
color: var(--text-light) !important;
|
|
||||||
border-radius: 16px !important;
|
|
||||||
box-shadow: var(--shadow) !important;
|
|
||||||
}
|
|
||||||
.leaflet-popup-tip { background: var(--bg-glass) !important; border: 1px solid var(--border-glass) !important; }
|
|
||||||
.leaflet-popup-content { margin: 16px !important; font-family: var(--font); }
|
|
||||||
.leaflet-popup-content h3 { margin: 0 0 8px 0; font-size: 1.1rem; border-bottom: 1px solid var(--border-glass); padding-bottom: 8px; }
|
|
||||||
.leaflet-popup-content p { margin: 4px 0; font-size: 0.9rem; color: var(--text-muted); }
|
|
||||||
.leaflet-popup-content .btn-delete {
|
|
||||||
background: rgba(239, 68, 68, 0.1);
|
|
||||||
color: #EF4444;
|
|
||||||
border: 1px solid rgba(239, 68, 68, 0.3);
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-top: 12px;
|
|
||||||
width: 100%;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
.leaflet-popup-content .btn-delete:hover { background: #EF4444; color: white; }
|
|
||||||
|
|
||||||
/* Custom Map Marker Icon Animation */
|
|
||||||
.custom-icon div {
|
|
||||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
.custom-icon:hover div {
|
|
||||||
transform: scale(1.1) translateY(-4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toast Notification */
|
|
||||||
.toast-container {
|
|
||||||
position: fixed; bottom: 24px; right: 24px; z-index: 9999;
|
|
||||||
display: flex; flex-direction: column; gap: 10px;
|
|
||||||
}
|
|
||||||
.toast {
|
|
||||||
background: var(--bg-glass); backdrop-filter: blur(16px);
|
|
||||||
border: 1px solid var(--border-glass); border-left: 4px solid var(--primary);
|
|
||||||
padding: 16px 20px; border-radius: 12px; color: white; font-size: 0.9rem;
|
|
||||||
box-shadow: var(--shadow); animation: slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
.toast.error { border-left-color: #EF4444; }
|
|
||||||
.toast.success { border-left-color: #10B981; }
|
|
||||||
|
|
||||||
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
/* form.css
|
|
||||||
* Tanggung Jawab: Styling komponen form (input, button, select).
|
|
||||||
*/
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-label {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
color: var(--text-main);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 14px;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
transition: var(--transition);
|
|
||||||
background: var(--surface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-control:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: var(--primary);
|
|
||||||
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Buttons */
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 10px 20px;
|
|
||||||
font-family: inherit;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: var(--transition);
|
|
||||||
gap: 8px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background: var(--primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background: var(--primary-hover);
|
|
||||||
transform: translateY(-1px);
|
|
||||||
box-shadow: var(--shadow-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger {
|
|
||||||
background: var(--danger);
|
|
||||||
color: white;
|
|
||||||
width: auto;
|
|
||||||
padding: 6px 12px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-danger:hover {
|
|
||||||
background: #DC2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Micro-animation for active state */
|
|
||||||
.btn:active {
|
|
||||||
transform: translateY(1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form Container Animation */
|
|
||||||
#form-container {
|
|
||||||
background: var(--surface);
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
margin-bottom: 20px;
|
|
||||||
animation: fadeIn 0.4s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from { opacity: 0; transform: scale(0.98); }
|
|
||||||
to { opacity: 1; transform: scale(1); }
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="id">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>WebGIS - Pertemuan 03</title>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
|
|
||||||
<link rel="stylesheet" href="css/main.css">
|
|
||||||
<style>
|
|
||||||
.result-item { background: rgba(255,255,255,0.05); padding: 8px 10px; border-radius: 8px; border: 1px solid var(--border-glass); display: flex; justify-content: space-between; align-items: center; }
|
|
||||||
.badge { background: var(--primary); padding: 2px 6px; border-radius: 12px; font-size: 0.7rem; font-weight: 700; color: white; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="map"></div>
|
|
||||||
<div class="floating-dock">
|
|
||||||
<div class="header">
|
|
||||||
<h2>P03: Analisis Spasial</h2>
|
|
||||||
<p>Kumulatif P01 & P02 + Choropleth Map (Point in Polygon).</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="menu-group">
|
|
||||||
<div class="menu-title">Draw Tools</div>
|
|
||||||
<button id="btn-draw-spbu" class="btn-action btn-spbu"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.242-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg> Tambah SPBU</button>
|
|
||||||
<button id="btn-draw-jalan" class="btn-action btn-jalan"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path></svg> Tambah Jalan</button>
|
|
||||||
<button id="btn-draw-kavling" class="btn-action btn-kavling"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z"></path></svg> Tambah Kavling</button>
|
|
||||||
<button id="btn-draw-rumah" class="btn-action btn-rumah"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path></svg> Tambah Rumah Ibadah</button>
|
|
||||||
<button id="btn-draw-warga" class="btn-action btn-warga"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path></svg> Tambah Warga Miskin</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="choropleth-panel" class="form-panel" style="margin-top: 5px; background: rgba(16, 185, 129, 0.1); border-color: rgba(16, 185, 129, 0.3);">
|
|
||||||
<h3 style="margin:0 0 5px 0; font-size:0.9rem; color:#10B981;">Statistik Kavling (Choropleth)</h3>
|
|
||||||
<p style="font-size:0.8rem; margin:0 0 10px 0; color:var(--text-muted);">Pewarnaan otomatis area kavling berdasarkan jumlah warga miskin yang bermukim di atasnya.</p>
|
|
||||||
<div style="display:flex; justify-content:space-between; margin-bottom:8px;">
|
|
||||||
<span style="font-size:0.8rem;">Total Penduduk Miskin:</span>
|
|
||||||
<strong id="stat-total-warga" style="color:white;">0</strong>
|
|
||||||
</div>
|
|
||||||
<div style="display:flex; justify-content:space-between;">
|
|
||||||
<span style="font-size:0.8rem;">Kavling Padat (>2 warga):</span>
|
|
||||||
<strong id="stat-kavling-merah" style="color:#EF4444;">0</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="form-container" style="display: none;"></div>
|
|
||||||
</div>
|
|
||||||
<div id="toast-container" class="toast-container"></div>
|
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
|
||||||
<script type="module" src="js/app.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import { initMap } from './modules/map.js';
|
|
||||||
import { setupDrawControls } from './modules/draw.js';
|
|
||||||
import { renderForm } from './modules/form.js';
|
|
||||||
import { spbuService, jalanService, kavlingService, rumahIbadahService, wargaMiskinService, BASE_URL } from './services/api.service.js';
|
|
||||||
|
|
||||||
let appMap;
|
|
||||||
let drawControl;
|
|
||||||
|
|
||||||
const statistikService = {
|
|
||||||
getChoropleth: async () => {
|
|
||||||
const res = await fetch(`${BASE_URL}/03/backend/api/statistik.php`);
|
|
||||||
const json = await res.json();
|
|
||||||
return json.data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createIcon = (svgPath, color) => L.divIcon({
|
|
||||||
className: 'custom-icon',
|
|
||||||
html: `<div style="background-color: ${color}; width: 34px; height: 34px; border-radius: 50%; display: flex; align-items: center; justify-content: center; box-shadow: 0 4px 10px rgba(0,0,0,0.5); border: 2px solid rgba(255,255,255,0.8); color: white;"><svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24">${svgPath}</svg></div>`,
|
|
||||||
iconSize: [34, 34], iconAnchor: [17, 34], popupAnchor: [0, -34]
|
|
||||||
});
|
|
||||||
|
|
||||||
const iconSPBU = (is24h) => createIcon('<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.242-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path>', is24h ? '#10B981' : '#EF4444');
|
|
||||||
const iconRumah = createIcon('<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>', '#8B5CF6');
|
|
||||||
const iconWarga = createIcon('<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', '#EF4444');
|
|
||||||
|
|
||||||
window.showToast = (msg, type='success') => {
|
|
||||||
const t = document.createElement('div'); t.className = `toast ${type}`; t.innerHTML = msg;
|
|
||||||
document.getElementById('toast-container').appendChild(t);
|
|
||||||
setTimeout(() => { t.style.opacity=0; setTimeout(()=>t.remove(),300); }, 3000);
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
appMap = initMap('map');
|
|
||||||
document.getElementById('btn-draw-rumah')?.addEventListener('click', () => { window.currentDrawType='rumah_ibadah'; new L.Draw.Marker(appMap).enable(); });
|
|
||||||
document.getElementById('btn-draw-warga')?.addEventListener('click', () => { window.currentDrawType='warga_miskin'; new L.Draw.Marker(appMap).enable(); });
|
|
||||||
drawControl = setupDrawControls(appMap, handleGeometryCreated);
|
|
||||||
await loadAllData();
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadAllData = async () => {
|
|
||||||
try {
|
|
||||||
const [spbu, jalan, choro, rumah, warga] = await Promise.all([
|
|
||||||
spbuService.getAll(), jalanService.getAll(), statistikService.getChoropleth(),
|
|
||||||
rumahIbadahService.getAll(), wargaMiskinService.getAll()
|
|
||||||
]);
|
|
||||||
|
|
||||||
L.geoJSON(spbu, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconSPBU(f.properties.buka_24_jam) }), onEachFeature: (f, l) => bindPopup(l, 'spbu', f.properties) }).addTo(appMap);
|
|
||||||
L.geoJSON(jalan, { style: { color: '#F59E0B', weight: 4 }, onEachFeature: (f, l) => bindPopup(l, 'jalan', f.properties) }).addTo(appMap);
|
|
||||||
L.geoJSON(rumah, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconRumah }), onEachFeature: (f, l) => bindPopup(l, 'rumah_ibadah', f.properties) }).addTo(appMap);
|
|
||||||
L.geoJSON(warga, { pointToLayer: (f, ll) => L.marker(ll, { icon: iconWarga }), onEachFeature: (f, l) => bindPopup(l, 'warga_miskin', f.properties) }).addTo(appMap);
|
|
||||||
|
|
||||||
let merahCount = 0;
|
|
||||||
L.geoJSON(choro, {
|
|
||||||
style: (f) => {
|
|
||||||
const count = f.properties.jumlah_warga_miskin;
|
|
||||||
let c = '#10B981'; // Hijau (0)
|
|
||||||
if(count > 0) c = '#F59E0B'; // Kuning (1-2)
|
|
||||||
if(count > 2) { c = '#EF4444'; merahCount++; } // Merah (>2)
|
|
||||||
return { color: c, fillColor: c, fillOpacity: 0.4, weight: 2 };
|
|
||||||
},
|
|
||||||
onEachFeature: (f, l) => bindPopup(l, 'kavling', f.properties)
|
|
||||||
}).addTo(appMap);
|
|
||||||
|
|
||||||
document.getElementById('stat-total-warga').innerText = warga.features.length;
|
|
||||||
document.getElementById('stat-kavling-merah').innerText = merahCount;
|
|
||||||
|
|
||||||
} catch (e) { window.showToast("Gagal meload data: "+e.message, 'error'); }
|
|
||||||
};
|
|
||||||
|
|
||||||
const bindPopup = (layer, type, props) => {
|
|
||||||
let ext = '';
|
|
||||||
if(type==='spbu') ext = `<p>24 Jam: <strong style="color:${props.buka_24_jam?'#10B981':'#EF4444'}">${props.buka_24_jam?'Ya':'Tidak'}</strong></p>`;
|
|
||||||
if(type==='kavling') ext = `<p>Jml Warga Miskin: <strong>${props.jumlah_warga_miskin||0} Jiwa</strong></p>`;
|
|
||||||
|
|
||||||
layer.bindPopup(`
|
|
||||||
<div class="popup-custom">
|
|
||||||
<h3>${props.nama}</h3>
|
|
||||||
<p>${props.deskripsi || ''}</p>
|
|
||||||
${ext}
|
|
||||||
<button class="btn-delete" onclick="window.deleteData('${type}', ${props.id})">Hapus</button>
|
|
||||||
</div>
|
|
||||||
`);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.deleteData = async (type, id) => {
|
|
||||||
if(!confirm('Yakin menghapus?')) return;
|
|
||||||
try {
|
|
||||||
if(type==='spbu') await spbuService.delete(id);
|
|
||||||
if(type==='jalan') await jalanService.delete(id);
|
|
||||||
if(type==='kavling') await kavlingService.delete(id);
|
|
||||||
if(type==='rumah_ibadah') await rumahIbadahService.delete(id);
|
|
||||||
if(type==='warga_miskin') await wargaMiskinService.delete(id);
|
|
||||||
window.showToast('Data dihapus'); setTimeout(()=>location.reload(), 800);
|
|
||||||
} catch(e) { window.showToast('Gagal hapus', 'error'); }
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGeometryCreated = (type, geometry, layer) => {
|
|
||||||
const tempLayer = L.geoJSON(geometry).addTo(appMap);
|
|
||||||
renderForm(type, geometry, async (payload) => {
|
|
||||||
try {
|
|
||||||
if(type==='spbu') await spbuService.create(payload);
|
|
||||||
if(type==='jalan') await jalanService.create(payload);
|
|
||||||
if(type==='kavling') await kavlingService.create(payload);
|
|
||||||
if(type==='rumah_ibadah') await rumahIbadahService.create(payload);
|
|
||||||
if(type==='warga_miskin') await wargaMiskinService.create(payload);
|
|
||||||
window.showToast('Tersimpan'); setTimeout(()=>location.reload(), 800);
|
|
||||||
} catch(e) { window.showToast('Gagal simpan', 'error'); }
|
|
||||||
}, () => appMap.removeLayer(tempLayer));
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
export const setupDrawControls = (map, onGeometryCreated) => {
|
|
||||||
const drawControl = new L.Control.Draw({
|
|
||||||
draw: { marker: true, polyline: true, polygon: true, circle: false, circlemarker: false, rectangle: false }
|
|
||||||
});
|
|
||||||
map.addControl(drawControl);
|
|
||||||
|
|
||||||
map.on(L.Draw.Event.CREATED, function (event) {
|
|
||||||
const layer = event.layer;
|
|
||||||
const type = event.layerType;
|
|
||||||
|
|
||||||
// Peta draw default tipe: marker -> spbu, polyline -> jalan, polygon -> kavling
|
|
||||||
let customType = type;
|
|
||||||
if(type === 'marker') customType = 'spbu';
|
|
||||||
if(type === 'polyline') customType = 'jalan';
|
|
||||||
if(type === 'polygon') customType = 'kavling';
|
|
||||||
if(window.currentDrawType) customType = window.currentDrawType; // Untuk P2/P3
|
|
||||||
|
|
||||||
onGeometryCreated(customType, layer.toGeoJSON().geometry, layer);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Custom Menu Handlers
|
|
||||||
const markerDrawer = new L.Draw.Marker(map, drawControl.options.draw.marker);
|
|
||||||
const polylineDrawer = new L.Draw.Polyline(map, drawControl.options.draw.polyline);
|
|
||||||
const polygonDrawer = new L.Draw.Polygon(map, drawControl.options.draw.polygon);
|
|
||||||
|
|
||||||
document.getElementById('btn-draw-spbu')?.addEventListener('click', () => { window.currentDrawType='spbu'; markerDrawer.enable(); });
|
|
||||||
document.getElementById('btn-draw-jalan')?.addEventListener('click', () => { window.currentDrawType='jalan'; polylineDrawer.enable(); });
|
|
||||||
document.getElementById('btn-draw-kavling')?.addEventListener('click', () => { window.currentDrawType='kavling'; polygonDrawer.enable(); });
|
|
||||||
|
|
||||||
return drawControl;
|
|
||||||
};
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
export const renderForm = (type, geometry, onSubmit, onCancel) => {
|
|
||||||
const container = document.getElementById('form-container');
|
|
||||||
container.style.display = 'flex';
|
|
||||||
|
|
||||||
const extraFields = type === 'spbu' ? `
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="checkbox-group">
|
|
||||||
<input type="checkbox" id="input-24jam">
|
|
||||||
<span>Buka 24 Jam</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
` : '';
|
|
||||||
|
|
||||||
container.innerHTML = `
|
|
||||||
<div class="form-panel">
|
|
||||||
<h3 style="margin:0 0 10px 0; font-size:1rem; border-bottom:1px solid rgba(255,255,255,0.1); padding-bottom:8px;">Simpan Data (${type.toUpperCase()})</h3>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="text" id="input-nama" class="form-control" placeholder="Nama / Label" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<input type="text" id="input-deskripsi" class="form-control" placeholder="Deskripsi Singkat">
|
|
||||||
</div>
|
|
||||||
${extraFields}
|
|
||||||
<div style="display:flex; gap:10px; margin-top:8px;">
|
|
||||||
<button id="btn-save" class="btn-submit" style="flex:1;">Simpan</button>
|
|
||||||
<button id="btn-cancel" class="btn-submit" style="background:rgba(255,255,255,0.1); color:var(--text-light); flex:1;">Batal</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.getElementById('btn-save').addEventListener('click', () => {
|
|
||||||
const payload = {
|
|
||||||
type,
|
|
||||||
geometry,
|
|
||||||
nama: document.getElementById('input-nama').value,
|
|
||||||
deskripsi: document.getElementById('input-deskripsi').value,
|
|
||||||
};
|
|
||||||
if (type === 'spbu') payload.buka_24_jam = document.getElementById('input-24jam').checked;
|
|
||||||
|
|
||||||
onSubmit(payload);
|
|
||||||
container.style.display = 'none';
|
|
||||||
container.innerHTML = '';
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btn-cancel').addEventListener('click', () => {
|
|
||||||
if(onCancel) onCancel();
|
|
||||||
container.style.display = 'none';
|
|
||||||
container.innerHTML = '';
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
/**
|
|
||||||
* map.js
|
|
||||||
* Tanggung Jawab: Inisialisasi peta Leaflet dasar dan base layer.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const initMap = (containerId) => {
|
|
||||||
// Pusat peta default (Universitas Tanjungpura, Pontianak)
|
|
||||||
const map = L.map(containerId, {
|
|
||||||
zoomControl: false
|
|
||||||
}).setView([-0.0583, 109.3448], 15);
|
|
||||||
|
|
||||||
// Pindahkan zoom control ke kanan bawah
|
|
||||||
L.control.zoom({
|
|
||||||
position: 'bottomright'
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
// Tile Layer Premium (CartoDB Dark Matter untuk Glass UI)
|
|
||||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
|
||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
|
||||||
subdomains: 'abcd',
|
|
||||||
maxZoom: 20
|
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
return map;
|
|
||||||
};
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
const getBaseUrl = () => {
|
|
||||||
const segments = window.location.pathname.split('/');
|
|
||||||
const targetIndex = segments.findIndex(seg => ['01', '02', '03', 'versi'].includes(seg));
|
|
||||||
if (targetIndex !== -1) {
|
|
||||||
return segments.slice(0, targetIndex).join('/');
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
export const BASE_URL = getBaseUrl();
|
|
||||||
|
|
||||||
const createService = (endpoint) => ({
|
|
||||||
getAll: async () => {
|
|
||||||
const res = await fetch(`${BASE_URL}/${endpoint}`);
|
|
||||||
const json = await res.json();
|
|
||||||
return json.data;
|
|
||||||
},
|
|
||||||
create: async (data) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/${endpoint}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
return json.data;
|
|
||||||
},
|
|
||||||
delete: async (id) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/${endpoint}?id=${id}`, {
|
|
||||||
method: 'DELETE'
|
|
||||||
});
|
|
||||||
const json = await res.json();
|
|
||||||
return json.data;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// P01
|
|
||||||
export const spbuService = createService('01/backend/api/spbu.php');
|
|
||||||
export const jalanService = createService('01/backend/api/jalan.php');
|
|
||||||
export const kavlingService = createService('01/backend/api/kavling.php');
|
|
||||||
|
|
||||||
// P02
|
|
||||||
export const rumahIbadahService = createService('02/backend/api/rumah_ibadah.php');
|
|
||||||
export const wargaMiskinService = createService('02/backend/api/warga_miskin.php');
|
|
||||||
export const haversineService = {
|
|
||||||
getDalamRadius: async (id, radius) => {
|
|
||||||
const res = await fetch(`${BASE_URL}/02/backend/api/haversine.php?rumah_ibadah_id=${id}&radius_km=${radius}`);
|
|
||||||
const json = await res.json();
|
|
||||||
return json.data;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { CONFIG } from '../config.js';
|
|
||||||
|
|
||||||
export const rumahIbadahService = {
|
|
||||||
getAll: async () => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/rumah_ibadah.php`);
|
|
||||||
return await res.json();
|
|
||||||
},
|
|
||||||
save: async (data) => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/rumah_ibadah.php`, {
|
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
return await res.json();
|
|
||||||
},
|
|
||||||
delete: async (id) => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/rumah_ibadah.php?id=${id}`, { method: 'DELETE' });
|
|
||||||
return await res.json();
|
|
||||||
},
|
|
||||||
getJangkauan: async (id, radius = 1) => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/rumah_ibadah.php?action=jangkauan&id=${id}&radius=${radius}`);
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export const CONFIG = {
|
|
||||||
BASE_URL: '/final/backend/api'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const statistikService = {
|
|
||||||
getKepadatan: async () => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/statistik.php`);
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { CONFIG } from '../config.js';
|
|
||||||
|
|
||||||
export const wargaMiskinService = {
|
|
||||||
getAll: async () => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/warga_miskin.php`);
|
|
||||||
return await res.json();
|
|
||||||
},
|
|
||||||
save: async (data) => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/warga_miskin.php`, {
|
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
return await res.json();
|
|
||||||
},
|
|
||||||
delete: async (id) => {
|
|
||||||
const res = await fetch(`${CONFIG.BASE_URL}/warga_miskin.php?id=${id}`, { method: 'DELETE' });
|
|
||||||
return await res.json();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user