MIF_E31231623/website/wisata_web/public/js/admin-dashboard-map.js

329 lines
10 KiB
JavaScript

(function () {
const lumajangCenter = [-8.1269, 113.2248];
const lumajangBounds = L.latLngBounds(
L.latLng(-8.55, 112.75),
L.latLng(-7.70, 113.55),
);
const defaultZoom = 10;
const singleMarkerZoom = 14;
const minZoom = 10;
const maxZoom = 18;
const markerColors = {
ar: '#dc2626',
video: '#2563eb',
};
let map;
let markerLayer;
let markers = [];
let activeMedia = 'all';
let searchQuery = '';
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function createMarkerIcon(mediaType) {
const color = markerColors[mediaType] || markerColors.ar;
return L.divIcon({
className: 'tourism-marker',
html: `<span class="tourism-marker-pin" style="--marker-color:${color}"></span>`,
iconSize: [30, 42],
iconAnchor: [15, 42],
popupAnchor: [0, -38],
});
}
function buildPopupContent(item) {
const isVideo = item.mediaType === 'video';
const badgeClass = isVideo ? 'text-bg-primary' : 'text-bg-danger';
return `
<div class="card map-popup-card border-0 shadow-sm">
<img src="${escapeHtml(item.imageUrl)}" class="card-img-top" alt="${escapeHtml(item.title)}">
<div class="card-body">
<div class="d-flex align-items-start justify-content-between gap-2 mb-2">
<h3 class="map-popup-title mb-0">${escapeHtml(item.title)}</h3>
<span class="badge ${badgeClass}">${escapeHtml(item.mediaLabel)}</span>
</div>
<div class="map-popup-row"><i class="bi bi-geo-alt"></i><span>${escapeHtml(item.location)}</span></div>
<div class="map-popup-row"><i class="bi bi-tag"></i><span>${escapeHtml(item.category)}</span></div>
<a class="btn btn-sm btn-ocean w-100 mt-3" href="${escapeHtml(item.editUrl)}">Lihat Detail</a>
</div>
</div>
`;
}
function markerMatches(marker) {
const item = marker.tourismData;
const matchesMedia = activeMedia === 'all' || item.mediaType === activeMedia;
const haystack = `${item.title} ${item.location} ${item.category}`.toLowerCase();
const matchesSearch = !searchQuery || haystack.includes(searchQuery);
return matchesMedia && matchesSearch;
}
function getVisibleMarkers() {
return markers.filter((marker) => markerLayer.hasLayer(marker));
}
function setMarkerVisibility() {
markers.forEach((marker) => {
const shouldShow = markerMatches(marker);
const isShown = markerLayer.hasLayer(marker);
if (shouldShow && !isShown) {
markerLayer.addLayer(marker);
}
if (!shouldShow && isShown) {
markerLayer.removeLayer(marker);
}
});
const visibleMarkers = getVisibleMarkers();
updateStats();
if (!visibleMarkers.length) {
map.closePopup();
return;
}
const exactSearchMatch = searchQuery
? visibleMarkers.find((marker) => marker.tourismData.title.toLowerCase().includes(searchQuery))
: null;
if (exactSearchMatch) {
focusMarker(exactSearchMatch);
return;
}
fitMarkers(visibleMarkers);
}
function updateStats() {
const totalElement = document.getElementById('mapTotalMarkers');
const arElement = document.getElementById('mapArMarkers');
const videoElement = document.getElementById('mapVideoMarkers');
const arCount = markers.filter((marker) => marker.tourismData.mediaType === 'ar').length;
const videoCount = markers.filter((marker) => marker.tourismData.mediaType === 'video').length;
if (totalElement) totalElement.textContent = markers.length;
if (arElement) arElement.textContent = arCount;
if (videoElement) videoElement.textContent = videoCount;
}
function fitMarkers(targetMarkers) {
if (!targetMarkers.length) {
map.setView(lumajangCenter, defaultZoom);
return;
}
if (targetMarkers.length === 1) {
map.setView(targetMarkers[0].getLatLng(), singleMarkerZoom);
return;
}
const group = L.featureGroup(targetMarkers);
map.fitBounds(group.getBounds().pad(0.2), {
maxZoom,
});
}
function refreshMapSize(callback, delay = 140) {
window.setTimeout(() => {
if (!map) {
return;
}
map.invalidateSize(true);
if (typeof callback === 'function') {
callback();
}
}, delay);
}
function scheduleMapInvalidation() {
setTimeout(() => {
map.invalidateSize(true);
}, 500);
setTimeout(() => {
map.invalidateSize(true);
}, 1000);
setTimeout(() => {
map.invalidateSize(true);
}, 2000);
}
function focusMarker(marker) {
map.setView(marker.getLatLng(), singleMarkerZoom);
marker.openPopup();
}
function resetView() {
searchQuery = '';
activeMedia = 'all';
const searchInput = document.getElementById('tourismMapSearch');
const mediaSelect = document.getElementById('tourismMapMedia');
if (searchInput) searchInput.value = '';
if (mediaSelect) mediaSelect.value = 'all';
markers.forEach((marker) => {
if (!markerLayer.hasLayer(marker)) {
markerLayer.addLayer(marker);
}
});
map.closePopup();
updateStats();
refreshMapSize(() => map.setView(lumajangCenter, defaultZoom), 120);
}
function addFullscreenControl() {
const FullscreenControl = L.Control.extend({
options: { position: 'topleft' },
onAdd() {
const button = L.DomUtil.create('button', 'leaflet-control fullscreen-control');
button.type = 'button';
button.title = 'Fullscreen';
button.setAttribute('aria-label', 'Fullscreen');
button.innerHTML = '<i class="bi bi-fullscreen"></i>';
L.DomEvent.disableClickPropagation(button);
L.DomEvent.on(button, 'click', () => {
const container = map.getContainer();
if (document.fullscreenElement) {
document.exitFullscreen();
} else if (container.requestFullscreen) {
container.requestFullscreen();
}
});
return button;
},
});
map.addControl(new FullscreenControl());
document.addEventListener('fullscreenchange', () => {
refreshMapSize(() => fitMarkers(getVisibleMarkers()), 180);
});
}
function bindControls() {
document.getElementById('tourismMapSearch')?.addEventListener('input', (event) => {
searchQuery = event.target.value.trim().toLowerCase();
setMarkerVisibility();
});
document.getElementById('tourismMapMedia')?.addEventListener('change', (event) => {
activeMedia = event.target.value;
setMarkerVisibility();
});
document.getElementById('tourismMapFit')?.addEventListener('click', () => {
fitMarkers(getVisibleMarkers());
});
document.getElementById('tourismMapReset')?.addEventListener('click', resetView);
window.addEventListener('resize', () => {
window.clearTimeout(window.tourismMapResizeTimer);
window.tourismMapResizeTimer = window.setTimeout(() => {
refreshMapSize(() => fitMarkers(getVisibleMarkers()), 40);
}, 180);
});
window.addEventListener('load', () => {
refreshMapSize(() => fitMarkers(getVisibleMarkers()), 220);
});
}
function initTourismMap() {
const mapElement = document.getElementById('tourismMap');
const data = Array.isArray(window.tourismMapData) ? window.tourismMapData : [];
if (!mapElement || typeof L === 'undefined') {
return;
}
if (mapElement._leaflet_id) return;
map = L.map(mapElement, {
center: lumajangCenter,
zoom: defaultZoom,
minZoom,
maxZoom,
maxBounds: lumajangBounds,
maxBoundsViscosity: 1,
zoomControl: true,
scrollWheelZoom: true,
});
map.setMaxBounds(lumajangBounds);
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
subdomains: 'abcd',
maxZoom: 20,
noWrap: true,
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <a href="https://carto.com/attributions">CARTO</a>',
}).addTo(map);
scheduleMapInvalidation();
markerLayer = L.layerGroup().addTo(map);
markers = data.map((item) => {
const marker = L.marker([Number(item.latitude), Number(item.longitude)], {
icon: createMarkerIcon(item.mediaType),
title: item.title,
});
marker.tourismData = item;
marker.bindPopup(buildPopupContent(item), {
className: 'tourism-popup',
maxWidth: 300,
minWidth: 260,
});
markerLayer.addLayer(marker);
return marker;
});
addFullscreenControl();
bindControls();
updateStats();
if (markers.length) {
refreshMapSize(() => map.setView(lumajangCenter, defaultZoom), 180);
} else {
mapElement.classList.add('is-empty');
refreshMapSize(() => map.setView(lumajangCenter, defaultZoom), 180);
}
}
function bootTourismMap() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initTourismMap, { once: true });
} else {
initTourismMap();
}
}
bootTourismMap();
})();