using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using RobotNet10.MapEditor.Shared.DTOs.Edge;
using RobotNet10.MapEditor.Shared.DTOs.Node;
using RobotNet10.MapEditor.Shared.DTOs.Requests;
using RobotNet10.MapEditor.Shared.DTOs.Responses;
using RobotNet10.MapManager.Services;
using System.Text.Json;
namespace RobotNet10.MapManager.Controllers;
///
/// Controller for managing edges with complex node detection and cascade delete logic
///
[ApiController]
[Route("api/edges")]
[Authorize]
public class EdgesController(
IEdgeService edgeService,
ILogger logger) : ControllerBase
{
private readonly IEdgeService _edgeService = edgeService;
private readonly ILogger _logger = logger;
///
/// Get all edges for a layout level
///
/// Layout level ID
/// List of edges with nodes and vehicle properties
[HttpGet("level/{layoutLevelId}")]
[ProducesResponseType(typeof(List), 200)]
public async Task>> GetEdgesByLevel(Guid layoutLevelId)
{
var edges = await _edgeService.GetEdgesByLevelAsync(layoutLevelId, includeNodes: true, includeVehicleProperties: true);
var dtos = edges.Select(MapToDto).ToList();
return Ok(dtos);
}
///
/// Get edge by ID
///
/// Edge database ID
/// Edge details with nodes and vehicle properties
[HttpGet("{edgeId}")]
[ProducesResponseType(typeof(EdgeDto), 200)]
[ProducesResponseType(404)]
public async Task> GetEdge(Guid edgeId)
{
var edge = await _edgeService.GetByIdAsync(edgeId, includeNodes: true, includeVehicleProperties: true);
if (edge == null)
{
return NotFound(CreateErrorResponse($"Edge with ID '{edgeId}' not found", "EDGE_NOT_FOUND"));
}
return Ok(MapToDto(edge));
}
///
/// Create edge with automatic node detection/creation
/// If start/end point is within NodeProximityRadius (default 0.35m) of existing node, connect to that node
/// Otherwise, create new node at exact coordinates
///
/// Edge creation request with coordinates in METERS
/// Created edge with connected nodes
[HttpPost]
[ProducesResponseType(typeof(EdgeDto), 201)]
[ProducesResponseType(400)]
public async Task> CreateEdge([FromBody] CreateEdgeRequest request)
{
try
{
var edge = await _edgeService.CreateAsync(request);
var dto = MapToDto(edge);
return CreatedAtAction(
nameof(GetEdge),
new { edgeId = edge.Id },
dto);
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, "Failed to create edge");
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
}
}
///
/// Update edge properties
///
/// Edge database ID
/// Update request
/// Updated edge
[HttpPut("{edgeId}")]
[ProducesResponseType(typeof(EdgeDto), 200)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task> UpdateEdge(
Guid edgeId,
[FromBody] UpdateEdgeRequest request)
{
try
{
var edge = await _edgeService.UpdateAsync(edgeId, request);
return Ok(MapToDto(edge));
}
catch (InvalidOperationException ex)
{
if(_logger.IsEnabled(LogLevel.Warning))_logger.LogWarning(ex, "Failed to update edge: {edgeId}", edgeId);
if (ex.Message.Contains("not found"))
return NotFound(CreateErrorResponse(ex.Message, "EDGE_NOT_FOUND"));
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
}
}
///
/// Delete edge
/// Cascade deletes orphan nodes (nodes not connected to any other edge)
/// Also deletes StationInteractionNodes referencing orphan nodes
///
/// Edge database ID
/// No content on success
[HttpDelete("{edgeId}")]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public async Task DeleteEdge(Guid edgeId)
{
var deleted = await _edgeService.DeleteAsync(edgeId);
if (!deleted)
{
return NotFound(CreateErrorResponse($"Edge with ID '{edgeId}' not found", "EDGE_NOT_FOUND"));
}
return NoContent();
}
///
/// Delete multiple edges in a transaction
/// All edges are deleted or none (transaction)
/// Cascade deletes orphan nodes and StationInteractionNodes
///
/// Batch delete request with edge IDs
/// No content on success
[HttpDelete("batch")]
[ProducesResponseType(204)]
[ProducesResponseType(400)]
public async Task DeleteEdgesBatch([FromBody] DeleteEdgesRequest request)
{
if (request.EdgeIds == null || request.EdgeIds.Count == 0)
{
return BadRequest(CreateErrorResponse("No edge IDs provided", "VALIDATION_ERROR"));
}
try
{
await _edgeService.DeleteBatchAsync(request.EdgeIds);
return NoContent();
}
catch (InvalidOperationException ex)
{
_logger.LogWarning(ex, "Failed to batch delete edges");
return BadRequest(CreateErrorResponse(ex.Message, "VALIDATION_ERROR"));
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to batch delete edges");
return StatusCode(500, CreateErrorResponse("Internal server error while deleting edges", "INTERNAL_ERROR"));
}
}
// Helper method to map entity to DTO
private static EdgeDto MapToDto(Data.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 ? new NodeDto
{
Id = edge.StartNode.Id,
NodeId = edge.StartNode.NodeId,
NodeName = edge.StartNode.NodeName,
X = edge.StartNode.X,
Y = edge.StartNode.Y
} : null,
EndNode = edge.EndNode != null ? new NodeDto
{
Id = edge.EndNode.Id,
NodeId = edge.EndNode.NodeId,
NodeName = edge.EndNode.NodeName,
X = edge.EndNode.X,
Y = edge.EndNode.Y
} : 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 List? SafeDeserializeLoadSetNames(string? json)
{
if (string.IsNullOrWhiteSpace(json)) return null;
try { return System.Text.Json.JsonSerializer.Deserialize>(json); }
catch (System.Text.Json.JsonException) { return null; }
}
private static ErrorResponseDto CreateErrorResponse(
string error,
string? errorCode = null,
Dictionary? details = null)
{
return new ErrorResponseDto
{
Error = error,
ErrorCode = errorCode,
Details = details
};
}
}