Skip to content

Commit

Permalink
[WIP] Add map
Browse files Browse the repository at this point in the history
  • Loading branch information
Keller253 committed Feb 26, 2024
1 parent 1472dc6 commit 39967dc
Show file tree
Hide file tree
Showing 17 changed files with 390 additions and 137 deletions.
42 changes: 42 additions & 0 deletions src/CyclingApp/Components/DataCard.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<MudPaper Class=@($"pa-4 {Class}") Style=@($"width: 100%;{Style}")>
<MudText>@Title.ToUpperInvariant()</MudText>
<MudText Typo="Typo.h4">@Value</MudText>
@if(Unit is not null)
{
<MudText Align="Align.Right">@Unit</MudText>
}
</MudPaper>

@code {

/// <summary>
/// HTML style attribute.
/// </summary>
[Parameter]
public string Style { get; set; } = string.Empty;

/// <summary>
/// HTML class attribute.
/// </summary>
[Parameter]
public string Class { get; set; } = string.Empty;

/// <summary>
/// Title of the data.
/// </summary>
[Parameter]
public string Title { get; set; } = string.Empty;

/// <summary>
/// Value of the data.
/// </summary>
[Parameter]
public string Value { get; set; } = string.Empty;

/// <summary>
/// Optional unit of the data.
/// </summary>
[Parameter]
public string? Unit { get; set; } = null;

}
4 changes: 4 additions & 0 deletions src/CyclingApp/Components/RouteMap.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div class=@($"overflow-hidden {Class}") style="@Style" @ref="ContainerReference">
@* <MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="OnButtonClick">Test</MudButton> *@
<BECanvas @ref="CanvasReference"></BECanvas>
</div>
62 changes: 62 additions & 0 deletions src/CyclingApp/Components/RouteMap.razor.JSInterop.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.Runtime.Versioning;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace CyclingApp.Components;

[SupportedOSPlatform("browser")]
public partial class RouteMap : IAsyncDisposable
{
private IJSObjectReference? _jSModule;

/// <summary>
/// TODO
/// </summary>
[Inject]
protected IJSRuntime JSRuntime { get; set; } = default!;

private async Task<ElementBoundingRectangle> GetBoundingRectangleAsync(ElementReference element)
{
var module = await GetOrImportJSModule();
return await module.InvokeAsync<ElementBoundingRectangle>("getBoundingRectangle", element);
}

private async Task<IDisposable> CreateResizeObserverAsync(ElementReference element, Action callback)
{
var target = callback.Target ?? throw new NotSupportedException("Static methods are not supported");
var targetReference = DotNetObjectReference.Create(target);
var module = await GetOrImportJSModule();
await module.InvokeVoidAsync("createResizeObserver", element, targetReference, callback.Method.Name);
return targetReference;
}

private async Task<IJSObjectReference> GetOrImportJSModule()
{
_jSModule ??= await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./Components/RouteMap.razor.js");
return _jSModule;
}

/// <inheritdoc/>
public async ValueTask DisposeAsync()
{
if (_jSModule is not null)
{
await _jSModule.DisposeAsync();
}

GC.SuppressFinalize(this);
}
}

[SupportedOSPlatform("browser")]
public class ElementBoundingRectangle

