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..1543a36c4 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 | +| Installation | Read | Read | CRUD | +| Asset | 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 253b92d6f..19ecaf521 100644 --- a/backend/api.test/EndpointTest.cs +++ b/backend/api.test/EndpointTest.cs @@ -102,17 +102,104 @@ public void Dispose() GC.SuppressFinalize(this); } + private async Task PopulateAreaDb(string assetName, string installationName, string deckName, string areaName) + { + string assetUrl = $"/assets"; + string installationUrl = $"/installations"; + 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 assetQuery = new CreateAssetQuery + { + AssetCode = assetName, + Name = assetName + }; + + var installationQuery = new CreateInstallationQuery + { + AssetCode = assetName, + InstallationCode = installationName, + Name = installationName + }; + + var deckQuery = new CreateDeckQuery + { + AssetCode = assetName, + InstallationCode = installationName, + Name = deckName + }; + + var areaQuery = new CreateAreaQuery + { + AssetCode = assetName, + InstallationCode = installationName, + DeckName = deckName, + AreaName = areaName, + DefaultLocalizationPose = testPose + }; + + var assetContent = new StringContent( + JsonSerializer.Serialize(assetQuery), + null, + "application/json" + ); + + var installationContent = new StringContent( + JsonSerializer.Serialize(installationQuery), + 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 assetResponse = await _client.PostAsync(assetUrl, assetContent); + Assert.NotNull(assetResponse); + var installationResponse = await _client.PostAsync(installationUrl, installationContent); + Assert.NotNull(installationResponse); + 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/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] @@ -177,22 +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 testAsset = "TestAsset"; + string testArea = "testArea"; + int echoMissionId = 95; // Act - url = "/missions"; var query = new ScheduledMissionQuery { RobotId = robotId, - AssetCode = "JSV", - AreaName = "testArea", - EchoMissionId = 95, + AssetCode = testAsset, + AreaName = testArea, + EchoMissionId = echoMissionId, DesiredStartTime = DateTimeOffset.UtcNow }; var content = new StringContent( @@ -200,14 +290,14 @@ 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] @@ -218,6 +308,9 @@ public async Task AreaTest() string testInstallation = "TestInstallation"; string testDeck = "testDeck2"; string testArea = "testArea"; + string assetUrl = $"/assets"; + string installationUrl = $"/installations"; + string deckUrl = $"/decks"; string areaUrl = $"/areas"; var testPose = new Pose { @@ -236,25 +329,69 @@ public async Task AreaTest() } }; - var query = new CreateAreaQuery + var assetQuery = new CreateAssetQuery + { + AssetCode = testAsset, + Name = testAsset + }; + + var installationQuery = new CreateInstallationQuery + { + AssetCode = testAsset, + InstallationCode = testInstallation, + Name = testInstallation + }; + + var deckQuery = new CreateDeckQuery + { + AssetCode = testAsset, + InstallationCode = testInstallation, + Name = testDeck + }; + + var areaQuery = new CreateAreaQuery { AssetCode = testAsset, - InstallationName = testInstallation, + InstallationCode = testInstallation, DeckName = testDeck, AreaName = testArea, DefaultLocalizationPose = testPose }; - var content = new StringContent( - JsonSerializer.Serialize(query), + var assetContent = new StringContent( + JsonSerializer.Serialize(assetQuery), + null, + "application/json" + ); + + var installationContent = new StringContent( + JsonSerializer.Serialize(installationQuery), + 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 areaResponse = await _client.PostAsync(areaUrl, content); + var assetResponse = await _client.PostAsync(assetUrl, assetContent); + var installationResponse = await _client.PostAsync(installationUrl, installationContent); + var deckResponse = await _client.PostAsync(deckUrl, deckContent); + var areaResponse = await _client.PostAsync(areaUrl, areaContent); // Assert + Assert.True(assetResponse.IsSuccessStatusCode); + Assert.True(installationResponse.IsSuccessStatusCode); + Assert.True(deckResponse.IsSuccessStatusCode); Assert.True(areaResponse.IsSuccessStatusCode); var area = await areaResponse.Content.ReadFromJsonAsync(_serializerOptions); Assert.True(area != null); @@ -291,6 +428,9 @@ public async Task SafePositionTest() null, "application/json" ); + + await PopulateAreaDb("testAsset", "testInstallation", "testDeck", "testArea"); + var areaResponse = await _client.PostAsync(addSafePositionUrl, content); Assert.True(areaResponse.IsSuccessStatusCode); var area = await areaResponse.Content.ReadFromJsonAsync(_serializerOptions); @@ -311,10 +451,10 @@ public async Task SafePositionTest() // 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) ); } diff --git a/backend/api.test/EventHandlers/TestMissionScheduler.cs b/backend/api.test/EventHandlers/TestMissionScheduler.cs index 2f5b95812..c0290b132 100644 --- a/backend/api.test/EventHandlers/TestMissionScheduler.cs +++ b/backend/api.test/EventHandlers/TestMissionScheduler.cs @@ -24,12 +24,12 @@ public class TestMissionScheduler : IDisposable { private static readonly Asset testAsset = new() { - ShortName = "test", + AssetCode = "test", Name = "test test" }; private static readonly Installation testInstallation = new() { - ShortName = "test", + InstallationCode = "test", Name = "test test", Asset = testAsset }; @@ -71,7 +71,7 @@ public class TestMissionScheduler : IDisposable }; private readonly MissionScheduler _scheduledMissionEventHandler; - private readonly IMissionRunService _missionService; + private readonly IMissionRunService _missionRunService; private readonly IRobotService _robotService; private readonly RobotControllerMock _robotControllerMock; private readonly FlotillaDbContext _context; @@ -85,7 +85,7 @@ public TestMissionScheduler(DatabaseFixture fixture) // Mock ScheduledMissionService: _context = fixture.NewContext; - _missionService = new MissionRunService(_context, missionLogger); + _missionRunService = new MissionRunService(_context, missionLogger); _robotService = new RobotService(_context); _robotControllerMock = new RobotControllerMock(); @@ -94,7 +94,7 @@ public TestMissionScheduler(DatabaseFixture fixture) // Mock injection of MissionService: mockServiceProvider .Setup(p => p.GetService(typeof(IMissionRunService))) - .Returns(_missionService); + .Returns(_missionRunService); // Mock injection of RobotService: mockServiceProvider .Setup(p => p.GetService(typeof(IRobotService))) @@ -137,7 +137,7 @@ public void Dispose() private async void AssertExpectedStatusChange( MissionStatus preStatus, MissionStatus postStatus, - MissionRun mission + MissionRun missionRun ) { // ARRANGE @@ -145,18 +145,18 @@ MissionRun 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); @@ -170,7 +170,7 @@ MissionRun 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); } @@ -179,33 +179,33 @@ MissionRun 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( @@ -218,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/Services/MissionService.cs b/backend/api.test/Services/MissionService.cs index 78016ddf8..010887420 100644 --- a/backend/api.test/Services/MissionService.cs +++ b/backend/api.test/Services/MissionService.cs @@ -17,13 +17,13 @@ public class MissionServiceTest : IDisposable { private readonly FlotillaDbContext _context; private readonly ILogger _logger; - private readonly MissionRunService _missionService; + private readonly MissionRunService _missionRunService; public MissionServiceTest(DatabaseFixture fixture) { _context = fixture.NewContext; _logger = new Mock>().Object; - _missionService = new MissionRunService(_context, _logger); + _missionRunService = new MissionRunService(_context, _logger); } public void Dispose() @@ -35,35 +35,36 @@ 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 + int nReportsBefore = _missionRunService .ReadAll(new MissionRunQueryStringParameters()) .Result.Count; var testAsset = new Asset { - ShortName = "test", + AssetCode = "test", Name = "test test" }; var testInstallation = new Installation { - ShortName = "test", + InstallationCode = "test", Name = "test test", Asset = testAsset }; - MissionRun mission = + MissionRun missionRun = new() { Name = "testMission", Robot = robot, + MissionId = Guid.NewGuid().ToString(), MapMetadata = new MapMetadata() { MapName = "testMap" }, Area = new Area { @@ -84,8 +85,8 @@ public async Task Create() DesiredStartTime = DateTime.Now }; - await _missionService.Create(mission); - int nReportsAfter = _missionService + await _missionRunService.Create(missionRun); + int nReportsAfter = _missionRunService .ReadAll(new MissionRunQueryStringParameters()) .Result.Count; diff --git a/backend/api/Controllers/AreaController.cs b/backend/api/Controllers/AreaController.cs index e0bd63951..42f4e579d 100644 --- a/backend/api/Controllers/AreaController.cs +++ b/backend/api/Controllers/AreaController.cs @@ -1,7 +1,7 @@ using Api.Controllers.Models; using Api.Database.Models; using Api.Services; -using Api.Utilities; +using Azure; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -28,61 +28,6 @@ IAreaService areaService _areaService = areaService; } - /// - /// List all asset areas in the Flotilla database - /// - /// - /// This query gets all asset 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(); - return Ok(areas); - } - 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(Area), 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}"); - return Ok(area); - } - catch (Exception e) - { - _logger.LogError(e, "Error during GET of areas from database"); - throw; - } - - } - /// /// Add a new area /// @@ -91,12 +36,13 @@ public async Task> GetAreaById([FromRoute] string id) /// [HttpPost] [Authorize(Roles = Role.Admin)] - [ProducesResponseType(typeof(Area), StatusCodes.Status201Created)] + [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) + public async Task> Create([FromBody] CreateAreaQuery area) { _logger.LogInformation("Creating new area"); try @@ -104,8 +50,8 @@ public async Task> Create([FromBody] CreateAreaQuery area) var existingArea = await _areaService.ReadByAssetAndName(area.AssetCode, area.AreaName); if (existingArea != null) { - _logger.LogInformation("An area for given name and asset already exists"); - return BadRequest($"Area already exists"); + _logger.LogWarning("An area for given name and asset already exists"); + return Conflict($"Area already exists"); } var newArea = await _areaService.Create(area); @@ -113,10 +59,21 @@ public async Task> Create([FromBody] CreateAreaQuery area) "Succesfully created new area with id '{areaId}'", newArea.Id ); + var response = new AreaResponse + { + Id = newArea.Id, + DeckName = newArea.Deck.Name, + InstallationCode = newArea.Installation.InstallationCode, + AssetCode = newArea.Asset.AssetCode, + AreaName = newArea.Name, + MapMetadata = newArea.MapMetadata, + DefaultLocalizationPose = newArea.DefaultLocalizationPose, + SafePositions = newArea.SafePositions + }; return CreatedAtAction( nameof(GetAreaById), new { id = newArea.Id }, - newArea + response ); } catch (Exception e) @@ -126,55 +83,56 @@ public async Task> Create([FromBody] CreateAreaQuery area) } } - /// - /// Add a safe position to a area + /// Add safe position to an area /// /// /// This query adds a new safe position to the database /// [HttpPost] [Authorize(Roles = Role.Admin)] - [Route("{asset}/{installationName}/{deckName}/{areaName}/safe-position")] - [ProducesResponseType(typeof(Area), StatusCodes.Status201Created)] + [Route("{assetCode}/{installationCode}/{deckName}/{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 asset, - [FromRoute] string installationName, + public async Task> AddSafePosition( + [FromRoute] string assetCode, + [FromRoute] string installationCode, [FromRoute] string deckName, [FromRoute] string areaName, [FromBody] Pose safePosition ) { - _logger.LogInformation("Adding new safe position"); + _logger.LogInformation(@"Adding new safe position to {Asset}, {Installation}, + {Deck}, {Area}", assetCode, installationCode, deckName, areaName); try { - var area = await _areaService.AddSafePosition(asset, areaName, new SafePosition(safePosition)); + var area = await _areaService.AddSafePosition(assetCode, areaName, new SafePosition(safePosition)); if (area != null) { - _logger.LogInformation("Succesfully added new safe position for asset '{assetId}' and name '{name}'", asset, areaName); - return CreatedAtAction(nameof(GetAreaById), new { id = area.Id }, area); ; + _logger.LogInformation(@"Successfully added new safe position for asset '{assetId}' + and name '{name}'", assetCode, areaName); + var response = new AreaResponse + { + Id = area.Id, + DeckName = area.Deck.Name, + InstallationCode = area.Installation.InstallationCode, + AssetCode = area.Asset.AssetCode, + AreaName = area.Name, + MapMetadata = area.MapMetadata, + DefaultLocalizationPose = area.DefaultLocalizationPose, + SafePositions = area.SafePositions + }; + return CreatedAtAction(nameof(GetAreaById), new { id = area.Id }, response); ; } else { - _logger.LogInformation("Creating Area for asset '{assetId}' and name '{name}'", asset, areaName); - // Cloning to avoid tracking same object - var tempPose = ObjectCopier.Clone(safePosition); - area = await _areaService.Create( - new CreateAreaQuery - { - AssetCode = asset, - AreaName = areaName, - InstallationName = installationName, - DeckName = deckName, - DefaultLocalizationPose = new Pose() - }, - new List { tempPose } - ); - return CreatedAtAction(nameof(GetAreaById), new { id = area.Id }, area); + _logger.LogInformation(@"No area with asset {assetCode}, installation {installationCode}, + deck {deckName} and name {areaName} could be found.", assetCode, installationCode, deckName, areaName); + return NotFound(@$"No area with asset {assetCode}, installation {installationCode}, + deck {deckName} and name {areaName} could be found."); } } catch (Exception e) @@ -190,17 +148,105 @@ [FromBody] Pose safePosition [HttpDelete] [Authorize(Roles = Role.Admin)] [Route("{id}")] - [ProducesResponseType(typeof(Area), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(AreaResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> DeleteArea([FromRoute] string id) + 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"); - return Ok(area); + var response = new AreaResponse + { + Id = area.Id, + DeckName = area.Deck.Name, + InstallationCode = area.Installation.InstallationCode, + AssetCode = area.Asset.AssetCode, + AreaName = area.Name, + MapMetadata = area.MapMetadata, + DefaultLocalizationPose = area.DefaultLocalizationPose, + SafePositions = area.SafePositions + }; + return Ok(response); + } + + /// + /// List all asset areas in the Flotilla database + /// + /// + /// This query gets all asset 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, + InstallationCode = area.Installation.InstallationCode, + AssetCode = area.Asset.AssetCode, + 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, + InstallationCode = area.Installation.InstallationCode, + AssetCode = area.Asset.AssetCode, + 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; + } + } /// @@ -219,30 +265,38 @@ public async Task> GetMapMetadata([FromRoute] string i var area = await _areaService.ReadById(id); if (area is null) { - _logger.LogError("Area not found for area ID {areaId}", id); - return NotFound("Could not find this area"); + string errorMessage = $"Area not found for area with ID {id}"; + _logger.LogError("{ErrorMessage}", errorMessage); + return NotFound(errorMessage); } - MapMetadata? map; + MapMetadata? mapMetadata; var positions = new List { area.DefaultLocalizationPose.Position }; try { - map = await _mapService.ChooseMapFromPositions(positions, area.Deck.Installation.Asset.ShortName); + mapMetadata = await _mapService.ChooseMapFromPositions(positions, area.Deck.Installation.Asset.AssetCode); + } + 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) + catch (ArgumentOutOfRangeException e) { - _logger.LogWarning("Unable to find a map for area '{areaId}'", area.Id); - return NotFound("Could not find map suited for the positions in this area"); + string errorMessage = $"Could not find a suitable map for area {area.Id}"; + _logger.LogError(e, "{ErrorMessage}", errorMessage); + return NotFound(errorMessage); } - if (map == null) + if (mapMetadata == null) { - return NotFound("Could not find map for this area"); + return NotFound("A map which contained at least half of the points in this mission could not be found"); } - return Ok(map); + return Ok(mapMetadata); } } } diff --git a/backend/api/Controllers/AssetController.cs b/backend/api/Controllers/AssetController.cs new file mode 100644 index 000000000..b6025d4f1 --- /dev/null +++ b/backend/api/Controllers/AssetController.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("assets")] + public class AssetController : ControllerBase + { + private readonly IAssetService _assetService; + + private readonly IMapService _mapService; + + private readonly ILogger _logger; + + public AssetController( + ILogger logger, + IMapService mapService, + IAssetService assetService + ) + { + _logger = logger; + _mapService = mapService; + _assetService = assetService; + } + + /// + /// List all assets in the Flotilla database + /// + /// + /// This query gets all assets + /// + [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>> GetAssets() + { + try + { + var assets = await _assetService.ReadAll(); + return Ok(assets); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of assets from database"); + throw; + } + } + + /// + /// Lookup asset by specified id. + /// + [HttpGet] + [Authorize(Roles = Role.Any)] + [Route("{id}")] + [ProducesResponseType(typeof(Asset), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetAssetById([FromRoute] string id) + { + try + { + var asset = await _assetService.ReadById(id); + if (asset == null) + return NotFound($"Could not find asset with id {id}"); + return Ok(asset); + } + catch (Exception e) + { + _logger.LogError(e, "Error during GET of asset from database"); + throw; + } + + } + + /// + /// Add a new asset + /// + /// + /// This query adds a new asset to the database + /// + [HttpPost] + [Authorize(Roles = Role.Admin)] + [ProducesResponseType(typeof(Asset), StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> Create([FromBody] CreateAssetQuery asset) + { + _logger.LogInformation("Creating new asset"); + try + { + var existingAsset = await _assetService.ReadByName(asset.AssetCode); + if (existingAsset != null) + { + _logger.LogInformation("An asset for given name and asset already exists"); + return BadRequest($"Asset already exists"); + } + + var newAsset = await _assetService.Create(asset); + _logger.LogInformation( + "Succesfully created new asset with id '{assetId}'", + newAsset.Id + ); + return CreatedAtAction( + nameof(GetAssetById), + new { id = newAsset.Id }, + newAsset + ); + } + catch (Exception e) + { + _logger.LogError(e, "Error while creating new asset"); + throw; + } + } + + /// + /// Deletes the asset with the specified id from the database. + /// + [HttpDelete] + [Authorize(Roles = Role.Admin)] + [Route("{id}")] + [ProducesResponseType(typeof(Asset), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> DeleteAsset([FromRoute] string id) + { + var asset = await _assetService.Delete(id); + if (asset is null) + return NotFound($"Asset with id {id} not found"); + return Ok(asset); + } + } +} diff --git a/backend/api/Controllers/DeckController.cs b/backend/api/Controllers/DeckController.cs new file mode 100644 index 000000000..f17089d85 --- /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 IAssetService _assetService; + private readonly IInstallationService _installationService; + + private readonly IMapService _mapService; + + private readonly ILogger _logger; + + public DeckController( + ILogger logger, + IMapService mapService, + IDeckService deckService, + IAssetService assetService, + IInstallationService installationService + ) + { + _logger = logger; + _mapService = mapService; + _deckService = deckService; + _assetService = assetService; + _installationService = installationService; + } + + /// + /// 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 existingAsset = await _assetService.ReadByName(deck.AssetCode); + if (existingAsset == null) + { + return NotFound($"Could not find asset with name {deck.AssetCode}"); + } + var existingInstallation = await _installationService.ReadByAssetAndName(existingAsset, deck.InstallationCode); + if (existingInstallation == null) + { + return NotFound($"Could not find installation with name {deck.InstallationCode}"); + } + var existingDeck = await _deckService.ReadByAssetAndInstallationAndName(existingAsset, existingInstallation, 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/InstallationController.cs b/backend/api/Controllers/InstallationController.cs new file mode 100644 index 000000000..b44853f4c --- /dev/null +++ b/backend/api/Controllers/InstallationController.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("installations")] + public class InstallationController : ControllerBase + { + private readonly IInstallationService _installationService; + private readonly IAssetService _assetService; + + private readonly IMapService _mapService; + + private readonly ILogger _logger; + + public InstallationController( + ILogger logger, + IMapService mapService, + IInstallationService installationService, + IAssetService assetService + ) + { + _logger = logger; + _mapService = mapService; + _installationService = installationService; + _assetService = assetService; + } + + /// + /// 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 existingAsset = await _assetService.ReadByName(installation.AssetCode); + if (existingAsset == null) + { + return NotFound($"Asset with asset code {installation.AssetCode} not found"); + } + var existingInstallation = await _installationService.ReadByAssetAndName(existingAsset, 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 7c88f6456..e77fc9397 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 Api.Controllers.Models; using Api.Database.Models; using Api.Services; @@ -17,7 +18,7 @@ public class MissionController : ControllerBase private readonly IAreaService _areaService; private readonly IRobotService _robotService; private readonly IEchoService _echoService; - private readonly ISourceService _sourceService; + private readonly ICustomMissionService _customMissionService; private readonly ILogger _logger; private readonly IStidService _stidService; private readonly IMapService _mapService; @@ -28,7 +29,7 @@ public MissionController( IAreaService areaService, IRobotService robotService, IEchoService echoService, - ISourceService sourceService, + ICustomMissionService customMissionService, ILogger logger, IMapService mapService, IStidService stidService @@ -39,7 +40,7 @@ IStidService stidService _areaService = areaService; _robotService = robotService; _echoService = echoService; - _sourceService = sourceService; + _customMissionService = customMissionService; _mapService = mapService; _stidService = stidService; _logger = logger; @@ -49,7 +50,7 @@ IStidService stidService /// List all mission runs in the Flotilla database /// /// - /// This query gets all missions + /// This query gets all mission runs /// [HttpGet("runs")] [Authorize(Roles = Role.Any)] @@ -75,10 +76,10 @@ [FromQuery] MissionRunQueryStringParameters parameters return BadRequest("Max EndTime cannot be less than min EndTime"); } - PagedList missions; + PagedList missionRuns; try { - missions = await _missionRunService.ReadAll(parameters); + missionRuns = await _missionRunService.ReadAll(parameters); } catch (InvalidDataException e) { @@ -88,12 +89,12 @@ [FromQuery] MissionRunQueryStringParameters parameters var metadata = new { - missions.TotalCount, - missions.PageSize, - missions.CurrentPage, - missions.TotalPages, - missions.HasNext, - missions.HasPrevious + missionRuns.TotalCount, + missionRuns.PageSize, + missionRuns.CurrentPage, + missionRuns.TotalPages, + missionRuns.HasNext, + missionRuns.HasPrevious }; Response.Headers.Add( @@ -101,14 +102,14 @@ [FromQuery] MissionRunQueryStringParameters parameters JsonSerializer.Serialize(metadata) ); - return Ok(missions); + return Ok(missionRuns); } /// /// List all mission definitions in the Flotilla database /// /// - /// This query gets all missions + /// This query gets all mission definitions /// [HttpGet("definitions")] [Authorize(Roles = Role.Any)] @@ -121,11 +122,10 @@ public async Task>> GetMissionDefinitions( [FromQuery] MissionDefinitionQueryStringParameters parameters ) { - // TODO: define new parameters using primarily area and source type - PagedList missions; + PagedList missionDefinitions; try { - missions = await _missionDefinitionService.ReadAll(parameters); + missionDefinitions = await _missionDefinitionService.ReadAll(parameters); } catch (InvalidDataException e) { @@ -135,12 +135,12 @@ [FromQuery] MissionDefinitionQueryStringParameters 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( @@ -148,7 +148,7 @@ [FromQuery] MissionDefinitionQueryStringParameters parameters JsonSerializer.Serialize(metadata) ); - return Ok(missions); + return Ok(missionDefinitions); } /// @@ -164,10 +164,10 @@ [FromQuery] MissionDefinitionQueryStringParameters parameters [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> GetMissionRunById([FromRoute] string id) { - var mission = await _missionRunService.ReadById(id); - if (mission == null) - return NotFound($"Could not find mission with id {id}"); - return Ok(mission); + var missioRun = await _missionRunService.ReadById(id); + if (missioRun == null) + return NotFound($"Could not find mission run with id {id}"); + return Ok(missioRun); } /// @@ -183,10 +183,10 @@ public async Task> GetMissionRunById([FromRoute] string [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> GetMissionDefinitionById([FromRoute] string id) { - var mission = await _missionDefinitionService.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); } /// @@ -216,12 +216,12 @@ public async Task> GetMap([FromRoute] string assetCode, str } /// - /// Reschedule an existing mission + /// Schedule an existing mission definition /// /// - /// This query reschedules an existing mission and adds it to the database + /// This query schedules an existing mission and adds it to the database /// - [HttpPost("{missionId}/reschedule")] + [HttpPost("schedule")] [Authorize(Roles = Role.User)] [ProducesResponseType(typeof(MissionRun), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] @@ -229,42 +229,26 @@ public async Task> GetMap([FromRoute] string assetCode, str [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task> Rechedule( - [FromRoute] string missionId, - [FromBody] RescheduleMissionQuery rescheduledMissionQuery + public async Task> Schedule( + [FromBody] ScheduleMissionQuery scheduledMissionQuery ) { - var robot = await _robotService.ReadById(rescheduledMissionQuery.RobotId); + var robot = await _robotService.ReadById(scheduledMissionQuery.RobotId); if (robot is null) - return NotFound($"Could not find robot with id {rescheduledMissionQuery.RobotId}"); - - MissionDefinition? missionDefinition; - try - { - missionDefinition = await _missionDefinitionService.ReadById(missionId); - if (missionDefinition == null) - return NotFound("Mission definition not found"); - } - catch (HttpRequestException e) - { - if (e.StatusCode.HasValue && (int)e.StatusCode.Value == 404) - { - _logger.LogWarning( - "Could not find mission definition with id={id}", - missionId - ); - return NotFound("Mission definition not found"); - } + return NotFound($"Could not find robot with id {scheduledMissionQuery.RobotId}"); - _logger.LogError(e, "Error getting mission database"); - return StatusCode(StatusCodes.Status502BadGateway, $"{e.Message}"); - } + var missionDefinition = await _missionDefinitionService.ReadById(scheduledMissionQuery.MissionDefinitionId); + if (missionDefinition == null) + return NotFound("Mission definition not found"); List? missionTasks; - switch (missionDefinition.Source.Type) + missionTasks = missionDefinition.Source.Type switch { - case MissionSourceType.Echo: - missionTasks = _echoService.GetMissionById(Int32.Parse(missionDefinition.Source.Id)).Result.Tags + 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 => { @@ -274,39 +258,37 @@ [FromBody] RescheduleMissionQuery rescheduledMissionQuery return new MissionTask(t, tagPosition); } ) - .ToList(); - break; - case MissionSourceType.Custom: - missionTasks = _sourceService.GetMissionTasksFromURL(missionDefinition.Source.URL); - break; - default: - return BadRequest("Invalid mission source type provided"); - } + .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 scheduledMission = new MissionRun + var missionRun = new MissionRun { Name = missionDefinition.Name, Robot = robot, MissionId = missionDefinition.Id, Status = MissionStatus.Pending, - DesiredStartTime = rescheduledMissionQuery.DesiredStartTime, + DesiredStartTime = scheduledMissionQuery.DesiredStartTime, Tasks = missionTasks, AssetCode = missionDefinition.AssetCode, Area = missionDefinition.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 newMission = await _missionRunService.Create(scheduledMission); + var newMissionRun = await _missionRunService.Create(missionRun); - return CreatedAtAction(nameof(GetMissionRunById), new { id = newMission.Id }, newMission); + return CreatedAtAction(nameof(GetMissionRunById), new { id = newMissionRun.Id }, newMissionRun); } /// @@ -327,6 +309,8 @@ 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}"); @@ -380,6 +364,7 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery 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 asset {scheduledMissionQuery.AssetCode}"); } @@ -390,8 +375,7 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery Id = Guid.NewGuid().ToString(), Source = new Source { - Id = Guid.NewGuid().ToString(), - URL = $"robots/robot-plan/{echoMission.Id}", // Could use echoMission.URL here, but that would necessitate new retrieval methods + SourceId = $"{echoMission.Id}", Type = MissionSourceType.Echo }, Name = echoMission.Name, @@ -400,7 +384,7 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery Area = area }; - var scheduledMission = new MissionRun + var missionRun = new MissionRun { Name = echoMission.Name, Robot = robot, @@ -413,16 +397,16 @@ [FromBody] ScheduledMissionQuery scheduledMissionQuery 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 _missionRunService.Create(scheduledMission); + var newMissionRun = await _missionRunService.Create(missionRun); - return CreatedAtAction(nameof(GetMissionRunById), new { id = newMission.Id }, newMission); + return CreatedAtAction(nameof(GetMissionRunById), new { id = newMissionRun.Id }, newMissionRun); } /// @@ -444,11 +428,6 @@ public async Task> Create( [FromBody] CustomMissionQuery customMissionQuery ) { - - // TODO: only allow admins - - // TODO: create new endpoint for scheduling existing custom missions - var robot = await _robotService.ReadById(customMissionQuery.RobotId); if (robot is null) return NotFound($"Could not find robot with id {customMissionQuery.RobotId}"); @@ -460,16 +439,13 @@ [FromBody] CustomMissionQuery customMissionQuery if (area == null) return NotFound($"Could not find area with name {customMissionQuery.AreaName} in asset {customMissionQuery.AssetCode}"); - string customMissionId = Guid.NewGuid().ToString(); - var sourceURL = await _sourceService.UploadSource(customMissionId, missionTasks); + string sourceURL = await _customMissionService.UploadSource(missionTasks); var customMissionDefinition = new MissionDefinition { - Id = customMissionId, Source = new Source { - Id = Guid.NewGuid().ToString(), - URL = sourceURL.ToString(), + SourceId = sourceURL.ToString(), Type = MissionSourceType.Echo }, Name = customMissionQuery.Name, @@ -500,9 +476,9 @@ [FromBody] CustomMissionQuery customMissionQuery var newMissionDefinition = await _missionDefinitionService.Create(customMissionDefinition); - var newMission = await _missionRunService.Create(scheduledMission); + var newMissionRun = await _missionRunService.Create(scheduledMission); - return CreatedAtAction(nameof(GetMissionRunById), new { id = newMission.Id }, newMission); + return CreatedAtAction(nameof(GetMissionRunById), new { id = newMissionRun.Id }, newMissionRun); } /// @@ -518,10 +494,10 @@ [FromBody] CustomMissionQuery customMissionQuery [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> DeleteMissionDefinition([FromRoute] string id) { - var mission = await _missionDefinitionService.Delete(id); - if (mission is null) + var missionDefinition = await _missionDefinitionService.Delete(id); + if (missionDefinition is null) return NotFound($"Mission definition with id {id} not found"); - return Ok(mission); + return Ok(missionDefinition); } /// @@ -537,9 +513,9 @@ public async Task> DeleteMissionDefinition([From [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task> DeleteMissionRun([FromRoute] string id) { - var mission = await _missionRunService.Delete(id); - if (mission is null) + var missionRun = await _missionRunService.Delete(id); + if (missionRun is null) return NotFound($"Mission run with id {id} not found"); - return Ok(mission); + 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..9b0a3ad3a --- /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 InstallationCode { get; set; } + + public string AssetCode { 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/CreateAreaQuery.cs b/backend/api/Controllers/Models/CreateAreaQuery.cs index ac25914af..2abd7f051 100644 --- a/backend/api/Controllers/Models/CreateAreaQuery.cs +++ b/backend/api/Controllers/Models/CreateAreaQuery.cs @@ -5,7 +5,7 @@ namespace Api.Controllers.Models public struct CreateAreaQuery { public string AssetCode { get; set; } - public string InstallationName { get; set; } + public string InstallationCode { get; set; } public string DeckName { get; set; } public string AreaName { get; set; } diff --git a/backend/api/Controllers/Models/CreateAssetDeckQuery.cs b/backend/api/Controllers/Models/CreateAssetDeckQuery.cs deleted file mode 100644 index c42fd473f..000000000 --- a/backend/api/Controllers/Models/CreateAssetDeckQuery.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Api.Database.Models; - -namespace Api.Controllers.Models -{ - public struct CreateAssetDeckQuery - { - public string AssetCode { get; set; } - - public string DeckName { get; set; } - - public Pose DefaultLocalizationPose { get; set; } - } -} diff --git a/backend/api/Controllers/Models/CreateAssetQuery.cs b/backend/api/Controllers/Models/CreateAssetQuery.cs new file mode 100644 index 000000000..9341141b8 --- /dev/null +++ b/backend/api/Controllers/Models/CreateAssetQuery.cs @@ -0,0 +1,8 @@ +namespace Api.Controllers.Models +{ + public struct CreateAssetQuery + { + public string AssetCode { get; set; } + public string Name { get; set; } + } +} diff --git a/backend/api/Controllers/Models/CreateDeckQuery.cs b/backend/api/Controllers/Models/CreateDeckQuery.cs new file mode 100644 index 000000000..235f5cf08 --- /dev/null +++ b/backend/api/Controllers/Models/CreateDeckQuery.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Models +{ + public struct CreateDeckQuery + { + public string AssetCode { get; set; } + public string InstallationCode { 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..4bab44759 --- /dev/null +++ b/backend/api/Controllers/Models/CreateInstallationQuery.cs @@ -0,0 +1,9 @@ +namespace Api.Controllers.Models +{ + public struct CreateInstallationQuery + { + public string AssetCode { get; set; } + public string InstallationCode { 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..5675176e1 100644 --- a/backend/api/Controllers/Models/CreateRobotQuery.cs +++ b/backend/api/Controllers/Models/CreateRobotQuery.cs @@ -14,7 +14,7 @@ public struct CreateRobotQuery public string CurrentAsset { get; set; } - public AssetDeck CurrentAssetDeck { get; set; } + public Area CurrentArea { get; set; } public IList VideoStreams { get; set; } diff --git a/backend/api/Controllers/Models/RescheduleMissionQuery.cs b/backend/api/Controllers/Models/RescheduleMissionQuery.cs deleted file mode 100644 index de0109324..000000000 --- a/backend/api/Controllers/Models/RescheduleMissionQuery.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Api.Database.Models; - -namespace Api.Controllers.Models -{ - public class RescheduleMissionQuery - { - public MissionSourceType MissionType { get; set; } - public string RobotId { get; set; } - public DateTimeOffset DesiredStartTime { get; set; } - public TimeSpan? InspectionFrequency { 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/RobotController.cs b/backend/api/Controllers/RobotController.cs index 33f8a8c64..91ae652ea 100644 --- a/backend/api/Controllers/RobotController.cs +++ b/backend/api/Controllers/RobotController.cs @@ -16,7 +16,7 @@ public class RobotController : ControllerBase private readonly ILogger _logger; private readonly IRobotService _robotService; private readonly IIsarService _isarService; - private readonly IMissionRunService _missionService; + private readonly IMissionRunService _missionRunService; private readonly IRobotModelService _robotModelService; private readonly IAreaService _areaService; @@ -24,7 +24,7 @@ public RobotController( ILogger logger, IRobotService robotService, IIsarService isarService, - IMissionRunService missionService, + IMissionRunService missionRunService, IRobotModelService robotModelService, IAreaService areaService ) @@ -32,7 +32,7 @@ IAreaService areaService _logger = logger; _robotService = robotService; _isarService = isarService; - _missionService = missionService; + _missionRunService = missionRunService; _robotModelService = robotModelService; _areaService = areaService; } @@ -324,14 +324,14 @@ [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}")] + [Route("{robotId}/start/{missionRunId}")] [ProducesResponseType(typeof(MissionRun), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] @@ -340,7 +340,7 @@ [FromBody] VideoStream videoStream [ProducesResponseType(StatusCodes.Status500InternalServerError)] 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); } /// @@ -679,11 +679,12 @@ [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery return NotFound("Area not found"); } - var mission = new MissionRun + var missionRun = new MissionRun { Name = "Localization Mission", Robot = robot, Area = area, + AssetCode = "NA", Status = MissionStatus.Pending, DesiredStartTime = DateTimeOffset.UtcNow, Tasks = new List(), @@ -714,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.CurrentArea = area; + robot.Status = RobotStatus.Busy; + robot.CurrentMissionId = missionRun.Id; await _robotService.Update(robot); - return Ok(mission); + robot.CurrentArea = area; + return Ok(missionRun); } /// @@ -741,7 +742,7 @@ [FromBody] ScheduleLocalizationMissionQuery scheduleLocalizationMissionQuery [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 areaName @@ -806,7 +807,8 @@ [FromRoute] string areaName InspectionTarget = new Position(), TaskOrder = 0 }; - var mission = new MissionRun + // TODO: The MissionId is nullable because of this mission + var missionRun = new MissionRun { Name = "Drive to Safe Position", Robot = robot, @@ -821,7 +823,7 @@ [FromRoute] string areaName IsarMission isarMission; try { - isarMission = await _isarService.StartMission(robot, mission); + isarMission = await _isarService.StartMission(robot, missionRun); } catch (HttpRequestException e) { @@ -842,15 +844,15 @@ [FromRoute] string areaName 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) @@ -859,14 +861,14 @@ private async void OnIsarUnavailable(Robot robot) 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 ); } } @@ -902,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/Database/Context/FlotillaDbContext.cs b/backend/api/Database/Context/FlotillaDbContext.cs index f5c5644a0..5d72c5d07 100644 --- a/backend/api/Database/Context/FlotillaDbContext.cs +++ b/backend/api/Database/Context/FlotillaDbContext.cs @@ -26,12 +26,12 @@ 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 => + missionRunEntity => { if (isSqlLite) - AddConverterForDateTimeOffsets(ref missionEntity); - missionEntity.OwnsMany( - mission => mission.Tasks, + AddConverterForDateTimeOffsets(ref missionRunEntity); + missionRunEntity.OwnsMany( + missionRun => missionRun.Tasks, taskEntity => { if (isSqlLite) @@ -55,11 +55,13 @@ 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.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); @@ -82,8 +84,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity().HasIndex(model => model.Type).IsUnique(); // There can only be one unique asset and installation shortname - modelBuilder.Entity().HasIndex(a => new { a.ShortName }).IsUnique(); - modelBuilder.Entity().HasIndex(a => new { a.ShortName }).IsUnique(); + modelBuilder.Entity().HasIndex(a => new { a.AssetCode }).IsUnique(); + modelBuilder.Entity().HasIndex(a => new { a.InstallationCode }).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 5b6d048f7..42c0ed231 100644 --- a/backend/api/Database/Context/InitDb.cs +++ b/backend/api/Database/Context/InitDb.cs @@ -84,7 +84,7 @@ private static List GetAssets() { Id = Guid.NewGuid().ToString(), Name = "Johan Sverdrup", - ShortName = "JSV" + AssetCode = "JSV" }; return new List(new Asset[] { asset1 }); @@ -97,7 +97,7 @@ private static List GetInstallations() Id = Guid.NewGuid().ToString(), Asset = assets[0], Name = "Johan Sverdrup - P1", - ShortName = "P1" + InstallationCode = "P1" }; return new List(new Installation[] { installation1 }); @@ -126,7 +126,7 @@ private static List GetAreas() Asset = decks[0].Installation.Asset, Name = "AP320", MapMetadata = new MapMetadata(), - DefaultLocalizationPose = new Pose {}, + DefaultLocalizationPose = new Pose { }, SafePositions = new List() }; @@ -138,7 +138,7 @@ private static List GetAreas() Asset = decks[0].Installation.Asset, Name = "AP330", MapMetadata = new MapMetadata(), - DefaultLocalizationPose = new Pose {}, + DefaultLocalizationPose = new Pose { }, SafePositions = new List() }; @@ -161,15 +161,13 @@ private static List GetSources() { var source1 = new Source { - Id = Guid.NewGuid().ToString(), - URL = "https://google.com/", + SourceId = "https://google.com/", Type = MissionSourceType.Echo }; var source2 = new Source { - Id = Guid.NewGuid().ToString(), - URL = "https://google.com/", + SourceId = "https://google.com/", Type = MissionSourceType.Custom }; @@ -178,11 +176,11 @@ private static List GetSources() private static List GetMissionDefinitions() { - var mission1 = new MissionDefinition + var missionDefinition1 = new MissionDefinition { - Name = "Placeholder Mission 1", Id = Guid.NewGuid().ToString(), - AssetCode = areas[0].Deck.Installation.Asset.ShortName, + Name = "Placeholder Mission 1", + AssetCode = areas[0].Deck.Installation.Asset.AssetCode, Area = areas[0], Source = sources[0], Comment = "Interesting comment", @@ -190,37 +188,37 @@ private static List GetMissionDefinitions() LastRun = null }; - var mission2 = new MissionDefinition + var missionDefinition2 = new MissionDefinition { - Name = "Placeholder Mission 2", Id = Guid.NewGuid().ToString(), - AssetCode = areas[1].Deck.Installation.Asset.ShortName, + Name = "Placeholder Mission 2", + AssetCode = areas[1].Deck.Installation.Asset.AssetCode, Area = areas[1], Source = sources[1], InspectionFrequency = new DateTime().AddDays(7) - new DateTime(), LastRun = null }; - var mission3 = new MissionDefinition + var missionDefinition3 = new MissionDefinition { - Name = "Placeholder Mission 3", Id = Guid.NewGuid().ToString(), - AssetCode = areas[1].Deck.Installation.Asset.ShortName, + Name = "Placeholder Mission 3", + AssetCode = areas[1].Deck.Installation.Asset.AssetCode, Area = areas[1], Source = sources[1], LastRun = null }; - return new List(new[] { mission1, mission2, mission3 }); + return new List(new[] { missionDefinition1, missionDefinition2, missionDefinition3 }); } private static List GetMissionRuns() { - var mission1 = new MissionRun + var missionRun1 = new MissionRun { Name = "Placeholder Mission 1", Robot = robots[0], - AssetCode = areas[0].Deck.Installation.Asset.ShortName, + AssetCode = areas[0].Deck.Installation.Asset.AssetCode, Area = areas[0], MissionId = missionDefinitions[0].Id, Status = MissionStatus.Successful, @@ -229,11 +227,11 @@ private static List GetMissionRuns() MapMetadata = new MapMetadata() }; - var mission2 = new MissionRun + var missionRun2 = new MissionRun { Name = "Placeholder Mission 2", Robot = robots[1], - AssetCode = areas[1].Deck.Installation.Asset.ShortName, + AssetCode = areas[1].Deck.Installation.Asset.AssetCode, Area = areas[1], MissionId = missionDefinitions[0].Id, Status = MissionStatus.Successful, @@ -241,12 +239,13 @@ private static List GetMissionRuns() Tasks = new List(), MapMetadata = new MapMetadata() }; + missionDefinitions[0].LastRun = missionRun2; - var mission3 = new MissionRun + var missionRun3 = new MissionRun { Name = "Placeholder Mission 3", Robot = robots[2], - AssetCode = areas[1].Deck.Installation.Asset.ShortName, + AssetCode = areas[1].Deck.Installation.Asset.AssetCode, Area = areas[1], MissionId = missionDefinitions[1].Id, Status = MissionStatus.Successful, @@ -255,7 +254,9 @@ private static List GetMissionRuns() MapMetadata = new MapMetadata() }; - return new List(new[] { mission1, mission2, mission3 }); + missionDefinitions[1].LastRun = missionRun3; + + return new List(new[] { missionRun1, missionRun2, missionRun3 }); } public static void PopulateDb(FlotillaDbContext context) @@ -291,13 +292,13 @@ 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 missionRuns) + 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(missionDefinitions); diff --git a/backend/api/Database/Models/Area.cs b/backend/api/Database/Models/Area.cs index 13eeb00c6..5c20757be 100644 --- a/backend/api/Database/Models/Area.cs +++ b/backend/api/Database/Models/Area.cs @@ -20,6 +20,7 @@ public class Area public Asset Asset { get; set; } [Required] + [MaxLength(200)] public string Name { get; set; } [Required] diff --git a/backend/api/Database/Models/Asset.cs b/backend/api/Database/Models/Asset.cs index e5e832c44..08c7ceae1 100644 --- a/backend/api/Database/Models/Asset.cs +++ b/backend/api/Database/Models/Asset.cs @@ -16,6 +16,6 @@ public class Asset [Required] [MaxLength(10)] - public string ShortName { get; set; } + public string AssetCode { get; set; } } } diff --git a/backend/api/Database/Models/AssetDeck.cs b/backend/api/Database/Models/AssetDeck.cs deleted file mode 100644 index 01fd60564..000000000 --- a/backend/api/Database/Models/AssetDeck.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -#pragma warning disable CS8618 -namespace Api.Database.Models -{ - public class AssetDeck - { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public string Id { get; set; } - - [Required] - [MaxLength(200)] - public string AssetCode { get; set; } - - [Required] - [MaxLength(200)] - public string DeckName { get; set; } - - [Required] - public Pose DefaultLocalizationPose { get; set; } - - public IList SafePositions { get; set; } - } -} diff --git a/backend/api/Database/Models/Installation.cs b/backend/api/Database/Models/Installation.cs index 236ec3af1..1e0157d9e 100644 --- a/backend/api/Database/Models/Installation.cs +++ b/backend/api/Database/Models/Installation.cs @@ -14,11 +14,11 @@ public class Installation public Asset Asset { get; set; } [Required] - [MaxLength(200)] - public string Name { get; set; } + [MaxLength(10)] + public string InstallationCode { get; set; } [Required] - [MaxLength(10)] - public string ShortName { get; set; } + [MaxLength(200)] + public string Name { get; set; } } } diff --git a/backend/api/Database/Models/MissionDefinition.cs b/backend/api/Database/Models/MissionDefinition.cs index d2de32cda..228eca2a2 100644 --- a/backend/api/Database/Models/MissionDefinition.cs +++ b/backend/api/Database/Models/MissionDefinition.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; #pragma warning disable CS8618 namespace Api.Database.Models @@ -6,8 +7,7 @@ namespace Api.Database.Models public class MissionDefinition { [Key] - [Required] - [MaxLength(200)] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public string Id { get; set; } [Required] @@ -17,16 +17,18 @@ public class MissionDefinition [MaxLength(200)] public string Name { get; set; } + [Required] + public string AssetCode { get; set; } + [MaxLength(1000)] public string? Comment { get; set; } public TimeSpan? InspectionFrequency { get; set; } - public MissionRun? LastRun { get; set; } - - [Required] - public string AssetCode { get; set; } + public virtual MissionRun? LastRun { get; set; } public Area? Area { get; set; } + + public bool Deprecated { get; set; } } } diff --git a/backend/api/Database/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs index c02ef058b..c8a7c4b61 100644 --- a/backend/api/Database/Models/MissionRun.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -11,35 +11,13 @@ public class MissionRun [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public string Id { get; set; } - [MaxLength(200)] + //[Required] // See "Drive to Safe Position" mission in RobotController.cs public string? MissionId { get; set; } - [MaxLength(200)] - public string? IsarMissionId { 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] - public string AssetCode { get; set; } - - public Area? Area { get; set; } - - [Required] - public virtual Robot Robot { get; set; } - - private MissionStatus _status; - [Required] public MissionStatus Status { @@ -55,6 +33,40 @@ public MissionStatus Status } } + [Required] + [MaxLength(200)] + public string AssetCode { 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 @@ -65,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; } @@ -79,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/Robot.cs b/backend/api/Database/Models/Robot.cs index 2f125c259..5ebe4cb21 100644 --- a/backend/api/Database/Models/Robot.cs +++ b/backend/api/Database/Models/Robot.cs @@ -99,7 +99,7 @@ public Robot(CreateRobotQuery createQuery) Name = createQuery.Name; SerialNumber = createQuery.SerialNumber; CurrentAsset = createQuery.CurrentAsset; - CurrentArea = createQuery.CurrentAssetDeck; + 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 54629e24b..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 index 08ea025d4..0168c2623 100644 --- a/backend/api/Database/Models/Source.cs +++ b/backend/api/Database/Models/Source.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; #pragma warning disable CS8618 namespace Api.Database.Models @@ -6,12 +7,11 @@ namespace Api.Database.Models public class Source { [Key] - [Required] - [MaxLength(200)] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public string Id { get; set; } [Required] - public string URL { get; set; } + public string SourceId { get; set; } [Required] public MissionSourceType Type { get; set; } diff --git a/backend/api/EventHandlers/IsarConnectionEventHandler.cs b/backend/api/EventHandlers/IsarConnectionEventHandler.cs index 36165b9ff..a3cd45eea 100644 --- a/backend/api/EventHandlers/IsarConnectionEventHandler.cs +++ b/backend/api/EventHandlers/IsarConnectionEventHandler.cs @@ -18,7 +18,7 @@ public class IsarConnectionEventHandler : EventHandlerBase private IRobotService RobotService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); - private IMissionRunService MissionService => + 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 e2d0488a5..bea7190e1 100644 --- a/backend/api/EventHandlers/MissionScheduler.cs +++ b/backend/api/EventHandlers/MissionScheduler.cs @@ -13,8 +13,8 @@ public class MissionScheduler : BackgroundService private readonly int _timeDelay; private readonly IServiceScopeFactory _scopeFactory; - private IList MissionQueue => - MissionService + private IList MissionRunQueue => + MissionRunService .ReadAll( new MissionRunQueryStringParameters { @@ -25,7 +25,7 @@ public class MissionScheduler : BackgroundService ) .Result; - private IMissionRunService MissionService => + private IMissionRunService MissionRunService => _scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); private RobotController RobotController => @@ -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(MissionRun 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(MissionRun 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 4b41e4318..ba1125ff2 100644 --- a/backend/api/EventHandlers/MqttEventHandler.cs +++ b/backend/api/EventHandlers/MqttEventHandler.cs @@ -248,7 +248,7 @@ private static void UpdateCurrentAssetIfChanged(string newCurrentAsset, ref Robo 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(); @@ -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,7 +325,7 @@ private async void OnMissionUpdate(object? sender, MqttReceivedArgs mqttArgs) long minEpochTime = DateTimeOffset.Now .AddDays(-timeRangeInDays) .ToUnixTimeSeconds(); - var missionsForEstimation = await missionService.ReadAll( + var missionRunsForEstimation = await missionRunService.ReadAll( new MissionRunQueryStringParameters { MinDesiredStartTime = minEpochTime, @@ -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/Options/StorageOptions.cs b/backend/api/Options/StorageOptions.cs index 69657226e..ff2a77f5d 100644 --- a/backend/api/Options/StorageOptions.cs +++ b/backend/api/Options/StorageOptions.cs @@ -3,6 +3,6 @@ public class StorageOptions { public string CustomMissionContainerName { get; set; } = ""; - public string ConnectionString { get; set; } = ""; + public string AccountName { get; set; } = ""; } } diff --git a/backend/api/Program.cs b/backend/api/Program.cs index 771af4de4..a64ab199b 100644 --- a/backend/api/Program.cs +++ b/backend/api/Program.cs @@ -48,9 +48,12 @@ 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(); diff --git a/backend/api/Services/AreaService.cs b/backend/api/Services/AreaService.cs index 61a0d09b7..7c451f515 100644 --- a/backend/api/Services/AreaService.cs +++ b/backend/api/Services/AreaService.cs @@ -1,6 +1,7 @@ using Api.Controllers.Models; using Api.Database.Context; using Api.Database.Models; +using Api.Utilities; using Microsoft.EntityFrameworkCore; namespace Api.Services @@ -11,9 +12,9 @@ public interface IAreaService public abstract Task ReadById(string id); - public abstract Task> ReadByAsset(string asset); + public abstract Task> ReadByAsset(string assetCode); - public abstract Task ReadByAssetAndName(string assetName, string name); + public abstract Task ReadByAssetAndName(string assetCode, string areaName); public abstract Task Create(CreateAreaQuery newArea); @@ -21,7 +22,7 @@ public interface IAreaService public abstract Task Update(Area area); - public abstract Task AddSafePosition(string asset, string name, SafePosition safePosition); + public abstract Task AddSafePosition(string assetCode, string areaName, SafePosition safePosition); public abstract Task Delete(string id); @@ -40,10 +41,17 @@ public interface IAreaService public class AreaService : IAreaService { private readonly FlotillaDbContext _context; + private readonly IAssetService _assetService; + private readonly IInstallationService _installationService; + private readonly IDeckService _deckService; - public AreaService(FlotillaDbContext context) + public AreaService( + FlotillaDbContext context, IAssetService assetService, IInstallationService installationService, IDeckService deckService) { _context = context; + _assetService = assetService; + _installationService = installationService; + _deckService = deckService; } public async Task> ReadAll() @@ -54,7 +62,7 @@ public async Task> ReadAll() private IQueryable GetAreas() { return _context.Areas.Include(a => a.SafePositions) - .Include(a => a.Deck).ThenInclude(d => d.Installation).ThenInclude(i => i.Asset); + .Include(a => a.Deck).Include(d => d.Installation).Include(i => i.Asset); } public async Task ReadById(string id) @@ -63,141 +71,100 @@ private IQueryable GetAreas() .FirstOrDefaultAsync(a => a.Id.Equals(id)); } - public async Task ReadByAssetAndName(Asset? asset, string name) + public async Task ReadByAssetAndName(Asset? asset, string areaName) { if (asset == null) return null; + return await _context.Areas.Where(a => - a.Name.ToLower().Equals(name.ToLower()) && - a.Asset.Equals(asset) - ).Include(a => a.SafePositions).FirstOrDefaultAsync(); + a.Name.ToLower().Equals(areaName.ToLower()) && + a.Asset.Id.Equals(asset.Id) + ).Include(a => a.SafePositions).Include(a => a.Asset) + .Include(a => a.Installation).Include(a => a.Deck).FirstOrDefaultAsync(); } - public async Task> ReadByAsset(string assetName) + public async Task ReadByAssetAndName(string assetCode, string areaName) { - var asset = await ReadAssetByName(assetName); + var asset = await _assetService.ReadByName(assetCode); if (asset == null) - return new List(); + return null; + return await _context.Areas.Where(a => - a.Asset.Equals(asset)).Include(a => a.SafePositions).ToListAsync(); + a.Asset.Id.Equals(asset.Id) && + a.Name.ToLower().Equals(areaName.ToLower()) + ).Include(a => a.SafePositions).Include(a => a.Asset) + .Include(a => a.Installation).Include(a => a.Deck).FirstOrDefaultAsync(); } - public async Task ReadByAssetAndName(string assetName, string name) + public async Task> ReadByAsset(string assetCode) { - // TODO: can we assume that this combination will be unique? Are area names specific enough? - var asset = await ReadAssetByName(assetName); + var asset = await _assetService.ReadByName(assetCode); if (asset == null) - return null; + return new List(); + return await _context.Areas.Where(a => - a.Asset.Equals(asset) && - a.Name.ToLower().Equals(name.ToLower()) - ).Include(a => a.SafePositions).FirstOrDefaultAsync(); + a.Asset.Id.Equals(asset.Id)).Include(a => a.SafePositions).Include(a => a.Asset) + .Include(a => a.Installation).Include(a => a.Deck).ToListAsync(); } - public async Task ReadAreaByAssetAndInstallationAndDeckAndName(Asset? asset, Installation? installation, Deck? deck, string name) + public async Task ReadByAssetAndInstallationAndDeckAndName(Asset? asset, Installation? installation, Deck? deck, string areaName) { if (asset == null || installation == null || deck == null) return null; + return await _context.Areas.Where(a => - a.Deck.Equals(deck) && - a.Installation.Equals(installation) && - a.Asset.Equals(asset) && - a.Name.ToLower().Equals(name.ToLower()) - ).Include(a => a.Deck).ThenInclude(d => d.Installation).ThenInclude(i => i.Asset) + a.Deck.Id.Equals(deck.Id) && + a.Installation.Id.Equals(installation.Id) && + a.Asset.Id.Equals(asset.Id) && + a.Name.ToLower().Equals(areaName.ToLower()) + ).Include(a => a.Deck).Include(d => d.Installation).Include(i => i.Asset) .Include(a => a.SafePositions).FirstOrDefaultAsync(); } - public async Task ReadDeckByAssetAndInstallationAndName(Asset? asset, Installation? installation, string name) - { - if (asset == null || installation == null) - return null; - return await _context.Decks.Where(a => - a.Installation.Equals(installation) && - a.Asset.Equals(asset) && - a.Name.ToLower().Equals(name.ToLower()) - ).Include(d => d.Installation).Include(i => i.Asset).FirstOrDefaultAsync(); - } - - public async Task ReadInstallationByAssetAndName(Asset? asset, string name) - { - if (asset == null) - return null; - return await _context.Installations.Where(a => - a.Asset.Equals(asset) && - a.Name.ToLower().Equals(name.ToLower()) - ).Include(i => i.Asset).FirstOrDefaultAsync(); - } - - public async Task ReadAssetByName(string asset) + public async Task Create(CreateAreaQuery newAreaQuery, List positions) { - return await _context.Assets.Where(a => - a.ShortName.ToLower().Equals(asset.ToLower()) - ).FirstOrDefaultAsync(); - } - - public async Task Create(CreateAreaQuery newAreaQuery, List safePositions) - { - var sp = new List(); - foreach (var p in safePositions) + var safePositions = new List(); + foreach (var pose in positions) { - sp.Add(new SafePosition(p)); + safePositions.Add(new SafePosition(pose)); } - var asset = await ReadAssetByName(newAreaQuery.AssetCode); + var asset = await _assetService.ReadByName(newAreaQuery.AssetCode); if (asset == null) { - asset = new Asset - { - Name = "", // TODO: - ShortName = newAreaQuery.AssetCode - }; - await _context.Assets.AddAsync(asset); - await _context.SaveChangesAsync(); + throw new AssetNotFoundException($"No asset with name {newAreaQuery.AssetCode} could be found"); } - var installation = await ReadInstallationByAssetAndName(asset, newAreaQuery.InstallationName); + var installation = await _installationService.ReadByAssetAndName(asset, newAreaQuery.InstallationCode); if (installation == null) { - installation = new Installation - { - Asset = asset, - Name = "", // TODO: - ShortName = newAreaQuery.InstallationName - }; - await _context.Installations.AddAsync(installation); - await _context.SaveChangesAsync(); + throw new InstallationNotFoundException($"No installation with name {newAreaQuery.InstallationCode} could be found"); } - var deck = await ReadDeckByAssetAndInstallationAndName(asset, installation, newAreaQuery.DeckName); + var deck = await _deckService.ReadByAssetAndInstallationAndName(asset, installation, newAreaQuery.DeckName); if (deck == null) { - deck = new Deck - { - Installation = installation, - Asset = asset, - Name = newAreaQuery.DeckName - }; - await _context.Decks.AddAsync(deck); - await _context.SaveChangesAsync(); + throw new DeckNotFoundException($"No deck with name {newAreaQuery.DeckName} could be found"); } - var existingArea = await ReadAreaByAssetAndInstallationAndDeckAndName( + var existingArea = await ReadByAssetAndInstallationAndDeckAndName( asset, installation, deck, newAreaQuery.AreaName); if (existingArea != null) { - // TODO: maybe just append safe positions, or return an error + throw new AreaNotFoundException($"No area with name {newAreaQuery.AreaName} could be found"); } var newArea = new Area { Name = newAreaQuery.AreaName, DefaultLocalizationPose = newAreaQuery.DefaultLocalizationPose, - SafePositions = sp, + SafePositions = safePositions, MapMetadata = new MapMetadata(), Deck = deck, Installation = installation, Asset = asset }; + await _context.Areas.AddAsync(newArea); await _context.SaveChangesAsync(); return newArea; @@ -209,13 +176,14 @@ public async Task Create(CreateAreaQuery newArea) return area; } - public async Task AddSafePosition(string asset, string name, SafePosition safePosition) + public async Task AddSafePosition(string assetCode, string areaName, SafePosition safePosition) { - var area = await ReadByAssetAndName(asset, name); + var area = await ReadByAssetAndName(assetCode, areaName); if (area is null) { return null; } + area.SafePositions.Add(safePosition); _context.Areas.Update(area); await _context.SaveChangesAsync(); diff --git a/backend/api/Services/AssetService.cs b/backend/api/Services/AssetService.cs new file mode 100644 index 000000000..8cbd6930e --- /dev/null +++ b/backend/api/Services/AssetService.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 IAssetService + { + public abstract Task> ReadAll(); + + public abstract Task ReadById(string id); + + public abstract Task ReadByName(string asset); + + public abstract Task Create(CreateAssetQuery newAsset); + + public abstract Task Update(Asset asset); + + 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 AssetService : IAssetService + { + private readonly FlotillaDbContext _context; + + public AssetService(FlotillaDbContext context) + { + _context = context; + } + + public async Task> ReadAll() + { + return await GetAssets().ToListAsync(); + } + + private IQueryable GetAssets() + { + return _context.Assets; + } + + public async Task ReadById(string id) + { + return await GetAssets() + .FirstOrDefaultAsync(a => a.Id.Equals(id)); + } + + public async Task ReadByName(string assetCode) + { + if (assetCode == null) + return null; + return await _context.Assets.Where(a => + a.AssetCode.ToLower().Equals(assetCode.ToLower()) + ).FirstOrDefaultAsync(); + } + + public async Task Create(CreateAssetQuery newAssetQuery) + { + var asset = await ReadByName(newAssetQuery.AssetCode); + if (asset == null) + { + asset = new Asset + { + Name = newAssetQuery.Name, + AssetCode = newAssetQuery.AssetCode + }; + await _context.Assets.AddAsync(asset); + await _context.SaveChangesAsync(); + } + + return asset!; + } + + public async Task Update(Asset asset) + { + var entry = _context.Update(asset); + await _context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task Delete(string id) + { + var asset = await GetAssets() + .FirstOrDefaultAsync(ev => ev.Id.Equals(id)); + if (asset is null) + { + return null; + } + + _context.Assets.Remove(asset); + await _context.SaveChangesAsync(); + + return asset; + } + } +} diff --git a/backend/api/Services/CustomMissionService.cs b/backend/api/Services/CustomMissionService.cs new file mode 100644 index 000000000..d3cbc0ea1 --- /dev/null +++ b/backend/api/Services/CustomMissionService.cs @@ -0,0 +1,59 @@ + + +using System.Text.Json; +using Api.Database.Models; +using Api.Options; +using Microsoft.Extensions.Options; + +namespace Api.Services +{ + + public interface ICustomMissionService + { + Task 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 async Task UploadFile(string fileName, Stream fileStream) + { + throw new NotImplementedException(); + } + + public async Task 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..fe27d1280 --- /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> ReadByAsset(string assetCode); + + public abstract Task ReadByName(string deckName); + + public abstract Task ReadByAssetAndInstallationAndName(Asset asset, Installation installation, 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 IAssetService _assetService; + private readonly IInstallationService _installationService; + + public DeckService(FlotillaDbContext context, IAssetService assetService, IInstallationService installationService) + { + _context = context; + _assetService = assetService; + _installationService = installationService; + } + + 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> ReadByAsset(string assetCode) + { + var asset = await _assetService.ReadByName(assetCode); + if (asset == null) + return new List(); + return await _context.Decks.Where(a => + a.Asset.Id.Equals(asset.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 ReadByAssetAndInstallationAndName(Asset asset, Installation installation, string name) + { + return await _context.Decks.Where(a => + a.Installation.Id.Equals(installation.Id) && + a.Asset.Id.Equals(asset.Id) && + a.Name.ToLower().Equals(name.ToLower()) + ).Include(d => d.Installation).Include(i => i.Asset).FirstOrDefaultAsync(); + } + + public async Task Create(CreateDeckQuery newDeckQuery) + { + var asset = await _assetService.ReadByName(newDeckQuery.AssetCode); + if (asset == null) + { + throw new AssetNotFoundException($"No asset with name {newDeckQuery.AssetCode} could be found"); + } + var installation = await _installationService.ReadByAssetAndName(asset, newDeckQuery.InstallationCode); + if (installation == null) + { + throw new InstallationNotFoundException($"No installation with name {newDeckQuery.InstallationCode} could be found"); + } + var deck = await ReadByAssetAndInstallationAndName(asset, installation, newDeckQuery.Name); + if (deck == null) + { + deck = new Deck + { + Name = newDeckQuery.Name, + Asset = asset, + Installation = installation + }; + 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 5648be8c7..6530fce71 100644 --- a/backend/api/Services/EchoService.cs +++ b/backend/api/Services/EchoService.cs @@ -15,7 +15,6 @@ public interface IEchoService public abstract Task GetMissionById(int missionId); public abstract Task> GetEchoPlantInfos(); - public abstract Task GetRobotPoseFromPoseId(int poseId); } @@ -79,40 +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; - } - - public async Task GetMissionByPath(string relativePath) - { - var response = await _echoApi.CallWebApiForAppAsync( - ServiceName, - options => - { - options.HttpMethod = HttpMethod.Get; - options.RelativePath = relativePath; - } - ); - - response.EnsureSuccessStatusCode(); - - var echoMission = await response.Content.ReadFromJsonAsync(); - - if (echoMission is null) - throw new JsonException("Failed to deserialize mission from Echo"); - - var mission = ProcessEchoMission(echoMission); - if (mission == null) - { - throw new InvalidDataException($"EchoMission with relative path: {relativePath} is invalid."); - } - - return mission; + return processedEchoMission; } public async Task> GetEchoPlantInfos() @@ -207,13 +179,13 @@ private List ProcessAvailableEchoMission(List> ReadAll(); + + public abstract Task ReadById(string id); + + public abstract Task> ReadByAsset(string assetCode); + + public abstract Task ReadByAssetAndName(Asset asset, string installationCode); + + public abstract Task ReadByAssetAndName(string assetCode, string installationCode); + + 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; + private readonly IAssetService _assetService; + + public InstallationService(FlotillaDbContext context, IAssetService assetService) + { + _context = context; + _assetService = assetService; + } + + public async Task> ReadAll() + { + return await GetInstallations().ToListAsync(); + } + + private IQueryable GetInstallations() + { + return _context.Installations.Include(i => i.Asset); + } + + public async Task ReadById(string id) + { + return await GetInstallations() + .FirstOrDefaultAsync(a => a.Id.Equals(id)); + } + + public async Task> ReadByAsset(string assetCode) + { + var asset = await _assetService.ReadByName(assetCode); + if (asset == null) + return new List(); + return await _context.Installations.Where(a => + a.Asset.Id.Equals(asset.Id)).ToListAsync(); + } + + public async Task ReadByAssetAndName(Asset asset, string installationCode) + { + return await _context.Installations.Where(a => + a.InstallationCode.ToLower().Equals(installationCode.ToLower()) && + a.Asset.Id.Equals(asset.Id)).FirstOrDefaultAsync(); + } + + public async Task ReadByAssetAndName(string assetCode, string installationCode) + { + var asset = await _assetService.ReadByName(assetCode); + if (asset == null) + return null; + return await _context.Installations.Where(a => + a.Asset.Id.Equals(asset.Id) && + a.InstallationCode.ToLower().Equals(installationCode.ToLower()) + ).FirstOrDefaultAsync(); + } + + public async Task Create(CreateInstallationQuery newInstallationQuery) + { + var asset = await _assetService.ReadByName(newInstallationQuery.AssetCode); + if (asset == null) + { + throw new AssetNotFoundException($"No asset with name {newInstallationQuery.AssetCode} could be found"); + } + var installation = await ReadByAssetAndName(asset, newInstallationQuery.InstallationCode); + if (installation == null) + { + installation = new Installation + { + Name = newInstallationQuery.Name, + InstallationCode = newInstallationQuery.InstallationCode, + Asset = asset, + }; + 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 5c28fbc8b..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, MissionRun 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, MissionRun 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 0977dba10..00dc61f59 100644 --- a/backend/api/Services/MapService.cs +++ b/backend/api/Services/MapService.cs @@ -3,6 +3,7 @@ using Api.Options; using Azure.Storage.Blobs.Models; using Microsoft.Extensions.Options; + namespace Api.Services { public interface IMapService @@ -70,21 +71,21 @@ public async Task FetchMapImage(string mapName, string assetCode) return map; } - public async Task AssignMapToMission(MissionRun 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.AssetCode); } 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(MissionRun 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 index b05804c0e..486eb874a 100644 --- a/backend/api/Services/MissionDefinitionService.cs +++ b/backend/api/Services/MissionDefinitionService.cs @@ -12,13 +12,13 @@ namespace Api.Services { public interface IMissionDefinitionService { - public abstract Task Create(MissionDefinition mission); + public abstract Task Create(MissionDefinition missionDefinition); public abstract Task ReadById(string id); public abstract Task> ReadAll(MissionDefinitionQueryStringParameters parameters); - public abstract Task Update(MissionDefinition mission); + public abstract Task Update(MissionDefinition missionDefinition); public abstract Task Delete(string id); } @@ -47,35 +47,34 @@ public MissionDefinitionService(FlotillaDbContext context) _context = context; } - public async Task Create(MissionDefinition mission) + public async Task Create(MissionDefinition missionDefinition) { - await _context.MissionDefinitions.AddAsync(mission); + await _context.MissionDefinitions.AddAsync(missionDefinition); await _context.SaveChangesAsync(); - return mission; + return missionDefinition; } - private IQueryable GetMissionsWithSubModels() + private IQueryable GetMissionDefinitionsWithSubModels() { return _context.MissionDefinitions - .Include(mission => mission.Area) - .ThenInclude(robot => robot.Deck) - .ThenInclude(robot => robot.Installation) - .ThenInclude(robot => robot.Asset) - .Include(mission => mission.Source) - .Include(mission => mission.LastRun) - .ThenInclude(planTask => planTask == null ? null : planTask.StartTime); + .Include(missionDefinition => missionDefinition.Area) + .ThenInclude(area => area.Deck) + .ThenInclude(area => area.Installation) + .ThenInclude(area => area.Asset) + .Include(missionDefinition => missionDefinition.Source) + .Include(missionDefinition => missionDefinition.LastRun); } public async Task ReadById(string id) { - return await GetMissionsWithSubModels() - .FirstOrDefaultAsync(mission => mission.Id.Equals(id)); + return await GetMissionDefinitionsWithSubModels().Where(m => m.Deprecated == false) + .FirstOrDefaultAsync(missionDefinition => missionDefinition.Id.Equals(id)); } public async Task> ReadAll(MissionDefinitionQueryStringParameters parameters) { - var query = GetMissionsWithSubModels(); + var query = GetMissionDefinitionsWithSubModels().Where(m => m.Deprecated == false); var filter = ConstructFilter(parameters); query = query.Where(filter); @@ -91,9 +90,9 @@ public async Task> ReadAll(MissionDefinitionQuerySt ); } - public async Task Update(MissionDefinition mission) + public async Task Update(MissionDefinition missionDefinition) { - var entry = _context.Update(mission); + var entry = _context.Update(missionDefinition); await _context.SaveChangesAsync(); return entry.Entity; } @@ -101,26 +100,26 @@ public async Task Update(MissionDefinition mission) public async Task Delete(string id) { // We do not delete the source here as more than one mission definition may be using it - var mission = await ReadById(id); - if (mission is null) + var missionDefinition = await ReadById(id); + if (missionDefinition is null) { return null; } - _context.MissionDefinitions.Remove(mission); + missionDefinition.Deprecated = true; await _context.SaveChangesAsync(); - return mission; + return missionDefinition; } - private static void SearchByName(ref IQueryable missions, string? name) + private static void SearchByName(ref IQueryable missionDefinitions, string? name) { - if (!missions.Any() || string.IsNullOrWhiteSpace(name)) + if (!missionDefinitions.Any() || string.IsNullOrWhiteSpace(name)) return; - missions = missions.Where( - mission => - mission.Name != null && mission.Name.ToLower().Contains(name.Trim().ToLower()) + missionDefinitions = missionDefinitions.Where( + missionDefinition => + missionDefinition.Name != null && missionDefinition.Name.ToLower().Contains(name.Trim().ToLower()) ); } @@ -135,44 +134,44 @@ MissionDefinitionQueryStringParameters parameters ) { Expression> areaFilter = parameters.Area is null - ? mission => true - : mission => - mission.Area.Name.ToLower().Equals(parameters.Area.Trim().ToLower()); + ? missionDefinition => true + : missionDefinition => + missionDefinition.Area.Name.ToLower().Equals(parameters.Area.Trim().ToLower()); Expression> assetFilter = parameters.AssetCode is null - ? mission => true - : mission => - mission.AssetCode.ToLower().Equals(parameters.AssetCode.Trim().ToLower()); + ? missionDefinition => true + : missionDefinition => + missionDefinition.AssetCode.ToLower().Equals(parameters.AssetCode.Trim().ToLower()); Expression> missionTypeFilter = parameters.SourceType is null - ? mission => true - : mission => - mission.Source.Type.Equals(parameters.SourceType); + ? missionDefinition => true + : missionDefinition => + missionDefinition.Source.Type.Equals(parameters.SourceType); // The parameter of the filter expression - var mission = Expression.Parameter(typeof(MissionRun)); + 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(assetFilter, mission), + Expression.Invoke(assetFilter, missionRunExpression), Expression.AndAlso( - Expression.Invoke(areaFilter, mission), - Expression.Invoke(missionTypeFilter, mission) + Expression.Invoke(areaFilter, missionRunExpression), + Expression.Invoke(missionTypeFilter, missionRunExpression) ) ); // Constructing the resulting lambda expression by combining parameter and body - return Expression.Lambda>(body, mission); + return Expression.Lambda>(body, missionRunExpression); } - private static void ApplySort(ref IQueryable missions, string orderByQueryString) + private static void ApplySort(ref IQueryable missionDefinitions, string orderByQueryString) { - if (!missions.Any()) + if (!missionDefinitions.Any()) return; if (string.IsNullOrWhiteSpace(orderByQueryString)) { - missions = missions.OrderBy(x => x.Name); + missionDefinitions = missionDefinitions.OrderBy(x => x.Name); return; } @@ -216,9 +215,9 @@ private static void ApplySort(ref IQueryable missions, string string orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' '); - missions = string.IsNullOrWhiteSpace(orderQuery) - ? missions.OrderBy(mission => mission.Name) - : missions.OrderBy(orderQuery); + missionDefinitions = string.IsNullOrWhiteSpace(orderQuery) + ? missionDefinitions.OrderBy(missionDefinition => missionDefinition.Name) + : missionDefinitions.OrderBy(orderQuery); } } } diff --git a/backend/api/Services/MissionRunService.cs b/backend/api/Services/MissionRunService.cs index a5efe1b54..ce1d950ae 100644 --- a/backend/api/Services/MissionRunService.cs +++ b/backend/api/Services/MissionRunService.cs @@ -14,7 +14,7 @@ namespace Api.Services { public interface IMissionRunService { - public abstract Task Create(MissionRun mission); + public abstract Task Create(MissionRun missionRun); public abstract Task> ReadAll(MissionRunQueryStringParameters parameters); @@ -22,7 +22,7 @@ public interface IMissionRunService public abstract Task Update(MissionRun mission); - public abstract Task UpdateMissionStatusByIsarMissionId( + public abstract Task UpdateMissionRunStatusByIsarMissionId( string isarMissionId, MissionStatus missionStatus ); @@ -69,34 +69,34 @@ public MissionRunService(FlotillaDbContext context, ILogger l _logger = logger; } - private IQueryable GetMissionsWithSubModels() + private IQueryable GetMissionRunsWithSubModels() { return _context.MissionRuns - .Include(mission => mission.Area) - .ThenInclude(a => a.Deck) - .ThenInclude(d => d.Installation) - .ThenInclude(i => i.Asset) - .Include(mission => mission.Robot) + .Include(missionRun => missionRun.Area) + .ThenInclude(area => area.Deck) + .ThenInclude(deck => deck.Installation) + .ThenInclude(installation => installation.Asset) + .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(MissionRun mission) + public async Task Create(MissionRun missionRun) { - await _context.MissionRuns.AddAsync(mission); + await _context.MissionRuns.AddAsync(missionRun); await _context.SaveChangesAsync(); - return mission; + return missionRun; } public async Task> ReadAll(MissionRunQueryStringParameters parameters) { - var query = GetMissionsWithSubModels(); + var query = GetMissionRunsWithSubModels(); var filter = ConstructFilter(parameters); query = query.Where(filter); @@ -116,50 +116,50 @@ public async Task> ReadAll(MissionRunQueryStringParameters 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(MissionRun 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) { - 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.MissionRuns.Remove(mission); + _context.MissionRuns.Remove(missionRun); await _context.SaveChangesAsync(); - return mission; + return missionRun; } #region ISAR Specific methods 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", @@ -168,11 +168,11 @@ MissionStatus missionStatus return null; } - mission.Status = missionStatus; + missionRun.Status = missionStatus; await _context.SaveChangesAsync(); - return mission; + return missionRun; } public async Task UpdateTaskStatusByIsarTaskId( @@ -181,18 +181,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( @@ -203,12 +203,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 ) ) @@ -233,8 +233,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", @@ -244,7 +244,7 @@ IsarStepStatus stepStatus return false; } - var task = mission.GetTaskByIsarId(isarTaskId); + var task = missionRun.GetTaskByIsarId(isarTaskId); if (task is null) { _logger.LogWarning( @@ -274,35 +274,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()) @@ -321,26 +321,26 @@ MissionRunQueryStringParameters parameters ) { Expression> areaFilter = parameters.Area is null - ? mission => true - : mission => - mission.Area.Name.ToLower().Equals(parameters.Area.Trim().ToLower()); + ? missionRun => true + : missionRun => + missionRun.Area.Name.ToLower().Equals(parameters.Area.Trim().ToLower()); Expression> assetFilter = parameters.AssetCode is null - ? mission => true - : mission => - mission.AssetCode.ToLower().Equals(parameters.AssetCode.Trim().ToLower()); + ? missionRun => true + : missionRun => + missionRun.AssetCode.ToLower().Equals(parameters.AssetCode.Trim().ToLower()); 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); + ? 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); + ? missionRun => true + : missionRun => missionRun.Robot.Id.Equals(parameters.RobotId); Expression> inspectionTypeFilter = parameters.InspectionTypes is null ? mission => true @@ -353,20 +353,20 @@ MissionRunQueryStringParameters 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( @@ -375,29 +375,29 @@ MissionRunQueryStringParameters 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(MissionRun)); + 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(assetFilter, 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) ) ) ) @@ -407,17 +407,17 @@ MissionRunQueryStringParameters parameters ); // Constructing the resulting lambda expression by combining parameter and body - return Expression.Lambda>(body, mission); + return Expression.Lambda>(body, missionRun); } - private static void ApplySort(ref IQueryable missions, string orderByQueryString) + private static void ApplySort(ref IQueryable missionRuns, string orderByQueryString) { - if (!missions.Any()) + if (!missionRuns.Any()) return; if (string.IsNullOrWhiteSpace(orderByQueryString)) { - missions = missions.OrderBy(x => x.Name); + missionRuns = missionRuns.OrderBy(x => x.Name); return; } @@ -448,7 +448,7 @@ private static void ApplySort(ref IQueryable missions, string orderB if (objectProperty == null) throw new InvalidDataException( - $"Mission has no property '{propertyFromQueryName}' for ordering" + $"MissionRun has no property '{propertyFromQueryName}' for ordering" ); string sortingOrder = param.EndsWith(" desc", StringComparison.OrdinalIgnoreCase) @@ -461,9 +461,9 @@ private static void ApplySort(ref IQueryable missions, string orderB string orderQuery = orderQueryBuilder.ToString().TrimEnd(',', ' '); - missions = string.IsNullOrWhiteSpace(orderQuery) - ? missions.OrderBy(mission => mission.Name) - : missions.OrderBy(orderQuery); + missionRuns = string.IsNullOrWhiteSpace(orderQuery) + ? missionRuns.OrderBy(missionRun => missionRun.Name) + : missionRuns.OrderBy(orderQuery); } } } diff --git a/backend/api/Services/Models/IsarMissionDefinition.cs b/backend/api/Services/Models/IsarMissionDefinition.cs index d230f72e7..3d3232798 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(MissionRun 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, MissionRun mission) + public IsarTaskDefinition(MissionTask missionTask, MissionRun missionRun) { Id = missionTask.IsarTaskId; Pose = new IsarPose(missionTask.RobotPose); @@ -55,7 +55,7 @@ public IsarTaskDefinition(MissionTask missionTask, MissionRun 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, MissionRun 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.AssetCode }, + { "mission_name", missionRun.Name }, + { "status_reason", missionRun.StatusReason } }; Metadata = metadata; } diff --git a/backend/api/Services/SortingService.cs b/backend/api/Services/SortingService.cs new file mode 100644 index 000000000..041d54e95 --- /dev/null +++ b/backend/api/Services/SortingService.cs @@ -0,0 +1,65 @@ +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/Services/SourceService.cs b/backend/api/Services/SourceService.cs deleted file mode 100644 index ef158c85b..000000000 --- a/backend/api/Services/SourceService.cs +++ /dev/null @@ -1,96 +0,0 @@ - - -using System.Text.Json; -using Api.Database.Models; -using Api.Options; -using Azure.Storage.Blobs; -using Microsoft.Extensions.Options; - -namespace Api.Services -{ - - public interface ISourceService - { - Task UploadSource(string id, List tasks); - List? GetMissionTasksFromMissionId(string id); - List? GetMissionTasksFromURL(string url); - } - - public class SourceService : ISourceService - { - private readonly IOptions _storageOptions; - - public SourceService(IOptions storageOptions) - { - _storageOptions = storageOptions; - } - - public async Task CreateContainer(string containerName) - { - var blobServiceClient = new BlobServiceClient(_storageOptions.Value.ConnectionString); - var containerClient = blobServiceClient.GetBlobContainerClient(containerName); - await containerClient.CreateIfNotExistsAsync(); - - return true; - } - - public async Task UploadFile(string fileName, Stream fileStream) - { - var blobServiceClient = new BlobServiceClient(_storageOptions.Value.ConnectionString); - var containerClient = blobServiceClient.GetBlobContainerClient(_storageOptions.Value.CustomMissionContainerName); - containerClient.CreateIfNotExists(); - - var blobClient = containerClient.GetBlobClient(fileName); - _ = await blobClient.UploadAsync(fileStream, true); - //var hash = $"0x{BitConverter.ToString(blobProperties.Value.ContentHash).Replace("-", string.Empty)}"; - return blobClient.Uri; - } - - public Task UploadSource(string id, List tasks) - { - var memoryStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(tasks))); - - var taskUri = UploadFile(id, memoryStream); - - return taskUri; - } - - public List? GetMissionTasksFromMissionId(string id) - { - var blobServiceClient = new BlobServiceClient(_storageOptions.Value.ConnectionString); - var containerClient = blobServiceClient.GetBlobContainerClient(_storageOptions.Value.CustomMissionContainerName); - containerClient.CreateIfNotExists(); - - var blobClient = containerClient.GetBlobClient(id); - - List? content; - try - { - content = blobClient.DownloadContent().Value.Content.ToObjectFromJson>(); - } - catch (Exception) - { - return null; - } - - return content; - } - - public List? GetMissionTasksFromURL(string url) - { - var blobClient = new BlobClient(new Uri(url)); - - List? content; - try - { - content = blobClient.DownloadContent().Value.Content.ToObjectFromJson>(); - } - catch (Exception) - { - return null; - } - - return content; - } - } -} diff --git a/backend/api/Utilities/Exceptions.cs b/backend/api/Utilities/Exceptions.cs index 09a053d17..731b9bf15 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 AssetNotFoundException : Exception + { + public AssetNotFoundException(string message) : base(message) { } + } + + public class InstallationNotFoundException : Exception + { + public InstallationNotFoundException(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/appsettings.Development.json b/backend/api/appsettings.Development.json index 47f239155..79ba4ee57 100644 --- a/backend/api/appsettings.Development.json +++ b/backend/api/appsettings.Development.json @@ -37,7 +37,7 @@ }, "Blob": { "CustomMissionContainerName": "custommission", - "ConnectionString": "" + "AccountName": "" }, "Database": { "UseInMemoryDatabase": true diff --git a/backend/api/appsettings.Production.json b/backend/api/appsettings.Production.json index 18688b217..06675d568 100644 --- a/backend/api/appsettings.Production.json +++ b/backend/api/appsettings.Production.json @@ -17,7 +17,7 @@ "https://*.equinor.com/" ], "Blob": { - "CustomMissionContainerName": "", + "CustomMissionContainerName": "custommission", "AccountName": "" }, "Mqtt": { diff --git a/backend/api/appsettings.Staging.json b/backend/api/appsettings.Staging.json index cf8e20187..4bb33df4f 100644 --- a/backend/api/appsettings.Staging.json +++ b/backend/api/appsettings.Staging.json @@ -19,7 +19,7 @@ "https://localhost:3001" ], "Blob": { - "CustomMissionContainerName": "", + "CustomMissionContainerName": "custommission", "AccountName": "" }, "Mqtt": { diff --git a/backend/api/appsettings.Test.json b/backend/api/appsettings.Test.json index 1fb3dab01..ca2dd4e60 100644 --- a/backend/api/appsettings.Test.json +++ b/backend/api/appsettings.Test.json @@ -12,7 +12,7 @@ "https://localhost:3001" ], "Blob": { - "CustomMissionContainerName": "", + "CustomMissionContainerName": "custommission", "AccountName": "" }, "Mqtt": { diff --git a/frontend/src/api/ApiCaller.tsx b/frontend/src/api/ApiCaller.tsx index 330c5bc1d..8f308229e 100644 --- a/frontend/src/api/ApiCaller.tsx +++ b/frontend/src/api/ApiCaller.tsx @@ -8,7 +8,7 @@ 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' @@ -226,8 +226,8 @@ export class BackendAPICaller { return result.content } - static async getMissionById(missionId: string): Promise { - const path: string = 'missions/' + missionId + 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 @@ -261,7 +261,7 @@ export class BackendAPICaller { echoMissionId: echoMissionId, desiredStartTime: new Date(), assetCode: assetCode, - areaName: '', + 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) @@ -341,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 @@ -360,7 +360,7 @@ 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 diff --git a/frontend/src/components/Pages/FrontPage/MissionOverview/MissionQueueView.tsx b/frontend/src/components/Pages/FrontPage/MissionOverview/MissionQueueView.tsx index 8cc0af54f..31a48a872 100644 --- a/frontend/src/components/Pages/FrontPage/MissionOverview/MissionQueueView.tsx +++ b/frontend/src/components/Pages/FrontPage/MissionOverview/MissionQueueView.tsx @@ -86,6 +86,7 @@ export function MissionQueueView({ refreshInterval }: RefreshProps) { if (selectedRobot === undefined) return selectedEchoMissions.forEach((mission: MissionDefinition) => { + // TODO: as a final parameter here we likely want mission.AreaName, and maybe also installation and deck codes BackendAPICaller.postMission(mission.echoMissionId, selectedRobot.id, assetCode) }) diff --git a/frontend/src/components/Pages/MissionPage/MissionPage.tsx b/frontend/src/components/Pages/MissionPage/MissionPage.tsx index 435f5842f..2f7807c34 100644 --- a/frontend/src/components/Pages/MissionPage/MissionPage.tsx +++ b/frontend/src/components/Pages/MissionPage/MissionPage.tsx @@ -40,7 +40,7 @@ export function MissionPage() { useEffect(() => { if (missionId) { - BackendAPICaller.getMissionById(missionId).then((mission) => { + BackendAPICaller.getMissionRunById(missionId).then((mission) => { setSelectedMission(mission) updateVideoStreams(mission) }) @@ -51,7 +51,7 @@ export function MissionPage() { const timeDelay = 1000 const id = setInterval(() => { if (missionId) { - BackendAPICaller.getMissionById(missionId).then((mission) => { + BackendAPICaller.getMissionRunById(missionId).then((mission) => { setSelectedMission(mission) }) } diff --git a/frontend/src/components/Pages/RobotPage/AssetDeckMapView.tsx b/frontend/src/components/Pages/RobotPage/AreaMapView.tsx similarity index 92% rename from frontend/src/components/Pages/RobotPage/AssetDeckMapView.tsx rename to frontend/src/components/Pages/RobotPage/AreaMapView.tsx index 8bb8be121..ba8ea83b6 100644 --- a/frontend/src/components/Pages/RobotPage/AssetDeckMapView.tsx +++ b/frontend/src/components/Pages/RobotPage/AreaMapView.tsx @@ -6,14 +6,14 @@ import { useErrorHandler } from 'react-error-boundary' import { PlaceRobotInMap, InverseCalculatePixelPosition } from '../../../utils/MapMarkers' import { BackendAPICaller } from 'api/ApiCaller' import { MapMetadata } from 'models/MapMetadata' -import { AssetDeck } from 'models/AssetDeck' +import { Area } from 'models/Area' import { Position } from 'models/Position' import { Pose } from 'models/Pose' import { TranslateText } from 'components/Contexts/LanguageContext' - import { MapCompass } from 'utils/MapCompass' -interface AssetDeckProps { - assetDeck: AssetDeck + +interface AreaProps { + area: Area localizationPose: Pose setLocalizationPose: (newPose: Pose) => void } @@ -44,7 +44,7 @@ const StyledMapCompass = styled.div` align-items: end; ` -export function AssetDeckMapView({ assetDeck, localizationPose, setLocalizationPose }: AssetDeckProps) { +export function AreaMapView({ area, localizationPose, setLocalizationPose }: AreaProps) { const handleError = useErrorHandler() const [mapCanvas, setMapCanvas] = useState(document.createElement('canvas')) const [mapImage, setMapImage] = useState(document.createElement('img')) @@ -75,10 +75,10 @@ export function AssetDeckMapView({ assetDeck, localizationPose, setLocalizationP useEffect(() => { setIsLoading(true) setImageObjectURL(undefined) - BackendAPICaller.getAssetDeckMapMetadata(assetDeck.id) + BackendAPICaller.getAreasMapMetadata(area.id) .then((mapMetadata) => { setMapMetadata(mapMetadata) - BackendAPICaller.getMap(assetDeck.assetCode, mapMetadata.mapName) + BackendAPICaller.getMap(area.assetCode, mapMetadata.mapName) .then((imageBlob) => { setImageObjectURL(URL.createObjectURL(imageBlob)) }) @@ -91,7 +91,7 @@ export function AssetDeckMapView({ assetDeck, localizationPose, setLocalizationP setImageObjectURL(NoMap) }) //.catch((e) => handleError(e)) - }, [assetDeck]) + }, [area]) useEffect(() => { if (!imageObjectURL) { @@ -139,7 +139,7 @@ export function AssetDeckMapView({ assetDeck, localizationPose, setLocalizationP return } const assetPosition = InverseCalculatePixelPosition(mapMetadata, pixelPosition) - let newPose: Pose = assetDeck.defaultLocalizationPose + let newPose: Pose = area.defaultLocalizationPose newPose.position.x = assetPosition[0] newPose.position.y = assetPosition[1] setLocalizationPose(newPose) diff --git a/frontend/src/components/Pages/RobotPage/LocalizationDialog.tsx b/frontend/src/components/Pages/RobotPage/LocalizationDialog.tsx index b38a3633a..5d977126c 100644 --- a/frontend/src/components/Pages/RobotPage/LocalizationDialog.tsx +++ b/frontend/src/components/Pages/RobotPage/LocalizationDialog.tsx @@ -4,9 +4,9 @@ import { useLanguageContext } from 'components/Contexts/LanguageContext' import { Icons } from 'utils/icons' import { useState, useEffect } from 'react' import { BackendAPICaller } from 'api/ApiCaller' -import { AssetDeck } from 'models/AssetDeck' +import { Area } from 'models/Area' import { Robot } from 'models/Robot' -import { AssetDeckMapView } from './AssetDeckMapView' +import { AreaMapView } from './AreaMapView' import { Pose } from 'models/Pose' import { Orientation } from 'models/Orientation' import { Mission, MissionStatus } from 'models/Mission' @@ -51,8 +51,8 @@ interface RobotProps { export const LocalizationDialog = ({ robot }: RobotProps): JSX.Element => { const [isLocalizationDialogOpen, setIsLocalizationDialogOpen] = useState(false) const [missionLocalizationStatus, setMissionLocalizationInfo] = useState() - const [selectedAssetDeck, setSelectedAssetDeck] = useState() - const [assetDecks, setAssetDecks] = useState() + const [selectedArea, setSelectedArea] = useState() + const [areas, setAreas] = useState() const [localizationPose, setLocalizationPose] = useState() const [selectedDirection, setSelectedDirecion] = useState() const [localizing, setLocalising] = useState(false) @@ -69,19 +69,19 @@ export const LocalizationDialog = ({ robot }: RobotProps): JSX.Element => { ]) useEffect(() => { - BackendAPICaller.getAssetDecks().then((response: AssetDeck[]) => { - setAssetDecks(response) + BackendAPICaller.getAreas().then((response: Area[]) => { + setAreas(response) }) }, []) useEffect(() => { - if (selectedAssetDeck && localizationPose && localizing) { - BackendAPICaller.postLocalizationMission(localizationPose, robot.id, selectedAssetDeck.id) + if (selectedArea && localizationPose && localizing) { + BackendAPICaller.postLocalizationMission(localizationPose, robot.id, selectedArea.id) .then((result: unknown) => result as Mission) .then(async (mission: Mission) => { - BackendAPICaller.getMissionById(mission.id) + BackendAPICaller.getMissionRunById(mission.id) while (mission.status == MissionStatus.Ongoing || mission.status == MissionStatus.Pending) { - mission = await BackendAPICaller.getMissionById(mission.id) + mission = await BackendAPICaller.getMissionRunById(mission.id) } setLocalising(false) return mission @@ -94,19 +94,18 @@ export const LocalizationDialog = ({ robot }: RobotProps): JSX.Element => { } }, [localizing]) - const getAssetDeckNames = (assetDecks: AssetDeck[]): Map => { - var assetDeckNameMap = new Map() - assetDecks.forEach((assetDeck: AssetDeck) => { - assetDeckNameMap.set(assetDeck.deckName, assetDeck) - }) - return assetDeckNameMap + const getAreaNames = (areas: Area[]): Map => { + var areaNameMap = new Map() + areas.forEach((area: Area) => + areaNameMap.set(area.deckName, area)) + return areaNameMap } const onSelectedDeck = (changes: AutocompleteChanges) => { const selectedDeckName = changes.selectedItems[0] - const selectedAssetDeck = assetDecks?.find((assetDeck) => assetDeck.deckName === selectedDeckName) - setSelectedAssetDeck(selectedAssetDeck) - let newPose = selectedAssetDeck?.defaultLocalizationPose + const selectedArea = areas?.find((area) => area.deckName === selectedDeckName) + setSelectedArea(selectedArea) + let newPose = selectedArea?.defaultLocalizationPose if (newPose && selectedDirection) { newPose.orientation = selectedDirection } @@ -129,7 +128,7 @@ export const LocalizationDialog = ({ robot }: RobotProps): JSX.Element => { const onLocalizationDialogClose = () => { setIsLocalizationDialogOpen(false) - setSelectedAssetDeck(undefined) + setSelectedArea(undefined) } const onClickLocalize = async () => { @@ -137,7 +136,7 @@ export const LocalizationDialog = ({ robot }: RobotProps): JSX.Element => { setLocalising(true) } - const assetDeckNames = assetDecks ? Array.from(getAssetDeckNames(assetDecks).keys()).sort() : [] + const areaNames = areas ? Array.from(getAreaNames(areas).keys()).sort() : [] return ( <> @@ -190,7 +189,7 @@ export const LocalizationDialog = ({ robot }: RobotProps): JSX.Element => { {translate('Localize robot')} @@ -200,9 +199,9 @@ export const LocalizationDialog = ({ robot }: RobotProps): JSX.Element => { onOptionsChange={onSelectedDirection} /> - {selectedAssetDeck && localizationPose && ( - @@ -218,7 +217,7 @@ export const LocalizationDialog = ({ robot }: RobotProps): JSX.Element => { {' '} {translate('Cancel')}{' '} - diff --git a/frontend/src/models/AssetDeck.ts b/frontend/src/models/Area.ts similarity index 62% rename from frontend/src/models/AssetDeck.ts rename to frontend/src/models/Area.ts index 1758186ae..ff3c21e57 100644 --- a/frontend/src/models/AssetDeck.ts +++ b/frontend/src/models/Area.ts @@ -1,7 +1,9 @@ import { Pose } from './Pose' -export interface AssetDeck { +export interface Area { id: string + areaName: string + installationCode: string assetCode: string deckName: string defaultLocalizationPose: Pose diff --git a/frontend/src/models/Robot.ts b/frontend/src/models/Robot.ts index f76674974..e62037196 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' @@ -27,5 +27,5 @@ export interface Robot { port?: number videoStreams?: VideoStream[] isarUri?: string - currentAssetDeck?: AssetDeck + currentArea?: Area }