Initial commit

This commit is contained in:
azgrey
2026-06-05 15:35:19 +07:00
parent e0aad3a893
commit b07e1197aa
12 changed files with 7667 additions and 10 deletions
+10
View File
@@ -0,0 +1,10 @@
# OS
.DS_Store
Thumbs.db
# Editor
.vscode/
.idea/
# XAMPP / PHP logs
*.log
+22 -10
View File
@@ -7,19 +7,22 @@ Kumpulan tugas WebGIS berbasis PHP + MySQL, dijalankan menggunakan **XAMPP** (Ap
## Cara Menjalankan
### 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/`|
| OS | Path htdocs |
| ------- | ----------------------------------- |
| Windows | `C:\xampp\htdocs\tugas\` |
| Linux | `/opt/lampp/htdocs/tugas/` |
| macOS | `/Applications/XAMPP/htdocs/tugas/` |
Struktur yang diharapkan:
```
htdocs/
└── tugas/
@@ -43,21 +46,23 @@ htdocs/
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 | 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:
```bash
mysql -u root -p < tugas/01/spbu_map.sql
mysql -u root -p < tugas/02/webgis_db.sql
@@ -68,10 +73,12 @@ mysql -u root -p < tugas/02/webgis_db.sql
## Daftar Tugas
### Tugas 01 — SPBU Map Pontianak
**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)
@@ -81,10 +88,12 @@ Peta interaktif SPBU di Pontianak menggunakan Leaflet.js. Fitur:
---
### 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)
@@ -95,10 +104,12 @@ WebGIS untuk manajemen data spasial jalan dan parsil tanah. Fitur:
---
### 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
@@ -107,6 +118,7 @@ Peta choropleth kepadatan penduduk per kecamatan di Pontianak menggunakan data G
---
### Tugas 04 — SPBU Map (Extended)
**URL:** `http://localhost/tugas/04/index.php`
**Database:** `spbu_map` (sama dengan Tugas 01)
+721
View File
@@ -0,0 +1,721 @@
<?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>
+25
View File
@@ -0,0 +1,25 @@
-- ============================================================
-- 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
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -0,0 +1,30 @@
-- ============================================================
-- 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;
+163
View File
@@ -0,0 +1,163 @@
<!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: '&copy; <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) : 57 → 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 &copy; <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>
+59
View File
@@ -0,0 +1,59 @@
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
+750
View File
@@ -0,0 +1,750 @@
<?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>
+25
View File
@@ -0,0 +1,25 @@
-- ============================================================
-- 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);