From e95811d6f0c7a9c8616ea81d0dcc2172d3a3d140 Mon Sep 17 00:00:00 2001
From: Paul-Winpenny <92634321+Paul-Winpenny@users.noreply.github.com>
Date: Mon, 4 Nov 2024 15:01:54 +0000
Subject: [PATCH] Added node selection, and beginings for A* pathfinding.

---
 App/RobobinApp/Models/Graph.cs                |  48 ++++-
 .../ViewModels/MainPageViewModel.cs           |   2 +-
 App/RobobinApp/Views/CentreGraph.xaml.cs      | 172 +++++++++++++++---
 App/RobobinApp/Views/MainPage.xaml            |   8 +-
 App/RobobinApp/Views/MainPage.xaml.cs         |  55 +++++-
 5 files changed, 242 insertions(+), 43 deletions(-)

diff --git a/App/RobobinApp/Models/Graph.cs b/App/RobobinApp/Models/Graph.cs
index e8fa370c..8e1c3b8a 100644
--- a/App/RobobinApp/Models/Graph.cs
+++ b/App/RobobinApp/Models/Graph.cs
@@ -1,22 +1,30 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Robobin.Models
+namespace Robobin.Models
 {
     public class Node
     {
         public string Name { get; set; }
         public float X { get; set; }
         public float Y { get; set; }
+        public List<Edge> Edges { get; set; } // Add edges to keep track of connections
 
         public Node(string name, float x, float y)
         {
             Name = name;
             X = x;
             Y = y;
+            Edges = new List<Edge>(); // Initialize the edges list
+        }
+    }
+
+    public class Edge
+    {
+        public Node TargetNode { get; set; }
+        public float Cost { get; set; } // Cost to traverse this edge
+
+        public Edge(Node targetNode, float cost)
+        {
+            TargetNode = targetNode;
+            Cost = cost;
         }
     }
 
@@ -31,13 +39,35 @@ namespace Robobin.Models
             Name = name;
             Nodes = nodes;
             AdjacencyMatrix = adjacencyMatrix;
+
+            BuildEdges(); // Create edges based on the adjacency matrix
         }
-        public Graph( List<Node> nodes, (float, float)[,] adjacencyMatrix)
+
+        public Graph(List<Node> nodes, (float, float)[,] adjacencyMatrix)
         {
             Name = "Graph";
             Nodes = nodes;
             AdjacencyMatrix = adjacencyMatrix;
+
+            BuildEdges(); // Create edges based on the adjacency matrix
         }
-    }
 
+        private void BuildEdges()
+        {
+            for (int i = 0; i < Nodes.Count; i++)
+            {
+                for (int j = 0; j < Nodes.Count; j++)
+                {
+                    if (AdjacencyMatrix[i, j].magnitude > 0) // Check if there's a connection
+                    {
+                        float angle = AdjacencyMatrix[i, j].angle;
+                        float x = Nodes[j].X; // Target node x
+                        float y = Nodes[j].Y; // Target node y
+                        Nodes[i].Edges.Add(new Edge(Nodes[j], AdjacencyMatrix[i, j].magnitude));
+                    }
+                }
+            }
+        }
+
+    }
 }
diff --git a/App/RobobinApp/ViewModels/MainPageViewModel.cs b/App/RobobinApp/ViewModels/MainPageViewModel.cs
index 5d1041cb..26c62ad1 100644
--- a/App/RobobinApp/ViewModels/MainPageViewModel.cs
+++ b/App/RobobinApp/ViewModels/MainPageViewModel.cs
@@ -119,7 +119,7 @@ namespace RobobinApp.ViewModels
 
             // Add graphs to collection
             Graphs = new ObservableCollection<Graph> { graph1, graph2, graph3 };
-            GraphNames = new ObservableCollection<string>(Graphs.Select(g => g.Name)); // Create list of names
+            GraphNames = new ObservableCollection<string>(Graphs.Select(g => g.Name));
             Graph = Graphs.First();
         }
 
diff --git a/App/RobobinApp/Views/CentreGraph.xaml.cs b/App/RobobinApp/Views/CentreGraph.xaml.cs
index 6c2d591e..7836b1d3 100644
--- a/App/RobobinApp/Views/CentreGraph.xaml.cs
+++ b/App/RobobinApp/Views/CentreGraph.xaml.cs
@@ -1,83 +1,211 @@
 using Microsoft.Maui.Graphics;
 using Robobin.Models;
