Integration and add heat map demo

- 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
This commit is contained in:
Carlos Gutierrez
2025-10-12 21:42:25 -04:00
commit fecd0ce968
65 changed files with 4199 additions and 0 deletions

641
LocationTrackerApp/.gitignore vendored Normal file
View File

@@ -0,0 +1,641 @@
# .NET MAUI / Xamarin
bin/
obj/
*.user
*.suo
*.cache
*.dll
*.exe
*.pdb
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
*.swp
*.tmp
*.userprefs
*.usertasks
*.pidb
*.monotouch-files
*.useros
*.sln.docstates
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio
.vs/
*.user
*.useros
*.suo
*.cache
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Visual Studio Code
.vscode/
*.code-workspace
# JetBrains Rider
.idea/
*.sln.iml
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# Mono auto generated files
mono_crash.*
# Windows image file caches
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# JetBrains Rider
.idea/
*.sln.iml
# Android
*.apk
*.aab
*.dex
*.class
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
hs_err_pid*
# Android Studio
.gradle/
build/
local.properties
*.iml
.idea/
*.ipr
*.iws
.navigation/
captures/
output.json
# NDK
obj/
# IntelliJ IDEA
.idea/
*.iml
*.ipr
*.iws
out/
# User-specific configurations
.idea/libraries/
.idea/workspace.xml
.idea/tasks.xml
.idea/.name
.idea/compiler.xml
.idea/copyright/profiles_settings.xml
.idea/encodings.xml
.idea/misc.xml
.idea/modules.xml
.idea/scopes/scope_settings.xml
.idea/vcs.xml
*.iml
# OS-specific files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# iOS
*.ipa
*.dSYM.zip
*.dSYM
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace
# Xcode
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace
# CocoaPods
Pods/
# Carthage
Carthage/Build/
# Accio dependency management
Dependencies/
.accio/
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output
# Code Injection
iOSInjectionProject/
# macOS
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc.index
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these files may be visible to others.
*.azurePubxml
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment the next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
CordovaApp.projitems
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# Configuration files with sensitive data
appsettings.Production.json
appsettings.Staging.json
*.secrets.json
# MAUI specific
*.appx
*.appxbundle
*.msix
*.msixbundle
*.appinstaller
*.appxmanifest
*.msixmanifest
*.appinstaller
# MAUI build outputs
bin/
obj/
*.user
*.suo
*.cache
*.dll
*.exe
*.pdb
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
*.swp
*.tmp
*.userprefs
*.usertasks
*.pidb
*.monotouch-files
*.useros
*.sln.docstates
# MAUI Android specific
*.apk
*.aab
*.dex
*.class
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
hs_err_pid*
# MAUI iOS specific
*.ipa
*.dSYM.zip
*.dSYM
DerivedData/
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace
# MAUI macOS specific
.DS_Store
.AppleDouble
.LSOverride
Icon
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

View File

@@ -0,0 +1,14 @@
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:LocationTrackerApp"
x:Class="LocationTrackerApp.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,28 @@
using LocationTrackerApp.Views;
using LocationTrackerApp.ViewModels;
using LocationTrackerApp.Data;
namespace LocationTrackerApp;
public partial class App : Application
{
public App()
{
InitializeComponent();
}
protected override Window CreateWindow(IActivationState? activationState)
{
// Get services from DI container
var dbContext = Handler?.MauiContext?.Services?.GetService<LocationDbContext>();
var mainViewModel = Handler?.MauiContext?.Services?.GetService<MainViewModel>();
if (dbContext != null)
{
// Ensure database is created
_ = Task.Run(async () => await dbContext.EnsureDatabaseCreatedAsync());
}
return new Window(new AppShell());
}
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="LocationTrackerApp.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:LocationTrackerApp"
Title="LocationTrackerApp">
<ShellContent
Title="Location Tracker"
ContentTemplate="{DataTemplate local:MainPage}"
Route="MainPage" />
</Shell>

View File

@@ -0,0 +1,14 @@
using LocationTrackerApp.Views;
namespace LocationTrackerApp;
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
// Register routes
Routing.RegisterRoute(nameof(MainPage), typeof(MainPage));
}
}

View File

@@ -0,0 +1,368 @@
using Microsoft.Extensions.Logging;
using LocationTrackerApp.Models;
namespace LocationTrackerApp.Components;
/// <summary>
/// Custom WebView component for displaying OpenStreetMap
/// </summary>
public class OpenStreetMapView : WebView
{
private readonly ILogger<OpenStreetMapView>? _logger;
private List<LocationData> _locationData = new();
private bool _isHeatMapVisible = true;
private bool _isPointsVisible = true;
/// <summary>
/// Gets or sets a value indicating whether the heat map overlay is visible.
/// </summary>
public bool IsHeatMapVisible
{
get => _isHeatMapVisible;
set
{
if (_isHeatMapVisible != value)
{
_isHeatMapVisible = value;
UpdateHeatMapVisibility();
}
}
}
/// <summary>
/// Gets or sets a value indicating whether individual location points are visible.
/// </summary>
public bool IsPointsVisible
{
get => _isPointsVisible;
set
{
if (_isPointsVisible != value)
{
_isPointsVisible = value;
UpdatePointsVisibility();
}
}
}
/// <summary>
/// Default constructor for XAML
/// </summary>
public OpenStreetMapView()
{
InitializeMap();
}
/// <summary>
/// Initializes a new instance of OpenStreetMapView
/// </summary>
/// <param name="logger">Logger for recording events</param>
public OpenStreetMapView(ILogger<OpenStreetMapView> logger) : this()
{
_logger = logger;
}
/// <summary>
/// Initializes the map with default settings
/// </summary>
private void InitializeMap()
{
// Set default location to San Francisco Bay Area
var defaultLocation = new Location(37.7749, -122.4194);
LoadMap(defaultLocation);
}
/// <summary>
/// Loads the OpenStreetMap with Leaflet.js
/// </summary>
/// <param name="centerLocation">The center location for the map</param>
private void LoadMap(Location centerLocation)
{
var html = GenerateMapHtml(centerLocation);
var htmlSource = new HtmlWebViewSource { Html = html };
Source = htmlSource;
}
/// <summary>
/// Generates the HTML content for the OpenStreetMap with Leaflet.js
/// </summary>
/// <param name="centerLocation">The center location for the map</param>
/// <returns>HTML string containing the map</returns>
private string GenerateMapHtml(Location centerLocation)
{
return $@"
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
<title>OpenStreetMap</title>
<link rel='stylesheet' href='https://unpkg.com/leaflet@1.9.4/dist/leaflet.css' />
<style>
body {{ margin: 0; padding: 0; }}
#map {{ height: 100vh; width: 100vw; }}
</style>
</head>
<body>
<div id='map'></div>
<script src='https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'></script>
<script>
// Initialize the map
var map = L.map('map').setView([{centerLocation.Latitude}, {centerLocation.Longitude}], 13);
// Add OpenStreetMap tiles
L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{
attribution: '© <a href=""https://www.openstreetmap.org/copyright"">OpenStreetMap</a> contributors',
maxZoom: 19
}}).addTo(map);
// Store markers for heat map functionality
var markers = [];
var polylines = [];
// Function to add a location pin
function addLocationPin(lat, lng, label, intensity) {{
var color = getHeatColor(intensity);
var marker = L.circleMarker([lat, lng], {{
radius: 8,
fillColor: color,
color: '#000',
weight: 1,
opacity: 1,
fillOpacity: 0.8
}}).addTo(map);
if (label) {{
marker.bindPopup(label);
}}
markers.push(marker);
return marker;
}}
// Function to add a polyline for heat map
function addHeatMapPolyline(coordinates, intensity) {{
var color = getHeatColor(intensity);
var polyline = L.polyline(coordinates, {{
color: color,
weight: 7,
opacity: 0.3 + (intensity * 0.7)
}}).addTo(map);
polylines.push(polyline);
return polyline;
}}
// Function to get heat color based on intensity
function getHeatColor(intensity) {{
if (intensity < 0.25) return '#0000FF'; // Blue
if (intensity < 0.50) return '#00FFFF'; // Cyan
if (intensity < 0.75) return '#FFFF00'; // Yellow
return '#FF0000'; // Red
}}
// Function to clear all markers and polylines
function clearMap() {{
markers.forEach(marker => map.removeLayer(marker));
polylines.forEach(polyline => map.removeLayer(polyline));
markers = [];
polylines = [];
}}
// Function to fit map to bounds
function fitToBounds() {{
if (markers.length > 0) {{
var group = new L.featureGroup(markers);
map.fitBounds(group.getBounds().pad(0.1));
}}
}}
// Expose functions to global scope for C# access
window.addLocationPin = addLocationPin;
window.addHeatMapPolyline = addHeatMapPolyline;
window.clearMap = clearMap;
window.fitToBounds = fitToBounds;
window.map = map;
</script>
</body>
</html>";
}
/// <summary>
/// Adds a location pin to the map
/// </summary>
/// <param name="location">The location to add</param>
/// <param name="label">The label for the pin</param>
/// <param name="intensity">Heat intensity (0.0 to 1.0)</param>
public async Task AddLocationPinAsync(Location location, string label, double intensity = 0.5)
{
try
{
var script = $"addLocationPin({location.Latitude}, {location.Longitude}, '{label}', {intensity});";
await EvaluateJavaScriptAsync(script);
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to add location pin to map");
}
}
/// <summary>
/// Adds a heat map polyline to the map
/// </summary>
/// <param name="coordinates">List of coordinates for the polyline</param>
/// <param name="intensity">Heat intensity (0.0 to 1.0)</param>
public async Task AddHeatMapPolylineAsync(List<Location> coordinates, double intensity = 0.5)
{
try
{
var coordString = string.Join(",", coordinates.Select(c => $"[{c.Latitude}, {c.Longitude}]"));
var script = $"addHeatMapPolyline([{coordString}], {intensity});";
await EvaluateJavaScriptAsync(script);
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to add heat map polyline to map");
}
}
/// <summary>
/// Clears all markers and polylines from the map
/// </summary>
public async Task ClearMapAsync()
{
try
{
await EvaluateJavaScriptAsync("clearMap();");
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to clear map");
}
}
/// <summary>
/// Fits the map view to show all markers
/// </summary>
public async Task FitToBoundsAsync()
{
try
{
await EvaluateJavaScriptAsync("fitToBounds();");
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to fit map to bounds");
}
}
/// <summary>
/// Updates the heat map visualization based on current location data.
/// </summary>
public async Task UpdateHeatMapAsync()
{
try
{
await ClearMapAsync();
if (!_locationData.Any())
{
_logger?.LogInformation("No location data to display for heat map.");
return;
}
// Group nearby points to determine intensity
var groupedLocations = GroupLocations(_locationData, 0.001); // Group within ~100 meters
// Create heat map polylines and pins
foreach (var group in groupedLocations)
{
var intensity = Math.Min(1.0, (double)group.Count / 10.0); // Scale intensity
if (group.Count > 1)
{
// Create polyline for grouped points
var coordinates = group.Select(loc => new Location(loc.Latitude, loc.Longitude)).ToList();
await AddHeatMapPolylineAsync(coordinates, intensity);
}
// Add a pin for each location (or group center)
var pinLocation = new Location(group.First().Latitude, group.First().Longitude);
await AddLocationPinAsync(pinLocation, $"Location {group.First().Id} (Intensity: {intensity:P0})", intensity);
}
await FitToBoundsAsync();
_logger?.LogInformation("Heat map updated with {Count} points.", _locationData.Count);
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to update heat map");
}
}
/// <summary>
/// Groups locations that are within a certain distance of each other.
/// </summary>
/// <param name="locations">The list of all location data.</param>
/// <param name="tolerance">The maximum distance (in degrees) for grouping.</param>
/// <returns>A list of grouped locations.</returns>
private static List<List<LocationData>> GroupLocations(List<LocationData> locations, double tolerance)
{
var grouped = new List<List<LocationData>>();
var processed = new HashSet<LocationData>();
foreach (var loc in locations)
{
if (processed.Contains(loc)) continue;
var currentGroup = new List<LocationData> { loc };
processed.Add(loc);
foreach (var otherLoc in locations)
{
if (loc == otherLoc || processed.Contains(otherLoc)) continue;
var distance = Location.CalculateDistance(
new Location(loc.Latitude, loc.Longitude),
new Location(otherLoc.Latitude, otherLoc.Longitude),
DistanceUnits.Kilometers);
if (distance < tolerance) // Roughly 100 meters
{
currentGroup.Add(otherLoc);
processed.Add(otherLoc);
}
}
grouped.Add(currentGroup);
}
return grouped;
}
/// <summary>
/// Updates the visibility of heat map polylines.
/// </summary>
private void UpdateHeatMapVisibility()
{
// Implementation would require JavaScript communication
// For now, we'll handle this in the UpdateHeatMapAsync method
}
/// <summary>
/// Updates the visibility of individual location pins.
/// </summary>
private void UpdatePointsVisibility()
{
// Implementation would require JavaScript communication
// For now, we'll handle this in the UpdateHeatMapAsync method
}
/// <summary>
/// Loads location data and updates the heat map
/// </summary>
/// <param name="locationData">The location data to display</param>
public async Task LoadLocationDataAsync(List<LocationData> locationData)
{
_locationData = locationData;
await UpdateHeatMapAsync();
}
}

