import "mapbox-gl/dist/mapbox-gl.css"; import mapboxgl, { LngLatBounds } from "mapbox-gl"; import React, { Dispatch, SetStateAction, useCallback, useEffect, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; import { GiObservatory } from "react-icons/gi"; import GeologicalEvent from "@appTypes/Event"; import getMagnitudeColor from "@utils/getMagnitudeColour"; interface MapComponentProps { events: GeologicalEvent[]; selectedEventId: GeologicalEvent["id"]; setSelectedEventId: Dispatch>; hoveredEventId: GeologicalEvent["id"]; setHoveredEventId: Dispatch>; mapType: String; } 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, padding: number = 150) => { if (map.current && bounds) { map.current!.fitBounds(bounds, { padding, maxZoom: 10, zoom: 1 }); } }, []); 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: GeologicalEvent) => { 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", () => { const bounds = new mapboxgl.LngLatBounds(); events.forEach((event) => { bounds.extend([event.longitude, event.latitude]); }); fitToBounds(bounds, 0); setMapBounds(bounds); events.forEach((event) => { const quakeElement = document.createElement("div"); const dotElement = document.createElement("div"); const pulseElement = document.createElement("div"); if (event.magnitude) { const color = getMagnitudeColor(event.magnitude); quakeElement.style.width = "50px"; quakeElement.style.height = "50px"; quakeElement.style.position = "absolute"; quakeElement.style.display = "flex"; quakeElement.style.alignItems = "center"; quakeElement.style.justifyContent = "center"; dotElement.style.width = "10px"; dotElement.style.height = "10px"; dotElement.style.backgroundColor = color; dotElement.style.borderRadius = "50%"; dotElement.style.position = "absolute"; dotElement.style.zIndex = "2"; pulseElement.className = "location-pulse"; pulseElement.style.width = "20px"; pulseElement.style.height = "20px"; pulseElement.style.backgroundColor = `${color}80`; pulseElement.style.borderRadius = "50%"; pulseElement.style.position = "absolute"; pulseElement.style.zIndex = "1"; } const observatoryElement = document.createElement("div"); const root = createRoot(observatoryElement); 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}

${mapType === "observatories" ? `

${event.text1}

` : `

Magnitude: ${event.magnitude}

`}

${event.text2}

`); marker.setPopup(popup); markers.current[event.id] = marker; const markerDomElement = marker.getElement(); markerDomElement.style.cursor = "pointer"; markerDomElement.addEventListener("mouseenter", () => setHoveredEventId(event.id)); markerDomElement.addEventListener("mouseleave", () => setHoveredEventId("")); markerDomElement.addEventListener("click", () => setSelectedEventId((prevEventId) => (prevEventId === event.id ? "" : event.id)) ); markerDomElement.dataset.listenersAdded = "true"; }); }); // Add map click handler to deselect when clicking outside markers map.current.on("click", (e) => { const target = e.originalEvent.target as HTMLElement; // Check if the click target is not a marker element if (!target.closest(".mapboxgl-marker")) { setSelectedEventId(""); } }); 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 && mapBounds) { fitToBounds(mapBounds, 0); } }, [events, selectedEventId, mapBounds, fitToBounds, flyToEvent]); useEffect(() => { clearAllPopups(); if (hoveredEventId && selectedEventId && hoveredEventId !== selectedEventId) { showPopup(hoveredEventId); showPopup(selectedEventId); } else if (hoveredEventId || selectedEventId) { showPopup(hoveredEventId || selectedEventId); } }, [hoveredEventId, selectedEventId, clearAllPopups, showPopup]); return (
); } 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 ( <> ); }