Upload seluruh tugas SIG Point Line dan Polygon
This commit is contained in:
@@ -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');
|
||||
?>
|
||||
@@ -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']);
|
||||
}
|
||||
?>
|
||||
@@ -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']);
|
||||
}
|
||||
?>
|
||||
@@ -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']);
|
||||
}
|
||||
?>
|
||||
@@ -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']);
|
||||
}
|
||||
?>
|
||||
@@ -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']);
|
||||
}
|
||||
?>
|
||||
@@ -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']);
|
||||
}
|
||||
?>
|
||||
@@ -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']);
|
||||
}
|
||||
?>
|
||||
@@ -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']);
|
||||
}
|
||||
?>
|
||||
@@ -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']);
|
||||
}
|
||||
?>
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>"']/g, m => map[m]);
|
||||
}
|
||||
@@ -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.
@@ -0,0 +1,3 @@
|
||||
db/*.db
|
||||
.DS_Store
|
||||
*.log
|
||||
@@ -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">×</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">×</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">×</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>
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return String(text).replace(/[&<>"']/g, m => map[m]);
|
||||
}
|
||||
@@ -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');
|
||||
?>
|
||||
@@ -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']);
|
||||
}
|
||||
?>
|
||||
@@ -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']);
|
||||
}
|
||||
?>
|
||||
@@ -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']);
|
||||
}
|
||||
?>
|
||||
@@ -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']);
|
||||
}
|
||||
?>
|
||||
@@ -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']);
|
||||
}
|
||||
?>
|
||||
@@ -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']);
|
||||
}
|
||||
?>
|
||||
@@ -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']);
|
||||
}
|
||||
?>
|
||||
@@ -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']);
|
||||
}
|
||||
?>
|
||||
@@ -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']);
|
||||
}
|
||||
?>
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>"']/g, m => map[m]);
|
||||
}
|
||||
@@ -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.
@@ -0,0 +1,3 @@
|
||||
db/*.db
|
||||
.DS_Store
|
||||
*.log
|
||||
@@ -0,0 +1,436 @@
|
||||
<?php
|
||||
/**
|
||||
* index.php
|
||||
* WebGIS Manajemen Data Jalan (Polyline) - PHP Native + MySQL + LeafletJS
|
||||
*
|
||||
* Contoh SQL untuk membuat database / tabel dan data sampel:
|
||||
*
|
||||
* CREATE DATABASE IF NOT EXISTS db_webgis CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
* USE db_webgis;
|
||||
*
|
||||
* CREATE TABLE IF NOT EXISTS t_jalan (
|
||||
* id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
* nama_jalan VARCHAR(255) NOT NULL,
|
||||
* status_jalan ENUM('nasional','provinsi','kabupaten') NOT NULL,
|
||||
* panjang_meter DECIMAL(10,2) NOT NULL,
|
||||
* koordinat_json LONGTEXT NOT NULL,
|
||||
* created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
* );
|
||||
*
|
||||
* INSERT INTO t_jalan (nama_jalan, status_jalan, panjang_meter, koordinat_json) VALUES
|
||||
* ('Jalan Ahmad Yani', 'nasional', 3200, '[[-0.0251,109.3292],[-0.0266,109.3338],[-0.0263,109.3425]]'),
|
||||
* ('Jalan Supadio', 'provinsi', 2400, '[[-0.0300,109.3270],[-0.0285,109.3335],[-0.0262,109.3420]]'),
|
||||
* ('Jalan Gajah Mada', 'kabupaten', 1800, '[[-0.0288,109.3389],[-0.0274,109.3452],[-0.0252,109.3501]]');
|
||||
*/
|
||||
|
||||
// Konfigurasi koneksi MySQL: sesuaikan dengan kredensial lokal Anda.
|
||||
$dbHost = 'localhost';
|
||||
$dbName = 'webgis_spbu';
|
||||
$dbUser = 'root';
|
||||
$dbPass = '';
|
||||
|
||||
$dsn = "mysql:host=$dbHost;dbname=$dbName;charset=utf8mb4";
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
|
||||
$errors = [];
|
||||
$success = '';
|
||||
$input = [
|
||||
'nama_jalan' => '',
|
||||
'status_jalan' => '',
|
||||
'panjang_meter' => '',
|
||||
'koordinat_json' => '',
|
||||
];
|
||||
|
||||
try {
|
||||
$pdo = new PDO($dsn, $dbUser, $dbPass, $options);
|
||||
|
||||
// Pastikan tabel t_jalan ada.
|
||||
$pdo->exec("CREATE TABLE IF NOT EXISTS t_jalan (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
nama_jalan VARCHAR(255) NOT NULL,
|
||||
status_jalan ENUM('nasional','provinsi','kabupaten') NOT NULL,
|
||||
panjang_meter DECIMAL(10,2) NOT NULL,
|
||||
koordinat_json LONGTEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$input['nama_jalan'] = trim($_POST['nama_jalan'] ?? '');
|
||||
$input['status_jalan'] = $_POST['status_jalan'] ?? '';
|
||||
$input['panjang_meter'] = $_POST['panjang_meter'] ?? '';
|
||||
$input['koordinat_json'] = $_POST['koordinat_json'] ?? '';
|
||||
|
||||
if ($input['nama_jalan'] === '') {
|
||||
$errors[] = 'Nama Jalan harus diisi.';
|
||||
}
|
||||
if (!in_array($input['status_jalan'], ['nasional', 'provinsi', 'kabupaten'], true)) {
|
||||
$errors[] = 'Status Jalan tidak valid.';
|
||||
}
|
||||
if ($input['panjang_meter'] === '' || !is_numeric($input['panjang_meter']) || (float)$input['panjang_meter'] <= 0) {
|
||||
$errors[] = 'Panjang Jalan harus dihitung otomatis dari peta dan tidak boleh kosong.';
|
||||
}
|
||||
$coords = json_decode($input['koordinat_json'], true);
|
||||
if (!is_array($coords) || count($coords) < 2) {
|
||||
$errors[] = 'Koordinat JSON tidak valid: minimal 2 titik diperlukan.';
|
||||
}
|
||||
|
||||
if (empty($errors)) {
|
||||
$stmt = $pdo->prepare('INSERT INTO t_jalan (nama_jalan, status_jalan, panjang_meter, koordinat_json) VALUES (:nama, :status, :panjang, :koordinat)');
|
||||
$stmt->execute([
|
||||
':nama' => $input['nama_jalan'],
|
||||
':status' => $input['status_jalan'],
|
||||
':panjang' => (float)$input['panjang_meter'],
|
||||
':koordinat' => json_encode($coords, JSON_UNESCAPED_UNICODE),
|
||||
]);
|
||||
$success = 'Data jalan berhasil disimpan ke database.';
|
||||
$input = ['nama_jalan' => '', 'status_jalan' => '', 'panjang_meter' => '', 'koordinat_json' => ''];
|
||||
}
|
||||
}
|
||||
|
||||
$stmt = $pdo->query('SELECT id, nama_jalan, status_jalan, panjang_meter, koordinat_json FROM t_jalan ORDER BY id DESC');
|
||||
$jalanList = $stmt->fetchAll();
|
||||
} catch (PDOException $e) {
|
||||
$errors[] = 'Koneksi database gagal: ' . htmlspecialchars($e->getMessage());
|
||||
$jalanList = [];
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="id">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebGIS Manajemen Data Jalan</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.css" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
background: #f4f7fb;
|
||||
color: #333;
|
||||
}
|
||||
header {
|
||||
background: #1f2937;
|
||||
color: #fff;
|
||||
padding: 18px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
main {
|
||||
max-width: 1160px;
|
||||
margin: 24px auto;
|
||||
padding: 0 16px 32px;
|
||||
}
|
||||
#map {
|
||||
width: 100% !important;
|
||||
height: 520px !important;
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
visibility: visible !important;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 24px rgba(15,23,42,.08);
|
||||
}
|
||||
.panel {
|
||||
margin-top: 24px;
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 8px 20px rgba(15,23,42,.06);
|
||||
}
|
||||
.card h2 {
|
||||
margin: 0 0 14px;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 10px;
|
||||
font-size: 0.98rem;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.form-group input[readonly] {
|
||||
background: #e2e8f0;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.button-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 12px 18px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
.btn-primary {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: #1d4ed8;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #64748b;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: #475569;
|
||||
}
|
||||
.alert {
|
||||
padding: 14px 16px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.alert-error {
|
||||
background: #fee2e2;
|
||||
border-color: #fca5a5;
|
||||
color: #b91c1c;
|
||||
}
|
||||
.alert-success {
|
||||
background: #dcfce7;
|
||||
border-color: #a7f3d0;
|
||||
color: #166534;
|
||||
}
|
||||
.note {
|
||||
font-size: 0.95rem;
|
||||
color: #475569;
|
||||
}
|
||||
.road-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
.road-item {
|
||||
border: 1px solid #e2e8f0;
|
||||
padding: 14px 16px;
|
||||
border-radius: 12px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.road-item strong {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
.meta {
|
||||
color: #475569;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>WebGIS Manajemen Data Jalan</h1>
|
||||
<p>Tambah, simpan, dan tampilkan polyline jalan dengan LeafletJS dan MySQL.</p>
|
||||
</header>
|
||||
<main>
|
||||
<section class="card">
|
||||
<h2>Petunjuk</h2>
|
||||
<p class="note">Klik peta berurutan untuk membuat garis jalan sementara. Sistem akan menghitung panjang otomatis dan menyimpan koordinat JSON ke database.</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div id="map"></div>
|
||||
</section>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
<section class="alert alert-error card">
|
||||
<strong>Terjadi kesalahan:</strong>
|
||||
<ul>
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<li><?= htmlspecialchars($error) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<section class="alert alert-success card">
|
||||
<?= htmlspecialchars($success) ?>
|
||||
</section>
|
||||
<?php endif; ?>
|
||||
|
||||
<section class="card">
|
||||
<h2>Form Tambah Jalan</h2>
|
||||
<form method="post" action="<?= htmlspecialchars($_SERVER['PHP_SELF']) ?>">
|
||||
<div class="form-group">
|
||||
<label for="nama_jalan">Nama Jalan</label>
|
||||
<input type="text" id="nama_jalan" name="nama_jalan" required value="<?= htmlspecialchars($input['nama_jalan']) ?>" placeholder="e.g. Jalan Ahmad Yani">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="status_jalan">Status Jalan</label>
|
||||
<select id="status_jalan" name="status_jalan" required>
|
||||
<option value="">Pilih status jalan</option>
|
||||
<option value="nasional" <?= $input['status_jalan'] === 'nasional' ? 'selected' : '' ?>>nasional</option>
|
||||
<option value="provinsi" <?= $input['status_jalan'] === 'provinsi' ? 'selected' : '' ?>>provinsi</option>
|
||||
<option value="kabupaten" <?= $input['status_jalan'] === 'kabupaten' ? 'selected' : '' ?>>kabupaten</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Panjang Jalan (otomatis)</label>
|
||||
<input type="text" id="panjang_tampil" readonly value="<?= $input['panjang_meter'] !== '' ? htmlspecialchars($input['panjang_meter']) . ' meter' : '0 meter' ?>">
|
||||
<input type="hidden" name="panjang_meter" id="panjang_meter" value="<?= htmlspecialchars($input['panjang_meter']) ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Koordinat JSON (otomatis)</label>
|
||||
<textarea readonly rows="3" id="koordinat_tampil" style="resize: vertical; background: #e2e8f0;"><?= htmlspecialchars($input['koordinat_json']) ?></textarea>
|
||||
<input type="hidden" name="koordinat_json" id="koordinat_json" value="<?= htmlspecialchars($input['koordinat_json']) ?>">
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button type="button" class="btn btn-secondary" id="reset-button">Reset Garis</button>
|
||||
<button type="submit" class="btn btn-primary">Simpan Data Jalan</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Daftar Jalan Tersimpan</h2>
|
||||
<div class="road-list">
|
||||
<?php if (count($jalanList) === 0): ?>
|
||||
<div class="road-item">Belum ada data jalan yang tersimpan.</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($jalanList as $jalan): ?>
|
||||
<div class="road-item">
|
||||
<strong><?= htmlspecialchars($jalan['nama_jalan']) ?></strong>
|
||||
<div class="meta">Status: <?= htmlspecialchars($jalan['status_jalan']) ?> | Panjang: <?= htmlspecialchars($jalan['panjang_meter']) ?> meter</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/leaflet.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script>
|
||||
const map = L.map('map').setView([-0.0263, 109.3425], 13);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
|
||||
const statusColors = {
|
||||
nasional: '#DC3545',
|
||||
provinsi: '#007BFF',
|
||||
kabupaten: '#28A745'
|
||||
};
|
||||
|
||||
const jalanData = <?= json_encode(array_map(function ($row) {
|
||||
return [
|
||||
'id' => $row['id'],
|
||||
'nama_jalan' => $row['nama_jalan'],
|
||||
'status_jalan' => $row['status_jalan'],
|
||||
'panjang_meter' => $row['panjang_meter'],
|
||||
'koordinat' => json_decode($row['koordinat_json'], true)
|
||||
];
|
||||
}, $jalanList), JSON_UNESCAPED_UNICODE) ?>;
|
||||
|
||||
jalanData.forEach(jalan => {
|
||||
if (!Array.isArray(jalan.koordinat) || jalan.koordinat.length < 2) {
|
||||
return;
|
||||
}
|
||||
const line = L.polyline(jalan.koordinat.map(([lat, lng]) => [lat, lng]), {
|
||||
color: statusColors[jalan.status_jalan] || '#444',
|
||||
weight: 6,
|
||||
opacity: 0.8,
|
||||
smoothFactor: 1
|
||||
}).addTo(map);
|
||||
line.bindPopup(`
|
||||
<strong>${jalan.nama_jalan}</strong><br>
|
||||
Status: ${jalan.status_jalan}<br>
|
||||
Panjang: ${jalan.panjang_meter} meter
|
||||
`);
|
||||
});
|
||||
|
||||
let currentPoints = [];
|
||||
let currentLine = null;
|
||||
|
||||
const panjangMeterInput = document.getElementById('panjang_meter');
|
||||
const panjangTampil = document.getElementById('panjang_tampil');
|
||||
const koordinatInput = document.getElementById('koordinat_json');
|
||||
const koordinatTampil = document.getElementById('koordinat_tampil');
|
||||
const resetButton = document.getElementById('reset-button');
|
||||
|
||||
map.on('click', function (event) {
|
||||
currentPoints.push(event.latlng);
|
||||
updateTemporaryPolyline();
|
||||
updateFormFields();
|
||||
});
|
||||
|
||||
resetButton.addEventListener('click', function () {
|
||||
currentPoints = [];
|
||||
if (currentLine) {
|
||||
map.removeLayer(currentLine);
|
||||
currentLine = null;
|
||||
}
|
||||
updateFormFields();
|
||||
});
|
||||
|
||||
function updateTemporaryPolyline() {
|
||||
if (currentLine) {
|
||||
map.removeLayer(currentLine);
|
||||
}
|
||||
if (currentPoints.length >= 2) {
|
||||
currentLine = L.polyline(currentPoints, {
|
||||
color: '#0f172a',
|
||||
weight: 5,
|
||||
dashArray: '8, 8',
|
||||
opacity: 0.85
|
||||
}).addTo(map);
|
||||
} else if (currentPoints.length === 1) {
|
||||
currentLine = L.circleMarker(currentPoints[0], {
|
||||
radius: 6,
|
||||
fillColor: '#0f172a',
|
||||
color: '#ffffff',
|
||||
weight: 2,
|
||||
fillOpacity: 1
|
||||
}).addTo(map);
|
||||
}
|
||||
}
|
||||
|
||||
function updateFormFields() {
|
||||
let totalDistance = 0;
|
||||
for (let i = 1; i < currentPoints.length; i++) {
|
||||
totalDistance += currentPoints[i - 1].distanceTo(currentPoints[i]);
|
||||
}
|
||||
const roundedDistance = Math.round(totalDistance);
|
||||
panjangMeterInput.value = roundedDistance;
|
||||
panjangTampil.value = `${roundedDistance} meter`;
|
||||
|
||||
const coordsArray = currentPoints.map(point => [point.lat, point.lng]);
|
||||
koordinatInput.value = JSON.stringify(coordsArray);
|
||||
koordinatTampil.value = koordinatInput.value;
|
||||
}
|
||||
|
||||
updateFormFields();
|
||||
|
||||
window.addEventListener('load', function () {
|
||||
map.invalidateSize();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return String(text).replace(/[&<>"']/g, m => map[m]);
|
||||
}
|
||||
Reference in New Issue
Block a user