From 1ea95be94f5f6906dc35c12d9f14269451e91dae Mon Sep 17 00:00:00 2001 From: Eddasol Date: Thu, 30 Jan 2025 08:22:26 +0100 Subject: [PATCH] Improve estimate for remainding time --- .../MissionSchedulingController.cs | 10 +- .../Controllers/Models/MissionRunResponse.cs | 4 +- backend/api/Database/Models/MissionRun.cs | 39 +- ...081334_RenameEstimatedDuration.Designer.cs | 1264 +++++++++++++++++ .../20250130081334_RenameEstimatedDuration.cs | 28 + .../FlotillaDbContextModelSnapshot.cs | 2 +- .../Services/Models/IsarMissionDefinition.cs | 4 - .../MissionOverview/MissionQueueCard.tsx | 16 +- .../MissionHeader/MissionHeader.tsx | 15 +- frontend/src/models/Mission.ts | 2 +- .../src/utils/CalculateRemaingingTime.tsx | 19 + 11 files changed, 1340 insertions(+), 63 deletions(-) create mode 100644 backend/api/Migrations/20250130081334_RenameEstimatedDuration.Designer.cs create mode 100644 backend/api/Migrations/20250130081334_RenameEstimatedDuration.cs create mode 100644 frontend/src/utils/CalculateRemaingingTime.tsx diff --git a/backend/api/Controllers/MissionSchedulingController.cs b/backend/api/Controllers/MissionSchedulingController.cs index 34174744..dfd3f6f3 100644 --- a/backend/api/Controllers/MissionSchedulingController.cs +++ b/backend/api/Controllers/MissionSchedulingController.cs @@ -100,7 +100,7 @@ [FromBody] ScheduleMissionQuery scheduledMissionQuery if (newMissionRun.Tasks.Any()) { - newMissionRun.CalculateEstimatedDuration(); + newMissionRun.SetEstimatedTaskDuration(); } // Compare with GetTasksFromSource @@ -221,7 +221,7 @@ await localizationService.EnsureRobotIsOnSameInstallationAsMission( if (missionRun.Tasks.Any()) { - missionRun.CalculateEstimatedDuration(); + missionRun.SetEstimatedTaskDuration(); } MissionRun newMissionRun; @@ -428,7 +428,7 @@ [.. missionInspectionAreaNames] if (missionRun.Tasks.Any()) { - missionRun.CalculateEstimatedDuration(); + missionRun.SetEstimatedTaskDuration(); } if (existingMissionDefinition == null) @@ -623,7 +623,7 @@ await localizationService.EnsureRobotIsOnSameInstallationAsMission( if (scheduledMission.Tasks.Any()) { - scheduledMission.CalculateEstimatedDuration(); + scheduledMission.SetEstimatedTaskDuration(); } else if ( scheduledMission.Robot.CurrentInspectionArea != null @@ -633,7 +633,7 @@ await localizationService.EnsureRobotIsOnSameInstallationAsMission( ) ) { - scheduledMission.CalculateEstimatedDuration(); + scheduledMission.SetEstimatedTaskDuration(); } if ( diff --git a/backend/api/Controllers/Models/MissionRunResponse.cs b/backend/api/Controllers/Models/MissionRunResponse.cs index d6cecdb9..ab7eadaf 100644 --- a/backend/api/Controllers/Models/MissionRunResponse.cs +++ b/backend/api/Controllers/Models/MissionRunResponse.cs @@ -35,7 +35,7 @@ public class MissionRunResponse public DateTime? EndTime { get; private set; } - public uint? EstimatedDuration { get; set; } + public uint? EstimatedTaskDuration { get; set; } public IList Tasks { get; set; } @@ -67,7 +67,7 @@ public MissionRunResponse(MissionRun mission) DesiredStartTime = mission.DesiredStartTime; StartTime = mission.StartTime; EndTime = mission.EndTime; - EstimatedDuration = mission.EstimatedDuration; + EstimatedTaskDuration = mission.EstimatedTaskDuration; Tasks = mission.Tasks; MissionRunType = mission.MissionRunType; } diff --git a/backend/api/Database/Models/MissionRun.cs b/backend/api/Database/Models/MissionRun.cs index 31284251..4af3c8a7 100644 --- a/backend/api/Database/Models/MissionRun.cs +++ b/backend/api/Database/Models/MissionRun.cs @@ -84,9 +84,9 @@ or MissionStatus.PartiallySuccessful public DateTime? EndTime { get; private set; } /// - /// The estimated duration of the mission in seconds + /// The estimated duration of each task in the mission in seconds /// - public uint? EstimatedDuration { get; set; } + public uint? EstimatedTaskDuration { get; set; } [Required] [MaxLength(200)] @@ -129,42 +129,11 @@ public static MissionStatus GetMissionStatusFromString(string status) }; } - public void CalculateEstimatedDuration() + public void SetEstimatedTaskDuration() { if (Robot.Model.AverageDurationPerTag is not null) { - float totalInspectionDuration = Tasks.Sum(task => - task.Inspection?.VideoDuration ?? 0 - ); - EstimatedDuration = (uint)( - (Robot.Model.AverageDurationPerTag * Tasks.Count) + totalInspectionDuration - ); - } - else - { - const double RobotVelocity = 1.5 * 1000 / 60; // km/t => m/min - const double EfficiencyFactor = 0.20; - const double InspectionTime = 2; // min/tag - const int AssumedXyMetersFromFirst = 20; - - double distance = 0; - var prevPosition = new Position( - Tasks.First().RobotPose.Position.X + AssumedXyMetersFromFirst, - Tasks.First().RobotPose.Position.Y + AssumedXyMetersFromFirst, - Tasks.First().RobotPose.Position.Z - ); - foreach (var task in Tasks) - { - var currentPosition = task.RobotPose.Position; - distance += - Math.Abs(currentPosition.X - prevPosition.X) - + Math.Abs(currentPosition.Y - prevPosition.Y); - prevPosition = currentPosition; - } - int estimate = (int)( - (distance / (RobotVelocity * EfficiencyFactor)) + InspectionTime - ); - EstimatedDuration = (uint)estimate * 60; + EstimatedTaskDuration = (uint)Robot.Model.AverageDurationPerTag; } } diff --git a/backend/api/Migrations/20250130081334_RenameEstimatedDuration.Designer.cs b/backend/api/Migrations/20250130081334_RenameEstimatedDuration.Designer.cs new file mode 100644 index 00000000..2c671045 --- /dev/null +++ b/backend/api/Migrations/20250130081334_RenameEstimatedDuration.Designer.cs @@ -0,0 +1,1264 @@ +// +using System; +using Api.Database.Context; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Api.Migrations +{ + [DbContext(typeof(FlotillaDbContext))] + [Migration("20250130081334_RenameEstimatedDuration")] + partial class RenameEstimatedDuration + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.11") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Api.Database.Models.AccessRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AccessLevel") + .IsRequired() + .HasColumnType("text"); + + b.Property("InstallationId") + .HasColumnType("text"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId"); + + b.ToTable("AccessRoles"); + }); + + modelBuilder.Entity("Api.Database.Models.Area", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("DefaultLocalizationPoseId") + .HasColumnType("text"); + + b.Property("InspectionAreaId") + .IsRequired() + .HasColumnType("text"); + + b.Property("InstallationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PlantId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DefaultLocalizationPoseId"); + + b.HasIndex("InspectionAreaId"); + + b.HasIndex("InstallationId"); + + b.HasIndex("PlantId"); + + b.ToTable("Areas"); + }); + + modelBuilder.Entity("Api.Database.Models.DefaultLocalizationPose", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("DockingEnabled") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("DefaultLocalizationPoses"); + }); + + modelBuilder.Entity("Api.Database.Models.Inspection", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AnalysisType") + .HasColumnType("text"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("InspectionTargetName") + .HasColumnType("text"); + + b.Property("InspectionType") + .IsRequired() + .HasColumnType("text"); + + b.Property("InspectionUrl") + .HasMaxLength(250) + .HasColumnType("character varying(250)"); + + b.Property("IsarInspectionId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsarTaskId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("VideoDuration") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.ToTable("Inspections"); + }); + + modelBuilder.Entity("Api.Database.Models.InspectionArea", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("DefaultLocalizationPoseId") + .HasColumnType("text"); + + b.Property("InstallationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PlantId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DefaultLocalizationPoseId"); + + b.HasIndex("InstallationId"); + + b.HasIndex("PlantId"); + + b.ToTable("InspectionAreas"); + }); + + modelBuilder.Entity("Api.Database.Models.InspectionFinding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("Finding") + .IsRequired() + .HasColumnType("text"); + + b.Property("InspectionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InspectionId") + .HasColumnType("text"); + + b.Property("IsarTaskId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InspectionId"); + + b.ToTable("InspectionFindings"); + }); + + modelBuilder.Entity("Api.Database.Models.Installation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("InstallationCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("InstallationCode") + .IsUnique(); + + b.ToTable("Installations"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("InspectionAreaId") + .HasColumnType("text"); + + b.Property("InspectionFrequency") + .HasColumnType("bigint"); + + b.Property("InstallationCode") + .IsRequired() + .HasColumnType("text"); + + b.Property("IsDeprecated") + .HasColumnType("boolean"); + + b.Property("LastSuccessfulRunId") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SourceId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InspectionAreaId"); + + b.HasIndex("LastSuccessfulRunId"); + + b.HasIndex("SourceId"); + + b.ToTable("MissionDefinitions"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("Comment") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Description") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("DesiredStartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("EstimatedTaskDuration") + .HasColumnType("bigint"); + + b.Property("InspectionAreaId") + .HasColumnType("text"); + + b.Property("InstallationCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsDeprecated") + .HasColumnType("boolean"); + + b.Property("IsarMissionId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MissionId") + .HasColumnType("text"); + + b.Property("MissionRunType") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RobotId") + .IsRequired() + .HasColumnType("text"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("StatusReason") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.HasKey("Id"); + + b.HasIndex("InspectionAreaId"); + + b.HasIndex("RobotId"); + + b.ToTable("MissionRuns"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionTask", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone"); + + b.Property("InspectionId") + .HasColumnType("text"); + + b.Property("IsarTaskId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MissionRunId") + .HasColumnType("text"); + + b.Property("PoseId") + .HasColumnType("integer"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.Property("TagId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TagLink") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("TaskOrder") + .HasColumnType("integer"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InspectionId"); + + b.HasIndex("MissionRunId"); + + b.ToTable("MissionTasks"); + }); + + modelBuilder.Entity("Api.Database.Models.Plant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("InstallationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("PlantCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId"); + + b.HasIndex("PlantCode") + .IsUnique(); + + b.ToTable("Plants"); + }); + + modelBuilder.Entity("Api.Database.Models.Robot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("BatteryLevel") + .HasColumnType("real"); + + b.Property("BatteryState") + .HasColumnType("text"); + + b.Property("CurrentInspectionAreaId") + .HasColumnType("text"); + + b.Property("CurrentInstallationId") + .IsRequired() + .HasColumnType("text"); + + b.Property("CurrentMissionId") + .HasColumnType("text"); + + b.Property("Deprecated") + .HasColumnType("boolean"); + + b.Property("FlotillaStatus") + .IsRequired() + .HasColumnType("text"); + + b.Property("Host") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsarConnected") + .HasColumnType("boolean"); + + b.Property("IsarId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("MissionQueueFrozen") + .HasColumnType("boolean"); + + b.Property("ModelId") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Port") + .HasColumnType("integer"); + + b.Property("PressureLevel") + .HasColumnType("real"); + + b.Property("RobotCapabilities") + .HasColumnType("text"); + + b.Property("SerialNumber") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CurrentInspectionAreaId"); + + b.HasIndex("CurrentInstallationId"); + + b.HasIndex("ModelId"); + + b.ToTable("Robots"); + }); + + modelBuilder.Entity("Api.Database.Models.RobotModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("AverageDurationPerTag") + .HasColumnType("real"); + + b.Property("BatteryMissionStartThreshold") + .HasColumnType("real"); + + b.Property("BatteryWarningThreshold") + .HasColumnType("real"); + + b.Property("LowerPressureWarningThreshold") + .HasColumnType("real"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpperPressureWarningThreshold") + .HasColumnType("real"); + + b.HasKey("Id"); + + b.HasIndex("Type") + .IsUnique(); + + b.ToTable("RobotModels"); + }); + + modelBuilder.Entity("Api.Database.Models.Source", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("CustomMissionTasks") + .HasColumnType("text"); + + b.Property("SourceId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Sources"); + }); + + modelBuilder.Entity("Api.Database.Models.UserInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b.Property("Oid") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("UserInfos"); + }); + + modelBuilder.Entity("Api.Services.MissionLoaders.TagInspectionMetadata", b => + { + b.Property("TagId") + .HasColumnType("text"); + + b.HasKey("TagId"); + + b.ToTable("TagInspectionMetadata"); + }); + + modelBuilder.Entity("Api.Database.Models.AccessRole", b => + { + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId"); + + b.Navigation("Installation"); + }); + + modelBuilder.Entity("Api.Database.Models.Area", b => + { + b.HasOne("Api.Database.Models.DefaultLocalizationPose", "DefaultLocalizationPose") + .WithMany() + .HasForeignKey("DefaultLocalizationPoseId"); + + b.HasOne("Api.Database.Models.InspectionArea", "InspectionArea") + .WithMany() + .HasForeignKey("InspectionAreaId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Api.Database.Models.Plant", "Plant") + .WithMany() + .HasForeignKey("PlantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.OwnsOne("Api.Database.Models.MapMetadata", "MapMetadata", b1 => + { + b1.Property("AreaId") + .HasColumnType("text"); + + b1.Property("MapName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.HasKey("AreaId"); + + b1.ToTable("Areas"); + + b1.WithOwner() + .HasForeignKey("AreaId"); + + b1.OwnsOne("Api.Database.Models.Boundary", "Boundary", b2 => + { + b2.Property("MapMetadataAreaId") + .HasColumnType("text"); + + b2.Property("X1") + .HasColumnType("double precision"); + + b2.Property("X2") + .HasColumnType("double precision"); + + b2.Property("Y1") + .HasColumnType("double precision"); + + b2.Property("Y2") + .HasColumnType("double precision"); + + b2.Property("Z1") + .HasColumnType("double precision"); + + b2.Property("Z2") + .HasColumnType("double precision"); + + b2.HasKey("MapMetadataAreaId"); + + b2.ToTable("Areas"); + + b2.WithOwner() + .HasForeignKey("MapMetadataAreaId"); + }); + + b1.OwnsOne("Api.Database.Models.TransformationMatrices", "TransformationMatrices", b2 => + { + b2.Property("MapMetadataAreaId") + .HasColumnType("text"); + + b2.Property("C1") + .HasColumnType("double precision"); + + b2.Property("C2") + .HasColumnType("double precision"); + + b2.Property("D1") + .HasColumnType("double precision"); + + b2.Property("D2") + .HasColumnType("double precision"); + + b2.HasKey("MapMetadataAreaId"); + + b2.ToTable("Areas"); + + b2.WithOwner() + .HasForeignKey("MapMetadataAreaId"); + }); + + b1.Navigation("Boundary") + .IsRequired(); + + b1.Navigation("TransformationMatrices") + .IsRequired(); + }); + + b.Navigation("DefaultLocalizationPose"); + + b.Navigation("InspectionArea"); + + b.Navigation("Installation"); + + b.Navigation("MapMetadata") + .IsRequired(); + + b.Navigation("Plant"); + }); + + modelBuilder.Entity("Api.Database.Models.DefaultLocalizationPose", b => + { + b.OwnsOne("Api.Database.Models.Pose", "Pose", b1 => + { + b1.Property("DefaultLocalizationPoseId") + .HasColumnType("text"); + + b1.HasKey("DefaultLocalizationPoseId"); + + b1.ToTable("DefaultLocalizationPoses"); + + b1.WithOwner() + .HasForeignKey("DefaultLocalizationPoseId"); + + b1.OwnsOne("Api.Database.Models.Orientation", "Orientation", b2 => + { + b2.Property("PoseDefaultLocalizationPoseId") + .HasColumnType("text"); + + b2.Property("W") + .HasColumnType("real"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseDefaultLocalizationPoseId"); + + b2.ToTable("DefaultLocalizationPoses"); + + b2.WithOwner() + .HasForeignKey("PoseDefaultLocalizationPoseId"); + }); + + b1.OwnsOne("Api.Database.Models.Position", "Position", b2 => + { + b2.Property("PoseDefaultLocalizationPoseId") + .HasColumnType("text"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseDefaultLocalizationPoseId"); + + b2.ToTable("DefaultLocalizationPoses"); + + b2.WithOwner() + .HasForeignKey("PoseDefaultLocalizationPoseId"); + }); + + b1.Navigation("Orientation") + .IsRequired(); + + b1.Navigation("Position") + .IsRequired(); + }); + + b.Navigation("Pose") + .IsRequired(); + }); + + modelBuilder.Entity("Api.Database.Models.Inspection", b => + { + b.OwnsOne("Api.Database.Models.Position", "InspectionTarget", b1 => + { + b1.Property("InspectionId") + .HasColumnType("text"); + + b1.Property("X") + .HasColumnType("real"); + + b1.Property("Y") + .HasColumnType("real"); + + b1.Property("Z") + .HasColumnType("real"); + + b1.HasKey("InspectionId"); + + b1.ToTable("Inspections"); + + b1.WithOwner() + .HasForeignKey("InspectionId"); + }); + + b.Navigation("InspectionTarget") + .IsRequired(); + }); + + modelBuilder.Entity("Api.Database.Models.InspectionArea", b => + { + b.HasOne("Api.Database.Models.DefaultLocalizationPose", "DefaultLocalizationPose") + .WithMany() + .HasForeignKey("DefaultLocalizationPoseId"); + + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Api.Database.Models.Plant", "Plant") + .WithMany() + .HasForeignKey("PlantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("DefaultLocalizationPose"); + + b.Navigation("Installation"); + + b.Navigation("Plant"); + }); + + modelBuilder.Entity("Api.Database.Models.InspectionFinding", b => + { + b.HasOne("Api.Database.Models.Inspection", null) + .WithMany("InspectionFindings") + .HasForeignKey("InspectionId"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionDefinition", b => + { + b.HasOne("Api.Database.Models.InspectionArea", "InspectionArea") + .WithMany() + .HasForeignKey("InspectionAreaId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Api.Database.Models.MissionRun", "LastSuccessfulRun") + .WithMany() + .HasForeignKey("LastSuccessfulRunId"); + + b.HasOne("Api.Database.Models.Source", "Source") + .WithMany() + .HasForeignKey("SourceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Api.Database.Models.MapMetadata", "Map", b1 => + { + b1.Property("MissionDefinitionId") + .HasColumnType("text"); + + b1.Property("MapName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.HasKey("MissionDefinitionId"); + + b1.ToTable("MissionDefinitions"); + + b1.WithOwner() + .HasForeignKey("MissionDefinitionId"); + + b1.OwnsOne("Api.Database.Models.Boundary", "Boundary", b2 => + { + b2.Property("MapMetadataMissionDefinitionId") + .HasColumnType("text"); + + b2.Property("X1") + .HasColumnType("double precision"); + + b2.Property("X2") + .HasColumnType("double precision"); + + b2.Property("Y1") + .HasColumnType("double precision"); + + b2.Property("Y2") + .HasColumnType("double precision"); + + b2.Property("Z1") + .HasColumnType("double precision"); + + b2.Property("Z2") + .HasColumnType("double precision"); + + b2.HasKey("MapMetadataMissionDefinitionId"); + + b2.ToTable("MissionDefinitions"); + + b2.WithOwner() + .HasForeignKey("MapMetadataMissionDefinitionId"); + }); + + b1.OwnsOne("Api.Database.Models.TransformationMatrices", "TransformationMatrices", b2 => + { + b2.Property("MapMetadataMissionDefinitionId") + .HasColumnType("text"); + + b2.Property("C1") + .HasColumnType("double precision"); + + b2.Property("C2") + .HasColumnType("double precision"); + + b2.Property("D1") + .HasColumnType("double precision"); + + b2.Property("D2") + .HasColumnType("double precision"); + + b2.HasKey("MapMetadataMissionDefinitionId"); + + b2.ToTable("MissionDefinitions"); + + b2.WithOwner() + .HasForeignKey("MapMetadataMissionDefinitionId"); + }); + + b1.Navigation("Boundary") + .IsRequired(); + + b1.Navigation("TransformationMatrices") + .IsRequired(); + }); + + b.Navigation("InspectionArea"); + + b.Navigation("LastSuccessfulRun"); + + b.Navigation("Map"); + + b.Navigation("Source"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionRun", b => + { + b.HasOne("Api.Database.Models.InspectionArea", "InspectionArea") + .WithMany() + .HasForeignKey("InspectionAreaId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Api.Database.Models.Robot", "Robot") + .WithMany() + .HasForeignKey("RobotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("InspectionArea"); + + b.Navigation("Robot"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionTask", b => + { + b.HasOne("Api.Database.Models.Inspection", "Inspection") + .WithMany() + .HasForeignKey("InspectionId"); + + b.HasOne("Api.Database.Models.MissionRun", null) + .WithMany("Tasks") + .HasForeignKey("MissionRunId"); + + b.OwnsOne("Api.Services.Models.IsarZoomDescription", "IsarZoomDescription", b1 => + { + b1.Property("MissionTaskId") + .HasColumnType("text"); + + b1.Property("ObjectHeight") + .HasColumnType("double precision") + .HasAnnotation("Relational:JsonPropertyName", "objectHeight"); + + b1.Property("ObjectWidth") + .HasColumnType("double precision") + .HasAnnotation("Relational:JsonPropertyName", "objectWidth"); + + b1.HasKey("MissionTaskId"); + + b1.ToTable("MissionTasks"); + + b1.WithOwner() + .HasForeignKey("MissionTaskId"); + }); + + b.OwnsOne("Api.Database.Models.Pose", "RobotPose", b1 => + { + b1.Property("MissionTaskId") + .HasColumnType("text"); + + b1.HasKey("MissionTaskId"); + + b1.ToTable("MissionTasks"); + + b1.WithOwner() + .HasForeignKey("MissionTaskId"); + + b1.OwnsOne("Api.Database.Models.Orientation", "Orientation", b2 => + { + b2.Property("PoseMissionTaskId") + .HasColumnType("text"); + + b2.Property("W") + .HasColumnType("real"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseMissionTaskId"); + + b2.ToTable("MissionTasks"); + + b2.WithOwner() + .HasForeignKey("PoseMissionTaskId"); + }); + + b1.OwnsOne("Api.Database.Models.Position", "Position", b2 => + { + b2.Property("PoseMissionTaskId") + .HasColumnType("text"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseMissionTaskId"); + + b2.ToTable("MissionTasks"); + + b2.WithOwner() + .HasForeignKey("PoseMissionTaskId"); + }); + + b1.Navigation("Orientation") + .IsRequired(); + + b1.Navigation("Position") + .IsRequired(); + }); + + b.Navigation("Inspection"); + + b.Navigation("IsarZoomDescription"); + + b.Navigation("RobotPose") + .IsRequired(); + }); + + modelBuilder.Entity("Api.Database.Models.Plant", b => + { + b.HasOne("Api.Database.Models.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Installation"); + }); + + modelBuilder.Entity("Api.Database.Models.Robot", b => + { + b.HasOne("Api.Database.Models.InspectionArea", "CurrentInspectionArea") + .WithMany() + .HasForeignKey("CurrentInspectionAreaId"); + + b.HasOne("Api.Database.Models.Installation", "CurrentInstallation") + .WithMany() + .HasForeignKey("CurrentInstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Api.Database.Models.RobotModel", "Model") + .WithMany() + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsMany("Api.Database.Models.DocumentInfo", "Documentation", b1 => + { + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("text"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.Property("RobotId") + .IsRequired() + .HasColumnType("text"); + + b1.Property("Url") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b1.HasKey("Id"); + + b1.HasIndex("RobotId"); + + b1.ToTable("DocumentInfo"); + + b1.WithOwner() + .HasForeignKey("RobotId"); + }); + + b.OwnsOne("Api.Database.Models.Pose", "Pose", b1 => + { + b1.Property("RobotId") + .HasColumnType("text"); + + b1.HasKey("RobotId"); + + b1.ToTable("Robots"); + + b1.WithOwner() + .HasForeignKey("RobotId"); + + b1.OwnsOne("Api.Database.Models.Orientation", "Orientation", b2 => + { + b2.Property("PoseRobotId") + .HasColumnType("text"); + + b2.Property("W") + .HasColumnType("real"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseRobotId"); + + b2.ToTable("Robots"); + + b2.WithOwner() + .HasForeignKey("PoseRobotId"); + }); + + b1.OwnsOne("Api.Database.Models.Position", "Position", b2 => + { + b2.Property("PoseRobotId") + .HasColumnType("text"); + + b2.Property("X") + .HasColumnType("real"); + + b2.Property("Y") + .HasColumnType("real"); + + b2.Property("Z") + .HasColumnType("real"); + + b2.HasKey("PoseRobotId"); + + b2.ToTable("Robots"); + + b2.WithOwner() + .HasForeignKey("PoseRobotId"); + }); + + b1.Navigation("Orientation") + .IsRequired(); + + b1.Navigation("Position") + .IsRequired(); + }); + + b.Navigation("CurrentInspectionArea"); + + b.Navigation("CurrentInstallation"); + + b.Navigation("Documentation"); + + b.Navigation("Model"); + + b.Navigation("Pose") + .IsRequired(); + }); + + modelBuilder.Entity("Api.Services.MissionLoaders.TagInspectionMetadata", b => + { + b.OwnsOne("Api.Services.Models.IsarZoomDescription", "ZoomDescription", b1 => + { + b1.Property("TagInspectionMetadataTagId") + .HasColumnType("text"); + + b1.Property("ObjectHeight") + .HasColumnType("double precision") + .HasAnnotation("Relational:JsonPropertyName", "objectHeight"); + + b1.Property("ObjectWidth") + .HasColumnType("double precision") + .HasAnnotation("Relational:JsonPropertyName", "objectWidth"); + + b1.HasKey("TagInspectionMetadataTagId"); + + b1.ToTable("TagInspectionMetadata"); + + b1.WithOwner() + .HasForeignKey("TagInspectionMetadataTagId"); + }); + + b.Navigation("ZoomDescription"); + }); + + modelBuilder.Entity("Api.Database.Models.Inspection", b => + { + b.Navigation("InspectionFindings"); + }); + + modelBuilder.Entity("Api.Database.Models.MissionRun", b => + { + b.Navigation("Tasks"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/api/Migrations/20250130081334_RenameEstimatedDuration.cs b/backend/api/Migrations/20250130081334_RenameEstimatedDuration.cs new file mode 100644 index 00000000..cf247491 --- /dev/null +++ b/backend/api/Migrations/20250130081334_RenameEstimatedDuration.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Api.Migrations +{ + /// + public partial class RenameEstimatedDuration : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "EstimatedDuration", + table: "MissionRuns", + newName: "EstimatedTaskDuration"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "EstimatedTaskDuration", + table: "MissionRuns", + newName: "EstimatedDuration"); + } + } +} diff --git a/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs b/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs index 4b19e493..29ec2fea 100644 --- a/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs +++ b/backend/api/Migrations/FlotillaDbContextModelSnapshot.cs @@ -297,7 +297,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("EndTime") .HasColumnType("timestamp with time zone"); - b.Property("EstimatedDuration") + b.Property("EstimatedTaskDuration") .HasColumnType("bigint"); b.Property("InspectionAreaId") diff --git a/backend/api/Services/Models/IsarMissionDefinition.cs b/backend/api/Services/Models/IsarMissionDefinition.cs index 04dbf73d..b8287d80 100644 --- a/backend/api/Services/Models/IsarMissionDefinition.cs +++ b/backend/api/Services/Models/IsarMissionDefinition.cs @@ -136,10 +136,6 @@ public IsarInspectionDefinition( { { "map", mapName }, { "description", missionRun.Description }, - { - "estimated_duration", - missionRun.EstimatedDuration?.ToString("D", CultureInfo.InvariantCulture) - }, { "asset_code", missionRun.InstallationCode }, { "mission_name", missionRun.Name }, { "status_reason", missionRun.StatusReason }, diff --git a/frontend/src/components/Pages/FrontPage/MissionOverview/MissionQueueCard.tsx b/frontend/src/components/Pages/FrontPage/MissionOverview/MissionQueueCard.tsx index ce1fbe9d..12ffe17e 100644 --- a/frontend/src/components/Pages/FrontPage/MissionOverview/MissionQueueCard.tsx +++ b/frontend/src/components/Pages/FrontPage/MissionOverview/MissionQueueCard.tsx @@ -8,6 +8,7 @@ import { useLanguageContext } from 'components/Contexts/LanguageContext' import { Icons } from 'utils/icons' import { StyledDialog } from 'components/Styles/StyledComponents' import { tokens } from '@equinor/eds-tokens' +import { calculateRemaindingTimeInMinutes } from 'utils/CalculateRemaingingTime' interface MissionQueueCardProps { order: number @@ -159,23 +160,24 @@ const RemoveMissionDialog = ({ const MissionDetails = ({ mission }: { mission: Mission }) => { const { TranslateText } = useLanguageContext() - const estimatedDuration = () => { + const getEstimatedDuration = () => { const translateEstimatedDuration = TranslateText('Estimated duration') const translateH = TranslateText('h') const translateMin = TranslateText('min') const translateNotAvailable = TranslateText('Estimated duration: not available') - if (mission.estimatedDuration !== undefined) { - const hours = Math.floor(mission.estimatedDuration / 3600) - const remainingSeconds = mission.estimatedDuration % 3600 - const minutes = Math.ceil(remainingSeconds / 60) - return `${translateEstimatedDuration}: ${hours} ${translateH} ${minutes} ${translateMin}` + if (mission.estimatedTaskDuration) { + const estimatedDuration = calculateRemaindingTimeInMinutes(mission.tasks, mission.estimatedTaskDuration) + const hours = Math.floor(estimatedDuration / 60) + const remainingMinutes = Math.ceil(estimatedDuration % 60) + + return `${translateEstimatedDuration}: ${hours} ${translateH} ${remainingMinutes} ${translateMin}` } return translateNotAvailable } const tasks = `${TranslateText('Tasks')}: ${mission.tasks.length}` - const missionDetails = `${tasks} | ${estimatedDuration()}` + const missionDetails = `${tasks} | ${getEstimatedDuration()}` return ( diff --git a/frontend/src/components/Pages/MissionPage/MissionHeader/MissionHeader.tsx b/frontend/src/components/Pages/MissionPage/MissionHeader/MissionHeader.tsx index 2a40fdb2..6876013e 100644 --- a/frontend/src/components/Pages/MissionPage/MissionHeader/MissionHeader.tsx +++ b/frontend/src/components/Pages/MissionPage/MissionHeader/MissionHeader.tsx @@ -10,6 +10,7 @@ import { StatusReason } from '../StatusReason' import { MissionRestartButton } from 'components/Displays/MissionButtons/MissionRestartButton' import { TaskStatus, TaskType } from 'models/Task' import { convertUTCDateToLocalDate, formatDateTime } from 'utils/StringFormatting' +import { calculateRemaindingTimeInMinutes } from 'utils/CalculateRemaingingTime' const HeaderSection = styled(Card)` width: 100%; @@ -96,11 +97,6 @@ const getStartUsedAndRemainingTime = ( let startDate: string let remainingTime: string let usedTimeInMinutes: number - let estimatedDurationInMinutes: number | undefined - if (mission.estimatedDuration) { - // Convert from seconds to minutes, rounding up - estimatedDurationInMinutes = Math.ceil(mission.estimatedDuration / 60) - } if (mission.endTime) { startTime = mission.startTime @@ -121,14 +117,17 @@ const getStartUsedAndRemainingTime = ( startTime = formatDateTime(mission.startTime, 'HH:mm') startDate = formatDateTime(mission.startTime, 'dd/MM/yyy') usedTimeInMinutes = differenceInMinutes(Date.now(), convertUTCDateToLocalDate(mission.startTime)) - if (estimatedDurationInMinutes) - remainingTime = Math.max(estimatedDurationInMinutes - usedTimeInMinutes, 0) + ' ' + translatedMinutes + if (mission.estimatedTaskDuration) + remainingTime = + calculateRemaindingTimeInMinutes(mission.tasks, mission.estimatedTaskDuration) + ' ' + translatedMinutes else remainingTime = 'N/A' } else { startTime = 'N/A' startDate = 'N/A' usedTimeInMinutes = 0 - if (estimatedDurationInMinutes) remainingTime = estimatedDurationInMinutes + ' ' + translatedMinutes + if (mission.estimatedTaskDuration) + remainingTime = + calculateRemaindingTimeInMinutes(mission.tasks, mission.estimatedTaskDuration) + ' ' + translatedMinutes else remainingTime = 'N/A' } const usedTime: string = usedTimeInMinutes + ' ' + translatedMinutes diff --git a/frontend/src/models/Mission.ts b/frontend/src/models/Mission.ts index 51ea2d56..91f1732c 100644 --- a/frontend/src/models/Mission.ts +++ b/frontend/src/models/Mission.ts @@ -38,7 +38,7 @@ export interface Mission { desiredStartTime: Date startTime?: Date endTime?: Date - estimatedDuration?: number + estimatedTaskDuration?: number tasks: Task[] } diff --git a/frontend/src/utils/CalculateRemaingingTime.tsx b/frontend/src/utils/CalculateRemaingingTime.tsx new file mode 100644 index 00000000..d5461cc1 --- /dev/null +++ b/frontend/src/utils/CalculateRemaingingTime.tsx @@ -0,0 +1,19 @@ +import { differenceInSeconds } from 'date-fns' +import { Task, TaskStatus } from 'models/Task' +import { convertUTCDateToLocalDate } from './StringFormatting' + +export const calculateRemaindingTimeInMinutes = (tasks: Task[], estimatedTaskDuration: number) => { + const estimatedTaskDurations = tasks.map((task) => { + if (task.status === TaskStatus.NotStarted || task.status === TaskStatus.Paused) { + return estimatedTaskDuration + (task.inspection.videoDuration ?? 0) + } else if (task.status === TaskStatus.InProgress) { + const timeUsed = task.startTime + ? differenceInSeconds(Date.now(), convertUTCDateToLocalDate(task.startTime)) + : 0 + return Math.max(estimatedTaskDuration + (task.inspection.videoDuration ?? 0) - timeUsed, 0) + } + return 0 + }) + const remandingTimeInSeconds = estimatedTaskDurations.reduce((sum, x) => sum + x) + return Math.ceil(remandingTimeInSeconds / 60) +}