import { useCallback, useState } from "react"; import React, { useRef, useEffect, Dispatch, SetStateAction } from "react"; import mapboxgl, { LngLatBounds } from "mapbox-gl"; import "mapbox-gl/dist/mapbox-gl.css"; import { GiObservatory } from "react-icons/gi"; import Event from "@appTypes/Event"; import getMagnitudeColor from "@utils/getMagnitudeColour"; import { userAgentFromString } from "next/server"; import ReactDOM from "react-dom"; import { createRoot } from "react-dom/client"; interface MapComponentProps { events: Event[]; selectedEventId: Event["id"]; setSelectedEventId: Dispatch>; hoveredEventId: Event["id"]; setHoveredEventId: Dispatch>; mapType: String; } // Map component with location-style pulsing dots, animations, and tooltips function MapComponent({ events, selectedEventId, setSelectedEventId, hoveredEventId, setHoveredEventId, mapType }: MapComponentProps) { const map = useRef(null); const markers = useRef<{ [key: string]: mapboxgl.Marker }>({}); const [mapBounds, setMapBounds] = useState(); const fitToBounds = useCallback((bounds: LngLatBounds) => { if (map.current && bounds) { map.current!.fitBounds(bounds, { padding: 150, maxZoom: 10 }); } }, []); const showPopup = useCallback((eventId: string) => { const marker = markers.current[eventId]; if (marker && map.current) { marker.getPopup()?.addTo(map.current); } }, []); const clearAllPopups = useCallback(() => { Object.values(markers.current).forEach((marker) => marker.getPopup()?.remove()); }, []); const flyToEvent = useCallback((event: Event) => { if (map.current) { map.current.flyTo({ center: [event.longitude, event.latitude], zoom: 4, speed: 1.5, curve: 1.42, }); } }, []); useEffect(() => { mapboxgl.accessToken = "pk.eyJ1IjoidGltaG93aXR6IiwiYSI6ImNtOGtjcXA5bDA3Ym4ya3NnOWxjbjlxZG8ifQ.6u_KgXEdLTakz910QRAorQ"; try { map.current = new mapboxgl.Map({ container: "map-container", style: "mapbox://styles/mapbox/light-v10", center: [0, 0], zoom: 1, }); } catch (error) { console.error("Map initialization failed:", error); return; } map.current.on("load", () => { // Fit map to bounds const bounds = new mapboxgl.LngLatBounds(); events.forEach((event) => { bounds.extend([event.longitude, event.latitude]); }); fitToBounds(bounds); setMapBounds(bounds); // Add markers with location pulse events.forEach((event) => { const color = getMagnitudeColor(event.magnitude); // // Create marker container const quakeElement = document.createElement("div"); quakeElement.style.width = "50px"; // Increased size to accommodate pulse quakeElement.style.height = "50px"; quakeElement.style.position = "absolute"; quakeElement.style.display = "flex"; quakeElement.style.alignItems = "center"; quakeElement.style.justifyContent = "center"; // Central dot const dotElement = document.createElement("div"); dotElement.style.width = "10px"; dotElement.style.height = "10px"; dotElement.style.backgroundColor = color; dotElement.style.borderRadius = "50%"; dotElement.style.position = "absolute"; dotElement.style.zIndex = "2"; // Ensure dot is above pulse // Pulsing ring const pulseElement = document.createElement("div"); pulseElement.className = "location-pulse"; pulseElement.style.width = "20px"; // Initial size pulseElement.style.height = "20px"; pulseElement.style.backgroundColor = `${color}80`; // Color with 50% opacity (hex alpha) pulseElement.style.borderRadius = "50%"; pulseElement.style.position = "absolute"; pulseElement.style.zIndex = "1"; // Observatory marker const observatoryElement = document.createElement("div"); observatoryElement.style.fontSize = "24px"; // Adjust icon size observatoryElement.style.color = "#FF5722"; // Set color const root = createRoot(observatoryElement); // `createRoot` is now the standard API root.render( ); quakeElement.appendChild(pulseElement); quakeElement.appendChild(dotElement); const marker = new mapboxgl.Marker({ element: mapType === "observatories"? observatoryElement : quakeElement }).setLngLat([event.longitude, event.latitude]).addTo(map.current!); const popup = new mapboxgl.Popup({ offset: 25, closeButton: false, anchor: "bottom" }).setHTML(`

${event.title}

Magnitude: ${event.magnitude}

${event.text2}

`); marker.setPopup(popup); markers.current[event.id] = marker; // Add hover events const markerDomElement = marker.getElement(); markerDomElement.style.cursor = "pointer"; // Optional: indicate interactivity markerDomElement.addEventListener("mouseenter", () => setHoveredEventId(event.id)); markerDomElement.addEventListener("mouseleave", () => setHoveredEventId("")); markerDomElement.addEventListener("click", () => setSelectedEventId((prevEventId) => (prevEventId === event.id ? "" : event.id))); // Cleanup event listeners on unmount markerDomElement.dataset.listenersAdded = "true"; // Mark for cleanup }); }); map.current.on("error", (e) => { console.error("Mapbox error:", e); }); return () => { map.current?.remove(); }; }, [events, setSelectedEventId, setHoveredEventId, fitToBounds]); useEffect(() => { const event = events.find((x) => x.id === selectedEventId); if (event) flyToEvent(event); else if (!selectedEventId) { if (mapBounds) fitToBounds(mapBounds); } }, [events, selectedEventId, mapBounds, fitToBounds, clearAllPopups, flyToEvent]); useEffect(() => { // Clear all popups first clearAllPopups(); // Handle both events if they exist and are different if (hoveredEventId && selectedEventId && hoveredEventId !== selectedEventId) { showPopup(hoveredEventId); showPopup(selectedEventId); } // Handle single event case (either hovered or selected) else if (hoveredEventId || selectedEventId) { showPopup(hoveredEventId || selectedEventId); } }, [hoveredEventId, selectedEventId, clearAllPopups, showPopup]); return (
); } // CSS for location-style pulsing animation const pulseStyles = ` .location-pulse { animation: locationPulse 2s infinite; } @keyframes locationPulse { 0% { transform: scale(0.5); opacity: 0.8; } 70% { transform: scale(2); opacity: 0; } 100% { transform: scale(2); opacity: 0; } } `; export default function Map(props: MapComponentProps) { return ( <> ); }