184 lines
6.7 KiB
Plaintext
184 lines
6.7 KiB
Plaintext
@inherits LocalizableComponentBase
|
||||
|
|
|
|||
|
|
@* The feed's 3D globe (wwwroot/js/globe.js on the vendored globe.gl): it looks at the average place of the posts in
|
|||
|
|
view and shows where a hovered post is. The posts' places go to the module in one call whenever they change; the
|
|||
|
|
corner shows the camera and the frame rate, its tooltip the rest of the globe's numbers. *@
|
|||
|
|
<div class="relative w-full h-full">
|
|||
|
|
<div @ref="element" class="absolute inset-0" aria-label="@Localizer["Globe of the posts in view"]" role="img"></div>
|
|||
|
|
|
|||
|
|
<div class="absolute left-2 bottom-2 flex items-center gap-2 text-[10px] font-mono opacity-70 pointer-events-auto">
|
|||
|
|
<NerdTip Heading="@Localizer["Globe"]" Rows="StatsRows()" Placement="DTooltipPlacement.Top" Align="DTooltipAlign.Start">
|
|||
|
|
<span class="badge badge-ghost badge-xs gap-1">
|
|||
|
|
<i class="ion-md-planet"></i>
|
|||
|
|
@if (stats is { Error: null } live)
|
|||
|
|
{
|
|||
|
|
<span>@Fixed(live.Latitude), @Fixed(live.Longitude) · @Fixed(live.Altitude) · @live.Fps fps</span>
|
|||
|
|
}
|
|||
|
|
else if (stats?.Error is { Length: > 0 })
|
|||
|
|
{
|
|||
|
|
<span>@Localizer["The globe could not start."]</span>
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
<span>@Localizer["loading"]…</span>
|
|||
|
|
}
|
|||
|
|
</span>
|
|||
|
|
</NerdTip>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
@code {
|
|||
|
|
[Inject] IJSRuntime JS { get; set; }
|
|||
|
|
[Inject] InstanceService InstanceService { get; set; }
|
|||
|
|
|
|||
|
|
/// <summary>The id of the element that scrolls the feed's cards.</summary>
|
|||
|
|
[Parameter, EditorRequired] public string ScrollContainerId { get; set; }
|
|||
|
|
[Parameter] public List<FeedPost> Posts { get; set; } = [];
|
|||
|
|
|
|||
|
|
ElementReference element;
|
|||
|
|
IJSObjectReference module;
|
|||
|
|
PeriodicTimer statsTimer;
|
|||
|
|
readonly CancellationTokenSource cancellation = new();
|
|||
|
|
string sentFingerprint;
|
|||
|
|
GlobeStats stats;
|
|||
|
|
|
|||
|
|
protected override void OnInitialized() => IsLoading = false;
|
|||
|
|
|
|||
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|||
|
|
{
|
|||
|
|
await base.OnAfterRenderAsync(firstRender);
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
if (firstRender)
|
|||
|
|
{
|
|||
|
|
module = await JS.InvokeAsync<IJSObjectReference>("import", "./js/globe.js");
|
|||
|
|
await module.InvokeVoidAsync("create", element, ScrollContainerId);
|
|||
|
|
statsTimer = new(TimeSpan.FromSeconds(1));
|
|||
|
|
_ = ReadStats(cancellation.Token);
|
|||
|
|
}
|
|||
|
|
await SendPosts();
|
|||
|
|
}
|
|||
|
|
catch (JSDisconnectedException)
|
|||
|
|
{
|
|||
|
|
}
|
|||
|
|
catch (Exception ex) when (ex is JSException or InvalidOperationException)
|
|||
|
|
{
|
|||
|
|
await CascadingState.ProcessError(ex, $"{nameof(GlobePanel)}.{nameof(OnAfterRenderAsync)}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>Sends the located posts when they changed since the last call.</summary>
|
|||
|
|
async Task SendPosts()
|
|||
|
|
{
|
|||
|
|
if (module is null)
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
var located = Posts.Where(p => p.Location is not null).ToList();
|
|||
|
|
var self = InstanceService.Known(InstanceService.SelfHost);
|
|||
|
|
var fingerprint = string.Join('|', located.Select(p => $"{p.Key}:{p.Location.Latitude}:{p.Location.Longitude}")) + $"#{self?.Geo?.Latitude}";
|
|||
|
|
if (fingerprint == sentFingerprint)
|
|||
|
|
return;
|
|||
|
|
sentFingerprint = fingerprint;
|
|||
|
|
|
|||
|
|
var points = located.Select(p =>
|
|||
|
|
{
|
|||
|
|
var server = InstanceService.Known(p.Host)?.Geo;
|
|||
|
|
return new GlobePoint(p.Key, p.Location.Latitude, p.Location.Longitude, p.Location.Source.ToString().ToLowerInvariant(), p.Location.Label, p.Host,
|
|||
|
|
server?.Latitude, server?.Longitude, p.Located ? p.RangeKm : null);
|
|||
|
|
}).ToList();
|
|||
|
|
await module.InvokeVoidAsync("setPosts", points, SelfPoint(self));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static GlobeSelf SelfPoint(ServerInfo self)
|
|||
|
|
{
|
|||
|
|
if (self?.Geo is { Latitude: { } latitude, Longitude: { } longitude } geo)
|
|||
|
|
return new(latitude, longitude, $"PrivaPub · {geo.City ?? self.Host}", self.Host);
|
|||
|
|
return CountryPoints.Of(self?.Geo?.Country) is { } country
|
|||
|
|
? new(country.Latitude, country.Longitude, $"PrivaPub · {InstanceService.CountryName(self.Geo.Country)}", self.Host)
|
|||
|
|
: null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
async Task ReadStats(CancellationToken cancellationToken)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
while (await statsTimer.WaitForNextTickAsync(cancellationToken))
|
|||
|
|
{
|
|||
|
|
stats = await module.InvokeAsync<GlobeStats>("stats", cancellationToken);
|
|||
|
|
await InvokeAsync(StateHasChanged);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (OperationCanceledException)
|
|||
|
|
{
|
|||
|
|
}
|
|||
|
|
catch (JSDisconnectedException)
|
|||
|
|
{
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
IEnumerable<NerdRow> StatsRows()
|
|||
|
|
{
|
|||
|
|
if (stats is null)
|
|||
|
|
yield break;
|
|||
|
|
yield return NerdRow.Of(Localizer["camera"], $"{Fixed(stats.Latitude)}, {Fixed(stats.Longitude)} @ {Fixed(stats.Altitude)}");
|
|||
|
|
yield return NerdRow.Of(Localizer["in view"], $"{stats.VisibleLocated}/{stats.Visible} {Localizer["placed"]}");
|
|||
|
|
yield return NerdRow.Of(Localizer["centre of view"], stats.CentroidLatitude is { } latitude ? $"{Fixed(latitude)}, {Fixed(stats.CentroidLongitude)}" : null);
|
|||
|
|
yield return NerdRow.Of(Localizer["spread"], stats.SpreadKm > 0 ? $"{stats.SpreadKm:N0} km" : null);
|
|||
|
|
yield return NerdRow.Of(Localizer["points"], $"{stats.Points} ({stats.Posts} {Localizer["posts"]}, {stats.Servers} {Localizer["servers"]})");
|
|||
|
|
yield return NerdRow.Of(Localizer["hovered"], stats.Hovered is { } hovered ? $"{hovered} ({stats.Arcs} {Localizer["arcs"]}, {stats.Rings} {Localizer["rings"]})" : null);
|
|||
|
|
yield return NerdRow.Of("fps", stats.Paused ? Localizer["paused"] : stats.Fps.ToString());
|
|||
|
|
yield return NerdRow.Of(Localizer["auto-rotate"], stats.AutoRotate);
|
|||
|
|
yield return NerdRow.Of(Localizer["texture"], stats.Texture is { } texture ? $"{texture} 4096×2048" : null);
|
|||
|
|
yield return NerdRow.Of("WebGL", stats.Renderer);
|
|||
|
|
yield return NerdRow.Of(Localizer["error"], stats.Error);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
static string Fixed(double? value) => value?.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture) ?? "–";
|
|||
|
|
|
|||
|
|
public override async ValueTask DisposeAsync()
|
|||
|
|
{
|
|||
|
|
await cancellation.CancelAsync();
|
|||
|
|
cancellation.Dispose();
|
|||
|
|
statsTimer?.Dispose();
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
if (module is not null)
|
|||
|
|
{
|
|||
|
|
await module.InvokeVoidAsync("dispose");
|
|||
|
|
await module.DisposeAsync();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (JSDisconnectedException)
|
|||
|
|
{
|
|||
|
|
}
|
|||
|
|
await base.DisposeAsync();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
sealed record GlobePoint(string Key, double Lat, double Lng, string Src, string Label, string Host, double? ServerLat, double? ServerLng, double? Range);
|
|||
|
|
|
|||
|
|
sealed record GlobeSelf(double Lat, double Lng, string Label, string Host);
|
|||
|
|
|
|||
|
|
sealed class GlobeStats
|
|||
|
|
{
|
|||
|
|
public double? Latitude { get; set; }
|
|||
|
|
public double? Longitude { get; set; }
|
|||
|
|
public double? Altitude { get; set; }
|
|||
|
|
public int Visible { get; set; }
|
|||
|
|
public int VisibleLocated { get; set; }
|
|||
|
|
public double? CentroidLatitude { get; set; }
|
|||
|
|
public double? CentroidLongitude { get; set; }
|
|||
|
|
public double SpreadKm { get; set; }
|
|||
|
|
public int Points { get; set; }
|
|||
|
|
public int Posts { get; set; }
|
|||
|
|
public int Servers { get; set; }
|
|||
|
|
public int Fps { get; set; }
|
|||
|
|
public string Renderer { get; set; }
|
|||
|
|
public string Texture { get; set; }
|
|||
|
|
public bool AutoRotate { get; set; }
|
|||
|
|
public bool Paused { get; set; }
|
|||
|
|
public string Hovered { get; set; }
|
|||
|
|
public int Arcs { get; set; }
|
|||
|
|
public int Rings { get; set; }
|
|||
|
|
public string Error { get; set; }
|
|||
|
|
}
|
|||
|
|
}
|