top of page

pathfinder
 

role: programmer
software: VS Code
framework: Java FX
duration: -

date: -
team size: 1
type: Navigation tool
---
github:
Here

 

 

 

overview.

PathFinder is a graphical JavaFX application that lets users build and explore a city-based navigation network. Users can place cities on a map, connect them with named and weighted roads, and compute the shortest path between any two cities. I built it as part of my studies at Stockholm University, with a focus on clean separation between the graph logic and the UI layer.


The graph itself is implemented behind a generic interface, meaning the data structure has no knowledge of cities or JavaFX — it works on any type. The City and PathFinder classes sit on top and handle the visual representation and user interactions.

 

 

​​

development.

I followed a structured and iterative approach during the development of this project, ensuring maintainability and expandability. Below is a breakdown of the key components and the thought process behind my design.​​

​

graph representation

The foundation is a Graph<T> interface that defines all fundamental operations: adding and removing nodes, connecting and disconnecting them, retrieving edges, checking connectivity, and finding paths. Using a generic interface here was a deliberate decision — the graph has no dependency on City or any other concrete type, which means it could be reused for any graph problem without modification.


The concrete implementation is ListGraph<T>, which stores nodes as keys in a HashMap where each value is a HashSet of Edge<T> objects. I chose an adjacency list over an adjacency matrix because the city network is sparse — most cities connect to only a handful of others — so a matrix would waste memory on empty entries. The HashMap gives average O(1) lookup when retrieving a node's edges, and the HashSet makes checking for duplicate connections straightforward.


Connections are undirected. When I call connect(node1, node2, name, weight), I add an edge in both directions — one from node1 to node2 and one from node2 to node1. The same applies to disconnect and setConnectionWeight, which updates both edge objects to keep them in sync. Error handling is explicit throughout: accessing a node that doesn't exist throws NoSuchElementException, negative weights throw IllegalArgumentException, and attempting to create a duplicate connection throws IllegalStateException.

​

Pathfinding
Path finding is implemented in ListGraph.getPath, which uses a modified Breadth-First Search (BFS) with weight tracking rather than a standard unweighted BFS. I maintain two maps: connections, which tracks each node's predecessor, and weights, which tracks the cheapest known total cost to reach each node. The algorithm processes nodes from a LinkedList queue, and for each neighbour it checks whether the new cost via the current node is cheaper than any previously found path. If it is, it updates both maps and re-adds the neighbour to the queue.


Once the destination is reached, gatherPath reconstructs the route by walking backwards through the connections map from destination to source, calling getEdgeBetween at each step to retrieve the actual edge objects, and prepending each to a LinkedList so the final list is ordered source-to-destination.

​​​

Code | Modified Breadth-First Search​

 

 

Connectivity checking uses a separate depth-first search in pathExists. Rather than running the full path algorithm just to check reachability, I use a recursive DFS that maintains a visited HashSet and short-circuits as soon as the target is found. This avoids the overhead of weight tracking when all I need is a yes or no answer.

​

Code | Depth-First Search

​


City and Circle
Cities are represented as JavaFX nodes. I created an abstract Circle class that extends JavaFX's Circle and declares paintRed() and paintBlue() as abstract methods, enforcing that any visual node in the system must implement its own colour behaviour. City extends this, adding a name, a marked boolean to track selection state, and a saveInfo() method that serialises the city's name and screen coordinates as a semicolon-separated string for file output.


Having City extend Circle directly means each city is itself a JavaFX scene node — I add it to the Pane and it renders immediately, without needing a separate shape object. Its JavaFX ID is set to its name, which makes it straightforward to identify in the scene graph.

​

PathFinder and the UI
The PathFinder class extends Application and owns everything at the UI level: the ListGraph<City> instance, references to the two currently selected cities (firstCity and secondCity), and all the JavaFX event handlers as inner classes.


The layout is a BorderPane with a VBox at the top containing a MenuBar and an HBox button bar, and a Pane in the centre that holds the map image and all city and line nodes. Each button and menu item gets its own handler class.


City selection is managed by ClickHandler. Clicking an unselected city marks it red and stores it in firstCity or secondCity. Clicking a marked city deselects it and restores it to blue. This toggle logic means the user always has clear visual feedback about what is selected, and accidental double-selection of the same city is prevented by checking identity before assigning.


Adding a new city switches the cursor to a crosshair and disables the button, then attaches a CityHandler to the pane's mouse click event. When the user clicks, a dialog asks for a name — empty names are rejected with an error — and a new City is created at the click coordinates, added to both the Pane and the ListGraph, and given a ClickHandler for future selection.


Creating a connection requires two cities to be selected and no existing edge between them. The handler opens a dialog with name and time fields rendered in a GridPane. On confirmation it parses the time as an integer — invalid input shows an error rather than crashing — calls listGraph.connect, and draws a Line between the two cities' centre coordinates. The line has setDisable(true) so it doesn't intercept mouse clicks intended for the cities behind it, and both cities are brought to the front with toFront().


Finding a path calls listGraph.getPath on the two selected cities and displays the result in an information alert. The path list is reversed before display since gatherPath builds it destination-to-source. Each edge is printed using Edge.toString() — "to [city] by [road] takes [time]" — and the total travel time is summed and appended at the end.

​

File I/O
The save format is a plain text file with three sections. The first line is the image file path. The second line is all city data serialised as name;x;y entries separated by semicolons, produced by City.saveInfo(). From the third line onward, each line represents one directed edge as source;destination;name;weight.


Loading reverses this exactly: line one sets the background image, line two splits on semicolons in groups of three to reconstruct each City and re-register its click handler, and subsequent lines find the matching city objects by name and call connect if the edge doesn't already exist, then draw the corresponding Line.


Both save and load track unsaved changes via a changed boolean. Any action that modifies the graph sets changed = true. Triggering "New Map", "Open", or closing the window while changed is true shows a confirmation dialog before proceeding. After a successful save, changed is reset to false.


The save image feature uses center.snapshot() to capture the current state of the pane as a WritableImage, converts it to a BufferedImage via SwingFXUtils.fromFXImage, and writes it to disk as a PNG using ImageIO.write.

​

​

​

what I learned.
This project gave me solid hands-on experience with graph data structures beyond what you get from just reading about them. Implementing the adjacency list, writing the pathfinding algorithm, and then having to debug cases where paths were reconstructed incorrectly or edges fell out of sync across both directions of a connection — that kind of work builds an understanding you don't get from theory alone.

Separating the generic Graph<T> interface from the JavaFX UI layer was also a useful exercise in keeping concerns apart. The graph has no idea it's being used to model cities on a map, and that made it much easier to test the logic in isolation and reason about correctness without the UI getting in the way.

Working with JavaFX's event model — inner handler classes, scene graph management, modal dialogs — gave me practical experience with how GUI frameworks structure interaction, which carried over well into later work with other UI systems.

bottom of page