219 lines
7.2 KiB
TypeScript
Raw Normal View History

2025-03-23 15:24:10 +00:00
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";
2025-03-31 13:36:35 +01:00
import { GiObservatory } from "react-icons/gi";
2025-03-23 15:24:10 +00:00
import Event from "@appTypes/Event";
import getMagnitudeColor from "@utils/getMagnitudeColour";
2025-03-31 14:22:57 +01:00
import { userAgentFromString } from "next/server";
import ReactDOM from "react-dom";
import { createRoot } from "react-dom/client";
2025-03-23 15:24:10 +00:00
interface MapComponentProps {
events: Event[];
selectedEventId: Event["id"];
setSelectedEventId: Dispatch<SetStateAction<string>>;
hoveredEventId: Event["id"];
setHoveredEventId: Dispatch<SetStateAction<string>>;
2025-03-31 14:22:57 +01:00
mapType: String;
2025-03-23 15:24:10 +00:00
}
// Map component with location-style pulsing dots, animations, and tooltips
2025-03-31 14:22:57 +01:00
function MapComponent({ events, selectedEventId, setSelectedEventId, hoveredEventId, setHoveredEventId, mapType }: MapComponentProps) {
2025-03-23 15:24:10 +00:00
const map = useRef<mapboxgl.Map | null>(null);
const markers = useRef<{ [key: string]: mapboxgl.Marker }>({});
const [mapBounds, setMapBounds] = useState<LngLatBounds>();
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);
2025-03-31 13:36:35 +01:00
//<GiObservatory />
2025-03-23 15:24:10 +00:00
// Create marker container
2025-03-31 14:22:57 +01:00
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";
2025-03-23 15:24:10 +00:00
// Central dot
const dotElement = document.createElement("div");
2025-03-31 13:36:35 +01:00
dotElement.style.width = "10px";
dotElement.style.height = "10px";
2025-03-23 15:24:10 +00:00
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";
2025-03-31 14:22:57 +01:00
// 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(
<GiObservatory style={{ fontSize: "24px", color: "#FF5722" }} />
);
2025-03-23 15:24:10 +00:00
2025-03-31 14:22:57 +01:00
quakeElement.appendChild(pulseElement);
quakeElement.appendChild(dotElement);
const marker = new mapboxgl.Marker({ element: mapType === "observatories"? observatoryElement : quakeElement }).setLngLat([event.longitude, event.latitude]).addTo(map.current!);
2025-03-23 15:24:10 +00:00
const popup = new mapboxgl.Popup({ offset: 25, closeButton: false, anchor: "bottom" }).setHTML(`
<div>
<h3>${event.title}</h3>
<p>Magnitude: ${event.magnitude}</p>
<p>${event.text2}</p>
</div>
`);
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 (
<div className="w-full h-full">
<div id="map-container" className="w-full h-full" />
</div>
);
}
// 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 (
<>
<style>{pulseStyles}</style>
<MapComponent {...props} />
</>
);
2025-03-31 14:22:57 +01:00
}