Initial commit
This commit is contained in:
@@ -0,0 +1,999 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using RobotNet.VDA5050;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Edge;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.LayoutData;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Node;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Requests;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Responses;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Station;
|
||||
using RobotNet10.MapManager.Data;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace RobotNet10.MapManager.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Service implementation for retrieving complete layout data
|
||||
/// </summary>
|
||||
public class LayoutDataService(
|
||||
MapDbContext context,
|
||||
LayoutLevelNamingService namingService,
|
||||
IEdgeService edgeService,
|
||||
INodeService nodeService) : ILayoutDataService
|
||||
{
|
||||
private readonly MapDbContext _context = context;
|
||||
private readonly LayoutLevelNamingService _namingService = namingService;
|
||||
private readonly IEdgeService _edgeService = edgeService;
|
||||
private readonly INodeService _nodeService = nodeService;
|
||||
|
||||
public async Task<LayoutDataDto> GetLayoutDataAsync(Guid layoutLevelId)
|
||||
{
|
||||
// Get all nodes with vehicle properties
|
||||
var nodes = await _context.Nodes
|
||||
.Where(n => n.LevelId == layoutLevelId)
|
||||
.Include(n => n.VehicleProperties)
|
||||
.ThenInclude(vp => vp.VehicleType)
|
||||
.OrderBy(n => n.NodeId)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
// Get all edges with vehicle properties and related nodes
|
||||
var edges = await _context.Edges
|
||||
.Where(e => e.LevelId == layoutLevelId)
|
||||
.Include(e => e.StartNode)
|
||||
.Include(e => e.EndNode)
|
||||
.Include(e => e.VehicleProperties)
|
||||
.ThenInclude(vp => vp.VehicleType)
|
||||
.OrderBy(e => e.EdgeId)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
// Get all stations with interaction nodes
|
||||
var stations = await _context.Stations
|
||||
.Where(s => s.LevelId == layoutLevelId)
|
||||
.Include(s => s.InteractionNodes)
|
||||
.ThenInclude(sin => sin.Node)
|
||||
.ThenInclude(n => n.VehicleProperties)
|
||||
.OrderBy(s => s.StationId)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
var dto = new LayoutDataDto
|
||||
{
|
||||
LayoutLevelId = layoutLevelId,
|
||||
Nodes = [.. nodes.Select(MapNodeToDto)],
|
||||
Edges = [.. edges.Select(MapEdgeToDto)],
|
||||
Stations = [.. stations.Select(MapStationToDto)]
|
||||
};
|
||||
return dto;
|
||||
}
|
||||
|
||||
// Helper methods to map entities to DTOs
|
||||
private static NodeDto MapNodeToDto(Node node)
|
||||
{
|
||||
return new NodeDto
|
||||
{
|
||||
Id = node.Id,
|
||||
LevelId = node.LevelId,
|
||||
NodeId = node.NodeId,
|
||||
NodeName = node.NodeName,
|
||||
NodeDescription = node.NodeDescription,
|
||||
MapId = node.MapId,
|
||||
X = node.X,
|
||||
Y = node.Y,
|
||||
VehicleProperties = node.VehicleProperties?.Select(vp => new NodeVehiclePropertyDto
|
||||
{
|
||||
Id = vp.Id,
|
||||
NodeId = vp.NodeId,
|
||||
VehicleTypeId = vp.VehicleTypeId,
|
||||
VehicleTypeIdString = vp.VehicleType?.VehicleTypeId,
|
||||
Theta = vp.Theta,
|
||||
Actions = vp.Actions,
|
||||
AllowedDeviationXY = vp.AllowedDeviationXY,
|
||||
AllowedDeviationTheta = vp.AllowedDeviationTheta
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private static EdgeDto MapEdgeToDto(Edge edge)
|
||||
{
|
||||
return new EdgeDto
|
||||
{
|
||||
Id = edge.Id,
|
||||
LevelId = edge.LevelId,
|
||||
EdgeId = edge.EdgeId,
|
||||
EdgeName = edge.EdgeName,
|
||||
EdgeDescription = edge.EdgeDescription,
|
||||
StartNodeId = edge.StartNodeId,
|
||||
EndNodeId = edge.EndNodeId,
|
||||
StartNode = edge.StartNode != null ? MapNodeToDto(edge.StartNode) : null,
|
||||
EndNode = edge.EndNode != null ? MapNodeToDto(edge.EndNode) : null,
|
||||
VehicleProperties = edge.VehicleProperties?.Select(vp => new EdgeVehiclePropertyDto
|
||||
{
|
||||
Id = vp.Id,
|
||||
EdgeId = vp.EdgeId,
|
||||
VehicleTypeId = vp.VehicleTypeId,
|
||||
VehicleTypeIdString = vp.VehicleType?.VehicleTypeId,
|
||||
VehicleOrientation = vp.VehicleOrientation,
|
||||
OrientationType = vp.OrientationType,
|
||||
RotationAllowed = vp.RotationAllowed,
|
||||
RotationAtStartNodeAllowed = vp.RotationAtStartNodeAllowed,
|
||||
RotationAtEndNodeAllowed = vp.RotationAtEndNodeAllowed,
|
||||
MaxSpeed = vp.MaxSpeed,
|
||||
MaxRotationSpeed = vp.MaxRotationSpeed,
|
||||
MinHeight = vp.MinHeight,
|
||||
MaxHeight = vp.MaxHeight,
|
||||
LoadRestriction = (vp.LoadRestriction_Unloaded.HasValue || vp.LoadRestriction_Loaded.HasValue || !string.IsNullOrWhiteSpace(vp.LoadRestriction_LoadSetNames))
|
||||
? new LoadRestrictionDto
|
||||
{
|
||||
Unloaded = vp.LoadRestriction_Unloaded,
|
||||
Loaded = vp.LoadRestriction_Loaded,
|
||||
LoadSetNames = SafeDeserializeLoadSetNames(vp.LoadRestriction_LoadSetNames)
|
||||
}
|
||||
: null,
|
||||
TrajectoryDegree = vp.TrajectoryDegree,
|
||||
TrajectoryControlPoint1X = vp.TrajectoryControlPoint1X,
|
||||
TrajectoryControlPoint1Y = vp.TrajectoryControlPoint1Y,
|
||||
TrajectoryControlPoint2X = vp.TrajectoryControlPoint2X,
|
||||
TrajectoryControlPoint2Y = vp.TrajectoryControlPoint2Y,
|
||||
CorridorLeftWidth = vp.CorridorLeftWidth,
|
||||
CorridorRightWidth = vp.CorridorRightWidth,
|
||||
CorridorRefPoint = vp.CorridorRefPoint
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private static StationDto MapStationToDto(Station station)
|
||||
{
|
||||
return new StationDto
|
||||
{
|
||||
Id = station.Id,
|
||||
LevelId = station.LevelId,
|
||||
StationId = station.StationId,
|
||||
StationName = station.StationName,
|
||||
StationDescription = station.StationDescription,
|
||||
StationHeight = station.StationHeight,
|
||||
X = station.X,
|
||||
Y = station.Y,
|
||||
Theta = station.Theta,
|
||||
InteractionNodes = station.InteractionNodes?.Select(sin => new StationInteractionNodeDto
|
||||
{
|
||||
Id = sin.Id,
|
||||
StationId = sin.StationId,
|
||||
NodeId = sin.NodeId,
|
||||
Node = sin.Node != null ? MapNodeToDto(sin.Node) : null
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// MERGE/SPLIT OPERATIONS
|
||||
// ==========================================
|
||||
|
||||
public async Task<MergeNodesResponse> MergeNodesAsync(MergeNodesRequest request)
|
||||
{
|
||||
if (request.NodeIds.Count < 2)
|
||||
{
|
||||
throw new InvalidOperationException("Need at least 2 nodes to merge");
|
||||
}
|
||||
|
||||
// Load nodes with all related data
|
||||
var nodesToMerge = await _context.Nodes
|
||||
.Where(n => request.NodeIds.Contains(n.Id) && n.LevelId == request.LevelId)
|
||||
.Include(n => n.VehicleProperties)
|
||||
.Include(n => n.StationInteractions)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
if (nodesToMerge.Count != request.NodeIds.Count)
|
||||
{
|
||||
throw new InvalidOperationException("Some nodes not found or belong to different level");
|
||||
}
|
||||
|
||||
// Get editor settings for validation
|
||||
var settings = await _context.LayoutLevelEditorSettings
|
||||
.FirstOrDefaultAsync(s => s.LevelId == request.LevelId);
|
||||
var proximityRadius = settings?.NodeProximityRadius ?? 0.35;
|
||||
|
||||
// Check distances between nodes
|
||||
var maxDistance = 0.0;
|
||||
for (int i = 0; i < nodesToMerge.Count; i++)
|
||||
{
|
||||
for (int j = i + 1; j < nodesToMerge.Count; j++)
|
||||
{
|
||||
var dx = nodesToMerge[i].X - nodesToMerge[j].X;
|
||||
var dy = nodesToMerge[i].Y - nodesToMerge[j].Y;
|
||||
var distance = Math.Sqrt(dx * dx + dy * dy);
|
||||
maxDistance = Math.Max(maxDistance, distance);
|
||||
}
|
||||
}
|
||||
|
||||
// If distance exceeds proximity radius, throw exception (frontend will show confirmation)
|
||||
if (maxDistance > proximityRadius)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Maximum distance between nodes ({maxDistance:F3}m) exceeds proximity radius ({proximityRadius:F3}m). " +
|
||||
"Please confirm merge operation.");
|
||||
}
|
||||
|
||||
// Check stations: if multiple nodes have stations, throw error
|
||||
var nodesWithStations = nodesToMerge
|
||||
.Where(n => n.StationInteractions.Count != 0)
|
||||
.ToList();
|
||||
|
||||
if (nodesWithStations.Count > 1)
|
||||
{
|
||||
var stationIds = nodesWithStations
|
||||
.SelectMany(n => n.StationInteractions.Select(sin => sin.StationId))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot merge nodes: Multiple nodes have stations. " +
|
||||
$"Found {nodesWithStations.Count} nodes with {stationIds.Count} different station(s). " +
|
||||
"Please remove stations from some nodes before merging.");
|
||||
}
|
||||
|
||||
// Calculate center position
|
||||
var centerX = request.CenterX ?? nodesToMerge.Average(n => n.X);
|
||||
var centerY = request.CenterY ?? nodesToMerge.Average(n => n.Y);
|
||||
|
||||
// Get all edges connected to these nodes
|
||||
var connectedEdges = await _context.Edges
|
||||
.Where(e => e.LevelId == request.LevelId &&
|
||||
(request.NodeIds.Contains(e.StartNodeId) || request.NodeIds.Contains(e.EndNodeId)))
|
||||
.Include(e => e.VehicleProperties)
|
||||
.ToListAsync();
|
||||
|
||||
// Start transaction
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
// Create merged node
|
||||
var nodeId = Guid.NewGuid().ToString("N")[..16];
|
||||
var nodeName = await _namingService.GenerateNodeNameAsync(request.LevelId);
|
||||
|
||||
var mergedNode = new Node
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
LevelId = request.LevelId,
|
||||
NodeId = nodeId,
|
||||
NodeName = nodeName,
|
||||
X = centerX,
|
||||
Y = centerY,
|
||||
NodeDescription = $"Merged from {nodesToMerge.Count} nodes"
|
||||
};
|
||||
|
||||
_context.Nodes.Add(mergedNode);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Merge vehicle properties from all nodes
|
||||
var allVehicleProperties = nodesToMerge
|
||||
.SelectMany(n => n.VehicleProperties)
|
||||
.GroupBy(vp => vp.VehicleTypeId)
|
||||
.Select(g => g.First()) // Take first property for each vehicle type (or merge logic can be enhanced)
|
||||
.ToList();
|
||||
|
||||
foreach (var vp in allVehicleProperties)
|
||||
{
|
||||
var newVp = new NodeVehicleProperty
|
||||
{
|
||||
NodeId = mergedNode.Id,
|
||||
VehicleTypeId = vp.VehicleTypeId,
|
||||
Theta = vp.Theta,
|
||||
Actions = vp.Actions,
|
||||
AllowedDeviationXY = vp.AllowedDeviationXY,
|
||||
AllowedDeviationTheta = vp.AllowedDeviationTheta
|
||||
};
|
||||
_context.NodeVehicleProperties.Add(newVp);
|
||||
}
|
||||
|
||||
// Update edges: change StartNodeId or EndNodeId to merged node
|
||||
var updatedEdges = new List<Edge>();
|
||||
foreach (var edge in connectedEdges)
|
||||
{
|
||||
var wasStartNode = request.NodeIds.Contains(edge.StartNodeId);
|
||||
var wasEndNode = request.NodeIds.Contains(edge.EndNodeId);
|
||||
|
||||
if (wasStartNode && wasEndNode)
|
||||
{
|
||||
// Both nodes are being merged - this becomes a self-loop, delete it
|
||||
_context.Edges.Remove(edge);
|
||||
}
|
||||
else if (wasStartNode)
|
||||
{
|
||||
edge.StartNodeId = mergedNode.Id;
|
||||
updatedEdges.Add(edge);
|
||||
}
|
||||
else if (wasEndNode)
|
||||
{
|
||||
edge.EndNodeId = mergedNode.Id;
|
||||
updatedEdges.Add(edge);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle station: if one node had station, assign to merged node
|
||||
if (nodesWithStations.Count == 1)
|
||||
{
|
||||
var nodeWithStation = nodesWithStations[0];
|
||||
var stationInteractions = nodeWithStation.StationInteractions.ToList();
|
||||
|
||||
foreach (var sin in stationInteractions)
|
||||
{
|
||||
// Update StationInteractionNode to point to merged node
|
||||
sin.NodeId = mergedNode.Id;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete old nodes (they will be orphaned after edge updates)
|
||||
_context.Nodes.RemoveRange(nodesToMerge);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
|
||||
// Reload merged node with all properties for response
|
||||
var reloadedMergedNode = await _context.Nodes
|
||||
.Where(n => n.Id == mergedNode.Id)
|
||||
.Include(n => n.VehicleProperties)
|
||||
.ThenInclude(vp => vp.VehicleType)
|
||||
.AsSplitQuery()
|
||||
.FirstAsync();
|
||||
|
||||
// Reload updated edges for response
|
||||
var reloadedEdges = await _context.Edges
|
||||
.Where(e => updatedEdges.Select(ue => ue.Id).Contains(e.Id))
|
||||
.Include(e => e.StartNode)
|
||||
.Include(e => e.EndNode)
|
||||
.Include(e => e.VehicleProperties)
|
||||
.ThenInclude(vp => vp.VehicleType)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
return new MergeNodesResponse
|
||||
{
|
||||
MergedNode = MapNodeToDto(reloadedMergedNode),
|
||||
UpdatedEdges = [.. reloadedEdges.Select(MapEdgeToDto)],
|
||||
DeletedNodeIds = [.. nodesToMerge.Select(n => n.Id)]
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SplitNodeResponse> SplitNodeAsync(SplitNodeRequest request)
|
||||
{
|
||||
// Load node with all related data
|
||||
var nodeToSplit = await _context.Nodes
|
||||
.Where(n => n.Id == request.NodeId && n.LevelId == request.LevelId)
|
||||
.Include(n => n.VehicleProperties)
|
||||
.Include(n => n.StationInteractions)
|
||||
.AsSplitQuery()
|
||||
.FirstOrDefaultAsync() ?? throw new InvalidOperationException($"Node with ID '{request.NodeId}' not found");
|
||||
|
||||
// Get all edges connected to this node
|
||||
var connectedEdges = await _context.Edges
|
||||
.Where(e => e.LevelId == request.LevelId &&
|
||||
(e.StartNodeId == request.NodeId || e.EndNodeId == request.NodeId))
|
||||
.Include(e => e.VehicleProperties)
|
||||
.Include(e => e.StartNode)
|
||||
.Include(e => e.EndNode)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
// Validate: node must have at least 2 edges
|
||||
if (connectedEdges.Count < 2)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot split node: Node must have at least 2 connected edges. " +
|
||||
$"Found {connectedEdges.Count} edge(s).");
|
||||
}
|
||||
|
||||
var offsetDistance = request.OffsetDistance ?? 0.1; // Default 10cm
|
||||
|
||||
// Start transaction
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
var newNodes = new List<Node>();
|
||||
var updatedEdges = new List<Edge>();
|
||||
|
||||
// Create a new node for each edge
|
||||
foreach (var (edge, index) in connectedEdges.Select((e, i) => (e, i)))
|
||||
{
|
||||
// Calculate offset position (perpendicular to edge direction)
|
||||
var otherNodeId = edge.StartNodeId == request.NodeId ? edge.EndNodeId : edge.StartNodeId;
|
||||
var otherNode = edge.StartNodeId == request.NodeId ? edge.EndNode : edge.StartNode;
|
||||
|
||||
double offsetX, offsetY;
|
||||
if (otherNode != null)
|
||||
{
|
||||
// Calculate perpendicular offset
|
||||
var dx = otherNode.X - nodeToSplit.X;
|
||||
var dy = otherNode.Y - nodeToSplit.Y;
|
||||
var length = Math.Sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (length > 0.001)
|
||||
{
|
||||
// Perpendicular vector (rotate 90 degrees counter-clockwise)
|
||||
var perpX = -dy / length * offsetDistance;
|
||||
var perpY = dx / length * offsetDistance;
|
||||
offsetX = nodeToSplit.X + perpX;
|
||||
offsetY = nodeToSplit.Y + perpY;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: circular offset
|
||||
var angle = (2 * Math.PI * index) / connectedEdges.Count;
|
||||
offsetX = nodeToSplit.X + offsetDistance * Math.Cos(angle);
|
||||
offsetY = nodeToSplit.Y + offsetDistance * Math.Sin(angle);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Circular offset
|
||||
var angle = (2 * Math.PI * index) / connectedEdges.Count;
|
||||
offsetX = nodeToSplit.X + offsetDistance * Math.Cos(angle);
|
||||
offsetY = nodeToSplit.Y + offsetDistance * Math.Sin(angle);
|
||||
}
|
||||
|
||||
// Create new node
|
||||
var newNodeId = Guid.NewGuid().ToString("N")[..16];
|
||||
var newNodeName = await _namingService.GenerateNodeNameAsync(request.LevelId);
|
||||
|
||||
var newNode = new Node
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
LevelId = request.LevelId,
|
||||
NodeId = newNodeId,
|
||||
NodeName = newNodeName,
|
||||
X = offsetX,
|
||||
Y = offsetY,
|
||||
NodeDescription = $"Split from node {nodeToSplit.NodeId}"
|
||||
};
|
||||
|
||||
_context.Nodes.Add(newNode);
|
||||
await _context.SaveChangesAsync(); // Save to get ID
|
||||
|
||||
// Copy vehicle properties from original node
|
||||
foreach (var vp in nodeToSplit.VehicleProperties)
|
||||
{
|
||||
var newVp = new NodeVehicleProperty
|
||||
{
|
||||
NodeId = newNode.Id,
|
||||
VehicleTypeId = vp.VehicleTypeId,
|
||||
Theta = vp.Theta,
|
||||
Actions = vp.Actions,
|
||||
AllowedDeviationXY = vp.AllowedDeviationXY,
|
||||
AllowedDeviationTheta = vp.AllowedDeviationTheta
|
||||
};
|
||||
_context.NodeVehicleProperties.Add(newVp);
|
||||
}
|
||||
|
||||
newNodes.Add(newNode);
|
||||
|
||||
// Update edge to point to new node
|
||||
if (edge.StartNodeId == request.NodeId)
|
||||
{
|
||||
edge.StartNodeId = newNode.Id;
|
||||
}
|
||||
else
|
||||
{
|
||||
edge.EndNodeId = newNode.Id;
|
||||
}
|
||||
updatedEdges.Add(edge);
|
||||
}
|
||||
|
||||
// Handle station: assign to specified node or first node
|
||||
if (nodeToSplit.StationInteractions.Count != 0)
|
||||
{
|
||||
var targetNodeId = request.StationNodeId ?? newNodes[0].Id;
|
||||
var targetNode = newNodes.FirstOrDefault(n => n.Id == targetNodeId) ?? throw new InvalidOperationException($"Target node ID '{request.StationNodeId}' not found in new nodes");
|
||||
var stationInteractions = nodeToSplit.StationInteractions.ToList();
|
||||
foreach (var sin in stationInteractions)
|
||||
{
|
||||
sin.NodeId = targetNode.Id;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete original node
|
||||
_context.Nodes.Remove(nodeToSplit);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
|
||||
// Reload new nodes with all properties for response
|
||||
var reloadedNewNodes = await _context.Nodes
|
||||
.Where(n => newNodes.Select(nn => nn.Id).Contains(n.Id))
|
||||
.Include(n => n.VehicleProperties)
|
||||
.ThenInclude(vp => vp.VehicleType)
|
||||
.ToListAsync();
|
||||
|
||||
// Reload updated edges for response
|
||||
var reloadedEdges = await _context.Edges
|
||||
.Where(e => updatedEdges.Select(ue => ue.Id).Contains(e.Id))
|
||||
.Include(e => e.StartNode)
|
||||
.Include(e => e.EndNode)
|
||||
.Include(e => e.VehicleProperties)
|
||||
.ThenInclude(vp => vp.VehicleType)
|
||||
.ToListAsync();
|
||||
|
||||
return new SplitNodeResponse
|
||||
{
|
||||
NewNodes = [.. reloadedNewNodes.Select(MapNodeToDto)],
|
||||
UpdatedEdges = [.. reloadedEdges.Select(MapEdgeToDto)],
|
||||
DeletedNodeId = nodeToSplit.Id
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SaveLayoutDataResponse> SaveLayoutDataAsync(SaveLayoutDataRequest request)
|
||||
{
|
||||
var response = new SaveLayoutDataResponse
|
||||
{
|
||||
Success = true,
|
||||
NodesUpdated = 0,
|
||||
EdgesUpdated = 0
|
||||
};
|
||||
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
// Batch load all needed nodes in ONE query
|
||||
var nodeIds = request.Nodes.Select(n => n.Id).ToList();
|
||||
var existingNodes = await _context.Nodes
|
||||
.Where(n => nodeIds.Contains(n.Id) && n.LevelId == request.LayoutLevelId)
|
||||
.ToDictionaryAsync(n => n.Id);
|
||||
|
||||
// Batch load all node vehicle properties in ONE query
|
||||
var existingNodeVehicleProps = await _context.NodeVehicleProperties
|
||||
.Where(nvp => nodeIds.Contains(nvp.NodeId))
|
||||
.ToListAsync();
|
||||
var nodeVehiclePropsLookup = existingNodeVehicleProps.GroupBy(p => p.NodeId)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
// Update nodes
|
||||
foreach (var nodeItem in request.Nodes)
|
||||
{
|
||||
if (!existingNodes.TryGetValue(nodeItem.Id, out var node))
|
||||
{
|
||||
// Node not found - skip (Option D: Force Overwrite)
|
||||
response.SkippedNodeIds.Add(nodeItem.Id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update position if provided
|
||||
if (nodeItem.X.HasValue)
|
||||
node.X = nodeItem.X.Value;
|
||||
if (nodeItem.Y.HasValue)
|
||||
node.Y = nodeItem.Y.Value;
|
||||
|
||||
// Update other properties
|
||||
if (nodeItem.NodeName != null)
|
||||
node.NodeName = nodeItem.NodeName;
|
||||
if (nodeItem.NodeDescription != null)
|
||||
node.NodeDescription = nodeItem.NodeDescription;
|
||||
if (nodeItem.MapId != null)
|
||||
node.MapId = nodeItem.MapId;
|
||||
|
||||
// Update vehicle properties if provided
|
||||
if (nodeItem.VehicleProperties != null)
|
||||
{
|
||||
// Remove existing properties
|
||||
if (nodeVehiclePropsLookup.TryGetValue(nodeItem.Id, out var existingProps))
|
||||
{
|
||||
_context.NodeVehicleProperties.RemoveRange(existingProps);
|
||||
}
|
||||
|
||||
// Add new properties
|
||||
foreach (var propDto in nodeItem.VehicleProperties)
|
||||
{
|
||||
var prop = new NodeVehicleProperty
|
||||
{
|
||||
NodeId = nodeItem.Id,
|
||||
VehicleTypeId = propDto.VehicleTypeId,
|
||||
Theta = propDto.Theta,
|
||||
Actions = propDto.Actions,
|
||||
AllowedDeviationXY = propDto.AllowedDeviationXY,
|
||||
AllowedDeviationTheta = propDto.AllowedDeviationTheta
|
||||
};
|
||||
_context.NodeVehicleProperties.Add(prop);
|
||||
}
|
||||
}
|
||||
|
||||
response.NodesUpdated++;
|
||||
}
|
||||
|
||||
// Batch load all needed edges in ONE query
|
||||
var edgeIds = request.Edges.Select(e => e.Id).ToList();
|
||||
var existingEdges = await _context.Edges
|
||||
.Where(e => edgeIds.Contains(e.Id) && e.LevelId == request.LayoutLevelId)
|
||||
.ToDictionaryAsync(e => e.Id);
|
||||
|
||||
// Batch load all edge vehicle properties in ONE query
|
||||
var existingEdgeVehicleProps = await _context.EdgeVehicleProperties
|
||||
.Where(evp => edgeIds.Contains(evp.EdgeId))
|
||||
.ToListAsync();
|
||||
var edgeVehiclePropsLookup = existingEdgeVehicleProps.GroupBy(p => p.EdgeId)
|
||||
.ToDictionary(g => g.Key, g => g.ToList());
|
||||
|
||||
// Update edges
|
||||
foreach (var edgeItem in request.Edges)
|
||||
{
|
||||
if (!existingEdges.TryGetValue(edgeItem.Id, out var edge))
|
||||
{
|
||||
// Edge not found - skip (Option D: Force Overwrite)
|
||||
response.SkippedEdgeIds.Add(edgeItem.Id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Update properties
|
||||
if (edgeItem.EdgeName != null)
|
||||
edge.EdgeName = edgeItem.EdgeName;
|
||||
if (edgeItem.EdgeDescription != null)
|
||||
edge.EdgeDescription = edgeItem.EdgeDescription;
|
||||
|
||||
// Update vehicle properties if provided
|
||||
if (edgeItem.VehicleProperties != null)
|
||||
{
|
||||
// Remove existing properties (from batch-loaded lookup)
|
||||
if (edgeVehiclePropsLookup.TryGetValue(edgeItem.Id, out var existingProps))
|
||||
{
|
||||
_context.EdgeVehicleProperties.RemoveRange(existingProps);
|
||||
}
|
||||
|
||||
// Add new properties
|
||||
foreach (var propDto in edgeItem.VehicleProperties)
|
||||
{
|
||||
var prop = new EdgeVehicleProperty
|
||||
{
|
||||
EdgeId = edgeItem.Id,
|
||||
VehicleTypeId = propDto.VehicleTypeId,
|
||||
VehicleOrientation = propDto.VehicleOrientation,
|
||||
OrientationType = propDto.OrientationType,
|
||||
RotationAllowed = propDto.RotationAllowed,
|
||||
RotationAtStartNodeAllowed = propDto.RotationAtStartNodeAllowed,
|
||||
RotationAtEndNodeAllowed = propDto.RotationAtEndNodeAllowed,
|
||||
MaxSpeed = propDto.MaxSpeed,
|
||||
MaxRotationSpeed = propDto.MaxRotationSpeed,
|
||||
MinHeight = propDto.MinHeight,
|
||||
MaxHeight = propDto.MaxHeight,
|
||||
LoadRestriction_Unloaded = propDto.LoadRestriction?.Unloaded,
|
||||
LoadRestriction_Loaded = propDto.LoadRestriction?.Loaded,
|
||||
LoadRestriction_LoadSetNames = propDto.LoadRestriction?.LoadSetNames != null && propDto.LoadRestriction.LoadSetNames.Count > 0
|
||||
? System.Text.Json.JsonSerializer.Serialize(propDto.LoadRestriction.LoadSetNames)
|
||||
: null,
|
||||
TrajectoryDegree = propDto.TrajectoryDegree,
|
||||
TrajectoryControlPoint1X = propDto.TrajectoryControlPoint1X,
|
||||
TrajectoryControlPoint1Y = propDto.TrajectoryControlPoint1Y,
|
||||
TrajectoryControlPoint2X = propDto.TrajectoryControlPoint2X,
|
||||
TrajectoryControlPoint2Y = propDto.TrajectoryControlPoint2Y,
|
||||
CorridorLeftWidth = propDto.CorridorLeftWidth,
|
||||
CorridorRightWidth = propDto.CorridorRightWidth,
|
||||
CorridorRefPoint = propDto.CorridorRefPoint
|
||||
};
|
||||
_context.EdgeVehicleProperties.Add(prop);
|
||||
}
|
||||
}
|
||||
|
||||
response.EdgesUpdated++;
|
||||
}
|
||||
|
||||
// Save all changes in transaction
|
||||
await _context.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
response.Success = false;
|
||||
response.ErrorMessage = ex.Message;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CopyNodesResponse> CopyNodesAsync(CopyNodesRequest request)
|
||||
{
|
||||
var response = new CopyNodesResponse
|
||||
{
|
||||
Success = true
|
||||
};
|
||||
|
||||
using var transaction = await _context.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
// Load source nodes and edges from database
|
||||
var sourceNodes = await _context.Nodes
|
||||
.Where(n => request.NodeIds.Contains(n.Id) && n.LevelId == request.LayoutLevelId)
|
||||
.Include(n => n.VehicleProperties)
|
||||
.ThenInclude(vp => vp.VehicleType)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
var sourceEdges = await _context.Edges
|
||||
.Where(e => request.EdgeIds.Contains(e.Id) && e.LevelId == request.LayoutLevelId)
|
||||
.Include(e => e.VehicleProperties)
|
||||
.ThenInclude(vp => vp.VehicleType)
|
||||
.AsSplitQuery()
|
||||
.ToListAsync();
|
||||
|
||||
if (sourceNodes.Count == 0)
|
||||
{
|
||||
response.Success = false;
|
||||
response.ErrorMessage = "No nodes found to copy";
|
||||
return response;
|
||||
}
|
||||
|
||||
// Get editor settings for validation
|
||||
var settings = await _context.LayoutLevelEditorSettings
|
||||
.FirstOrDefaultAsync(s => s.LevelId == request.LayoutLevelId);
|
||||
|
||||
// Step 1: Create all new nodes with offset
|
||||
var nodeIdMapping = new Dictionary<Guid, Guid>();
|
||||
|
||||
foreach (var sourceNode in sourceNodes)
|
||||
{
|
||||
var newX = sourceNode.X + request.OffsetX;
|
||||
var newY = sourceNode.Y + request.OffsetY;
|
||||
|
||||
// Validate coordinates (same validation as in CreateEdge)
|
||||
if (!await _nodeService.ValidateCoordinatesAsync(request.LayoutLevelId, newX, newY))
|
||||
{
|
||||
throw new InvalidOperationException($"Coordinates ({newX}, {newY}) are outside valid bounds");
|
||||
}
|
||||
|
||||
// Generate NodeId and NodeName (same as in FindOrCreateNodeAsync)
|
||||
var nodeId = Guid.NewGuid().ToString("N")[..16];
|
||||
var nodeName = sourceNode.NodeName;
|
||||
if (string.IsNullOrEmpty(nodeName) && settings?.NodeNameAutoGenerate == true)
|
||||
{
|
||||
nodeName = await _namingService.GenerateNodeNameAsync(request.LayoutLevelId);
|
||||
}
|
||||
|
||||
// Create new node directly
|
||||
var newNode = new Node
|
||||
{
|
||||
LevelId = request.LayoutLevelId,
|
||||
NodeId = nodeId,
|
||||
NodeName = nodeName,
|
||||
NodeDescription = sourceNode.NodeDescription,
|
||||
MapId = sourceNode.MapId,
|
||||
X = newX,
|
||||
Y = newY
|
||||
};
|
||||
|
||||
_context.Nodes.Add(newNode);
|
||||
await _context.SaveChangesAsync(); // Save to get the new node's Id
|
||||
|
||||
// Copy vehicle properties
|
||||
if (sourceNode.VehicleProperties != null && sourceNode.VehicleProperties.Count > 0)
|
||||
{
|
||||
foreach (var sourceProp in sourceNode.VehicleProperties)
|
||||
{
|
||||
var newProp = new NodeVehicleProperty
|
||||
{
|
||||
NodeId = newNode.Id,
|
||||
VehicleTypeId = sourceProp.VehicleTypeId,
|
||||
Theta = sourceProp.Theta,
|
||||
Actions = sourceProp.Actions,
|
||||
AllowedDeviationXY = sourceProp.AllowedDeviationXY,
|
||||
AllowedDeviationTheta = sourceProp.AllowedDeviationTheta
|
||||
};
|
||||
_context.NodeVehicleProperties.Add(newProp);
|
||||
}
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// Store mapping
|
||||
nodeIdMapping[sourceNode.Id] = newNode.Id;
|
||||
|
||||
// Reload node with vehicle properties for response
|
||||
var reloadedNode = await _nodeService.GetByIdAsync(newNode.Id, includeVehicleProperties: true);
|
||||
if (reloadedNode != null)
|
||||
{
|
||||
response.NewNodes.Add(MapNodeToDto(reloadedNode));
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Create all new edges using the node ID mapping
|
||||
var processedEdges = new HashSet<Guid>();
|
||||
|
||||
foreach (var sourceEdge in sourceEdges)
|
||||
{
|
||||
// Skip if already processed
|
||||
if (processedEdges.Contains(sourceEdge.Id))
|
||||
continue;
|
||||
|
||||
// Only copy edges where both start and end nodes are in the selection
|
||||
if (nodeIdMapping.TryGetValue(sourceEdge.StartNodeId, out var newStartNodeId) &&
|
||||
nodeIdMapping.TryGetValue(sourceEdge.EndNodeId, out var newEndNodeId))
|
||||
{
|
||||
// Get new nodes to calculate edge length for validation
|
||||
var newStartNode = await _nodeService.GetByIdAsync(newStartNodeId, includeVehicleProperties: false);
|
||||
var newEndNode = await _nodeService.GetByIdAsync(newEndNodeId, includeVehicleProperties: false);
|
||||
|
||||
if (newStartNode != null && newEndNode != null)
|
||||
{
|
||||
// Validate edge length (same validation as in CreateEdge)
|
||||
var dx = newEndNode.X - newStartNode.X;
|
||||
var dy = newEndNode.Y - newStartNode.Y;
|
||||
var edgeLength = Math.Sqrt(dx * dx + dy * dy);
|
||||
var minEdgeLength = settings?.EdgeMinLengthCreate ?? 0.1;
|
||||
|
||||
if (edgeLength < minEdgeLength)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Edge length ({edgeLength:F3}m) is less than minimum required ({minEdgeLength:F3}m)");
|
||||
}
|
||||
|
||||
// Check if this edge has a reverse edge (2-way edge)
|
||||
var reverseEdge = sourceEdges.FirstOrDefault(e =>
|
||||
e.Id != sourceEdge.Id &&
|
||||
e.StartNodeId == sourceEdge.EndNodeId &&
|
||||
e.EndNodeId == sourceEdge.StartNodeId);
|
||||
|
||||
// Copy the forward edge
|
||||
var edgeId = Guid.NewGuid().ToString("N")[..16];
|
||||
var edgeName = sourceEdge.EdgeName;
|
||||
if (string.IsNullOrEmpty(edgeName) && settings?.EdgeNameAutoGenerate == true)
|
||||
{
|
||||
edgeName = await _namingService.GenerateEdgeNameAsync(request.LayoutLevelId);
|
||||
}
|
||||
|
||||
var newEdge = new Edge
|
||||
{
|
||||
LevelId = request.LayoutLevelId,
|
||||
EdgeId = edgeId,
|
||||
EdgeName = edgeName,
|
||||
EdgeDescription = sourceEdge.EdgeDescription,
|
||||
StartNodeId = newStartNodeId,
|
||||
EndNodeId = newEndNodeId
|
||||
};
|
||||
|
||||
_context.Edges.Add(newEdge);
|
||||
await _context.SaveChangesAsync(); // Save to get the new edge's Id
|
||||
|
||||
// Copy vehicle properties
|
||||
if (sourceEdge.VehicleProperties != null && sourceEdge.VehicleProperties.Count > 0)
|
||||
{
|
||||
foreach (var sourceProp in sourceEdge.VehicleProperties)
|
||||
{
|
||||
var newProp = new EdgeVehicleProperty
|
||||
{
|
||||
EdgeId = newEdge.Id,
|
||||
VehicleTypeId = sourceProp.VehicleTypeId,
|
||||
VehicleOrientation = sourceProp.VehicleOrientation,
|
||||
OrientationType = sourceProp.OrientationType,
|
||||
RotationAllowed = sourceProp.RotationAllowed,
|
||||
RotationAtStartNodeAllowed = sourceProp.RotationAtStartNodeAllowed,
|
||||
RotationAtEndNodeAllowed = sourceProp.RotationAtEndNodeAllowed,
|
||||
MaxSpeed = sourceProp.MaxSpeed,
|
||||
MaxRotationSpeed = sourceProp.MaxRotationSpeed,
|
||||
MinHeight = sourceProp.MinHeight,
|
||||
MaxHeight = sourceProp.MaxHeight,
|
||||
LoadRestriction_Unloaded = sourceProp.LoadRestriction_Unloaded,
|
||||
LoadRestriction_Loaded = sourceProp.LoadRestriction_Loaded,
|
||||
LoadRestriction_LoadSetNames = sourceProp.LoadRestriction_LoadSetNames,
|
||||
TrajectoryDegree = sourceProp.TrajectoryDegree,
|
||||
TrajectoryControlPoint1X = sourceProp.TrajectoryControlPoint1X + request.OffsetX,
|
||||
TrajectoryControlPoint1Y = sourceProp.TrajectoryControlPoint1Y + request.OffsetY,
|
||||
TrajectoryControlPoint2X = sourceProp.TrajectoryControlPoint2X + request.OffsetX,
|
||||
TrajectoryControlPoint2Y = sourceProp.TrajectoryControlPoint2Y + request.OffsetY,
|
||||
CorridorLeftWidth = sourceProp.CorridorLeftWidth,
|
||||
CorridorRightWidth = sourceProp.CorridorRightWidth,
|
||||
CorridorRefPoint = sourceProp.CorridorRefPoint,
|
||||
};
|
||||
_context.EdgeVehicleProperties.Add(newProp);
|
||||
}
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// Reload edge with full details for response
|
||||
var reloadedEdge = await _edgeService.GetByIdAsync(newEdge.Id, includeNodes: true, includeVehicleProperties: true);
|
||||
if (reloadedEdge != null)
|
||||
{
|
||||
response.NewEdges.Add(MapEdgeToDto(reloadedEdge));
|
||||
}
|
||||
processedEdges.Add(sourceEdge.Id);
|
||||
|
||||
// If it's a 2-way edge, copy the reverse edge too
|
||||
if (reverseEdge != null && !processedEdges.Contains(reverseEdge.Id))
|
||||
{
|
||||
var reverseEdgeId = Guid.NewGuid().ToString("N")[..16];
|
||||
var reverseEdgeName = reverseEdge.EdgeName;
|
||||
if (string.IsNullOrEmpty(reverseEdgeName) && settings?.EdgeNameAutoGenerate == true)
|
||||
{
|
||||
reverseEdgeName = await _namingService.GenerateEdgeNameAsync(request.LayoutLevelId);
|
||||
}
|
||||
|
||||
var newReverseEdge = new Edge
|
||||
{
|
||||
LevelId = request.LayoutLevelId,
|
||||
EdgeId = reverseEdgeId,
|
||||
EdgeName = reverseEdgeName,
|
||||
EdgeDescription = reverseEdge.EdgeDescription,
|
||||
StartNodeId = newEndNodeId,
|
||||
EndNodeId = newStartNodeId
|
||||
};
|
||||
|
||||
_context.Edges.Add(newReverseEdge);
|
||||
await _context.SaveChangesAsync(); // Save to get the new edge's Id
|
||||
|
||||
// Copy vehicle properties for reverse edge
|
||||
if (reverseEdge.VehicleProperties != null && reverseEdge.VehicleProperties.Count > 0)
|
||||
{
|
||||
foreach (var sourceProp in reverseEdge.VehicleProperties)
|
||||
{
|
||||
var newProp = new EdgeVehicleProperty
|
||||
{
|
||||
EdgeId = newReverseEdge.Id,
|
||||
VehicleTypeId = sourceProp.VehicleTypeId,
|
||||
VehicleOrientation = sourceProp.VehicleOrientation,
|
||||
OrientationType = sourceProp.OrientationType,
|
||||
RotationAllowed = sourceProp.RotationAllowed,
|
||||
RotationAtStartNodeAllowed = sourceProp.RotationAtStartNodeAllowed,
|
||||
RotationAtEndNodeAllowed = sourceProp.RotationAtEndNodeAllowed,
|
||||
MaxSpeed = sourceProp.MaxSpeed,
|
||||
MaxRotationSpeed = sourceProp.MaxRotationSpeed,
|
||||
MinHeight = sourceProp.MinHeight,
|
||||
MaxHeight = sourceProp.MaxHeight,
|
||||
LoadRestriction_Unloaded = sourceProp.LoadRestriction_Unloaded,
|
||||
LoadRestriction_Loaded = sourceProp.LoadRestriction_Loaded,
|
||||
LoadRestriction_LoadSetNames = sourceProp.LoadRestriction_LoadSetNames,
|
||||
TrajectoryDegree = sourceProp.TrajectoryDegree,
|
||||
TrajectoryControlPoint1X = sourceProp.TrajectoryControlPoint1X + request.OffsetX,
|
||||
TrajectoryControlPoint1Y = sourceProp.TrajectoryControlPoint1Y + request.OffsetY,
|
||||
TrajectoryControlPoint2X = sourceProp.TrajectoryControlPoint2X + request.OffsetX,
|
||||
TrajectoryControlPoint2Y = sourceProp.TrajectoryControlPoint2Y + request.OffsetY,
|
||||
CorridorLeftWidth = sourceProp.CorridorLeftWidth,
|
||||
CorridorRightWidth = sourceProp.CorridorRightWidth,
|
||||
CorridorRefPoint = sourceProp.CorridorRefPoint,
|
||||
};
|
||||
_context.EdgeVehicleProperties.Add(newProp);
|
||||
}
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// Reload reverse edge with full details for response
|
||||
var reloadedReverseEdge = await _edgeService.GetByIdAsync(newReverseEdge.Id, includeNodes: true, includeVehicleProperties: true);
|
||||
if (reloadedReverseEdge != null)
|
||||
{
|
||||
response.NewEdges.Add(MapEdgeToDto(reloadedReverseEdge));
|
||||
}
|
||||
processedEdges.Add(reverseEdge.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set node ID mapping in response
|
||||
response.NodeIdMapping = nodeIdMapping;
|
||||
|
||||
await transaction.CommitAsync();
|
||||
return response;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
response.Success = false;
|
||||
response.ErrorMessage = ex.Message;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<string>? SafeDeserializeLoadSetNames(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json)) return null;
|
||||
try { return System.Text.Json.JsonSerializer.Deserialize<List<string>>(json); }
|
||||
catch (System.Text.Json.JsonException) { return null; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user