262 lines
9.8 KiB
C#
262 lines
9.8 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Controller for managing edges with complex node detection and cascade delete logic
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("api/edges")]
|
|
[Authorize]
|
|
public class EdgesController(
|
|
IEdgeService edgeService,
|
|
ILogger<EdgesController> logger) : ControllerBase
|
|
{
|
|
private readonly IEdgeService _edgeService = edgeService;
|
|
private readonly ILogger<EdgesController> _logger = logger;
|
|
|
|
/// <summary>
|
|
/// Get all edges for a layout level
|
|
/// </summary>
|
|
/// <param name="layoutLevelId">Layout level ID</param>
|
|
/// <returns>List of edges with nodes and vehicle properties</returns>
|
|
[HttpGet("level/{layoutLevelId}")]
|
|
[ProducesResponseType(typeof(List<EdgeDto>), 200)]
|
|
public async Task<ActionResult<List<EdgeDto>>> GetEdgesByLevel(Guid layoutLevelId)
|
|
{
|
|
var edges = await _edgeService.GetEdgesByLevelAsync(layoutLevelId, includeNodes: true, includeVehicleProperties: true);
|
|
var dtos = edges.Select(MapToDto).ToList();
|
|
|
|
return Ok(dtos);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get edge by ID
|
|
/// </summary>
|
|
/// <param name="edgeId">Edge database ID</param>
|
|
/// <returns>Edge details with nodes and vehicle properties</returns>
|
|
[HttpGet("{edgeId}")]
|
|
[ProducesResponseType(typeof(EdgeDto), 200)]
|
|
[ProducesResponseType(404)]
|
|
public async Task<ActionResult<EdgeDto>> 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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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
|
|
/// </summary>
|
|
/// <param name="request">Edge creation request with coordinates in METERS</param>
|
|
/// <returns>Created edge with connected nodes</returns>
|
|
[HttpPost]
|
|
[ProducesResponseType(typeof(EdgeDto), 201)]
|
|
[ProducesResponseType(400)]
|
|
public async Task<ActionResult<EdgeDto>> 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"));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update edge properties
|
|
/// </summary>
|
|
/// <param name="edgeId">Edge database ID</param>
|
|
/// <param name="request">Update request</param>
|
|
/// <returns>Updated edge</returns>
|
|
[HttpPut("{edgeId}")]
|
|
[ProducesResponseType(typeof(EdgeDto), 200)]
|
|
[ProducesResponseType(400)]
|
|
[ProducesResponseType(404)]
|
|
public async Task<ActionResult<EdgeDto>> 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"));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delete edge
|
|
/// Cascade deletes orphan nodes (nodes not connected to any other edge)
|
|
/// Also deletes StationInteractionNodes referencing orphan nodes
|
|
/// </summary>
|
|
/// <param name="edgeId">Edge database ID</param>
|
|
/// <returns>No content on success</returns>
|
|
[HttpDelete("{edgeId}")]
|
|
[ProducesResponseType(204)]
|
|
[ProducesResponseType(404)]
|
|
public async Task<IActionResult> 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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delete multiple edges in a transaction
|
|
/// All edges are deleted or none (transaction)
|
|
/// Cascade deletes orphan nodes and StationInteractionNodes
|
|
/// </summary>
|
|
/// <param name="request">Batch delete request with edge IDs</param>
|
|
/// <returns>No content on success</returns>
|
|
[HttpDelete("batch")]
|
|
[ProducesResponseType(204)]
|
|
[ProducesResponseType(400)]
|
|
public async Task<IActionResult> 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<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; }
|
|
}
|
|
|
|
private static ErrorResponseDto CreateErrorResponse(
|
|
string error,
|
|
string? errorCode = null,
|
|
Dictionary<string, object>? details = null)
|
|
{
|
|
return new ErrorResponseDto
|
|
{
|
|
Error = error,
|
|
ErrorCode = errorCode,
|
|
Details = details
|
|
};
|
|
}
|
|
}
|
|
|