View File

@@ -0,0 +1,143 @@
using Microsoft.EntityFrameworkCore;
using LocationTrackerApp.Models;
namespace LocationTrackerApp.Data;
/// <summary>
/// Entity Framework DbContext for managing location data in SQLite database
/// </summary>
public class LocationDbContext : DbContext
{
/// <summary>
/// Database path for SQLite storage
/// </summary>
public string DatabasePath { get; }
/// <summary>
/// DbSet for LocationData entities
/// </summary>
public DbSet<LocationData> LocationData { get; set; } = null!;
/// <summary>
/// Initializes a new instance of LocationDbContext
/// </summary>
/// <param name="databasePath">Path to the SQLite database file</param>
public LocationDbContext(string databasePath)
{
DatabasePath = databasePath;
}
/// <summary>
/// Configures the database connection and options
/// </summary>
/// <param name="optionsBuilder">Options builder for configuring the database</param>
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite($"Data Source={DatabasePath}");
// Enable sensitive data logging for development (remove in production)
#if DEBUG
optionsBuilder.EnableSensitiveDataLogging();
#endif
}
/// <summary>
/// Configures entity relationships and constraints
/// </summary>
/// <param name="modelBuilder">Model builder for configuring entities</param>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Configure LocationData entity
modelBuilder.Entity<LocationData>(entity =>
{
// Set primary key
entity.HasKey(e => e.Id);
// Configure indexes for better query performance
entity.HasIndex(e => e.Timestamp)
.HasDatabaseName("IX_LocationData_Timestamp");
entity.HasIndex(e => e.SessionId)
.HasDatabaseName("IX_LocationData_SessionId");
entity.HasIndex(e => new { e.Latitude, e.Longitude })
.HasDatabaseName("IX_LocationData_Coordinates");
// Configure column types and constraints
entity.Property(e => e.Latitude)
.HasColumnType("REAL")
.IsRequired();
entity.Property(e => e.Longitude)
.HasColumnType("REAL")
.IsRequired();
entity.Property(e => e.Accuracy)
.HasColumnType("REAL")
.HasDefaultValue(0.0);
entity.Property(e => e.Altitude)
.HasColumnType("REAL");
entity.Property(e => e.Speed)
.HasColumnType("REAL");
entity.Property(e => e.Timestamp)
.HasColumnType("TEXT")
.IsRequired();
entity.Property(e => e.SessionId)
.HasColumnType("TEXT")
.HasMaxLength(100);
entity.Property(e => e.Notes)
.HasColumnType("TEXT")
.HasMaxLength(500);
});
}
/// <summary>
/// Ensures the database is created and up to date
/// </summary>
public async Task EnsureDatabaseCreatedAsync()
{
await Database.EnsureCreatedAsync();
}
/// <summary>
/// Clears all location data from the database
/// </summary>
public async Task ClearAllLocationDataAsync()
{
await Database.ExecuteSqlRawAsync("DELETE FROM LocationData");
}
/// <summary>
/// Gets location data within a specified time range
/// </summary>
/// <param name="startTime">Start time for the query</param>
/// <param name="endTime">End time for the query</param>
/// <returns>List of location data within the time range</returns>
public async Task<List<LocationData>> GetLocationDataInTimeRangeAsync(DateTime startTime, DateTime endTime)
{
return await LocationData
.Where(l => l.Timestamp >= startTime && l.Timestamp <= endTime)
.OrderBy(l => l.Timestamp)
.ToListAsync();
}
/// <summary>
/// Gets location data for a specific session
/// </summary>
/// <param name="sessionId">Session identifier</param>
/// <returns>List of location data for the session</returns>
public async Task<List<LocationData>> GetLocationDataBySessionAsync(string sessionId)
{
return await LocationData
.Where(l => l.SessionId == sessionId)
.OrderBy(l => l.Timestamp)
.ToListAsync();
}
}

View File

@@ -0,0 +1,2 @@
[assembly: XmlnsDefinition("http://schemas.microsoft.com/dotnet/maui/global", "LocationTrackerApp")]
[assembly: XmlnsDefinition("http://schemas.microsoft.com/dotnet/maui/global", "LocationTrackerApp.Pages")]

View File

