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

258 lines
8.4 KiB
TypeScript

"use client";
import React, { useEffect, useState, useRef, useMemo } from 'react';
import dynamic from 'next/dynamic';
import { fmtBytes } from '@/lib/utils';
import { useTheme } from "next-themes";
// Dynamically import Globe to avoid SSR issues
const Globe = dynamic(() => import('react-globe.gl'), { ssr: false });
const COUNTRY_COORDS: Record<string, [number, number]> = {
"ID": [113.9213, -0.7893],
"US": [-95.7129, 37.0902],
"HK": [114.1095, 22.3964],
"SG": [103.8198, 1.3521],
"JP": [138.2529, 36.2048],
"SC": [55.4920, -4.6796],
"GB": [-3.4360, 55.3781],
"IT": [12.5674, 41.8719],
"CN": [104.1954, 35.8617],
"KR": [127.7669, 35.9078],
"CA": [-106.3468, 56.1304],
"LU": [6.1296, 49.8153],
"NL": [5.2913, 52.1326],
"TR": [35.2433, 38.9637],
"UA": [31.1656, 48.3794]
};
// Jakarta, Indonesia as Origin
const ORIGIN_LAT = -6.2088;
const ORIGIN_LNG = 106.8456;
interface GlobeMapProps {
data: {
countryCode: string;
value: number;
label: string;
}[];
}
export const GlobeMap = ({ data }: GlobeMapProps) => {
const globeRef = useRef<any>(null);
const [dimensions, setDimensions] = useState({ width: 800, height: 500 });
const [countries, setCountries] = useState<any>(null);
const containerRef = useRef<HTMLDivElement>(null);
const { theme } = useTheme();
const isLight = theme === 'light';
// Generate a beautiful, ultra-fresh gradient blue texture for the Light Mode ocean
const lightGlobeTexture = useMemo(() => {
if (typeof window === 'undefined') return '';
const canvas = document.createElement('canvas');
canvas.width = 1024;
canvas.height = 512;
const ctx = canvas.getContext('2d');
if (ctx) {
const gradient = ctx.createLinearGradient(0, 0, 0, 512);
gradient.addColorStop(0, '#bae6fd'); // sky-200 (light bright blue at the top)
gradient.addColorStop(0.5, '#38bdf8'); // sky-400 (vibrant blue equator)
gradient.addColorStop(1, '#0284c7'); // sky-600 (deeper rich blue at the bottom)
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 1024, 512);
}
return canvas.toDataURL('image/png');
}, []);
// Generate a sleek, deep gradient dark texture for the Dark Mode ocean
const darkGlobeTexture = useMemo(() => {
if (typeof window === 'undefined') return '';
const canvas = document.createElement('canvas');
canvas.width = 1024;
canvas.height = 512;
const ctx = canvas.getContext('2d');
if (ctx) {
const gradient = ctx.createLinearGradient(0, 0, 0, 512);
gradient.addColorStop(0, '#0f172a'); // slate-900
gradient.addColorStop(0.5, '#020617'); // slate-950 (deepest dark blue/black)
gradient.addColorStop(1, '#000000'); // black
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 1024, 512);
}
return canvas.toDataURL('image/png');
}, []);
useEffect(() => {
// Handle resizing
const updateDimensions = () => {
if (containerRef.current) {
setDimensions({
width: containerRef.current.offsetWidth,
height: containerRef.current.offsetHeight || 500
});
}
};
updateDimensions();
window.addEventListener('resize', updateDimensions);
// Fetch high-res vector polygons for both themes' continents
fetch('https://unpkg.com/globe.gl/example/datasets/ne_110m_admin_0_countries.geojson')
.then(res => res.json())
.then(setCountries)
.catch(err => console.error('Failed to load countries', err));
return () => window.removeEventListener('resize', updateDimensions);
}, []);
useEffect(() => {
// Auto-rotate and controls configuration
// Since Globe is dynamically imported, we must wait until the ref is actually populated.
let checkInterval = setInterval(() => {
if (globeRef.current && globeRef.current.controls) {
clearInterval(checkInterval);
const controls = globeRef.current.controls();
controls.autoRotate = true;
controls.autoRotateSpeed = 1.2; // Adjusted auto-rotate speed to 1.5
// Enable inertia/damping for smooth deceleration after dragging
controls.enableDamping = true;
controls.dampingFactor = 0.035;
// Adjust the manual rotation sensitivity
controls.rotateSpeed = 0.8;
// Remove boundaries to allow 360 degree rotation in all directions (as much as OrbitControls allows)
controls.minPolarAngle = 0; // The top pole (North)
controls.maxPolarAngle = Math.PI; // The bottom pole (South)
// Focus on Southeast Asia initially
globeRef.current.pointOfView({ lat: ORIGIN_LAT, lng: ORIGIN_LNG, altitude: 1.8 }, 2000);
}
}, 100);
return () => clearInterval(checkInterval);
}, []);
const arcsData = useMemo(() => {
return data
.filter(d => d.countryCode !== "ID" && COUNTRY_COORDS[d.countryCode])
.map(d => {
const [lng, lat] = COUNTRY_COORDS[d.countryCode];
return {
startLat: ORIGIN_LAT,
startLng: ORIGIN_LNG,
endLat: lat,
endLng: lng,
color: ['#0ea5e9', '#38bdf8'],
name: d.label,
value: d.value
};
});
}, [data]);
const pointsData = useMemo(() => {
const points = arcsData.map(arc => ({
lat: arc.endLat,
lng: arc.endLng,
size: 0.1,
color: '#38bdf8',
name: arc.name,
value: arc.value
}));
// Add origin point
points.push({
lat: ORIGIN_LAT,
lng: ORIGIN_LNG,
size: 0.2,
color: '#f43f5e',
name: 'Indonesia (Origin)',
value: 0
});
return points;
}, [arcsData, isLight]);
return (
<div
ref={containerRef}
className="relative w-full h-[500px] rounded-xl overflow-visible bg-transparent cursor-move flex items-center justify-center transition-all duration-500"
>
<Globe
ref={globeRef}
width={dimensions.width}
height={dimensions.height}
backgroundColor="rgba(0,0,0,0)"
showGraticules={true}
globeImageUrl={isLight ? lightGlobeTexture : darkGlobeTexture}
showAtmosphere={true}
atmosphereColor={isLight ? "#0ea5e9" : "#1e40af"}
atmosphereAltitude={isLight ? 0.15 : 0.25}
// Polygons Configuration for Unified Vector Designs
polygonsData={countries ? countries.features : []}
polygonAltitude={0.015}
polygonCapColor={() => isLight ? '#4ade80' : '#0f172a'} // Fresh green for light, dark slate for dark
polygonSideColor={() => isLight ? '#22c55e' : '#020617'} // Depth colors
polygonStrokeColor={() => isLight ? '#bbf7d0' : '#0284c7'} // Crisp bright borders for light, neon blue for dark
// Arcs configuration
arcsData={arcsData}
arcStartLat={(d: any) => d.startLat}
arcStartLng={(d: any) => d.startLng}
arcEndLat={(d: any) => d.endLat}
arcEndLng={(d: any) => d.endLng}
arcColor={(d: any) => d.color}
arcDashLength={0.4}
arcDashGap={0.2}
arcDashAnimateTime={2000}
arcAltitude={0.3}
arcStroke={0.7}
// Points configuration
pointsData={pointsData}
pointLat="lat"
pointLng="lng"
pointColor="color"
pointAltitude={0.01}
pointRadius="size"
pointsMerge={true}
// Rings configuration (Ping effect for points)
ringsData={pointsData}
ringLat="lat"
ringLng="lng"
ringColor="color"
ringMaxRadius={(d: any) => d.name === 'Indonesia (Origin)' ? 3 : 1.5}
ringPropagationSpeed={3}
ringRepeatPeriod={1000}
// HTML Labels configuration for better readability with text-shadow
htmlElementsData={pointsData}
htmlLat="lat"
htmlLng="lng"
htmlAltitude={0.03}
htmlElement={(d: any) => {
const el = document.createElement('div');
el.innerHTML = d.name;
el.style.color = isLight ? '#0f172a' : '#f8fafc';
el.style.fontSize = '12px';
el.style.fontWeight = '700';
el.style.fontFamily = 'Inter, sans-serif';
// Creates a thick outline effect so text doesn't blend into arcs/continents
el.style.textShadow = isLight
? '0 0 4px #ffffff, 0 0 6px #ffffff, 0 0 8px #ffffff'
: '0 0 4px #000000, 0 0 6px #000000, 0 0 8px #000000';
el.style.pointerEvents = 'none';
el.style.whiteSpace = 'nowrap';
el.style.transform = 'translate(-50%, -100%)'; // shift slightly up
return el;
}}
/>
</div>
);
};