Check warning on line 52 in src/CyclingApp/Components/RouteMap.razor.JSInterop.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'ElementBoundingRectangle'
{
public double X { get; set; }

Check warning on line 54 in src/CyclingApp/Components/RouteMap.razor.JSInterop.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'ElementBoundingRectangle.X'
public double Y { get; set; }

Check warning on line 55 in src/CyclingApp/Components/RouteMap.razor.JSInterop.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'ElementBoundingRectangle.Y'
public double Width { get; set; }

Check warning on line 56 in src/CyclingApp/Components/RouteMap.razor.JSInterop.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'ElementBoundingRectangle.Width'
public double Height { get; set; }

Check warning on line 57 in src/CyclingApp/Components/RouteMap.razor.JSInterop.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'ElementBoundingRectangle.Height'
public double Top { get; set; }

Check warning on line 58 in src/CyclingApp/Components/RouteMap.razor.JSInterop.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'ElementBoundingRectangle.Top'
public double Right { get; set; }

Check warning on line 59 in src/CyclingApp/Components/RouteMap.razor.JSInterop.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'ElementBoundingRectangle.Right'
public double Bottom { get; set; }

Check warning on line 60 in src/CyclingApp/Components/RouteMap.razor.JSInterop.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'ElementBoundingRectangle.Bottom'
public double Left { get; set; }

Check warning on line 61 in src/CyclingApp/Components/RouteMap.razor.JSInterop.cs

View workflow job for this annotation

GitHub Actions / build

Missing XML comment for publicly visible type or member 'ElementBoundingRectangle.Left'
}
159 changes: 159 additions & 0 deletions src/CyclingApp/Components/RouteMap.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
using Blazor.Extensions;
using Blazor.Extensions.Canvas.Canvas2D;
using CyclingApp.Models;
using CyclingApp.Resources;
using Geolocation;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace CyclingApp.Components;

/// <summary>
/// Component visualizing a given route.
/// </summary>
public partial class RouteMap : IDisposable
{
private Canvas2DContext _context = default!;
private IDisposable _containerResizeObserver = default!;
private bool _isRendered;

/// <summary>
/// Route to be visualized by the component;
/// </summary>
[Parameter]
public RouteBase? Route { get; set; } = new Route();

/// <summary>
/// HTML style attribute.
/// </summary>
[Parameter]
public string Style { get; set; } = string.Empty;

/// <summary>
/// HTML class attribute.
/// </summary>
[Parameter]
public string Class { get; set; } = string.Empty;

/// <summary>
/// Reference to the components container.
/// </summary>
protected ElementReference ContainerReference { get; private set; } = default!;

/// <summary>
/// Reference to the underlying canvas.
/// </summary>
protected BECanvasComponent CanvasReference { get; private set; } = default!;

/// <inheritdoc/>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
_context = await CanvasReference.CreateCanvas2DAsync();

await DrawCanvas();
_containerResizeObserver = await CreateResizeObserverAsync(ContainerReference, OnContainerResize);
}
_isRendered = true;
}

/// <inheritdoc/>
protected override async Task OnParametersSetAsync()
{
if (_isRendered)
{
await DrawCanvas();
}
}

/// <inheritdoc/>
public void Dispose()
{
_containerResizeObserver.Dispose();

GC.SuppressFinalize(this);
}

/// <summary>
/// Callback to be invoked when the <see cref="ContainerReference"/> resizes.
/// </summary>
[JSInvokable]
public async void OnContainerResize()
{
await DrawCanvas();
}

