RobotApp/RobotApp.Client/Pages/Components/Mapping/MapView.razor
2025-10-02 09:49:16 +07:00

318 lines
12 KiB
Plaintext

@inject IJSRuntime JS
@using Excubo.Blazor.Canvas.Contexts
<div class="view">
<div class="toolbar">
<MudTooltip Text="Start Localization" role="button" Placement="Placement.Bottom" Color="Color.Info">
<button type="button" class="btn btn-secondary action-button" disabled="@(false)">
<i class="mdi mdi-play icon-button"></i>
</button>
</MudTooltip>
<MudTooltip Text="Stop Localization" role="button" Placement="Placement.Bottom" Color="Color.Info">
<button type="button" class="btn btn-secondary action-button" disabled="@(true)">
<i class="mdi mdi-pause icon-button"></i>
</button>
</MudTooltip>
<MudTooltip Text="Reset View" role="button" Placement="Placement.Bottom" Color="Color.Info">
<button type="button" class="btn btn-secondary action-button" @onclick="ResetView">
<i class="mdi mdi-restore icon-button"></i>
</button>
</MudTooltip>
<div class="ms-auto d-flex align-items-center">
<div class="zoom-info-container">
<small class="zoom-info">
<span class="info-item">
<i class="mdi mdi-magnify"></i>
Zoom: @($"{ZoomScale:F2}x")
</span>
<span class="info-separator">|</span>
<span class="info-item">
<i class="mdi mdi-crosshairs-gps"></i>
Mouse: (@($"{MouseX:F0}"), @($"{MouseY:F0}"))
</span>
<span class="info-separator">|</span>
<span class="info-item">
<i class="mdi mdi-map-marker"></i>
World: (@($"{WorldMouseX:F2}m"), @($"{WorldMouseY:F2}m"))
</span>
<span class="info-separator">|</span>
<span class="info-item">
<i class="mdi mdi-map-marker"></i>
Translate: (@($"{CanvasTranslateX:F2}"), @($"{CanvasTranslateY:F2}"))
</span>
</small>
</div>
</div>
</div>
<div @ref="ViewContainerRef" class="d-flex position-relative w-100 flex-grow-1 overflow-hidden">
<canvas @ref="CanvasRef"
@onwheel="HandleWheel"
@onwheel:preventDefault="true"
@onmousemove="HandleMouseMove"
@onmouseleave="HandleMouseLeave"
style="display: block; cursor: crosshair; transform: scale(1, -1)"></canvas>
</div>
</div>
@code {
private ElementReference CanvasRef;
private ElementReference ViewContainerRef;
private double ZoomScale = 1.0;
private const double MIN_ZOOM = 0.1;
private const double MAX_ZOOM = 5.0;
private const double BASE_PIXELS_PER_METER = 50.0;
private bool IsMouseInCanvas = false;
private double MouseX;
private double MouseY;
private double OriginX = 0;
private double OriginY = 0;
private double WorldMouseX;
private double WorldMouseY;
private double CanvasWidth;
private double CanvasHeight;
private double CanvasTranslateX = 0;
private double CanvasTranslateY = 0;
protected override async Task OnAfterRenderAsync(bool first_render)
{
await base.OnAfterRenderAsync(first_render);
if (!first_render) return;
var parentSize = await JS.InvokeAsync<DomRect>("getElementSize", ViewContainerRef);
CanvasWidth = parentSize.Width;
CanvasHeight = parentSize.Height;
await JS.InvokeVoidAsync("setCanvasSize", CanvasRef, CanvasWidth, CanvasHeight);
CanvasTranslateX = CanvasWidth / 2;
CanvasTranslateY = CanvasHeight / 2;
await DrawCanvas();
}
private async Task DrawCanvas()
{
await using var ctx = await JS.GetContext2DAsync(CanvasRef);
await ctx.ClearRectAsync(0, 0, CanvasWidth, CanvasHeight);
await ctx.SaveAsync();
await ctx.TranslateAsync(CanvasTranslateX, CanvasTranslateY);
await ctx.ScaleAsync(ZoomScale, ZoomScale);
await DrawGrid(ctx);
await DrawAxes(ctx);
await ctx.RestoreAsync();
}
private async Task DrawAxes(Context2D ctx)
{
double originCanvasX = OriginX * BASE_PIXELS_PER_METER;
double originCanvasY = OriginY * BASE_PIXELS_PER_METER;
await ctx.FillStyleAsync("red");
await ctx.BeginPathAsync();
await ctx.ArcAsync(originCanvasX, originCanvasY, 10 / ZoomScale, 0, Math.PI * 2);
await ctx.FillAsync(FillRule.NonZero);
await ctx.LineWidthAsync(2 / ZoomScale);
await ctx.StrokeStyleAsync("blue");
await ctx.BeginPathAsync();
await ctx.MoveToAsync(-CanvasWidth / ZoomScale, originCanvasY);
await ctx.LineToAsync(CanvasWidth / ZoomScale, originCanvasY);
await ctx.StrokeAsync();
await ctx.StrokeStyleAsync("red");
await ctx.BeginPathAsync();
await ctx.MoveToAsync(originCanvasX, -CanvasHeight / ZoomScale);
await ctx.LineToAsync(originCanvasX, CanvasHeight / ZoomScale);
await ctx.StrokeAsync();
double gridSpacingMeters = GetGridSpacingMeters();
double arrowLength = gridSpacingMeters * BASE_PIXELS_PER_METER;
double arrowHeadSize = 16 / ZoomScale;
await ctx.FillStyleAsync("blue");
await ctx.StrokeStyleAsync("blue");
await ctx.LineWidthAsync(4 / ZoomScale);
await ctx.BeginPathAsync();
await ctx.MoveToAsync(originCanvasX, originCanvasY);
await ctx.LineToAsync(originCanvasX + arrowLength, originCanvasY);
await ctx.StrokeAsync();
await ctx.BeginPathAsync();
await ctx.MoveToAsync(originCanvasX + arrowLength, originCanvasY);
await ctx.LineToAsync(originCanvasX + arrowLength - arrowHeadSize, originCanvasY - arrowHeadSize / 2);
await ctx.LineToAsync(originCanvasX + arrowLength - arrowHeadSize, originCanvasY + arrowHeadSize / 2);
await ctx.ClosePathAsync();
await ctx.FillAsync(FillRule.NonZero);
await ctx.FillStyleAsync("red");
await ctx.StrokeStyleAsync("red");
await ctx.LineWidthAsync(4 / ZoomScale);
await ctx.BeginPathAsync();
await ctx.MoveToAsync(originCanvasX, originCanvasY);
await ctx.LineToAsync(originCanvasX, originCanvasY + arrowLength);
await ctx.StrokeAsync();
await ctx.BeginPathAsync();
await ctx.MoveToAsync(originCanvasX, originCanvasY + arrowLength);
await ctx.LineToAsync(originCanvasX - arrowHeadSize / 2, originCanvasY + arrowLength - arrowHeadSize);
await ctx.LineToAsync(originCanvasX + arrowHeadSize / 2, originCanvasY + arrowLength - arrowHeadSize);
await ctx.ClosePathAsync();
await ctx.FillAsync(FillRule.NonZero);
}
private async Task DrawGrid(Context2D ctx)
{
await ctx.StrokeStyleAsync("rgba(200, 200, 200, 0.4)");
await ctx.LineWidthAsync(1 / ZoomScale);
await ctx.SetLineDashAsync(new double[] { 5 / ZoomScale, 5 / ZoomScale });
double gridSpacingMeters = GetGridSpacingMeters();
double gridSpacingPixels = gridSpacingMeters * BASE_PIXELS_PER_METER;
double visibleLeft = -CanvasTranslateX / ZoomScale;
double visibleRight = (CanvasWidth - CanvasTranslateX) / ZoomScale;
double visibleTop = -CanvasTranslateY / ZoomScale;
double visibleBottom = (CanvasHeight - CanvasTranslateY) / ZoomScale;
double startX = Math.Floor(visibleLeft / gridSpacingPixels) * gridSpacingPixels;
double startY = Math.Floor(visibleTop / gridSpacingPixels) * gridSpacingPixels;
for (double x = startX; x <= visibleRight; x += gridSpacingPixels)
{
await ctx.BeginPathAsync();
await ctx.MoveToAsync(x, visibleTop);
await ctx.LineToAsync(x, visibleBottom);
await ctx.StrokeAsync();
}
for (double y = startY; y <= visibleBottom; y += gridSpacingPixels)
{
await ctx.BeginPathAsync();
await ctx.MoveToAsync(visibleLeft, y);
await ctx.LineToAsync(visibleRight, y);
await ctx.StrokeAsync();
}
await ctx.SetLineDashAsync(new double[] { });
}
private double GetGridSpacingMeters()
{
if (BASE_PIXELS_PER_METER * ZoomScale >= 300) return 0.2;
else if (BASE_PIXELS_PER_METER * ZoomScale >= 150) return 0.5;
else if (BASE_PIXELS_PER_METER * ZoomScale >= 75) return 1.0;
else if (BASE_PIXELS_PER_METER * ZoomScale >= 40) return 2.0;
else if (BASE_PIXELS_PER_METER * ZoomScale >= 20) return 5.0;
else if (BASE_PIXELS_PER_METER * ZoomScale >= 10) return 10.0;
else return 20.0;
}
private double CanvasToWorldX(double canvasX)
{
return (canvasX - CanvasTranslateX) / ZoomScale / BASE_PIXELS_PER_METER - OriginX;
}
private double CanvasToWorldY(double canvasY)
{
return (canvasY - CanvasTranslateY) / ZoomScale / BASE_PIXELS_PER_METER - OriginY;
}
private double WorldToCanvasX(double worldX)
{
return (worldX + OriginX) * BASE_PIXELS_PER_METER * ZoomScale + CanvasTranslateX;
}
private double WorldToCanvasY(double worldY)
{
return (worldY + OriginY) * BASE_PIXELS_PER_METER * ZoomScale + CanvasTranslateY;
}
private async Task HandleMouseMove(MouseEventArgs e)
{
MouseX = e.OffsetX;
MouseY = e.OffsetY;
IsMouseInCanvas = true;
WorldMouseX = CanvasToWorldX(MouseX);
WorldMouseY = CanvasToWorldY(MouseY);
StateHasChanged();
if (e.Buttons == 4)
{
CanvasTranslateX += e.MovementX;
CanvasTranslateY -= e.MovementY;
await DrawCanvas();
}
}
private async Task HandleMouseLeave(MouseEventArgs e)
{
IsMouseInCanvas = false;
MouseX = 0;
MouseY = 0;
StateHasChanged();
await DrawCanvas();
}
private async Task HandleWheel(WheelEventArgs e)
{
if (e.Buttons == 4) return;
const double zoomFactor = 0.1;
double oldZoom = ZoomScale;
if (e.DeltaY < 0) ZoomScale = Math.Min(MAX_ZOOM, ZoomScale * (1 + zoomFactor));
else ZoomScale = Math.Max(MIN_ZOOM, ZoomScale * (1 - zoomFactor));
if (Math.Abs(ZoomScale - oldZoom) < 0.001) return;
MouseX = e.OffsetX;
MouseY = e.OffsetY;
WorldMouseX = CanvasToWorldX(MouseX);
WorldMouseY = CanvasToWorldY(MouseY);
double zoomPointWorldX = (MouseX - CanvasTranslateX) / oldZoom / BASE_PIXELS_PER_METER - OriginX;
double zoomPointWorldY = (MouseY - CanvasTranslateY) / oldZoom / BASE_PIXELS_PER_METER - OriginY;
double newZoomPointCanvasX = (zoomPointWorldX + OriginX) * BASE_PIXELS_PER_METER * ZoomScale;
double newZoomPointCanvasY = (zoomPointWorldY + OriginY) * BASE_PIXELS_PER_METER * ZoomScale;
CanvasTranslateX = MouseX - newZoomPointCanvasX;
CanvasTranslateY = MouseY - newZoomPointCanvasY;
StateHasChanged();
await DrawCanvas();
}
private async Task ResetView()
{
CanvasTranslateX = CanvasWidth / 2;
CanvasTranslateY = CanvasHeight / 2;
ZoomScale = 1.0;
StateHasChanged();
await DrawCanvas();
}
public class DomRect
{
public double Width { get; set; }
public double Height { get; set; }
}
}