Files
ulapp/FieldLogger/ViewModels/HomeViewModel.cs

169 lines
5.5 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 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()
{
// First launch: nothing saved yet, send the user to device selection.
if (!_manager.HasSavedDevice(DeviceKind.Locator) && !_manager.HasSavedDevice(DeviceKind.RtkGps))
{
await Shell.Current.GoToAsync($"devicescan?kind={DeviceKind.Locator}");
return;
}
_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");
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++;
}
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"));
}