Initial commit untuk UAS SIG
This commit is contained in:
+255
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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']);
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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.';
|
||||
@@ -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();
|
||||
?>
|
||||
@@ -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();
|
||||
?>
|
||||
@@ -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();
|
||||
?>
|
||||
@@ -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();
|
||||
?>
|
||||
@@ -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();
|
||||
?>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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']);
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
session_start();
|
||||
session_destroy();
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['status' => 'success', 'message' => 'Logged out']);
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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();
|
||||
@@ -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']);
|
||||
}
|
||||
@@ -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();
|
||||
?>
|
||||
@@ -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();
|
||||
?>
|
||||
@@ -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();
|
||||
?>
|
||||
@@ -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();
|
||||
?>
|
||||
@@ -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();
|
||||
?>
|
||||
@@ -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();
|
||||
@@ -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
@@ -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>
|
||||
@@ -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: '© OpenStreetMap'
|
||||
});
|
||||
|
||||
const satellite = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {
|
||||
maxZoom: 19, attribution: 'Tiles © Esri'
|
||||
});
|
||||
|
||||
const dark = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
|
||||
maxZoom: 19, attribution: '© 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} m²
|
||||
</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, '&').replace(/</g, '<')
|
||||
.replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// Start
|
||||
loadAllData();
|
||||
+166
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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); }
|
||||
@@ -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';
|
||||
@@ -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'));
|
||||
Reference in New Issue
Block a user