446 lines
17 KiB
HTML
446 lines
17 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>SPBU Web GIS</title>
|
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
|
|
<style>
|
|
body, html { margin: 0; padding: 0; height: 100%; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
|
|
#map { height: 100vh; width: 100%; }
|
|
|
|
/* ---- Popup form ---- */
|
|
.popup-form { display: flex; flex-direction: column; gap: 8px; width: 250px; }
|
|
.popup-form input, .popup-form select { padding: 5px; }
|
|
.popup-form button { padding: 8px; cursor: pointer; font-weight: bold; }
|
|
.popup-form .btn-save { background-color: #4CAF50; color: white; border: none; border-radius: 4px; }
|
|
.popup-form .btn-update { background-color: #2196F3; color: white; border: none; border-radius: 4px; }
|
|
.popup-form .btn-delete { background-color: #f44336; color: white; border: none; border-radius: 4px; }
|
|
.popup-form .btn-cancel { background-color: #777; color: white; border: none; border-radius: 4px; }
|
|
.popup-form .btn-group { display: flex; gap: 5px; }
|
|
.popup-form .btn-group button { flex: 1; }
|
|
|
|
/* ---- Custom Layer Toggle Control (bottom-right) ---- */
|
|
#layer-control {
|
|
position: absolute;
|
|
bottom: 30px;
|
|
right: 12px;
|
|
z-index: 1000;
|
|
background: rgba(255, 255, 255, 0.92);
|
|
backdrop-filter: blur(10px);
|
|
border-radius: 14px;
|
|
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.18);
|
|
border: 1px solid rgba(255, 255, 255, 0.35);
|
|
padding: 14px 18px;
|
|
min-width: 200px;
|
|
user-select: none;
|
|
}
|
|
|
|
#layer-control h4 {
|
|
margin: 0 0 12px 0;
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
color: #111;
|
|
border-bottom: 2px solid #f0f0f0;
|
|
padding-bottom: 8px;
|
|
letter-spacing: 0.3px;
|
|
}
|
|
|
|
.layer-toggle-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-bottom: 9px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.layer-toggle-row:last-child { margin-bottom: 0; }
|
|
|
|
/* Custom pill toggle switch */
|
|
.toggle-switch {
|
|
position: relative;
|
|
width: 36px;
|
|
height: 20px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.toggle-switch input { opacity: 0; width: 0; height: 0; }
|
|
|
|
.toggle-slider {
|
|
position: absolute;
|
|
inset: 0;
|
|
border-radius: 20px;
|
|
background: #ccc;
|
|
transition: background 0.25s;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.toggle-slider::before {
|
|
content: '';
|
|
position: absolute;
|
|
width: 14px;
|
|
height: 14px;
|
|
left: 3px;
|
|
top: 3px;
|
|
border-radius: 50%;
|
|
background: white;
|
|
transition: transform 0.25s;
|
|
box-shadow: 0 1px 3px rgba(0,0,0,0.25);
|
|
}
|
|
|
|
.toggle-switch input:checked + .toggle-slider { background: #4CAF50; }
|
|
.toggle-switch input:checked + .toggle-slider::before { transform: translateX(16px); }
|
|
|
|
/* Override green for the "not 24h" toggle */
|
|
.toggle-switch.red input:checked + .toggle-slider { background: #f44336; }
|
|
|
|
.layer-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
font-size: 12.5px;
|
|
font-weight: 500;
|
|
color: #333;
|
|
flex: 1;
|
|
}
|
|
|
|
.layer-dot {
|
|
width: 11px;
|
|
height: 11px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
border: 1.5px solid rgba(0,0,0,0.12);
|
|
}
|
|
|
|
.layer-count {
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
background: #555;
|
|
border-radius: 10px;
|
|
padding: 1px 7px;
|
|
min-width: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.layer-count.green { background: #4CAF50; }
|
|
.layer-count.red { background: #f44336; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="map"></div>
|
|
|
|
<!-- Custom Layer Control Panel -->
|
|
<div id="layer-control">
|
|
<h4>🗺️ Layer SPBU</h4>
|
|
|
|
<!-- 24-hour toggle -->
|
|
<div class="layer-toggle-row" onclick="toggleLayer('h24')">
|
|
<label class="toggle-switch" onclick="event.stopPropagation()">
|
|
<input type="checkbox" id="toggle-h24" checked onchange="toggleLayer('h24')">
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
<span class="layer-label">
|
|
<span class="layer-dot" style="background:#4CAF50;"></span>
|
|
24 Jam
|
|
</span>
|
|
<span class="layer-count green" id="count-h24">0</span>
|
|
</div>
|
|
|
|
<!-- Not-24-hour toggle -->
|
|
<div class="layer-toggle-row" onclick="toggleLayer('not')">
|
|
<label class="toggle-switch red" onclick="event.stopPropagation()">
|
|
<input type="checkbox" id="toggle-not" checked onchange="toggleLayer('not')">
|
|
<span class="toggle-slider"></span>
|
|
</label>
|
|
<span class="layer-label">
|
|
<span class="layer-dot" style="background:#f44336;"></span>
|
|
Tidak 24 Jam
|
|
</span>
|
|
<span class="layer-count red" id="count-not">0</span>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
|
<script>
|
|
// ----------------------------------------------------------------
|
|
// MAP INITIALISATION
|
|
// ----------------------------------------------------------------
|
|
const map = L.map('map').setView([-0.0263, 109.3425], 13);
|
|
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap contributors'
|
|
}).addTo(map);
|
|
|
|
// ----------------------------------------------------------------
|
|
// LAYER GROUPS — one per status category
|
|
// ----------------------------------------------------------------
|
|
const layer24h = L.layerGroup().addTo(map); // always-open SPBU
|
|
const layerNot = L.layerGroup().addTo(map); // non-24h SPBU
|
|
|
|
// ----------------------------------------------------------------
|
|
// CUSTOM ICONS
|
|
// ----------------------------------------------------------------
|
|
const greenIcon = new L.Icon({
|
|
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-green.png',
|
|
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
|
|
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
|
|
});
|
|
|
|
const redIcon = new L.Icon({
|
|
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-red.png',
|
|
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
|
|
iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41]
|
|
});
|
|
|
|
// ----------------------------------------------------------------
|
|
// LAYER TOGGLE — driven by the custom control panel
|
|
// ----------------------------------------------------------------
|
|
function toggleLayer(type) {
|
|
if (type === 'h24') {
|
|
const cb = document.getElementById('toggle-h24');
|
|
// If row was clicked (not the checkbox itself), sync the checkbox state
|
|
if (event.target !== cb) cb.checked = !cb.checked;
|
|
cb.checked ? layer24h.addTo(map) : map.removeLayer(layer24h);
|
|
} else {
|
|
const cb = document.getElementById('toggle-not');
|
|
if (event.target !== cb) cb.checked = !cb.checked;
|
|
cb.checked ? layerNot.addTo(map) : map.removeLayer(layerNot);
|
|
}
|
|
}
|
|
|
|
// Update the badge counters in the control panel
|
|
function updateCounts() {
|
|
document.getElementById('count-h24').textContent = layer24h.getLayers().length;
|
|
document.getElementById('count-not').textContent = layerNot.getLayers().length;
|
|
}
|
|
|
|
// Helper: resolve which LayerGroup a marker belongs to
|
|
function resolveGroup(open_window) {
|
|
return open_window === '24 hours' ? layer24h : layerNot;
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// MARKERS STORE { id: { marker, spbu, group } }
|
|
// ----------------------------------------------------------------
|
|
const API_URL = '/api/spbus';
|
|
let tempPopup = null;
|
|
let markers = {};
|
|
|
|
// ----------------------------------------------------------------
|
|
// LOAD MARKERS FROM BACKEND
|
|
// ----------------------------------------------------------------
|
|
async function loadMarkers() {
|
|
try {
|
|
const response = await fetch(API_URL);
|
|
const data = await response.json();
|
|
|
|
data.forEach(spbu => addMarkerToMap(spbu));
|
|
updateCounts();
|
|
} catch (error) {
|
|
console.error('Error loading markers:', error);
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// ADD A SINGLE MARKER TO THE CORRECT LAYER GROUP
|
|
// ----------------------------------------------------------------
|
|
function addMarkerToMap(spbu) {
|
|
const icon = spbu.open_window === '24 hours' ? greenIcon : redIcon;
|
|
const group = resolveGroup(spbu.open_window);
|
|
const marker = L.marker([spbu.lat, spbu.lng], { icon }).addTo(group);
|
|
|
|
markers[spbu.id] = { marker, spbu, group };
|
|
|
|
marker.on('click', function () {
|
|
showMarkerDetails(spbu.id);
|
|
});
|
|
|
|
return marker;
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// SHOW DETAIL / EDIT POPUP FOR AN EXISTING MARKER
|
|
// ----------------------------------------------------------------
|
|
function showMarkerDetails(spbuId) {
|
|
const { spbu } = markers[spbuId];
|
|
|
|
const formContent = `
|
|
<div class="popup-form">
|
|
<h4>${spbu.name}</h4>
|
|
<label><strong>Name:</strong></label>
|
|
<input type="text" id="updateName" value="${spbu.name}" required>
|
|
<label><strong>Status:</strong></label>
|
|
<select id="updateWindow">
|
|
<option value="24 hours" ${spbu.open_window === '24 hours' ? 'selected' : ''}>24 hours</option>
|
|
<option value="not" ${spbu.open_window === 'not' ? 'selected' : ''}>Not 24 hours</option>
|
|
</select>
|
|
<label><strong>SPBU Number:</strong></label>
|
|
<input type="number" id="updateNumber" value="${spbu.spbu_number}" step="0.1" required>
|
|
<div class="btn-group">
|
|
<button class="btn-update" onclick="updateSpbu(${spbuId})">Update</button>
|
|
<button class="btn-delete" onclick="deleteSpbu(${spbuId})">Delete</button>
|
|
</div>
|
|
<button class="btn-cancel" onclick="map.closePopup()">Close</button>
|
|
</div>
|
|
`;
|
|
|
|
tempPopup = L.popup()
|
|
.setLatLng([spbu.lat, spbu.lng])
|
|
.setContent(formContent)
|
|
.openOn(map);
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// UPDATE SPBU — also moves marker between layer groups if status changed
|
|
// ----------------------------------------------------------------
|
|
async function updateSpbu(spbuId) {
|
|
const name = document.getElementById('updateName').value;
|
|
const open_window = document.getElementById('updateWindow').value;
|
|
const spbu_number = parseFloat(document.getElementById('updateNumber').value);
|
|
|
|
if (!name || !spbu_number) { alert('Please fill all fields!'); return; }
|
|
|
|
const { spbu } = markers[spbuId];
|
|
const updateData = { name, open_window, spbu_number, lat: spbu.lat, lng: spbu.lng };
|
|
|
|
try {
|
|
const response = await fetch(`${API_URL}/${spbuId}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(updateData)
|
|
});
|
|
|
|
if (response.ok) {
|
|
const oldGroup = markers[spbuId].group;
|
|
const newGroup = resolveGroup(open_window);
|
|
|
|
// Move marker between groups when status changes
|
|
if (oldGroup !== newGroup) {
|
|
oldGroup.removeLayer(markers[spbuId].marker);
|
|
markers[spbuId].marker.addTo(newGroup);
|
|
markers[spbuId].group = newGroup;
|
|
}
|
|
|
|
// Update icon and stored data
|
|
const icon = open_window === '24 hours' ? greenIcon : redIcon;
|
|
markers[spbuId].marker.setIcon(icon);
|
|
markers[spbuId].spbu = { ...spbu, ...updateData };
|
|
|
|
map.closePopup();
|
|
updateCounts();
|
|
alert('SPBU updated successfully!');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating data:', error);
|
|
alert('Failed to update data.');
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// DELETE SPBU
|
|
// ----------------------------------------------------------------
|
|
async function deleteSpbu(spbuId) {
|
|
if (!confirm('Are you sure you want to delete this SPBU?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`${API_URL}/${spbuId}`, {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Remove from its specific layer group
|
|
markers[spbuId].group.removeLayer(markers[spbuId].marker);
|
|
delete markers[spbuId];
|
|
|
|
map.closePopup();
|
|
updateCounts();
|
|
alert('SPBU deleted successfully!');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting data:', error);
|
|
alert('Failed to delete data.');
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// MAP CLICK — open "Add new SPBU" form popup
|
|
// ----------------------------------------------------------------
|
|
map.on('click', function (e) {
|
|
const lat = e.latlng.lat;
|
|
const lng = e.latlng.lng;
|
|
|
|
const formContent = `
|
|
<div class="popup-form">
|
|
<h4>Add New SPBU</h4>
|
|
<input type="text" id="spbuName" placeholder="SPBU Name" required>
|
|
<select id="spbuWindow">
|
|
<option value="24 hours">24 hours</option>
|
|
<option value="not">Not 24 hours</option>
|
|
</select>
|
|
<input type="number" id="spbuNumber" placeholder="SPBU Number" step="0.1" required>
|
|
<button class="btn-save" onclick="saveSpbu(${lat}, ${lng})">Save</button>
|
|
<button class="btn-cancel" onclick="map.closePopup()">Cancel</button>
|
|
</div>
|
|
`;
|
|
|
|
tempPopup = L.popup()
|
|
.setLatLng(e.latlng)
|
|
.setContent(formContent)
|
|
.openOn(map);
|
|
});
|
|
|
|
// ----------------------------------------------------------------
|
|
// SAVE NEW SPBU
|
|
// ----------------------------------------------------------------
|
|
async function saveSpbu(lat, lng) {
|
|
const name = document.getElementById('spbuName').value;
|
|
const open_window = document.getElementById('spbuWindow').value;
|
|
const spbu_number = document.getElementById('spbuNumber').value;
|
|
|
|
if (!name || !spbu_number) { alert('Please fill all fields!'); return; }
|
|
|
|
const spbuData = { name, open_window, spbu_number: parseFloat(spbu_number), lat, lng };
|
|
|
|
try {
|
|
const response = await fetch(API_URL, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(spbuData)
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
const newId = result.id;
|
|
|
|
map.closePopup();
|
|
|
|
// Add to the correct layer group and markers store
|
|
const icon = open_window === '24 hours' ? greenIcon : redIcon;
|
|
const group = resolveGroup(open_window);
|
|
const marker = L.marker([lat, lng], { icon }).addTo(group);
|
|
|
|
markers[newId] = {
|
|
marker,
|
|
spbu: { id: newId, ...spbuData },
|
|
group
|
|
};
|
|
|
|
marker.on('click', function () { showMarkerDetails(newId); });
|
|
updateCounts();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error saving data:', error);
|
|
alert('Failed to save data. Make sure backend is running.');
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------
|
|
// BOOT
|
|
// ----------------------------------------------------------------
|
|
loadMarkers();
|
|
</script>
|
|
</body>
|
|
</html> |