first commit
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
# WebGIS — Spatial Data Management System
|
||||
## Setup & Installation Guide
|
||||
|
||||
---
|
||||
|
||||
## Folder Structure
|
||||
|
||||
```
|
||||
webgis/
|
||||
├── index.html ← Main frontend (single-page app)
|
||||
├── database.sql ← Database schema + seed data
|
||||
├── config/
|
||||
│ ├── db.php ← PDO database connection
|
||||
│ └── helpers.php ← Shared utility functions
|
||||
├── api/
|
||||
│ ├── points.php ← CRUD for point features
|
||||
│ ├── roads.php ← CRUD for road polylines
|
||||
│ └── parcels.php ← CRUD for land parcel polygons
|
||||
├── assets/
|
||||
│ ├── css/
|
||||
│ │ └── style.css ← Application stylesheet
|
||||
│ └── js/
|
||||
│ └── app.js ← Main application JavaScript
|
||||
└── docs/
|
||||
└── SRS.md ← Software Requirements Specification
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- **PHP** 8.0 or higher (with PDO + PDO_MySQL extension)
|
||||
- **MySQL** 8.0 or higher (or MariaDB 10.6+)
|
||||
- **Web Server**: Apache (XAMPP/WAMP) or Nginx
|
||||
- **Modern Browser**: Chrome 90+, Firefox 88+, Edge 90+
|
||||
|
||||
---
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Copy Files
|
||||
Place the entire `webgis/` folder inside your web server's document root:
|
||||
- XAMPP: `C:/xampp/htdocs/webgis/`
|
||||
- WAMP: `C:/wamp64/www/webgis/`
|
||||
- Linux: `/var/www/html/webgis/`
|
||||
|
||||
### 2. Create the Database
|
||||
Open phpMyAdmin or MySQL CLI and run:
|
||||
```sql
|
||||
source /path/to/webgis/database.sql;
|
||||
```
|
||||
This creates the `webgis_db` database and all tables with sample data.
|
||||
|
||||
### 3. Configure Database Connection
|
||||
Edit `config/db.php`:
|
||||
```php
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_NAME', 'webgis_db');
|
||||
define('DB_USER', 'root'); // your MySQL username
|
||||
define('DB_PASS', ''); // your MySQL password
|
||||
```
|
||||
|
||||
### 4. Start the Server
|
||||
Start Apache and MySQL in XAMPP/WAMP, then visit:
|
||||
```
|
||||
http://localhost/webgis/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature Usage
|
||||
|
||||
### Adding a Point (SPBU / Mosque / Poor Population)
|
||||
1. Click the **POINTS** tab in the sidebar
|
||||
2. Click **"Add Point on Map"**
|
||||
3. Click anywhere on the map
|
||||
4. Fill in the modal form (name, category, etc.)
|
||||
5. Click **"Add Point"** — marker appears on map
|
||||
|
||||
### Drawing a Road
|
||||
1. Click the **ROADS** tab
|
||||
2. Click **"Draw Road on Map"**
|
||||
3. Click multiple points on the map to draw the polyline
|
||||
4. **Double-click** to finish drawing
|
||||
5. Modal appears with **length auto-calculated** (read-only)
|
||||
6. Enter road name and type, then save
|
||||
|
||||
### Drawing a Land Parcel
|
||||
1. Click the **PARCELS** tab
|
||||
2. Click **"Draw Parcel on Map"**
|
||||
3. Click to draw polygon vertices; **double-click** to close
|
||||
4. Modal appears with **area auto-calculated** in m²
|
||||
5. Enter owner name and ownership type, then save
|
||||
|
||||
### Layer Visibility
|
||||
- Use the **LAYERS** tab to toggle any layer on/off
|
||||
- Each sub-layer (SPBU 24h, National Road, SHM parcels, etc.) is independently controllable
|
||||
|
||||
### Edit / Delete
|
||||
- Click **✏️** icon on any list item or in a popup to edit
|
||||
- Click **🗑️** to delete (with confirmation)
|
||||
- Press **Escape** to cancel any active drawing
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
All endpoints return JSON. CORS headers are set for local development.
|
||||
|
||||
| Method | URL | Description |
|
||||
|--------|-----|-------------|
|
||||
| GET | `/api/points.php` | List all points as GeoJSON FeatureCollection |
|
||||
| GET | `/api/points.php?id=N` | Get single point |
|
||||
| GET | `/api/points.php?category=spbu` | Filter by category |
|
||||
| POST | `/api/points.php` | Create a point |
|
||||
| PUT | `/api/points.php?id=N` | Update a point |
|
||||
| DELETE | `/api/points.php?id=N` | Delete a point |
|
||||
| GET | `/api/roads.php` | List all roads |
|
||||
| POST | `/api/roads.php` | Create a road |
|
||||
| PUT | `/api/roads.php?id=N` | Update a road |
|
||||
| DELETE | `/api/roads.php?id=N` | Delete a road |
|
||||
| GET | `/api/parcels.php` | List all parcels |
|
||||
| POST | `/api/parcels.php` | Create a parcel |
|
||||
| PUT | `/api/parcels.php?id=N` | Update a parcel |
|
||||
| DELETE | `/api/parcels.php?id=N` | Delete a parcel |
|
||||
|
||||
---
|
||||
|
||||
## Libraries Used (CDN — no installation required)
|
||||
|
||||
| Library | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| Leaflet.js | 1.9.4 | Core mapping library |
|
||||
| Leaflet.draw | 1.0.4 | Drawing tools (polyline, polygon) |
|
||||
| leaflet-geometryutil | 0.10.3 | Geodesic area calculation |
|
||||
| Google Fonts | — | Space Mono + DM Sans typography |
|
||||
|
||||
---
|
||||
# WebGIS — Spatial Data Management System
|
||||
## Setup & Installation Guide
|
||||
|
||||
---
|
||||
|
||||
## Folder Structure
|
||||
|
||||
```
|
||||
webgis/
|
||||
├── index.html ← Main frontend (single-page app)
|
||||
├── database.sql ← Database schema + seed data
|
||||
├── config/
|
||||
│ ├── db.php ← PDO database connection
|
||||
│ └── helpers.php ← Shared utility functions
|
||||
├── api/
|
||||
│ ├── points.php ← CRUD for point features
|
||||
│ ├── roads.php ← CRUD for road polylines
|
||||
│ └── parcels.php ← CRUD for land parcel polygons
|
||||
├── assets/
|
||||
│ ├── css/
|
||||
│ │ └── style.css ← Application stylesheet
|
||||
│ └── js/
|
||||
│ └── app.js ← Main application JavaScript
|
||||
└── docs/
|
||||
└── SRS.md ← Software Requirements Specification
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- **PHP** 8.0 or higher (with PDO + PDO_MySQL extension)
|
||||
- **MySQL** 8.0 or higher (or MariaDB 10.6+)
|
||||
- **Web Server**: Apache (XAMPP/WAMP) or Nginx
|
||||
- **Modern Browser**: Chrome 90+, Firefox 88+, Edge 90+
|
||||
|
||||
---
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Copy Files
|
||||
Place the entire `webgis/` folder inside your web server's document root:
|
||||
- XAMPP: `C:/xampp/htdocs/webgis/`
|
||||
- WAMP: `C:/wamp64/www/webgis/`
|
||||
- Linux: `/var/www/html/webgis/`
|
||||
|
||||
### 2. Create the Database
|
||||
Open phpMyAdmin or MySQL CLI and run:
|
||||
```sql
|
||||
source /path/to/webgis/database.sql;
|
||||
```
|
||||
This creates the `webgis_db` database and all tables with sample data.
|
||||
|
||||
### 3. Configure Database Connection
|
||||
Edit `config/db.php`:
|
||||
```php
|
||||
define('DB_HOST', 'localhost');
|
||||
define('DB_NAME', 'webgis_db');
|
||||
define('DB_USER', 'root'); // your MySQL username
|
||||
define('DB_PASS', ''); // your MySQL password
|
||||
```
|
||||
|
||||
### 4. Start the Server
|
||||
Start Apache and MySQL in XAMPP/WAMP, then visit:
|
||||
```
|
||||
http://localhost/webgis/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Feature Usage
|
||||
|
||||
### Adding a Point (SPBU / Mosque / Poor Population)
|
||||
1. Click the **POINTS** tab in the sidebar
|
||||
2. Click **"Add Point on Map"**
|
||||
3. Click anywhere on the map
|
||||
4. Fill in the modal form (name, category, etc.)
|
||||
5. Click **"Add Point"** — marker appears on map
|
||||
|
||||
### Drawing a Road
|
||||
1. Click the **ROADS** tab
|
||||
2. Click **"Draw Road on Map"**
|
||||
3. Click multiple points on the map to draw the polyline
|
||||
4. **Double-click** to finish drawing
|
||||
5. Modal appears with **length auto-calculated** (read-only)
|
||||
6. Enter road name and type, then save
|
||||
|
||||
### Drawing a Land Parcel
|
||||
1. Click the **PARCELS** tab
|
||||
2. Click **"Draw Parcel on Map"**
|
||||
3. Click to draw polygon vertices; **double-click** to close
|
||||
4. Modal appears with **area auto-calculated** in m²
|
||||
5. Enter owner name and ownership type, then save
|
||||
|
||||
### Layer Visibility
|
||||
- Use the **LAYERS** tab to toggle any layer on/off
|
||||
- Each sub-layer (SPBU 24h, National Road, SHM parcels, etc.) is independently controllable
|
||||
|
||||
### Edit / Delete
|
||||
- Click **✏️** icon on any list item or in a popup to edit
|
||||
- Click **🗑️** to delete (with confirmation)
|
||||
- Press **Escape** to cancel any active drawing
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
All endpoints return JSON. CORS headers are set for local development.
|
||||
|
||||
| Method | URL | Description |
|
||||
|--------|-----|-------------|
|
||||
| GET | `/api/points.php` | List all points as GeoJSON FeatureCollection |
|
||||
| GET | `/api/points.php?id=N` | Get single point |
|
||||
| GET | `/api/points.php?category=spbu` | Filter by category |
|
||||
| POST | `/api/points.php` | Create a point |
|
||||
| PUT | `/api/points.php?id=N` | Update a point |
|
||||
| DELETE | `/api/points.php?id=N` | Delete a point |
|
||||
| GET | `/api/roads.php` | List all roads |
|
||||
| POST | `/api/roads.php` | Create a road |
|
||||
| PUT | `/api/roads.php?id=N` | Update a road |
|
||||
| DELETE | `/api/roads.php?id=N` | Delete a road |
|
||||
| GET | `/api/parcels.php` | List all parcels |
|
||||
| POST | `/api/parcels.php` | Create a parcel |
|
||||
| PUT | `/api/parcels.php?id=N` | Update a parcel |
|
||||
| DELETE | `/api/parcels.php?id=N` | Delete a parcel |
|
||||
|
||||
---
|
||||
|
||||
## Libraries Used (CDN — no installation required)
|
||||
|
||||
| Library | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| Leaflet.js | 1.9.4 | Core mapping library |
|
||||
| Leaflet.draw | 1.0.4 | Drawing tools (polyline, polygon) |
|
||||
| leaflet-geometryutil | 0.10.3 | Geodesic area calculation |
|
||||
| Google Fonts | — | Space Mono + DM Sans typography |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
- This application is designed for academic/prototype use. For production, add:
|
||||
- User authentication (sessions/JWT)
|
||||
- Input rate limiting
|
||||
- HTTPS enforcement
|
||||
- Database backup strategy
|
||||
|
||||
## Notes
|
||||
- This application is designed for academic/prototype use. For production, add:
|
||||
- User authentication (sessions/JWT)
|
||||
- Input rate limiting
|
||||
- HTTPS enforcement
|
||||
- Database backup strategy
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../config/db.php';
|
||||
require_once __DIR__ . '/../config/helpers.php';
|
||||
|
||||
handlePreflight();
|
||||
|
||||
$method = getMethod();
|
||||
$db = getDB();
|
||||
|
||||
switch ($method) {
|
||||
|
||||
case 'GET':
|
||||
|
||||
if(isset($_GET['point_id'])){
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"SELECT *
|
||||
FROM bantuan
|
||||
WHERE point_id=?
|
||||
ORDER BY tanggal_bantuan DESC"
|
||||
);
|
||||
|
||||
$stmt->execute([
|
||||
(int)$_GET['point_id']
|
||||
]);
|
||||
|
||||
jsonResponse(
|
||||
$stmt->fetchAll()
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
|
||||
$body = getRequestBody();
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO bantuan
|
||||
(
|
||||
point_id,
|
||||
jenis_bantuan,
|
||||
nominal,
|
||||
tanggal_bantuan,
|
||||
instansi_pemberi,
|
||||
keterangan
|
||||
)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
|
||||
$stmt->execute([
|
||||
|
||||
$body['point_id'],
|
||||
|
||||
$body['jenis_bantuan'],
|
||||
|
||||
$body['nominal'],
|
||||
|
||||
$body['tanggal_bantuan'],
|
||||
|
||||
$body['instansi_pemberi'],
|
||||
|
||||
$body['keterangan']
|
||||
|
||||
]);
|
||||
|
||||
jsonResponse([
|
||||
'message'=>'Bantuan berhasil ditambahkan'
|
||||
],201);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
jsonResponse([
|
||||
'error'=>'Method not allowed'
|
||||
],405);
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/../config/db.php';
|
||||
require_once __DIR__ . '/../config/helpers.php';
|
||||
|
||||
handlePreflight();
|
||||
|
||||
$method = getMethod();
|
||||
$db = getDB();
|
||||
|
||||
switch ($method) {
|
||||
|
||||
case 'GET':
|
||||
|
||||
if (isset($_GET['id'])) {
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"SELECT * FROM laporan_masyarakat WHERE id=?"
|
||||
);
|
||||
|
||||
$stmt->execute([(int)$_GET['id']]);
|
||||
|
||||
$data = $stmt->fetch();
|
||||
|
||||
if (!$data) {
|
||||
jsonResponse([
|
||||
'error' => 'Laporan tidak ditemukan'
|
||||
], 404);
|
||||
}
|
||||
|
||||
jsonResponse($data);
|
||||
}
|
||||
|
||||
$stmt = $db->query(
|
||||
"SELECT * FROM laporan_masyarakat
|
||||
ORDER BY created_at DESC"
|
||||
);
|
||||
|
||||
jsonResponse($stmt->fetchAll());
|
||||
|
||||
break;
|
||||
|
||||
case 'POST':
|
||||
|
||||
$body = getRequestBody();
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"INSERT INTO laporan_masyarakat
|
||||
(
|
||||
nama_pelapor,
|
||||
no_hp,
|
||||
nama_warga,
|
||||
alamat,
|
||||
latitude,
|
||||
longitude,
|
||||
keterangan,
|
||||
status
|
||||
)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
|
||||
$stmt->execute([
|
||||
$body['nama_pelapor'] ?? '',
|
||||
$body['no_hp'] ?? '',
|
||||
$body['nama_warga'] ?? '',
|
||||
$body['alamat'] ?? '',
|
||||
$body['latitude'] ?? null,
|
||||
$body['longitude'] ?? null,
|
||||
$body['keterangan'] ?? '',
|
||||
'Menunggu Verifikasi'
|
||||
]);
|
||||
|
||||
jsonResponse([
|
||||
'message' => 'Laporan berhasil dikirim'
|
||||
], 201);
|
||||
|
||||
break;
|
||||
|
||||
case 'PUT':
|
||||
|
||||
if (empty($_GET['id'])) {
|
||||
jsonResponse([
|
||||
'error' => 'id wajib'
|
||||
], 400);
|
||||
}
|
||||
|
||||
$body = getRequestBody();
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"UPDATE laporan_masyarakat
|
||||
SET status=?
|
||||
WHERE id=?"
|
||||
);
|
||||
|
||||
$stmt->execute([
|
||||
$body['status'],
|
||||
(int)$_GET['id']
|
||||
]);
|
||||
|
||||
jsonResponse([
|
||||
'message' => 'Status laporan diperbarui'
|
||||
]);
|
||||
|
||||
break;
|
||||
|
||||
case 'DELETE':
|
||||
|
||||
if (empty($_GET['id'])) {
|
||||
jsonResponse([
|
||||
'error' => 'id wajib'
|
||||
], 400);
|
||||
}
|
||||
|
||||
$stmt = $db->prepare(
|
||||
"DELETE FROM laporan_masyarakat
|
||||
WHERE id=?"
|
||||
);
|
||||
|
||||
$stmt->execute([
|
||||
(int)$_GET['id']
|
||||
]);
|
||||
|
||||
jsonResponse([
|
||||
'message' => 'Laporan dihapus'
|
||||
]);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
jsonResponse([
|
||||
'error' => 'Method not allowed'
|
||||
], 405);
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api/parcels.php — CRUD for land parcel polygons
|
||||
// Ownership: SHM, HGB, HGU, HP
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../config/db.php';
|
||||
require_once __DIR__ . '/../config/helpers.php';
|
||||
|
||||
handlePreflight();
|
||||
|
||||
$method = getMethod();
|
||||
$db = getDB();
|
||||
|
||||
switch ($method) {
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// GET /api/parcels.php → all parcels as GeoJSON
|
||||
// GET /api/parcels.php?id=N → single parcel
|
||||
// GET /api/parcels.php?type=SHM → filter by ownership
|
||||
// ----------------------------------------------------------
|
||||
case 'GET':
|
||||
if (isset($_GET['id'])) {
|
||||
$stmt = $db->prepare('SELECT * FROM parcels WHERE id = ?');
|
||||
$stmt->execute([(int)$_GET['id']]);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) jsonResponse(['error' => 'Parcel not found'], 404);
|
||||
$row['geojson'] = json_decode($row['geojson'], true);
|
||||
jsonResponse($row);
|
||||
}
|
||||
|
||||
$where = '';
|
||||
$params = [];
|
||||
if (!empty($_GET['type'])) {
|
||||
$where = 'WHERE ownership_type = ?';
|
||||
$params = [$_GET['type']];
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("SELECT * FROM parcels $where ORDER BY created_at DESC");
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
$features = [];
|
||||
foreach ($rows as $row) {
|
||||
$geo = json_decode($row['geojson'], true);
|
||||
$features[] = [
|
||||
'type' => 'Feature',
|
||||
'geometry' => $geo,
|
||||
'properties' => [
|
||||
'id' => $row['id'],
|
||||
'owner_name' => $row['owner_name'],
|
||||
'ownership_type' => $row['ownership_type'],
|
||||
'area_m2' => (float)$row['area_m2'],
|
||||
'description' => $row['description'],
|
||||
'created_at' => $row['created_at'],
|
||||
],
|
||||
];
|
||||
}
|
||||
jsonResponse(['type' => 'FeatureCollection', 'features' => $features]);
|
||||
break;
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// POST /api/parcels.php
|
||||
// Body: { owner_name, ownership_type, area_m2, geojson, description }
|
||||
// ----------------------------------------------------------
|
||||
case 'POST':
|
||||
$body = getRequestBody();
|
||||
|
||||
foreach (['owner_name', 'ownership_type', 'geojson'] as $f) {
|
||||
if (empty($body[$f])) jsonResponse(['error' => "Field '$f' is required"], 422);
|
||||
}
|
||||
|
||||
$allowed = ['SHM', 'HGB', 'HGU', 'HP'];
|
||||
if (!in_array($body['ownership_type'], $allowed, true)) {
|
||||
jsonResponse(['error' => 'Invalid ownership_type'], 422);
|
||||
}
|
||||
|
||||
$geojson = is_array($body['geojson'])
|
||||
? json_encode($body['geojson'])
|
||||
: $body['geojson'];
|
||||
|
||||
$stmt = $db->prepare(
|
||||
'INSERT INTO parcels (owner_name, ownership_type, area_m2, geojson, description)
|
||||
VALUES (?, ?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([
|
||||
$body['owner_name'],
|
||||
$body['ownership_type'],
|
||||
(float)($body['area_m2'] ?? 0),
|
||||
$geojson,
|
||||
$body['description'] ?? null,
|
||||
]);
|
||||
|
||||
$id = (int)$db->lastInsertId();
|
||||
$stmt2 = $db->prepare('SELECT * FROM parcels WHERE id = ?');
|
||||
$stmt2->execute([$id]);
|
||||
$row = $stmt2->fetch();
|
||||
$row['geojson'] = json_decode($row['geojson'], true);
|
||||
jsonResponse($row, 201);
|
||||
break;
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// PUT /api/parcels.php?id=N
|
||||
// ----------------------------------------------------------
|
||||
case 'PUT':
|
||||
if (empty($_GET['id'])) jsonResponse(['error' => 'id required'], 400);
|
||||
$body = getRequestBody();
|
||||
$id = (int)$_GET['id'];
|
||||
|
||||
$check = $db->prepare('SELECT id FROM parcels WHERE id = ?');
|
||||
$check->execute([$id]);
|
||||
if (!$check->fetch()) jsonResponse(['error' => 'Parcel not found'], 404);
|
||||
|
||||
$geojson = is_array($body['geojson'] ?? null)
|
||||
? json_encode($body['geojson'])
|
||||
: ($body['geojson'] ?? '{}');
|
||||
|
||||
$stmt = $db->prepare(
|
||||
'UPDATE parcels SET owner_name=?, ownership_type=?, area_m2=?, geojson=?, description=? WHERE id=?'
|
||||
);
|
||||
$stmt->execute([
|
||||
$body['owner_name'] ?? '',
|
||||
$body['ownership_type'] ?? 'SHM',
|
||||
(float)($body['area_m2'] ?? 0),
|
||||
$geojson,
|
||||
$body['description'] ?? null,
|
||||
$id,
|
||||
]);
|
||||
|
||||
$stmt2 = $db->prepare('SELECT * FROM parcels WHERE id = ?');
|
||||
$stmt2->execute([$id]);
|
||||
$row = $stmt2->fetch();
|
||||
$row['geojson'] = json_decode($row['geojson'], true);
|
||||
jsonResponse($row);
|
||||
break;
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// DELETE /api/parcels.php?id=N
|
||||
// ----------------------------------------------------------
|
||||
case 'DELETE':
|
||||
if (empty($_GET['id'])) jsonResponse(['error' => 'id required'], 400);
|
||||
$id = (int)$_GET['id'];
|
||||
$stmt = $db->prepare('DELETE FROM parcels WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
if ($stmt->rowCount() === 0) jsonResponse(['error' => 'Parcel not found'], 404);
|
||||
jsonResponse(['message' => 'Parcel deleted', 'id' => $id]);
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
||||
}
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api/points.php — CRUD for point features
|
||||
// Supports: SPBU, Mosque, Poor Population
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../config/db.php';
|
||||
require_once __DIR__ . '/../config/helpers.php';
|
||||
|
||||
handlePreflight();
|
||||
|
||||
$method = getMethod();
|
||||
$db = getDB();
|
||||
|
||||
switch ($method) {
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// GET /api/points.php → list all points
|
||||
// GET /api/points.php?id=N → single point
|
||||
// GET /api/points.php?category=spbu → filter by category
|
||||
// ----------------------------------------------------------
|
||||
case 'GET':
|
||||
if (isset($_GET['id'])) {
|
||||
$stmt = $db->prepare('SELECT * FROM points WHERE id = ?');
|
||||
$stmt->execute([(int)$_GET['id']]);
|
||||
$row = $stmt->fetch();
|
||||
if ($row) {
|
||||
jsonResponse($row);
|
||||
} else {
|
||||
jsonResponse(['error' => 'Point not found'], 404);
|
||||
}
|
||||
}
|
||||
|
||||
$where = '';
|
||||
$params = [];
|
||||
if (!empty($_GET['category'])) {
|
||||
$where = 'WHERE category = ?';
|
||||
$params = [$_GET['category']];
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("SELECT * FROM points $where ORDER BY created_at DESC");
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
// Return as GeoJSON FeatureCollection
|
||||
$features = [];
|
||||
foreach ($rows as $row) {
|
||||
$features[] = [
|
||||
'type' => 'Feature',
|
||||
'geometry' => [
|
||||
'type' => 'Point',
|
||||
'coordinates' => [(float)$row['longitude'], (float)$row['latitude']],
|
||||
],
|
||||
'properties' => [
|
||||
'id' => $row['id'],
|
||||
'name' => $row['name'],
|
||||
'category' => $row['category'],
|
||||
'subtype' => $row['subtype'],
|
||||
'description' => $row['description'],
|
||||
'created_at' => $row['created_at'],
|
||||
'tanggal_lahir' => $row['tanggal_lahir'],
|
||||
'pendidikan' => $row['pendidikan'],
|
||||
'pekerjaan' => $row['pekerjaan'],
|
||||
'jumlah_tanggungan' => $row['jumlah_tanggungan'],
|
||||
'riwayat_penyakit' => $row['riwayat_penyakit'],
|
||||
'alamat' => $row['alamat'],
|
||||
'status_verifikasi' => $row['status_verifikasi'],
|
||||
],
|
||||
];
|
||||
}
|
||||
jsonResponse([
|
||||
'type' => 'FeatureCollection',
|
||||
'features' => $features,
|
||||
]);
|
||||
break;
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// POST /api/points.php — create a point
|
||||
// Body: { name, category, subtype, latitude, longitude, description }
|
||||
// ----------------------------------------------------------
|
||||
case 'POST':
|
||||
$body = getRequestBody();
|
||||
|
||||
$required = ['name', 'category', 'latitude', 'longitude'];
|
||||
foreach ($required as $field) {
|
||||
if (empty($body[$field]) && $body[$field] !== 0) {
|
||||
jsonResponse(['error' => "Field '$field' is required"], 422);
|
||||
}
|
||||
}
|
||||
|
||||
$allowed = ['spbu', 'mosque', 'poor'];
|
||||
if (!in_array($body['category'], $allowed, true)) {
|
||||
jsonResponse(['error' => 'Invalid category'], 422);
|
||||
}
|
||||
|
||||
$stmt = $db->prepare(
|
||||
'INSERT INTO points (
|
||||
name,
|
||||
category,
|
||||
subtype,
|
||||
latitude,
|
||||
longitude,
|
||||
description,
|
||||
tanggal_lahir,
|
||||
pendidikan,
|
||||
pekerjaan,
|
||||
jumlah_tanggungan,
|
||||
riwayat_penyakit,
|
||||
alamat,
|
||||
status_verifikasi
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([
|
||||
$body['name'],
|
||||
$body['category'],
|
||||
$body['subtype'] ?? null,
|
||||
(float)$body['latitude'],
|
||||
(float)$body['longitude'],
|
||||
$body['description'] ?? null,
|
||||
|
||||
$body['tanggal_lahir'] ?? null,
|
||||
$body['pendidikan'] ?? null,
|
||||
$body['pekerjaan'] ?? null,
|
||||
$body['jumlah_tanggungan'] ?? null,
|
||||
$body['riwayat_penyakit'] ?? null,
|
||||
$body['alamat'] ?? null,
|
||||
|
||||
$body['status_verifikasi']
|
||||
?? 'Menunggu Verifikasi'
|
||||
]);
|
||||
|
||||
$id = (int)$db->lastInsertId();
|
||||
$stmt2 = $db->prepare('SELECT * FROM points WHERE id = ?');
|
||||
$stmt2->execute([$id]);
|
||||
jsonResponse($stmt2->fetch(), 201);
|
||||
break;
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// PUT /api/points.php?id=N — update a point
|
||||
// Body: { name, category, subtype, latitude, longitude, description }
|
||||
// ----------------------------------------------------------
|
||||
case 'PUT':
|
||||
if (empty($_GET['id'])) {
|
||||
jsonResponse(['error' => 'id is required'], 400);
|
||||
}
|
||||
$body = getRequestBody();
|
||||
$id = (int)$_GET['id'];
|
||||
|
||||
// Check exists
|
||||
$stmt = $db->prepare('SELECT id FROM points WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
if (!$stmt->fetch()) {
|
||||
jsonResponse(['error' => 'Point not found'], 404);
|
||||
}
|
||||
|
||||
$stmt = $db->prepare(
|
||||
'UPDATE points SET
|
||||
name=?,
|
||||
category=?,
|
||||
subtype=?,
|
||||
latitude=?,
|
||||
longitude=?,
|
||||
description=?,
|
||||
tanggal_lahir=?,
|
||||
pendidikan=?,
|
||||
pekerjaan=?,
|
||||
jumlah_tanggungan=?,
|
||||
riwayat_penyakit=?,
|
||||
alamat=?,
|
||||
status_verifikasi=?
|
||||
WHERE id=?'
|
||||
);
|
||||
$stmt->execute([
|
||||
$body['name'] ?? '',
|
||||
$body['category'] ?? 'spbu',
|
||||
$body['subtype'] ?? null,
|
||||
|
||||
(float)($body['latitude'] ?? 0),
|
||||
(float)($body['longitude'] ?? 0),
|
||||
|
||||
$body['description'] ?? null,
|
||||
|
||||
$body['tanggal_lahir'] ?? null,
|
||||
$body['pendidikan'] ?? null,
|
||||
$body['pekerjaan'] ?? null,
|
||||
$body['jumlah_tanggungan'] ?? null,
|
||||
$body['riwayat_penyakit'] ?? null,
|
||||
$body['alamat'] ?? null,
|
||||
|
||||
$body['status_verifikasi']
|
||||
?? 'Menunggu Verifikasi',
|
||||
|
||||
$id
|
||||
]);
|
||||
|
||||
$stmt2 = $db->prepare('SELECT * FROM points WHERE id = ?');
|
||||
$stmt2->execute([$id]);
|
||||
jsonResponse($stmt2->fetch());
|
||||
break;
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// DELETE /api/points.php?id=N — delete a point
|
||||
// ----------------------------------------------------------
|
||||
case 'DELETE':
|
||||
if (empty($_GET['id'])) {
|
||||
jsonResponse(['error' => 'id is required'], 400);
|
||||
}
|
||||
$id = (int)$_GET['id'];
|
||||
$stmt = $db->prepare('DELETE FROM points WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
if ($stmt->rowCount() === 0) {
|
||||
jsonResponse(['error' => 'Point not found'], 404);
|
||||
}
|
||||
jsonResponse(['message' => 'Point deleted', 'id' => $id]);
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// api/roads.php — CRUD for road polylines
|
||||
// Road types: national (red), provincial (orange), district (blue)
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../config/db.php';
|
||||
require_once __DIR__ . '/../config/helpers.php';
|
||||
|
||||
handlePreflight();
|
||||
|
||||
$method = getMethod();
|
||||
$db = getDB();
|
||||
|
||||
switch ($method) {
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// GET /api/roads.php → all roads as GeoJSON
|
||||
// GET /api/roads.php?id=N → single road
|
||||
// GET /api/roads.php?type=national → filter by type
|
||||
// ----------------------------------------------------------
|
||||
case 'GET':
|
||||
if (isset($_GET['id'])) {
|
||||
$stmt = $db->prepare('SELECT * FROM roads WHERE id = ?');
|
||||
$stmt->execute([(int)$_GET['id']]);
|
||||
$row = $stmt->fetch();
|
||||
if (!$row) jsonResponse(['error' => 'Road not found'], 404);
|
||||
$row['geojson'] = json_decode($row['geojson'], true);
|
||||
jsonResponse($row);
|
||||
}
|
||||
|
||||
$where = '';
|
||||
$params = [];
|
||||
if (!empty($_GET['type'])) {
|
||||
$where = 'WHERE road_type = ?';
|
||||
$params = [$_GET['type']];
|
||||
}
|
||||
|
||||
$stmt = $db->prepare("SELECT * FROM roads $where ORDER BY created_at DESC");
|
||||
$stmt->execute($params);
|
||||
$rows = $stmt->fetchAll();
|
||||
|
||||
$features = [];
|
||||
foreach ($rows as $row) {
|
||||
$geo = json_decode($row['geojson'], true);
|
||||
$features[] = [
|
||||
'type' => 'Feature',
|
||||
'geometry' => $geo,
|
||||
'properties' => [
|
||||
'id' => $row['id'],
|
||||
'name' => $row['name'],
|
||||
'road_type' => $row['road_type'],
|
||||
'length_m' => (float)$row['length_m'],
|
||||
'description' => $row['description'],
|
||||
'created_at' => $row['created_at'],
|
||||
],
|
||||
];
|
||||
}
|
||||
jsonResponse(['type' => 'FeatureCollection', 'features' => $features]);
|
||||
break;
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// POST /api/roads.php
|
||||
// Body: { name, road_type, length_m, geojson, description }
|
||||
// ----------------------------------------------------------
|
||||
case 'POST':
|
||||
$body = getRequestBody();
|
||||
|
||||
foreach (['name', 'road_type', 'geojson'] as $f) {
|
||||
if (empty($body[$f])) jsonResponse(['error' => "Field '$f' is required"], 422);
|
||||
}
|
||||
|
||||
$allowed = ['national', 'provincial', 'district'];
|
||||
if (!in_array($body['road_type'], $allowed, true)) {
|
||||
jsonResponse(['error' => 'Invalid road_type'], 422);
|
||||
}
|
||||
|
||||
$geojson = is_array($body['geojson'])
|
||||
? json_encode($body['geojson'])
|
||||
: $body['geojson'];
|
||||
|
||||
$stmt = $db->prepare(
|
||||
'INSERT INTO roads (name, road_type, length_m, geojson, description)
|
||||
VALUES (?, ?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([
|
||||
$body['name'],
|
||||
$body['road_type'],
|
||||
(float)($body['length_m'] ?? 0),
|
||||
$geojson,
|
||||
$body['description'] ?? null,
|
||||
]);
|
||||
|
||||
$id = (int)$db->lastInsertId();
|
||||
$stmt2 = $db->prepare('SELECT * FROM roads WHERE id = ?');
|
||||
$stmt2->execute([$id]);
|
||||
$row = $stmt2->fetch();
|
||||
$row['geojson'] = json_decode($row['geojson'], true);
|
||||
jsonResponse($row, 201);
|
||||
break;
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// PUT /api/roads.php?id=N
|
||||
// ----------------------------------------------------------
|
||||
case 'PUT':
|
||||
if (empty($_GET['id'])) jsonResponse(['error' => 'id required'], 400);
|
||||
$body = getRequestBody();
|
||||
$id = (int)$_GET['id'];
|
||||
|
||||
$check = $db->prepare('SELECT id FROM roads WHERE id = ?');
|
||||
$check->execute([$id]);
|
||||
if (!$check->fetch()) jsonResponse(['error' => 'Road not found'], 404);
|
||||
|
||||
$geojson = is_array($body['geojson'] ?? null)
|
||||
? json_encode($body['geojson'])
|
||||
: ($body['geojson'] ?? '{}');
|
||||
|
||||
$stmt = $db->prepare(
|
||||
'UPDATE roads SET name=?, road_type=?, length_m=?, geojson=?, description=? WHERE id=?'
|
||||
);
|
||||
$stmt->execute([
|
||||
$body['name'] ?? '',
|
||||
$body['road_type'] ?? 'national',
|
||||
(float)($body['length_m'] ?? 0),
|
||||
$geojson,
|
||||
$body['description'] ?? null,
|
||||
$id,
|
||||
]);
|
||||
|
||||
$stmt2 = $db->prepare('SELECT * FROM roads WHERE id = ?');
|
||||
$stmt2->execute([$id]);
|
||||
$row = $stmt2->fetch();
|
||||
$row['geojson'] = json_decode($row['geojson'], true);
|
||||
jsonResponse($row);
|
||||
break;
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// DELETE /api/roads.php?id=N
|
||||
// ----------------------------------------------------------
|
||||
case 'DELETE':
|
||||
if (empty($_GET['id'])) jsonResponse(['error' => 'id required'], 400);
|
||||
$id = (int)$_GET['id'];
|
||||
$stmt = $db->prepare('DELETE FROM roads WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
if ($stmt->rowCount() === 0) jsonResponse(['error' => 'Road not found'], 404);
|
||||
jsonResponse(['message' => 'Road deleted', 'id' => $id]);
|
||||
break;
|
||||
|
||||
default:
|
||||
jsonResponse(['error' => 'Method not allowed'], 405);
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
/* ============================================================
|
||||
WebGIS — Main Stylesheet
|
||||
Aesthetic: Industrial-cartographic, dark sidebar + bright map
|
||||
============================================================ */
|
||||
|
||||
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500;600&display=swap');
|
||||
|
||||
/* ── CSS Variables ─────────────────────────────────────────── */
|
||||
:root {
|
||||
--bg: #0f1117;
|
||||
--sidebar: #161922;
|
||||
--surface: #1e2130;
|
||||
--surface2: #252a3a;
|
||||
--border: #2d3348;
|
||||
--accent: #4ade80;
|
||||
--accent2: #38bdf8;
|
||||
--warn: #fb923c;
|
||||
--danger: #f87171;
|
||||
--text: #e2e8f0;
|
||||
--text-muted: #64748b;
|
||||
--text-dim: #94a3b8;
|
||||
|
||||
/* Road/feature colors */
|
||||
--national: #ef4444;
|
||||
--provincial: #f97316;
|
||||
--district: #3b82f6;
|
||||
--spbu24: #22c55e;
|
||||
--spbuNot: #eab308;
|
||||
--mosque: #a78bfa;
|
||||
--poor: #f43f5e;
|
||||
|
||||
/* Ownership colors */
|
||||
--shm: #10b981;
|
||||
--hgb: #6366f1;
|
||||
--hgu: #f59e0b;
|
||||
--hp: #ec4899;
|
||||
|
||||
--sidebar-w: 340px;
|
||||
--header-h: 52px;
|
||||
--radius: 8px;
|
||||
--font-mono: 'Space Mono', monospace;
|
||||
--font-body: 'DM Sans', sans-serif;
|
||||
--transition: 0.18s ease;
|
||||
}
|
||||
|
||||
/* ── Reset ──────────────────────────────────────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { height: 100%; overflow: hidden; }
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* ── Header ─────────────────────────────────────────────────── */
|
||||
#header {
|
||||
height: var(--header-h);
|
||||
background: var(--sidebar);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
#header .logo {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#header .logo span { color: var(--text-muted); font-weight: 400; }
|
||||
#header .tagline {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
border-left: 1px solid var(--border);
|
||||
padding-left: 12px;
|
||||
}
|
||||
#header .spacer { flex: 1; }
|
||||
#header .status-badge {
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
background: #0f2d1e;
|
||||
color: var(--accent);
|
||||
border: 1px solid #16532d;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
/* ── Layout ─────────────────────────────────────────────────── */
|
||||
#app {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Sidebar ─────────────────────────────────────────────────── */
|
||||
#sidebar {
|
||||
width: var(--sidebar-w);
|
||||
background: var(--sidebar);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#sidebar-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
padding: 10px 6px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text-muted);
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
letter-spacing: 0.04em;
|
||||
transition: var(--transition);
|
||||
border-bottom: 2px solid transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.tab-btn svg { width: 16px; height: 16px; }
|
||||
.tab-btn:hover { color: var(--text); background: var(--surface); }
|
||||
.tab-btn.active {
|
||||
color: var(--accent);
|
||||
border-bottom-color: var(--accent);
|
||||
background: rgba(74,222,128,0.05);
|
||||
}
|
||||
|
||||
#sidebar-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border) transparent;
|
||||
}
|
||||
#sidebar-content::-webkit-scrollbar { width: 4px; }
|
||||
#sidebar-content::-webkit-scrollbar-track { background: transparent; }
|
||||
#sidebar-content::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
|
||||
/* ── Panel (tab content) ─────────────────────────────────────── */
|
||||
.panel { display: none; }
|
||||
.panel.active { display: block; }
|
||||
|
||||
/* ── Section Headings ────────────────────────────────────────── */
|
||||
.section-title {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
margin: 12px 0 8px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
/* ── Forms ───────────────────────────────────────────────────── */
|
||||
.form-group { margin-bottom: 10px; }
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text);
|
||||
font-family: var(--font-body);
|
||||
font-size: 13px;
|
||||
padding: 7px 10px;
|
||||
outline: none;
|
||||
transition: border-color var(--transition);
|
||||
}
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
border-color: var(--accent2);
|
||||
}
|
||||
.form-group input[readonly] {
|
||||
color: var(--text-muted);
|
||||
cursor: not-allowed;
|
||||
background: var(--surface);
|
||||
}
|
||||
.form-group textarea { resize: vertical; min-height: 60px; }
|
||||
.form-group select option { background: var(--surface2); }
|
||||
|
||||
.coord-row { display: flex; gap: 8px; }
|
||||
.coord-row .form-group { flex: 1; }
|
||||
|
||||
/* ── Buttons ─────────────────────────────────────────────────── */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 14px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid transparent;
|
||||
font-family: var(--font-body);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: #0a1a0f;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.btn-primary:hover { background: #22c55e; }
|
||||
.btn-secondary {
|
||||
background: var(--surface2);
|
||||
color: var(--text);
|
||||
border-color: var(--border);
|
||||
}
|
||||
.btn-secondary:hover { background: var(--surface); border-color: var(--text-muted); }
|
||||
.btn-danger {
|
||||
background: transparent;
|
||||
color: var(--danger);
|
||||
border-color: #7f1d1d;
|
||||
}
|
||||
.btn-danger:hover { background: rgba(248,113,113,0.1); }
|
||||
.btn-warn {
|
||||
background: transparent;
|
||||
color: var(--warn);
|
||||
border-color: #7c2d12;
|
||||
}
|
||||
.btn-warn:hover { background: rgba(251,146,60,0.1); }
|
||||
.btn-sm { padding: 4px 10px; font-size: 12px; }
|
||||
.btn-full { width: 100%; justify-content: center; }
|
||||
.btn-row { display: flex; gap: 8px; margin-top: 10px; }
|
||||
|
||||
/* ── Info box ────────────────────────────────────────────────── */
|
||||
.info-box {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.info-box.accent { border-color: var(--accent2); color: var(--accent2); background: rgba(56,189,248,0.05); }
|
||||
.info-box.success { border-color: var(--accent); color: var(--accent); background: rgba(74,222,128,0.05); }
|
||||
.info-box.warn { border-color: var(--warn); color: var(--warn); background: rgba(251,146,60,0.05); }
|
||||
|
||||
/* ── Feature list ────────────────────────────────────────────── */
|
||||
.feature-list { display: flex; flex-direction: column; gap: 6px; }
|
||||
.feature-item {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 8px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
position: relative;
|
||||
}
|
||||
.feature-item:hover { border-color: var(--accent2); background: var(--surface2); }
|
||||
.feature-item.selected { border-color: var(--accent); background: rgba(74,222,128,0.05); }
|
||||
.feature-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.feature-dot.road { border-radius: 2px; height: 4px; width: 20px; }
|
||||
.feature-dot.poly { border-radius: 2px; width: 14px; height: 14px; opacity: 0.7; }
|
||||
.feature-info { flex: 1; min-width: 0; }
|
||||
.feature-name {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.feature-meta {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
margin-top: 1px;
|
||||
}
|
||||
.feature-actions { display: flex; gap: 4px; opacity: 0; transition: var(--transition); }
|
||||
.feature-item:hover .feature-actions { opacity: 1; }
|
||||
|
||||
/* ── Layer control ───────────────────────────────────────────── */
|
||||
.layer-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 7px 10px;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.layer-item:hover { background: var(--surface2); }
|
||||
.layer-cb { width: 16px; height: 16px; accent-color: var(--accent); cursor: pointer; }
|
||||
.layer-swatch {
|
||||
width: 24px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.layer-swatch.circle { width: 14px; height: 14px; border-radius: 50%; }
|
||||
.layer-label { font-size: 13px; color: var(--text-dim); flex: 1; }
|
||||
.layer-count {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
background: var(--surface2);
|
||||
padding: 1px 6px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* ── Map area ────────────────────────────────────────────────── */
|
||||
#map-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
#map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* ── Map toolbar ─────────────────────────────────────────────── */
|
||||
#map-toolbar {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
z-index: 500;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.map-tool-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: var(--sidebar);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text-dim);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
font-size: 16px;
|
||||
}
|
||||
.map-tool-btn:hover { border-color: var(--accent2); color: var(--accent2); }
|
||||
.map-tool-btn.active { border-color: var(--accent); color: var(--accent); background: rgba(74,222,128,0.1); }
|
||||
|
||||
/* ── Toast ───────────────────────────────────────────────────── */
|
||||
#toast-container {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.toast {
|
||||
background: var(--surface2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
min-width: 220px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
|
||||
animation: slideIn 0.2s ease;
|
||||
}
|
||||
.toast.success { border-color: var(--accent); }
|
||||
.toast.error { border-color: var(--danger); }
|
||||
.toast.warn { border-color: var(--warn); }
|
||||
@keyframes slideIn {
|
||||
from { opacity: 0; transform: translateX(20px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
/* ── Modal ───────────────────────────────────────────────────── */
|
||||
#modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
z-index: 8000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
#modal-overlay.open { display: flex; }
|
||||
#modal {
|
||||
background: var(--sidebar);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
width: min(480px, 95vw);
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
padding: 24px;
|
||||
}
|
||||
#modal h3 {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 14px;
|
||||
color: var(--accent);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* ── Leaflet overrides ───────────────────────────────────────── */
|
||||
.leaflet-popup-content-wrapper {
|
||||
background: var(--sidebar) !important;
|
||||
color: var(--text) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
border-radius: var(--radius) !important;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.5) !important;
|
||||
}
|
||||
.leaflet-popup-tip { background: var(--sidebar) !important; }
|
||||
.leaflet-popup-content { font-family: var(--font-body); font-size: 13px; }
|
||||
.popup-title { font-weight: 600; margin-bottom: 6px; color: var(--accent2); }
|
||||
.popup-row { display: flex; gap: 6px; margin-bottom: 3px; }
|
||||
.popup-key { color: var(--text-muted); font-size: 11px; width: 90px; flex-shrink: 0; }
|
||||
.popup-val { color: var(--text); font-size: 12px; }
|
||||
.popup-actions { margin-top: 8px; display: flex; gap: 6px; }
|
||||
|
||||
/* ── Draw toolbar overrides ──────────────────────────────────── */
|
||||
.leaflet-draw-toolbar a {
|
||||
background-color: var(--sidebar) !important;
|
||||
color: var(--text) !important;
|
||||
border-color: var(--border) !important;
|
||||
}
|
||||
.leaflet-draw-toolbar a:hover { background-color: var(--surface) !important; }
|
||||
|
||||
/* ── Coords display ──────────────────────────────────────────── */
|
||||
#coords-display {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
z-index: 500;
|
||||
background: rgba(15,17,23,0.85);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 4px 10px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ── Stats bar ───────────────────────────────────────────────── */
|
||||
#stats-bar {
|
||||
background: var(--surface);
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 16px;
|
||||
gap: 20px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.stat-item { display: flex; gap: 6px; }
|
||||
.stat-label { color: var(--text-muted); }
|
||||
.stat-val { color: var(--text-dim); }
|
||||
|
||||
/* ── Scrollbar (global) ──────────────────────────────────────── */
|
||||
* { scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
|
||||
*::-webkit-scrollbar { width: 4px; }
|
||||
*::-webkit-scrollbar-track { background: transparent; }
|
||||
*::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
|
||||
+1557
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// config/db.php — Database connection (PDO)
|
||||
// ============================================================
|
||||
|
||||
define('DB_HOST', '127.0.0.1'); // ← ganti dari localhost ke 127.0.0.1
|
||||
define('DB_NAME', 'webgis_db'); // ← ganti dari webgis_db ke webgis
|
||||
define('DB_USER', 'root');
|
||||
define('DB_PASS', 'root123');
|
||||
define('DB_CHARSET', 'utf8mb4');
|
||||
|
||||
function getDB(): PDO {
|
||||
static $pdo = null;
|
||||
if ($pdo === null) {
|
||||
$dsn = sprintf('mysql:host=%s;dbname=%s;charset=%s', DB_HOST, DB_NAME, DB_CHARSET);
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
try {
|
||||
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['error' => 'Database connection failed: ' . $e->getMessage()]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
return $pdo;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// config/helpers.php — Shared utility functions
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Set JSON response headers and output data.
|
||||
*/
|
||||
function jsonResponse(mixed $data, int $status = 200): void {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
http_response_code($status);
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return decoded JSON body from the request.
|
||||
*/
|
||||
function getRequestBody(): array {
|
||||
$raw = file_get_contents('php://input');
|
||||
$data = json_decode($raw, true);
|
||||
return is_array($data) ? $data : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTTP request method.
|
||||
*/
|
||||
function getMethod(): string {
|
||||
return strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle CORS preflight.
|
||||
*/
|
||||
function handlePreflight(): void {
|
||||
if (getMethod() === 'OPTIONS') {
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS — Spatial Data Management System</title>
|
||||
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||
<!-- Leaflet Draw CSS -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css">
|
||||
<!-- App CSS -->
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ═══════════════════════ HEADER ═══════════════════════ -->
|
||||
<header id="header">
|
||||
<div class="logo">WEB<span>GIS</span></div>
|
||||
<div class="tagline">Spatial Data Management System · Soreang, West Java</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="status-badge">● LIVE</div>
|
||||
</header>
|
||||
|
||||
<!-- ═══════════════════════ MAIN APP ═══════════════════════ -->
|
||||
<div id="app">
|
||||
|
||||
<!-- ───────── SIDEBAR ───────── -->
|
||||
<aside id="sidebar">
|
||||
|
||||
<!-- Tab Navigation -->
|
||||
<nav id="sidebar-tabs">
|
||||
<button class="tab-btn active" data-tab="layers">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
|
||||
</svg>
|
||||
LAYERS
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="points">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="4"/><circle cx="12" cy="12" r="10" stroke-dasharray="2 3"/>
|
||||
</svg>
|
||||
POINTS
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="roads">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M5 3v18M19 3v18M5 12h14"/>
|
||||
</svg>
|
||||
ROADS
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="parcels">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
<path d="M9 3v18M3 9h18"/>
|
||||
</svg>
|
||||
PARCELS
|
||||
</button>
|
||||
<button class="tab-btn" data-tab="laporan">
|
||||
📢 LAPORAN
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div id="sidebar-content">
|
||||
|
||||
<!-- ════ LAYERS PANEL ════ -->
|
||||
<div class="panel active" id="panel-layers">
|
||||
<div class="section-title">⛽ SPBU — Gas Stations</div>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" class="layer-cb" data-layer="spbu24" checked>
|
||||
<div class="layer-swatch circle" style="background:#22c55e"></div>
|
||||
<span class="layer-label">24 Hours SPBU</span>
|
||||
<span class="layer-count" data-count-layer="spbu24">0</span>
|
||||
</label>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" class="layer-cb" data-layer="spbuNot" checked>
|
||||
<div class="layer-swatch circle" style="background:#eab308"></div>
|
||||
<span class="layer-label">Not 24 Hours SPBU</span>
|
||||
<span class="layer-count" data-count-layer="spbuNot">0</span>
|
||||
</label>
|
||||
|
||||
<div class="section-title">🕌 Other Points</div>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" class="layer-cb" data-layer="mosque" checked>
|
||||
<div class="layer-swatch circle" style="background:#a78bfa"></div>
|
||||
<span class="layer-label">Mosques</span>
|
||||
<span class="layer-count" data-count-layer="mosque">0</span>
|
||||
</label>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" class="layer-cb" data-layer="poor" checked>
|
||||
<div class="layer-swatch circle" style="background:#f43f5e"></div>
|
||||
<span class="layer-label">Poor Population Zones</span>
|
||||
<span class="layer-count" data-count-layer="poor">0</span>
|
||||
</label>
|
||||
|
||||
<div class="section-title">🛣️ Roads</div>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" class="layer-cb" data-layer="national" checked>
|
||||
<div class="layer-swatch" style="background:#ef4444"></div>
|
||||
<span class="layer-label">National Road</span>
|
||||
<span class="layer-count" data-count-layer="national">0</span>
|
||||
</label>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" class="layer-cb" data-layer="provincial" checked>
|
||||
<div class="layer-swatch" style="background:#f97316"></div>
|
||||
<span class="layer-label">Provincial Road</span>
|
||||
<span class="layer-count" data-count-layer="provincial">0</span>
|
||||
</label>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" class="layer-cb" data-layer="district" checked>
|
||||
<div class="layer-swatch" style="background:#3b82f6"></div>
|
||||
<span class="layer-label">District Road</span>
|
||||
<span class="layer-count" data-count-layer="district">0</span>
|
||||
</label>
|
||||
|
||||
<div class="section-title">🗺️ Land Parcels</div>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" class="layer-cb" data-layer="shm" checked>
|
||||
<div class="layer-swatch" style="background:#10b981"></div>
|
||||
<span class="layer-label">SHM — Freehold</span>
|
||||
<span class="layer-count" data-count-layer="shm">0</span>
|
||||
</label>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" class="layer-cb" data-layer="hgb" checked>
|
||||
<div class="layer-swatch" style="background:#6366f1"></div>
|
||||
<span class="layer-label">HGB — Building Rights</span>
|
||||
<span class="layer-count" data-count-layer="hgb">0</span>
|
||||
</label>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" class="layer-cb" data-layer="hgu" checked>
|
||||
<div class="layer-swatch" style="background:#f59e0b"></div>
|
||||
<span class="layer-label">HGU — Business Rights</span>
|
||||
<span class="layer-count" data-count-layer="hgu">0</span>
|
||||
</label>
|
||||
<label class="layer-item">
|
||||
<input type="checkbox" class="layer-cb" data-layer="hp" checked>
|
||||
<div class="layer-swatch" style="background:#ec4899"></div>
|
||||
<span class="layer-label">HP — Usage Rights</span>
|
||||
<span class="layer-count" data-count-layer="hp">0</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- ════ POINTS PANEL ════ -->
|
||||
<div class="panel" id="panel-points">
|
||||
<div class="info-box accent">
|
||||
Click the map to place a point, then fill in the form.
|
||||
</div>
|
||||
<div class="btn-row" style="margin-top:0;margin-bottom:12px">
|
||||
<button class="btn btn-primary btn-full" data-mode="point" onclick="enablePointAdd()">
|
||||
➕ Add Point on Map
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn btn-secondary btn-full"
|
||||
onclick="enableLaporanAdd()"
|
||||
style="margin-top:8px">
|
||||
|
||||
📢 Laporkan Warga Miskin
|
||||
|
||||
</button>
|
||||
|
||||
</div>
|
||||
<div class="section-title">All Points</div>
|
||||
<div class="feature-list" id="point-list">
|
||||
<div class="info-box">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ════ ROADS PANEL ════ -->
|
||||
<div class="panel" id="panel-roads">
|
||||
<div class="info-box accent">
|
||||
Draw a polyline on the map. Length is calculated automatically.
|
||||
</div>
|
||||
<div class="btn-row" style="margin-top:0;margin-bottom:12px">
|
||||
<button class="btn btn-primary btn-full" data-mode="road" onclick="startAddRoad()">
|
||||
✏️ Draw Road on Map
|
||||
</button>
|
||||
</div>
|
||||
<div class="section-title">All Roads</div>
|
||||
<div class="feature-list" id="road-list">
|
||||
<div class="info-box">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ════ PARCELS PANEL ════ -->
|
||||
<div class="panel" id="panel-parcels">
|
||||
<div class="info-box accent">
|
||||
Draw a polygon on the map. Area is calculated automatically.
|
||||
</div>
|
||||
<div class="btn-row" style="margin-top:0;margin-bottom:12px">
|
||||
<button class="btn btn-primary btn-full" data-mode="parcel" onclick="startAddParcel()">
|
||||
🗺️ Draw Parcel on Map
|
||||
</button>
|
||||
</div>
|
||||
<div class="section-title">All Parcels</div>
|
||||
<div class="feature-list" id="parcel-list">
|
||||
<div class="info-box">Loading…</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ════ LAPORAN PANEL ════ -->
|
||||
<div class="panel" id="panel-laporan">
|
||||
|
||||
<div class="info-box accent">
|
||||
Daftar laporan masyarakat yang menunggu verifikasi.
|
||||
</div>
|
||||
|
||||
<div class="section-title">
|
||||
Laporan Masyarakat
|
||||
</div>
|
||||
|
||||
<div class="feature-list" id="laporan-list">
|
||||
<div class="info-box">
|
||||
Loading...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div><!-- /sidebar-content -->
|
||||
</aside>
|
||||
|
||||
|
||||
|
||||
<!-- ───────── MAP CONTAINER ───────── -->
|
||||
<div id="map-container">
|
||||
|
||||
<!-- 🔥 PINDAH KE SINI -->
|
||||
<div style="
|
||||
position:absolute;
|
||||
top:10px;
|
||||
left:50px;
|
||||
z-index:1000;
|
||||
background:white;
|
||||
padding:10px;
|
||||
border-radius:8px;
|
||||
box-shadow:0 2px 6px rgba(0,0,0,0.2);
|
||||
">
|
||||
Radius: <span id="radiusValue">500</span> m<br>
|
||||
<input type="range" id="radiusSlider" min="100" max="2000" value="500">
|
||||
</div>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<!-- Coordinates display -->
|
||||
<div id="coords-display">0.000000, 0.000000</div>
|
||||
|
||||
<!-- Cancel draw hint (shown only during draw mode) -->
|
||||
<div id="draw-hint" style="
|
||||
display:none;
|
||||
position:absolute;
|
||||
top:12px;
|
||||
left:50%;
|
||||
transform:translateX(-50%);
|
||||
z-index:500;
|
||||
background:rgba(15,17,23,0.9);
|
||||
border:1px solid var(--accent2);
|
||||
color:var(--accent2);
|
||||
border-radius:8px;
|
||||
padding:6px 16px;
|
||||
font-size:12px;
|
||||
font-family:'Space Mono',monospace;
|
||||
pointer-events:none;
|
||||
">Press <strong>ESC</strong> to cancel</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /app -->
|
||||
|
||||
<!-- ═══════════════════════ STATS BAR ═══════════════════════ -->
|
||||
<footer id="stats-bar">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">POINTS</span>
|
||||
<span class="stat-val" id="stat-points">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">ROADS</span>
|
||||
<span class="stat-val" id="stat-roads">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">PARCELS</span>
|
||||
<span class="stat-val" id="stat-parcels">0</span>
|
||||
</div>
|
||||
<span style="margin-left:auto;color:var(--text-muted)">© WebGIS · OpenStreetMap</span>
|
||||
</footer>
|
||||
|
||||
<!-- ═══════════════════════ MODAL ═══════════════════════ -->
|
||||
<div id="modal-overlay">
|
||||
<div id="modal"><!-- dynamically filled --></div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════════════ TOAST ═══════════════════════ -->
|
||||
<div id="toast-container"></div>
|
||||
|
||||
<!-- ─── Leaflet JS ─── -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<!-- Leaflet Draw -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
|
||||
<!-- Leaflet Geometry Util (for geodesic area calculation) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/leaflet-geometryutil@0.10.3/src/leaflet.geometryutil.js"></script>
|
||||
<!-- App JS -->
|
||||
<script src="assets/js/app.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,65 @@
|
||||
-- ============================================================
|
||||
-- WebGIS Database Schema
|
||||
-- Supports: Points (SPBU, Mosque, Poor Population),
|
||||
-- Lines (Roads), Polygons (Land Parcels)
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS webgis_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE webgis_db;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- TABLE: points
|
||||
-- Stores all point-type features (SPBU, Mosque, Poor Population)
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS points (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
category ENUM('spbu','mosque','poor') NOT NULL COMMENT 'spbu=Gas Station, mosque=Mosque, poor=Poor Population',
|
||||
subtype VARCHAR(100) DEFAULT NULL COMMENT 'For SPBU: 24hours or not24hours',
|
||||
latitude DECIMAL(10,8) NOT NULL,
|
||||
longitude DECIMAL(11,8) NOT NULL,
|
||||
description TEXT DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- TABLE: roads
|
||||
-- Stores road polylines with GeoJSON geometry
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS roads (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
road_type ENUM('national','provincial','district') NOT NULL,
|
||||
length_m DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT 'Length in meters, auto-calculated by frontend',
|
||||
geojson LONGTEXT NOT NULL COMMENT 'GeoJSON LineString geometry',
|
||||
description TEXT DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- TABLE: parcels
|
||||
-- Stores land parcel polygons with GeoJSON geometry
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS parcels (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
owner_name VARCHAR(255) NOT NULL,
|
||||
ownership_type ENUM('SHM','HGB','HGU','HP') NOT NULL,
|
||||
area_m2 DECIMAL(15,4) NOT NULL DEFAULT 0 COMMENT 'Area in square meters, auto-calculated by frontend',
|
||||
geojson LONGTEXT NOT NULL COMMENT 'GeoJSON Polygon geometry',
|
||||
description TEXT DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Sample seed data (optional)
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO points (name, category, subtype, latitude, longitude, description) VALUES
|
||||
('SPBU Jl. Sudirman', 'spbu', '24hours', -6.9175, 107.6191, 'SPBU di pusat kota'),
|
||||
('SPBU Jl. Asia Afrika', 'spbu', 'not24hours', -6.9214, 107.6062, 'SPBU selatan kota'),
|
||||
('Masjid Agung', 'mosque', NULL, -6.9200, 107.6100, 'Masjid Agung Bandung'),
|
||||
('Masjid Al-Ikhlas', 'mosque', NULL, -6.9150, 107.6250, 'Masjid Kelurahan'),
|
||||
('Lokasi Kemiskinan A', 'poor', NULL, -6.9300, 107.6050, 'Kawasan padat penduduk'),
|
||||
('Lokasi Kemiskinan B', 'poor', NULL, -6.9350, 107.6150, 'RW 07 Kebon Jeruk');
|
||||
@@ -0,0 +1,65 @@
|
||||
-- ============================================================
|
||||
-- WebGIS Database Schema
|
||||
-- Supports: Points (SPBU, Mosque, Poor Population),
|
||||
-- Lines (Roads), Polygons (Land Parcels)
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS webgis_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
USE webgis_db;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- TABLE: points
|
||||
-- Stores all point-type features (SPBU, Mosque, Poor Population)
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS points (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
category ENUM('spbu','mosque','poor') NOT NULL COMMENT 'spbu=Gas Station, mosque=Mosque, poor=Poor Population',
|
||||
subtype VARCHAR(100) DEFAULT NULL COMMENT 'For SPBU: 24hours or not24hours',
|
||||
latitude DECIMAL(10,8) NOT NULL,
|
||||
longitude DECIMAL(11,8) NOT NULL,
|
||||
description TEXT DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- TABLE: roads
|
||||
-- Stores road polylines with GeoJSON geometry
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS roads (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
road_type ENUM('national','provincial','district') NOT NULL,
|
||||
length_m DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT 'Length in meters, auto-calculated by frontend',
|
||||
geojson LONGTEXT NOT NULL COMMENT 'GeoJSON LineString geometry',
|
||||
description TEXT DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- TABLE: parcels
|
||||
-- Stores land parcel polygons with GeoJSON geometry
|
||||
-- ------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS parcels (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
owner_name VARCHAR(255) NOT NULL,
|
||||
ownership_type ENUM('SHM','HGB','HGU','HP') NOT NULL,
|
||||
area_m2 DECIMAL(15,4) NOT NULL DEFAULT 0 COMMENT 'Area in square meters, auto-calculated by frontend',
|
||||
geojson LONGTEXT NOT NULL COMMENT 'GeoJSON Polygon geometry',
|
||||
description TEXT DEFAULT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- Sample seed data (optional)
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO points (name, category, subtype, latitude, longitude, description) VALUES
|
||||
('SPBU Jl. Sudirman', 'spbu', '24hours', -6.9175, 107.6191, 'SPBU di pusat kota'),
|
||||
('SPBU Jl. Asia Afrika', 'spbu', 'not24hours', -6.9214, 107.6062, 'SPBU selatan kota'),
|
||||
('Masjid Agung', 'mosque', NULL, -6.9200, 107.6100, 'Masjid Agung Bandung'),
|
||||
('Masjid Al-Ikhlas', 'mosque', NULL, -6.9150, 107.6250, 'Masjid Kelurahan'),
|
||||
('Lokasi Kemiskinan A', 'poor', NULL, -6.9300, 107.6050, 'Kawasan padat penduduk'),
|
||||
('Lokasi Kemiskinan B', 'poor', NULL, -6.9350, 107.6150, 'RW 07 Kebon Jeruk');
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
# WebGIS — Software Requirements Specification (SRS)
|
||||
## Spatial Data Management System for Poverty Reduction Support
|
||||
|
||||
---
|
||||
|
||||
## 1. INTRODUCTION
|
||||
|
||||
### 1.1 Purpose
|
||||
This document defines the functional and non-functional requirements for a **WebGIS (Web-based Geographic Information System)** application designed to manage spatial data — including point features, road networks, and land parcels — in support of local government decision-making with a focus on poverty reduction programs.
|
||||
|
||||
### 1.2 Scope
|
||||
The system is named **WebGIS-SPD** (Spatial Poverty Data). It allows government officers, planners, and community stakeholders to:
|
||||
- Register, visualize, update, and delete spatial features (points, lines, polygons)
|
||||
- Identify the spatial distribution of poor population areas
|
||||
- Overlay infrastructure data (roads, SPBU) with poverty indicators
|
||||
- Support location-based decision-making for resource allocation
|
||||
|
||||
### 1.3 Intended Audience
|
||||
- Academic reviewers and instructors
|
||||
- Local government (dinas/pemda) administrators
|
||||
- GIS officers and data entry staff
|
||||
- NGO field coordinators
|
||||
|
||||
### 1.4 Definitions
|
||||
| Term | Meaning |
|
||||
|------|---------|
|
||||
| GIS | Geographic Information System |
|
||||
| SPBU | Stasiun Pengisian Bahan Bakar Umum (Gas Station) |
|
||||
| SHM | Sertifikat Hak Milik (Freehold Title) |
|
||||
| HGB | Hak Guna Bangunan (Building Rights) |
|
||||
| HGU | Hak Guna Usaha (Business Rights) |
|
||||
| HP | Hak Pakai (Usage Rights) |
|
||||
| CRUD | Create, Read, Update, Delete |
|
||||
| GeoJSON | Geographic JSON — standard geospatial format |
|
||||
|
||||
---
|
||||
|
||||
## 2. SYSTEM OVERVIEW
|
||||
|
||||
WebGIS-SPD is a web application built with:
|
||||
- **Frontend**: HTML5, CSS3, Vanilla JavaScript, Leaflet.js
|
||||
- **Backend**: PHP (procedural, no framework)
|
||||
- **Database**: MySQL 8+
|
||||
- **Map Tiles**: OpenStreetMap (free, community-maintained)
|
||||
- **Drawing Tools**: Leaflet.draw plugin
|
||||
|
||||
The system follows a **client-server architecture**. All spatial data is stored in MySQL as GeoJSON text and served via RESTful PHP endpoints.
|
||||
|
||||
---
|
||||
|
||||
## 3. FUNCTIONAL REQUIREMENTS
|
||||
|
||||
### FR-01: Point Data Management (SPBU, Mosque, Poor Population)
|
||||
| ID | Requirement |
|
||||
|----|-------------|
|
||||
| FR-01.1 | User can add a point by clicking on the map |
|
||||
| FR-01.2 | Coordinates are captured automatically from click event |
|
||||
| FR-01.3 | Each point has: name, category, optional subtype |
|
||||
| FR-01.4 | SPBU points have a subtype: "24 Hours" or "Not 24 Hours" |
|
||||
| FR-01.5 | User can edit point attributes |
|
||||
| FR-01.6 | User can delete a point |
|
||||
| FR-01.7 | Points display custom icons by category |
|
||||
| FR-01.8 | Clicking a marker shows a popup with attributes and action buttons |
|
||||
|
||||
### FR-02: Road Polyline Management
|
||||
| ID | Requirement |
|
||||
|----|-------------|
|
||||
| FR-02.1 | User draws roads using Leaflet.draw polyline tool |
|
||||
| FR-02.2 | Road length is calculated automatically in meters |
|
||||
| FR-02.3 | Length is NOT manually editable |
|
||||
| FR-02.4 | Road types: National (red), Provincial (orange), District (blue) |
|
||||
| FR-02.5 | Each road stores: name, type, length, GeoJSON geometry |
|
||||
| FR-02.6 | Roads are styled by type using distinct colors |
|
||||
| FR-02.7 | User can edit road name/type |
|
||||
| FR-02.8 | User can delete a road |
|
||||
|
||||
### FR-03: Land Parcel Polygon Management
|
||||
| ID | Requirement |
|
||||
|----|-------------|
|
||||
| FR-03.1 | User draws parcels using Leaflet.draw polygon tool |
|
||||
| FR-03.2 | Parcel area is calculated automatically in m² using geodesic formula |
|
||||
| FR-03.3 | Area is NOT manually editable |
|
||||
| FR-03.4 | Ownership types: SHM, HGB, HGU, HP |
|
||||
| FR-03.5 | Each parcel stores: owner name, ownership type, area, GeoJSON geometry |
|
||||
| FR-03.6 | Parcels are styled by ownership type |
|
||||
| FR-03.7 | User can edit parcel attributes |
|
||||
| FR-03.8 | User can delete a parcel |
|
||||
|
||||
### FR-04: Layer Control
|
||||
| ID | Requirement |
|
||||
|----|-------------|
|
||||
| FR-04.1 | Each feature category has an independent toggle checkbox |
|
||||
| FR-04.2 | SPBU is split into two toggleable sub-layers |
|
||||
| FR-04.3 | Roads are split by type into three sub-layers |
|
||||
| FR-04.4 | Parcels are split by ownership into four sub-layers |
|
||||
| FR-04.5 | Layer counts are displayed next to each toggle |
|
||||
|
||||
### FR-05: Data Listing and Navigation
|
||||
| ID | Requirement |
|
||||
|----|-------------|
|
||||
| FR-05.1 | Sidebar shows a scrollable list of all features per type |
|
||||
| FR-05.2 | Clicking a list item zooms the map to that feature |
|
||||
| FR-05.3 | List items have inline Edit and Delete actions |
|
||||
|
||||
---
|
||||
|
||||
## 4. NON-FUNCTIONAL REQUIREMENTS
|
||||
|
||||
| ID | Category | Requirement |
|
||||
|----|----------|-------------|
|
||||
| NFR-01 | Performance | Map renders within 2 seconds on standard broadband |
|
||||
| NFR-02 | Usability | All map interactions require no more than 3 clicks |
|
||||
| NFR-03 | Reliability | API returns structured JSON for all success/error states |
|
||||
| NFR-04 | Security | All inputs are sanitized via PDO prepared statements |
|
||||
| NFR-05 | Scalability | Database schema supports >10,000 features per table |
|
||||
| NFR-06 | Compatibility | Works in modern browsers (Chrome, Firefox, Edge) |
|
||||
| NFR-07 | Maintainability | Code is modular, commented, and follows single responsibility |
|
||||
| NFR-08 | Openness | Uses open-source libraries (Leaflet, OSM) — no licensing cost |
|
||||
|
||||
---
|
||||
|
||||
## 5. USER ROLES
|
||||
|
||||
| Role | Permissions |
|
||||
|------|-------------|
|
||||
| **GIS Admin** | Full CRUD on all layers; manage system configuration |
|
||||
| **Data Entry Officer** | Add and edit features; cannot delete |
|
||||
| **Viewer** | Read-only; can pan/zoom map and view popups |
|
||||
| **Community Supervisor** | Add poor population points; view mosques and infrastructure |
|
||||
|
||||
---
|
||||
|
||||
## 6. USE CASE DESCRIPTIONS
|
||||
|
||||
### UC-01: Add Gas Station (SPBU)
|
||||
- **Actor**: GIS Admin / Data Entry Officer
|
||||
- **Precondition**: User is on the Points tab
|
||||
- **Flow**:
|
||||
1. User clicks "Add Point on Map"
|
||||
2. User clicks a location on the map
|
||||
3. Modal opens with coordinates pre-filled
|
||||
4. User selects category = SPBU, subtype = 24 Hours / Not 24 Hours
|
||||
5. User enters name and optionally description
|
||||
6. User clicks "Add Point"
|
||||
7. Marker appears on map; entry added to list
|
||||
- **Postcondition**: Point stored in database; shown on correct sub-layer
|
||||
|
||||
### UC-02: Draw and Save a Road
|
||||
- **Actor**: GIS Admin
|
||||
- **Precondition**: User is on the Roads tab
|
||||
- **Flow**:
|
||||
1. User clicks "Draw Road on Map"
|
||||
2. User draws polyline by clicking multiple points; double-clicks to finish
|
||||
3. Modal opens with length auto-filled (read-only)
|
||||
4. User enters road name and type
|
||||
5. User saves; road appears on map with correct color
|
||||
- **Postcondition**: Road stored with GeoJSON + calculated length
|
||||
|
||||
### UC-03: Draw and Save a Land Parcel
|
||||
- **Actor**: GIS Admin
|
||||
- **Flow**: Similar to UC-02 but with polygon; area auto-calculated
|
||||
|
||||
### UC-04: Toggle Layer Visibility
|
||||
- **Actor**: Any user
|
||||
- **Flow**: User clicks checkbox next to a layer name; features appear/disappear
|
||||
|
||||
### UC-05: Edit a Feature
|
||||
- **Actor**: GIS Admin / Data Entry
|
||||
- **Flow**: User clicks "✏️" on a list item or in popup → modal opens pre-filled → saves updated data
|
||||
|
||||
### UC-06: Delete a Feature
|
||||
- **Actor**: GIS Admin
|
||||
- **Flow**: User clicks "🗑️" → confirmation dialog → feature removed from map and database
|
||||
|
||||
---
|
||||
|
||||
## 7. BUSINESS PROCESS ANALYSIS
|
||||
|
||||
### Current Process (Without System)
|
||||
1. Field officers collect data on paper forms
|
||||
2. Data is manually entered into Excel spreadsheets
|
||||
3. Reports are produced monthly; no spatial context
|
||||
4. Decision-makers have no map-based view of poverty distribution
|
||||
5. Infrastructure gaps (missing roads, no SPBU access) are not correlated with poverty
|
||||
|
||||
### Improved Process (With WebGIS-SPD)
|
||||
1. Field officers directly enter poor population locations on the web map
|
||||
2. Data is instantly visible to supervisors and planners
|
||||
3. GIS admin overlays road network + poverty zones to identify accessibility gaps
|
||||
4. Land parcel ownership data helps identify available land for social housing
|
||||
5. Mosque locations help identify community focal points for program delivery
|
||||
|
||||
---
|
||||
|
||||
## 8. POVERTY REDUCTION SYSTEM DESIGN
|
||||
|
||||
### 8.1 Actors Involved
|
||||
|
||||
| Actor | Role | Data They Contribute |
|
||||
|-------|------|----------------------|
|
||||
| **Local Government (Dinas Sosial)** | Decision makers, program funders | Program areas, budget allocation zones |
|
||||
| **GIS Officers (BPS/Bappeda)** | Data managers, analysts | Road networks, land parcels, administrative boundaries |
|
||||
| **Community (RW/RT Leaders)** | Ground truth validators | Poor household locations, family counts |
|
||||
| **Mosques / Religious Institutions** | Community focal points | Zakat distribution data, beneficiary lists |
|
||||
| **NGOs / Social Organizations** | Program implementors | Intervention areas, beneficiary tracking |
|
||||
| **Health Centers (Puskesmas)** | Health services data | Malnourishment points, access to healthcare |
|
||||
|
||||
### 8.2 Required Data Attributes Per Actor
|
||||
|
||||
**Poor Population Points** (contributed by Community + Dinas Sosial):
|
||||
- Head of household name
|
||||
- Number of family members
|
||||
- Monthly income (IDR)
|
||||
- Access to clean water: yes/no
|
||||
- Distance to nearest road (m) — calculated from road layer
|
||||
- Distance to nearest SPBU — calculated from point layer
|
||||
- Receiving social assistance: yes/no
|
||||
- Assistance program type (PKH, BPNT, etc.)
|
||||
|
||||
**Road Network** (contributed by GIS Officers):
|
||||
- Road name, type, surface condition
|
||||
- Last maintained date
|
||||
- Accessibility rating (1–5)
|
||||
|
||||
**Land Parcels** (contributed by BPN / GIS Officers):
|
||||
- Ownership type (SHM/HGB/HGU/HP)
|
||||
- Current usage (residential, agricultural, vacant)
|
||||
- Available for social housing: yes/no
|
||||
|
||||
**Mosque Data** (contributed by Religious Affairs):
|
||||
- Name, capacity
|
||||
- Zakat collection amount (IDR/year)
|
||||
- Zakat distribution radius (m)
|
||||
|
||||
### 8.3 Example Use Case: Targeting Poverty Aid
|
||||
|
||||
**Scenario**: Government wants to identify poor households with the worst infrastructure access.
|
||||
|
||||
**System Query**:
|
||||
1. Display all "Poor Population" points
|
||||
2. Toggle on "District Roads" layer
|
||||
3. Visually identify poor zones more than 500m from any road
|
||||
4. Overlay land parcel layer → identify HGU/HP parcels near these zones
|
||||
5. Overlay mosque layer → find nearest mosque to use as aid distribution point
|
||||
|
||||
**Result**: Decision maker can now select 50 priority households based on spatial proximity to infrastructure gaps, identify community delivery channels (mosques), and plan land use for social housing — all from a single map interface.
|
||||
|
||||
### 8.4 How the System Contributes to Poverty Reduction
|
||||
|
||||
| Contribution | Mechanism |
|
||||
|-------------|-----------|
|
||||
| **Targeted Aid Delivery** | Spatial query identifies worst-off zones first |
|
||||
| **Infrastructure Planning** | Road gaps near poor zones inform public works budgets |
|
||||
| **Land Use Planning** | Vacant HP/HGU parcels near poor zones identified for social housing |
|
||||
| **Community Empowerment** | Mosques mapped as distribution hubs for zakat + government aid |
|
||||
| **Evidence-Based Policy** | Spatial statistics replace anecdotal reports |
|
||||
| **Monitoring** | Change detection over time shows if interventions are working |
|
||||
| **Transparency** | Web-based, shareable map increases government accountability |
|
||||
|
||||
---
|
||||
|
||||
## 9. SYSTEM ARCHITECTURE DIAGRAM
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ CLIENT (Browser) │
|
||||
│ ┌────────────┐ ┌─────────────┐ ┌────────────────────┐ │
|
||||
│ │ Leaflet.js │ │ Leaflet │ │ Vanilla JS App │ │
|
||||
│ │ Base Map │ │ Draw Tools │ │ (app.js) │ │
|
||||
│ └─────┬──────┘ └──────┬──────┘ └────────┬───────────┘ │
|
||||
│ └────────────────┴──────────────────┘ │
|
||||
│ │ fetch() HTTP │
|
||||
└───────────────────────────┼──────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────────────┼──────────────────────────────────┐
|
||||
│ SERVER (PHP) │ │
|
||||
│ ┌──────────────────────────────────┐ │ │
|
||||
│ │ api/points.php │ │ │
|
||||
│ │ api/roads.php │◄──┘ │
|
||||
│ │ api/parcels.php │ │
|
||||
│ └───────────────┬──────────────────┘ │
|
||||
│ │ PDO │
|
||||
│ ┌───────────────▼──────────────────┐ │
|
||||
│ │ MySQL Database │ │
|
||||
│ │ tables: points, roads, parcels │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. DATABASE ENTITY RELATIONSHIP
|
||||
|
||||
```
|
||||
POINTS
|
||||
├── id (PK)
|
||||
├── name
|
||||
├── category [spbu|mosque|poor]
|
||||
├── subtype [24hours|not24hours|null]
|
||||
├── latitude, longitude
|
||||
├── description
|
||||
└── timestamps
|
||||
|
||||
ROADS
|
||||
├── id (PK)
|
||||
├── name
|
||||
├── road_type [national|provincial|district]
|
||||
├── length_m (auto-calculated)
|
||||
├── geojson (LineString)
|
||||
├── description
|
||||
└── timestamps
|
||||
|
||||
PARCELS
|
||||
├── id (PK)
|
||||
├── owner_name
|
||||
├── ownership_type [SHM|HGB|HGU|HP]
|
||||
├── area_m2 (auto-calculated)
|
||||
├── geojson (Polygon)
|
||||
├── description
|
||||
└── timestamps
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Document Version: 1.0 | Prepared for Academic Submission | WebGIS-SPD*
|
||||
@@ -0,0 +1,132 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<title>SinergiSpasial</title>
|
||||
|
||||
<link rel="stylesheet" href="assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<div class="logo">
|
||||
🕌 SinergiSpasial
|
||||
</div>
|
||||
|
||||
<nav>
|
||||
<a href="#tentang">Tentang</a>
|
||||
<a href="#fitur">Fitur</a>
|
||||
<a href="#preview">Preview</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section class="hero">
|
||||
|
||||
<div class="hero-content">
|
||||
|
||||
<h1>
|
||||
Sistem Informasi Geografis
|
||||
Berbasis Web untuk Analisis
|
||||
Sebaran Kemiskinan
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
Membantu identifikasi wilayah bantuan
|
||||
berdasarkan jangkauan masjid
|
||||
di Kota Pontianak.
|
||||
</p>
|
||||
|
||||
<a href="dashboard.html" class="btn">
|
||||
Masuk ke Sistem
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="tentang" class="section">
|
||||
|
||||
<h2>Tentang Sistem</h2>
|
||||
|
||||
<p>
|
||||
SinergiSpasial merupakan WebGIS yang
|
||||
dirancang untuk membantu visualisasi
|
||||
dan analisis sebaran penduduk miskin
|
||||
menggunakan pendekatan geospasial
|
||||
berbasis radius jangkauan masjid.
|
||||
</p>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="fitur" class="section">
|
||||
|
||||
<h2>Fitur Unggulan</h2>
|
||||
|
||||
<div class="cards">
|
||||
|
||||
<div class="card">
|
||||
<h3>🕌 Data Masjid</h3>
|
||||
<p>Manajemen lokasi masjid sebagai pusat bantuan.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>👨👩👧👦 Data Penduduk</h3>
|
||||
<p>Pemetaan penduduk miskin berbasis lokasi.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>📍 Analisis Radius</h3>
|
||||
<p>Menentukan jangkauan bantuan secara otomatis.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section id="preview" class="section">
|
||||
|
||||
<h2>Preview Sistem</h2>
|
||||
|
||||
<img src="assets/img/preview.png"
|
||||
alt="Preview Dashboard"
|
||||
class="preview">
|
||||
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
|
||||
<h2>Alur Analisis</h2>
|
||||
|
||||
<div class="flow">
|
||||
|
||||
<div>Pilih Masjid</div>
|
||||
<div>↓</div>
|
||||
|
||||
<div>Tentukan Radius</div>
|
||||
<div>↓</div>
|
||||
|
||||
<div>Hitung Jarak</div>
|
||||
<div>↓</div>
|
||||
|
||||
<div>Identifikasi Penduduk</div>
|
||||
<div>↓</div>
|
||||
|
||||
<div>Rekomendasi Bantuan</div>
|
||||
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
|
||||
<p>
|
||||
© 2026 SinergiSpasial —
|
||||
Sistem Informasi Geografis Kota Pontianak
|
||||
</p>
|
||||
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user