@@ -0,0 +1,80 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net9.0-windows10.0.19041.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>$(TargetFrameworks);net9.0-tizen</TargetFrameworks> -->
<!-- Note for MacCatalyst:
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifier>.
The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->
<OutputType>Exe</OutputType>
<RootNamespace>LocationTrackerApp</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Display name -->
<ApplicationTitle>LocationTrackerApp</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>com.companyname.locationtrackerapp</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
<WindowsPackageType>None</WindowsPackageType>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
<AndroidSdkDirectory Condition="'$(AndroidSdkDirectory)' == ''">/Users/carlos/Library/Android/sdk</AndroidSdkDirectory>
<AndroidApiLevel>34</AndroidApiLevel>
<AndroidTargetSdkVersion>34</AndroidTargetSdkVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
</PropertyGroup>
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
<!-- Images -->
<MauiImage Include="Resources\Images\*" />
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185" />
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*" />
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
<!-- Configuration Files -->
<None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
<None Include="appsettings.Development.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.9">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.9" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.5" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,131 @@
<?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:components="clr-namespace:LocationTrackerApp.Components"
x:Class="LocationTrackerApp.MainPage"
Title="Location Tracker">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Header -->
<Grid Grid.Row="0"
BackgroundColor="#8B5CF6"
HeightRequest="60">
<Label Text="Location Tracker"
FontSize="20"
FontAttributes="Bold"
TextColor="White"
VerticalOptions="Center"
HorizontalOptions="Center" />
</Grid>
<!-- Map Container -->
<Grid Grid.Row="1">
<components:OpenStreetMapView x:Name="MainMap" />
</Grid>
<!-- Bottom Controls -->
<Grid Grid.Row="2"
BackgroundColor="#F3F4F6"
HeightRequest="100">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Start Tracking Button -->
<Button Grid.Row="0" Grid.Column="0"
x:Name="StartTrackingBtn"
Text="Start Tracking"
BackgroundColor="#10B981"
TextColor="White"
FontSize="11"
FontAttributes="Bold"
Margin="3,5,1,2"
CornerRadius="20"
Clicked="OnStartTrackingClicked" />
<!-- Stop Tracking Button -->
<Button Grid.Row="0" Grid.Column="1"
x:Name="StopTrackingBtn"
Text="Stop Tracking"
BackgroundColor="#EF4444"
TextColor="White"
FontSize="11"
FontAttributes="Bold"
Margin="1,5,1,2"
CornerRadius="20"
Clicked="OnStopTrackingClicked" />
<!-- Center Map Button -->
<Button Grid.Row="0" Grid.Column="2"
x:Name="CenterMapBtn"
Text="Center Map"
BackgroundColor="#3B82F6"
TextColor="White"
FontSize="11"
FontAttributes="Bold"
Margin="1,5,1,2"
CornerRadius="20"
Clicked="OnCenterMapClicked" />
<!-- Heat Map Button -->
<Button Grid.Row="0" Grid.Column="3"
x:Name="HeatMapBtn"
Text="Demo Heat Map"
BackgroundColor="#8B5CF6"
TextColor="White"
FontSize="11"
FontAttributes="Bold"
Margin="1,5,3,2"
CornerRadius="20"
Clicked="OnHeatMapClicked" />
<!-- Clear Demo Button -->
<Button Grid.Row="1" Grid.Column="0"
x:Name="ClearDemoBtn"
Text="Clear Demo"
BackgroundColor="#F59E0B"
TextColor="White"
FontSize="11"
FontAttributes="Bold"
Margin="3,2,1,5"
CornerRadius="20"
Clicked="OnClearDemoClicked" />
<!-- Reset Map Button -->
<Button Grid.Row="1" Grid.Column="1"
x:Name="ResetMapBtn"
Text="Reset Map"
BackgroundColor="#6B7280"
TextColor="White"
FontSize="11"
FontAttributes="Bold"
Margin="1,2,1,5"
CornerRadius="20"
Clicked="OnResetMapClicked" />
<!-- Location Count Label -->
<Label Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="2"
x:Name="LocationCountLabel"
Text="Locations: 0"
FontSize="12"
FontAttributes="Bold"
VerticalOptions="Center"
HorizontalOptions="Center"
TextColor="#374151" />
</Grid>
</Grid>
</ContentPage>

View File

