191 lines
6.0 KiB
C#
191 lines
6.0 KiB
C#
using System.Collections.ObjectModel;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using FieldLogger.Models;
|
|
using FieldLogger.Services;
|
|
using FieldLogger.Services.Data;
|
|
using FieldLogger.Services.Sync;
|
|
|
|
namespace FieldLogger.ViewModels;
|
|
|
|
public sealed partial class HomeViewModel : ObservableObject
|
|
{
|
|
private readonly DeviceConnectionManager _manager;
|
|
private readonly MaglinkService _gps;
|
|
private readonly PointLogger _pointLogger;
|
|
private readonly AppDatabase _database;
|
|
private readonly SettingsService _settings;
|
|
private readonly IMqttSyncService _sync;
|
|
|
|
public DeviceConnectionManager Manager => _manager;
|
|
|
|
[ObservableProperty]
|
|
private string _activeJobName = "No active job";
|
|
|
|
[ObservableProperty]
|
|
private bool _hasActiveJob;
|
|
|
|
[ObservableProperty]
|
|
private int _pointCount;
|
|
|
|
[ObservableProperty]
|
|
private string _fixSummary = "Waiting for GPS…";
|
|
|
|
[ObservableProperty]
|
|
private string _fixStatusLabel = "—";
|
|
|
|
[ObservableProperty]
|
|
private string _fixAccuracy = "";
|
|
|
|
[ObservableProperty]
|
|
private string _syncStatus = "";
|
|
|
|
public bool HasNoActiveJob => !HasActiveJob;
|
|
|
|
public bool IsDeviceConsoleAvailable { get; } =
|
|
#if DEBUG
|
|
true;
|
|
#else
|
|
false;
|
|
#endif
|
|
|
|
public string ActiveJobPointSummary => PointCount == 1
|
|
? "1 point saved locally"
|
|
: $"{PointCount} points saved locally";
|
|
|
|
public string CaptureReadinessText => HasActiveJob
|
|
? "READY · Press LOG on the locating receiver"
|
|
: "BLOCKED · Choose or create an active job first";
|
|
|
|
public ObservableCollection<LoggedPoint> RecentPoints { get; } = new();
|
|
|
|
public HomeViewModel(DeviceConnectionManager manager, MaglinkService gps,
|
|
PointLogger pointLogger, AppDatabase database, SettingsService settings, IMqttSyncService sync)
|
|
{
|
|
_manager = manager;
|
|
_gps = gps;
|
|
_pointLogger = pointLogger;
|
|
_database = database;
|
|
_settings = settings;
|
|
_sync = sync;
|
|
SyncStatus = sync.Status;
|
|
|
|
_gps.FixReceived += OnFixReceived;
|
|
_pointLogger.PointSaved += OnPointSaved;
|
|
_pointLogger.PacketIgnoredNoJob += OnPacketIgnoredNoJob;
|
|
_sync.StatusChanged += OnSyncStatusChanged;
|
|
_sync.PointRejected += OnPointRejected;
|
|
}
|
|
|
|
/// <summary>Called from the page's OnAppearing.</summary>
|
|
public async Task InitializeAsync()
|
|
{
|
|
_manager.Start();
|
|
await RefreshJobAsync();
|
|
}
|
|
|
|
public async Task RefreshJobAsync()
|
|
{
|
|
var jobId = _settings.ActiveJobId;
|
|
if (jobId is null)
|
|
{
|
|
HasActiveJob = false;
|
|
ActiveJobName = "No active job";
|
|
PointCount = 0;
|
|
RecentPoints.Clear();
|
|
return;
|
|
}
|
|
|
|
var job = await _database.GetJobAsync(jobId.Value);
|
|
if (job is null)
|
|
{
|
|
_settings.ActiveJobId = null;
|
|
await RefreshJobAsync();
|
|
return;
|
|
}
|
|
|
|
HasActiveJob = true;
|
|
ActiveJobName = job.Name;
|
|
PointCount = await _database.GetPointCountAsync(job.Id);
|
|
|
|
var points = await _database.GetPointsAsync(job.Id);
|
|
RecentPoints.Clear();
|
|
foreach (var p in points.OrderByDescending(p => p.TimestampUtc).Take(10))
|
|
RecentPoints.Add(p);
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task NewJobAsync()
|
|
{
|
|
var name = await Shell.Current.DisplayPromptAsync("New Job", "Job name:",
|
|
placeholder: $"Job {DateTime.Now:yyyy-MM-dd}");
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
return;
|
|
|
|
var job = await _database.CreateJobAsync(name.Trim());
|
|
_settings.ActiveJobId = job.Id;
|
|
await RefreshJobAsync();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private Task SelectJobAsync() => Shell.Current.GoToAsync("//jobs");
|
|
|
|
[RelayCommand]
|
|
private Task SelectLocatorAsync() => Shell.Current.GoToAsync($"devicescan?kind={DeviceKind.Locator}");
|
|
|
|
[RelayCommand]
|
|
private Task SelectGpsAsync() => Shell.Current.GoToAsync($"devicescan?kind={DeviceKind.RtkGps}");
|
|
|
|
[RelayCommand]
|
|
private Task ViewMapAsync() => Shell.Current.GoToAsync("//map");
|
|
|
|
[RelayCommand]
|
|
private Task OpenDeviceConsoleAsync() => Shell.Current.GoToAsync("deviceconsole");
|
|
|
|
private void OnFixReceived(object? sender, GnssFix fix)
|
|
{
|
|
MainThread.BeginInvokeOnMainThread(() =>
|
|
{
|
|
FixStatusLabel = fix.StatusLabel;
|
|
FixSummary = fix.HasPosition
|
|
? $"{fix.Latitude:F8}, {fix.Longitude:F8} · {fix.AltitudeCorrected:F2} m"
|
|
: "No position";
|
|
FixAccuracy = $"H ±{fix.Hrms:F3} m · V ±{fix.Vrms:F3} m · {fix.SatellitesUsed} sats · batt {fix.BatteryPercent}%";
|
|
});
|
|
}
|
|
|
|
private void OnPointSaved(object? sender, LoggedPoint point)
|
|
{
|
|
RecentPoints.Insert(0, point);
|
|
while (RecentPoints.Count > 10)
|
|
RecentPoints.RemoveAt(RecentPoints.Count - 1);
|
|
PointCount++;
|
|
}
|
|
|
|
partial void OnHasActiveJobChanged(bool value)
|
|
{
|
|
OnPropertyChanged(nameof(HasNoActiveJob));
|
|
OnPropertyChanged(nameof(CaptureReadinessText));
|
|
}
|
|
|
|
partial void OnPointCountChanged(int value) =>
|
|
OnPropertyChanged(nameof(ActiveJobPointSummary));
|
|
|
|
private async void OnPacketIgnoredNoJob(object? sender, EventArgs e)
|
|
{
|
|
await Shell.Current.DisplayAlert("No Active Job",
|
|
"A point was logged on the receiver, but no job is active so it was not saved. Create or select a job first.",
|
|
"OK");
|
|
}
|
|
|
|
private void OnSyncStatusChanged(object? sender, string status) =>
|
|
MainThread.BeginInvokeOnMainThread(() => SyncStatus = status);
|
|
|
|
private void OnPointRejected(object? sender, PointSyncFailureEventArgs e) =>
|
|
MainThread.BeginInvokeOnMainThread(async () =>
|
|
await Shell.Current.DisplayAlert(
|
|
"Point Saved Locally — Sync Needs Attention",
|
|
$"Point #{e.Point.Id} remains on this device and was not uploaded. Reason: {e.ReasonCode}.",
|
|
"OK"));
|
|
}
|