Initial commit

This commit is contained in:
2026-07-13 09:25:40 +07:00
parent c08ff54676
commit bccfb156d7
1938 changed files with 641646 additions and 0 deletions

View File

@@ -0,0 +1,721 @@
using RobotNet.VDA5050;
using RobotNet10.MapEditor.Shared.DTOs.Edge;
using RobotNet10.MapEditor.Shared.DTOs.Layout;
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.MapEditor.Shared.DTOs.VehicleType;
using System.Globalization;
using System.Net.Http.Headers;
using System.Net.Http.Json;
namespace RobotNet10.MapEditor.Services.API;
/// <summary>
/// Service for communicating with MapManager REST API
/// </summary>
public class MapManagerApiService(HttpClient httpClient)
{
private readonly HttpClient _httpClient = httpClient;
private readonly string? _baseUrl = httpClient.BaseAddress?.AbsoluteUri;
// ==========================================
// LAYOUTS
// ==========================================
/// <summary>
/// Search layouts (returns nested Versions and Levels)
/// </summary>
public async Task<List<LayoutDto>> SearchLayoutsAsync(string? search = null)
{
var url = $"{_baseUrl}api/layouts";
if (!string.IsNullOrEmpty(search))
url += $"?search={Uri.EscapeDataString(search)}";
try
{
var response = await _httpClient.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
var errorMessage = await GetDetailedErrorMessageAsync(response, url);
throw new HttpRequestException(errorMessage);
}
return await response.Content.ReadFromJsonAsync<List<LayoutDto>>() ?? new();
}
catch (HttpRequestException)
{
throw; // Re-throw with detailed message
}
catch (Exception ex)
{
throw new HttpRequestException($"Failed to load layouts from {url}: {ex.Message}", ex);
}
}
/// <summary>
/// Get detailed error message from HTTP response
/// </summary>
private async Task<string> GetDetailedErrorMessageAsync(HttpResponseMessage response, string url)
{
var statusCode = response.StatusCode;
var statusText = response.ReasonPhrase ?? statusCode.ToString();
// Try to read error message from response body
string? errorDetail = null;
try
{
var content = await response.Content.ReadAsStringAsync();
if (!string.IsNullOrWhiteSpace(content) && content.Length < 500)
{
// Try to parse as JSON
try
{
using var doc = System.Text.Json.JsonDocument.Parse(content);
var root = doc.RootElement;
if (root.TryGetProperty("error", out var errorProp))
errorDetail = errorProp.GetString();
else if (root.TryGetProperty("message", out var messageProp))
errorDetail = messageProp.GetString();
else if (root.ValueKind == System.Text.Json.JsonValueKind.String)
errorDetail = root.GetString();
}
catch
{
// If not JSON, use content as-is if reasonable length
errorDetail = content;
}
}
}
catch
{
// Ignore errors reading response body
}
// Build detailed error message
var message = $"HTTP {(int)statusCode} {statusText}";
if (!string.IsNullOrWhiteSpace(errorDetail))
{
message += $": {errorDetail}";
}
message += $" (URL: {url})";
return message;
}
/// <summary>
/// Get layout by ID
/// </summary>
public async Task<LayoutDto?> GetLayoutAsync(Guid layoutId)
{
return await _httpClient.GetFromJsonAsync<LayoutDto>(
$"{_baseUrl}api/layouts/{layoutId}");
}
/// <summary>
/// Create new layout
/// </summary>
public async Task<LayoutDto> CreateLayoutAsync(CreateLayoutRequest request)
{
var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}api/layouts", request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<LayoutDto>()
?? throw new Exception("Failed to create layout");
}
/// <summary>
/// Update layout
/// </summary>
public async Task<LayoutDto> UpdateLayoutAsync(Guid layoutId, UpdateLayoutRequest request)
{
var response = await _httpClient.PutAsJsonAsync($"{_baseUrl}api/layouts/{layoutId}", request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<LayoutDto>()
?? throw new Exception("Failed to update layout");
}
/// <summary>
/// Delete layout
/// </summary>
public async Task DeleteLayoutAsync(Guid layoutId)
{
var response = await _httpClient.DeleteAsync($"{_baseUrl}api/layouts/{layoutId}");
response.EnsureSuccessStatusCode();
}
/// <summary>
/// Activate layout
/// </summary>
public async Task<LayoutDto> ActivateLayoutAsync(Guid layoutId)
{
var response = await _httpClient.PostAsync($"{_baseUrl}api/layouts/{layoutId}/activate", null);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<LayoutDto>()
?? throw new Exception("Failed to activate layout");
}
/// <summary>
/// Deactivate layout
/// </summary>
public async Task<LayoutDto> DeactivateLayoutAsync(Guid layoutId)
{
var response = await _httpClient.PostAsync($"{_baseUrl}api/layouts/{layoutId}/deactivate", null);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<LayoutDto>()
?? throw new Exception("Failed to deactivate layout");
}
// ==========================================
// VERSIONS
// ==========================================
/// <summary>
/// Create new version for a layout
/// </summary>
public async Task<LayoutVersionDto> CreateVersionAsync(Guid layoutId, CreateLayoutVersionRequest request)
{
var response = await _httpClient.PostAsJsonAsync(
$"{_baseUrl}api/layouts/{layoutId}/versions", request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<LayoutVersionDto>()
?? throw new Exception("Failed to create version");
}
/// <summary>
/// Get all versions for a layout
/// </summary>
public async Task<List<LayoutVersionDto>> GetVersionsAsync(Guid layoutId)
{
return await _httpClient.GetFromJsonAsync<List<LayoutVersionDto>>(
$"{_baseUrl}api/layouts/{layoutId}/versions") ?? new();
}
/// <summary>
/// Get version by ID
/// </summary>
public async Task<LayoutVersionDto?> GetVersionAsync(Guid versionId)
{
return await _httpClient.GetFromJsonAsync<LayoutVersionDto>(
$"{_baseUrl}api/layouts/versions/{versionId}");
}
/// <summary>
/// Delete version
/// </summary>
public async Task DeleteVersionAsync(Guid versionId)
{
var response = await _httpClient.DeleteAsync($"{_baseUrl}api/layouts/versions/{versionId}");
response.EnsureSuccessStatusCode();
}
// ==========================================
// LEVELS
// ==========================================
/// <summary>
/// Create new level for a version
/// </summary>
public async Task<LayoutLevelDto> CreateLevelAsync(Guid versionId, CreateLayoutLevelRequest request)
{
var response = await _httpClient.PostAsJsonAsync(
$"{_baseUrl}api/layouts/versions/{versionId}/levels", request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<LayoutLevelDto>()
?? throw new Exception("Failed to create level");
}
/// <summary>
/// Create a new level with background image in a single request
/// </summary>
/// <param name="versionId">Version ID</param>
/// <param name="layoutLevelId">Layout level identifier string</param>
/// <param name="levelOrder">Level order</param>
/// <param name="resolution">Resolution in meters per pixel</param>
/// <param name="originX">Origin X coordinate in meters</param>
/// <param name="originY">Origin Y coordinate in meters</param>
/// <param name="imageStream">Image stream (PNG format)</param>
/// <param name="fileName">Image file name</param>
/// <returns>Created layout level DTO</returns>
public async Task<LayoutLevelDto> CreateLevelWithImageAsync(
Guid versionId,
string layoutLevelId,
int levelOrder,
double resolution,
double originX,
double originY,
Stream imageStream,
string fileName)
{
using var content = new MultipartFormDataContent();
// Add form fields
content.Add(new StringContent(layoutLevelId), "layoutLevelId");
content.Add(new StringContent(levelOrder.ToString()), "levelOrder");
content.Add(new StringContent(resolution.ToString(CultureInfo.InvariantCulture)), "resolution");
content.Add(new StringContent(originX.ToString(CultureInfo.InvariantCulture)), "originX");
content.Add(new StringContent(originY.ToString(CultureInfo.InvariantCulture)), "originY");
// Add image file
var streamContent = new StreamContent(imageStream);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("image/png");
content.Add(streamContent, "file", fileName);
var response = await _httpClient.PostAsync(
$"{_baseUrl}api/layouts/versions/{versionId}/levels/with-image",
content);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<LayoutLevelDto>()
?? throw new Exception("Failed to create level with image");
}
/// <summary>
/// Get all levels for a version
/// </summary>
public async Task<List<LayoutLevelDto>> GetLevelsAsync(Guid versionId)
{
return await _httpClient.GetFromJsonAsync<List<LayoutLevelDto>>(
$"{_baseUrl}api/layouts/versions/{versionId}/levels") ?? new();
}
/// <summary>
/// Get level by ID
/// </summary>
public async Task<LayoutLevelDto?> GetLevelAsync(Guid levelId)
{
return await _httpClient.GetFromJsonAsync<LayoutLevelDto>(
$"{_baseUrl}api/layouts/levels/{levelId}");
}
/// <summary>
/// Update level
/// </summary>
public async Task<LayoutLevelDto> UpdateLevelAsync(Guid levelId, UpdateLayoutLevelRequest request)
{
var response = await _httpClient.PutAsJsonAsync($"{_baseUrl}api/layouts/levels/{levelId}", request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<LayoutLevelDto>()
?? throw new Exception("Failed to update level");
}
/// <summary>
/// Delete level
/// </summary>
public async Task DeleteLevelAsync(Guid levelId)
{
var response = await _httpClient.DeleteAsync($"{_baseUrl}api/layouts/levels/{levelId}");
response.EnsureSuccessStatusCode();
}
// ==========================================
// LAYOUT DATA (For Preview)
// ==========================================
/// <summary>
/// Get comprehensive layout data (nodes, edges, stations)
/// </summary>
public async Task<LayoutDataDto> GetLayoutDataAsync(Guid layoutLevelId)
{
return await _httpClient.GetFromJsonAsync<LayoutDataDto>(
$"{_baseUrl}api/layout-data/{layoutLevelId}") ?? new();
}
// ==========================================
// IMAGES
// ==========================================
/// <summary>
/// Get background image for a layout level
/// </summary>
public async Task<byte[]?> GetLayoutImageAsync(Guid layoutLevelId)
{
try
{
return await _httpClient.GetByteArrayAsync($"{_baseUrl}api/images/layout/{layoutLevelId}");
}
catch (HttpRequestException)
{
return null; // Image doesn't exist
}
}
/// <summary>
/// Upload background image for a layout level
/// </summary>
public async Task UploadLayoutImageAsync(Guid layoutLevelId, Stream imageStream, string fileName)
{
using var content = new MultipartFormDataContent();
var streamContent = new StreamContent(imageStream);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("image/png");
content.Add(streamContent, "file", fileName);
var response = await _httpClient.PostAsync(
$"{_baseUrl}api/images/layout/{layoutLevelId}", content);
response.EnsureSuccessStatusCode();
}
/// <summary>
/// Delete background image for a layout level
/// </summary>
public async Task DeleteLayoutImageAsync(Guid layoutLevelId)
{
var response = await _httpClient.DeleteAsync($"{_baseUrl}api/images/layout/{layoutLevelId}");
response.EnsureSuccessStatusCode();
}
// ==========================================
// NODES
// ==========================================
/// <summary>
/// Get node by ID
/// </summary>
public async Task<NodeDto?> GetNodeAsync(Guid nodeId)
{
return await _httpClient.GetFromJsonAsync<NodeDto>($"{_baseUrl}api/nodes/{nodeId}");
}
/// <summary>
/// Update node properties
/// </summary>
public async Task<NodeDto> UpdateNodeAsync(Guid nodeId, UpdateNodeRequest request)
{
var response = await _httpClient.PutAsJsonAsync($"{_baseUrl}api/nodes/{nodeId}", request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<NodeDto>()
?? throw new Exception("Failed to update node");
}
// ==========================================
// EDGES
// ==========================================
/// <summary>
/// Create a new edge with automatic node detection/creation
/// </summary>
public async Task<EdgeDto> CreateEdgeAsync(CreateEdgeRequest request)
{
var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}api/edges", request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<EdgeDto>()
?? throw new Exception("Failed to create edge");
}
/// <summary>
/// Get edge by ID
/// </summary>
public async Task<EdgeDto?> GetEdgeAsync(Guid edgeId)
{
return await _httpClient.GetFromJsonAsync<EdgeDto>($"{_baseUrl}api/edges/{edgeId}");
}
/// <summary>
/// Update edge properties
/// </summary>
public async Task<EdgeDto> UpdateEdgeAsync(Guid edgeId, UpdateEdgeRequest request)
{
var response = await _httpClient.PutAsJsonAsync($"{_baseUrl}api/edges/{edgeId}", request);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<EdgeDto>()
?? throw new Exception("Failed to update edge");
}
/// <summary>
/// Delete edge
/// </summary>
public async Task DeleteEdgeAsync(Guid edgeId)
{
var response = await _httpClient.DeleteAsync($"{_baseUrl}api/edges/{edgeId}");
response.EnsureSuccessStatusCode();
}
/// <summary>
/// Delete multiple edges in batch
/// </summary>
public async Task DeleteEdgesBatchAsync(List<Guid> edgeIds)
{
var request = new DeleteEdgesRequest { EdgeIds = edgeIds };
// Send DELETE request with body (non-standard but required by API)
var content = new StringContent(
System.Text.Json.JsonSerializer.Serialize(request, JsonOptionExtends.Write),
System.Text.Encoding.UTF8,
"application/json");
var deleteRequest = new HttpRequestMessage(HttpMethod.Delete, $"{_baseUrl}api/edges/batch")
{
Content = content
};
var response = await _httpClient.SendAsync(deleteRequest);
response.EnsureSuccessStatusCode();
}
// ==========================================
// MERGE/SPLIT OPERATIONS
// ==========================================
/// <summary>
/// Merge multiple nodes into one node
/// </summary>
public async Task<MergeNodesResponse> MergeNodesAsync(MergeNodesRequest request)
{
var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}api/layout-data/merge-nodes", request);
if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException($"Failed to merge nodes: {errorContent}");
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<MergeNodesResponse>()
?? throw new Exception("Failed to merge nodes");
}
/// <summary>
/// Split a node into multiple nodes
/// </summary>
public async Task<SplitNodeResponse> SplitNodeAsync(SplitNodeRequest request)
{
var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}api/layout-data/split-node", request);
if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException($"Failed to split node: {errorContent}");
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<SplitNodeResponse>()
?? throw new Exception("Failed to split node");
}
/// <summary>
/// Save all layout changes (nodes and edges) in a batch operation
/// </summary>
public async Task<SaveLayoutDataResponse> SaveLayoutDataAsync(SaveLayoutDataRequest request)
{
var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}api/layout-data/save", request);
if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException($"Failed to save layout data: {errorContent}");
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<SaveLayoutDataResponse>()
?? throw new Exception("Failed to save layout data");
}
/// <summary>
/// Copy selected nodes and edges with an offset
/// </summary>
public async Task<CopyNodesResponse> CopyNodesAsync(CopyNodesRequest request)
{
var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}api/layout-data/copy-nodes", request);
if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException($"Failed to copy nodes: {errorContent}");
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<CopyNodesResponse>()
?? throw new Exception("Failed to copy nodes");
}
// ==========================================
// VEHICLE TYPES
// ==========================================
/// <summary>
/// Get all vehicle types, optionally filtered by active status
/// </summary>
public async Task<List<VehicleTypeDto>> GetVehicleTypesAsync(bool? isActive = null)
{
var url = $"{_baseUrl}api/vehicles";
if (isActive.HasValue)
url += $"?isActive={isActive.Value}";
return await _httpClient.GetFromJsonAsync<List<VehicleTypeDto>>(url) ?? new();
}
/// <summary>
/// Get vehicle type by database ID
/// </summary>
public async Task<VehicleTypeDto?> GetVehicleTypeAsync(Guid id)
{
return await _httpClient.GetFromJsonAsync<VehicleTypeDto>($"{_baseUrl}api/vehicles/{id}");
}
/// <summary>
/// Get vehicle type by VehicleTypeId string
/// </summary>
public async Task<VehicleTypeDto?> GetVehicleTypeByStringIdAsync(string vehicleTypeId)
{
return await _httpClient.GetFromJsonAsync<VehicleTypeDto>(
$"{_baseUrl}api/vehicles/vehicleTypeId/{Uri.EscapeDataString(vehicleTypeId)}");
}
/// <summary>
/// Search vehicle types by query string
/// </summary>
public async Task<List<VehicleTypeDto>> SearchVehicleTypesAsync(string query)
{
if (string.IsNullOrWhiteSpace(query))
return new List<VehicleTypeDto>();
var url = $"{_baseUrl}api/vehicles/search?query={Uri.EscapeDataString(query)}";
return await _httpClient.GetFromJsonAsync<List<VehicleTypeDto>>(url) ?? new();
}
/// <summary>
/// Create a new vehicle type
/// </summary>
public async Task<VehicleTypeDto> CreateVehicleTypeAsync(CreateVehicleTypeRequest request)
{
var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}api/vehicles", request);
if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException($"Failed to create vehicle type: {errorContent}");
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<VehicleTypeDto>()
?? throw new Exception("Failed to create vehicle type");
}
/// <summary>
/// Update an existing vehicle type
/// </summary>
public async Task<VehicleTypeDto> UpdateVehicleTypeAsync(Guid id, UpdateVehicleTypeRequest request)
{
var response = await _httpClient.PutAsJsonAsync($"{_baseUrl}api/vehicles/{id}", request);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException($"Vehicle type not found: {errorContent}");
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<VehicleTypeDto>()
?? throw new Exception("Failed to update vehicle type");
}
/// <summary>
/// Delete a vehicle type
/// </summary>
public async Task DeleteVehicleTypeAsync(Guid id)
{
var response = await _httpClient.DeleteAsync($"{_baseUrl}api/vehicles/{id}");
if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException($"Cannot delete vehicle type: {errorContent}");
}
response.EnsureSuccessStatusCode();
}
/// <summary>
/// Get usage information for a vehicle type
/// </summary>
public async Task<VehicleTypeUsageInfoDto> GetVehicleTypeUsageAsync(Guid id)
{
return await _httpClient.GetFromJsonAsync<VehicleTypeUsageInfoDto>(
$"{_baseUrl}api/vehicles/{id}/usage")
?? throw new Exception("Failed to get vehicle type usage info");
}
// ==========================================
// STATIONS
// ==========================================
/// <summary>
/// Get all stations for a layout level
/// </summary>
public async Task<List<StationDto>> GetStationsByLevelAsync(Guid layoutLevelId)
{
return await _httpClient.GetFromJsonAsync<List<StationDto>>(
$"{_baseUrl}api/stations/level/{layoutLevelId}") ?? new();
}
/// <summary>
/// Get station by database ID
/// </summary>
public async Task<StationDto?> GetStationAsync(Guid stationId)
{
return await _httpClient.GetFromJsonAsync<StationDto>(
$"{_baseUrl}api/stations/{stationId}");
}
/// <summary>
/// Create a new station
/// </summary>
public async Task<StationDto> CreateStationAsync(CreateStationRequest request)
{
var response = await _httpClient.PostAsJsonAsync($"{_baseUrl}api/stations", request);
if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException($"Failed to create station: {errorContent}");
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<StationDto>()
?? throw new Exception("Failed to create station");
}
/// <summary>
/// Update an existing station
/// </summary>
public async Task<StationDto> UpdateStationAsync(Guid stationId, UpdateStationRequest request)
{
var response = await _httpClient.PutAsJsonAsync($"{_baseUrl}api/stations/{stationId}", request);
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException($"Station not found: {errorContent}");
}
if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException($"Failed to update station: {errorContent}");
}
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<StationDto>()
?? throw new Exception("Failed to update station");
}
/// <summary>
/// Delete a station
/// </summary>
public async Task DeleteStationAsync(Guid stationId)
{
var response = await _httpClient.DeleteAsync($"{_baseUrl}api/stations/{stationId}");
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new InvalidOperationException($"Station not found: {errorContent}");
}
response.EnsureSuccessStatusCode();
}
}