Files
eve-signaler/frontend/src/components/MapNode.tsx

294 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState } from 'react';
import { getSecurityColor } from '../utils/securityColors';
interface MapNodeProps {
id: string;
name: string;
position: { x: number; y: number };
onClick: () => void;
onDoubleClick?: (e: React.MouseEvent) => void;
onDragStart?: (e: React.MouseEvent) => void;
onDrag?: (e: React.MouseEvent) => void;
onDragEnd?: (e: React.MouseEvent) => void;
onContextMenu?: (e: React.MouseEvent) => void;
type: 'region' | 'system';
security?: number;
signatures?: number;
isDraggable?: boolean;
disableNavigate?: boolean;
jumps?: number;
kills?: number;
showJumps?: boolean;
showKills?: boolean;
viewBoxWidth?: number; // Add viewBox width for scaling calculations
}
export const MapNode: React.FC<MapNodeProps> = ({
id,
name,
position,
onClick,
onDoubleClick,
onDragStart,
onDrag,
onDragEnd,
onContextMenu,
type,
security,
signatures,
isDraggable = false,
disableNavigate = false,
jumps,
kills,
showJumps = false,
showKills = false,
viewBoxWidth = 1200,
}) => {
const [isHovered, setIsHovered] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const handleMouseDown = (e: React.MouseEvent) => {
console.log('MapNode handleMouseDown', { isDraggable, type, isDragging });
if (!isDraggable || type !== 'system') return;
e.stopPropagation();
onDragStart?.(e);
};
const handleMouseMove = (e: React.MouseEvent) => {
console.log('MapNode handleMouseMove', { isDragging, isDraggable, type });
if (!isDragging || !isDraggable || type !== 'system') return;
e.stopPropagation();
onDrag?.(e);
};
const handleMouseUp = (e: React.MouseEvent) => {
console.log('MapNode handleMouseUp', { isDragging, isDraggable, type });
if (!isDragging || !isDraggable || type !== 'system') return;
e.stopPropagation();
setIsDragging(false);
onDragEnd?.(e);
};
const handleMouseLeave = (e: React.MouseEvent) => {
console.log('MapNode handleMouseLeave', { isDragging, isDraggable, type });
if (!isDragging || !isDraggable || type !== 'system') return;
e.stopPropagation();
setIsDragging(false);
onDragEnd?.(e);
};
const nodeColor = security !== undefined ? getSecurityColor(security) : '#a855f7';
if (type === 'region') {
const pillWidth = Math.max(name.length * 5, 40);
const pillHeight = 18;
return (
<g
transform={`translate(${position.x}, ${position.y})`}
className="cursor-pointer select-none"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onClick={(e) => {
e.stopPropagation();
onClick();
}}
>
{/* Glow effect */}
<rect
x={-pillWidth / 2 - 3}
y={-pillHeight / 2 - 3}
width={pillWidth + 6}
height={pillHeight + 6}
rx={(pillHeight + 6) / 2}
fill={nodeColor}
opacity={isHovered ? 0.3 : 0.1}
filter="url(#glow)"
className="transition-all duration-300"
/>
{/* Main pill */}
<rect
x={-pillWidth / 2}
y={-pillHeight / 2}
width={pillWidth}
height={pillHeight}
rx={pillHeight / 2}
fill={nodeColor}
stroke="#ffffff"
strokeWidth="1.5"
filter="url(#glow)"
className={`transition-all duration-300 ${isHovered ? 'drop-shadow-lg' : ''
}`}
/>
{/* Text inside pill - made smaller */}
<text
x="0"
y="3"
textAnchor="middle"
fill="#ffffff"
fontSize="8"
fontWeight="bold"
className="transition-all duration-300 pointer-events-none select-none"
style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.8)' }}
>
{name}
</text>
</g>
);
} else {
const nodeSize = 6;
const textOffset = 20;
return (
<g
transform={`translate(${position.x}, ${position.y})`}
className={`cursor-pointer select-none ${isDraggable ? 'cursor-move' : ''}`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
onMouseDown={handleMouseDown}
onClick={(e) => {
e.stopPropagation();
onClick();
}}
onDoubleClick={(e) => {
e.stopPropagation();
onDoubleClick?.(e);
}}
onContextMenu={(e) => {
e.stopPropagation();
onContextMenu?.(e);
}}
>
{/* Node glow effect */}
<circle
r={isHovered ? nodeSize + 4 : nodeSize}
fill={nodeColor}
opacity={isHovered ? 0.3 : 0.1}
filter="url(#glow)"
className="transition-all duration-300"
/>
{/* Main node - removed stroke to eliminate white border */}
<circle
r={nodeSize}
fill={nodeColor}
filter="url(#glow)"
className={`transition-all duration-300 ${isHovered ? 'drop-shadow-lg' : ''
}`}
/>
{/* Inner core */}
<circle
r={nodeSize - 3}
fill={isHovered ? '#ffffff' : nodeColor}
opacity={0.8}
className="transition-all duration-300"
/>
{/* Node label - fixed visual size regardless of zoom */}
<text
x="0"
y={textOffset}
textAnchor="middle"
fill="#ffffff"
fontSize="10"
fontWeight="bold"
className={`transition-all duration-300 ${isHovered ? 'fill-purple-200' : 'fill-white'
} pointer-events-none select-none`}
style={{
textShadow: '2px 2px 4px rgba(0,0,0,0.8)',
vectorEffect: 'non-scaling-stroke'
}}
transform={`scale(${1 / (1200 / viewBoxWidth)})`}
transformOrigin="0 0"
>
{name} {security !== undefined && (
<tspan fill={getSecurityColor(security)}>{security.toFixed(1)}</tspan>
)}
</text>
{/* Dynamic text positioning based on what's shown */}
{(() => {
let currentY = textOffset + 15;
const textElements = [];
// Add signatures if present
if (signatures !== undefined && signatures > 0) {
textElements.push(
<text
key="signatures"
x="0"
y={currentY}
textAnchor="middle"
fill="#a3a3a3"
fontSize="12"
className="pointer-events-none select-none"
style={{
textShadow: '1px 1px 2px rgba(0,0,0,0.8)',
vectorEffect: 'non-scaling-stroke'
}}
transform={`scale(${1 / (1200 / viewBoxWidth)})`}
transformOrigin="0 0"
>
📡 {signatures}
</text>
);
currentY += 15;
}
// Add jumps if enabled and present
if (showJumps && jumps !== undefined) {
textElements.push(
<text
key="jumps"
x="0"
y={currentY}
textAnchor="middle"
fill="#60a5fa"
fontSize="10"
className="pointer-events-none select-none"
style={{
textShadow: '1px 1px 2px rgba(0,0,0,0.8)',
vectorEffect: 'non-scaling-stroke'
}}
transform={`scale(${1 / (1200 / viewBoxWidth)})`}
transformOrigin="0 0"
>
🚀 {jumps}
</text>
);
currentY += 15;
}
// Add kills if enabled and present
if (showKills && kills !== undefined) {
textElements.push(
<text
key="kills"
x="0"
y={currentY}
textAnchor="middle"
fill="#f87171"
fontSize="10"
className="pointer-events-none select-none"
style={{
textShadow: '1px 1px 2px rgba(0,0,0,0.8)',
vectorEffect: 'non-scaling-stroke'
}}
transform={`scale(${1 / (1200 / viewBoxWidth)})`}
transformOrigin="0 0"
>
{kills}
</text>
);
}
return textElements;
})()}
</g>
);
}
};