// Initialize the map const map = new maplibregl.Map({ container: 'map', style: 'https://tiles.openfreemap.org/styles/liberty', center: [2.3522, 48.8566], // Default center (Paris) zoom: 4 }); // Add navigation controls map.addControl(new maplibregl.NavigationControl()); // Add attribution control with OpenStreetMap attribution map.addControl(new maplibregl.AttributionControl({ customAttribution: '© OpenStreetMap contributors' })); // Store all events and their types let allEvents = null; let eventTypes = new Set(); let eventsByType = {}; let markersByType = {}; let colorsByType = {}; // Generate a color for each event type function getColorForType(type, index) { // Predefined colors for better visual distinction const colors = [ '#FF5722', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', '#8BC34A', '#CDDC39', '#FFEB3B', '#FFC107', '#FF9800' ]; return colors[index % colors.length]; } // Fetch events when the map is loaded map.on('load', function() { fetchEvents(); }); // Function to fetch events from the API function fetchEvents() { // Update event info document.getElementById('event-info').innerHTML = '

Loading events...

'; // Fetch events from the public API - using limit=1000 to get more events fetch('https://api.openeventdatabase.org/event?limit=1000') .then(response => response.json()) .then(data => { if (data.features && data.features.length > 0) { // Store all events allEvents = data; // Process events by type processEventsByType(data); // Create filter UI createFilterUI(); // Add all events to the map initially addAllEventsToMap(); // Fit map to events bounds fitMapToBounds(data); // Update event info document.getElementById('event-info').innerHTML = `

Found ${data.features.length} events across ${eventTypes.size} different types.

`; } else { document.getElementById('event-info').innerHTML = '

No events found.

'; document.getElementById('filter-list').innerHTML = '
  • No event types available.
  • '; } }) .catch(error => { console.error('Error fetching events:', error); document.getElementById('event-info').innerHTML = `

    Error loading events: ${error.message}

    `; }); } // Process events by their "what" type function processEventsByType(data) { eventTypes = new Set(); eventsByType = {}; // Group events by their "what" type data.features.forEach(feature => { const properties = feature.properties; const what = properties.what || 'Unknown'; // Add to set of event types eventTypes.add(what); // Add to events by type if (!eventsByType[what]) { eventsByType[what] = []; } eventsByType[what].push(feature); }); // Assign colors to each type let index = 0; eventTypes.forEach(type => { colorsByType[type] = getColorForType(type, index); index++; }); } // Create the filter UI function createFilterUI() { const filterList = document.getElementById('filter-list'); filterList.innerHTML = ''; // Sort event types alphabetically const sortedTypes = Array.from(eventTypes).sort(); // Create a checkbox for each event type sortedTypes.forEach(type => { const count = eventsByType[type].length; const color = colorsByType[type]; const li = document.createElement('li'); li.className = 'filter-item'; const checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.id = `filter-${type}`; checkbox.checked = true; checkbox.addEventListener('change', () => { toggleEventType(type, checkbox.checked); }); const colorDot = document.createElement('span'); colorDot.className = 'color-dot'; colorDot.style.backgroundColor = color; const label = document.createElement('label'); label.htmlFor = `filter-${type}`; label.appendChild(colorDot); label.appendChild(document.createTextNode(type)); const countSpan = document.createElement('span'); countSpan.className = 'filter-count'; countSpan.textContent = `(${count})`; label.appendChild(countSpan); li.appendChild(checkbox); li.appendChild(label); filterList.appendChild(li); }); // Add event listeners for select/deselect all buttons document.getElementById('select-all').addEventListener('click', selectAllEventTypes); document.getElementById('deselect-all').addEventListener('click', deselectAllEventTypes); } // Add all events to the map function addAllEventsToMap() { // Clear existing markers clearAllMarkers(); // Add markers for each event type Object.keys(eventsByType).forEach(type => { addEventsOfTypeToMap(type); }); } // Add events of a specific type to the map function addEventsOfTypeToMap(type) { if (!markersByType[type]) { markersByType[type] = []; } const events = eventsByType[type]; const color = colorsByType[type]; events.forEach(feature => { const coordinates = feature.geometry.coordinates.slice(); const properties = feature.properties; // Create popup content let popupContent = '
    '; popupContent += `

    ${properties.label || 'Event'}

    `; popupContent += `

    Type: ${type}

    `; // Display all properties popupContent += '
    '; popupContent += ''; // Sort properties alphabetically for better organization const sortedKeys = Object.keys(properties).sort(); for (const key of sortedKeys) { // Skip the label as it's already displayed as the title if (key === 'label') continue; const value = properties[key]; let displayValue; // Format the value based on its type if (value === null || value === undefined) { displayValue = 'null'; } else if (typeof value === 'object') { displayValue = `
    ${JSON.stringify(value, null, 2)}
    `; } else if (typeof value === 'string' && value.startsWith('http')) { displayValue = `${value}`; } else { displayValue = String(value); } popupContent += ` `; } popupContent += '
    ${key}: ${displayValue}
    '; popupContent += '
    '; popupContent += '
    '; // Create popup const popup = new maplibregl.Popup({ closeButton: true, closeOnClick: true }).setHTML(popupContent); // Add marker with popup const marker = new maplibregl.Marker({ color: color }) .setLngLat(coordinates) .setPopup(popup) .addTo(map); // Store marker reference markersByType[type].push(marker); }); } // Toggle visibility of events by type function toggleEventType(type, visible) { if (!markersByType[type]) return; markersByType[type].forEach(marker => { if (visible) { marker.addTo(map); } else { marker.remove(); } }); } // Select all event types function selectAllEventTypes() { const checkboxes = document.querySelectorAll('#filter-list input[type="checkbox"]'); checkboxes.forEach(checkbox => { checkbox.checked = true; const type = checkbox.id.replace('filter-', ''); toggleEventType(type, true); }); } // Deselect all event types function deselectAllEventTypes() { const checkboxes = document.querySelectorAll('#filter-list input[type="checkbox"]'); checkboxes.forEach(checkbox => { checkbox.checked = false; const type = checkbox.id.replace('filter-', ''); toggleEventType(type, false); }); } // Clear all markers from the map function clearAllMarkers() { Object.keys(markersByType).forEach(type => { if (markersByType[type]) { markersByType[type].forEach(marker => marker.remove()); } }); markersByType = {}; } // Function to fit map to events bounds function fitMapToBounds(geojson) { if (geojson.features.length === 0) return; // Create a bounds object const bounds = new maplibregl.LngLatBounds(); // Extend bounds with each feature geojson.features.forEach(feature => { bounds.extend(feature.geometry.coordinates); }); // Fit map to bounds with padding map.fitBounds(bounds, { padding: 50, maxZoom: 12 }); }