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
|
|
|
}
|