using Microsoft.Maui.Graphics; using Robobin.Models; using RobobinApp.Models; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; namespace RobobinApp.Views { public class GraphicsDrawable : IDrawable { private Graph _graph; private (float, float) _location; public const float BaseNodeRadius = 5f; // Make this public if needed elsewhere private static readonly Color DefaultNodeColor = Colors.Red; private static readonly Color HomeNodeColor = Colors.Green; private static readonly Color BackgroundColor = Color.FromHex("#2C2F33"); private static readonly Color ConnectionColor = Colors.Grey; private static readonly Color HighlightedNodeColor = Colors.Yellow; // For the highlighted node private static readonly Color PathColor = Colors.Blue; // For the path private RectF storedDirtyRect; public float CurrentScale { get; private set; } = 1.0f; public (float X, float Y) CurrentOffset { get; private set; } = (0, 0); private Node _highlightedNode; private List<Node> _path; public void SetGraph(Graph graph) { _graph = graph ?? throw new ArgumentNullException(nameof(graph)); _highlightedNode = null; _path = null; } public void SetLocation((float, float) location) { _location = location; } public void Draw(ICanvas canvas, RectF dirtyRect) { storedDirtyRect = dirtyRect; if (_graph == null) return; DrawBackground(canvas, dirtyRect); (float minX, float minY, float maxX, float maxY) = CalculateBounds(); CurrentScale = CalculateScale(dirtyRect, minX, minY, maxX, maxY); (float offsetX, float offsetY) = CalculateOffsets(dirtyRect, minX, minY, maxX, maxY, CurrentScale); CurrentOffset = (offsetX, offsetY); float nodeRadius = BaseNodeRadius * CurrentScale; DrawConnections(canvas, CurrentScale, offsetX, offsetY); DrawPath(canvas, CurrentScale, offsetX, offsetY); DrawNodes(canvas, CurrentScale, offsetX, offsetY, nodeRadius, _highlightedNode); DrawLocation(canvas, CurrentScale, offsetX, offsetY); } private void DrawLocation(ICanvas canvas, float scale, float offsetX, float offsetY) { if (_location.Item1 == 0 && _location.Item2 == 0) { return; } Debug.WriteLine($"Drawing Location {_location.Item1} {_location.Item2}"); float scaledX = _location.Item1 * scale + offsetX; float flippedY = storedDirtyRect.Height - (_location.Item2 * scale + offsetY); canvas.FillColor = Colors.Yellow; canvas.FillCircle(scaledX, flippedY, 5); canvas.FontColor = Colors.White; canvas.DrawString($"Robobin ({_location.Item1} {_location.Item2})", scaledX + 7, flippedY - 5, HorizontalAlignment.Left); } private void DrawBackground(ICanvas canvas, RectF dirtyRect) { canvas.FillColor = BackgroundColor; canvas.FillRectangle(dirtyRect); } private (float minX, float minY, float maxX, float maxY) CalculateBounds() { float minX = _graph.Nodes.Min(node => node.X); float minY = _graph.Nodes.Min(node => node.Y); float maxX = _graph.Nodes.Max(node => node.X); float maxY = _graph.Nodes.Max(node => node.Y); return (minX, minY, maxX, maxY); } private float CalculateScale(RectF dirtyRect, float minX, float minY, float maxX, float maxY) { float graphWidth = maxX - minX; float graphHeight = maxY - minY; float scaleX = dirtyRect.Width / graphWidth; float scaleY = dirtyRect.Height / graphHeight; return Math.Min(scaleX, scaleY) * 0.8f; } private (float offsetX, float offsetY) CalculateOffsets(RectF dirtyRect, float minX, float minY, float maxX, float maxY, float scale) { float offsetX = dirtyRect.Center.X - ((minX + maxX) * 0.5f) * scale; float offsetY = dirtyRect.Center.Y - ((minY + maxY) * 0.5f) * scale; return (offsetX, offsetY); } private void DrawConnections(ICanvas canvas, float scale, float offsetX, float offsetY) { canvas.StrokeColor = ConnectionColor; canvas.StrokeSize = 2; foreach (var node in _graph.Nodes) { int nodeIndex = _graph.Nodes.IndexOf(node); float startX = node.X * scale + offsetX; float startY = storedDirtyRect.Height - (node.Y * scale + offsetY); // flip Y for (int j = 0; j < _graph.Nodes.Count; j++) { var (magnitude, angle) = _graph.AdjacencyMatrix[nodeIndex, j]; if (magnitude > 0) { float targetX = (node.X + magnitude * MathF.Cos(MathF.PI * angle / 180)) * scale + offsetX; float targetRawY = (node.Y + magnitude * MathF.Sin(MathF.PI * angle / 180)) * scale + offsetY; float targetY = storedDirtyRect.Height - targetRawY; // flip Y canvas.DrawLine(startX, startY, targetX, targetY); } } } } private void DrawNodes(ICanvas canvas, float scale, float offsetX, float offsetY, float nodeRadius, Node highlightedNode) { foreach (var node in _graph.Nodes) { float scaledX = node.X * scale + offsetX; float flippedY = storedDirtyRect.Height - (node.Y * scale + offsetY); // flip Y canvas.FillColor = (highlightedNode == node) ? HighlightedNodeColor : (node.Name == "Home" ? HomeNodeColor : DefaultNodeColor); canvas.FillCircle(scaledX, flippedY, nodeRadius); canvas.FontColor = Colors.White; var nodeDetails = $"{node.Name}"; canvas.DrawString(nodeDetails, scaledX + nodeRadius + 2, flippedY - nodeRadius, HorizontalAlignment.Left); } } public Node FindNearestNode(float clickX, float clickY, float scale, float offsetX, float offsetY) { // The coordinates given (clickX, clickY) are in the original coordinate system // where the y-axis goes down. We must transform them similarly to how we did // when drawing: flip the Y using storedDirtyRect. float flippedClickY = clickY; Node nearestNode = null; float minDistance = float.MaxValue; foreach (var node in _graph.Nodes) { float scaledX = node.X * scale + offsetX; // Flip Y when comparing distances float flippedY = storedDirtyRect.Height - (node.Y * scale + offsetY); float dx = clickX - scaledX; float dy = flippedClickY - flippedY; float distance = MathF.Sqrt(dx * dx + dy * dy); if (distance < minDistance) { minDistance = distance; nearestNode = node; } } return nearestNode; } public void HighlightNode(Node node) { _highlightedNode = node; } public List<Node> AStarPathfinding(Node startNode, Node targetNode) { var openSet = new List<Node> { startNode }; var cameFrom = new Dictionary<Node, Node>(); var gScore = new Dictionary<Node, float>(); var fScore = new Dictionary<Node, float>(); foreach (var node in _graph.Nodes) { gScore[node] = float.MaxValue; fScore[node] = float.MaxValue; } gScore[startNode] = 0; fScore[startNode] = Heuristic(startNode, targetNode); while (openSet.Count > 0) { Node currentNode = openSet.OrderBy(node => fScore[node]).First(); if (currentNode == targetNode) { return ReconstructPath(cameFrom, currentNode); } openSet.Remove(currentNode); foreach (var edge in currentNode.Edges) { Node neighbor = edge.TargetNode; float tentativeGScore = gScore[currentNode] + edge.Cost; if (tentativeGScore < gScore[neighbor]) { cameFrom[neighbor] = currentNode; gScore[neighbor] = tentativeGScore; fScore[neighbor] = gScore[neighbor] + Heuristic(neighbor, targetNode); if (!openSet.Contains(neighbor)) { openSet.Add(neighbor); } } } } return new List<Node>(); } private float Heuristic(Node a, Node b) { return MathF.Sqrt((a.X - b.X) * (a.X - b.X) + (a.Y - b.Y) * (a.Y - b.Y)); } private List<Node> ReconstructPath(Dictionary<Node, Node> cameFrom, Node currentNode) { var totalPath = new List<Node> { currentNode }; while (cameFrom.ContainsKey(currentNode)) { currentNode = cameFrom[currentNode]; totalPath.Add(currentNode); } totalPath.Reverse(); return totalPath; } public Node GetHomeNode() { return _graph.Nodes.FirstOrDefault(n => n.Name == "Home"); } public List<Node> Path { get => _path; set => _path = value; } private void DrawPath(ICanvas canvas, float scale, float offsetX, float offsetY) { if (_path != null && _path.Count > 0) { canvas.StrokeColor = PathColor; canvas.StrokeSize = 3; for (int i = 0; i < _path.Count - 1; i++) { float startX = _path[i].X * scale + offsetX; float startRawY = _path[i].Y * scale + offsetY; float startY = storedDirtyRect.Height - startRawY; // flip Y float endX = _path[i + 1].X * scale + offsetX; float endRawY = _path[i + 1].Y * scale + offsetY; float endY = storedDirtyRect.Height - endRawY; // flip Y canvas.DrawLine(startX, startY, endX, endY); } } } } }