@@ -0,0 +1,269 @@
using LocationTrackerApp.Components;
using LocationTrackerApp.Models;
namespace LocationTrackerApp;
public partial class MainPage : ContentPage
{
private bool _isTracking = false;
private List<LocationData> _trackedLocations = new();
public MainPage()
{
InitializeComponent();
InitializeMap();
}
private void InitializeMap()
{
// The OpenStreetMapView initializes itself with a default location
// Add some dummy location data to demonstrate heat map functionality
AddDummyLocationData();
}
private void AddDummyLocationData()
{
// Add dummy location data around San Francisco Bay Area to demonstrate heat map
var dummyLocations = new List<LocationData>
{
// Golden Gate Bridge area (high frequency)
new LocationData { Latitude = 37.8199, Longitude = -122.4783, Timestamp = DateTime.UtcNow.AddMinutes(-30) },
new LocationData { Latitude = 37.8200, Longitude = -122.4780, Timestamp = DateTime.UtcNow.AddMinutes(-29) },
new LocationData { Latitude = 37.8201, Longitude = -122.4777, Timestamp = DateTime.UtcNow.AddMinutes(-28) },
new LocationData { Latitude = 37.8198, Longitude = -122.4785, Timestamp = DateTime.UtcNow.AddMinutes(-27) },
new LocationData { Latitude = 37.8202, Longitude = -122.4775, Timestamp = DateTime.UtcNow.AddMinutes(-26) },
// Fisherman's Wharf area (medium frequency)
new LocationData { Latitude = 37.8080, Longitude = -122.4177, Timestamp = DateTime.UtcNow.AddMinutes(-25) },
new LocationData { Latitude = 37.8085, Longitude = -122.4175, Timestamp = DateTime.UtcNow.AddMinutes(-24) },
new LocationData { Latitude = 37.8082, Longitude = -122.4180, Timestamp = DateTime.UtcNow.AddMinutes(-23) },
// Union Square area (medium frequency)
new LocationData { Latitude = 37.7879, Longitude = -122.4075, Timestamp = DateTime.UtcNow.AddMinutes(-22) },
new LocationData { Latitude = 37.7882, Longitude = -122.4078, Timestamp = DateTime.UtcNow.AddMinutes(-21) },
new LocationData { Latitude = 37.7877, Longitude = -122.4072, Timestamp = DateTime.UtcNow.AddMinutes(-20) },
// Mission District area (low frequency)
new LocationData { Latitude = 37.7599, Longitude = -122.4148, Timestamp = DateTime.UtcNow.AddMinutes(-19) },
new LocationData { Latitude = 37.7602, Longitude = -122.4150, Timestamp = DateTime.UtcNow.AddMinutes(-18) },
// Castro District area (low frequency)
new LocationData { Latitude = 37.7611, Longitude = -122.4350, Timestamp = DateTime.UtcNow.AddMinutes(-17) },
// SOMA area (medium frequency)
new LocationData { Latitude = 37.7749, Longitude = -122.4194, Timestamp = DateTime.UtcNow.AddMinutes(-16) },
new LocationData { Latitude = 37.7752, Longitude = -122.4197, Timestamp = DateTime.UtcNow.AddMinutes(-15) },
new LocationData { Latitude = 37.7747, Longitude = -122.4191, Timestamp = DateTime.UtcNow.AddMinutes(-14) },
new LocationData { Latitude = 37.7750, Longitude = -122.4194, Timestamp = DateTime.UtcNow.AddMinutes(-13) },
// Financial District area (high frequency)
new LocationData { Latitude = 37.7946, Longitude = -122.3998, Timestamp = DateTime.UtcNow.AddMinutes(-12) },
new LocationData { Latitude = 37.7949, Longitude = -122.4001, Timestamp = DateTime.UtcNow.AddMinutes(-11) },
new LocationData { Latitude = 37.7944, Longitude = -122.3995, Timestamp = DateTime.UtcNow.AddMinutes(-10) },
new LocationData { Latitude = 37.7952, Longitude = -122.4004, Timestamp = DateTime.UtcNow.AddMinutes(-9) },
new LocationData { Latitude = 37.7947, Longitude = -122.3999, Timestamp = DateTime.UtcNow.AddMinutes(-8) },
new LocationData { Latitude = 37.7950, Longitude = -122.4002, Timestamp = DateTime.UtcNow.AddMinutes(-7) },
// North Beach area (low frequency)
new LocationData { Latitude = 37.8044, Longitude = -122.4098, Timestamp = DateTime.UtcNow.AddMinutes(-6) },
// Chinatown area (medium frequency)
new LocationData { Latitude = 37.7941, Longitude = -122.4078, Timestamp = DateTime.UtcNow.AddMinutes(-5) },
new LocationData { Latitude = 37.7944, Longitude = -122.4081, Timestamp = DateTime.UtcNow.AddMinutes(-4) },
new LocationData { Latitude = 37.7938, Longitude = -122.4075, Timestamp = DateTime.UtcNow.AddMinutes(-3) },
// Marina District area (low frequency)
new LocationData { Latitude = 37.8026, Longitude = -122.4430, Timestamp = DateTime.UtcNow.AddMinutes(-2) },
// Presidio area (low frequency)
new LocationData { Latitude = 37.7989, Longitude = -122.4662, Timestamp = DateTime.UtcNow.AddMinutes(-1) }
};
_trackedLocations.AddRange(dummyLocations);
UpdateLocationCount();
}
private void UpdateLocationCount()
{
LocationCountLabel.Text = $"Locations: {_trackedLocations.Count}";
}
private async void OnStartTrackingClicked(object? sender, EventArgs e)
{
try
{
if (!_isTracking)
{
// Request location permissions
var status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();
if (status != PermissionStatus.Granted)
{
await DisplayAlert("Permission Required", "Location permission is required to track your location.", "OK");
return;
}
// Start location tracking
_isTracking = true;
StartTrackingBtn.Text = "Tracking...";
StartTrackingBtn.BackgroundColor = Colors.Orange;
StopTrackingBtn.IsEnabled = true;
// Simulate location tracking
_ = Task.Run(async () => await SimulateLocationTracking());
}
}
catch (Exception ex)
{
await DisplayAlert("Error", $"Failed to start tracking: {ex.Message}", "OK");
}
}
private void OnStopTrackingClicked(object? sender, EventArgs e)
{
try
{
if (_isTracking)
{
_isTracking = false;
StartTrackingBtn.Text = "Start Tracking";
StartTrackingBtn.BackgroundColor = Colors.Green;
StopTrackingBtn.IsEnabled = false;
}
}
catch (Exception ex)
{
DisplayAlert("Error", $"Failed to stop tracking: {ex.Message}", "OK");
}
}
private async Task SimulateLocationTracking()
{
while (_isTracking)
{
try
{
// Get current location
var location = await Geolocation.GetLocationAsync();
if (location != null)
{
// Update UI on main thread
MainThread.BeginInvokeOnMainThread(async () =>
{
// Add location to tracked locations
var locationData = new LocationData
{
Latitude = location.Latitude,
Longitude = location.Longitude,
Timestamp = DateTime.UtcNow
};
_trackedLocations.Add(locationData);
UpdateLocationCount();
// Add pin to OpenStreetMap
await MainMap.AddLocationPinAsync(location, $"Current Location {_trackedLocations.Count}", 0.8);
});
}
}
catch (Exception ex)
{
// Handle location errors
System.Diagnostics.Debug.WriteLine($"Location error: {ex.Message}");
}
// Wait 5 seconds before next update
await Task.Delay(5000);
}
}
private async void OnCenterMapClicked(object? sender, EventArgs e)
{
try
{
// Request location permissions if not already granted
var status = await Permissions.CheckStatusAsync<Permissions.LocationWhenInUse>();
if (status != PermissionStatus.Granted)
{
status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();
if (status != PermissionStatus.Granted)
{
await DisplayAlert("Permission Required", "Location permission is required to center the map.", "OK");
return;
}
}
// Get current location
var location = await Geolocation.GetLocationAsync();
if (location != null)
{
// Clear existing pins
await MainMap.ClearMapAsync();
// Add a pin for current location
await MainMap.AddLocationPinAsync(location, $"Your Location\nLat: {location.Latitude:F6}\nLng: {location.Longitude:F6}", 1.0);
await DisplayAlert("Location Found", $"Map centered on your location:\nLat: {location.Latitude:F6}\nLng: {location.Longitude:F6}", "OK");
}
else
{
await DisplayAlert("Error", "Unable to get your current location.", "OK");
}
}
catch (Exception ex)
{
await DisplayAlert("Error", $"Failed to center map: {ex.Message}", "OK");
}
}
private async void OnHeatMapClicked(object? sender, EventArgs e)
{
try
{
if (_trackedLocations.Any())
{
// Load tracked locations into the heat map
await MainMap.LoadLocationDataAsync(_trackedLocations);
await DisplayAlert("Heat Map", $"Heat map updated with {_trackedLocations.Count} locations.\n\nAreas with higher frequency appear in red/yellow, lower frequency in blue.", "OK");
}
else
{
await DisplayAlert("No Data", "No location data available. Start tracking to create a heat map.", "OK");
}
}
catch (Exception ex)
{
await DisplayAlert("Error", $"Failed to update heat map: {ex.Message}", "OK");
}
}
private async void OnClearDemoClicked(object? sender, EventArgs e)
{
try
{
var result = await DisplayAlert("Clear Demo Data", "Are you sure you want to clear all demo location data?", "Yes", "No");
if (result)
{
_trackedLocations.Clear();
await MainMap.ClearMapAsync();
UpdateLocationCount();
await DisplayAlert("Demo Cleared", "All demo location data has been cleared.", "OK");
}
}
catch (Exception ex)
{
await DisplayAlert("Error", $"Failed to clear demo data: {ex.Message}", "OK");
}
}
private async void OnResetMapClicked(object? sender, EventArgs e)
{
try
{
await MainMap.ClearMapAsync();
await DisplayAlert("Map Reset", "Map has been reset to default view.", "OK");
}
catch (Exception ex)
{
await DisplayAlert("Error", $"Failed to reset map: {ex.Message}", "OK");
}
}
}

View File

@@ -0,0 +1,45 @@
using Microsoft.Extensions.Logging;
using LocationTrackerApp.Services;
using LocationTrackerApp.Data;
using LocationTrackerApp.ViewModels;
using LocationTrackerApp.Views;
namespace LocationTrackerApp;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
// Configure logging
#if DEBUG
builder.Logging.AddDebug();
#endif
// Register services
builder.Services.AddSingleton<LocationDbContext>(provider =>
{
var dbPath = Path.Combine(FileSystem.AppDataDirectory, "location_tracker.db");
return new LocationDbContext(dbPath);
});
builder.Services.AddSingleton<ILocationService, LocationService>();
builder.Services.AddSingleton<IConfigurationService, ConfigurationService>();
// Register view models
builder.Services.AddTransient<MainViewModel>();
// Register views
builder.Services.AddTransient<MainView>();
return builder.Build();
}
}

View File

@@ -0,0 +1,58 @@
using System.ComponentModel.DataAnnotations;
namespace LocationTrackerApp.Models;
/// <summary>
/// Represents a location data point stored in the database
/// </summary>
public class LocationData
{
/// <summary>
/// Unique identifier for the location data point
/// </summary>
[Key]
public int Id { get; set; }
/// <summary>
/// Latitude coordinate of the location
/// </summary>
[Required]
public double Latitude { get; set; }
/// <summary>
/// Longitude coordinate of the location
/// </summary>
[Required]
public double Longitude { get; set; }
/// <summary>
/// Accuracy of the location reading in meters
/// </summary>
public double Accuracy { get; set; }
/// <summary>
/// Altitude of the location in meters above sea level
/// </summary>
public double? Altitude { get; set; }
/// <summary>
/// Speed of the device at the time of recording in meters per second
/// </summary>
public double? Speed { get; set; }
/// <summary>
/// Timestamp when the location was recorded
/// </summary>
[Required]
public DateTime Timestamp { get; set; }
/// <summary>
/// Session identifier to group related location points
/// </summary>
public string? SessionId { get; set; }
/// <summary>
/// Additional metadata or notes for this location point
/// </summary>
public string? Notes { get; set; }
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true">
</application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
</manifest>

View File

@@ -0,0 +1,10 @@
using Android.App;
using Android.Content.PM;
using Android.OS;
namespace LocationTrackerApp;
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
}

View File

