import { useState, useRef, useEffect } from "react";
import Panzoom from '@panzoom/panzoom';
import io from 'socket.io-client';
export default function RealTimeLocation({ FenceSettings, fenceSettingsView }) {
const [cursorPosition, setCursorPosition] = useState({ x: 0, y: 0 });
const [panzoom, setPanzoom] = useState(null);
const [locationData, setLocationData] = useState([]);
const [fenceData, setFenceData] = useState(null);
const svgRef = useRef(null);
// 맵 크기 및 스케일 계산
const width = Number(FenceSettings?.mapX) || 0;
const height = Number(FenceSettings?.mapY) || 0;
const PHX = Number(FenceSettings?.PHX) || 0;
const PHY = Number(FenceSettings?.PHY) || 0;
const scale = 1 / 100;
// 패딩 계산
const padding = Math.max(
Math.abs(PHX * scale),
Math.abs(PHY * scale),
Math.abs((width - PHX) * scale),
Math.abs((height - PHY) * scale)
);
// 1. viewBox 관련 계산
const viewBoxWidth = (width * scale) + (padding * 2);
const viewBoxHeight = (height * scale) + (padding * 2);
// 2. elementScale 계산
const baseSize = Math.min(viewBoxWidth, viewBoxHeight);
const elementScale = Math.max(baseSize / 50, 0.1);
// 3. fontSize 계산 (elementScale 사용)
const fontSize = Math.min(Math.max(0.6 * elementScale, 0.2), 2);
// 실제 크기 계산
const realWidth = width * scale;
const realHeight = height * scale;
// 요소 크기 계산
const strokeWidth = 0.04 * elementScale;
useEffect(() => {
setFenceData(fenceSettingsView);
}, [fenceSettingsView]);
// 소켓 연결
useEffect(() => {
// Socket.IO 연결 생성
//const socket = io(process.env.REACT_APP_SOCKET_URL); // 환경변수 사용
const socket = io('localhost:8401'); // Socket.IO 서버 포트
// 연결 성공 시
socket.on('connect', () => {
console.log('Socket.IO Connected');
});
// 8401포트에서 받은 location이라는 이벤트 이름으로 받은 데이터를 받음!!
socket.on('location', (data) => {
console.log('Received location data:', data);
setLocationData(prevMessages => [...prevMessages, data]);
});
// 연결 에러 시
socket.on('connect_error', (error) => {
console.error('Socket.IO Connection Error:', error);
});
// 연결 종료 시
socket.on('disconnect', () => {
console.log('Socket.IO Disconnected');
});
// 컴포넌트 언마운트 시 정리
return () => {
socket.disconnect();
};
}, []);
// PanZoom 초기화
useEffect(() => {
if (svgRef.current) {
const pz = Panzoom(svgRef.current, {
maxScale: 30,
minScale: 0.5,
initialZoom: 1,
bounds: true,
boundsPadding: 0.1
});
setPanzoom(pz);
svgRef.current.parentElement.addEventListener('wheel', pz.zoomWithWheel);
return () => {
svgRef.current?.parentElement?.removeEventListener('wheel', pz.zoomWithWheel);
pz.destroy();
};
}
}, []);
// 커서 위치 변환 함수
const convertToRealCoordinates = (event) => {
if (!FenceSettings?.mapX || !FenceSettings?.mapY || !FenceSettings?.PHX || !FenceSettings?.PHY) {
return { x: 0, y: 0 };
}
const svgPoint = svgRef.current.createSVGPoint();
svgPoint.x = event.clientX;
svgPoint.y = event.clientY;
const transformMatrix = svgRef.current.getScreenCTM().inverse();
const transformedPoint = svgPoint.matrixTransform(transformMatrix);
const realX = ((transformedPoint.x - padding)) * 100 + Number(FenceSettings.PHX);
const realY = ((transformedPoint.y - padding)) * 100 + Number(FenceSettings.PHY);
return { x: realX, y: realY };
};
// transformTagHistoryData 함수 수정
const transformTagHistoryData = (x, y) => {
const cmX = x * 100;
const cmY = y * 100;
// 오프라인/온라인 모드에 따른 원점 좌표 사용
// const originX = getCheckOffline ?
// Number(offlineMapSettings?.PHX) || 0 :
// Number(FenceSettings?.PHX) || 0;
// const originY = getCheckOffline ?
// Number(offlineMapSettings?.PHY) || 0 :
// Number(FenceSettings?.PHY) || 0;
const originX = Number(FenceSettings?.PHX) || 0;
const originY = Number(FenceSettings?.PHY) || 0;
const transformedX = ((cmX - originX) * scale) + padding;
const transformedY = ((cmY - originY) * scale) + padding;
return {
x: transformedX,
y: transformedY
};
};
// 마우스 이동 핸들러
const handleMouseMove = (event) => {
const coords = convertToRealCoordinates(event);
setCursorPosition({
x: Number(coords.x.toFixed(2)),
y: Number(coords.y.toFixed(2))
});
};
// locationData를 처리하여 각 태그의 최신 위치만 필터링
const getLatestLocations = (data) => {
const latestLocations = new Map();
// 각 태그별로 가장 최신 데이터만 저장
data.forEach(item => {
const existing = latestLocations.get(item.tagId);
if (!existing || item.timestamp > existing.timestamp) {
latestLocations.set(item.tagId, item);
}
});
// Map을 배열로 변환
return Array.from(latestLocations.values());
};
// posGroup 문자열을 SVG points 형식으로 변환하는 함수
const formatPoints = (posGroup) => {
try {
const points = Array.isArray(posGroup) ? posGroup : JSON.parse(posGroup);
// 오프라인/온라인 모드에 따른 원점 좌표 사용
const originX =
Number(FenceSettings?.PHX) || 0;
const originY =
Number(FenceSettings?.PHY) || 0;
return points.map(point => {
const cmX = point[0] * 100;
const cmY = point[1] * 100;
const x = ((cmX - originX) * scale) + padding;
const y = ((cmY - originY) * scale) + padding;
return `${x},${y}`;
}).join(' ');
} catch (error) {
console.error('Failed to process posGroup:', error);
return '';
}
};
// getFenceCenter 함수 수정
const getFenceCenter = (posGroup) => {
try {
const points = Array.isArray(posGroup) ? posGroup : JSON.parse(posGroup);
// 오프라인/온라인 모드에 따른 원점 좌표 사용
const originX =
Number(FenceSettings?.PHX) || 0;
const originY =
Number(FenceSettings?.PHY) || 0;
const sum = points.reduce((acc, point) => {
const cmX = point[0] * 100;
const cmY = point[1] * 100;
const x = ((cmX - originX) * scale) + padding;
const y = ((cmY - originY) * scale) + padding;
return {
x: acc.x + x,
y: acc.y + y
};
}, { x: 0, y: 0 });
return {
x: sum.x / points.length,
y: sum.y / points.length
};
} catch (error) {
console.error('Failed to calculate fence center:', error);
return { x: 0, y: 0 };
}
};
return (
<div style={{
width: '100%',
height: '100%',
position: 'relative'
}}>
{/* 맵 컨테이너 */}
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
position: 'relative',
border: '1px solid rgba(0, 0, 0, 0.1)',
borderRadius: '12px',
overflow: 'hidden'
}}
onMouseMove={handleMouseMove}
>
<svg
ref={svgRef}
width="100%"
height="100%"
viewBox={`0 0 ${viewBoxWidth} ${viewBoxHeight}`}
preserveAspectRatio="xMidYMid meet"
style={{ cursor: 'grab' }}
>
{/* 맵 영역 */}
<rect
x={padding}
y={padding}
width={realWidth}
height={realHeight}
fill="none"
stroke="black"
strokeWidth={strokeWidth}
vectorEffect="non-scaling-stroke"
/>
{/* 맵 이미지 */}
<image
href={FenceSettings?.mapUrl || ''}
x={padding}
y={padding}
width={realWidth}
height={realHeight}
/>
{fenceData && fenceData.map((data, index) => (
<g key={index}>
<polygon
points={formatPoints(data.posGroup)}
fill={data.fillColor === "null" ? 'rgba(128, 0, 128, 0.2)' : data.fillColor}
stroke={data.frameColor === "null" ? 'purple' : data.frameColor}
strokeWidth={strokeWidth}
vectorEffect="non-scaling-stroke"
/>
<text
x={getFenceCenter(data.posGroup).x}
y={getFenceCenter(data.posGroup).y}
textAnchor="middle"
fill="purple"
fontSize={fontSize}
fontWeight="bold"
vectorEffect="non-scaling-stroke"
>
{data.fenceName || `Fence ${index + 1}`}
</text>
</g>
))}
{getLatestLocations(locationData).map((data, index) => {
const coords = transformTagHistoryData(data.x, data.y);
return (
<g key={data.tagId || index}>
{/* 위치 표시 원 */}
<circle
cx={coords.x}
cy={coords.y}
r={strokeWidth * 2}
fill="rgba(46, 204, 113, 0.6)"
stroke="#27ae60"
strokeWidth={strokeWidth * 0.5}
style={{ cursor: 'pointer' }}
/>
{/* 태그 ID 표시 */}
<text
x={coords.x}
y={coords.y - (strokeWidth * 3)}
textAnchor="middle"
fill="#2c3e50"
fontSize={`${strokeWidth * 6}px`}
fontWeight="bold"
style={{
textShadow: '0 0 3px white'
}}
>
{data.tagId || `Tag ${index + 1}`}
</text>
{/* 좌표 값 표시 */}
<text
x={coords.x}
y={coords.y + (strokeWidth * 4)}
textAnchor="middle"
fill="#34495e"
fontSize={`${strokeWidth * 4}px`}
style={{
textShadow: '0 0 3px white'
}}
>
({data.x}, {data.y})
</text>
</g>
);
})}
</svg>
{/* 커서 위치 표시 */}
<div style={{
position: 'absolute',
bottom: '24px',
left: '10%',
transform: 'translateX(-50%)',
backgroundColor: 'rgba(255, 255, 255, 0.9)',
padding: '8px 16px',
borderRadius: '24px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '14px',
color: '#1a237e',
backdropFilter: 'blur(8px)',
border: '1px solid rgba(255, 255, 255, 0.2)',
zIndex: 100
}}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2L2 22l10-6 10 6L12 2z" />
</svg>
<span>
X: <strong>{cursorPosition.x}</strong>cm,{' '}
Y: <strong>{cursorPosition.y}</strong>cm
</span>
</div>
</div>
</div>
);
}