using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Runtime.CompilerServices; using LiveChartsCore.Geo; using LiveChartsCore.SkiaSharpView; using LiveChartsCore.SkiaSharpView.Drawing.Geometries; using Pulsar.Server.Statistics; #nullable enable namespace Pulsar.Server.Controls.Wpf { public sealed class HeatMapViewModel : INotifyPropertyChanged { private readonly ObservableCollection _statCards = new() { new StatCardViewModel("Total Clients"), new StatCardViewModel("Geolocated"), new StatCardViewModel("Unknown Location"), new StatCardViewModel("Unique Countries") }; private readonly ObservableCollection _topCountries = new(); private HeatLandSeries[] _series = Array.Empty(); private bool _isLoading; private bool _hasError; private string? _errorMessage; private string _lastUpdated = string.Empty; private bool _isDarkMode; private ClientGeoSnapshot? _lastSnapshot; public event PropertyChangedEventHandler? PropertyChanged; public ReadOnlyObservableCollection StatCards { get; } public HeatLandSeries[] Series { get => _series; private set => SetField(ref _series, value); } public bool IsLoading { get => _isLoading; private set { if (SetField(ref _isLoading, value)) { OnPropertyChanged(nameof(IsContentVisible)); } } } public bool HasError { get => _hasError; private set { if (SetField(ref _hasError, value)) { OnPropertyChanged(nameof(IsContentVisible)); } } } public bool IsContentVisible => !IsLoading && !HasError; public string? ErrorMessage { get => _errorMessage; private set => SetField(ref _errorMessage, value); } public string LastUpdated { get => _lastUpdated; private set => SetField(ref _lastUpdated, value); } public ReadOnlyObservableCollection TopCountries { get; } public HeatMapViewModel() { StatCards = new ReadOnlyObservableCollection(_statCards); TopCountries = new ReadOnlyObservableCollection(_topCountries); } public void SetLoading() { ErrorMessage = null; HasError = false; IsLoading = true; } public void SetError(string message) { _lastSnapshot = null; ErrorMessage = message; HasError = true; IsLoading = false; LastUpdated = string.Empty; ClearData(); } public void UpdateSnapshot(ClientGeoSnapshot snapshot) { _lastSnapshot = snapshot; ErrorMessage = snapshot.ErrorMessage; HasError = snapshot.HasError; IsLoading = false; if (snapshot.HasError) { LastUpdated = string.Empty; ClearData(); return; } LastUpdated = $"Updated {snapshot.GeneratedAtUtc.ToLocalTime():g}"; UpdateCards(snapshot); UpdateTopCountries(snapshot); BuildSeries(); } public void UpdateTheme(bool isDarkMode) { _isDarkMode = isDarkMode; BuildSeries(); } private void UpdateCards(ClientGeoSnapshot snapshot) { _statCards[0].Update(snapshot.TotalClients.ToString("N0"), "Records processed"); _statCards[1].Update(snapshot.MappedClients.ToString("N0"), "Known country"); _statCards[2].Update(snapshot.UnknownClients.ToString("N0"), "No location data"); _statCards[3].Update(snapshot.UniqueCountryCount.ToString("N0"), "Countries represented"); } private void UpdateTopCountries(ClientGeoSnapshot snapshot) { _topCountries.Clear(); var rank = 1; foreach (var country in snapshot.Countries.Take(15)) { _topCountries.Add(new CountryHeatItem(rank++, country.Name, country.CountryCode3.ToUpperInvariant(), country.Count, country.Share)); } } private void BuildSeries() { if (_lastSnapshot == null || _lastSnapshot.HasError) { Series = Array.Empty(); return; } var lands = _lastSnapshot.Countries .Where(c => !string.IsNullOrWhiteSpace(c.CountryCode3)) .Select(c => new HeatLand { Name = c.CountryCode3.ToLowerInvariant(), Value = c.Count }) .ToArray(); if (lands.Length == 0) { Series = Array.Empty(); return; } Series = new[] { new HeatLandSeries { Lands = lands } }; } private void ClearData() { Series = Array.Empty(); _topCountries.Clear(); } private bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) { if (Equals(field, value)) { return false; } field = value; OnPropertyChanged(propertyName); return true; } private void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } public sealed class CountryHeatItem { public CountryHeatItem(int rank, string country, string code, int count, double share) { Rank = rank; Country = string.IsNullOrWhiteSpace(country) ? "Unknown" : country; Code = string.IsNullOrWhiteSpace(code) ? "" : code; Count = count; Share = share; } public int Rank { get; } public string Country { get; } public string Code { get; } public int Count { get; } public double Share { get; } public string SharePercent => Share > 0 ? Share.ToString("P1") : "0%"; } }