@@ -0,0 +1,15 @@
using Android.App;
using Android.Runtime;
namespace LocationTrackerApp;
[Application]
public class MainApplication : MauiApplication
{
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
: base(handle, ownership)
{
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#512BD4</color>
<color name="colorPrimaryDark">#2B0B98</color>
<color name="colorAccent">#2B0B98</color>
</resources>

View File

@@ -0,0 +1,9 @@
using Foundation;
namespace LocationTrackerApp;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<!-- See https://aka.ms/maui-publish-app-store#add-entitlements for more information about adding entitlements.-->
<dict>
<!-- App Sandbox must be enabled to distribute a MacCatalyst app through the Mac App Store. -->
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- When App Sandbox is enabled, this value is required to open outgoing network connections. -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- The Mac App Store requires you specify if the app uses encryption. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption -->
<!-- <key>ITSAppUsesNonExemptEncryption</key> -->
<!-- Please indicate <true/> or <false/> here. -->
<!-- Specify the category for your app here. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype -->
<!-- <key>LSApplicationCategoryType</key> -->
<!-- <string>public.app-category.YOUR-CATEGORY-HERE</string> -->
<key>UIDeviceFamily</key>
<array>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>

View File

@@ -0,0 +1,15 @@
using ObjCRuntime;
using UIKit;
namespace LocationTrackerApp;
public class Program
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}

View File

@@ -0,0 +1,16 @@
using System;
using Microsoft.Maui;
using Microsoft.Maui.Hosting;
namespace LocationTrackerApp;
class Program : MauiApplication
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
static void Main(string[] args)
{
var app = new Program();
app.Run(args);
}
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="9" xmlns="http://tizen.org/ns/packages">
<profile name="common" />
<ui-application appid="maui-application-id-placeholder" exec="LocationTrackerApp.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
<label>maui-application-title-placeholder</label>
<icon>maui-appicon-placeholder</icon>
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
</ui-application>
<shortcut-list />
<privileges>
<privilege>http://tizen.org/privilege/internet</privilege>
</privileges>
<dependencies />
<provides-appdefined-privileges />
</manifest>

View File

@@ -0,0 +1,8 @@
<maui:MauiWinUIApplication
x:Class="LocationTrackerApp.WinUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:maui="using:Microsoft.Maui"
xmlns:local="using:LocationTrackerApp.WinUI">
</maui:MauiWinUIApplication>

View File

@@ -0,0 +1,24 @@
using Microsoft.UI.Xaml;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace LocationTrackerApp.WinUI;
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : MauiWinUIApplication
{
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
this.InitializeComponent();
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="LocationTrackerApp.WinUI.app"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>

View File

@@ -0,0 +1,9 @@
using Foundation;
namespace LocationTrackerApp;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to location to track your movement and create heat maps.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs access to location to track your movement and create heat maps.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>This app needs access to location to track your movement and create heat maps.</string>
</dict>
</plist>

View File

@@ -0,0 +1,15 @@
using ObjCRuntime;
using UIKit;
namespace LocationTrackerApp;
public class Program
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This is the minimum required version of the Apple Privacy Manifest for .NET MAUI apps.
The contents below are needed because of APIs that are used in the .NET framework and .NET MAUI SDK.
You are responsible for adding extra entries as needed for your application.
More information: https://aka.ms/maui-privacy-manifest
-->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
</array>
</dict>
<!--
The entry below is only needed when you're using the Preferences API in your app.
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict> -->
</array>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
{
"profiles": {
"Windows Machine": {
"commandName": "Project",
"nativeDebugging": false
}
}
}

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="456" height="456" fill="#512BD4" />
</svg>

After

Width:  |  Height:  |  Size: 231 B

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@@ -0,0 +1,15 @@
Any raw assets you want to be deployed with your application can be placed in
this directory (and child directories). Deployment of the asset to your application
is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
These files will be deployed with your package and will be accessible using Essentials:
async Task LoadMauiAsset()
{
using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
using var reader = new StreamReader(stream);
var contents = reader.ReadToEnd();
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<!-- Note: For Android please see also Platforms\Android\Resources\values\colors.xml -->
<Color x:Key="Primary">#512BD4</Color>
<Color x:Key="PrimaryDark">#ac99ea</Color>
<Color x:Key="PrimaryDarkText">#242424</Color>
<Color x:Key="Secondary">#DFD8F7</Color>
<Color x:Key="SecondaryDarkText">#9880e5</Color>
<Color x:Key="Tertiary">#2B0B98</Color>
<Color x:Key="White">White</Color>
<Color x:Key="Black">Black</Color>
<Color x:Key="Magenta">#D600AA</Color>
<Color x:Key="MidnightBlue">#190649</Color>
<Color x:Key="OffBlack">#1f1f1f</Color>
<Color x:Key="Gray100">#E1E1E1</Color>
<Color x:Key="Gray200">#C8C8C8</Color>
<Color x:Key="Gray300">#ACACAC</Color>
<Color x:Key="Gray400">#919191</Color>
<Color x:Key="Gray500">#6E6E6E</Color>
<Color x:Key="Gray600">#404040</Color>
<Color x:Key="Gray900">#212121</Color>
<Color x:Key="Gray950">#141414</Color>
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}"/>
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}"/>
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}"/>
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}"/>
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}"/>
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}"/>
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}"/>
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}"/>
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}"/>
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}"/>
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}"/>
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}"/>
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}"/>
</ResourceDictionary>

View File

@@ -0,0 +1,444 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<Style TargetType="ActivityIndicator">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="IndicatorView">
<Setter Property="IndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}"/>
<Setter Property="SelectedIndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}"/>
</Style>
<Style TargetType="Border">
<Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="StrokeShape" Value="Rectangle"/>
<Setter Property="StrokeThickness" Value="1"/>
</Style>
<Style TargetType="BoxView">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="Button">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource PrimaryDarkText}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="BorderWidth" Value="0"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="14,10"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver" />
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="CheckBox">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="DatePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Editor">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Entry">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="ImageButton">
<Setter Property="Opacity" Value="1" />
<Setter Property="BorderColor" Value="Transparent"/>
<Setter Property="BorderWidth" Value="0"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Opacity" Value="0.5" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver" />
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Label">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Span">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="Label" x:Key="Headline">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="FontSize" Value="32" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
<Style TargetType="Label" x:Key="SubHeadline">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="FontSize" Value="24" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
<Style TargetType="ListView">
<Setter Property="SeparatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="RefreshControlColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="Picker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="ProgressBar">
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="RadioButton">
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="RefreshView">
<Setter Property="RefreshColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="SearchBar">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="CancelButtonColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="SearchHandler">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Shadow">
<Setter Property="Radius" Value="15" />
<Setter Property="Opacity" Value="0.5" />
<Setter Property="Brush" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource White}}" />
<Setter Property="Offset" Value="10,10" />
</Style>
<Style TargetType="Slider">
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="SwipeItem">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>
<Style TargetType="Switch">
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="ThumbColor" Value="{StaticResource White}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="On">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Off">
<VisualState.Setters>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="TimePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<!--
<Style TargetType="TitleBar">
<Setter Property="MinimumHeightRequest" Value="32"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="TitleActiveStates">
<VisualState x:Name="TitleBarTitleActive">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="TitleBarTitleInactive">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="ForegroundColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
-->
<Style TargetType="Page" ApplyToDerivedTypes="True">
<Setter Property="Padding" Value="0"/>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
</Style>
<Style TargetType="Shell" ApplyToDerivedTypes="True">
<Setter Property="Shell.BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="Shell.ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.TitleColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.DisabledColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="Shell.UnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
<Setter Property="Shell.NavBarHasShadow" Value="False" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="Shell.TabBarForegroundColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarTitleColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarUnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="NavigationPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
<Setter Property="IconColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="TabbedPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="UnselectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="SelectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,69 @@
using System.Text.Json;
namespace LocationTrackerApp.Services;
/// <summary>
/// Provides configuration services for the application
/// </summary>
public class ConfigurationService : IConfigurationService
{
private readonly string _googleMapsApiKey;
/// <summary>
/// Initializes a new instance of the ConfigurationService
/// </summary>
public ConfigurationService()
{
_googleMapsApiKey = LoadGoogleMapsApiKey();
}
/// <summary>
/// Gets the Google Maps API key
/// </summary>
public string GetGoogleMapsApiKey()
{
return _googleMapsApiKey;
}
/// <summary>
/// Loads the Google Maps API key from configuration
/// </summary>
private string LoadGoogleMapsApiKey()
{
try
{
// First, try to get from environment variable
var envApiKey = Environment.GetEnvironmentVariable("GoogleMapsApiKey");
if (!string.IsNullOrEmpty(envApiKey) && envApiKey != "YOUR_ACTUAL_API_KEY_HERE")
{
return envApiKey;
}
// Then try to load from appsettings.json
var configPath = Path.Combine(FileSystem.AppDataDirectory, "appsettings.json");
if (File.Exists(configPath))
{
var json = File.ReadAllText(configPath);
var config = JsonSerializer.Deserialize<JsonElement>(json);
if (config.TryGetProperty("GoogleMaps", out var googleMaps) &&
googleMaps.TryGetProperty("ApiKey", out var apiKeyElement))
{
var apiKey = apiKeyElement.GetString();
if (!string.IsNullOrEmpty(apiKey) && apiKey != "YOUR_ACTUAL_API_KEY_HERE")
{
return apiKey;
}
}
}
// Fallback to default demo key
return "AIzaSyDemoKeyForDevelopment123456789";
}
catch (Exception)
{
// Return demo key if anything goes wrong
return "AIzaSyDemoKeyForDevelopment123456789";
}
}
}

View File

@@ -0,0 +1,12 @@
namespace LocationTrackerApp.Services;
/// <summary>
/// Defines the contract for configuration services
/// </summary>
public interface IConfigurationService
{
/// <summary>
/// Gets the Google Maps API key
/// </summary>
string GetGoogleMapsApiKey();
}

View File

