153 lines
5.0 KiB
TypeScript
153 lines
5.0 KiB
TypeScript
"use client";
|
|
|
|
import React, { useMemo, useState } from 'react';
|
|
import { ComposableMap, Geographies, Geography, Line, Marker, ZoomableGroup } from "react-simple-maps";
|
|
import { useTheme } from "next-themes";
|
|
import { useGlobeData, GlobeDataInput } from './useGlobeData';
|
|
|
|
const geoUrl = "/ne_110m_admin_0_countries.geojson";
|
|
|
|
interface GlobeMapProps {
|
|
data: GlobeDataInput[];
|
|
}
|
|
|
|
export const GlobeMap = ({ data }: GlobeMapProps) => {
|
|
const { theme } = useTheme();
|
|
const isLight = theme === 'light';
|
|
|
|
const [position, setPosition] = useState({ coordinates: [0, 20], zoom: 1 });
|
|
function handleMoveEnd(newPosition: any) {
|
|
setPosition(newPosition);
|
|
}
|
|
|
|
const { arcsData, pointsData, labelsData } = useGlobeData(data, isLight);
|
|
|
|
const mapBgColor = "transparent";
|
|
const mapBorderColor = isLight ? "#ffffff" : "#0ea5e9";
|
|
const mapFillColor = isLight ? "#4ade80" : "#0f172a";
|
|
const mapHoverFill = isLight ? "#22c55e" : "#1e293b";
|
|
|
|
return (
|
|
<div className="relative w-full h-[500px] rounded-xl overflow-hidden bg-transparent flex items-center justify-center cursor-grab active:cursor-grabbing">
|
|
<style>{`
|
|
@keyframes flowDashGlobe {
|
|
to {
|
|
stroke-dashoffset: -100;
|
|
}
|
|
}
|
|
.globe-animated-line {
|
|
animation: flowDashGlobe 1.5s linear infinite;
|
|
}
|
|
`}</style>
|
|
<ComposableMap
|
|
projection="geoMercator"
|
|
projectionConfig={{
|
|
scale: 120,
|
|
center: [0, 20]
|
|
}}
|
|
width={800}
|
|
height={500}
|
|
style={{ width: "100%", height: "100%", outline: "none" }}
|
|
>
|
|
<ZoomableGroup
|
|
zoom={position.zoom}
|
|
center={position.coordinates as [number, number]}
|
|
onMoveEnd={handleMoveEnd}
|
|
minZoom={1}
|
|
maxZoom={8}
|
|
>
|
|
<Geographies geography={geoUrl}>
|
|
{({ geographies }) =>
|
|
geographies.map((geo) => (
|
|
<Geography
|
|
key={geo.rsmKey}
|
|
geography={geo}
|
|
fill={mapFillColor}
|
|
stroke={mapBorderColor}
|
|
strokeWidth={0.5}
|
|
style={{
|
|
default: { outline: "none" },
|
|
hover: { fill: mapHoverFill, outline: "none" },
|
|
pressed: { outline: "none" },
|
|
}}
|
|
/>
|
|
))
|
|
}
|
|
</Geographies>
|
|
|
|
{arcsData.map((arc: any, i: number) => {
|
|
const isLocal = arc.startLng === arc.endLng && arc.startLat === arc.endLat;
|
|
const color = Array.isArray(arc.color) ? arc.color[0] : arc.color || '#0ea5e9';
|
|
|
|
if (isLocal) {
|
|
return (
|
|
<Marker key={`arc-${i}`} coordinates={[arc.startLng, arc.startLat]}>
|
|
<circle r={8} fill="none" stroke={color} strokeWidth={1.5} opacity={0.6} strokeDasharray="2,2" />
|
|
</Marker>
|
|
);
|
|
}
|
|
|
|
// Force the line to go straight across the map (the "front")
|
|
// by interpolating multiple points. This prevents d3-geo from
|
|
// wrapping around the dateline for long distances.
|
|
const coordinates: [number, number][] = [];
|
|
const steps = 30;
|
|
for (let j = 0; j <= steps; j++) {
|
|
const t = j / steps;
|
|
let lng = arc.startLng + (arc.endLng - arc.startLng) * t;
|
|
|
|
// Dynamic arc height based on distance
|
|
const dist = Math.sqrt(
|
|
Math.pow(arc.endLng - arc.startLng, 2) +
|
|
Math.pow(arc.endLat - arc.startLat, 2)
|
|
);
|
|
const height = Math.min(dist * 0.15, 25);
|
|
let lat = arc.startLat + (arc.endLat - arc.startLat) * t + Math.sin(t * Math.PI) * height;
|
|
|
|
coordinates.push([lng, lat]);
|
|
}
|
|
|
|
return (
|
|
<Line
|
|
key={`arc-${i}`}
|
|
coordinates={coordinates}
|
|
stroke={color}
|
|
strokeWidth={1.5}
|
|
strokeLinecap="round"
|
|
strokeOpacity={0.9}
|
|
strokeDasharray="2 4 4 4 8 78"
|
|
className="globe-animated-line"
|
|
style={{ pointerEvents: 'none' }}
|
|
/>
|
|
);
|
|
})}
|
|
|
|
{pointsData.map((pt: any, i: number) => (
|
|
<Marker key={`marker-${i}`} coordinates={[pt.lng, pt.lat]}>
|
|
<circle r={pt.size * 2} fill={pt.color} opacity={0.8} />
|
|
</Marker>
|
|
))}
|
|
|
|
{labelsData.map((lbl: any, i: number) => (
|
|
<Marker key={`label-${i}`} coordinates={[lbl.lng, lbl.lat]}>
|
|
<text
|
|
textAnchor="middle"
|
|
y={-10}
|
|
style={{
|
|
fontFamily: "system-ui",
|
|
fill: lbl.color,
|
|
fontSize: `${lbl.size * 6}px`,
|
|
fontWeight: "bold",
|
|
pointerEvents: "none"
|
|
}}
|
|
>
|
|
{lbl.name}
|
|
</text>
|
|
</Marker>
|
|
))}
|
|
</ZoomableGroup>
|
|
</ComposableMap>
|
|
</div>
|
|
);
|
|
};
|