-using RobobinApp.Models; // Ensure you have the correct namespace for your Models
+using RobobinApp.Models;
+using System;
+using System.Linq;
 
 namespace RobobinApp.Views
 {
     public class GraphicsDrawable : IDrawable
     {
         private Graph _graph;
+        public const float BaseNodeRadius = 5f; // Make this public
+        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; // Color for the highlighted node
+
+        // Properties to keep track of scale and offset
+        public float CurrentScale { get; private set; } = 1.0f; // Initialize to default scale
+        public (float X, float Y) CurrentOffset { get; private set; } = (0, 0); // Initialize to default offset
+        private Node _highlightedNode; // Field to keep track of the highlighted node
 
         public void SetGraph(Graph graph)
         {
-            _graph = graph;
+            _graph = graph ?? throw new ArgumentNullException(nameof(graph));
+            _highlightedNode = null; // Reset highlighted node when setting a new graph
         }
 
         public void Draw(ICanvas canvas, RectF dirtyRect)
         {
             if (_graph == null) return;
 
-      
-            canvas.FillColor = Color.FromHex("#2C2F33");
+            DrawBackground(canvas, dirtyRect);
+            (float minX, float minY, float maxX, float maxY) = CalculateBounds();
+
+            CurrentScale = CalculateScale(dirtyRect, minX, minY, maxX, maxY); // Update CurrentScale
+            (float offsetX, float offsetY) = CalculateOffsets(dirtyRect, minX, minY, maxX, maxY, CurrentScale);
+            CurrentOffset = (offsetX, offsetY); // Update CurrentOffset
+            float nodeRadius = BaseNodeRadius * CurrentScale;
+
+            DrawConnections(canvas, CurrentScale, offsetX, offsetY);
+            DrawNodes(canvas, CurrentScale, offsetX, offsetY, nodeRadius, _highlightedNode);
+        }
+
+        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;
-            float scale = Math.Min(scaleX, scaleY) * 0.8f; 
+            return Math.Min(scaleX, scaleY) * 0.8f; // Scale with some margin
+        }
 
+        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);
+        }
 
-     
-            const float baseNodeRadius = 5;
-            float nodeRadius = baseNodeRadius * scale;
+        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);
                 for (int j = 0; j < _graph.Nodes.Count; j++)
                 {
-                    var (magnitude, angle) = _graph.AdjacencyMatrix[_graph.Nodes.IndexOf(node), 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 targetY = (node.Y + magnitude * MathF.Sin(MathF.PI * angle / 180)) * scale + offsetY;
 
-      
-                        canvas.StrokeColor = Colors.Grey;
-                        canvas.StrokeSize = 2;
                         canvas.DrawLine(node.X * scale + offsetX, node.Y * scale + offsetY, targetX, targetY);
                     }
                 }
             }
+        }
 
-            // Draw nodes
+        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 scaledY = node.Y * scale + offsetY;
-                canvas.FillColor = Colors.Red;
 
-                if (node.Name == "Home")
-                {
-                    canvas.FillColor = Colors.Green;
-
-                }
+                // Use different color for highlighted node
+                canvas.FillColor = (highlightedNode != null && highlightedNode == node) ? HighlightedNodeColor : (node.Name == "Home" ? HomeNodeColor : DefaultNodeColor);
                 canvas.FillCircle(scaledX, scaledY, nodeRadius);
                 canvas.FontColor = Colors.White;
                 canvas.DrawString(node.Name, scaledX + nodeRadius + 2, scaledY - nodeRadius, HorizontalAlignment.Left);
             }
         }
 
+        public Node FindNearestNode(float clickX, float clickY, float scale, float offsetX, float offsetY)
+        {
+            Node nearestNode = null;
+            float minDistance = float.MaxValue;
+
+            foreach (var node in _graph.Nodes)
+            {
+                float scaledX = node.X * scale + offsetX;
+                float scaledY = node.Y * scale + offsetY;
+
+                float distance = MathF.Sqrt(MathF.Pow(clickX - scaledX, 2) + MathF.Pow(clickY - scaledY, 2));
+                if (distance < minDistance)
+                {
+                    minDistance = distance;
+                    nearestNode = node;
+                }
+            }
+
+            return nearestNode;
+        }
+
+        public void HighlightNode(Node node)
+        {
+            _highlightedNode = node; // Store the node to highlight
+        }
+        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>(); // Cost from start to node
+            var fScore = new Dictionary<Node, float>(); // Estimated cost from start to target through node
+
+            foreach (var node in _graph.Nodes)
+            {
+                gScore[node] = float.MaxValue; // Initial cost is infinite
+                fScore[node] = float.MaxValue; // Initial cost is infinite
+            }
+            gScore[startNode] = 0;
+            fScore[startNode] = Heuristic(startNode, targetNode); // Initial heuristic
+
+            while (openSet.Count > 0)
+            {
+                // Find node with the lowest fScore
+                Node currentNode = openSet.OrderBy(node => fScore[node]).First();
+
+                if (currentNode == targetNode)
+                {
+                    return ReconstructPath(cameFrom, currentNode);
+                }
+
+                openSet.Remove(currentNode);
+
+                // Check neighboring nodes
+                foreach (var edge in currentNode.Edges)
+                {
+                    Node neighbor = edge.TargetNode; // Get the neighboring node
+                    float tentativeGScore = gScore[currentNode] + edge.Cost; // Calculate tentative gScore
+
+                    if (tentativeGScore < gScore[neighbor]) // A better path has been found
+                    {
+                        cameFrom[neighbor] = currentNode;
+                        gScore[neighbor] = tentativeGScore;
+                        fScore[neighbor] = gScore[neighbor] + Heuristic(neighbor, targetNode);
+
+                        if (!openSet.Contains(neighbor))
+                        {
+                            openSet.Add(neighbor);
+                        }
+                    }
+                }
+            }
+
+            return new List<Node>(); // Return empty path if there is no path
+        }
+
+        private float Heuristic(Node a, Node b)
+        {
+            // Euclidean distance
+            return MathF.Sqrt(MathF.Pow(a.X - b.X, 2) + MathF.Pow(a.Y - b.Y, 2));
+        }
+
+        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(); // Reverse the path to start from the initial node
+            return totalPath;
+        }
+
     }
 }
