commit semua
This commit is contained in:
+28
-5
@@ -1,10 +1,33 @@
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
dist-ssr/
|
||||
|
||||
# Vite
|
||||
.vite/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# Editor
|
||||
.vscode/
|
||||
.idea/
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# XAMPP / PHP logs
|
||||
*.log
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -1,148 +1,99 @@
|
||||
# Tugas WebGIS — PHP & Leaflet
|
||||
# WebGIS Portal — Pontianak
|
||||
|
||||
Kumpulan tugas WebGIS berbasis PHP + MySQL, dijalankan menggunakan **XAMPP** (Apache + MySQL).
|
||||
Portal pemetaan interaktif berbasis React + Leaflet.js untuk empat proyek WebGIS:
|
||||
|
||||
| # | Proyek | Fitur |
|
||||
| --- | -------------------- | ------------------------------------- |
|
||||
| 01 | SPBU Map | Tambah / hapus / geser marker SPBU |
|
||||
| 02 | WebGIS Jalan & Tanah | Gambar polyline & polygon, CRUD penuh |
|
||||
| 03 | Kepadatan Penduduk | Choropleth per kecamatan Pontianak |
|
||||
| 04 | SPBU + Layer Control | Seperti P01 + toggle layer 24 jam |
|
||||
|
||||
---
|
||||
|
||||
## Cara Menjalankan
|
||||
## Cara Deploy
|
||||
|
||||
### 1. Prasyarat
|
||||
|
||||
- [XAMPP](https://www.apachefriends.org/) sudah terinstall
|
||||
- Apache dan MySQL sudah aktif (buka XAMPP Control Panel → klik **Start** pada keduanya)
|
||||
|
||||
### 2. Letakkan folder project
|
||||
|
||||
Salin folder `tugas/` ke dalam direktori `htdocs` milik XAMPP:
|
||||
|
||||
| OS | Path htdocs |
|
||||
| ------- | ----------------------------------- |
|
||||
| Windows | `C:\xampp\htdocs\tugas\` |
|
||||
| Linux | `/opt/lampp/htdocs/tugas/` |
|
||||
| macOS | `/Applications/XAMPP/htdocs/tugas/` |
|
||||
|
||||
Struktur yang diharapkan:
|
||||
|
||||
```
|
||||
htdocs/
|
||||
└── tugas/
|
||||
├── 01/
|
||||
│ ├── index.php
|
||||
│ └── spbu_map.sql
|
||||
├── 02/
|
||||
│ ├── index.php
|
||||
│ └── webgis_db.sql
|
||||
├── 03/
|
||||
│ ├── index.php
|
||||
│ ├── pontianak.js
|
||||
│ ├── pontianak.json
|
||||
│ └── us-states.js
|
||||
└── 04/
|
||||
├── index.php
|
||||
└── spbu_map.sql
|
||||
```
|
||||
|
||||
### 3. Import database (untuk tugas yang menggunakan database)
|
||||
|
||||
Buka **phpMyAdmin** di `http://localhost/phpmyadmin`, lalu import file SQL:
|
||||
|
||||
| Tugas | File SQL | Database |
|
||||
| ----- | ------------------ | ----------- |
|
||||
| 01 | `01/spbu_map.sql` | `spbu_map` |
|
||||
| 02 | `02/webgis_db.sql` | `webgis_db` |
|
||||
| 04 | `04/spbu_map.sql` | `spbu_map` |
|
||||
|
||||
> Tugas 03 **tidak memerlukan database** — data sudah ada di file `.js` dan `.json`.
|
||||
|
||||
Cara import via phpMyAdmin:
|
||||
|
||||
1. Buka `http://localhost/phpmyadmin`
|
||||
2. Klik tab **Import** di menu atas
|
||||
3. Pilih file `.sql` yang sesuai
|
||||
4. Klik **Go / Kirim**
|
||||
|
||||
Atau bisa juga melalui MySQL CLI:
|
||||
### 1 — Install & jalankan lokal
|
||||
|
||||
```bash
|
||||
mysql -u root -p < tugas/01/spbu_map.sql
|
||||
mysql -u root -p < tugas/02/webgis_db.sql
|
||||
# Masuk ke folder project
|
||||
cd semua-tugas
|
||||
|
||||
# Install dependencies (Node.js ≥ 18 diperlukan)
|
||||
npm install
|
||||
|
||||
# Jalankan development server
|
||||
npm run dev
|
||||
# → Buka http://localhost:5173
|
||||
```
|
||||
|
||||
### 2 — Build untuk produksi
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
# Output ada di folder dist/
|
||||
```
|
||||
|
||||
### 3 — Deploy ke Vercel (gratis, paling mudah)
|
||||
|
||||
```bash
|
||||
npm install -g vercel
|
||||
vercel # ikuti petunjuk, pilih "Vite" framework
|
||||
```
|
||||
|
||||
### 4 — Deploy ke Netlify
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
# Drag-and-drop folder dist/ ke https://app.netlify.com/drop
|
||||
```
|
||||
|
||||
### 5 — Deploy ke GitHub Pages
|
||||
|
||||
```bash
|
||||
# 1. Push project ini ke GitHub repository
|
||||
# 2. Di vite.config.js tambahkan: base: '/<nama-repo>/'
|
||||
# 3. npm run build
|
||||
# 4. Deploy folder dist/ sebagai GitHub Pages
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Daftar Tugas
|
||||
## Database MySQL
|
||||
|
||||
### Tugas 01 — SPBU Map Pontianak
|
||||
File `webgis-portal.sql` berisi schema lengkap untuk semua proyek.
|
||||
|
||||
**URL:** `http://localhost/tugas/01/index.php`
|
||||
**Database:** `spbu_map`
|
||||
|
||||
Peta interaktif SPBU di Pontianak menggunakan Leaflet.js. Fitur:
|
||||
|
||||
- Tambah SPBU dengan klik pada peta (isi nama, nomor WA, status 24 jam)
|
||||
- Marker berwarna: **hijau** = buka 24 jam, **merah** = tidak 24 jam
|
||||
- Drag marker untuk pindah lokasi (otomatis tersimpan)
|
||||
- Hapus data SPBU
|
||||
- Popup dengan info detail dan link WhatsApp
|
||||
|
||||
---
|
||||
|
||||
### Tugas 02 — WebGIS Jalan dan Parsil Tanah
|
||||
|
||||
**URL:** `http://localhost/tugas/02/index.php`
|
||||
**Database:** `webgis_db`
|
||||
|
||||
WebGIS untuk manajemen data spasial jalan dan parsil tanah. Fitur:
|
||||
|
||||
- Gambar **polyline** (jalan) dan **polygon** (parsil tanah) langsung di peta
|
||||
- Undo titik saat menggambar
|
||||
- CRUD atribut: nama, status jalan (Nasional/Provinsi/Kabupaten), status kepemilikan (SHM/HGB/HGU/HP)
|
||||
- Edit geometri (seret titik untuk ubah bentuk)
|
||||
- Kalkulasi otomatis panjang jalan (meter) dan luas tanah (m²)
|
||||
- Legenda warna per status
|
||||
|
||||
---
|
||||
|
||||
### Tugas 03 — Peta Kepadatan Penduduk Pontianak
|
||||
|
||||
**URL:** `http://localhost/tugas/03/index.php`
|
||||
**Database:** Tidak diperlukan
|
||||
|
||||
Peta choropleth kepadatan penduduk per kecamatan di Pontianak menggunakan data GeoJSON lokal. Fitur:
|
||||
|
||||
- Visualisasi gradasi warna berdasarkan kepadatan penduduk
|
||||
- Data koordinat UTM (Zone 49N) dikonversi ke WGS84 via Proj4js
|
||||
- Info panel saat hover kecamatan
|
||||
- Legenda kepadatan
|
||||
|
||||
---
|
||||
|
||||
### Tugas 04 — SPBU Map (Extended)
|
||||
|
||||
**URL:** `http://localhost/tugas/04/index.php`
|
||||
**Database:** `spbu_map` (sama dengan Tugas 01)
|
||||
|
||||
Versi lanjutan dari Tugas 01 dengan fitur tambahan pada tampilan dan fungsionalitas peta SPBU Pontianak.
|
||||
|
||||
---
|
||||
|
||||
## Konfigurasi Database
|
||||
|
||||
Semua file PHP menggunakan koneksi default XAMPP:
|
||||
|
||||
```php
|
||||
$conn = new mysqli("localhost", "root", "", "nama_database");
|
||||
```bash
|
||||
mysql -u root -p < webgis-portal.sql
|
||||
```
|
||||
|
||||
Jika password MySQL Anda berbeda dari default (kosong), ubah parameter ketiga sesuai password Anda pada masing-masing file `index.php`.
|
||||
> **Catatan:** Versi front-end ini menggunakan data statis (embedded).
|
||||
> Untuk menghubungkan ke MySQL nyata, tambahkan REST API (Express.js / Laravel)
|
||||
> dan ganti data di `spbuDataRef` dengan fetch ke endpoint API Anda.
|
||||
|
||||
---
|
||||
|
||||
## Teknologi yang Digunakan
|
||||
## Struktur File
|
||||
|
||||
- **PHP** — backend & CRUD database
|
||||
- **MySQL** — penyimpanan data spasial
|
||||
- **Leaflet.js** — library peta interaktif
|
||||
- **Leaflet-Editable** — edit geometri di peta (Tugas 02)
|
||||
- **Proj4js** — konversi proyeksi koordinat (Tugas 03)
|
||||
- **OpenStreetMap** — tile layer peta dasar
|
||||
```
|
||||
webgis-portal/
|
||||
├── index.html ← entry point HTML
|
||||
├── vite.config.js ← konfigurasi Vite
|
||||
├── package.json ← dependencies
|
||||
├── public/
|
||||
│ └── favicon.svg
|
||||
└── src/
|
||||
├── main.jsx ← React root
|
||||
└── App.jsx ← semua komponen (Landing + 4 proyek)
|
||||
```
|
||||
|
||||
## Stack
|
||||
|
||||
- **React 18** — UI framework
|
||||
- **Vite 5** — build tool
|
||||
- **Leaflet.js 1.9.4** — peta interaktif (dimuat via CDN)
|
||||
- **OpenStreetMap** — tile basemap
|
||||
|
||||
---
|
||||
|
||||
_Dikembangkan untuk Tugas WebGIS · Pontianak, Kalimantan Barat_
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>WebGIS Portal — Pontianak</title>
|
||||
<meta name="description" content="Portal WebGIS interaktif Pontianak, Kalimantan Barat. Peta SPBU, jalan, tanah, dan kepadatan penduduk." />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1719
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "webgis-portal",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"vite": "^5.4.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="8" fill="#1C1C1E"/>
|
||||
<circle cx="16" cy="14" r="5" fill="none" stroke="#0A84FF" stroke-width="2.5"/>
|
||||
<circle cx="16" cy="14" r="2" fill="#0A84FF"/>
|
||||
<path d="M16 19 C16 19 10 24 10 27 Q16 30 22 27 C22 24 16 19 16 19Z" fill="#0A84FF" opacity="0.4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 356 B |
+1797
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
@@ -1,721 +0,0 @@
|
||||
<?php
|
||||
$conn = new mysqli("localhost", "root", "", "spbu_map");
|
||||
|
||||
// HANDLE INSERT
|
||||
if(isset($_POST['nama'])){
|
||||
$nama = $_POST['nama'];
|
||||
$wa = $_POST['wa'];
|
||||
$buka = $_POST['buka'];
|
||||
$lat = $_POST['lat'];
|
||||
$lng = $_POST['lng'];
|
||||
|
||||
$conn->query("INSERT INTO spbu (nama_spbu, nomor_wa, buka_24_jam, latitude, longitude)
|
||||
VALUES ('$nama','$wa','$buka','$lat','$lng')");
|
||||
exit("success");
|
||||
}
|
||||
|
||||
// HANDLE DELETE
|
||||
if(isset($_POST['delete_id'])){
|
||||
$id = intval($_POST['delete_id']);
|
||||
$conn->query("DELETE FROM spbu WHERE id = $id");
|
||||
exit("deleted");
|
||||
}
|
||||
|
||||
// HANDLE MOVE (update lat/lng)
|
||||
if(isset($_POST['move_id'])){
|
||||
$id = intval($_POST['move_id']);
|
||||
$lat = $_POST['move_lat'];
|
||||
$lng = $_POST['move_lng'];
|
||||
$conn->query("UPDATE spbu SET latitude='$lat', longitude='$lng' WHERE id=$id");
|
||||
exit("moved");
|
||||
}
|
||||
|
||||
// HANDLE GET DATA
|
||||
if(isset($_GET['get'])){
|
||||
$result = $conn->query("SELECT * FROM spbu");
|
||||
$data = [];
|
||||
while($row = $result->fetch_assoc()){
|
||||
$data[] = $row;
|
||||
}
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SPBU Map — Pontianak</title>
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--fuel-orange: #FF5722;
|
||||
--fuel-amber: #FFA726;
|
||||
--night: #0D0F14;
|
||||
--panel: #13161E;
|
||||
--panel-border: rgba(255,255,255,0.07);
|
||||
--text: #F0F0F0;
|
||||
--text-muted: #8A8FA0;
|
||||
--green: #00C896;
|
||||
--red: #FF4757;
|
||||
--radius: 14px;
|
||||
--shadow: 0 20px 60px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
background: var(--night);
|
||||
color: var(--text);
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#map {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
filter: brightness(0.9) saturate(0.85);
|
||||
}
|
||||
|
||||
/* ── HEADER ── */
|
||||
#header {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 50px;
|
||||
padding: 10px 22px 10px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
box-shadow: var(--shadow), 0 0 0 1px rgba(255,87,34,0.15);
|
||||
backdrop-filter: blur(20px);
|
||||
animation: slideDown 0.5s cubic-bezier(0.34,1.56,0.64,1) both;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
background: linear-gradient(135deg, var(--fuel-orange), var(--fuel-amber));
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#header h1 {
|
||||
font-family: 'Syne', sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
#header p {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
/* ── LEGEND ── */
|
||||
#legend {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: 20px;
|
||||
z-index: 1000;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 14px 18px;
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(20px);
|
||||
animation: slideUp 0.5s 0.1s cubic-bezier(0.34,1.56,0.64,1) both;
|
||||
}
|
||||
|
||||
#legend h3 {
|
||||
font-family: 'Syne', sans-serif;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
margin-bottom: 7px;
|
||||
font-size: 12.5px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.legend-item:last-child { margin-bottom: 0; }
|
||||
|
||||
.legend-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 8px currentColor;
|
||||
}
|
||||
|
||||
.dot-green { background: var(--green); color: var(--green); }
|
||||
.dot-red { background: var(--red); color: var(--red); }
|
||||
|
||||
/* ── HINT CHIP ── */
|
||||
#hint {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 50px;
|
||||
padding: 10px 16px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(20px);
|
||||
animation: slideUp 0.5s 0.2s cubic-bezier(0.34,1.56,0.64,1) both;
|
||||
}
|
||||
|
||||
.hint-pulse {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--fuel-orange);
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── TOAST ── */
|
||||
#toast {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-10px);
|
||||
z-index: 2000;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 50px;
|
||||
padding: 10px 20px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(20px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s, transform 0.25s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#toast.show {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
.toast-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── LEAFLET POPUP OVERRIDE ── */
|
||||
.leaflet-popup-content-wrapper {
|
||||
background: var(--panel) !important;
|
||||
color: var(--text) !important;
|
||||
border: 1px solid var(--panel-border) !important;
|
||||
border-radius: var(--radius) !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 0 !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
background: var(--panel) !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-close-button {
|
||||
color: var(--text-muted) !important;
|
||||
font-size: 18px !important;
|
||||
top: 10px !important;
|
||||
right: 12px !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-close-button:hover {
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
/* ── POPUP CONTENT ── */
|
||||
.popup-header {
|
||||
background: linear-gradient(135deg, rgba(255,87,34,0.15), rgba(255,167,38,0.08));
|
||||
padding: 16px 18px 14px;
|
||||
border-bottom: 1px solid var(--panel-border);
|
||||
}
|
||||
|
||||
.popup-header .popup-label {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--fuel-orange);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.popup-header .popup-title {
|
||||
font-family: 'Syne', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
padding: 14px 18px 16px;
|
||||
}
|
||||
|
||||
.popup-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 9px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.popup-row:last-child { margin-bottom: 0; }
|
||||
|
||||
.popup-row svg { flex-shrink: 0; opacity: 0.7; }
|
||||
|
||||
.popup-row a { color: var(--fuel-amber); text-decoration: none; }
|
||||
.popup-row a:hover { text-decoration: underline; }
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 50px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-green {
|
||||
background: rgba(0,200,150,0.12);
|
||||
color: var(--green);
|
||||
border: 1px solid rgba(0,200,150,0.2);
|
||||
}
|
||||
|
||||
.badge-red {
|
||||
background: rgba(255,71,87,0.12);
|
||||
color: var(--red);
|
||||
border: 1px solid rgba(255,71,87,0.2);
|
||||
}
|
||||
|
||||
/* ── POPUP FOOTER ── */
|
||||
.popup-footer {
|
||||
border-top: 1px solid var(--panel-border);
|
||||
padding: 10px 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.drag-hint {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: rgba(255,71,87,0.1);
|
||||
color: var(--red);
|
||||
border: 1px solid rgba(255,71,87,0.2);
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.15s;
|
||||
}
|
||||
|
||||
.btn-delete:hover { background: rgba(255,71,87,0.22); transform: translateY(-1px); }
|
||||
.btn-delete:active { transform: translateY(0); }
|
||||
|
||||
/* ── FORM POPUP ── */
|
||||
.form-popup-header {
|
||||
background: linear-gradient(135deg, rgba(255,87,34,0.15), rgba(255,167,38,0.08));
|
||||
padding: 14px 18px 12px;
|
||||
border-bottom: 1px solid var(--panel-border);
|
||||
}
|
||||
|
||||
.form-popup-header .fp-label {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--fuel-orange);
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.form-popup-header .fp-title {
|
||||
font-family: 'Syne', sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.form-popup-body {
|
||||
padding: 14px 18px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.fp-field label {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.fp-field input,
|
||||
.fp-field select {
|
||||
width: 100%;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 8px;
|
||||
padding: 9px 12px;
|
||||
font-size: 13px;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.fp-field input::placeholder { color: rgba(255,255,255,0.2); }
|
||||
|
||||
.fp-field input:focus,
|
||||
.fp-field select:focus {
|
||||
border-color: rgba(255,87,34,0.5);
|
||||
background: rgba(255,87,34,0.05);
|
||||
}
|
||||
|
||||
.fp-field select option { background: #1a1d26; color: var(--text); }
|
||||
|
||||
.fp-btn {
|
||||
width: 100%;
|
||||
background: linear-gradient(135deg, var(--fuel-orange), var(--fuel-amber));
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 11px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-family: 'Syne', sans-serif;
|
||||
letter-spacing: 0.04em;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, transform 0.15s;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.fp-btn:hover { opacity: 0.88; transform: translateY(-1px); }
|
||||
.fp-btn:active { transform: translateY(0); opacity: 1; }
|
||||
|
||||
/* ── ANIMATIONS ── */
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateX(-50%) translateY(-20px); }
|
||||
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.4; transform: scale(0.8); }
|
||||
}
|
||||
|
||||
.leaflet-control-attribution {
|
||||
background: rgba(13,15,20,0.7) !important;
|
||||
color: #666 !important;
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: 6px 0 0 0 !important;
|
||||
}
|
||||
|
||||
.leaflet-control-attribution a { color: #888 !important; }
|
||||
|
||||
.leaflet-control-zoom a {
|
||||
background: var(--panel) !important;
|
||||
color: var(--text) !important;
|
||||
border-color: var(--panel-border) !important;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom a:hover {
|
||||
background: rgba(255,87,34,0.15) !important;
|
||||
color: var(--fuel-orange) !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<div id="header">
|
||||
<div class="header-icon">⛽</div>
|
||||
<div>
|
||||
<h1>SPBU Map</h1>
|
||||
<p>Pontianak, Kalimantan Barat</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="legend">
|
||||
<h3>Keterangan</h3>
|
||||
<div class="legend-item"><div class="legend-dot dot-green"></div>Buka 24 Jam</div>
|
||||
<div class="legend-item"><div class="legend-dot dot-red"></div>Tidak 24 Jam</div>
|
||||
</div>
|
||||
|
||||
<div id="hint">
|
||||
<div class="hint-pulse"></div>
|
||||
Klik peta untuk tambah SPBU
|
||||
</div>
|
||||
|
||||
<div id="toast">
|
||||
<div class="toast-dot" id="toast-dot"></div>
|
||||
<span id="toast-msg"></span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
// ── TOAST ──
|
||||
function showToast(msg, color) {
|
||||
const toast = document.getElementById('toast');
|
||||
document.getElementById('toast-dot').style.background = color || '#00C896';
|
||||
document.getElementById('toast-msg').textContent = msg;
|
||||
toast.classList.add('show');
|
||||
clearTimeout(toast._t);
|
||||
toast._t = setTimeout(() => toast.classList.remove('show'), 2500);
|
||||
}
|
||||
|
||||
// ── ICONS ──
|
||||
const greenIcon = new L.Icon({
|
||||
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-green.png',
|
||||
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
|
||||
iconSize: [25, 41], iconAnchor: [12, 41]
|
||||
});
|
||||
|
||||
const redIcon = new L.Icon({
|
||||
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png',
|
||||
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
|
||||
iconSize: [25, 41], iconAnchor: [12, 41]
|
||||
});
|
||||
|
||||
// ── MAP ──
|
||||
const map = L.map('map').setView([-0.05538, 109.34947], 13);
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
// ── BUILD POPUP HTML ──
|
||||
function buildPopup(d) {
|
||||
const is24 = d.buka_24_jam == 1;
|
||||
return `
|
||||
<div class="popup-header">
|
||||
<div class="popup-label">Stasiun BBM</div>
|
||||
<div class="popup-title">${d.nama_spbu}</div>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<div class="popup-row">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 10.8a19.79 19.79 0 01-3.07-8.63A2 2 0 012 0h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 14h-1.08"/></svg>
|
||||
<a href="https://wa.me/${d.nomor_wa}" target="_blank">${d.nomor_wa}</a>
|
||||
</div>
|
||||
<div class="popup-row">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
<span class="badge ${is24 ? 'badge-green' : 'badge-red'}">${is24 ? '✓ Buka 24 Jam' : '✗ Tidak 24 Jam'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="popup-footer">
|
||||
<span class="drag-hint">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="5 9 2 12 5 15"/><polyline points="9 5 12 2 15 5"/><polyline points="15 19 12 22 9 19"/><polyline points="19 9 22 12 19 15"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="12" y1="2" x2="12" y2="22"/></svg>
|
||||
Tarik untuk pindah
|
||||
</span>
|
||||
<button class="btn-delete" onclick="deleteEntry(${d.id}, this)">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/></svg>
|
||||
Hapus
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ── LOAD & CREATE MARKERS ──
|
||||
// Keep a map of id → marker for deletion
|
||||
const markerMap = {};
|
||||
|
||||
function loadData() {
|
||||
fetch('index.php?get=1')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
data.forEach(d => addMarker(d));
|
||||
});
|
||||
}
|
||||
|
||||
function addMarker(d) {
|
||||
const is24 = d.buka_24_jam == 1;
|
||||
const marker = L.marker([d.latitude, d.longitude], {
|
||||
icon: is24 ? greenIcon : redIcon,
|
||||
draggable: true
|
||||
}).addTo(map);
|
||||
|
||||
marker.bindPopup(buildPopup(d));
|
||||
markerMap[d.id] = marker;
|
||||
|
||||
// DRAG → AUTO SAVE
|
||||
marker.on('dragend', function(e) {
|
||||
map.closePopup();
|
||||
const { lat, lng } = e.target.getLatLng();
|
||||
const fd = new FormData();
|
||||
fd.append('move_id', d.id);
|
||||
fd.append('move_lat', lat);
|
||||
fd.append('move_lng', lng);
|
||||
fetch('index.php', { method: 'POST', body: fd })
|
||||
.then(() => {
|
||||
d.latitude = lat;
|
||||
d.longitude = lng;
|
||||
showToast('Posisi disimpan ✓', '#FFA726');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
loadData();
|
||||
|
||||
// ── DELETE ──
|
||||
function deleteEntry(id) {
|
||||
if (!confirm('Hapus SPBU ini?')) return;
|
||||
const fd = new FormData();
|
||||
fd.append('delete_id', id);
|
||||
fetch('index.php', { method: 'POST', body: fd })
|
||||
.then(() => {
|
||||
map.closePopup();
|
||||
if (markerMap[id]) {
|
||||
map.removeLayer(markerMap[id]);
|
||||
delete markerMap[id];
|
||||
}
|
||||
showToast('SPBU dihapus', '#FF4757');
|
||||
});
|
||||
}
|
||||
|
||||
// ── ADD FORM ──
|
||||
function createForm(lat, lng) {
|
||||
return `
|
||||
<div class="form-popup-header">
|
||||
<div class="fp-label">Tambah Titik</div>
|
||||
<div class="fp-title">SPBU Baru</div>
|
||||
</div>
|
||||
<div class="form-popup-body">
|
||||
<div class="fp-field">
|
||||
<label>Nama SPBU</label>
|
||||
<input id="nama" placeholder="Contoh: Pertamina Sungai Raya">
|
||||
</div>
|
||||
<div class="fp-field">
|
||||
<label>Nomor WhatsApp</label>
|
||||
<input id="wa" placeholder="628xx...">
|
||||
</div>
|
||||
<div class="fp-field">
|
||||
<label>Buka 24 Jam?</label>
|
||||
<select id="buka">
|
||||
<option value="1">✓ Iya, 24 Jam</option>
|
||||
<option value="0">✗ Tidak</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="fp-btn" onclick="submitData(${lat}, ${lng})">Simpan SPBU</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ── SUBMIT ──
|
||||
function submitData(lat, lng) {
|
||||
const nama = document.getElementById('nama').value;
|
||||
const wa = document.getElementById('wa').value;
|
||||
const buka = document.getElementById('buka').value;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('nama', nama);
|
||||
fd.append('wa', wa);
|
||||
fd.append('buka', buka);
|
||||
fd.append('lat', lat);
|
||||
fd.append('lng', lng);
|
||||
|
||||
fetch('index.php', { method: 'POST', body: fd })
|
||||
.then(res => res.text())
|
||||
.then(() => {
|
||||
// Fetch the newly inserted row (last inserted id) by re-fetching all
|
||||
// and finding the one not yet in markerMap
|
||||
fetch('index.php?get=1')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
data.forEach(d => {
|
||||
if (!markerMap[d.id]) addMarker(d);
|
||||
});
|
||||
});
|
||||
map.closePopup();
|
||||
showToast('SPBU berhasil disimpan ✓', '#00C896');
|
||||
});
|
||||
}
|
||||
|
||||
// ── CLICK MAP → ADD FORM ──
|
||||
map.on('click', function(e) {
|
||||
L.popup()
|
||||
.setLatLng(e.latlng)
|
||||
.setContent(createForm(e.latlng.lat, e.latlng.lng))
|
||||
.openOn(map);
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,25 +0,0 @@
|
||||
-- ============================================================
|
||||
-- Database: spbu_map
|
||||
-- Digunakan oleh: tugas/01 dan tugas/04
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS spbu_map
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE spbu_map;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spbu (
|
||||
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||
nama_spbu VARCHAR(255) NOT NULL,
|
||||
nomor_wa VARCHAR(20) NOT NULL,
|
||||
buka_24_jam TINYINT(1) NOT NULL DEFAULT 0,
|
||||
latitude DECIMAL(10,7) NOT NULL,
|
||||
longitude DECIMAL(10,7) NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Data contoh
|
||||
INSERT INTO spbu (nama_spbu, nomor_wa, buka_24_jam, latitude, longitude) VALUES
|
||||
('SPBU Pertamina Sungai Raya', '6281234567890', 1, -0.0553800, 109.3494700),
|
||||
('SPBU Pertamina Ahmad Yani', '6289876543210', 0, -0.0621000, 109.3420000);
|
||||
-1468
File diff suppressed because it is too large
Load Diff
@@ -1,30 +0,0 @@
|
||||
-- ============================================================
|
||||
-- Database: webgis_db
|
||||
-- Digunakan oleh: tugas/02
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS webgis_db
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE webgis_db;
|
||||
|
||||
-- Tabel Jalan (Polyline)
|
||||
CREATE TABLE IF NOT EXISTS jalan (
|
||||
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||
nama_jalan VARCHAR(255) NOT NULL,
|
||||
status_jalan ENUM('Nasional','Provinsi','Kabupaten') NOT NULL DEFAULT 'Kabupaten',
|
||||
panjang_meter FLOAT NOT NULL DEFAULT 0,
|
||||
geojson LONGTEXT NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Tabel Parsil Tanah (Polygon)
|
||||
CREATE TABLE IF NOT EXISTS parsil_tanah (
|
||||
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||
nama_parsil VARCHAR(255) NOT NULL,
|
||||
status_kepemilikan ENUM('SHM','HGB','HGU','HP') NOT NULL DEFAULT 'SHM',
|
||||
luas_m2 FLOAT NOT NULL DEFAULT 0,
|
||||
geojson LONGTEXT NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
@@ -1,163 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base target="_top">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Kepadatan Penduduk Pontianak</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.9.0/proj4.js"></script>
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#map {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
}
|
||||
.info {
|
||||
padding: 6px 8px;
|
||||
font: 14px/16px Arial, Helvetica, sans-serif;
|
||||
background: rgba(255,255,255,0.85);
|
||||
box-shadow: 0 0 15px rgba(0,0,0,0.2);
|
||||
border-radius: 5px;
|
||||
}
|
||||
.info h4 { margin: 0 0 5px; color: #777; }
|
||||
.legend { text-align: left; line-height: 22px; color: #555; }
|
||||
.legend i { width: 18px; height: 18px; float: left; margin-right: 8px; opacity: 0.7; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
<script>
|
||||
// ── 1. Register UTM Zone 49N (covers Pontianak at ~109°E) ──────────────────
|
||||
proj4.defs('EPSG:32649', '+proj=utm +zone=49 +datum=WGS84 +units=m +no_defs');
|
||||
|
||||
// Convert a ring of [easting, northing] pairs → [lng, lat]
|
||||
function convertRings(rings) {
|
||||
return rings.map(ring =>
|
||||
ring.map(([x, y]) => proj4('EPSG:32649', 'WGS84', [x, y]))
|
||||
);
|
||||
}
|
||||
|
||||
// ── 2. Raw data (UTM coords) ───────────────────────────────────────────────
|
||||
var rawData = {
|
||||
type: "FeatureCollection",
|
||||
features: [
|
||||
{ type:"Feature", id:"1", properties:{ name:"Pontianak Selatan", density:5.673 }, geometry:{ type:"Polygon", coordinates:[[[315791.7496880172,-2872.6773100267],[315493.9173600795,-3043.9080895001625],[315326.6521934429,-3151.333180362999],[315140.4188381387,-3281.935194921576],[315066.9918184001,-3327.8058926928916],[314996.8467820659,-3371.6262979517323],[314888.67104393616,-3478.080758171731],[314812.70706351567,-3552.7994604061805],[314746.1257071411,-3618.2893480278017],[314655.6433510417,-3707.2884260803185],[314606.10591966007,-3756.013789919325],[314454.0137261031,-3905.612734831022],[314344.4320643814,-3996.3010035972147],[314259.6778418068,-4066.4424268491857],[314085.032268201,-4210.976689869447],[313840.1791924136,-4413.613711391907],[313536.192724674,-4712.359024276697],[313300.8322324334,-4943.661570138169],[312916.42216176353,-5321.443869765168],[312612.50988089014,-5620.11627486908],[312370.45105957426,-5854.688712254208],[312057.7659053877,-6157.703154576355],[311585.92455374077,-6614.9514189124275],[311333.4393242495,-6891.340090787351],[313474.832878937,-8889.931704557468],[313757.1209584046,-8461.458752544078],[314215.6677178042,-7765.450320278544],[314471.81653076597,-7473.440609211619],[314877.1271904595,-7011.386355436478],[315170.0992892701,-6667.707583206353],[315565.04540335014,-6204.405459439875],[315762.9765556948,-5956.206301724727],[315946.9456108697,-5783.346926725002],[316115.1479424136,-5571.442019498811],[316233.71287194826,-5370.136871324419],[316413.0633267751,-5142.274731615995],[316588.3382888399,-4973.07120414943],[315803.17076956015,-2870.648806607189],[315791.7496880172,-2872.6773100267]]] } },
|
||||
{ type:"Feature", id:"2", properties:{ name:"Pontianak Utara", density:3.642 }, geometry:{ type:"Polygon", coordinates:[[[309688.2266975902,951.2434479881485],[310155.9051329456,933.8867466094162],[310357.96077316906,885.1327602118836],[311332.6877226224,552.8154289115046],[312269.31459587533,306.7524367839651],[313016.67581282463,28.516046978937084],[313225.22203908674,-84.19626616407731],[313546.372705549,-322.5867385597503],[313720.9980547987,-448.5286571102988],[314170.79062105156,-811.0085487379129],[314504.16628780216,-895.1462170118866],[314775.1001630025,-1443.89314784007],[315291.5124306837,-1928.5005853951407],[315734.48124843184,-2324.9574099686342],[316159.40293160826,-2368.349163418803],[316654.17475448735,-2251.9322639187267],[317038.8796905633,-2161.4445829436972],[317913.59393999074,-2046.6151866172168],[318283.48217976745,-2054.023534766595],[318775.15660756174,-2147.859083549327],[319287.81429954246,-2395.9329130315673],[319416.881835456,-2033.8234388107558],[319526.1020538965,-1800.9896398106011],[319533.72206913494,-1723.0961506912936],[319510.86202341504,-1622.342615849966],[319574.3621504167,-1479.2556630090148],[319598.91553285625,-1507.1957188887664],[319482.0752991745,-1853.4830781301353],[319424.3083182946,-874.973119525288],[319320.91374074016,-644.5676602992062],[318847.9647948425,226.72111005606712],[318658.4169157464,989.0401346929066],[316567.76460495684,2999.9040346896763],[314562.85392847005,3541.7717850915287],[312083.8089703787,4381.666798215181],[309688.2266975902,951.2434479881485]]] } },
|
||||
{ type:"Feature", id:"3", properties:{ name:"Pontianak Timur", density:9.196 }, geometry:{ type:"Polygon", coordinates:[[[319887.57044806925,-5373.344392278639],[319702.88422977563,-5132.534790727044],[319580.270434435,-4979.559484159208],[319416.78537397756,-4779.407060143594],[319288.3328264768,-4635.773757029033],[319063.0274988217,-4397.196101115128],[318712.6388119413,-4027.525809986318],[319240.45505877363,-3500.8371123406214],[319482.70326267194,-3416.1079811805625],[319930.7033710063,-3290.5899437818016],[320060.2158619128,-2885.3158597254237],[319668.47127330344,-2832.0761712812755],[319302.34583165945,-2724.1663001296456],[319016.246975861,-2582.2846226671395],[318611.86326987745,-2402.6633110638345],[318312.0934654712,-2349.9022867323624],[317831.7500457056,-2325.396742533832],[317450.6828969274,-2376.0793589098607],[316961.72475783527,-2448.1651844011494],[316290.7174679831,-2617.4144481542617],[315891.63306421525,-2646.1325691096436],[316108.6562515756,-3080.1789438243754],[316374.4062489303,-3648.9273296033716],[316666.3438568887,-4078.0756132950055],[316960.2915036416,-4459.3151686055135],[317511.1983193059,-4982.14799761562],[318003.96436076245,-5498.157720267835],[318525.46364961116,-6295.854170744091],[318997.8492128665,-6873.389132317587],[319200.2395574156,-6725.148423245205],[319464.4036415865,-6506.445073789114],[319929.8147521868,-6173.089029280636],[320230.3441586008,-5948.45374608942],[319892.69977100904,-5382.870277737669],[319887.57044806925,-5373.344392278639]]] } },
|
||||
{ type:"Feature", id:"4", properties:{ name:"Pontianak Tenggara", density:3.103 }, geometry:{ type:"Polygon", coordinates:[[[316509.95719540527,-5101.833566810722],[316277.42321839795,-5368.326560621595],[315586.95250728936,-6227.493567962928],[314650.4343267438,-7312.870467518172],[313943.0478521116,-8266.304411587676],[313570.5827409144,-8808.522535934284],[314482.8144881896,-9756.429398367298],[315386.15532470803,-10595.173154648382],[315969.86388160125,-10324.183934093831],[316202.0711808827,-9921.28120289028],[316389.8178299225,-9632.15192111897],[316955.59155603434,-8811.299556391281],[317370.6954695009,-8105.002669764531],[318021.28465803474,-6941.185008999439],[318253.2629668124,-6576.773915689321],[318236.68267524854,-6280.690804057879],[317914.4077884978,-5744.010290518615],[317403.09039107355,-5217.314926254428],[316771.0250557211,-4765.816074840871],[316509.95719540527,-5101.833566810722]]] } },
|
||||
{ type:"Feature", id:"5", properties:{ name:"Pontianak Kota", density:8.102 }, geometry:{ type:"Polygon", coordinates:[[[310276.47296142485,-3455.9797973638633],[309858.4304809589,-3797.2929687509313],[309527.70056152344,-4114.79357910133],[309256.67931494024,-4394.240588930203],[309517.11724853516,-4686.294799805153],[309900.76403808687,-5093.753967286088],[310421.994140625,-5638.796691895346],[310977.62023925874,-6427.256591796759],[311295.12091064546,-6863.819946288946],[311784.60089111235,-6451.0691528309835],[312297.8936767578,-5916.60974120989],[312922.31152343843,-5310.71270752023],[313407.04170880467,-4843.868031024118],[313851.00109863374,-4424.356750487466],[314247.8766479511,-4080.397766113165],[314713.5443115253,-3659.709350586054],[315263.87866211124,-3191.395935058943],[315803.6297607431,-2876.541137695778],[315533.75421142764,-2532.582153319032],[315086.40909403935,-2251.8851285779383],[314597.12731933594,-1876.4141235346906],[314187.02246093843,-2037.810302733793],[313546.7294311533,-2148.9354858405422],[312935.5408325214,-2270.64410400379],[312366.6854858408,-2413.519348145579],[311906.3095092783,-2564.3322143565165],[311385.0793457031,-2783.936767577892],[310773.8905639658,-3093.4998779299203],[310276.47296142485,-3455.9797973638633]]] } },
|
||||
{ type:"Feature", id:"6", properties:{ name:"Pontianak Barat", density:9.331 }, geometry:{ type:"Polygon", coordinates:[[[314496.5855712909,-1831.4348754876992],[314333.6083123619,-1673.1813620609464],[313976.42009798624,-1469.4517879342893],[313709.19039686024,-1196.9304095584666],[313412.8564708587,-1024.9508989329915],[313156.21012423374,-776.2420681805816],[312690.5425262293,-424.3455310544232],[312129.62473772746,-204.74092517967802],[311526.37353122514,-96.26154155412223],[310896.6639384739,-149.17831405519973],[310303.9960864708,-154.46999130456243],[309732.49494346976,70.42629182199016],[309351.49418146536,70.42629182199016],[309319.74411796685,-400.5329834310105],[309364.7233745931,-715.3877798067406],[309351.49418146536,-974.679965057876],[309232.4314433411,-1506.493528684834],[309176.8688322166,-1879.5567748110043],[309205.97305709217,-2040.952930937754],[308552.45091671497,-2472.2246268143645],[308147.6376070874,-3011.9757063165307],[308430.74233996496,-3734.289650943014],[308827.6181337144,-4363.999243696919],[309142.47293009236,-4490.9994976965245],[309479.5459254207,-4168.759302856284],[310007.24186643306,-3657.238962248084],[310429.9315185547,-3344.854614257114],[310773.8905639658,-3093.4998779299203],[311336.9246401787,-2797.156559649622],[311906.3095092783,-2564.3322143565165],[312525.43572998233,-2360.602661131881],[313125.56844345294,-2227.800214529503],[313970.063720705,-2077.4978637696477],[314456.89801025484,-1908.1641845697304],[314496.5855712909,-1831.4348754876992]]] } }
|
||||
]
|
||||
};
|
||||
|
||||
// ── 3. Reproject every feature to WGS84 ───────────────────────────────────
|
||||
var statesData = {
|
||||
type: "FeatureCollection",
|
||||
features: rawData.features.map(function(f) {
|
||||
return Object.assign({}, f, {
|
||||
geometry: Object.assign({}, f.geometry, {
|
||||
coordinates: convertRings(f.geometry.coordinates)
|
||||
})
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
// ── 4. Map & tiles ─────────────────────────────────────────────────────────
|
||||
const map = L.map('map').setView([-0.03, 109.33], 12);
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
|
||||
// ── 5. Hover info box ──────────────────────────────────────────────────────
|
||||
const info = L.control();
|
||||
info.onAdd = function() {
|
||||
this._div = L.DomUtil.create('div', 'info');
|
||||
this.update();
|
||||
return this._div;
|
||||
};
|
||||
info.update = function(props) {
|
||||
const contents = props
|
||||
? `<b>${props.name}</b><br />${props.density} ribu jiwa / km<sup>2</sup>`
|
||||
: 'Arahkan kursor ke kecamatan';
|
||||
this._div.innerHTML = `<h4>Kepadatan Penduduk Pontianak</h4>${contents}`;
|
||||
};
|
||||
info.addTo(map);
|
||||
|
||||
// ── 6. Three-level colour scale ────────────────────────────────────────────
|
||||
// Rendah (low) : ≤ 5 → light yellow
|
||||
// Sedang (medium) : 5–7 → orange
|
||||
// Tinggi (high) : > 7 → red
|
||||
function getColor(d) {
|
||||
return d > 7 ? '#E31A1C'
|
||||
: d > 5 ? '#FD8D3C'
|
||||
: '#FFEDA0';
|
||||
}
|
||||
|
||||
function style(feature) {
|
||||
return {
|
||||
weight: 2, opacity: 1, color: 'white', dashArray: '3',
|
||||
fillOpacity: 0.7,
|
||||
fillColor: getColor(feature.properties.density)
|
||||
};
|
||||
}
|
||||
|
||||
// ── 7. Interaction ─────────────────────────────────────────────────────────
|
||||
function highlightFeature(e) {
|
||||
const layer = e.target;
|
||||
layer.setStyle({ weight: 5, color: '#666', dashArray: '', fillOpacity: 0.7 });
|
||||
layer.bringToFront();
|
||||
info.update(layer.feature.properties);
|
||||
}
|
||||
|
||||
function resetHighlight(e) {
|
||||
geojson.resetStyle(e.target);
|
||||
info.update();
|
||||
}
|
||||
|
||||
function zoomToFeature(e) {
|
||||
map.fitBounds(e.target.getBounds());
|
||||
}
|
||||
|
||||
function onEachFeature(feature, layer) {
|
||||
layer.on({ mouseover: highlightFeature, mouseout: resetHighlight, click: zoomToFeature });
|
||||
}
|
||||
|
||||
const geojson = L.geoJson(statesData, { style, onEachFeature }).addTo(map);
|
||||
|
||||
// Auto-fit the map to the actual data extent
|
||||
map.fitBounds(geojson.getBounds());
|
||||
|
||||
map.attributionControl.addAttribution(
|
||||
'Data penduduk © <a href="https://pontianakkota.bps.go.id/">BPS Kota Pontianak</a>'
|
||||
);
|
||||
|
||||
// ── 8. Legend ──────────────────────────────────────────────────────────────
|
||||
const legend = L.control({ position: 'bottomright' });
|
||||
legend.onAdd = function() {
|
||||
const div = L.DomUtil.create('div', 'info legend');
|
||||
const levels = [
|
||||
{ threshold: 0, label: '≤ 5 (Rendah)' },
|
||||
{ threshold: 5, label: '5 – 7 (Sedang)' },
|
||||
{ threshold: 7, label: '> 7 (Tinggi)' }
|
||||
];
|
||||
levels.forEach(({ threshold, label }) => {
|
||||
div.innerHTML +=
|
||||
`<i style="background:${getColor(threshold + 0.1)}"></i> ${label}<br>`;
|
||||
});
|
||||
div.innerHTML += '<small>ribu jiwa / km²</small>';
|
||||
return div;
|
||||
};
|
||||
legend.addTo(map);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,59 +0,0 @@
|
||||
var statesData = {
|
||||
type: "FeatureCollection",
|
||||
features: [
|
||||
{
|
||||
type: "Feature",
|
||||
id: "1",
|
||||
properties: { name: "Pontianak Selatan", density: 5.673 },
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [[[315791.7496880172,-2872.6773100267],[315493.9173600795,-3043.9080895001625],[315326.6521934429,-3151.333180362999],[315140.4188381387,-3281.935194921576],[315066.9918184001,-3327.8058926928916],[314996.8467820659,-3371.6262979517323],[314888.67104393616,-3478.080758171731],[314812.70706351567,-3552.7994604061805],[314746.1257071411,-3618.2893480278017],[314655.6433510417,-3707.2884260803185],[314606.10591966007,-3756.013789919325],[314454.0137261031,-3905.612734831022],[314344.4320643814,-3996.3010035972147],[314259.6778418068,-4066.4424268491857],[314085.032268201,-4210.976689869447],[313840.1791924136,-4413.613711391907],[313536.192724674,-4712.359024276697],[313300.8322324334,-4943.661570138169],[312916.42216176353,-5321.443869765168],[312612.50988089014,-5620.11627486908],[312370.45105957426,-5854.688712254208],[312057.7659053877,-6157.703154576355],[311585.92455374077,-6614.9514189124275],[311333.4393242495,-6891.340090787351],[313474.832878937,-8889.931704557468],[313757.1209584046,-8461.458752544078],[314215.6677178042,-7765.450320278544],[314471.81653076597,-7473.440609211619],[314877.1271904595,-7011.386355436478],[315170.0992892701,-6667.707583206353],[315565.04540335014,-6204.405459439875],[315762.9765556948,-5956.206301724727],[315946.9456108697,-5783.346926725002],[316115.1479424136,-5571.442019498811],[316233.71287194826,-5370.136871324419],[316413.0633267751,-5142.274731615995],[316588.3382888399,-4973.07120414943],[315803.17076956015,-2870.648806607189],[315791.7496880172,-2872.6773100267]]]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "Feature",
|
||||
id: "2",
|
||||
properties: { name: "Pontianak Utara", density: 3.642 },
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [[[309688.2266975902,951.2434479881485],[310155.9051329456,933.8867466094162],[310357.96077316906,885.1327602118836],[311332.6877226224,552.8154289115046],[312269.31459587533,306.7524367839651],[313016.67581282463,28.516046978937084],[313225.22203908674,-84.19626616407731],[313546.372705549,-322.5867385597503],[313720.9980547987,-448.5286571102988],[314170.79062105156,-811.0085487379129],[314504.16628780216,-895.1462170118866],[314775.1001630025,-1443.89314784007],[315291.5124306837,-1928.5005853951407],[315734.48124843184,-2324.9574099686342],[316159.40293160826,-2368.349163418803],[316654.17475448735,-2251.9322639187267],[317038.8796905633,-2161.4445829436972],[317913.59393999074,-2046.6151866172168],[318283.48217976745,-2054.023534766595],[318775.15660756174,-2147.859083549327],[319287.81429954246,-2395.9329130315673],[319416.881835456,-2033.8234388107558],[319526.1020538965,-1800.9896398106011],[319533.72206913494,-1723.0961506912936],[319510.86202341504,-1622.342615849966],[319574.3621504167,-1479.2556630090148],[319598.91553285625,-1507.1957188887664],[319482.0752991745,-1853.4830781301353],[319424.3083182946,-874.973119525288],[319320.91374074016,-644.5676602992062],[318847.9647948425,226.72111005606712],[318658.4169157464,989.0401346929066],[316567.76460495684,2999.9040346896763],[314562.85392847005,3541.7717850915287],[312083.8089703787,4381.666798215181],[309688.2266975902,951.2434479881485]]]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "Feature",
|
||||
id: "3",
|
||||
properties: { name: "Pontianak Timur", density: 9.196 },
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [[[319887.57044806925,-5373.344392278639],[319702.88422977563,-5132.534790727044],[319580.270434435,-4979.559484159208],[319416.78537397756,-4779.407060143594],[319288.3328264768,-4635.773757029033],[319063.0274988217,-4397.196101115128],[318712.6388119413,-4027.525809986318],[319240.45505877363,-3500.8371123406214],[319482.70326267194,-3416.1079811805625],[319930.7033710063,-3290.5899437818016],[320060.2158619128,-2885.3158597254237],[319668.47127330344,-2832.0761712812755],[319302.34583165945,-2724.1663001296456],[319016.246975861,-2582.2846226671395],[318611.86326987745,-2402.6633110638345],[318312.0934654712,-2349.9022867323624],[317831.7500457056,-2325.396742533832],[317450.6828969274,-2376.0793589098607],[316961.72475783527,-2448.1651844011494],[316290.7174679831,-2617.4144481542617],[315891.63306421525,-2646.1325691096436],[316108.6562515756,-3080.1789438243754],[316374.4062489303,-3648.9273296033716],[316666.3438568887,-4078.0756132950055],[316960.2915036416,-4459.3151686055135],[317511.1983193059,-4982.14799761562],[318003.96436076245,-5498.157720267835],[318525.46364961116,-6295.854170744091],[318997.8492128665,-6873.389132317587],[319200.2395574156,-6725.148423245205],[319464.4036415865,-6506.445073789114],[319929.8147521868,-6173.089029280636],[320230.3441586008,-5948.45374608942],[319892.69977100904,-5382.870277737669],[319887.57044806925,-5373.344392278639]]]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "Feature",
|
||||
id: "4",
|
||||
properties: { name: "Pontianak Tenggara", density: 3.103 },
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [[[316509.95719540527,-5101.833566810722],[316277.42321839795,-5368.326560621595],[315586.95250728936,-6227.493567962928],[314650.4343267438,-7312.870467518172],[313943.0478521116,-8266.304411587676],[313570.5827409144,-8808.522535934284],[314482.8144881896,-9756.429398367298],[315386.15532470803,-10595.173154648382],[315969.86388160125,-10324.183934093831],[316202.0711808827,-9921.28120289028],[316389.8178299225,-9632.15192111897],[316955.59155603434,-8811.299556391281],[317370.6954695009,-8105.002669764531],[318021.28465803474,-6941.185008999439],[318253.2629668124,-6576.773915689321],[318236.68267524854,-6280.690804057879],[317914.4077884978,-5744.010290518615],[317403.09039107355,-5217.314926254428],[316771.0250557211,-4765.816074840871],[316509.95719540527,-5101.833566810722]]]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "Feature",
|
||||
id: "5",
|
||||
properties: { name: "Pontianak Kota", density: 8.102 },
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [[[310276.47296142485,-3455.9797973638633],[309858.4304809589,-3797.2929687509313],[309527.70056152344,-4114.79357910133],[309256.67931494024,-4394.240588930203],[309517.11724853516,-4686.294799805153],[309900.76403808687,-5093.753967286088],[310421.994140625,-5638.796691895346],[310977.62023925874,-6427.256591796759],[311295.12091064546,-6863.819946288946],[311784.60089111235,-6451.0691528309835],[312297.8936767578,-5916.60974120989],[312922.31152343843,-5310.71270752023],[313407.04170880467,-4843.868031024118],[313851.00109863374,-4424.356750487466],[314247.8766479511,-4080.397766113165],[314713.5443115253,-3659.709350586054],[315263.87866211124,-3191.395935058943],[315803.6297607431,-2876.541137695778],[315533.75421142764,-2532.582153319032],[315086.40909403935,-2251.8851285779383],[314597.12731933594,-1876.4141235346906],[314187.02246093843,-2037.810302733793],[313546.7294311533,-2148.9354858405422],[312935.5408325214,-2270.64410400379],[312366.6854858408,-2413.519348145579],[311906.3095092783,-2564.3322143565165],[311385.0793457031,-2783.936767577892],[310773.8905639658,-3093.4998779299203],[310276.47296142485,-3455.9797973638633]]]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "Feature",
|
||||
id: "6",
|
||||
properties: { name: "Pontianak Barat", density: 9.331 },
|
||||
geometry: {
|
||||
type: "Polygon",
|
||||
coordinates: [[[314496.5855712909,-1831.4348754876992],[314333.6083123619,-1673.1813620609464],[313976.42009798624,-1469.4517879342893],[313709.19039686024,-1196.9304095584666],[313412.8564708587,-1024.9508989329915],[313156.21012423374,-776.2420681805816],[312690.5425262293,-424.3455310544232],[312129.62473772746,-204.74092517967802],[311526.37353122514,-96.26154155412223],[310896.6639384739,-149.17831405519973],[310303.9960864708,-154.46999130456243],[309732.49494346976,70.42629182199016],[309351.49418146536,70.42629182199016],[309319.74411796685,-400.5329834310105],[309364.7233745931,-715.3877798067406],[309351.49418146536,-974.679965057876],[309232.4314433411,-1506.493528684834],[309176.8688322166,-1879.5567748110043],[309205.97305709217,-2040.952930937754],[308552.45091671497,-2472.2246268143645],[308147.6376070874,-3011.9757063165307],[308430.74233996496,-3734.289650943014],[308827.6181337144,-4363.999243696919],[309142.47293009236,-4490.9994976965245],[309479.5459254207,-4168.759302856284],[310007.24186643306,-3657.238962248084],[310429.9315185547,-3344.854614257114],[310773.8905639658,-3093.4998779299203],[311336.9246401787,-2797.156559649622],[311906.3095092783,-2564.3322143565165],[312525.43572998233,-2360.602661131881],[313125.56844345294,-2227.800214529503],[313970.063720705,-2077.4978637696477],[314456.89801025484,-1908.1641845697304],[314496.5855712909,-1831.4348754876992]]]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,750 +0,0 @@
|
||||
<?php
|
||||
$conn = new mysqli("localhost", "root", "", "spbu_map");
|
||||
|
||||
// HANDLE INSERT
|
||||
if(isset($_POST['nama'])){
|
||||
$nama = $_POST['nama'];
|
||||
$wa = $_POST['wa'];
|
||||
$buka = $_POST['buka'];
|
||||
$lat = $_POST['lat'];
|
||||
$lng = $_POST['lng'];
|
||||
|
||||
$conn->query("INSERT INTO spbu (nama_spbu, nomor_wa, buka_24_jam, latitude, longitude)
|
||||
VALUES ('$nama','$wa','$buka','$lat','$lng')");
|
||||
exit("success");
|
||||
}
|
||||
|
||||
// HANDLE DELETE
|
||||
if(isset($_POST['delete_id'])){
|
||||
$id = intval($_POST['delete_id']);
|
||||
$conn->query("DELETE FROM spbu WHERE id = $id");
|
||||
exit("deleted");
|
||||
}
|
||||
|
||||
// HANDLE MOVE (update lat/lng)
|
||||
if(isset($_POST['move_id'])){
|
||||
$id = intval($_POST['move_id']);
|
||||
$lat = $_POST['move_lat'];
|
||||
$lng = $_POST['move_lng'];
|
||||
$conn->query("UPDATE spbu SET latitude='$lat', longitude='$lng' WHERE id=$id");
|
||||
exit("moved");
|
||||
}
|
||||
|
||||
// HANDLE GET DATA
|
||||
if(isset($_GET['get'])){
|
||||
$result = $conn->query("SELECT * FROM spbu");
|
||||
$data = [];
|
||||
while($row = $result->fetch_assoc()){
|
||||
$data[] = $row;
|
||||
}
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SPBU Map — Pontianak</title>
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap" rel="stylesheet">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--fuel-orange: #FF5722;
|
||||
--fuel-amber: #FFA726;
|
||||
--night: #0D0F14;
|
||||
--panel: #13161E;
|
||||
--panel-border: rgba(255,255,255,0.07);
|
||||
--text: #F0F0F0;
|
||||
--text-muted: #8A8FA0;
|
||||
--green: #00C896;
|
||||
--red: #FF4757;
|
||||
--radius: 14px;
|
||||
--shadow: 0 20px 60px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
background: var(--night);
|
||||
color: var(--text);
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#map {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
filter: brightness(0.9) saturate(0.85);
|
||||
}
|
||||
|
||||
/* ── HEADER ── */
|
||||
#header {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 50px;
|
||||
padding: 10px 22px 10px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
box-shadow: var(--shadow), 0 0 0 1px rgba(255,87,34,0.15);
|
||||
backdrop-filter: blur(20px);
|
||||
animation: slideDown 0.5s cubic-bezier(0.34,1.56,0.64,1) both;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.header-icon {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
background: linear-gradient(135deg, var(--fuel-orange), var(--fuel-amber));
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#header h1 {
|
||||
font-family: 'Syne', sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
#header p {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 300;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
/* ── LEGEND ── */
|
||||
#legend {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
left: 20px;
|
||||
z-index: 1000;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 14px 18px;
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(20px);
|
||||
animation: slideUp 0.5s 0.1s cubic-bezier(0.34,1.56,0.64,1) both;
|
||||
}
|
||||
|
||||
#legend h3 {
|
||||
font-family: 'Syne', sans-serif;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
margin-bottom: 7px;
|
||||
font-size: 12.5px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.legend-item:last-child { margin-bottom: 0; }
|
||||
|
||||
.legend-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 8px currentColor;
|
||||
}
|
||||
|
||||
.dot-green { background: var(--green); color: var(--green); }
|
||||
.dot-red { background: var(--red); color: var(--red); }
|
||||
|
||||
/* ── HINT CHIP ── */
|
||||
#hint {
|
||||
position: absolute;
|
||||
bottom: 30px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 50px;
|
||||
padding: 10px 16px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(20px);
|
||||
animation: slideUp 0.5s 0.2s cubic-bezier(0.34,1.56,0.64,1) both;
|
||||
}
|
||||
|
||||
.hint-pulse {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--fuel-orange);
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── TOAST ── */
|
||||
#toast {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-10px);
|
||||
z-index: 2000;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 50px;
|
||||
padding: 10px 20px;
|
||||
font-size: 13px;
|
||||
color: var(--text);
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(20px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.25s, transform 0.25s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#toast.show {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
|
||||
.toast-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── LEAFLET POPUP OVERRIDE ── */
|
||||
.leaflet-popup-content-wrapper {
|
||||
background: var(--panel) !important;
|
||||
color: var(--text) !important;
|
||||
border: 1px solid var(--panel-border) !important;
|
||||
border-radius: var(--radius) !important;
|
||||
box-shadow: var(--shadow) !important;
|
||||
padding: 0 !important;
|
||||
overflow: hidden;
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.leaflet-popup-content {
|
||||
margin: 0 !important;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
background: var(--panel) !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-close-button {
|
||||
color: var(--text-muted) !important;
|
||||
font-size: 18px !important;
|
||||
top: 10px !important;
|
||||
right: 12px !important;
|
||||
}
|
||||
|
||||
.leaflet-popup-close-button:hover {
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
||||
/* ── POPUP CONTENT ── */
|
||||
.popup-header {
|
||||
background: linear-gradient(135deg, rgba(255,87,34,0.15), rgba(255,167,38,0.08));
|
||||
padding: 16px 18px 14px;
|
||||
border-bottom: 1px solid var(--panel-border);
|
||||
}
|
||||
|
||||
.popup-header .popup-label {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--fuel-orange);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.popup-header .popup-title {
|
||||
font-family: 'Syne', sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.popup-body {
|
||||
padding: 14px 18px 16px;
|
||||
}
|
||||
|
||||
.popup-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 9px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.popup-row:last-child { margin-bottom: 0; }
|
||||
|
||||
.popup-row svg { flex-shrink: 0; opacity: 0.7; }
|
||||
|
||||
.popup-row a { color: var(--fuel-amber); text-decoration: none; }
|
||||
.popup-row a:hover { text-decoration: underline; }
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 50px;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-green {
|
||||
background: rgba(0,200,150,0.12);
|
||||
color: var(--green);
|
||||
border: 1px solid rgba(0,200,150,0.2);
|
||||
}
|
||||
|
||||
.badge-red {
|
||||
background: rgba(255,71,87,0.12);
|
||||
color: var(--red);
|
||||
border: 1px solid rgba(255,71,87,0.2);
|
||||
}
|
||||
|
||||
/* ── POPUP FOOTER ── */
|
||||
.popup-footer {
|
||||
border-top: 1px solid var(--panel-border);
|
||||
padding: 10px 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.drag-hint {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: rgba(255,71,87,0.1);
|
||||
color: var(--red);
|
||||
border: 1px solid rgba(255,71,87,0.2);
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.15s;
|
||||
}
|
||||
|
||||
.btn-delete:hover { background: rgba(255,71,87,0.22); transform: translateY(-1px); }
|
||||
.btn-delete:active { transform: translateY(0); }
|
||||
|
||||
/* ── FORM POPUP ── */
|
||||
.form-popup-header {
|
||||
background: linear-gradient(135deg, rgba(255,87,34,0.15), rgba(255,167,38,0.08));
|
||||
padding: 14px 18px 12px;
|
||||
border-bottom: 1px solid var(--panel-border);
|
||||
}
|
||||
|
||||
.form-popup-header .fp-label {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--fuel-orange);
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.form-popup-header .fp-title {
|
||||
font-family: 'Syne', sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.form-popup-body {
|
||||
padding: 14px 18px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.fp-field label {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.fp-field input,
|
||||
.fp-field select {
|
||||
width: 100%;
|
||||
background: rgba(255,255,255,0.05);
|
||||
border: 1px solid var(--panel-border);
|
||||
border-radius: 8px;
|
||||
padding: 9px 12px;
|
||||
font-size: 13px;
|
||||
font-family: 'DM Sans', sans-serif;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.fp-field input::placeholder { color: rgba(255,255,255,0.2); }
|
||||
|
||||
.fp-field input:focus,
|
||||
.fp-field select:focus {
|
||||
border-color: rgba(255,87,34,0.5);
|
||||
background: rgba(255,87,34,0.05);
|
||||
}
|
||||
|
||||
.fp-field select option { background: #1a1d26; color: var(--text); }
|
||||
|
||||
.fp-btn {
|
||||
width: 100%;
|
||||
background: linear-gradient(135deg, var(--fuel-orange), var(--fuel-amber));
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 11px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
font-family: 'Syne', sans-serif;
|
||||
letter-spacing: 0.04em;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, transform 0.15s;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.fp-btn:hover { opacity: 0.88; transform: translateY(-1px); }
|
||||
.fp-btn:active { transform: translateY(0); opacity: 1; }
|
||||
|
||||
/* ── ANIMATIONS ── */
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateX(-50%) translateY(-20px); }
|
||||
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: scale(1); }
|
||||
50% { opacity: 0.4; transform: scale(0.8); }
|
||||
}
|
||||
|
||||
.leaflet-control-attribution {
|
||||
background: rgba(13,15,20,0.7) !important;
|
||||
color: #666 !important;
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: 6px 0 0 0 !important;
|
||||
}
|
||||
|
||||
.leaflet-control-attribution a { color: #888 !important; }
|
||||
|
||||
.leaflet-control-zoom a {
|
||||
background: var(--panel) !important;
|
||||
color: var(--text) !important;
|
||||
border-color: var(--panel-border) !important;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom a:hover {
|
||||
background: rgba(255,87,34,0.15) !important;
|
||||
color: var(--fuel-orange) !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<div id="header">
|
||||
<div class="header-icon">⛽</div>
|
||||
<div>
|
||||
<h1>SPBU Map</h1>
|
||||
<p>Pontianak, Kalimantan Barat</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="legend">
|
||||
<h3>Keterangan</h3>
|
||||
<div class="legend-item"><div class="legend-dot dot-green"></div>Buka 24 Jam</div>
|
||||
<div class="legend-item"><div class="legend-dot dot-red"></div>Tidak 24 Jam</div>
|
||||
</div>
|
||||
|
||||
<div id="hint">
|
||||
<div class="hint-pulse"></div>
|
||||
Klik peta untuk tambah SPBU
|
||||
</div>
|
||||
|
||||
<div id="toast">
|
||||
<div class="toast-dot" id="toast-dot"></div>
|
||||
<span id="toast-msg"></span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
// ── TOAST ──
|
||||
function showToast(msg, color) {
|
||||
const toast = document.getElementById('toast');
|
||||
document.getElementById('toast-dot').style.background = color || '#00C896';
|
||||
document.getElementById('toast-msg').textContent = msg;
|
||||
toast.classList.add('show');
|
||||
clearTimeout(toast._t);
|
||||
toast._t = setTimeout(() => toast.classList.remove('show'), 2500);
|
||||
}
|
||||
|
||||
// ── ICONS ──
|
||||
const greenIcon = new L.Icon({
|
||||
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-green.png',
|
||||
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
|
||||
iconSize: [25, 41], iconAnchor: [12, 41]
|
||||
});
|
||||
|
||||
const redIcon = new L.Icon({
|
||||
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png',
|
||||
shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png',
|
||||
iconSize: [25, 41], iconAnchor: [12, 41]
|
||||
});
|
||||
|
||||
// ── MAP ──
|
||||
const map = L.map('map').setView([-0.05538, 109.34947], 16);
|
||||
|
||||
// ── TILE ──
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19
|
||||
}).addTo(map);
|
||||
|
||||
// ── LAYER GROUPS (MUST BE HERE) ──
|
||||
const layer24 = L.layerGroup();
|
||||
const layerNon24 = L.layerGroup();
|
||||
|
||||
// ── CONTROL ──
|
||||
const overlayMaps = {
|
||||
"SPBU 24 Jam": layer24,
|
||||
"SPBU Tidak 24 Jam": layerNon24
|
||||
};
|
||||
|
||||
L.control.layers(null, overlayMaps, { collapsed: false }).addTo(map);
|
||||
|
||||
// ── DEFAULT SHOW ──
|
||||
layer24.addTo(map);
|
||||
layerNon24.addTo(map);
|
||||
|
||||
// ── BUILD POPUP HTML ──
|
||||
function buildPopup(d) {
|
||||
const is24 = d.buka_24_jam == 1;
|
||||
return `
|
||||
<div class="popup-header">
|
||||
<div class="popup-label">Stasiun BBM</div>
|
||||
<div class="popup-title">${d.nama_spbu}</div>
|
||||
</div>
|
||||
<div class="popup-body">
|
||||
<div class="popup-row">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 10.8a19.79 19.79 0 01-3.07-8.63A2 2 0 012 0h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L6.09 7.91a16 16 0 006 6l1.27-1.27a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 14h-1.08"/></svg>
|
||||
<a href="https://wa.me/${d.nomor_wa}" target="_blank">${d.nomor_wa}</a>
|
||||
</div>
|
||||
<div class="popup-row">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
<span class="badge ${is24 ? 'badge-green' : 'badge-red'}">${is24 ? '✓ Buka 24 Jam' : '✗ Tidak 24 Jam'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="popup-footer">
|
||||
<span class="drag-hint">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="5 9 2 12 5 15"/><polyline points="9 5 12 2 15 5"/><polyline points="15 19 12 22 9 19"/><polyline points="19 9 22 12 19 15"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="12" y1="2" x2="12" y2="22"/></svg>
|
||||
Tarik untuk pindah
|
||||
</span>
|
||||
<button class="btn-delete" onclick="deleteEntry(${d.id}, this)">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4h6v2"/></svg>
|
||||
Hapus
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ── LOAD & CREATE MARKERS ──
|
||||
// Keep a map of id → marker for deletion
|
||||
const markerMap = {};
|
||||
|
||||
function loadData() {
|
||||
// CLEAR OLD MARKERS FIRST
|
||||
layer24.clearLayers();
|
||||
layerNon24.clearLayers();
|
||||
|
||||
fetch('index.php?get=1')
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
data.forEach(d => addMarker(d));
|
||||
});
|
||||
}
|
||||
|
||||
function addMarker(d) {
|
||||
const is24 = d.buka_24_jam == 1;
|
||||
const marker = L.marker([d.latitude, d.longitude], {
|
||||
icon: is24 ? greenIcon : redIcon,
|
||||
draggable: true
|
||||
});
|
||||
|
||||
if (is24) {
|
||||
marker.addTo(layer24);
|
||||
} else {
|
||||
marker.addTo(layerNon24);
|
||||
}
|
||||
|
||||
marker.bindPopup(buildPopup(d));
|
||||
markerMap[d.id] = marker;
|
||||
|
||||
// DRAG → AUTO SAVE
|
||||
marker.on('dragend', function(e) {
|
||||
map.closePopup();
|
||||
const { lat, lng } = e.target.getLatLng();
|
||||
const fd = new FormData();
|
||||
fd.append('move_id', d.id);
|
||||
fd.append('move_lat', lat);
|
||||
fd.append('move_lng', lng);
|
||||
fetch('index.php', { method: 'POST', body: fd })
|
||||
.then(() => {
|
||||
d.latitude = lat;
|
||||
d.longitude = lng;
|
||||
showToast('Posisi disimpan ✓', '#FFA726');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── LAYER GROUPS ──
|
||||
loadData();
|
||||
|
||||
// ── DELETE ──
|
||||
function deleteEntry(id) {
|
||||
if (!confirm('Hapus SPBU ini?')) return;
|
||||
const fd = new FormData();
|
||||
fd.append('delete_id', id);
|
||||
fetch('index.php', { method: 'POST', body: fd })
|
||||
.then(() => {
|
||||
map.closePopup();
|
||||
if (markerMap[id]) {
|
||||
layer24.removeLayer(markerMap[id]);
|
||||
layerNon24.removeLayer(markerMap[id]);
|
||||
delete markerMap[id];
|
||||
}
|
||||
showToast('SPBU dihapus', '#FF4757');
|
||||
});
|
||||
}
|
||||
|
||||
// ── ADD FORM ──
|
||||
function createForm(lat, lng) {
|
||||
return `
|
||||
<div class="form-popup-header">
|
||||
<div class="fp-label">Tambah Titik</div>
|
||||
<div class="fp-title">SPBU Baru</div>
|
||||
</div>
|
||||
<div class="form-popup-body">
|
||||
<div class="fp-field">
|
||||
<label>Nama SPBU</label>
|
||||
<input id="nama" placeholder="Contoh: Pertamina Sungai Raya">
|
||||
</div>
|
||||
<div class="fp-field">
|
||||
<label>Nomor WhatsApp</label>
|
||||
<input id="wa" placeholder="628xx...">
|
||||
</div>
|
||||
<div class="fp-field">
|
||||
<label>Buka 24 Jam?</label>
|
||||
<select id="buka">
|
||||
<option value="1">✓ Iya, 24 Jam</option>
|
||||
<option value="0">✗ Tidak</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="fp-btn" onclick="submitData(${lat}, ${lng})">Simpan SPBU</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ── SUBMIT ──
|
||||
function submitData(lat, lng) {
|
||||
const nama = document.getElementById('nama').value;
|
||||
const wa = document.getElementById('wa').value;
|
||||
const buka = document.getElementById('buka').value;
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append('nama', nama);
|
||||
fd.append('wa', wa);
|
||||
fd.append('buka', buka);
|
||||
fd.append('lat', lat);
|
||||
fd.append('lng', lng);
|
||||
|
||||
fetch('index.php', { method: 'POST', body: fd })
|
||||
.then(res => res.text())
|
||||
.then(() => {
|
||||
// Fetch the newly inserted row (last inserted id) by re-fetching all
|
||||
// and finding the one not yet in markerMap
|
||||
fetch('index.php?get=1')
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
data.forEach(d => {
|
||||
if (!markerMap[d.id]) addMarker(d);
|
||||
});
|
||||
});
|
||||
map.closePopup();
|
||||
showToast('SPBU berhasil disimpan ✓', '#00C896');
|
||||
});
|
||||
}
|
||||
|
||||
// ── CLICK MAP → ADD FORM ──
|
||||
map.on('click', function(e) {
|
||||
L.popup()
|
||||
.setLatLng(e.latlng)
|
||||
.setContent(createForm(e.latlng.lat, e.latlng.lng))
|
||||
.openOn(map);
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,25 +0,0 @@
|
||||
-- ============================================================
|
||||
-- Database: spbu_map
|
||||
-- Digunakan oleh: tugas/01 dan tugas/04
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS spbu_map
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE spbu_map;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS spbu (
|
||||
id INT(11) NOT NULL AUTO_INCREMENT,
|
||||
nama_spbu VARCHAR(255) NOT NULL,
|
||||
nomor_wa VARCHAR(20) NOT NULL,
|
||||
buka_24_jam TINYINT(1) NOT NULL DEFAULT 0,
|
||||
latitude DECIMAL(10,7) NOT NULL,
|
||||
longitude DECIMAL(10,7) NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
-- Data contoh
|
||||
INSERT INTO spbu (nama_spbu, nomor_wa, buka_24_jam, latitude, longitude) VALUES
|
||||
('SPBU Pertamina Sungai Raya', '6281234567890', 1, -0.0553800, 109.3494700),
|
||||
('SPBU Pertamina Ahmad Yani', '6289876543210', 0, -0.0621000, 109.3420000);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
@@ -0,0 +1,209 @@
|
||||
-- ============================================================
|
||||
-- WebGIS Portal — Combined Database Schema
|
||||
-- Covers: Project 01 (SPBU Map), 02 (WebGIS Jalan & Tanah),
|
||||
-- 03 (Kepadatan Penduduk — reference), 04 (SPBU + Layer)
|
||||
--
|
||||
-- Usage:
|
||||
-- mysql -u root -p < webgis-portal.sql
|
||||
-- ============================================================
|
||||
|
||||
-- ============================================================
|
||||
-- 1. spbu_map — used by Project 01 and Project 04
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS spbu_map
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE spbu_map;
|
||||
|
||||
DROP TABLE IF EXISTS spbu;
|
||||
|
||||
CREATE TABLE spbu (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_spbu VARCHAR(200) NOT NULL,
|
||||
nomor_wa VARCHAR(20) NOT NULL DEFAULT '',
|
||||
buka_24_jam TINYINT(1) NOT NULL DEFAULT 0
|
||||
COMMENT '1 = buka 24 jam, 0 = tidak buka 24 jam',
|
||||
latitude DECIMAL(10, 7) NOT NULL,
|
||||
longitude DECIMAL(10, 7) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_buka (buka_24_jam),
|
||||
INDEX idx_coords (latitude, longitude)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci;
|
||||
|
||||
-- Sample data — SPBU di area Pontianak
|
||||
INSERT INTO spbu (nama_spbu, nomor_wa, buka_24_jam, latitude, longitude) VALUES
|
||||
('Pertamina Ahmad Yani', '6281234567890', 1, -0.0500000, 109.3450000),
|
||||
('Pertamina Sungai Raya', '6281234567891', 0, -0.0870000, 109.3620000),
|
||||
('Shell Pontianak Tengah', '6281234567892', 1, -0.0320000, 109.3300000),
|
||||
('Pertamina Tanjung Raya', '6281234567893', 0, -0.0130000, 109.3550000),
|
||||
('Vivo Khatulistiwa', '6281234567894', 1, 0.0020000, 109.3280000),
|
||||
('Pertamina Khatulistiwa', '6281234567895', 0, -0.0650000, 109.3400000),
|
||||
('SPBU Gajah Mada', '6281234567896', 1, -0.0210000, 109.3350000);
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 2. webgis_db — used by Project 02 (Jalan & Tanah)
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS webgis_db
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE webgis_db;
|
||||
|
||||
-- ── 2a. Jalan (polylines) ──────────────────────────────────
|
||||
|
||||
DROP TABLE IF EXISTS jalan;
|
||||
|
||||
CREATE TABLE jalan (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_jalan VARCHAR(200) NOT NULL,
|
||||
status_jalan ENUM('Nasional','Provinsi','Kabupaten')
|
||||
NOT NULL DEFAULT 'Kabupaten',
|
||||
panjang_meter DECIMAL(12, 2) NOT NULL DEFAULT 0.00
|
||||
COMMENT 'Panjang jalan dalam meter, dihitung otomatis',
|
||||
geojson MEDIUMTEXT NOT NULL
|
||||
COMMENT 'GeoJSON LineString geometry (WGS84)',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_status (status_jalan)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci;
|
||||
|
||||
-- Sample jalan data
|
||||
INSERT INTO jalan (nama_jalan, status_jalan, panjang_meter, geojson) VALUES
|
||||
(
|
||||
'Jl. Ahmad Yani',
|
||||
'Nasional',
|
||||
4200.00,
|
||||
'{"type":"LineString","coordinates":[[109.325,-0.045],[109.330,-0.042],[109.335,-0.038],[109.340,-0.034],[109.345,-0.030]]}'
|
||||
),
|
||||
(
|
||||
'Jl. Gajah Mada',
|
||||
'Provinsi',
|
||||
2800.00,
|
||||
'{"type":"LineString","coordinates":[[109.330,-0.020],[109.333,-0.018],[109.336,-0.016],[109.340,-0.014]]}'
|
||||
),
|
||||
(
|
||||
'Jl. Teuku Umar',
|
||||
'Kabupaten',
|
||||
1900.00,
|
||||
'{"type":"LineString","coordinates":[[109.315,-0.055],[109.318,-0.052],[109.322,-0.050],[109.326,-0.048]]}'
|
||||
),
|
||||
(
|
||||
'Jl. Sultan Abdurrahman',
|
||||
'Provinsi',
|
||||
3300.00,
|
||||
'{"type":"LineString","coordinates":[[109.320,-0.028],[109.323,-0.025],[109.327,-0.022],[109.331,-0.019]]}'
|
||||
),
|
||||
(
|
||||
'Jl. Tanjungpura',
|
||||
'Nasional',
|
||||
5100.00,
|
||||
'{"type":"LineString","coordinates":[[109.310,-0.035],[109.315,-0.033],[109.320,-0.031],[109.326,-0.029],[109.333,-0.027]]}'
|
||||
);
|
||||
|
||||
-- ── 2b. Parsil Tanah (polygons) ────────────────────────────
|
||||
|
||||
DROP TABLE IF EXISTS parsil_tanah;
|
||||
|
||||
CREATE TABLE parsil_tanah (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_parsil VARCHAR(200) NOT NULL,
|
||||
status_kepemilikan ENUM('SHM','HGB','HGU','HP')
|
||||
NOT NULL DEFAULT 'SHM',
|
||||
luas_m2 DECIMAL(14, 2) NOT NULL DEFAULT 0.00
|
||||
COMMENT 'Luas parsil dalam meter persegi, dihitung otomatis',
|
||||
geojson MEDIUMTEXT NOT NULL
|
||||
COMMENT 'GeoJSON Polygon geometry (WGS84)',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_status (status_kepemilikan)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci;
|
||||
|
||||
-- Sample parsil data
|
||||
INSERT INTO parsil_tanah (nama_parsil, status_kepemilikan, luas_m2, geojson) VALUES
|
||||
(
|
||||
'Kavling A-01',
|
||||
'SHM',
|
||||
2400.00,
|
||||
'{"type":"Polygon","coordinates":[[[109.328,-0.038],[109.330,-0.038],[109.330,-0.040],[109.328,-0.040],[109.328,-0.038]]]}'
|
||||
),
|
||||
(
|
||||
'Kavling B-03',
|
||||
'HGB',
|
||||
3100.00,
|
||||
'{"type":"Polygon","coordinates":[[[109.340,-0.025],[109.343,-0.025],[109.343,-0.028],[109.340,-0.028],[109.340,-0.025]]]}'
|
||||
),
|
||||
(
|
||||
'Kavling C-07',
|
||||
'HGU',
|
||||
4800.00,
|
||||
'{"type":"Polygon","coordinates":[[[109.318,-0.050],[109.322,-0.050],[109.322,-0.054],[109.318,-0.054],[109.318,-0.050]]]}'
|
||||
),
|
||||
(
|
||||
'Kavling D-02',
|
||||
'HP',
|
||||
1750.00,
|
||||
'{"type":"Polygon","coordinates":[[[109.335,-0.016],[109.337,-0.016],[109.337,-0.018],[109.335,-0.018],[109.335,-0.016]]]}'
|
||||
);
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- 3. kepadatan_db — reference for Project 03
|
||||
-- (geometry is embedded statically in the front-end via
|
||||
-- UTM reprojection; this table stores the attribute data)
|
||||
-- ============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS kepadatan_db
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE kepadatan_db;
|
||||
|
||||
DROP TABLE IF EXISTS kecamatan;
|
||||
|
||||
CREATE TABLE kecamatan (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
nama VARCHAR(100) NOT NULL,
|
||||
density DECIMAL(6, 3) NOT NULL
|
||||
COMMENT 'Kepadatan penduduk: ribu jiwa per km²',
|
||||
color_class ENUM('rendah','sedang','tinggi')
|
||||
NOT NULL
|
||||
COMMENT 'rendah ≤5, sedang 5-7, tinggi >7',
|
||||
geojson MEDIUMTEXT NOT NULL DEFAULT ''
|
||||
COMMENT 'GeoJSON Polygon geometry (WGS84); populasi via front-end UTM reprojection',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_class (color_class)
|
||||
) ENGINE = InnoDB
|
||||
DEFAULT CHARSET = utf8mb4
|
||||
COLLATE = utf8mb4_unicode_ci;
|
||||
|
||||
-- Data kepadatan dari BPS Kota Pontianak
|
||||
INSERT INTO kecamatan (nama, density, color_class) VALUES
|
||||
('Pontianak Selatan', 5.673, 'sedang'),
|
||||
('Pontianak Utara', 3.642, 'rendah'),
|
||||
('Pontianak Timur', 9.196, 'tinggi'),
|
||||
('Pontianak Tenggara', 3.103, 'rendah'),
|
||||
('Pontianak Kota', 8.102, 'tinggi'),
|
||||
('Pontianak Barat', 9.331, 'tinggi');
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- End of schema
|
||||
-- ============================================================
|
||||
Reference in New Issue
Block a user