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