Files
WebGIS-SIGSemua/WebGIS_SPBU_dan_LayerControl/index.html
T
2026-06-12 00:05:22 +07:00

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: '&copy; 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 = 'http://localhost:3000/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>