diff --git a/App/RobobinApp/Models/Graph.cs b/App/RobobinApp/Models/Graph.cs index e8fa370cb33143056d2e88d8e2bf1bfab78a2673..8e1c3b8a26c530c28119298f301e7fb281191ed4 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 5d1041cbe089b20c4dcc5ce472862a08f1e8dea2..26c62ad1a904827d1c99676c992bd3b0d2bfa842 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 6c2d591e611a6d0371a3db33c8e9b8fd0adc96be..7836b1d37924baaff77201b685c3f498c3d00114 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 bd416ccc45cac5ab19ebe2dcde447587167dd116..6dfef3e9dee53f2e3338dba0d6c1086eba548073 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 886ab3f3e07a93cf9f1a91c4bb0fb6602f4c30fa..5b06ec4194ad416c4b6a419c536a3687b7067263 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(); } } + }