a working product with ugly ui
This commit is contained in:
338
public/src/components/map/DrawingHandler.tsx
Normal file
338
public/src/components/map/DrawingHandler.tsx
Normal file
@@ -0,0 +1,338 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMapEvents, Polyline, Marker } from 'react-leaflet';
|
||||
import L from 'leaflet';
|
||||
import { useDrawingStore } from '../../stores/drawingStore';
|
||||
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 [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;
|
||||
alert(`${deviceName} has no available ports (${usedPorts}/${portCount} ports used). Please select a different device or increase the port count.`);
|
||||
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)) {
|
||||
alert('Wireless mesh can only connect between Access Points. Please click on an AP.');
|
||||
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;
|
||||
alert(`${apName} already has the maximum of 4 wireless mesh connections. Please select a different AP.`);
|
||||
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) {
|
||||
alert('Cannot create wireless mesh to the same Access Point. Please select a different AP.');
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user