implemented function to calculate and rank paths and return a list of best paths

This commit is contained in:
Ksan 2025-07-27 17:43:37 +02:00
parent 38fa1c69e0
commit 5f68391cdc
7 changed files with 282 additions and 53 deletions

View File

@ -27,6 +27,7 @@ public class MainController {
@FXML private GridPane map;
@FXML private Label welcomeText;
@FXML private Text selectedFileText;
@FXML private Button mapSelectButton;
private City startCity = null;
private City endCity = null;
private boolean selectingStart = false;
@ -37,7 +38,6 @@ public class MainController {
@FXML private Button endCityButton;
City[][] cities;
private CityManager cityManager;
private File selectedFile;
@FXML private Button openFileButton;
@FXML private TextField nTextField;
@ -153,18 +153,6 @@ public class MainController {
updateMap();
}
private boolean cityExists(String name) {
for (int row = 0; row < cities.length; row++) {
for (int col = 0; col < cities[0].length; col++) {
City city = cities[row][col];
if (city != null && city.getName().equalsIgnoreCase(name.trim())) {
return true;
}
}
}
return false;
}
@FXML
public void initialize() {
endCityText
@ -175,7 +163,7 @@ public class MainController {
endCity = null;
endCityText.setStyle("-fx-text-fill: black;");
} else {
City match = cityManager.getCityByName(newText.trim());
City match = CityManager.getCityByName(newText.trim());
if (match != null) {
endCity = match;
endCityText.setStyle("-fx-text-fill: green;");
@ -195,7 +183,7 @@ public class MainController {
startCity = null;
startCityText.setStyle("-fx-text-fill: black;");
} else {
City match = cityManager.getCityByName(newText.trim());
City match = CityManager.getCityByName(newText.trim());
if (match != null) {
startCity = match;
startCityText.setStyle("-fx-text-fill: green;");
@ -228,6 +216,7 @@ public class MainController {
}
private void getData() {
CityManager.clear();
List<City> cities = JsonParser.parseCities(selectedFile.toString(), "stations");
List<Departure> departures = JsonParser.getDeparturesList("transport_data.json", "departures");
cities = JsonParser.loadDepartures(cities, departures);
@ -260,19 +249,7 @@ public class MainController {
}
getData();
updateMap();
this.mapSelectButton.setText(selectedFile.getName().toString());
}
/*
@FXML
void openLoadedFileDesktop(){
selectedFileText.setOnMouseClicked(event -> {
if(selectedFile != null && selectedFile.exists()){
try{
Desktop.getDesktop().open(selectedFile);
}
catch(Exception e){
e.printStackTrace();
}
}
});
}*/
}

View File

@ -5,16 +5,20 @@ import dev.ksan.travelpathoptimizer.model.Departure;
import dev.ksan.travelpathoptimizer.model.Location;
import dev.ksan.travelpathoptimizer.service.CityManager;
import dev.ksan.travelpathoptimizer.util.JsonParser;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;
import java.util.Set;
public class Graph {
private City[][] matrix;
/*
private List<PathResult> allPaths = new ArrayList<>();
private int pathIdCounter = 1;
public static void main(String[] args) {
List<City> cities = JsonParser.parseCities("transport_data.json", "stations");
List<Departure> departures = JsonParser.getDeparturesList("transport_data.json", "departures");
@ -23,16 +27,144 @@ public class Graph {
Graph graph = new Graph(map);
cities = JsonParser.loadDepartures(cities, departures);
System.out.println(cities.getFirst().getName() + " do " + cities.getLast().getName());
System.out.println(cities.getFirst().getName() + " do " + cities.get(3).getName());
for (City city : cities) {
CityManager.addCity(city);
}
Map<Location, Double> result =
graph.calculateShortestPath(cities.getFirst(), cities.get(3), "hops");
System.out.println(
cities.getLast().getName() + " = " + result.get(cities.get(3).getLocation()));
graph.calculateShortestPath(cities.getFirst(), cities.get(3), "time");
System.out.println(cities.get(3).getName() + " = " + result.get(cities.get(3).getLocation()));
List<PathResult> ranked = graph.calculateRankedPaths(cities.getFirst(), cities.get(3), "time");
for (PathResult path : ranked) {
String cityNames =
path.getPath().stream().map(City::getName).reduce((x, y) -> x + " -> " + y).orElse("");
System.out.println("Path ID: " + path.getId());
System.out.println("Cities: " + cityNames);
System.out.println("Cost: " + path.getCost());
System.out.println("Departures:");
for (Departure dep : path.getDepartures()) {
System.out.println(
" "
+ dep.getDestinationCity().getName()
+ " (Price: "
+ dep.getPrice()
+ ", Time: "
+ dep.getDuration()
+ ")");
}
System.out.println("-----");
}
}
private boolean hasCycle(List<City> path) {
Set<String> seen = new HashSet<>();
for (City c : path) {
if (!seen.add(c.getName())) return true;
}
return false;
}
public List<PathResult> calculateRankedPaths(City startCity, City endCity, String type) {
List<PathResult> allFoundPaths = new ArrayList<>();
PriorityQueue<PathState> pq = new PriorityQueue<>(Comparator.comparingDouble(s -> s.cost));
pq.add(new PathState(startCity, new ArrayList<>(List.of(startCity)), new ArrayList<>(), 0.0));
Set<String> visitedPaths = new HashSet<>();
while (!pq.isEmpty()) {
PathState current = pq.poll();
City currentCity = current.getCity();
String pathKey = current.getPathSignature();
// if (visitedPaths.contains(pathKey)) continue;
if (hasCycle(current.getPath())) continue;
visitedPaths.add(pathKey);
if (currentCity.getName().equals(endCity.getName())) {
allFoundPaths.add(
new PathResult(
pathIdCounter++,
new ArrayList<>(current.getPath()),
new ArrayList<>(current.getDepartures()),
current.getCost()));
continue;
}
for (Departure dep : currentCity.getDestinations()) {
City nextCity = dep.getDestinationCity();
double additionalCost =
switch (type) {
case "price" -> dep.getPrice();
case "time" -> dep.getDuration();
case "hops" -> 1.0;
default -> throw new IllegalArgumentException("Invalid type: " + type);
};
List<City> newPath = new ArrayList<>(current.getPath());
newPath.add(nextCity);
List<Departure> newDepartures = new ArrayList<>(current.getDepartures());
newDepartures.add(dep);
pq.add(new PathState(nextCity, newPath, newDepartures, current.getCost() + additionalCost));
}
}
// allFoundPaths.sort(Comparator.comparingDouble(PathResult::getCost));
// int toStore = Math.max(5, (int) Math.ceil(allFoundPaths.size() * 0.05));
// List<PathResult> selectedPaths = allFoundPaths.subList(0, Math.min(toStore,
// allFoundPaths.size()));
Map<String, PathResult> bestPaths = new HashMap<>();
for (PathResult pathResult : allFoundPaths) {
String signature = getRichPathSignature(pathResult.getPath(), pathResult.getDepartures());
bestPaths.merge(
signature,
pathResult,
(existing, candidate) -> candidate.getCost() < existing.getCost() ? candidate : existing);
}
List<PathResult> selectedPaths =
bestPaths.values().stream()
.sorted(Comparator.comparingDouble(PathResult::getCost))
.limit(5)
.toList();
allPaths.clear();
allPaths.addAll(selectedPaths);
return selectedPaths;
}
private String getRichPathSignature(List<City> path, List<Departure> departures) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < path.size(); i++) {
City city = path.get(i);
sb.append(city.getLocation().toString());
if (i > 0) {
Departure dep = departures.get(i - 1);
sb.append(":")
.append(dep.getType())
.append("-")
.append(dep.getDuration())
.append("-")
.append(dep.getPrice());
}
if (i < path.size() - 1) {
sb.append("->");
}
}
return sb.toString();
}
*/
public Map<Location, Double> calculateShortestPath(City startCity, City endCity, String type) {
int n = matrix.length;
@ -83,6 +215,10 @@ public class Graph {
return distances;
}
public List<PathResult> getAllPaths() {
return allPaths;
}
public Graph(City[][] matrix) {
this.matrix = matrix;
}

View File

@ -0,0 +1,51 @@
package dev.ksan.travelpathoptimizer.graph;
import dev.ksan.travelpathoptimizer.model.City;
import dev.ksan.travelpathoptimizer.model.Departure;
import java.util.List;
public class PathResult {
private int id;
private List<City> path;
private List<Departure> departures;
private double cost;
public PathResult(int id, List<City> path, List<Departure> departures, double cost) {
this.id = id;
this.path = path;
this.departures = departures;
this.cost = cost;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public List<City> getPath() {
return path;
}
public void setPath(List<City> path) {
this.path = path;
}
public List<Departure> getDepartures() {
return departures;
}
public void setDepartures(List<Departure> departures) {
this.departures = departures;
}
public double getCost() {
return cost;
}
public void setCost(double cost) {
this.cost = cost;
}
}

View File

@ -0,0 +1,56 @@
package dev.ksan.travelpathoptimizer.graph;
import dev.ksan.travelpathoptimizer.model.City;
import dev.ksan.travelpathoptimizer.model.Departure;
import java.util.List;
import java.util.stream.Collectors;
public class PathState {
private City city;
private List<City> path;
private List<Departure> departures;
double cost;
public PathState(City city, List<City> path, List<Departure> departures, double cost) {
this.city = city;
this.path = path;
this.departures = departures;
this.cost = cost;
}
public String getPathSignature() {
return path.stream().map(c -> c.getLocation().toString()).collect(Collectors.joining("->"));
}
public City getCity() {
return city;
}
public void setCity(City city) {
this.city = city;
}
public List<City> getPath() {
return path;
}
public void setPath(List<City> path) {
this.path = path;
}
public List<Departure> getDepartures() {
return departures;
}
public void setDepartures(List<Departure> departures) {
this.departures = departures;
}
public double getCost() {
return cost;
}
public void setCost(double cost) {
this.cost = cost;
}
}

View File

@ -40,4 +40,9 @@ public class Location {
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return x + "_" + y;
}
}

View File

@ -1,13 +1,16 @@
package dev.ksan.travelpathoptimizer.service;
import dev.ksan.travelpathoptimizer.model.City;
import java.util.HashMap;
import java.util.Map;
public class CityManager {
private static Map<String, City> cities = new HashMap<>();
public static void clear() {
cities.clear();
}
public static void addCity(City city) {
cities.put(city.getName(), city);
}

View File

@ -3,7 +3,6 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
@ -101,6 +100,20 @@
</VBox>
<VBox fx:id="calculatorSideBar" managed="true" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" prefHeight="354.0" prefWidth="180.0" visible="true">
<children>
<HBox prefHeight="32.0" prefWidth="180.0">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Map:" textAlignment="CENTER" wrappingWidth="37.562997817993164">
<HBox.margin>
<Insets left="10.0" right="10.0" top="5.0" />
</HBox.margin>
</Text>
<Button fx:id="mapSelectButton" mnemonicParsing="false" onAction="#showMapSideBar" prefHeight="26.0" prefWidth="111.0" text="select">
<HBox.margin>
<Insets left="10.0" right="10.0" top="3.0" />
</HBox.margin>
</Button>
</children>
</HBox>
<Text fx:id="sectionText1" strokeType="OUTSIDE" strokeWidth="0.0" text="Path Calculator" textAlignment="CENTER" wrappingWidth="179.2100067138672" />
<HBox prefHeight="39.0" prefWidth="180.0">
<children>
@ -140,29 +153,17 @@
<Insets left="10.0" right="10.0" top="-5.0" />
</VBox.margin>
</Button>
<VBox prefHeight="80.0" prefWidth="180.0">
<children>
<Button fx:id="openFileButton1" maxWidth="1.7976931348623157E308" minHeight="0.0" minWidth="0.0" mnemonicParsing="false" text="Load Map" VBox.vgrow="ALWAYS">
<VBox.margin>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
</VBox.margin>
</Button>
<Text fx:id="selectedFileText1" strokeType="OUTSIDE" strokeWidth="0.0" text="no file" textAlignment="CENTER" textOrigin="CENTER" wrappingWidth="147.13000106811523" VBox.vgrow="ALWAYS">
<VBox.margin>
<Insets left="15.0" right="15.0" />
</VBox.margin>
</Text>
</children>
</VBox>
</children>
</VBox>
<GridPane fx:id="map" alignment="CENTER" gridLinesVisible="true" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" HBox.hgrow="ALWAYS">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
</rowConstraints>
<padding>
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />