Inital Commit: Landing Page & Folder Tugas-Tugas

This commit is contained in:
Vinlio Andriarles
2026-06-10 19:05:05 +07:00
commit c1dca27e27
24 changed files with 1853 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
FROM php:8.2-apache
RUN docker-php-ext-install mysqli pdo pdo_mysql
COPY . /var/www/html/
RUN chown -R www-data:www-data /var/www/html/
EXPOSE 80
+120
View File
@@ -0,0 +1,120 @@
html, body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
font-family: sans-serif;
background-color: #ecf0f5;
}
.app-container {
display: flex;
height: 100%;
width: 100%;
}
#map {
flex-grow: 1;
height: 100%;
z-index: 1;
}
.popup-content {
font-family: sans-serif;
line-height: 1.6;
min-width: 200px;
}
.btn-delete {
background-color: #ff4d4d;
color: white;
border: none;
padding: 5px;
border-radius: 4px;
cursor: pointer;
width: 48%;
}
.btn-edit {
background-color: #ffc107;
color: black;
border: none;
padding: 5px;
border-radius: 4px;
cursor: pointer;
width: 48%;
}
.btn-save {
background-color: #28a745;
color: white;
border: none;
padding: 8px;
border-radius: 4px;
cursor: pointer;
margin-top: 10px;
width: 100%;
}
.button-group {
display: flex;
justify-content: space-between;
margin-top: 10px;
}
input,
select {
margin-bottom: 5px;
padding: 5px;
width: 95%;
}
#sidebar {
width: 320px;
min-width: 320px;
height: 100%;
background-color: #ffffff;
border-right: 1px solid #ccc;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
padding: 20px;
box-sizing: border-box;
overflow-y: auto;
z-index: 1000;
}
#sidebar h2 {
margin-top: 0;
font-size: 1.4rem;
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}
#sidebar h4 {
margin: 15px 0 5px 0;
color: #34495e;
font-size: 1rem;
}
.search-container {
display: flex;
flex-direction: column;
gap: 5px;
}
.search-container button {
background-color: #3498db;
color: white;
border: none;
padding: 8px 12px;
border-radius: 3px;
cursor: pointer;
width: 100%;
font-weight: bold;
}
.search-container button:hover {
background-color: #2980b9;
}
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
<?php
$koneksi = mysqli_connect("localhost", "root", "", "webgis");
if (!$koneksi) {
die("Koneksi gagal: " . mysqli_connect_error());
}
?>
+289
View File
@@ -0,0 +1,289 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tugas WebGIS - Alvin Andrianto Ciputra</title>
<!-- Google Fonts: Inter -->
<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=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<!-- FontAwesome untuk Ikon -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary-color: #2980b9;
--secondary-color: #8e44ad;
--bg-color: #f4f7f6;
--text-main: #2c3e50;
--text-muted: #7f8c8d;
--card-bg: #ffffff;
}
* {
box-sizing: border-box;
}
html, body {
min-height: 100vh;
width: 100%;
margin: 0;
padding: 0;
font-family: 'Inter', sans-serif;
background-color: var(--bg-color);
background-image: radial-gradient(#d5d8dc 1px, transparent 1px);
background-size: 20px 20px;
color: var(--text-main);
display: flex;
flex-direction: column;
align-items: center;
}
.container {
width: 100%;
max-width: 1100px;
padding: 40px 20px;
}
.header {
text-align: center;
margin-bottom: 50px;
animation: fadeInDown 0.8s ease-out;
}
.header h1 {
font-size: 2.5rem;
font-weight: 800;
margin-bottom: 10px;
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header h4 {
font-size: 1.1rem;
font-weight: 500;
color: var(--text-muted);
margin-top: 0;
margin-bottom: 30px;
letter-spacing: 1px;
text-transform: uppercase;
}
.profile-card {
display: inline-block;
background-color: var(--card-bg);
padding: 20px 40px;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
border-top: 4px solid var(--primary-color);
text-align: left;
}
.cards {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 25px;
animation: fadeInUp 0.8s ease-out;
width: 100%;
}
.card {
flex: 1 1 300px;
max-width: 340px;
background-color: var(--card-bg);
border-radius: 16px;
padding: 30px 25px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.04);
text-align: center;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
border: 1px solid #eee;
}
.card.tugas-besar {
max-width: 600px;
border: 2px solid var(--secondary-color);
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
opacity: 0;
transition: opacity 0.3s ease;
}
.card:hover {
transform: translateY(-8px);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
}
.card:hover::before {
opacity: 1;
}
.card-icon {
font-size: 2.5rem;
color: var(--primary-color);
margin-bottom: 15px;
}
.task-number {
font-size: 0.85rem;
font-weight: 700;
color: var(--secondary-color);
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 10px;
}
.task-title {
font-size: 1.2rem;
font-weight: 600;
color: var(--text-main);
margin-bottom: 25px;
line-height: 1.4;
}
.btn-link {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 24px;
background-color: var(--bg-color);
color: var(--primary-color);
text-decoration: none;
font-weight: 600;
border-radius: 8px;
transition: all 0.3s ease;
width: 100%;
}
.btn-link:hover {
background-color: var(--primary-color);
color: white;
}
.btn-link i {
transition: transform 0.3s ease;
}
.btn-link:hover i {
transform: translateX(5px);
}
@keyframes fadeInDown {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
footer {
margin-top: auto;
padding: 30px;
text-align: center;
color: var(--text-muted);
font-size: 0.9rem;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Manajemen Data Spasial Kota Pontianak</h1>
<h4>Kumpulan Tugas SIG</h4>
<div class="profile-card">
<table>
<tr>
<td><strong>Nama</strong></td>
<td>:</td>
<td><strong>Alvin Andrianto Ciputra</strong></td>
</tr>
<tr>
<td><strong>NIM</strong></td>
<td>:</td>
<td><strong>D1041231074</strong></td>
</tr>
<tr>
<td><strong>Kelas</strong></td>
<td>:</td>
<td><strong>SIG C</strong></td>
</tr>
</table>
</div>
</div>
<div class="cards">
<div class="card">
<div class="card-icon">
<i class="fas fa-map-marker-alt"></i>
</div>
<div class="task-number">Tugas 01</div>
<div class="task-title">Manajemen Data SPBU (Titik / Point)</div>
<a href="tugas_01/index.php" class="btn-link">
Buka Proyek <i class="fas fa-arrow-right"></i>
</a>
</div>
<div class="card">
<div class="card-icon">
<i class="fas fa-route"></i>
</div>
<div class="task-number">Tugas 02</div>
<div class="task-title">Manajemen Data Jalan (Polyline) & Parsil (Polygon)</div>
<a href="tugas_02/index.php" class="btn-link">
Buka Proyek <i class="fas fa-arrow-right"></i>
</a>
</div>
<div class="card">
<div class="card-icon">
<i class="fas fa-layer-group"></i>
</div>
<div class="task-number">Tugas 03</div>
<div class="task-title">Penerapan Layer Groups & Layers Controls</div>
<a href="tugas_03/index.php" class="btn-link">
Buka Proyek <i class="fas fa-arrow-right"></i>
</a>
</div>
<div class="card tugas-besar">
<div class="card-icon" style="color: #8e44ad;">
<i class="fas fa-globe-asia"></i>
</div>
<div class="task-number" style="color: #8e44ad;">Tugas Besar </div>
<div class="task-title">WebGIS Poverty Mapping</div>
<a href="proverty_mapping/index.php" class="btn-link" style="background-color: #8e44ad; color: white;">
Buka Proyek <i class="fas fa-arrow-right"></i>
</a>
</div>
</div>
</div>
<footer>
&copy; 2026 Alvin Andrianto Ciputra - Teknik Informatika UNTAN
</footer>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
<?php
include "../config/koneksi.php";
$q = mysqli_query($koneksi, "SELECT * FROM jalan");
$features = [];
while($row = mysqli_fetch_assoc($q)) {
// GeoJSON menggunakan format [longitude, latitude]
// Kita perlu membalik urutan jika di database tersimpan sebagai [lat, lng]
$raw_coords = json_decode($row['koordinat']);
$formatted_coords = [];
foreach($raw_coords as $c) {
$formatted_coords[] = [$c->lng, $c->lat];
}
$features[] = [
"type" => "Feature",
"geometry" => [
"type" => "LineString",
"coordinates" => $formatted_coords
],
"properties" => [
"id" => $row['id'],
"nama" => $row['nama_jalan'],
"status" => $row['status_jalan'],
"panjang" => $row['panjang_jalan']
]
];
}
$geojson = ["type" => "FeatureCollection", "features" => $features];
echo json_encode($geojson);
?>
+11
View File
@@ -0,0 +1,11 @@
<?php
include "../config/koneksi.php";
$id = $_GET['id'];
$query = "DELETE FROM jalan WHERE id = '$id'";
if(mysqli_query($koneksi, $query)) {
echo json_encode(['status' => 'success']);
} else {
echo json_encode(['status' => 'error']);
}
?>
+7
View File
@@ -0,0 +1,7 @@
<?php
include "../config/koneksi.php";
$query = "INSERT INTO jalan (nama_jalan, status_jalan, panjang_jalan, koordinat) VALUES
('".$_POST['nama']."', '".$_POST['status']."', '".$_POST['panjang']."', '".$_POST['koordinat']."')";
mysqli_query($koneksi, $query);
?>
+14
View File
@@ -0,0 +1,14 @@
<?php
include "../config/koneksi.php";
$id = $_POST['id'];
$nama = $_POST['nama'];
$status = $_POST['status'];
$query = "UPDATE jalan SET nama_jalan='$nama', status_jalan='$status' WHERE id='$id'";
if(mysqli_query($koneksi, $query)) {
echo json_encode(['status' => 'success']);
} else {
echo json_encode(['status' => 'error']);
}
?>
+47
View File
@@ -0,0 +1,47 @@
<?php
include "../config/koneksi.php";
// Set header agar browser tahu ini adalah file JSON
header('Content-Type: application/json');
$q = mysqli_query($koneksi, "SELECT * FROM parsil");
$features = [];
while($row = mysqli_fetch_assoc($q)) {
$raw_coords = json_decode($row['koordinat']);
$formatted_coords = [];
// GeoJSON Polygon membutuhkan koordinat dalam array 3 lapis: [[[lng, lat], [lng, lat]]]
// Kita ambil dari koordinat mentah dan balik urutannya ke [longitude, latitude]
$polygon_ring = [];
foreach($raw_coords as $c) {
$polygon_ring[] = [(float)$c->lng, (float)$c->lat];
}
// Pastikan poligon tertutup (titik pertama sama dengan titik terakhir)
if (!empty($polygon_ring) && ($polygon_ring[0] !== end($polygon_ring))) {
$polygon_ring[] = $polygon_ring[0];
}
$features[] = [
"type" => "Feature",
"geometry" => [
"type" => "Polygon",
"coordinates" => [$polygon_ring] // Array bersarang untuk Polygon
],
"properties" => [
"id" => $row['id'],
"pemilik" => $row['nama_pemilik'],
"status" => $row['status_kepemilikan'],
"luas" => $row['luas_tanah']
]
];
}
$geojson = [
"type" => "FeatureCollection",
"features" => $features
];
echo json_encode($geojson);
?>
+11
View File
@@ -0,0 +1,11 @@
<?php
include "../config/koneksi.php";
$id = $_GET['id'];
$query = "DELETE FROM parsil WHERE id = '$id'";
if(mysqli_query($koneksi, $query)) {
echo json_encode(['status' => 'success']);
} else {
echo json_encode(['status' => 'error']);
}
?>
+7
View File
@@ -0,0 +1,7 @@
<?php
include "../config/koneksi.php";
$query = "INSERT INTO parsil (nama_pemilik, status_kepemilikan, luas_tanah, koordinat) VALUES
('".$_POST['pemilik']."', '".$_POST['status']."', '".$_POST['luas']."', '".$_POST['koordinat']."')";
mysqli_query($koneksi, $query);
?>
+14
View File
@@ -0,0 +1,14 @@
<?php
include "../config/koneksi.php";
$id = $_POST['id'];
$pemilik = $_POST['pemilik'];
$status = $_POST['status'];
$query = "UPDATE parsil SET nama_pemilik='$pemilik', status_kepemilikan='$status' WHERE id='$id'";
if(mysqli_query($koneksi, $query)) {
echo json_encode(['status' => 'success']);
} else {
echo json_encode(['status' => 'error']);
}
?>
+20
View File
@@ -0,0 +1,20 @@
<?php
include "../config/koneksi.php";
$id = $_POST['id'];
$type = $_POST['type']; // Akan berisi 'jalan' atau 'parsil'
$nilai = $_POST['nilai']; // Akan berisi panjang (meter) atau luas (meter persegi) baru
$koordinat = $_POST['koordinat'];
if ($type === 'jalan') {
$query = "UPDATE jalan SET panjang_jalan='$nilai', koordinat='$koordinat' WHERE id='$id'";
} else if ($type === 'parsil') {
$query = "UPDATE parsil SET luas_tanah='$nilai', koordinat='$koordinat' WHERE id='$id'";
}
if (mysqli_query($koneksi, $query)) {
echo json_encode(['status' => 'success']);
} else {
echo json_encode(['status' => 'error', 'message' => mysqli_error($koneksi)]);
}
?>
+12
View File
@@ -0,0 +1,12 @@
<?php
include "../config/koneksi.php";
$id = $_GET['id'];
$query = "DELETE FROM spbu WHERE id = '$id'";
if(mysqli_query($koneksi, $query)) {
echo json_encode(['status' => 'success']);
} else {
echo json_encode(['status' => 'error']);
}
?>
+18
View File
@@ -0,0 +1,18 @@
<?php
include "../config/koneksi.php";
$nama = $_POST['nama_spbu'];
$wa = $_POST['no_wa'];
$buka = $_POST['buka_24'];
$lat = $_POST['latitude'];
$lng = $_POST['longitude'];
$query = "INSERT INTO spbu (nama_spbu, no_wa, buka_24, latitude, longitude)
VALUES ('$nama', '$wa', '$buka', '$lat', '$lng')";
if(mysqli_query($koneksi, $query)) {
echo json_encode(['status' => 'success', 'id' => mysqli_insert_id($koneksi)]);
} else {
echo json_encode(['status' => 'error', 'message' => mysqli_error($koneksi)]);
}
?>
+8
View File
@@ -0,0 +1,8 @@
<?php
include "../congif/koneksi.php";
$id = $_POST['id'];
$lat = $_POST['lat'];
$lng = $_POST['lng'];
mysqli_query($koneksi, "UPDATE spbu SET latitude='$lat', longitude='$lng' WHERE id='$id'");
?>
+15
View File
@@ -0,0 +1,15 @@
<?php
include "../config/koneksi.php";
$id = $_POST['id'];
$nama = $_POST['nama'];
$wa = $_POST['wa'];
$buka = $_POST['buka'];
$query = "UPDATE spbu SET nama_spbu='$nama', no_wa='$wa', buka_24='$buka' WHERE id='$id'";
if(mysqli_query($koneksi, $query)) {
echo json_encode(['status' => 'success']);
} else {
echo json_encode(['status' => 'error']);
}
?>
+61
View File
@@ -0,0 +1,61 @@
<?php
include "../config/koneksi.php";
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebGIS Manajemen Spasial Kota 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="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<script src="https://cdn.jsdelivr.net/npm/leaflet-geodesic"></script>
<link rel="stylesheet" href="../assets/css/style.css">
</head>
<body>
<div class="app-container">
<div id="sidebar">
<h2>WebGIS Manajemen Spasial Kota Pontianak</h2>
<h4>Tugas 01 SIG: Manajemen Data Point (SPBU)</h4>
<h4>Pencarian Data</h4>
<div class="search-container">
<input type="text" id="searchInput" placeholder="Cari SPBU ..."
onkeypress="handleEnter(event)">
<button onclick="cariData()">Cari Lokasi</button>
</div>
<h4>Tambah SPBU (Point Marker):</h4>
<div id="drawControlWrapper"></div>
</div>
<div id="map"></div>
</div>
<script>
const DATA_SPBU = <?php
$query = mysqli_query($koneksi, "SELECT id, nama_spbu as nama, no_wa as wa, buka_24 as buka, latitude as lat, longitude as lng FROM spbu");
$data = [];
if($query) {
while ($row = mysqli_fetch_assoc($query)) {
$row['lat'] = (float) $row['lat'];
$row['lng'] = (float) $row['lng'];
$data[] = $row;
}
}
echo json_encode($data);
?>;
</script>
<script src="script.js"></script>
</body>
</html>
+217
View File
@@ -0,0 +1,217 @@
let tempLat, tempLng;
const map = L.map('map').setView([-0.0263, 109.3425], 13);
const baseOsm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors'
}).addTo(map);
const greenIcon = new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
});
const redIcon = new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
});
const layerSPBU = L.layerGroup().addTo(map);
const drawnItems = new L.FeatureGroup().addTo(map);
let searchableLayers = [];
// Setup Leaflet Draw Toolbar
const drawControl = new L.Control.Draw({
draw: {
marker: { icon: redIcon },
circle: false, rectangle: false, circlemarker: false, polyline: false, polygon: false
},
edit: { featureGroup: drawnItems }
}).addTo(map);
// Memindahkan UI Draw Toolbar dari peta ke dalam Sidebar
document.getElementById('drawControlWrapper').appendChild(drawControl.getContainer());
// FUNGSI RENDER DATA SPBU
function createSPBUMarker(data) {
const statusBuka = data.buka.trim();
const markerIcon = (statusBuka === 'Ya') ? redIcon : greenIcon;
const marker = L.marker([data.lat, data.lng], { icon: markerIcon, draggable: true });
function updatePopupContent(id, nama, wa, buka, lat, lng, leafletId) {
return `<div class="popup-content">
<b>${nama}</b><br>WA: ${wa}<br>Buka 24: ${buka}<br>
Lat: ${lat.toFixed(6)}<br>Lng: ${lng.toFixed(6)}<br>
<div class="button-group">
<button class="btn-edit" onclick="editData(${id}, '${nama}', '${wa}', '${buka}')">Edit</button>
<button class="btn-delete" onclick="deleteMarker(${id}, ${leafletId})">Hapus</button>
</div>
</div>`;
}
marker.bindPopup(updatePopupContent(data.id, data.nama, data.wa, data.buka, data.lat, data.lng, marker._leaflet_id));
marker.on('dragend', async function (e) {
const newLatLng = marker.getLatLng();
const fd = new FormData();
fd.append('id', data.id); fd.append('lat', newLatLng.lat); fd.append('lng', newLatLng.lng);
await fetch('../spbu/update_koordinat.php', { method: 'POST', body: fd });
marker.setPopupContent(updatePopupContent(data.id, data.nama, data.wa, data.buka, newLatLng.lat, newLatLng.lng, marker._leaflet_id));
});
marker.addTo(layerSPBU);
searchableLayers.push({ nama: data.nama, layer: marker, tipe: 'titik' });
}
// Mengeksekusi variabel global DATA_SPBU yang dilempar dari index.php
if (typeof DATA_SPBU !== 'undefined') {
DATA_SPBU.forEach(spbu => createSPBUMarker(spbu));
}
// EVENT HANDLING LEAFLET DRAW
map.on(L.Draw.Event.CREATED, async function (e) {
const layer = e.layer;
const latlng = layer.getLatLng();
tempLat = latlng.lat; tempLng = latlng.lng;
const formContent = `<div class="popup-content">
<h4>Tambah SPBU Baru</h4>
<input type="text" id="input_nama" placeholder="Nama SPBU">
<input type="text" id="input_wa" placeholder="No WA">
<select id="input_buka">
<option value="Ya">Buka 24 Jam</option>
<option value="Tidak">Tidak 24 Jam</option>
</select>
<button class="btn-save" onclick="simpanData()">Simpan</button>
</div>`;
L.popup().setLatLng(latlng).setContent(formContent).openOn(map);
map.removeLayer(layer);
});
// CRUD BACKEND (FETCH API)
window.simpanData = async function() {
const formData = new FormData();
formData.append('nama_spbu', document.getElementById('input_nama').value);
formData.append('no_wa', document.getElementById('input_wa').value);
formData.append('buka_24', document.getElementById('input_buka').value);
formData.append('latitude', tempLat); formData.append('longitude', tempLng);
const response = await fetch('../spbu/tambah_spbu.php', { method: 'POST', body: formData });
if ((await response.json()).status === 'success') location.reload();
}
window.updateData = async function(id) {
const formData = new FormData();
formData.append('id', id);
formData.append('nama', document.getElementById('edit_nama').value);
formData.append('wa', document.getElementById('edit_wa').value);
formData.append('buka', document.getElementById('edit_buka').value);
const response = await fetch('../spbu/update_spbu.php', { method: 'POST', body: formData });
if ((await response.json()).status === 'success') location.reload();
}
window.deleteMarker = async function(id, leafletId) {
if (confirm("Hapus data?")) {
const response = await fetch(`../spbu/hapus_spbu.php?id=${id}`);
if ((await response.json()).status === 'success') location.reload();
}
}
window.editData = function(id, nama, wa, buka) {
const editForm = `<div class="popup-content">
<h4>Edit SPBU</h4>
<input type="text" id="edit_nama" value="${nama}">
<input type="text" id="edit_wa" value="${wa}">
<select id="edit_buka">
<option value="Ya" ${buka === 'Ya' ? 'selected' : ''}>Buka 24 Jam</option>
<option value="Tidak" ${buka === 'Tidak' ? 'selected' : ''}>Tidak 24 Jam</option>
</select>
<button class="btn-save" onclick="updateData(${id})">Update Data</button>
</div>`;
map.closePopup();
L.popup().setLatLng(map.getCenter()).setContent(editForm).openOn(map);
}
// FUNGSI PENCARIAN DATA
window.cariData = function() {
const keyword = document.getElementById('searchInput').value.toLowerCase().trim();
if (!keyword) { alert("Masukkan kata kunci pencarian!"); return; }
let found = false;
for (let item of searchableLayers) {
if (item.nama && item.nama.toLowerCase().includes(keyword)) {
found = true;
if (item.tipe === 'titik') { map.flyTo(item.layer.getLatLng(), 17); }
else { map.flyToBounds(item.layer.getBounds(), { maxZoom: 17 }); }
item.layer.openPopup();
break;
}
}
if (!found) alert("Data tidak ditemukan! Pastikan nama SPBU sudah benar.");
}
window.handleEnter = function(e) {
if (e.key === 'Enter') cariData();
}
// MUAT BASEMAP KECAMATAN
async function loadBasemapPontianak() {
const adminGrp = L.layerGroup().addTo(map);
async function fetchGeoJSON(url) {
const response = await fetch(url);
if (!response.ok) throw new Error(`File tidak ditemukan: ${url}`);
return await response.json();
}
function getColorKecamatan(nama) {
if (!nama) return "#cccccc";
const n = nama.toUpperCase();
if (n.includes("BARAT")) return "#e41a1c";
if (n.includes("KOTA")) return "#377eb8";
if (n.includes("SELATAN")) return "#4daf4a";
if (n.includes("TENGGARA")) return "#984ea3";
if (n.includes("TIMUR")) return "#ff7f00";
if (n.includes("UTARA")) return "#f1c40f";
return "#a65628";
}
try {
const dataAdmin = await fetchGeoJSON('../assets/data/Admin_Kecamatan.json');
L.geoJSON(dataAdmin, {
style: function (feature) {
const namaKecamatan = feature.properties.Ket || "";
return { color: "black", weight: 2, fillOpacity: 0.2, fillColor:getColorKecamatan(namaKecamatan) };
},
onEachFeature: function (feature, layer) {
const namaKecamatan = feature.properties.Ket || "Tidak Diketahui";
const jumlahPenduduk = feature.properties.penduduk || 0;
const formatPenduduk = new Intl.NumberFormat('id-ID').format(jumlahPenduduk);
layer.bindPopup(`
<div style="text-align: center;">
<b>Kecamatan ${namaKecamatan}</b><br>
<hr style="margin: 5px 0;">
Jumlah Penduduk: <b>${formatPenduduk} Jiwa</b>
</div>
`);
}
}).addTo(adminGrp);
} catch (e) {
console.error("Gagal memuat peta kecamatan:", e.message);
}
const baseLayers = { "OpenStreetMap": baseOsm };
const overlays = {
"Batas Kecamatan": adminGrp,
"SPBU (Titik)": layerSPBU,
"Alat Digitasi": drawnItems
};
L.control.layers(baseLayers, overlays).addTo(map);
}
loadBasemapPontianak();
+62
View File
@@ -0,0 +1,62 @@
<?php
include "../config/koneksi.php";
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebGIS Manajemen Spasial Kota 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="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<script src="https://cdn.jsdelivr.net/npm/leaflet-geodesic"></script>
<link rel="stylesheet" href="../assets/css/style.css">
</head>
<body>
<div class="app-container">
<div id="sidebar">
<h2>WebGIS Manajemen Spasial Kota Pontianak</h2>
<h4>Tugas 02 SIG: Manajemen Data Polyline (Jalan) & Polygon (Parsil)</h4>
<h4>Pencarian Data</h4>
<div class="search-container">
<input type="text" id="searchInput" placeholder="Cari SPBU, Jalan, Parsil..."
onkeypress="handleEnter(event)">
<button onclick="cariData()">Cari Lokasi</button>
</div>
<h4>Alat Digitasi</h4>
<div id="drawControlWrapper"></div>
</div>
<div id="map"></div>
</div>
<script>
const DATA_SPBU = <?php
// Pemanggilan data dari PHP (Backend) sebelum file JS dimuat
$query = mysqli_query($koneksi, "SELECT id, nama_spbu as nama, no_wa as wa, buka_24 as buka, latitude as lat, longitude as lng FROM spbu");
$data = [];
if ($query) {
while ($row = mysqli_fetch_assoc($query)) {
$row['lat'] = (float) $row['lat'];
$row['lng'] = (float) $row['lng'];
$data[] = $row;
}
}
echo json_encode($data);
?>;
</script>
<script src="script.js"></script>
</body>
</html>
+390
View File
@@ -0,0 +1,390 @@
let tempLat, tempLng;
const map = L.map('map').setView([-0.0263, 109.3425], 13);
const baseOsm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors'
}).addTo(map);
const greenIcon = new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
});
const redIcon = new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
});
const layerSPBU = L.layerGroup().addTo(map);
const drawnItems = new L.FeatureGroup().addTo(map);
let searchableLayers = [];
// SETUP LEAFLET DRAW TOOLBAR
const drawControl = new L.Control.Draw({
draw: {
polyline: { shapeOptions: { color: 'blue' } },
polygon: { shapeOptions: { color: 'orange' } },
marker: { icon: redIcon },
circle: false, rectangle: false, circlemarker: false
},
edit: { featureGroup: drawnItems }
}).addTo(map);
document.getElementById('drawControlWrapper').appendChild(drawControl.getContainer());
// FUNGSI RENDER DATA SPBU (POINT)
function createSPBUMarker(data) {
const statusBuka = data.buka.trim();
const markerIcon = (statusBuka === 'Ya') ? greenIcon : redIcon;
const marker = L.marker([data.lat, data.lng], { icon: markerIcon, draggable: true });
function updatePopupContent(id, nama, wa, buka, lat, lng, leafletId) {
return `<div class="popup-content">
<b>${nama}</b><br>WA: ${wa}<br>Buka 24: ${buka}<br>
Lat: ${lat.toFixed(6)}<br>Lng: ${lng.toFixed(6)}<br>
<div class="button-group">
<button class="btn-edit" onclick="editData(${id}, '${nama}', '${wa}', '${buka}')">Edit</button>
<button class="btn-delete" onclick="deleteMarker(${id}, ${leafletId})">Hapus</button>
</div>
</div>`;
}
marker.bindPopup(updatePopupContent(data.id, data.nama, data.wa, data.buka, data.lat, data.lng, marker._leaflet_id));
marker.on('dragend', async function (e) {
const newLatLng = marker.getLatLng();
const fd = new FormData();
fd.append('id', data.id); fd.append('lat', newLatLng.lat); fd.append('lng', newLatLng.lng);
await fetch('../spbu/update_koordinat.php', { method: 'POST', body: fd });
marker.setPopupContent(updatePopupContent(data.id, data.nama, data.wa, data.buka, newLatLng.lat, newLatLng.lng, marker._leaflet_id));
});
marker.addTo(layerSPBU);
searchableLayers.push({ nama: data.nama, layer: marker, tipe: 'titik' });
}
if (typeof DATA_SPBU !== 'undefined') {
DATA_SPBU.forEach(spbu => createSPBUMarker(spbu));
}
// EVENT HANDLING LEAFLET DRAW (CREATED & EDITED)
map.on(L.Draw.Event.CREATED, async function (e) {
const layer = e.layer;
const type = e.layerType;
if (type === 'polyline') {
const coords = layer.getLatLngs();
let panjang = 0;
for (let i = 0; i < coords.length - 1; i++) { panjang += coords[i].distanceTo(coords[i + 1]); }
const nama = prompt("Nama Jalan:");
const status = prompt("Status (Nasional/Provinsi/Kabupaten):");
if (nama && status) {
const fd = new FormData();
fd.append('nama', nama); fd.append('status', status); fd.append('panjang', panjang.toFixed(2)); fd.append('koordinat', JSON.stringify(coords));
await fetch('../jalan/tambah_jalan.php', { method: 'POST', body: fd }); location.reload();
}
} else if (type === 'polygon') {
const coords = layer.getLatLngs()[0];
const luas = L.GeometryUtil.geodesicArea(coords);
const pemilik = prompt("Nama Pemilik:");
const status = prompt("Status (SHM/HGB/HGU/HP):");
if (pemilik && status) {
const fd = new FormData();
fd.append('pemilik', pemilik); fd.append('status', status); fd.append('luas', luas.toFixed(2)); fd.append('koordinat', JSON.stringify(coords));
await fetch('../parsil/tambah_parsil.php', { method: 'POST', body: fd }); location.reload();
}
} else if (type === 'marker') {
const latlng = layer.getLatLng();
tempLat = latlng.lat; tempLng = latlng.lng;
const formContent = `<div class="popup-content">
<h4>Tambah SPBU Baru</h4>
<input type="text" id="input_nama" placeholder="Nama SPBU">
<input type="text" id="input_wa" placeholder="No WA">
<select id="input_buka">
<option value="Ya">Buka 24 Jam</option>
<option value="Tidak">Tidak 24 Jam</option>
</select>
<button class="btn-save" onclick="simpanData()">Simpan</button>
</div>`;
L.popup().setLatLng(latlng).setContent(formContent).openOn(map);
map.removeLayer(layer);
}
});
map.on(L.Draw.Event.EDITED, async function (e) {
const layers = e.layers;
layers.eachLayer(async function (layer) {
const id = layer.db_id;
const type = layer.db_type;
if (id && type) {
let coords_str = '';
let nilai_baru = 0;
if (type === 'jalan') {
const coords = layer.getLatLngs();
let panjang = 0;
for (let i = 0; i < coords.length - 1; i++) { panjang += coords[i].distanceTo(coords[i + 1]); }
nilai_baru = panjang.toFixed(2);
coords_str = JSON.stringify(coords);
} else if (type === 'parsil') {
const coords = layer.getLatLngs()[0];
nilai_baru = L.GeometryUtil.geodesicArea(coords).toFixed(2);
coords_str = JSON.stringify(coords);
}
const fd = new FormData();
fd.append('id', id); fd.append('type', type); fd.append('nilai', nilai_baru); fd.append('koordinat', coords_str);
await fetch('../shared/update_geometri.php', { method: 'POST', body: fd });
}
});
alert("Perubahan garis/bidang berhasil disimpan!");
location.reload();
});
// CRUD SPBU
window.simpanData = async function() {
const formData = new FormData();
formData.append('nama_spbu', document.getElementById('input_nama').value);
formData.append('no_wa', document.getElementById('input_wa').value);
formData.append('buka_24', document.getElementById('input_buka').value);
formData.append('latitude', tempLat); formData.append('longitude', tempLng);
const response = await fetch('../spbu/tambah_spbu.php', { method: 'POST', body: formData });
if ((await response.json()).status === 'success') location.reload();
}
window.updateData = async function(id) {
const formData = new FormData();
formData.append('id', id);
formData.append('nama', document.getElementById('edit_nama').value);
formData.append('wa', document.getElementById('edit_wa').value);
formData.append('buka', document.getElementById('edit_buka').value);
const response = await fetch('../spbu/update_spbu.php', { method: 'POST', body: formData });
if ((await response.json()).status === 'success') location.reload();
}
window.deleteMarker = async function(id, leafletId) {
if (confirm("Hapus data?")) {
const response = await fetch(`../spbu/hapus_spbu.php?id=${id}`);
if ((await response.json()).status === 'success') location.reload();
}
}
window.editData = function(id, nama, wa, buka) {
const editForm = `<div class="popup-content">
<h4>Edit SPBU</h4>
<input type="text" id="edit_nama" value="${nama}">
<input type="text" id="edit_wa" value="${wa}">
<select id="edit_buka">
<option value="Ya" ${buka === 'Ya' ? 'selected' : ''}>Buka 24 Jam</option>
<option value="Tidak" ${buka === 'Tidak' ? 'selected' : ''}>Tidak 24 Jam</option>
</select>
<button class="btn-save" onclick="updateData(${id})">Update Data</button>
</div>`;
map.closePopup();
L.popup().setLatLng(map.getCenter()).setContent(editForm).openOn(map);
}
// CRUD JALAN (POLYLINE)
window.editDataJalan = function(id, nama, status) {
const editForm = `<div class="popup-content">
<h4>Edit Jalan</h4>
<input type="text" id="edit_nama_jalan" value="${nama}">
<select id="edit_status_jalan">
<option value="Nasional" ${status === 'Nasional' ? 'selected' : ''}>Nasional</option>
<option value="Provinsi" ${status === 'Provinsi' ? 'selected' : ''}>Provinsi</option>
<option value="Kabupaten" ${status === 'Kabupaten' ? 'selected' : ''}>Kabupaten</option>
</select>
<button class="btn-save" onclick="updateDataJalan(${id})">Update Jalan</button>
</div>`;
map.closePopup();
L.popup().setLatLng(map.getCenter()).setContent(editForm).openOn(map);
}
window.updateDataJalan = async function(id) {
const fd = new FormData();
fd.append('id', id);
fd.append('nama', document.getElementById('edit_nama_jalan').value);
fd.append('status', document.getElementById('edit_status_jalan').value);
const response = await fetch('../jalan/update_jalan.php', { method: 'POST', body: fd });
if ((await response.json()).status === 'success') location.reload();
}
window.deleteJalan = async function(id) {
if (confirm("Hapus data jalan ini?")) {
const response = await fetch(`../jalan/hapus_jalan.php?id=${id}`);
if ((await response.json()).status === 'success') location.reload();
}
}
// CRUD PARSIL (POLYGON)
window.editDataParsil = function(id, pemilik, status) {
const editForm = `<div class="popup-content">
<h4>Edit Parsil</h4>
<input type="text" id="edit_pemilik_parsil" value="${pemilik}">
<select id="edit_status_parsil">
<option value="SHM" ${status === 'SHM' ? 'selected' : ''}>SHM</option>
<option value="HGB" ${status === 'HGB' ? 'selected' : ''}>HGB</option>
<option value="HGU" ${status === 'HGU' ? 'selected' : ''}>HGU</option>
<option value="HP" ${status === 'HP' ? 'selected' : ''}>HP</option>
</select>
<button class="btn-save" onclick="updateDataParsil(${id})">Update Parsil</button>
</div>`;
map.closePopup();
L.popup().setLatLng(map.getCenter()).setContent(editForm).openOn(map);
}
window.updateDataParsil = async function(id) {
const fd = new FormData();
fd.append('id', id);
fd.append('pemilik', document.getElementById('edit_pemilik_parsil').value);
fd.append('status', document.getElementById('edit_status_parsil').value);
const response = await fetch('../parsil/update_parsil.php', { method: 'POST', body: fd });
if ((await response.json()).status === 'success') location.reload();
}
window.deleteParsil = async function(id) {
if (confirm("Hapus data parsil ini?")) {
const response = await fetch(`../parsil/hapus_parsil.php?id=${id}`);
if ((await response.json()).status === 'success') location.reload();
}
}
// RENDER SPASIAL (LOAD DARI DB)
function getColorJalan(status) { return status === 'Nasional' ? 'red' : status === 'Provinsi' ? 'blue' : 'green'; }
function getColorParsil(status) {
const colors = { 'SHM': '#8B4513', 'HGB': '#FFD700', 'HGU': '#ADFF2F', 'HP': '#87CEEB' };
return colors[status] || '#CCCCCC';
}
async function loadSpasial() {
const resJ = await fetch('../jalan/get_jalan.php');
const dataJ = await resJ.json();
L.geoJSON(dataJ, {
style: function (f) { return { color: getColorJalan(f.properties.status), weight: 4 }; },
onEachFeature: function (f, layer) {
layer.bindPopup(`
<div class="popup-content">
<b>${f.properties.nama}</b><br>
Status: ${f.properties.status}<br>
Panjang: ${f.properties.panjang} m<br>
<div class="button-group">
<button class="btn-edit" onclick="editDataJalan(${f.properties.id}, '${f.properties.nama}', '${f.properties.status}')">Edit</button>
<button class="btn-delete" onclick="deleteJalan(${f.properties.id})">Hapus</button>
</div>
</div>
`);
layer.db_id = f.properties.id;
layer.db_type = 'jalan';
drawnItems.addLayer(layer);
searchableLayers.push({ nama: f.properties.nama, layer: layer, tipe: 'garis' });
}
});
const resP = await fetch('../parsil/get_parsil.php');
const dataP = await resP.json();
L.geoJSON(dataP, {
style: function (f) {
return { fillColor: getColorParsil(f.properties.status), fillOpacity: 0.6, color: "black", weight: 1 };
},
onEachFeature: function (f, layer) {
layer.bindPopup(`
<div class="popup-content">
<b>Pemilik: ${f.properties.pemilik}</b><br>
Status: ${f.properties.status}<br>
Luas: ${f.properties.luas} m²<br>
<div class="button-group">
<button class="btn-edit" onclick="editDataParsil(${f.properties.id}, '${f.properties.pemilik}', '${f.properties.status}')">Edit</button>
<button class="btn-delete" onclick="deleteParsil(${f.properties.id})">Hapus</button>
</div>
</div>
`);
layer.db_id = f.properties.id;
layer.db_type = 'parsil';
drawnItems.addLayer(layer);
searchableLayers.push({ nama: f.properties.pemilik, layer: layer, tipe: 'bidang' });
}
});
}
// LOAD BASEMAP KECAMATAN
async function loadBasemapPontianak() {
const adminGrp = L.layerGroup().addTo(map);
async function fetchGeoJSON(url) {
const response = await fetch(url);
if (!response.ok) throw new Error(`File tidak ditemukan: ${url}`);
return await response.json();
}
function getColorKecamatan(nama) {
if (!nama) return "#cccccc";
const n = nama.toUpperCase();
if (n.includes("BARAT")) return "#e41a1c";
if (n.includes("KOTA")) return "#377eb8";
if (n.includes("SELATAN")) return "#4daf4a";
if (n.includes("TENGGARA")) return "#984ea3";
if (n.includes("TIMUR")) return "#ff7f00";
if (n.includes("UTARA")) return "#f1c40f";
return "#a65628";
}
try {
const dataAdmin = await fetchGeoJSON('../assets/data/Admin_Kecamatan.json');
L.geoJSON(dataAdmin, {
style: function (feature) {
const namaKecamatan = feature.properties.Ket || "";
return { color: "black", weight: 2, fillOpacity: 0.2, fillColor:getColorKecamatan(namaKecamatan) };
},
onEachFeature: function (feature, layer) {
const namaKecamatan = feature.properties.Ket || "Tidak Diketahui";
const jumlahPenduduk = feature.properties.penduduk || 0;
const formatPenduduk = new Intl.NumberFormat('id-ID').format(jumlahPenduduk);
layer.bindPopup(`
<div style="text-align: center;">
<b>Kecamatan ${namaKecamatan}</b><br>
<hr style="margin: 5px 0;">
Jumlah Penduduk: <b>${formatPenduduk} Jiwa</b>
</div>
`);
}
}).addTo(adminGrp);
} catch (e) {
console.error("Gagal memuat peta kecamatan:", e.message);
}
const baseLayers = { "OpenStreetMap": baseOsm };
const overlays = {
"Batas Kecamatan": adminGrp,
"SPBU (Titik)": layerSPBU,
"Jalan & Tanah (Mode Edit)": drawnItems
};
L.control.layers(baseLayers, overlays).addTo(map);
}
// FUNGSI PENCARIAN DATA
window.cariData = function() {
const keyword = document.getElementById('searchInput').value.toLowerCase().trim();
if (!keyword) { alert("Masukkan kata kunci pencarian!"); return; }
let found = false;
for (let item of searchableLayers) {
if (item.nama && item.nama.toLowerCase().includes(keyword)) {
found = true;
if (item.tipe === 'titik') { map.flyTo(item.layer.getLatLng(), 17); }
else { map.flyToBounds(item.layer.getBounds(), { maxZoom: 17 }); }
item.layer.openPopup();
break;
}
}
if (!found) alert("Data tidak ditemukan! Pastikan nama SPBU, Jalan, atau Pemilik sudah benar.");
}
window.handleEnter = function(e) {
if (e.key === 'Enter') cariData();
}
loadSpasial();
loadBasemapPontianak();
+64
View File
@@ -0,0 +1,64 @@
<?php
include "../config/koneksi.php";
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>WebGIS Manajemen Spasial Kota 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="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<script src="https://cdn.jsdelivr.net/npm/leaflet-geodesic"></script>
<link rel="stylesheet" href="../assets/css/style.css">
</head>
<body>
<div class="app-container">
<div id="sidebar">
<h2>WebGIS Manajemen Spasial Kota Pontianak</h2>
<h4>Tugas 03 SIG: Penerapan Layer Groups & Layer Controls</h4>
<h4>Pencarian Data</h4>
<div class="search-container">
<input type="text" id="searchInput" placeholder="Cari SPBU, Jalan, Parsil..."
onkeypress="handleEnter(event)">
<button onclick="cariData()">Cari Lokasi</button>
</div>
<h4>Alat Digitasi</h4>
<div id="drawControlWrapper"></div>
<h4>Menu Layer & Peta</h4>
<div id="layerControlWrapper"></div>
</div>
<div id="map"></div>
</div>
<script>
const DATA_SPBU = <?php
$query = mysqli_query($koneksi, "SELECT id, nama_spbu as nama, no_wa as wa, buka_24 as buka, latitude as lat, longitude as lng FROM spbu");
$data = [];
if ($query) {
while ($row = mysqli_fetch_assoc($query)) {
$row['lat'] = (float) $row['lat'];
$row['lng'] = (float) $row['lng'];
$data[] = $row;
}
}
echo json_encode($data);
?>;
</script>
<script src="script.js"></script>
</body>
</html>
+409
View File
@@ -0,0 +1,409 @@
let tempLat, tempLng;
const map = L.map('map', { zoomControl: false }).setView([-0.0263, 109.3425], 13);
L.control.zoom({ position: 'topright' }).addTo(map);
const baseOsm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors'
}).addTo(map);
const greenIcon = new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
});
const redIcon = new L.Icon({
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
});
// Layer Groups Visual
const layerSPBU24 = L.layerGroup().addTo(map);
const layerSPBUNon24 = L.layerGroup().addTo(map);
const layerJalanDB = L.layerGroup().addTo(map);
const layerParsilDB = L.layerGroup().addTo(map);
const drawnItems = new L.FeatureGroup().addTo(map);
let searchableLayers = [];
// SETUP LEAFLET DRAW TOOLBAR
const drawControl = new L.Control.Draw({
draw: {
polyline: { shapeOptions: { color: 'blue' } },
polygon: { shapeOptions: { color: 'orange' } },
marker: { icon: redIcon },
circle: false, rectangle: false, circlemarker: false
},
edit: { featureGroup: drawnItems }
}).addTo(map);
// Memindahkan UI Draw Toolbar ke dalam Sidebar
document.getElementById('drawControlWrapper').appendChild(drawControl.getContainer());
// FUNGSI RENDER DATA SPBU (DENGAN KONDISI LAYER GROUP)
function createSPBUMarker(data) {
const statusBuka = data.buka.trim();
// Buka 24 jam = GreenIcon, Tidak = RedIcon
const markerIcon = (statusBuka === 'Ya') ? greenIcon : redIcon;
const marker = L.marker([data.lat, data.lng], { icon: markerIcon, draggable: true });
function updatePopupContent(id, nama, wa, buka, lat, lng, leafletId) {
return `<div class="popup-content">
<b>${nama}</b><br>WA: ${wa}<br>Buka 24: ${buka}<br>
Lat: ${lat.toFixed(6)}<br>Lng: ${lng.toFixed(6)}<br>
<div class="button-group">
<button class="btn-edit" onclick="editData(${id}, '${nama}', '${wa}', '${buka}')">Edit</button>
<button class="btn-delete" onclick="deleteMarker(${id}, ${leafletId})">Hapus</button>
</div>
</div>`;
}
marker.bindPopup(updatePopupContent(data.id, data.nama, data.wa, data.buka, data.lat, data.lng, marker._leaflet_id));
marker.on('dragend', async function (e) {
const newLatLng = marker.getLatLng();
const fd = new FormData();
fd.append('id', data.id); fd.append('lat', newLatLng.lat); fd.append('lng', newLatLng.lng);
await fetch('../spbu/update_koordinat.php', { method: 'POST', body: fd });
marker.setPopupContent(updatePopupContent(data.id, data.nama, data.wa, data.buka, newLatLng.lat, newLatLng.lng, marker._leaflet_id));
});
// Memasukkan marker ke kelompok layer group yang sesuai
if (statusBuka === 'Ya') {
marker.addTo(layerSPBU24);
} else {
marker.addTo(layerSPBUNon24);
}
searchableLayers.push({ nama: data.nama, layer: marker, tipe: 'titik' });
}
if (typeof DATA_SPBU !== 'undefined') {
DATA_SPBU.forEach(spbu => createSPBUMarker(spbu));
}
// EVENT HANDLING LEAFLET DRAW (CREATED & EDITED)
map.on(L.Draw.Event.CREATED, async function (e) {
const layer = e.layer;
const type = e.layerType;
if (type === 'polyline') {
const coords = layer.getLatLngs();
let panjang = 0;
for (let i = 0; i < coords.length - 1; i++) { panjang += coords[i].distanceTo(coords[i + 1]); }
const nama = prompt("Nama Jalan:");
const status = prompt("Status (Nasional/Provinsi/Kabupaten):");
if (nama && status) {
const fd = new FormData();
fd.append('nama', nama); fd.append('status', status); fd.append('panjang', panjang.toFixed(2)); fd.append('koordinat', JSON.stringify(coords));
await fetch('../jalan/tambah_jalan.php', { method: 'POST', body: fd }); location.reload();
}
} else if (type === 'polygon') {
const coords = layer.getLatLngs()[0];
const luas = L.GeometryUtil.geodesicArea(coords);
const pemilik = prompt("Nama Pemilik:");
const status = prompt("Status (SHM/HGB/HGU/HP):");
if (pemilik && status) {
const fd = new FormData();
fd.append('pemilik', pemilik); fd.append('status', status); fd.append('luas', luas.toFixed(2)); fd.append('koordinat', JSON.stringify(coords));
await fetch('../parsil/tambah_parsil.php', { method: 'POST', body: fd }); location.reload();
}
} else if (type === 'marker') {
const latlng = layer.getLatLng();
tempLat = latlng.lat; tempLng = latlng.lng;
const formContent = `<div class="popup-content">
<h4>Tambah SPBU Baru</h4>
<input type="text" id="input_nama" placeholder="Nama SPBU">
<input type="text" id="input_wa" placeholder="No WA">
<select id="input_buka">
<option value="Ya">Buka 24 Jam</option>
<option value="Tidak">Tidak 24 Jam</option>
</select>
<button class="btn-save" onclick="simpanData()">Simpan</button>
</div>`;
L.popup().setLatLng(latlng).setContent(formContent).openOn(map);
map.removeLayer(layer);
}
});
map.on(L.Draw.Event.EDITED, async function (e) {
const layers = e.layers;
layers.eachLayer(async function (layer) {
const id = layer.db_id;
const type = layer.db_type;
if (id && type) {
let coords_str = '';
let nilai_baru = 0;
if (type === 'jalan') {
const coords = layer.getLatLngs();
let panjang = 0;
for (let i = 0; i < coords.length - 1; i++) { panjang += coords[i].distanceTo(coords[i + 1]); }
nilai_baru = panjang.toFixed(2);
coords_str = JSON.stringify(coords);
} else if (type === 'parsil') {
const coords = layer.getLatLngs()[0];
nilai_baru = L.GeometryUtil.geodesicArea(coords).toFixed(2);
coords_str = JSON.stringify(coords);
}
const fd = new FormData();
fd.append('id', id); fd.append('type', type); fd.append('nilai', nilai_baru); fd.append('koordinat', coords_str);
await fetch('../shared/update_geometri.php', { method: 'POST', body: fd });
}
});
alert("Perubahan garis/bidang berhasil disimpan!");
location.reload();
});
// CRUD SPBU
window.simpanData = async function() {
const formData = new FormData();
formData.append('nama_spbu', document.getElementById('input_nama').value);
formData.append('no_wa', document.getElementById('input_wa').value);
formData.append('buka_24', document.getElementById('input_buka').value);
formData.append('latitude', tempLat); formData.append('longitude', tempLng);
const response = await fetch('../spbu/tambah_spbu.php', { method: 'POST', body: formData });
if ((await response.json()).status === 'success') location.reload();
}
window.updateData = async function(id) {
const formData = new FormData();
formData.append('id', id);
formData.append('nama', document.getElementById('edit_nama').value);
formData.append('wa', document.getElementById('edit_wa').value);
formData.append('buka', document.getElementById('edit_buka').value);
const response = await fetch('../spbu/update_spbu.php', { method: 'POST', body: formData });
if ((await response.json()).status === 'success') location.reload();
}
window.deleteMarker = async function(id, leafletId) {
if (confirm("Hapus data?")) {
const response = await fetch(`../spbu/hapus_spbu.php?id=${id}`);
if ((await response.json()).status === 'success') location.reload();
}
}
window.editData = function(id, nama, wa, buka) {
const editForm = `<div class="popup-content">
<h4>Edit SPBU</h4>
<input type="text" id="edit_nama" value="${nama}">
<input type="text" id="edit_wa" value="${wa}">
<select id="edit_buka">
<option value="Ya" ${buka === 'Ya' ? 'selected' : ''}>Buka 24 Jam</option>
<option value="Tidak" ${buka === 'Tidak' ? 'selected' : ''}>Tidak 24 Jam</option>
</select>
<button class="btn-save" onclick="updateData(${id})">Update Data</button>
</div>`;
map.closePopup();
L.popup().setLatLng(map.getCenter()).setContent(editForm).openOn(map);
}
// CRUD JALAN & PARSIL
window.editDataJalan = function(id, nama, status) {
const editForm = `<div class="popup-content">
<h4>Edit Jalan</h4>
<input type="text" id="edit_nama_jalan" value="${nama}">
<select id="edit_status_jalan">
<option value="Nasional" ${status === 'Nasional' ? 'selected' : ''}>Nasional</option>
<option value="Provinsi" ${status === 'Provinsi' ? 'selected' : ''}>Provinsi</option>
<option value="Kabupaten" ${status === 'Kabupaten' ? 'selected' : ''}>Kabupaten</option>
</select>
<button class="btn-save" onclick="updateDataJalan(${id})">Update Jalan</button>
</div>`;
map.closePopup();
L.popup().setLatLng(map.getCenter()).setContent(editForm).openOn(map);
}
window.updateDataJalan = async function(id) {
const fd = new FormData();
fd.append('id', id); fd.append('nama', document.getElementById('edit_nama_jalan').value); fd.append('status', document.getElementById('edit_status_jalan').value);
const response = await fetch('../jalan/update_jalan.php', { method: 'POST', body: fd });
if ((await response.json()).status === 'success') location.reload();
}
window.deleteJalan = async function(id) {
if (confirm("Hapus data jalan ini?")) {
const response = await fetch(`../jalan/hapus_jalan.php?id=${id}`);
if ((await response.json()).status === 'success') location.reload();
}
}
window.editDataParsil = function(id, pemilik, status) {
const editForm = `<div class="popup-content">
<h4>Edit Parsil</h4>
<input type="text" id="edit_pemilik_parsil" value="${pemilik}">
<select id="edit_status_parsil">
<option value="SHM" ${status === 'SHM' ? 'selected' : ''}>SHM</option>
<option value="HGB" ${status === 'HGB' ? 'selected' : ''}>HGB</option>
<option value="HGU" ${status === 'HGU' ? 'selected' : ''}>HGU</option>
<option value="HP" ${status === 'HP' ? 'selected' : ''}>HP</option>
</select>
<button class="btn-save" onclick="updateDataParsil(${id})">Update Parsil</button>
</div>`;
map.closePopup();
L.popup().setLatLng(map.getCenter()).setContent(editForm).openOn(map);
}
window.updateDataParsil = async function(id) {
const fd = new FormData();
fd.append('id', id); fd.append('pemilik', document.getElementById('edit_pemilik_parsil').value); fd.append('status', document.getElementById('edit_status_parsil').value);
const response = await fetch('../parsil/update_parsil.php', { method: 'POST', body: fd });
if ((await response.json()).status === 'success') location.reload();
}
window.deleteParsil = async function(id) {
if (confirm("Hapus data parsil ini?")) {
const response = await fetch(`../parsil/hapus_parsil.php?id=${id}`);
if ((await response.json()).status === 'success') location.reload();
}
}
// RENDER DATA SPASIAL (DIKUNCI KE LAYER GROUP)
function getColorJalan(status) { return status === 'Nasional' ? 'red' : status === 'Provinsi' ? 'blue' : 'green'; }
function getColorParsil(status) {
const colors = { 'SHM': '#8B4513', 'HGB': '#FFD700', 'HGU': '#ADFF2F', 'HP': '#87CEEB' };
return colors[status] || '#CCCCCC';
}
async function loadSpasial() {
const resJ = await fetch('../jalan/get_jalan.php');
const dataJ = await resJ.json();
L.geoJSON(dataJ, {
style: function (f) { return { color: getColorJalan(f.properties.status), weight: 4 }; },
onEachFeature: function (f, layer) {
layer.bindPopup(`
<div class="popup-content">
<b>${f.properties.nama}</b><br>
Status: ${f.properties.status}<br>
Panjang: ${f.properties.panjang} m<br>
<div class="button-group">
<button class="btn-edit" onclick="editDataJalan(${f.properties.id}, '${f.properties.nama}', '${f.properties.status}')">Edit</button>
<button class="btn-delete" onclick="deleteJalan(${f.properties.id})">Hapus</button>
</div>
</div>
`);
layer.db_id = f.properties.id; layer.db_type = 'jalan';
drawnItems.addLayer(layer);
searchableLayers.push({ nama: f.properties.nama, layer: layer, tipe: 'garis' });
}
}).addTo(layerJalanDB); // Memasukkan ke Layer Group Jalan
const resP = await fetch('../parsil/get_parsil.php');
const dataP = await resP.json();
L.geoJSON(dataP, {
style: function (f) {
return { fillColor: getColorParsil(f.properties.status), fillOpacity: 0.6, color: "black", weight: 1 };
},
onEachFeature: function (f, layer) {
layer.bindPopup(`
<div class="popup-content">
<b>Pemilik: ${f.properties.pemilik}</b><br>
Status: ${f.properties.status}<br>
Luas: ${f.properties.luas} m²<br>
<div class="button-group">
<button class="btn-edit" onclick="editDataParsil(${f.properties.id}, '${f.properties.pemilik}', '${f.properties.status}')">Edit</button>
<button class="btn-delete" onclick="deleteParsil(${f.properties.id})">Hapus</button>
</div>
</div>
`);
layer.db_id = f.properties.id; layer.db_type = 'parsil';
drawnItems.addLayer(layer);
searchableLayers.push({ nama: f.properties.pemilik, layer: layer, tipe: 'bidang' });
}
}).addTo(layerParsilDB); // Memasukkan ke Layer Group Parsil
}
// MUAT BASEMAP KECAMATAN & MENU LAYER INTERAKTIF SIDEBAR
async function loadBasemapPontianak() {
const adminGrp = L.layerGroup().addTo(map);
async function fetchGeoJSON(url) {
const response = await fetch(url);
if (!response.ok) throw new Error(`File tidak ditemukan: ${url}`);
return await response.json();
}
function getColorKecamatan(nama) {
if (!nama) return "#cccccc";
const n = nama.toUpperCase();
if (n.includes("BARAT")) return "#e41a1c";
if (n.includes("KOTA")) return "#377eb8";
if (n.includes("SELATAN")) return "#4daf4a";
if (n.includes("TENGGARA")) return "#984ea3";
if (n.includes("TIMUR")) return "#ff7f00";
if (n.includes("UTARA")) return "#f1c40f";
return "#a65628";
}
try {
const dataAdmin = await fetchGeoJSON('../assets/data/Admin_Kecamatan.json');
L.geoJSON(dataAdmin, {
style: function (feature) {
const namaKecamatan = feature.properties.Ket || "";
return { color: "black", weight: 2, fillOpacity: 0.2, fillColor: getColorKecamatan(namaKecamatan) };
},
onEachFeature: function (feature, layer) {
const namaKecamatan = feature.properties.Ket || "Tidak Diketahui";
const jumlahPenduduk = feature.properties.penduduk || 0;
const formatPenduduk = new Intl.NumberFormat('id-ID').format(jumlahPenduduk);
layer.bindPopup(`
<div style="text-align: center;">
<b>Kecamatan ${namaKecamatan}</b><br>
<hr style="margin: 5px 0;">
Jumlah Penduduk: <b>${formatPenduduk} Jiwa</b>
</div>
`);
}
}).addTo(adminGrp);
} catch (e) {
console.error("Gagal memuat peta kecamatan:", e.message);
}
const baseLayers = { "OpenStreetMap": baseOsm };
// Grouping Overlays
const overlays = {
"Batas Kecamatan": adminGrp,
"SPBU Buka 24 Jam": layerSPBU24,
"SPBU Tidak Buka 24 Jam": layerSPBUNon24,
"Data Jalan (Polyline)": layerJalanDB,
"Data Parsil (Polygon)": layerParsilDB
};
// Membuat menu kontrol layer selalu terbuka (collapsed: false) dan dimasukkan ke sidebar
const layerControl = L.control.layers(baseLayers, overlays, { collapsed: false }).addTo(map);
document.getElementById('layerControlWrapper').appendChild(layerControl.getContainer());
// Memaksa peta kalkulasi ulang ukurannya di dalam layout CSS Flexbox
setTimeout(function () { map.invalidateSize(); }, 500);
}
// FUNGSI PENCARIAN DATA
window.cariData = function() {
const keyword = document.getElementById('searchInput').value.toLowerCase().trim();
if (!keyword) { alert("Masukkan kata kunci pencarian!"); return; }
let found = false;
for (let item of searchableLayers) {
if (item.nama && item.nama.toLowerCase().includes(keyword)) {
found = true;
if (item.tipe === 'titik') { map.flyTo(item.layer.getLatLng(), 17); }
else { map.flyToBounds(item.layer.getBounds(), { maxZoom: 17 }); }
item.layer.openPopup();
break;
}
}
if (!found) alert("Data tidak ditemukan! Pastikan nama SPBU, Jalan, atau Pemilik sudah benar.");
}
window.handleEnter = function(e) {
if (e.key === 'Enter') cariData();
}
loadSpasial();
loadBasemapPontianak();