@inject IJSRuntime JS
@if (MonitorData?.RobotPosition != null) {
Robot: X: @MonitorData.RobotPosition.X.ToString("F2")m | Y: @MonitorData.RobotPosition.Y.ToString("F2")m | θ: @((MonitorData.RobotPosition.Theta * 180 / Math.PI).ToString("F1"))°
} @if (MouseWorldX.HasValue && MouseWorldY.HasValue) {
Mouse: X: @MouseWorldX.Value.ToString("F2")m | Y: @MouseWorldY.Value.ToString("F2")m
} @* @(IsConnected ? "Connected" : "Disconnected") *@
@* Arrow markers for origin *@ @* Background Map Image *@ @if (MapImageLoaded && MapImageWidth > 0 && MapImageHeight > 0) { } @* Origin Marker (2 arrows: X+ and Y+) *@ @* X+ Arrow (pointing right) *@ @* Y+ Arrow (pointing up in world, down in SVG) *@ @* Origin point *@ @if (MonitorData?.HasOrder == true) { @foreach (var node in MonitorData.NodeStates) { } } @* Render Robot *@ @if (MonitorData?.RobotPosition != null) { @{ var (robotX, robotY, robotWidth, robotHeight) = GetRobotSize(); } }
@code { [Parameter] public RobotMonitorDto? MonitorData { get; set; } [Parameter] public bool IsConnected { get; set; } public class ElementSize { public double Width { get; set; } public double Height { get; set; } } public class ElementBoundingRect { public double X { get; set; } public double Y { get; set; } public double Width { get; set; } public double Height { get; set; } } private ElementReference SvgRef; private ElementReference SvgContainerRef; private double ZoomScale = 2.0; // Zoom vào robot hơn khi mở private const double MIN_ZOOM = 0.1; private const double MAX_ZOOM = 5.0; private const double BASE_PIXELS_PER_METER = 50.0; private double TranslateX = 0; private double TranslateY = 0; private bool IsPanning = false; private double PanStartX = 0; private double PanStartY = 0; private double SvgWidth = 800; private double SvgHeight = 600; private string PathView = ""; private string PathIsNot = "hidden"; // Mouse world coordinates private double? MouseWorldX = null; private double? MouseWorldY = null; // Map image properties private const double MapImageOriginX = -20.0; // OriginX in world coordinates (meters) private const double MapImageOriginY = -20.0; // OriginY in world coordinates (meters) private const double MapImageResolution = 0.1; // Resolution: meters per pixel private const string MapImageUrl = "images/gara20250309.png"; private bool MapImageLoaded = false; private double MapImageWidth = 0; // Width in world coordinates (meters) private double MapImageHeight = 0; // Height in world coordinates (meters) protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { var containerSize = await JS.InvokeAsync("robotMonitor.getElementSize", SvgContainerRef); SvgWidth = containerSize.Width; SvgHeight = containerSize.Height; // Load map image and get dimensions await LoadMapImage(); // Center view on robot if available with initial zoom if (MonitorData?.RobotPosition != null) { // Zoom vào robot hơn một chút ZoomScale = 2.5; TranslateX = SvgWidth / 2 - MonitorData.RobotPosition.X * BASE_PIXELS_PER_METER * ZoomScale; TranslateY = SvgHeight / 2 + MonitorData.RobotPosition.Y * BASE_PIXELS_PER_METER * ZoomScale; } else { TranslateX = SvgWidth / 2; TranslateY = SvgHeight / 2; } StateHasChanged(); } } private async Task LoadMapImage() { try { var imageDimensions = await JS.InvokeAsync("robotMonitor.loadImageAndGetDimensions", MapImageUrl); // Convert pixel dimensions to world coordinates (meters) MapImageWidth = imageDimensions.Width * MapImageResolution; MapImageHeight = imageDimensions.Height * MapImageResolution; Console.WriteLine($"Map image loaded: {imageDimensions.Width}x{imageDimensions.Height} pixels, {MapImageWidth}x{MapImageHeight} meters"); if (MapImageWidth > 0 && MapImageHeight > 0) { MapImageLoaded = true; await InvokeAsync(StateHasChanged); // Force re-render after image is loaded } } catch (Exception ex) { MapImageLoaded = false; Console.WriteLine($"Failed to load map image: {ex.Message}"); } } private string GetTransform() { return $"translate({TranslateX}, {TranslateY}) scale({ZoomScale * BASE_PIXELS_PER_METER})"; } private string GetRobotTransform() { if (MonitorData?.RobotPosition == null) return ""; var x = WorldToSvgX(MonitorData.RobotPosition.X); var y = WorldToSvgY(MonitorData.RobotPosition.Y); // Theta là radian, convert sang độ // SVG rotate quay theo chiều kim đồng hồ, cần đảo dấu var angleDegrees = -MonitorData.RobotPosition.Theta * 180 / Math.PI; return $"translate({x}, {y}) rotate({angleDegrees})"; } private (double x, double y, double width, double height) GetRobotSize() { // Kích thước robot trong world coordinates (mét) const double RobotWidthMeters = 0.606; const double RobotLengthMeters = 1.106; // Điều chỉnh kích thước dựa trên ZoomScale // Tăng kích thước lên 1.5x để robot to hơn double scaleFactor = 3 / ZoomScale; // Tăng kích thước hiển thị double width = RobotWidthMeters * scaleFactor; double height = RobotLengthMeters * scaleFactor; double x = -width / 2; double y = -height / 2; return (x, y, width, height); } private double GetNodeRadius() { // Kích thước node cơ bản trong world coordinates - tăng lên 1.5x const double BaseNodeRadius = 0.15; // Điều chỉnh theo ZoomScale tương tự robot double scaleFactor = 1.5 / ZoomScale; // Tăng kích thước hiển thị return BaseNodeRadius * scaleFactor; } private double GetNodeStrokeWidth() { // Stroke width cơ bản - tăng lên một chút const double BaseStrokeWidth = 0.03; // Điều chỉnh theo ZoomScale double scaleFactor = 1.5 / ZoomScale; return BaseStrokeWidth * scaleFactor; } private double WorldToSvgX(double worldX) { return worldX; } private double WorldToSvgY(double worldY) { // Flip Y axis: World Y↑ → SVG Y↓ return -worldY; } private string GetOriginMarkerTransform() { // Origin is at (MapImageOriginX, MapImageOriginY) in world coordinates // In SVG: (MapImageOriginX, -MapImageOriginY) var x = WorldToSvgX(MapImageOriginX); var y = WorldToSvgY(MapImageOriginY); return $"translate({x}, {y})"; } private double GetOriginMarkerSize() { // Marker size in world coordinates (meters) const double BaseMarkerSize = 0.5; // 1 meter double scaleFactor = 1.0 / ZoomScale; // Keep visual size constant return BaseMarkerSize * scaleFactor; } private double GetOriginMarkerStrokeWidth() { // Stroke width in world coordinates const double BaseStrokeWidth = 0.05; // 5cm double scaleFactor = 1.0 / ZoomScale; // Keep visual size constant return BaseStrokeWidth * scaleFactor; } public void UpdatePath() { if (MonitorData is not null && MonitorData.EdgeStates.Length > 0) { var path = MonitorData.EdgeStates.Select(e => new EdgeStateDto { Degree = e.Degree, StartX = WorldToSvgX(e.StartX), StartY = WorldToSvgY(e.StartY), EndX = WorldToSvgX(e.EndX), EndY = WorldToSvgY(e.EndY), ControlPoint1X = WorldToSvgX(e.ControlPoint1X), ControlPoint1Y = WorldToSvgY(e.ControlPoint1Y), ControlPoint2X = WorldToSvgX(e.ControlPoint2X), ControlPoint2Y = WorldToSvgY(e.ControlPoint2Y), }).ToList(); var inPath = $"M {path[0].StartX} {path[0].StartY}"; for (int i = 0; i < path.Count; i++) { if (path[i].Degree == 1) inPath = $"{inPath} L {path[i].EndX} {path[i].EndY}"; else if (path[i].Degree == 2) inPath = $"{inPath} Q {path[i].ControlPoint1X} {path[i].ControlPoint1Y} {path[i].EndX} {path[i].EndY}"; else inPath = $"{inPath} C {path[i].ControlPoint1X} {path[i].ControlPoint1Y}, {path[i].ControlPoint2X} {path[i].ControlPoint2Y}, {path[i].EndX} {path[i].EndY}"; } PathView = inPath; PathIsNot = "visible"; } else { PathView = ""; PathIsNot = "hidden"; } StateHasChanged(); } private async Task ZoomIn() { ZoomScale = Math.Min(MAX_ZOOM, ZoomScale * 1.15); await Task.CompletedTask; StateHasChanged(); } private async Task ZoomOut() { ZoomScale = Math.Max(MIN_ZOOM, ZoomScale * 0.85); await Task.CompletedTask; StateHasChanged(); } private async Task ResetView() { // Reset về zoom ban đầu (2.5x) khi có robot position if (MonitorData?.RobotPosition != null) { ZoomScale = 2.5; TranslateX = SvgWidth / 2 - MonitorData.RobotPosition.X * BASE_PIXELS_PER_METER * ZoomScale; TranslateY = SvgHeight / 2 + MonitorData.RobotPosition.Y * BASE_PIXELS_PER_METER * ZoomScale; } else { ZoomScale = 1.0; TranslateX = SvgWidth / 2; TranslateY = SvgHeight / 2; } await Task.CompletedTask; StateHasChanged(); } private void HandleMouseDown(MouseEventArgs e) { IsPanning = true; PanStartX = e.ClientX - TranslateX; PanStartY = e.ClientY - TranslateY; } private async Task HandleMouseMove(MouseEventArgs e) { // Calculate world coordinates of mouse var svgRect = await JS.InvokeAsync("robotMonitor.getElementBoundingRect", SvgRef); double mouseX = e.ClientX - svgRect.X; double mouseY = e.ClientY - svgRect.Y; // Convert to world coordinates // World X = (mouseX - TranslateX) / (ZoomScale * BASE_PIXELS_PER_METER) MouseWorldX = (mouseX - TranslateX) / (ZoomScale * BASE_PIXELS_PER_METER); // World Y = -(mouseY - TranslateY) / (ZoomScale * BASE_PIXELS_PER_METER) (flip Y axis) MouseWorldY = -(mouseY - TranslateY) / (ZoomScale * BASE_PIXELS_PER_METER); if (IsPanning) { TranslateX = e.ClientX - PanStartX; TranslateY = e.ClientY - PanStartY; } StateHasChanged(); } private void HandleMouseUp(MouseEventArgs e) { IsPanning = false; } private void HandleMouseLeave(MouseEventArgs e) { IsPanning = false; MouseWorldX = null; MouseWorldY = null; StateHasChanged(); } private async Task HandleWheel(WheelEventArgs e) { 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; // Zoom at mouse position var svgRect = await JS.InvokeAsync("robotMonitor.getElementBoundingRect", SvgRef); double mouseX = e.ClientX - svgRect.X; double mouseY = e.ClientY - svgRect.Y; // Calculate world coordinates at mouse position double worldX = (mouseX - TranslateX) / (oldZoom * BASE_PIXELS_PER_METER); double worldY = -(mouseY - TranslateY) / (oldZoom * BASE_PIXELS_PER_METER); // Adjust translate to keep world point under mouse TranslateX = mouseX - worldX * ZoomScale * BASE_PIXELS_PER_METER; TranslateY = mouseY + worldY * ZoomScale * BASE_PIXELS_PER_METER; StateHasChanged(); } }