Initial commit untuk UAS SIG

This commit is contained in:
z0rayy
2026-06-08 21:16:23 +07:00
commit c90ce9aedc
39 changed files with 7105 additions and 0 deletions
+255
View File
@@ -0,0 +1,255 @@
# WebGIS SPBU System - Feature Development Guide
## Status Fitur yang Sudah Dikembangkan
### ✅ Completed (Selesai)
#### 1. **Redesigned UI dengan Sidebar Navigation**
- Modern dashboard dengan sidebar menu di sebelah kiri
- Navigasi panel yang mudah antar fitur
- Responsive design untuk berbagai ukuran layar
- Professional color scheme tanpa emoji
**Files:**
- `index_new.html` - UI baru dengan sidebar (rename ke `index.html` untuk menggunakan)
- `js/app.js` - Main application logic
#### 2. **Dashboard & Analytics**
- Menampilkan total statistik data (SPBU, Jalan, Parsil, Ibadah, Miskin)
- Analytics cards dengan data counters
- Real-time update data badges
**Panel:** Analytics Dashboard
#### 3. **Data Management (Create, Read, Update, Delete)**
- ✅ Tabel management untuk setiap tipe data
- ✅ Modal forms untuk Add/Edit data
- ✅ Delete functionality dengan confirmation
- ✅ Real-time validation
**Backend Files:**
- `backend/create.php` - Create SPBU
- `backend/read.php` - Read SPBU
- `backend/update.php` - Update SPBU (NEW)
- `backend/delete.php` - Delete SPBU (NEW)
- `backend/create_jalan.php` - Create Jalan
- `backend/read_jalan.php` - Read Jalan
- `backend/update_jalan.php` - Update Jalan (NEW)
- `backend/delete_jalan.php` - Delete Jalan (NEW)
- `backend/create_parsil.php` - Create Parsil
- `backend/read_parsil.php` - Read Parsil
- `backend/update_parsil.php` - Update Parsil (NEW)
- `backend/delete_parsil.php` - Delete Parsil (NEW)
- `backend/create_ibadah.php` - Create Ibadah
- `backend/read_ibadah.php` - Read Ibadah
- `backend/update_ibadah.php` - Update Ibadah (NEW)
- `backend/delete_ibadah.php` - Delete Ibadah (NEW)
- `backend/create_miskin.php` - Create Miskin
- `backend/read_miskin.php` - Read Miskin
- `backend/update_miskin.php` - Update Miskin (NEW)
- `backend/delete_miskin.php` - Delete Miskin (NEW)
#### 4. **Advanced Search & Filter**
- Search input di setiap panel data
- Filter buttons untuk kategori spesifik
- Real-time table filtering
**Features:**
- SPBU: Filter by status (24 Jam / Tidak 24 Jam)
- Jalan: Filter by type (Nasional / Provinsi / Kabupaten)
- Parsil: Filter by type (SHM / HGB / HGU / HP)
- Global search functionality
#### 5. **Map Integration**
- Interactive Leaflet map dengan multiple basemaps
- Layer visualization untuk setiap tipe data
- Color-coded markers dan shapes
- Popup info saat klik marker
---
## 🚀 Cara Menggunakan Sistem Baru
### 1. **Setup**
```bash
# Buat database menggunakan database.sql
mysql -u root -p < database.sql
# File sudah ready di:
# - index_new.html (rename jadi index.html untuk use)
# - js/app.js
# - backend/update*.php, delete*.php files
```
### 2. **Access Application**
```
http://localhost/webgis-sig/index_new.html
```
### 3. **Navigasi Sidebar**
- **Dashboard Map** - Peta interaktif dengan semua layer
- **Analytics** - Dashboard dengan statistik data
- **SPBU / Jalan / Parsil / Ibadah / Pend. Miskin** - Data management panels
- **Search** - Advanced search & filter
- **Routing** - Find routes & nearest locations
- **Export** - Export/Import data
- **Account** - User settings
### 4. **Fitur-Fitur Utama**
#### Data Management
1. Klik panel di sidebar (e.g., "SPBU")
2. Gunakan search input untuk cari data
3. Gunakan filter buttons untuk kategori
4. Klik "Add" button untuk tambah data baru
5. Klik "Edit" atau "Delete" di action column
#### Dashboard
1. Pilih "Analytics" di sidebar
2. Lihat statistik real-time
3. Data auto-update saat ada perubahan
#### Map Visualization
1. Pilih "Dashboard Map" di sidebar
2. Lihat semua layer pada peta
3. Gunakan layer control (kanan bawah) untuk toggle layer
4. Klik marker untuk lihat detail
#### Search
1. Pilih "Search" di sidebar
2. Gunakan "Search by Keywords" untuk cari nama/lokasi
3. Gunakan "Search by Location" dengan radius slider
---
## 📋 API Endpoints
### SPBU
- POST `/backend/create.php` - Create SPBU
- GET `/backend/read.php` - Read all SPBU
- POST `/backend/update.php` - Update SPBU (NEW)
- POST `/backend/delete.php` - Delete SPBU (NEW)
### Jalan
- POST `/backend/create_jalan.php` - Create Jalan
- GET `/backend/read_jalan.php` - Read all Jalan
- POST `/backend/update_jalan.php` - Update Jalan (NEW)
- POST `/backend/delete_jalan.php` - Delete Jalan (NEW)
### Parsil
- POST `/backend/create_parsil.php` - Create Parsil
- GET `/backend/read_parsil.php` - Read all Parsil
- POST `/backend/update_parsil.php` - Update Parsil (NEW)
- POST `/backend/delete_parsil.php` - Delete Parsil (NEW)
### Ibadah
- POST `/backend/create_ibadah.php` - Create Ibadah
- GET `/backend/read_ibadah.php` - Read all Ibadah
- POST `/backend/update_ibadah.php` - Update Ibadah (NEW)
- POST `/backend/delete_ibadah.php` - Delete Ibadah (NEW)
### Miskin
- POST `/backend/create_miskin.php` - Create Miskin
- GET `/backend/read_miskin.php` - Read all Miskin
- POST `/backend/update_miskin.php` - Update Miskin (NEW)
- POST `/backend/delete_miskin.php` - Delete Miskin (NEW)
---
## 🔄 Fitur yang Masih Dalam Pengembangan
### ⏳ In Progress / Planned
1. **Advanced Map Features**
- Heatmap layer untuk kepadatan data
- Cluster markers untuk marker berdekatan
- Measure distance/area tool
- Advanced draw/annotation tools
2. **User Authentication**
- Login system
- User registration
- Role-based access control
- Password management
3. **Routing & Navigation**
- Find optimal route antar lokasi
- Nearest SPBU finder
- Distance/time estimation
- Navigation instructions
4. **Export & Import**
- Export to CSV, XLSX, GeoJSON, PDF
- Import data dari file (bulk upload)
- Data validation saat import
5. **Notifications & Alerts**
- Real-time notifications
- Data change alerts
- Duplicate data detection
6. **Mobile Responsive Design**
- Optimize untuk mobile/tablet
- Touch-friendly controls
- Responsive layout
---
## 🛠️ Development Notes
### Frontend Stack
- HTML5 / CSS3 (Custom responsive design)
- Vanilla JavaScript (ES6+)
- Leaflet.js - Map library
- Turf.js - Geospatial analysis
### Backend Stack
- PHP 7.4+
- MySQL/MariaDB
- RESTful API design
- JSON responses
### File Structure
```
webgis-sig/
├── index.html (original, akan diganti)
├── index_new.html (new UI dengan sidebar) ← GUNAKAN INI
├── database.sql
├── backend/
│ ├── koneksi.php
│ ├── create.php, read.php, update.php, delete.php (SPBU)
│ ├── create_jalan.php, read_jalan.php, update_jalan.php, delete_jalan.php (JALAN)
│ ├── create_parsil.php, read_parsil.php, update_parsil.php, delete_parsil.php (PARSIL)
│ ├── create_ibadah.php, read_ibadah.php, update_ibadah.php, delete_ibadah.php (IBADAH)
│ ├── create_miskin.php, read_miskin.php, update_miskin.php, delete_miskin.php (MISKIN)
│ └── [Future: auth.php, export.php, import.php, etc]
├── js/
│ ├── app.js (main logic) ← NEW
│ └── map.js (original, bisa dihapus)
└── test_*.php (for testing)
```
---
## ⚡ Next Steps
1. **Rename index_new.html ke index.html** (atau update server config)
2. **Test semua CRUD operations** untuk memastikan semuanya berjalan
3. **Lanjutkan fitur Authentication** untuk user management
4. **Implementasi Advanced Map Features** seperti heatmap & clustering
5. **Tambahkan Export/Import functionality**
---
## 📞 Support
Untuk issues atau pertanyaan, silakan check:
- Console browser (F12) untuk error messages
- Network tab untuk API request/response
- Server logs di XAMPP untuk PHP errors
---
**Last Updated:** May 2026
**Status:** Ongoing Development
**Version:** 2.0 (New UI & CRUD Features)
+2018
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
<?php
session_start();
header('Content-Type: application/json');
if (isset($_SESSION['user_id'])) {
echo json_encode(['status' => 'success', 'username' => $_SESSION['username']]);
} else {
http_response_code(401);
echo json_encode(['status' => 'error', 'message' => 'Unauthorized']);
}
+96
View File
@@ -0,0 +1,96 @@
<?php
// ============================================
// create.php — INSERT data SPBU
// ============================================
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
// Handle pre-flight request
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
// Hanya terima metode POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metode tidak diizinkan.']);
exit;
}
require_once __DIR__ . '/koneksi.php';
// Ambil body JSON dari request
$body = file_get_contents('php://input');
$data = json_decode($body, true);
if (!$data) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Body request tidak valid (bukan JSON).']);
exit;
}
// ---- Validasi field wajib ----
$required = ['nama', 'no_spbu', 'status', 'latitude', 'longitude'];
foreach ($required as $field) {
if (!isset($data[$field]) || trim((string)$data[$field]) === '') {
http_response_code(422);
echo json_encode(['success' => false, 'message' => "Field '$field' tidak boleh kosong."]);
exit;
}
}
$nama = trim($data['nama']);
$no_spbu = trim($data['no_spbu']);
$status = $data['status'];
$latitude = (float) $data['latitude'];
$longitude = (float) $data['longitude'];
// Validasi nilai enum status
if (!in_array($status, ['24jam', 'tidak'], true)) {
http_response_code(422);
echo json_encode(['success' => false, 'message' => "Nilai status tidak valid. Gunakan '24jam' atau 'tidak'."]);
exit;
}
// ---- Simpan ke database dengan prepared statement ----
$conn = getConnection();
$sql = "INSERT INTO spbu (nama, no_spbu, status, latitude, longitude) VALUES (?, ?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
if (!$stmt) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Gagal menyiapkan query: ' . $conn->error]);
$conn->close();
exit;
}
// s = string, d = double/float
$stmt->bind_param('sssdd', $nama, $no_spbu, $status, $latitude, $longitude);
if ($stmt->execute()) {
$newId = $stmt->insert_id;
http_response_code(201);
echo json_encode([
'success' => true,
'message' => 'Data SPBU berhasil disimpan.',
'data' => [
'id' => $newId,
'nama' => $nama,
'no_spbu' => $no_spbu,
'status' => $status,
'latitude' => $latitude,
'longitude' => $longitude,
],
]);
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Gagal menyimpan data: ' . $stmt->error]);
}
$stmt->close();
$conn->close();
+49
View File
@@ -0,0 +1,49 @@
<?php
// backend/create_ibadah.php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['success' => false, 'message' => 'Metode tidak diizinkan.']); exit; }
require_once __DIR__ . '/koneksi.php';
$body = file_get_contents('php://input');
$data = json_decode($body, true);
if (!$data) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Body request tidak valid.']); exit; }
$required = ['nama', 'alamat', 'latitude', 'longitude'];
foreach ($required as $field) {
if (!isset($data[$field]) || trim((string)$data[$field]) === '') {
http_response_code(422);
echo json_encode(['success' => false, 'message' => "Field '$field' wajib diisi."]);
exit;
}
}
$nama = trim($data['nama']);
$alamat = trim($data['alamat']);
$latitude = (float) $data['latitude'];
$longitude = (float) $data['longitude'];
$conn = getConnection();
$sql = "INSERT INTO rumah_ibadah (nama, alamat, latitude, longitude) VALUES (?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
if (!$stmt) { http_response_code(500); echo json_encode(['success' => false, 'message' => 'Query error: ' . $conn->error]); $conn->close(); exit; }
$stmt->bind_param('ssdd', $nama, $alamat, $latitude, $longitude);
if ($stmt->execute()) {
http_response_code(201);
echo json_encode([
'success' => true,
'message' => 'Data Rumah Ibadah disimpan.',
'data' => [ 'id' => $stmt->insert_id, 'nama' => $nama, 'alamat' => $alamat, 'latitude' => $latitude, 'longitude' => $longitude ]
]);
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Gagal menyimpan: ' . $stmt->error]);
}
$stmt->close();
$conn->close();
+91
View File
@@ -0,0 +1,91 @@
<?php
// ============================================
// create_jalan.php — INSERT data Jalan (Garis)
// ============================================
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metode tidak diizinkan.']);
exit;
}
require_once __DIR__ . '/koneksi.php';
$body = file_get_contents('php://input');
$data = json_decode($body, true);
if (!$data) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Body request tidak valid.']);
exit;
}
$required = ['nama_jalan', 'status', 'panjang', 'geom'];
foreach ($required as $field) {
if (!isset($data[$field])) {
http_response_code(422);
echo json_encode(['success' => false, 'message' => "Field '$field' wajib diisi."]);
exit;
}
if (!is_array($data[$field]) && trim((string)$data[$field]) === '') {
http_response_code(422);
echo json_encode(['success' => false, 'message' => "Field '$field' tidak boleh kosong."]);
exit;
}
}
$nama_jalan = trim($data['nama_jalan']);
$status = $data['status'];
$panjang = (float) $data['panjang'];
$geom = is_array($data['geom']) ? json_encode($data['geom']) : $data['geom'];
if (!in_array($status, ['Nasional', 'Provinsi', 'Kabupaten'], true)) {
http_response_code(422);
echo json_encode(['success' => false, 'message' => "Status jalan tidak valid."]);
exit;
}
$conn = getConnection();
$sql = "INSERT INTO jalan (nama_jalan, status, panjang, geom) VALUES (?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
if (!$stmt) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Gagal menyiapkan query: ' . $conn->error]);
$conn->close();
exit;
}
$stmt->bind_param('ssds', $nama_jalan, $status, $panjang, $geom);
if ($stmt->execute()) {
$newId = $stmt->insert_id;
http_response_code(201);
echo json_encode([
'success' => true,
'message' => 'Data Jalan berhasil disimpan.',
'data' => [
'id' => $newId,
'nama_jalan' => $nama_jalan,
'status' => $status,
'panjang' => $panjang,
'geom' => json_decode($geom, true),
],
]);
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Gagal menyimpan data: ' . $stmt->error]);
}
$stmt->close();
$conn->close();
+49
View File
@@ -0,0 +1,49 @@
<?php
// backend/create_miskin.php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode(['success' => false, 'message' => 'Metode tidak diizinkan.']); exit; }
require_once __DIR__ . '/koneksi.php';
$body = file_get_contents('php://input');
$data = json_decode($body, true);
if (!$data) { http_response_code(400); echo json_encode(['success' => false, 'message' => 'Body request tidak valid.']); exit; }
$required = ['nama_kk', 'latitude', 'longitude'];
foreach ($required as $field) {
if (!isset($data[$field]) || trim((string)$data[$field]) === '') {
http_response_code(422);
echo json_encode(['success' => false, 'message' => "Field '$field' wajib diisi."]);
exit;
}
}
$nama_kk = trim($data['nama_kk']);
$keterangan= isset($data['keterangan']) ? trim($data['keterangan']) : '';
$latitude = (float) $data['latitude'];
$longitude = (float) $data['longitude'];
$conn = getConnection();
$sql = "INSERT INTO penduduk_miskin (nama_kk, keterangan, latitude, longitude) VALUES (?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
if (!$stmt) { http_response_code(500); echo json_encode(['success' => false, 'message' => 'Query error: ' . $conn->error]); $conn->close(); exit; }
$stmt->bind_param('ssdd', $nama_kk, $keterangan, $latitude, $longitude);
if ($stmt->execute()) {
http_response_code(201);
echo json_encode([
'success' => true,
'message' => 'Data Penduduk Miskin disimpan.',
'data' => [ 'id' => $stmt->insert_id, 'nama_kk' => $nama_kk, 'keterangan' => $keterangan, 'latitude' => $latitude, 'longitude' => $longitude ]
]);
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Gagal menyimpan: ' . $stmt->error]);
}
$stmt->close();
$conn->close();
+91
View File
@@ -0,0 +1,91 @@
<?php
// ============================================
// create_parsil.php — INSERT data Parsil (Polygon)
// ============================================
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metode tidak diizinkan.']);
exit;
}
require_once __DIR__ . '/koneksi.php';
$body = file_get_contents('php://input');
$data = json_decode($body, true);
if (!$data) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Body request tidak valid.']);
exit;
}
$required = ['nama_pemilik', 'status', 'luas', 'geom'];
foreach ($required as $field) {
if (!isset($data[$field])) {
http_response_code(422);
echo json_encode(['success' => false, 'message' => "Field '$field' wajib diisi."]);
exit;
}
if (!is_array($data[$field]) && trim((string)$data[$field]) === '') {
http_response_code(422);
echo json_encode(['success' => false, 'message' => "Field '$field' tidak boleh kosong."]);
exit;
}
}
$nama_pemilik = trim($data['nama_pemilik']);
$status = $data['status'];
$luas = (float) $data['luas'];
$geom = is_array($data['geom']) ? json_encode($data['geom']) : $data['geom'];
if (!in_array($status, ['SHM', 'HGB', 'HGU', 'HP'], true)) {
http_response_code(422);
echo json_encode(['success' => false, 'message' => "Status parsil tidak valid."]);
exit;
}
$conn = getConnection();
$sql = "INSERT INTO parsil (nama_pemilik, status, luas, geom) VALUES (?, ?, ?, ?)";
$stmt = $conn->prepare($sql);
if (!$stmt) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Gagal menyiapkan query: ' . $conn->error]);
$conn->close();
exit;
}
$stmt->bind_param('ssds', $nama_pemilik, $status, $luas, $geom);
if ($stmt->execute()) {
$newId = $stmt->insert_id;
http_response_code(201);
echo json_encode([
'success' => true,
'message' => 'Data Parsil berhasil disimpan.',
'data' => [
'id' => $newId,
'nama_pemilik' => $nama_pemilik,
'status' => $status,
'luas' => $luas,
'geom' => json_decode($geom, true),
],
]);
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Gagal menyimpan data: ' . $stmt->error]);
}
$stmt->close();
$conn->close();
+18
View File
@@ -0,0 +1,18 @@
<?php
require 'koneksi.php';
$c = getConnection();
$sql = "CREATE TABLE IF NOT EXISTS users (
id INT NOT NULL AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;";
$c->query($sql);
$passwordHash = password_hash('admin123', PASSWORD_DEFAULT);
$insert = "INSERT IGNORE INTO users (username, password) VALUES ('admin', '$passwordHash')";
$c->query($insert);
echo 'Users table created and admin user inserted.';
+37
View File
@@ -0,0 +1,37 @@
<?php
// ============================================
// delete.php — Delete SPBU Data
// ============================================
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
exit;
}
$conn = getConnection();
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'ID is required']);
exit;
}
$id = intval($data['id']);
$stmt = $conn->prepare('DELETE FROM spbu WHERE id = ?');
$stmt->bind_param('i', $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'SPBU deleted successfully']);
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Failed to delete SPBU: ' . $conn->error]);
}
$stmt->close();
$conn->close();
?>
+37
View File
@@ -0,0 +1,37 @@
<?php
// ============================================
// delete_ibadah.php — Delete Rumah Ibadah Data
// ============================================
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
exit;
}
$conn = getConnection();
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'ID is required']);
exit;
}
$id = intval($data['id']);
$stmt = $conn->prepare('DELETE FROM rumah_ibadah WHERE id = ?');
$stmt->bind_param('i', $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Rumah Ibadah deleted successfully']);
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Failed to delete Rumah Ibadah: ' . $conn->error]);
}
$stmt->close();
$conn->close();
?>
+37
View File
@@ -0,0 +1,37 @@
<?php
// ============================================
// delete_jalan.php — Delete Jalan Data
// ============================================
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
exit;
}
$conn = getConnection();
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'ID is required']);
exit;
}
$id = intval($data['id']);
$stmt = $conn->prepare('DELETE FROM jalan WHERE id = ?');
$stmt->bind_param('i', $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Jalan deleted successfully']);
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Failed to delete Jalan: ' . $conn->error]);
}
$stmt->close();
$conn->close();
?>
+37
View File
@@ -0,0 +1,37 @@
<?php
// ============================================
// delete_miskin.php — Delete Penduduk Miskin Data
// ============================================
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
exit;
}
$conn = getConnection();
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'ID is required']);
exit;
}
$id = intval($data['id']);
$stmt = $conn->prepare('DELETE FROM penduduk_miskin WHERE id = ?');
$stmt->bind_param('i', $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Penduduk Miskin deleted successfully']);
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Failed to delete Penduduk Miskin: ' . $conn->error]);
}
$stmt->close();
$conn->close();
?>
+37
View File
@@ -0,0 +1,37 @@
<?php
// ============================================
// delete_parsil.php — Delete Parsil Data
// ============================================
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
exit;
}
$conn = getConnection();
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'ID is required']);
exit;
}
$id = intval($data['id']);
$stmt = $conn->prepare('DELETE FROM parsil WHERE id = ?');
$stmt->bind_param('i', $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Parsil deleted successfully']);
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Failed to delete Parsil: ' . $conn->error]);
}
$stmt->close();
$conn->close();
?>
+22
View File
@@ -0,0 +1,22 @@
<?php
// ============================================
// koneksi.php — Konfigurasi Database
// ============================================
define('DB_HOST', 'localhost');
define('DB_USER', 'root'); // Sesuaikan dengan user MySQL Anda
define('DB_PASS', ''); // Sesuaikan dengan password MySQL Anda
define('DB_NAME', 'webgis_spbu');
function getConnection(): mysqli {
$conn = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME);
if ($conn->connect_error) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Koneksi database gagal: ' . $conn->connect_error]);
exit;
}
$conn->set_charset('utf8mb4');
return $conn;
}
+41
View File
@@ -0,0 +1,41 @@
<?php
session_start();
header('Content-Type: application/json');
require 'koneksi.php';
$c = getConnection();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['status' => 'error', 'message' => 'Invalid request method']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
if (!$input && isset($_POST['username'])) {
$input = $_POST;
}
$username = $input['username'] ?? '';
$password = $input['password'] ?? '';
if (empty($username) || empty($password)) {
echo json_encode(['status' => 'error', 'message' => 'Username and password required']);
exit;
}
$stmt = $c->prepare("SELECT id, username, password FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$result = $stmt->get_result();
if ($row = $result->fetch_assoc()) {
if (password_verify($password, $row['password'])) {
// Success
$_SESSION['user_id'] = $row['id'];
$_SESSION['username'] = $row['username'];
echo json_encode(['status' => 'success', 'message' => 'Login successful']);
exit;
}
}
echo json_encode(['status' => 'error', 'message' => 'Invalid username or password']);
+5
View File
@@ -0,0 +1,5 @@
<?php
session_start();
session_destroy();
header('Content-Type: application/json');
echo json_encode(['status' => 'success', 'message' => 'Logged out']);
+50
View File
@@ -0,0 +1,50 @@
<?php
// ============================================
// read.php — SELECT semua data SPBU
// ============================================
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET');
// Hanya terima metode GET
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metode tidak diizinkan.']);
exit;
}
require_once __DIR__ . '/koneksi.php';
$conn = getConnection();
$sql = "SELECT id, nama, no_spbu, status, latitude, longitude FROM spbu ORDER BY created_at DESC";
$result = $conn->query($sql);
if (!$result) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Query gagal: ' . $conn->error]);
$conn->close();
exit;
}
$spbuList = [];
while ($row = $result->fetch_assoc()) {
$spbuList[] = [
'id' => (int) $row['id'],
'nama' => $row['nama'],
'no_spbu' => $row['no_spbu'],
'status' => $row['status'],
'latitude' => (float) $row['latitude'],
'longitude' => (float) $row['longitude'],
];
}
echo json_encode([
'success' => true,
'count' => count($spbuList),
'data' => $spbuList,
]);
$result->free();
$conn->close();
+21
View File
@@ -0,0 +1,21 @@
<?php
// backend/read_ibadah.php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
require_once __DIR__ . '/koneksi.php';
$conn = getConnection();
$result = $conn->query("SELECT id, nama, alamat, latitude, longitude FROM rumah_ibadah ORDER BY id DESC");
$data = [];
if ($result && $result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$row['latitude'] = (float) $row['latitude'];
$row['longitude'] = (float) $row['longitude'];
$data[] = $row;
}
}
echo json_encode(['success' => true, 'count' => count($data), 'data' => $data]);
$conn->close();
+47
View File
@@ -0,0 +1,47 @@
<?php
// ============================================
// read_jalan.php — SELECT semua data Jalan
// ============================================
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET');
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metode tidak diizinkan.']);
exit;
}
require_once __DIR__ . '/koneksi.php';
$conn = getConnection();
$sql = "SELECT id, nama_jalan, status, panjang, geom FROM jalan ORDER BY created_at DESC";
$result = $conn->query($sql);
if (!$result) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Query gagal: ' . $conn->error]);
$conn->close();
exit;
}
$jalanList = [];
while ($row = $result->fetch_assoc()) {
$jalanList[] = [
'id' => (int) $row['id'],
'nama_jalan' => $row['nama_jalan'],
'status' => $row['status'],
'panjang' => (float) $row['panjang'],
'geom' => json_decode($row['geom'], true),
];
}
echo json_encode([
'success' => true,
'count' => count($jalanList),
'data' => $jalanList,
]);
$result->free();
$conn->close();
+21
View File
@@ -0,0 +1,21 @@
<?php
// backend/read_miskin.php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
require_once __DIR__ . '/koneksi.php';
$conn = getConnection();
$result = $conn->query("SELECT id, nama_kk, keterangan, latitude, longitude FROM penduduk_miskin ORDER BY id DESC");
$data = [];
if ($result && $result->num_rows > 0) {
while ($row = $result->fetch_assoc()) {
$row['latitude'] = (float) $row['latitude'];
$row['longitude'] = (float) $row['longitude'];
$data[] = $row;
}
}
echo json_encode(['success' => true, 'count' => count($data), 'data' => $data]);
$conn->close();
+47
View File
@@ -0,0 +1,47 @@
<?php
// ============================================
// read_parsil.php — SELECT semua data Parsil
// ============================================
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET');
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Metode tidak diizinkan.']);
exit;
}
require_once __DIR__ . '/koneksi.php';
$conn = getConnection();
$sql = "SELECT id, nama_pemilik, status, luas, geom FROM parsil ORDER BY created_at DESC";
$result = $conn->query($sql);
if (!$result) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Query gagal: ' . $conn->error]);
$conn->close();
exit;
}
$parsilList = [];
while ($row = $result->fetch_assoc()) {
$parsilList[] = [
'id' => (int) $row['id'],
'nama_pemilik' => $row['nama_pemilik'],
'status' => $row['status'],
'luas' => (float) $row['luas'],
'geom' => json_decode($row['geom'], true),
];
}
echo json_encode([
'success' => true,
'count' => count($parsilList),
'data' => $parsilList,
]);
$result->free();
$conn->close();
+44
View File
@@ -0,0 +1,44 @@
<?php
header('Content-Type: application/json');
require 'koneksi.php';
$c = getConnection();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
echo json_encode(['status' => 'error', 'message' => 'Invalid request method']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
if (!$input && isset($_POST['username'])) {
$input = $_POST;
}
$username = $input['username'] ?? '';
$password = $input['password'] ?? '';
if (empty($username) || empty($password)) {
echo json_encode(['status' => 'error', 'message' => 'Username and password required']);
exit;
}
// Check if username already exists
$stmt = $c->prepare("SELECT id FROM users WHERE username = ?");
$stmt->bind_param("s", $username);
$stmt->execute();
$result = $stmt->get_result();
if ($result->fetch_assoc()) {
echo json_encode(['status' => 'error', 'message' => 'Username sudah digunakan']);
exit;
}
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
$stmt = $c->prepare("INSERT INTO users (username, password) VALUES (?, ?)");
$stmt->bind_param("ss", $username, $passwordHash);
if ($stmt->execute()) {
echo json_encode(['status' => 'success', 'message' => 'Registrasi berhasil, silakan login']);
} else {
echo json_encode(['status' => 'error', 'message' => 'Gagal mendaftar akun']);
}
+56
View File
@@ -0,0 +1,56 @@
<?php
// ============================================
// update.php — Update SPBU Data
// ============================================
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
exit;
}
$conn = getConnection();
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'ID is required']);
exit;
}
$id = intval($data['id']);
$nama = $data['nama'] ?? null;
$no_spbu = $data['no_spbu'] ?? null;
$status = $data['status'] ?? null;
$latitude = $data['latitude'] ?? null;
$longitude = $data['longitude'] ?? null;
$errors = [];
if (empty($nama)) $errors[] = 'Nama is required';
if (empty($no_spbu)) $errors[] = 'No SPBU is required';
if (empty($status)) $errors[] = 'Status is required';
if ($latitude === null) $errors[] = 'Latitude is required';
if ($longitude === null) $errors[] = 'Longitude is required';
if (!empty($errors)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => implode(', ', $errors)]);
exit;
}
$stmt = $conn->prepare('UPDATE spbu SET nama = ?, no_spbu = ?, status = ?, latitude = ?, longitude = ? WHERE id = ?');
$stmt->bind_param('sssddi', $nama, $no_spbu, $status, $latitude, $longitude, $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'SPBU updated successfully', 'data' => ['id' => $id]]);
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Failed to update SPBU: ' . $conn->error]);
}
$stmt->close();
$conn->close();
?>
+54
View File
@@ -0,0 +1,54 @@
<?php
// ============================================
// update_ibadah.php — Update Rumah Ibadah Data
// ============================================
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
exit;
}
$conn = getConnection();
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'ID is required']);
exit;
}
$id = intval($data['id']);
$nama = $data['nama'] ?? null;
$alamat = $data['alamat'] ?? null;
$latitude = $data['latitude'] ?? null;
$longitude = $data['longitude'] ?? null;
$errors = [];
if (empty($nama)) $errors[] = 'Nama is required';
if (empty($alamat)) $errors[] = 'Alamat is required';
if ($latitude === null) $errors[] = 'Latitude is required';
if ($longitude === null) $errors[] = 'Longitude is required';
if (!empty($errors)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => implode(', ', $errors)]);
exit;
}
$stmt = $conn->prepare('UPDATE rumah_ibadah SET nama = ?, alamat = ?, latitude = ?, longitude = ? WHERE id = ?');
$stmt->bind_param('ssddi', $nama, $alamat, $latitude, $longitude, $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Rumah Ibadah updated successfully', 'data' => ['id' => $id]]);
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Failed to update Rumah Ibadah: ' . $conn->error]);
}
$stmt->close();
$conn->close();
?>
+54
View File
@@ -0,0 +1,54 @@
<?php
// ============================================
// update_jalan.php — Update Jalan Data
// ============================================
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
exit;
}
$conn = getConnection();
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'ID is required']);
exit;
}
$id = intval($data['id']);
$nama_jalan = $data['nama_jalan'] ?? null;
$status = $data['status'] ?? null;
$panjang = $data['panjang'] ?? null;
$geom = $data['geom'] ?? null;
$errors = [];
if (empty($nama_jalan)) $errors[] = 'Nama Jalan is required';
if (empty($status)) $errors[] = 'Status is required';
if ($panjang === null) $errors[] = 'Panjang is required';
if (empty($geom)) $errors[] = 'Geometry is required';
if (!empty($errors)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => implode(', ', $errors)]);
exit;
}
$stmt = $conn->prepare('UPDATE jalan SET nama_jalan = ?, status = ?, panjang = ?, geom = ? WHERE id = ?');
$stmt->bind_param('ssdsi', $nama_jalan, $status, $panjang, $geom, $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Jalan updated successfully', 'data' => ['id' => $id]]);
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Failed to update Jalan: ' . $conn->error]);
}
$stmt->close();
$conn->close();
?>
+53
View File
@@ -0,0 +1,53 @@
<?php
// ============================================
// update_miskin.php — Update Penduduk Miskin Data
// ============================================
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
exit;
}
$conn = getConnection();
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'ID is required']);
exit;
}
$id = intval($data['id']);
$nama_kk = $data['nama_kk'] ?? null;
$keterangan = $data['keterangan'] ?? null;
$latitude = $data['latitude'] ?? null;
$longitude = $data['longitude'] ?? null;
$errors = [];
if (empty($nama_kk)) $errors[] = 'Nama KK is required';
if ($latitude === null) $errors[] = 'Latitude is required';
if ($longitude === null) $errors[] = 'Longitude is required';
if (!empty($errors)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => implode(', ', $errors)]);
exit;
}
$stmt = $conn->prepare('UPDATE penduduk_miskin SET nama_kk = ?, keterangan = ?, latitude = ?, longitude = ? WHERE id = ?');
$stmt->bind_param('ssddi', $nama_kk, $keterangan, $latitude, $longitude, $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Penduduk Miskin updated successfully', 'data' => ['id' => $id]]);
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Failed to update Penduduk Miskin: ' . $conn->error]);
}
$stmt->close();
$conn->close();
?>
+54
View File
@@ -0,0 +1,54 @@
<?php
// ============================================
// update_parsil.php — Update Parsil Data
// ============================================
require_once 'koneksi.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
exit;
}
$conn = getConnection();
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'ID is required']);
exit;
}
$id = intval($data['id']);
$nama_pemilik = $data['nama_pemilik'] ?? null;
$status = $data['status'] ?? null;
$luas = $data['luas'] ?? null;
$geom = $data['geom'] ?? null;
$errors = [];
if (empty($nama_pemilik)) $errors[] = 'Nama Pemilik is required';
if (empty($status)) $errors[] = 'Status is required';
if ($luas === null) $errors[] = 'Luas is required';
if (empty($geom)) $errors[] = 'Geometry is required';
if (!empty($errors)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => implode(', ', $errors)]);
exit;
}
$stmt = $conn->prepare('UPDATE parsil SET nama_pemilik = ?, status = ?, luas = ?, geom = ? WHERE id = ?');
$stmt->bind_param('ssdsi', $nama_pemilik, $status, $luas, $geom, $id);
if ($stmt->execute()) {
echo json_encode(['success' => true, 'message' => 'Parsil updated successfully', 'data' => ['id' => $id]]);
} else {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Failed to update Parsil: ' . $conn->error]);
}
$stmt->close();
$conn->close();
?>
+406
View File
@@ -0,0 +1,406 @@
const fs = require('fs');
const admin = fs.readFileSync('admin.html', 'utf8');
// Extract CSS
const cssMatch = admin.match(/<style>([\s\S]*?)<\/style>/);
const css = cssMatch ? cssMatch[1] : '';
// Extract Sidebar
const sidebarMatch = admin.match(/<aside class="sidebar">([\s\S]*?)<\/aside>/);
let sidebarBase = sidebarMatch ? sidebarMatch[0] : '';
// Function to build SPBU
function buildSPBU() {
let sidebar = sidebarBase;
sidebar = sidebar.replace(/<div class="sidebar-section-title">Data<\/div>[\s\S]*?<div class="sidebar-section">/, '<div class="sidebar-section">');
sidebar = sidebar.replace(/<div class="sidebar-section-title">Tools<\/div>[\s\S]*?<div class="sidebar-section">/, '<div class="sidebar-section">');
sidebar = sidebar.replace(/<div class="sidebar-item" data-panel="panel-dashboard">[\s\S]*?<\/div>/g, '');
sidebar = sidebar.replace(/<div class="sidebar-section-title">Settings<\/div>[\s\S]*?<\/nav>/, '<div class="sidebar-section-title">Account</div><div class="sidebar-menu"><a href="login.html" class="sidebar-item" style="text-decoration:none;"><div class="sidebar-item-icon">⎋</div><div class="sidebar-item-text">Login Admin</div></a></div></nav>');
const html = `<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Peta SPBU Publik</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
${css}
.controls-panel { position: absolute; top: 80px; right: 20px; background: white; padding: 16px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); z-index: 1000; width: 280px; color:#333; }
.controls-panel h3 { font-size: 14px; margin-bottom: 12px; color: #1e293b; }
.btn-action { display: block; width: 100%; padding: 10px; background: #3b82f6; color: white; border: none; border-radius: 6px; font-weight: 600; cursor: pointer; transition: 0.2s; margin-bottom: 10px; }
.btn-action:hover { background: #2563eb; }
.btn-action:disabled { background: #94a3b8; cursor: not-allowed; }
.info-box { background: #f1f5f9; padding: 12px; border-radius: 6px; font-size: 13px; color: #334155; margin-top: 10px; display: none; }
.info-box.active { display: block; }
.search-box2 { width: 100%; padding: 10px; margin-bottom: 15px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 13px; outline: none; }
.search-box2:focus { border-color: #3b82f6; }
.legend-panel { position: absolute; bottom: 30px; right: 20px; background: white; padding: 16px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); z-index: 1000; width: 200px; font-size: 13px; color: #334155; }
.legend-panel h3 { font-size: 14px; margin-bottom: 12px; color: #1e293b; }
.legend-item2 { display: flex; align-items: center; margin-bottom: 8px; }
.legend-icon2 { width: 16px; height: 16px; border-radius: 50%; margin-right: 10px; display: inline-block; }
.bg-blue { background: #3b82f6; }
.bg-red { background: #ef4444; }
</style>
</head>
<body>
<div class="app-wrapper">
${sidebar}
<div class="main-wrapper">
<header>
<div class="header-title">
<h1 id="page-title">Peta Persebaran SPBU</h1>
<div class="header-breadcrumb">
<span id="breadcrumb-text">Public Map</span>
</div>
</div>
</header>
<div class="content-container">
<div class="content-panel active" style="position: relative;">
<div id="map" style="width:100%; height:100%; z-index:1;"></div>
<div class="controls-panel">
<h3>Fitur SPBU Terdekat</h3>
<input type="text" id="search-spbu" class="search-box2" placeholder="Cari nama SPBU...">
<button id="btn-location" class="btn-action">Dapatkan Lokasi Saya</button>
<button id="btn-nearest" class="btn-action" disabled>Cari SPBU Terdekat</button>
<div id="info-box" class="info-box">
<strong>SPBU Terdekat:</strong> <span id="nearest-name">-</span><br>
<strong>Jarak:</strong> <span id="nearest-distance">-</span> km
</div>
</div>
<div class="legend-panel">
<h3>Legenda</h3>
<div class="legend-item2">
<span class="legend-icon2 bg-blue"></span> SPBU
</div>
<div class="legend-item2">
<span class="legend-icon2 bg-red"></span> Lokasi Anda
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js"></script>
<script>
// Sidebar toggle
document.getElementById('sidebar-toggle').addEventListener('click', () => {
document.querySelector('.sidebar').classList.toggle('collapsed');
});
// Init Map
const map = L.map('map').setView([-0.026, 109.342], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(map);
let spbuData = [];
let spbuLayer = L.layerGroup().addTo(map);
let userMarker = null;
let routeLine = null;
let userLocation = null;
const spbuIcon = L.icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-blue.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
});
const userIcon = L.icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
});
async function loadSPBU() {
try {
const res = await fetch('backend/read.php');
const result = await res.json();
if (result.status === 'success' && result.data) {
spbuData = result.data;
renderSPBU();
}
} catch (err) { console.error("Gagal memuat SPBU:", err); }
}
function renderSPBU(filterText = '') {
spbuLayer.clearLayers();
spbuData.forEach(spbu => {
if (filterText && !spbu.nama.toLowerCase().includes(filterText.toLowerCase())) return;
const lat = parseFloat(spbu.latitude);
const lng = parseFloat(spbu.longitude);
if (!isNaN(lat) && !isNaN(lng)) {
L.marker([lat, lng], {icon: spbuIcon})
.bindPopup('<strong>' + spbu.nama + '</strong><br>No: ' + spbu.no_spbu + '<br>Status: ' + spbu.status)
.addTo(spbuLayer);
}
});
}
document.getElementById('search-spbu').addEventListener('input', (e) => {
renderSPBU(e.target.value);
});
document.getElementById('btn-location').addEventListener('click', () => {
if (!navigator.geolocation) return alert("Geolocation tidak didukung");
const btn = document.getElementById('btn-location');
btn.textContent = "Mencari...";
navigator.geolocation.getCurrentPosition((pos) => {
const lat = pos.coords.latitude;
const lng = pos.coords.longitude;
userLocation = [lng, lat];
if (userMarker) map.removeLayer(userMarker);
userMarker = L.marker([lat, lng], {icon: userIcon}).bindPopup("Lokasi Anda").addTo(map);
map.flyTo([lat, lng], 14);
btn.textContent = "Lokasi Diperbarui";
document.getElementById('btn-nearest').disabled = false;
}, (err) => {
alert("Gagal: " + err.message);
btn.textContent = "Dapatkan Lokasi Saya";
});
});
document.getElementById('btn-nearest').addEventListener('click', () => {
if (!userLocation || spbuData.length === 0) return;
let nearestSPBU = null, shortestDistance = Infinity;
const userPoint = turf.point(userLocation);
spbuData.forEach(spbu => {
const lat = parseFloat(spbu.latitude);
const lng = parseFloat(spbu.longitude);
if (isNaN(lat) || isNaN(lng)) return;
const dist = turf.distance(userPoint, turf.point([lng, lat]), {units: 'kilometers'});
if (dist < shortestDistance) { shortestDistance = dist; nearestSPBU = spbu; }
});
if (nearestSPBU) {
if (routeLine) map.removeLayer(routeLine);
const lat = parseFloat(nearestSPBU.latitude);
const lng = parseFloat(nearestSPBU.longitude);
routeLine = L.polyline([[userLocation[1], userLocation[0]], [lat, lng]], { color: '#ef4444', weight: 4, dashArray: '10, 10' }).addTo(map);
map.fitBounds(routeLine.getBounds(), { padding: [50, 50] });
document.getElementById('nearest-name').textContent = nearestSPBU.nama;
document.getElementById('nearest-distance').textContent = shortestDistance.toFixed(2);
document.getElementById('info-box').classList.add('active');
}
});
loadSPBU();
</script>
</body>
</html>`;
fs.writeFileSync('spbu.html', html);
console.log("spbu.html updated");
}
// Function to build Poverty
function buildPoverty() {
let sidebar = sidebarBase;
sidebar = sidebar.replace(/<div class="sidebar-section-title">Data<\/div>[\s\S]*?<div class="sidebar-section">/, '<div class="sidebar-section">');
sidebar = sidebar.replace(/<div class="sidebar-section-title">Tools<\/div>[\s\S]*?<div class="sidebar-section">/, '<div class="sidebar-section">');
sidebar = sidebar.replace(/<div class="sidebar-item" data-panel="panel-dashboard">[\s\S]*?<\/div>/g, '');
sidebar = sidebar.replace(/<div class="sidebar-section-title">Settings<\/div>[\s\S]*?<\/nav>/, '<div class="sidebar-section-title">Account</div><div class="sidebar-menu"><a href="admin.html" class="sidebar-item" style="text-decoration:none;"><div class="sidebar-item-icon">⚙</div><div class="sidebar-item-text">Dashboard Admin</div></a><div class="sidebar-item" id="logout-btn"><div class="sidebar-item-icon">⎋</div><div class="sidebar-item-text">Logout</div></div></div></nav>');
const html = `<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Peta Kemiskinan & Rumah Ibadah</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
${css}
.controls-panel { position: absolute; top: 80px; right: 20px; background: white; padding: 16px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); z-index: 1000; width: 280px; color:#333; }
.controls-panel h3 { font-size: 14px; margin-bottom: 12px; color: #1e293b; }
.search-box2 { width: 100%; padding: 10px; margin-bottom: 10px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 13px; outline: none; }
.search-box2:focus { border-color: #3b82f6; }
.popup-slider { width: 100%; margin-top: 10px; }
.popup-radius-label { font-size: 12px; font-weight: 600; color: #334155; display: block; margin-top: 10px; }
.legend-panel { position: absolute; bottom: 30px; right: 20px; background: white; padding: 16px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.2); z-index: 1000; width: 250px; font-size: 13px; color: #334155; }
.legend-panel h3 { font-size: 14px; margin-bottom: 12px; color: #1e293b; }
.legend-item2 { display: flex; align-items: center; margin-bottom: 8px; }
.legend-icon2 { width: 16px; height: 16px; border-radius: 50%; margin-right: 10px; display: inline-block; }
.bg-black { background: #1e293b; }
.bg-red { background: #ef4444; }
.bg-green { background: #22c55e; }
#loader { position: fixed; top:0; left:0; width:100%; height:100%; background: #0f172a; color: white; display: flex; justify-content: center; align-items: center; z-index: 9999; font-size: 18px; }
</style>
</head>
<body>
<div id="loader">Verifikasi Akses...</div>
<div class="app-wrapper">
${sidebar}
<div class="main-wrapper">
<header>
<div class="header-title">
<h1 id="page-title">Peta Analisis Kemiskinan</h1>
<div class="header-breadcrumb">
<span id="breadcrumb-text" style="color:#fff; font-weight:normal;"></span>
</div>
</div>
</header>
<div class="content-container">
<div class="content-panel active" style="position: relative;">
<div id="map" style="width:100%; height:100%; z-index:1;"></div>
<div class="controls-panel">
<h3>Pencarian Data</h3>
<input type="text" id="search-ibadah" class="search-box2" placeholder="Cari Rumah Ibadah...">
<input type="text" id="search-miskin" class="search-box2" placeholder="Cari Penduduk Miskin (Nama KK)...">
</div>
<div class="legend-panel">
<h3>Legenda</h3>
<div class="legend-item2">
<span class="legend-icon2 bg-green"></span> Rumah Ibadah
</div>
<div class="legend-item2">
<span class="legend-icon2 bg-red"></span> Penduduk Miskin (Dalam Radius)
</div>
<div class="legend-item2">
<span class="legend-icon2 bg-black"></span> Penduduk Miskin (Luar Radius)
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js"></script>
<script>
// Sidebar toggle
document.getElementById('sidebar-toggle').addEventListener('click', () => {
document.querySelector('.sidebar').classList.toggle('collapsed');
});
async function checkAuth() {
try {
const res = await fetch('backend/check_auth.php');
if (!res.ok) { window.location.href = 'login.html?redirect=poverty.html'; return; }
const data = await res.json();
document.getElementById('breadcrumb-text').textContent = 'Halo, ' + data.username;
document.getElementById('loader').style.display = 'none';
initMap();
} catch (err) { window.location.href = 'login.html?redirect=poverty.html'; }
}
document.getElementById('logout-btn').addEventListener('click', async () => {
await fetch('backend/logout.php');
window.location.href = 'index.html';
});
// Global variables
let ibadahData = []; let miskinData = [];
let ibadahLayer; let miskinLayer; let map;
let ibadahRadiuses = {}; let ibadahPoints = [];
let ibadahIcon, miskinRedIcon, miskinBlackIcon;
async function initMap() {
map = L.map('map').setView([-0.026, 109.342], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(map);
ibadahLayer = L.layerGroup().addTo(map);
miskinLayer = L.layerGroup().addTo(map);
ibadahIcon = L.icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41] });
miskinRedIcon = L.icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41] });
miskinBlackIcon = L.icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-black.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41] });
try {
const [ibadahRes, miskinRes] = await Promise.all([ fetch('backend/read_ibadah.php'), fetch('backend/read_miskin.php') ]);
ibadahData = (await ibadahRes.json()).data || [];
miskinData = (await miskinRes.json()).data || [];
ibadahData.forEach(item => { ibadahRadiuses[item.id] = 0; });
renderMap();
} catch (err) { console.error("Error loading data"); }
}
window.updateRadius = function(id, value, lat, lng) {
ibadahRadiuses[id] = parseInt(value);
document.getElementById('radius-val-'+id).textContent = value;
renderBuffers();
renderMiskin();
};
function renderBuffers() {
map.eachLayer((layer) => { if (layer.options && layer.options.isBuffer) { map.removeLayer(layer); } });
ibadahData.forEach(item => {
const radius = ibadahRadiuses[item.id];
if (radius > 0) {
const lat = parseFloat(item.latitude), lng = parseFloat(item.longitude);
if (!isNaN(lat) && !isNaN(lng)) {
L.circle([lat, lng], { radius: radius, color: '#ef4444', fillColor: '#ef4444', fillOpacity: 0.1, weight: 2, isBuffer: true }).addTo(map);
}
}
});
}
function renderMap() { renderIbadah(); renderBuffers(); renderMiskin(); }
function renderIbadah() {
ibadahLayer.clearLayers(); ibadahPoints = [];
const filterIbadah = document.getElementById('search-ibadah').value.toLowerCase();
ibadahData.forEach(item => {
if (filterIbadah && !item.nama.toLowerCase().includes(filterIbadah)) return;
const lat = parseFloat(item.latitude), lng = parseFloat(item.longitude);
if (!isNaN(lat) && !isNaN(lng)) {
ibadahPoints.push({ id: item.id, pt: turf.point([lng, lat]) });
const popupContent = '<strong>' + item.nama + '</strong><br>' + item.alamat + '<br><span class="popup-radius-label">Atur Radius: <span id="radius-val-' + item.id + '">' + ibadahRadiuses[item.id] + '</span> m</span><input type="range" class="popup-slider" id="radius-slider-' + item.id + '" min="0" max="5000" step="100" value="' + ibadahRadiuses[item.id] + '" oninput="window.updateRadius(' + item.id + ', this.value, ' + lat + ', ' + lng + ')">';
L.marker([lat, lng], {icon: ibadahIcon}).bindPopup(popupContent).addTo(ibadahLayer);
}
});
}
function renderMiskin() {
miskinLayer.clearLayers();
const filterMiskin = document.getElementById('search-miskin').value.toLowerCase();
miskinData.forEach(item => {
if (filterMiskin && !item.nama_kk.toLowerCase().includes(filterMiskin)) return;
const lat = parseFloat(item.latitude), lng = parseFloat(item.longitude);
if (isNaN(lat) || isNaN(lng)) return;
const miskinPt = turf.point([lng, lat]);
let isWithinRadius = false;
for (const ibadahObj of ibadahPoints) {
const currentRadiusKm = ibadahRadiuses[ibadahObj.id] / 1000;
if (currentRadiusKm > 0) {
const distance = turf.distance(miskinPt, ibadahObj.pt, {units: 'kilometers'});
if (distance <= currentRadiusKm) { isWithinRadius = true; break; }
}
}
const iconToUse = isWithinRadius ? miskinRedIcon : miskinBlackIcon;
L.marker([lat, lng], {icon: iconToUse}).bindPopup('<strong>KK: ' + item.nama_kk + '</strong><br>' + (item.keterangan || '-') + '<br>Status: ' + (isWithinRadius ? 'Masuk Area Radius' : 'Luar Area Radius')).addTo(miskinLayer);
});
}
document.getElementById('search-ibadah').addEventListener('input', renderIbadah);
document.getElementById('search-miskin').addEventListener('input', renderMiskin);
checkAuth();
</script>
</body>
</html>`;
fs.writeFileSync('poverty.html', html);
console.log("poverty.html updated");
}
buildSPBU();
buildPoverty();
+80
View File
@@ -0,0 +1,80 @@
-- ============================================
-- WebGIS SPBU - Database Schema
-- ============================================
CREATE DATABASE IF NOT EXISTS webgis_spbu
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE webgis_spbu;
CREATE TABLE IF NOT EXISTS spbu (
id INT NOT NULL AUTO_INCREMENT,
nama VARCHAR(150) NOT NULL,
no_spbu VARCHAR(50) NOT NULL,
status ENUM('24jam', 'tidak') NOT NULL DEFAULT 'tidak',
latitude DECIMAL(10, 8) NOT NULL,
longitude DECIMAL(11, 8) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ============================================
-- Contoh data awal (opsional)
-- ============================================
INSERT INTO spbu (nama, no_spbu, status, latitude, longitude) VALUES
('SPBU Sudirman', '64.751.01', '24jam', -0.0260000, 109.3425000),
('SPBU Ahmad Yani', '64.751.02', 'tidak', -0.0310000, 109.3218000),
('SPBU Sutan Syahrir','64.751.03', '24jam', -0.0195000, 109.3612000);
-- ============================================
-- Tabel Jalan (Line)
-- ============================================
CREATE TABLE IF NOT EXISTS jalan (
id INT NOT NULL AUTO_INCREMENT,
nama_jalan VARCHAR(150) NOT NULL,
status ENUM('Nasional', 'Provinsi', 'Kabupaten') NOT NULL,
panjang DECIMAL(10, 2) NOT NULL, -- dalam meter
geom LONGTEXT NOT NULL, -- GeoJSON Feature atau Geometry LineString
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ============================================
-- Tabel Parsil Tanah (Polygon)
-- ============================================
CREATE TABLE IF NOT EXISTS parsil (
id INT NOT NULL AUTO_INCREMENT,
nama_pemilik VARCHAR(150) NOT NULL,
status ENUM('SHM', 'HGB', 'HGU', 'HP') NOT NULL,
luas DECIMAL(12, 2) NOT NULL, -- dalam meter persegi
geom LONGTEXT NOT NULL, -- GeoJSON Feature atau Geometry Polygon
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ============================================
-- Tabel Rumah Ibadah (Point)
-- ============================================
CREATE TABLE IF NOT EXISTS rumah_ibadah (
id INT NOT NULL AUTO_INCREMENT,
nama VARCHAR(150) NOT NULL,
alamat TEXT NOT NULL,
latitude DECIMAL(10, 8) NOT NULL,
longitude DECIMAL(11, 8) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- ============================================
-- Tabel Penduduk Miskin (Point)
-- ============================================
CREATE TABLE IF NOT EXISTS penduduk_miskin (
id INT NOT NULL AUTO_INCREMENT,
nama_kk VARCHAR(150) NOT NULL,
keterangan TEXT,
latitude DECIMAL(10, 8) NOT NULL,
longitude DECIMAL(11, 8) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+83
View File
@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebGIS Portal - SPBU & Kemiskinan</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Inter', sans-serif; background: #0f172a; color: #f1f5f9; min-height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; overflow: hidden; }
/* Background decorative */
.bg-pattern { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-image: radial-gradient(#334155 1px, transparent 1px); background-size: 40px 40px; opacity: 0.3; z-index: -1; }
.glow { position: absolute; width: 600px; height: 600px; background: radial-gradient(circle, rgba(59,130,246,0.15) 0%, rgba(15,23,42,0) 70%); top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: -1; pointer-events: none; }
header { text-align: center; margin-bottom: 60px; z-index: 10; }
header h1 { font-size: 42px; font-weight: 800; background: linear-gradient(135deg, #60a5fa, #a855f7); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 16px; }
header p { font-size: 18px; color: #cbd5e1; max-width: 600px; margin: 0 auto; line-height: 1.6; }
.cards-container { display: flex; gap: 40px; max-width: 1000px; width: 100%; padding: 0 20px; z-index: 10; flex-wrap: wrap; justify-content: center; }
.choice-card { background: rgba(30, 41, 59, 0.7); border: 1px solid #334155; border-radius: 20px; padding: 40px 30px; width: calc(50% - 20px); min-width: 300px; text-align: center; cursor: pointer; transition: all 0.3s ease; backdrop-filter: blur(10px); display: flex; flex-direction: column; align-items: center; text-decoration: none; color: inherit; }
.choice-card:hover { transform: translateY(-10px); border-color: #3b82f6; box-shadow: 0 20px 40px rgba(0,0,0,0.4), 0 0 20px rgba(59,130,246,0.2); }
.choice-card .icon-box { width: 80px; height: 80px; border-radius: 20px; display: flex; align-items: center; justify-content: center; font-size: 36px; margin-bottom: 24px; transition: all 0.3s ease; }
/* SPBU Card Styles */
.card-spbu:hover .icon-box { background: rgba(59, 130, 246, 0.2); color: #60a5fa; box-shadow: 0 0 15px rgba(59,130,246,0.5); }
.card-spbu .icon-box { background: rgba(59, 130, 246, 0.1); color: #3b82f6; }
/* Poverty Card Styles */
.card-poverty:hover .icon-box { background: rgba(239, 68, 68, 0.2); color: #f87171; box-shadow: 0 0 15px rgba(239,68,68,0.5); }
.card-poverty .icon-box { background: rgba(239, 68, 68, 0.1); color: #ef4444; }
.choice-card h2 { font-size: 24px; font-weight: 700; margin-bottom: 12px; color: #f8fafc; }
.choice-card p { font-size: 15px; color: #94a3b8; line-height: 1.6; margin-bottom: 24px; flex-grow: 1; }
.badge { display: inline-block; padding: 6px 12px; border-radius: 999px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
.badge-public { background: rgba(34, 197, 94, 0.1); color: #4ade80; border: 1px solid rgba(34, 197, 94, 0.2); }
.badge-private { background: rgba(245, 158, 11, 0.1); color: #fbbf24; border: 1px solid rgba(245, 158, 11, 0.2); }
.admin-link { position: absolute; bottom: 30px; color: #64748b; text-decoration: none; font-size: 14px; transition: color 0.2s; z-index: 10; }
.admin-link:hover { color: #f8fafc; }
@media (max-width: 768px) {
.choice-card { width: 100%; }
header h1 { font-size: 32px; }
}
</style>
</head>
<body>
<div class="bg-pattern"></div>
<div class="glow"></div>
<header>
<h1>Portal WebGIS Cerdas</h1>
<p>Pilih sistem informasi spasial yang ingin Anda akses. Jelajahi data geospasial dengan mudah dan interaktif.</p>
</header>
<div class="cards-container">
<!-- Opsi 1: SPBU -->
<a href="spbu.html" class="choice-card card-spbu">
<div class="icon-box"><i class="fa-solid fa-gas-pump"></i></div>
<h2>Peta SPBU Terdekat</h2>
<p>Jelajahi lokasi SPBU di sekitar Anda. Temukan SPBU terdekat dengan mudah menggunakan fitur deteksi lokasi otomatis tanpa perlu login.</p>
<span class="badge badge-public"><i class="fa-solid fa-globe mr-1"></i> Akses Publik</span>
</a>
<!-- Opsi 2: Poverty Map -->
<a href="login.html?redirect=poverty.html" class="choice-card card-poverty">
<div class="icon-box"><i class="fa-solid fa-house-chimney-crack"></i></div>
<h2>Peta Kemiskinan</h2>
<p>Sistem analisis spasial untuk mendeteksi rumah penduduk miskin dalam radius 500 meter dari fasilitas ibadah. Memerlukan autentikasi khusus.</p>
<span class="badge badge-private"><i class="fa-solid fa-lock mr-1"></i> Perlu Login</span>
</a>
</div>
<a href="login.html?redirect=admin.html" class="admin-link">Masuk sebagai Administrator</a>
</body>
</html>
+1172
View File
File diff suppressed because it is too large Load Diff
+882
View File
@@ -0,0 +1,882 @@
// ============================================
// map.js — Logic Peta WebGIS Terpadu
// ============================================
// ------- Konfigurasi endpoint backend -------
const API_READ_SPBU = 'backend/read.php';
const API_CREATE_SPBU = 'backend/create.php';
const API_READ_JALAN = 'backend/read_jalan.php';
const API_CREATE_JALAN= 'backend/create_jalan.php';
const API_READ_PARSIL = 'backend/read_parsil.php';
const API_CREATE_PARSIL='backend/create_parsil.php';
const API_READ_IBADAH = 'backend/read_ibadah.php';
const API_CREATE_IBADAH='backend/create_ibadah.php';
const API_READ_MISKIN = 'backend/read_miskin.php';
const API_CREATE_MISKIN='backend/create_miskin.php';
// ------- State Internal & Data Storage -------
window.appData = {
spbu: [],
jalan: [],
parsil: [],
ibadah: [],
miskin: []
};
// ------- Inisialisasi Basemap -------
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19, attribution: '&copy; OpenStreetMap'
});
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
maxZoom: 19, attribution: 'Tiles &copy; Esri'
});
const dark = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
maxZoom: 19, attribution: '&copy; CartoDB'
});
// ------- Inisialisasi Peta -------
const map = L.map('map', {
center: [-0.0260, 109.3425], // Pusat Kota Pontianak
zoom: 13,
zoomControl: true,
layers: [osm] // Default basemap
});
// Tambah Basemap & Overlay Switcher (Layer Control)
const baseMaps = {
"Peta Standar (OSM)": osm,
"Citra Satelit": satellite,
"Peta Gelap": dark
};
// Inisialisasi Layer Groups
const layerSpbu24 = L.layerGroup().addTo(map);
const layerSpbuTidak = L.layerGroup().addTo(map);
const layerJalan = L.layerGroup().addTo(map);
const layerParsil = L.layerGroup().addTo(map);
const layerIbadah = L.layerGroup().addTo(map);
const layerMiskin = L.layerGroup().addTo(map);
const overlays = {
"SPBU Buka 24 Jam": layerSpbu24,
"SPBU Tidak 24 Jam": layerSpbuTidak,
"Informasi Jalan": layerJalan,
"Parsil Tanah": layerParsil,
"Rumah Ibadah": layerIbadah,
"Penduduk Miskin": layerMiskin
};
L.control.layers(baseMaps, overlays, { position: 'bottomleft' }).addTo(map);
// ------- Tombol Geolocation (Lokasi Saya) -------
const LocateControl = L.Control.extend({
options: { position: 'topleft' },
onAdd: function (map) {
const container = L.DomUtil.create('div', 'leaflet-bar leaflet-control');
container.style.backgroundColor = 'white';
container.style.width = '34px';
container.style.height = '34px';
container.style.cursor = 'pointer';
container.style.display = 'flex';
container.style.justifyContent = 'center';
container.style.alignItems = 'center';
container.innerHTML = '<span style="font-size:18px;" title="Lokasi Saya">📍</span>';
container.onclick = function(e) {
e.stopPropagation();
showToast('Mencari lokasi...', 'success');
map.locate({ setView: true, maxZoom: 17, enableHighAccuracy: true });
}
return container;
}
});
map.addControl(new LocateControl());
let gpsMarker = null;
let gpsPulse = null;
map.on('locationfound', function(e) {
if (gpsMarker) map.removeLayer(gpsMarker);
if (gpsPulse) map.removeLayer(gpsPulse);
const pulseIcon = L.divIcon({ className: 'gps-pulse', iconSize: [40, 40] });
const dotIcon = L.divIcon({ className: 'gps-marker', iconSize: [16, 16] });
gpsPulse = L.marker(e.latlng, { icon: pulseIcon }).addTo(map);
gpsMarker = L.marker(e.latlng, { icon: dotIcon }).addTo(map);
showToast('Lokasi ditemukan!', 'success');
});
map.on('locationerror', function(e) {
showToast('Tidak dapat mengakses GPS.', 'error');
});
// ------- Mode State -------
let currentMode = 'spbu'; // 'spbu', 'jalan', 'parsil', 'ibadah', 'miskin'
let inputPopup = null;
let currentDrawnLayer = null; // layer temp saat menggambar
// Inisialisasi Leaflet-Geoman
map.pm.addControls({
position: 'topleft',
drawMarker: false,
drawCircleMarker: false,
drawPolyline: false,
drawRectangle: false,
drawPolygon: false,
drawCircle: false,
drawText: false,
editMode: false,
dragMode: false,
cutPolygon: false,
removalMode: true
});
// Transisi Mode UI
const btnSpbu = document.getElementById('btn-mode-spbu');
const btnJalan = document.getElementById('btn-mode-jalan');
const btnParsil = document.getElementById('btn-mode-parsil');
const btnIbadah = document.getElementById('btn-mode-ibadah');
const btnMiskin = document.getElementById('btn-mode-miskin');
const hintBar = document.getElementById('hint-bar');
function switchMode(mode) {
currentMode = mode;
btnSpbu.classList.toggle('active', mode === 'spbu');
btnJalan.classList.toggle('active', mode === 'jalan');
btnParsil.classList.toggle('active', mode === 'parsil');
btnIbadah.classList.toggle('active', mode === 'ibadah');
btnMiskin.classList.toggle('active', mode === 'miskin');
// Matikan PM Draw
map.pm.disableDraw();
if (inputPopup) map.closePopup(inputPopup);
if (currentDrawnLayer) {
map.removeLayer(currentDrawnLayer);
currentDrawnLayer = null;
}
if (mode === 'spbu') {
hintBar.innerHTML = '<span>Klik</span> pada peta untuk menambahkan SPBU baru';
} else if (mode === 'ibadah') {
hintBar.innerHTML = '<span>Klik</span> pada peta untuk menambahkan Rumah Ibadah';
} else if (mode === 'miskin') {
hintBar.innerHTML = '<span>Klik</span> pada peta untuk menambahkan Penduduk Miskin';
} else if (mode === 'jalan') {
hintBar.innerHTML = '<span>Klik start</span> lalu gambar garis jalan, klik titik terakhir untuk selesai';
map.pm.enableDraw('Line', { snappable: true, snapDistance: 20, pathOptions: { color: '#3b82f6', weight: 4 } });
} else if (mode === 'parsil') {
hintBar.innerHTML = '<span>Klik start</span> lalu gambar poligon, klik titik awal untuk selesai';
map.pm.enableDraw('Polygon', { snappable: true, snapDistance: 20, pathOptions: { color: '#3b82f6', weight: 2, fillColor: '#3b82f6', fillOpacity: 0.4 } });
}
}
btnSpbu.addEventListener('click', () => switchMode('spbu'));
btnJalan.addEventListener('click', () => switchMode('jalan'));
btnParsil.addEventListener('click', () => switchMode('parsil'));
btnIbadah.addEventListener('click', () => switchMode('ibadah'));
btnMiskin.addEventListener('click', () => switchMode('miskin'));
// ============================================
// SPBU LOGIC
// ============================================
let totalSpbu = 0;
function updateCounter(count) {
totalSpbu = count;
document.getElementById('spbu-count').textContent = totalSpbu;
}
function createSpbuIcon(status) {
const color = status === '24jam' ? '#22c55e' : '#f59e0b';
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 48" width="36" height="48">
<path d="M18 0C8.06 0 0 8.06 0 18c0 13.5 18 30 18 30s18-16.5 18-30C36 8.06 27.94 0 18 0z" fill="${color}" stroke="#fff" stroke-width="2"/>
<text x="18" y="22" text-anchor="middle" fill="#fff" font-family="Arial,sans-serif" font-size="11" font-weight="bold">SPBU</text>
</svg>`;
return L.divIcon({ html: svg, className: '', iconSize: [36, 48], iconAnchor: [18, 48], popupAnchor: [0, -50] });
}
function spbuBadge(status) {
return status === '24jam'
? '<span class="badge badge-active">24 Jam</span>'
: '<span class="badge badge-inactive">Tidak 24 Jam</span>';
}
function buildSpbuPopup(d) {
return `<div class="popup-info">
<div class="popup-title">${escapeHtml(d.nama)}</div>
<table class="popup-table">
<tr><td>No. SPBU</td><td>${escapeHtml(d.no_spbu)}</td></tr>
<tr><td>Status</td><td>${spbuBadge(d.status)}</td></tr>
<tr><td>Lat/Lng</td><td>${d.latitude}, ${d.longitude}</td></tr>
</table>
</div>`;
}
function addSpbuToMap(d) {
const marker = L.marker([d.latitude, d.longitude], { icon: createSpbuIcon(d.status) });
marker.bindPopup(buildSpbuPopup(d), { maxWidth: 260 });
if (d.status === '24jam') {
marker.addTo(layerSpbu24);
} else {
marker.addTo(layerSpbuTidak);
}
window.appData.spbu.push({ data: d, layer: marker });
}
// SPBU, IBADAH, MISKIN Map Click logic
map.on('click', async function (e) {
// Hide search results if clicking map
document.getElementById('search-results').classList.remove('show');
if (!['spbu', 'ibadah', 'miskin'].includes(currentMode)) return; // abaikan jika bukan point mode
if (inputPopup) map.closePopup(inputPopup);
const { lat, lng } = e.latlng;
let formHtml = '';
if (currentMode === 'spbu') {
formHtml = `
<div class="popup-form">
<div class="popup-form-title">Tambah SPBU Baru</div>
<form id="form-spbu" novalidate>
<div class="form-group">
<label>Nama SPBU</label>
<input id="f-nama" type="text" placeholder="cth. SPBU Sudirman" required />
</div>
<div class="form-group">
<label>Nomor SPBU</label>
<input id="f-no" type="text" placeholder="cth. 64.751.01" required />
</div>
<div class="form-group">
<label>Status</label>
<select id="f-status">
<option value="24jam">24 Jam</option>
<option value="tidak">Tidak 24 Jam</option>
</select>
</div>
<div class="form-actions">
<button type="button" id="btn-cancel" class="btn-cancel">Batal</button>
<button type="submit" id="btn-save" class="btn-save">Simpan</button>
</div>
</form>
</div>`;
} else if (currentMode === 'ibadah') {
showToast('Mendapatkan alamat otomatis...', 'success');
let autoAddress = '';
try {
const resp = await fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lng}`);
const jData = await resp.json();
if(jData && jData.display_name) autoAddress = jData.display_name;
} catch(err) { console.error(err); }
formHtml = `
<div class="popup-form">
<div class="popup-form-title">Tambah Rumah Ibadah</div>
<form id="form-ibadah" novalidate>
<div class="form-group">
<label>Nama Rumah Ibadah</label>
<input id="fi-nama" type="text" placeholder="cth. Masjid Raya" required />
</div>
<div class="form-group">
<label>Alamat Otomatis</label>
<input id="fi-alamat" type="text" value="${escapeHtml(autoAddress)}" required />
</div>
<div class="form-actions">
<button type="button" id="btn-cancel" class="btn-cancel">Batal</button>
<button type="submit" id="btn-save" class="btn-save">Simpan</button>
</div>
</form>
</div>`;
} else if (currentMode === 'miskin') {
formHtml = `
<div class="popup-form">
<div class="popup-form-title">Tambah Penduduk Miskin</div>
<form id="form-miskin" novalidate>
<div class="form-group">
<label>Nama Kepala Keluarga</label>
<input id="fm-nama" type="text" placeholder="cth. Bpk. Joni" required />
</div>
<div class="form-group">
<label>Keterangan Lengkap</label>
<input id="fm-ket" type="text" placeholder="cth. Rumah Papan, dll" />
</div>
<div class="form-actions">
<button type="button" id="btn-cancel" class="btn-cancel">Batal</button>
<button type="submit" id="btn-save" class="btn-save">Simpan</button>
</div>
</form>
</div>`;
}
inputPopup = L.popup({ maxWidth: 320, closeButton: false })
.setLatLng(e.latlng).setContent(formHtml).openOn(map);
setTimeout(() => {
document.getElementById('btn-cancel')?.addEventListener('click', () => map.closePopup(inputPopup));
// SPBU Handler
document.getElementById('form-spbu')?.addEventListener('submit', async (ev) => {
ev.preventDefault();
const payload = {
nama: document.getElementById('f-nama').value.trim(),
no_spbu: document.getElementById('f-no').value.trim(),
status: document.getElementById('f-status').value,
latitude: lat, longitude: lng
};
if(!payload.nama || !payload.no_spbu) return;
document.getElementById('btn-save').disabled = true;
try {
const res = await fetch(API_CREATE_SPBU, { method: 'POST', body: JSON.stringify(payload) });
const json = await res.json();
if (json.success) {
map.closePopup(inputPopup);
addSpbuToMap(json.data);
updateCounter(totalSpbu + 1);
showToast('SPBU berhasil ditambahkan!');
} else showToast(json.message, 'error');
} catch (err) { showToast('Error server', 'error'); }
});
// Ibadah Handler
document.getElementById('form-ibadah')?.addEventListener('submit', async (ev) => {
ev.preventDefault();
const payload = { nama: document.getElementById('fi-nama').value.trim(), alamat: document.getElementById('fi-alamat').value.trim(), latitude: lat, longitude: lng };
if(!payload.nama || !payload.alamat) return;
document.getElementById('btn-save').disabled = true;
try {
const res = await fetch(API_CREATE_IBADAH, { method: 'POST', body: JSON.stringify(payload) });
const json = await res.json();
if (json.success) {
map.closePopup(inputPopup); addIbadahToMap(json.data); showToast('Rumah Ibadah ditambahkan!');
} else showToast(json.message, 'error');
} catch (err) { showToast('Error server', 'error'); }
});
// Miskin Handler
document.getElementById('form-miskin')?.addEventListener('submit', async (ev) => {
ev.preventDefault();
const payload = { nama_kk: document.getElementById('fm-nama').value.trim(), keterangan: document.getElementById('fm-ket').value.trim(), latitude: lat, longitude: lng };
if(!payload.nama_kk) return;
document.getElementById('btn-save').disabled = true;
try {
const res = await fetch(API_CREATE_MISKIN, { method: 'POST', body: JSON.stringify(payload) });
const json = await res.json();
if (json.success) {
map.closePopup(inputPopup); addMiskinToMap(json.data); showToast('Data keluarga ditambahkan!');
} else showToast(json.message, 'error');
} catch (err) { showToast('Error server', 'error'); }
});
}, 0);
});
// ============================================
// IBADAH & MISKIN LOGIC (Radius Analysis)
// ============================================
function createIbadahIcon() {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 48" width="36" height="48">
<path d="M18 0C8.06 0 0 8.06 0 18c0 13.5 18 30 18 30s18-16.5 18-30C36 8.06 27.94 0 18 0z" fill="#8b5cf6" stroke="#fff" stroke-width="2"/>
<text x="18" y="22" text-anchor="middle" fill="#fff" font-family="Arial,sans-serif" font-size="11" font-weight="bold">🕌</text>
</svg>`;
return L.divIcon({ html: svg, className: '', iconSize: [36, 48], iconAnchor: [18, 48], popupAnchor: [0, -48] });
}
let activeRadiusCircle = null;
window.updateRadius = function(id, radius, lat, lng) {
document.getElementById(`radius-val-${id}`).innerText = `${radius} Meter`;
if (activeRadiusCircle) map.removeLayer(activeRadiusCircle);
activeRadiusCircle = L.circle([lat, lng], {
color: '#8b5cf6',
fillColor: '#8b5cf6',
fillOpacity: 0.2,
radius: parseInt(radius)
}).addTo(map);
let caughtCount = 0;
const centerPoint = turf.point([lng, lat]);
window.appData.miskin.forEach(m => {
const targetPoint = turf.point([m.data.longitude, m.data.latitude]);
const distance = turf.distance(centerPoint, targetPoint, { units: 'meters' });
if (distance <= radius) {
caughtCount++;
// Set point menjadi merah
m.layer.setStyle({ fillColor: '#ef4444', color: '#ef4444', radius: 8 });
} else {
// Kembali menjadi hitam
m.layer.setStyle({ fillColor: '#000000', color: '#000000', radius: 6 });
}
});
document.getElementById(`radius-res-${id}`).innerHTML = `<b>${caughtCount}</b> rumah keluarga miskin dalam radius.`;
};
// When popup closes, remove radius circle
map.on('popupclose', () => {
if (activeRadiusCircle) {
map.removeLayer(activeRadiusCircle);
activeRadiusCircle = null;
window.appData.miskin.forEach(m => {
m.layer.setStyle({ fillColor: '#000000', color: '#000000', radius: 6 });
});
}
});
function buildIbadahPopup(d) {
return `<div class="popup-info">
<div class="popup-title">${escapeHtml(d.nama)}</div>
<table class="popup-table">
<tr><td>Alamat</td><td style="white-space:normal; line-height:1.4;">${escapeHtml(d.alamat)}</td></tr>
</table>
<div class="radius-analysis-container">
<label style="font-size:.75rem; color:var(--clr-muted); margin-bottom:4px; display:block">Analisis Radius Kemiskinan</label>
<input type="range" id="radius-slider-${d.id}" min="100" max="5000" step="100" value="0"
oninput="updateRadius(${d.id}, this.value, ${d.latitude}, ${d.longitude})" />
<div style="display:flex; justify-content:space-between; font-size:.75rem;">
<span id="radius-val-${d.id}" class="radius-value">0 Meter</span>
<span id="radius-res-${d.id}" style="color:var(--clr-muted)">Geser slider</span>
</div>
</div>
</div>`;
}
function buildMiskinPopup(d) {
return `<div class="popup-info">
<div class="popup-title">Kepala Keluarga: ${escapeHtml(d.nama_kk)}</div>
<table class="popup-table">
<tr><td>Keterangan</td><td style="white-space:normal;">${escapeHtml(d.keterangan)}</td></tr>
</table>
</div>`;
}
function addIbadahToMap(d) {
const marker = L.marker([d.latitude, d.longitude], { icon: createIbadahIcon() });
marker.bindPopup(buildIbadahPopup(d), { maxWidth: 300, minWidth: 260 });
marker.addTo(layerIbadah);
window.appData.ibadah.push({ data: d, layer: marker });
}
function addMiskinToMap(d) {
// Gunakan L.circleMarker sebagai point sederhana (warna bawaan Hitam)
const marker = L.circleMarker([d.latitude, d.longitude], {
radius: 6,
fillColor: "#000000",
color: "#000000",
weight: 2,
opacity: 1,
fillOpacity: 0.8
});
marker.bindPopup(buildMiskinPopup(d), { maxWidth: 260 });
marker.addTo(layerMiskin);
window.appData.miskin.push({ data: d, layer: marker });
}
// ============================================
// JALAN & PARSIL DRAW LOGIC
// ============================================
// Pemetaan Warna
function getJalanColor(status) {
if(status === 'Nasional') return '#ef4444';
if(status === 'Provinsi') return '#eab308';
return '#10b981'; // Kabupaten
}
function getParsilStyle(status) {
if (status === 'SHM') return { color: '#2563eb', fillColor: '#3b82f6', fillOpacity: 0.5 };
if (status === 'HGB') return { color: '#7c3aed', fillColor: '#8b5cf6', fillOpacity: 0.5 };
if (status === 'HGU') return { color: '#db2777', fillColor: '#ec4899', fillOpacity: 0.5 };
return { color: '#0d9488', fillColor: '#14b8a6', fillOpacity: 0.5 }; // HP
}
// Leaflet Geoman event saat entitas selesai digambar
map.on('pm:create', (e) => {
currentDrawnLayer = e.layer;
const geom = currentDrawnLayer.toGeoJSON().geometry;
if (currentMode === 'jalan' && e.shape === 'Line') {
// Hitung panjang dalam meter via Turf.js
const lengthKm = turf.length(currentDrawnLayer.toGeoJSON());
const panjangMeter = (lengthKm * 1000).toFixed(2);
openJalanForm(e.layer, geom, panjangMeter);
// Refresh mode drawing
map.pm.enableDraw('Line');
}
else if (currentMode === 'parsil' && e.shape === 'Polygon') {
// Hitung luas dalam meter persegi via Turf.js
const areaSqMeters = turf.area(currentDrawnLayer.toGeoJSON());
const luasMeter = areaSqMeters.toFixed(2);
openParsilForm(e.layer, geom, luasMeter);
// Refresh mode drawing
map.pm.enableDraw('Polygon');
}
});
function openJalanForm(layer, geom, panjang) {
const centerLatlng = layer.getBounds().getCenter();
const formHtml = `
<div class="popup-form">
<div class="popup-form-title">Tambah Data Jalan</div>
<form id="form-jalan" novalidate>
<div class="form-group">
<label>Nama Jalan</label>
<input id="fj-nama" type="text" placeholder="cth. Jl. Ahmad Yani" required />
</div>
<div class="form-group">
<label>Status Jalan</label>
<select id="fj-status">
<option value="Nasional">Nasional</option>
<option value="Provinsi">Provinsi</option>
<option value="Kabupaten">Kabupaten</option>
</select>
</div>
<div class="form-group">
<label>Panjang (otomatis)</label>
<input type="text" value="${panjang} Meter" readonly />
</div>
<div class="form-actions">
<button type="button" id="btn-cancel-draw" class="btn-cancel">Batal</button>
<button type="submit" id="btn-save-draw" class="btn-save">Simpan</button>
</div>
</form>
</div>`;
inputPopup = L.popup({ maxWidth: 320, closeButton: false })
.setLatLng(centerLatlng).setContent(formHtml).openOn(map);
setTimeout(() => {
document.getElementById('btn-cancel-draw')?.addEventListener('click', () => {
map.closePopup(inputPopup);
map.removeLayer(layer);
});
document.getElementById('form-jalan')?.addEventListener('submit', async (ev) => {
ev.preventDefault();
const payload = {
nama_jalan: document.getElementById('fj-nama').value.trim(),
status: document.getElementById('fj-status').value,
panjang: panjang,
geom: geom
};
if(!payload.nama_jalan) return;
document.getElementById('btn-save-draw').disabled = true;
try {
const res = await fetch(API_CREATE_JALAN, { method: 'POST', body: JSON.stringify(payload) });
const json = await res.json();
if (json.success) {
map.closePopup(inputPopup);
map.removeLayer(layer);
currentDrawnLayer = null;
addJalanToMap(json.data);
showToast('Jalan berhasil ditambahkan!');
} else showToast(json.message, 'error');
} catch (e) { showToast('Error server', 'error'); }
document.getElementById('btn-save-draw').disabled = false;
});
}, 0);
}
function openParsilForm(layer, geom, luas) {
const centerLatlng = layer.getBounds().getCenter();
const formHtml = `
<div class="popup-form">
<div class="popup-form-title">Tambah Parsil Tanah</div>
<form id="form-parsil" novalidate>
<div class="form-group">
<label>Nama Pemilik</label>
<input id="fp-nama" type="text" placeholder="cth. Budi Santoso" required />
</div>
<div class="form-group">
<label>Status Kepemilikan</label>
<select id="fp-status">
<option value="SHM">Sertifikat Hak Milik (SHM)</option>
<option value="HGB">Hak Guna Bangunan (HGB)</option>
<option value="HGU">Hak Guna Usaha (HGU)</option>
<option value="HP">Hak Pakai (HP)</option>
</select>
</div>
<div class="form-group">
<label>Luas (otomatis)</label>
<input type="text" value="${luas} m²" readonly />
</div>
<div class="form-actions">
<button type="button" id="btn-cancel-draw" class="btn-cancel">Batal</button>
<button type="submit" id="btn-save-draw" class="btn-save">Simpan</button>
</div>
</form>
</div>`;
inputPopup = L.popup({ maxWidth: 320, closeButton: false })
.setLatLng(centerLatlng).setContent(formHtml).openOn(map);
setTimeout(() => {
document.getElementById('btn-cancel-draw')?.addEventListener('click', () => {
map.closePopup(inputPopup);
map.removeLayer(layer);
});
document.getElementById('form-parsil')?.addEventListener('submit', async (ev) => {
ev.preventDefault();
const payload = {
nama_pemilik: document.getElementById('fp-nama').value.trim(),
status: document.getElementById('fp-status').value,
luas: luas,
geom: geom
};
if(!payload.nama_pemilik) return;
document.getElementById('btn-save-draw').disabled = true;
try {
const res = await fetch(API_CREATE_PARSIL, { method: 'POST', body: JSON.stringify(payload) });
const json = await res.json();
if (json.success) {
map.closePopup(inputPopup);
map.removeLayer(layer);
currentDrawnLayer = null;
addParsilToMap(json.data);
showToast('Parsil berhasil ditambahkan!');
} else showToast(json.message, 'error');
} catch (e) { showToast('Error server', 'error'); }
document.getElementById('btn-save-draw').disabled = false;
});
}, 0);
}
// Popups Data Terload
function buildJalanPopup(d) {
return `<div class="popup-info">
<div class="popup-title">${escapeHtml(d.nama_jalan)}</div>
<table class="popup-table">
<tr><td>Status</td><td><span class="badge" style="background:#475569;color:#fff">${d.status}</span></td></tr>
<tr><td>Panjang</td><td>${d.panjang} m</td></tr>
</table>
</div>`;
}
function buildParsilPopup(d) {
return `<div class="popup-info">
<div class="popup-title">Pemilik: ${escapeHtml(d.nama_pemilik)}</div>
<table class="popup-table">
<tr><td>Sertifikat</td><td><span class="badge" style="background:#475569;color:#fff">${d.status}</span></td></tr>
<tr><td>Luas</td><td>${d.luas} m²</td></tr>
</table>
</div>`;
}
// Add GeoJSON to Layer
function addJalanToMap(d) {
const layer = L.geoJSON(d.geom, {
style: { color: getJalanColor(d.status), weight: 5, opacity: 0.9 }
}).bindPopup(buildJalanPopup(d)).addTo(layerJalan);
window.appData.jalan.push({ data: d, layer: layer });
}
// ------ Fitur Hover Info Parsil ------
function highlightParsil(e) {
const layer = e.target;
layer.setStyle({
weight: 4,
color: '#fff',
dashArray: '',
fillOpacity: 0.8
});
layer.bringToFront();
}
function resetParsilHighlight(e) {
const layer = e.target;
const status = layer.featureProps.status;
layer.setStyle(getParsilStyle(status));
}
function addParsilToMap(d) {
const layerGroup = L.geoJSON(d.geom, {
style: getParsilStyle(d.status),
onEachFeature: function(feature, layer) {
layer.featureProps = d; // Simpan properti referensi ke dalam layer
const tooltipContent = `
<div class="info-hover">
<h4>Informasi Parsil Tanah</h4>
<b>Pemilik: ${escapeHtml(d.nama_pemilik)}</b><br />
Sertifikat: <span class="badge" style="background:#475569;color:#fff">${d.status}</span><br />
Luas: ${d.luas}
</div>`;
layer.bindTooltip(tooltipContent, {
sticky: true, // Tooltip bergeser mengekori kursor kursor
direction: 'top',
className: 'custom-tooltip'
});
layer.on({
mouseover: highlightParsil,
mouseout: resetParsilHighlight
});
}
});
layerGroup.bindPopup(buildParsilPopup(d)).addTo(layerParsil);
window.appData.parsil.push({ data: d, layer: layerGroup });
}
// ============================================
// LOGIKA PENCARIAN (SEARCH)
// ============================================
const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');
const filterBtns = document.querySelectorAll('.filter-btn');
let currentSearchFilter = 'all';
filterBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
filterBtns.forEach(b => b.classList.remove('active'));
e.target.classList.add('active');
currentSearchFilter = e.target.dataset.filter;
performSearch(searchInput.value);
});
});
searchInput.addEventListener('input', (e) => performSearch(e.target.value));
// Tambahkan event focus jika user kembali nge-klik kolom search yg sudah ada isinya
searchInput.addEventListener('focus', (e) => performSearch(e.target.value));
function performSearch(query) {
query = query.toLowerCase().trim();
searchResults.innerHTML = '';
if (!query) {
searchResults.classList.remove('show');
return;
}
let results = [];
// Filter SPBU
if (currentSearchFilter === 'all' || currentSearchFilter === 'spbu') {
appData.spbu.forEach(item => {
if (item.data.nama.toLowerCase().includes(query) || item.data.no_spbu.toLowerCase().includes(query)) {
results.push({ type: 'SPBU', title: item.data.nama, meta: `No: ${item.data.no_spbu}`, item: item });
}
});
}
// Filter Jalan
if (currentSearchFilter === 'all' || currentSearchFilter === 'jalan') {
appData.jalan.forEach(item => {
if (item.data.nama_jalan.toLowerCase().includes(query)) {
results.push({ type: 'Jalan', title: item.data.nama_jalan, meta: `${item.data.status} · ${item.data.panjang}m`, item: item });
}
});
}
// Filter Parsil
if (currentSearchFilter === 'all' || currentSearchFilter === 'parsil') {
appData.parsil.forEach(item => {
if (item.data.nama_pemilik.toLowerCase().includes(query)) {
results.push({ type: 'Parsil', title: item.data.nama_pemilik, meta: `Sertifikat: ${item.data.status}`, item: item });
}
});
}
if (results.length === 0) {
searchResults.innerHTML = '<div class="search-result-item"><div class="result-title" style="color:var(--clr-muted)">Tidak ada hasil pemetaan</div></div>';
} else {
results.forEach(r => {
const div = document.createElement('div');
div.className = 'search-result-item';
div.innerHTML = `
<div class="result-title">${escapeHtml(r.title)}</div>
<div class="result-meta"><span><b>${r.type}</b></span> <span>${escapeHtml(r.meta)}</span></div>
`;
div.addEventListener('click', () => zoomToResult(r));
searchResults.appendChild(div);
});
}
searchResults.classList.add('show');
}
function zoomToResult(result) {
searchResults.classList.remove('show');
searchInput.value = ''; // Opsional: clear search box saat dipilih
const layer = result.item.layer;
if (result.type === 'SPBU') {
// FlyTo SPBU marker
map.flyTo(layer.getLatLng(), 18, { duration: 1.5 });
// Buka popup saat flyTo selesai (mendekati target)
setTimeout(() => layer.openPopup(), 1000);
} else {
// GeoJSON Layers berbentuk grup (bisa lebih dari satu poligon/garis)
const bounds = layer.getBounds();
map.flyToBounds(bounds, { duration: 1.5, maxZoom: 18 });
setTimeout(() => layer.openPopup(), 1000);
}
}
// ============================================
// DATA LOADER
// ============================================
async function loadAllData() {
try {
// Load SPBU
const r1 = await fetch(API_READ_SPBU);
const j1 = await r1.json();
if(j1.success) { j1.data.forEach(d => addSpbuToMap(d)); updateCounter(j1.count); }
// Load Jalan
const r2 = await fetch(API_READ_JALAN);
const j2 = await r2.json();
if(j2.success) j2.data.forEach(d => addJalanToMap(d));
// Load Parsil
const r3 = await fetch(API_READ_PARSIL);
const j3 = await r3.json();
if(j3.success) j3.data.forEach(d => addParsilToMap(d));
// Load Ibadah
const r4 = await fetch(API_READ_IBADAH);
const j4 = await r4.json();
if(j4.success) j4.data.forEach(d => addIbadahToMap(d));
// Load Miskin
const r5 = await fetch(API_READ_MISKIN);
const j5 = await r5.json();
if(j5.success) j5.data.forEach(d => addMiskinToMap(d));
} catch (err) {
showToast('Gagal memuat beberapa data.', 'error');
}
}
// ------- Helpers -------
function showToast(message, type = 'success') {
const container = document.getElementById('toast-container');
const t = document.createElement('div');
t.className = `toast toast-${type}`;
t.innerHTML = `<span class="toast-icon">${type === 'success' ? '✓' : '✕'}</span><span>${message}</span>`;
container.appendChild(t);
requestAnimationFrame(() => t.classList.add('toast-show'));
setTimeout(() => {
t.classList.remove('toast-show');
t.addEventListener('transitionend', () => t.remove(), { once: true });
}, 3000);
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// Start
loadAllData();
+166
View File
@@ -0,0 +1,166 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - WebGIS SPBU & Kemiskinan</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Inter', sans-serif; background: #0f172a; color: #f1f5f9; display: flex; align-items: center; justify-content: center; height: 100vh; }
.login-container { background: #1e293b; padding: 40px; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); width: 100%; max-width: 400px; position: relative; }
.login-container h2 { margin-bottom: 24px; text-align: center; color: #3b82f6; }
.form-group { margin-bottom: 20px; }
.form-group label { display: block; margin-bottom: 8px; font-size: 14px; color: #cbd5e1; }
.form-group input { width: 100%; padding: 12px; border: 1px solid #334155; border-radius: 8px; background: #0f172a; color: #f1f5f9; font-size: 14px; outline: none; }
.form-group input:focus { border-color: #3b82f6; }
.btn { width: 100%; padding: 12px; background: #3b82f6; color: white; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; transition: 0.2s; }
.btn:hover { background: #2563eb; }
.btn-success { background: #22c55e; }
.btn-success:hover { background: #16a34a; }
.error-message, .success-message { font-size: 14px; margin-bottom: 16px; text-align: center; display: none; padding: 10px; border-radius: 6px; }
.error-message { background: rgba(239, 68, 68, 0.1); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.2); }
.success-message { background: rgba(34, 197, 94, 0.1); color: #4ade80; border: 1px solid rgba(34, 197, 94, 0.2); }
.back-link { display: block; text-align: center; margin-top: 20px; color: #94a3b8; text-decoration: none; font-size: 14px; }
.back-link:hover { color: #f1f5f9; }
.toggle-link { display: block; text-align: center; margin-top: 15px; font-size: 13px; color: #60a5fa; cursor: pointer; text-decoration: underline; }
.form-section { display: none; }
.form-section.active { display: block; }
</style>
</head>
<body>
<div class="login-container">
<div id="error-message" class="error-message"></div>
<div id="success-message" class="success-message"></div>
<!-- Login Form -->
<div id="login-section" class="form-section active">
<h2>Login Sistem</h2>
<form id="login-form">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" required placeholder="Masukkan username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" required placeholder="Masukkan password">
</div>
<button type="submit" class="btn">Login</button>
</form>
<span class="toggle-link" onclick="toggleForm('register')">Belum punya akun? Daftar di sini</span>
</div>
<!-- Register Form -->
<div id="register-section" class="form-section">
<h2>Daftar Akun Baru</h2>
<form id="register-form">
<div class="form-group">
<label for="reg-username">Username Baru</label>
<input type="text" id="reg-username" required placeholder="Pilih username">
</div>
<div class="form-group">
<label for="reg-password">Password Baru</label>
<input type="password" id="reg-password" required placeholder="Buat password">
</div>
<button type="submit" class="btn btn-success">Daftar</button>
</form>
<span class="toggle-link" onclick="toggleForm('login')">Sudah punya akun? Login di sini</span>
</div>
<a href="index.html" class="back-link">← Kembali ke Beranda</a>
</div>
<script>
function toggleForm(type) {
document.getElementById('error-message').style.display = 'none';
document.getElementById('success-message').style.display = 'none';
document.getElementById('login-section').classList.remove('active');
document.getElementById('register-section').classList.remove('active');
if (type === 'login') {
document.getElementById('login-section').classList.add('active');
} else {
document.getElementById('register-section').classList.add('active');
}
}
function showMessage(type, msg) {
const errDiv = document.getElementById('error-message');
const succDiv = document.getElementById('success-message');
errDiv.style.display = 'none';
succDiv.style.display = 'none';
if (type === 'error') {
errDiv.textContent = msg;
errDiv.style.display = 'block';
} else {
succDiv.textContent = msg;
succDiv.style.display = 'block';
}
}
// Login Handler
document.getElementById('login-form').addEventListener('submit', async function(e) {
e.preventDefault();
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
try {
const response = await fetch('backend/login.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.status === 'success') {
const urlParams = new URLSearchParams(window.location.search);
const redirect = urlParams.get('redirect') || 'admin.html';
if (redirect.includes('admin.html') && username !== 'admin') {
showMessage('error', 'Akses Ditolak: Akun Anda bukan Administrator.');
// Optionally log them out since they logged into the wrong place
fetch('backend/logout.php');
return;
}
window.location.href = redirect;
} else {
showMessage('error', data.message);
}
} catch (err) {
showMessage('error', 'Terjadi kesalahan server saat login');
}
});
// Register Handler
document.getElementById('register-form').addEventListener('submit', async function(e) {
e.preventDefault();
const username = document.getElementById('reg-username').value;
const password = document.getElementById('reg-password').value;
try {
const response = await fetch('backend/register.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.status === 'success') {
showMessage('success', data.message);
document.getElementById('register-form').reset();
setTimeout(() => { toggleForm('login'); }, 2000);
} else {
showMessage('error', data.message);
}
} catch (err) {
showMessage('error', 'Terjadi kesalahan server saat registrasi');
}
});
</script>
</body>
</html>
+429
View File
@@ -0,0 +1,429 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Peta Kemiskinan & Rumah Ibadah — WebGIS</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--clr-bg: #0f172a; --clr-surface: #1e293b; --clr-surface-light: #334155;
--clr-border: #334155; --clr-text: #f1f5f9; --clr-muted: #94a3b8;
--clr-primary: #3b82f6; --clr-primary-dark: #2563eb;
--clr-success: #22c55e; --clr-danger: #ef4444;
--radius: 10px; --shadow: 0 4px 24px rgba(0,0,0,.45);
--transition: .2s ease;
--sidebar-width: 260px; --sidebar-collapsed: 70px;
}
html, body { height: 100%; font-family: 'Inter', sans-serif; background: var(--clr-bg); color: var(--clr-text); overflow: hidden; }
/* ---- Layout ---- */
.app-wrapper { display: flex; height: 100vh; }
/* ---- Sidebar ---- */
.sidebar {
width: var(--sidebar-width);
background: linear-gradient(135deg, #1a2a42 0%, #0f172a 100%);
border-right: 1px solid var(--clr-border);
display: flex; flex-direction: column;
transition: width var(--transition); z-index: 100;
box-shadow: 2px 0 8px rgba(0,0,0,.3); overflow: hidden;
}
.sidebar.collapsed { width: var(--sidebar-collapsed); }
.sidebar-header {
padding: 20px; border-bottom: 1px solid var(--clr-border);
display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;
}
.sidebar-logo { display: flex; align-items: center; gap: 10px; flex: 1; overflow: hidden; }
.sidebar-logo-icon {
width: 40px; height: 40px; flex-shrink: 0;
background: linear-gradient(135deg, #ef4444, #8b5cf6);
border-radius: 8px; display: flex; align-items: center; justify-content: center;
font-size: 14px; font-weight: 700; box-shadow: 0 0 12px rgba(239,68,68,.4);
}
.sidebar-logo-text { overflow: hidden; }
.sidebar-logo-text h2 { font-size: 14px; font-weight: 700; white-space: nowrap; }
.sidebar-logo-text p { font-size: 11px; color: var(--clr-muted); white-space: nowrap; }
.sidebar.collapsed .sidebar-logo-text { display: none; }
.sidebar-toggle { background: transparent; border: none; color: var(--clr-muted); cursor: pointer; font-size: 20px; padding: 4px; flex-shrink: 0; transition: color var(--transition); }
.sidebar-toggle:hover { color: var(--clr-text); }
.sidebar-nav { flex: 1; overflow-y: auto; padding: 16px 0; }
.sidebar-section { padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,.05); }
.sidebar-section:last-child { border-bottom: none; }
.sidebar-section-title { padding: 10px 16px; font-size: 10px; font-weight: 700; color: var(--clr-muted); text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; overflow: hidden; }
.sidebar.collapsed .sidebar-section-title { opacity: 0; }
.sidebar-menu { display: flex; flex-direction: column; }
.sidebar-item {
display: flex; align-items: center; gap: 12px; padding: 13px 16px;
color: var(--clr-muted); cursor: pointer; text-decoration: none;
transition: all var(--transition); border-left: 3px solid transparent;
}
.sidebar-item:hover { color: var(--clr-text); background: rgba(255,255,255,.05); border-left-color: var(--clr-primary); }
.sidebar-item.active { color: var(--clr-danger); background: rgba(239,68,68,.1); border-left-color: var(--clr-danger); }
.sidebar-item-icon { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; }
.sidebar-item-text { flex: 1; font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; }
.sidebar.collapsed .sidebar-item-text { display: none; }
.sidebar-item-user { font-size: 11px; color: var(--clr-muted); padding: 4px 16px 12px; white-space: nowrap; overflow: hidden; }
.sidebar.collapsed .sidebar-item-user { display: none; }
/* ---- Main ---- */
.main-wrapper { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
header {
flex-shrink: 0; height: 60px;
background: linear-gradient(135deg, #3d1a1a 0%, #0f172a 100%);
border-bottom: 1px solid var(--clr-border);
display: flex; align-items: center; justify-content: space-between;
padding: 0 20px; box-shadow: 0 2px 12px rgba(0,0,0,.5);
}
.header-title h1 { font-size: 18px; font-weight: 700; color: #fca5a5; }
.header-breadcrumb { font-size: 13px; color: var(--clr-muted); margin-top: 2px; }
.header-user { font-size: 13px; color: var(--clr-muted); background: rgba(255,255,255,.06); border: 1px solid var(--clr-border); padding: 6px 14px; border-radius: 8px; }
.content-area { flex: 1; position: relative; overflow: hidden; }
/* ---- Map ---- */
#map { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; }
/* ---- Controls Panel ---- */
.controls-panel {
position: absolute; top: 16px; right: 16px;
background: rgba(30, 41, 59, 0.95); backdrop-filter: blur(8px);
border: 1px solid var(--clr-border); border-radius: var(--radius);
padding: 16px; z-index: 1000; width: 280px;
box-shadow: var(--shadow);
}
.controls-panel h3 { font-size: 13px; font-weight: 700; color: var(--clr-text); margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
.ctrl-input {
width: 100%; padding: 9px 12px; margin-bottom: 10px;
background: var(--clr-bg); border: 1px solid var(--clr-border);
border-radius: 6px; color: var(--clr-text); font-family: inherit; font-size: 13px; outline: none;
}
.ctrl-input:focus { border-color: var(--clr-primary); }
/* ---- Search Panel (Left) ---- */
.search-panel {
position: absolute; top: 16px; left: 60px; /* offset for leaflet zoom controls */
background: rgba(30, 41, 59, 0.95); backdrop-filter: blur(8px);
border: 1px solid var(--clr-border); border-radius: var(--radius);
padding: 12px; z-index: 1000; width: 300px;
box-shadow: var(--shadow); display: flex; flex-direction: column;
}
.search-panel h3 { font-size: 13px; font-weight: 700; color: var(--clr-text); margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; display: flex; align-items: center; gap: 8px; }
.search-input-wrap { position: relative; margin-bottom: 8px; }
.search-input-wrap .ctrl-input { margin-bottom: 0; padding-right: 30px; }
.search-results {
max-height: 180px; overflow-y: auto; margin-top: 4px;
display: none; flex-direction: column; gap: 4px;
}
.search-results.active { display: flex; margin-bottom: 8px; }
.search-result-item {
padding: 8px 10px; background: rgba(255,255,255,0.05); border-radius: 6px;
cursor: pointer; transition: 0.2s; border: 1px solid transparent;
}
.search-result-item:hover { background: rgba(59,130,246,0.1); border-color: rgba(59,130,246,0.3); }
.search-result-name { font-size: 13px; font-weight: 600; color: #f1f5f9; display: block; margin-bottom: 2px; }
.search-result-desc { font-size: 11px; color: #94a3b8; display: block; }
.legend-panel {
position: absolute; bottom: 16px; right: 16px;
background: rgba(30, 41, 59, 0.95); backdrop-filter: blur(8px);
border: 1px solid var(--clr-border); border-radius: var(--radius);
padding: 14px; z-index: 1000; width: 230px; font-size: 13px;
box-shadow: var(--shadow);
}
.legend-panel h3 { font-size: 11px; font-weight: 700; color: var(--clr-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; }
.legend-row { display: flex; align-items: center; gap: 10px; margin-bottom: 7px; color: var(--clr-text); font-size: 12px; }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
/* ---- Popup Slider ---- */
.popup-slider-wrap { margin-top: 10px; }
.popup-slider-label { font-size: 12px; font-weight: 600; color: #334155; display: block; margin-bottom: 4px; }
.popup-slider { width: 100%; }
/* ---- Loader ---- */
#loader { position: fixed; inset: 0; background: var(--clr-bg); color: var(--clr-text); display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 9999; gap: 12px; }
.loader-spinner { width: 36px; height: 36px; border: 3px solid var(--clr-border); border-top-color: var(--clr-danger); border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div id="loader">
<div class="loader-spinner"></div>
<span>Verifikasi Akses...</span>
</div>
<div class="app-wrapper">
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="sidebar-logo">
<div class="sidebar-logo-icon">KM</div>
<div class="sidebar-logo-text">
<h2>WebGIS</h2>
<p>Peta Kemiskinan</p>
</div>
</div>
<button class="sidebar-toggle" id="sidebar-toggle"></button>
</div>
<nav class="sidebar-nav">
<div class="sidebar-section">
<div class="sidebar-section-title">Navigasi</div>
<div class="sidebar-menu">
<div class="sidebar-item active">
<div class="sidebar-item-icon">🗺</div>
<div class="sidebar-item-text">Peta Kemiskinan</div>
</div>
<a href="index.html" class="sidebar-item">
<div class="sidebar-item-icon">🏠</div>
<div class="sidebar-item-text">Beranda</div>
</a>
</div>
</div>
<div class="sidebar-section">
<div class="sidebar-menu">
<div class="sidebar-item" id="logout-btn">
<div class="sidebar-item-icon"></div>
<div class="sidebar-item-text">Logout</div>
</div>
</div>
</div>
</nav>
</aside>
<!-- Main -->
<div class="main-wrapper">
<header>
<div class="header-title">
<h1>Peta Analisis Kemiskinan</h1>
<div class="header-breadcrumb">Peta terproteksi — Login diperlukan</div>
</div>
<div class="header-user" id="header-user">Loading...</div>
</header>
<div class="content-area">
<div id="map"></div>
<!-- Search Panel (Left) -->
<div class="search-panel">
<h3><i class="fa-solid fa-magnifying-glass" style="color:var(--clr-primary)"></i> Pencarian</h3>
<div style="font-size:11px; font-weight:600; color:#94a3b8; margin-bottom:4px;">RUMAH IBADAH</div>
<div class="search-input-wrap">
<input id="search-ibadah" class="ctrl-input" type="text" placeholder="Cari Rumah Ibadah..." autocomplete="off">
</div>
<div id="results-ibadah" class="search-results"></div>
<div style="font-size:11px; font-weight:600; color:#94a3b8; margin-top:8px; margin-bottom:4px;">PENDUDUK MISKIN</div>
<div class="search-input-wrap">
<input id="search-miskin" class="ctrl-input" type="text" placeholder="Cari Nama KK Penduduk Miskin..." autocomplete="off">
</div>
<div id="results-miskin" class="search-results"></div>
</div>
<div class="legend-panel">
<h3>Legenda</h3>
<div class="legend-row"><div class="legend-dot" style="background:#22c55e;"></div> Rumah Ibadah</div>
<div class="legend-row"><div class="legend-dot" style="background:#ef4444;"></div> Penduduk Miskin (Dalam Radius)</div>
<div class="legend-row"><div class="legend-dot" style="background:#1e293b; border: 1px solid #64748b;"></div> Penduduk Miskin (Luar Radius)</div>
<hr style="border-color:#334155; margin: 8px 0;">
<div style="font-size:11px; color: var(--clr-muted);">💡 Klik Rumah Ibadah untuk atur radius jangkauan</div>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js"></script>
<script>
// Sidebar toggle
document.getElementById('sidebar-toggle').addEventListener('click', () => {
document.getElementById('sidebar').classList.toggle('collapsed');
});
// Logout
document.getElementById('logout-btn').addEventListener('click', async () => {
await fetch('backend/logout.php');
window.location.href = 'index.html';
});
// Global state
let ibadahData = [], miskinData = [];
let ibadahLayer, miskinLayer, map;
let ibadahRadiuses = {}, ibadahPoints = [];
let ibadahIcon, miskinRedIcon, miskinBlackIcon;
async function checkAuth() {
try {
const res = await fetch('backend/check_auth.php');
if (!res.ok) { window.location.href = 'login.html?redirect=poverty.html'; return; }
const data = await res.json();
document.getElementById('header-user').textContent = '👤 ' + data.username;
document.getElementById('loader').style.display = 'none';
initMap();
} catch (e) { window.location.href = 'login.html?redirect=poverty.html'; }
}
async function initMap() {
map = L.map('map').setView([-0.026, 109.342], 13);
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap', maxZoom: 19 });
const dark = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { maxZoom: 19 });
osm.addTo(map);
L.control.layers({ 'Standard': osm, 'Dark': dark }).addTo(map);
ibadahLayer = L.layerGroup().addTo(map);
miskinLayer = L.layerGroup().addTo(map);
ibadahIcon = L.icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25,41], iconAnchor: [12,41], popupAnchor: [1,-34], shadowSize: [41,41] });
miskinRedIcon = L.icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25,41], iconAnchor: [12,41], popupAnchor: [1,-34], shadowSize: [41,41] });
miskinBlackIcon = L.icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-black.png', shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png', iconSize: [25,41], iconAnchor: [12,41], popupAnchor: [1,-34], shadowSize: [41,41] });
try {
const [ir, mr] = await Promise.all([fetch('backend/read_ibadah.php'), fetch('backend/read_miskin.php')]);
ibadahData = (await ir.json()).data || [];
miskinData = (await mr.json()).data || [];
ibadahData.forEach(item => { ibadahRadiuses[item.id] = 0; });
renderAll();
} catch (e) { console.error('Error loading data:', e); }
}
window.updateRadius = function(id, val) {
ibadahRadiuses[id] = parseInt(val);
const el = document.getElementById('radius-val-' + id);
if (el) el.textContent = val;
renderBuffers();
renderMiskin();
};
function renderBuffers() {
map.eachLayer(l => { if (l.options && l.options.isBuffer) map.removeLayer(l); });
ibadahData.forEach(item => {
const r = ibadahRadiuses[item.id];
if (r > 0) {
const lat = parseFloat(item.latitude), lng = parseFloat(item.longitude);
if (!isNaN(lat) && !isNaN(lng)) {
L.circle([lat, lng], { radius: r, color: '#ef4444', fillColor: '#ef4444', fillOpacity: 0.12, weight: 2, isBuffer: true, dashArray: '6,4' }).addTo(map);
}
}
});
}
function renderAll() { renderIbadah(); renderBuffers(); renderMiskin(); }
function renderIbadah(autoZoom = false) {
ibadahLayer.clearLayers(); ibadahPoints = [];
const bounds = [];
const f = document.getElementById('search-ibadah').value.toLowerCase();
ibadahData.forEach(item => {
if (f && !item.nama.toLowerCase().includes(f)) return;
const lat = parseFloat(item.latitude), lng = parseFloat(item.longitude);
if (isNaN(lat) || isNaN(lng)) return;
ibadahPoints.push({ id: item.id, pt: turf.point([lng, lat]) });
const r = ibadahRadiuses[item.id] || 0;
const popup = '<strong style="color:#166534;">' + item.nama + '</strong><br><small>' + item.alamat + '</small>'
+ '<div class="popup-slider-wrap">'
+ '<span class="popup-slider-label">Radius: <b><span id="radius-val-' + item.id + '">' + r + '</span> m</b></span>'
+ '<input type="range" class="popup-slider" min="0" max="5000" step="100" value="' + r + '" oninput="window.updateRadius(' + item.id + ', this.value)">'
+ '</div>';
L.marker([lat, lng], { icon: ibadahIcon }).bindPopup(popup, { minWidth: 220 }).addTo(ibadahLayer);
bounds.push([lat, lng]);
});
if (autoZoom && f && bounds.length > 0) {
if (bounds.length === 1) map.flyTo(bounds[0], 16);
else map.fitBounds(bounds, { padding: [50, 50] });
}
}
function renderMiskin(autoZoom = false) {
miskinLayer.clearLayers();
const bounds = [];
const f = document.getElementById('search-miskin').value.toLowerCase();
miskinData.forEach(item => {
if (f && !item.nama_kk.toLowerCase().includes(f)) return;
const lat = parseFloat(item.latitude), lng = parseFloat(item.longitude);
if (isNaN(lat) || isNaN(lng)) return;
const pt = turf.point([lng, lat]);
let inRadius = false;
for (const ib of ibadahPoints) {
const km = ibadahRadiuses[ib.id] / 1000;
if (km > 0 && turf.distance(pt, ib.pt, { units: 'kilometers' }) <= km) { inRadius = true; break; }
}
L.marker([lat, lng], { icon: inRadius ? miskinRedIcon : miskinBlackIcon })
.bindPopup('<strong>KK: ' + item.nama_kk + '</strong><br>' + (item.keterangan || '-') + '<br>Status: ' + (inRadius ? '🔴 Dalam Radius' : '⚫ Luar Radius'))
.addTo(miskinLayer);
bounds.push([lat, lng]);
});
if (autoZoom && f && bounds.length > 0) {
if (bounds.length === 1) map.flyTo(bounds[0], 17);
else map.fitBounds(bounds, { padding: [50, 50] });
}
}
document.getElementById('search-ibadah').addEventListener('input', e => {
const f = e.target.value.toLowerCase();
const resDiv = document.getElementById('results-ibadah');
if (!f) {
resDiv.classList.remove('active');
renderIbadah(false);
return;
}
const matched = ibadahData.filter(i => i.nama.toLowerCase().includes(f));
if (matched.length > 0) {
resDiv.innerHTML = matched.map(m => `
<div class="search-result-item" onclick="window.selectIbadah(${m.id})">
<span class="search-result-name">${m.nama}</span>
<span class="search-result-desc">${m.alamat}</span>
</div>
`).join('');
} else {
resDiv.innerHTML = '<div style="padding:8px 10px; color:#94a3b8; font-size:12px;">Tidak ditemukan.</div>';
}
resDiv.classList.add('active');
renderIbadah(false);
});
window.selectIbadah = function(id) {
const item = ibadahData.find(i => i.id == id);
if (!item) return;
document.getElementById('search-ibadah').value = item.nama;
document.getElementById('results-ibadah').classList.remove('active');
renderIbadah(true);
};
document.getElementById('search-miskin').addEventListener('input', e => {
const f = e.target.value.toLowerCase();
const resDiv = document.getElementById('results-miskin');
if (!f) {
resDiv.classList.remove('active');
renderMiskin(false);
return;
}
const matched = miskinData.filter(m => m.nama_kk.toLowerCase().includes(f));
if (matched.length > 0) {
resDiv.innerHTML = matched.map(m => `
<div class="search-result-item" onclick="window.selectMiskin(${m.id})">
<span class="search-result-name">KK: ${m.nama_kk}</span>
<span class="search-result-desc">${m.keterangan || 'Tanpa keterangan'}</span>
</div>
`).join('');
} else {
resDiv.innerHTML = '<div style="padding:8px 10px; color:#94a3b8; font-size:12px;">Tidak ditemukan.</div>';
}
resDiv.classList.add('active');
renderMiskin(false);
});
window.selectMiskin = function(id) {
const item = miskinData.find(i => i.id == id);
if (!item) return;
document.getElementById('search-miskin').value = item.nama_kk;
document.getElementById('results-miskin').classList.remove('active');
renderMiskin(true);
};
checkAuth();
</script>
</body>
</html>
+403
View File
@@ -0,0 +1,403 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Peta SPBU Publik — WebGIS</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--clr-bg: #0f172a; --clr-surface: #1e293b; --clr-surface-light: #334155;
--clr-border: #334155; --clr-text: #f1f5f9; --clr-muted: #94a3b8;
--clr-primary: #3b82f6; --clr-primary-dark: #2563eb;
--clr-success: #22c55e; --clr-danger: #ef4444;
--radius: 10px; --shadow: 0 4px 24px rgba(0,0,0,.45);
--transition: .2s ease;
--sidebar-width: 260px; --sidebar-collapsed: 70px;
}
html, body { height: 100%; font-family: 'Inter', sans-serif; background: var(--clr-bg); color: var(--clr-text); overflow: hidden; }
/* ---- Layout ---- */
.app-wrapper { display: flex; height: 100vh; }
/* ---- Sidebar ---- */
.sidebar {
width: var(--sidebar-width);
background: linear-gradient(135deg, #1a2a42 0%, #0f172a 100%);
border-right: 1px solid var(--clr-border);
display: flex; flex-direction: column;
transition: width var(--transition); z-index: 100;
box-shadow: 2px 0 8px rgba(0,0,0,.3); overflow: hidden;
}
.sidebar.collapsed { width: var(--sidebar-collapsed); }
.sidebar-header {
padding: 20px; border-bottom: 1px solid var(--clr-border);
display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;
}
.sidebar-logo { display: flex; align-items: center; gap: 10px; flex: 1; overflow: hidden; }
.sidebar-logo-icon {
width: 40px; height: 40px; flex-shrink: 0;
background: linear-gradient(135deg, var(--clr-primary), #8b5cf6);
border-radius: 8px; display: flex; align-items: center; justify-content: center;
font-size: 14px; font-weight: 700; box-shadow: 0 0 12px rgba(59,130,246,.4);
}
.sidebar-logo-text { overflow: hidden; }
.sidebar-logo-text h2 { font-size: 14px; font-weight: 700; white-space: nowrap; }
.sidebar-logo-text p { font-size: 11px; color: var(--clr-muted); white-space: nowrap; }
.sidebar.collapsed .sidebar-logo-text { display: none; }
.sidebar-toggle { background: transparent; border: none; color: var(--clr-muted); cursor: pointer; font-size: 20px; padding: 4px; flex-shrink: 0; transition: color var(--transition); }
.sidebar-toggle:hover { color: var(--clr-text); }
.sidebar-nav { flex: 1; overflow-y: auto; padding: 16px 0; }
.sidebar-section { padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,.05); }
.sidebar-section:last-child { border-bottom: none; }
.sidebar-section-title { padding: 10px 16px; font-size: 10px; font-weight: 700; color: var(--clr-muted); text-transform: uppercase; letter-spacing: 0.5px; white-space: nowrap; overflow: hidden; }
.sidebar.collapsed .sidebar-section-title { opacity: 0; }
.sidebar-menu { display: flex; flex-direction: column; }
.sidebar-item {
display: flex; align-items: center; gap: 12px; padding: 13px 16px;
color: var(--clr-muted); cursor: pointer; text-decoration: none;
transition: all var(--transition); border-left: 3px solid transparent;
}
.sidebar-item:hover { color: var(--clr-text); background: rgba(255,255,255,.05); border-left-color: var(--clr-primary); }
.sidebar-item.active { color: var(--clr-primary); background: rgba(59,130,246,.1); border-left-color: var(--clr-primary); }
.sidebar-item-icon { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; }
.sidebar-item-text { flex: 1; font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; }
.sidebar.collapsed .sidebar-item-text { display: none; }
/* ---- Main ---- */
.main-wrapper { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
header {
flex-shrink: 0; height: 60px;
background: linear-gradient(135deg, #1e3a5f 0%, #0f172a 100%);
border-bottom: 1px solid var(--clr-border);
display: flex; align-items: center; justify-content: space-between;
padding: 0 20px; box-shadow: 0 2px 12px rgba(0,0,0,.5);
}
.header-title h1 { font-size: 18px; font-weight: 700; }
.header-breadcrumb { font-size: 13px; color: var(--clr-muted); margin-top: 2px; }
.content-area { flex: 1; position: relative; overflow: hidden; }
/* ---- Map ---- */
#map { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 1; }
/* ---- Controls Panel ---- */
.controls-panel {
position: absolute; top: 16px; right: 16px;
background: rgba(30, 41, 59, 0.95); backdrop-filter: blur(8px);
border: 1px solid var(--clr-border); border-radius: var(--radius);
padding: 16px; z-index: 1000; width: 280px;
box-shadow: var(--shadow);
}
.controls-panel h3 { font-size: 13px; font-weight: 700; color: var(--clr-text); margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
.ctrl-input {
width: 100%; padding: 9px 12px; margin-bottom: 10px;
background: var(--clr-bg); border: 1px solid var(--clr-border);
border-radius: 6px; color: var(--clr-text); font-family: inherit; font-size: 13px; outline: none;
}
.ctrl-input:focus { border-color: var(--clr-primary); }
.ctrl-btn {
display: block; width: 100%; padding: 10px; margin-bottom: 8px;
background: var(--clr-primary); color: white; border: none; border-radius: 6px;
font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer; transition: 0.2s;
}
.ctrl-btn:hover { background: var(--clr-primary-dark); }
.ctrl-btn:disabled { background: var(--clr-surface-light); cursor: not-allowed; color: var(--clr-muted); }
.info-box {
display: none; background: rgba(59,130,246,.1); border: 1px solid rgba(59,130,246,.3);
border-radius: 6px; padding: 10px; margin-top: 8px; font-size: 13px;
}
.info-box.active { display: block; }
.info-box strong { color: var(--clr-primary); }
/* ---- Search Panel (Left) ---- */
.search-panel {
position: absolute; top: 16px; left: 60px; /* offset for leaflet zoom controls */
background: rgba(30, 41, 59, 0.95); backdrop-filter: blur(8px);
border: 1px solid var(--clr-border); border-radius: var(--radius);
padding: 12px; z-index: 1000; width: 300px;
box-shadow: var(--shadow); display: flex; flex-direction: column;
}
.search-panel h3 { font-size: 13px; font-weight: 700; color: var(--clr-text); margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.5px; display: flex; align-items: center; gap: 8px; }
.search-input-wrap { position: relative; }
.search-input-wrap .ctrl-input { margin-bottom: 0; padding-right: 30px; }
.search-results {
max-height: 250px; overflow-y: auto; margin-top: 8px;
display: none; flex-direction: column; gap: 4px;
}
.search-results.active { display: flex; }
.search-result-item {
padding: 8px 10px; background: rgba(255,255,255,0.05); border-radius: 6px;
cursor: pointer; transition: 0.2s; border: 1px solid transparent;
}
.search-result-item:hover { background: rgba(59,130,246,0.1); border-color: rgba(59,130,246,0.3); }
.search-result-name { font-size: 13px; font-weight: 600; color: #f1f5f9; display: block; margin-bottom: 2px; }
.search-result-desc { font-size: 11px; color: #94a3b8; display: block; }
/* ---- Legend Panel ---- */
.legend-panel {
position: absolute; bottom: 16px; right: 16px;
background: rgba(30, 41, 59, 0.95); backdrop-filter: blur(8px);
border: 1px solid var(--clr-border); border-radius: var(--radius);
padding: 14px; z-index: 1000; width: 200px; font-size: 13px;
box-shadow: var(--shadow);
}
.legend-panel h3 { font-size: 11px; font-weight: 700; color: var(--clr-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; }
.legend-row { display: flex; align-items: center; gap: 10px; margin-bottom: 7px; color: var(--clr-text); font-size: 12px; }
.legend-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
</style>
</head>
<body>
<div class="app-wrapper">
<!-- Sidebar -->
<aside class="sidebar" id="sidebar">
<div class="sidebar-header">
<div class="sidebar-logo">
<div class="sidebar-logo-icon">WG</div>
<div class="sidebar-logo-text">
<h2>WebGIS</h2>
<p>SPBU System</p>
</div>
</div>
<button class="sidebar-toggle" id="sidebar-toggle"></button>
</div>
<nav class="sidebar-nav">
<div class="sidebar-section">
<div class="sidebar-section-title">Navigasi</div>
<div class="sidebar-menu">
<div class="sidebar-item active">
<div class="sidebar-item-icon">🗺</div>
<div class="sidebar-item-text">Peta SPBU</div>
</div>
<a href="index.html" class="sidebar-item">
<div class="sidebar-item-icon">🏠</div>
<div class="sidebar-item-text">Beranda</div>
</a>
</div>
</div>
<div class="sidebar-section">
</div>
</nav>
</aside>
<!-- Main -->
<div class="main-wrapper">
<header>
<div class="header-title">
<h1>Peta Persebaran SPBU</h1>
<div class="header-breadcrumb">Peta Publik — Data dikelola oleh Administrator</div>
</div>
<div style="display:flex; align-items:center; gap:10px;">
<span id="data-count" style="font-size:13px; color:#94a3b8; background:rgba(255,255,255,.06); border:1px solid #334155; padding:5px 12px; border-radius:7px;">Memuat...</span>
<button id="btn-refresh" title="Refresh data terbaru" style="background:rgba(59,130,246,.15); border:1px solid rgba(59,130,246,.3); color:#60a5fa; border-radius:7px; padding:6px 12px; cursor:pointer; font-size:13px; font-family:inherit;">🔄 Refresh</button>
</div>
</header>
<div class="content-area">
<div id="map"></div>
<!-- Search Panel (Left) -->
<div class="search-panel">
<h3><i class="fa-solid fa-magnifying-glass" style="color:var(--clr-primary)"></i> Cari SPBU</h3>
<div class="search-input-wrap">
<input id="search-spbu" class="ctrl-input" type="text" placeholder="Ketik nama atau no SPBU..." autocomplete="off">
</div>
<div id="search-results" class="search-results"></div>
</div>
<div class="controls-panel">
<h3>📍 Lokasi & Rute</h3>
<button id="btn-location" class="ctrl-btn">📍 Dapatkan Lokasi Saya</button>
<button id="btn-nearest" class="ctrl-btn" disabled>🎯 Cari SPBU Terdekat</button>
<div id="info-box" class="info-box">
<strong>SPBU Terdekat:</strong><br>
<span id="nearest-name">-</span><br>
<strong>Jarak:</strong> <span id="nearest-distance">-</span> km
</div>
</div>
<div class="legend-panel">
<h3>Legenda</h3>
<div class="legend-row"><div class="legend-dot" style="background:#22c55e;"></div> SPBU 24 Jam</div>
<div class="legend-row"><div class="legend-dot" style="background:#f59e0b;"></div> SPBU Tidak 24 Jam</div>
<div class="legend-row"><div class="legend-dot" style="background:#ef4444;"></div> Lokasi Anda</div>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js"></script>
<script>
// Sidebar toggle
document.getElementById('sidebar-toggle').addEventListener('click', () => {
document.getElementById('sidebar').classList.toggle('collapsed');
});
// Init Map
const map = L.map('map').setView([-0.026, 109.342], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors', maxZoom: 19
}).addTo(map);
const baseMaps = {
'Standard': L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19, attribution: '© OpenStreetMap' }),
'Dark': L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { maxZoom: 19 })
};
L.control.layers(baseMaps).addTo(map);
let spbuData = [], spbuLayer = L.layerGroup().addTo(map);
let userMarker = null, routeLine = null, userLocation = null;
const icon24 = L.circleMarker ? null : null; // will use circleMarker
async function loadSPBU(silent = false) {
try {
const res = await fetch('backend/read.php');
const result = await res.json();
// Fix: read.php returns {success: true, data:[...]}, not {status:'success'}
if (result.success && result.data) {
spbuData = result.data;
renderSPBU(document.getElementById('search-spbu').value);
if (!silent) {
document.getElementById('data-count').textContent = spbuData.length + ' SPBU';
}
}
} catch (err) { console.error('Error loading SPBU:', err); }
}
function renderSPBU(filterText = '', autoZoom = false) {
spbuLayer.clearLayers();
const bounds = [];
spbuData.forEach(spbu => {
if (filterText && !spbu.nama.toLowerCase().includes(filterText.toLowerCase()) && !spbu.no_spbu.toLowerCase().includes(filterText.toLowerCase())) return;
const lat = parseFloat(spbu.latitude), lng = parseFloat(spbu.longitude);
if (isNaN(lat) || isNaN(lng)) return;
const color = spbu.status === '24jam' ? '#22c55e' : '#f59e0b';
const marker = L.circleMarker([lat, lng], { radius: 7, fillColor: color, color: '#fff', weight: 2, opacity: 1, fillOpacity: 0.9 })
.bindPopup('<strong>' + spbu.nama + '</strong><br>No: ' + spbu.no_spbu + '<br>Status: ' + (spbu.status === '24jam' ? '✅ 24 Jam' : '⏰ Tidak 24 Jam'))
.addTo(spbuLayer);
bounds.push([lat, lng]);
});
if (autoZoom && filterText && bounds.length > 0) {
if (bounds.length === 1) {
map.flyTo(bounds[0], 16);
} else {
map.fitBounds(bounds, { padding: [50, 50] });
}
}
}
document.getElementById('search-spbu').addEventListener('input', e => {
const filterText = e.target.value.toLowerCase();
const resultsDiv = document.getElementById('search-results');
if (!filterText) {
resultsDiv.classList.remove('active');
renderSPBU(''); // render all, no zoom
return;
}
// Filter data for list
const matched = spbuData.filter(spbu =>
spbu.nama.toLowerCase().includes(filterText) ||
spbu.no_spbu.toLowerCase().includes(filterText)
);
if (matched.length > 0) {
resultsDiv.innerHTML = matched.map(m => `
<div class="search-result-item" onclick="window.selectSPBU(${m.id})">
<span class="search-result-name">${m.nama}</span>
<span class="search-result-desc">No: ${m.no_spbu}${m.status === '24jam' ? '24 Jam' : 'Tidak 24 Jam'}</span>
</div>
`).join('');
} else {
resultsDiv.innerHTML = '<div style="padding:8px 10px; color:#94a3b8; font-size:12px;">Tidak ditemukan.</div>';
}
resultsDiv.classList.add('active');
// Filter map markers, but don't auto-zoom on every keystroke
renderSPBU(filterText, false);
});
window.selectSPBU = function(id) {
const spbu = spbuData.find(s => s.id == id);
if (!spbu) return;
// Set input value to name
document.getElementById('search-spbu').value = spbu.nama;
document.getElementById('search-results').classList.remove('active');
// Render and zoom to specific SPBU
renderSPBU(spbu.nama.toLowerCase(), true);
};
const userIcon = L.icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
});
document.getElementById('btn-location').addEventListener('click', () => {
if (!navigator.geolocation) return alert('Geolocation tidak didukung browser Anda.');
const btn = document.getElementById('btn-location');
btn.textContent = '⏳ Mencari lokasi...';
btn.disabled = true;
navigator.geolocation.getCurrentPosition(pos => {
const lat = pos.coords.latitude, lng = pos.coords.longitude;
userLocation = [lng, lat];
if (userMarker) map.removeLayer(userMarker);
userMarker = L.marker([lat, lng], { icon: userIcon }).bindPopup('📍 Lokasi Anda').addTo(map);
map.flyTo([lat, lng], 14);
btn.textContent = '✅ Lokasi Diperbarui';
btn.disabled = false;
document.getElementById('btn-nearest').disabled = false;
}, err => {
alert('Gagal mendapatkan lokasi: ' + err.message);
btn.textContent = '📍 Dapatkan Lokasi Saya';
btn.disabled = false;
});
});
document.getElementById('btn-nearest').addEventListener('click', () => {
if (!userLocation || !spbuData.length) return;
let nearestSPBU = null, minDist = Infinity;
const userPt = turf.point(userLocation);
spbuData.forEach(spbu => {
const lat = parseFloat(spbu.latitude), lng = parseFloat(spbu.longitude);
if (isNaN(lat) || isNaN(lng)) return;
const dist = turf.distance(userPt, turf.point([lng, lat]), { units: 'kilometers' });
if (dist < minDist) { minDist = dist; nearestSPBU = spbu; }
});
if (!nearestSPBU) return;
if (routeLine) map.removeLayer(routeLine);
const lat = parseFloat(nearestSPBU.latitude), lng = parseFloat(nearestSPBU.longitude);
routeLine = L.polyline([[userLocation[1], userLocation[0]], [lat, lng]], {
color: '#ef4444', weight: 3, dashArray: '8, 8', opacity: 0.85
}).addTo(map);
map.fitBounds(routeLine.getBounds(), { padding: [60, 60] });
document.getElementById('nearest-name').textContent = nearestSPBU.nama + ' (No. ' + nearestSPBU.no_spbu + ')';
document.getElementById('nearest-distance').textContent = minDist.toFixed(2);
document.getElementById('info-box').classList.add('active');
});
document.getElementById('btn-refresh').addEventListener('click', () => {
const btn = document.getElementById('btn-refresh');
btn.textContent = 'Memuat...';
loadSPBU().then(() => {
btn.textContent = '🔄 Refresh';
});
});
// Auto-refresh every 30 seconds
setInterval(() => {
loadSPBU(true); // silent refresh
}, 30000);
loadSPBU();
</script>
</body>
</html>
+8
View File
@@ -0,0 +1,8 @@
<?php
$ch = curl_init('http://localhost/webgis-sig/backend/create_jalan.php');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, '{"nama_jalan":"Test Error","status":"Nasional","panjang":"123.45","geom":{"type":"LineString","coordinates":[[1,1],[2,2]]}}');
$res = curl_exec($ch);
var_dump($res);
if ($res === false) { echo curl_error($ch); }
+27
View File
@@ -0,0 +1,27 @@
<?php
require 'c:/xampp/htdocs/webgis-sig/backend/koneksi.php';
$c = getConnection();
$sql1 = "CREATE TABLE IF NOT EXISTS rumah_ibadah (
id INT NOT NULL AUTO_INCREMENT,
nama VARCHAR(150) NOT NULL,
alamat TEXT NOT NULL,
latitude DECIMAL(10, 8) NOT NULL,
longitude DECIMAL(11, 8) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;";
$c->query($sql1);
$sql2 = "CREATE TABLE IF NOT EXISTS penduduk_miskin (
id INT NOT NULL AUTO_INCREMENT,
nama_kk VARCHAR(150) NOT NULL,
keterangan TEXT,
latitude DECIMAL(10, 8) NOT NULL,
longitude DECIMAL(11, 8) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;";
$c->query($sql2);
echo 'Tables OK';
+18
View File
@@ -0,0 +1,18 @@
const fs = require('fs');
const admin = fs.readFileSync('admin.html', 'utf8');
const app = fs.readFileSync('js/app.js', 'utf8');
// Check onclick attributes
console.log('SPBU onclick:', admin.includes("onclick=\"window.mapToolActivatePoint('spbu'"));
console.log('Ibadah onclick:', admin.includes("onclick=\"window.mapToolActivatePoint('ibadah'"));
console.log('Miskin onclick:', admin.includes("onclick=\"window.mapToolActivatePoint('miskin'"));
console.log('Jalan onclick:', admin.includes("onclick=\"window.mapToolActivateDraw('jalan')\""));
console.log('Parsil onclick:', admin.includes("onclick=\"window.mapToolActivateDraw('parsil')\""));
console.log('Cancel onclick:', admin.includes('onclick="window.mapToolCancel()"'));
// Check global functions in app.js
console.log('\nGlobal mapToolActivatePoint:', app.includes('window.mapToolActivatePoint = function'));
console.log('Global mapToolActivateDraw:', app.includes('window.mapToolActivateDraw = function'));
console.log('Global mapToolCancel:', app.includes('window.mapToolCancel = function'));
console.log('map.click handler in initMap:', app.includes("map.on('click', function(e)"));
console.log('pm null check:', app.includes('if (map.pm && typeof map.pm.addControls'));