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"/>