Files
Deep-Package-Inspection/src/components/ui/GlobeMap.tsx
T

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