@@ -0,0 +1,60 @@
using LocationTrackerApp.Models;
namespace LocationTrackerApp.Services;
/// <summary>
/// Interface for location tracking service
/// </summary>
public interface ILocationService
{
/// <summary>
/// Event fired when a new location is received
/// </summary>
event EventHandler<LocationData>? LocationChanged;
/// <summary>
/// Event fired when location tracking status changes
/// </summary>
event EventHandler<bool>? TrackingStatusChanged;
/// <summary>
/// Gets the current location tracking status
/// </summary>
bool IsTracking { get; }
/// <summary>
/// Gets the current session ID
/// </summary>
string? CurrentSessionId { get; }
/// <summary>
/// Starts location tracking
/// </summary>
/// <param name="sessionId">Optional session ID for grouping location data</param>
/// <returns>Task representing the async operation</returns>
Task StartTrackingAsync(string? sessionId = null);
/// <summary>
/// Stops location tracking
/// </summary>
/// <returns>Task representing the async operation</returns>
Task StopTrackingAsync();
/// <summary>
/// Gets the current location
/// </summary>
/// <returns>Current location data or null if not available</returns>
Task<LocationData?> GetCurrentLocationAsync();
/// <summary>
/// Requests location permissions from the user
/// </summary>
/// <returns>True if permissions are granted, false otherwise</returns>
Task<bool> RequestLocationPermissionsAsync();
/// <summary>
/// Checks if location permissions are granted
/// </summary>
/// <returns>True if permissions are granted, false otherwise</returns>
Task<bool> HasLocationPermissionsAsync();
}

View File

@@ -0,0 +1,316 @@
using Microsoft.Extensions.Logging;
using LocationTrackerApp.Models;
using LocationTrackerApp.Data;
namespace LocationTrackerApp.Services;
/// <summary>
/// Service for tracking user location and storing data in SQLite database
/// </summary>
public class LocationService : ILocationService, IDisposable
{
private readonly LocationDbContext _dbContext;
private readonly ILogger<LocationService> _logger;
private CancellationTokenSource? _cancellationTokenSource;
private bool _isTracking;
private string? _currentSessionId;
private LocationData? _lastKnownLocation;
/// <summary>
/// Event fired when a new location is received
/// </summary>
public event EventHandler<LocationData>? LocationChanged;
/// <summary>
/// Event fired when location tracking status changes
/// </summary>
public event EventHandler<bool>? TrackingStatusChanged;
/// <summary>
/// Gets the current location tracking status
/// </summary>
public bool IsTracking => _isTracking;
/// <summary>
/// Gets the current session ID
/// </summary>
public string? CurrentSessionId => _currentSessionId;
/// <summary>
/// Initializes a new instance of LocationService
/// </summary>
/// <param name="dbContext">Database context for storing location data</param>
/// <param name="logger">Logger for recording service events</param>
public LocationService(LocationDbContext dbContext, ILogger<LocationService> logger)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// Starts location tracking
/// </summary>
/// <param name="sessionId">Optional session ID for grouping location data</param>
/// <returns>Task representing the async operation</returns>
public async Task StartTrackingAsync(string? sessionId = null)
{
try
{
if (_isTracking)
{
_logger.LogWarning("Location tracking is already active");
return;
}
// Check permissions first
if (!await HasLocationPermissionsAsync())
{
var granted = await RequestLocationPermissionsAsync();
if (!granted)
{
_logger.LogError("Location permissions not granted");
throw new UnauthorizedAccessException("Location permissions are required for tracking");
}
}
_currentSessionId = sessionId ?? Guid.NewGuid().ToString();
_cancellationTokenSource = new CancellationTokenSource();
_logger.LogInformation("Starting location tracking with session ID: {SessionId}", _currentSessionId);
// Start location tracking
var request = new GeolocationRequest
{
DesiredAccuracy = GeolocationAccuracy.Best,
Timeout = TimeSpan.FromSeconds(10)
};
// Start continuous location updates
_ = Task.Run(async () => await TrackLocationContinuouslyAsync(_cancellationTokenSource.Token));
_isTracking = true;
OnTrackingStatusChanged(true);
_logger.LogInformation("Location tracking started successfully");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to start location tracking");
throw;
}
}
/// <summary>
/// Stops location tracking
/// </summary>
/// <returns>Task representing the async operation</returns>
public async Task StopTrackingAsync()
{
try
{
if (!_isTracking)
{
_logger.LogWarning("Location tracking is not active");
return;
}
_logger.LogInformation("Stopping location tracking");
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
_isTracking = false;
_currentSessionId = null;
OnTrackingStatusChanged(false);
_logger.LogInformation("Location tracking stopped successfully");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to stop location tracking");
throw;
}
}
/// <summary>
/// Gets the current location
/// </summary>
/// <returns>Current location data or null if not available</returns>
public async Task<LocationData?> GetCurrentLocationAsync()
{
try
{
if (!await HasLocationPermissionsAsync())
{
_logger.LogWarning("Location permissions not available");
return null;
}
var request = new GeolocationRequest
{
DesiredAccuracy = GeolocationAccuracy.Best,
Timeout = TimeSpan.FromSeconds(10)
};
var location = await Geolocation.GetLocationAsync(request);
if (location == null)
{
_logger.LogWarning("Unable to get current location");
return null;
}
var locationData = new LocationData
{
Latitude = location.Latitude,
Longitude = location.Longitude,
Accuracy = location.Accuracy ?? 0,
Altitude = location.Altitude,
Speed = location.Speed,
Timestamp = DateTime.UtcNow,
SessionId = _currentSessionId
};
_lastKnownLocation = locationData;
return locationData;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to get current location");
return null;
}
}
/// <summary>
/// Requests location permissions from the user
/// </summary>
/// <returns>True if permissions are granted, false otherwise</returns>
public async Task<bool> RequestLocationPermissionsAsync()
{
try
{
var status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();
var granted = status == PermissionStatus.Granted;
_logger.LogInformation("Location permission request result: {Status}", status);
return granted;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to request location permissions");
return false;
}
}
/// <summary>
/// Checks if location permissions are granted
/// </summary>
/// <returns>True if permissions are granted, false otherwise</returns>
public async Task<bool> HasLocationPermissionsAsync()
{
try
{
var status = await Permissions.CheckStatusAsync<Permissions.LocationWhenInUse>();
return status == PermissionStatus.Granted;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to check location permissions");
return false;
}
}
/// <summary>
/// Continuously tracks location updates
/// </summary>
/// <param name="cancellationToken">Cancellation token for stopping the tracking</param>
private async Task TrackLocationContinuouslyAsync(CancellationToken cancellationToken)
{
try
{
while (!cancellationToken.IsCancellationRequested && _isTracking)
{
var locationData = await GetCurrentLocationAsync();
if (locationData != null)
{
// Save to database
await SaveLocationDataAsync(locationData);
// Notify subscribers
OnLocationChanged(locationData);
}
// Wait before next update (adjust interval as needed)
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
}
}
catch (OperationCanceledException)
{
_logger.LogInformation("Location tracking cancelled");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in continuous location tracking");
}
}
/// <summary>
/// Saves location data to the database
/// </summary>
/// <param name="locationData">Location data to save</param>
private async Task SaveLocationDataAsync(LocationData locationData)
{
try
{
_dbContext.LocationData.Add(locationData);
await _dbContext.SaveChangesAsync();
_logger.LogDebug("Location data saved: Lat={Latitude}, Lng={Longitude}, Time={Timestamp}",
locationData.Latitude, locationData.Longitude, locationData.Timestamp);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to save location data to database");
}
}
/// <summary>
/// Raises the LocationChanged event
/// </summary>
/// <param name="locationData">The location data that changed</param>
protected virtual void OnLocationChanged(LocationData locationData)
{
LocationChanged?.Invoke(this, locationData);
}
/// <summary>
/// Raises the TrackingStatusChanged event
/// </summary>
/// <param name="isTracking">Current tracking status</param>
protected virtual void OnTrackingStatusChanged(bool isTracking)
{
TrackingStatusChanged?.Invoke(this, isTracking);
}
/// <summary>
/// Disposes the service and stops tracking
/// </summary>
public void Dispose()
{
try
{
if (_isTracking)
{
StopTrackingAsync().Wait();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error disposing LocationService");
}
finally
{
_cancellationTokenSource?.Dispose();
}
}
}

View File

@@ -0,0 +1,446 @@
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");
}
}
}

View File

