- Implemented OpenStreetMap using WebView with Leaflet.js - Added OpenStreetMapView component with interactive map functionality - Created heat map visualization with color-coded intensity - Added 30 dummy location points around San Francisco Bay Area - Implemented location tracking with real-time pin placement - Added comprehensive UI with two-row button layout - Features: Start/Stop tracking, Center map, Demo heat map, Clear demo, Reset map - Added location count display and confirmation dialogs - Updated project structure and documentation - All functionality tested and working on Android emulator
447 lines
14 KiB
C#
447 lines
14 KiB
C#
using System.ComponentModel;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Windows.Input;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using LocationTrackerApp.Models;
|
|
using LocationTrackerApp.Services;
|
|
using LocationTrackerApp.Data;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace LocationTrackerApp.ViewModels;
|
|
|
|
/// <summary>
|
|
/// View model for the main view of the location tracking application
|
|
/// </summary>
|
|
public class MainViewModel : INotifyPropertyChanged
|
|
{
|
|
private readonly ILocationService _locationService;
|
|
private readonly LocationDbContext _dbContext;
|
|
private readonly ILogger<MainViewModel> _logger;
|
|
private bool _isTracking;
|
|
private bool _isHeatMapVisible = true;
|
|
private bool _isPointsVisible = true;
|
|
private string _trackingStatusText = "Not Tracking";
|
|
private string _currentLocationText = "No location";
|
|
private string _locationCountText = "0 points";
|
|
private string _trackingButtonText = "Start Tracking";
|
|
private Color _trackingButtonColor = Colors.Green;
|
|
private LocationData? _currentLocation;
|
|
private int _locationCount;
|
|
|
|
/// <summary>
|
|
/// Event fired when a property value changes
|
|
/// </summary>
|
|
public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether location tracking is active
|
|
/// </summary>
|
|
public bool IsTracking
|
|
{
|
|
get => _isTracking;
|
|
set
|
|
{
|
|
if (_isTracking != value)
|
|
{
|
|
_isTracking = value;
|
|
OnPropertyChanged();
|
|
UpdateTrackingUI();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether the heat map visualization is visible
|
|
/// </summary>
|
|
public bool IsHeatMapVisible
|
|
{
|
|
get => _isHeatMapVisible;
|
|
set
|
|
{
|
|
if (_isHeatMapVisible != value)
|
|
{
|
|
_isHeatMapVisible = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets whether individual location points are visible
|
|
/// </summary>
|
|
public bool IsPointsVisible
|
|
{
|
|
get => _isPointsVisible;
|
|
set
|
|
{
|
|
if (_isPointsVisible != value)
|
|
{
|
|
_isPointsVisible = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the tracking status text
|
|
/// </summary>
|
|
public string TrackingStatusText
|
|
{
|
|
get => _trackingStatusText;
|
|
set
|
|
{
|
|
if (_trackingStatusText != value)
|
|
{
|
|
_trackingStatusText = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the current location text
|
|
/// </summary>
|
|
public string CurrentLocationText
|
|
{
|
|
get => _currentLocationText;
|
|
set
|
|
{
|
|
if (_currentLocationText != value)
|
|
{
|
|
_currentLocationText = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the location count text
|
|
/// </summary>
|
|
public string LocationCountText
|
|
{
|
|
get => _locationCountText;
|
|
set
|
|
{
|
|
if (_locationCountText != value)
|
|
{
|
|
_locationCountText = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the tracking button text
|
|
/// </summary>
|
|
public string TrackingButtonText
|
|
{
|
|
get => _trackingButtonText;
|
|
set
|
|
{
|
|
if (_trackingButtonText != value)
|
|
{
|
|
_trackingButtonText = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the tracking button color
|
|
/// </summary>
|
|
public Color TrackingButtonColor
|
|
{
|
|
get => _trackingButtonColor;
|
|
set
|
|
{
|
|
if (_trackingButtonColor != value)
|
|
{
|
|
_trackingButtonColor = value;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Command to toggle location tracking
|
|
/// </summary>
|
|
public ICommand ToggleTrackingCommand { get; }
|
|
|
|
/// <summary>
|
|
/// Command to toggle heat map visibility
|
|
/// </summary>
|
|
public ICommand ToggleHeatMapCommand { get; }
|
|
|
|
/// <summary>
|
|
/// Command to toggle points visibility
|
|
/// </summary>
|
|
public ICommand TogglePointsCommand { get; }
|
|
|
|
/// <summary>
|
|
/// Command to center map on user location
|
|
/// </summary>
|
|
public ICommand CenterOnUserCommand { get; }
|
|
|
|
/// <summary>
|
|
/// Command to clear all location data
|
|
/// </summary>
|
|
public ICommand ClearDataCommand { get; }
|
|
|
|
/// <summary>
|
|
/// Command to load location data
|
|
/// </summary>
|
|
public ICommand LoadDataCommand { get; }
|
|
|
|
/// <summary>
|
|
/// Command to open settings
|
|
/// </summary>
|
|
public ICommand SettingsCommand { get; }
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of MainViewModel
|
|
/// </summary>
|
|
/// <param name="locationService">Location tracking service</param>
|
|
/// <param name="dbContext">Database context for location data</param>
|
|
/// <param name="logger">Logger for recording events</param>
|
|
public MainViewModel(ILocationService locationService, LocationDbContext dbContext, ILogger<MainViewModel> logger)
|
|
{
|
|
_locationService = locationService ?? throw new ArgumentNullException(nameof(locationService));
|
|
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
|
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
|
|
// Initialize commands
|
|
ToggleTrackingCommand = new Command(async () => await ToggleTrackingAsync());
|
|
ToggleHeatMapCommand = new Command(() => IsHeatMapVisible = !IsHeatMapVisible);
|
|
TogglePointsCommand = new Command(() => IsPointsVisible = !IsPointsVisible);
|
|
CenterOnUserCommand = new Command(async () => await CenterOnUserAsync());
|
|
ClearDataCommand = new Command(async () => await ClearDataAsync());
|
|
LoadDataCommand = new Command(async () => await LoadDataAsync());
|
|
SettingsCommand = new Command(async () => await OpenSettingsAsync());
|
|
|
|
// Subscribe to location service events
|
|
_locationService.LocationChanged += OnLocationChanged;
|
|
_locationService.TrackingStatusChanged += OnTrackingStatusChanged;
|
|
|
|
// Initialize UI
|
|
UpdateTrackingUI();
|
|
_ = Task.Run(async () => await LoadLocationCountAsync());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Toggles location tracking on/off
|
|
/// </summary>
|
|
private async Task ToggleTrackingAsync()
|
|
{
|
|
try
|
|
{
|
|
if (IsTracking)
|
|
{
|
|
await _locationService.StopTrackingAsync();
|
|
_logger.LogInformation("Location tracking stopped by user");
|
|
}
|
|
else
|
|
{
|
|
await _locationService.StartTrackingAsync();
|
|
_logger.LogInformation("Location tracking started by user");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to toggle location tracking");
|
|
await Application.Current!.Windows[0].Page!.DisplayAlert("Error", "Failed to toggle location tracking", "OK");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Centers the map on the user's current location
|
|
/// </summary>
|
|
private async Task CenterOnUserAsync()
|
|
{
|
|
try
|
|
{
|
|
var location = await _locationService.GetCurrentLocationAsync();
|
|
if (location != null)
|
|
{
|
|
// This would need to be implemented in the HeatMapView
|
|
_logger.LogInformation("Centering map on user location: {Latitude}, {Longitude}",
|
|
location.Latitude, location.Longitude);
|
|
}
|
|
else
|
|
{
|
|
await Application.Current!.Windows[0].Page!.DisplayAlert("Error", "Unable to get current location", "OK");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to center on user location");
|
|
await Application.Current!.Windows[0].Page!.DisplayAlert("Error", "Failed to center on user location", "OK");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all location data from the database
|
|
/// </summary>
|
|
private async Task ClearDataAsync()
|
|
{
|
|
try
|
|
{
|
|
var result = await Application.Current!.Windows[0].Page!.DisplayAlert(
|
|
"Clear Data",
|
|
"Are you sure you want to clear all location data? This action cannot be undone.",
|
|
"Yes",
|
|
"No");
|
|
|
|
if (result)
|
|
{
|
|
await _dbContext.ClearAllLocationDataAsync();
|
|
_locationCount = 0;
|
|
LocationCountText = "0 points";
|
|
_logger.LogInformation("All location data cleared by user");
|
|
await Application.Current.Windows[0].Page!.DisplayAlert("Success", "All location data has been cleared", "OK");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to clear location data");
|
|
await Application.Current!.Windows[0].Page!.DisplayAlert("Error", "Failed to clear location data", "OK");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads location data and updates the UI
|
|
/// </summary>
|
|
private async Task LoadDataAsync()
|
|
{
|
|
try
|
|
{
|
|
await LoadLocationCountAsync();
|
|
_logger.LogInformation("Location data loaded by user");
|
|
await Application.Current!.Windows[0].Page!.DisplayAlert("Success", "Location data has been loaded", "OK");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to load location data");
|
|
await Application.Current!.Windows[0].Page!.DisplayAlert("Error", "Failed to load location data", "OK");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Opens the settings page
|
|
/// </summary>
|
|
private async Task OpenSettingsAsync()
|
|
{
|
|
try
|
|
{
|
|
// This would navigate to a settings page
|
|
_logger.LogInformation("Settings page requested by user");
|
|
await Application.Current!.Windows[0].Page!.DisplayAlert("Settings", "Settings page coming soon!", "OK");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to open settings");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles location changed events from the location service
|
|
/// </summary>
|
|
/// <param name="sender">Event sender</param>
|
|
/// <param name="locationData">New location data</param>
|
|
private void OnLocationChanged(object? sender, LocationData locationData)
|
|
{
|
|
try
|
|
{
|
|
_currentLocation = locationData;
|
|
CurrentLocationText = $"Lat: {locationData.Latitude:F6}, Lng: {locationData.Longitude:F6}";
|
|
|
|
// Update location count
|
|
_locationCount++;
|
|
LocationCountText = $"{_locationCount} points";
|
|
|
|
_logger.LogDebug("Location updated: {Latitude}, {Longitude}", locationData.Latitude, locationData.Longitude);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to handle location changed event");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles tracking status changed events from the location service
|
|
/// </summary>
|
|
/// <param name="sender">Event sender</param>
|
|
/// <param name="isTracking">Current tracking status</param>
|
|
private void OnTrackingStatusChanged(object? sender, bool isTracking)
|
|
{
|
|
try
|
|
{
|
|
IsTracking = isTracking;
|
|
_logger.LogInformation("Tracking status changed: {IsTracking}", isTracking);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to handle tracking status changed event");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates the tracking-related UI elements
|
|
/// </summary>
|
|
private void UpdateTrackingUI()
|
|
{
|
|
if (IsTracking)
|
|
{
|
|
TrackingStatusText = "Tracking Active";
|
|
TrackingButtonText = "Stop Tracking";
|
|
TrackingButtonColor = Colors.Red;
|
|
}
|
|
else
|
|
{
|
|
TrackingStatusText = "Not Tracking";
|
|
TrackingButtonText = "Start Tracking";
|
|
TrackingButtonColor = Colors.Green;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the current location count from the database
|
|
/// </summary>
|
|
private async Task LoadLocationCountAsync()
|
|
{
|
|
try
|
|
{
|
|
_locationCount = await _dbContext.LocationData.CountAsync();
|
|
LocationCountText = $"{_locationCount} points";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to load location count");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raises the PropertyChanged event
|
|
/// </summary>
|
|
/// <param name="propertyName">Name of the property that changed</param>
|
|
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
|
{
|
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Disposes the view model and unsubscribes from events
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
try
|
|
{
|
|
_locationService.LocationChanged -= OnLocationChanged;
|
|
_locationService.TrackingStatusChanged -= OnTrackingStatusChanged;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error disposing MainViewModel");
|
|
}
|
|
}
|
|
}
|