private async Task DrawCanvas()
{
// Clear canvas
await _context.ClearRectAsync(0, 0, CanvasReference.Width, CanvasReference.Height);

// Resize canvas
var size = await GetBoundingRectangleAsync(ContainerReference);
await CanvasReference.SetParametersAsync(ParameterView.FromDictionary(new Dictionary<string, object?>
{
{ nameof(CanvasReference.Width), (long)size.Width },
{ nameof(CanvasReference.Height), (long)size.Height }
}));

var centerX = size.Width / 2;
var centerY = size.Height / 2;
var scale = Math.Min(size.Width, size.Height) / 650; // Scale to always show at least 650m

// Draw background circles
await _context.SetLineWidthAsync(1);
await _context.SetStrokeStyleAsync(Themes.Default.PaletteDark.Surface.Value);
var stepSize = 100 * scale;
for (var i = stepSize; i < Math.Max(size.Width, size.Height + stepSize); i += stepSize)
{
await _context.BeginPathAsync();
await _context.ArcAsync(centerX, centerY, i, 0, 2 * Math.PI);
await _context.StrokeAsync();
}

if (Route is null)
return;

// Draw route
await _context.SetStrokeStyleAsync(Themes.Default.PaletteDark.Primary.Value);
await _context.SetFillStyleAsync(Themes.Default.PaletteDark.Primary.Value);
// Current
await _context.BeginPathAsync();
await _context.ArcAsync(centerX, centerY, 8, 0, 2 * Math.PI);
await _context.FillAsync();
// Waypoints
if (Route.Waypoints.Count > 0)
{
await _context.SetLineWidthAsync(4);
await _context.BeginPathAsync();
await _context.MoveToAsync(centerX, centerY);

var waypoints = Route.Waypoints.OrderByDescending(x => x.Timestamp).ToArray();
var previousX = centerX;
var previousY = centerY;

for (var i = 1; i < waypoints.Length; i++)
{
var previous = waypoints[i - 1];
var current = waypoints[i];

var bearing = GeoCalculator.GetBearing(previous.Latitude, previous.Longitude,
current.Latitude, current.Longitude);
var distance = GeoCalculator.GetDistance(previous.Latitude, previous.Longitude,
current.Latitude, current.Longitude,
0, DistanceUnit.Meters);
var distanceScaled = distance * scale;
var bearingTransformed = bearing + -90;
var bearingRad = (Math.PI / 180) * bearingTransformed;
var x = distanceScaled * Math.Cos(bearingRad);
var y = distanceScaled * Math.Sin(bearingRad);
previousX += x;
previousY += y;
await _context.LineToAsync(previousX, previousY);
}

await _context.StrokeAsync();
}
}
}
7 changes: 7 additions & 0 deletions src/CyclingApp/Components/RouteMap.razor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function getBoundingRectangle(element) {
return element.getBoundingClientRect();
}

