98 lines
2.8 KiB
TypeScript
98 lines
2.8 KiB
TypeScript
'use client'
|
|
|
|
import { useRef, useEffect, useState, useCallback } from 'react'
|
|
import Globe from 'react-globe.gl'
|
|
|
|
export interface GlobePoint {
|
|
lat: number
|
|
lng: number
|
|
id: string
|
|
label: string
|
|
color: string
|
|
size?: number
|
|
}
|
|
|
|
interface GlobeVisualizationProps {
|
|
points: GlobePoint[]
|
|
onPointClick: (point: GlobePoint) => void
|
|
height?: number
|
|
autoRotate?: boolean
|
|
focusLat?: number
|
|
focusLng?: number
|
|
}
|
|
|
|
export function GlobeVisualization({
|
|
points,
|
|
onPointClick,
|
|
height = 500,
|
|
autoRotate = true,
|
|
focusLat,
|
|
focusLng,
|
|
}: GlobeVisualizationProps) {
|
|
const globeRef = useRef<any>(null)
|
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
const [containerWidth, setContainerWidth] = useState(0)
|
|
|
|
// Responsive width
|
|
useEffect(() => {
|
|
if (!containerRef.current) return
|
|
const observer = new ResizeObserver((entries) => {
|
|
for (const entry of entries) {
|
|
setContainerWidth(entry.contentRect.width)
|
|
}
|
|
})
|
|
observer.observe(containerRef.current)
|
|
setContainerWidth(containerRef.current.clientWidth)
|
|
return () => observer.disconnect()
|
|
}, [])
|
|
|
|
// Configure controls after globe is ready
|
|
const handleGlobeReady = useCallback(() => {
|
|
const globe = globeRef.current
|
|
if (!globe) return
|
|
const controls = globe.controls()
|
|
if (controls) {
|
|
controls.autoRotate = autoRotate
|
|
controls.autoRotateSpeed = 0.5
|
|
controls.enableDamping = true
|
|
controls.dampingFactor = 0.1
|
|
}
|
|
// Set initial view
|
|
globe.pointOfView({ lat: focusLat ?? 20, lng: focusLng ?? 0, altitude: 2.5 }, 0)
|
|
}, [autoRotate, focusLat, focusLng])
|
|
|
|
// Focus on specific coordinates when they change
|
|
useEffect(() => {
|
|
if (globeRef.current && focusLat != null && focusLng != null) {
|
|
globeRef.current.pointOfView({ lat: focusLat, lng: focusLng, altitude: 1.5 }, 1000)
|
|
}
|
|
}, [focusLat, focusLng])
|
|
|
|
return (
|
|
<div ref={containerRef} className="w-full" style={{ height }}>
|
|
{containerWidth > 0 && (
|
|
<Globe
|
|
ref={globeRef}
|
|
width={containerWidth}
|
|
height={height}
|
|
globeImageUrl="/textures/earth-blue-marble.jpg"
|
|
backgroundColor="rgba(0,0,0,0)"
|
|
showAtmosphere={true}
|
|
atmosphereColor="rgba(100,150,255,0.3)"
|
|
atmosphereAltitude={0.15}
|
|
pointsData={points}
|
|
pointLat="lat"
|
|
pointLng="lng"
|
|
pointColor="color"
|
|
pointAltitude={0.01}
|
|
pointRadius={(d: any) => Math.max(0.15, Math.min(0.6, (d.size || 1) * 0.12))}
|
|
pointLabel={(d: any) => `<div class="text-sm font-medium px-2 py-1 bg-background/90 border border-border rounded shadow-lg">${d.label}</div>`}
|
|
onPointClick={(point: any) => onPointClick(point as GlobePoint)}
|
|
onGlobeReady={handleGlobeReady}
|
|
enablePointerInteraction={true}
|
|
/>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|