using Microsoft.Extensions.Logging;
using LocationTrackerApp.Models;
namespace LocationTrackerApp.Components;
///
/// Custom WebView component for displaying OpenStreetMap
///
public class OpenStreetMapView : WebView
{
private readonly ILogger? _logger;
private List _locationData = new();
private bool _isHeatMapVisible = true;
private bool _isPointsVisible = true;
///
/// Gets or sets a value indicating whether the heat map overlay is visible.
///
public bool IsHeatMapVisible
{
get => _isHeatMapVisible;
set
{
if (_isHeatMapVisible != value)
{
_isHeatMapVisible = value;
UpdateHeatMapVisibility();
}
}
}
///
/// Gets or sets a value indicating whether individual location points are visible.
///
public bool IsPointsVisible
{
get => _isPointsVisible;
set
{
if (_isPointsVisible != value)
{
_isPointsVisible = value;
UpdatePointsVisibility();
}
}
}
///
/// Default constructor for XAML
///
public OpenStreetMapView()
{
InitializeMap();
}
///
/// Initializes a new instance of OpenStreetMapView
///
/// Logger for recording events
public OpenStreetMapView(ILogger logger) : this()
{
_logger = logger;
}
///
/// Initializes the map with default settings
///
private void InitializeMap()
{
// Set default location to San Francisco Bay Area
var defaultLocation = new Location(37.7749, -122.4194);
LoadMap(defaultLocation);
}
///
/// Loads the OpenStreetMap with Leaflet.js
///
/// The center location for the map
private void LoadMap(Location centerLocation)
{
var html = GenerateMapHtml(centerLocation);
var htmlSource = new HtmlWebViewSource { Html = html };
Source = htmlSource;
}
///
/// Generates the HTML content for the OpenStreetMap with Leaflet.js
///
/// The center location for the map
/// HTML string containing the map
private string GenerateMapHtml(Location centerLocation)
{
return $@"
OpenStreetMap
";
}
///
/// Adds a location pin to the map
///
/// The location to add
/// The label for the pin
/// Heat intensity (0.0 to 1.0)
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");
}
}
///
/// Adds a heat map polyline to the map
///
/// List of coordinates for the polyline
/// Heat intensity (0.0 to 1.0)
public async Task AddHeatMapPolylineAsync(List 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");
}
}
///
/// Clears all markers and polylines from the map
///
public async Task ClearMapAsync()
{
try
{
await EvaluateJavaScriptAsync("clearMap();");
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to clear map");
}
}
///
/// Fits the map view to show all markers
///
public async Task FitToBoundsAsync()
{
try
{
await EvaluateJavaScriptAsync("fitToBounds();");
}
catch (Exception ex)
{
_logger?.LogError(ex, "Failed to fit map to bounds");
}
}
///
/// Updates the heat map visualization based on current location data.
///
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");
}
}
///
/// Groups locations that are within a certain distance of each other.
///
/// The list of all location data.
/// The maximum distance (in degrees) for grouping.
/// A list of grouped locations.
private static List> GroupLocations(List locations, double tolerance)
{
var grouped = new List>();
var processed = new HashSet();
foreach (var loc in locations)
{
if (processed.Contains(loc)) continue;
var currentGroup = new List { 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;
}
///
/// Updates the visibility of heat map polylines.
///
private void UpdateHeatMapVisibility()
{
// Implementation would require JavaScript communication
// For now, we'll handle this in the UpdateHeatMapAsync method
}
///
/// Updates the visibility of individual location pins.
///
private void UpdatePointsVisibility()
{
// Implementation would require JavaScript communication
// For now, we'll handle this in the UpdateHeatMapAsync method
}
///
/// Loads location data and updates the heat map
///
/// The location data to display
public async Task LoadLocationDataAsync(List locationData)
{
_locationData = locationData;
await UpdateHeatMapAsync();
}
}