Upload seluruh tugas SIG Point Line dan Polygon

This commit is contained in:
2026-06-11 17:04:19 +07:00
commit ba7c5c2d4d
71 changed files with 11212 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
<?php
// Database configuration
define('DB_PATH', __DIR__ . '/../db/markers.db');
// Create db directory if it doesn't exist
if (!is_dir(__DIR__ . '/../db')) {
mkdir(__DIR__ . '/../db', 0755, true);
}
// Connect to SQLite database
function getDB() {
try {
$db = new PDO('sqlite:' . DB_PATH);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Initialize database schema
initializeDB($db);
return $db;
} catch (PDOException $e) {
die(json_encode(['error' => 'Database connection failed: ' . $e->getMessage()]));
}
}
// Initialize database schema
function initializeDB($db) {
$db->exec('
CREATE TABLE IF NOT EXISTS markers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(255) NOT NULL,
latitude REAL NOT NULL,
longitude REAL NOT NULL,
open_24_hours BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS roads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(255) NOT NULL,
status VARCHAR(100) NOT NULL,
length REAL NOT NULL,
coordinates TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS parcels (
id INTEGER PRIMARY KEY AUTOINCREMENT,
owner_status VARCHAR(100) NOT NULL,
area REAL NOT NULL,
coordinates TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
');
}
// Set JSON header
header('Content-Type: application/json');
?>
+27
View File
@@ -0,0 +1,27 @@
<?php
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing parcel id']);
exit;
}
$db = getDB();
$stmt = $db->prepare('DELETE FROM parcels WHERE id = ?');
$stmt->execute([$data['id']]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+27
View File
@@ -0,0 +1,27 @@
<?php
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing marker id']);
exit;
}
$db = getDB();
$stmt = $db->prepare('DELETE FROM markers WHERE id = ?');
$stmt->execute([$data['id']]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+27
View File
@@ -0,0 +1,27 @@
<?php
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['id'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing road id']);
exit;
}
$db = getDB();
$stmt = $db->prepare('DELETE FROM roads WHERE id = ?');
$stmt->execute([$data['id']]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+22
View File
@@ -0,0 +1,22 @@
<?php
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$stmt = $db->query('SELECT id, owner_status, area, coordinates, created_at FROM parcels ORDER BY created_at DESC');
$parcels = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'parcels' => $parcels
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+22
View File
@@ -0,0 +1,22 @@
<?php
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$stmt = $db->query('SELECT id, name, latitude, longitude, open_24_hours, created_at FROM markers ORDER BY created_at DESC');
$markers = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'markers' => $markers
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+22
View File
@@ -0,0 +1,22 @@
<?php
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
try {
$db = getDB();
$stmt = $db->query('SELECT id, name, status, length, coordinates, created_at FROM roads ORDER BY created_at DESC');
$roads = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'success' => true,
'roads' => $roads
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+37
View File
@@ -0,0 +1,37 @@
<?php
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['owner_status'], $data['area'], $data['coordinates'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required fields']);
exit;
}
$db = getDB();
$stmt = $db->prepare('INSERT INTO parcels (owner_status, area, coordinates) VALUES (?, ?, ?)');
$stmt->execute([
$data['owner_status'],
(float)$data['area'],
json_encode($data['coordinates'])
]);
echo json_encode([
'success' => true,
'id' => $db->lastInsertId(),
'owner_status' => $data['owner_status'],
'area' => (float)$data['area'],
'coordinates' => json_encode($data['coordinates'])
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+49
View File
@@ -0,0 +1,49 @@
<?php
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
// Validate required fields
if (!isset($data['name']) || !isset($data['latitude']) || !isset($data['longitude'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required fields: name, latitude, or longitude']);
exit;
}
// Validate coordinates are numeric
if (!is_numeric($data['latitude']) || !is_numeric($data['longitude'])) {
http_response_code(400);
echo json_encode(['error' => 'Latitude and longitude must be numeric values']);
exit;
}
$db = getDB();
$stmt = $db->prepare('INSERT INTO markers (name, latitude, longitude, open_24_hours) VALUES (?, ?, ?, ?)');
$open_24_hours = isset($data['open_24_hours']) ? (int)$data['open_24_hours'] : 0;
$stmt->execute([
$data['name'],
(float)$data['latitude'],
(float)$data['longitude'],
$open_24_hours
]);
echo json_encode([
'success' => true,
'id' => $db->lastInsertId(),
'name' => $data['name'],
'latitude' => (float)$data['latitude'],
'longitude' => (float)$data['longitude'],
'open_24_hours' => $open_24_hours
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+39
View File
@@ -0,0 +1,39 @@
<?php
require_once 'config.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
$data = json_decode(file_get_contents('php://input'), true);
if (!isset($data['name'], $data['status'], $data['length'], $data['coordinates'])) {
http_response_code(400);
echo json_encode(['error' => 'Missing required fields']);
exit;
}
$db = getDB();
$stmt = $db->prepare('INSERT INTO roads (name, status, length, coordinates) VALUES (?, ?, ?, ?)');
$stmt->execute([
$data['name'],
$data['status'],
(float)$data['length'],
json_encode($data['coordinates'])
]);
echo json_encode([
'success' => true,
'id' => $db->lastInsertId(),
'name' => $data['name'],
'status' => $data['status'],
'length' => (float)$data['length'],
'coordinates' => json_encode($data['coordinates'])
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
}
?>
+255
View File
@@ -0,0 +1,255 @@
let map;
let markers = {};
let markerCount = 0;
let currentMapClickCoords = null;
// Initialize map and load markers
document.addEventListener('DOMContentLoaded', function() {
initMap();
loadMarkers();
setupModal();
});
function setupModal() {
const modal = document.getElementById('point-modal');
const addBtn = document.getElementById('add-point-btn');
const closeBtn = document.querySelector('.close');
const form = document.getElementById('point-form');
addBtn.onclick = function() {
openPointModal(null);
}
closeBtn.onclick = function() {
closePointModal();
}
window.onclick = function(event) {
if (event.target === modal) {
closePointModal();
}
}
form.onsubmit = function(e) {
e.preventDefault();
submitPointForm();
}
}
function openPointModal(coords) {
const modal = document.getElementById('point-modal');
const form = document.getElementById('point-form');
const latInput = document.getElementById('point-latitude');
const lngInput = document.getElementById('point-longitude');
const nameInput = document.getElementById('point-name');
const formInfo = document.getElementById('form-info');
const modalTitle = document.getElementById('modal-title');
form.reset();
currentMapClickCoords = coords;
if (coords) {
latInput.value = coords.lat.toFixed(6);
lngInput.value = coords.lng.toFixed(6);
latInput.readOnly = true;
lngInput.readOnly = true;
modalTitle.textContent = 'Create Point';
formInfo.textContent = '✓ Coordinates auto-filled from map click. Just add a name!';
formInfo.classList.add('show');
nameInput.focus();
} else {
latInput.value = '';
lngInput.value = '';
latInput.readOnly = false;
lngInput.readOnly = false;
modalTitle.textContent = 'Add New Point';
formInfo.classList.remove('show');
nameInput.focus();
}
modal.classList.add('show');
}
function closePointModal() {
const modal = document.getElementById('point-modal');
modal.classList.remove('show');
currentMapClickCoords = null;
}
function submitPointForm() {
const name = document.getElementById('point-name').value.trim();
const latitude = parseFloat(document.getElementById('point-latitude').value);
const longitude = parseFloat(document.getElementById('point-longitude').value);
const open_24_hours = document.getElementById('point-24hours').checked ? 1 : 0;
// Validate inputs
if (!name) {
showNotification('Please enter a name for the point', true);
return;
}
if (isNaN(latitude) || latitude < -90 || latitude > 90) {
showNotification('Please enter a valid latitude (-90 to 90)', true);
return;
}
if (isNaN(longitude) || longitude < -180 || longitude > 180) {
showNotification('Please enter a valid longitude (-180 to 180)', true);
return;
}
addMarker(name, latitude, longitude, open_24_hours);
}
function initMap() {
// Create map centered on a default location (San Francisco)
map = L.map('map').setView([37.7749, -122.4194], 13);
// Add OpenStreetMap tile layer
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 19
}).addTo(map);
// Handle map click to add marker with auto-filled coordinates
map.on('click', function(e) {
openPointModal({
lat: e.latlng.lat,
lng: e.latlng.lng
});
});
}
function addMarker(name, lat, lng, open_24_hours) {
// Save to database
fetch('api/save_point.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: name,
latitude: lat,
longitude: lng,
open_24_hours: open_24_hours
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
displayMarker(data.id, data.name, data.latitude, data.longitude, data.open_24_hours);
updateMarkerCount();
showNotification('✓ Point added successfully');
closePointModal();
} else {
showNotification('Error adding point: ' + data.error, true);
}
})
.catch(error => {
console.error('Error:', error);
showNotification('Error adding point', true);
});
}
function displayMarker(id, name, lat, lng, open_24_hours) {
const marker = L.marker([lat, lng]).addTo(map);
const hoursText = open_24_hours ? '🕐 Open 24 Hours' : '🕐 Limited Hours';
const popupContent = `
<div class="marker-popup">
<div class="marker-popup-name">${escapeHtml(name)}</div>
<div class="marker-popup-hours">${hoursText}</div>
<div class="marker-popup-info">
<span>Lat: ${lat.toFixed(4)}</span><br>
<span>Lng: ${lng.toFixed(4)}</span>
</div>
<button onclick="deleteMarker(${id})">Delete</button>
</div>
`;
marker.bindPopup(popupContent);
markers[id] = marker;
markerCount++;
}
function loadMarkers() {
fetch('api/get_points.php')
.then(response => response.json())
.then(data => {
if (data.success && data.markers) {
data.markers.forEach(marker => {
displayMarker(
marker.id,
marker.name,
marker.latitude,
marker.longitude,
marker.open_24_hours
);
});
updateMarkerCount();
}
})
.catch(error => console.error('Error loading markers:', error));
}
function deleteMarker(id) {
if (!confirm('Are you sure you want to delete this point?')) {
return;
}
fetch('api/delete_point.php', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ id: id })
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (markers[id]) {
map.removeLayer(markers[id]);
delete markers[id];
markerCount--;
updateMarkerCount();
showNotification('✓ Point deleted');
}
} else {
showNotification('Error deleting point: ' + data.error, true);
}
})
.catch(error => {
console.error('Error:', error);
showNotification('Error deleting point', true);
});
}
function updateMarkerCount() {
const count = Object.keys(markers).length;
const countElement = document.getElementById('marker-count');
if (countElement) {
countElement.textContent = count;
}
}
function showNotification(message, isError = false) {
const notification = document.createElement('div');
notification.className = 'notification' + (isError ? ' error' : '');
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, m => map[m]);
}
+567
View File
@@ -0,0 +1,567 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
background-color: #f0f2f5;
color: #333;
}
html, body, .app-container {
height: 100%;
width: 100%;
}
.app-container {
display: flex;
overflow: hidden;
}
/* ============ SIDEBAR ============ */
.sidebar {
width: 320px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
overflow-y: auto;
display: flex;
flex-direction: column;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
z-index: 100;
}
.sidebar-header {
padding: 30px 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
text-align: center;
}
.sidebar-header h1 {
font-size: 28px;
margin-bottom: 5px;
font-weight: 700;
letter-spacing: -0.5px;
}
.subtitle {
font-size: 13px;
opacity: 0.9;
font-weight: 400;
}
.sidebar-content {
flex: 1;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.stats-card {
background: rgba(255, 255, 255, 0.15);
padding: 20px;
border-radius: 12px;
text-align: center;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.stat-number {
font-size: 36px;
font-weight: 700;
margin-bottom: 8px;
}
.stat-label {
font-size: 13px;
opacity: 0.9;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* ============ BUTTONS ============ */
.sidebar-actions,
.draw-actions {
display: grid;
gap: 10px;
}
.hidden {
display: none !important;
}
.btn-primary,
.btn-secondary {
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
padding: 12px 20px;
display: inline-flex;
align-items: center;
gap: 8px;
width: 100%;
justify-content: center;
}
.btn-primary {
background-color: #4CAF50;
color: white;
}
.btn-primary:hover {
background-color: #45a049;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
}
.btn-primary.btn-lg {
padding: 16px 24px;
font-size: 15px;
}
.btn-icon {
font-size: 18px;
font-weight: bold;
}
.btn-secondary {
background-color: rgba(255, 255, 255, 0.2);
color: white;
}
.btn-secondary:hover {
background-color: rgba(255, 255, 255, 0.3);
}
/* ============ INSTRUCTION BOX ============ */
.instructions-box,
.info-box {
background: rgba(255, 255, 255, 0.1);
padding: 15px;
border-radius: 8px;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.15);
}
.instructions-box h3,
.info-box h3 {
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
opacity: 0.95;
}
.instructions-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.instruction-item {
display: flex;
align-items: flex-start;
gap: 10px;
font-size: 13px;
}
.instruction-num {
background: #4CAF50;
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 12px;
flex-shrink: 0;
}
.info-box p {
font-size: 12px;
line-height: 1.5;
opacity: 0.9;
}
/* ============ MAIN CONTENT ============ */
.main-content {
flex: 1;
position: relative;
overflow: hidden;
}
#map {
width: 100%;
height: 100%;
display: block;
}
/* ============ MODAL ============ */
.modal {
display: none;
position: fixed;
z-index: 2000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
animation: fadeIn 0.3s ease-in-out;
align-items: center;
justify-content: center;
}
.modal.show {
display: flex;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal-content {
background-color: white;
padding: 30px;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
animation: slideUp 0.3s ease-in-out;
}
@keyframes slideUp {
from {
transform: translateY(30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 2px solid #f0f2f5;
}
.modal-header h2 {
color: #333;
font-size: 20px;
margin: 0;
}
.close {
color: #999;
font-size: 28px;
font-weight: bold;
cursor: pointer;
transition: color 0.2s;
line-height: 1;
background: none;
border: none;
padding: 0;
}
.close:hover {
color: #333;
}
/* ============ FORM ============ */
#point-form {
display: flex;
flex-direction: column;
gap: 18px;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group label {
font-weight: 600;
margin-bottom: 8px;
color: #333;
font-size: 14px;
}
.form-group input[type="text"],
.form-group input[type="number"] {
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
font-family: inherit;
transition: all 0.2s;
}
.form-group input[type="text"]:focus,
.form-group input[type="number"]:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.form-group input[type="text"][readonly-display],
.form-group input[type="number"][readonly-display] {
background-color: #f9f9f9;
cursor: default;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
.checkbox-group {
flex-direction: row;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 10px;
font-weight: 500;
cursor: pointer;
font-size: 14px;
}
.checkbox-label input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #667eea;
}
.form-info {
padding: 10px 12px;
background-color: #e8f4f8;
border-left: 4px solid #667eea;
border-radius: 4px;
font-size: 12px;
color: #555;
display: none;
}
.form-info.show {
display: block;
}
.form-actions {
display: flex;
gap: 10px;
margin-top: 15px;
justify-content: flex-end;
}
.form-actions .btn-primary,
.form-actions .btn-secondary {
width: auto;
flex: 1;
}
/* ============ NOTIFICATIONS ============ */
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 14px 20px;
border-radius: 8px;
background-color: #4CAF50;
color: white;
z-index: 3000;
animation: slideInRight 0.3s ease-in-out;
max-width: 350px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
font-size: 14px;
font-weight: 500;
}
.notification.error {
background-color: #f44336;
}
@keyframes slideInRight {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* ============ LEAFLET MARKER POPUPS ============ */
.leaflet-popup-content {
margin: 0;
padding: 0;
}
.marker-popup {
padding: 12px;
text-align: center;
}
.marker-popup-name {
font-weight: 700;
font-size: 15px;
margin-bottom: 10px;
color: #333;
}
.marker-popup-hours {
font-size: 13px;
margin: 8px 0;
color: #4CAF50;
font-weight: 600;
}
.marker-popup-info {
font-size: 12px;
color: #666;
margin: 10px 0;
line-height: 1.6;
}
.marker-popup button {
background-color: #f44336;
color: white;
border: none;
padding: 10px 14px;
border-radius: 6px;
cursor: pointer;
width: 100%;
margin-top: 10px;
font-size: 13px;
font-weight: 600;
transition: background-color 0.2s;
}
.marker-popup button:hover {
background-color: #da190b;
}
/* ============ SCROLLBAR ============ */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
/* ============ RESPONSIVE ============ */
@media (max-width: 768px) {
.app-container {
flex-direction: column;
}
.sidebar {
width: 100%;
max-height: 40vh;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
border-right: none;
}
.sidebar-header {
padding: 20px 15px;
}
.sidebar-header h1 {
font-size: 24px;
}
.sidebar-content {
flex-direction: row;
gap: 15px;
overflow-x: auto;
padding: 15px;
}
.stats-card {
min-width: 150px;
}
.instructions-box,
.info-box {
display: none;
}
.modal-content {
width: calc(100% - 20px);
max-height: 80vh;
}
.form-row {
grid-template-columns: 1fr;
}
.notification {
left: 10px;
right: 10px;
max-width: none;
}
}
@media (max-width: 480px) {
.sidebar {
max-height: 35vh;
}
.sidebar-header {
padding: 15px 10px;
}
.sidebar-header h1 {
font-size: 20px;
}
.modal-content {
padding: 20px;
}
.form-actions {
flex-direction: column;
}
.form-actions .btn-primary,
.form-actions .btn-secondary {
width: 100%;
}
}
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
db/*.db
.DS_Store
*.log
+212
View File
@@ -0,0 +1,212 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Map Point Manager</title>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" />
<!-- Custom CSS -->
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div class="app-container">
<!-- Sidebar -->
<aside class="sidebar">
<div class="sidebar-header">
<h1>🗺️ MapPoints</h1>
<p class="subtitle">Manage Your Locations</p>
</div>
<div class="sidebar-content">
<div class="stats-grid">
<div class="stats-card">
<div class="stat-number" id="marker-count">0</div>
<div class="stat-label">Point</div>
</div>
<div class="stats-card">
<div class="stat-number" id="road-count">0</div>
<div class="stat-label">Road</div>
</div>
<div class="stats-card">
<div class="stat-number" id="parcel-count">0</div>
<div class="stat-label">Parcel</div>
</div>
</div>
<div class="sidebar-actions">
<button id="add-point-btn" class="btn-primary btn-lg">
<span class="btn-icon">+</span>
Add Point Manually
</button>
<button id="draw-road-btn" class="btn-primary btn-lg">
<span class="btn-icon">🛣️</span>
Draw Road
</button>
<button id="draw-parcel-btn" class="btn-primary btn-lg">
<span class="btn-icon">🧱</span>
Draw Parcel
</button>
</div>
<div class="draw-actions hidden" id="draw-actions">
<button id="finish-draw-btn" class="btn-secondary">Finish Draw</button>
<button id="cancel-draw-btn" class="btn-secondary">Cancel Draw</button>
</div>
<div class="instructions-box">
<h3>How to Use</h3>
<div class="instructions-list">
<div class="instruction-item">
<span class="instruction-num">1</span>
<span>Click on the map to create a point or draw a line/polygon.</span>
</div>
<div class="instruction-item">
<span class="instruction-num">2</span>
<span>Use Draw Road or Draw Parcel, then click vertices on the map.</span>
</div>
<div class="instruction-item">
<span class="instruction-num">3</span>
<span>Finish the shape and save; length/area is calculated automatically.</span>
</div>
</div>
</div>
<div class="info-box">
<h3>💡 Tip</h3>
<p>Click on map for quick point creation with auto-filled coordinates, or use "Add Point Manually" button for custom entry.</p>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="main-content">
<div id="map"></div>
</main>
</div>
<!-- Feature Modal -->
<div id="feature-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title">Add New Feature</h2>
<span class="close">&times;</span>
</div>
<form id="feature-form">
<div class="form-group" id="field-name-group">
<label for="feature-name" id="label-feature-name">Name *</label>
<input type="text" id="feature-name" name="name" required
placeholder="e.g., Coffee Shop, Jalan Utama, Kavling A">
</div>
<div class="form-group" id="field-status-group" style="display:none;">
<label for="feature-status">Status *</label>
<select id="feature-status" name="status">
<option value="">Select status</option>
</select>
</div>
<div class="form-row" id="point-coords">
<div class="form-group">
<label for="feature-latitude">Latitude *</label>
<input type="number" id="feature-latitude" name="latitude"
step="any" placeholder="Auto-filled" readonly-display>
</div>
<div class="form-group">
<label for="feature-longitude">Longitude *</label>
<input type="number" id="feature-longitude" name="longitude"
step="any" placeholder="Auto-filled" readonly-display>
</div>
</div>
<div class="form-group checkbox-group" id="feature-checkbox-group">
<label class="checkbox-label" for="feature-24hours">
<input type="checkbox" id="feature-24hours" name="open_24_hours">
<span>Open 24 Hours</span>
</label>
</div>
<div class="form-info" id="feature-form-info"></div>
<div class="form-actions">
<button type="button" class="btn-secondary" onclick="closeFeatureModal()">Cancel</button>
<button type="submit" class="btn-primary">Save</button>
</div>
</form>
</div>
</div>
<!-- Road Modal -->
<div id="road-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="road-modal-title">Add Road</h2>
<span class="close road-close">&times;</span>
</div>
<form id="road-form">
<div class="form-group">
<label for="road-name">Road Name *</label>
<input type="text" id="road-name" name="roadName" required placeholder="e.g., Jalan Ahmad Yani">
</div>
<div class="form-group">
<label for="road-status">Road Status *</label>
<select id="road-status" name="roadStatus" required>
<option value="Jalan Nasional">Jalan Nasional</option>
<option value="Jalan Provinsi">Jalan Provinsi</option>
<option value="Jalan Kabupaten">Jalan Kabupaten</option>
</select>
</div>
<div class="form-group">
<label for="road-length">Panjang Jalan (meter)</label>
<input type="number" id="road-length" name="roadLength" readonly-display readonly>
</div>
<div class="form-info" id="road-form-info"></div>
<div class="form-actions">
<button type="button" class="btn-secondary" onclick="closeRoadModal()">Cancel</button>
<button type="submit" class="btn-primary">Save Road</button>
</div>
</form>
</div>
</div>
<!-- Parcel Modal -->
<div id="parcel-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="parcel-modal-title">Add Parcel</h2>
<span class="close parcel-close">&times;</span>
</div>
<form id="parcel-form">
<div class="form-group">
<label for="parcel-owner">Status Kepemilikan *</label>
<select id="parcel-owner" name="parcelOwner" required>
<option value="SHM">Sertifikat Hak Milik (SHM)</option>
<option value="HGB">Sertifikat Hak Guna Bangunan (HGB)</option>
<option value="HGU">Sertifikat Hak Guna Usaha (HGU)</option>
<option value="HP">Sertifikat Hak Pakai (HP)</option>
</select>
</div>
<div class="form-group">
<label for="parcel-area">Luas Tanah (m²)</label>
<input type="number" id="parcel-area" name="parcelArea" readonly-display readonly>
</div>
<div class="form-info" id="parcel-form-info"></div>
<div class="form-actions">
<button type="button" class="btn-secondary" onclick="closeParcelModal()">Cancel</button>
<button type="submit" class="btn-primary">Save Parcel</button>
</div>
</form>
</div>
</div>
<!-- Leaflet JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.geometryutil/0.9.3/leaflet.geometryutil.min.js"></script>
<!-- Custom JS -->
<script src="js/app.js"></script>
</body>
</html>
+866
View File
@@ -0,0 +1,866 @@
let map;
let points = {};
let roads = {};
let parcels = {};
let currentMapClickCoords = null;
let drawMode = null;
let currentDrawCoords = [];
let currentDrawLayer = null;
const STORAGE_KEYS = {
points: 'mappoints_local_points',
roads: 'mappoints_local_roads',
parcels: 'mappoints_local_parcels'
};
// Initialize map and load stored data
document.addEventListener('DOMContentLoaded', function() {
initMap();
setupModal();
loadAllData();
});
function setupModal() {
const addPointBtn = document.getElementById('add-point-btn');
const drawRoadBtn = document.getElementById('draw-road-btn');
const drawParcelBtn = document.getElementById('draw-parcel-btn');
const finishDrawBtn = document.getElementById('finish-draw-btn');
const cancelDrawBtn = document.getElementById('cancel-draw-btn');
const featureCloseBtn = document.querySelector('#feature-modal .close');
const featureForm = document.getElementById('feature-form');
const roadCloseBtn = document.querySelector('.road-close');
const roadForm = document.getElementById('road-form');
const parcelCloseBtn = document.querySelector('.parcel-close');
const parcelForm = document.getElementById('parcel-form');
addPointBtn.onclick = function() {
cancelDraw();
openPointModal(null);
};
drawRoadBtn.onclick = function() {
startDraw('road');
};
drawParcelBtn.onclick = function() {
startDraw('parcel');
};
finishDrawBtn.onclick = function() {
finishDraw();
};
cancelDrawBtn.onclick = function() {
cancelDraw();
};
featureCloseBtn.onclick = function() {
closePointModal();
};
featureForm.onsubmit = function(e) {
e.preventDefault();
submitPointForm();
};
roadCloseBtn.onclick = closeRoadModal;
roadForm.onsubmit = function(e) {
e.preventDefault();
submitRoadForm();
};
parcelCloseBtn.onclick = closeParcelModal;
parcelForm.onsubmit = function(e) {
e.preventDefault();
submitParcelForm();
};
}
function initMap() {
map = L.map('map').setView([-0.0225, 109.3425], 13);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 19
}).addTo(map);
map.on('click', function(e) {
if (drawMode) {
addDrawVertex(e.latlng);
} else {
openPointModal(e.latlng);
}
});
}
function loadAllData() {
loadPoints();
loadRoads();
loadParcels();
}
function loadPoints() {
fetch('api/get_points.php')
.then(response => response.json())
.then(data => {
if (data.success && Array.isArray(data.markers)) {
data.markers.forEach(point => {
displayPoint(point.id, point.name, point.latitude, point.longitude, point.open_24_hours);
});
}
})
.catch(error => console.warn('Unable to load points from backend:', error))
.finally(() => {
loadLocalPoints().forEach(point => {
displayPoint(point.id, point.name, point.latitude, point.longitude, point.open_24_hours, true);
});
updateCounts();
});
}
function loadRoads() {
fetch('api/get_roads.php')
.then(response => response.json())
.then(data => {
if (data.success && Array.isArray(data.roads)) {
data.roads.forEach(road => {
displayRoad(road.id, road.name, road.status, parseFloat(road.length), JSON.parse(road.coordinates));
});
}
})
.catch(error => console.warn('Unable to load roads from backend:', error))
.finally(() => {
loadLocalRoads().forEach(road => {
displayRoad(road.id, road.name, road.status, road.length, road.coordinates, true);
});
updateCounts();
});
}
function loadParcels() {
fetch('api/get_parcels.php')
.then(response => response.json())
.then(data => {
if (data.success && Array.isArray(data.parcels)) {
data.parcels.forEach(parcel => {
displayParcel(parcel.id, parcel.owner_status, parseFloat(parcel.area), JSON.parse(parcel.coordinates));
});
}
})
.catch(error => console.warn('Unable to load parcels from backend:', error))
.finally(() => {
loadLocalParcels().forEach(parcel => {
displayParcel(parcel.id, parcel.owner_status, parcel.area, parcel.coordinates, true);
});
updateCounts();
});
}
function loadLocalPoints() {
try {
const raw = localStorage.getItem(STORAGE_KEYS.points);
const data = raw ? JSON.parse(raw) : [];
return Array.isArray(data) ? data : [];
} catch {
return [];
}
}
function loadLocalRoads() {
try {
const raw = localStorage.getItem(STORAGE_KEYS.roads);
const data = raw ? JSON.parse(raw) : [];
return Array.isArray(data) ? data : [];
} catch {
return [];
}
}
function loadLocalParcels() {
try {
const raw = localStorage.getItem(STORAGE_KEYS.parcels);
const data = raw ? JSON.parse(raw) : [];
return Array.isArray(data) ? data : [];
} catch {
return [];
}
}
function persistLocalPoints() {
const localPoints = Object.values(points)
.filter(item => item._local)
.map(item => ({
id: item._localId,
name: item._name,
latitude: item.getLatLng().lat,
longitude: item.getLatLng().lng,
open_24_hours: item._open24
}));
localStorage.setItem(STORAGE_KEYS.points, JSON.stringify(localPoints));
}
function persistLocalRoads() {
const localRoads = Object.values(roads)
.filter(item => item._local)
.map(item => ({
id: item._localId,
name: item._name,
status: item._status,
length: item._length,
coordinates: item._coordinates
}));
localStorage.setItem(STORAGE_KEYS.roads, JSON.stringify(localRoads));
}
function persistLocalParcels() {
const localParcels = Object.values(parcels)
.filter(item => item._local)
.map(item => ({
id: item._localId,
owner_status: item._ownerStatus,
area: item._area,
coordinates: item._coordinates
}));
localStorage.setItem(STORAGE_KEYS.parcels, JSON.stringify(localParcels));
}
function generateLocalId(prefix) {
return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
}
function startDraw(type) {
cancelDraw();
drawMode = type;
currentDrawCoords = [];
showDrawControls(true);
showNotification(type === 'road' ? 'Draw road vertices on the map.' : 'Draw parcel vertices on the map.');
}
function showDrawControls(show) {
const drawActions = document.getElementById('draw-actions');
if (drawActions) {
drawActions.classList.toggle('hidden', !show);
}
}
function cancelDraw() {
drawMode = null;
currentDrawCoords = [];
if (currentDrawLayer) {
map.removeLayer(currentDrawLayer);
currentDrawLayer = null;
}
showDrawControls(false);
}
function finishDraw() {
if (!drawMode) {
return;
}
if (drawMode === 'road') {
if (currentDrawCoords.length < 2) {
showNotification('Road must have at least 2 points.', true);
return;
}
openRoadModal();
} else if (drawMode === 'parcel') {
if (currentDrawCoords.length < 3) {
showNotification('Parcel must have at least 3 points.', true);
return;
}
openParcelModal();
}
}
function addDrawVertex(latlng) {
currentDrawCoords.push(latlng);
if (currentDrawLayer) {
map.removeLayer(currentDrawLayer);
currentDrawLayer = null;
}
if (drawMode === 'road') {
currentDrawLayer = L.polyline(currentDrawCoords, {
color: '#ff9800',
dashArray: '5,8',
weight: 5
}).addTo(map);
} else if (drawMode === 'parcel') {
currentDrawLayer = L.polygon(currentDrawCoords, {
color: '#4CAF50',
dashArray: '5,8',
weight: 4,
fillOpacity: 0.15
}).addTo(map);
}
showNotification(`Vertex added (${currentDrawCoords.length})`);
}
function openPointModal(coords) {
currentMapClickCoords = coords;
const modal = document.getElementById('feature-modal');
const form = document.getElementById('feature-form');
const latInput = document.getElementById('feature-latitude');
const lngInput = document.getElementById('feature-longitude');
const nameInput = document.getElementById('feature-name');
const statusGroup = document.getElementById('field-status-group');
const checkboxGroup = document.getElementById('feature-checkbox-group');
const modalTitle = document.getElementById('modal-title');
const formInfo = document.getElementById('feature-form-info');
form.reset();
statusGroup.style.display = 'none';
checkboxGroup.style.display = 'flex';
formInfo.classList.remove('show');
if (coords) {
latInput.value = coords.lat.toFixed(6);
lngInput.value = coords.lng.toFixed(6);
latInput.readOnly = true;
lngInput.readOnly = true;
modalTitle.textContent = 'Create Point';
formInfo.textContent = 'Coordinates auto-filled from map click.';
formInfo.classList.add('show');
} else {
latInput.value = '';
lngInput.value = '';
latInput.readOnly = false;
lngInput.readOnly = false;
modalTitle.textContent = 'Add New Point';
formInfo.textContent = 'Enter coordinates manually or click on the map.';
formInfo.classList.add('show');
}
nameInput.focus();
modal.classList.add('show');
}
function closePointModal() {
const modal = document.getElementById('feature-modal');
modal.classList.remove('show');
currentMapClickCoords = null;
}
function closeFeatureModal() {
closePointModal();
}
function submitPointForm() {
const name = document.getElementById('feature-name').value.trim();
const latitude = parseFloat(document.getElementById('feature-latitude').value);
const longitude = parseFloat(document.getElementById('feature-longitude').value);
const open_24_hours = document.getElementById('feature-24hours').checked ? 1 : 0;
if (!name) {
showNotification('Please enter a point name.', true);
return;
}
if (isNaN(latitude) || isNaN(longitude)) {
showNotification('Please enter valid coordinates.', true);
return;
}
addPoint(name, latitude, longitude, open_24_hours);
closePointModal();
}
function addPoint(name, lat, lng, open_24_hours) {
fetch('api/save_point.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, latitude: lat, longitude: lng, open_24_hours })
})
.then(response => response.json())
.then(data => {
if (data.success) {
displayPoint(data.id, data.name, data.latitude, data.longitude, data.open_24_hours);
showNotification('Point saved successfully.');
updateCounts();
} else {
addLocalPoint(name, lat, lng, open_24_hours);
}
})
.catch(() => {
addLocalPoint(name, lat, lng, open_24_hours);
});
}
function addLocalPoint(name, lat, lng, open_24_hours) {
const id = generateLocalId('point');
displayPoint(id, name, lat, lng, open_24_hours, true);
persistLocalPoints();
showNotification('Point saved locally.');
updateCounts();
}
function displayPoint(id, name, lat, lng, open_24_hours, isLocal = false) {
if (points[id]) {
map.removeLayer(points[id]);
delete points[id];
}
const marker = L.marker([lat, lng], { draggable: true }).addTo(map);
marker._local = isLocal;
marker._localId = id;
marker._name = name;
marker._open24 = open_24_hours;
marker._type = 'point';
marker.bindPopup(getPointPopupContent(id, name, lat, lng, open_24_hours));
marker.on('popupopen', function(e) {
bindPopupDeleteAction(e.popup.getElement(), 'point', id);
});
marker.on('dragend', function(e) {
const position = e.target.getLatLng();
updatePointPosition(id, position.lat, position.lng);
});
points[id] = marker;
}
function updatePointPosition(id, lat, lng) {
const marker = points[id];
if (!marker) {
return;
}
marker._name = marker._name || marker._localId;
marker.bindPopup(getPointPopupContent(id, marker._name, lat, lng, marker._open24));
marker.setLatLng([lat, lng]);
if (marker._local) {
persistLocalPoints();
}
}
function getPointPopupContent(id, name, lat, lng, open_24_hours) {
const hoursText = open_24_hours ? '🕐 Open 24 Hours' : '🕐 Limited Hours';
return `
<div class="marker-popup" data-feature-id="${escapeHtml(String(id))}" data-feature-type="point">
<div class="marker-popup-name">${escapeHtml(name)}</div>
<div class="marker-popup-hours">${hoursText}</div>
<div class="marker-popup-info">
<span>Lat: ${lat.toFixed(4)}</span><br>
<span>Lng: ${lng.toFixed(4)}</span>
</div>
<button type="button" class="delete-feature-btn">Delete</button>
</div>
`;
}
function openRoadModal() {
const modal = document.getElementById('road-modal');
const roadLength = document.getElementById('road-length');
const roadFormInfo = document.getElementById('road-form-info');
const roadName = document.getElementById('road-name');
roadName.value = '';
roadLength.value = computeRoadLength(currentDrawCoords).toFixed(2);
roadFormInfo.textContent = `Calculated automatically from ${currentDrawCoords.length} vertices.`;
modal.classList.add('show');
}
function closeRoadModal() {
const modal = document.getElementById('road-modal');
modal.classList.remove('show');
}
function submitRoadForm() {
const name = document.getElementById('road-name').value.trim();
const status = document.getElementById('road-status').value;
const length = parseFloat(document.getElementById('road-length').value);
if (!name) {
showNotification('Please enter a road name.', true);
return;
}
if (!status) {
showNotification('Please choose a road status.', true);
return;
}
addRoad(name, status, currentDrawCoords.slice(), length);
closeRoadModal();
cancelDraw();
}
function addRoad(name, status, coordinates, length) {
fetch('api/save_road.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, status, length, coordinates })
})
.then(response => response.json())
.then(data => {
if (data.success) {
displayRoad(data.id, data.name, data.status, parseFloat(data.length), JSON.parse(data.coordinates));
showNotification('Road saved successfully.');
updateCounts();
} else {
addLocalRoad(name, status, length, coordinates);
}
})
.catch(() => {
addLocalRoad(name, status, length, coordinates);
});
}
function addLocalRoad(name, status, length, coordinates) {
const id = generateLocalId('road');
displayRoad(id, name, status, length, coordinates, true);
persistLocalRoads();
showNotification('Road saved locally.');
updateCounts();
}
function displayRoad(id, name, status, length, coordinates, isLocal = false) {
if (roads[id]) {
map.removeLayer(roads[id]);
delete roads[id];
}
const latlngs = coordinates.map(coord => L.latLng(coord.lat, coord.lng));
const layer = L.polyline(latlngs, {
color: getRoadColor(status),
weight: 6
}).addTo(map);
layer._local = isLocal;
layer._localId = id;
layer._name = name;
layer._status = status;
layer._length = length;
layer._coordinates = coordinates;
layer._type = 'road';
layer.bindPopup(getRoadPopupContent(id, name, status, length));
layer.on('popupopen', function(e) {
bindPopupDeleteAction(e.popup.getElement(), 'road', id);
});
roads[id] = layer;
}
function getRoadPopupContent(id, name, status, length) {
return `
<div class="marker-popup" data-feature-id="${escapeHtml(String(id))}" data-feature-type="road">
<div class="marker-popup-name">${escapeHtml(name)}</div>
<div class="marker-popup-hours">${escapeHtml(status)}</div>
<div class="marker-popup-info">
<span>Length: ${length.toFixed(2)} m</span>
</div>
<button type="button" class="delete-feature-btn">Delete</button>
</div>
`;
}
function openParcelModal() {
const modal = document.getElementById('parcel-modal');
const parcelArea = document.getElementById('parcel-area');
const parcelFormInfo = document.getElementById('parcel-form-info');
const area = computeParcelArea(currentDrawCoords);
parcelArea.value = area.toFixed(2);
parcelFormInfo.textContent = `Calculated automatically from ${currentDrawCoords.length} vertices.`;
modal.classList.add('show');
}
function closeParcelModal() {
const modal = document.getElementById('parcel-modal');
modal.classList.remove('show');
}
function submitParcelForm() {
const ownerStatus = document.getElementById('parcel-owner').value;
const area = parseFloat(document.getElementById('parcel-area').value);
if (!ownerStatus) {
showNotification('Please choose an ownership status.', true);
return;
}
addParcel(ownerStatus, area, currentDrawCoords.slice());
closeParcelModal();
cancelDraw();
}
function addParcel(ownerStatus, area, coordinates) {
fetch('api/save_parcel.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ owner_status: ownerStatus, area, coordinates })
})
.then(response => response.json())
.then(data => {
if (data.success) {
displayParcel(data.id, data.owner_status, parseFloat(data.area), JSON.parse(data.coordinates));
showNotification('Parcel saved successfully.');
updateCounts();
} else {
addLocalParcel(ownerStatus, area, coordinates);
}
})
.catch(() => {
addLocalParcel(ownerStatus, area, coordinates);
});
}
function addLocalParcel(ownerStatus, area, coordinates) {
const id = generateLocalId('parcel');
displayParcel(id, ownerStatus, area, coordinates, true);
persistLocalParcels();
showNotification('Parcel saved locally.');
updateCounts();
}
function displayParcel(id, ownerStatus, area, coordinates, isLocal = false) {
if (parcels[id]) {
map.removeLayer(parcels[id]);
delete parcels[id];
}
const latlngs = coordinates.map(coord => L.latLng(coord.lat, coord.lng));
const layer = L.polygon(latlngs, {
color: getParcelColor(ownerStatus),
fillColor: getParcelColor(ownerStatus),
fillOpacity: 0.3,
weight: 4
}).addTo(map);
layer._local = isLocal;
layer._localId = id;
layer._ownerStatus = ownerStatus;
layer._area = area;
layer._coordinates = coordinates;
layer._type = 'parcel';
layer.bindPopup(getParcelPopupContent(id, ownerStatus, area));
layer.on('popupopen', function(e) {
bindPopupDeleteAction(e.popup.getElement(), 'parcel', id);
});
parcels[id] = layer;
}
function getParcelPopupContent(id, ownerStatus, area) {
return `
<div class="marker-popup" data-feature-id="${escapeHtml(String(id))}" data-feature-type="parcel">
<div class="marker-popup-name">${escapeHtml(ownerStatus)}</div>
<div class="marker-popup-hours">Parcel Status</div>
<div class="marker-popup-info">
<span>Area: ${area.toFixed(2)} m²</span>
</div>
<button type="button" class="delete-feature-btn">Delete</button>
</div>
`;
}
function bindPopupDeleteAction(popupElement, type, id) {
if (!popupElement) {
return;
}
const deleteBtn = popupElement.querySelector('.delete-feature-btn');
if (deleteBtn) {
deleteBtn.onclick = function() {
deleteFeature(type, id);
};
}
}
function deleteFeature(type, id) {
if (!confirm('Are you sure you want to delete this item?')) {
return;
}
if (type === 'point') {
deletePoint(id);
} else if (type === 'road') {
deleteRoad(id);
} else if (type === 'parcel') {
deleteParcel(id);
}
}
function deletePoint(id) {
const point = points[id];
if (!point) {
return;
}
if (point._local) {
map.removeLayer(point);
delete points[id];
persistLocalPoints();
updateCounts();
showNotification('Point deleted locally.');
return;
}
fetch('api/delete_point.php', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
})
.then(response => response.json())
.then(data => {
if (data.success) {
map.removeLayer(point);
delete points[id];
updateCounts();
showNotification('Point deleted successfully.');
} else {
showNotification('Unable to delete point on server.', true);
}
})
.catch(() => {
showNotification('Server unavailable, point deletion aborted.', true);
});
}
function deleteRoad(id) {
const road = roads[id];
if (!road) {
return;
}
if (road._local) {
map.removeLayer(road);
delete roads[id];
persistLocalRoads();
updateCounts();
showNotification('Road deleted locally.');
return;
}
fetch('api/delete_road.php', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
})
.then(response => response.json())
.then(data => {
if (data.success) {
map.removeLayer(road);
delete roads[id];
updateCounts();
showNotification('Road deleted successfully.');
} else {
showNotification('Unable to delete road on server.', true);
}
})
.catch(() => {
showNotification('Server unavailable, road deletion aborted.', true);
});
}
function deleteParcel(id) {
const parcel = parcels[id];
if (!parcel) {
return;
}
if (parcel._local) {
map.removeLayer(parcel);
delete parcels[id];
persistLocalParcels();
updateCounts();
showNotification('Parcel deleted locally.');
return;
}
fetch('api/delete_parcel.php', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id })
})
.then(response => response.json())
.then(data => {
if (data.success) {
map.removeLayer(parcel);
delete parcels[id];
updateCounts();
showNotification('Parcel deleted successfully.');
} else {
showNotification('Unable to delete parcel on server.', true);
}
})
.catch(() => {
showNotification('Server unavailable, parcel deletion aborted.', true);
});
}
function updateCounts() {
document.getElementById('marker-count').textContent = Object.keys(points).length;
document.getElementById('road-count').textContent = Object.keys(roads).length;
document.getElementById('parcel-count').textContent = Object.keys(parcels).length;
}
function getRoadColor(status) {
switch (status) {
case 'Jalan Nasional':
return '#E53935';
case 'Jalan Provinsi':
return '#FB8C00';
case 'Jalan Kabupaten':
return '#1E88E5';
default:
return '#9E9E9E';
}
}
function getParcelColor(ownerStatus) {
switch (ownerStatus) {
case 'SHM':
return '#388E3C';
case 'HGB':
return '#8E24AA';
case 'HGU':
return '#00897B';
case 'HP':
return '#0288D1';
default:
return '#616161';
}
}
function computeRoadLength(latlngs) {
if (!latlngs || latlngs.length < 2) {
return 0;
}
let total = 0;
for (let i = 1; i < latlngs.length; i++) {
total += map.distance(latlngs[i - 1], latlngs[i]);
}
return total;
}
function computeParcelArea(latlngs) {
if (!latlngs || latlngs.length < 3) {
return 0;
}
if (window.L && L.GeometryUtil && typeof L.GeometryUtil.geodesicArea === 'function') {
return L.GeometryUtil.geodesicArea(latlngs);
}
return approximatePolygonArea(latlngs);
}
function approximatePolygonArea(latlngs) {
const rad = degrees => degrees * Math.PI / 180;
let area = 0;
const radius = 6378137;
for (let i = 0, len = latlngs.length; i < len; i++) {
const p1 = latlngs[i];
const p2 = latlngs[(i + 1) % len];
area += rad(p2.lng - p1.lng) * (2 + Math.sin(rad(p1.lat)) + Math.sin(rad(p2.lat)));
}
return Math.abs(area * radius * radius / 2);
}
function showNotification(message, isError = false) {
const notification = document.createElement('div');
notification.className = 'notification' + (isError ? ' error' : '');
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
}
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return String(text).replace(/[&<>"']/g, m => map[m]);
}