From 099f9cf52bbf5065fa5cb69fd3f1b5e5349c75d9 Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Mon, 17 Mar 2025 09:08:43 +0200 Subject: [PATCH] add map support for gps denied --- Azaion.Annotator/Annotator.xaml | 63 +++- Azaion.Annotator/Annotator.xaml.cs | 24 ++ Azaion.Annotator/Azaion.Annotator.csproj | 1 + Azaion.Annotator/Controls/CircleVisual.cs | 348 ++++++++++++++++++ Azaion.Annotator/Controls/MapMatcher.xaml | 118 ++++++ Azaion.Annotator/Controls/MapMatcher.xaml.cs | 152 ++++++++ Azaion.Common/Azaion.Common.csproj | 1 + Azaion.Common/Constants.cs | 1 + Azaion.Common/DTO/Config/AppConfig.cs | 24 +- Azaion.Common/DTO/Config/MapConfig.cs | 7 + Azaion.Common/DTO/GpsCsvResult.cs | 50 +++ .../Azaion.CommonSecurity.csproj | 2 +- Azaion.Suite/Login.xaml | 4 +- Azaion.Suite/config.json | 7 +- build/installer.iss | 2 + 15 files changed, 783 insertions(+), 21 deletions(-) create mode 100644 Azaion.Annotator/Controls/CircleVisual.cs create mode 100644 Azaion.Annotator/Controls/MapMatcher.xaml create mode 100644 Azaion.Annotator/Controls/MapMatcher.xaml.cs create mode 100644 Azaion.Common/DTO/Config/MapConfig.cs create mode 100644 Azaion.Common/DTO/GpsCsvResult.cs diff --git a/Azaion.Annotator/Annotator.xaml b/Azaion.Annotator/Annotator.xaml index 0eca01d..3df8359 100644 --- a/Azaion.Annotator/Annotator.xaml +++ b/Azaion.Annotator/Annotator.xaml @@ -3,7 +3,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:wpf="clr-namespace:LibVLCSharp.WPF;assembly=LibVLCSharp.WPF" xmlns:controls="clr-namespace:Azaion.Annotator.Controls" + xmlns:wpf="clr-namespace:LibVLCSharp.WPF;assembly=LibVLCSharp.WPF" + xmlns:controls="clr-namespace:Azaion.Annotator.Controls" xmlns:controls1="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common" xmlns:controls2="clr-namespace:Azaion.Annotator.Controls;assembly=Azaion.Common" mc:Ignorable="d" @@ -54,9 +55,11 @@ ShowGridLines="False" Background="Black"> - - - + + + + + + + + + @@ -290,7 +309,7 @@ @@ -306,7 +325,9 @@ - + + + - + - - diff --git a/Azaion.Annotator/Annotator.xaml.cs b/Azaion.Annotator/Annotator.xaml.cs index 10679e4..c8c34da 100644 --- a/Azaion.Annotator/Annotator.xaml.cs +++ b/Azaion.Annotator/Annotator.xaml.cs @@ -43,6 +43,7 @@ public partial class Annotator private ObservableCollection AnnotationClasses { get; set; } = new(); private bool _suspendLayout; + private bool _gpsPanelVisible = false; public readonly CancellationTokenSource MainCancellationSource = new(); public CancellationTokenSource DetectionCancellationSource = new(); @@ -105,6 +106,7 @@ public partial class Annotator }; Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time); + MapMatcherComponent.Init(_appConfig); } private void OnLoaded(object sender, RoutedEventArgs e) @@ -587,6 +589,28 @@ public partial class Annotator }); } + private void SwitchGpsPanel(object sender, RoutedEventArgs e) + { + _gpsPanelVisible = !_gpsPanelVisible; + + if (_gpsPanelVisible) + { + GpsSplitterRow.Height = new GridLength(4); + GpsSplitter.Visibility = Visibility.Visible; + + GpsSectionRow.Height = new GridLength(1, GridUnitType.Star); + MapMatcherComponent.Visibility = Visibility.Visible; + } + else + { + GpsSplitterRow.Height = new GridLength(0); + GpsSplitter.Visibility = Visibility.Collapsed; + + GpsSectionRow.Height = new GridLength(0); + MapMatcherComponent.Visibility = Visibility.Collapsed; + } + } + private void SoundDetections(object sender, RoutedEventArgs e) { throw new NotImplementedException(); diff --git a/Azaion.Annotator/Azaion.Annotator.csproj b/Azaion.Annotator/Azaion.Annotator.csproj index a098643..66dcac9 100644 --- a/Azaion.Annotator/Azaion.Annotator.csproj +++ b/Azaion.Annotator/Azaion.Annotator.csproj @@ -8,6 +8,7 @@ + diff --git a/Azaion.Annotator/Controls/CircleVisual.cs b/Azaion.Annotator/Controls/CircleVisual.cs new file mode 100644 index 0000000..9362bec --- /dev/null +++ b/Azaion.Annotator/Controls/CircleVisual.cs @@ -0,0 +1,348 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Effects; +using GMap.NET.WindowsPresentation; + +namespace Azaion.Annotator.Controls +{ + public class CircleVisual : FrameworkElement + { + public readonly GMapMarker Marker; + + public CircleVisual(GMapMarker m, Brush background) + { + Marker = m; + Marker.ZIndex = 100; + + SizeChanged += CircleVisual_SizeChanged; + MouseEnter += CircleVisual_MouseEnter; + MouseLeave += CircleVisual_MouseLeave; + Loaded += OnLoaded; + + Text = "?"; + + StrokeArrow.EndLineCap = PenLineCap.Triangle; + StrokeArrow.LineJoin = PenLineJoin.Round; + + RenderTransform = _scale; + + Width = Height = 22; + FontSize = Width / 1.55; + + Background = background; + Angle = null; + } + + void CircleVisual_SizeChanged(object sender, SizeChangedEventArgs e) + { + Marker.Offset = new Point(-e.NewSize.Width / 2, -e.NewSize.Height / 2); + _scale.CenterX = -Marker.Offset.X; + _scale.CenterY = -Marker.Offset.Y; + } + + void OnLoaded(object sender, RoutedEventArgs e) + { + UpdateVisual(true); + } + + readonly ScaleTransform _scale = new ScaleTransform(1, 1); + + void CircleVisual_MouseLeave(object sender, MouseEventArgs e) + { + + Marker.ZIndex -= 10000; + Cursor = Cursors.Arrow; + + Effect = null; + + _scale.ScaleY = 1; + _scale.ScaleX = 1; + } + + void CircleVisual_MouseEnter(object sender, MouseEventArgs e) + { + Marker.ZIndex += 10000; + Cursor = Cursors.Hand; + + Effect = ShadowEffect; + + _scale.ScaleY = 1.5; + _scale.ScaleX = 1.5; + } + + public DropShadowEffect ShadowEffect; + + static readonly Typeface Font = new Typeface(new FontFamily("Arial"), + FontStyles.Normal, + FontWeights.Bold, + FontStretches.Normal); + + FormattedText _fText; + + private Brush _background = Brushes.Blue; + + public Brush Background + { + get + { + return _background; + } + set + { + if (_background != value) + { + _background = value; + IsChanged = true; + } + } + } + + private Brush _foreground = Brushes.White; + + public Brush Foreground + { + get + { + return _foreground; + } + set + { + if (_foreground != value) + { + _foreground = value; + IsChanged = true; + + ForceUpdateText(); + } + } + } + + private Pen _stroke = new Pen(Brushes.Blue, 2.0); + + public Pen Stroke + { + get + { + return _stroke; + } + set + { + if (_stroke != value) + { + _stroke = value; + IsChanged = true; + } + } + } + + private Pen _strokeArrow = new Pen(Brushes.Blue, 2.0); + + public Pen StrokeArrow + { + get + { + return _strokeArrow; + } + set + { + if (_strokeArrow != value) + { + _strokeArrow = value; + IsChanged = true; + } + } + } + + public double FontSize = 16; + + private double? _angle = 0; + + public double? Angle + { + get + { + return _angle; + } + set + { + if (!Angle.HasValue || !value.HasValue || + Angle.HasValue && value.HasValue && Math.Abs(_angle.Value - value.Value) > 11) + { + _angle = value; + IsChanged = true; + } + } + } + + public bool IsChanged = true; + + void ForceUpdateText() + { + _fText = new FormattedText(_text, + CultureInfo.InvariantCulture, + FlowDirection.LeftToRight, + Font, + FontSize, + Foreground); + IsChanged = true; + } + + string _text; + + public string Text + { + get + { + return _text; + } + set + { + if (_text != value) + { + _text = value; + ForceUpdateText(); + } + } + } + + Visual _child; + + public virtual Visual Child + { + get + { + return _child; + } + set + { + if (_child != value) + { + if (_child != null) + { + RemoveLogicalChild(_child); + RemoveVisualChild(_child); + } + + if (value != null) + { + AddVisualChild(value); + AddLogicalChild(value); + } + + // cache the new child + _child = value; + + InvalidateVisual(); + } + } + } + + public bool UpdateVisual(bool forceUpdate) + { + if (forceUpdate || IsChanged) + { + Child = Create(); + IsChanged = false; + return true; + } + + return false; + } + + int _countCreate; + + private DrawingVisual Create() + { + _countCreate++; + + var square = new DrawingVisualFx(); + + using (var dc = square.RenderOpen()) + { + dc.DrawEllipse(null, + Stroke, + new Point(Width / 2, Height / 2), + Width / 2 + Stroke.Thickness / 2, + Height / 2 + Stroke.Thickness / 2); + + if (Angle.HasValue) + { + dc.PushTransform(new RotateTransform(Angle.Value, Width / 2, Height / 2)); + { + var polySeg = new PolyLineSegment(new[] + { + new Point(Width * 0.2, Height * 0.3), new Point(Width * 0.8, Height * 0.3) + }, + true); + var pathFig = new PathFigure(new Point(Width * 0.5, -Height * 0.22), + new PathSegment[] {polySeg}, + true); + var pathGeo = new PathGeometry(new[] {pathFig}); + dc.DrawGeometry(Brushes.AliceBlue, StrokeArrow, pathGeo); + } + dc.Pop(); + } + + dc.DrawEllipse(Background, null, new Point(Width / 2, Height / 2), Width / 2, Height / 2); + dc.DrawText(_fText, new Point(Width / 2 - _fText.Width / 2, Height / 2 - _fText.Height / 2)); + } + + return square; + } + + #region Necessary Overrides -- Needed by WPF to maintain bookkeeping of our hosted visuals + + protected override int VisualChildrenCount + { + get + { + return Child == null ? 0 : 1; + } + } + + protected override Visual GetVisualChild(int index) + { + return Child; + } + + #endregion + } + + public class DrawingVisualFx : DrawingVisual + { + public static readonly DependencyProperty EffectProperty = DependencyProperty.Register("Effect", + typeof(Effect), + typeof(DrawingVisualFx), + new FrameworkPropertyMetadata(null, + FrameworkPropertyMetadataOptions.AffectsRender, + OnEffectChanged)); + + public new Effect Effect + { + get + { + return (Effect)GetValue(EffectProperty); + } + set + { + SetValue(EffectProperty, value); + } + } + + private static void OnEffectChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) + { + var drawingVisualFx = o as DrawingVisualFx; + if (drawingVisualFx != null) + { + drawingVisualFx.SetMyProtectedVisualEffect((Effect)e.NewValue); + } + } + + private void SetMyProtectedVisualEffect(Effect effect) + { + VisualEffect = effect; + } + } +} diff --git a/Azaion.Annotator/Controls/MapMatcher.xaml b/Azaion.Annotator/Controls/MapMatcher.xaml new file mode 100644 index 0000000..798552d --- /dev/null +++ b/Azaion.Annotator/Controls/MapMatcher.xaml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Azaion.Annotator/Controls/MapMatcher.xaml.cs b/Azaion.Annotator/Controls/MapMatcher.xaml.cs new file mode 100644 index 0000000..99d163a --- /dev/null +++ b/Azaion.Annotator/Controls/MapMatcher.xaml.cs @@ -0,0 +1,152 @@ +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Media; +using Azaion.Common; +using Azaion.Common.Database; +using Azaion.Common.DTO; +using Azaion.Common.DTO.Config; +using Azaion.Common.Extensions; +using GMap.NET; +using GMap.NET.MapProviders; +using GMap.NET.WindowsPresentation; +using Microsoft.WindowsAPICodePack.Dialogs; + +namespace Azaion.Annotator.Controls; + +public partial class MapMatcher : UserControl +{ + private AppConfig _appConfig; + List _allMediaFiles; + private Dictionary _annotations; + private string _currentDir; + + public MapMatcher() + { + InitializeComponent(); + } + + public void Init(AppConfig appConfig) + { + _appConfig = appConfig; + GoogleMapProvider.Instance.ApiKey = appConfig.MapConfig.ApiKey; + SatelliteMap.MapProvider = GMapProviders.GoogleSatelliteMap; + SatelliteMap.Position = new PointLatLng(48.295985271707664, 37.14477539062501); + SatelliteMap.MultiTouchEnabled = true; + + GpsFiles.MouseDoubleClick += async (sender, args) => + { + var media = (GpsFiles.SelectedItem as MediaFileInfo)!; + var ann = _annotations.GetValueOrDefault(Path.GetFileNameWithoutExtension(media.Name)); + GpsImageEditor.Background = new ImageBrush + { + ImageSource = await Path.Combine(_currentDir, ann.Name).OpenImage() + }; + SatelliteMap.Position = new PointLatLng(ann.Lat, ann.Lon); + }; + } + + private void GpsFilesContextOpening(object sender, ContextMenuEventArgs e) + { + var listItem = sender as ListViewItem; + GpsFilesContextMenu.DataContext = listItem!.DataContext; + } + + private void OpenContainingFolder(object sender, RoutedEventArgs e) + { + var mediaFileInfo = (sender as MenuItem)?.DataContext as MediaFileInfo; + if (mediaFileInfo == null) + return; + + Process.Start("explorer.exe", "/select,\"" + mediaFileInfo.Path +"\""); + } + + private async void OpenGpsTilesFolderClick(object sender, RoutedEventArgs e) + { + var dlg = new CommonOpenFileDialog + { + Title = "Open Video folder", + IsFolderPicker = true, + InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) + }; + var dialogResult = dlg.ShowDialog(); + + if (dialogResult != CommonFileDialogResult.Ok || string.IsNullOrEmpty(dlg.FileName)) + return; + + TbGpsMapFolder.Text = dlg.FileName; + _currentDir = dlg.FileName; + var dir = new DirectoryInfo(dlg.FileName); + var mediaFiles = dir.GetFiles(_appConfig.AnnotationConfig.ImageFormats.ToArray()) + .Select(x => new MediaFileInfo + { + Name = x.Name, + Path = x.FullName, + MediaType = MediaTypes.Image + }).ToList(); + // var allFiles = videoFiles.Concat(imageFiles).ToList(); + // + + // var labelsDict = await _dbFactory.Run(async db => await db.Annotations + // .GroupBy(x => x.Name.Substring(0, x.Name.Length - 7)) + // .Where(x => allFileNames.Contains(x.Key)) + // .ToDictionaryAsync(x => x.Key, x => x.Key)); + // + // foreach (var mediaFile in allFiles) + // mediaFile.HasAnnotations = labelsDict.ContainsKey(mediaFile.FName); + // + // AllMediaFiles = new ObservableCollection(allFiles); + + _allMediaFiles = mediaFiles; + GpsFiles.ItemsSource = new ObservableCollection(_allMediaFiles); + var annotations = SetFromCsv(mediaFiles); + await Task.Delay(TimeSpan.FromSeconds(10)); + SetMarkers(annotations); + } + + private Dictionary SetFromCsv(List mediaFiles) + { + _annotations = mediaFiles.Select(x => new Annotation + { + Name = x.Name, + OriginalMediaName = x.Name + }).ToDictionary(x => Path.GetFileNameWithoutExtension(x.OriginalMediaName)); + + var csvResults = GpsCsvResult.ReadFromCsv(Constants.CSV_PATH); + var csvDict = csvResults.ToDictionary(x => x.Image); + foreach (var ann in _annotations) + { + var csvRes = csvDict.GetValueOrDefault(ann.Key); + if (csvRes == null) + continue; + + ann.Value.Lat = csvRes.Latitude; + ann.Value.Lon = csvRes.Longitude; + } + return _annotations; + } + + private void SetMarkers(Dictionary annotations) + { + if (!annotations.Any()) + return; + + var firstAnnotation = annotations.FirstOrDefault(); + SatelliteMap.Position = new PointLatLng(firstAnnotation.Value.Lat, firstAnnotation.Value.Lon); + foreach (var ann in annotations) + { + var marker = new GMapMarker(new PointLatLng(ann.Value.Lat, ann.Value.Lon)); + var circle = new CircleVisual(marker, System.Windows.Media.Brushes.Blue) + { + Text = " " + }; + marker.Shape = circle; + SatelliteMap.Markers.Add(marker); + } + + } +} \ No newline at end of file diff --git a/Azaion.Common/Azaion.Common.csproj b/Azaion.Common/Azaion.Common.csproj index 8be44a7..9cc3e5a 100644 --- a/Azaion.Common/Azaion.Common.csproj +++ b/Azaion.Common/Azaion.Common.csproj @@ -7,6 +7,7 @@ + diff --git a/Azaion.Common/Constants.cs b/Azaion.Common/Constants.cs index ff62c6a..e665813 100644 --- a/Azaion.Common/Constants.cs +++ b/Azaion.Common/Constants.cs @@ -90,4 +90,5 @@ public class Constants #endregion + public const string CSV_PATH = "D:\\matches.csv"; } \ No newline at end of file diff --git a/Azaion.Common/DTO/Config/AppConfig.cs b/Azaion.Common/DTO/Config/AppConfig.cs index 952ca2e..8c23323 100644 --- a/Azaion.Common/DTO/Config/AppConfig.cs +++ b/Azaion.Common/DTO/Config/AppConfig.cs @@ -8,17 +8,19 @@ namespace Azaion.Common.DTO.Config; public class AppConfig { - public PythonConfig PythonConfig { get; set; } = null!; + public PythonConfig PythonConfig { get; set; } = null!; - public QueueConfig QueueConfig { get; set; } = null!; + public QueueConfig QueueConfig { get; set; } = null!; - public DirectoriesConfig DirectoriesConfig { get; set; } = null!; + public DirectoriesConfig DirectoriesConfig { get; set; } = null!; - public AnnotationConfig AnnotationConfig { get; set; } = null!; + public AnnotationConfig AnnotationConfig { get; set; } = null!; public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!; - public ThumbnailConfig ThumbnailConfig { get; set; } = null!; + public ThumbnailConfig ThumbnailConfig { get; set; } = null!; + + public MapConfig MapConfig{ get; set; } = null!; } public interface IConfigUpdater @@ -80,6 +82,16 @@ public class ConfigUpdater : IConfigUpdater public void Save(AppConfig config) { - File.WriteAllText(SecurityConstants.CONFIG_PATH, JsonConvert.SerializeObject(config, Formatting.Indented), Encoding.UTF8); + //Save without sensitive info + var publicConfig = new + { + PythonConfig = config.PythonConfig, + DirectoriesConfig = config.DirectoriesConfig, + AnnotationConfig = config.AnnotationConfig, + AIRecognitionConfig = config.AIRecognitionConfig, + ThumbnailConfig = config.ThumbnailConfig + }; + + File.WriteAllText(SecurityConstants.CONFIG_PATH, JsonConvert.SerializeObject(publicConfig, Formatting.Indented), Encoding.UTF8); } } diff --git a/Azaion.Common/DTO/Config/MapConfig.cs b/Azaion.Common/DTO/Config/MapConfig.cs new file mode 100644 index 0000000..923316e --- /dev/null +++ b/Azaion.Common/DTO/Config/MapConfig.cs @@ -0,0 +1,7 @@ +namespace Azaion.Common.DTO.Config; + +public class MapConfig +{ + public string Service { get; set; } + public string ApiKey { get; set; } +} \ No newline at end of file diff --git a/Azaion.Common/DTO/GpsCsvResult.cs b/Azaion.Common/DTO/GpsCsvResult.cs new file mode 100644 index 0000000..0b57466 --- /dev/null +++ b/Azaion.Common/DTO/GpsCsvResult.cs @@ -0,0 +1,50 @@ +namespace Azaion.Common.DTO; +using System.Collections.Generic; +using System.IO; + +public class GpsCsvResult +{ + public string Image { get; set; } + public double Latitude { get; set; } + public double Longitude { get; set; } + public int Keypoints { get; set; } + public int Rotation { get; set; } + public string MatchType { get; set; } + + public static List ReadFromCsv(string csvFilePath) + { + var imageDatas = new List(); + + using var reader = new StreamReader(csvFilePath); + //read header + reader.ReadLine(); + if (reader.EndOfStream) + return new List(); + + while (!reader.EndOfStream) + { + var line = reader.ReadLine(); + if (string.IsNullOrWhiteSpace(line)) + continue; + var values = line.Split(','); + if (values.Length == 6) + { + imageDatas.Add(new GpsCsvResult + { + Image = GetFilename(values[0]), + Latitude = double.Parse(values[1]), + Longitude = double.Parse(values[2]), + Keypoints = int.Parse(values[3]), + Rotation = int.Parse(values[4]), + MatchType = values[5] + }); + } + } + + return imageDatas; + } + + private static string GetFilename(string imagePath) => + Path.GetFileNameWithoutExtension(imagePath) + .Replace("-small", ""); +} \ No newline at end of file diff --git a/Azaion.CommonSecurity/Azaion.CommonSecurity.csproj b/Azaion.CommonSecurity/Azaion.CommonSecurity.csproj index 996abc8..64162ea 100644 --- a/Azaion.CommonSecurity/Azaion.CommonSecurity.csproj +++ b/Azaion.CommonSecurity/Azaion.CommonSecurity.csproj @@ -1,7 +1,7 @@  - net8.0 + net8.0-windows enable enable diff --git a/Azaion.Suite/Login.xaml b/Azaion.Suite/Login.xaml index 8b419d6..64b3bce 100644 --- a/Azaion.Suite/Login.xaml +++ b/Azaion.Suite/Login.xaml @@ -74,7 +74,7 @@ BorderBrush="DimGray" BorderThickness="0,0,0,1" HorizontalAlignment="Left" - Text="" + Text="admin@azaion.com" /> + Password="Az@1on1000Odm$n"/>