изменение логики работы поиска датчиков из дашборд и истории тревог по конкретному этажу
This commit is contained in:
317
frontend/components/model/Canvas2DPlan.tsx
Normal file
317
frontend/components/model/Canvas2DPlan.tsx
Normal file
@@ -0,0 +1,317 @@
|
||||
'use client'
|
||||
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { AbstractMesh, Vector3 } from '@babylonjs/core'
|
||||
|
||||
interface Canvas2DPlanProps {
|
||||
meshes: AbstractMesh[]
|
||||
sensorStatusMap: Record<string, string>
|
||||
onClose: () => void
|
||||
onSensorClick?: (sensorId: string) => void
|
||||
}
|
||||
|
||||
interface Sensor2D {
|
||||
id: string
|
||||
x: number
|
||||
y: number
|
||||
status: string
|
||||
}
|
||||
|
||||
const Canvas2DPlan: React.FC<Canvas2DPlanProps> = ({
|
||||
meshes,
|
||||
sensorStatusMap,
|
||||
onClose,
|
||||
onSensorClick,
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const [sensors, setSensors] = useState<Sensor2D[]>([])
|
||||
const [hoveredSensor, setHoveredSensor] = useState<string | null>(null)
|
||||
const [scale, setScale] = useState(10)
|
||||
const [offset, setOffset] = useState({ x: 0, y: 0 })
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
|
||||
|
||||
// Извлечение датчиков из mesh'ей
|
||||
useEffect(() => {
|
||||
const extractedSensors: Sensor2D[] = []
|
||||
|
||||
console.log('[Canvas2DPlan] Extracting sensors from meshes:', meshes.length)
|
||||
console.log('[Canvas2DPlan] sensorStatusMap:', sensorStatusMap)
|
||||
|
||||
let meshesWithMetadata = 0
|
||||
let meshesWithSensorID = 0
|
||||
let meshesInStatusMap = 0
|
||||
|
||||
meshes.forEach((mesh, index) => {
|
||||
if (mesh.metadata) {
|
||||
meshesWithMetadata++
|
||||
if (index < 3) {
|
||||
console.log(`[Canvas2DPlan] Sample mesh[${index}] metadata:`, mesh.metadata)
|
||||
}
|
||||
}
|
||||
|
||||
const sensorId = mesh.metadata?.Sensor_ID
|
||||
if (sensorId) {
|
||||
meshesWithSensorID++
|
||||
if (index < 3) {
|
||||
console.log(`[Canvas2DPlan] Sample mesh[${index}] Sensor_ID:`, sensorId, 'in map?', !!sensorStatusMap[sensorId])
|
||||
}
|
||||
}
|
||||
|
||||
if (sensorId && sensorStatusMap[sensorId]) {
|
||||
meshesInStatusMap++
|
||||
const position = mesh.getAbsolutePosition()
|
||||
extractedSensors.push({
|
||||
id: sensorId,
|
||||
x: position.x,
|
||||
y: position.z, // Используем Z как Y для вида сверху
|
||||
status: sensorStatusMap[sensorId],
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
console.log('[Canvas2DPlan] Meshes with metadata:', meshesWithMetadata)
|
||||
console.log('[Canvas2DPlan] Meshes with Sensor_ID:', meshesWithSensorID)
|
||||
console.log('[Canvas2DPlan] Meshes in statusMap:', meshesInStatusMap)
|
||||
|
||||
console.log('[Canvas2DPlan] Extracted sensors:', extractedSensors.length, extractedSensors)
|
||||
|
||||
setSensors(extractedSensors)
|
||||
|
||||
// Автоматическое центрирование
|
||||
if (extractedSensors.length > 0 && canvasRef.current) {
|
||||
const minX = Math.min(...extractedSensors.map((s) => s.x))
|
||||
const maxX = Math.max(...extractedSensors.map((s) => s.x))
|
||||
const minY = Math.min(...extractedSensors.map((s) => s.y))
|
||||
const maxY = Math.max(...extractedSensors.map((s) => s.y))
|
||||
|
||||
const centerX = (minX + maxX) / 2
|
||||
const centerY = (minY + maxY) / 2
|
||||
|
||||
const canvas = canvasRef.current
|
||||
setOffset({
|
||||
x: canvas.width / 2 - centerX * scale,
|
||||
y: canvas.height / 2 - centerY * scale,
|
||||
})
|
||||
}
|
||||
}, [meshes, sensorStatusMap, scale])
|
||||
|
||||
// Рендеринг canvas
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
// Очистка
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// Фон
|
||||
ctx.fillStyle = '#0e111a'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
// Сетка
|
||||
ctx.strokeStyle = '#1a1d2e'
|
||||
ctx.lineWidth = 1
|
||||
const gridSize = 50
|
||||
for (let x = 0; x < canvas.width; x += gridSize) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, 0)
|
||||
ctx.lineTo(x, canvas.height)
|
||||
ctx.stroke()
|
||||
}
|
||||
for (let y = 0; y < canvas.height; y += gridSize) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(0, y)
|
||||
ctx.lineTo(canvas.width, y)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// Рисуем датчики
|
||||
sensors.forEach((sensor) => {
|
||||
const x = sensor.x * scale + offset.x
|
||||
const y = sensor.y * scale + offset.y
|
||||
|
||||
// Определяем цвет по статусу
|
||||
let color = '#6b7280' // gray
|
||||
if (sensor.status === 'critical') color = '#ef4444' // red
|
||||
else if (sensor.status === 'warning') color = '#f59e0b' // amber
|
||||
else if (sensor.status === 'normal') color = '#10b981' // green
|
||||
|
||||
// Внешний круг (подсветка при hover)
|
||||
if (hoveredSensor === sensor.id) {
|
||||
ctx.fillStyle = color + '40'
|
||||
ctx.beginPath()
|
||||
ctx.arc(x, y, 20, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
// Основной круг датчика
|
||||
ctx.fillStyle = color
|
||||
ctx.beginPath()
|
||||
ctx.arc(x, y, 8, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
|
||||
// Обводка
|
||||
ctx.strokeStyle = '#ffffff'
|
||||
ctx.lineWidth = 2
|
||||
ctx.stroke()
|
||||
|
||||
// Подпись
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = '12px Inter, sans-serif'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText(sensor.id, x, y - 15)
|
||||
})
|
||||
|
||||
// Легенда
|
||||
const legendX = 20
|
||||
const legendY = canvas.height - 80
|
||||
ctx.fillStyle = '#161824cc'
|
||||
ctx.fillRect(legendX - 10, legendY - 10, 180, 70)
|
||||
|
||||
const statuses = [
|
||||
{ label: 'Критический', color: '#ef4444' },
|
||||
{ label: 'Предупреждение', color: '#f59e0b' },
|
||||
{ label: 'Нормальный', color: '#10b981' },
|
||||
]
|
||||
|
||||
statuses.forEach((status, index) => {
|
||||
const y = legendY + index * 20
|
||||
ctx.fillStyle = status.color
|
||||
ctx.beginPath()
|
||||
ctx.arc(legendX, y, 6, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = '12px Inter, sans-serif'
|
||||
ctx.textAlign = 'left'
|
||||
ctx.fillText(status.label, legendX + 15, y + 4)
|
||||
})
|
||||
}, [sensors, scale, offset, hoveredSensor])
|
||||
|
||||
// Обработка клика
|
||||
const handleCanvasClick = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const clickX = e.clientX - rect.left
|
||||
const clickY = e.clientY - rect.top
|
||||
|
||||
// Проверяем клик по датчику
|
||||
for (const sensor of sensors) {
|
||||
const x = sensor.x * scale + offset.x
|
||||
const y = sensor.y * scale + offset.y
|
||||
const distance = Math.sqrt((clickX - x) ** 2 + (clickY - y) ** 2)
|
||||
|
||||
if (distance <= 10) {
|
||||
onSensorClick?.(sensor.id)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка hover
|
||||
const handleCanvasMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (isDragging) {
|
||||
const dx = e.clientX - dragStart.x
|
||||
const dy = e.clientY - dragStart.y
|
||||
setOffset((prev) => ({ x: prev.x + dx, y: prev.y + dy }))
|
||||
setDragStart({ x: e.clientX, y: e.clientY })
|
||||
return
|
||||
}
|
||||
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const mouseX = e.clientX - rect.left
|
||||
const mouseY = e.clientY - rect.top
|
||||
|
||||
let foundSensor: string | null = null
|
||||
for (const sensor of sensors) {
|
||||
const x = sensor.x * scale + offset.x
|
||||
const y = sensor.y * scale + offset.y
|
||||
const distance = Math.sqrt((mouseX - x) ** 2 + (mouseY - y) ** 2)
|
||||
|
||||
if (distance <= 10) {
|
||||
foundSensor = sensor.id
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
setHoveredSensor(foundSensor)
|
||||
}
|
||||
|
||||
// Обработка zoom
|
||||
const handleWheel = (e: React.WheelEvent<HTMLCanvasElement>) => {
|
||||
e.preventDefault()
|
||||
const delta = e.deltaY > 0 ? 0.9 : 1.1
|
||||
setScale((prev) => Math.max(1, Math.min(50, prev * delta)))
|
||||
}
|
||||
|
||||
// Обработка drag
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
setIsDragging(true)
|
||||
setDragStart({ x: e.clientX, y: e.clientY })
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsDragging(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] bg-black/80 flex items-center justify-center p-4">
|
||||
<div className="relative bg-[#161824] rounded-lg shadow-2xl border border-white/10 p-6 w-full h-full max-w-[1400px] max-h-[900px] flex flex-col">
|
||||
{/* Заголовок */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold text-white">2D План-схема</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
>
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Canvas */}
|
||||
<div className="flex-1 min-h-0">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={1200}
|
||||
height={700}
|
||||
onClick={handleCanvasClick}
|
||||
onMouseMove={handleCanvasMove}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
onWheel={handleWheel}
|
||||
className="border border-white/10 rounded cursor-move w-full h-full"
|
||||
style={{ cursor: isDragging ? 'grabbing' : 'grab', maxHeight: '700px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Подсказка */}
|
||||
<div className="mt-4 text-sm text-gray-400 text-center">
|
||||
<p>Колесико мыши - масштаб | Перетаскивание - перемещение | Клик по датчику - подробности</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Canvas2DPlan
|
||||
Reference in New Issue
Block a user