diff --git a/App/RobobinApp/Views/MainPage.xaml b/App/RobobinApp/Views/MainPage.xaml
index bd416ccc..6dfef3e9 100644
--- a/App/RobobinApp/Views/MainPage.xaml
+++ b/App/RobobinApp/Views/MainPage.xaml
@@ -45,7 +45,13 @@
                VerticalOptions="FillAndExpand"
                Grid.Column="1" Grid.Row="0">
             <GraphicsView x:Name="GraphicsView"
-                  Drawable="{StaticResource drawable}" />
+               Drawable="{StaticResource drawable}">
+                <GraphicsView.GestureRecognizers>
+                    <TapGestureRecognizer Tapped="OnGraphicsViewTapped" />
+                </GraphicsView.GestureRecognizers>
+            </GraphicsView>
+
+
         </Frame>
 
         <VerticalStackLayout StyleClass="sideFrame" 
diff --git a/App/RobobinApp/Views/MainPage.xaml.cs b/App/RobobinApp/Views/MainPage.xaml.cs
index 886ab3f3..5b06ec41 100644
--- a/App/RobobinApp/Views/MainPage.xaml.cs
+++ b/App/RobobinApp/Views/MainPage.xaml.cs
@@ -1,7 +1,8 @@
 using Microsoft.Maui.Controls;
-using RobobinApp.ViewModels; 
+using RobobinApp.ViewModels;
 using RobobinApp.Models;
 using System.Diagnostics;
+using RobobinApp.Views;
 
 namespace RobobinApp.Views
 {
@@ -11,10 +12,8 @@ namespace RobobinApp.Views
         {
             InitializeComponent();
 
-            // Get the drawable from resources
+      
             var drawable = (GraphicsDrawable)Resources["drawable"];
-
-            // Set the graph in the drawable using the existing BindingContext
             if (BindingContext is MainPageViewModel viewModel)
             {
                 drawable.SetGraph(viewModel.Graph);
@@ -23,21 +22,57 @@ namespace RobobinApp.Views
 
         private void OnGraphSelected(object sender, EventArgs e)
         {
+
             if (sender is Picker picker && BindingContext is MainPageViewModel viewModel)
             {
-                // Update the selected graph
+            
                 viewModel.SelectGraphCommand.Execute(picker.SelectedIndex);
 
-                // Refresh the drawable with the new graph
+    
                 var drawable = (GraphicsDrawable)Resources["drawable"];
                 var newGraph = viewModel.Graphs.FirstOrDefault(g => g.Name == viewModel.Graph.Name);
-                drawable.SetGraph(newGraph);
 
-                // Invalidate the GraphicsView to redraw the updated graph
-                GraphicsView.Invalidate(); 
+ 
+                if (newGraph != null)
+                {
+                    drawable.SetGraph(newGraph);
+                    GraphicsView.Invalidate(); 
+                    Debug.WriteLine("New Graph picked and drawn");
+                }
+                else
+                {
+                    Debug.WriteLine("Graph not found!");
+                }
+            }
+        }
+
+        private void OnGraphicsViewTapped(object sender, TappedEventArgs e)
+        {
+            var tapPoint = e.GetPosition(GraphicsView);
+            var drawable = (GraphicsDrawable)Resources["drawable"];
+
+            float scale = drawable.CurrentScale;
+            var offset = drawable.CurrentOffset;
 
-                Debug.WriteLine("New Graph picked and drawn");
+            var tapPointX = (float)tapPoint.Value.X;
+            var tapPointY = (float)tapPoint.Value.Y;
+            var nearestNode = drawable.FindNearestNode(tapPointX, tapPointY, scale, offset.X, offset.Y);
+
+
+            drawable.HighlightNode(nearestNode);
+
+   
+            if (nearestNode != null)
+            {
+                DisplayAlert("Node Clicked", $"You clicked on node: {nearestNode.Name}", "OK");
             }
+            else
+            {
+                DisplayAlert("No Node", "No node was clicked.", "OK");
+            }
+
+            GraphicsView.Invalidate();
         }
     }
+    
 }
-- 
GitLab