Files
Location-Tracker/LocationTrackerApp/Components/OpenStreetMapView.cs
Carlos Gutierrez fecd0ce968 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
2025-10-12 21:42:25 -04:00

369 lines
12 KiB
C#

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();
}
}