Initial commit: FieldLogger MAUI app with Maglink BLE support
Added Maglink RTK GNSS receiver integration with correct BLE UUIDs and device name filtering (ML-* prefix). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
44
FieldLogger/Views/DeviceScanPage.xaml
Normal file
44
FieldLogger/Views/DeviceScanPage.xaml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
||||
xmlns:ble="clr-namespace:FieldLogger.Services.Ble"
|
||||
x:Class="FieldLogger.Views.DeviceScanPage"
|
||||
x:DataType="vm:DeviceScanViewModel"
|
||||
Title="{Binding Title}">
|
||||
<Grid RowDefinitions="Auto,Auto,*" Padding="16" RowSpacing="12">
|
||||
|
||||
<Label Grid.Row="0" Text="{Binding Hint}" FontSize="13" TextColor="Gray" />
|
||||
|
||||
<HorizontalStackLayout Grid.Row="1" Spacing="12">
|
||||
<Button Text="{Binding ScanButtonText}" Command="{Binding ScanCommand}" IsEnabled="{Binding IsScanning, Converter={StaticResource InvertBool}}" />
|
||||
<ActivityIndicator IsRunning="{Binding IsScanning}" VerticalOptions="Center" />
|
||||
</HorizontalStackLayout>
|
||||
|
||||
<CollectionView Grid.Row="2" ItemsSource="{Binding Devices}">
|
||||
<CollectionView.EmptyView>
|
||||
<Label Text="No devices found yet." TextColor="Gray" Margin="8" />
|
||||
</CollectionView.EmptyView>
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="ble:DiscoveredDevice">
|
||||
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}"
|
||||
StrokeShape="RoundRectangle 10" Padding="12" Margin="0,4">
|
||||
<Border.GestureRecognizers>
|
||||
<TapGestureRecognizer
|
||||
Command="{Binding SelectCommand, Source={RelativeSource AncestorType={x:Type vm:DeviceScanViewModel}}}"
|
||||
CommandParameter="{Binding .}" />
|
||||
</Border.GestureRecognizers>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<VerticalStackLayout Spacing="2">
|
||||
<Label Text="{Binding Name}" FontAttributes="Bold" />
|
||||
<Label Text="{Binding Id}" FontSize="11" TextColor="Gray" />
|
||||
</VerticalStackLayout>
|
||||
<Label Grid.Column="1" VerticalOptions="Center" FontSize="12" TextColor="Gray"
|
||||
Text="{Binding Rssi, StringFormat='{0} dBm'}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</CollectionView>
|
||||
</Grid>
|
||||
</ContentPage>
|
||||
26
FieldLogger/Views/DeviceScanPage.xaml.cs
Normal file
26
FieldLogger/Views/DeviceScanPage.xaml.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using FieldLogger.ViewModels;
|
||||
|
||||
namespace FieldLogger.Views;
|
||||
|
||||
public partial class DeviceScanPage : ContentPage
|
||||
{
|
||||
private readonly DeviceScanViewModel _viewModel;
|
||||
|
||||
public DeviceScanPage(DeviceScanViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
BindingContext = _viewModel = viewModel;
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _viewModel.ScanAsync();
|
||||
}
|
||||
|
||||
protected override void OnDisappearing()
|
||||
{
|
||||
_viewModel.StopScan();
|
||||
base.OnDisappearing();
|
||||
}
|
||||
}
|
||||
108
FieldLogger/Views/HomePage.xaml
Normal file
108
FieldLogger/Views/HomePage.xaml
Normal file
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
||||
xmlns:models="clr-namespace:FieldLogger.Models"
|
||||
x:Class="FieldLogger.Views.HomePage"
|
||||
x:DataType="vm:HomeViewModel"
|
||||
Title="Field Logger">
|
||||
<ScrollView>
|
||||
<VerticalStackLayout Padding="16" Spacing="12">
|
||||
|
||||
<!-- Device connection status -->
|
||||
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}" StrokeShape="RoundRectangle 12" Padding="12">
|
||||
<Grid ColumnDefinitions="16,*,Auto" RowDefinitions="Auto,Auto" RowSpacing="10" ColumnSpacing="10">
|
||||
<Ellipse Grid.Row="0" Grid.Column="0" WidthRequest="12" HeightRequest="12" VerticalOptions="Center"
|
||||
Fill="{Binding Manager.LocatorState, Converter={StaticResource StateColor}}" />
|
||||
<VerticalStackLayout Grid.Row="0" Grid.Column="1" Spacing="0">
|
||||
<Label Text="{Binding Manager.LocatorName, TargetNullValue='Locating Receiver — not selected'}" FontAttributes="Bold" />
|
||||
<Label Text="{Binding Manager.LocatorState, Converter={StaticResource StateText}}" FontSize="12" TextColor="Gray" />
|
||||
</VerticalStackLayout>
|
||||
<Button Grid.Row="0" Grid.Column="2" Text="Select" FontSize="12" Padding="10,4"
|
||||
Command="{Binding SelectLocatorCommand}" />
|
||||
|
||||
<Ellipse Grid.Row="1" Grid.Column="0" WidthRequest="12" HeightRequest="12" VerticalOptions="Center"
|
||||
Fill="{Binding Manager.GpsState, Converter={StaticResource StateColor}}" />
|
||||
<VerticalStackLayout Grid.Row="1" Grid.Column="1" Spacing="0">
|
||||
<Label Text="{Binding Manager.GpsName, TargetNullValue='Maglink RTK GPS — not selected'}" FontAttributes="Bold" />
|
||||
<Label Text="{Binding Manager.GpsState, Converter={StaticResource StateText}}" FontSize="12" TextColor="Gray" />
|
||||
</VerticalStackLayout>
|
||||
<Button Grid.Row="1" Grid.Column="2" Text="Select" FontSize="12" Padding="10,4"
|
||||
Command="{Binding SelectGpsCommand}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Live GNSS fix -->
|
||||
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}" StrokeShape="RoundRectangle 12" Padding="12">
|
||||
<VerticalStackLayout Spacing="4">
|
||||
<HorizontalStackLayout Spacing="8">
|
||||
<Label Text="GPS" FontAttributes="Bold" />
|
||||
<Label Text="{Binding FixStatusLabel}" TextColor="DodgerBlue" FontAttributes="Bold" />
|
||||
</HorizontalStackLayout>
|
||||
<Label Text="{Binding FixSummary}" FontFamily="Courier New" FontSize="13" />
|
||||
<Label Text="{Binding FixAccuracy}" FontSize="12" TextColor="Gray" />
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
|
||||
<!-- Active job -->
|
||||
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}" StrokeShape="RoundRectangle 12" Padding="12">
|
||||
<VerticalStackLayout Spacing="8">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<VerticalStackLayout Spacing="0">
|
||||
<Label Text="{Binding ActiveJobName}" FontAttributes="Bold" FontSize="16" />
|
||||
<Label FontSize="12" TextColor="Gray">
|
||||
<Label.Text>
|
||||
<Binding Path="PointCount" StringFormat="{}{0} points logged" />
|
||||
</Label.Text>
|
||||
</Label>
|
||||
</VerticalStackLayout>
|
||||
<HorizontalStackLayout Grid.Column="1" Spacing="6">
|
||||
<Button Text="New Job" FontSize="12" Padding="10,4" Command="{Binding NewJobCommand}" />
|
||||
<Button Text="Jobs" FontSize="12" Padding="10,4" Command="{Binding SelectJobCommand}" />
|
||||
</HorizontalStackLayout>
|
||||
</Grid>
|
||||
<Label Text="Press the log button on the receiver to capture a point."
|
||||
FontSize="12" TextColor="Gray" IsVisible="{Binding HasActiveJob}" />
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
|
||||
<!-- Recent points -->
|
||||
<Label Text="Recent Points" FontAttributes="Bold" Margin="4,8,0,0" />
|
||||
<CollectionView ItemsSource="{Binding RecentPoints}" HeightRequest="320">
|
||||
<CollectionView.EmptyView>
|
||||
<Label Text="No points logged yet." TextColor="Gray" Margin="8" />
|
||||
</CollectionView.EmptyView>
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:LoggedPoint">
|
||||
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#FAFAFA, Dark=#242426}"
|
||||
StrokeShape="RoundRectangle 8" Padding="10" Margin="0,2">
|
||||
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto">
|
||||
<Label Grid.Row="0" FontSize="13" FontAttributes="Bold">
|
||||
<Label.Text>
|
||||
<MultiBinding StringFormat="{}{0} · {1} Hz · depth {2}">
|
||||
<Binding Path="Utility" Converter="{StaticResource UtilityName}" />
|
||||
<Binding Path="Frequency" />
|
||||
<Binding Path="DepthRaw" />
|
||||
</MultiBinding>
|
||||
</Label.Text>
|
||||
</Label>
|
||||
<Label Grid.Row="0" Grid.Column="1" FontSize="12" TextColor="Gray"
|
||||
Text="{Binding TimestampUtc, StringFormat='{0:HH:mm:ss}'}" />
|
||||
<Label Grid.Row="1" Grid.ColumnSpan="2" FontSize="12" TextColor="Gray">
|
||||
<Label.Text>
|
||||
<MultiBinding StringFormat="{}{0:F7}, {1:F7} · {2}">
|
||||
<Binding Path="Latitude" />
|
||||
<Binding Path="Longitude" />
|
||||
<Binding Path="FixStatus" Converter="{StaticResource FixStatusName}" />
|
||||
</MultiBinding>
|
||||
</Label.Text>
|
||||
</Label>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</CollectionView>
|
||||
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
</ContentPage>
|
||||
29
FieldLogger/Views/HomePage.xaml.cs
Normal file
29
FieldLogger/Views/HomePage.xaml.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using FieldLogger.ViewModels;
|
||||
|
||||
namespace FieldLogger.Views;
|
||||
|
||||
public partial class HomePage : ContentPage
|
||||
{
|
||||
private readonly HomeViewModel _viewModel;
|
||||
private bool _initialized;
|
||||
|
||||
public HomePage(HomeViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
BindingContext = _viewModel = viewModel;
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
if (!_initialized)
|
||||
{
|
||||
_initialized = true;
|
||||
await _viewModel.InitializeAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await _viewModel.RefreshJobAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
77
FieldLogger/Views/JobDetailPage.xaml
Normal file
77
FieldLogger/Views/JobDetailPage.xaml
Normal file
@@ -0,0 +1,77 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
||||
xmlns:models="clr-namespace:FieldLogger.Models"
|
||||
x:Class="FieldLogger.Views.JobDetailPage"
|
||||
x:DataType="vm:JobDetailViewModel"
|
||||
Title="{Binding JobName}">
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="Map" Command="{Binding ViewMapCommand}" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<Grid RowDefinitions="Auto,*" Padding="16" RowSpacing="8">
|
||||
<Label Grid.Row="0" FontSize="13" TextColor="Gray"
|
||||
Text="{Binding PointCount, StringFormat='{0} logged points'}" />
|
||||
|
||||
<CollectionView Grid.Row="1" ItemsSource="{Binding Points}">
|
||||
<CollectionView.EmptyView>
|
||||
<Label Text="No points logged for this job yet." TextColor="Gray" Margin="8" />
|
||||
</CollectionView.EmptyView>
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="models:LoggedPoint">
|
||||
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}"
|
||||
StrokeShape="RoundRectangle 10" Padding="12" Margin="0,4">
|
||||
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto,Auto,Auto" RowSpacing="2">
|
||||
<Label Grid.Row="0" FontAttributes="Bold" FontSize="14">
|
||||
<Label.Text>
|
||||
<MultiBinding StringFormat="{}#{0} · {1} · {2} Hz">
|
||||
<Binding Path="Id" />
|
||||
<Binding Path="Utility" Converter="{StaticResource UtilityName}" />
|
||||
<Binding Path="Frequency" />
|
||||
</MultiBinding>
|
||||
</Label.Text>
|
||||
</Label>
|
||||
<Label Grid.Row="0" Grid.Column="1" FontSize="12" TextColor="Gray"
|
||||
Text="{Binding TimestampUtc, StringFormat='{0:g}'}" />
|
||||
|
||||
<Label Grid.Row="1" Grid.ColumnSpan="2" FontSize="12">
|
||||
<Label.Text>
|
||||
<MultiBinding StringFormat="Depth {0} · Current {1} · Signal {2} · Gain {3} dB">
|
||||
<Binding Path="DepthRaw" />
|
||||
<Binding Path="CurrentRaw" />
|
||||
<Binding Path="Signal" />
|
||||
<Binding Path="GainDb" />
|
||||
</MultiBinding>
|
||||
</Label.Text>
|
||||
</Label>
|
||||
|
||||
<Label Grid.Row="2" Grid.ColumnSpan="2" FontSize="12" FontFamily="Courier New">
|
||||
<Label.Text>
|
||||
<MultiBinding StringFormat="{}{0:F8}, {1:F8}">
|
||||
<Binding Path="Latitude" />
|
||||
<Binding Path="Longitude" />
|
||||
</MultiBinding>
|
||||
</Label.Text>
|
||||
</Label>
|
||||
|
||||
<Label Grid.Row="3" FontSize="11" TextColor="Gray">
|
||||
<Label.Text>
|
||||
<MultiBinding StringFormat="{}{0} · H ±{1:F3} m · {2} sats">
|
||||
<Binding Path="FixStatus" Converter="{StaticResource FixStatusName}" />
|
||||
<Binding Path="Hrms" />
|
||||
<Binding Path="SatellitesUsed" />
|
||||
</MultiBinding>
|
||||
</Label.Text>
|
||||
</Label>
|
||||
<Button Grid.Row="3" Grid.Column="1" Text="Delete" FontSize="11" Padding="8,2"
|
||||
BackgroundColor="Transparent" TextColor="IndianRed"
|
||||
Command="{Binding DeletePointCommand, Source={RelativeSource AncestorType={x:Type vm:JobDetailViewModel}}}"
|
||||
CommandParameter="{Binding .}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</CollectionView>
|
||||
</Grid>
|
||||
</ContentPage>
|
||||
20
FieldLogger/Views/JobDetailPage.xaml.cs
Normal file
20
FieldLogger/Views/JobDetailPage.xaml.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using FieldLogger.ViewModels;
|
||||
|
||||
namespace FieldLogger.Views;
|
||||
|
||||
public partial class JobDetailPage : ContentPage
|
||||
{
|
||||
private readonly JobDetailViewModel _viewModel;
|
||||
|
||||
public JobDetailPage(JobDetailViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
BindingContext = _viewModel = viewModel;
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _viewModel.RefreshAsync();
|
||||
}
|
||||
}
|
||||
60
FieldLogger/Views/JobsPage.xaml
Normal file
60
FieldLogger/Views/JobsPage.xaml
Normal file
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
||||
x:Class="FieldLogger.Views.JobsPage"
|
||||
x:DataType="vm:JobsViewModel"
|
||||
Title="Jobs">
|
||||
<ContentPage.ToolbarItems>
|
||||
<ToolbarItem Text="New" Command="{Binding NewJobCommand}" />
|
||||
</ContentPage.ToolbarItems>
|
||||
|
||||
<Grid Padding="16">
|
||||
<CollectionView ItemsSource="{Binding Jobs}">
|
||||
<CollectionView.EmptyView>
|
||||
<VerticalStackLayout Spacing="8" VerticalOptions="Center">
|
||||
<Label Text="No jobs yet." TextColor="Gray" HorizontalOptions="Center" />
|
||||
<Button Text="Create First Job" Command="{Binding NewJobCommand}" HorizontalOptions="Center" />
|
||||
</VerticalStackLayout>
|
||||
</CollectionView.EmptyView>
|
||||
<CollectionView.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:JobListItem">
|
||||
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}"
|
||||
StrokeShape="RoundRectangle 10" Padding="12" Margin="0,4">
|
||||
<Border.GestureRecognizers>
|
||||
<TapGestureRecognizer
|
||||
Command="{Binding OpenCommand, Source={RelativeSource AncestorType={x:Type vm:JobsViewModel}}}"
|
||||
CommandParameter="{Binding .}" />
|
||||
</Border.GestureRecognizers>
|
||||
<Grid ColumnDefinitions="*,Auto,Auto" ColumnSpacing="8">
|
||||
<VerticalStackLayout Spacing="2">
|
||||
<HorizontalStackLayout Spacing="8">
|
||||
<Label Text="{Binding Name}" FontAttributes="Bold" />
|
||||
<Border IsVisible="{Binding IsActive}" Background="DodgerBlue"
|
||||
StrokeThickness="0" StrokeShape="RoundRectangle 6" Padding="6,1">
|
||||
<Label Text="ACTIVE" FontSize="10" TextColor="White" />
|
||||
</Border>
|
||||
</HorizontalStackLayout>
|
||||
<Label FontSize="12" TextColor="Gray">
|
||||
<Label.Text>
|
||||
<MultiBinding StringFormat="{}{0} · {1} points">
|
||||
<Binding Path="CreatedLabel" />
|
||||
<Binding Path="PointCount" />
|
||||
</MultiBinding>
|
||||
</Label.Text>
|
||||
</Label>
|
||||
</VerticalStackLayout>
|
||||
<Button Grid.Column="1" Text="Set Active" FontSize="12" Padding="10,4" VerticalOptions="Center"
|
||||
Command="{Binding SetActiveCommand, Source={RelativeSource AncestorType={x:Type vm:JobsViewModel}}}"
|
||||
CommandParameter="{Binding .}" />
|
||||
<Button Grid.Column="2" Text="✕" FontSize="12" Padding="10,4" VerticalOptions="Center"
|
||||
BackgroundColor="IndianRed" TextColor="White"
|
||||
Command="{Binding DeleteCommand, Source={RelativeSource AncestorType={x:Type vm:JobsViewModel}}}"
|
||||
CommandParameter="{Binding .}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</CollectionView.ItemTemplate>
|
||||
</CollectionView>
|
||||
</Grid>
|
||||
</ContentPage>
|
||||
20
FieldLogger/Views/JobsPage.xaml.cs
Normal file
20
FieldLogger/Views/JobsPage.xaml.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using FieldLogger.ViewModels;
|
||||
|
||||
namespace FieldLogger.Views;
|
||||
|
||||
public partial class JobsPage : ContentPage
|
||||
{
|
||||
private readonly JobsViewModel _viewModel;
|
||||
|
||||
public JobsPage(JobsViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
BindingContext = _viewModel = viewModel;
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await _viewModel.RefreshAsync();
|
||||
}
|
||||
}
|
||||
13
FieldLogger/Views/MapPage.xaml
Normal file
13
FieldLogger/Views/MapPage.xaml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
||||
x:Class="FieldLogger.Views.MapPage"
|
||||
x:DataType="vm:MapViewModel"
|
||||
Title="Map">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<Label Grid.Row="0" Text="{Binding StatusText}" FontSize="12" TextColor="Gray" Padding="12,6" />
|
||||
<!-- Populated in code-behind: native Map control on iOS/Android/macOS, Google Maps WebView on Windows. -->
|
||||
<ContentView Grid.Row="1" x:Name="MapHost" />
|
||||
</Grid>
|
||||
</ContentPage>
|
||||
151
FieldLogger/Views/MapPage.xaml.cs
Normal file
151
FieldLogger/Views/MapPage.xaml.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using FieldLogger.Models;
|
||||
using FieldLogger.ViewModels;
|
||||
|
||||
#if WINDOWS
|
||||
using System.Text.Json;
|
||||
#else
|
||||
using Microsoft.Maui.Controls.Maps;
|
||||
using Microsoft.Maui.Maps;
|
||||
using Map = Microsoft.Maui.Controls.Maps.Map;
|
||||
#endif
|
||||
|
||||
namespace FieldLogger.Views;
|
||||
|
||||
public partial class MapPage : ContentPage
|
||||
{
|
||||
private readonly MapViewModel _viewModel;
|
||||
|
||||
#if WINDOWS
|
||||
private WebView? _webView;
|
||||
private bool _webMapReady;
|
||||
private List<LoggedPoint>? _pendingPoints;
|
||||
#else
|
||||
private Map? _map;
|
||||
#endif
|
||||
|
||||
public MapPage(MapViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
BindingContext = _viewModel = viewModel;
|
||||
}
|
||||
|
||||
protected override async void OnAppearing()
|
||||
{
|
||||
base.OnAppearing();
|
||||
await EnsureMapCreatedAsync();
|
||||
var points = await _viewModel.LoadPointsAsync();
|
||||
ShowPoints(points);
|
||||
}
|
||||
|
||||
#if WINDOWS
|
||||
|
||||
private async Task EnsureMapCreatedAsync()
|
||||
{
|
||||
if (_webView is not null)
|
||||
return;
|
||||
|
||||
var apiKey = _viewModel.Settings.GoogleMapsApiKey;
|
||||
if (string.IsNullOrWhiteSpace(apiKey))
|
||||
{
|
||||
MapHost.Content = new Label
|
||||
{
|
||||
Text = "Google Maps API key is not set.\nEnter your Maps JavaScript API key on the Settings page.",
|
||||
HorizontalOptions = LayoutOptions.Center,
|
||||
VerticalOptions = LayoutOptions.Center,
|
||||
HorizontalTextAlignment = TextAlignment.Center,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
using var stream = await FileSystem.OpenAppPackageFileAsync("map.html");
|
||||
using var reader = new StreamReader(stream);
|
||||
var html = (await reader.ReadToEndAsync()).Replace("%%GOOGLE_MAPS_API_KEY%%", apiKey);
|
||||
|
||||
_webView = new WebView { Source = new HtmlWebViewSource { Html = html } };
|
||||
_webView.Navigated += async (_, _) =>
|
||||
{
|
||||
_webMapReady = true;
|
||||
if (_pendingPoints is not null)
|
||||
{
|
||||
var pts = _pendingPoints;
|
||||
_pendingPoints = null;
|
||||
await PushPointsToWebAsync(pts);
|
||||
}
|
||||
};
|
||||
MapHost.Content = _webView;
|
||||
}
|
||||
|
||||
private async void ShowPoints(List<LoggedPoint> points)
|
||||
{
|
||||
if (_webView is null)
|
||||
return;
|
||||
if (!_webMapReady)
|
||||
{
|
||||
_pendingPoints = points;
|
||||
return;
|
||||
}
|
||||
await PushPointsToWebAsync(points);
|
||||
}
|
||||
|
||||
private async Task PushPointsToWebAsync(List<LoggedPoint> points)
|
||||
{
|
||||
var payload = points.Select(p => new
|
||||
{
|
||||
id = p.Id,
|
||||
lat = p.Latitude,
|
||||
lng = p.Longitude,
|
||||
utility = ((UmUtility)p.Utility).ToString(),
|
||||
depth = p.DepthRaw,
|
||||
current = p.CurrentRaw,
|
||||
frequency = p.Frequency,
|
||||
fix = ((GnssFixStatus)p.FixStatus).ToString(),
|
||||
hrms = p.Hrms,
|
||||
time = p.TimestampUtc.ToLocalTime().ToString("g"),
|
||||
});
|
||||
var json = JsonSerializer.Serialize(payload);
|
||||
await _webView!.EvaluateJavaScriptAsync($"setPoints({json})");
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
private Task EnsureMapCreatedAsync()
|
||||
{
|
||||
if (_map is null)
|
||||
{
|
||||
_map = new Map { MapType = MapType.Hybrid, IsShowingUser = false };
|
||||
MapHost.Content = _map;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void ShowPoints(List<LoggedPoint> points)
|
||||
{
|
||||
if (_map is null)
|
||||
return;
|
||||
|
||||
_map.Pins.Clear();
|
||||
foreach (var p in points)
|
||||
{
|
||||
_map.Pins.Add(new Pin
|
||||
{
|
||||
Label = $"#{p.Id} {(UmUtility)p.Utility} · depth {p.DepthRaw}",
|
||||
Address = $"{(GnssFixStatus)p.FixStatus} · ±{p.Hrms:F3} m · {p.TimestampUtc.ToLocalTime():g}",
|
||||
Location = new Location(p.Latitude, p.Longitude),
|
||||
});
|
||||
}
|
||||
|
||||
if (points.Count > 0)
|
||||
{
|
||||
var minLat = points.Min(p => p.Latitude);
|
||||
var maxLat = points.Max(p => p.Latitude);
|
||||
var minLon = points.Min(p => p.Longitude);
|
||||
var maxLon = points.Max(p => p.Longitude);
|
||||
var center = new Location((minLat + maxLat) / 2, (minLon + maxLon) / 2);
|
||||
var spanKm = Math.Max(0.05,
|
||||
Location.CalculateDistance(minLat, minLon, maxLat, maxLon, DistanceUnits.Kilometers) * 0.7);
|
||||
_map.MoveToRegion(MapSpan.FromCenterAndRadius(center, Distance.FromKilometers(spanKm)));
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
64
FieldLogger/Views/SettingsPage.xaml
Normal file
64
FieldLogger/Views/SettingsPage.xaml
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
||||
x:Class="FieldLogger.Views.SettingsPage"
|
||||
x:DataType="vm:SettingsViewModel"
|
||||
Title="Settings">
|
||||
<ScrollView>
|
||||
<VerticalStackLayout Padding="16" Spacing="12">
|
||||
|
||||
<Label Text="Devices" FontAttributes="Bold" FontSize="16" />
|
||||
|
||||
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}" StrokeShape="RoundRectangle 12" Padding="12">
|
||||
<VerticalStackLayout Spacing="10">
|
||||
<Grid ColumnDefinitions="16,*,Auto,Auto" ColumnSpacing="8">
|
||||
<Ellipse WidthRequest="12" HeightRequest="12" VerticalOptions="Center"
|
||||
Fill="{Binding Manager.LocatorState, Converter={StaticResource StateColor}}" />
|
||||
<VerticalStackLayout Grid.Column="1" Spacing="0">
|
||||
<Label Text="Locating Receiver" FontSize="12" TextColor="Gray" />
|
||||
<Label Text="{Binding Manager.LocatorName, TargetNullValue='Not selected'}" FontAttributes="Bold" />
|
||||
</VerticalStackLayout>
|
||||
<Button Grid.Column="2" Text="Change" FontSize="12" Padding="10,4" Command="{Binding ChangeLocatorCommand}" />
|
||||
<Button Grid.Column="3" Text="Forget" FontSize="12" Padding="10,4"
|
||||
BackgroundColor="Transparent" TextColor="IndianRed" Command="{Binding ForgetLocatorCommand}" />
|
||||
</Grid>
|
||||
|
||||
<BoxView HeightRequest="1" Color="{AppThemeBinding Light=#E0E0E0, Dark=#333}" />
|
||||
|
||||
<Grid ColumnDefinitions="16,*,Auto,Auto" ColumnSpacing="8">
|
||||
<Ellipse WidthRequest="12" HeightRequest="12" VerticalOptions="Center"
|
||||
Fill="{Binding Manager.GpsState, Converter={StaticResource StateColor}}" />
|
||||
<VerticalStackLayout Grid.Column="1" Spacing="0">
|
||||
<Label Text="Maglink RTK GPS" FontSize="12" TextColor="Gray" />
|
||||
<Label Text="{Binding Manager.GpsName, TargetNullValue='Not selected'}" FontAttributes="Bold" />
|
||||
</VerticalStackLayout>
|
||||
<Button Grid.Column="2" Text="Change" FontSize="12" Padding="10,4" Command="{Binding ChangeGpsCommand}" />
|
||||
<Button Grid.Column="3" Text="Forget" FontSize="12" Padding="10,4"
|
||||
BackgroundColor="Transparent" TextColor="IndianRed" Command="{Binding ForgetGpsCommand}" />
|
||||
</Grid>
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
|
||||
<Label Text="Map" FontAttributes="Bold" FontSize="16" Margin="0,12,0,0" />
|
||||
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}" StrokeShape="RoundRectangle 12" Padding="12">
|
||||
<VerticalStackLayout Spacing="6">
|
||||
<Label Text="Google Maps API Key (Windows map view)" FontSize="12" TextColor="Gray" />
|
||||
<Entry Text="{Binding MapsApiKey}" Placeholder="AIza…" IsPassword="False" />
|
||||
<Label FontSize="11" TextColor="Gray"
|
||||
Text="On Android the key is configured in AndroidManifest.xml; iOS and macOS use Apple Maps and need no key." />
|
||||
</VerticalStackLayout>
|
||||
</Border>
|
||||
|
||||
<Label Text="Sync" FontAttributes="Bold" FontSize="16" Margin="0,12,0,0" />
|
||||
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}" StrokeShape="RoundRectangle 12" Padding="12">
|
||||
<Label Text="MQTT job sync is planned. Data is currently stored locally on this device."
|
||||
FontSize="12" TextColor="Gray" />
|
||||
</Border>
|
||||
|
||||
<Label Text="{Binding AppVersion, StringFormat='Field Logger v{0}'}"
|
||||
FontSize="11" TextColor="Gray" HorizontalOptions="Center" Margin="0,20,0,0" />
|
||||
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
</ContentPage>
|
||||
12
FieldLogger/Views/SettingsPage.xaml.cs
Normal file
12
FieldLogger/Views/SettingsPage.xaml.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using FieldLogger.ViewModels;
|
||||
|
||||
namespace FieldLogger.Views;
|
||||
|
||||
public partial class SettingsPage : ContentPage
|
||||
{
|
||||
public SettingsPage(SettingsViewModel viewModel)
|
||||
{
|
||||
InitializeComponent();
|
||||
BindingContext = viewModel;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user