diff --git a/src/main/java/dev/ksan/travelpathoptimizer/graph/Graph.java b/src/main/java/dev/ksan/travelpathoptimizer/graph/Graph.java index 5a82529..031963b 100644 --- a/src/main/java/dev/ksan/travelpathoptimizer/graph/Graph.java +++ b/src/main/java/dev/ksan/travelpathoptimizer/graph/Graph.java @@ -5,167 +5,291 @@ 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; +import java.time.LocalTime; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; public class Graph { private City[][] matrix; private List allPaths = new ArrayList<>(); private int pathIdCounter = 1; - + private static int nextid = 0; + private static Set visitedRoutes = new HashSet<>(); + public static PriorityQueue topPaths = new PriorityQueue<>(5, Comparator.comparingDouble(PathResult::getCost).reversed()); public static void main(String[] args) { List cities = JsonParser.parseCities("transport_data.json", "stations"); List departures = JsonParser.getDeparturesList("transport_data.json", "departures"); + for (Departure dep : departures) { + for (City city : cities) { + if (dep.getTo().equals(city.getName())) { + dep.setToCity(city); + } + } + } cities = JsonParser.loadDepartures(cities, departures); City[][] map = JsonParser.loadMap("transport_data.json", "countryMap", cities); Graph graph = new Graph(map); cities = JsonParser.loadDepartures(cities, departures); - System.out.println(cities.getFirst().getName() + " do " + cities.get(3).getName()); + System.out.println(cities.getFirst().getName() + " do " + cities.getLast().getName()); for (City city : cities) { CityManager.addCity(city); } - Map result = - graph.calculateShortestPath(cities.getFirst(), cities.get(3), "time"); - System.out.println(cities.get(3).getName() + " = " + result.get(cities.get(3).getLocation())); - List 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()); + Map result = + graph.calculateShortestPath(cities.getFirst(), cities.getLast(), "time"); + System.out.println( + cities.get(1).getName() + " = " + result.get(cities.getLast().getLocation())); + + + + + /* + graph.calculateTopPaths(cities.getFirst(), cities.getLast()); + + List sorted = new ArrayList<>(graph.topPaths); + sorted.sort(Comparator.comparingDouble(PathResult::getCost)); + for (PathResult res : sorted) { + System.out.println("Path ID: " + res.getId()); + System.out.print("Cities: "); + for (City city : res.getPath()) { + System.out.print(city.getName() + " → "); + } + System.out.println("End"); System.out.println("Departures:"); - for (Departure dep : path.getDepartures()) { - System.out.println( - " " - + dep.getDestinationCity().getName() - + " (Price: " - + dep.getPrice() - + ", Time: " - + dep.getDuration() - + ")"); - } - System.out.println("-----"); + + System.out.println("Total Cost: " + res.getCost() + " min"); + System.out.println("Arrival Time: " + res.getArrivalTime()); + System.out.println("--------------------------------------------------"); + } + + */ + + List path = new ArrayList<>(); + List departuress = new ArrayList<>(); + LocalTime currentTime = LocalTime.of(1, 0); // Assume starting at 8:00 AM + double totalCost = 0.0; + + calculateTopPaths( + cities.getFirst(), cities.getLast(), path, totalCost, currentTime, departuress); + System.out.println(topPaths.size()); + // Output the top 5 paths + printTopPaths(); } + + + public static void addToTopPaths(PathResult newPath) { + String routeHash = generateRouteHash(newPath.getDeparturesUsed()); + + if (visitedRoutes.contains(routeHash)) { + return; + } + + visitedRoutes.add(routeHash); + + if (topPaths.size() < 5) { + topPaths.offer(newPath); + } else { + if (newPath.getCost() < topPaths.peek().getCost()) { + System.out.println("Removing path with cost: " + topPaths.peek().getCost()); + topPaths.poll(); + topPaths.offer(newPath); + } } } - private boolean hasCycle(List path) { - Set seen = new HashSet<>(); - for (City c : path) { - if (!seen.add(c.getName())) return true; - } - return false; - } - public List calculateRankedPaths(City startCity, City endCity, String type) { - - List allFoundPaths = new ArrayList<>(); - - PriorityQueue pq = new PriorityQueue<>(Comparator.comparingDouble(s -> s.cost)); - pq.add(new PathState(startCity, new ArrayList<>(List.of(startCity)), new ArrayList<>(), 0.0)); - - Set 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 newPath = new ArrayList<>(current.getPath()); - newPath.add(nextCity); - - List 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 selectedPaths = allFoundPaths.subList(0, Math.min(toStore, - // allFoundPaths.size())); - - Map 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 selectedPaths = - bestPaths.values().stream() - .sorted(Comparator.comparingDouble(PathResult::getCost)) - .limit(5) - .toList(); - - allPaths.clear(); - allPaths.addAll(selectedPaths); - return selectedPaths; - } - - private String getRichPathSignature(List path, List departures) { + private static String generateRouteHash(List 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("->"); - } + for (Integer depId : departures) { + sb.append(depId).append("-"); } return sb.toString(); } + public static void calculateTopPaths( + City currentCity, + City endCity, + List path, + double totalCost, + LocalTime currentTime, + List departures) { + if (currentCity.getLocation().equals(endCity.getLocation())) { + + addToTopPaths(new PathResult(nextid++, new ArrayList<>(path), new ArrayList<>(departures), totalCost, currentTime)); + } + + for (Departure dep : currentCity.getDestinations()) { + if (dep.getDepartureTime().isBefore(currentTime)) continue; + City nextCity; + if (path.contains(dep.getDestinationCity())) { + continue; + } else { + nextCity = dep.getDestinationCity(); + } + LocalTime arrivalTime = dep.getDepartureTime().plusMinutes(dep.getDuration()); + + if (topPaths.size() >= 5 && totalCost + dep.getDuration() > topPaths.peek().getCost()) { + return; + } + path.add(nextCity); + departures.add(dep.getIdCounter()); + calculateTopPaths( + nextCity, endCity, path, totalCost + dep.getDuration(), arrivalTime, departures); + + departures.remove(departures.size() - 1); + path.remove(path.size() - 1); + } + } + + public static void printTopPaths() { + System.out.println("Top 5 Paths:"); + int rank = 5; + while (!topPaths.isEmpty()) { + PathResult pathResult = topPaths.poll(); + System.out.println("Rank: " + rank--); + System.out.println("ID: " + pathResult.getId()); + System.out.println("Cost: " + pathResult.getCost()); + System.out.println("Path: " + pathResult.getPath().size()); + System.out.println("Departures: " + pathResult.getDeparturesUsed()); + System.out.println(); + } + } + +/* + private PriorityQueue topPaths = + new PriorityQueue<>(Comparator.comparingDouble(PathResult::getCost).reversed()); + private final AtomicInteger pathIdGenerator = new AtomicInteger(1); // Start from 1 + private final Set uniquePathKeys = new HashSet<>(); + + private int makeDeparturePathHash(Set departures) { + // A simple approach: sum of hashes or use Objects.hash(...) + return departures.stream() + .mapToInt( + dep -> + Objects.hash( + dep.getDepartureTime() + .toSecondOfDay(), // convert time to int seconds for consistency + dep.getDestinationCity() + .getLocation(), // assuming Location implements hashCode properly + Double.valueOf(dep.getDuration()).hashCode())) + .sorted() // sort to avoid order affecting hash + .reduce(1, (a, b) -> 31 * a + b); // combine with a hash combiner + } + + + + + private HashSet uniquePathHashes = new HashSet(); + public void calculateTopPaths(City start, City end) { + topPaths.clear(); + uniquePathKeys.clear(); + pathIdGenerator.set(1); + + for (Departure dep : start.getDestinations()) { + City next = dep.getDestinationCity(); + double duration = dep.getDuration(); + LocalTime depTime = dep.getDepartureTime(); + LocalTime arrivalTime = depTime.plusMinutes((long) duration); + + List initialPath = new ArrayList<>(); + Set usedDepartures = new HashSet<>(); + usedDepartures.add(dep); + + initialPath.add(start); // include start + initialPath.add(next); // move to first destination + List initialDepartures = new ArrayList<>(); + initialDepartures.add(dep.getIdCounter()); + Set visited = new HashSet<>(); + visited.add(start.getLocation()); + + findPaths( + next, + end, + initialPath, + usedDepartures, + initialDepartures, + duration, + arrivalTime, + visited); + } + } + + public void findPaths( + City current, + City end, + List pathSoFar, + Set usedDepartures, + List departuresSoFar, + double totalCost, + LocalTime currentTime, + Set visitedCities + ) { + if (visitedCities.contains(current.getLocation())) return; + + visitedCities.add(current.getLocation()); + + if (current.getLocation().equals(end.getLocation())) { + int pathHash = makeDeparturePathHash(usedDepartures); + if (uniquePathHashes.contains(pathHash)) { + visitedCities.remove(current.getLocation()); + return; + } + + uniquePathHashes.add(pathHash); + int pathId = pathIdGenerator.getAndIncrement(); + topPaths.add( + new PathResult( + pathId, + new ArrayList<>(pathSoFar), + new ArrayList<>(departuresSoFar), + totalCost, + currentTime)); + + if (topPaths.size() > 5) topPaths.poll(); + + visitedCities.remove(current.getLocation()); // backtrack + return; + } + + for (Departure dep : current.getDestinations()) { + if (usedDepartures.contains(dep)) continue; + if (dep.getDepartureTime().isBefore(currentTime)) continue; + + City next = dep.getDestinationCity(); + if (visitedCities.contains(next.getLocation())) continue; // skip if already visited + + LocalTime arrivalTime = dep.getDepartureTime().plusMinutes((long) dep.getDuration()); + + usedDepartures.add(dep); + departuresSoFar.add(dep.getIdCounter()); + pathSoFar.add(next); + if (!(topPaths.peek() == null)) { + if (totalCost + dep.getDuration() > topPaths.peek().getCost()) { + continue; + } + } + findPaths( + next, + end, + pathSoFar, + usedDepartures, + departuresSoFar, + totalCost + dep.getDuration(), + arrivalTime, + visitedCities); + + // backtrack + pathSoFar.remove(pathSoFar.size() - 1); + departuresSoFar.remove(departuresSoFar.size() - 1); + usedDepartures.remove(dep); + } + + visitedCities.remove(current.getLocation()); // backtrack + } +*/ public Map calculateShortestPath(City startCity, City endCity, String type) { int n = matrix.length; int m = matrix[0].length; @@ -186,7 +310,6 @@ public class Graph { while (!pq.isEmpty()) { City current = pq.poll(); - // If we reached the end city, we can stop early if (current == endCity) { break; } @@ -215,6 +338,8 @@ public class Graph { return distances; } + + public List getAllPaths() { return allPaths; } diff --git a/src/main/java/dev/ksan/travelpathoptimizer/graph/PathResult.java b/src/main/java/dev/ksan/travelpathoptimizer/graph/PathResult.java index efa13a1..a76843c 100644 --- a/src/main/java/dev/ksan/travelpathoptimizer/graph/PathResult.java +++ b/src/main/java/dev/ksan/travelpathoptimizer/graph/PathResult.java @@ -2,50 +2,47 @@ package dev.ksan.travelpathoptimizer.graph; import dev.ksan.travelpathoptimizer.model.City; import dev.ksan.travelpathoptimizer.model.Departure; + +import java.time.LocalTime; +import java.util.ArrayList; import java.util.List; public class PathResult { - private int id; - private List path; - private List departures; - private double cost; + private int id; + private List path = new ArrayList<>(); + private List departuresUsed = new ArrayList<>(); + private double cost; + private LocalTime arrivalTime; - public PathResult(int id, List path, List departures, double cost) { + public PathResult(int id, List path, List departuresUsed, double cost, LocalTime arrivalTime) { this.id = id; this.path = path; - this.departures = departures; + this.departuresUsed = departuresUsed; this.cost = cost; + this.arrivalTime = arrivalTime; } public int getId() { return id; } - public void setId(int id) { - this.id = id; - } - public List getPath() { return path; } - public void setPath(List path) { - this.path = path; - } - - public List getDepartures() { - return departures; - } - - public void setDepartures(List departures) { - this.departures = departures; + public List getDeparturesUsed() { + return departuresUsed; } public double getCost() { return cost; } - public void setCost(double cost) { - this.cost = cost; + public LocalTime getArrivalTime() { + return arrivalTime; + } + @Override + public String toString() { + return "PathResult{id=" + id + " cost = " + cost + ", path=" + path + ", arrivalTime=" + arrivalTime + '}'; } } diff --git a/src/main/java/dev/ksan/travelpathoptimizer/graph/PathState.java b/src/main/java/dev/ksan/travelpathoptimizer/graph/PathState.java index d9b259a..d7c2fa2 100644 --- a/src/main/java/dev/ksan/travelpathoptimizer/graph/PathState.java +++ b/src/main/java/dev/ksan/travelpathoptimizer/graph/PathState.java @@ -19,7 +19,14 @@ public class PathState { } public String getPathSignature() { - return path.stream().map(c -> c.getLocation().toString()).collect(Collectors.joining("->")); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < path.size(); i++) { + sb.append(path.get(i).getLocation().toString()); + if (i < path.size() - 1) { + sb.append("->"); + } + } + return sb.toString(); } public City getCity() { diff --git a/src/main/java/dev/ksan/travelpathoptimizer/model/Departure.java b/src/main/java/dev/ksan/travelpathoptimizer/model/Departure.java index fdf8313..f5ee20c 100644 --- a/src/main/java/dev/ksan/travelpathoptimizer/model/Departure.java +++ b/src/main/java/dev/ksan/travelpathoptimizer/model/Departure.java @@ -13,7 +13,9 @@ public class Departure { private LocalTime departureTime; private double price; private int minTransferTime; - + private City toCity; + private static int idCounter = 0; + private int id; public Departure( TransportType type, String from, @@ -22,6 +24,7 @@ public class Departure { int duration, double price, int minTransferTime) { + this.id = idCounter++; this.type = type; this.from = from; this.to = to; @@ -30,12 +33,21 @@ public class Departure { this.price = price; this.minTransferTime = minTransferTime; } - + public int getIdCounter() { + return id; + } + public void setToCity(City toCity) { + this.toCity = toCity; + } public City getDestinationCity() { - return CityManager.getCityByName(to); + return toCity; +// return CityManager.getCityByName(to); } + public LocalTime getDepartureTime() { + return departureTime; + } public TransportType getType() { return type; } diff --git a/src/main/java/dev/ksan/travelpathoptimizer/service/CityManager.java b/src/main/java/dev/ksan/travelpathoptimizer/service/CityManager.java index 24a8c20..46c190c 100644 --- a/src/main/java/dev/ksan/travelpathoptimizer/service/CityManager.java +++ b/src/main/java/dev/ksan/travelpathoptimizer/service/CityManager.java @@ -1,18 +1,26 @@ package dev.ksan.travelpathoptimizer.service; import dev.ksan.travelpathoptimizer.model.City; +import dev.ksan.travelpathoptimizer.model.Location; import java.util.HashMap; import java.util.Map; public class CityManager { private static Map cities = new HashMap<>(); + private static Map citiesByLocation = new HashMap<>(); public static void clear() { + citiesByLocation.clear(); cities.clear(); } public static void addCity(City city) { cities.put(city.getName(), city); + citiesByLocation.put(city.getLocation(), city); + } + + public static City getCityByLocation(Location loc) { + return citiesByLocation.get(loc); } public static City getCityByName(String cityName) {