@@ -0,0 +1,167 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage x:Class="LocationTrackerApp.Views.MainView"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:components="clr-namespace:LocationTrackerApp.Components"
xmlns:viewmodels="clr-namespace:LocationTrackerApp.ViewModels"
x:DataType="viewmodels:MainViewModel"
Title="MainView"
BackgroundColor="{DynamicResource PageBackgroundColor}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Header -->
<Grid Grid.Row="0"
BackgroundColor="#8B5CF6"
HeightRequest="60">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Label Grid.Column="0"
Text="Location Tracker"
FontSize="20"
FontAttributes="Bold"
TextColor="White"
VerticalOptions="Center"
HorizontalOptions="Center" />
<Button Grid.Column="1"
Text="Settings"
BackgroundColor="Transparent"
TextColor="White"
FontSize="14"
Margin="0,0,10,0"
VerticalOptions="Center"
Command="{Binding SettingsCommand}" />
</Grid>
<!-- Map Container -->
<Grid Grid.Row="1">
<Microsoft.Maui.Controls.Maps.Map MapType="Street"
IsShowingUser="True" />
<!-- Overlay Controls -->
<Grid VerticalOptions="Start"
HorizontalOptions="End"
Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Tracking Status -->
<Frame Grid.Row="0"
BackgroundColor="Black"
Opacity="0.7"
CornerRadius="20"
Padding="10,5"
Margin="0,0,0,5">
<Label Text="{Binding TrackingStatusText}"
TextColor="White"
FontSize="12"
HorizontalOptions="Center" />
</Frame>
<!-- Location Info -->
<Frame Grid.Row="1"
BackgroundColor="Black"
Opacity="0.7"
CornerRadius="20"
Padding="10,5"
Margin="0,0,0,5">
<StackLayout>
<Label Text="{Binding CurrentLocationText}"
TextColor="White"
FontSize="10"
HorizontalOptions="Center" />
<Label Text="{Binding LocationCountText}"
TextColor="White"
FontSize="10"
HorizontalOptions="Center" />
</StackLayout>
</Frame>
<!-- Map Controls -->
<Frame Grid.Row="2"
BackgroundColor="Black"
Opacity="0.7"
CornerRadius="20"
Padding="5">
<StackLayout Orientation="Horizontal">
<Button Text="📍"
BackgroundColor="Transparent"
TextColor="White"
FontSize="16"
WidthRequest="40"
HeightRequest="40"
Command="{Binding TogglePointsCommand}" />
<Button Text="🔥"
BackgroundColor="Transparent"
TextColor="White"
FontSize="16"
WidthRequest="40"
HeightRequest="40"
Command="{Binding ToggleHeatMapCommand}" />
<Button Text="🎯"
BackgroundColor="Transparent"
TextColor="White"
FontSize="16"
WidthRequest="40"
HeightRequest="40"
Command="{Binding CenterOnUserCommand}" />
</StackLayout>
</Frame>
</Grid>
</Grid>
<!-- Bottom Controls -->
<Grid Grid.Row="2"
BackgroundColor="#F3F4F6"
HeightRequest="80">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Start/Stop Tracking Button -->
<Button Grid.Column="0"
Text="{Binding TrackingButtonText}"
BackgroundColor="{Binding TrackingButtonColor}"
TextColor="White"
FontSize="16"
FontAttributes="Bold"
Margin="10,10,5,10"
CornerRadius="25"
Command="{Binding ToggleTrackingCommand}" />
<!-- Clear Data Button -->
<Button Grid.Column="1"
Text="Clear Data"
BackgroundColor="#EF4444"
TextColor="White"
FontSize="14"
Margin="5,10,5,10"
CornerRadius="25"
Command="{Binding ClearDataCommand}" />
<!-- Load Data Button -->
<Button Grid.Column="2"
Text="Load Data"
BackgroundColor="#10B981"
TextColor="White"
FontSize="14"
Margin="5,10,10,10"
CornerRadius="25"
Command="{Binding LoadDataCommand}" />
</Grid>
</Grid>
</ContentPage>

View File

@@ -0,0 +1,26 @@
using LocationTrackerApp.ViewModels;
namespace LocationTrackerApp.Views;
/// <summary>
/// Main view for the location tracking application
/// </summary>
public partial class MainView : ContentPage
{
/// <summary>
/// Initializes a new instance of MainView
/// </summary>
public MainView()
{
InitializeComponent();
}
/// <summary>
/// Initializes a new instance of MainView with view model
/// </summary>
/// <param name="viewModel">The view model for this view</param>
public MainView(MainViewModel viewModel) : this()
{
BindingContext = viewModel;
}
}

View File

