diff --git a/RobotApp.Client/Pages/Components/Mapping/MapTable.razor b/RobotApp.Client/Pages/Components/Mapping/MapTable.razor index b1851bd..01b8555 100644 --- a/RobotApp.Client/Pages/Components/Mapping/MapTable.razor +++ b/RobotApp.Client/Pages/Components/Mapping/MapTable.razor @@ -1,53 +1,57 @@ @inject HttpClient Http +@inject IJSRuntime JS + +
+ + +

Maps

+
+ + Nr + Name + Width (m) + Height (m) + Resolution (m/px) + OriginX + OriginY + + + + + @(Table?.CurrentPage * Table?.RowsPerPage + MapsShow.IndexOf(context) + 1) + + + @context.Name + + + @context.Width + + + @context.Height + + + @context.Resolution + + + @context.OriginX + + + @context.OriginY + + + Active + Delete + + + +
+ +
+
+
+
- - - Maps - - - Nr - Name - Width (m) - Height (m) - Resolution (m/px) - OriginX - OriginY - - - - - @(Table?.CurrentPage * Table?.RowsPerPage + MapsShow.IndexOf(context) + 1) - - - @context.Name - - - @context.Width - - - @context.Height - - - @context.Resolution - - - @context.OriginX - - - @context.OriginY - - - Active - Delete - - - -
- -
-
-
@code { private string txtSearch = ""; @@ -60,15 +64,21 @@ private MapDto MapSelected = new(); private MudTable? Table; + private ElementReference ViewContainerRef; + private ElementReference ToolbarRef; private MapPreview? MapPreviewRef; + private double TableHeight = 105; protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); if (!firstRender) return; + var containerSize = await JS.InvokeAsync("getElementSize", ViewContainerRef); + TableHeight = containerSize.Height - 105; // await LoadMaps(); + StateHasChanged(); } private async Task LoadMaps() @@ -120,4 +130,10 @@ MapsShow = tasks.Skip(state.Page * state.PageSize).Take(state.PageSize).ToList(); return Task.FromResult(new TableData() { TotalItems = tasks.Count, Items = MapsShow }); } + + public class DomRect + { + public double Width { get; set; } + public double Height { get; set; } + } } diff --git a/RobotApp.Client/Pages/Components/Mapping/MapView.razor b/RobotApp.Client/Pages/Components/Mapping/MapView.razor index 86aaa64..9009314 100644 --- a/RobotApp.Client/Pages/Components/Mapping/MapView.razor +++ b/RobotApp.Client/Pages/Components/Mapping/MapView.razor @@ -1,16 +1,15 @@ @inject IJSRuntime JS -@inject HttpClient Http @using Excubo.Blazor.Canvas.Contexts
- - @@ -32,27 +31,23 @@ - - - -
+ @onmouseleave="HandleMouseLeave" + @ontouchstart="HandleTouchStart" + @ontouchmove="HandleTouchMove" + @ontouchend="HandleTouchEnd">
@@ -91,22 +86,27 @@ private bool MapImageLoaded = false; private const double ImageX = -10; - private const double ImageY = -10; + private const double ImageY = -5; private const double ImageResolution = 0.05; private double MapImageWidth = 0; private double MapImageHeight = 0; private const string MAP_CACHE_KEY = "map_image"; + private TouchPoint? LastTouchPoint; + private TouchPoint? LastSecondTouchPoint; + private double LastTouchDistance = 0; + private bool IsTouching = false; + protected override async Task OnAfterRenderAsync(bool first_render) { await base.OnAfterRenderAsync(first_render); if (!first_render) return; - var parentSize = await JS.InvokeAsync("getElementSize", ViewContainerRef); + var containerSize = await JS.InvokeAsync("getElementSize", ViewContainerRef); - CanvasWidth = parentSize.Width; - CanvasHeight = parentSize.Height; + CanvasWidth = containerSize.Width; + CanvasHeight = containerSize.Height; await JS.InvokeVoidAsync("setCanvasSize", CanvasRef, CanvasWidth, CanvasHeight); @@ -137,11 +137,10 @@ try { MapImageLoaded = false; - string baseUrl = Http.BaseAddress?.ToString() ?? ""; - string apiUrl = $"{baseUrl}api/images/map"; + string apiUrl = "api/images/mapping"; await JS.InvokeVoidAsync("preloadImageFromUrl", apiUrl, MAP_CACHE_KEY); - var imageDimensions = await JS.InvokeAsync("getImageDimensions", MAP_CACHE_KEY); + var imageDimensions = await JS.InvokeAsync("getImageDimensions", MAP_CACHE_KEY); MapImageWidth = imageDimensions.Width * ImageResolution; MapImageHeight = imageDimensions.Height * ImageResolution; if (MapImageWidth > 0 && MapImageHeight > 0) MapImageLoaded = true; @@ -152,545 +151,61 @@ } } - private async Task DrawCanvas() + private async Task ResetView() { - await using var ctx = await JS.GetContext2DAsync(CanvasRef); + CanvasTranslateX = CanvasWidth / 2; + CanvasTranslateY = CanvasHeight / 2; + ZoomScale = 1.0; + StateHasChanged(); + await DrawCanvas(); + } - await ctx.ClearRectAsync(0, 0, CanvasWidth, CanvasHeight); - - await DrawRulers(ctx); - - await ctx.SaveAsync(); - - await ctx.TranslateAsync(CanvasTranslateX, CanvasTranslateY); - await ctx.ScaleAsync(ZoomScale, ZoomScale); + private async Task ZoomIn() + { + const double zoomFactor = 0.15; + double oldZoom = ZoomScale; - await DrawMapImage(ctx); - await DrawGrid(ctx); - await DrawAxes(ctx); - await DrawLaserScannerPoints(ctx); - await ctx.RestoreAsync(); + ZoomScale = Math.Min(MAX_ZOOM, ZoomScale * (1 + zoomFactor)); + + if (Math.Abs(ZoomScale - oldZoom) < 0.001) return; + + await ZoomAtCenter(oldZoom); + } + private async Task ZoomOut() + { + const double zoomFactor = 0.15; + double oldZoom = ZoomScale; + + ZoomScale = Math.Max(MIN_ZOOM, ZoomScale * (1 - zoomFactor)); + + if (Math.Abs(ZoomScale - oldZoom) < 0.001) return; + + await ZoomAtCenter(oldZoom); + } + + private async Task ZoomAtCenter(double oldZoom) + { + double centerX = CanvasWidth / 2; + double centerY = CanvasHeight / 2; + + double centerWorldX = (centerX - CanvasTranslateX) / oldZoom / BASE_PIXELS_PER_METER - OriginX; + double centerWorldY = (centerY - CanvasTranslateY) / oldZoom / BASE_PIXELS_PER_METER - OriginY; + + double newCenterCanvasX = (centerWorldX + OriginX) * BASE_PIXELS_PER_METER * ZoomScale; + double newCenterCanvasY = (centerWorldY + OriginY) * BASE_PIXELS_PER_METER * ZoomScale; + + CanvasTranslateX = centerX - newCenterCanvasX; + CanvasTranslateY = centerY - newCenterCanvasY; + if (IsMouseInCanvas) { - await DrawMouseIndicator(ctx); + WorldMouseX = CanvasToWorldX(MouseX); + WorldMouseY = CanvasToWorldY(MouseY); } - } - - private async Task DrawMapImage(Context2D ctx) - { - if (!MapImageLoaded) - { - return; - } - - await ctx.SaveAsync(); - - try - { - - double imageWidthCanvas = MapImageWidth * BASE_PIXELS_PER_METER; - double imageHeightCanvas = MapImageHeight * BASE_PIXELS_PER_METER; - - double mapCanvasX = ImageX * BASE_PIXELS_PER_METER; - double mapCanvasY = (ImageY + MapImageHeight) * BASE_PIXELS_PER_METER; - - bool success = await JS.InvokeAsync("drawCachedImageOnCanvas", - CanvasRef, - MAP_CACHE_KEY, - mapCanvasX, - mapCanvasY - imageHeightCanvas, - imageWidthCanvas, - imageHeightCanvas); - } - catch - { - } - - await ctx.RestoreAsync(); - } - - private async Task DrawLaserScannerPoints(Context2D ctx) - { - var scanData = GenerateLaserScanData(); - - double robotCanvasX = scanData.RobotX * BASE_PIXELS_PER_METER; - double robotCanvasY = scanData.RobotY * BASE_PIXELS_PER_METER; - - await ctx.SaveAsync(); - - if (scanData.Points.Count > 0) - { - await ctx.BeginPathAsync(); - - for (int i = 0; i < scanData.Points.Count; i++) - { - var point = scanData.Points[i]; - double pointCanvasX = point.X * BASE_PIXELS_PER_METER; - double pointCanvasY = point.Y * BASE_PIXELS_PER_METER; - - if (i == 0) - { - await ctx.MoveToAsync(pointCanvasX, pointCanvasY); - } - else - { - await ctx.LineToAsync(pointCanvasX, pointCanvasY); - } - } - - await ctx.StrokeStyleAsync("rgba(255, 100, 100, 0.8)"); - await ctx.LineWidthAsync(2 / ZoomScale); - await ctx.StrokeAsync(); - - await ctx.LineToAsync(robotCanvasX, robotCanvasY); - await ctx.ClosePathAsync(); - await ctx.FillStyleAsync("rgba(255, 100, 100, 0.1)"); - await ctx.FillAsync(FillRule.NonZero); - } - - await DrawRobotImage(ctx, robotCanvasX, robotCanvasY, scanData.RobotOrientation); - await DrawRobotOrientationArrows(ctx, robotCanvasX, robotCanvasY, scanData.RobotOrientation); - - await ctx.RestoreAsync(); - } - - private async Task DrawRobotImage(Context2D ctx, double robotCanvasX, double robotCanvasY, double robotOrientation) - { - if (!RobotImageLoaded) - { - // Fallback to circle if image not loaded - await ctx.FillStyleAsync("rgba(0, 255, 0, 0.8)"); - await ctx.BeginPathAsync(); - await ctx.ArcAsync(robotCanvasX, robotCanvasY, 8 / ZoomScale, 0, Math.PI * 2); - await ctx.FillAsync(FillRule.NonZero); - return; - } - - await ctx.SaveAsync(); - - double robotWidthPixels = RobotWidth * BASE_PIXELS_PER_METER; - double robotLengthPixels = RobotLength * BASE_PIXELS_PER_METER; - - double scaledWidth = ZoomScale < 1 ? robotWidthPixels / ZoomScale : robotWidthPixels; - double scaledLength = ZoomScale < 1 ? robotLengthPixels / ZoomScale : robotLengthPixels; - - await ctx.TranslateAsync(robotCanvasX, robotCanvasY); - await ctx.RotateAsync(robotOrientation); - - try - { - bool success = await JS.InvokeAsync("drawImageOnCanvas", - CanvasRef, - "images/AMR-250.png", - -scaledLength / 2, - -scaledWidth / 2, - scaledLength, - scaledWidth); - - if (!success) - { - await ctx.FillStyleAsync("rgba(0, 255, 0, 0.8)"); - await ctx.FillRectAsync(-scaledLength / 2, -scaledWidth / 2, scaledLength, scaledWidth); - } - } - catch - { - await ctx.FillStyleAsync("rgba(0, 255, 0, 0.8)"); - await ctx.FillRectAsync(-scaledLength / 2, -scaledWidth / 2, scaledLength, scaledWidth); - } - - await ctx.RestoreAsync(); - } - - private async Task DrawRobotOrientationArrows(Context2D ctx, double robotCanvasX, double robotCanvasY, double robotOrientation) - { - double arrowLength = 30 / ZoomScale; - double arrowHeadSize = 10 / ZoomScale; - - await ctx.StrokeStyleAsync("rgba(0, 100, 255, 1.0)"); - await ctx.FillStyleAsync("rgba(0, 100, 255, 1.0)"); - await ctx.LineWidthAsync(3 / ZoomScale); - - await ctx.BeginPathAsync(); - await ctx.MoveToAsync(robotCanvasX, robotCanvasY); - double xAxisEndX = robotCanvasX + Math.Cos(robotOrientation) * (arrowLength - arrowHeadSize + 1); - double xAxisEndY = robotCanvasY + Math.Sin(robotOrientation) * (arrowLength - arrowHeadSize + 1); - await ctx.LineToAsync(xAxisEndX, xAxisEndY); - await ctx.StrokeAsync(); - - await ctx.BeginPathAsync(); - double xArrowTipX = robotCanvasX + Math.Cos(robotOrientation) * arrowLength; - double xArrowTipY = robotCanvasY + Math.Sin(robotOrientation) * arrowLength; - await ctx.MoveToAsync(xArrowTipX, xArrowTipY); - double xArrowAngle = robotOrientation + Math.PI; - await ctx.LineToAsync(xArrowTipX + Math.Cos(xArrowAngle + Math.PI / 6) * arrowHeadSize, xArrowTipY + Math.Sin(xArrowAngle + Math.PI / 6) * arrowHeadSize); - await ctx.LineToAsync(xArrowTipX + Math.Cos(xArrowAngle - Math.PI / 6) * arrowHeadSize, xArrowTipY + Math.Sin(xArrowAngle - Math.PI / 6) * arrowHeadSize); - await ctx.ClosePathAsync(); - await ctx.FillAsync(FillRule.NonZero); - - await ctx.StrokeStyleAsync("rgba(255, 50, 50, 1.0)"); - await ctx.FillStyleAsync("rgba(255, 50, 50, 1.0)"); - await ctx.LineWidthAsync(3 / ZoomScale); - - double yAxisAngle = robotOrientation + Math.PI / 2; - - await ctx.BeginPathAsync(); - await ctx.MoveToAsync(robotCanvasX, robotCanvasY); - double yAxisEndX = robotCanvasX + Math.Cos(yAxisAngle) * (arrowLength - arrowHeadSize + 1); - double yAxisEndY = robotCanvasY + Math.Sin(yAxisAngle) * (arrowLength - arrowHeadSize + 1); - await ctx.LineToAsync(yAxisEndX, yAxisEndY); - await ctx.StrokeAsync(); - - await ctx.BeginPathAsync(); - double yArrowTipX = robotCanvasX + Math.Cos(yAxisAngle) * arrowLength; - double yArrowTipY = robotCanvasY + Math.Sin(yAxisAngle) * arrowLength; - await ctx.MoveToAsync(yArrowTipX, yArrowTipY); - double yArrowAngle = yAxisAngle + Math.PI; - await ctx.LineToAsync(yArrowTipX + Math.Cos(yArrowAngle + Math.PI / 6) * arrowHeadSize, yArrowTipY + Math.Sin(yArrowAngle + Math.PI / 6) * arrowHeadSize); - await ctx.LineToAsync(yArrowTipX + Math.Cos(yArrowAngle - Math.PI / 6) * arrowHeadSize, yArrowTipY + Math.Sin(yArrowAngle - Math.PI / 6) * arrowHeadSize); - await ctx.ClosePathAsync(); - await ctx.FillAsync(FillRule.NonZero); - } - - private async Task DrawMouseIndicator(Context2D ctx) - { - await ctx.SaveAsync(); - - await ctx.StrokeStyleAsync("rgba(255, 50, 50, 0.8)"); - await ctx.LineWidthAsync(1); - await ctx.SetLineDashAsync(new double[] { 3, 3 }); - - await ctx.BeginPathAsync(); - await ctx.MoveToAsync(MouseX, RulerHeight); - await ctx.LineToAsync(MouseX, CanvasHeight); - await ctx.StrokeAsync(); - - await ctx.BeginPathAsync(); - await ctx.MoveToAsync(RulerHeight, MouseY); - await ctx.LineToAsync(CanvasWidth, MouseY); - await ctx.StrokeAsync(); - - await ctx.SetLineDashAsync(new double[] { }); - - const double labelPadding = 7; - const double labelMargin = 8; - - string coordinateText = $"({WorldMouseX:F2}m, {WorldMouseY:F2}m)"; - - await ctx.FontAsync("bold 12px Arial"); - var textMetrics = await ctx.MeasureTextAsync(coordinateText); - double textWidth = textMetrics.Width; - double textHeight = 16; - - double labelX = MouseX + labelMargin; - double labelY = MouseY - textHeight - labelPadding * 2 - labelMargin; - - if (labelX + textWidth + labelPadding * 2 > CanvasWidth) - { - labelX = MouseX - textWidth - labelPadding * 2 - labelMargin; - } - if (labelY - textHeight - labelPadding * 2 < RulerHeight) - { - labelY = MouseY + labelMargin; - } - - await ctx.FillStyleAsync("rgba(0, 0, 0, 0.8)"); - await ctx.FillRectAsync(labelX, labelY, textWidth + labelPadding * 2, textHeight + labelPadding * 2); - - await ctx.StrokeStyleAsync("rgba(255,255,255,0.6)"); - await ctx.LineWidthAsync(1); - await ctx.StrokeRectAsync(labelX, labelY, textWidth + labelPadding * 2, textHeight + labelPadding * 2); - - await ctx.FillStyleAsync("rgba(255, 50, 50, 0.9)"); - await ctx.BeginPathAsync(); - await ctx.ArcAsync(MouseX, MouseY, 3, 0, Math.PI * 2); - await ctx.FillAsync(FillRule.NonZero); - - await ctx.StrokeStyleAsync("rgba(255, 255, 255, 0.8)"); - await ctx.LineWidthAsync(2); - await ctx.BeginPathAsync(); - await ctx.ArcAsync(MouseX, MouseY, 6, 0, Math.PI * 2); - await ctx.StrokeAsync(); - - await ctx.SaveAsync(); - - await ctx.TranslateAsync(labelX + labelPadding + textWidth / 2, labelY + textHeight / 2); - await ctx.ScaleAsync(1, -1); - - await ctx.FillStyleAsync("white"); - await ctx.FontAsync("bold 12px Arial"); - await ctx.TextAlignAsync(TextAlign.Center); - await ctx.TextBaseLineAsync(TextBaseLine.Bottom); - await ctx.FillTextAsync(coordinateText, 0, 0); - - await ctx.RestoreAsync(); - } - - private async Task DrawRulers(Context2D ctx) - { - double visibleWorldLeft = CanvasToWorldX(0); - double visibleWorldRight = CanvasToWorldX(CanvasWidth); - double visibleWorldTop = CanvasToWorldY(0); - double visibleWorldBottom = CanvasToWorldY(CanvasHeight); - - double scaleInterval = GetRulerScaleInterval(); - - await DrawXRuler(ctx, RulerHeight, visibleWorldLeft, visibleWorldRight, scaleInterval); - await DrawYRuler(ctx, RulerHeight, visibleWorldTop, visibleWorldBottom, scaleInterval); - } - - private double GetRulerScaleInterval() - { - double pixelsPerMeter = BASE_PIXELS_PER_METER * ZoomScale; - - if (pixelsPerMeter >= 400) return 0.1; - else if (pixelsPerMeter >= 200) return 0.2; - else if (pixelsPerMeter >= 100) return 0.5; - else if (pixelsPerMeter >= 50) return 1.0; - else if (pixelsPerMeter >= 25) return 2.0; - else if (pixelsPerMeter >= 12) return 5.0; - else if (pixelsPerMeter >= 6) return 10.0; - else return 20.0; - } - - private async Task DrawXRuler(Context2D ctx, double rulerHeight, double visibleWorldLeft, double visibleWorldRight, double scaleInterval) - { - await ctx.FillStyleAsync("rgba(240, 240, 240, 0.9)"); - await ctx.FillRectAsync(0, 0, CanvasWidth, rulerHeight); - - await ctx.StrokeStyleAsync("rgba(100, 100, 100, 0.8)"); - await ctx.LineWidthAsync(1); - await ctx.BeginPathAsync(); - await ctx.MoveToAsync(0, rulerHeight); - await ctx.LineToAsync(CanvasWidth, rulerHeight); - await ctx.StrokeAsync(); - - double startWorld = Math.Floor(visibleWorldLeft / scaleInterval) * scaleInterval; - double endWorld = Math.Ceiling(visibleWorldRight / scaleInterval) * scaleInterval; - - startWorld -= scaleInterval; - endWorld += scaleInterval; - - for (double worldX = startWorld; worldX <= endWorld; worldX += scaleInterval) - { - double canvasX = WorldToCanvasX(worldX); - - if (canvasX < -50 || canvasX > CanvasWidth + 50) continue; - - bool isMajorTick = IsNearMultiple(worldX, scaleInterval * 2) || Math.Abs(worldX) < 0.001; - double tickHeight = isMajorTick ? rulerHeight * 0.4 : rulerHeight * 0.2; - - await ctx.StrokeStyleAsync("rgba(60, 60, 60, 0.8)"); - await ctx.LineWidthAsync(1); - await ctx.BeginPathAsync(); - await ctx.MoveToAsync(canvasX, rulerHeight); - await ctx.LineToAsync(canvasX, rulerHeight - tickHeight); - await ctx.StrokeAsync(); - - if (isMajorTick && canvasX >= -20 && canvasX <= CanvasWidth + 20) - { - await ctx.SaveAsync(); - - await ctx.TranslateAsync(canvasX, rulerHeight - tickHeight - 8); - await ctx.ScaleAsync(1, -1); - - await ctx.FillStyleAsync("blue"); - await ctx.FontAsync("bold 10px Arial"); - await ctx.TextAlignAsync(TextAlign.Center); - - string labelText = FormatRulerLabel(worldX, scaleInterval); - await ctx.FillTextAsync(labelText, 0, 0); - - await ctx.RestoreAsync(); - } - } - } - - private async Task DrawYRuler(Context2D ctx, double rulerWidth, double visibleWorldTop, double visibleWorldBottom, double scaleInterval) - { - await ctx.FillStyleAsync("rgba(240, 240, 240, 0.9)"); - await ctx.FillRectAsync(0, 0, rulerWidth, CanvasHeight); - - await ctx.StrokeStyleAsync("rgba(100, 100, 100, 0.8)"); - await ctx.LineWidthAsync(1); - await ctx.BeginPathAsync(); - await ctx.MoveToAsync(rulerWidth, 0); - await ctx.LineToAsync(rulerWidth, CanvasHeight); - await ctx.StrokeAsync(); - - double startWorld = Math.Floor(visibleWorldTop / scaleInterval) * scaleInterval; - double endWorld = Math.Ceiling(visibleWorldBottom / scaleInterval) * scaleInterval; - - startWorld -= scaleInterval; - endWorld += scaleInterval; - - for (double worldY = startWorld; worldY <= endWorld; worldY += scaleInterval) - { - double canvasY = WorldToCanvasY(worldY); - - if (canvasY < -50 || canvasY > CanvasHeight + 50) continue; - - bool isMajorTick = IsNearMultiple(worldY, scaleInterval * 2) || Math.Abs(worldY) < 0.001; - double tickWidth = isMajorTick ? rulerWidth * 0.4 : rulerWidth * 0.2; - - await ctx.StrokeStyleAsync("rgba(60, 60, 60, 0.8)"); - await ctx.LineWidthAsync(1); - await ctx.BeginPathAsync(); - await ctx.MoveToAsync(rulerWidth, canvasY); - await ctx.LineToAsync(rulerWidth - tickWidth, canvasY); - await ctx.StrokeAsync(); - - if (isMajorTick && canvasY >= -20 && canvasY <= CanvasHeight + 20) - { - await ctx.SaveAsync(); - - await ctx.TranslateAsync(rulerWidth - tickWidth - 2, canvasY); - await ctx.ScaleAsync(1, -1); - await ctx.RotateAsync(-Math.PI / 2); - - await ctx.FillStyleAsync("blue"); - await ctx.FontAsync("bold 10px Arial"); - await ctx.TextAlignAsync(TextAlign.Center); - - string labelText = FormatRulerLabel(worldY, scaleInterval); - await ctx.FillTextAsync(labelText, 0, 0); - - await ctx.RestoreAsync(); - } - } - } - - private bool IsNearMultiple(double value, double multiple) - { - if (multiple == 0) return false; - double remainder = Math.Abs(value % multiple); - double epsilon = multiple * 0.001; - return remainder < epsilon || remainder > multiple - epsilon; - } - - private string FormatRulerLabel(double worldValue, double scaleInterval) - { - return scaleInterval < 1.0 ? $"{worldValue:F1}m" : $"{worldValue:F0}m"; - } - - 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, 8 / ZoomScale, 0, Math.PI * 2); - await ctx.FillAsync(FillRule.NonZero); - - 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 - arrowHeadSize, originCanvasY); - await ctx.StrokeAsync(); - - await ctx.BeginPathAsync(); - double xArrowTipX = originCanvasX + arrowLength; - double xArrowTipY = originCanvasY; - await ctx.MoveToAsync(xArrowTipX, xArrowTipY); - await ctx.LineToAsync(xArrowTipX - arrowHeadSize, xArrowTipY - arrowHeadSize / 2); - await ctx.LineToAsync(xArrowTipX - arrowHeadSize, xArrowTipY + 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 - arrowHeadSize); - await ctx.StrokeAsync(); - - await ctx.BeginPathAsync(); - double yArrowTipX = originCanvasX; - double yArrowTipY = originCanvasY + arrowLength; - await ctx.MoveToAsync(yArrowTipX, yArrowTipY); - await ctx.LineToAsync(yArrowTipX - arrowHeadSize / 2, yArrowTipY - arrowHeadSize); - await ctx.LineToAsync(yArrowTipX + arrowHeadSize / 2, yArrowTipY - 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() - { - double PixelsPerMeter = BASE_PIXELS_PER_METER * ZoomScale; - if (PixelsPerMeter >= 300) return 0.2; - else if (PixelsPerMeter >= 150) return 0.5; - else if (PixelsPerMeter >= 75) return 1.0; - else if (PixelsPerMeter >= 40) return 2.0; - else if (PixelsPerMeter >= 20) return 5.0; - else if (PixelsPerMeter >= 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; + + StateHasChanged(); + await DrawCanvas(); } private async Task HandleMouseMove(MouseEventArgs e) @@ -751,21 +266,136 @@ await DrawCanvas(); } - private async Task ResetView() + private async Task HandleTouchMove(TouchEventArgs e) { - CanvasTranslateX = CanvasWidth / 2; - CanvasTranslateY = CanvasHeight / 2; - ZoomScale = 1.0; + if (e.Touches.Length == 1) + { + await HandleSingleTouchMove(e.Touches[0]); + } + else if (e.Touches.Length == 2) + { + await HandlePinchZoom(e.Touches[0], e.Touches[1]); + } + StateHasChanged(); await DrawCanvas(); } - private async Task ReloadMapImage() + private void HandleTouchStart(TouchEventArgs e) { - MapImageLoaded = false; - await LoadMapImage(); - await DrawCanvas(); - StateHasChanged(); + IsTouching = true; + + if (e.Touches.Length == 1) + { + LastTouchPoint = new TouchPoint + { + X = e.Touches[0].ClientX, + Y = e.Touches[0].ClientY + }; + } + else if (e.Touches.Length == 2) + { + LastTouchPoint = new TouchPoint + { + X = e.Touches[0].ClientX, + Y = e.Touches[0].ClientY + }; + LastSecondTouchPoint = new TouchPoint + { + X = e.Touches[1].ClientX, + Y = e.Touches[1].ClientY + }; + + LastTouchDistance = CalculateTouchDistance(LastTouchPoint, LastSecondTouchPoint); + } + } + + private void HandleTouchEnd(TouchEventArgs e) + { + IsTouching = false; + LastTouchPoint = null; + LastSecondTouchPoint = null; + LastTouchDistance = 0; + } + + private async Task HandleSingleTouchMove(Microsoft.AspNetCore.Components.Web.TouchPoint touch) + { + if (LastTouchPoint == null) return; + + var currentPoint = new TouchPoint + { + X = touch.ClientX, + Y = touch.ClientY + }; + + double deltaX = currentPoint.X - LastTouchPoint.X; + double deltaY = currentPoint.Y - LastTouchPoint.Y; + + CanvasTranslateX += deltaX; + CanvasTranslateY += deltaY; + + LastTouchPoint = currentPoint; + + var canvasRect = await JS.InvokeAsync("getElementBoundingRect", CanvasRef); + MouseX = currentPoint.X - canvasRect.X; + MouseY = currentPoint.Y - canvasRect.Y; + IsMouseInCanvas = true; + + WorldMouseX = CanvasToWorldX(MouseX); + WorldMouseY = CanvasToWorldY(MouseY); + } + + private async Task HandlePinchZoom(Microsoft.AspNetCore.Components.Web.TouchPoint touch1, Microsoft.AspNetCore.Components.Web.TouchPoint touch2) + { + if (LastTouchPoint == null || LastSecondTouchPoint == null) return; + + var currentTouch1 = new TouchPoint { X = touch1.ClientX, Y = touch1.ClientY }; + var currentTouch2 = new TouchPoint { X = touch2.ClientX, Y = touch2.ClientY }; + + double currentDistance = CalculateTouchDistance(currentTouch1, currentTouch2); + + if (LastTouchDistance > 0) + { + double distanceRatio = currentDistance / LastTouchDistance; + double oldZoom = ZoomScale; + + ZoomScale = Math.Max(MIN_ZOOM, Math.Min(MAX_ZOOM, ZoomScale * distanceRatio)); + + if (Math.Abs(ZoomScale - oldZoom) > 0.001) + { + double centerX = (currentTouch1.X + currentTouch2.X) / 2; + double centerY = (currentTouch1.Y + currentTouch2.Y) / 2; + + var canvasRect = await JS.InvokeAsync("getElementBoundingRect", CanvasRef); + double canvasCenterX = centerX - canvasRect.X; + double canvasCenterY = centerY - canvasRect.Y; + + ZoomAtPoint(oldZoom, canvasCenterX, canvasCenterY); + } + } + + LastTouchDistance = currentDistance; + LastTouchPoint = currentTouch1; + LastSecondTouchPoint = currentTouch2; + } + + private double CalculateTouchDistance(TouchPoint point1, TouchPoint point2) + { + double deltaX = point2.X - point1.X; + double deltaY = point2.Y - point1.Y; + return Math.Sqrt(deltaX * deltaX + deltaY * deltaY); + } + + private void ZoomAtPoint(double oldZoom, double pointX, double pointY) + { + double pointWorldX = (pointX - CanvasTranslateX) / oldZoom / BASE_PIXELS_PER_METER - OriginX; + double pointWorldY = (pointY - CanvasTranslateY) / oldZoom / BASE_PIXELS_PER_METER - OriginY; + + double newPointCanvasX = (pointWorldX + OriginX) * BASE_PIXELS_PER_METER * ZoomScale; + double newPointCanvasY = (pointWorldY + OriginY) * BASE_PIXELS_PER_METER * ZoomScale; + + CanvasTranslateX = pointX - newPointCanvasX; + CanvasTranslateY = pointY - newPointCanvasY; } private LaserScanData GenerateLaserScanData() @@ -779,7 +409,7 @@ // Laser scanner parameters const double maxRange = 8.0; // meters - const double minRange = 0.2; // meters (fix: was 7.0, should be minimum) + const double minRange = 0.5; // meters (fix: was 7.0, should be minimum) const int numPoints = 270; // Number of laser points const double startAngle = -Math.PI / 2 - Math.PI / 4; const double endAngle = Math.PI / 2 + Math.PI / 4; @@ -830,30 +460,4 @@ return scanData; } - - public class LaserScanPoint - { - public double X { get; set; } - public double Y { get; set; } - } - - public class LaserScanData - { - public double RobotX { get; set; } - public double RobotY { get; set; } - public double RobotOrientation { get; set; } - public List Points { get; set; } = new(); - } - - public class DomRect - { - public double Width { get; set; } - public double Height { get; set; } - } - - public class ImageDimensions - { - public double Width { get; set; } - public double Height { get; set; } - } } diff --git a/RobotApp.Client/Pages/Components/Mapping/MapView.razor.cs b/RobotApp.Client/Pages/Components/Mapping/MapView.razor.cs new file mode 100644 index 0000000..5746543 --- /dev/null +++ b/RobotApp.Client/Pages/Components/Mapping/MapView.razor.cs @@ -0,0 +1,576 @@ +using Excubo.Blazor.Canvas; +using Excubo.Blazor.Canvas.Contexts; +using Microsoft.JSInterop; + +namespace RobotApp.Client.Pages.Components.Mapping; + +public partial class MapView +{ + public class TouchPoint + { + public double X { get; set; } + public double Y { get; set; } + } + + public class LaserScanPoint + { + public double X { get; set; } + public double Y { get; set; } + } + + public class LaserScanData + { + public double RobotX { get; set; } + public double RobotY { get; set; } + public double RobotOrientation { get; set; } + public List Points { get; set; } = []; + } + + public class DomRect + { + public double Width { get; set; } + public double Height { get; set; } + public double X { get; set; } + public double Y { get; set; } + public double Left => X; + public double Top => Y; + } + + 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 DrawMapImage(ctx); + await DrawGrid(ctx); + await DrawAxes(ctx); + await DrawLaserScannerPoints(ctx); + await ctx.RestoreAsync(); + + if (IsMouseInCanvas) + { + await DrawMouseIndicator(ctx); + } + await DrawRulers(ctx); + } + + private async Task DrawMouseIndicator(Context2D ctx) + { + await ctx.SaveAsync(); + + await ctx.StrokeStyleAsync("rgba(255, 50, 50, 0.8)"); + await ctx.LineWidthAsync(1); + await ctx.SetLineDashAsync([3, 3]); + + await ctx.BeginPathAsync(); + await ctx.MoveToAsync(MouseX, RulerHeight); + await ctx.LineToAsync(MouseX, CanvasHeight); + await ctx.StrokeAsync(); + + await ctx.BeginPathAsync(); + await ctx.MoveToAsync(RulerHeight, MouseY); + await ctx.LineToAsync(CanvasWidth, MouseY); + await ctx.StrokeAsync(); + + await ctx.SetLineDashAsync([]); + + const double labelPadding = 7; + const double labelMargin = 8; + + string coordinateText = $"({WorldMouseX:F2}m, {WorldMouseY:F2}m)"; + + await ctx.FontAsync("bold 12px Arial"); + var textMetrics = await ctx.MeasureTextAsync(coordinateText); + double textWidth = textMetrics.Width; + double textHeight = 16; + + double labelX = MouseX + labelMargin; + double labelY = MouseY - textHeight - labelPadding * 2 - labelMargin; + + if (labelX + textWidth + labelPadding * 2 > CanvasWidth) + { + labelX = MouseX - textWidth - labelPadding * 2 - labelMargin; + } + if (labelY - textHeight - labelPadding * 2 < RulerHeight) + { + labelY = MouseY + labelMargin; + } + + await ctx.FillStyleAsync("rgba(0, 0, 0, 0.8)"); + await ctx.FillRectAsync(labelX, labelY, textWidth + labelPadding * 2, textHeight + labelPadding * 2); + + await ctx.StrokeStyleAsync("rgba(255,255,255,0.6)"); + await ctx.LineWidthAsync(1); + await ctx.StrokeRectAsync(labelX, labelY, textWidth + labelPadding * 2, textHeight + labelPadding * 2); + + await ctx.FillStyleAsync("rgba(255, 50, 50, 0.9)"); + await ctx.BeginPathAsync(); + await ctx.ArcAsync(MouseX, MouseY, 3, 0, Math.PI * 2); + await ctx.FillAsync(FillRule.NonZero); + + await ctx.StrokeStyleAsync("rgba(255, 255, 255, 0.8)"); + await ctx.LineWidthAsync(2); + await ctx.BeginPathAsync(); + await ctx.ArcAsync(MouseX, MouseY, 6, 0, Math.PI * 2); + await ctx.StrokeAsync(); + + await ctx.SaveAsync(); + + await ctx.TranslateAsync(labelX + labelPadding + textWidth / 2, labelY + textHeight / 2); + await ctx.ScaleAsync(1, -1); + + await ctx.FillStyleAsync("white"); + await ctx.FontAsync("bold 12px Arial"); + await ctx.TextAlignAsync(TextAlign.Center); + await ctx.TextBaseLineAsync(TextBaseLine.Bottom); + await ctx.FillTextAsync(coordinateText, 0, 0); + + await ctx.RestoreAsync(); + } + + private async Task DrawRulers(Context2D ctx) + { + double visibleWorldLeft = CanvasToWorldX(0); + double visibleWorldRight = CanvasToWorldX(CanvasWidth); + double visibleWorldTop = CanvasToWorldY(0); + double visibleWorldBottom = CanvasToWorldY(CanvasHeight); + + double scaleInterval = GetRulerScaleInterval(); + + await DrawXRuler(ctx, RulerHeight, visibleWorldLeft, visibleWorldRight, scaleInterval); + await DrawYRuler(ctx, RulerHeight, visibleWorldTop, visibleWorldBottom, scaleInterval); + } + + private double GetRulerScaleInterval() + { + double pixelsPerMeter = BASE_PIXELS_PER_METER * ZoomScale; + + if (pixelsPerMeter >= 400) return 0.1; + else if (pixelsPerMeter >= 200) return 0.2; + else if (pixelsPerMeter >= 100) return 0.5; + else if (pixelsPerMeter >= 50) return 1.0; + else if (pixelsPerMeter >= 25) return 2.0; + else if (pixelsPerMeter >= 12) return 5.0; + else if (pixelsPerMeter >= 6) return 10.0; + else return 20.0; + } + + private async Task DrawXRuler(Context2D ctx, double rulerHeight, double visibleWorldLeft, double visibleWorldRight, double scaleInterval) + { + await ctx.FillStyleAsync("rgba(240, 240, 240, 0.9)"); + await ctx.FillRectAsync(0, 0, CanvasWidth, rulerHeight); + + await ctx.StrokeStyleAsync("rgba(100, 100, 100, 0.8)"); + await ctx.LineWidthAsync(1); + await ctx.BeginPathAsync(); + await ctx.MoveToAsync(0, rulerHeight); + await ctx.LineToAsync(CanvasWidth, rulerHeight); + await ctx.StrokeAsync(); + + double startWorld = Math.Floor(visibleWorldLeft / scaleInterval) * scaleInterval; + double endWorld = Math.Ceiling(visibleWorldRight / scaleInterval) * scaleInterval; + + startWorld -= scaleInterval; + endWorld += scaleInterval; + + for (double worldX = startWorld; worldX <= endWorld; worldX += scaleInterval) + { + double canvasX = WorldToCanvasX(worldX); + + if (canvasX < -50 || canvasX > CanvasWidth + 50) continue; + + bool isMajorTick = IsNearMultiple(worldX, scaleInterval * 2) || Math.Abs(worldX) < 0.001; + double tickHeight = isMajorTick ? rulerHeight * 0.4 : rulerHeight * 0.2; + + await ctx.StrokeStyleAsync("rgba(60, 60, 60, 0.8)"); + await ctx.LineWidthAsync(1); + await ctx.BeginPathAsync(); + await ctx.MoveToAsync(canvasX, rulerHeight); + await ctx.LineToAsync(canvasX, rulerHeight - tickHeight); + await ctx.StrokeAsync(); + + if (isMajorTick && canvasX >= -20 && canvasX <= CanvasWidth + 20) + { + await ctx.SaveAsync(); + + await ctx.TranslateAsync(canvasX, rulerHeight - tickHeight - 8); + await ctx.ScaleAsync(1, -1); + + await ctx.FillStyleAsync("blue"); + await ctx.FontAsync("bold 10px Arial"); + await ctx.TextAlignAsync(TextAlign.Center); + + string labelText = FormatRulerLabel(worldX, scaleInterval); + await ctx.FillTextAsync(labelText, 0, 0); + + await ctx.RestoreAsync(); + } + } + } + + private async Task DrawYRuler(Context2D ctx, double rulerWidth, double visibleWorldTop, double visibleWorldBottom, double scaleInterval) + { + await ctx.FillStyleAsync("rgba(240, 240, 240, 0.9)"); + await ctx.FillRectAsync(0, 0, rulerWidth, CanvasHeight); + + await ctx.StrokeStyleAsync("rgba(100, 100, 100, 0.8)"); + await ctx.LineWidthAsync(1); + await ctx.BeginPathAsync(); + await ctx.MoveToAsync(rulerWidth, 0); + await ctx.LineToAsync(rulerWidth, CanvasHeight); + await ctx.StrokeAsync(); + + double startWorld = Math.Floor(visibleWorldTop / scaleInterval) * scaleInterval; + double endWorld = Math.Ceiling(visibleWorldBottom / scaleInterval) * scaleInterval; + + startWorld -= scaleInterval; + endWorld += scaleInterval; + + for (double worldY = startWorld; worldY <= endWorld; worldY += scaleInterval) + { + double canvasY = WorldToCanvasY(worldY); + + if (canvasY < -50 || canvasY > CanvasHeight + 50) continue; + + bool isMajorTick = IsNearMultiple(worldY, scaleInterval * 2) || Math.Abs(worldY) < 0.001; + double tickWidth = isMajorTick ? rulerWidth * 0.4 : rulerWidth * 0.2; + + await ctx.StrokeStyleAsync("rgba(60, 60, 60, 0.8)"); + await ctx.LineWidthAsync(1); + await ctx.BeginPathAsync(); + await ctx.MoveToAsync(rulerWidth, canvasY); + await ctx.LineToAsync(rulerWidth - tickWidth, canvasY); + await ctx.StrokeAsync(); + + if (isMajorTick && canvasY >= -20 && canvasY <= CanvasHeight + 20) + { + await ctx.SaveAsync(); + + await ctx.TranslateAsync(rulerWidth - tickWidth - 2, canvasY); + await ctx.ScaleAsync(1, -1); + await ctx.RotateAsync(-Math.PI / 2); + + await ctx.FillStyleAsync("blue"); + await ctx.FontAsync("bold 10px Arial"); + await ctx.TextAlignAsync(TextAlign.Center); + + string labelText = FormatRulerLabel(worldY, scaleInterval); + await ctx.FillTextAsync(labelText, 0, 0); + + await ctx.RestoreAsync(); + } + } + } + + private static bool IsNearMultiple(double value, double multiple) + { + if (multiple == 0) return false; + double remainder = Math.Abs(value % multiple); + double epsilon = multiple * 0.001; + return remainder < epsilon || remainder > multiple - epsilon; + } + + private static string FormatRulerLabel(double worldValue, double scaleInterval) + { + return scaleInterval < 1.0 ? $"{worldValue:F1}m" : $"{worldValue:F0}m"; + } + + 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, 8 / ZoomScale, 0, Math.PI * 2); + await ctx.FillAsync(FillRule.NonZero); + + 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 - arrowHeadSize, originCanvasY); + await ctx.StrokeAsync(); + + await ctx.BeginPathAsync(); + double xArrowTipX = originCanvasX + arrowLength; + double xArrowTipY = originCanvasY; + await ctx.MoveToAsync(xArrowTipX, xArrowTipY); + await ctx.LineToAsync(xArrowTipX - arrowHeadSize, xArrowTipY - arrowHeadSize / 2); + await ctx.LineToAsync(xArrowTipX - arrowHeadSize, xArrowTipY + 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 - arrowHeadSize); + await ctx.StrokeAsync(); + + await ctx.BeginPathAsync(); + double yArrowTipX = originCanvasX; + double yArrowTipY = originCanvasY + arrowLength; + await ctx.MoveToAsync(yArrowTipX, yArrowTipY); + await ctx.LineToAsync(yArrowTipX - arrowHeadSize / 2, yArrowTipY - arrowHeadSize); + await ctx.LineToAsync(yArrowTipX + arrowHeadSize / 2, yArrowTipY - 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([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([]); + } + + private double GetGridSpacingMeters() + { + double PixelsPerMeter = BASE_PIXELS_PER_METER * ZoomScale; + if (PixelsPerMeter >= 300) return 0.2; + else if (PixelsPerMeter >= 150) return 0.5; + else if (PixelsPerMeter >= 75) return 1.0; + else if (PixelsPerMeter >= 40) return 2.0; + else if (PixelsPerMeter >= 20) return 5.0; + else if (PixelsPerMeter >= 10) return 10.0; + else return 20.0; + } + + private async Task DrawMapImage(Context2D ctx) + { + if (!MapImageLoaded) + { + return; + } + + await ctx.SaveAsync(); + + try + { + double imageWidthCanvas = MapImageWidth * BASE_PIXELS_PER_METER; + double imageHeightCanvas = MapImageHeight * BASE_PIXELS_PER_METER; + + double mapCanvasX = ImageX * BASE_PIXELS_PER_METER; + double mapCanvasY = (ImageY + MapImageHeight) * BASE_PIXELS_PER_METER; + + bool success = await JS.InvokeAsync("drawCachedImageOnCanvas", + CanvasRef, + MAP_CACHE_KEY, + mapCanvasX, + mapCanvasY - imageHeightCanvas, + imageWidthCanvas, + imageHeightCanvas); + } + catch + { + } + + await ctx.RestoreAsync(); + } + + private async Task DrawLaserScannerPoints(Context2D ctx) + { + var scanData = GenerateLaserScanData(); + + double robotCanvasX = scanData.RobotX * BASE_PIXELS_PER_METER; + double robotCanvasY = scanData.RobotY * BASE_PIXELS_PER_METER; + + await ctx.SaveAsync(); + + if (scanData.Points.Count > 0) + { + await ctx.BeginPathAsync(); + + for (int i = 0; i < scanData.Points.Count; i++) + { + var point = scanData.Points[i]; + double pointCanvasX = point.X * BASE_PIXELS_PER_METER; + double pointCanvasY = point.Y * BASE_PIXELS_PER_METER; + + if (i == 0) + { + await ctx.MoveToAsync(pointCanvasX, pointCanvasY); + } + else + { + await ctx.LineToAsync(pointCanvasX, pointCanvasY); + } + } + + await ctx.StrokeStyleAsync("rgba(255, 100, 100, 0.8)"); + await ctx.LineWidthAsync(2 / ZoomScale); + await ctx.StrokeAsync(); + + await ctx.LineToAsync(robotCanvasX, robotCanvasY); + await ctx.ClosePathAsync(); + await ctx.FillStyleAsync("rgba(255, 100, 100, 0.1)"); + await ctx.FillAsync(FillRule.NonZero); + } + + await DrawRobotImage(ctx, robotCanvasX, robotCanvasY, scanData.RobotOrientation); + await DrawRobotOrientationArrows(ctx, robotCanvasX, robotCanvasY, scanData.RobotOrientation); + + await ctx.RestoreAsync(); + } + + private async Task DrawRobotImage(Context2D ctx, double robotCanvasX, double robotCanvasY, double robotOrientation) + { + if (!RobotImageLoaded) + { + await ctx.FillStyleAsync("rgba(0, 255, 0, 0.8)"); + await ctx.BeginPathAsync(); + await ctx.ArcAsync(robotCanvasX, robotCanvasY, 8 / ZoomScale, 0, Math.PI * 2); + await ctx.FillAsync(FillRule.NonZero); + return; + } + + await ctx.SaveAsync(); + + double robotWidthPixels = RobotWidth * BASE_PIXELS_PER_METER; + double robotLengthPixels = RobotLength * BASE_PIXELS_PER_METER; + + double scaledWidth = ZoomScale < 1 ? robotWidthPixels / ZoomScale : robotWidthPixels; + double scaledLength = ZoomScale < 1 ? robotLengthPixels / ZoomScale : robotLengthPixels; + + await ctx.TranslateAsync(robotCanvasX, robotCanvasY); + await ctx.RotateAsync(robotOrientation); + + try + { + bool success = await JS.InvokeAsync("drawImageOnCanvas", + CanvasRef, + "images/AMR-250.png", + -scaledLength / 2, + -scaledWidth / 2, + scaledLength, + scaledWidth); + + if (!success) + { + await ctx.FillStyleAsync("rgba(0, 255, 0, 0.8)"); + await ctx.FillRectAsync(-scaledLength / 2, -scaledWidth / 2, scaledLength, scaledWidth); + } + } + catch + { + await ctx.FillStyleAsync("rgba(0, 255, 0, 0.8)"); + await ctx.FillRectAsync(-scaledLength / 2, -scaledWidth / 2, scaledLength, scaledWidth); + } + + await ctx.RestoreAsync(); + } + + private async Task DrawRobotOrientationArrows(Context2D ctx, double robotCanvasX, double robotCanvasY, double robotOrientation) + { + double arrowLength = 30 / ZoomScale; + double arrowHeadSize = 10 / ZoomScale; + + await ctx.StrokeStyleAsync("rgba(0, 100, 255, 1.0)"); + await ctx.FillStyleAsync("rgba(0, 100, 255, 1.0)"); + await ctx.LineWidthAsync(3 / ZoomScale); + + await ctx.BeginPathAsync(); + await ctx.MoveToAsync(robotCanvasX, robotCanvasY); + double xAxisEndX = robotCanvasX + Math.Cos(robotOrientation) * (arrowLength - arrowHeadSize + 1); + double xAxisEndY = robotCanvasY + Math.Sin(robotOrientation) * (arrowLength - arrowHeadSize + 1); + await ctx.LineToAsync(xAxisEndX, xAxisEndY); + await ctx.StrokeAsync(); + + await ctx.BeginPathAsync(); + double xArrowTipX = robotCanvasX + Math.Cos(robotOrientation) * arrowLength; + double xArrowTipY = robotCanvasY + Math.Sin(robotOrientation) * arrowLength; + await ctx.MoveToAsync(xArrowTipX, xArrowTipY); + double xArrowAngle = robotOrientation + Math.PI; + await ctx.LineToAsync(xArrowTipX + Math.Cos(xArrowAngle + Math.PI / 6) * arrowHeadSize, xArrowTipY + Math.Sin(xArrowAngle + Math.PI / 6) * arrowHeadSize); + await ctx.LineToAsync(xArrowTipX + Math.Cos(xArrowAngle - Math.PI / 6) * arrowHeadSize, xArrowTipY + Math.Sin(xArrowAngle - Math.PI / 6) * arrowHeadSize); + await ctx.ClosePathAsync(); + await ctx.FillAsync(FillRule.NonZero); + + await ctx.StrokeStyleAsync("rgba(255, 50, 50, 1.0)"); + await ctx.FillStyleAsync("rgba(255, 50, 50, 1.0)"); + await ctx.LineWidthAsync(3 / ZoomScale); + + double yAxisAngle = robotOrientation + Math.PI / 2; + + await ctx.BeginPathAsync(); + await ctx.MoveToAsync(robotCanvasX, robotCanvasY); + double yAxisEndX = robotCanvasX + Math.Cos(yAxisAngle) * (arrowLength - arrowHeadSize + 1); + double yAxisEndY = robotCanvasY + Math.Sin(yAxisAngle) * (arrowLength - arrowHeadSize + 1); + await ctx.LineToAsync(yAxisEndX, yAxisEndY); + await ctx.StrokeAsync(); + + await ctx.BeginPathAsync(); + double yArrowTipX = robotCanvasX + Math.Cos(yAxisAngle) * arrowLength; + double yArrowTipY = robotCanvasY + Math.Sin(yAxisAngle) * arrowLength; + await ctx.MoveToAsync(yArrowTipX, yArrowTipY); + double yArrowAngle = yAxisAngle + Math.PI; + await ctx.LineToAsync(yArrowTipX + Math.Cos(yArrowAngle + Math.PI / 6) * arrowHeadSize, yArrowTipY + Math.Sin(yArrowAngle + Math.PI / 6) * arrowHeadSize); + await ctx.LineToAsync(yArrowTipX + Math.Cos(yArrowAngle - Math.PI / 6) * arrowHeadSize, yArrowTipY + Math.Sin(yArrowAngle - Math.PI / 6) * arrowHeadSize); + await ctx.ClosePathAsync(); + await ctx.FillAsync(FillRule.NonZero); + } + + 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; + } +} diff --git a/RobotApp.Client/Pages/Components/Mapping/RobotInfomation.razor b/RobotApp.Client/Pages/Components/Mapping/RobotInfomation.razor new file mode 100644 index 0000000..c99dada --- /dev/null +++ b/RobotApp.Client/Pages/Components/Mapping/RobotInfomation.razor @@ -0,0 +1,6 @@ +
+
+ +@code { + +} diff --git a/RobotApp.Client/Pages/Components/Mapping/RobotInfomation.razor.css b/RobotApp.Client/Pages/Components/Mapping/RobotInfomation.razor.css new file mode 100644 index 0000000..0099b24 --- /dev/null +++ b/RobotApp.Client/Pages/Components/Mapping/RobotInfomation.razor.css @@ -0,0 +1,12 @@ +.view { + height: 100%; + width: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + position: relative; + background-color: var(--mud-palette-surface); + border-radius: var(--mud-default-borderradius); + transition: box-shadow 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms; + box-shadow: var(--mud-elevation-10); +} diff --git a/RobotApp.Client/Pages/MapsManager.razor b/RobotApp.Client/Pages/MapsManager.razor index 819c5e6..f41651d 100644 --- a/RobotApp.Client/Pages/MapsManager.razor +++ b/RobotApp.Client/Pages/MapsManager.razor @@ -7,8 +7,13 @@ Map Manager
-
- +
+
+ +
+
+ +
diff --git a/RobotApp.Client/wwwroot/js/canvas.js b/RobotApp.Client/wwwroot/js/canvas.js index e956683..c8ab64b 100644 --- a/RobotApp.Client/wwwroot/js/canvas.js +++ b/RobotApp.Client/wwwroot/js/canvas.js @@ -5,12 +5,23 @@ }; } +window.getElementBoundingRect = (element) => { + const rect = element.getBoundingClientRect(); + return { + width: rect.width, + height: rect.height, + x: rect.x, + y: rect.y, + left: rect.left, + top: rect.top + }; +} + window.setCanvasSize = (canvas, width, height) => { canvas.width = width; canvas.height = height; } -// Image loading and caching functionality window.imageCache = new Map(); window.preloadImage = (imagePath) => { @@ -47,11 +58,6 @@ window.preloadImageFromUrl = (url, cacheKey) => { img.onerror = (error) => { reject(new Error(`Failed to load image from URL: ${url}`)); }; - - // Don't set crossOrigin for same-origin requests - // Only set it if you're loading from a different domain - // img.crossOrigin = 'anonymous'; - img.src = url; }); }; diff --git a/RobotApp.sln b/RobotApp.sln index 258b320..b17dbf7 100644 --- a/RobotApp.sln +++ b/RobotApp.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.14.36511.14 d17.14 +VisualStudioVersion = 17.14.36511.14 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RobotApp", "RobotApp\RobotApp.csproj", "{BF0BB137-2EF9-4E1B-944E-9BF41C5284F7}" EndProject diff --git a/RobotApp/Controllers/ImagesController.cs b/RobotApp/Controllers/ImagesController.cs index b1995f6..a46ae4a 100644 --- a/RobotApp/Controllers/ImagesController.cs +++ b/RobotApp/Controllers/ImagesController.cs @@ -1,6 +1,5 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using RobotApp.Services; namespace RobotApp.Controllers; @@ -10,7 +9,7 @@ namespace RobotApp.Controllers; public class ImagesController(Services.Logger Logger) : ControllerBase { [HttpGet] - [Route("map")] + [Route("mapping")] public async Task GetMapImage() { try