import { useEffect, useState } from 'react'; import { useMapEvents, Polyline, Marker } from 'react-leaflet'; import L from 'leaflet'; import { useDrawingStore } from '../../stores/drawingStore'; import { useUIStore } from '../../stores/uiStore'; import { mapItemService } from '../../services/mapItemService'; import { CABLE_COLORS, type CableType } from '../../types/mapItem'; interface DrawingHandlerProps { mapId: string; onItemCreated: () => void; } export function DrawingHandler({ mapId, onItemCreated }: DrawingHandlerProps) { const { activeTool, isDrawing, drawingPoints, setIsDrawing, addDrawingPoint, resetDrawing, setActiveTool } = useDrawingStore(); const { showToast } = useUIStore(); const [cursorPosition, setCursorPosition] = useState<[number, number] | null>(null); const [allItems, setAllItems] = useState([]); const [startDeviceId, setStartDeviceId] = useState(null); const [endDeviceId, setEndDeviceId] = useState(null); const isCableTool = ['fiber', 'cat6', 'cat6_poe'].includes(activeTool); const isDeviceTool = ['switch', 'indoor_ap', 'outdoor_ap'].includes(activeTool); const isWirelessTool = activeTool === 'wireless_mesh'; // Load all map items for snapping useEffect(() => { const loadItems = async () => { try { const items = await mapItemService.getMapItems(mapId); setAllItems(items); } catch (error) { console.error('Failed to load items for snapping:', error); } }; loadItems(); }, [mapId]); // Find nearby device for snapping const findNearbyDevice = (lat: number, lng: number, radiusMeters = 5): any | null => { const devices = allItems.filter(item => ['switch', 'indoor_ap', 'outdoor_ap'].includes(item.type) && item.geometry.type === 'Point' ); for (const device of devices) { const [deviceLng, deviceLat] = device.geometry.coordinates; const distance = map.distance([lat, lng], [deviceLat, deviceLng]); if (distance <= radiusMeters) { return device; } } return null; }; // Check if device has available ports const hasAvailablePorts = (device: any): boolean => { const portCount = device.properties.port_count || 0; const usedPorts = device.properties.connections?.length || 0; return usedPorts < portCount; }; // Count wireless mesh connections for an AP const getWirelessMeshCount = (apId: string): number => { const meshLinks = allItems.filter(item => item.type === 'wireless_mesh' && (item.properties.start_ap_id === apId || item.properties.end_ap_id === apId) ); return meshLinks.length; }; const map = useMapEvents({ click: async (e) => { if (activeTool === 'select') return; let { lat, lng } = e.latlng; let clickedDevice = null; // Check for nearby device when drawing cables if (isCableTool && map) { clickedDevice = findNearbyDevice(lat, lng); if (clickedDevice) { // Check if device has available ports if (!hasAvailablePorts(clickedDevice)) { const portCount = clickedDevice.properties.port_count || 0; const usedPorts = clickedDevice.properties.connections?.length || 0; const deviceName = clickedDevice.properties.name || clickedDevice.type; showToast(`${deviceName} has no available ports (${usedPorts}/${portCount} ports used). Please select a different device or increase the port count.`, 'error'); return; } // Snap to device center const [deviceLng, deviceLat] = clickedDevice.geometry.coordinates; lat = deviceLat; lng = deviceLng; // Track device connection if (!isDrawing) { setStartDeviceId(clickedDevice.id); } else { setEndDeviceId(clickedDevice.id); } } } // Device placement - single click if (isDeviceTool) { try { await mapItemService.createMapItem(mapId, { type: activeTool as any, geometry: { type: 'Point', coordinates: [lng, lat], }, properties: { name: `${activeTool} at ${lat.toFixed(4)}, ${lng.toFixed(4)}`, port_count: activeTool === 'switch' ? 5 : activeTool === 'outdoor_ap' ? 1 : 4, connections: [], }, }); onItemCreated(); // Reload items for snapping const items = await mapItemService.getMapItems(mapId); setAllItems(items); } catch (error) { console.error('Failed to create device:', error); } return; } // Cable drawing - multi-point if (isCableTool) { if (!isDrawing) { setIsDrawing(true); addDrawingPoint([lat, lng]); } else { addDrawingPoint([lat, lng]); } } // Wireless mesh - connect AP to AP only if (isWirelessTool) { // Must click on an AP const ap = findNearbyDevice(lat, lng, 5); if (!ap || !['indoor_ap', 'outdoor_ap'].includes(ap.type)) { showToast('Wireless mesh can only connect between Access Points. Please click on an AP.', 'error'); return; } // Check wireless mesh connection limit (max 4 per AP) const meshCount = getWirelessMeshCount(ap.id); if (meshCount >= 4) { const apName = ap.properties.name || ap.type; showToast(`${apName} already has the maximum of 4 wireless mesh connections. Please select a different AP.`, 'error'); return; } if (!isDrawing) { // First AP clicked setIsDrawing(true); setStartDeviceId(ap.id); const [apLng, apLat] = ap.geometry.coordinates; addDrawingPoint([apLat, apLng]); } else if (drawingPoints.length === 1) { // Second AP clicked - check if it's different from first if (ap.id === startDeviceId) { showToast('Cannot create wireless mesh to the same Access Point. Please select a different AP.', 'error'); return; } // Second AP clicked - finish immediately with both points const [apLng, apLat] = ap.geometry.coordinates; const secondPoint: [number, number] = [apLat, apLng]; await finishWirelessMesh(ap.id, secondPoint); } } }, mousemove: (e) => { // Update cursor position for preview line if (isDrawing && (isCableTool || isWirelessTool)) { setCursorPosition([e.latlng.lat, e.latlng.lng]); } }, contextmenu: async (e) => { // Right-click to finish cable drawing if (isCableTool && isDrawing && drawingPoints.length >= 2) { e.originalEvent.preventDefault(); e.originalEvent.stopPropagation(); await finishCable(); return false; } if (!isDrawing) { e.originalEvent.preventDefault(); } }, }); useEffect(() => { // Listen for Escape key globally const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' && isDrawing) { resetDrawing(); setCursorPosition(null); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [isDrawing, resetDrawing]); const finishCable = async () => { if (drawingPoints.length < 2) return; try { const cableType = activeTool as CableType; await mapItemService.createMapItem(mapId, { type: 'cable', geometry: { type: 'LineString', coordinates: drawingPoints.map(([lat, lng]) => [lng, lat]), }, properties: { cable_type: cableType, name: `${cableType} cable`, start_device_id: startDeviceId, end_device_id: endDeviceId, }, }); onItemCreated(); resetDrawing(); setCursorPosition(null); setStartDeviceId(null); setEndDeviceId(null); // Reload items const items = await mapItemService.getMapItems(mapId); setAllItems(items); } catch (error) { console.error('Failed to create cable:', error); } }; const finishWirelessMesh = async (endApId: string, secondPoint: [number, number]) => { if (drawingPoints.length !== 1) { console.error('Wireless mesh requires exactly 1 starting point'); return; } try { // Create line with first point from state and second point from parameter const coordinates = [ [drawingPoints[0][1], drawingPoints[0][0]], // First point: [lng, lat] [secondPoint[1], secondPoint[0]], // Second point: [lng, lat] ]; await mapItemService.createMapItem(mapId, { type: 'wireless_mesh', geometry: { type: 'LineString', coordinates: coordinates, }, properties: { name: 'Wireless mesh link', start_ap_id: startDeviceId, end_ap_id: endApId, }, }); onItemCreated(); // Reset drawing state but keep the wireless mesh tool active resetDrawing(); setCursorPosition(null); setStartDeviceId(null); setEndDeviceId(null); // Reload items const items = await mapItemService.getMapItems(mapId); setAllItems(items); } catch (error) { console.error('Failed to create wireless mesh:', error); // Reset on error too resetDrawing(); setCursorPosition(null); setStartDeviceId(null); setEndDeviceId(null); } }; // Render drawing preview if (isDrawing && drawingPoints.length > 0) { const color = isCableTool ? CABLE_COLORS[activeTool as CableType] : isWirelessTool ? '#10B981' : '#6B7280'; const dashArray = isWirelessTool ? '10, 10' : undefined; // Create preview line from last point to cursor const previewPositions = cursorPosition ? [...drawingPoints, cursorPosition] : drawingPoints; return ( <> {/* Main line connecting all points */} {drawingPoints.length > 1 && ( )} {/* Preview line from last point to cursor */} {cursorPosition && drawingPoints.length > 0 && ( )} {/* Markers at each point */} {drawingPoints.map((point, idx) => ( ))} ); } return null; }