341 lines
11 KiB
TypeScript
341 lines
11 KiB
TypeScript
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<any[]>([]);
|
|
const [startDeviceId, setStartDeviceId] = useState<string | null>(null);
|
|
const [endDeviceId, setEndDeviceId] = useState<string | null>(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 && (
|
|
<Polyline
|
|
positions={drawingPoints}
|
|
color={color}
|
|
weight={3}
|
|
dashArray={dashArray}
|
|
opacity={0.8}
|
|
/>
|
|
)}
|
|
|
|
{/* Preview line from last point to cursor */}
|
|
{cursorPosition && drawingPoints.length > 0 && (
|
|
<Polyline
|
|
positions={[drawingPoints[drawingPoints.length - 1], cursorPosition]}
|
|
color={color}
|
|
weight={3}
|
|
dashArray="5, 5"
|
|
opacity={0.5}
|
|
/>
|
|
)}
|
|
|
|
{/* Markers at each point */}
|
|
{drawingPoints.map((point, idx) => (
|
|
<Marker key={idx} position={point} />
|
|
))}
|
|
</>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|