diff --git a/.gitignore b/.gitignore index 2a9fa9b22..362b36ce5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ frontend/build/ .idea *.log *.vs +*.code-workspace # Environment variables *.env diff --git a/backend/README.md b/backend/README.md index 6e6390dfe..f13e00e91 100644 --- a/backend/README.md +++ b/backend/README.md @@ -246,7 +246,10 @@ The access matrix looks like this: | | **Read Only** | **User** | **Admin** | | -------------------------- | ------------- | -------- | --------- | -| Asset Decks | Read | Read | CRUD | +| Area | Read | Read | CRUD | +| Deck | Read | Read | CRUD | +| Plant | Read | Read | CRUD | +| Installation | Read | Read | CRUD | | Echo | Read | Read | CRUD | | Missions | Read | Read | CRUD | | Robots | Read | Read | CRUD | diff --git a/backend/api.test/EndpointTest.cs b/backend/api.test/EndpointTest.cs index 623b614d0..db6be4573 100644 --- a/backend/api.test/EndpointTest.cs +++ b/backend/api.test/EndpointTest.cs @@ -102,24 +102,111 @@ public void Dispose() GC.SuppressFinalize(this); } + private async Task PopulateAreaDb(string installationCode, string plantCode, string deckName, string areaName) + { + string installationUrl = $"/installations"; + string plantUrl = $"/plants"; + string deckUrl = $"/decks"; + string areaUrl = $"/areas"; + var testPose = new Pose + { + Position = new Position + { + X = 1, + Y = 2, + Z = 2 + }, + Orientation = new Orientation + { + X = 0, + Y = 0, + Z = 0, + W = 1 + } + }; + + var installationQuery = new CreateInstallationQuery + { + InstallationCode = installationCode, + Name = installationCode + }; + + var plantQuery = new CreatePlantQuery + { + InstallationCode = installationCode, + PlantCode = plantCode, + Name = plantCode + }; + + var deckQuery = new CreateDeckQuery + { + InstallationCode = installationCode, + PlantCode = plantCode, + Name = deckName + }; + + var areaQuery = new CreateAreaQuery + { + InstallationCode = installationCode, + PlantCode = plantCode, + DeckName = deckName, + AreaName = areaName, + DefaultLocalizationPose = testPose + }; + + var installationContent = new StringContent( + JsonSerializer.Serialize(installationQuery), + null, + "application/json" + ); + + var plantContent = new StringContent( + JsonSerializer.Serialize(plantQuery), + null, + "application/json" + ); + + var deckContent = new StringContent( + JsonSerializer.Serialize(deckQuery), + null, + "application/json" + ); + + var areaContent = new StringContent( + JsonSerializer.Serialize(areaQuery), + null, + "application/json" + ); + + // Act + var installationResponse = await _client.PostAsync(installationUrl, installationContent); + Assert.NotNull(installationResponse); + var plantResponse = await _client.PostAsync(plantUrl, plantContent); + Assert.NotNull(plantResponse); + var deckResponse = await _client.PostAsync(deckUrl, deckContent); + Assert.NotNull(deckResponse); + var areaResponse = await _client.PostAsync(areaUrl, areaContent); + Assert.NotNull(areaResponse); + } + #region MissionsController [Fact] public async Task MissionsTest() { - string url = "/missions"; + string url = "/missions/runs"; var response = await _client.GetAsync(url); - var missions = await response.Content.ReadFromJsonAsync>( + var missionRuns = await response.Content.ReadFromJsonAsync>( _serializerOptions ); Assert.True(response.IsSuccessStatusCode); - Assert.True(missions != null && missions.Count == 3); + Assert.True(missionRuns != null && missionRuns.Count == 3); } [Fact] public async Task GetMissionById_ShouldReturnNotFound() { string missionId = "RandomString"; - string url = "/missions/" + missionId; + string url = "/missions/runs/" + missionId; var response = await _client.GetAsync(url); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -128,7 +215,7 @@ public async Task GetMissionById_ShouldReturnNotFound() public async Task DeleteMission_ShouldReturnNotFound() { string missionId = "RandomString"; - string url = "/missions/" + missionId; + string url = "/missions/runs/" + missionId; var response = await _client.DeleteAsync(url); Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } @@ -177,21 +264,25 @@ public async Task GetRobotById_ShouldReturnRobot() public async Task StartMissionTest() { // Arrange - string url = "/robots"; - var response = await _client.GetAsync(url); + string robotUrl = "/robots"; + string missionsUrl = "/missions"; + var response = await _client.GetAsync(robotUrl); Assert.True(response.IsSuccessStatusCode); var robots = await response.Content.ReadFromJsonAsync>(_serializerOptions); Assert.True(robots != null); var robot = robots[0]; string robotId = robot.Id; + string testInstallation = "TestInstallation"; + string testArea = "testArea"; + int echoMissionId = 95; // Act - url = "/missions"; var query = new ScheduledMissionQuery { RobotId = robotId, - AssetCode = "test", - EchoMissionId = 95, + InstallationCode = testInstallation, + AreaName = testArea, + EchoMissionId = echoMissionId, DesiredStartTime = DateTimeOffset.UtcNow }; var content = new StringContent( @@ -199,23 +290,28 @@ public async Task StartMissionTest() null, "application/json" ); - response = await _client.PostAsync(url, content); + response = await _client.PostAsync(missionsUrl, content); // Assert Assert.True(response.IsSuccessStatusCode); - var mission = await response.Content.ReadFromJsonAsync(_serializerOptions); - Assert.True(mission != null); - Assert.True(mission.Id != null); - Assert.True(mission.Status == MissionStatus.Pending); + var missionRun = await response.Content.ReadFromJsonAsync(_serializerOptions); + Assert.True(missionRun != null); + Assert.True(missionRun.Id != null); + Assert.True(missionRun.Status == MissionStatus.Pending); } [Fact] - public async Task AssetDeckTest() + public async Task AreaTest() { // Arrange - string testAsset = "testAsset"; - string testDeck = "testDeck"; - string assetDeckUrl = $"/asset-decks"; + string testInstallation = "TestInstallation"; + string testPlant = "TestPlant"; + string testDeck = "testDeck2"; + string testArea = "testArea"; + string installationUrl = $"/installations"; + string plantUrl = $"/plants"; + string deckUrl = $"/decks"; + string areaUrl = $"/areas"; var testPose = new Pose { Position = new Position @@ -233,35 +329,81 @@ public async Task AssetDeckTest() } }; - var query = new CreateAssetDeckQuery + var installationQuery = new CreateInstallationQuery + { + InstallationCode = testInstallation, + Name = testInstallation + }; + + var plantQuery = new CreatePlantQuery { - AssetCode = testAsset, + InstallationCode = testInstallation, + PlantCode = testPlant, + Name = testPlant + }; + + var deckQuery = new CreateDeckQuery + { + InstallationCode = testInstallation, + PlantCode = testPlant, + Name = testDeck + }; + + var areaQuery = new CreateAreaQuery + { + InstallationCode = testInstallation, + PlantCode = testPlant, DeckName = testDeck, + AreaName = testArea, DefaultLocalizationPose = testPose }; - var content = new StringContent( - JsonSerializer.Serialize(query), + var installationContent = new StringContent( + JsonSerializer.Serialize(installationQuery), + null, + "application/json" + ); + + var plantContent = new StringContent( + JsonSerializer.Serialize(plantQuery), + null, + "application/json" + ); + + var deckContent = new StringContent( + JsonSerializer.Serialize(deckQuery), + null, + "application/json" + ); + + var areaContent = new StringContent( + JsonSerializer.Serialize(areaQuery), null, "application/json" ); // Act - var assetDeckResponse = await _client.PostAsync(assetDeckUrl, content); + var installationResponse = await _client.PostAsync(installationUrl, installationContent); + var plantResponse = await _client.PostAsync(plantUrl, plantContent); + var deckResponse = await _client.PostAsync(deckUrl, deckContent); + var areaResponse = await _client.PostAsync(areaUrl, areaContent); // Assert - Assert.True(assetDeckResponse.IsSuccessStatusCode); - var assetDeck = await assetDeckResponse.Content.ReadFromJsonAsync(_serializerOptions); - Assert.True(assetDeck != null); + Assert.True(installationResponse.IsSuccessStatusCode); + Assert.True(plantResponse.IsSuccessStatusCode); + Assert.True(deckResponse.IsSuccessStatusCode); + Assert.True(areaResponse.IsSuccessStatusCode); + var area = await areaResponse.Content.ReadFromJsonAsync(_serializerOptions); + Assert.True(area != null); } [Fact] public async Task SafePositionTest() { // Arrange - Add Safe Position - string testAsset = "testAsset"; - string testDeck = "testDeck"; - string addSafePositionUrl = $"/asset-decks/{testAsset}/{testDeck}/safe-position"; + string testInstallation = "testInstallation"; + string testArea = "testArea"; + string addSafePositionUrl = $"/areas/{testInstallation}/{testArea}/safe-position"; var testPosition = new Position { X = 1, @@ -284,10 +426,13 @@ public async Task SafePositionTest() null, "application/json" ); - var assetDeckResponse = await _client.PostAsync(addSafePositionUrl, content); - Assert.True(assetDeckResponse.IsSuccessStatusCode); - var assetDeck = await assetDeckResponse.Content.ReadFromJsonAsync(_serializerOptions); - Assert.True(assetDeck != null); + + await PopulateAreaDb("testInstallation", "testPlant", "testDeck", "testArea"); + + var areaResponse = await _client.PostAsync(addSafePositionUrl, content); + Assert.True(areaResponse.IsSuccessStatusCode); + var area = await areaResponse.Content.ReadFromJsonAsync(_serializerOptions); + Assert.True(area != null); // Arrange - Get a Robot string url = "/robots"; @@ -299,15 +444,15 @@ public async Task SafePositionTest() string robotId = robot.Id; // Act - string goToSafePositionUrl = $"/robots/{robotId}/{testAsset}/{testDeck}/go-to-safe-position"; + string goToSafePositionUrl = $"/robots/{robotId}/{testInstallation}/{testArea}/go-to-safe-position"; var missionResponse = await _client.PostAsync(goToSafePositionUrl, null); // Assert Assert.True(missionResponse.IsSuccessStatusCode); - var mission = await missionResponse.Content.ReadFromJsonAsync(_serializerOptions); - Assert.True(mission != null); + var missionRun = await missionResponse.Content.ReadFromJsonAsync(_serializerOptions); + Assert.True(missionRun != null); Assert.True( - JsonSerializer.Serialize(mission.Tasks[0].RobotPose.Position) == + JsonSerializer.Serialize(missionRun.Tasks[0].RobotPose.Position) == JsonSerializer.Serialize(testPosition) ); } @@ -322,11 +467,10 @@ public async Task GetMapMetadata() foreach (string input in inputOutputPairs.Keys) { - string assetDeckId = input; - string url = $"/asset-decks/{assetDeckId}/map-metadata"; + string areaId = input; + string url = $"/areas/{areaId}/map-metadata"; var response = await _client.GetAsync(url); Assert.Equal(inputOutputPairs[input], response.StatusCode); - } } } diff --git a/backend/api.test/EventHandlers/TestMissionScheduler.cs b/backend/api.test/EventHandlers/TestMissionScheduler.cs index b67c9e4d8..eebf92ffa 100644 --- a/backend/api.test/EventHandlers/TestMissionScheduler.cs +++ b/backend/api.test/EventHandlers/TestMissionScheduler.cs @@ -22,24 +22,56 @@ namespace Api.Test.EventHandlers [Collection("Database collection")] public class TestMissionScheduler : IDisposable { - private static Mission ScheduledMission => + private static readonly Installation testInstallation = new() + { + InstallationCode = "test", + Name = "test test" + }; + private static readonly Plant testPlant = new() + { + PlantCode = "test", + Name = "test test", + Installation = testInstallation + }; + + private static MissionRun ScheduledMission => new() { Name = "testMission", - EchoMissionId = 2, + MissionId = Guid.NewGuid().ToString(), Status = MissionStatus.Pending, DesiredStartTime = DateTimeOffset.Now, - AssetCode = "TestAsset", + Area = new Area + { + Deck = new Deck + { + Plant = testPlant, + Installation = testInstallation, + Name = "testDeck" + }, + Installation = testInstallation, + Plant = testPlant, + Name = "testArea", + MapMetadata = new MapMetadata() + { + MapName = "TestMap", + Boundary = new(), + TransformationMatrices = new() + }, + DefaultLocalizationPose = new Pose(), + SafePositions = new List() + }, MapMetadata = new MapMetadata() { MapName = "TestMap", Boundary = new(), TransformationMatrices = new() - } + }, + InstallationCode = "testInstallation" }; private readonly MissionScheduler _scheduledMissionEventHandler; - private readonly IMissionService _missionService; + private readonly IMissionRunService _missionRunService; private readonly IRobotService _robotService; private readonly RobotControllerMock _robotControllerMock; private readonly FlotillaDbContext _context; @@ -49,11 +81,11 @@ public TestMissionScheduler(DatabaseFixture fixture) // Using Moq https://github.com/moq/moq4 var schedulerLogger = new Mock>().Object; - var missionLogger = new Mock>().Object; + var missionLogger = new Mock>().Object; // Mock ScheduledMissionService: _context = fixture.NewContext; - _missionService = new MissionService(_context, missionLogger); + _missionRunService = new MissionRunService(_context, missionLogger); _robotService = new RobotService(_context); _robotControllerMock = new RobotControllerMock(); @@ -61,8 +93,8 @@ public TestMissionScheduler(DatabaseFixture fixture) // Mock injection of MissionService: mockServiceProvider - .Setup(p => p.GetService(typeof(IMissionService))) - .Returns(_missionService); + .Setup(p => p.GetService(typeof(IMissionRunService))) + .Returns(_missionRunService); // Mock injection of RobotService: mockServiceProvider .Setup(p => p.GetService(typeof(IRobotService))) @@ -105,7 +137,7 @@ public void Dispose() private async void AssertExpectedStatusChange( MissionStatus preStatus, MissionStatus postStatus, - Mission mission + MissionRun missionRun ) { // ARRANGE @@ -113,18 +145,18 @@ Mission mission var cts = new CancellationTokenSource(); // Add Scheduled mission - await _missionService.Create(mission); + await _missionRunService.Create(missionRun); _robotControllerMock.RobotServiceMock - .Setup(service => service.ReadById(mission.Robot.Id)) - .Returns(async () => mission.Robot); + .Setup(service => service.ReadById(missionRun.Robot.Id)) + .Returns(async () => missionRun.Robot); _robotControllerMock.MissionServiceMock - .Setup(service => service.ReadById(mission.Id)) - .Returns(async () => mission); + .Setup(service => service.ReadById(missionRun.Id)) + .Returns(async () => missionRun); // Assert start conditions - var preMission = await _missionService.ReadById(mission.Id); + var preMission = await _missionRunService.ReadById(missionRun.Id); Assert.NotNull(preMission); Assert.Equal(preStatus, preMission!.Status); @@ -138,7 +170,7 @@ Mission mission // ASSERT // Verify status change - var postMission = await _missionService.ReadById(mission.Id); + var postMission = await _missionRunService.ReadById(missionRun.Id); Assert.NotNull(postMission); Assert.Equal(postStatus, postMission!.Status); } @@ -147,33 +179,33 @@ Mission mission // Test that if robot is busy, mission awaits available robot public async void ScheduledMissionPendingIfRobotBusy() { - var mission = ScheduledMission; + var missionRun = ScheduledMission; // Get real robot to avoid error on robot model var robot = (await _robotService.ReadAll()).First( r => r is { Status: RobotStatus.Busy, Enabled: true } ); - mission.Robot = robot; + missionRun.Robot = robot; // Expect failed because robot does not exist - AssertExpectedStatusChange(MissionStatus.Pending, MissionStatus.Pending, mission); + AssertExpectedStatusChange(MissionStatus.Pending, MissionStatus.Pending, missionRun); } [Fact] // Test that if robot is available, mission is started public async void ScheduledMissionStartedIfRobotAvailable() { - var mission = ScheduledMission; + var missionRun = ScheduledMission; // Get real robot to avoid error on robot model var robot = (await _robotService.ReadAll()).First( r => r is { Status: RobotStatus.Available, Enabled: true } ); - mission.Robot = robot; + missionRun.Robot = robot; // Mock successful Start Mission: _robotControllerMock.IsarServiceMock - .Setup(isar => isar.StartMission(robot, mission)) + .Setup(isar => isar.StartMission(robot, missionRun)) .Returns( async () => new IsarMission( @@ -186,29 +218,29 @@ public async void ScheduledMissionStartedIfRobotAvailable() ); // Expect failed because robot does not exist - AssertExpectedStatusChange(MissionStatus.Pending, MissionStatus.Ongoing, mission); + AssertExpectedStatusChange(MissionStatus.Pending, MissionStatus.Ongoing, missionRun); } [Fact] // Test that if ISAR fails, mission is set to failed public async void ScheduledMissionFailedIfIsarUnavailable() { - var mission = ScheduledMission; + var missionRun = ScheduledMission; // Get real robot to avoid error on robot model var robot = (await _robotService.ReadAll()).First(); robot.Enabled = true; robot.Status = RobotStatus.Available; await _robotService.Update(robot); - mission.Robot = robot; + missionRun.Robot = robot; // Mock failing ISAR: _robotControllerMock.IsarServiceMock - .Setup(isar => isar.StartMission(robot, mission)) + .Setup(isar => isar.StartMission(robot, missionRun)) .Throws(new MissionException("ISAR Failed test message")); // Expect failed because robot does not exist - AssertExpectedStatusChange(MissionStatus.Pending, MissionStatus.Failed, mission); + AssertExpectedStatusChange(MissionStatus.Pending, MissionStatus.Failed, missionRun); } } } diff --git a/backend/api.test/Mocks/EchoServiceMock.cs b/backend/api.test/Mocks/EchoServiceMock.cs index ef50014e0..4331eb18b 100644 --- a/backend/api.test/Mocks/EchoServiceMock.cs +++ b/backend/api.test/Mocks/EchoServiceMock.cs @@ -19,17 +19,17 @@ public class MockEchoService : IEchoService Tags = new List() }; - public MissionDefinition MockMissionDefinition = + public CondensedMissionDefinition MockMissionDefinition = new() { EchoMissionId = 1, Name = "test", }; - public async Task> GetAvailableMissions(string? installationCode) + public async Task> GetAvailableMissions(string? installationCode) { await Task.Run(() => Thread.Sleep(1)); - return new List(new MissionDefinition[] { MockMissionDefinition }); + return new List(new CondensedMissionDefinition[] { MockMissionDefinition }); } public async Task GetMissionById(int missionId) @@ -48,5 +48,10 @@ public async Task GetRobotPoseFromPoseId(int poseId) await Task.Run(() => Thread.Sleep(1)); return new EchoPoseResponse(); } + + public Task GetMissionByPath(string relativePath) + { + throw new NotImplementedException(); + } } } diff --git a/backend/api.test/Mocks/IsarServiceMock.cs b/backend/api.test/Mocks/IsarServiceMock.cs index 6194c27c4..ec928e02a 100644 --- a/backend/api.test/Mocks/IsarServiceMock.cs +++ b/backend/api.test/Mocks/IsarServiceMock.cs @@ -9,7 +9,7 @@ namespace Api.Test.Mocks { public class MockIsarService : IIsarService { - public async Task StartMission(Robot robot, Mission mission) + public async Task StartMission(Robot robot, MissionRun mission) { await Task.Run(() => Thread.Sleep(1)); var isarServiceMissionResponse = new IsarMission( diff --git a/backend/api.test/Mocks/MapServiceMock.cs b/backend/api.test/Mocks/MapServiceMock.cs index 70f352339..a5a5e4a7a 100644 --- a/backend/api.test/Mocks/MapServiceMock.cs +++ b/backend/api.test/Mocks/MapServiceMock.cs @@ -9,18 +9,18 @@ namespace Api.Test.Mocks { public class MockMapService : IMapService { - public async Task ChooseMapFromPositions(IList positions, string assetCode) + public async Task ChooseMapFromPositions(IList positions, string installationCode) { await Task.Run(() => Thread.Sleep(1)); return new MapMetadata(); } - public async Task AssignMapToMission(Mission mission) + public async Task AssignMapToMission(MissionRun mission) { await Task.Run(() => Thread.Sleep(1)); } - public async Task FetchMapImage(string mapName, string assetCode) + public async Task FetchMapImage(string mapName, string installationCode) { await Task.Run(() => Thread.Sleep(1)); string filePath = Directory.GetCurrentDirectory() + "Images/MockMapImage.png"; diff --git a/backend/api.test/Mocks/RobotControllerMock.cs b/backend/api.test/Mocks/RobotControllerMock.cs index 6cdf207c5..33d097a2d 100644 --- a/backend/api.test/Mocks/RobotControllerMock.cs +++ b/backend/api.test/Mocks/RobotControllerMock.cs @@ -10,17 +10,17 @@ internal class RobotControllerMock public Mock IsarServiceMock; public Mock RobotServiceMock; public Mock RobotModelServiceMock; - public Mock MissionServiceMock; + public Mock MissionServiceMock; public Mock Mock; - public Mock AssetDeckServiceMock; + public Mock AreaServiceMock; public RobotControllerMock() { - MissionServiceMock = new Mock(); + MissionServiceMock = new Mock(); IsarServiceMock = new Mock(); RobotServiceMock = new Mock(); RobotModelServiceMock = new Mock(); - AssetDeckServiceMock = new Mock(); + AreaServiceMock = new Mock(); var mockLoggerController = new Mock>(); @@ -30,7 +30,7 @@ public RobotControllerMock() IsarServiceMock.Object, MissionServiceMock.Object, RobotModelServiceMock.Object, - AssetDeckServiceMock.Object + AreaServiceMock.Object ) { CallBase = true diff --git a/backend/api.test/Services/MissionService.cs b/backend/api.test/Services/MissionService.cs index 22e54d3f4..275634129 100644 --- a/backend/api.test/Services/MissionService.cs +++ b/backend/api.test/Services/MissionService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Api.Controllers.Models; @@ -15,14 +16,14 @@ namespace Api.Test.Services public class MissionServiceTest : IDisposable { private readonly FlotillaDbContext _context; - private readonly ILogger _logger; - private readonly MissionService _missionService; + private readonly ILogger _logger; + private readonly MissionRunService _missionRunService; public MissionServiceTest(DatabaseFixture fixture) { _context = fixture.NewContext; - _logger = new Mock>().Object; - _missionService = new MissionService(_context, _logger); + _logger = new Mock>().Object; + _missionRunService = new MissionRunService(_context, _logger); } public void Dispose() @@ -34,31 +35,59 @@ public void Dispose() [Fact] public async Task ReadIdDoesNotExist() { - var mission = await _missionService.ReadById("some_id_that_does_not_exist"); - Assert.Null(mission); + var missionRun = await _missionRunService.ReadById("some_id_that_does_not_exist"); + Assert.Null(missionRun); } [Fact] public async Task Create() { var robot = _context.Robots.First(); - int nReportsBefore = _missionService - .ReadAll(new MissionQueryStringParameters()) + int nReportsBefore = _missionRunService + .ReadAll(new MissionRunQueryStringParameters()) .Result.Count; - Mission mission = + var testInstallation = new Installation + { + InstallationCode = "test", + Name = "test test" + }; + var testPlant = new Plant + { + PlantCode = "test", + Name = "test test", + Installation = testInstallation + }; + + MissionRun missionRun = new() { Name = "testMission", Robot = robot, + MissionId = Guid.NewGuid().ToString(), MapMetadata = new MapMetadata() { MapName = "testMap" }, - AssetCode = "testAsset", + Area = new Area + { + Deck = new Deck + { + Plant = testPlant, + Installation = testInstallation, + Name = "testDeck" + }, + Installation = testInstallation, + Plant = testPlant, + Name = "testArea", + MapMetadata = new MapMetadata() { MapName = "testMap" }, + DefaultLocalizationPose = new Pose(), + SafePositions = new List() + }, + InstallationCode = "testInstallation", DesiredStartTime = DateTime.Now }; - await _missionService.Create(mission); - int nReportsAfter = _missionService - .ReadAll(new MissionQueryStringParameters()) + await _missionRunService.Create(missionRun); + int nReportsAfter = _missionRunService + .ReadAll(new MissionRunQueryStringParameters()) .Result.Count; Assert.Equal(nReportsBefore + 1, nReportsAfter); diff --git a/backend/api.test/Services/RobotService.cs b/backend/api.test/Services/RobotService.cs index d2482c086..18121cf50 100644 --- a/backend/api.test/Services/RobotService.cs +++ b/backend/api.test/Services/RobotService.cs @@ -71,7 +71,7 @@ public async Task Create() IsarId = "", SerialNumber = "", VideoStreams = new List() { videoStreamQuery }, - CurrentAsset = "", + CurrentInstallation = "", RobotType = RobotType.Robot, Host = "", Port = 1, diff --git a/backend/api/Controllers/AreaController.cs b/backend/api/Controllers/AreaController.cs new file mode 100644 index 000000000..195052fde --- /dev/null +++ b/backend/api/Controllers/AreaController.cs @@ -0,0 +1,297 @@ +using Api.Controllers.Models; +using Api.Database.Models; +using Api.Services; +using Azure; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers +{ + [ApiController] + [Route("areas")] + public class AreaController : ControllerBase + { + private readonly IAreaService _areaService; + + private readonly IMapService _mapService; + + private readonly ILogger _logger; + + public AreaController( + ILogger logger, + IMapService mapService, + IAreaService areaService + ) + { + _logger = logger; + _mapService = mapService; + _areaService = areaService; + } + + /// + /// Add a new area + /// + /// + /// This query adds a new area to the database + /// + [HttpPost] + [Authorize(Roles = Role.Admin)] + [ProducesResponseType(typeof(AreaResponse), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> Create([FromBody] CreateAreaQuery area) + { + _logger.LogInformation("Creating new area"); + try + { + var existingArea = await _areaService.ReadByInstallationAndName(area.InstallationCode, area.AreaName); + if (existingArea != null) + { + _logger.LogWarning("An area for given name and installation already exists"); + return Conflict($"Area already exists"); + } + + var newArea = await _areaService.Create(area); + _logger.LogInformation( + "Succesfully created new area with id '{areaId}'", + newArea.Id + ); + var response = new AreaResponse + { + Id = newArea.Id, + DeckName = newArea.Deck.Name, + PlantCode = newArea.Plant.PlantCode, + InstallationCode = newArea.Installation.InstallationCode, + AreaName = newArea.Name, + MapMetadata = newArea.MapMetadata, + DefaultLocalizationPose = newArea.DefaultLocalizationPose, + SafePositions = newArea.SafePositions + }; + return CreatedAtAction( + nameof(GetAreaById), + new { id = newArea.Id }, + response + ); + } + catch (Exception e) + { + _logger.LogError(e, "Error while creating new area"); + throw; + } + } + + /// + /// Add safe position to an area + /// + /// + /// This query adds a new safe position to the database + /// + [HttpPost] + [Authorize(Roles = Role.Admin)] + [Route("{installationCode}/{areaName}/safe-position")] + [ProducesResponseType(typeof(AreaResponse), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> AddSafePosition( + [FromRoute] string installationCode, + [FromRoute] string areaName, + [FromBody] Pose safePosition + ) + { + _logger.LogInformation(@"Adding new safe position to {Installation}, {Area}", installationCode, areaName); + try + { + var area = await _areaService.AddSafePosition(installationCode, areaName, new SafePosition(safePosition)); + if (area != null) + { + _logger.LogInformation(@"Successfully added new safe position for installation '{installationId}' + and name '{name}'", installationCode, areaName); + var response = new AreaResponse + { + Id = area.Id, + DeckName = area.Deck.Name, + PlantCode = area.Plant.PlantCode, + InstallationCode = area.Installation.InstallationCode, + AreaName = area.Name, + MapMetadata = area.MapMetadata, + DefaultLocalizationPose = area.DefaultLocalizationPose, + SafePositions = area.SafePositions + }; + return CreatedAtAction(nameof(GetAreaById), new { id = area.Id }, response); ; + } + else + { + _logger.LogInformation(@"No area with installation {installationCode} and name {areaName} could be found.", installationCode, areaName); + return NotFound(@$"No area with installation {installationCode} and name {areaName} could be found."); + } + } + catch (Exception e) + { + _logger.LogError(e, "Error while creating or adding new safe zone"); + throw; + } + } + + /// + /// Deletes the area with the specified id from the database. + /// + [HttpDelete] + [Authorize(Roles = Role.Admin)] + [Route("{id}")] + [ProducesResponseType(typeof(AreaResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> DeleteArea([FromRoute] string id) + { + var area = await _areaService.Delete(id); + if (area is null) + return NotFound($"Area with id {id} not found"); + var response = new AreaResponse + { + Id = area.Id, + DeckName = area.Deck.Name, + PlantCode = area.Plant.PlantCode, + InstallationCode = area.Installation.InstallationCode, + AreaName = area.Name, + MapMetadata = area.MapMetadata, + DefaultLocalizationPose = area.DefaultLocalizationPose, + SafePositions = area.SafePositions + }; + return Ok(response); + } + + /// + /// List all installation areas in the Flotilla database + /// + /// + /// This query gets all installation areas + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetAreas() + { + try + { + var areas = await _areaService.ReadAll(); + var response = areas.Select(area => new AreaResponse + { + Id = area.Id, + DeckName = area.Deck.Name, + PlantCode = area.Plant.PlantCode, + InstallationCode = area.Installation.InstallationCode, + AreaName = area.Name, + MapMetadata = area.MapMetadata, + DefaultLocalizationPose = area.DefaultLocalizationPose, + SafePositions = area.SafePositions + }); + return Ok(response); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of areas from database"); + throw; + } + } + + /// + /// Lookup area by specified id. + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [Route("{id}")] + [ProducesResponseType(typeof(AreaResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetAreaById([FromRoute] string id) + { + try + { + var area = await _areaService.ReadById(id); + if (area == null) + return NotFound($"Could not find area with id {id}"); + var response = new AreaResponse + { + Id = area.Id, + DeckName = area.Deck.Name, + PlantCode = area.Plant.PlantCode, + InstallationCode = area.Installation.InstallationCode, + AreaName = area.Name, + MapMetadata = area.MapMetadata, + DefaultLocalizationPose = area.DefaultLocalizationPose, + SafePositions = area.SafePositions + }; + return Ok(response); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of areas from database"); + throw; + } + + } + + /// + /// Gets map metadata for localization poses belonging to area with specified id + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [Route("{id}/map-metadata")] + [ProducesResponseType(typeof(MapMetadata), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetMapMetadata([FromRoute] string id) + { + var area = await _areaService.ReadById(id); + if (area is null) + { + string errorMessage = $"Area not found for area with ID {id}"; + _logger.LogError("{ErrorMessage}", errorMessage); + return NotFound(errorMessage); + } + + MapMetadata? mapMetadata; + var positions = new List + { + area.DefaultLocalizationPose.Position + }; + try + { + mapMetadata = await _mapService.ChooseMapFromPositions(positions, area.Deck.Plant.Installation.InstallationCode); + } + catch (RequestFailedException e) + { + string errorMessage = $"An error occurred while retrieving the map for area {area.Id}"; + _logger.LogError(e, "{ErrorMessage}", errorMessage); + return StatusCode(StatusCodes.Status502BadGateway, errorMessage); + } + catch (ArgumentOutOfRangeException e) + { + string errorMessage = $"Could not find a suitable map for area {area.Id}"; + _logger.LogError(e, "{ErrorMessage}", errorMessage); + return NotFound(errorMessage); + } + + if (mapMetadata == null) + { + return NotFound("A map which contained at least half of the points in this mission could not be found"); + } + return Ok(mapMetadata); + } + } +} diff --git a/backend/api/Controllers/AssetDeckController.cs b/backend/api/Controllers/AssetDeckController.cs deleted file mode 100644 index 3927f17a7..000000000 --- a/backend/api/Controllers/AssetDeckController.cs +++ /dev/null @@ -1,256 +0,0 @@ -using Api.Controllers.Models; -using Api.Database.Models; -using Api.Services; -using Api.Utilities; -using Azure; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -namespace Api.Controllers -{ - [ApiController] - [Route("asset-decks")] - public class AssetDeckController : ControllerBase - { - private readonly IAssetDeckService _assetDeckService; - - private readonly ILogger _logger; - - private readonly IMapService _mapService; - - public AssetDeckController( - ILogger logger, - IMapService mapService, - IAssetDeckService assetDeckService - ) - { - _logger = logger; - _mapService = mapService; - _assetDeckService = assetDeckService; - } - - /// - /// List all asset decks in the Flotilla database - /// - /// - /// This query gets all asset decks - /// - [HttpGet] - [Authorize(Roles = Role.Any)] - [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task>> GetAssetDecks() - { - try - { - var assetDecks = await _assetDeckService.ReadAll(); - return Ok(assetDecks); - } - catch (Exception e) - { - _logger.LogError(e, "Error during GET of asset decks from database"); - throw; - } - } - - /// - /// Lookup asset deck by specified id. - /// - [HttpGet] - [Authorize(Roles = Role.Any)] - [Route("{id}")] - [ProducesResponseType(typeof(AssetDeck), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> GetAssetDeckById([FromRoute] string id) - { - var assetDeck = await _assetDeckService.ReadById(id); - if (assetDeck == null) - { - return NotFound($"Could not find assetDeck with id {id}"); - } - return Ok(assetDeck); - } - - /// - /// Add a new asset deck - /// - /// - /// This query adds a new asset deck to the database - /// - [HttpPost] - [Authorize(Roles = Role.User)] - [ProducesResponseType(typeof(AssetDeck), StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> Create([FromBody] CreateAssetDeckQuery assetDeck) - { - _logger.LogInformation("Creating new asset deck"); - try - { - var existingAssetDeck = await _assetDeckService.ReadByAssetAndDeck(assetDeck.AssetCode, assetDeck.DeckName); - if (existingAssetDeck != null) - { - _logger.LogInformation("An asset deck for given deck and asset already exists"); - return BadRequest("Asset deck already exists"); - } - - var newAssetDeck = await _assetDeckService.Create(assetDeck); - _logger.LogInformation( - "Successfully created new asset deck with id '{AssetDeckId}'", - newAssetDeck.Id - ); - return CreatedAtAction( - nameof(GetAssetDeckById), - new - { - id = newAssetDeck.Id - }, - newAssetDeck - ); - } - catch (Exception e) - { - _logger.LogError(e, "Error while creating new asset deck"); - throw; - } - } - - - /// - /// Add a safe position to a asset deck - /// - /// - /// This query adds a new safe position to the database - /// - [HttpPost] - [Authorize(Roles = Role.Admin)] - [Route("{asset}/{deck}/safe-position")] - [ProducesResponseType(typeof(AssetDeck), StatusCodes.Status201Created)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> AddSafePosition( - [FromRoute] string asset, - [FromRoute] string deck, - [FromBody] Pose safePosition - ) - { - _logger.LogInformation("Adding new safe position"); - try - { - var assetDeck = await _assetDeckService.AddSafePosition(asset, deck, new SafePosition(safePosition)); - if (assetDeck != null) - { - _logger.LogInformation("Successfully added new safe position for asset '{AssetId}' and deck '{DeckId}'", asset, deck); - return CreatedAtAction(nameof(GetAssetDeckById), new - { - id = assetDeck.Id - }, assetDeck); - ; - } - _logger.LogInformation("Creating AssetDeck for asset '{AssetId}' and deck '{DeckId}'", asset, deck); - // Cloning to avoid tracking same object - var tempPose = ObjectCopier.Clone(safePosition); - assetDeck = await _assetDeckService.Create( - new CreateAssetDeckQuery - { - AssetCode = asset, - DeckName = deck, - DefaultLocalizationPose = new Pose() - }, - new List - { - tempPose - } - ); - return CreatedAtAction(nameof(GetAssetDeckById), new - { - id = assetDeck.Id - }, assetDeck); - } - catch (Exception e) - { - _logger.LogError(e, "Error while creating or adding new safe zone"); - throw; - } - } - - /// - /// Deletes the asset deck with the specified id from the database. - /// - [HttpDelete] - [Authorize(Roles = Role.Admin)] - [Route("{id}")] - [ProducesResponseType(typeof(AssetDeck), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> DeleteAssetDeck([FromRoute] string id) - { - var assetDeck = await _assetDeckService.Delete(id); - if (assetDeck is null) - { - return NotFound($"Asset deck with id {id} not found"); - } - return Ok(assetDeck); - } - - /// - /// Gets map metadata for localization poses belonging to asset deck with specified id - /// - [HttpGet] - [Authorize(Roles = Role.Any)] - [Route("{id}/map-metadata")] - [ProducesResponseType(typeof(MapMetadata), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> GetMapMetadata([FromRoute] string id) - { - var assetDeck = await _assetDeckService.ReadById(id); - if (assetDeck is null) - { - _logger.LogError("Asset deck not found for asset deck ID {AssetDeckId}", id); - return NotFound("Could not find this asset deck"); - } - - MapMetadata? mapMetadata; - var positions = new List - { - assetDeck.DefaultLocalizationPose.Position - }; - try - { - mapMetadata = await _mapService.ChooseMapFromPositions(positions, assetDeck.AssetCode); - } - catch (RequestFailedException e) - { - string errorMessage = $"An error occurred while retrieving the map for asset deck {assetDeck.Id}"; - _logger.LogError(e, "{ErrorMessage}", errorMessage); - return StatusCode(StatusCodes.Status502BadGateway, errorMessage); - } - catch (ArgumentOutOfRangeException e) - { - string errorMessage = $"Could not find a suitable map for asset deck {assetDeck.Id}"; - _logger.LogError(e, "{ErrorMessage}", errorMessage); - return NotFound(errorMessage); - } - - if (mapMetadata == null) - { - return NotFound("A map which contained at least half of the points in this mission could not be found"); - } - return Ok(mapMetadata); - } - } -} diff --git a/backend/api/Controllers/DeckController.cs b/backend/api/Controllers/DeckController.cs new file mode 100644 index 000000000..596f79a32 --- /dev/null +++ b/backend/api/Controllers/DeckController.cs @@ -0,0 +1,163 @@ +using Api.Controllers.Models; +using Api.Database.Models; +using Api.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers +{ + [ApiController] + [Route("decks")] + public class DeckController : ControllerBase + { + private readonly IDeckService _deckService; + private readonly IInstallationService _installationService; + private readonly IPlantService _plantService; + + private readonly IMapService _mapService; + + private readonly ILogger _logger; + + public DeckController( + ILogger logger, + IMapService mapService, + IDeckService deckService, + IInstallationService installationService, + IPlantService plantService + ) + { + _logger = logger; + _mapService = mapService; + _deckService = deckService; + _installationService = installationService; + _plantService = plantService; + } + + /// + /// List all decks in the Flotilla database + /// + /// + /// This query gets all decks + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetDecks() + { + try + { + var decks = await _deckService.ReadAll(); + return Ok(decks); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of decks from database"); + throw; + } + } + + /// + /// Lookup deck by specified id. + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [Route("{id}")] + [ProducesResponseType(typeof(Deck), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetDeckById([FromRoute] string id) + { + try + { + var deck = await _deckService.ReadById(id); + if (deck == null) + return NotFound($"Could not find deck with id {id}"); + return Ok(deck); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of deck from database"); + throw; + } + + } + + /// + /// Add a new deck + /// + /// + /// This query adds a new deck to the database + /// + [HttpPost] + [Authorize(Roles = Role.Admin)] + [ProducesResponseType(typeof(Deck), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> Create([FromBody] CreateDeckQuery deck) + { + _logger.LogInformation("Creating new deck"); + try + { + var existingInstallation = await _installationService.ReadByName(deck.InstallationCode); + if (existingInstallation == null) + { + return NotFound($"Could not find installation with name {deck.InstallationCode}"); + } + var existingPlant = await _plantService.ReadByInstallationAndName(existingInstallation, deck.PlantCode); + if (existingPlant == null) + { + return NotFound($"Could not find plant with name {deck.PlantCode}"); + } + var existingDeck = await _deckService.ReadByInstallationAndPlantAndName(existingInstallation, existingPlant, deck.Name); + if (existingDeck != null) + { + _logger.LogInformation("An deck for given name and deck already exists"); + return BadRequest($"Deck already exists"); + } + + var newDeck = await _deckService.Create(deck); + _logger.LogInformation( + "Succesfully created new deck with id '{deckId}'", + newDeck.Id + ); + return CreatedAtAction( + nameof(GetDeckById), + new { id = newDeck.Id }, + newDeck + ); + } + catch (Exception e) + { + _logger.LogError(e, "Error while creating new deck"); + throw; + } + } + + /// + /// Deletes the deck with the specified id from the database. + /// + [HttpDelete] + [Authorize(Roles = Role.Admin)] + [Route("{id}")] + [ProducesResponseType(typeof(Deck), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> DeleteDeck([FromRoute] string id) + { + var deck = await _deckService.Delete(id); + if (deck is null) + return NotFound($"Deck with id {id} not found"); + return Ok(deck); + } + } +} diff --git a/backend/api/Controllers/EchoController.cs b/backend/api/Controllers/EchoController.cs index 631d53426..fa5ba1546 100644 --- a/backend/api/Controllers/EchoController.cs +++ b/backend/api/Controllers/EchoController.cs @@ -23,24 +23,24 @@ public EchoController(ILogger logger, IEchoService echoService) } /// - /// List all available Echo missions for the asset + /// List all available Echo missions for the installation /// /// /// These missions are created in the Echo mission planner /// [HttpGet] [Route("available-missions")] - [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status502BadGateway)] - public async Task>> GetAvailableEchoMissions(string? installationCode) + public async Task>> GetAvailableEchoMissions(string? plantCode) { try { - var missions = await _echoService.GetAvailableMissions(installationCode); + var missions = await _echoService.GetAvailableMissions(plantCode); return Ok(missions); } catch (HttpRequestException e) diff --git a/backend/api/Controllers/InstallationController.cs b/backend/api/Controllers/InstallationController.cs new file mode 100644 index 000000000..1ddda7522 --- /dev/null +++ b/backend/api/Controllers/InstallationController.cs @@ -0,0 +1,147 @@ +using Api.Controllers.Models; +using Api.Database.Models; +using Api.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers +{ + [ApiController] + [Route("installations")] + public class InstallationController : ControllerBase + { + private readonly IInstallationService _installationService; + + private readonly IMapService _mapService; + + private readonly ILogger _logger; + + public InstallationController( + ILogger logger, + IMapService mapService, + IInstallationService installationService + ) + { + _logger = logger; + _mapService = mapService; + _installationService = installationService; + } + + /// + /// List all installations in the Flotilla database + /// + /// + /// This query gets all installations + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetInstallations() + { + try + { + var installations = await _installationService.ReadAll(); + return Ok(installations); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of installations from database"); + throw; + } + } + + /// + /// Lookup installation by specified id. + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [Route("{id}")] + [ProducesResponseType(typeof(Installation), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetInstallationById([FromRoute] string id) + { + try + { + var installation = await _installationService.ReadById(id); + if (installation == null) + return NotFound($"Could not find installation with id {id}"); + return Ok(installation); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of installation from database"); + throw; + } + + } + + /// + /// Add a new installation + /// + /// + /// This query adds a new installation to the database + /// + [HttpPost] + [Authorize(Roles = Role.Admin)] + [ProducesResponseType(typeof(Installation), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> Create([FromBody] CreateInstallationQuery installation) + { + _logger.LogInformation("Creating new installation"); + try + { + var existingInstallation = await _installationService.ReadByName(installation.InstallationCode); + if (existingInstallation != null) + { + _logger.LogInformation("An installation for given name and installation already exists"); + return BadRequest($"Installation already exists"); + } + + var newInstallation = await _installationService.Create(installation); + _logger.LogInformation( + "Succesfully created new installation with id '{installationId}'", + newInstallation.Id + ); + return CreatedAtAction( + nameof(GetInstallationById), + new { id = newInstallation.Id }, + newInstallation + ); + } + catch (Exception e) + { + _logger.LogError(e, "Error while creating new installation"); + throw; + } + } + + /// + /// Deletes the installation with the specified id from the database. + /// + [HttpDelete] + [Authorize(Roles = Role.Admin)] + [Route("{id}")] + [ProducesResponseType(typeof(Installation), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> DeleteInstallation([FromRoute] string id) + { + var installation = await _installationService.Delete(id); + if (installation is null) + return NotFound($"Installation with id {id} not found"); + return Ok(installation); + } + } +} diff --git a/backend/api/Controllers/MissionController.cs b/backend/api/Controllers/MissionController.cs index 54dd190d7..6561ac49c 100644 --- a/backend/api/Controllers/MissionController.cs +++ b/backend/api/Controllers/MissionController.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Globalization; +using System.Text.Json; using Api.Controllers.Models; using Api.Database.Models; using Api.Services; @@ -12,45 +13,54 @@ namespace Api.Controllers; [Route("missions")] public class MissionController : ControllerBase { - private readonly IMissionService _missionService; + private readonly IMissionDefinitionService _missionDefinitionService; + private readonly IMissionRunService _missionRunService; + private readonly IAreaService _areaService; private readonly IRobotService _robotService; private readonly IEchoService _echoService; + private readonly ICustomMissionService _customMissionService; private readonly ILogger _logger; private readonly IStidService _stidService; private readonly IMapService _mapService; public MissionController( - IMissionService missionService, + IMissionDefinitionService missionDefinitionService, + IMissionRunService missionRunService, + IAreaService areaService, IRobotService robotService, IEchoService echoService, + ICustomMissionService customMissionService, ILogger logger, IMapService mapService, IStidService stidService ) { - _missionService = missionService; + _missionDefinitionService = missionDefinitionService; + _missionRunService = missionRunService; + _areaService = areaService; _robotService = robotService; _echoService = echoService; + _customMissionService = customMissionService; _mapService = mapService; _stidService = stidService; _logger = logger; } /// - /// List all missions in the Flotilla database + /// List all mission runs in the Flotilla database /// /// - /// This query gets all missions + /// This query gets all mission runs /// - [HttpGet] + [HttpGet("runs")] [Authorize(Roles = Role.Any)] - [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task>> GetMissions( - [FromQuery] MissionQueryStringParameters parameters + public async Task>> GetMissionRuns( + [FromQuery] MissionRunQueryStringParameters parameters ) { if (parameters.MaxDesiredStartTime < parameters.MinDesiredStartTime) @@ -66,10 +76,56 @@ [FromQuery] MissionQueryStringParameters parameters return BadRequest("Max EndTime cannot be less than min EndTime"); } - PagedList missions; + PagedList missionRuns; + try + { + missionRuns = await _missionRunService.ReadAll(parameters); + } + catch (InvalidDataException e) + { + _logger.LogError(e.Message); + return BadRequest(e.Message); + } + + var metadata = new + { + missionRuns.TotalCount, + missionRuns.PageSize, + missionRuns.CurrentPage, + missionRuns.TotalPages, + missionRuns.HasNext, + missionRuns.HasPrevious + }; + + Response.Headers.Add( + QueryStringParameters.PaginationHeader, + JsonSerializer.Serialize(metadata) + ); + + return Ok(missionRuns); + } + + /// + /// List all mission definitions in the Flotilla database + /// + /// + /// This query gets all mission definitions + /// + [HttpGet("definitions")] + [Authorize(Roles = Role.Any)] + [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetMissionDefinitions( + [FromQuery] MissionDefinitionQueryStringParameters parameters + ) + { + PagedList missionDefinitions; try { - missions = await _missionService.ReadAll(parameters); + missionDefinitions = await _missionDefinitionService.ReadAll(parameters); } catch (InvalidDataException e) { @@ -79,12 +135,12 @@ [FromQuery] MissionQueryStringParameters parameters var metadata = new { - missions.TotalCount, - missions.PageSize, - missions.CurrentPage, - missions.TotalPages, - missions.HasNext, - missions.HasPrevious + missionDefinitions.TotalCount, + missionDefinitions.PageSize, + missionDefinitions.CurrentPage, + missionDefinitions.TotalPages, + missionDefinitions.HasNext, + missionDefinitions.HasPrevious }; Response.Headers.Add( @@ -92,26 +148,45 @@ [FromQuery] MissionQueryStringParameters parameters JsonSerializer.Serialize(metadata) ); - return Ok(missions); + return Ok(missionDefinitions); + } + + /// + /// Lookup mission run by specified id. + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [Route("runs/{id}")] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetMissionRunById([FromRoute] string id) + { + var missioRun = await _missionRunService.ReadById(id); + if (missioRun == null) + return NotFound($"Could not find mission run with id {id}"); + return Ok(missioRun); } /// - /// Lookup mission by specified id. + /// Lookup mission definition by specified id. /// [HttpGet] [Authorize(Roles = Role.Any)] - [Route("{id}")] - [ProducesResponseType(typeof(Mission), StatusCodes.Status200OK)] + [Route("definitions/{id}")] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> GetMissionById([FromRoute] string id) + public async Task> GetMissionDefinitionById([FromRoute] string id) { - var mission = await _missionService.ReadById(id); - if (mission == null) - return NotFound($"Could not find mission with id {id}"); - return Ok(mission); + var missionDefinition = await _missionDefinitionService.ReadById(id); + if (missionDefinition == null) + return NotFound($"Could not find mission definition with id {id}"); + return Ok(missionDefinition); } /// @@ -119,7 +194,7 @@ public async Task> GetMissionById([FromRoute] string id) /// [HttpGet] [Authorize(Roles = Role.Any)] - [Route("{assetCode}/{mapName}/map")] + [Route("{installationCode}/{mapName}/map")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] @@ -127,11 +202,11 @@ public async Task> GetMissionById([FromRoute] string id) [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] [ProducesResponseType(StatusCodes.Status502BadGateway)] - public async Task> GetMap([FromRoute] string assetCode, string mapName) + public async Task> GetMap([FromRoute] string installationCode, string mapName) { try { - byte[] mapStream = await _mapService.FetchMapImage(mapName, assetCode); + byte[] mapStream = await _mapService.FetchMapImage(mapName, installationCode); return File(mapStream, "image/png"); } catch (Azure.RequestFailedException) @@ -141,23 +216,101 @@ public async Task> GetMap([FromRoute] string assetCode, str } /// - /// Schedule a new mission + /// Schedule an existing mission definition /// /// - /// This query schedules a new mission and adds it to the database + /// This query schedules an existing mission and adds it to the database + /// + [HttpPost("schedule")] + [Authorize(Roles = Role.User)] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> Schedule( + [FromBody] ScheduleMissionQuery scheduledMissionQuery + ) + { + var robot = await _robotService.ReadById(scheduledMissionQuery.RobotId); + if (robot is null) + return NotFound($"Could not find robot with id {scheduledMissionQuery.RobotId}"); + + var missionDefinition = await _missionDefinitionService.ReadById(scheduledMissionQuery.MissionDefinitionId); + if (missionDefinition == null) + return NotFound("Mission definition not found"); + + List? missionTasks; + missionTasks = missionDefinition.Source.Type switch + { + MissionSourceType.Echo => + // CultureInfo is not important here since we are not using decimal points + missionTasks = _echoService.GetMissionById( + int.Parse(missionDefinition.Source.SourceId, new CultureInfo("en-US")) + ).Result.Tags + .Select( + t => + { + var tagPosition = _stidService + .GetTagPosition(t.TagId, missionDefinition.InstallationCode) + .Result; + return new MissionTask(t, tagPosition); + } + ) + .ToList(), + MissionSourceType.Custom => + missionTasks = await _customMissionService.GetMissionTasksFromMissionId(missionDefinition.Source.SourceId), + _ => + throw new MissionSourceTypeException($"Mission type {missionDefinition.Source.Type} is not accounted for") + }; + + if (missionTasks == null) + return NotFound("No mission tasks were found for the requested mission"); + + var missionRun = new MissionRun + { + Name = missionDefinition.Name, + Robot = robot, + MissionId = missionDefinition.Id, + Status = MissionStatus.Pending, + DesiredStartTime = scheduledMissionQuery.DesiredStartTime, + Tasks = missionTasks, + InstallationCode = missionDefinition.InstallationCode, + Area = missionDefinition.Area, + MapMetadata = new MapMetadata() + }; + + await _mapService.AssignMapToMission(missionRun); + + if (missionRun.Tasks.Any()) + missionRun.CalculateEstimatedDuration(); + + var newMissionRun = await _missionRunService.Create(missionRun); + + return CreatedAtAction(nameof(GetMissionRunById), new { id = newMissionRun.Id }, newMissionRun); + } + + /// + /// Schedule a new echo mission + /// + /// + /// This query schedules a new echo mission and adds it to the database /// [HttpPost] [Authorize(Roles = Role.User)] - [ProducesResponseType(typeof(Mission), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> Create( + public async Task> Create( [FromBody] ScheduledMissionQuery scheduledMissionQuery ) { + // TODO: once we have a good way of creating mission definitions for echo missions, + // we can delete this endpoint var robot = await _robotService.ReadById(scheduledMissionQuery.RobotId); if (robot is null) return NotFound($"Could not find robot with id {scheduledMissionQuery.RobotId}"); @@ -200,33 +353,60 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery t => { var tagPosition = _stidService - .GetTagPosition(t.TagId, scheduledMissionQuery.AssetCode) + .GetTagPosition(t.TagId, scheduledMissionQuery.InstallationCode) .Result; return new MissionTask(t, tagPosition); } ) .ToList(); - var scheduledMission = new Mission + var area = await _areaService.ReadByInstallationAndName(scheduledMissionQuery.InstallationCode, scheduledMissionQuery.AreaName); + + if (area == null) + { + // This is disabled for now as the Area database is not yet populated + //return NotFound($"Could not find area with name {scheduledMissionQuery.AreaName} in installation {scheduledMissionQuery.InstallationCode}"); + } + + // TODO: search for if a source with the given type and URL exists, then reuse it + + var scheduledMissionDefinition = new MissionDefinition + { + Id = Guid.NewGuid().ToString(), + Source = new Source + { + SourceId = $"{echoMission.Id}", + Type = MissionSourceType.Echo + }, + Name = echoMission.Name, + InspectionFrequency = scheduledMissionQuery.InspectionFrequency, + InstallationCode = scheduledMissionQuery.InstallationCode, + Area = area + }; + + var missionRun = new MissionRun { Name = echoMission.Name, Robot = robot, - EchoMissionId = scheduledMissionQuery.EchoMissionId, + MissionId = scheduledMissionDefinition.Id, Status = MissionStatus.Pending, DesiredStartTime = scheduledMissionQuery.DesiredStartTime, Tasks = missionTasks, - AssetCode = scheduledMissionQuery.AssetCode, + InstallationCode = scheduledMissionQuery.InstallationCode, + Area = area, MapMetadata = new MapMetadata() }; - await _mapService.AssignMapToMission(scheduledMission); + await _mapService.AssignMapToMission(missionRun); - if (scheduledMission.Tasks.Any()) - scheduledMission.CalculateEstimatedDuration(); + if (missionRun.Tasks.Any()) + missionRun.CalculateEstimatedDuration(); + + var newMissionDefinition = await _missionDefinitionService.Create(scheduledMissionDefinition); - var newMission = await _missionService.Create(scheduledMission); + var newMissionRun = await _missionRunService.Create(missionRun); - return CreatedAtAction(nameof(GetMissionById), new { id = newMission.Id }, newMission); + return CreatedAtAction(nameof(GetMissionRunById), new { id = newMissionRun.Id }, newMissionRun); } /// @@ -238,13 +418,13 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery [HttpPost] [Authorize(Roles = Role.User)] [Route("custom")] - [ProducesResponseType(typeof(Mission), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> Create( + public async Task> Create( [FromBody] CustomMissionQuery customMissionQuery ) { @@ -254,16 +434,38 @@ [FromBody] CustomMissionQuery customMissionQuery var missionTasks = customMissionQuery.Tasks.Select(task => new MissionTask(task)).ToList(); - var scheduledMission = new Mission + var area = await _areaService.ReadByInstallationAndName(customMissionQuery.InstallationCode, customMissionQuery.AreaName); + + if (area == null) + return NotFound($"Could not find area with name {customMissionQuery.AreaName} in installation {customMissionQuery.InstallationCode}"); + + string sourceURL = _customMissionService.UploadSource(missionTasks); + + var customMissionDefinition = new MissionDefinition + { + Source = new Source + { + SourceId = sourceURL.ToString(), + Type = MissionSourceType.Echo + }, + Name = customMissionQuery.Name, + InspectionFrequency = customMissionQuery.InspectionFrequency, + InstallationCode = customMissionQuery.InstallationCode, + Area = area + }; + + var scheduledMission = new MissionRun { Name = customMissionQuery.Name, Description = customMissionQuery.Description, + MissionId = customMissionDefinition.Id, Comment = customMissionQuery.Comment, Robot = robot, Status = MissionStatus.Pending, DesiredStartTime = customMissionQuery.DesiredStartTime ?? DateTimeOffset.UtcNow, Tasks = missionTasks, - AssetCode = customMissionQuery.AssetCode, + InstallationCode = customMissionQuery.InstallationCode, + Area = area, MapMetadata = new MapMetadata() }; @@ -272,27 +474,48 @@ [FromBody] CustomMissionQuery customMissionQuery if (scheduledMission.Tasks.Any()) scheduledMission.CalculateEstimatedDuration(); - var newMission = await _missionService.Create(scheduledMission); + var newMissionDefinition = await _missionDefinitionService.Create(customMissionDefinition); + + var newMissionRun = await _missionRunService.Create(scheduledMission); + + return CreatedAtAction(nameof(GetMissionRunById), new { id = newMissionRun.Id }, newMissionRun); + } - return CreatedAtAction(nameof(GetMissionById), new { id = newMission.Id }, newMission); + /// + /// Deletes the mission definition with the specified id from the database. + /// + [HttpDelete] + [Authorize(Roles = Role.Admin)] + [Route("definitions/{id}")] + [ProducesResponseType(typeof(MissionDefinition), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> DeleteMissionDefinition([FromRoute] string id) + { + var missionDefinition = await _missionDefinitionService.Delete(id); + if (missionDefinition is null) + return NotFound($"Mission definition with id {id} not found"); + return Ok(missionDefinition); } /// - /// Deletes the mission with the specified id from the database. + /// Deletes the mission run with the specified id from the database. /// [HttpDelete] [Authorize(Roles = Role.Admin)] - [Route("{id}")] - [ProducesResponseType(typeof(Mission), StatusCodes.Status200OK)] + [Route("runs/{id}")] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> DeleteMission([FromRoute] string id) + public async Task> DeleteMissionRun([FromRoute] string id) { - var mission = await _missionService.Delete(id); - if (mission is null) - return NotFound($"Mission with id {id} not found"); - return Ok(mission); + var missionRun = await _missionRunService.Delete(id); + if (missionRun is null) + return NotFound($"Mission run with id {id} not found"); + return Ok(missionRun); } } diff --git a/backend/api/Controllers/Models/AreaResponse.cs b/backend/api/Controllers/Models/AreaResponse.cs new file mode 100644 index 000000000..b0d5e2b8b --- /dev/null +++ b/backend/api/Controllers/Models/AreaResponse.cs @@ -0,0 +1,22 @@ +#pragma warning disable CS8618 +namespace Api.Database.Models +{ + public class AreaResponse + { + public string Id { get; set; } + + public string DeckName { get; set; } + + public string PlantCode { get; set; } + + public string InstallationCode { get; set; } + + public string AreaName { get; set; } + + public MapMetadata MapMetadata { get; set; } + + public Pose DefaultLocalizationPose { get; set; } + + public IList SafePositions { get; set; } + } +} diff --git a/backend/api/Controllers/Models/MissionDefinition.cs b/backend/api/Controllers/Models/CondensedMissionDefinition.cs similarity index 62% rename from backend/api/Controllers/Models/MissionDefinition.cs rename to backend/api/Controllers/Models/CondensedMissionDefinition.cs index 415287018..65d832226 100644 --- a/backend/api/Controllers/Models/MissionDefinition.cs +++ b/backend/api/Controllers/Models/CondensedMissionDefinition.cs @@ -1,12 +1,12 @@ # nullable disable namespace Api.Controllers.Models { - public class MissionDefinition + public class CondensedMissionDefinition { public int EchoMissionId { get; set; } public string Name { get; set; } - public string AssetCode { get; set; } + public string InstallationCode { get; set; } } } diff --git a/backend/api/Controllers/Models/CreateAssetDeckQuery.cs b/backend/api/Controllers/Models/CreateAreaQuery.cs similarity index 50% rename from backend/api/Controllers/Models/CreateAssetDeckQuery.cs rename to backend/api/Controllers/Models/CreateAreaQuery.cs index 107249548..a90ea30f8 100644 --- a/backend/api/Controllers/Models/CreateAssetDeckQuery.cs +++ b/backend/api/Controllers/Models/CreateAreaQuery.cs @@ -2,11 +2,12 @@ namespace Api.Controllers.Models { - public struct CreateAssetDeckQuery + public struct CreateAreaQuery { - public string AssetCode { get; set; } - + public string InstallationCode { get; set; } + public string PlantCode { get; set; } public string DeckName { get; set; } + public string AreaName { get; set; } public Pose DefaultLocalizationPose { get; set; } } diff --git a/backend/api/Controllers/Models/CreateDeckQuery.cs b/backend/api/Controllers/Models/CreateDeckQuery.cs new file mode 100644 index 000000000..284e0584b --- /dev/null +++ b/backend/api/Controllers/Models/CreateDeckQuery.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Models +{ + public struct CreateDeckQuery + { + public string InstallationCode { get; set; } + public string PlantCode { get; set; } + public string Name { get; set; } + } +} diff --git a/backend/api/Controllers/Models/CreateInstallationQuery.cs b/backend/api/Controllers/Models/CreateInstallationQuery.cs new file mode 100644 index 000000000..e28214ab9 --- /dev/null +++ b/backend/api/Controllers/Models/CreateInstallationQuery.cs @@ -0,0 +1,8 @@ +namespace Api.Controllers.Models +{ + public struct CreateInstallationQuery + { + public string InstallationCode { get; set; } + public string Name { get; set; } + } +} diff --git a/backend/api/Controllers/Models/CreatePlantQuery.cs b/backend/api/Controllers/Models/CreatePlantQuery.cs new file mode 100644 index 000000000..7812c7793 --- /dev/null +++ b/backend/api/Controllers/Models/CreatePlantQuery.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Models +{ + public struct CreatePlantQuery + { + public string InstallationCode { get; set; } + public string PlantCode { get; set; } + public string Name { get; set; } + } +} diff --git a/backend/api/Controllers/Models/CreateRobotQuery.cs b/backend/api/Controllers/Models/CreateRobotQuery.cs index b165d6763..b0ce047df 100644 --- a/backend/api/Controllers/Models/CreateRobotQuery.cs +++ b/backend/api/Controllers/Models/CreateRobotQuery.cs @@ -12,9 +12,9 @@ public struct CreateRobotQuery public string SerialNumber { get; set; } - public string CurrentAsset { get; set; } + public string CurrentInstallation { get; set; } - public AssetDeck CurrentAssetDeck { get; set; } + public Area CurrentArea { get; set; } public IList VideoStreams { get; set; } diff --git a/backend/api/Controllers/Models/CustomMissionQuery.cs b/backend/api/Controllers/Models/CustomMissionQuery.cs index ccb14fca6..06227546b 100644 --- a/backend/api/Controllers/Models/CustomMissionQuery.cs +++ b/backend/api/Controllers/Models/CustomMissionQuery.cs @@ -32,7 +32,11 @@ public struct CustomMissionQuery public DateTimeOffset? DesiredStartTime { get; set; } - public string AssetCode { get; set; } + public string InstallationCode { get; set; } + + public TimeSpan? InspectionFrequency { get; set; } + + public string AreaName { get; set; } public string Name { get; set; } diff --git a/backend/api/Controllers/Models/EchoMission.cs b/backend/api/Controllers/Models/EchoMission.cs index 9c4d053a3..2b8a27765 100644 --- a/backend/api/Controllers/Models/EchoMission.cs +++ b/backend/api/Controllers/Models/EchoMission.cs @@ -7,7 +7,7 @@ public class EchoMission public string Name { get; set; } - public string AssetCode { get; set; } + public string InstallationCode { get; set; } public Uri URL { get; set; } diff --git a/backend/api/Controllers/Models/EchoPlantInfo.cs b/backend/api/Controllers/Models/EchoPlantInfo.cs index c03366abc..9e955004d 100644 --- a/backend/api/Controllers/Models/EchoPlantInfo.cs +++ b/backend/api/Controllers/Models/EchoPlantInfo.cs @@ -3,7 +3,7 @@ namespace Api.Controllers.Models { public class EchoPlantInfo { - public string InstallationCode { get; set; } + public string PlantCode { get; set; } public string ProjectDescription { get; set; } } } diff --git a/backend/api/Controllers/Models/MissionDefinitionQueryStringParameters.cs b/backend/api/Controllers/Models/MissionDefinitionQueryStringParameters.cs new file mode 100644 index 000000000..70f5da587 --- /dev/null +++ b/backend/api/Controllers/Models/MissionDefinitionQueryStringParameters.cs @@ -0,0 +1,33 @@ +using Api.Database.Models; + +namespace Api.Controllers.Models +{ + public class MissionDefinitionQueryStringParameters : QueryStringParameters + { + public MissionDefinitionQueryStringParameters() + { + // Default order is desired start time + OrderBy = "DesiredStartTime desc"; + } + + /// + /// Filter for the installation code of the mission + /// + public string? InstallationCode { get; set; } + + /// + /// Filter for the area of the mission + /// + public string? Area { get; set; } + + /// + /// The search parameter for the mission name + /// + public string? NameSearch { get; set; } + + /// + /// The search parameter for the mission source type + /// + public MissionSourceType? SourceType { get; set; } + } +} diff --git a/backend/api/Controllers/Models/MissionQueryStringParameters.cs b/backend/api/Controllers/Models/MissionRunQueryStringParameters.cs similarity index 87% rename from backend/api/Controllers/Models/MissionQueryStringParameters.cs rename to backend/api/Controllers/Models/MissionRunQueryStringParameters.cs index daf735c5a..dbff7e22a 100644 --- a/backend/api/Controllers/Models/MissionQueryStringParameters.cs +++ b/backend/api/Controllers/Models/MissionRunQueryStringParameters.cs @@ -2,9 +2,9 @@ namespace Api.Controllers.Models { - public class MissionQueryStringParameters : QueryStringParameters + public class MissionRunQueryStringParameters : QueryStringParameters { - public MissionQueryStringParameters() + public MissionRunQueryStringParameters() { // Default order is desired start time OrderBy = "DesiredStartTime desc"; @@ -16,9 +16,14 @@ public MissionQueryStringParameters() public List? Statuses { get; set; } /// - /// Filter for the asset code of the mission + /// Filter for the installation code of the mission /// - public string? AssetCode { get; set; } + public string? InstallationCode { get; set; } + + /// + /// Filter for the area of the mission + /// + public string? Area { get; set; } /// /// Filter for the robot id of the robot assigned to the mission @@ -83,6 +88,5 @@ public MissionQueryStringParameters() public long MaxDesiredStartTime { get; set; } = DateTimeOffset.MaxValue.ToUnixTimeSeconds(); #endregion Time Filters - } } diff --git a/backend/api/Controllers/Models/ScheduleLocalizationMissionQuery.cs b/backend/api/Controllers/Models/ScheduleLocalizationMissionQuery.cs index 7a160bd06..7a9fd8f23 100644 --- a/backend/api/Controllers/Models/ScheduleLocalizationMissionQuery.cs +++ b/backend/api/Controllers/Models/ScheduleLocalizationMissionQuery.cs @@ -5,7 +5,7 @@ namespace Api.Controllers.Models public struct ScheduleLocalizationMissionQuery { public string RobotId { get; set; } - public string DeckId { get; set; } + public string AreaId { get; set; } public Pose LocalizationPose { get; set; } } } diff --git a/backend/api/Controllers/Models/ScheduleMissionQuery.cs b/backend/api/Controllers/Models/ScheduleMissionQuery.cs new file mode 100644 index 000000000..921c714cd --- /dev/null +++ b/backend/api/Controllers/Models/ScheduleMissionQuery.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Models +{ + public class ScheduleMissionQuery + { + public string MissionDefinitionId { get; set; } + public string RobotId { get; set; } + public DateTimeOffset DesiredStartTime { get; set; } + } +} diff --git a/backend/api/Controllers/Models/ScheduledMissionQuery.cs b/backend/api/Controllers/Models/ScheduledMissionQuery.cs index 1960936fc..52e580984 100644 --- a/backend/api/Controllers/Models/ScheduledMissionQuery.cs +++ b/backend/api/Controllers/Models/ScheduledMissionQuery.cs @@ -5,6 +5,8 @@ public struct ScheduledMissionQuery public string RobotId { get; set; } public int EchoMissionId { get; set; } public DateTimeOffset DesiredStartTime { get; set; } - public string AssetCode { get; set; } + public string InstallationCode { get; set; } + public string AreaName { get; set; } + public TimeSpan? InspectionFrequency { get; set; } } } diff --git a/backend/api/Controllers/PlantController.cs b/backend/api/Controllers/PlantController.cs new file mode 100644 index 000000000..99391ac64 --- /dev/null +++ b/backend/api/Controllers/PlantController.cs @@ -0,0 +1,155 @@ +using Api.Controllers.Models; +using Api.Database.Models; +using Api.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Api.Controllers +{ + [ApiController] + [Route("plants")] + public class PlantController : ControllerBase + { + private readonly IPlantService _plantService; + private readonly IInstallationService _installationService; + + private readonly IMapService _mapService; + + private readonly ILogger _logger; + + public PlantController( + ILogger logger, + IMapService mapService, + IPlantService plantService, + IInstallationService installationService + ) + { + _logger = logger; + _mapService = mapService; + _plantService = plantService; + _installationService = installationService; + } + + /// + /// List all plants in the Flotilla database + /// + /// + /// This query gets all plants + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [ProducesResponseType(typeof(IList), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task>> GetPlants() + { + try + { + var plants = await _plantService.ReadAll(); + return Ok(plants); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of plants from database"); + throw; + } + } + + /// + /// Lookup plant by specified id. + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [Route("{id}")] + [ProducesResponseType(typeof(Plant), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetPlantById([FromRoute] string id) + { + try + { + var plant = await _plantService.ReadById(id); + if (plant == null) + return NotFound($"Could not find plant with id {id}"); + return Ok(plant); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of plant from database"); + throw; + } + + } + + /// + /// Add a new plant + /// + /// + /// This query adds a new plant to the database + /// + [HttpPost] + [Authorize(Roles = Role.Admin)] + [ProducesResponseType(typeof(Plant), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> Create([FromBody] CreatePlantQuery plant) + { + _logger.LogInformation("Creating new plant"); + try + { + var existingInstallation = await _installationService.ReadByName(plant.InstallationCode); + if (existingInstallation == null) + { + return NotFound($"Installation with installation code {plant.InstallationCode} not found"); + } + var existingPlant = await _plantService.ReadByInstallationAndName(existingInstallation, plant.PlantCode); + if (existingPlant != null) + { + _logger.LogInformation("A plant for given name and plant already exists"); + return BadRequest($"Plant already exists"); + } + + var newPlant = await _plantService.Create(plant); + _logger.LogInformation( + "Succesfully created new plant with id '{plantId}'", + newPlant.Id + ); + return CreatedAtAction( + nameof(GetPlantById), + new { id = newPlant.Id }, + newPlant + ); + } + catch (Exception e) + { + _logger.LogError(e, "Error while creating new plant"); + throw; + } + } + + /// + /// Deletes the plant with the specified id from the database. + /// + [HttpDelete] + [Authorize(Roles = Role.Admin)] + [Route("{id}")] + [ProducesResponseType(typeof(Plant), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> DeletePlant([FromRoute] string id) + { + var plant = await _plantService.Delete(id); + if (plant is null) + return NotFound($"Plant with id {id} not found"); + return Ok(plant); + } + } +} diff --git a/backend/api/Controllers/RobotController.cs b/backend/api/Controllers/RobotController.cs index 9c195b077..e1131aef8 100644 --- a/backend/api/Controllers/RobotController.cs +++ b/backend/api/Controllers/RobotController.cs @@ -16,29 +16,29 @@ public class RobotController : ControllerBase private readonly ILogger _logger; private readonly IRobotService _robotService; private readonly IIsarService _isarService; - private readonly IMissionService _missionService; + private readonly IMissionRunService _missionRunService; private readonly IRobotModelService _robotModelService; - private readonly IAssetDeckService _assetDeckService; + private readonly IAreaService _areaService; public RobotController( ILogger logger, IRobotService robotService, IIsarService isarService, - IMissionService missionService, + IMissionRunService missionRunService, IRobotModelService robotModelService, - IAssetDeckService assetDeckService + IAreaService areaService ) { _logger = logger; _robotService = robotService; _isarService = isarService; - _missionService = missionService; + _missionRunService = missionRunService; _robotModelService = robotModelService; - _assetDeckService = assetDeckService; + _areaService = areaService; } /// - /// List all robots on the asset. + /// List all robots on the installation. /// /// /// This query gets all robots @@ -190,7 +190,7 @@ [FromBody] Robot robot [HttpDelete] [Authorize(Roles = Role.Admin)] [Route("{id}")] - [ProducesResponseType(typeof(Mission), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -324,23 +324,23 @@ [FromBody] VideoStream videoStream } /// - /// Start the mission in the database with the corresponding 'missionId' for the robot with id 'robotId' + /// Start the mission in the database with the corresponding 'missionRunId' for the robot with id 'robotId' /// /// /// This query starts a mission for a given robot /// [HttpPost] [Authorize(Roles = Role.Admin)] - [Route("{robotId}/start/{missionId}")] - [ProducesResponseType(typeof(Mission), StatusCodes.Status200OK)] + [Route("{robotId}/start/{missionRunId}")] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> StartMission( + public async Task> StartMission( [FromRoute] string robotId, - [FromRoute] string missionId + [FromRoute] string missionRunId ) { var robot = await _robotService.ReadById(robotId); @@ -360,18 +360,18 @@ [FromRoute] string missionId return Conflict($"The Robot is not available ({robot.Status})"); } - var mission = await _missionService.ReadById(missionId); + var missionRun = await _missionRunService.ReadById(missionRunId); - if (mission == null) + if (missionRun == null) { - _logger.LogWarning("Could not find mission with id={id}", missionId); + _logger.LogWarning("Could not find mission with id={id}", missionRunId); return NotFound("Mission not found"); } IsarMission isarMission; try { - isarMission = await _isarService.StartMission(robot, mission); + isarMission = await _isarService.StartMission(robot, missionRun); } catch (HttpRequestException e) { @@ -399,26 +399,26 @@ [FromRoute] string missionId return StatusCode(StatusCodes.Status500InternalServerError, message); } - mission.UpdateWithIsarInfo(isarMission); - mission.Status = MissionStatus.Ongoing; + missionRun.UpdateWithIsarInfo(isarMission); + missionRun.Status = MissionStatus.Ongoing; - await _missionService.Update(mission); + await _missionRunService.Update(missionRun); if (robot.CurrentMissionId != null) { - var orphanedMission = await _missionService.ReadById(robot.CurrentMissionId); - if (orphanedMission != null) + var orphanedMissionRun = await _missionRunService.ReadById(robot.CurrentMissionId); + if (orphanedMissionRun != null) { - orphanedMission.SetToFailed(); - await _missionService.Update(orphanedMission); + orphanedMissionRun.SetToFailed(); + await _missionRunService.Update(orphanedMissionRun); } } robot.Status = RobotStatus.Busy; - robot.CurrentMissionId = mission.Id; + robot.CurrentMissionId = missionRun.Id; await _robotService.Update(robot); - return Ok(mission); + return Ok(missionRun); } /// @@ -644,15 +644,14 @@ [FromRoute] string armPosition [HttpPost] [Authorize(Roles = Role.User)] [Route("start-localization")] - [ProducesResponseType(typeof(Mission), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> StartLocalizationMission( + public async Task> StartLocalizationMission( [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery - ) { var robot = await _robotService.ReadById(scheduleLocalizationMissionQuery.RobotId); @@ -672,25 +671,26 @@ [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery return Conflict($"The Robot is not available ({robot.Status})"); } - var mission = new Mission + var area = await _areaService.ReadById(scheduleLocalizationMissionQuery.AreaId); + + if (area == null) + { + _logger.LogWarning("Could not find area with id={id}", scheduleLocalizationMissionQuery.AreaId); + return NotFound("Area not found"); + } + + var missionRun = new MissionRun { Name = "Localization Mission", Robot = robot, - AssetCode = "NA", + InstallationCode = "NA", + Area = area, Status = MissionStatus.Pending, DesiredStartTime = DateTimeOffset.UtcNow, Tasks = new List(), MapMetadata = new MapMetadata() }; - var deck = await _assetDeckService.ReadById(scheduleLocalizationMissionQuery.DeckId); - - if (deck == null) - { - _logger.LogWarning("Could not find deck with id={id}", scheduleLocalizationMissionQuery.DeckId); - return NotFound("Deck not found"); - } - IsarMission isarMission; try { @@ -715,16 +715,16 @@ [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery return StatusCode(StatusCodes.Status500InternalServerError, message); } - mission.UpdateWithIsarInfo(isarMission); - mission.Status = MissionStatus.Ongoing; - await _missionService.Create(mission); + missionRun.UpdateWithIsarInfo(isarMission); + missionRun.Status = MissionStatus.Ongoing; - robot.Status = RobotStatus.Busy; - robot.CurrentMissionId = mission.Id; + await _missionRunService.Create(missionRun); - robot.CurrentAssetDeck = deck; + robot.Status = RobotStatus.Busy; + robot.CurrentMissionId = missionRun.Id; await _robotService.Update(robot); - return Ok(mission); + robot.CurrentArea = area; + return Ok(missionRun); } /// @@ -734,18 +734,18 @@ [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery /// This query starts a localization for a given robot /// [HttpPost] - [Route("{robotId}/{asset}/{deck}/go-to-safe-position")] + [Route("{robotId}/{installation}/{areaName}/go-to-safe-position")] [Authorize(Roles = Role.User)] - [ProducesResponseType(typeof(Mission), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> SafePosition( + public async Task> SendRobotToSafePosition( [FromRoute] string robotId, - [FromRoute] string asset, - [FromRoute] string deck + [FromRoute] string installation, + [FromRoute] string areaName ) { var robot = await _robotService.ReadById(robotId); @@ -755,24 +755,24 @@ [FromRoute] string deck return NotFound("Robot not found"); } - var assets = await _assetDeckService.ReadByAsset(asset); + var installations = await _areaService.ReadByInstallation(installation); - if (!assets.Any()) + if (!installations.Any()) { - _logger.LogWarning("Could not find asset={asset}", asset); - return NotFound("No asset found"); + _logger.LogWarning("Could not find installation={installation}", installation); + return NotFound("No installation found"); } - var assetDeck = await _assetDeckService.ReadByAssetAndDeck(asset, deck); - if (assetDeck is null) + var area = await _areaService.ReadByInstallationAndName(installation, areaName); + if (area is null) { - _logger.LogWarning("Could not find deck={deck}", deck); - return NotFound("No deck found"); + _logger.LogWarning("Could not find area={areaName}", areaName); + return NotFound("No area found"); } - if (assetDeck.SafePositions.Count < 1) + if (area.SafePositions.Count < 1) { - _logger.LogWarning("No safe position for asset={asset}, deck={deck}", asset, deck); + _logger.LogWarning("No safe position for installation={installation}, area={areaName}", installation, areaName); return NotFound("No safe positions found"); } @@ -797,7 +797,7 @@ [FromRoute] string deck return StatusCode(StatusCodes.Status502BadGateway, message); } - var closestSafePosition = ClosestSafePosition(robot.Pose, assetDeck.SafePositions); + var closestSafePosition = ClosestSafePosition(robot.Pose, area.SafePositions); // Cloning to avoid tracking same object var clonedPose = ObjectCopier.Clone(closestSafePosition); var customTaskQuery = new CustomTaskQuery @@ -807,11 +807,13 @@ [FromRoute] string deck InspectionTarget = new Position(), TaskOrder = 0 }; - var mission = new Mission + // TODO: The MissionId is nullable because of this mission + var missionRun = new MissionRun { Name = "Drive to Safe Position", Robot = robot, - AssetCode = assetDeck.AssetCode, + InstallationCode = installation, + Area = area, Status = MissionStatus.Pending, DesiredStartTime = DateTimeOffset.UtcNow, Tasks = new List(new[] { new MissionTask(customTaskQuery) }), @@ -821,7 +823,7 @@ [FromRoute] string deck IsarMission isarMission; try { - isarMission = await _isarService.StartMission(robot, mission); + isarMission = await _isarService.StartMission(robot, missionRun); } catch (HttpRequestException e) { @@ -842,32 +844,31 @@ [FromRoute] string deck return StatusCode(StatusCodes.Status500InternalServerError, message); } - mission.UpdateWithIsarInfo(isarMission); - mission.Status = MissionStatus.Ongoing; + missionRun.UpdateWithIsarInfo(isarMission); + missionRun.Status = MissionStatus.Ongoing; - await _missionService.Create(mission); + await _missionRunService.Create(missionRun); robot.Status = RobotStatus.Busy; - robot.CurrentMissionId = mission.Id; + robot.CurrentMissionId = missionRun.Id; await _robotService.Update(robot); - return Ok(mission); + return Ok(missionRun); } - private async void OnIsarUnavailable(Robot robot) { robot.Enabled = false; robot.Status = RobotStatus.Offline; if (robot.CurrentMissionId != null) { - var mission = await _missionService.ReadById(robot.CurrentMissionId); - if (mission != null) + var missionRun = await _missionRunService.ReadById(robot.CurrentMissionId); + if (missionRun != null) { - mission.SetToFailed(); - await _missionService.Update(mission); + missionRun.SetToFailed(); + await _missionRunService.Update(missionRun); _logger.LogWarning( "Mission '{id}' failed because ISAR could not be reached", - mission.Id + missionRun.Id ); } } @@ -903,5 +904,4 @@ private static float CalculateDistance(Pose pose1, Pose pose2) var pos2 = pose2.Position; return (float)Math.Sqrt(Math.Pow(pos1.X - pos2.X, 2) + Math.Pow(pos1.Y - pos2.Y, 2) + Math.Pow(pos1.Z - pos2.Z, 2)); } - } diff --git a/backend/api/Controllers/RobotModelController.cs b/backend/api/Controllers/RobotModelController.cs index b7ae325c3..8a749cb69 100644 --- a/backend/api/Controllers/RobotModelController.cs +++ b/backend/api/Controllers/RobotModelController.cs @@ -213,7 +213,7 @@ public async Task> DeleteRobotModel([FromRoute] string { var robotModel = await _robotModelService.Delete(id); if (robotModel is null) - return NotFound($"Asset deck with id {id} not found"); + return NotFound($"Area with id {id} not found"); return Ok(robotModel); } diff --git a/backend/api/Database/Context/FlotillaDbContext.cs b/backend/api/Database/Context/FlotillaDbContext.cs index 073daaaf0..d6c958f0b 100644 --- a/backend/api/Database/Context/FlotillaDbContext.cs +++ b/backend/api/Database/Context/FlotillaDbContext.cs @@ -9,8 +9,14 @@ public class FlotillaDbContext : DbContext { public DbSet Robots => Set(); public DbSet RobotModels => Set(); - public DbSet Missions => Set(); + public DbSet MissionRuns => Set(); + public DbSet MissionDefinitions => Set(); + public DbSet Plants => Set(); + public DbSet Installations => Set(); + public DbSet Decks => Set(); public DbSet AssetDecks => Set(); + public DbSet Areas => Set(); + public DbSet Sources => Set(); public DbSet SafePositions => Set(); public FlotillaDbContext(DbContextOptions options) : base(options) { } @@ -21,13 +27,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // https://docs.microsoft.com/en-us/ef/core/modeling/owned-entities // https://docs.microsoft.com/en-us/ef/core/modeling/owned-entities#collections-of-owned-types - modelBuilder.Entity( - missionEntity => + modelBuilder.Entity( + missionRunEntity => { if (isSqlLite) - AddConverterForDateTimeOffsets(ref missionEntity); - missionEntity.OwnsMany( - mission => mission.Tasks, + AddConverterForDateTimeOffsets(ref missionRunEntity); + missionRunEntity.OwnsMany( + missionRun => missionRun.Tasks, taskEntity => { if (isSqlLite) @@ -51,19 +57,29 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) ); } ); + //missionRunEntity.HasOne(missionRun => missionRun.MissionDefinition); } ); - modelBuilder.Entity().OwnsOne(m => m.MapMetadata).OwnsOne(t => t.TransformationMatrices); - modelBuilder.Entity().OwnsOne(m => m.MapMetadata).OwnsOne(b => b.Boundary); + modelBuilder.Entity().HasOne(m => m.Source); + modelBuilder.Entity().OwnsOne(m => m.MapMetadata).OwnsOne(t => t.TransformationMatrices); + modelBuilder.Entity().OwnsOne(m => m.MapMetadata).OwnsOne(b => b.Boundary); + //modelBuilder.Entity().HasOne(m => m.LastRun).WithOne(m => m.MissionDefinition).HasForeignKey(m => m.Id); modelBuilder.Entity().OwnsOne(r => r.Pose).OwnsOne(p => p.Orientation); modelBuilder.Entity().OwnsOne(r => r.Pose).OwnsOne(p => p.Position); modelBuilder.Entity().OwnsMany(r => r.VideoStreams); - modelBuilder.Entity().OwnsOne(a => a.DefaultLocalizationPose, poseBuilder => + modelBuilder.Entity().OwnsOne(a => a.DefaultLocalizationPose, poseBuilder => { poseBuilder.OwnsOne(pose => pose.Position); poseBuilder.OwnsOne(pose => pose.Orientation); }); + modelBuilder.Entity().HasOne(a => a.Deck); + modelBuilder.Entity().HasOne(a => a.Installation); + modelBuilder.Entity().HasOne(a => a.Plant); + modelBuilder.Entity().HasOne(d => d.Plant); + modelBuilder.Entity().HasOne(d => d.Installation); + modelBuilder.Entity().HasOne(a => a.Installation); + modelBuilder.Entity().OwnsOne(s => s.Pose, poseBuilder => { poseBuilder.OwnsOne(pose => pose.Position); @@ -73,8 +89,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // There can only be one robot model per robot type modelBuilder.Entity().HasIndex(model => model.Type).IsUnique(); - // There can only be one asset deck - modelBuilder.Entity().HasIndex(a => new { a.AssetCode, a.DeckName }).IsUnique(); + // There can only be one unique installation and plant shortname + modelBuilder.Entity().HasIndex(a => new { a.InstallationCode }).IsUnique(); + modelBuilder.Entity().HasIndex(a => new { a.PlantCode }).IsUnique(); } // SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations diff --git a/backend/api/Database/Context/InitDb.cs b/backend/api/Database/Context/InitDb.cs index f4b9e2613..4a944b66e 100644 --- a/backend/api/Database/Context/InitDb.cs +++ b/backend/api/Database/Context/InitDb.cs @@ -5,8 +5,13 @@ namespace Api.Database.Context; public static class InitDb { private static readonly List robots = GetRobots(); - private static readonly List missions = GetMissions(); - private static readonly List assetDecks = GetAssetDecks(); + private static readonly List installations = GetInstallations(); + private static readonly List plants = GetPlants(); + private static readonly List decks = GetDecks(); + private static readonly List areas = GetAreas(); + private static readonly List sources = GetSources(); + private static readonly List missionDefinitions = GetMissionDefinitions(); + private static readonly List missionRuns = GetMissionRuns(); private static VideoStream VideoStream => new() @@ -29,6 +34,102 @@ public static class InitDb RobotPose = new Pose() }; + private static List GetInstallations() + { + var installation1 = new Installation + { + Id = Guid.NewGuid().ToString(), + Name = "Johan Sverdrup", + InstallationCode = "JSV" + }; + + return new List(new Installation[] { installation1 }); + } + + private static List GetPlants() + { + var plant1 = new Plant + { + Id = Guid.NewGuid().ToString(), + Installation = installations[0], + Name = "Johan Sverdrup - P1", + PlantCode = "P1" + }; + + return new List(new Plant[] { plant1 }); + } + + private static List GetDecks() + { + var deck1 = new Deck + { + Id = Guid.NewGuid().ToString(), + Plant = plants[0], + Installation = plants[0].Installation, + Name = "TestDeck" + }; + + return new List(new Deck[] { deck1 }); + } + + private static List GetAreas() + { + var area1 = new Area + { + Id = Guid.NewGuid().ToString(), + Deck = decks[0], + Plant = decks[0].Plant, + Installation = decks[0].Plant.Installation, + Name = "AP320", + MapMetadata = new MapMetadata(), + DefaultLocalizationPose = new Pose { }, + SafePositions = new List() + }; + + var area2 = new Area + { + Id = Guid.NewGuid().ToString(), + Deck = decks[0], + Plant = decks[0].Plant, + Installation = decks[0].Plant.Installation, + Name = "AP330", + MapMetadata = new MapMetadata(), + DefaultLocalizationPose = new Pose { }, + SafePositions = new List() + }; + + var area3 = new Area + { + Id = "TestId", + Deck = decks[0], + Plant = decks[0].Plant, + Installation = decks[0].Plant.Installation, + Name = "testArea", + MapMetadata = new MapMetadata(), + DefaultLocalizationPose = new Pose { }, + SafePositions = new List() + }; + + return new List(new Area[] { area1, area2, area3 }); + } + + private static List GetSources() + { + var source1 = new Source + { + SourceId = "https://google.com/", + Type = MissionSourceType.Echo + }; + + var source2 = new Source + { + SourceId = "https://google.com/", + Type = MissionSourceType.Custom + }; + + return new List(new Source[] { source1, source2 }); + } + private static List GetRobots() { var robot1 = new Robot @@ -40,6 +141,7 @@ private static List GetRobots() Enabled = true, Host = "localhost", Port = 3000, + CurrentInstallation = "JSV", VideoStreams = new List(), Pose = new Pose() }; @@ -53,6 +155,7 @@ private static List GetRobots() Enabled = true, Host = "localhost", Port = 3000, + CurrentInstallation = "JSV", VideoStreams = new List(), Pose = new Pose() }; @@ -66,6 +169,7 @@ private static List GetRobots() Enabled = false, Host = "localhost", Port = 3000, + CurrentInstallation = "JSV", VideoStreams = new List(), Pose = new Pose() }; @@ -73,57 +177,89 @@ private static List GetRobots() return new List(new Robot[] { robot1, robot2, robot3 }); } - private static List GetMissions() + private static List GetMissionDefinitions() + { + var missionDefinition1 = new MissionDefinition + { + Id = Guid.NewGuid().ToString(), + Name = "Placeholder Mission 1", + InstallationCode = areas[0].Deck.Plant.Installation.InstallationCode, + Area = areas[0], + Source = sources[0], + Comment = "Interesting comment", + InspectionFrequency = new DateTime().AddDays(12) - new DateTime(), + LastRun = null + }; + + var missionDefinition2 = new MissionDefinition + { + Id = Guid.NewGuid().ToString(), + Name = "Placeholder Mission 2", + InstallationCode = areas[1].Deck.Plant.Installation.InstallationCode, + Area = areas[1], + Source = sources[1], + InspectionFrequency = new DateTime().AddDays(7) - new DateTime(), + LastRun = null + }; + + var missionDefinition3 = new MissionDefinition + { + Id = Guid.NewGuid().ToString(), + Name = "Placeholder Mission 3", + InstallationCode = areas[1].Deck.Plant.Installation.InstallationCode, + Area = areas[1], + Source = sources[1], + LastRun = null + }; + + return new List(new[] { missionDefinition1, missionDefinition2, missionDefinition3 }); + } + + private static List GetMissionRuns() { - var mission1 = new Mission + var missionRun1 = new MissionRun { Name = "Placeholder Mission 1", Robot = robots[0], - AssetCode = "test", - EchoMissionId = 95, + InstallationCode = areas[0].Deck.Plant.Installation.InstallationCode, + Area = areas[0], + MissionId = missionDefinitions[0].Id, Status = MissionStatus.Successful, DesiredStartTime = DateTimeOffset.UtcNow, Tasks = new List(), MapMetadata = new MapMetadata() }; - var mission2 = new Mission + var missionRun2 = new MissionRun { Name = "Placeholder Mission 2", Robot = robots[1], - AssetCode = "test", - EchoMissionId = 95, + InstallationCode = areas[1].Deck.Plant.Installation.InstallationCode, + Area = areas[1], + MissionId = missionDefinitions[0].Id, Status = MissionStatus.Successful, DesiredStartTime = DateTimeOffset.UtcNow, Tasks = new List(), MapMetadata = new MapMetadata() }; + missionDefinitions[0].LastRun = missionRun2; - var mission3 = new Mission + var missionRun3 = new MissionRun { Name = "Placeholder Mission 3", Robot = robots[2], - AssetCode = "kaa", + InstallationCode = areas[1].Deck.Plant.Installation.InstallationCode, + Area = areas[1], + MissionId = missionDefinitions[1].Id, Status = MissionStatus.Successful, DesiredStartTime = DateTimeOffset.UtcNow, Tasks = new List(), MapMetadata = new MapMetadata() }; - return new List(new[] { mission1, mission2, mission3 }); - } - - private static List GetAssetDecks() - { - var assetDeck1 = new AssetDeck - { - Id = "TestId", - DeckName = "Placeholder deck 1", - AssetCode = "Placeholder asset", - DefaultLocalizationPose = new Pose() - }; + missionDefinitions[1].LastRun = missionRun3; - return new List(new[] { assetDeck1 }); + return new List(new[] { missionRun1, missionRun2, missionRun3 }); } public static void PopulateDb(FlotillaDbContext context) @@ -159,17 +295,18 @@ public static void PopulateDb(FlotillaDbContext context) robots[1].Model = models.Find(model => model.Type == RobotType.ExR2)!; robots[2].Model = models.Find(model => model.Type == RobotType.AnymalX)!; - foreach (var mission in missions) + foreach (var missionRun in missionRuns) { var task = ExampleTask; task.Inspections.Add(Inspection); task.Inspections.Add(Inspection2); var tasks = new List { task }; - mission.Tasks = tasks; + missionRun.Tasks = tasks; } context.AddRange(robots); - context.AddRange(missions); - context.AddRange(assetDecks); + context.AddRange(missionDefinitions); + context.AddRange(missionRuns); + context.AddRange(areas); context.SaveChanges(); } } diff --git a/backend/api/Database/Models/Area.cs b/backend/api/Database/Models/Area.cs new file mode 100644 index 000000000..029a9c9b4 --- /dev/null +++ b/backend/api/Database/Models/Area.cs @@ -0,0 +1,53 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +#pragma warning disable CS8618 +namespace Api.Database.Models +{ + public class Area + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } + + [Required] + public virtual Deck Deck { get; set; } + + [Required] + public virtual Plant Plant { get; set; } + + [Required] + public virtual Installation Installation { get; set; } + + [Required] + [MaxLength(200)] + public string Name { get; set; } + + [Required] + public MapMetadata MapMetadata { get; set; } + + [Required] + public Pose DefaultLocalizationPose { get; set; } + + public IList SafePositions { get; set; } + } + + public class SafePosition + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } + + public Pose Pose { get; set; } + + public SafePosition() + { + Pose = new Pose(); + } + + public SafePosition(Pose pose) + { + Pose = pose; + } + } +} diff --git a/backend/api/Database/Models/AssetDeck.cs b/backend/api/Database/Models/AssetDeck.cs index d1fa63e2f..7cca64621 100644 --- a/backend/api/Database/Models/AssetDeck.cs +++ b/backend/api/Database/Models/AssetDeck.cs @@ -18,28 +18,9 @@ public class AssetDeck [MaxLength(200)] public string DeckName { get; set; } - [Required] - public Pose DefaultLocalizationPose { get; set; } - - public IList SafePositions { get; set; } - } - - public class SafePosition - { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public string Id { get; set; } - - public Pose Pose { get; set; } - - public SafePosition() - { - Pose = new Pose(); - } + //[Required] + //public Pose DefaultLocalizationPose { get; set; } - public SafePosition(Pose pose) - { - Pose = pose; - } + //public IList SafePositions { get; set; } } } diff --git a/backend/api/Database/Models/Deck.cs b/backend/api/Database/Models/Deck.cs new file mode 100644 index 000000000..cd7508aa5 --- /dev/null +++ b/backend/api/Database/Models/Deck.cs @@ -0,0 +1,23 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +#pragma warning disable CS8618 +namespace Api.Database.Models +{ + public class Deck + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } + + [Required] + public virtual Plant Plant { get; set; } + + [Required] + public virtual Installation Installation { get; set; } + + [Required] + [MaxLength(200)] + public string Name { get; set; } + } +} diff --git a/backend/api/Database/Models/Installation.cs b/backend/api/Database/Models/Installation.cs new file mode 100644 index 000000000..b8200464b --- /dev/null +++ b/backend/api/Database/Models/Installation.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +#pragma warning disable CS8618 +namespace Api.Database.Models +{ + public class Installation + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } + + [Required] + [MaxLength(200)] + public string Name { get; set; } + + [Required] + [MaxLength(10)] + public string InstallationCode { get; set; } + } +} diff --git a/backend/api/Database/Models/MissionDefinition.cs b/backend/api/Database/Models/MissionDefinition.cs new file mode 100644 index 000000000..d7e9a2cc9 --- /dev/null +++ b/backend/api/Database/Models/MissionDefinition.cs @@ -0,0 +1,34 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +#pragma warning disable CS8618 +namespace Api.Database.Models +{ + public class MissionDefinition : SortableRecord + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } + + [Required] + public Source Source { get; set; } + + [Required] + [MaxLength(200)] + public string Name { get; set; } + + [Required] + public string InstallationCode { get; set; } + + [MaxLength(1000)] + public string? Comment { get; set; } + + public TimeSpan? InspectionFrequency { get; set; } + + public virtual MissionRun? LastRun { get; set; } + + public Area? Area { get; set; } + + public bool IsDeprecated { get; set; } + } +} diff --git a/backend/api/Database/Models/Mission.cs b/backend/api/Database/Models/MissionRun.cs similarity index 95% rename from backend/api/Database/Models/Mission.cs rename to backend/api/Database/Models/MissionRun.cs index edfe1c7e8..dd104464a 100644 --- a/backend/api/Database/Models/Mission.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -5,40 +5,19 @@ #pragma warning disable CS8618 namespace Api.Database.Models { - public class Mission + public class MissionRun : SortableRecord { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public string Id { get; set; } - [MaxLength(200)] - public int? EchoMissionId { get; set; } - - [MaxLength(200)] - public string? IsarMissionId { get; set; } + //[Required] // See "Drive to Safe Position" mission in RobotController.cs + public string? MissionId { get; set; } [Required] [MaxLength(200)] public string Name { get; set; } - [MaxLength(450)] - public string? Description { get; set; } - - [MaxLength(450)] - public string? StatusReason { get; set; } - - [MaxLength(1000)] - public string? Comment { get; set; } - - [Required] - [MaxLength(200)] - public string AssetCode { get; set; } - - [Required] - public virtual Robot Robot { get; set; } - - private MissionStatus _status; - [Required] public MissionStatus Status { @@ -54,6 +33,40 @@ public MissionStatus Status } } + [Required] + [MaxLength(200)] + public string InstallationCode { get; set; } + + [Required] + public DateTimeOffset DesiredStartTime { get; set; } + + [Required] + public virtual Robot Robot { get; set; } + + // The tasks are always returned ordered by their order field + [Required] + public IList Tasks + { + get { return _tasks.OrderBy(t => t.TaskOrder).ToList(); } + set { _tasks = value; } + } + + [MaxLength(200)] + public string? IsarMissionId { get; set; } + + [MaxLength(450)] + public string? Description { get; set; } + + [MaxLength(450)] + public string? StatusReason { get; set; } + + [MaxLength(1000)] + public string? Comment { get; set; } + + public Area? Area { get; set; } + + private MissionStatus _status; + public bool IsCompleted => _status is MissionStatus.Aborted @@ -64,9 +77,6 @@ or MissionStatus.PartiallySuccessful public MapMetadata? MapMetadata { get; set; } - [Required] - public DateTimeOffset DesiredStartTime { get; set; } - public DateTimeOffset? StartTime { get; private set; } public DateTimeOffset? EndTime { get; private set; } @@ -78,14 +88,6 @@ or MissionStatus.PartiallySuccessful private IList _tasks; - // The tasks are always returned ordered by their order field - [Required] - public IList Tasks - { - get { return _tasks.OrderBy(t => t.TaskOrder).ToList(); } - set { _tasks = value; } - } - public void UpdateWithIsarInfo(IsarMission isarMission) { IsarMissionId = isarMission.IsarMissionId; diff --git a/backend/api/Database/Models/Plant.cs b/backend/api/Database/Models/Plant.cs new file mode 100644 index 000000000..7a7141bb3 --- /dev/null +++ b/backend/api/Database/Models/Plant.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +#pragma warning disable CS8618 +namespace Api.Database.Models +{ + public class Plant + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } + + [Required] + public virtual Installation Installation { get; set; } + + [Required] + [MaxLength(10)] + public string PlantCode { get; set; } + + [Required] + [MaxLength(200)] + public string Name { get; set; } + } +} diff --git a/backend/api/Database/Models/Robot.cs b/backend/api/Database/Models/Robot.cs index a9002a045..02871e8ef 100644 --- a/backend/api/Database/Models/Robot.cs +++ b/backend/api/Database/Models/Robot.cs @@ -26,9 +26,9 @@ public class Robot [MaxLength(200)] public string SerialNumber { get; set; } - public string CurrentAsset { get; set; } + public string CurrentInstallation { get; set; } - public AssetDeck? CurrentAssetDeck { get; set; } + public Area? CurrentArea { get; set; } public float BatteryLevel { get; set; } @@ -73,7 +73,7 @@ public Robot() IsarId = "defaultIsarId"; Name = "defaultId"; SerialNumber = "defaultSerialNumber"; - CurrentAsset = "defaultAsset"; + CurrentInstallation = "defaultAsset"; Status = RobotStatus.Offline; Enabled = false; Host = "localhost"; @@ -98,8 +98,8 @@ public Robot(CreateRobotQuery createQuery) IsarId = createQuery.IsarId; Name = createQuery.Name; SerialNumber = createQuery.SerialNumber; - CurrentAsset = createQuery.CurrentAsset; - CurrentAssetDeck = createQuery.CurrentAssetDeck; + CurrentInstallation = createQuery.CurrentInstallation; + CurrentArea = createQuery.CurrentArea; VideoStreams = videoStreams; Host = createQuery.Host; Port = createQuery.Port; diff --git a/backend/api/Database/Models/RobotModel.cs b/backend/api/Database/Models/RobotModel.cs index 38f4b897c..cd4e9e444 100644 --- a/backend/api/Database/Models/RobotModel.cs +++ b/backend/api/Database/Models/RobotModel.cs @@ -76,27 +76,27 @@ public void Update(UpdateRobotModelQuery updateQuery) } /// - /// Updates the based on the data in the provided + /// Updates the based on the data in the provided /// - /// - public void UpdateAverageDurationPerTag(List recentMissionsForModelType) + /// + public void UpdateAverageDurationPerTag(List recentMissionRunsForModelType) { - if (recentMissionsForModelType.Any(mission => mission.Robot.Model.Type != Type)) + if (recentMissionRunsForModelType.Any(missionRun => missionRun.Robot.Model.Type != Type)) throw new ArgumentException( string.Format( CultureInfo.CurrentCulture, "{0} should only include missions for this model type ('{1}')", - nameof(recentMissionsForModelType), + nameof(recentMissionRunsForModelType), Type ), - nameof(recentMissionsForModelType) + nameof(recentMissionRunsForModelType) ); // The time spent on each tasks, not including the duration of video/audio recordings - var timeSpentPerTask = recentMissionsForModelType + var timeSpentPerTask = recentMissionRunsForModelType .SelectMany( - mission => - mission.Tasks + missionRun => + missionRun.Tasks .Where(task => task.EndTime is not null && task.StartTime is not null) .Select( task => diff --git a/backend/api/Database/Models/SortableRecord.cs b/backend/api/Database/Models/SortableRecord.cs new file mode 100644 index 000000000..33127dc6c --- /dev/null +++ b/backend/api/Database/Models/SortableRecord.cs @@ -0,0 +1,8 @@ +#pragma warning disable CS8618 +namespace Api.Database.Models +{ + public interface SortableRecord + { + string Name { get; set; } + } +} diff --git a/backend/api/Database/Models/Source.cs b/backend/api/Database/Models/Source.cs new file mode 100644 index 000000000..0168c2623 --- /dev/null +++ b/backend/api/Database/Models/Source.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +#pragma warning disable CS8618 +namespace Api.Database.Models +{ + public class Source + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } + + [Required] + public string SourceId { get; set; } + + [Required] + public MissionSourceType Type { get; set; } + } + + public enum MissionSourceType + { + Echo, Custom + } +} diff --git a/backend/api/EventHandlers/IsarConnectionEventHandler.cs b/backend/api/EventHandlers/IsarConnectionEventHandler.cs index 3c601a055..a3cd45eea 100644 --- a/backend/api/EventHandlers/IsarConnectionEventHandler.cs +++ b/backend/api/EventHandlers/IsarConnectionEventHandler.cs @@ -18,8 +18,8 @@ public class IsarConnectionEventHandler : EventHandlerBase private IRobotService RobotService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private IMissionService MissionService => - _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IMissionRunService MissionRunService => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); private readonly Dictionary _isarConnectionTimers = new(); @@ -119,16 +119,16 @@ private async void OnTimeoutEvent(IsarRobotStatusMessage robotStatusMessage) robot.Status = RobotStatus.Offline; if (robot.CurrentMissionId != null) { - var mission = await MissionService.ReadById(robot.CurrentMissionId); - if (mission != null) + var missionRun = await MissionRunService.ReadById(robot.CurrentMissionId); + if (missionRun != null) { _logger.LogError( "Mission '{missionId}' ('{missionName}') failed due to ISAR timeout", - mission.Id, - mission.Name + missionRun.Id, + missionRun.Name ); - mission.SetToFailed(); - await MissionService.Update(mission); + missionRun.SetToFailed(); + await MissionRunService.Update(missionRun); } } robot.CurrentMissionId = null; diff --git a/backend/api/EventHandlers/MissionScheduler.cs b/backend/api/EventHandlers/MissionScheduler.cs index a5f8b74bb..bea7190e1 100644 --- a/backend/api/EventHandlers/MissionScheduler.cs +++ b/backend/api/EventHandlers/MissionScheduler.cs @@ -13,10 +13,10 @@ public class MissionScheduler : BackgroundService private readonly int _timeDelay; private readonly IServiceScopeFactory _scopeFactory; - private IList MissionQueue => - MissionService + private IList MissionRunQueue => + MissionRunService .ReadAll( - new MissionQueryStringParameters + new MissionRunQueryStringParameters { Statuses = new List { MissionStatus.Pending }, OrderBy = "DesiredStartTime", @@ -25,8 +25,8 @@ public class MissionScheduler : BackgroundService ) .Result; - private IMissionService MissionService => - _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + private IMissionRunService MissionRunService => + _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); private RobotController RobotController => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); @@ -42,17 +42,17 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { - foreach (var queuedMission in MissionQueue) + foreach (var queuedMissionRun in MissionRunQueue) { - var freshMission = MissionService.ReadById(queuedMission.Id).Result; - if (freshMission == null) + var freshMissionRun = MissionRunService.ReadById(queuedMissionRun.Id).Result; + if (freshMissionRun == null) { continue; } if ( - freshMission.Robot.Status is not RobotStatus.Available - || !freshMission.Robot.Enabled - || freshMission.DesiredStartTime > DateTimeOffset.UtcNow + freshMissionRun.Robot.Status is not RobotStatus.Available + || !freshMissionRun.Robot.Enabled + || freshMissionRun.DesiredStartTime > DateTimeOffset.UtcNow ) { continue; @@ -60,31 +60,31 @@ freshMission.Robot.Status is not RobotStatus.Available try { - await StartMission(queuedMission); + await StartMissionRun(queuedMissionRun); } catch (MissionException e) { const MissionStatus NewStatus = MissionStatus.Failed; _logger.LogWarning( - "Mission {id} was not started successfully. Status updated to '{status}'.\nReason: {failReason}", - queuedMission.Id, + "Mission run {id} was not started successfully. Status updated to '{status}'.\nReason: {failReason}", + queuedMissionRun.Id, NewStatus, e.Message ); - queuedMission.Status = NewStatus; - queuedMission.StatusReason = $"Failed to start: '{e.Message}'"; - await MissionService.Update(queuedMission); + queuedMissionRun.Status = NewStatus; + queuedMissionRun.StatusReason = $"Failed to start: '{e.Message}'"; + await MissionRunService.Update(queuedMissionRun); } } await Task.Delay(_timeDelay, stoppingToken); } } - private async Task StartMission(Mission queuedMission) + private async Task StartMissionRun(MissionRun queuedMissionRun) { var result = await RobotController.StartMission( - queuedMission.Robot.Id, - queuedMission.Id + queuedMissionRun.Robot.Id, + queuedMissionRun.Id ); if (result.Result is not OkObjectResult) { @@ -93,7 +93,7 @@ private async Task StartMission(Mission queuedMission) errorMessage = returnObject.Value?.ToString() ?? errorMessage; throw new MissionException(errorMessage); } - _logger.LogInformation("Started mission '{id}'", queuedMission.Id); + _logger.LogInformation("Started mission run '{id}'", queuedMissionRun.Id); } } } diff --git a/backend/api/EventHandlers/MqttEventHandler.cs b/backend/api/EventHandlers/MqttEventHandler.cs index dd32fb7e5..b832e4eb0 100644 --- a/backend/api/EventHandlers/MqttEventHandler.cs +++ b/backend/api/EventHandlers/MqttEventHandler.cs @@ -122,7 +122,7 @@ private async void OnIsarRobotInfo(object? sender, MqttReceivedArgs mqttArgs) Name = isarRobotInfo.RobotName, RobotType = isarRobotInfo.RobotType, SerialNumber = isarRobotInfo.SerialNumber, - CurrentAsset = isarRobotInfo.CurrentAsset, + CurrentInstallation = isarRobotInfo.CurrentInstallation, VideoStreams = isarRobotInfo.VideoStreamQueries, Host = isarRobotInfo.Host, Port = isarRobotInfo.Port, @@ -166,9 +166,9 @@ private async void OnIsarRobotInfo(object? sender, MqttReceivedArgs mqttArgs) UpdatePortIfChanged(isarRobotInfo.Port, ref robot, ref updatedFields); - if (isarRobotInfo.CurrentAsset is not null) + if (isarRobotInfo.CurrentInstallation is not null) { - UpdateCurrentAssetIfChanged(isarRobotInfo.CurrentAsset, ref robot, ref updatedFields); + UpdateCurrentInstallationIfChanged(isarRobotInfo.CurrentInstallation, ref robot, ref updatedFields); } if (!updatedFields.IsNullOrEmpty()) @@ -233,22 +233,22 @@ private static void UpdatePortIfChanged(int port, ref Robot robot, robot.Port = port; } - private static void UpdateCurrentAssetIfChanged(string newCurrentAsset, ref Robot robot, + private static void UpdateCurrentInstallationIfChanged(string newCurrentInstallation, ref Robot robot, ref List updatedFields) { - if (newCurrentAsset.Equals(robot.CurrentAsset, StringComparison.Ordinal)) + if (newCurrentInstallation.Equals(robot.CurrentInstallation, StringComparison.Ordinal)) { return; } - updatedFields.Add($"\nCurrentAsset ({robot.CurrentAsset} -> {newCurrentAsset})\n"); - robot.CurrentAsset = newCurrentAsset; + updatedFields.Add($"\nCurrentInstallation ({robot.CurrentInstallation} -> {newCurrentInstallation})\n"); + robot.CurrentInstallation = newCurrentInstallation; } private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) { var provider = GetServiceProvider(); - var missionService = provider.GetRequiredService(); + var missionRunService = provider.GetRequiredService(); var robotService = provider.GetRequiredService(); var robotModelService = provider.GetRequiredService(); @@ -256,7 +256,7 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) MissionStatus status; try { - status = Mission.MissionStatusFromString(isarMission.Status); + status = MissionRun.MissionStatusFromString(isarMission.Status); } catch (ArgumentException e) { @@ -268,12 +268,12 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) return; } - var flotillaMission = await missionService.UpdateMissionStatusByIsarMissionId( + var flotillaMissionRun = await missionRunService.UpdateMissionRunStatusByIsarMissionId( isarMission.MissionId, status ); - if (flotillaMission is null) + if (flotillaMissionRun is null) { _logger.LogError( "No mission found with ISARMissionId '{isarMissionId}'. Could not update status to '{status}'", @@ -285,7 +285,7 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) _logger.LogInformation( "Mission '{id}' (ISARMissionID='{isarId}') status updated to '{status}' for robot '{robotName}' with ISAR id '{isarId}'", - flotillaMission.Id, + flotillaMissionRun.Id, isarMission.MissionId, isarMission.Status, isarMission.RobotName, @@ -303,8 +303,8 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) return; } - robot.Status = flotillaMission.IsCompleted ? RobotStatus.Available : RobotStatus.Busy; - if (flotillaMission.IsCompleted) + robot.Status = flotillaMissionRun.IsCompleted ? RobotStatus.Available : RobotStatus.Busy; + if (flotillaMissionRun.IsCompleted) { robot.CurrentMissionId = null; } @@ -317,7 +317,7 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) robot.Status ); - if (flotillaMission.IsCompleted) + if (flotillaMissionRun.IsCompleted) { int timeRangeInDays = _configuration.GetValue( "TimeRangeForMissionDurationEstimationInDays" @@ -325,8 +325,8 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) long minEpochTime = DateTimeOffset.Now .AddDays(-timeRangeInDays) .ToUnixTimeSeconds(); - var missionsForEstimation = await missionService.ReadAll( - new MissionQueryStringParameters + var missionRunsForEstimation = await missionRunService.ReadAll( + new MissionRunQueryStringParameters { MinDesiredStartTime = minEpochTime, RobotModelType = robot.Model.Type, @@ -334,7 +334,7 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) } ); var model = robot.Model; - model.UpdateAverageDurationPerTag(missionsForEstimation); + model.UpdateAverageDurationPerTag(missionRunsForEstimation); await robotModelService.Update(model); @@ -349,7 +349,7 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) private async void OnTaskUpdate(object? sender, MqttReceivedArgs mqttArgs) { var provider = GetServiceProvider(); - var missionService = provider.GetRequiredService(); + var missionRunService = provider.GetRequiredService(); var task = (IsarTaskMessage)mqttArgs.Message; IsarTaskStatus status; try @@ -366,7 +366,7 @@ private async void OnTaskUpdate(object? sender, MqttReceivedArgs mqttArgs) return; } - bool success = await missionService.UpdateTaskStatusByIsarTaskId( + bool success = await missionRunService.UpdateTaskStatusByIsarTaskId( task.MissionId, task.TaskId, status @@ -387,7 +387,7 @@ private async void OnTaskUpdate(object? sender, MqttReceivedArgs mqttArgs) private async void OnStepUpdate(object? sender, MqttReceivedArgs mqttArgs) { var provider = GetServiceProvider(); - var missionService = provider.GetRequiredService(); + var missionRunService = provider.GetRequiredService(); var step = (IsarStepMessage)mqttArgs.Message; @@ -413,7 +413,7 @@ private async void OnStepUpdate(object? sender, MqttReceivedArgs mqttArgs) return; } - bool success = await missionService.UpdateStepStatusByIsarStepId( + bool success = await missionRunService.UpdateStepStatusByIsarStepId( step.MissionId, step.TaskId, step.StepId, diff --git a/backend/api/MQTT/MessageModels/IsarRobotInfo.cs b/backend/api/MQTT/MessageModels/IsarRobotInfo.cs index 4c00c1c79..7884d6176 100644 --- a/backend/api/MQTT/MessageModels/IsarRobotInfo.cs +++ b/backend/api/MQTT/MessageModels/IsarRobotInfo.cs @@ -20,7 +20,7 @@ public class IsarRobotInfoMessage : MqttMessage public string SerialNumber { get; set; } [JsonPropertyName("robot_asset")] - public string CurrentAsset { get; set; } + public string CurrentInstallation { get; set; } [JsonPropertyName("video_streams")] public List VideoStreamQueries { get; set; } diff --git a/backend/api/Migrations/20230718121036_ChangeAssetDeckToAreaAndAddMissionDefinition.Designer.cs b/backend/api/Migrations/20230718121036_ChangeAssetDeckToAreaAndAddMissionDefinition.Designer.cs new file mode 100644 index 000000000..67cf45c53 --- /dev/null +++ b/backend/api/Migrations/20230718121036_ChangeAssetDeckToAreaAndAddMissionDefinition.Designer.cs @@ -0,0 +1,1141 @@ +// +using System; +using Api.Database.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Api.Migrations +{ + [DbContext(typeof(FlotillaDbContext))] + [Migration("20230718121036_ChangeAssetDeckToAreaAndAddMissionDefinition")] + partial class ChangeAssetDeckToAreaAndAddMissionDefinition + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Api.Database.Models.Area", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("DeckId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("InstallationId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PlantId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("DeckId"); + + b.HasIndex("InstallationId"); + + b.HasIndex("PlantId"); + + b.ToTable("Areas"); + }); + + modelBuilder.Entity("Api.Database.Models.AssetDeck", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("AssetCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("DeckName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.ToTable("AssetDecks"); + }); + + modelBuilder.Entity("Api.Database.Models.Deck", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("InstallationId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PlantId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId"); + + b.HasIndex("PlantId"); + + b.ToTable("Decks"); + }); + + modelBuilder.Entity("Api.Database.Models.Installation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("InstallationCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("InstallationCode") + .IsUnique(); + + b.ToTable("Installations"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("AreaId") + .HasColumnType("nvarchar(450)"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("InspectionFrequency") + .HasColumnType("time"); + + b.Property("InstallationCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeprecated") + .HasColumnType("bit"); + + b.Property("LastRunId") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SourceId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("AreaId"); + + b.HasIndex("LastRunId"); + + b.HasIndex("SourceId"); + + b.ToTable("MissionDefinitions"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("AreaId") + .HasColumnType("nvarchar(450)"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Description") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.Property("DesiredStartTime") + .HasColumnType("datetimeoffset"); + + b.Property("EndTime") + .HasColumnType("datetimeoffset"); + + b.Property("EstimatedDuration") + .HasColumnType("bigint"); + + b.Property("InstallationCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IsarMissionId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("MissionId") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("RobotId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("StartTime") + .HasColumnType("datetimeoffset"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StatusReason") + .HasMaxLength(450) + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("AreaId"); + + b.HasIndex("RobotId"); + + b.ToTable("MissionRuns"); + }); + + modelBuilder.Entity("Api.Database.Models.Plant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("InstallationId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PlantCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId"); + + b.HasIndex("PlantCode") + .IsUnique(); + + b.ToTable("Plants"); + }); + + modelBuilder.Entity("Api.Database.Models.Robot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("BatteryLevel") + .HasColumnType("real"); + + b.Property("CurrentAreaId") + .HasColumnType("nvarchar(450)"); + + b.Property("CurrentInstallation") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CurrentMissionId") + .HasColumnType("nvarchar(max)"); + + b.Property("Enabled") + .HasColumnType("bit"); + + b.Property("Host") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("IsarId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ModelId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Port") + .HasColumnType("int"); + + b.Property("PressureLevel") + .HasColumnType("real"); + + b.Property("SerialNumber") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("Status") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("CurrentAreaId"); + + b.HasIndex("ModelId"); + + b.ToTable("Robots"); + }); + + modelBuilder.Entity("Api.Database.Models.RobotModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("AverageDurationPerTag") + .HasColumnType("real"); + + b.Property("BatteryWarningThreshold") + .HasColumnType("real"); + + b.Property("LowerPressureWarningThreshold") + .HasColumnType("real"); + + b.Property("Type") + .IsRequired() + .HasColumnType("nvarchar(56)"); + + b.Property("UpperPressureWarningThreshold") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("Type") + .IsUnique(); + + b.ToTable("RobotModels"); + }); + + modelBuilder.Entity("Api.Database.Models.SafePosition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("AreaId") + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("AreaId"); + + b.ToTable("SafePositions"); + }); + + modelBuilder.Entity("Api.Database.Models.Source", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("SourceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Sources"); + }); + + modelBuilder.Entity("Api.Database.Models.Area", b => + { + b.HasOne("Api.Database.Models.Deck", "Deck") + .WithMany() + .HasForeignKey("DeckId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Api.Database.Models.Plant", "Plant") + .WithMany() + .HasForeignKey("PlantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Api.Database.Models.Pose", "DefaultLocalizationPose", b1 => + { + b1.Property("AreaId") + .HasColumnType("nvarchar(450)"); + + b1.HasKey("AreaId"); + + b1.ToTable("Areas"); + + b1.WithOwner() + .HasForeignKey("AreaId"); + + b1.OwnsOne("Api.Database.Models.Orientation", "Orientation", b2 => + { + b2.Property("PoseAreaId") + .HasColumnType("nvarchar(450)"); + + b2.Property("W") + .HasColumnType("real"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseAreaId"); + + b2.ToTable("Areas"); + + b2.WithOwner() + .HasForeignKey("PoseAreaId"); + }); + + b1.OwnsOne("Api.Database.Models.Position", "Position", b2 => + { + b2.Property("PoseAreaId") + .HasColumnType("nvarchar(450)"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseAreaId"); + + b2.ToTable("Areas"); + + b2.WithOwner() + .HasForeignKey("PoseAreaId"); + }); + + b1.Navigation("Orientation") + .IsRequired(); + + b1.Navigation("Position") + .IsRequired(); + }); + + b.OwnsOne("Api.Database.Models.MapMetadata", "MapMetadata", b1 => + { + b1.Property("AreaId") + .HasColumnType("nvarchar(450)"); + + b1.Property("MapName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b1.HasKey("AreaId"); + + b1.ToTable("Areas"); + + b1.WithOwner() + .HasForeignKey("AreaId"); + + b1.OwnsOne("Api.Database.Models.Boundary", "Boundary", b2 => + { + b2.Property("MapMetadataAreaId") + .HasColumnType("nvarchar(450)"); + + b2.Property("X1") + .HasColumnType("float"); + + b2.Property("X2") + .HasColumnType("float"); + + b2.Property("Y1") + .HasColumnType("float"); + + b2.Property("Y2") + .HasColumnType("float"); + + b2.Property("Z1") + .HasColumnType("float"); + + b2.Property("Z2") + .HasColumnType("float"); + + b2.HasKey("MapMetadataAreaId"); + + b2.ToTable("Areas"); + + b2.WithOwner() + .HasForeignKey("MapMetadataAreaId"); + }); + + b1.OwnsOne("Api.Database.Models.TransformationMatrices", "TransformationMatrices", b2 => + { + b2.Property("MapMetadataAreaId") + .HasColumnType("nvarchar(450)"); + + b2.Property("C1") + .HasColumnType("float"); + + b2.Property("C2") + .HasColumnType("float"); + + b2.Property("D1") + .HasColumnType("float"); + + b2.Property("D2") + .HasColumnType("float"); + + b2.HasKey("MapMetadataAreaId"); + + b2.ToTable("Areas"); + + b2.WithOwner() + .HasForeignKey("MapMetadataAreaId"); + }); + + b1.Navigation("Boundary") + .IsRequired(); + + b1.Navigation("TransformationMatrices") + .IsRequired(); + }); + + b.Navigation("Deck"); + + b.Navigation("DefaultLocalizationPose") + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("MapMetadata") + .IsRequired(); + + b.Navigation("Plant"); + }); + + modelBuilder.Entity("Api.Database.Models.Deck", b => + { + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Api.Database.Models.Plant", "Plant") + .WithMany() + .HasForeignKey("PlantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Plant"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionDefinition", b => + { + b.HasOne("Api.Database.Models.Area", "Area") + .WithMany() + .HasForeignKey("AreaId"); + + b.HasOne("Api.Database.Models.MissionRun", "LastRun") + .WithMany() + .HasForeignKey("LastRunId"); + + b.HasOne("Api.Database.Models.Source", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Area"); + + b.Navigation("LastRun"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionRun", b => + { + b.HasOne("Api.Database.Models.Area", "Area") + .WithMany() + .HasForeignKey("AreaId"); + + b.HasOne("Api.Database.Models.Robot", "Robot") + .WithMany() + .HasForeignKey("RobotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Api.Database.Models.MapMetadata", "MapMetadata", b1 => + { + b1.Property("MissionRunId") + .HasColumnType("nvarchar(450)"); + + b1.Property("MapName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b1.HasKey("MissionRunId"); + + b1.ToTable("MissionRuns"); + + b1.WithOwner() + .HasForeignKey("MissionRunId"); + + b1.OwnsOne("Api.Database.Models.Boundary", "Boundary", b2 => + { + b2.Property("MapMetadataMissionRunId") + .HasColumnType("nvarchar(450)"); + + b2.Property("X1") + .HasColumnType("float"); + + b2.Property("X2") + .HasColumnType("float"); + + b2.Property("Y1") + .HasColumnType("float"); + + b2.Property("Y2") + .HasColumnType("float"); + + b2.Property("Z1") + .HasColumnType("float"); + + b2.Property("Z2") + .HasColumnType("float"); + + b2.HasKey("MapMetadataMissionRunId"); + + b2.ToTable("MissionRuns"); + + b2.WithOwner() + .HasForeignKey("MapMetadataMissionRunId"); + }); + + b1.OwnsOne("Api.Database.Models.TransformationMatrices", "TransformationMatrices", b2 => + { + b2.Property("MapMetadataMissionRunId") + .HasColumnType("nvarchar(450)"); + + b2.Property("C1") + .HasColumnType("float"); + + b2.Property("C2") + .HasColumnType("float"); + + b2.Property("D1") + .HasColumnType("float"); + + b2.Property("D2") + .HasColumnType("float"); + + b2.HasKey("MapMetadataMissionRunId"); + + b2.ToTable("MissionRuns"); + + b2.WithOwner() + .HasForeignKey("MapMetadataMissionRunId"); + }); + + b1.Navigation("Boundary") + .IsRequired(); + + b1.Navigation("TransformationMatrices") + .IsRequired(); + }); + + b.OwnsMany("Api.Database.Models.MissionTask", "Tasks", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b1.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b1.Property("EchoPoseId") + .HasColumnType("int"); + + b1.Property("EchoTagLink") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b1.Property("EndTime") + .HasColumnType("datetimeoffset"); + + b1.Property("IsarTaskId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b1.Property("MissionRunId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b1.Property("StartTime") + .HasColumnType("datetimeoffset"); + + b1.Property("Status") + .HasColumnType("int"); + + b1.Property("TagId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b1.Property("TaskOrder") + .HasColumnType("int"); + + b1.HasKey("Id"); + + b1.HasIndex("MissionRunId"); + + b1.ToTable("MissionTask"); + + b1.WithOwner() + .HasForeignKey("MissionRunId"); + + b1.OwnsMany("Api.Database.Models.Inspection", "Inspections", b2 => + { + b2.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b2.Property("AnalysisTypes") + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b2.Property("EndTime") + .HasColumnType("datetimeoffset"); + + b2.Property("InspectionType") + .HasColumnType("int"); + + b2.Property("InspectionUrl") + .HasMaxLength(250) + .HasColumnType("nvarchar(250)"); + + b2.Property("IsarStepId") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b2.Property("MissionTaskId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b2.Property("StartTime") + .HasColumnType("datetimeoffset"); + + b2.Property("Status") + .HasColumnType("int"); + + b2.Property("VideoDuration") + .HasColumnType("real"); + + b2.HasKey("Id"); + + b2.HasIndex("MissionTaskId"); + + b2.ToTable("Inspection"); + + b2.WithOwner() + .HasForeignKey("MissionTaskId"); + }); + + b1.OwnsOne("Api.Database.Models.Position", "InspectionTarget", b2 => + { + b2.Property("MissionTaskId") + .HasColumnType("nvarchar(450)"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("MissionTaskId"); + + b2.ToTable("MissionTask"); + + b2.WithOwner() + .HasForeignKey("MissionTaskId"); + }); + + b1.OwnsOne("Api.Database.Models.Pose", "RobotPose", b2 => + { + b2.Property("MissionTaskId") + .HasColumnType("nvarchar(450)"); + + b2.HasKey("MissionTaskId"); + + b2.ToTable("MissionTask"); + + b2.WithOwner() + .HasForeignKey("MissionTaskId"); + + b2.OwnsOne("Api.Database.Models.Orientation", "Orientation", b3 => + { + b3.Property("PoseMissionTaskId") + .HasColumnType("nvarchar(450)"); + + b3.Property("W") + .HasColumnType("real"); + + b3.Property("X") + .HasColumnType("real"); + + b3.Property("Y") + .HasColumnType("real"); + + b3.Property("Z") + .HasColumnType("real"); + + b3.HasKey("PoseMissionTaskId"); + + b3.ToTable("MissionTask"); + + b3.WithOwner() + .HasForeignKey("PoseMissionTaskId"); + }); + + b2.OwnsOne("Api.Database.Models.Position", "Position", b3 => + { + b3.Property("PoseMissionTaskId") + .HasColumnType("nvarchar(450)"); + + b3.Property("X") + .HasColumnType("real"); + + b3.Property("Y") + .HasColumnType("real"); + + b3.Property("Z") + .HasColumnType("real"); + + b3.HasKey("PoseMissionTaskId"); + + b3.ToTable("MissionTask"); + + b3.WithOwner() + .HasForeignKey("PoseMissionTaskId"); + }); + + b2.Navigation("Orientation") + .IsRequired(); + + b2.Navigation("Position") + .IsRequired(); + }); + + b1.Navigation("InspectionTarget") + .IsRequired(); + + b1.Navigation("Inspections"); + + b1.Navigation("RobotPose") + .IsRequired(); + }); + + b.Navigation("Area"); + + b.Navigation("MapMetadata"); + + b.Navigation("Robot"); + + b.Navigation("Tasks"); + }); + + modelBuilder.Entity("Api.Database.Models.Plant", b => + { + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + }); + + modelBuilder.Entity("Api.Database.Models.Robot", b => + { + b.HasOne("Api.Database.Models.Area", "CurrentArea") + .WithMany() + .HasForeignKey("CurrentAreaId"); + + b.HasOne("Api.Database.Models.RobotModel", "Model") + .WithMany() + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Api.Database.Models.Pose", "Pose", b1 => + { + b1.Property("RobotId") + .HasColumnType("nvarchar(450)"); + + b1.HasKey("RobotId"); + + b1.ToTable("Robots"); + + b1.WithOwner() + .HasForeignKey("RobotId"); + + b1.OwnsOne("Api.Database.Models.Orientation", "Orientation", b2 => + { + b2.Property("PoseRobotId") + .HasColumnType("nvarchar(450)"); + + b2.Property("W") + .HasColumnType("real"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseRobotId"); + + b2.ToTable("Robots"); + + b2.WithOwner() + .HasForeignKey("PoseRobotId"); + }); + + b1.OwnsOne("Api.Database.Models.Position", "Position", b2 => + { + b2.Property("PoseRobotId") + .HasColumnType("nvarchar(450)"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseRobotId"); + + b2.ToTable("Robots"); + + b2.WithOwner() + .HasForeignKey("PoseRobotId"); + }); + + b1.Navigation("Orientation") + .IsRequired(); + + b1.Navigation("Position") + .IsRequired(); + }); + + b.OwnsMany("Api.Database.Models.VideoStream", "VideoStreams", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b1.Property("RobotId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b1.Property("ShouldRotate270Clockwise") + .HasColumnType("bit"); + + b1.Property("Type") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b1.Property("Url") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b1.HasKey("Id"); + + b1.HasIndex("RobotId"); + + b1.ToTable("VideoStream"); + + b1.WithOwner() + .HasForeignKey("RobotId"); + }); + + b.Navigation("CurrentArea"); + + b.Navigation("Model"); + + b.Navigation("Pose") + .IsRequired(); + + b.Navigation("VideoStreams"); + }); + + modelBuilder.Entity("Api.Database.Models.SafePosition", b => + { + b.HasOne("Api.Database.Models.Area", null) + .WithMany("SafePositions") + .HasForeignKey("AreaId"); + + b.OwnsOne("Api.Database.Models.Pose", "Pose", b1 => + { + b1.Property("SafePositionId") + .HasColumnType("nvarchar(450)"); + + b1.HasKey("SafePositionId"); + + b1.ToTable("SafePositions"); + + b1.WithOwner() + .HasForeignKey("SafePositionId"); + + b1.OwnsOne("Api.Database.Models.Orientation", "Orientation", b2 => + { + b2.Property("PoseSafePositionId") + .HasColumnType("nvarchar(450)"); + + b2.Property("W") + .HasColumnType("real"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseSafePositionId"); + + b2.ToTable("SafePositions"); + + b2.WithOwner() + .HasForeignKey("PoseSafePositionId"); + }); + + b1.OwnsOne("Api.Database.Models.Position", "Position", b2 => + { + b2.Property("PoseSafePositionId") + .HasColumnType("nvarchar(450)"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseSafePositionId"); + + b2.ToTable("SafePositions"); + + b2.WithOwner() + .HasForeignKey("PoseSafePositionId"); + }); + + b1.Navigation("Orientation") + .IsRequired(); + + b1.Navigation("Position") + .IsRequired(); + }); + + b.Navigation("Pose") + .IsRequired(); + }); + + modelBuilder.Entity("Api.Database.Models.Area", b => + { + b.Navigation("SafePositions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/api/Migrations/20230718121036_ChangeAssetDeckToAreaAndAddMissionDefinition.cs b/backend/api/Migrations/20230718121036_ChangeAssetDeckToAreaAndAddMissionDefinition.cs new file mode 100644 index 000000000..733b20697 --- /dev/null +++ b/backend/api/Migrations/20230718121036_ChangeAssetDeckToAreaAndAddMissionDefinition.cs @@ -0,0 +1,586 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Api.Migrations +{ + /// + public partial class ChangeAssetDeckToAreaAndAddMissionDefinition : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_MissionTask_Missions_MissionId", + table: "MissionTask"); + + migrationBuilder.DropForeignKey( + name: "FK_Robots_AssetDecks_CurrentAssetDeckId", + table: "Robots"); + + migrationBuilder.DropForeignKey( + name: "FK_SafePositions_AssetDecks_AssetDeckId", + table: "SafePositions"); + + migrationBuilder.DropTable( + name: "Missions"); + + migrationBuilder.DropIndex( + name: "IX_AssetDecks_AssetCode_DeckName", + table: "AssetDecks"); + + migrationBuilder.DropColumn( + name: "DefaultLocalizationPose_Orientation_W", + table: "AssetDecks"); + + migrationBuilder.DropColumn( + name: "DefaultLocalizationPose_Orientation_X", + table: "AssetDecks"); + + migrationBuilder.DropColumn( + name: "DefaultLocalizationPose_Orientation_Y", + table: "AssetDecks"); + + migrationBuilder.DropColumn( + name: "DefaultLocalizationPose_Orientation_Z", + table: "AssetDecks"); + + migrationBuilder.DropColumn( + name: "DefaultLocalizationPose_Position_X", + table: "AssetDecks"); + + migrationBuilder.DropColumn( + name: "DefaultLocalizationPose_Position_Y", + table: "AssetDecks"); + + migrationBuilder.DropColumn( + name: "DefaultLocalizationPose_Position_Z", + table: "AssetDecks"); + + migrationBuilder.RenameColumn( + name: "AssetDeckId", + table: "SafePositions", + newName: "AreaId"); + + migrationBuilder.RenameIndex( + name: "IX_SafePositions_AssetDeckId", + table: "SafePositions", + newName: "IX_SafePositions_AreaId"); + + migrationBuilder.RenameColumn( + name: "CurrentAssetDeckId", + table: "Robots", + newName: "CurrentAreaId"); + + migrationBuilder.RenameColumn( + name: "CurrentAsset", + table: "Robots", + newName: "CurrentInstallation"); + + migrationBuilder.RenameIndex( + name: "IX_Robots_CurrentAssetDeckId", + table: "Robots", + newName: "IX_Robots_CurrentAreaId"); + + migrationBuilder.RenameColumn( + name: "MissionId", + table: "MissionTask", + newName: "MissionRunId"); + + migrationBuilder.RenameIndex( + name: "IX_MissionTask_MissionId", + table: "MissionTask", + newName: "IX_MissionTask_MissionRunId"); + + migrationBuilder.CreateTable( + name: "Installations", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + InstallationCode = table.Column(type: "nvarchar(10)", maxLength: 10, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Installations", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Sources", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + SourceId = table.Column(type: "nvarchar(max)", nullable: false), + Type = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Sources", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Plants", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + InstallationId = table.Column(type: "nvarchar(450)", nullable: false), + PlantCode = table.Column(type: "nvarchar(10)", maxLength: 10, nullable: false), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Plants", x => x.Id); + table.ForeignKey( + name: "FK_Plants_Installations_InstallationId", + column: x => x.InstallationId, + principalTable: "Installations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Decks", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + PlantId = table.Column(type: "nvarchar(450)", nullable: false), + InstallationId = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Decks", x => x.Id); + table.ForeignKey( + name: "FK_Decks_Installations_InstallationId", + column: x => x.InstallationId, + principalTable: "Installations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Decks_Plants_PlantId", + column: x => x.PlantId, + principalTable: "Plants", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Areas", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + DeckId = table.Column(type: "nvarchar(450)", nullable: false), + PlantId = table.Column(type: "nvarchar(450)", nullable: false), + InstallationId = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + MapMetadata_MapName = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + MapMetadata_Boundary_X1 = table.Column(type: "float", nullable: false), + MapMetadata_Boundary_X2 = table.Column(type: "float", nullable: false), + MapMetadata_Boundary_Y1 = table.Column(type: "float", nullable: false), + MapMetadata_Boundary_Y2 = table.Column(type: "float", nullable: false), + MapMetadata_Boundary_Z1 = table.Column(type: "float", nullable: false), + MapMetadata_Boundary_Z2 = table.Column(type: "float", nullable: false), + MapMetadata_TransformationMatrices_C1 = table.Column(type: "float", nullable: false), + MapMetadata_TransformationMatrices_C2 = table.Column(type: "float", nullable: false), + MapMetadata_TransformationMatrices_D1 = table.Column(type: "float", nullable: false), + MapMetadata_TransformationMatrices_D2 = table.Column(type: "float", nullable: false), + DefaultLocalizationPose_Position_X = table.Column(type: "real", nullable: false), + DefaultLocalizationPose_Position_Y = table.Column(type: "real", nullable: false), + DefaultLocalizationPose_Position_Z = table.Column(type: "real", nullable: false), + DefaultLocalizationPose_Orientation_X = table.Column(type: "real", nullable: false), + DefaultLocalizationPose_Orientation_Y = table.Column(type: "real", nullable: false), + DefaultLocalizationPose_Orientation_Z = table.Column(type: "real", nullable: false), + DefaultLocalizationPose_Orientation_W = table.Column(type: "real", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Areas", x => x.Id); + table.ForeignKey( + name: "FK_Areas_Decks_DeckId", + column: x => x.DeckId, + principalTable: "Decks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Areas_Installations_InstallationId", + column: x => x.InstallationId, + principalTable: "Installations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_Areas_Plants_PlantId", + column: x => x.PlantId, + principalTable: "Plants", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "MissionRuns", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + MissionId = table.Column(type: "nvarchar(max)", nullable: true), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + Status = table.Column(type: "int", nullable: false), + InstallationCode = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + DesiredStartTime = table.Column(type: "datetimeoffset", nullable: false), + RobotId = table.Column(type: "nvarchar(450)", nullable: false), + IsarMissionId = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + Description = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: true), + StatusReason = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: true), + Comment = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: true), + AreaId = table.Column(type: "nvarchar(450)", nullable: true), + MapMetadata_MapName = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + MapMetadata_Boundary_X1 = table.Column(type: "float", nullable: true), + MapMetadata_Boundary_X2 = table.Column(type: "float", nullable: true), + MapMetadata_Boundary_Y1 = table.Column(type: "float", nullable: true), + MapMetadata_Boundary_Y2 = table.Column(type: "float", nullable: true), + MapMetadata_Boundary_Z1 = table.Column(type: "float", nullable: true), + MapMetadata_Boundary_Z2 = table.Column(type: "float", nullable: true), + MapMetadata_TransformationMatrices_C1 = table.Column(type: "float", nullable: true), + MapMetadata_TransformationMatrices_C2 = table.Column(type: "float", nullable: true), + MapMetadata_TransformationMatrices_D1 = table.Column(type: "float", nullable: true), + MapMetadata_TransformationMatrices_D2 = table.Column(type: "float", nullable: true), + StartTime = table.Column(type: "datetimeoffset", nullable: true), + EndTime = table.Column(type: "datetimeoffset", nullable: true), + EstimatedDuration = table.Column(type: "bigint", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_MissionRuns", x => x.Id); + table.ForeignKey( + name: "FK_MissionRuns_Areas_AreaId", + column: x => x.AreaId, + principalTable: "Areas", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_MissionRuns_Robots_RobotId", + column: x => x.RobotId, + principalTable: "Robots", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "MissionDefinitions", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + SourceId = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + InstallationCode = table.Column(type: "nvarchar(max)", nullable: false), + Comment = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: true), + InspectionFrequency = table.Column(type: "time", nullable: true), + LastRunId = table.Column(type: "nvarchar(450)", nullable: true), + AreaId = table.Column(type: "nvarchar(450)", nullable: true), + IsDeprecated = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_MissionDefinitions", x => x.Id); + table.ForeignKey( + name: "FK_MissionDefinitions_Areas_AreaId", + column: x => x.AreaId, + principalTable: "Areas", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_MissionDefinitions_MissionRuns_LastRunId", + column: x => x.LastRunId, + principalTable: "MissionRuns", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_MissionDefinitions_Sources_SourceId", + column: x => x.SourceId, + principalTable: "Sources", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Areas_DeckId", + table: "Areas", + column: "DeckId"); + + migrationBuilder.CreateIndex( + name: "IX_Areas_InstallationId", + table: "Areas", + column: "InstallationId"); + + migrationBuilder.CreateIndex( + name: "IX_Areas_PlantId", + table: "Areas", + column: "PlantId"); + + migrationBuilder.CreateIndex( + name: "IX_Decks_InstallationId", + table: "Decks", + column: "InstallationId"); + + migrationBuilder.CreateIndex( + name: "IX_Decks_PlantId", + table: "Decks", + column: "PlantId"); + + migrationBuilder.CreateIndex( + name: "IX_Installations_InstallationCode", + table: "Installations", + column: "InstallationCode", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_MissionDefinitions_AreaId", + table: "MissionDefinitions", + column: "AreaId"); + + migrationBuilder.CreateIndex( + name: "IX_MissionDefinitions_LastRunId", + table: "MissionDefinitions", + column: "LastRunId"); + + migrationBuilder.CreateIndex( + name: "IX_MissionDefinitions_SourceId", + table: "MissionDefinitions", + column: "SourceId"); + + migrationBuilder.CreateIndex( + name: "IX_MissionRuns_AreaId", + table: "MissionRuns", + column: "AreaId"); + + migrationBuilder.CreateIndex( + name: "IX_MissionRuns_RobotId", + table: "MissionRuns", + column: "RobotId"); + + migrationBuilder.CreateIndex( + name: "IX_Plants_InstallationId", + table: "Plants", + column: "InstallationId"); + + migrationBuilder.CreateIndex( + name: "IX_Plants_PlantCode", + table: "Plants", + column: "PlantCode", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_MissionTask_MissionRuns_MissionRunId", + table: "MissionTask", + column: "MissionRunId", + principalTable: "MissionRuns", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Robots_Areas_CurrentAreaId", + table: "Robots", + column: "CurrentAreaId", + principalTable: "Areas", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_SafePositions_Areas_AreaId", + table: "SafePositions", + column: "AreaId", + principalTable: "Areas", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_MissionTask_MissionRuns_MissionRunId", + table: "MissionTask"); + + migrationBuilder.DropForeignKey( + name: "FK_Robots_Areas_CurrentAreaId", + table: "Robots"); + + migrationBuilder.DropForeignKey( + name: "FK_SafePositions_Areas_AreaId", + table: "SafePositions"); + + migrationBuilder.DropTable( + name: "MissionDefinitions"); + + migrationBuilder.DropTable( + name: "MissionRuns"); + + migrationBuilder.DropTable( + name: "Sources"); + + migrationBuilder.DropTable( + name: "Areas"); + + migrationBuilder.DropTable( + name: "Decks"); + + migrationBuilder.DropTable( + name: "Plants"); + + migrationBuilder.DropTable( + name: "Installations"); + + migrationBuilder.RenameColumn( + name: "AreaId", + table: "SafePositions", + newName: "AssetDeckId"); + + migrationBuilder.RenameIndex( + name: "IX_SafePositions_AreaId", + table: "SafePositions", + newName: "IX_SafePositions_AssetDeckId"); + + migrationBuilder.RenameColumn( + name: "CurrentInstallation", + table: "Robots", + newName: "CurrentAsset"); + + migrationBuilder.RenameColumn( + name: "CurrentAreaId", + table: "Robots", + newName: "CurrentAssetDeckId"); + + migrationBuilder.RenameIndex( + name: "IX_Robots_CurrentAreaId", + table: "Robots", + newName: "IX_Robots_CurrentAssetDeckId"); + + migrationBuilder.RenameColumn( + name: "MissionRunId", + table: "MissionTask", + newName: "MissionId"); + + migrationBuilder.RenameIndex( + name: "IX_MissionTask_MissionRunId", + table: "MissionTask", + newName: "IX_MissionTask_MissionId"); + + migrationBuilder.AddColumn( + name: "DefaultLocalizationPose_Orientation_W", + table: "AssetDecks", + type: "real", + nullable: false, + defaultValue: 0f); + + migrationBuilder.AddColumn( + name: "DefaultLocalizationPose_Orientation_X", + table: "AssetDecks", + type: "real", + nullable: false, + defaultValue: 0f); + + migrationBuilder.AddColumn( + name: "DefaultLocalizationPose_Orientation_Y", + table: "AssetDecks", + type: "real", + nullable: false, + defaultValue: 0f); + + migrationBuilder.AddColumn( + name: "DefaultLocalizationPose_Orientation_Z", + table: "AssetDecks", + type: "real", + nullable: false, + defaultValue: 0f); + + migrationBuilder.AddColumn( + name: "DefaultLocalizationPose_Position_X", + table: "AssetDecks", + type: "real", + nullable: false, + defaultValue: 0f); + + migrationBuilder.AddColumn( + name: "DefaultLocalizationPose_Position_Y", + table: "AssetDecks", + type: "real", + nullable: false, + defaultValue: 0f); + + migrationBuilder.AddColumn( + name: "DefaultLocalizationPose_Position_Z", + table: "AssetDecks", + type: "real", + nullable: false, + defaultValue: 0f); + + migrationBuilder.CreateTable( + name: "Missions", + columns: table => new + { + Id = table.Column(type: "nvarchar(450)", nullable: false), + RobotId = table.Column(type: "nvarchar(450)", nullable: false), + AssetCode = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + Comment = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: true), + Description = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: true), + DesiredStartTime = table.Column(type: "datetimeoffset", nullable: false), + EchoMissionId = table.Column(type: "int", maxLength: 200, nullable: true), + EndTime = table.Column(type: "datetimeoffset", nullable: true), + EstimatedDuration = table.Column(type: "bigint", nullable: true), + IsarMissionId = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + Name = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + StartTime = table.Column(type: "datetimeoffset", nullable: true), + Status = table.Column(type: "int", nullable: false), + StatusReason = table.Column(type: "nvarchar(450)", maxLength: 450, nullable: true), + MapMetadata_MapName = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: true), + MapMetadata_Boundary_X1 = table.Column(type: "float", nullable: true), + MapMetadata_Boundary_X2 = table.Column(type: "float", nullable: true), + MapMetadata_Boundary_Y1 = table.Column(type: "float", nullable: true), + MapMetadata_Boundary_Y2 = table.Column(type: "float", nullable: true), + MapMetadata_Boundary_Z1 = table.Column(type: "float", nullable: true), + MapMetadata_Boundary_Z2 = table.Column(type: "float", nullable: true), + MapMetadata_TransformationMatrices_C1 = table.Column(type: "float", nullable: true), + MapMetadata_TransformationMatrices_C2 = table.Column(type: "float", nullable: true), + MapMetadata_TransformationMatrices_D1 = table.Column(type: "float", nullable: true), + MapMetadata_TransformationMatrices_D2 = table.Column(type: "float", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Missions", x => x.Id); + table.ForeignKey( + name: "FK_Missions_Robots_RobotId", + column: x => x.RobotId, + principalTable: "Robots", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AssetDecks_AssetCode_DeckName", + table: "AssetDecks", + columns: new[] { "AssetCode", "DeckName" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_Missions_RobotId", + table: "Missions", + column: "RobotId"); + + migrationBuilder.AddForeignKey( + name: "FK_MissionTask_Missions_MissionId", + table: "MissionTask", + column: "MissionId", + principalTable: "Missions", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Robots_AssetDecks_CurrentAssetDeckId", + table: "Robots", + column: "CurrentAssetDeckId", + principalTable: "AssetDecks", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_SafePositions_AssetDecks_AssetDeckId", + table: "SafePositions", + column: "AssetDeckId", + principalTable: "AssetDecks", + principalColumn: "Id"); + } + } +} diff --git a/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs b/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs index 1a1f553b8..aa4ef433d 100644 --- a/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs +++ b/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs @@ -17,10 +17,44 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "6.0.13") + .HasAnnotation("ProductVersion", "7.0.8") .HasAnnotation("Relational:MaxIdentifierLength", 128); - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Api.Database.Models.Area", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("DeckId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("InstallationId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PlantId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("DeckId"); + + b.HasIndex("InstallationId"); + + b.HasIndex("PlantId"); + + b.ToTable("Areas"); + }); modelBuilder.Entity("Api.Database.Models.AssetDeck", b => { @@ -40,23 +74,116 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("AssetCode", "DeckName") + b.ToTable("AssetDecks"); + }); + + modelBuilder.Entity("Api.Database.Models.Deck", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("InstallationId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PlantId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId"); + + b.HasIndex("PlantId"); + + b.ToTable("Decks"); + }); + + modelBuilder.Entity("Api.Database.Models.Installation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("InstallationCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("InstallationCode") .IsUnique(); - b.ToTable("AssetDecks"); + b.ToTable("Installations"); }); - modelBuilder.Entity("Api.Database.Models.Mission", b => + modelBuilder.Entity("Api.Database.Models.MissionDefinition", b => { b.Property("Id") .ValueGeneratedOnAdd() .HasColumnType("nvarchar(450)"); - b.Property("AssetCode") + b.Property("AreaId") + .HasColumnType("nvarchar(450)"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("InspectionFrequency") + .HasColumnType("time"); + + b.Property("InstallationCode") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsDeprecated") + .HasColumnType("bit"); + + b.Property("LastRunId") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") .IsRequired() .HasMaxLength(200) .HasColumnType("nvarchar(200)"); + b.Property("SourceId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.HasKey("Id"); + + b.HasIndex("AreaId"); + + b.HasIndex("LastRunId"); + + b.HasIndex("SourceId"); + + b.ToTable("MissionDefinitions"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("AreaId") + .HasColumnType("nvarchar(450)"); + b.Property("Comment") .HasMaxLength(1000) .HasColumnType("nvarchar(1000)"); @@ -68,20 +195,24 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DesiredStartTime") .HasColumnType("datetimeoffset"); - b.Property("EchoMissionId") - .HasMaxLength(200) - .HasColumnType("int"); - b.Property("EndTime") .HasColumnType("datetimeoffset"); b.Property("EstimatedDuration") .HasColumnType("bigint"); + b.Property("InstallationCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + b.Property("IsarMissionId") .HasMaxLength(200) .HasColumnType("nvarchar(200)"); + b.Property("MissionId") + .HasColumnType("nvarchar(max)"); + b.Property("Name") .IsRequired() .HasMaxLength(200) @@ -103,9 +234,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("AreaId"); + b.HasIndex("RobotId"); - b.ToTable("Missions"); + b.ToTable("MissionRuns"); + }); + + modelBuilder.Entity("Api.Database.Models.Plant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("InstallationId") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PlantCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId"); + + b.HasIndex("PlantCode") + .IsUnique(); + + b.ToTable("Plants"); }); modelBuilder.Entity("Api.Database.Models.Robot", b => @@ -117,13 +280,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("BatteryLevel") .HasColumnType("real"); - b.Property("CurrentAsset") + b.Property("CurrentAreaId") + .HasColumnType("nvarchar(450)"); + + b.Property("CurrentInstallation") .IsRequired() .HasColumnType("nvarchar(max)"); - b.Property("CurrentAssetDeckId") - .HasColumnType("nvarchar(450)"); - b.Property("CurrentMissionId") .HasColumnType("nvarchar(max)"); @@ -165,7 +328,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); - b.HasIndex("CurrentAssetDeckId"); + b.HasIndex("CurrentAreaId"); b.HasIndex("ModelId"); @@ -208,33 +371,69 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("nvarchar(450)"); - b.Property("AssetDeckId") + b.Property("AreaId") .HasColumnType("nvarchar(450)"); b.HasKey("Id"); - b.HasIndex("AssetDeckId"); + b.HasIndex("AreaId"); b.ToTable("SafePositions"); }); - modelBuilder.Entity("Api.Database.Models.AssetDeck", b => + modelBuilder.Entity("Api.Database.Models.Source", b => { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("nvarchar(450)"); + + b.Property("SourceId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Sources"); + }); + + modelBuilder.Entity("Api.Database.Models.Area", b => + { + b.HasOne("Api.Database.Models.Deck", "Deck") + .WithMany() + .HasForeignKey("DeckId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Api.Database.Models.Plant", "Plant") + .WithMany() + .HasForeignKey("PlantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + b.OwnsOne("Api.Database.Models.Pose", "DefaultLocalizationPose", b1 => { - b1.Property("AssetDeckId") + b1.Property("AreaId") .HasColumnType("nvarchar(450)"); - b1.HasKey("AssetDeckId"); + b1.HasKey("AreaId"); - b1.ToTable("AssetDecks"); + b1.ToTable("Areas"); b1.WithOwner() - .HasForeignKey("AssetDeckId"); + .HasForeignKey("AreaId"); b1.OwnsOne("Api.Database.Models.Orientation", "Orientation", b2 => { - b2.Property("PoseAssetDeckId") + b2.Property("PoseAreaId") .HasColumnType("nvarchar(450)"); b2.Property("W") @@ -249,17 +448,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b2.Property("Z") .HasColumnType("real"); - b2.HasKey("PoseAssetDeckId"); + b2.HasKey("PoseAreaId"); - b2.ToTable("AssetDecks"); + b2.ToTable("Areas"); b2.WithOwner() - .HasForeignKey("PoseAssetDeckId"); + .HasForeignKey("PoseAreaId"); }); b1.OwnsOne("Api.Database.Models.Position", "Position", b2 => { - b2.Property("PoseAssetDeckId") + b2.Property("PoseAreaId") .HasColumnType("nvarchar(450)"); b2.Property("X") @@ -271,12 +470,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b2.Property("Z") .HasColumnType("real"); - b2.HasKey("PoseAssetDeckId"); + b2.HasKey("PoseAreaId"); - b2.ToTable("AssetDecks"); + b2.ToTable("Areas"); b2.WithOwner() - .HasForeignKey("PoseAssetDeckId"); + .HasForeignKey("PoseAreaId"); }); b1.Navigation("Orientation") @@ -286,12 +485,147 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + b.OwnsOne("Api.Database.Models.MapMetadata", "MapMetadata", b1 => + { + b1.Property("AreaId") + .HasColumnType("nvarchar(450)"); + + b1.Property("MapName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b1.HasKey("AreaId"); + + b1.ToTable("Areas"); + + b1.WithOwner() + .HasForeignKey("AreaId"); + + b1.OwnsOne("Api.Database.Models.Boundary", "Boundary", b2 => + { + b2.Property("MapMetadataAreaId") + .HasColumnType("nvarchar(450)"); + + b2.Property("X1") + .HasColumnType("float"); + + b2.Property("X2") + .HasColumnType("float"); + + b2.Property("Y1") + .HasColumnType("float"); + + b2.Property("Y2") + .HasColumnType("float"); + + b2.Property("Z1") + .HasColumnType("float"); + + b2.Property("Z2") + .HasColumnType("float"); + + b2.HasKey("MapMetadataAreaId"); + + b2.ToTable("Areas"); + + b2.WithOwner() + .HasForeignKey("MapMetadataAreaId"); + }); + + b1.OwnsOne("Api.Database.Models.TransformationMatrices", "TransformationMatrices", b2 => + { + b2.Property("MapMetadataAreaId") + .HasColumnType("nvarchar(450)"); + + b2.Property("C1") + .HasColumnType("float"); + + b2.Property("C2") + .HasColumnType("float"); + + b2.Property("D1") + .HasColumnType("float"); + + b2.Property("D2") + .HasColumnType("float"); + + b2.HasKey("MapMetadataAreaId"); + + b2.ToTable("Areas"); + + b2.WithOwner() + .HasForeignKey("MapMetadataAreaId"); + }); + + b1.Navigation("Boundary") + .IsRequired(); + + b1.Navigation("TransformationMatrices") + .IsRequired(); + }); + + b.Navigation("Deck"); + b.Navigation("DefaultLocalizationPose") .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("MapMetadata") + .IsRequired(); + + b.Navigation("Plant"); }); - modelBuilder.Entity("Api.Database.Models.Mission", b => + modelBuilder.Entity("Api.Database.Models.Deck", b => { + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Api.Database.Models.Plant", "Plant") + .WithMany() + .HasForeignKey("PlantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Plant"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionDefinition", b => + { + b.HasOne("Api.Database.Models.Area", "Area") + .WithMany() + .HasForeignKey("AreaId"); + + b.HasOne("Api.Database.Models.MissionRun", "LastRun") + .WithMany() + .HasForeignKey("LastRunId"); + + b.HasOne("Api.Database.Models.Source", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Area"); + + b.Navigation("LastRun"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionRun", b => + { + b.HasOne("Api.Database.Models.Area", "Area") + .WithMany() + .HasForeignKey("AreaId"); + b.HasOne("Api.Database.Models.Robot", "Robot") .WithMany() .HasForeignKey("RobotId") @@ -300,7 +634,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.OwnsOne("Api.Database.Models.MapMetadata", "MapMetadata", b1 => { - b1.Property("MissionId") + b1.Property("MissionRunId") .HasColumnType("nvarchar(450)"); b1.Property("MapName") @@ -308,16 +642,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(200) .HasColumnType("nvarchar(200)"); - b1.HasKey("MissionId"); + b1.HasKey("MissionRunId"); - b1.ToTable("Missions"); + b1.ToTable("MissionRuns"); b1.WithOwner() - .HasForeignKey("MissionId"); + .HasForeignKey("MissionRunId"); b1.OwnsOne("Api.Database.Models.Boundary", "Boundary", b2 => { - b2.Property("MapMetadataMissionId") + b2.Property("MapMetadataMissionRunId") .HasColumnType("nvarchar(450)"); b2.Property("X1") @@ -338,17 +672,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b2.Property("Z2") .HasColumnType("float"); - b2.HasKey("MapMetadataMissionId"); + b2.HasKey("MapMetadataMissionRunId"); - b2.ToTable("Missions"); + b2.ToTable("MissionRuns"); b2.WithOwner() - .HasForeignKey("MapMetadataMissionId"); + .HasForeignKey("MapMetadataMissionRunId"); }); b1.OwnsOne("Api.Database.Models.TransformationMatrices", "TransformationMatrices", b2 => { - b2.Property("MapMetadataMissionId") + b2.Property("MapMetadataMissionRunId") .HasColumnType("nvarchar(450)"); b2.Property("C1") @@ -363,12 +697,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b2.Property("D2") .HasColumnType("float"); - b2.HasKey("MapMetadataMissionId"); + b2.HasKey("MapMetadataMissionRunId"); - b2.ToTable("Missions"); + b2.ToTable("MissionRuns"); b2.WithOwner() - .HasForeignKey("MapMetadataMissionId"); + .HasForeignKey("MapMetadataMissionRunId"); }); b1.Navigation("Boundary") @@ -402,7 +736,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasMaxLength(200) .HasColumnType("nvarchar(200)"); - b1.Property("MissionId") + b1.Property("MissionRunId") .IsRequired() .HasColumnType("nvarchar(450)"); @@ -421,12 +755,12 @@ protected override void BuildModel(ModelBuilder modelBuilder) b1.HasKey("Id"); - b1.HasIndex("MissionId"); + b1.HasIndex("MissionRunId"); b1.ToTable("MissionTask"); b1.WithOwner() - .HasForeignKey("MissionId"); + .HasForeignKey("MissionRunId"); b1.OwnsMany("Api.Database.Models.Inspection", "Inspections", b2 => { @@ -572,6 +906,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); + b.Navigation("Area"); + b.Navigation("MapMetadata"); b.Navigation("Robot"); @@ -579,11 +915,22 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Tasks"); }); + modelBuilder.Entity("Api.Database.Models.Plant", b => + { + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + }); + modelBuilder.Entity("Api.Database.Models.Robot", b => { - b.HasOne("Api.Database.Models.AssetDeck", "CurrentAssetDeck") + b.HasOne("Api.Database.Models.Area", "CurrentArea") .WithMany() - .HasForeignKey("CurrentAssetDeckId"); + .HasForeignKey("CurrentAreaId"); b.HasOne("Api.Database.Models.RobotModel", "Model") .WithMany() @@ -695,7 +1042,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("RobotId"); }); - b.Navigation("CurrentAssetDeck"); + b.Navigation("CurrentArea"); b.Navigation("Model"); @@ -707,9 +1054,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Api.Database.Models.SafePosition", b => { - b.HasOne("Api.Database.Models.AssetDeck", null) + b.HasOne("Api.Database.Models.Area", null) .WithMany("SafePositions") - .HasForeignKey("AssetDeckId"); + .HasForeignKey("AreaId"); b.OwnsOne("Api.Database.Models.Pose", "Pose", b1 => { @@ -781,7 +1128,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); }); - modelBuilder.Entity("Api.Database.Models.AssetDeck", b => + modelBuilder.Entity("Api.Database.Models.Area", b => { b.Navigation("SafePositions"); }); diff --git a/backend/api/Options/StorageOptions.cs b/backend/api/Options/StorageOptions.cs new file mode 100644 index 000000000..ff2a77f5d --- /dev/null +++ b/backend/api/Options/StorageOptions.cs @@ -0,0 +1,8 @@ +namespace Api.Options +{ + public class StorageOptions + { + public string CustomMissionContainerName { get; set; } = ""; + public string AccountName { get; set; } = ""; + } +} diff --git a/backend/api/Program.cs b/backend/api/Program.cs index 43534c1ab..c7c4d7da2 100644 --- a/backend/api/Program.cs +++ b/backend/api/Program.cs @@ -40,15 +40,20 @@ builder.Services.AddApplicationInsightsTelemetry(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); @@ -57,6 +62,7 @@ builder.Services.Configure(builder.Configuration.GetSection("AzureAd")); builder.Services.Configure(builder.Configuration.GetSection("Maps")); +builder.Services.Configure(builder.Configuration.GetSection("Blob")); builder.Services .AddControllers() diff --git a/backend/api/Services/AreaService.cs b/backend/api/Services/AreaService.cs new file mode 100644 index 000000000..550f74c83 --- /dev/null +++ b/backend/api/Services/AreaService.cs @@ -0,0 +1,212 @@ +using Api.Controllers.Models; +using Api.Database.Context; +using Api.Database.Models; +using Api.Utilities; +using Microsoft.EntityFrameworkCore; + +namespace Api.Services +{ + public interface IAreaService + { + public abstract Task> ReadAll(); + + public abstract Task ReadById(string id); + + public abstract Task> ReadByInstallation(string installationCode); + + public abstract Task ReadByInstallationAndName(string installationCode, string areaName); + + public abstract Task Create(CreateAreaQuery newArea); + + public abstract Task Create(CreateAreaQuery newArea, List safePositions); + + public abstract Task Update(Area area); + + public abstract Task AddSafePosition(string installationCode, string areaName, SafePosition safePosition); + + public abstract Task Delete(string id); + + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1309:Use ordinal StringComparison", + Justification = "EF Core refrains from translating string comparison overloads to SQL" + )] + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1304:Specify CultureInfo", + Justification = "Entity framework does not support translating culture info to SQL calls" + )] + public class AreaService : IAreaService + { + private readonly FlotillaDbContext _context; + private readonly IInstallationService _installationService; + private readonly IPlantService _plantService; + private readonly IDeckService _deckService; + + public AreaService( + FlotillaDbContext context, IInstallationService installationService, IPlantService plantService, IDeckService deckService) + { + _context = context; + _installationService = installationService; + _plantService = plantService; + _deckService = deckService; + } + + public async Task> ReadAll() + { + return await GetAreas().ToListAsync(); + } + + private IQueryable GetAreas() + { + return _context.Areas.Include(a => a.SafePositions) + .Include(a => a.Deck).Include(d => d.Plant).Include(i => i.Installation); + } + + public async Task ReadById(string id) + { + return await GetAreas() + .FirstOrDefaultAsync(a => a.Id.Equals(id)); + } + + public async Task ReadByInstallationAndName(Installation? installation, string areaName) + { + if (installation == null) + return null; + + return await _context.Areas.Where(a => + a.Name.ToLower().Equals(areaName.ToLower()) && + a.Installation.Id.Equals(installation.Id) + ).Include(a => a.SafePositions).Include(a => a.Installation) + .Include(a => a.Plant).Include(a => a.Deck).FirstOrDefaultAsync(); + } + + public async Task ReadByInstallationAndName(string installationCode, string areaName) + { + var installation = await _installationService.ReadByName(installationCode); + if (installation == null) + return null; + + return await _context.Areas.Where(a => + a.Installation.Id.Equals(installation.Id) && + a.Name.ToLower().Equals(areaName.ToLower()) + ).Include(a => a.SafePositions).Include(a => a.Installation) + .Include(a => a.Plant).Include(a => a.Deck).FirstOrDefaultAsync(); + } + + public async Task> ReadByInstallation(string installationCode) + { + var installation = await _installationService.ReadByName(installationCode); + if (installation == null) + return new List(); + + return await _context.Areas.Where(a => + a.Installation.Id.Equals(installation.Id)).Include(a => a.SafePositions).Include(a => a.Installation) + .Include(a => a.Plant).Include(a => a.Deck).ToListAsync(); + } + + public async Task ReadByInstallationAndPlantAndDeckAndName(Installation installation, Plant plant, Deck deck, string areaName) + { + return await _context.Areas.Where(a => + a.Deck.Id.Equals(deck.Id) && + a.Plant.Id.Equals(plant.Id) && + a.Installation.Id.Equals(installation.Id) && + a.Name.ToLower().Equals(areaName.ToLower()) + ).Include(a => a.Deck).Include(d => d.Plant).Include(i => i.Installation) + .Include(a => a.SafePositions).FirstOrDefaultAsync(); + } + + public async Task Create(CreateAreaQuery newAreaQuery, List positions) + { + var safePositions = new List(); + foreach (var pose in positions) + { + safePositions.Add(new SafePosition(pose)); + } + + var installation = await _installationService.ReadByName(newAreaQuery.InstallationCode); + if (installation == null) + { + throw new InstallationNotFoundException($"No installation with name {newAreaQuery.InstallationCode} could be found"); + } + + var plant = await _plantService.ReadByInstallationAndName(installation, newAreaQuery.PlantCode); + if (plant == null) + { + throw new PlantNotFoundException($"No plant with name {newAreaQuery.PlantCode} could be found"); + } + + var deck = await _deckService.ReadByInstallationAndPlantAndName(installation, plant, newAreaQuery.DeckName); + if (deck == null) + { + throw new DeckNotFoundException($"No deck with name {newAreaQuery.DeckName} could be found"); + } + + var existingArea = await ReadByInstallationAndPlantAndDeckAndName( + installation, plant, deck, newAreaQuery.AreaName); + if (existingArea != null) + { + throw new AreaNotFoundException($"No area with name {newAreaQuery.AreaName} could be found"); + } + + var newArea = new Area + { + Name = newAreaQuery.AreaName, + DefaultLocalizationPose = newAreaQuery.DefaultLocalizationPose, + SafePositions = safePositions, + MapMetadata = new MapMetadata(), + Deck = deck, + Plant = plant, + Installation = installation + }; + + await _context.Areas.AddAsync(newArea); + await _context.SaveChangesAsync(); + return newArea; + } + + public async Task Create(CreateAreaQuery newArea) + { + var area = await Create(newArea, new List()); + return area; + } + + public async Task AddSafePosition(string installationCode, string areaName, SafePosition safePosition) + { + var area = await ReadByInstallationAndName(installationCode, areaName); + if (area is null) + { + return null; + } + + area.SafePositions.Add(safePosition); + _context.Areas.Update(area); + await _context.SaveChangesAsync(); + return area; + } + + public async Task Update(Area area) + { + var entry = _context.Update(area); + await _context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task Delete(string id) + { + var area = await GetAreas() + .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); + if (area is null) + { + return null; + } + + _context.Areas.Remove(area); + await _context.SaveChangesAsync(); + + return area; + } + } +} diff --git a/backend/api/Services/AssetDeckService.cs b/backend/api/Services/AssetDeckService.cs deleted file mode 100644 index 9a5980bda..000000000 --- a/backend/api/Services/AssetDeckService.cs +++ /dev/null @@ -1,140 +0,0 @@ -using Api.Controllers.Models; -using Api.Database.Context; -using Api.Database.Models; -using Microsoft.EntityFrameworkCore; - -namespace Api.Services -{ - public interface IAssetDeckService - { - public abstract Task> ReadAll(); - - public abstract Task ReadById(string id); - - public abstract Task> ReadByAsset(string asset); - - public abstract Task ReadByAssetAndDeck(string asset, string deck); - - public abstract Task Create(CreateAssetDeckQuery newAssetDeck); - - public abstract Task Create(CreateAssetDeckQuery newAssetDeck, List safePositions); - - public abstract Task Update(AssetDeck assetDeck); - - public abstract Task AddSafePosition(string asset, string deck, SafePosition safePosition); - - public abstract Task Delete(string id); - - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Globalization", - "CA1309:Use ordinal StringComparison", - Justification = "EF Core refrains from translating string comparison overloads to SQL" - )] - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Globalization", - "CA1304:Specify CultureInfo", - Justification = "Entity framework does not support translating culture info to SQL calls" - )] - public class AssetDeckService : IAssetDeckService - { - private readonly FlotillaDbContext _context; - - public AssetDeckService(FlotillaDbContext context) - { - _context = context; - } - - public async Task> ReadAll() - { - return await GetAssetDecks().ToListAsync(); - } - - private IQueryable GetAssetDecks() - { - return _context.AssetDecks.Include(a => a.SafePositions); - } - - public async Task ReadById(string id) - { - return await GetAssetDecks() - .FirstOrDefaultAsync(assetDeck => assetDeck.Id.Equals(id)); - } - - public async Task> ReadByAsset(string asset) - { - - return await _context.AssetDecks.Where(a => - a.AssetCode.ToLower().Equals(asset.ToLower())).Include(a => a.SafePositions).ToListAsync(); - } - - public async Task ReadByAssetAndDeck(string asset, string deck) - { - return await _context.AssetDecks.Where(a => - a.AssetCode.ToLower().Equals(asset.ToLower()) && - a.DeckName.ToLower().Equals(deck.ToLower()) - ).Include(a => a.SafePositions).FirstOrDefaultAsync(); - } - - public async Task Create(CreateAssetDeckQuery newAssetDeck, List safePositions) - { - var sp = new List(); - foreach (var p in safePositions) - { - sp.Add(new SafePosition(p)); - } - var assetDeck = new AssetDeck - { - AssetCode = newAssetDeck.AssetCode, - DeckName = newAssetDeck.DeckName, - DefaultLocalizationPose = newAssetDeck.DefaultLocalizationPose, - SafePositions = sp - }; - await _context.AssetDecks.AddAsync(assetDeck); - await _context.SaveChangesAsync(); - return assetDeck; - } - - public async Task Create(CreateAssetDeckQuery newAssetDeck) - { - var assetDeck = await Create(newAssetDeck, new List()); - return assetDeck; - } - - public async Task AddSafePosition(string asset, string deck, SafePosition safePosition) - { - var assetDeck = await ReadByAssetAndDeck(asset, deck); - if (assetDeck is null) - { - return null; - } - assetDeck.SafePositions.Add(safePosition); - _context.AssetDecks.Update(assetDeck); - await _context.SaveChangesAsync(); - return assetDeck; - } - - public async Task Update(AssetDeck assetDeck) - { - var entry = _context.Update(assetDeck); - await _context.SaveChangesAsync(); - return entry.Entity; - } - - public async Task Delete(string id) - { - var assetDeck = await GetAssetDecks() - .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); - if (assetDeck is null) - { - return null; - } - - _context.AssetDecks.Remove(assetDeck); - await _context.SaveChangesAsync(); - - return assetDeck; - } - } -} diff --git a/backend/api/Services/CustomMissionService.cs b/backend/api/Services/CustomMissionService.cs new file mode 100644 index 000000000..b6566e20f --- /dev/null +++ b/backend/api/Services/CustomMissionService.cs @@ -0,0 +1,54 @@ + + +using System.Text.Json; +using Api.Database.Models; +using Api.Options; +using Microsoft.Extensions.Options; + +namespace Api.Services +{ + + public interface ICustomMissionService + { + string UploadSource(List tasks); + Task?> GetMissionTasksFromMissionId(string id); + } + + public class CustomMissionService : ICustomMissionService + { + private readonly IOptions _storageOptions; + private readonly IBlobService _blobService; + + public CustomMissionService(IOptions storageOptions, IBlobService blobService) + { + _storageOptions = storageOptions; + _blobService = blobService; + } + + public string UploadSource(List tasks) + { + string json = JsonSerializer.Serialize(tasks); + string id = Guid.NewGuid().ToString(); + _blobService.UploadJsonToBlob(json, id, _storageOptions.Value.CustomMissionContainerName, _storageOptions.Value.AccountName, false); + + return id; + } + + public async Task?> GetMissionTasksFromMissionId(string id) + { + List? content; + try + { + byte[] rawContent = await _blobService.DownloadBlob(id, _storageOptions.Value.CustomMissionContainerName, _storageOptions.Value.AccountName); + var rawBinaryContent = new BinaryData(rawContent); + content = rawBinaryContent.ToObjectFromJson>(); + } + catch (Exception) + { + return null; + } + + return content; + } + } +} diff --git a/backend/api/Services/DeckService.cs b/backend/api/Services/DeckService.cs new file mode 100644 index 000000000..bd75576bb --- /dev/null +++ b/backend/api/Services/DeckService.cs @@ -0,0 +1,144 @@ +using Api.Controllers.Models; +using Api.Database.Context; +using Api.Database.Models; +using Api.Utilities; +using Microsoft.EntityFrameworkCore; + +namespace Api.Services +{ + public interface IDeckService + { + public abstract Task> ReadAll(); + + public abstract Task ReadById(string id); + + public abstract Task> ReadByInstallation(string installationCode); + + public abstract Task ReadByName(string deckName); + + public abstract Task ReadByInstallationAndPlantAndName(Installation installation, Plant plant, string deckName); + + public abstract Task Create(CreateDeckQuery newDeck); + + public abstract Task Update(Deck deck); + + public abstract Task Delete(string id); + + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1309:Use ordinal StringComparison", + Justification = "EF Core refrains from translating string comparison overloads to SQL" + )] + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1304:Specify CultureInfo", + Justification = "Entity framework does not support translating culture info to SQL calls" + )] + public class DeckService : IDeckService + { + private readonly FlotillaDbContext _context; + private readonly IInstallationService _installationService; + private readonly IPlantService _plantService; + + public DeckService(FlotillaDbContext context, IInstallationService installationService, IPlantService plantService) + { + _context = context; + _installationService = installationService; + _plantService = plantService; + } + + public async Task> ReadAll() + { + return await GetDecks().ToListAsync(); + } + + private IQueryable GetDecks() + { + return _context.Decks; + } + + public async Task ReadById(string id) + { + return await GetDecks() + .FirstOrDefaultAsync(a => a.Id.Equals(id)); + } + + public async Task> ReadByInstallation(string installationCode) + { + var installation = await _installationService.ReadByName(installationCode); + if (installation == null) + return new List(); + return await _context.Decks.Where(a => + a.Installation.Id.Equals(installation.Id)).ToListAsync(); + } + + public async Task ReadByName(string deckName) + { + if (deckName == null) + return null; + return await _context.Decks.Where(a => + a.Name.ToLower().Equals(deckName.ToLower()) + ).FirstOrDefaultAsync(); + } + + public async Task ReadByInstallationAndPlantAndName(Installation installation, Plant plant, string name) + { + return await _context.Decks.Where(a => + a.Plant.Id.Equals(plant.Id) && + a.Installation.Id.Equals(installation.Id) && + a.Name.ToLower().Equals(name.ToLower()) + ).Include(d => d.Plant).Include(i => i.Installation).FirstOrDefaultAsync(); + } + + public async Task Create(CreateDeckQuery newDeckQuery) + { + var installation = await _installationService.ReadByName(newDeckQuery.InstallationCode); + if (installation == null) + { + throw new InstallationNotFoundException($"No installation with name {newDeckQuery.InstallationCode} could be found"); + } + var plant = await _plantService.ReadByInstallationAndName(installation, newDeckQuery.PlantCode); + if (plant == null) + { + throw new PlantNotFoundException($"No plant with name {newDeckQuery.PlantCode} could be found"); + } + var deck = await ReadByInstallationAndPlantAndName(installation, plant, newDeckQuery.Name); + if (deck == null) + { + deck = new Deck + { + Name = newDeckQuery.Name, + Installation = installation, + Plant = plant + }; + await _context.Decks.AddAsync(deck); + await _context.SaveChangesAsync(); + } + return deck!; + } + + public async Task Update(Deck deck) + { + var entry = _context.Update(deck); + await _context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task Delete(string id) + { + var deck = await GetDecks() + .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); + if (deck is null) + { + return null; + } + + _context.Decks.Remove(deck); + await _context.SaveChangesAsync(); + + return deck; + } + } +} diff --git a/backend/api/Services/EchoService.cs b/backend/api/Services/EchoService.cs index f4ed55622..4f0d4642e 100644 --- a/backend/api/Services/EchoService.cs +++ b/backend/api/Services/EchoService.cs @@ -10,12 +10,11 @@ namespace Api.Services { public interface IEchoService { - public abstract Task> GetAvailableMissions(string? installationCode); + public abstract Task> GetAvailableMissions(string? installationCode); public abstract Task GetMissionById(int missionId); public abstract Task> GetEchoPlantInfos(); - public abstract Task GetRobotPoseFromPoseId(int poseId); } @@ -31,7 +30,7 @@ public EchoService(IDownstreamWebApi downstreamWebApi, ILogger logg _logger = logger; } - public async Task> GetAvailableMissions(string? installationCode) + public async Task> GetAvailableMissions(string? installationCode) { string relativePath = string.IsNullOrEmpty(installationCode) ? $"robots/robot-plan?Status=Ready" @@ -79,13 +78,13 @@ public async Task GetMissionById(int missionId) if (echoMission is null) throw new JsonException("Failed to deserialize mission from Echo"); - var mission = ProcessEchoMission(echoMission); - if (mission == null) + var processedEchoMission = ProcessEchoMission(echoMission); + if (processedEchoMission == null) { throw new InvalidDataException($"EchoMission with id: {missionId} is invalid."); } - return mission; + return processedEchoMission; } public async Task> GetEchoPlantInfos() @@ -170,9 +169,9 @@ private List ProcessPlanItems(List planItems, string installa return tags; } - private List ProcessAvailableEchoMission(List echoMissions) + private List ProcessAvailableEchoMission(List echoMissions) { - var availableMissions = new List(); + var availableMissions = new List(); foreach (var echoMission in echoMissions) { @@ -180,13 +179,13 @@ private List ProcessAvailableEchoMission(List ProcessAvailableEchoMission(List echoPlantInfoResponse var echoPlantInfo = new EchoPlantInfo() { - InstallationCode = plant.InstallationCode, + PlantCode = plant.InstallationCode, ProjectDescription = plant.ProjectDescription }; diff --git a/backend/api/Services/InstallationService.cs b/backend/api/Services/InstallationService.cs new file mode 100644 index 000000000..7ae952d4b --- /dev/null +++ b/backend/api/Services/InstallationService.cs @@ -0,0 +1,107 @@ +using Api.Controllers.Models; +using Api.Database.Context; +using Api.Database.Models; +using Microsoft.EntityFrameworkCore; + +namespace Api.Services +{ + public interface IInstallationService + { + public abstract Task> ReadAll(); + + public abstract Task ReadById(string id); + + public abstract Task ReadByName(string installation); + + public abstract Task Create(CreateInstallationQuery newInstallation); + + public abstract Task Update(Installation installation); + + public abstract Task Delete(string id); + + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1309:Use ordinal StringComparison", + Justification = "EF Core refrains from translating string comparison overloads to SQL" + )] + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1304:Specify CultureInfo", + Justification = "Entity framework does not support translating culture info to SQL calls" + )] + public class InstallationService : IInstallationService + { + private readonly FlotillaDbContext _context; + + public InstallationService(FlotillaDbContext context) + { + _context = context; + } + + public async Task> ReadAll() + { + return await GetInstallations().ToListAsync(); + } + + private IQueryable GetInstallations() + { + return _context.Installations; + } + + public async Task ReadById(string id) + { + return await GetInstallations() + .FirstOrDefaultAsync(a => a.Id.Equals(id)); + } + + public async Task ReadByName(string installationCode) + { + if (installationCode == null) + return null; + return await _context.Installations.Where(a => + a.InstallationCode.ToLower().Equals(installationCode.ToLower()) + ).FirstOrDefaultAsync(); + } + + public async Task Create(CreateInstallationQuery newInstallationQuery) + { + var installation = await ReadByName(newInstallationQuery.InstallationCode); + if (installation == null) + { + installation = new Installation + { + Name = newInstallationQuery.Name, + InstallationCode = newInstallationQuery.InstallationCode + }; + await _context.Installations.AddAsync(installation); + await _context.SaveChangesAsync(); + } + + return installation!; + } + + public async Task Update(Installation installation) + { + var entry = _context.Update(installation); + await _context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task Delete(string id) + { + var installation = await GetInstallations() + .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); + if (installation is null) + { + return null; + } + + _context.Installations.Remove(installation); + await _context.SaveChangesAsync(); + + return installation; + } + } +} diff --git a/backend/api/Services/IsarService.cs b/backend/api/Services/IsarService.cs index 13aa336a2..6efb457d3 100644 --- a/backend/api/Services/IsarService.cs +++ b/backend/api/Services/IsarService.cs @@ -8,7 +8,7 @@ namespace Api.Services { public interface IIsarService { - public abstract Task StartMission(Robot robot, Mission mission); + public abstract Task StartMission(Robot robot, MissionRun missionRun); public abstract Task StopMission(Robot robot); @@ -68,13 +68,13 @@ private async Task CallApi( return response; } - public async Task StartMission(Robot robot, Mission mission) + public async Task StartMission(Robot robot, MissionRun missionRun) { var response = await CallApi( HttpMethod.Post, robot.IsarUri, "schedule/start-mission", - new { mission_definition = new IsarMissionDefinition(mission) } + new { mission_definition = new IsarMissionDefinition(missionRun) } ); if (!response.IsSuccessStatusCode) diff --git a/backend/api/Services/MapService.cs b/backend/api/Services/MapService.cs index 6aee8aa7f..ce5169433 100644 --- a/backend/api/Services/MapService.cs +++ b/backend/api/Services/MapService.cs @@ -3,13 +3,14 @@ using Api.Options; using Azure.Storage.Blobs.Models; using Microsoft.Extensions.Options; + namespace Api.Services { public interface IMapService { - public Task FetchMapImage(string mapName, string assetCode); - public Task ChooseMapFromPositions(IList positions, string assetCode); - public Task AssignMapToMission(Mission mission); + public Task FetchMapImage(string mapName, string installationCode); + public Task ChooseMapFromPositions(IList positions, string installationCode); + public Task AssignMapToMission(MissionRun mission); } public class MapService : IMapService @@ -30,17 +31,17 @@ IBlobService blobService _blobService = blobService; } - public async Task FetchMapImage(string mapName, string assetCode) + public async Task FetchMapImage(string mapName, string installationCode) { - return await _blobService.DownloadBlob(mapName, assetCode, _blobOptions.Value.StorageAccount); + return await _blobService.DownloadBlob(mapName, installationCode, _blobOptions.Value.StorageAccount); } - public async Task ChooseMapFromPositions(IList positions, string assetCode) + public async Task ChooseMapFromPositions(IList positions, string installationCode) { var boundaries = new Dictionary(); var imageSizes = new Dictionary(); - var blobs = _blobService.FetchAllBlobs(assetCode, _blobOptions.Value.StorageAccount); + var blobs = _blobService.FetchAllBlobs(installationCode, _blobOptions.Value.StorageAccount); await foreach (var blob in blobs) { @@ -70,21 +71,21 @@ public async Task FetchMapImage(string mapName, string assetCode) return map; } - public async Task AssignMapToMission(Mission mission) + public async Task AssignMapToMission(MissionRun missionRun) { MapMetadata? mapMetadata; var positions = new List(); - foreach (var task in mission.Tasks) + foreach (var task in missionRun.Tasks) { positions.Add(task.InspectionTarget); } try { - mapMetadata = await ChooseMapFromPositions(positions, mission.AssetCode); + mapMetadata = await ChooseMapFromPositions(positions, missionRun.InstallationCode); } catch (ArgumentOutOfRangeException) { - _logger.LogWarning("Unable to find a map for mission '{missionId}'", mission.Id); + _logger.LogWarning("Unable to find a map for mission '{missionId}'", missionRun.Id); return; } @@ -93,8 +94,8 @@ public async Task AssignMapToMission(Mission mission) return; } - mission.MapMetadata = mapMetadata; - _logger.LogInformation("Assigned map {map} to mission {mission}", mapMetadata.MapName, mission.Name); + missionRun.MapMetadata = mapMetadata; + _logger.LogInformation("Assigned map {map} to mission {mission}", mapMetadata.MapName, missionRun.Name); } private Boundary ExtractMapMetadata(BlobItem map) diff --git a/backend/api/Services/MissionDefinitionService.cs b/backend/api/Services/MissionDefinitionService.cs new file mode 100644 index 000000000..fec3af743 --- /dev/null +++ b/backend/api/Services/MissionDefinitionService.cs @@ -0,0 +1,165 @@ +using System.Linq.Dynamic.Core; +using System.Linq.Expressions; +using Api.Controllers.Models; +using Api.Database.Context; +using Api.Database.Models; +using Api.Utilities; +using Microsoft.EntityFrameworkCore; + +namespace Api.Services +{ + public interface IMissionDefinitionService + { + public abstract Task Create(MissionDefinition missionDefinition); + + public abstract Task ReadById(string id); + + public abstract Task> ReadAll(MissionDefinitionQueryStringParameters parameters); + + public abstract Task Update(MissionDefinition missionDefinition); + + public abstract Task Delete(string id); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1309:Use ordinal StringComparison", + Justification = "EF Core refrains from translating string comparison overloads to SQL" + )] + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1304:Specify CultureInfo", + Justification = "Entity framework does not support translating culture info to SQL calls" + )] + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1307:Specify CultureInfo", + Justification = "Entity framework does not support translating culture info to SQL calls" + )] + public class MissionDefinitionService : IMissionDefinitionService + { + private readonly FlotillaDbContext _context; + + public MissionDefinitionService(FlotillaDbContext context) + { + _context = context; + } + + public async Task Create(MissionDefinition missionDefinition) + { + await _context.MissionDefinitions.AddAsync(missionDefinition); + await _context.SaveChangesAsync(); + + return missionDefinition; + } + + private IQueryable GetMissionDefinitionsWithSubModels() + { + return _context.MissionDefinitions + .Include(missionDefinition => missionDefinition.Area) + .ThenInclude(area => area.Deck) + .ThenInclude(area => area.Plant) + .ThenInclude(area => area.Installation) + .Include(missionDefinition => missionDefinition.Source) + .Include(missionDefinition => missionDefinition.LastRun); + } + + public async Task ReadById(string id) + { + return await GetMissionDefinitionsWithSubModels().Where(m => m.IsDeprecated == false) + .FirstOrDefaultAsync(missionDefinition => missionDefinition.Id.Equals(id)); + } + + public async Task> ReadAll(MissionDefinitionQueryStringParameters parameters) + { + var query = GetMissionDefinitionsWithSubModels().Where(m => m.IsDeprecated == false); + var filter = ConstructFilter(parameters); + + query = query.Where(filter); + + SearchByName(ref query, parameters.NameSearch); + + SortingService.ApplySort(ref query, parameters.OrderBy); + + return await PagedList.ToPagedListAsync( + query, + parameters.PageNumber, + parameters.PageSize + ); + } + + public async Task Update(MissionDefinition missionDefinition) + { + var entry = _context.Update(missionDefinition); + await _context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task Delete(string id) + { + // We do not delete the source here as more than one mission definition may be using it + var missionDefinition = await ReadById(id); + if (missionDefinition is null) + { + return null; + } + + missionDefinition.IsDeprecated = true; + await _context.SaveChangesAsync(); + + return missionDefinition; + } + + private static void SearchByName(ref IQueryable missionDefinitions, string? name) + { + if (!missionDefinitions.Any() || string.IsNullOrWhiteSpace(name)) + return; + + missionDefinitions = missionDefinitions.Where( + missionDefinition => + missionDefinition.Name != null && missionDefinition.Name.ToLower().Contains(name.Trim().ToLower()) + ); + } + + /// + /// Filters by and + /// + /// Uses LINQ Expression trees (see ) + /// + /// The variable containing the filter params + private static Expression> ConstructFilter( + MissionDefinitionQueryStringParameters parameters + ) + { + Expression> areaFilter = parameters.Area is null + ? missionDefinition => true + : missionDefinition => + missionDefinition.Area.Name.ToLower().Equals(parameters.Area.Trim().ToLower()); + + Expression> installationFilter = parameters.InstallationCode is null + ? missionDefinition => true + : missionDefinition => + missionDefinition.InstallationCode.ToLower().Equals(parameters.InstallationCode.Trim().ToLower()); + + Expression> missionTypeFilter = parameters.SourceType is null + ? missionDefinition => true + : missionDefinition => + missionDefinition.Source.Type.Equals(parameters.SourceType); + + // The parameter of the filter expression + var missionRunExpression = Expression.Parameter(typeof(MissionRun)); + + // Combining the body of the filters to create the combined filter, using invoke to force parameter substitution + Expression body = Expression.AndAlso( + Expression.Invoke(installationFilter, missionRunExpression), + Expression.AndAlso( + Expression.Invoke(areaFilter, missionRunExpression), + Expression.Invoke(missionTypeFilter, missionRunExpression) + ) + ); + + // Constructing the resulting lambda expression by combining parameter and body + return Expression.Lambda>(body, missionRunExpression); + } + } +} diff --git a/backend/api/Services/MissionService.cs b/backend/api/Services/MissionRunService.cs similarity index 53% rename from backend/api/Services/MissionService.cs rename to backend/api/Services/MissionRunService.cs index d656d2284..9a3b269aa 100644 --- a/backend/api/Services/MissionService.cs +++ b/backend/api/Services/MissionRunService.cs @@ -1,7 +1,5 @@ using System.Linq.Dynamic.Core; using System.Linq.Expressions; -using System.Reflection; -using System.Text; using Api.Controllers.Models; using Api.Database.Context; using Api.Database.Models; @@ -12,17 +10,17 @@ namespace Api.Services { - public interface IMissionService + public interface IMissionRunService { - public abstract Task Create(Mission mission); + public abstract Task Create(MissionRun missionRun); - public abstract Task> ReadAll(MissionQueryStringParameters parameters); + public abstract Task> ReadAll(MissionRunQueryStringParameters parameters); - public abstract Task ReadById(string id); + public abstract Task ReadById(string id); - public abstract Task Update(Mission mission); + public abstract Task Update(MissionRun mission); - public abstract Task UpdateMissionStatusByIsarMissionId( + public abstract Task UpdateMissionRunStatusByIsarMissionId( string isarMissionId, MissionStatus missionStatus ); @@ -40,7 +38,7 @@ public abstract Task UpdateStepStatusByIsarStepId( IsarStepStatus stepStatus ); - public abstract Task Delete(string id); + public abstract Task Delete(string id); } [System.Diagnostics.CodeAnalysis.SuppressMessage( @@ -58,41 +56,45 @@ IsarStepStatus stepStatus "CA1307:Specify CultureInfo", Justification = "Entity framework does not support translating culture info to SQL calls" )] - public class MissionService : IMissionService + public class MissionRunService : IMissionRunService { private readonly FlotillaDbContext _context; - private readonly ILogger _logger; + private readonly ILogger _logger; - public MissionService(FlotillaDbContext context, ILogger logger) + public MissionRunService(FlotillaDbContext context, ILogger logger) { _context = context; _logger = logger; } - private IQueryable GetMissionsWithSubModels() + private IQueryable GetMissionRunsWithSubModels() { - return _context.Missions - .Include(mission => mission.Robot) + return _context.MissionRuns + .Include(missionRun => missionRun.Area) + .ThenInclude(area => area.Deck) + .ThenInclude(deck => deck.Plant) + .ThenInclude(plant => plant.Installation) + .Include(missionRun => missionRun.Robot) .ThenInclude(robot => robot.VideoStreams) - .Include(mission => mission.Robot) + .Include(missionRun => missionRun.Robot) .ThenInclude(robot => robot.Model) - .Include(mission => mission.Tasks) + .Include(missionRun => missionRun.Tasks) .ThenInclude(planTask => planTask.Inspections) - .Include(mission => mission.Tasks) + .Include(missionRun => missionRun.Tasks) .ThenInclude(task => task.Inspections); } - public async Task Create(Mission mission) + public async Task Create(MissionRun missionRun) { - await _context.Missions.AddAsync(mission); + await _context.MissionRuns.AddAsync(missionRun); await _context.SaveChangesAsync(); - return mission; + return missionRun; } - public async Task> ReadAll(MissionQueryStringParameters parameters) + public async Task> ReadAll(MissionRunQueryStringParameters parameters) { - var query = GetMissionsWithSubModels(); + var query = GetMissionRunsWithSubModels(); var filter = ConstructFilter(parameters); query = query.Where(filter); @@ -101,61 +103,61 @@ public async Task> ReadAll(MissionQueryStringParameters param SearchByRobotName(ref query, parameters.RobotNameSearch); SearchByTag(ref query, parameters.TagSearch); - ApplySort(ref query, parameters.OrderBy); + SortingService.ApplySort(ref query, parameters.OrderBy); - return await PagedList.ToPagedListAsync( + return await PagedList.ToPagedListAsync( query, parameters.PageNumber, parameters.PageSize ); } - public async Task ReadById(string id) + public async Task ReadById(string id) { - return await GetMissionsWithSubModels() - .FirstOrDefaultAsync(mission => mission.Id.Equals(id)); + return await GetMissionRunsWithSubModels() + .FirstOrDefaultAsync(missionRun => missionRun.Id.Equals(id)); } - public async Task Update(Mission mission) + public async Task Update(MissionRun missionRun) { - var entry = _context.Update(mission); + var entry = _context.Update(missionRun); await _context.SaveChangesAsync(); return entry.Entity; } - public async Task Delete(string id) + public async Task Delete(string id) { - var mission = await GetMissionsWithSubModels() + var missionRun = await GetMissionRunsWithSubModels() .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); - if (mission is null) + if (missionRun is null) { return null; } - _context.Missions.Remove(mission); + _context.MissionRuns.Remove(missionRun); await _context.SaveChangesAsync(); - return mission; + return missionRun; } #region ISAR Specific methods - private async Task ReadByIsarMissionId(string isarMissionId) + private async Task ReadByIsarMissionId(string isarMissionId) { - return await GetMissionsWithSubModels() + return await GetMissionRunsWithSubModels() .FirstOrDefaultAsync( - mission => - mission.IsarMissionId != null && mission.IsarMissionId.Equals(isarMissionId) + missionRun => + missionRun.IsarMissionId != null && missionRun.IsarMissionId.Equals(isarMissionId) ); } - public async Task UpdateMissionStatusByIsarMissionId( + public async Task UpdateMissionRunStatusByIsarMissionId( string isarMissionId, MissionStatus missionStatus ) { - var mission = await ReadByIsarMissionId(isarMissionId); - if (mission is null) + var missionRun = await ReadByIsarMissionId(isarMissionId); + if (missionRun is null) { _logger.LogWarning( "Could not update mission status for ISAR mission with id: {id} as the mission was not found", @@ -164,11 +166,11 @@ MissionStatus missionStatus return null; } - mission.Status = missionStatus; + missionRun.Status = missionStatus; await _context.SaveChangesAsync(); - return mission; + return missionRun; } public async Task UpdateTaskStatusByIsarTaskId( @@ -177,18 +179,18 @@ public async Task UpdateTaskStatusByIsarTaskId( IsarTaskStatus taskStatus ) { - var mission = await ReadByIsarMissionId(isarMissionId); - if (mission is null) + var missionRun = await ReadByIsarMissionId(isarMissionId); + if (missionRun is null) { _logger.LogWarning( - "Could not update task status for ISAR task with id: {id} in mission with id: {missionId} as the mission was not found", + "Could not update task status for ISAR task with id: {id} in mission run with id: {missionId} as the mission was not found", isarTaskId, isarMissionId ); return false; } - var task = mission.GetTaskByIsarId(isarTaskId); + var task = missionRun.GetTaskByIsarId(isarTaskId); if (task is null) { _logger.LogWarning( @@ -199,12 +201,12 @@ IsarTaskStatus taskStatus } task.UpdateStatus(taskStatus); - if (taskStatus == IsarTaskStatus.InProgress && mission.Status != MissionStatus.Ongoing) + if (taskStatus == IsarTaskStatus.InProgress && missionRun.Status != MissionStatus.Ongoing) { // If mission was set to failed and then ISAR recovered connection, we need to reset the coming tasks - mission.Status = MissionStatus.Ongoing; + missionRun.Status = MissionStatus.Ongoing; foreach ( - var taskItem in mission.Tasks.Where( + var taskItem in missionRun.Tasks.Where( taskItem => taskItem.TaskOrder > task.TaskOrder ) ) @@ -229,8 +231,8 @@ public async Task UpdateStepStatusByIsarStepId( IsarStepStatus stepStatus ) { - var mission = await ReadByIsarMissionId(isarMissionId); - if (mission is null) + var missionRun = await ReadByIsarMissionId(isarMissionId); + if (missionRun is null) { _logger.LogWarning( "Could not update step status for ISAR inspection with id: {id} in mission with id: {missionId} as the mission was not found", @@ -240,7 +242,7 @@ IsarStepStatus stepStatus return false; } - var task = mission.GetTaskByIsarId(isarTaskId); + var task = missionRun.GetTaskByIsarId(isarTaskId); if (task is null) { _logger.LogWarning( @@ -270,35 +272,35 @@ IsarStepStatus stepStatus #endregion ISAR Specific methods - private static void SearchByName(ref IQueryable missions, string? name) + private static void SearchByName(ref IQueryable missionRuns, string? name) { - if (!missions.Any() || string.IsNullOrWhiteSpace(name)) + if (!missionRuns.Any() || string.IsNullOrWhiteSpace(name)) return; - missions = missions.Where( - mission => - mission.Name != null && mission.Name.ToLower().Contains(name.Trim().ToLower()) + missionRuns = missionRuns.Where( + missionRun => + missionRun.Name != null && missionRun.Name.ToLower().Contains(name.Trim().ToLower()) ); } - private static void SearchByRobotName(ref IQueryable missions, string? robotName) + private static void SearchByRobotName(ref IQueryable missionRuns, string? robotName) { - if (!missions.Any() || string.IsNullOrWhiteSpace(robotName)) + if (!missionRuns.Any() || string.IsNullOrWhiteSpace(robotName)) return; - missions = missions.Where( - mission => mission.Robot.Name.ToLower().Contains(robotName.Trim().ToLower()) + missionRuns = missionRuns.Where( + missionRun => missionRun.Robot.Name.ToLower().Contains(robotName.Trim().ToLower()) ); } - private static void SearchByTag(ref IQueryable missions, string? tag) + private static void SearchByTag(ref IQueryable missionRuns, string? tag) { - if (!missions.Any() || string.IsNullOrWhiteSpace(tag)) + if (!missionRuns.Any() || string.IsNullOrWhiteSpace(tag)) return; - missions = missions.Where( - mission => - mission.Tasks.Any( + missionRuns = missionRuns.Where( + missionRun => + missionRun.Tasks.Any( task => task.TagId != null && task.TagId.ToLower().Contains(tag.Trim().ToLower()) @@ -307,33 +309,38 @@ private static void SearchByTag(ref IQueryable missions, string? tag) } /// - /// Filters by and + /// Filters by and /// /// Uses LINQ Expression trees (see ) /// /// The variable containing the filter params - private static Expression> ConstructFilter( - MissionQueryStringParameters parameters + private static Expression> ConstructFilter( + MissionRunQueryStringParameters parameters ) { - Expression> assetFilter = parameters.AssetCode is null - ? mission => true - : mission => - mission.AssetCode.ToLower().Equals(parameters.AssetCode.Trim().ToLower()); + Expression> areaFilter = parameters.Area is null + ? missionRun => true + : missionRun => + missionRun.Area.Name.ToLower().Equals(parameters.Area.Trim().ToLower()); + + Expression> installationFilter = parameters.InstallationCode is null + ? missionRun => true + : missionRun => + missionRun.InstallationCode.ToLower().Equals(parameters.InstallationCode.Trim().ToLower()); - Expression> statusFilter = parameters.Statuses is null + Expression> statusFilter = parameters.Statuses is null ? mission => true : mission => parameters.Statuses.Contains(mission.Status); - Expression> robotTypeFilter = parameters.RobotModelType is null - ? mission => true - : mission => mission.Robot.Model.Type.Equals(parameters.RobotModelType); + Expression> robotTypeFilter = parameters.RobotModelType is null + ? missionRun => true + : missionRun => missionRun.Robot.Model.Type.Equals(parameters.RobotModelType); - Expression> robotIdFilter = parameters.RobotId is null - ? mission => true - : mission => mission.Robot.Id.Equals(parameters.RobotId); + Expression> robotIdFilter = parameters.RobotId is null + ? missionRun => true + : missionRun => missionRun.Robot.Id.Equals(parameters.RobotId); - Expression> inspectionTypeFilter = parameters.InspectionTypes is null + Expression> inspectionTypeFilter = parameters.InspectionTypes is null ? mission => true : mission => mission.Tasks.Any( task => @@ -344,20 +351,20 @@ MissionQueryStringParameters parameters var minStartTime = DateTimeOffset.FromUnixTimeSeconds(parameters.MinStartTime); var maxStartTime = DateTimeOffset.FromUnixTimeSeconds(parameters.MaxStartTime); - Expression> startTimeFilter = mission => - mission.StartTime == null + Expression> startTimeFilter = missionRun => + missionRun.StartTime == null || ( - DateTimeOffset.Compare(mission.StartTime.Value, minStartTime) >= 0 - && DateTimeOffset.Compare(mission.StartTime.Value, maxStartTime) <= 0 + DateTimeOffset.Compare(missionRun.StartTime.Value, minStartTime) >= 0 + && DateTimeOffset.Compare(missionRun.StartTime.Value, maxStartTime) <= 0 ); var minEndTime = DateTimeOffset.FromUnixTimeSeconds(parameters.MinEndTime); var maxEndTime = DateTimeOffset.FromUnixTimeSeconds(parameters.MaxEndTime); - Expression> endTimeFilter = mission => - mission.EndTime == null + Expression> endTimeFilter = missionRun => + missionRun.EndTime == null || ( - DateTimeOffset.Compare(mission.EndTime.Value, minEndTime) >= 0 - && DateTimeOffset.Compare(mission.EndTime.Value, maxEndTime) <= 0 + DateTimeOffset.Compare(missionRun.EndTime.Value, minEndTime) >= 0 + && DateTimeOffset.Compare(missionRun.EndTime.Value, maxEndTime) <= 0 ); var minDesiredStartTime = DateTimeOffset.FromUnixTimeSeconds( @@ -366,29 +373,29 @@ MissionQueryStringParameters parameters var maxDesiredStartTime = DateTimeOffset.FromUnixTimeSeconds( parameters.MaxDesiredStartTime ); - Expression> desiredStartTimeFilter = mission => - DateTimeOffset.Compare(mission.DesiredStartTime, minDesiredStartTime) >= 0 - && DateTimeOffset.Compare(mission.DesiredStartTime, maxDesiredStartTime) <= 0; + Expression> desiredStartTimeFilter = missionRun => + DateTimeOffset.Compare(missionRun.DesiredStartTime, minDesiredStartTime) >= 0 + && DateTimeOffset.Compare(missionRun.DesiredStartTime, maxDesiredStartTime) <= 0; // The parameter of the filter expression - var mission = Expression.Parameter(typeof(Mission)); + var missionRun = Expression.Parameter(typeof(MissionRun)); // Combining the body of the filters to create the combined filter, using invoke to force parameter substitution Expression body = Expression.AndAlso( - Expression.Invoke(assetFilter, mission), + Expression.Invoke(installationFilter, missionRun), Expression.AndAlso( - Expression.Invoke(statusFilter, mission), + Expression.Invoke(statusFilter, missionRun), Expression.AndAlso( - Expression.Invoke(robotIdFilter, mission), + Expression.Invoke(robotIdFilter, missionRun), Expression.AndAlso( - Expression.Invoke(inspectionTypeFilter, mission), + Expression.Invoke(inspectionTypeFilter, missionRun), Expression.AndAlso( - Expression.Invoke(desiredStartTimeFilter, mission), + Expression.Invoke(desiredStartTimeFilter, missionRun), Expression.AndAlso( - Expression.Invoke(startTimeFilter, mission), + Expression.Invoke(startTimeFilter, missionRun), Expression.AndAlso( - Expression.Invoke(endTimeFilter, mission), - Expression.Invoke(robotTypeFilter, mission) + Expression.Invoke(endTimeFilter, missionRun), + Expression.Invoke(robotTypeFilter, missionRun) ) ) ) @@ -398,63 +405,7 @@ MissionQueryStringParameters parameters ); // Constructing the resulting lambda expression by combining parameter and body - return Expression.Lambda>(body, mission); - } - - private static void ApplySort(ref IQueryable missions, string orderByQueryString) - { - if (!missions.Any()) - return; - - if (string.IsNullOrWhiteSpace(orderByQueryString)) - { - missions = missions.OrderBy(x => x.Name); - return; - } - - string[] orderParams = orderByQueryString - .Trim() - .Split(',') - .Select(parameterString => parameterString.Trim()) - .ToArray(); - - var propertyInfos = typeof(Mission).GetProperties( - BindingFlags.Public | BindingFlags.Instance - ); - var orderQueryBuilder = new StringBuilder(); - - foreach (string param in orderParams) - { - if (string.IsNullOrWhiteSpace(param)) - continue; - - string propertyFromQueryName = param.Split(" ")[0]; - var objectProperty = propertyInfos.FirstOrDefault( - pi => - pi.Name.Equals( - propertyFromQueryName, - StringComparison.InvariantCultureIgnoreCase - ) - ); - - if (objectProperty == null) - throw new InvalidDataException( - $"Mission has no property '{propertyFromQueryName}' for ordering" - ); - - string sortingOrder = param.EndsWith(" desc", StringComparison.OrdinalIgnoreCase) - ? "descending" - : "ascending"; - - string sortParameter = $"{objectProperty.Name} {sortingOrder}, "; - orderQueryBuilder.Append(sortParameter); - } - - string orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' '); - - missions = string.IsNullOrWhiteSpace(orderQuery) - ? missions.OrderBy(mission => mission.Name) - : missions.OrderBy(orderQuery); + return Expression.Lambda>(body, missionRun); } } } diff --git a/backend/api/Services/Models/IsarMissionDefinition.cs b/backend/api/Services/Models/IsarMissionDefinition.cs index 8144386b7..023ef8dca 100644 --- a/backend/api/Services/Models/IsarMissionDefinition.cs +++ b/backend/api/Services/Models/IsarMissionDefinition.cs @@ -25,11 +25,11 @@ public IsarMissionDefinition(List tasks) Tasks = tasks; } - public IsarMissionDefinition(Mission mission) + public IsarMissionDefinition(MissionRun missionRun) { - Id = mission.IsarMissionId; - Name = mission.Name; - Tasks = mission.Tasks.Select(task => new IsarTaskDefinition(task, mission)).ToList(); + Id = missionRun.IsarMissionId; + Name = missionRun.Name; + Tasks = missionRun.Tasks.Select(task => new IsarTaskDefinition(task, missionRun)).ToList(); } } @@ -47,7 +47,7 @@ public struct IsarTaskDefinition [JsonPropertyName("inspections")] public List Inspections { get; set; } - public IsarTaskDefinition(MissionTask missionTask, Mission mission) + public IsarTaskDefinition(MissionTask missionTask, MissionRun missionRun) { Id = missionTask.IsarTaskId; Pose = new IsarPose(missionTask.RobotPose); @@ -55,7 +55,7 @@ public IsarTaskDefinition(MissionTask missionTask, Mission mission) var isarInspections = new List(); foreach (var inspection in missionTask.Inspections) { - isarInspections.Add(new IsarInspectionDefinition(inspection, missionTask, mission)); + isarInspections.Add(new IsarInspectionDefinition(inspection, missionTask, missionRun)); } Inspections = isarInspections; } @@ -81,7 +81,7 @@ public struct IsarInspectionDefinition [JsonPropertyName("metadata")] public Dictionary? Metadata { get; set; } - public IsarInspectionDefinition(Inspection inspection, MissionTask task, Mission mission) + public IsarInspectionDefinition(Inspection inspection, MissionTask task, MissionRun missionRun) { Id = inspection.IsarStepId; Type = inspection.InspectionType.ToString(); @@ -95,12 +95,12 @@ public IsarInspectionDefinition(Inspection inspection, MissionTask task, Mission Duration = inspection.VideoDuration; var metadata = new Dictionary { - { "map", mission.MapMetadata?.MapName }, - { "description", mission.Description }, - { "estimated_duration", mission.EstimatedDuration.ToString() }, - { "asset_code", mission.AssetCode }, - { "mission_name", mission.Name }, - { "status_reason", mission.StatusReason } + { "map", missionRun.MapMetadata?.MapName }, + { "description", missionRun.Description }, + { "estimated_duration", missionRun.EstimatedDuration.ToString() }, + { "asset_code", missionRun.InstallationCode }, + { "mission_name", missionRun.Name }, + { "status_reason", missionRun.StatusReason } }; Metadata = metadata; } diff --git a/backend/api/Services/PlantService.cs b/backend/api/Services/PlantService.cs new file mode 100644 index 000000000..c9ff13d0a --- /dev/null +++ b/backend/api/Services/PlantService.cs @@ -0,0 +1,137 @@ +using Api.Controllers.Models; +using Api.Database.Context; +using Api.Database.Models; +using Api.Utilities; +using Microsoft.EntityFrameworkCore; + +namespace Api.Services +{ + public interface IPlantService + { + public abstract Task> ReadAll(); + + public abstract Task ReadById(string id); + + public abstract Task> ReadByInstallation(string installationCode); + + public abstract Task ReadByInstallationAndName(Installation installation, string plantCode); + + public abstract Task ReadByInstallationAndName(string installationCode, string plantCode); + + public abstract Task Create(CreatePlantQuery newPlant); + + public abstract Task Update(Plant plant); + + public abstract Task Delete(string id); + + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1309:Use ordinal StringComparison", + Justification = "EF Core refrains from translating string comparison overloads to SQL" + )] + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Globalization", + "CA1304:Specify CultureInfo", + Justification = "Entity framework does not support translating culture info to SQL calls" + )] + public class PlantService : IPlantService + { + private readonly FlotillaDbContext _context; + private readonly IInstallationService _installationService; + + public PlantService(FlotillaDbContext context, IInstallationService installationService) + { + _context = context; + _installationService = installationService; + } + + public async Task> ReadAll() + { + return await GetPlants().ToListAsync(); + } + + private IQueryable GetPlants() + { + return _context.Plants.Include(i => i.Installation); + } + + public async Task ReadById(string id) + { + return await GetPlants() + .FirstOrDefaultAsync(a => a.Id.Equals(id)); + } + + public async Task> ReadByInstallation(string installationCode) + { + var installation = await _installationService.ReadByName(installationCode); + if (installation == null) + return new List(); + return await _context.Plants.Where(a => + a.Installation.Id.Equals(installation.Id)).ToListAsync(); + } + + public async Task ReadByInstallationAndName(Installation installation, string plantCode) + { + return await _context.Plants.Where(a => + a.PlantCode.ToLower().Equals(plantCode.ToLower()) && + a.Installation.Id.Equals(installation.Id)).FirstOrDefaultAsync(); + } + + public async Task ReadByInstallationAndName(string installationCode, string plantCode) + { + var installation = await _installationService.ReadByName(installationCode); + if (installation == null) + return null; + return await _context.Plants.Where(a => + a.Installation.Id.Equals(installation.Id) && + a.PlantCode.ToLower().Equals(plantCode.ToLower()) + ).FirstOrDefaultAsync(); + } + + public async Task Create(CreatePlantQuery newPlantQuery) + { + var installation = await _installationService.ReadByName(newPlantQuery.InstallationCode); + if (installation == null) + { + throw new InstallationNotFoundException($"No installation with name {newPlantQuery.InstallationCode} could be found"); + } + var plant = await ReadByInstallationAndName(installation, newPlantQuery.PlantCode); + if (plant == null) + { + plant = new Plant + { + Name = newPlantQuery.Name, + PlantCode = newPlantQuery.PlantCode, + Installation = installation, + }; + await _context.Plants.AddAsync(plant); + await _context.SaveChangesAsync(); + } + return plant!; + } + + public async Task Update(Plant plant) + { + var entry = _context.Update(plant); + await _context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task Delete(string id) + { + var plant = await GetPlants() + .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); + if (plant is null) + { + return null; + } + + _context.Plants.Remove(plant); + await _context.SaveChangesAsync(); + + return plant; + } + } +} diff --git a/backend/api/Services/RobotService.cs b/backend/api/Services/RobotService.cs index 6721be469..a668c6ed9 100644 --- a/backend/api/Services/RobotService.cs +++ b/backend/api/Services/RobotService.cs @@ -30,7 +30,7 @@ public RobotService(FlotillaDbContext context) private IQueryable GetRobotsWithSubModels() { - return _context.Robots.Include(r => r.VideoStreams).Include(r => r.Model).Include(r => r.CurrentAssetDeck); + return _context.Robots.Include(r => r.VideoStreams).Include(r => r.Model).Include(r => r.CurrentArea); } public async Task Create(Robot newRobot) diff --git a/backend/api/Services/SortingService.cs b/backend/api/Services/SortingService.cs new file mode 100644 index 000000000..f2eb2982d --- /dev/null +++ b/backend/api/Services/SortingService.cs @@ -0,0 +1,66 @@ +using System.Linq.Dynamic.Core; +using System.Reflection; +using System.Text; +using Api.Database.Models; + +namespace Api.Services +{ + public class SortingService + { + public static void ApplySort(ref IQueryable missions, string orderByQueryString) where T : SortableRecord + { + if (!missions.Any()) + return; + + if (string.IsNullOrWhiteSpace(orderByQueryString)) + { + missions = missions.OrderBy(x => x.Name); + return; + } + + string[] orderParams = orderByQueryString + .Trim() + .Split(',') + .Select(parameterString => parameterString.Trim()) + .ToArray(); + + var propertyInfos = typeof(T).GetProperties( + BindingFlags.Public | BindingFlags.Instance + ); + var orderQueryBuilder = new StringBuilder(); + + foreach (string param in orderParams) + { + if (string.IsNullOrWhiteSpace(param)) + continue; + + string propertyFromQueryName = param.Split(" ")[0]; + var objectProperty = propertyInfos.FirstOrDefault( + pi => + pi.Name.Equals( + propertyFromQueryName, + StringComparison.Ordinal + ) + ); + + if (objectProperty == null) + throw new InvalidDataException( + $"Mission has no property '{propertyFromQueryName}' for ordering" + ); + + string sortingOrder = param.EndsWith(" desc", StringComparison.OrdinalIgnoreCase) + ? "descending" + : "ascending"; + + string sortParameter = $"{objectProperty.Name} {sortingOrder}, "; + orderQueryBuilder.Append(sortParameter); + } + + string orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' '); + + missions = string.IsNullOrWhiteSpace(orderQuery) + ? missions.OrderBy(mission => mission.Name) + : missions.OrderBy(orderQuery); + } + } +} diff --git a/backend/api/Utilities/Exceptions.cs b/backend/api/Utilities/Exceptions.cs index 09a053d17..2a903f45a 100644 --- a/backend/api/Utilities/Exceptions.cs +++ b/backend/api/Utilities/Exceptions.cs @@ -11,6 +11,31 @@ public MissionException(string message, int isarStatusCode) : base(message) } } + public class MissionSourceTypeException : Exception + { + public MissionSourceTypeException(string message) : base(message) { } + } + + public class InstallationNotFoundException : Exception + { + public InstallationNotFoundException(string message) : base(message) { } + } + + public class PlantNotFoundException : Exception + { + public PlantNotFoundException(string message) : base(message) { } + } + + public class DeckNotFoundException : Exception + { + public DeckNotFoundException(string message) : base(message) { } + } + + public class AreaNotFoundException : Exception + { + public AreaNotFoundException(string message) : base(message) { } + } + public class MissionNotFoundException : Exception { public MissionNotFoundException(string message) : base(message) { } diff --git a/backend/api/api.csproj b/backend/api/api.csproj index 334af6d55..7df3efc30 100644 --- a/backend/api/api.csproj +++ b/backend/api/api.csproj @@ -16,12 +16,12 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + diff --git a/backend/api/appsettings.Development.json b/backend/api/appsettings.Development.json index 7a5c89e15..49fddfd73 100644 --- a/backend/api/appsettings.Development.json +++ b/backend/api/appsettings.Development.json @@ -35,6 +35,10 @@ "MaxRetryAttempts": 5, "ShouldFailOnMaxRetries": false }, + "Blob": { + "CustomMissionContainerName": "custommission", + "AccountName": "flotilladevsa" + }, "Database": { "UseInMemoryDatabase": true } diff --git a/backend/api/appsettings.Production.json b/backend/api/appsettings.Production.json index eca23c9a6..eab04f6dc 100644 --- a/backend/api/appsettings.Production.json +++ b/backend/api/appsettings.Production.json @@ -16,6 +16,10 @@ "AllowedOrigins": [ "https://*.equinor.com/" ], + "Blob": { + "CustomMissionContainerName": "custommission", + "AccountName": "flotillaprodsa" + }, "Mqtt": { "Host": "localhost", "Port": 1883, diff --git a/backend/api/appsettings.Staging.json b/backend/api/appsettings.Staging.json index 65c24f9e6..1bcc21a1c 100644 --- a/backend/api/appsettings.Staging.json +++ b/backend/api/appsettings.Staging.json @@ -18,6 +18,10 @@ "http://localhost:3001", "https://localhost:3001" ], + "Blob": { + "CustomMissionContainerName": "custommission", + "AccountName": "flotillastagingsa" + }, "Mqtt": { "Host": "localhost", "Port": 1883, diff --git a/backend/api/appsettings.Test.json b/backend/api/appsettings.Test.json index 16ca42855..ada9b8f99 100644 --- a/backend/api/appsettings.Test.json +++ b/backend/api/appsettings.Test.json @@ -11,6 +11,10 @@ "http://localhost:3001", "https://localhost:3001" ], + "Blob": { + "CustomMissionContainerName": "custommission", + "AccountName": "flotillatestsa" + }, "Mqtt": { "Host": "localhost", "Port": 1883, diff --git a/flotilla.code-workspace b/flotilla.code-workspace new file mode 100644 index 000000000..0097397f4 --- /dev/null +++ b/flotilla.code-workspace @@ -0,0 +1,28 @@ +{ + "folders": [ + { + "name": "Frontend", + "path": "frontend/" + }, + { + "name": "Backend", + "path": "backend" + }, + { + "name": "Root", + "path": "." + } + ], + "launch": { + "version": "0.2.0", + "compounds": [ + { + "name": "Launch flotilla", + "configurations": [ + "Launch backend", + "Launch frontend" + ] + } + ] + } +} \ No newline at end of file diff --git a/frontend/src/api/ApiCaller.tsx b/frontend/src/api/ApiCaller.tsx index e9921c81d..d37889d61 100644 --- a/frontend/src/api/ApiCaller.tsx +++ b/frontend/src/api/ApiCaller.tsx @@ -4,21 +4,23 @@ import { Mission } from 'models/Mission' import { Robot } from 'models/Robot' import { VideoStream } from 'models/VideoStream' import { filterRobots } from 'utils/filtersAndSorts' -import { MissionQueryParameters } from 'models/MissionQueryParameters' +import { MissionRunQueryParameters } from 'models/MissionRunQueryParameters' +import { MissionDefinitionQueryParameters, SourceType } from 'models/MissionDefinitionQueryParameters' import { PaginatedResponse, PaginationHeader, PaginationHeaderName } from 'models/PaginatedResponse' import { Pose } from 'models/Pose' -import { AssetDeck } from 'models/AssetDeck' +import { Area } from 'models/Area' import { timeout } from 'utils/timeout' import { tokenReverificationInterval } from 'components/Contexts/AuthProvider' import { TaskStatus } from 'models/Task' import { CreateCustomMission, CustomMissionQuery } from 'models/CustomMission' import { MapMetadata } from 'models/MapMetadata' import { MissionDefinition } from 'models/MissionDefinition' +import { EchoMission } from 'models/EchoMission' /** Implements the request sent to the backend api. */ export class BackendAPICaller { static accessToken: string - static assetCode: string + static installationCode: string /** API is not ready until access token has been set for the first time */ private static async ApiReady() { @@ -125,12 +127,21 @@ export class BackendAPICaller { return result.content } - static async getMissions(parameters: MissionQueryParameters): Promise> { - let path: string = 'missions?' + static async getAllEchoMissions(): Promise { + const path: string = 'echo/missions' + const result = await BackendAPICaller.GET(path).catch((e) => { + console.error(`Failed to GET /${path}: ` + e) + throw e + }) + return result.content + } + + static async getMissionRuns(parameters: MissionRunQueryParameters): Promise> { + let path: string = 'missions/runs?' - // Always filter by currently selected asset - const assetCode: string | null = BackendAPICaller.assetCode - if (assetCode) path = path + 'AssetCode=' + assetCode + '&' + // Always filter by currently selected installation + const installationCode: string | null = BackendAPICaller.installationCode + if (installationCode) path = path + 'InstallationCode=' + installationCode + '&' if (parameters.statuses) { parameters.statuses.forEach((status) => { @@ -143,6 +154,7 @@ export class BackendAPICaller { }) } + if (parameters.area) path = path + 'Area=' + parameters.area + '&' if (parameters.pageNumber) path = path + 'PageNumber=' + parameters.pageNumber + '&' if (parameters.pageSize) path = path + 'PageSize=' + parameters.pageSize + '&' if (parameters.orderBy) path = path + 'OrderBy=' + parameters.orderBy + '&' @@ -177,8 +189,45 @@ export class BackendAPICaller { return result.content } - static async getMissionById(missionId: string): Promise { - const path: string = 'missions/' + missionId + static async getMissionDefinitions( + parameters: MissionDefinitionQueryParameters + ): Promise> { + let path: string = 'missions/definitions?' + + // Always filter by currently selected installation + const installationCode: string | null = BackendAPICaller.installationCode + if (installationCode) path = path + 'InstallationCode=' + installationCode + '&' + + if (parameters.area) path = path + 'Area=' + parameters.area + '&' + if (parameters.sourceType) path = path + 'SourceType=' + parameters.sourceType + '&' + if (parameters.pageNumber) path = path + 'PageNumber=' + parameters.pageNumber + '&' + if (parameters.pageSize) path = path + 'PageSize=' + parameters.pageSize + '&' + if (parameters.orderBy) path = path + 'OrderBy=' + parameters.orderBy + '&' + if (parameters.nameSearch) path = path + 'NameSearch=' + parameters.nameSearch + '&' + if (parameters.sourceType) path = path + 'SourceType=' + parameters.sourceType + '&' + + const result = await BackendAPICaller.GET(path).catch((e) => { + console.error(`Failed to GET /${path}: ` + e) + throw e + }) + if (!result.headers.has(PaginationHeaderName)) { + console.error('No Pagination header received ("' + PaginationHeaderName + '")') + } + const pagination: PaginationHeader = JSON.parse(result.headers.get(PaginationHeaderName)!) + return { pagination: pagination, content: result.content } + } + + static async getEchoMissions(installationCode: string = ''): Promise { + const path: string = 'echo/missions?installationCode=' + installationCode + const result = await BackendAPICaller.GET(path).catch((e) => { + console.error(`Failed to GET /${path}: ` + e) + throw e + }) + return result.content + } + + static async getMissionRunById(missionId: string): Promise { + const path: string = 'missions/runs/' + missionId const result = await BackendAPICaller.GET(path).catch((e) => { console.error(`Failed to GET /${path}: ` + e) throw e @@ -203,7 +252,7 @@ export class BackendAPICaller { }) return result.content } - static async postMission(echoMissionId: number, robotId: string, assetCode: string | null) { + static async postMission(echoMissionId: number, robotId: string, installationCode: string | null) { const path: string = 'missions' const robots: Robot[] = await BackendAPICaller.getEnabledRobots() const desiredRobot = filterRobots(robots, robotId) @@ -211,7 +260,8 @@ export class BackendAPICaller { robotId: desiredRobot[0].id, echoMissionId: echoMissionId, desiredStartTime: new Date(), - assetCode: assetCode, + installationCode: installationCode, + areaName: '', // TODO: we need a way of populating the area database, then including area in MissionDefinition } const result = await BackendAPICaller.POST(path, body).catch((e) => { console.error(`Failed to POST /${path}: ` + e) @@ -265,8 +315,8 @@ export class BackendAPICaller { return BackendAPICaller.postControlMissionRequest(path, robotId) } - static async getMap(assetCode: string, mapName: string): Promise { - const path: string = 'missions/' + assetCode + '/' + mapName + '/map' + static async getMap(installationCode: string, mapName: string): Promise { + const path: string = 'missions/' + installationCode + '/' + mapName + '/map' const url = `${config.BACKEND_URL}/${path}` const headers = { @@ -291,17 +341,17 @@ export class BackendAPICaller { } } - static async getAssetDecks(): Promise { - const path: string = 'asset-decks' - const result = await this.GET(path).catch((e) => { + static async getAreas(): Promise { + const path: string = 'areas' + const result = await this.GET(path).catch((e) => { console.error(`Failed to GET /${path}: ` + e) throw e }) return result.content } - static async getAssetDeckMapMetadata(id: string): Promise { - const path: string = 'asset-decks/' + id + '/map-metadata' + static async getAreasMapMetadata(id: string): Promise { + const path: string = 'areas/' + id + '/map-metadata' const result = await this.GET(path).catch((e) => { console.error(`Failed to GET /${path}: ` + e) throw e @@ -310,7 +360,9 @@ export class BackendAPICaller { } static async reRunMission(missionId: string, failedTasksOnly: boolean = false): Promise { - let mission = await this.getMissionById(missionId) + let mission = await this.getMissionRunById(missionId) + + // TODO: utilise reschedule endpoint instead of copying if (failedTasksOnly) { mission.tasks = mission.tasks.filter( diff --git a/frontend/src/components/Contexts/APIUpdater.tsx b/frontend/src/components/Contexts/APIUpdater.tsx index 0cec0c3aa..fb240a7e6 100644 --- a/frontend/src/components/Contexts/APIUpdater.tsx +++ b/frontend/src/components/Contexts/APIUpdater.tsx @@ -1,6 +1,6 @@ import { BackendAPICaller } from 'api/ApiCaller' import { useContext } from 'react' -import { AssetContext } from './AssetContext' +import { InstallationContext } from './InstallationContext' import { AuthContext } from './AuthProvider' type Props = { @@ -10,8 +10,8 @@ type Props = { // Can't use contexts inside the static class so we need a component to update it export const APIUpdater = (props: Props) => { const accessToken = useContext(AuthContext) - const assetCode = useContext(AssetContext).assetCode + const installationCode = useContext(InstallationContext).installationCode BackendAPICaller.accessToken = accessToken - BackendAPICaller.assetCode = assetCode + BackendAPICaller.installationCode = installationCode return <>{props.children} } diff --git a/frontend/src/components/Contexts/AssetContext.tsx b/frontend/src/components/Contexts/AssetContext.tsx deleted file mode 100644 index 16971abb5..000000000 --- a/frontend/src/components/Contexts/AssetContext.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { createContext, FC, useContext, useState } from 'react' - -interface IAssetContext { - assetCode: string - switchAsset: (selectedAsset: string) => void -} - -interface Props { - children: React.ReactNode -} - -const defaultAsset = { - assetCode: '', - switchAsset: (selectedAsset: string) => {}, -} - -export const AssetContext = createContext(defaultAsset) - -export const AssetProvider: FC = ({ children }) => { - const previousAsset = window.localStorage.getItem('assetString') - const [assetCode, setAsset] = useState(previousAsset || defaultAsset.assetCode) - - const switchAsset = (selectedAsset: string) => { - setAsset(selectedAsset.toLowerCase()) - window.localStorage.setItem('assetString', selectedAsset.toLowerCase()) - } - - return ( - - {children} - - ) -} - -export const useAssetContext = () => useContext(AssetContext) diff --git a/frontend/src/components/Contexts/InstallationContext.tsx b/frontend/src/components/Contexts/InstallationContext.tsx new file mode 100644 index 000000000..bec17c544 --- /dev/null +++ b/frontend/src/components/Contexts/InstallationContext.tsx @@ -0,0 +1,40 @@ +import { createContext, FC, useContext, useState } from 'react' + +interface IInstallationContext { + installationCode: string + switchInstallation: (selectedInstallation: string) => void +} + +interface Props { + children: React.ReactNode +} + +const defaultInstallation = { + installationCode: '', + switchInstallation: (selectedInstallation: string) => {}, +} + +export const InstallationContext = createContext(defaultInstallation) + +export const InstallationProvider: FC = ({ children }) => { + const previousInstallation = window.localStorage.getItem('installationString') + const [installationCode, setInstallation] = useState(previousInstallation || defaultInstallation.installationCode) + + const switchInstallation = (selectedInstallation: string) => { + setInstallation(selectedInstallation.toLowerCase()) + window.localStorage.setItem('installationString', selectedInstallation.toLowerCase()) + } + + return ( + + {children} + + ) +} + +export const useInstallationContext = () => useContext(InstallationContext) diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx index a2325f53b..dc1e6f4f4 100644 --- a/frontend/src/components/Header/Header.tsx +++ b/frontend/src/components/Header/Header.tsx @@ -1,7 +1,7 @@ import { config } from 'config' import { Button, Icon, TopBar, Autocomplete, Typography } from '@equinor/eds-core-react' import { BackendAPICaller } from 'api/ApiCaller' -import { useAssetContext } from 'components/Contexts/AssetContext' +import { useInstallationContext } from 'components/Contexts/InstallationContext' import { EchoPlantInfo } from 'models/EchoMission' import { useEffect, useState } from 'react' import styled from 'styled-components' @@ -51,7 +51,7 @@ export function Header({ page }: { page: string }) { - {AssetPicker(page)} + {InstallationPicker(page)} - diff --git a/frontend/src/language/en.json b/frontend/src/language/en.json index f6d4d63f5..fdc70ddcc 100644 --- a/frontend/src/language/en.json +++ b/frontend/src/language/en.json @@ -10,8 +10,8 @@ "Status": "Status", "Name": "Name", "Completion Time": "Completion Time", - "Please select asset": "Please select asset", - "This asset has no missions - Please create mission": "This asset has no missions - Please create mission", + "Please select installation": "Please select installation", + "This installation has no missions - Please create mission": "This installation has no missions - Please create mission", "Cancel": "Cancel", "Add mission": "Add mission", "Select missions": "Select missions", @@ -64,7 +64,7 @@ "ThermalVideo": "Thermal video", "Audio": "Audio", "Camera": "Camera", - "Select asset": "Select asset", + "Select installation": "Select installation", "Save": "Save", "Select language": "Select language", "History": "History", diff --git a/frontend/src/language/no.json b/frontend/src/language/no.json index aa700676f..f55af7ad9 100644 --- a/frontend/src/language/no.json +++ b/frontend/src/language/no.json @@ -10,8 +10,8 @@ "Status": "Status", "Name": "Navn", "Completion Time": "Fullført tid", - "Please select asset": "Vennligst velg anlegg", - "This asset has no missions - Please create mission": "Dette anlegget har ingen oppdrag - Vennligst lag oppdrag", + "Please select installation": "Vennligst velg anlegg", + "This installation has no missions - Please create mission": "Dette anlegget har ingen oppdrag - Vennligst lag oppdrag", "Cancel": "Avbryt", "Add mission": "Legg til oppdrag", "Select missions": "Velg oppdrag", @@ -64,7 +64,7 @@ "ThermalVideo": "Termisk video", "Audio": "Lyd", "Camera": "Kamera", - "Select asset": "Velg anlegg", + "Select installation": "Velg anlegg", "Save": "Lagre", "Select language": "Velg språk", "History": "Historikk", diff --git a/frontend/src/models/AssetDeck.ts b/frontend/src/models/Area.ts similarity index 51% rename from frontend/src/models/AssetDeck.ts rename to frontend/src/models/Area.ts index 1758186ae..7899da138 100644 --- a/frontend/src/models/AssetDeck.ts +++ b/frontend/src/models/Area.ts @@ -1,8 +1,10 @@ import { Pose } from './Pose' -export interface AssetDeck { +export interface Area { id: string - assetCode: string + areaName: string + plantCode: string + installationCode: string deckName: string defaultLocalizationPose: Pose } diff --git a/frontend/src/models/CustomMission.ts b/frontend/src/models/CustomMission.ts index 46b88cd26..41cf80c1a 100644 --- a/frontend/src/models/CustomMission.ts +++ b/frontend/src/models/CustomMission.ts @@ -7,7 +7,7 @@ export interface CustomMissionQuery { name: string description?: string comment?: string - assetCode?: string + installationCode?: string robotId: string desiredStartTime: Date tasks: CustomTaskQuery[] @@ -33,7 +33,7 @@ export function CreateCustomMission(mission: Mission): CustomMissionQuery { name: mission.name, description: mission.description, comment: mission.comment, - assetCode: mission.assetCode, + installationCode: mission.installationCode, robotId: mission.robot.id, desiredStartTime: new Date(), tasks: mission.tasks.map((task) => { diff --git a/frontend/src/models/EchoMission.ts b/frontend/src/models/EchoMission.ts index 64710a304..870696dbf 100644 --- a/frontend/src/models/EchoMission.ts +++ b/frontend/src/models/EchoMission.ts @@ -18,6 +18,6 @@ export interface EchoInspection { } export interface EchoPlantInfo { - installationCode: string + plantCode: string projectDescription: string } diff --git a/frontend/src/models/Mission.ts b/frontend/src/models/Mission.ts index b07e15d79..83e10fd3c 100644 --- a/frontend/src/models/Mission.ts +++ b/frontend/src/models/Mission.ts @@ -21,7 +21,7 @@ export interface Mission { description?: string statusReason?: string comment?: string - assetCode?: string + installationCode?: string robot: Robot status: MissionStatus isCompleted: boolean diff --git a/frontend/src/models/MissionDefinitionQueryParameters.ts b/frontend/src/models/MissionDefinitionQueryParameters.ts new file mode 100644 index 000000000..8008442a3 --- /dev/null +++ b/frontend/src/models/MissionDefinitionQueryParameters.ts @@ -0,0 +1,14 @@ +export enum SourceType { + Echo, + Custom, +} + +export interface MissionDefinitionQueryParameters { + nameSearch?: string + robotNameSearch?: string + area?: string + sourceType?: SourceType + pageNumber?: number + pageSize?: number + orderBy?: string +} diff --git a/frontend/src/models/MissionQueryParameters.ts b/frontend/src/models/MissionRunQueryParameters.ts similarity index 88% rename from frontend/src/models/MissionQueryParameters.ts rename to frontend/src/models/MissionRunQueryParameters.ts index e6325e473..c65bc304b 100644 --- a/frontend/src/models/MissionQueryParameters.ts +++ b/frontend/src/models/MissionRunQueryParameters.ts @@ -1,13 +1,14 @@ import { InspectionType } from './Inspection' import { MissionStatus } from './Mission' -export interface MissionQueryParameters { +export interface MissionRunQueryParameters { statuses?: MissionStatus[] robotId?: string nameSearch?: string robotNameSearch?: string tagSearch?: string inspectionTypes?: InspectionType[] + area?: string minStartTime?: number maxStartTime?: number minEndTime?: number diff --git a/frontend/src/models/Robot.ts b/frontend/src/models/Robot.ts index f76674974..6d680a467 100644 --- a/frontend/src/models/Robot.ts +++ b/frontend/src/models/Robot.ts @@ -1,4 +1,4 @@ -import { AssetDeck } from './AssetDeck' +import { Area } from './Area' import { BatteryStatus } from './Battery' import { Pose } from './Pose' import { RobotModel } from './RobotModel' @@ -15,7 +15,7 @@ export interface Robot { name?: string model: RobotModel serialNumber?: string - currentAsset: string + currentInstallation: string batteryLevel?: number batteryStatus?: BatteryStatus pressureLevel?: number @@ -27,5 +27,5 @@ export interface Robot { port?: number videoStreams?: VideoStream[] isarUri?: string - currentAssetDeck?: AssetDeck + currentArea?: Area }