401 lines
14 KiB
C#
401 lines
14 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using RobotNet10.MapEditor.Shared.DTOs.Requests;
|
|
using RobotNet10.MapManager.Data;
|
|
|
|
namespace RobotNet10.MapManager.Services;
|
|
|
|
/// <summary>
|
|
/// Service implementation for managing layouts, versions, and levels
|
|
/// </summary>
|
|
public class LayoutService(MapDbContext context) : ILayoutService
|
|
{
|
|
private readonly MapDbContext _context = context;
|
|
|
|
// ==========================================
|
|
// LAYOUT OPERATIONS
|
|
// ==========================================
|
|
|
|
public async Task<Layout> CreateLayoutAsync(CreateLayoutRequest request)
|
|
{
|
|
// Check if LayoutId already exists
|
|
var exists = await _context.Layouts.AnyAsync(l => l.LayoutId == request.LayoutId);
|
|
if (exists)
|
|
{
|
|
throw new InvalidOperationException($"Layout with ID '{request.LayoutId}' already exists");
|
|
}
|
|
|
|
var layout = new Layout
|
|
{
|
|
LayoutId = request.LayoutId,
|
|
LayoutName = request.LayoutName,
|
|
Description = request.Description,
|
|
IsActive = false,
|
|
CreatedDate = DateTime.UtcNow,
|
|
ModifiedDate = DateTime.UtcNow,
|
|
CreatedBy = request.CreatedBy
|
|
};
|
|
|
|
_context.Layouts.Add(layout);
|
|
await _context.SaveChangesAsync();
|
|
|
|
return layout;
|
|
}
|
|
|
|
public async Task<List<Layout>> SearchLayoutsAsync(string? searchText)
|
|
{
|
|
var query = _context.Layouts.Include(l => l.Versions)
|
|
.ThenInclude(v => v.Levels)
|
|
.ThenInclude(l => l.EditorSettings)
|
|
.AsSplitQuery()
|
|
.AsQueryable();
|
|
|
|
if (!string.IsNullOrWhiteSpace(searchText))
|
|
{
|
|
var search = searchText.ToLower();
|
|
query = query.Where(l =>
|
|
l.LayoutId.ToLower().Contains(search) ||
|
|
l.LayoutName.ToLower().Contains(search));
|
|
}
|
|
|
|
return await query
|
|
.OrderByDescending(l => l.ModifiedDate)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<Layout?> GetLayoutByIdAsync(Guid layoutId)
|
|
{
|
|
return await _context.Layouts
|
|
.Include(l => l.Versions)
|
|
.ThenInclude(v => v.Levels)
|
|
.ThenInclude(l => l.EditorSettings)
|
|
.FirstOrDefaultAsync(l => l.Id == layoutId);
|
|
}
|
|
|
|
public async Task<Layout?> GetLayoutByLayoutIdAsync(string layoutId)
|
|
{
|
|
return await _context.Layouts
|
|
.Include(l => l.Versions)
|
|
.ThenInclude(v => v.Levels)
|
|
.ThenInclude(l => l.EditorSettings)
|
|
.FirstOrDefaultAsync(l => l.LayoutId == layoutId);
|
|
}
|
|
|
|
public async Task<Layout?> GetLayoutByNameAsync(string layoutName)
|
|
{
|
|
return await _context.Layouts
|
|
.Include(l => l.Versions)
|
|
.ThenInclude(v => v.Levels)
|
|
.ThenInclude(l => l.EditorSettings)
|
|
.FirstOrDefaultAsync(l => l.LayoutName == layoutName);
|
|
}
|
|
|
|
public async Task<Layout> UpdateLayoutAsync(Guid layoutId, UpdateLayoutRequest request)
|
|
{
|
|
var layout = await GetLayoutByIdAsync(layoutId) ??
|
|
throw new InvalidOperationException($"Layout with ID '{layoutId}' not found");
|
|
|
|
layout.LayoutName = request.LayoutName;
|
|
layout.Description = request.Description;
|
|
layout.ModifiedDate = DateTime.UtcNow;
|
|
layout.ModifiedBy = request.ModifiedBy;
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
return layout;
|
|
}
|
|
|
|
public async Task<bool> DeleteLayoutAsync(Guid layoutId)
|
|
{
|
|
var layout = await GetLayoutByIdAsync(layoutId);
|
|
if (layout == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check if layout is active
|
|
if (layout.IsActive)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Cannot delete active layout '{layout.LayoutId}'. Deactivate it first.");
|
|
}
|
|
|
|
// Hard delete - EF Core cascade will handle:
|
|
// Layout → Versions → Levels → Nodes/Edges/Stations → Properties
|
|
_context.Layouts.Remove(layout);
|
|
await _context.SaveChangesAsync();
|
|
|
|
return true;
|
|
}
|
|
|
|
public async Task<Layout> ActivateLayoutAsync(Guid layoutId)
|
|
{
|
|
var layout = await GetLayoutByIdAsync(layoutId) ??
|
|
throw new InvalidOperationException($"Layout with ID '{layoutId}' not found");
|
|
|
|
layout.IsActive = true;
|
|
layout.ModifiedDate = DateTime.UtcNow;
|
|
await _context.SaveChangesAsync();
|
|
|
|
return layout;
|
|
}
|
|
|
|
public async Task<Layout> DeactivateLayoutAsync(Guid layoutId)
|
|
{
|
|
var layout = await GetLayoutByIdAsync(layoutId) ??
|
|
throw new InvalidOperationException($"Layout with ID '{layoutId}' not found");
|
|
layout.IsActive = false;
|
|
layout.ModifiedDate = DateTime.UtcNow;
|
|
await _context.SaveChangesAsync();
|
|
|
|
return layout;
|
|
}
|
|
|
|
// ==========================================
|
|
// VERSION OPERATIONS
|
|
// ==========================================
|
|
|
|
public async Task<LayoutVersion> CreateVersionAsync(Guid layoutId, CreateLayoutVersionRequest request)
|
|
{
|
|
var layout = await GetLayoutByIdAsync(layoutId) ??
|
|
throw new InvalidOperationException($"Layout with ID '{layoutId}' not found");
|
|
|
|
// Check if version already exists
|
|
var exists = await _context.LayoutVersions
|
|
.AnyAsync(v => v.LayoutId == layoutId && v.Version == request.Version);
|
|
if (exists)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Version '{request.Version}' already exists for layout '{layout.LayoutId}'");
|
|
}
|
|
|
|
var version = new LayoutVersion
|
|
{
|
|
LayoutId = layoutId,
|
|
Version = request.Version,
|
|
LayoutDescription = request.LayoutDescription,
|
|
CreatedBy = request.CreatedBy,
|
|
CreatedDate = DateTime.UtcNow,
|
|
IsActive = false // New versions start as inactive
|
|
};
|
|
|
|
_context.LayoutVersions.Add(version);
|
|
await _context.SaveChangesAsync();
|
|
|
|
return version;
|
|
}
|
|
|
|
public async Task<List<LayoutVersion>> GetVersionsAsync(Guid layoutId)
|
|
{
|
|
return await _context.LayoutVersions
|
|
.Where(v => v.LayoutId == layoutId)
|
|
.Include(v => v.Levels)
|
|
.ThenInclude(l => l.EditorSettings)
|
|
.OrderByDescending(v => v.CreatedDate)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<LayoutVersion?> GetVersionAsync(Guid versionId)
|
|
{
|
|
return await _context.LayoutVersions
|
|
.Include(v => v.Layout)
|
|
.Include(v => v.Levels)
|
|
.ThenInclude(l => l.EditorSettings)
|
|
.FirstOrDefaultAsync(v => v.Id == versionId);
|
|
}
|
|
|
|
public async Task<LayoutVersion> UpdateVersionAsync(Guid versionId, UpdateLayoutRequest request)
|
|
{
|
|
var version = await GetVersionAsync(versionId) ??
|
|
throw new InvalidOperationException($"Version with ID '{versionId}' not found");
|
|
version.LayoutDescription = request.Description;
|
|
await _context.SaveChangesAsync();
|
|
|
|
return version;
|
|
}
|
|
|
|
public async Task<bool> DeleteVersionAsync(Guid versionId)
|
|
{
|
|
var version = await GetVersionAsync(versionId);
|
|
if (version == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check if parent layout is active
|
|
if (version.Layout.IsActive)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Cannot delete version from active layout '{version.Layout.LayoutId}'. " +
|
|
"Deactivate the layout first.");
|
|
}
|
|
|
|
// Hard delete - cascade will handle levels and all nested data
|
|
_context.LayoutVersions.Remove(version);
|
|
await _context.SaveChangesAsync();
|
|
|
|
return true;
|
|
}
|
|
|
|
// ==========================================
|
|
// LEVEL OPERATIONS
|
|
// ==========================================
|
|
|
|
public async Task<LayoutLevel> CreateLevelAsync(Guid versionId, CreateLayoutLevelRequest request)
|
|
{
|
|
var version = await GetVersionAsync(versionId) ??
|
|
throw new InvalidOperationException($"Version with ID '{versionId}' not found");
|
|
|
|
// Check if level already exists
|
|
var exists = await _context.LayoutLevels
|
|
.AnyAsync(l => l.VersionId == versionId && l.LayoutLevelId == request.LayoutLevelId);
|
|
if (exists)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Level '{request.LayoutLevelId}' already exists in this version");
|
|
}
|
|
|
|
var level = new LayoutLevel
|
|
{
|
|
VersionId = versionId,
|
|
LayoutLevelId = request.LayoutLevelId,
|
|
LevelOrder = request.LevelOrder
|
|
};
|
|
|
|
_context.LayoutLevels.Add(level);
|
|
await _context.SaveChangesAsync();
|
|
|
|
// Create editor settings if coordinate system provided
|
|
if (request.CoordinateSystem != null)
|
|
{
|
|
var settings = new LayoutLevelEditorSettings
|
|
{
|
|
LevelId = level.Id,
|
|
OriginX = request.CoordinateSystem.OriginX,
|
|
OriginY = request.CoordinateSystem.OriginY,
|
|
Resolution = request.CoordinateSystem.Resolution,
|
|
BoundsMinX = request.CoordinateSystem.BoundsMinX,
|
|
BoundsMaxX = request.CoordinateSystem.BoundsMaxX,
|
|
BoundsMinY = request.CoordinateSystem.BoundsMinY,
|
|
BoundsMaxY = request.CoordinateSystem.BoundsMaxY,
|
|
ImageWidth = request.CoordinateSystem.ImageWidth,
|
|
ImageHeight = request.CoordinateSystem.ImageHeight,
|
|
CreatedDate = DateTime.UtcNow,
|
|
ModifiedDate = DateTime.UtcNow
|
|
};
|
|
_context.LayoutLevelEditorSettings.Add(settings);
|
|
await _context.SaveChangesAsync();
|
|
}
|
|
|
|
return level;
|
|
}
|
|
|
|
public async Task<List<LayoutLevel>> GetLevelsAsync(Guid versionId)
|
|
{
|
|
return await _context.LayoutLevels
|
|
.Where(l => l.VersionId == versionId)
|
|
.Include(l => l.EditorSettings)
|
|
.OrderBy(l => l.LevelOrder)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<LayoutLevel?> GetLevelAsync(Guid levelId)
|
|
{
|
|
return await _context.LayoutLevels
|
|
.Include(l => l.Version)
|
|
.ThenInclude(v => v.Layout)
|
|
.Include(l => l.EditorSettings)
|
|
.FirstOrDefaultAsync(l => l.Id == levelId);
|
|
}
|
|
|
|
public async Task<LayoutLevel> UpdateLevelAsync(Guid levelId, UpdateLayoutLevelRequest request)
|
|
{
|
|
var level = await GetLevelAsync(levelId) ?? throw new InvalidOperationException($"Level with ID '{levelId}' not found");
|
|
|
|
// Update level properties
|
|
if (request.LayoutLevelId != null)
|
|
level.LayoutLevelId = request.LayoutLevelId;
|
|
|
|
if (request.LevelOrder.HasValue)
|
|
level.LevelOrder = request.LevelOrder.Value;
|
|
|
|
// Get or create editor settings
|
|
var settings = await _context.LayoutLevelEditorSettings
|
|
.FirstOrDefaultAsync(s => s.LevelId == levelId);
|
|
|
|
if (settings == null)
|
|
{
|
|
// Create new settings
|
|
settings = new LayoutLevelEditorSettings
|
|
{
|
|
LevelId = levelId,
|
|
CreatedDate = DateTime.UtcNow
|
|
};
|
|
_context.LayoutLevelEditorSettings.Add(settings);
|
|
}
|
|
|
|
// Update coordinate system if provided
|
|
if (request.CoordinateSystem != null)
|
|
{
|
|
settings.OriginX = request.CoordinateSystem.OriginX;
|
|
settings.OriginY = request.CoordinateSystem.OriginY;
|
|
settings.Resolution = request.CoordinateSystem.Resolution;
|
|
settings.BoundsMinX = request.CoordinateSystem.BoundsMinX;
|
|
settings.BoundsMaxX = request.CoordinateSystem.BoundsMaxX;
|
|
settings.BoundsMinY = request.CoordinateSystem.BoundsMinY;
|
|
settings.BoundsMaxY = request.CoordinateSystem.BoundsMaxY;
|
|
settings.ImageWidth = request.CoordinateSystem.ImageWidth;
|
|
settings.ImageHeight = request.CoordinateSystem.ImageHeight;
|
|
}
|
|
|
|
// Update editor settings if provided
|
|
if (request.EditorSettings != null)
|
|
{
|
|
if (request.EditorSettings.EdgeMinLengthCreate.HasValue)
|
|
settings.EdgeMinLengthCreate = request.EditorSettings.EdgeMinLengthCreate.Value;
|
|
|
|
if (request.EditorSettings.EdgeNameAutoGenerate.HasValue)
|
|
settings.EdgeNameAutoGenerate = request.EditorSettings.EdgeNameAutoGenerate.Value;
|
|
|
|
if (request.EditorSettings.NodeNameAutoGenerate.HasValue)
|
|
settings.NodeNameAutoGenerate = request.EditorSettings.NodeNameAutoGenerate.Value;
|
|
|
|
if (request.EditorSettings.NodeProximityRadius.HasValue)
|
|
settings.NodeProximityRadius = request.EditorSettings.NodeProximityRadius.Value;
|
|
}
|
|
|
|
// Update modified date if any settings were changed
|
|
if (request.CoordinateSystem != null || request.EditorSettings != null)
|
|
{
|
|
settings.ModifiedDate = DateTime.UtcNow;
|
|
}
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
return level;
|
|
}
|
|
|
|
public async Task<bool> DeleteLevelAsync(Guid levelId)
|
|
{
|
|
var level = await GetLevelAsync(levelId);
|
|
if (level == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Check if parent layout is active
|
|
if (level.Version.Layout.IsActive)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Cannot delete level from active layout '{level.Version.Layout.LayoutId}'. " +
|
|
"Deactivate the layout first.");
|
|
}
|
|
|
|
// Hard delete - cascade will handle all nested data
|
|
_context.LayoutLevels.Remove(level);
|
|
await _context.SaveChangesAsync();
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|