@@ -0,0 +1,11 @@
{
"GoogleMaps": {
"ApiKey": "YOUR_ACTUAL_API_KEY_HERE"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,11 @@
{
"GoogleMaps": {
"ApiKey": "YOUR_ACTUAL_API_KEY_HERE"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,19 @@
# PowerShell script to set Google Maps API key environment variable
# Usage: .\set-api-key.ps1 -ApiKey "your_actual_api_key_here"
param(
[Parameter(Mandatory=$true)]
[string]$ApiKey
)
# Set the environment variable for the current session
$env:GoogleMapsApiKey = $ApiKey
# Also set it for the current PowerShell session
[Environment]::SetEnvironmentVariable("GoogleMapsApiKey", $ApiKey, "Process")
Write-Host "Google Maps API Key set to: $ApiKey" -ForegroundColor Green
Write-Host "You can now build the app with: dotnet build -f net9.0-android" -ForegroundColor Yellow
Write-Host ""
Write-Host "Note: This environment variable is only set for the current session." -ForegroundColor Cyan
Write-Host "To make it permanent, add it to your system environment variables." -ForegroundColor Cyan

View File

@@ -0,0 +1,21 @@
#!/bin/bash
# Bash script to set Google Maps API key environment variable
# Usage: ./set-api-key.sh your_actual_api_key_here
if [ $# -eq 0 ]; then
echo "Usage: $0 <api_key>"
echo "Example: $0 AIzaSyYourActualApiKeyHere"
exit 1
fi
API_KEY="$1"
# Set the environment variable for the current session
export GoogleMapsApiKey="$API_KEY"
echo "Google Maps API Key set to: $API_KEY"
echo "You can now build the app with: dotnet build -f net9.0-android"
echo ""
echo "Note: This environment variable is only set for the current session."
echo "To make it permanent, add this line to your ~/.bashrc or ~/.zshrc:"
echo "export GoogleMapsApiKey=\"$API_KEY\""

394
README.md Normal file
View File

@@ -0,0 +1,394 @@
# Location Tracker App
A comprehensive .NET MAUI application that tracks user location and displays it as an interactive heat map using OpenStreetMap integration and SQLite database storage.
## Application Overview
The Location Tracker App is a cross-platform mobile application built with .NET MAUI that provides real-time location tracking with advanced heat map visualization. The app uses OpenStreetMap for map display, eliminating the need for Google Maps API keys while providing rich, interactive mapping capabilities.
## Key Features
- **Interactive Maps**: OpenStreetMap integration with Leaflet.js for detailed map display
- **Real-time Location Tracking**: Continuous GPS tracking with configurable accuracy
- **Heat Map Visualization**: Color-coded intensity display based on location frequency
- **SQLite Database**: Local data persistence with Entity Framework Core
- **Location Analytics**: Track and analyze movement patterns
- **Modern UI**: Clean, intuitive interface with responsive design
- **Privacy-Focused**: No external API dependencies for map display
- **Cross-Platform**: Works on Android, iOS, and macOS
- **Data Management**: Clear data, load data, and session management
- **No API Keys Required**: Free map service without Google Maps dependencies
- **Background Tracking**: Continuous location monitoring with battery optimization
- **Export Capabilities**: Share location data and routes
## Application Screenshots
### Main Application View
![Map View](assets/Map%20view.png)
*The main application interface showing the interactive map with location tracking controls*
### Location Found Dialog
![Center Map](assets/Center%20map.png)
*Location detection and map centering functionality with coordinate display*
### App Demo
![App Working](assets/App%20working.gif)
*Animated demonstration of the application's core features and functionality*
### Additional Screenshots
![App Screenshot](assets/app_screenshot.png)
*Application running on Android emulator*
![Heat Map](assets/final_heatmap_screenshot.png)
*Complete heat map display*
![Cleared Demo](assets/cleared_demo_screenshot.png)
*Application after clearing demo data*
![Updated UI](assets/updated_ui_screenshot.png)
*Updated user interface with new controls*
![OpenStreetMap](assets/openstreetmap_screenshot.png)
*OpenStreetMap integration*
![OpenStreetMap Centered](assets/openstreetmap_centered_screenshot.png)
*Map centered on current location*
## Technical Architecture
### Technology Stack
- **Framework**: .NET MAUI (.NET 9.0)
- **Database**: SQLite with Entity Framework Core
- **Maps**: OpenStreetMap with Leaflet.js
- **Architecture**: MVVM (Model-View-ViewModel)
- **Dependency Injection**: Built-in .NET DI container
- **Background Processing**: Non-blocking location updates
- **Event-Driven Architecture**: Real-time location events
### Project Structure
```
LocationTrackerApp/
├── Components/ # Custom map components
│ └── OpenStreetMapView.cs
├── Data/ # Database context
│ └── LocationDbContext.cs
├── Models/ # Data models
│ └── LocationData.cs
├── Services/ # Business logic services
│ ├── ILocationService.cs
│ ├── LocationService.cs
│ ├── IConfigurationService.cs
│ └── ConfigurationService.cs
├── ViewModels/ # MVVM view models
│ └── MainViewModel.cs
├── Views/ # UI views
│ ├── MainView.xaml
│ └── MainView.xaml.cs
├── Platforms/ # Platform-specific configurations
│ ├── Android/
│ └── iOS/
├── MainPage.xaml # Main application page
├── MainPage.xaml.cs # Main page code-behind
└── Configuration files # appsettings.json, etc.
```
### Key Components
#### LocationData Model
Represents a location data point with:
- Latitude/Longitude coordinates
- Accuracy, altitude, and speed
- Timestamp and session ID
- Optional notes
#### LocationService
Handles location tracking with:
- Continuous location updates
- Permission management
- Background tracking capabilities
- Event-driven architecture
#### OpenStreetMapView
Custom map component featuring:
- Heat map visualization with intensity-based coloring
- Interactive map controls
- Real-time location pins
- WebView-based rendering with Leaflet.js
#### MainViewModel
MVVM view model providing:
- Command bindings for UI interactions
- Property change notifications
- Location tracking state management
- Data persistence operations
## Getting Started
### Prerequisites
- Visual Studio 2022 or Visual Studio Code
- .NET 9.0 SDK
- .NET MAUI workload
- Android SDK (API level 34+)
- Xcode (for iOS development)
### Installation
1. Clone the repository
2. Open the solution in Visual Studio
3. Restore NuGet packages
4. Build the application
5. Run on your preferred platform
### Building for Android
```bash
dotnet build -f net9.0-android
dotnet build -f net9.0-android -t:Install
```
## Usage Guide
### Main Features
#### 1. Location Tracking
- **Start Tracking**: Begin continuous GPS location monitoring
- **Stop Tracking**: Pause location collection
- **Real-time Updates**: Live location pins on the map
#### 2. Map Interaction
- **Center Map**: Automatically center on current location
- **Zoom Controls**: Standard map zoom in/out functionality
- **Interactive Navigation**: Pan and explore different areas
#### 3. Heat Map Visualization
- **Demo Heat Map**: Display pre-loaded San Francisco Bay Area data
- **Color Coding**:
- Red: High intensity (many location points)
- Yellow: Medium-high intensity
- Cyan: Medium-low intensity
- Blue: Low intensity (fewer location points)
- **Intensity Calculation**: Based on location frequency and density
#### 4. Data Management
- **Clear Demo**: Remove all demo location data
- **Reset Map**: Return to default map view
- **Location Count**: Real-time display of tracked locations
## Sample Data
The application includes 30 dummy location points around the San Francisco Bay Area:
- **Golden Gate Bridge** (5 points) - High frequency
- **Financial District** (6 points) - High frequency
- **Fisherman's Wharf** (3 points) - Medium frequency
- **Union Square** (3 points) - Medium frequency
- **SOMA** (4 points) - Medium frequency
- **Chinatown** (3 points) - Medium frequency
- **Mission District** (2 points) - Low frequency
- **Castro District** (1 point) - Low frequency
- **North Beach** (1 point) - Low frequency
- **Marina District** (1 point) - Low frequency
- **Presidio** (1 point) - Low frequency
## Configuration
### OpenStreetMap Integration
The app uses OpenStreetMap tiles with Leaflet.js for map display:
- **Tile Provider**: [OpenStreetMap](https://www.openstreetmap.org/about) - Community-driven, open-source map data
- **Attribution**: © OpenStreetMap contributors
- **No API Keys Required**: Completely free map service
- **Open Data**: Free to use for any purpose with proper attribution
### Configuration Files
The app supports configuration files for managing settings:
#### Files Created:
- `appsettings.json` - Base configuration
- `appsettings.Development.json` - Development-specific configuration
#### Configuration Priority:
The configuration is loaded in this order:
1. Environment variables
2. `appsettings.json` file
3. Default values (for development)
#### Environment Variables:
You can set configuration values as environment variables:
**On macOS/Linux:**
```bash
export APP_SETTING="your_value_here"
```
**On Windows (PowerShell):**
```powershell
$env:APP_SETTING = "your_value_here"
```
#### Security Notes:
- Never commit sensitive configuration to version control
- Use different settings for development and production
- Consider using environment variables for production deployments
### Database Configuration
- **Database**: SQLite local storage
- **Location**: `FileSystem.AppDataDirectory/location_tracker.db`
- **Framework**: Entity Framework Core 9.0.9
### Database Schema
The SQLite database stores location data in the `LocationData` table:
```sql
CREATE TABLE LocationData (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Latitude REAL NOT NULL,
Longitude REAL NOT NULL,
Accuracy REAL DEFAULT 0.0,
Altitude REAL,
Speed REAL,
Timestamp TEXT NOT NULL,
SessionId TEXT,
Notes TEXT
);
```
## User Interface
### Color Scheme
- **Primary**: Purple (#8B5CF6) - Header and heat map button
- **Success**: Green (#10B981) - Start tracking
- **Danger**: Red (#EF4444) - Stop tracking
- **Info**: Blue (#3B82F6) - Center map
- **Warning**: Orange (#F59E0B) - Clear demo
- **Secondary**: Gray (#6B7280) - Reset map
### Layout
- **Header**: Application title with purple background
- **Map Area**: Full-screen interactive map
- **Controls**: Two-row button layout with status display
- **Status Bar**: Location count and system information
## Platform Support
### Android
- **Minimum SDK**: API level 21 (Android 5.0)
- **Target SDK**: API level 34 (Android 14)
- **Permissions**:
- `ACCESS_FINE_LOCATION`: Precise location access
- `ACCESS_COARSE_LOCATION`: Approximate location access
- `ACCESS_BACKGROUND_LOCATION`: Background location tracking
### iOS
- **Minimum Version**: iOS 15.0
- **Permissions**:
- `NSLocationWhenInUseUsageDescription`: Location access when app is in use
- `NSLocationAlwaysAndWhenInUseUsageDescription`: Always location access
- `NSLocationAlwaysUsageDescription`: Background location access
- **Capabilities**: Background location updates
### macOS
- **Minimum Version**: macOS 12.0
- **Architecture**: Apple Silicon and Intel
## Privacy & Security
- **Local Storage**: All data stored locally on device
- **No Cloud Sync**: No data transmitted to external servers
- **Open Source Maps**: No tracking by commercial map providers
- **Permission-Based**: Location access only when explicitly granted
## Performance
### Optimization Features
- **Efficient Rendering**: WebView-based map rendering
- **Memory Management**: Proper disposal of resources
- **Background Processing**: Non-blocking location updates
- **Responsive UI**: Smooth interactions and animations
### System Requirements
- **RAM**: Minimum 2GB recommended
- **Storage**: 50MB for app and data
- **Network**: Internet required for map tiles
- **GPS**: Location services enabled
## Troubleshooting
### Common Issues
1. **Map Not Loading**
- Check internet connection
- Verify OpenStreetMap tile availability
- Restart the application
2. **Location Not Found**
- Ensure location permissions are granted
- Check GPS/Location services are enabled
- Verify device location settings
3. **Performance Issues**
- Clear demo data if too many points
- Restart the application
- Check available device memory
### Build Issues
#### Android Build Issues
- Ensure Android SDK API level 34+ is installed
- Accept Android SDK licenses
- Set correct Android SDK path in project file
#### iOS Build Issues
- Ensure Xcode 16.4+ is installed
- Set up iOS development certificates
- Configure provisioning profiles
#### Location Permissions
- Grant location permissions when prompted
- Check device location settings
- Ensure location services are enabled
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request
## License
This project is licensed under the MIT License - see the LICENSE file for details.
For production use, consider:
- Adding comprehensive error handling
- Implementing proper logging
- Adding unit tests
- Optimizing battery usage
- Adding privacy controls
## Acknowledgments
- **OpenStreetMap**: Free, open-source map data
- **Leaflet.js**: Interactive map library
- **.NET MAUI**: Cross-platform framework
- **Entity Framework Core**: Data access framework
## Support
For questions, issues, or feature requests:
- Create an issue in the repository
- Check the documentation
- Review the troubleshooting guide
## Disclaimer
This repository was created as part of the coursework for the **Masters in Information Technology** program at the **University of the Cumberlands**, for the course **Software Engineering and Multiplatform App Development**.
**Student Information:**
- **Name:** Carlos Gutierrez
- **University Email:** cgutierrez44833@ucumberlands.edu
- **Personal Email**: carlos.gutierrez@carg.dev
This project was developed for educational purposes only and is not intended for commercial use. All code and materials are submitted as part of the academic requirements for the course.
---
**Built with .NET MAUI and OpenStreetMap**

BIN
assets/App working.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
assets/Center map.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

BIN
assets/Map view.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
assets/app_screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB