first commit

This commit is contained in:
2026-06-10 22:46:10 +07:00
commit a7cb1f2de0
15 changed files with 4045 additions and 0 deletions
+290
View File
@@ -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
+80
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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);
}
+493
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -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;
}
+46
View File
@@ -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
View File
@@ -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>
+65
View File
@@ -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');
+65
View File
@@ -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
View File
@@ -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 (15)
**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*
+132
View File
@@ -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>