export function createResizeObserver(element, instance, callbackName) {
new ResizeObserver(() => instance.invokeMethodAsync(callbackName)).observe(element);
}
3 changes: 3 additions & 0 deletions src/CyclingApp/CyclingApp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<ServiceWorkerAssetsManifest>service-worker-assets.js</ServiceWorkerAssetsManifest>
<!-- Required to allow the code generator in the Roslyn compiler to use pointers for JS interop. -->
<!--<AllowUnsafeBlocks>true</AllowUnsafeBlocks>-->
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Blazor.Extensions.Canvas" Version="1.1.1" />
<PackageReference Include="Blazor.Geolocation" Version="8.0.0" />
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Geolocation" Version="1.2.1" />
Expand Down
2 changes: 1 addition & 1 deletion src/CyclingApp/DTOs/ConvertExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal static class ConvertExtensions
/// <returns>The created model.</returns>
public static GeoLocation ToModel(this StoreGeoLocationDto dto)
{
return new GeoLocation(dto.Timestamp, dto.Longitude, dto.Latitude, dto.Accuracy, dto.Altitude,
return new GeoLocation(dto.Timestamp, dto.Latitude, dto.Longitude, dto.Accuracy, dto.Altitude,
dto.AltitudeAccuracy, dto.Heading, dto.Speed);
}

Expand Down
10 changes: 5 additions & 5 deletions src/CyclingApp/Models/GeoLocation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
public class GeoLocation
{
/// <summary>
/// Longitude in decimal degrees.
/// Latitude in decimal degrees.
/// </summary>
public double Longitude { get; }
public double Latitude { get; }

/// <summary>
/// Latitude in decimal degrees.
/// Longitude in decimal degrees.
/// </summary>
public double Latitude { get; }
public double Longitude { get; }

/// <summary>
/// Accuracy of the <see cref="Latitude"/> and <see cref="Longitude"/> properties, expressed in meters.
Expand Down Expand Up @@ -63,7 +63,7 @@ public class GeoLocation
/// <summary>
/// Initializes a new instance of <see cref="GeoLocation"/> class.
/// </summary>
public GeoLocation(DateTime timestamp, double longitude, double latitude, double accuracy,
public GeoLocation(DateTime timestamp, double latitude, double longitude, double accuracy,
double? altitude, double? altitudeAccuracy, double? heading, double? speed)
{
Timestamp = timestamp;
Expand Down
3 changes: 2 additions & 1 deletion src/CyclingApp/Models/RouteBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ protected RouteBase(IList<GeoLocation> waypoints)
/// <returns>The average speed in kilometers per hour.</returns>
public double CalculateAvgSpeed(TimeSpan duration)
{
return Distance / 1000 / duration.TotalHours;
var result = Distance / 1000 / duration.TotalHours;
return double.IsNaN(result) ? 0 : result;
}

/// <summary>
Expand Down
49 changes: 17 additions & 32 deletions src/CyclingApp/Pages/Activity.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,20 @@

<PageTitle>Activity @_activity?.CreationTime.ToString("ddd, dd.MM.yy")</PageTitle>

<div Class="d-flex justify-center" Style="height: 100%; background: var(--mud-palette-surface);">
<div Style="max-width: 60vh; width: 100vw; background: var(--mud-palette-background);">
<MudStack Style="height: 100%;" Class="pa-4" Spacing="4">
<MudStack Row="true" Class="d-flex justify-space-between flex-grow-1 align-center">
<MudIconButton Size="@Size.Large" OnClick="@NavigateToHome" Icon="@Icons.Material.Filled.ArrowBackIosNew" aria-label="Navigate home" />
<MudText Typo="Typo.h5">@_activity?.CreationTime.ToString("ddd, dd.MM.yy")</MudText>
<MudIconButton Size="@Size.Large" OnClick="@NavigateToHome" Icon="@Icons.Material.Filled.Edit" aria-label="Edit" />
</MudStack>
<MudGrid Spacing="2">
<MudItem xs="6">
<MudPaper Class="pa-4">
<MudText>DISTANCE</MudText>
<MudText Typo="Typo.h4">@((_activity?.Route.Distance / 1000)?.ToString("N2"))</MudText>
<MudText>km</MudText>
</MudPaper>
</MudItem>
<MudItem xs="6">
<MudPaper Class="pa-4">
<MudText>AVG SPEED</MudText>
<MudText Typo="Typo.h4">@_activity?.AvgSpeed.ToString("N2")</MudText>
<MudText>km/h</MudText>
</MudPaper>
</MudItem>
</MudGrid>
<div Style="height: 100%;" />
<MudPaper Class="pa-4" Style="width: 100%;">
<MudText>TIME</MudText>
<MudText Typo="Typo.h4">@_activity?.Duration.ToString("hh\\:mm\\:ss")</MudText>
</MudPaper>
</MudStack>
</div>
</div>
<MudStack Class="d-flex flex-column pa-4 flex-1 overflow-auto" Spacing="4">
<MudStack Row="true" Class="d-flex justify-space-between align-center">
<MudIconButton Size="@Size.Large" OnClick="@NavigateToHome" Icon="@Icons.Material.Filled.ArrowBackIosNew" aria-label="Navigate home" />
<MudText Typo="Typo.h5">@_activity?.CreationTime.ToString("ddd, dd.MM.yy")</MudText>
<MudIconButton Size="@Size.Large" OnClick="@NavigateToHome" Icon="@Icons.Material.Filled.Edit" aria-label="Edit" />
</MudStack>
<MudGrid Spacing="2">
<MudItem xs="6">
<CyclingApp.Components.DataCard Title="Distance" Value=@((_activity?.Route.Distance / 1000)?.ToString("N2")) Unit="km" />
</MudItem>
<MudItem xs="6">
<CyclingApp.Components.DataCard Title="Avg speed" Value=@_activity?.AvgSpeed.ToString("N2") Unit="km/h" />
</MudItem>
</MudGrid>
<CyclingApp.Components.RouteMap Class="flex-grow-1" Route=@_activity?.Route />
<CyclingApp.Components.DataCard Title="Time" Value=@_activity?.Duration.ToString("hh\\:mm\\:ss") />
</MudStack>
Loading

0 comments on commit 39967dc

Please sign in to comment.