329 lines
10 KiB
JavaScript
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, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
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: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <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();
|
|
})();
|