Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ jobs:

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
Expand Down Expand Up @@ -87,6 +87,6 @@ jobs:
exit 1

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"
4 changes: 2 additions & 2 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ jobs:
runs-on: windows-latest

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v6
with:
submodules: recursive
- name: Setup .NET
uses: actions/setup-dotnet@v3
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Restore dependencies
Expand Down
14 changes: 7 additions & 7 deletions PhotoLocator/Controls/CropControl.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,19 +87,19 @@ bool SetProperty<T>(ref T field, T newValue, [CallerMemberName] string? property
return true;
}

public SolidColorBrush? CropBorderColor { get => field; private set => SetProperty(ref field, value); }
public SolidColorBrush? CropBorderColor { get; private set => SetProperty(ref field, value); }

public GridLength CropLeftOffset { get => field; set => SetProperty(ref field, value); }
public GridLength CropLeftOffset { get; set => SetProperty(ref field, value); }

public GridLength CropWidth { get => field; set => SetProperty(ref field, value); }
public GridLength CropWidth { get; set => SetProperty(ref field, value); }

public GridLength CropRightOffset { get => field; set => SetProperty(ref field, value); }
public GridLength CropRightOffset { get; set => SetProperty(ref field, value); }

public GridLength CropTopOffset { get => field; set => SetProperty(ref field, value); }
public GridLength CropTopOffset { get; set => SetProperty(ref field, value); }

public GridLength CropHeight { get => field; set => SetProperty(ref field, value); }
public GridLength CropHeight { get; set => SetProperty(ref field, value); }

public GridLength CropBottomOffset { get => field; set => SetProperty(ref field, value); }
public GridLength CropBottomOffset { get; set => SetProperty(ref field, value); }

public Rect CropRectangle
{
Expand Down
76 changes: 76 additions & 0 deletions PhotoLocator/Helpers/NominatimLocationSearcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using MapControl;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;

namespace PhotoLocator.Helpers
{
public sealed class NominatimLocationSearcher
{
static readonly Uri _searchEndpoint = new("https://nominatim.openstreetmap.org/search");
Comment thread
meesoft marked this conversation as resolved.
static HttpClient? _sharedHttpClient;

static HttpClient CreateHttpClient()
{
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("PhotoLocator");
return httpClient;
}

readonly HttpClient _httpClient;

public NominatimLocationSearcher(HttpClient? httpClient = null)
{
_httpClient = httpClient ?? (_sharedHttpClient ??= CreateHttpClient());
}

public async Task<IReadOnlyList<LocationWithName>> SearchAsync(string searchText, int resultLimit = 10, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(searchText);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(resultLimit);

var requestUri = new UriBuilder(_searchEndpoint)
{
Query = $"q={Uri.EscapeDataString(searchText)}&format=jsonv2&limit={resultLimit}"
}.Uri;

var results = await _httpClient.GetFromJsonAsync<NominatimResult[]>(requestUri, cancellationToken)
.ConfigureAwait(false);

if (results is null)
return [];

return results
.Where(result =>
result.DisplayName is not null &&
double.TryParse(result.Latitude, NumberStyles.Float, CultureInfo.InvariantCulture, out _) &&
double.TryParse(result.Longitude, NumberStyles.Float, CultureInfo.InvariantCulture, out _))
.Select(result => new LocationWithName(
result.DisplayName!,
new Location(
double.Parse(result.Latitude!, CultureInfo.InvariantCulture),
double.Parse(result.Longitude!, CultureInfo.InvariantCulture))))
.ToArray();
}

sealed class NominatimResult
{
[JsonPropertyName("display_name")]
public string? DisplayName { get; set; }

[JsonPropertyName("lat")]
public string? Latitude { get; set; }

[JsonPropertyName("lon")]
public string? Longitude { get; set; }
}
}

public sealed record LocationWithName(string DisplayName, Location Location);
}
97 changes: 97 additions & 0 deletions PhotoLocator/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,16 @@ public sealed class MainViewModel : INotifyPropertyChanged, IDisposable, IMainVi
CancellationTokenSource? _loadCancellation;
CancellationTokenSource? _previewCancellation;
CancellationTokenSource? _processCancellation;
CancellationTokenSource? _locationSearchCancellation;
FileSystemWatcher? _fileSystemWatcher;
double _loadImagesProgress;
bool _titleUpdatePending;
bool _loadPicturesPending;
readonly List<(string Path, BitmapSource Picture)> _pictureCache = [];
readonly HashSet<string> _gpsTraceFiles = [];
Location? _sunAndMoonMapCenter;
NominatimLocationSearcher? _locationSearcher;
LocationWithName? _previewedLocationSearchResult;

public event PropertyChangedEventHandler? PropertyChanged;

Expand Down Expand Up @@ -149,6 +152,93 @@ public Location? MapCenter
set => SetProperty(ref field, value);
}

