Files
mapmaker/public/src/components/map/DrawingHandler.tsx

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;
}