diff --git a/Azaion.Annotator/Annotator.xaml.cs b/Azaion.Annotator/Annotator.xaml.cs index 700c4c7..76b901f 100644 --- a/Azaion.Annotator/Annotator.xaml.cs +++ b/Azaion.Annotator/Annotator.xaml.cs @@ -417,7 +417,8 @@ public partial class Annotator mediaFile.HasAnnotations = labelsDict.ContainsKey(mediaFile.FName); AllMediaFiles = new ObservableCollection(allFiles); - MediaFilesDict = AllMediaFiles.ToDictionary(x => x.FName); + MediaFilesDict = AllMediaFiles.GroupBy(x => x.Name) + .ToDictionary(gr => gr.Key, gr => gr.First()); LvFiles.ItemsSource = AllMediaFiles; DataContext = this; } @@ -463,7 +464,7 @@ public partial class Annotator { Title = "Open Video folder", IsFolderPicker = true, - InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) + InitialDirectory = Path.GetDirectoryName(_appConfig.DirectoriesConfig.VideosDirectory) }; var dialogResult = dlg.ShowDialog(); diff --git a/Azaion.Annotator/AnnotatorEventHandler.cs b/Azaion.Annotator/AnnotatorEventHandler.cs index 05fce65..c2258dc 100644 --- a/Azaion.Annotator/AnnotatorEventHandler.cs +++ b/Azaion.Annotator/AnnotatorEventHandler.cs @@ -1,6 +1,7 @@ using System.IO; using System.Windows; using System.Windows.Input; +using System.Windows.Threading; using Azaion.Annotator.DTO; using Azaion.Common; using Azaion.Common.DTO; @@ -276,31 +277,42 @@ public class AnnotatorEventHandler( mainWindow.AddAnnotation(annotation); } - public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) + public Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) { - var namesSet = notification.AnnotationNames.ToHashSet(); - - var remainAnnotations = formState.AnnotationResults - .Where(x => !namesSet.Contains(x.Annotation?.Name ?? "")).ToList(); - formState.AnnotationResults.Clear(); - foreach (var ann in remainAnnotations) - formState.AnnotationResults.Add(ann); - - var timedAnnsToRemove = mainWindow.TimedAnnotations - .Where(x => namesSet.Contains(x.Value.Name)) - .Select(x => x.Value).ToList(); - mainWindow.TimedAnnotations.Remove(timedAnnsToRemove); - - if (formState.AnnotationResults.Count == 0) + try { - var media = mainWindow.AllMediaFiles.FirstOrDefault(x => x.Name == formState.CurrentMedia?.Name); - if (media != null) + mainWindow.Dispatcher.Invoke(() => { - media.HasAnnotations = false; - mainWindow.LvFiles.Items.Refresh(); - } + var namesSet = notification.AnnotationNames.ToHashSet(); + + var remainAnnotations = formState.AnnotationResults + .Where(x => !namesSet.Contains(x.Annotation?.Name ?? "")).ToList(); + formState.AnnotationResults.Clear(); + foreach (var ann in remainAnnotations) + formState.AnnotationResults.Add(ann); + + var timedAnnsToRemove = mainWindow.TimedAnnotations + .Where(x => namesSet.Contains(x.Value.Name)) + .Select(x => x.Value).ToList(); + mainWindow.TimedAnnotations.Remove(timedAnnsToRemove); + + if (formState.AnnotationResults.Count == 0) + { + var media = mainWindow.AllMediaFiles.FirstOrDefault(x => x.Name == formState.CurrentMedia?.Name); + if (media != null) + { + media.HasAnnotations = false; + mainWindow.LvFiles.Items.Refresh(); + } + } + }); } - await Task.CompletedTask; + catch (Exception e) + { + logger.LogError(e, e.Message); + throw; + } + return Task.CompletedTask; } public Task Handle(AnnotationAddedEvent e, CancellationToken cancellationToken) diff --git a/Azaion.Common/Constants.cs b/Azaion.Common/Constants.cs index ceadd67..e59e1d7 100644 --- a/Azaion.Common/Constants.cs +++ b/Azaion.Common/Constants.cs @@ -97,12 +97,7 @@ public class Constants #endregion - #region Queue - public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations"; - public const string MQ_ANNOTATIONS_CONFIRM_QUEUE = "azaion-annotations-confirm"; - - #endregion #region Database diff --git a/Azaion.Common/Database/Annotation.cs b/Azaion.Common/Database/Annotation.cs index 2d0425a..013f4a6 100644 --- a/Azaion.Common/Database/Annotation.cs +++ b/Azaion.Common/Database/Annotation.cs @@ -59,7 +59,8 @@ public enum AnnotationStatus { None = 0, Created = 10, - Validated = 20, - ValidatedEdited = 25, - Deleted = 30 + ValidatedEdited = 20, + + Validated = 30, + Deleted = 40 } \ No newline at end of file diff --git a/Azaion.Common/Database/AnnotationQueueRecord.cs b/Azaion.Common/Database/AnnotationQueueRecord.cs index 8d95f6d..0e1524b 100644 --- a/Azaion.Common/Database/AnnotationQueueRecord.cs +++ b/Azaion.Common/Database/AnnotationQueueRecord.cs @@ -2,7 +2,8 @@ namespace Azaion.Common.Database; public class AnnotationQueueRecord { - public DateTime DateTime { get; set; } - public AnnotationStatus Operation { get; set; } + public Guid Id { get; set; } + public DateTime DateTime { get; set; } + public AnnotationStatus Operation { get; set; } public List AnnotationNames { get; set; } = null!; } \ No newline at end of file diff --git a/Azaion.Common/Database/AnnotationsDb.cs b/Azaion.Common/Database/AnnotationsDb.cs index febcf8d..c8434d9 100644 --- a/Azaion.Common/Database/AnnotationsDb.cs +++ b/Azaion.Common/Database/AnnotationsDb.cs @@ -9,5 +9,4 @@ public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions public ITable Annotations => this.GetTable(); public ITable AnnotationsQueueRecords => this.GetTable(); public ITable Detections => this.GetTable(); - public ITable QueueOffsets => this.GetTable(); } \ No newline at end of file diff --git a/Azaion.Common/Database/DbFactory.cs b/Azaion.Common/Database/DbFactory.cs index 25f0104..6b33fbe 100644 --- a/Azaion.Common/Database/DbFactory.cs +++ b/Azaion.Common/Database/DbFactory.cs @@ -53,32 +53,24 @@ public class DbFactory : IDbFactory .UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema); if (!File.Exists(_annConfig.AnnotationsDbFile)) - CreateDb(); + SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile); + RecreateTables(); + _fileConnection.Open(); _fileConnection.BackupDatabase(_memoryConnection, "main", "main", -1, null, -1); } - private void CreateDb() + private void RecreateTables() { - SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile); using var db = new AnnotationsDb(_fileDataOptions); - db.CreateTable(); - db.CreateTable(); - db.CreateTable(); - db.CreateTable(); - db.QueueOffsets.BulkCopy(new List - { - new() - { - Offset = 0, - QueueName = Constants.MQ_ANNOTATIONS_QUEUE - }, - new() - { - Offset = 0, - QueueName = Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE - } - }); + var schema = db.DataProvider.GetSchemaProvider().GetSchema(db); + var existingTables = schema.Tables.Select(x => x.TableName).ToHashSet(); + if (!existingTables.Contains(Constants.ANNOTATIONS_TABLENAME)) + db.CreateTable(); + if (!existingTables.Contains(Constants.DETECTIONS_TABLENAME)) + db.CreateTable(); + if (!existingTables.Contains(Constants.ANNOTATIONS_QUEUE_TABLENAME)) + db.CreateTable(); } public async Task Run(Func> func) @@ -138,6 +130,7 @@ public static class AnnotationsDbSchemaHolder builder.Entity() .HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME) + .HasPrimaryKey(x => x.Id) .Property(x => x.AnnotationNames) .HasDataType(DataType.NVarChar) .HasConversion(list => JsonConvert.SerializeObject(list), str => JsonConvert.DeserializeObject>(str) ?? new List()); diff --git a/Azaion.Common/Database/QueueOffset.cs b/Azaion.Common/Database/QueueOffset.cs deleted file mode 100644 index 1fe3d0f..0000000 --- a/Azaion.Common/Database/QueueOffset.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Azaion.Common.Database; - -public class QueueOffset -{ - public string QueueName { get; set; } = null!; - public ulong Offset { get; set; } -} \ No newline at end of file diff --git a/Azaion.Common/Services/AnnotationService.cs b/Azaion.Common/Services/AnnotationService.cs index 843cd86..64c9c23 100644 --- a/Azaion.Common/Services/AnnotationService.cs +++ b/Azaion.Common/Services/AnnotationService.cs @@ -77,40 +77,43 @@ public class AnnotationService : IAnnotationService, INotificationHandler { try { var email = (string)message.ApplicationProperties[nameof(User.Email)]!; - if (email == _api.CurrentUser.Email) //Don't process messages by yourself + if (!Enum.TryParse((string)message.ApplicationProperties[nameof(AnnotationStatus)], out var annotationStatus)) return; - var annotationStatus = (AnnotationStatus)message.ApplicationProperties[nameof(AnnotationStatus)]; - if (annotationStatus.In(AnnotationStatus.Created, AnnotationStatus.ValidatedEdited)) - { - var msg = MessagePackSerializer.Deserialize(message.Data.Contents); - await SaveAnnotationInner( - msg.CreatedDate, - msg.OriginalMediaName, - msg.Time, - JsonConvert.DeserializeObject>(msg.Detections) ?? [], - msg.Source, - new MemoryStream(msg.Image), - msg.Role, - msg.Email, - fromQueue: true, - token: cancellationToken); - } - else - { - var msg = MessagePackSerializer.Deserialize(message.Data.Contents); - if (annotationStatus == AnnotationStatus.Validated) - await ValidateAnnotations(msg.AnnotationNames.ToList(), true, cancellationToken); - if (annotationStatus == AnnotationStatus.Deleted) - await _mediator.Publish(new AnnotationsDeletedEvent(msg.AnnotationNames.ToList(), fromQueue:true), cancellationToken); - } - offsets.AnnotationsOffset = context.Offset; + if (email != _api.CurrentUser.Email) //Don't process messages by yourself + { + if (annotationStatus.In(AnnotationStatus.Created, AnnotationStatus.ValidatedEdited)) + { + var msg = MessagePackSerializer.Deserialize(message.Data.Contents); + await SaveAnnotationInner( + msg.CreatedDate, + msg.OriginalMediaName, + msg.Time, + JsonConvert.DeserializeObject>(msg.Detections) ?? [], + msg.Source, + new MemoryStream(msg.Image), + msg.Role, + msg.Email, + fromQueue: true, + token: cancellationToken); + } + else + { + var msg = MessagePackSerializer.Deserialize(message.Data.Contents); + if (annotationStatus == AnnotationStatus.Validated) + await ValidateAnnotations(msg.AnnotationNames.ToList(), true, cancellationToken); + if (annotationStatus == AnnotationStatus.Deleted) + await _mediator.Publish(new AnnotationsDeletedEvent(msg.AnnotationNames.ToList(), fromQueue:true), cancellationToken); + } + } + + offsets.AnnotationsOffset = context.Offset + 1; //to consume on the next launch from the next message ThrottleExt.Throttle(() => { _api.UpdateOffsets(offsets); @@ -154,12 +157,6 @@ public class AnnotationService : IAnnotationService, INotificationHandler x.Detections) .FirstOrDefaultAsync(x => x.Name == fName, token: token); - if (userRole.IsValidator() && source == SourceEnum.Manual) - status = AnnotationStatus.Validated; - - if (fromQueue && ann is { AnnotationStatus: AnnotationStatus.Validated }) - return ann; - await db.Detections.DeleteAsync(x => x.AnnotationName == fName, token: token); if (ann != null) //Annotation is already exists @@ -169,10 +166,9 @@ public class AnnotationService : IAnnotationService, INotificationHandler x.Source, source) .Set(x => x.CreatedRole, userRole); - if (status == AnnotationStatus.Validated) + if (userRole.IsValidator() && source == SourceEnum.Manual) { - if (status == AnnotationStatus.Validated) - status = AnnotationStatus.ValidatedEdited; //For further processing mark Annotations *edited* by Validator, not just simply Validated by button. + status = AnnotationStatus.ValidatedEdited; annotationUpdatable = annotationUpdatable .Set(x => x.ValidateDate, createdDate) @@ -212,9 +208,6 @@ public class AnnotationService : IAnnotationService, INotificationHandler @@ -275,7 +268,8 @@ public class AnnotationService : IAnnotationService, INotificationHandler diff --git a/Azaion.Common/Services/FailsafeProducer.cs b/Azaion.Common/Services/FailsafeProducer.cs index 750be53..8e93f08 100644 --- a/Azaion.Common/Services/FailsafeProducer.cs +++ b/Azaion.Common/Services/FailsafeProducer.cs @@ -23,14 +23,22 @@ public class FailsafeAnnotationsProducer private readonly IDbFactory _dbFactory; private readonly IAzaionApi _azaionApi; private readonly QueueConfig _queueConfig; + private readonly UIConfig _uiConfig; + private Producer _annotationProducer = null!; - public FailsafeAnnotationsProducer(ILogger logger, IDbFactory dbFactory, IOptions queueConfig, IAzaionApi azaionApi) + + public FailsafeAnnotationsProducer(ILogger logger, + IDbFactory dbFactory, + IOptions queueConfig, + IOptions uiConfig, + IAzaionApi azaionApi) { _logger = logger; _dbFactory = dbFactory; _azaionApi = azaionApi; _queueConfig = queueConfig.Value; + _uiConfig = uiConfig.Value; Task.Run(async () => await ProcessQueue()); } @@ -71,7 +79,7 @@ public class FailsafeAnnotationsProducer { var appProperties = new ApplicationProperties { - { nameof(AnnotationStatus), record.Operation }, + { nameof(AnnotationStatus), record.Operation.ToString() }, { nameof(User.Email), _azaionApi.CurrentUser.Email } }; @@ -86,7 +94,7 @@ public class FailsafeAnnotationsProducer } else { - var annotation = annotationsDict.GetValueOrDefault(record.AnnotationNames.FirstOrDefault()); + var annotation = annotationsDict!.GetValueOrDefault(record.AnnotationNames.FirstOrDefault()); if (annotation == null) continue; @@ -118,7 +126,8 @@ public class FailsafeAnnotationsProducer if (result.messages.Any()) { await _annotationProducer.Send(result.messages, CompressionType.Gzip); - await _dbFactory.Run(async db => await db.DeleteAsync(result.records, token: ct)); + var ids = result.records.Select(x => x.Id).ToList(); + var removed = await _dbFactory.Run(async db => await db.AnnotationsQueueRecords.DeleteAsync(x => ids.Contains(x.Id), token: ct)); sent = true; _dbFactory.SaveToDisk(); } @@ -136,9 +145,12 @@ public class FailsafeAnnotationsProducer public async Task SendToInnerQueue(List annotationNames, AnnotationStatus status, CancellationToken cancellationToken = default) { + if (_uiConfig.SilentDetection) + return; await _dbFactory.Run(async db => await db.InsertAsync(new AnnotationQueueRecord { + Id = Guid.NewGuid(), DateTime = DateTime.UtcNow, Operation = status, AnnotationNames = annotationNames diff --git a/Azaion.Common/Services/GPSMatcherService.cs b/Azaion.Common/Services/GPSMatcherService.cs index f633caf..03c4370 100644 --- a/Azaion.Common/Services/GPSMatcherService.cs +++ b/Azaion.Common/Services/GPSMatcherService.cs @@ -41,7 +41,7 @@ public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDow var indexOffset = 0; while (routeFiles.Any()) { - //await satelliteTileDownloader.GetTiles(currentLat, currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, detectToken); + await satelliteTileDownloader.GetTiles(currentLat, currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, detectToken); gpsMatcherClient.StartMatching(new StartMatchingEvent { ImagesCount = POINTS_COUNT, diff --git a/Azaion.Common/Services/InferenceClient.cs b/Azaion.Common/Services/InferenceClient.cs index 28ad8fe..0d6771d 100644 --- a/Azaion.Common/Services/InferenceClient.cs +++ b/Azaion.Common/Services/InferenceClient.cs @@ -126,7 +126,7 @@ public class InferenceClient : IInferenceClient, IResourceLoader _waitFileCancelSource.Cancel(); } - bytes = command.Data; + bytes = command.Data!; _waitFileCancelSource.Cancel(); } _waitFileCancelSource.Token.WaitForCancel(timeout ?? TimeSpan.FromSeconds(15)); diff --git a/Azaion.Dataset/DatasetExplorerEventHandler.cs b/Azaion.Dataset/DatasetExplorerEventHandler.cs index 752cb88..f0cd874 100644 --- a/Azaion.Dataset/DatasetExplorerEventHandler.cs +++ b/Azaion.Dataset/DatasetExplorerEventHandler.cs @@ -1,19 +1,17 @@ -using System.IO; -using System.Windows; -using System.Windows.Input; -using System.Windows.Threading; +using System.Windows.Input; using Azaion.Common.Database; using Azaion.Common.DTO; -using Azaion.Common.DTO.Queue; using Azaion.Common.Events; using Azaion.Common.Services; using Azaion.CommonSecurity.DTO; using Azaion.CommonSecurity.Services; using MediatR; +using Microsoft.Extensions.Logging; namespace Azaion.Dataset; public class DatasetExplorerEventHandler( + ILogger logger, DatasetExplorer datasetExplorer, IAnnotationService annotationService, IAzaionApi azaionApi) : @@ -22,8 +20,6 @@ public class DatasetExplorerEventHandler( INotificationHandler, INotificationHandler { - private readonly IAzaionApi _azaionApi = azaionApi; - private readonly Dictionary _keysControlEnumDict = new() { { Key.Enter, PlaybackControlEnum.SaveAnnotations }, @@ -127,7 +123,7 @@ public class DatasetExplorerEventHandler( if (annotation.Classes.Contains(selectedClass) || selectedClass == -1) { var index = 0; - var annThumb = new AnnotationThumbnail(annotation, _azaionApi.CurrentUser.Role.IsValidator()); + var annThumb = new AnnotationThumbnail(annotation, azaionApi.CurrentUser.Role.IsValidator()); if (datasetExplorer.SelectedAnnotationDict.ContainsKey(annThumb.Annotation.Name)) { datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name); @@ -148,14 +144,25 @@ public class DatasetExplorerEventHandler( public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) { - var annThumbs = datasetExplorer.SelectedAnnotationDict - .Where(x => notification.AnnotationNames.Contains(x.Key)) - .Select(x => x.Value) - .ToList(); - foreach (var annThumb in annThumbs) + try { - datasetExplorer.SelectedAnnotations.Remove(annThumb); - datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name); + datasetExplorer.Dispatcher.Invoke(() => + { + var annThumbs = datasetExplorer.SelectedAnnotationDict + .Where(x => notification.AnnotationNames.Contains(x.Key)) + .Select(x => x.Value) + .ToList(); + foreach (var annThumb in annThumbs) + { + datasetExplorer.SelectedAnnotations.Remove(annThumb); + datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name); + } + }); + } + catch (Exception e) + { + logger.LogError(e, e.Message); + throw; } await Task.CompletedTask; } diff --git a/Azaion.Suite/App.xaml.cs b/Azaion.Suite/App.xaml.cs index f0dfe4d..6eeb083 100644 --- a/Azaion.Suite/App.xaml.cs +++ b/Azaion.Suite/App.xaml.cs @@ -145,6 +145,9 @@ public partial class App StartMain(); _host.Start(); EventManager.RegisterClassHandler(typeof(UIElement), UIElement.PreviewKeyDownEvent, new RoutedEventHandler(GlobalKeyHandler)); + var datasetExplorer = _host.Services.GetRequiredService(); + datasetExplorer.Show(); + datasetExplorer.Hide(); _host.Services.GetRequiredService().Show(); }; login.Closed += (sender, args) => diff --git a/Azaion.Suite/config.json b/Azaion.Suite/config.json index f5c76fd..d4652ef 100644 --- a/Azaion.Suite/config.json +++ b/Azaion.Suite/config.json @@ -27,6 +27,6 @@ "LeftPanelWidth": 220.0, "RightPanelWidth": 230.0, "GenerateAnnotatedImage": true, - "SilentDetection": true + "SilentDetection": false } } \ No newline at end of file