public bool IsLocationSearchVisible
{
get;
set
{
if (SetProperty(ref field, value) && !value)
{
_locationSearchCancellation?.Cancel();
_previewedLocationSearchResult = null;
SelectedLocationSearchResult = null;
}
}
}
Comment thread
meesoft marked this conversation as resolved.

public string LocationSearchText
{
get;
set
{
if (SetProperty(ref field, value))
SearchLocationsAsync(value).WithExceptionLogging();
Comment thread
meesoft marked this conversation as resolved.
}
} = string.Empty;

public ObservableCollection<LocationWithName> LocationSearchResults { get; } = [];

public LocationWithName? SelectedLocationSearchResult
{
get;
set
{
if (!SetProperty(ref field, value) || value is null)
return;
MapCenter = value.Location;
if (!IsMapVisible)
ViewModeCommand?.Execute(null);
IsLocationSearchVisible = false;
}
}

public void PreviewLocationSearchResult(LocationWithName result)
{
if (ReferenceEquals(_previewedLocationSearchResult, result))
return;
_previewedLocationSearchResult = result;
MapCenter = result.Location;
}

async Task SearchLocationsAsync(string searchText)
{
#pragma warning disable CA1849 // Call async methods when in an async method
_locationSearchCancellation?.Cancel();
#pragma warning restore CA1849 // Call async methods when in an async method
if (string.IsNullOrWhiteSpace(searchText))
return;
_locationSearchCancellation?.Dispose();
_locationSearchCancellation = null;

Mouse.OverrideCursor = Cursors.AppStarting;
var cancellation = new CancellationTokenSource();
var ct = cancellation.Token;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
_locationSearchCancellation = cancellation;
try
{
await Task.Delay(1000, ct);
_locationSearcher ??= new NominatimLocationSearcher();
var results = await _locationSearcher.SearchAsync(searchText, 10, ct);
if (ReferenceEquals(_locationSearchCancellation, cancellation))
{
LocationSearchResults.Clear();
foreach (var result in results)
LocationSearchResults.Add(result);
}
}
finally
{
if (ReferenceEquals(_locationSearchCancellation, cancellation))
{
_locationSearchCancellation = null;
cancellation.Dispose();
Mouse.OverrideCursor = null;
}
}
}

public ICommand LocationSearchCommand => new RelayCommand(o => IsLocationSearchVisible = !IsLocationSearchVisible);

public Location? SavedLocation
{
get;
Expand Down Expand Up @@ -245,6 +335,8 @@ public PictureItemViewModel? SelectedItem

public IEnumerable<PictureItemViewModel> GetSelectedItems(bool filesOnly)
{
IsLocationSearchVisible = false;
IsCropControlVisible = false;
var firstChecked = SelectedItem is not null && SelectedItem.IsChecked
&& (SelectedItem.IsFile || !filesOnly)
? SelectedItem : null;
Expand Down Expand Up @@ -461,6 +553,8 @@ public async Task UpdatePreviewPictureAsync(string? skipTo = null)
public async Task RunProcessWithProgressBarAsync(Func<Action<double>, CancellationToken, Task> body, string text, PictureItemViewModel? focusItem = null)
{
using var cursor = new MouseCursorOverride();
IsLocationSearchVisible = false;
IsCropControlVisible = false;
ProgressBarIsIndeterminate = false;
ProgressBarValue = 0;
TaskbarProgressState = TaskbarItemProgressState.Normal;
Expand Down Expand Up @@ -1400,6 +1494,9 @@ public void Dispose()
_processCancellation?.Cancel();
_processCancellation?.Dispose();
_processCancellation = null;
_locationSearchCancellation?.Cancel();
_locationSearchCancellation?.Dispose();
_locationSearchCancellation = null;
DisposeFileSystemWatcher();
}
}
Expand Down
24 changes: 22 additions & 2 deletions PhotoLocator/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<KeyBinding Key="D4" Modifiers="Ctrl" Command="{Binding Zoom400Command}" />
<KeyBinding Key="E" Modifiers="Ctrl" Command="{Binding ExploreCommand}" />
<KeyBinding Key="F" Modifiers="Ctrl" Command="{Binding QuickSearchCommand}" />
<KeyBinding Key="G" Modifiers="Ctrl" Command="{Binding LocationSearchCommand}" />
<KeyBinding Key="L" Modifiers="Ctrl" Command="{Binding ImageTransformCommands.LocalContrastCommand}" />
<KeyBinding Key="O" Modifiers="Ctrl" Command="{Binding BrowseForPhotosCommand}" />
<KeyBinding Key="P" Modifiers="Ctrl" Command="{Binding VideoTransformCommandsShared.ProcessSelected}" />
Expand Down Expand Up @@ -121,6 +122,25 @@
<Button Content="Save changes" Width="100" Command="{Binding SaveGeotagsCommand}"
ToolTip="Save changed geotags to photo files (Ctrl+S)" />

<Button Content="&#xE707;" FontFamily="Segoe MDL2 Assets" FontSize="18" Command="{Binding LocationSearchCommand}"
ToolTip="Search for a location (Ctrl+G)" Width="32" Name="LocationSearchButton" />
<Popup IsOpen="{Binding IsLocationSearchVisible}" PlacementTarget="{Binding ElementName=LocationSearchButton}" Placement="Bottom" >
<Border BorderBrush="Gray" BorderThickness="1" CornerRadius="2">
<StackPanel>
<TextBox Name="LocationSearchTextBox" Text="{Binding LocationSearchText, UpdateSourceTrigger=PropertyChanged}"
Height="22" ToolTip="Enter a city, street, or place name" PreviewKeyUp="HandleLocationSearchKeyUp"/>
<ListBox Width="500" ItemsSource="{Binding LocationSearchResults}" SelectedItem="{Binding SelectedLocationSearchResult}"
PreviewMouseMove="HandleLocationSearchResultMouseMove">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName}" TextWrapping="Wrap" Margin="3" Foreground="White" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Border>
</Popup>

