first commit

This commit is contained in:
2026-06-17 15:41:34 +07:00
commit be2f139a8f
14 changed files with 1494 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
FROM php:8.2-apache
# Install mysqli extension
RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli
# Copy application files to the web server root
COPY . /var/www/html/
# Expose web port
EXPOSE 80
+1
View File
@@ -0,0 +1 @@
# gis_tugas
+33
View File
@@ -0,0 +1,33 @@
DROP DATABASE IF EXISTS gis_tugas;
CREATE DATABASE gis_tugas;
USE gis_tugas;
-- From gis_point
CREATE TABLE data_spbu (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_spbu VARCHAR(255) NOT NULL,
alamat TEXT NOT NULL,
latitude DOUBLE NOT NULL,
longitude DOUBLE NOT NULL,
jenis_layanan ENUM('24 Jam', 'Reguler') NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- From gis_polyline (and gis_polygon)
CREATE TABLE data_jalan (
id INT AUTO_INCREMENT PRIMARY KEY,
nama_jalan VARCHAR(255) NOT NULL,
status_jalan ENUM('Nasional', 'Provinsi', 'Kabupaten') NOT NULL,
geojson_data TEXT NOT NULL,
panjang_jalan FLOAT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- From gis_polygon
CREATE TABLE data_parsil (
id INT AUTO_INCREMENT PRIMARY KEY,
nomor_parsil VARCHAR(100),
status_sertifikat ENUM('SHM', 'HGB', 'HGU', 'HP'),
geojson_data TEXT,
luas_m2 FLOAT
);
+28
View File
@@ -0,0 +1,28 @@
version: '3.8'
services:
web:
build:
context: .
dockerfile: Dockerfile
environment:
- DB_HOST=db
- DB_USER=root
- DB_PASS=coolify_gis_secure_pass
- DB_NAME=gis_tugas
depends_on:
- db
db:
image: mysql:8.0
restart: always
environment:
- MYSQL_ROOT_PASSWORD=coolify_gis_secure_pass
- MYSQL_DATABASE=gis_tugas
volumes:
- db-data:/var/lib/mysql
# Automatically initialize database schema on first run
- ./database.sql:/docker-entrypoint-initdb.d/database.sql:ro
volumes:
db-data:
+322
View File
@@ -0,0 +1,322 @@
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoPortal - Dashboard WebGIS</title>
<!-- Google Fonts: Inter & Outfit -->
<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@300;400;500;600;700&family=Outfit:wght@400;600;700;800&display=swap" rel="stylesheet">
<!-- FontAwesome for Premium Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--bg-color: #0b0f19;
--card-bg: rgba(255, 255, 255, 0.03);
--card-border: rgba(255, 255, 255, 0.08);
--text-primary: #f3f4f6;
--text-secondary: #9ca3af;
--accent-primary: #3b82f6; /* Modern Blue */
--accent-point: #ec4899; /* Vibrant Pink/Red */
--accent-polyline: #8b5cf6; /* Vibrant Purple */
--accent-polygon: #10b981; /* Vibrant Green */
--glow-point: rgba(236, 72, 153, 0.15);
--glow-polyline: rgba(139, 92, 246, 0.15);
--glow-polygon: rgba(16, 185, 129, 0.15);
--glow-kesra: rgba(59, 130, 246, 0.15);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-color);
color: var(--text-primary);
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow-x: hidden;
position: relative;
}
/* Abstract Premium Background Shapes */
body::before, body::after {
content: '';
position: absolute;
width: 400px;
height: 400px;
border-radius: 50%;
filter: blur(150px);
z-index: -1;
opacity: 0.4;
}
body::before {
top: -100px;
left: -100px;
background: radial-gradient(circle, var(--accent-polyline) 0%, transparent 80%);
}
body::after {
bottom: -100px;
right: -100px;
background: radial-gradient(circle, var(--accent-primary) 0%, transparent 80%);
}
header {
padding: 3rem 2rem 1rem 2rem;
text-align: center;
}
h1 {
font-family: 'Outfit', sans-serif;
font-size: 2.8rem;
font-weight: 800;
letter-spacing: -0.05em;
background: linear-gradient(135deg, #ffffff 30%, #a5b4fc 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 0.5rem;
}
.subtitle {
font-size: 1.1rem;
color: var(--text-secondary);
max-width: 600px;
margin: 0 auto;
line-height: 1.6;
}
main {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 1.5rem;
width: 100%;
max-width: 1200px;
}
.card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 20px;
padding: 2.5rem 2rem;
text-align: center;
text-decoration: none;
color: inherit;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
position: relative;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
}
.card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.05) 0%, transparent 100%);
opacity: 0;
transition: opacity 0.4s ease;
}
.card:hover::before {
opacity: 1;
}
/* Specific card color themes */
.card-kesra {
--theme-color: var(--accent-primary);
--theme-glow: var(--glow-kesra);
}
.card-point {
--theme-color: var(--accent-point);
--theme-glow: var(--glow-point);
}
.card-polyline {
--theme-color: var(--accent-polyline);
--theme-glow: var(--glow-polyline);
}
.card-polygon {
--theme-color: var(--accent-polygon);
--theme-glow: var(--glow-polygon);
}
.card:hover {
transform: translateY(-8px);
border-color: var(--theme-color);
box-shadow: 0 20px 40px -15px var(--theme-glow);
}
.icon-wrapper {
width: 80px;
height: 80px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--card-border);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1.5rem;
font-size: 2rem;
color: var(--theme-color);
transition: all 0.4s ease;
}
.card:hover .icon-wrapper {
background: var(--theme-color);
color: var(--bg-color);
box-shadow: 0 0 20px var(--theme-color);
transform: scale(1.1);
}
.card h2 {
font-family: 'Outfit', sans-serif;
font-size: 1.6rem;
font-weight: 700;
margin-bottom: 0.75rem;
transition: color 0.3s ease;
}
.card:hover h2 {
color: var(--theme-color);
}
.card p {
font-size: 0.95rem;
color: var(--text-secondary);
line-height: 1.5;
margin-bottom: 2rem;
flex-grow: 1;
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border-radius: 12px;
font-size: 0.9rem;
font-weight: 600;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--card-border);
color: var(--text-primary);
transition: all 0.3s ease;
}
.card:hover .btn {
background: var(--theme-color);
color: var(--bg-color);
border-color: var(--theme-color);
}
footer {
padding: 2rem;
text-align: center;
border-top: 1px solid rgba(255, 255, 255, 0.05);
font-size: 0.85rem;
color: var(--text-secondary);
}
footer a {
color: var(--accent-primary);
text-decoration: none;
}
footer a:hover {
text-decoration: underline;
}
@media (max-width: 768px) {
h1 {
font-size: 2.2rem;
}
.grid-container {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<header>
<h1>Sistem Informasi Geografis</h1>
<p class="subtitle">Platform visualisasi data pemetaan berbasis spasial.</p>
</header>
<main>
<div class="grid-container">
<!-- Kesra Card -->
<a href="http://wg84wo0wsocccs4csg8w88ks.203.24.51.230.sslip.io/" class="card card-kesra" target="_blank" rel="noopener noreferrer">
<div class="icon-wrapper">
<i class="fa-solid fa-hand-holding-heart"></i>
</div>
<h2>WebGIS Kesra</h2>
<p>Sistem Informasi Geografis terpadu untuk mendata, memetakan, dan menyalurkan bantuan kepada warga yang membutuhkan melalui jaringan penyalur yang tepercaya.</p>
<div class="btn">
Buka Modul <i class="fa-solid fa-arrow-right-from-bracket"></i>
</div>
</a>
<!-- Point Card -->
<a href="point/" class="card card-point">
<div class="icon-wrapper">
<i class="fa-solid fa-location-dot"></i>
</div>
<h2>GIS Point</h2>
<p>Visualisasi dan pengelolaan data lokasi berbentuk titik koordinat seperti tempat wisata, gedung, atau fasilitas umum.</p>
<div class="btn">
Buka Modul <i class="fa-solid fa-arrow-right"></i>
</div>
</a>
<!-- Polyline Card -->
<a href="polyline/" class="card card-polyline">
<div class="icon-wrapper">
<i class="fa-solid fa-route"></i>
</div>
<h2>GIS Polyline</h2>
<p>Visualisasi jalur, rute jalan, batas garis lintang, sungai, atau jaringan transportasi menggunakan garis penghubung.</p>
<div class="btn">
Buka Modul <i class="fa-solid fa-arrow-right"></i>
</div>
</a>
<!-- Polygon Card -->
<a href="polygon/" class="card card-polygon">
<div class="icon-wrapper">
<i class="fa-solid fa-draw-polygon"></i>
</div>
<h2>GIS Polygon</h2>
<p>Visualisasi wilayah, area zonasi teritorial, batas administrasi kelurahan, daerah rawan, maupun luasan lahan.</p>
<div class="btn">
Buka Modul <i class="fa-solid fa-arrow-right"></i>
</div>
</a>
</div>
</main>
<footer>
<p>&copy; 2026 WebGIS Application. Deployed with <i class="fa-solid fa-heart" style="color: #ec4899;"></i> using Coolify.</p>
</footer>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
<?php
$host = getenv('DB_HOST') ?: "localhost";
$user = getenv('DB_USER') ?: "root";
$pass = getenv('DB_PASS') ?: "";
$name = getenv('DB_NAME') ?: "gis_tugas";
$conn = mysqli_connect($host, $user, $pass, $name);
if (!$conn) { die("Koneksi gagal: " . mysqli_connect_error()); }
?>
+366
View File
@@ -0,0 +1,366 @@
<?php include 'config.php'; ?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<title>WebGIS SPBU - Layer Groups & Control</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
:root {
--primary-navy: #0F2027; /* Gelap & Elegan */
--secondary-navy: #203A43;
--accent-blue: #2C5364;
--bg-color: #F8FAFC;
--card-bg: #FFFFFF;
--text-main: #1E293B;
--text-muted: #64748B;
--border-color: #E2E8F0;
}
body {
font-family: 'Inter', sans-serif;
margin: 0;
background: var(--bg-color);
color: var(--text-main);
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, var(--primary-navy), var(--secondary-navy));
color: white;
padding: 20px 30px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.header h2 {
margin: 0;
font-size: 22px;
font-weight: 700;
letter-spacing: -0.5px;
display: flex;
align-items: center;
gap: 10px;
}
.container {
max-width: 1200px;
margin: 30px auto;
padding: 0 20px;
}
.panel, .table-container {
background: var(--card-bg);
padding: 25px 30px;
border-radius: 16px;
margin-bottom: 25px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
border: 1px solid var(--border-color);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.panel:hover, .table-container:hover {
box-shadow: 0 12px 20px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.04);
}
.panel h3, .table-container h3 {
color: var(--primary-navy);
font-weight: 700;
margin-top: 0;
font-size: 18px;
margin-bottom: 20px;
}
.form-inline {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.input-group {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
min-width: 200px;
}
.input-group strong {
font-size: 14px;
color: var(--secondary-navy);
font-weight: 600;
}
input, select {
padding: 12px 16px;
border: 1px solid var(--border-color);
border-radius: 8px;
font-family: 'Inter', sans-serif;
font-size: 14px;
color: var(--text-main);
background-color: var(--bg-color);
transition: all 0.3s ease;
outline: none;
width: 100%;
box-sizing: border-box;
}
input:focus, select:focus {
border-color: var(--accent-blue);
box-shadow: 0 0 0 3px rgba(44, 83, 100, 0.15);
background-color: #FFF;
}
small {
color: var(--text-muted);
font-size: 13px;
width: 100%;
display: block;
margin-top: 5px;
}
#map {
height: 550px;
width: 100%;
border-radius: 16px;
border: none;
box-shadow: 0 10px 25px rgba(15, 32, 39, 0.1);
z-index: 1;
margin-bottom: 25px;
}
/* Tabel Styling */
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin-top: 15px;
}
th, td {
padding: 16px 20px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
th {
background: linear-gradient(135deg, var(--primary-navy), var(--secondary-navy));
color: white;
font-weight: 600;
text-transform: uppercase;
font-size: 12px;
letter-spacing: 0.8px;
}
th:first-child { border-top-left-radius: 10px; }
th:last-child { border-top-right-radius: 10px; }
tbody tr {
transition: background-color 0.2s ease;
}
tbody tr:hover {
background-color: #F1F5F9;
}
/* Badges & Icons */
.badge-24h {
background: rgba(16, 185, 129, 0.1);
color: #059669;
padding: 6px 14px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
border: 1px solid rgba(16, 185, 129, 0.2);
display: inline-block;
}
.badge-reg {
background: rgba(245, 158, 11, 0.1);
color: #D97706;
padding: 6px 14px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
border: 1px solid rgba(245, 158, 11, 0.2);
display: inline-block;
}
.icon-24h {
background-color: #10B981;
border-radius: 50% 50% 50% 0;
border: 2px solid #fff;
width: 24px !important; height: 24px !important;
transform: rotate(-45deg);
margin-left: -12px; margin-top: -12px;
box-shadow: 2px 2px 6px rgba(0,0,0,0.3);
transition: transform 0.2s ease;
}
.icon-reg {
background-color: #F59E0B;
border-radius: 50% 50% 50% 0;
border: 2px solid #fff;
width: 24px !important; height: 24px !important;
transform: rotate(-45deg);
margin-left: -12px; margin-top: -12px;
box-shadow: 2px 2px 6px rgba(0,0,0,0.3);
transition: transform 0.2s ease;
}
/* Leaflet Overrides */
.leaflet-control-layers, .leaflet-bar {
border: none !important;
box-shadow: 0 4px 15px rgba(0,0,0,0.1) !important;
border-radius: 12px !important;
}
.leaflet-control-layers { padding: 8px !important; font-family: 'Inter', sans-serif; }
.leaflet-bar { overflow: hidden; }
.leaflet-bar a {
border-bottom: 1px solid #eee !important;
color: var(--primary-navy) !important;
width: 32px !important;
height: 32px !important;
line-height: 32px !important;
}
.leaflet-bar a:hover {
background-color: #f8f9fa !important;
color: var(--accent-blue) !important;
}
.leaflet-popup-content-wrapper {
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.leaflet-popup-content { font-family: 'Inter', sans-serif; font-size: 14px; line-height: 1.5; }
.leaflet-popup-content b { color: var(--primary-navy); font-size: 15px; }
</style>
</head>
<body>
<div class="header">
<h2>📍 Manajemen SPBU (Point)</h2>
</div>
<div class="container">
<div class="panel">
<h3>Input Data Baru</h3>
<div class="form-inline">
<div class="input-group">
<strong>Nama SPBU:</strong>
<input type="text" id="nama_spbu" placeholder="Masukkan Nama SPBU">
</div>
<div class="input-group">
<strong>Alamat:</strong>
<input type="text" id="alamat_spbu" placeholder="Masukkan Alamat">
</div>
<div class="input-group">
<strong>Jenis Layanan:</strong>
<select id="jenis_layanan">
<option value="24 Jam">Buka 24 Jam</option>
<option value="Reguler">Reguler (Tidak 24 Jam)</option>
</select>
</div>
</div>
<small>Pilih jenis layanan, lalu gunakan ikon marker di kiri peta untuk meletakkan titik.</small>
</div>
<div id="map"></div>
<div class="table-container">
<h3>Data Informasi</h3>
<table>
<thead>
<tr>
<th>Nama SPBU</th>
<th>Alamat</th>
<th>Status</th>
<th>Koordinat</th>
</tr>
</thead>
<tbody>
<?php
$res = mysqli_query($conn, "SELECT * FROM data_spbu ORDER BY id DESC");
while($row = mysqli_fetch_assoc($res)) {
$badge = ($row['jenis_layanan'] == '24 Jam') ? 'badge-24h' : 'badge-reg';
echo "<tr>
<td>{$row['nama_spbu']}</td>
<td>{$row['alamat']}</td>
<td><span class='$badge'>{$row['jenis_layanan']}</span></td>
<td>{$row['latitude']}, {$row['longitude']}</td>
</tr>";
}
?>
</tbody>
</table>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<script>
// 1. Setup Peta & Base Maps
var osm = L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OSM' });
var sat = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { attribution: 'Esri' });
var map = L.map('map', {
center: [-0.02, 109.34],
zoom: 13,
layers: [osm]
});
// 2. Definisi Layer Groups
var group24Jam = L.layerGroup().addTo(map);
var groupReguler = L.layerGroup().addTo(map);
// 3. Layers Control (Checkbox di Pojok Kanan Atas)
var baseMaps = { "Peta Standar": osm, "Satelit": sat };
var overlayMaps = {
"<span style='color:green'>SPBU 24 Jam</span>": group24Jam,
"<span style='color:orange'>SPBU Reguler</span>": groupReguler
};
L.control.layers(baseMaps, overlayMaps, { collapsed: false }).addTo(map);
// 4. Custom Icons
var icon24h = L.divIcon({className: 'icon-24h'});
var iconReg = L.divIcon({className: 'icon-reg'});
// 5. Input Data (Drawing)
var drawnItems = new L.FeatureGroup();
map.addLayer(drawnItems);
new L.Control.Draw({
draw: { polyline:false, polygon:false, circle:false, rectangle:false, circlemarker:false, marker:true }
}).addTo(map);
map.on(L.Draw.Event.CREATED, function (e) {
var layer = e.layer;
var ll = layer.getLatLng();
var nama = document.getElementById('nama_spbu').value;
var alamat = document.getElementById('alamat_spbu').value;
var jenis = document.getElementById('jenis_layanan').value;
if(!nama || !alamat) return alert("Lengkapi data!");
var fd = new FormData();
fd.append('nama', nama); fd.append('alamat', alamat);
fd.append('lat', ll.lat); fd.append('lng', ll.lng); fd.append('jenis', jenis);
fetch('save.php', { method: 'POST', body: fd })
.then(res => res.json())
.then(data => {
if(data.status === 'success') location.reload();
});
});
// 6. Tampilkan Marker dari Database dengan Warna Berbeda
<?php
$res = mysqli_query($conn, "SELECT * FROM data_spbu");
while($row = mysqli_fetch_assoc($res)) {
$j = $row['jenis_layanan'];
$target = ($j == '24 Jam') ? 'group24Jam' : 'groupReguler';
$icon = ($j == '24 Jam') ? 'icon24h' : 'iconReg';
echo "L.marker([{$row['latitude']}, {$row['longitude']}], {icon: $icon}).addTo($target)
.bindPopup('<b>{$row['nama_spbu']}</b><br>{$row['alamat']}');\n";
}
?>
</script>
</div>
</body>
</html>
+20
View File
@@ -0,0 +1,20 @@
<?php
include 'config.php';
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$nama = mysqli_real_escape_string($conn, $_POST['nama']);
$alamat = mysqli_real_escape_string($conn, $_POST['alamat']);
$lat = $_POST['lat'];
$lng = $_POST['lng'];
$jenis = $_POST['jenis'];
$query = "INSERT INTO data_spbu (nama_spbu, alamat, latitude, longitude, jenis_layanan)
VALUES ('$nama', '$alamat', '$lat', '$lng', '$jenis')";
if (mysqli_query($conn, $query)) {
echo json_encode(['status' => 'success']);
} else {
echo json_encode(['status' => 'error']);
}
}
?>
+9
View File
@@ -0,0 +1,9 @@
<?php
$db_host = getenv('DB_HOST') ?: 'localhost';
$db_user = getenv('DB_USER') ?: 'root';
$db_pass = getenv('DB_PASS') ?: '';
$db_name = getenv('DB_NAME') ?: 'gis_tugas';
$conn = mysqli_connect($db_host, $db_user, $db_pass, $db_name);
if (!$conn) { die("Koneksi gagal: " . mysqli_connect_error()); }
?>
+267
View File
@@ -0,0 +1,267 @@
<?php include 'config.php'; ?>
<!DOCTYPE html>
<html>
<head>
<title>WebGIS Jalan & Parsil</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
:root {
--primary-navy: #0F2027; /* Gelap & Elegan */
--secondary-navy: #203A43;
--accent-blue: #2C5364;
--bg-color: #F8FAFC;
--card-bg: #FFFFFF;
--text-main: #1E293B;
--text-muted: #64748B;
--border-color: #E2E8F0;
}
body {
font-family: 'Inter', sans-serif;
margin: 0;
background: var(--bg-color);
color: var(--text-main);
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, var(--primary-navy), var(--secondary-navy));
color: white;
padding: 20px 30px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.header h2 {
margin: 0;
font-size: 22px;
font-weight: 700;
letter-spacing: -0.5px;
display: flex;
align-items: center;
gap: 10px;
}
.container {
max-width: 1200px;
margin: 30px auto;
padding: 0 20px;
}
.control-panel, .panel, .table-container {
background: var(--card-bg);
padding: 25px 30px;
border-radius: 16px;
margin-bottom: 25px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
border: 1px solid var(--border-color);
transition: transform 0.3s ease, box-shadow 0.3s ease;
display: flex;
gap: 20px;
align-items: center;
flex-wrap: wrap;
}
.control-panel:hover, .panel:hover, .table-container:hover {
box-shadow: 0 12px 20px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.04);
}
.input-group {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
min-width: 200px;
}
.input-group strong {
font-size: 14px;
color: var(--secondary-navy);
font-weight: 600;
}
input, select {
padding: 12px 16px;
border: 1px solid var(--border-color);
border-radius: 8px;
font-family: 'Inter', sans-serif;
font-size: 14px;
color: var(--text-main);
background-color: var(--bg-color);
transition: all 0.3s ease;
outline: none;
width: 100%;
box-sizing: border-box;
}
input:focus, select:focus {
border-color: var(--accent-blue);
box-shadow: 0 0 0 3px rgba(44, 83, 100, 0.15);
background-color: #FFF;
}
#map {
height: 600px;
width: 100%;
border-radius: 16px;
border: none;
box-shadow: 0 10px 25px rgba(15, 32, 39, 0.1);
z-index: 1;
margin-bottom: 25px;
}
/* Leaflet Overrides */
.leaflet-control-layers, .leaflet-bar {
border: none !important;
box-shadow: 0 4px 15px rgba(0,0,0,0.1) !important;
border-radius: 12px !important;
}
.leaflet-control-layers { padding: 8px !important; font-family: 'Inter', sans-serif; }
.leaflet-bar { overflow: hidden; }
.leaflet-bar a {
border-bottom: 1px solid #eee !important;
color: var(--primary-navy) !important;
width: 32px !important;
height: 32px !important;
line-height: 32px !important;
}
.leaflet-bar a:hover {
background-color: #f8f9fa !important;
color: var(--accent-blue) !important;
}
.leaflet-popup-content-wrapper {
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.leaflet-popup-content { font-family: 'Inter', sans-serif; font-size: 14px; line-height: 1.5; }
.leaflet-popup-content b { color: var(--primary-navy); font-size: 15px; }
</style>
</head>
<body>
<div class="header">
<h2>🗺️ Manajemen Data Geospasial (Polygon & Polyline)</h2>
</div>
<div class="container">
<div class="control-panel">
<div class="input-group">
<strong>Mode Operasi:</strong>
<select id="mode_input">
<option value="jalan">Manajemen Jalan (Line)</option>
<option value="parsil">Parsil Tanah (Polygon)</option>
</select>
</div>
<div class="input-group">
<strong>Identitas Data:</strong>
<input type="text" id="nama_input" placeholder="Masukkan Nama Jalan / No Parsil">
</div>
<div class="input-group">
<strong>Status Kepemilikan/Jalan:</strong>
<select id="status_input"></select>
</div>
</div>
<div id="map"></div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js"></script>
<script>
var map = L.map('map').setView([-0.02, 109.34], 14);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
// Konfigurasi Status & Warna
const config = {
jalan: {
status: ['Nasional', 'Provinsi', 'Kabupaten'],
warna: { 'Nasional': 'red', 'Provinsi': 'blue', 'Kabupaten': 'green' }
},
parsil: {
status: ['SHM', 'HGB', 'HGU', 'HP'],
warna: { 'SHM': '#f1c40f', 'HGB': '#9b59b6', 'HGU': '#e67e22', 'HP': '#1abc9c' }
}
};
// Update Dropdown Status Otomatis
const modeSelect = document.getElementById('mode_input');
const statusSelect = document.getElementById('status_input');
function updateStatusOptions() {
const mode = modeSelect.value;
statusSelect.innerHTML = config[mode].status.map(s => `<option value="${s}">${s}</option>`).join('');
}
modeSelect.onchange = updateStatusOptions;
updateStatusOptions();
// Fitur Drawing
var drawnItems = new L.FeatureGroup().addTo(map);
var drawControl = new L.Control.Draw({
draw: { circle: false, marker: false, circlemarker: false, rectangle: true, polyline: true, polygon: true },
edit: { featureGroup: drawnItems }
});
map.addControl(drawControl);
map.on(L.Draw.Event.CREATED, function (e) {
var layer = e.layer;
var mode = modeSelect.value;
var nama = document.getElementById('nama_input').value;
var status = statusSelect.value;
if(!nama) return alert("Isi Nama/Nomor dulu!");
let nilaiHitung = 0;
let latlngs = layer.getLatLngs();
// LOGIKA PERHITUNGAN OTOMATIS
if (mode === 'jalan') {
// Hitung Panjang (Linear distance)
for (let i = 0; i < latlngs.length - 1; i++) {
nilaiHitung += latlngs[i].distanceTo(latlngs[i+1]);
}
} else {
// Hitung Luas (Area menggunakan Turf.js)
var geojson = layer.toGeoJSON();
nilaiHitung = turf.area(geojson);
}
// AJAX Simpan ke PHP
var fd = new FormData();
fd.append('type', mode);
fd.append('nama', nama);
fd.append('status', status);
fd.append('coords', JSON.stringify(latlngs));
fd.append('nilai', nilaiHitung.toFixed(2));
fetch('save.php', { method: 'POST', body: fd })
.then(res => res.json())
.then(data => {
if(data.status === 'success') {
layer.setStyle({ color: config[mode].warna[status], fillColor: config[mode].warna[status] });
let unit = mode === 'jalan' ? ' Meter' : ' m²';
layer.bindPopup(`<b>${nama}</b><br>Status: ${status}<br>Nilai: ${nilaiHitung.toFixed(2)}${unit}`).openPopup();
drawnItems.addLayer(layer);
alert("Berhasil disimpan!");
}
});
});
// Load data lama dari database (Jalan)
<?php
$q1 = mysqli_query($conn, "SELECT * FROM data_jalan");
while($r = mysqli_fetch_assoc($q1)) {
echo "L.polyline({$r['geojson_data']}, {color: config.jalan.warna['{$r['status_jalan']}']}).addTo(map).bindPopup('<b>{$r['nama_jalan']}</b><br>Panjang: {$r['panjang_jalan']} m');";
}
// Load data lama dari database (Parsil)
$q2 = mysqli_query($conn, "SELECT * FROM data_parsil");
while($r = mysqli_fetch_assoc($q2)) {
echo "L.polygon({$r['geojson_data']}, {color: config.parsil.warna['{$r['status_sertifikat']}'], fillColor: config.parsil.warna['{$r['status_sertifikat']}']}).addTo(map).bindPopup('<b>{$r['nomor_parsil']}</b><br>Luas: {$r['luas_m2']} m²');";
}
?>
</script>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
<?php
include 'config.php';
$type = $_POST['type']; // 'jalan' atau 'parsil'
$nama = mysqli_real_escape_string($conn, $_POST['nama']);
$status = $_POST['status'];
$coords = $_POST['coords'];
$nilai = $_POST['nilai'];
if ($type == 'jalan') {
$sql = "INSERT INTO data_jalan (nama_jalan, status_jalan, geojson_data, panjang_jalan)
VALUES ('$nama', '$status', '$coords', '$nilai')";
} else {
$sql = "INSERT INTO data_parsil (nomor_parsil, status_sertifikat, geojson_data, luas_m2)
VALUES ('$nama', '$status', '$coords', '$nilai')";
}
if (mysqli_query($conn, $sql)) {
echo json_encode(['status' => 'success']);
} else {
echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
}
?>
+12
View File
@@ -0,0 +1,12 @@
<?php
$host = getenv('DB_HOST') ?: 'localhost';
$user = getenv('DB_USER') ?: 'root';
$pass = getenv('DB_PASS') ?: '';
$db = getenv('DB_NAME') ?: 'gis_tugas';
$conn = mysqli_connect($host, $user, $pass, $db);
if (!$conn) {
die("Koneksi gagal: " . mysqli_connect_error());
}
?>
+375
View File
@@ -0,0 +1,375 @@
<?php include 'config.php'; ?>
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<title>WebGIS Manajemen Jalan - Search & Filter</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
:root {
--primary-navy: #0F2027; /* Gelap & Elegan */
--secondary-navy: #203A43;
--accent-blue: #2C5364;
--bg-color: #F8FAFC;
--card-bg: #FFFFFF;
--text-main: #1E293B;
--text-muted: #64748B;
--border-color: #E2E8F0;
}
body {
font-family: 'Inter', sans-serif;
margin: 0;
background: var(--bg-color);
color: var(--text-main);
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, var(--primary-navy), var(--secondary-navy));
color: white;
padding: 20px 30px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}
.header h2 {
margin: 0;
font-size: 22px;
font-weight: 700;
letter-spacing: -0.5px;
display: flex;
align-items: center;
gap: 10px;
}
.container {
max-width: 1200px;
margin: 30px auto;
padding: 0 20px;
}
.panel, .table-container {
background: var(--card-bg);
padding: 25px 30px;
border-radius: 16px;
margin-bottom: 25px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
border: 1px solid var(--border-color);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.panel:hover, .table-container:hover {
box-shadow: 0 12px 20px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -2px rgba(0, 0, 0, 0.04);
}
.panel h3, .table-container h3 {
color: var(--primary-navy);
font-weight: 700;
margin-top: 0;
font-size: 18px;
margin-bottom: 20px;
}
.form-inline {
display: flex;
gap: 15px;
align-items: center;
flex-wrap: wrap;
}
.input-group {
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
min-width: 200px;
}
.input-group strong {
font-size: 14px;
color: var(--secondary-navy);
font-weight: 600;
}
input, select {
padding: 12px 16px;
border: 1px solid var(--border-color);
border-radius: 8px;
font-family: 'Inter', sans-serif;
font-size: 14px;
color: var(--text-main);
background-color: var(--bg-color);
transition: all 0.3s ease;
outline: none;
width: 100%;
box-sizing: border-box;
}
input:focus, select:focus {
border-color: var(--accent-blue);
box-shadow: 0 0 0 3px rgba(44, 83, 100, 0.15);
background-color: #FFF;
}
small {
color: var(--text-muted);
font-size: 13px;
width: 100%;
display: block;
margin-top: 5px;
}
#map {
height: 550px;
width: 100%;
border-radius: 16px;
border: none;
box-shadow: 0 10px 25px rgba(15, 32, 39, 0.1);
z-index: 1;
margin-bottom: 25px;
}
/* Tabel Styling */
table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
margin-top: 15px;
}
th, td {
padding: 16px 20px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
th {
background: linear-gradient(135deg, var(--primary-navy), var(--secondary-navy));
color: white;
font-weight: 600;
text-transform: uppercase;
font-size: 12px;
letter-spacing: 0.8px;
}
th:first-child { border-top-left-radius: 10px; }
th:last-child { border-top-right-radius: 10px; }
tbody tr {
transition: background-color 0.2s ease;
}
tbody tr:hover {
background-color: #F1F5F9;
}
.badge {
padding: 6px 14px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
display: inline-block;
color: white;
}
/* Leaflet Overrides */
.leaflet-control-layers, .leaflet-bar {
border: none !important;
box-shadow: 0 4px 15px rgba(0,0,0,0.1) !important;
border-radius: 12px !important;
}
.leaflet-control-layers { padding: 8px !important; font-family: 'Inter', sans-serif; }
.leaflet-bar { overflow: hidden; }
.leaflet-bar a {
border-bottom: 1px solid #eee !important;
color: var(--primary-navy) !important;
width: 32px !important;
height: 32px !important;
line-height: 32px !important;
}
.leaflet-bar a:hover {
background-color: #f8f9fa !important;
color: var(--accent-blue) !important;
}
.leaflet-popup-content-wrapper {
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.leaflet-popup-content { font-family: 'Inter', sans-serif; font-size: 14px; line-height: 1.5; }
.leaflet-popup-content b { color: var(--primary-navy); font-size: 15px; }
</style>
</head>
<body>
<div class="header">
<h2>🛣️ Manajemen Data Jalan (Polyline)</h2>
</div>
<div class="container">
<div class="panel">
<h3>Input Data Baru</h3>
<div class="form-inline">
<div class="input-group">
<strong>Nama Jalan:</strong>
<input type="text" id="nama_jalan" placeholder="Nama Jalan Baru...">
</div>
<div class="input-group">
<strong>Status Jalan:</strong>
<select id="status_jalan">
<option value="Nasional">Nasional (Merah)</option>
<option value="Provinsi">Provinsi (Biru)</option>
<option value="Kabupaten">Kabupaten (Hijau)</option>
</select>
</div>
</div>
<small>Isi nama dan status, lalu gambar garis di peta untuk menyimpan data.</small>
</div>
<div class="panel search-box">
<h3>Pencarian & Filter</h3>
<div class="form-inline">
<div class="input-group">
<strong>Cari Nama:</strong>
<input type="text" id="search_input" placeholder="Ketik nama jalan..." onkeyup="filterData()">
</div>
<div class="input-group">
<strong>Filter Status:</strong>
<select id="filter_status" onchange="filterData()">
<option value="Semua">Semua Status</option>
<option value="Nasional">Nasional</option>
<option value="Provinsi">Provinsi</option>
<option value="Kabupaten">Kabupaten</option>
</select>
</div>
</div>
</div>
<div id="map"></div>
<div class="table-container">
<h3>Daftar Jalan</h3>
<table id="tabel_jalan">
<thead>
<tr>
<th>Nama Jalan</th>
<th>Status</th>
<th>Panjang</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
<script>
// 1. Inisialisasi Map
var map = L.map('map').setView([-0.02, 109.34], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);
const gayaJalan = {
"Nasional": { color: 'red', weight: 6 },
"Provinsi": { color: 'blue', weight: 4 },
"Kabupaten": { color: 'green', weight: 3 }
};
// Arry untuk menampung semua data dari database agar bisa difilter di sisi client
let dataJalanGlobal = [];
let layerGroup = L.layerGroup().addTo(map);
// 2. Fungsi Load Data dari PHP ke JS
function loadData() {
<?php
$res = mysqli_query($conn, "SELECT * FROM data_jalan ORDER BY id DESC");
$data = [];
while($row = mysqli_fetch_assoc($res)) {
$data[] = $row;
}
echo "dataJalanGlobal = " . json_encode($data) . ";";
?>
renderData(dataJalanGlobal);
}
// 3. Fungsi Menampilkan Data ke Peta dan Tabel
function renderData(data) {
layerGroup.clearLayers(); // Bersihkan peta
const tbody = document.querySelector("#tabel_jalan tbody");
tbody.innerHTML = ""; // Bersihkan tabel
data.forEach(item => {
// Tambahkan ke Peta
const coords = JSON.parse(item.geojson_data);
const poly = L.polyline(coords, gayaJalan[item.status_jalan])
.bindPopup(`<b>${item.nama_jalan}</b><br>Status: ${item.status_jalan}<br>Panjang: ${item.panjang_jalan} m`);
layerGroup.addLayer(poly);
// Tambahkan ke Tabel
const color = gayaJalan[item.status_jalan].color;
const row = `<tr>
<td>${item.nama_jalan}</td>
<td><span class="badge" style="background:${color}">${item.status_jalan}</span></td>
<td>${parseFloat(item.panjang_jalan).toLocaleString()} m</td>
</tr>`;
tbody.innerHTML += row;
});
}
// 4. Fungsi Search dan Filter
function filterData() {
const keyword = document.getElementById('search_input').value.toLowerCase();
const statusFilt = document.getElementById('filter_status').value;
const hasilFilter = dataJalanGlobal.filter(item => {
const matchNama = item.nama_jalan.toLowerCase().includes(keyword);
const matchStatus = (statusFilt === "Semua") || (item.status_jalan === statusFilt);
return matchNama && matchStatus;
});
renderData(hasilFilter);
}
// 5. Fitur Menggambar (Leaflet Draw)
var drawnItems = new L.FeatureGroup();
map.addLayer(drawnItems);
var drawControl = new L.Control.Draw({
draw: { polygon: false, circle: false, marker: false, circlemarker: false, rectangle: false,
polyline: { shapeOptions: { color: '#333' } }
}
});
map.addControl(drawControl);
map.on(L.Draw.Event.CREATED, function (e) {
var layer = e.layer;
var latlngs = layer.getLatLngs();
var totalPanjang = 0;
for (let i = 0; i < latlngs.length - 1; i++) {
totalPanjang += latlngs[i].distanceTo(latlngs[i + 1]);
}
var nama = document.getElementById('nama_jalan').value;
var status = document.getElementById('status_jalan').value;
if(!nama) { alert("Isi Nama Jalan dulu!"); return; }
var formData = new FormData();
formData.append('nama', nama);
formData.append('status', status);
formData.append('coords', JSON.stringify(latlngs));
formData.append('panjang', totalPanjang.toFixed(2));
fetch('save.php', { method: 'POST', body: formData })
.then(res => res.json())
.then(data => {
if(data.status === 'success') {
alert("Data Berhasil Disimpan!");
location.reload();
}
});
});
// Jalankan saat pertama kali buka
loadData();
</script>
</body>
</html>
+19
View File
@@ -0,0 +1,19 @@
<?php
include 'config.php';
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$nama = mysqli_real_escape_string($conn, $_POST['nama']);
$status = $_POST['status'];
$coords = $_POST['coords'];
$panjang = $_POST['panjang'];
$query = "INSERT INTO data_jalan (nama_jalan, status_jalan, geojson_data, panjang_jalan)
VALUES ('$nama', '$status', '$coords', '$panjang')";
if (mysqli_query($conn, $query)) {
echo json_encode(['status' => 'success']);
} else {
echo json_encode(['status' => 'error', 'message' => mysqli_error($conn)]);
}
}
?>