<Button Content="&#xE8AC;" FontFamily="Segoe MDL2 Assets" FontSize="18" Command="{Binding RenameCommand}"
ToolTip="Rename (F2)" Width="32" />
<Button Content="&#xECC5;" FontFamily="Segoe MDL2 Assets" FontSize="18" Command="{Binding AdjustTimestampsCommand}"
Expand All @@ -136,7 +156,7 @@
<Button Content="&#xE768;" FontFamily="Segoe MDL2 Assets" FontSize="18" Command="{Binding SlideShowCommand}"
ToolTip="Full screen slide show with map (F3)" Width="32" />
<Button Content="&#xE721;" FontFamily="Segoe MDL2 Assets" FontSize="18" Command="{Binding QuickSearchCommand}"
ToolTip="Search (Ctrl+F)" Width="32"/>
ToolTip="File search (Ctrl+F)" Width="32"/>
<Button Content="&#xE713;" FontFamily="Segoe MDL2 Assets" FontSize="18" Command="{Binding SettingsCommand}"
ToolTip="Settings (F12)" Width="32" />
<Button Content="&#xE946;" FontFamily="Segoe MDL2 Assets" FontSize="18" Command="{Binding AboutCommand}"
Expand Down Expand Up @@ -176,7 +196,7 @@
<MenuItem Header="Non-geotagged (Num+)" Command="{Binding SelectCandidatesCommand}" />
<MenuItem Header="None (Num-)" Command="{Binding DeselectAllCommand}" />
<MenuItem Header="Invert (Num*)" Command="{Binding InvertSelectionCommand}" />
<MenuItem Header="Search (Ctrl+F)" Command="{Binding QuickSearchCommand}">
<MenuItem Header="File search (Ctrl+F)" Command="{Binding QuickSearchCommand}">
<MenuItem.Icon>
<TextBlock FontFamily="Segoe MDL2 Assets" Text="&#xE721;" FontSize="14" />
</MenuItem.Icon>
Expand Down
38 changes: 38 additions & 0 deletions PhotoLocator/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,44 @@ private void HandleViewModelPropertyChanged(object? sender, PropertyChangedEvent
if (_viewModel.IsCropControlVisible)
ShowCropControl();
}
else if (e.PropertyName is nameof(_viewModel.IsLocationSearchVisible))
{
Dispatcher.BeginInvoke(() =>
{
if (_viewModel.IsLocationSearchVisible)
{
LocationSearchTextBox.Focus();
LocationSearchTextBox.SelectAll();
}
else
{
FocusListBoxItem(PictureListBox.SelectedItem);
}
});
}
}

private void HandleLocationSearchResultMouseMove(object sender, MouseEventArgs e)
{
if (e.OriginalSource is not DependencyObject source)
return;
if (ItemsControl.ContainerFromElement((ItemsControl)sender, source) is ListBoxItem item &&
item.DataContext is LocationWithName result)
_viewModel.PreviewLocationSearchResult(result);
}

private void HandleLocationSearchKeyUp(object sender, KeyEventArgs e)
{
if ((e.Key == Key.Enter || e.Key == Key.Down) && _viewModel.LocationSearchResults.Count > 0)
{
e.Handled = true;
_viewModel.SelectedLocationSearchResult = _viewModel.LocationSearchResults[0];
}
else if (e.Key == Key.Escape)
{
e.Handled = true;
_viewModel.IsLocationSearchVisible = false;
}
}

private void UpdateLogView()
Expand Down
2 changes: 1 addition & 1 deletion PhotoLocator/PictureItemView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<Image Source="{Binding ThumbnailImage}" VerticalAlignment="Top"/>

<Grid Margin="2" >
<CheckBox IsChecked="{Binding IsChecked}" HorizontalAlignment="Left" VerticalAlignment="Top" />
<CheckBox IsChecked="{Binding IsChecked}" HorizontalAlignment="Left" VerticalAlignment="Top" Focusable="False" />

<TextBlock Text="&#xE840;" FontSize="18" FontFamily="Segoe MDL2 Assets"
VerticalAlignment="Top" HorizontalAlignment="Right" Foreground="White"
Expand Down
Loading