diff --git a/.gitignore b/.gitignore index d38cf6a..f76f414 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ transport_data.json - - +ticket_counter.txt +todo +receipts/ target/ !.mvn/wrapper/maven-wrapper.jar !**/src/main/**/target/ diff --git a/src/main/java/dev/ksan/travelpathoptimizer/app/TravelPathOptimizerApplication.java b/src/main/java/dev/ksan/travelpathoptimizer/app/TravelPathOptimizerApplication.java index 26e3bae..3da747a 100644 --- a/src/main/java/dev/ksan/travelpathoptimizer/app/TravelPathOptimizerApplication.java +++ b/src/main/java/dev/ksan/travelpathoptimizer/app/TravelPathOptimizerApplication.java @@ -1,6 +1,8 @@ package dev.ksan.travelpathoptimizer.app; import java.io.IOException; + +import dev.ksan.travelpathoptimizer.util.TicketPrinter; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Group; @@ -12,7 +14,7 @@ import javafx.stage.Stage; public class TravelPathOptimizerApplication extends Application { @Override public void start(Stage stage) throws IOException { - + TicketPrinter.loadCounter(); FXMLLoader fxmlLoader = new FXMLLoader(TravelPathOptimizerApplication.class.getResource("main.fxml")); diff --git a/src/main/java/dev/ksan/travelpathoptimizer/controller/MainController.java b/src/main/java/dev/ksan/travelpathoptimizer/controller/MainController.java index bd3cad9..c424009 100644 --- a/src/main/java/dev/ksan/travelpathoptimizer/controller/MainController.java +++ b/src/main/java/dev/ksan/travelpathoptimizer/controller/MainController.java @@ -2,20 +2,32 @@ package dev.ksan.travelpathoptimizer.controller; import dev.ksan.travelpathoptimizer.app.TransportDataGenerator; import dev.ksan.travelpathoptimizer.graph.Graph; +import dev.ksan.travelpathoptimizer.graph.PathResult; import dev.ksan.travelpathoptimizer.model.City; 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 dev.ksan.travelpathoptimizer.util.TicketPrinter; import java.io.File; import java.time.LocalTime; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Optional; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.ChoiceBox; import javafx.scene.control.Label; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; import javafx.scene.control.TextField; +import javafx.scene.control.cell.PropertyValueFactory; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; @@ -41,9 +53,9 @@ public class MainController { @FXML private TextField endCityText; @FXML private Button startCityButton; @FXML private Button endCityButton; - + private HashMap departuresMap = new HashMap<>(); private Graph graph; - City[][] cities; + private City[][] cities; private File selectedFile; @FXML private Button openFileButton; @FXML private TextField nTextField; @@ -58,30 +70,132 @@ public class MainController { @FXML private HBox routeView; @FXML private Text totalTicketPriceText; @FXML private Button buyButton; + @FXML private TableView resultTable; + @FXML private TableColumn tabDepartureCol; + @FXML private TableColumn tabArrivalCol; + @FXML private TableColumn tabTypeCol; + @FXML private TableColumn tabCostCol; + @FXML private ChoiceBox pathChoiceBox; + + @FXML + private void buyTicket() { + + TicketPrinter printer = new TicketPrinter(); + printer.generateTicketReceipt( + updateUiGetList(), Double.parseDouble(totalTicketPriceText.getText())); + } @FXML private void findTopPaths() { graph.reset(); - if (this.startCity != null && this.endCity != null) { - List path = new ArrayList<>(); + updateUiGetList(); + pathChoiceBox.getItems().clear(); + startButton.setDisable(true); + startCityText.setDisable(true); + startCityButton.setDisable(true); + endCityButton.setDisable(true); + endCityText.setDisable(true); - List departuress = new ArrayList<>(); - LocalTime currentTime = LocalTime.of(1, 0); - double totalCost = 0.0; + Task task = + new Task() { + @Override + protected Void call() throws Exception { + graph.reset(); + if (startCity != null && endCity != null) { + List path = new ArrayList<>(); + List departures = new ArrayList<>(); + LocalTime currentTime = LocalTime.of(1, 0); + double totalCost = 0.0; - graph.calculateTopPaths( - this.startCity, - this.endCity, - path, - totalCost, - currentTime, - departuress, - categoryBox.getValue().toString()); - System.out.println(graph.getTopPaths().size()); - // Output the top 5 paths - graph.printTopPaths(); - System.out.println(startCity.getName() + endCity.getName()); + System.out.println(categoryBox.getValue().toString()); + graph.calculateTopPaths( + startCity, + endCity, + path, + totalCost, + currentTime, + departures, + categoryBox.getValue().toString()); + + System.out.println(graph.getTopPaths().size()); + System.out.println(startCity.getName() + endCity.getName()); + if (graph.getTopPaths().isEmpty()) return null; + Platform.runLater( + () -> { + for (PathResult pathResult : graph.getSortedPaths()) { + pathChoiceBox.getItems().add("Route: " + String.valueOf(pathResult.getId())); + } + pathChoiceBox.setValue( + "Route: " + String.valueOf(graph.getSortedPaths().getFirst().getId())); + updateUiGetList(); + }); + } + return null; + } + + @Override + protected void succeeded() { + super.succeeded(); + startButton.setDisable(false); + startCityText.setDisable(false); + startCityButton.setDisable(false); + endCityButton.setDisable(false); + endCityText.setDisable(false); + if (!routeView.isVisible()) showRouteView(); + } + + @Override + protected void failed() { + super.failed(); + startButton.setDisable(false); + startCityText.setDisable(false); + startCityButton.setDisable(false); + endCityButton.setDisable(false); + endCityText.setDisable(false); + } + }; + + new Thread(task).start(); + } + + private synchronized ObservableList updateUiGetList() { + + ObservableList departureList = FXCollections.observableArrayList(); + while (pathChoiceBox == null) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + e.printStackTrace(); + } } + if (graph.getTopPaths().size() > 0) { + Optional> departuresOpt = + graph.getSortedPaths().stream() + .filter( + item -> + item.getId() + == Integer.parseInt(pathChoiceBox.getValue().replaceAll("[^0-9]", ""))) + .map(PathResult::getDeparturesUsed) + .findFirst(); + + if (departuresOpt.isPresent()) { + for (Integer dep : departuresOpt.get()) { + departureList.add(departuresMap.get(dep)); + } + } else { + System.out.println("No matching PathResult found."); + } + } + System.out.println(departureList.size()); + resultTable.setItems(departureList); + resultTable.refresh(); + + double totalTicketPrice = calculateTotalCost(departureList); + Platform.runLater( + () -> { + totalTicketPriceText.setText(String.format("%.2f", totalTicketPrice)); + }); + return departureList; } @FXML @@ -153,12 +267,12 @@ public class MainController { startCity = cities[currentRow][currentCol]; startCityText.setText(startCity.getName()); selectingStart = false; - updateMap(); // redraw grid with updated startCity style + updateMap(); } else if (selectingEnd) { endCity = cities[currentRow][currentCol]; endCityText.setText(endCity.getName()); selectingEnd = false; - updateMap(); // redraw grid with updated endCity style + updateMap(); } }); thisCell.setStyle("-fx-border-color: black; -fx-background-color: white;"); @@ -200,10 +314,64 @@ public class MainController { @FXML public void initialize() { + pathChoiceBox.setOnAction( + event -> { + updateUiGetList(); + }); categoryBox.getItems().addAll("time", "price", "hops"); categoryBox.setValue("time"); + tabDepartureCol.setCellFactory( + column -> + new TableCell() { + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + + if (empty || getTableRow() == null || getTableRow().getItem() == null) { + setText(null); + } else { + Departure dep = getTableRow().getItem(); + String departureInfo = + dep.getFrom() + " (" + dep.getDepartureTime().toString() + ")"; + setText(departureInfo); + } + } + }); + tabArrivalCol.setCellFactory( + column -> + new TableCell() { + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + + if (empty || getTableRow() == null || getTableRow().getItem() == null) { + setText(null); + } else { + Departure dep = getTableRow().getItem(); + String departureInfo = dep.getTo() + " (" + dep.getArrivalTime().toString() + ")"; + setText(departureInfo); + } + } + }); + tabTypeCol.setCellFactory( + column -> + new TableCell() { + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + + if (empty || getTableRow() == null || getTableRow().getItem() == null) { + setText(null); + } else { + Departure dep = getTableRow().getItem(); + setText(dep.getType().toString()); + } + } + }); + tabCostCol.setCellValueFactory(new PropertyValueFactory<>("price")); + endCityText .textProperty() .addListener( @@ -214,8 +382,8 @@ public class MainController { } else { City match = CityManager.getCityByName(newText.trim()); - System.out.println("Selected end: " + match.getName()); if (match != null) { + System.out.println("Selected end: " + match.getName()); endCity = match; endCityText.setStyle("-fx-text-fill: green;"); } else { @@ -236,8 +404,9 @@ public class MainController { } else { City match = CityManager.getCityByName(newText.trim()); - System.out.println("Selected start: " + match.getName()); if (match != null) { + + System.out.println("Selected start: " + match.getName()); startCity = match; startCityText.setStyle("-fx-text-fill: green;"); } else { @@ -250,6 +419,14 @@ public class MainController { }); } + private double calculateTotalCost(ObservableList depList) { + double totalCost = 0.0; + for (Departure dep : depList) { + totalCost += dep.getPrice(); + } + return totalCost; + } + @FXML void generateNewMap() { if (!nTextField.getText().isEmpty() && !mTextField.getText().isEmpty()) { @@ -272,7 +449,9 @@ public class MainController { CityManager.clear(); List cities = JsonParser.parseCities("transport_data.json", "stations"); List departures = JsonParser.getDeparturesList("transport_data.json", "departures"); + for (Departure dep : departures) { + this.departuresMap.put(dep.getIdCounter(), dep); for (City city : cities) { if (dep.getTo().equals(city.getName())) { dep.setToCity(city); diff --git a/src/main/java/dev/ksan/travelpathoptimizer/graph/Graph.java b/src/main/java/dev/ksan/travelpathoptimizer/graph/Graph.java index e2ce22b..34d3d83 100644 --- a/src/main/java/dev/ksan/travelpathoptimizer/graph/Graph.java +++ b/src/main/java/dev/ksan/travelpathoptimizer/graph/Graph.java @@ -78,6 +78,11 @@ public class Graph { printTopPaths(); } + public List getSortedPaths(){ + List pathList = new ArrayList<>(topPaths); + pathList.sort(Comparator.comparingDouble(PathResult::getCost)); + return pathList; + } public void reset() { topPaths.clear(); pathIdCounter = 1; @@ -155,6 +160,7 @@ public class Graph { cost += dep.getPrice(); } else if (type.equals("hops")) { cost++; + if(!topPaths.isEmpty() && totalCost + cost >= topPaths.peek().getCost()) continue; } else { return; } diff --git a/src/main/java/dev/ksan/travelpathoptimizer/model/Departure.java b/src/main/java/dev/ksan/travelpathoptimizer/model/Departure.java index f5ee20c..4987aa0 100644 --- a/src/main/java/dev/ksan/travelpathoptimizer/model/Departure.java +++ b/src/main/java/dev/ksan/travelpathoptimizer/model/Departure.java @@ -11,6 +11,7 @@ public class Departure { private String to; private int duration; private LocalTime departureTime; + private LocalTime arrivalTime; private double price; private int minTransferTime; private City toCity; @@ -32,6 +33,7 @@ public class Departure { this.duration = duration; this.price = price; this.minTransferTime = minTransferTime; + this.arrivalTime = this.departureTime.plusMinutes(duration); } public int getIdCounter() { return id; @@ -48,6 +50,9 @@ public class Departure { public LocalTime getDepartureTime() { return departureTime; } + public LocalTime getArrivalTime() { + return arrivalTime; + } public TransportType getType() { return type; } diff --git a/src/main/java/dev/ksan/travelpathoptimizer/util/TicketPrinter.java b/src/main/java/dev/ksan/travelpathoptimizer/util/TicketPrinter.java new file mode 100644 index 0000000..1133773 --- /dev/null +++ b/src/main/java/dev/ksan/travelpathoptimizer/util/TicketPrinter.java @@ -0,0 +1,103 @@ +package dev.ksan.travelpathoptimizer.util; + +import dev.ksan.travelpathoptimizer.model.Departure; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDate; +import java.util.List; + +public class TicketPrinter { + private static final String COUNTER_FILE = "ticket_counter.txt"; + private static final String RECEIPTS_DIRECTORY = "receipts"; + private static final double totalProfit = 0.0; + private static int ticketIdCounter = 0; + + public static void generateTicketReceipt(List departures, double totalPrice) { + LocalDate currentDate = LocalDate.now(); + + StringBuilder receipt = new StringBuilder(); + + receipt.append("=====================================================\n"); + receipt.append(" TICKET RECEIPT\n"); + receipt.append(" Ksan Travel Optimizer\n"); + receipt.append("=====================================================\n"); + + receipt.append("Ticket ID: ").append(getNextId()).append("\n"); + saveCounter(); + receipt.append("Date: ").append(currentDate).append("\n"); + receipt.append("From: ").append(departures.getFirst().getFrom()).append("\n"); + receipt.append("To: ").append(departures.getLast().getTo()).append("\n"); + receipt.append("-----------------------------------------------------\n"); + + receipt.append("Departure Prices:\n"); + for (Departure dep : departures) { + receipt + .append(" - ") + .append(dep.getFrom()) + .append(" -> ") + .append(dep.getTo()) + .append(" | Price: $") + .append(String.format("%.2f", dep.getPrice())) + .append("\n"); + } + + receipt.append("-----------------------------------------------------\n"); + receipt.append("Total Price: $").append(String.format("%.2f", totalPrice)).append("\n"); + + receipt.append("=====================================================\n"); + receipt.append("Thank you for choosing our service!\n"); + folderExists(); + + writeReceipt(receipt.toString()); + } + + private static void writeReceipt(String receipt) { + Path receiptFile = Paths.get(RECEIPTS_DIRECTORY, "receipt_" + ticketIdCounter + ".txt"); + try { + Files.write(receiptFile, receipt.getBytes()); + System.out.println("Receipt saved to: " + receiptFile.toString()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static void folderExists() { + Path path = Paths.get(RECEIPTS_DIRECTORY); + + if (!Files.exists(path)) { + try { + Files.createDirectory(path); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + public static void loadCounter() { + try { + Path path = Paths.get(COUNTER_FILE); + if (Files.exists(path)) { + String counter = new String(Files.readAllBytes(path)).trim(); + ticketIdCounter = Integer.parseInt(counter); + } else { + ticketIdCounter = 0; + } + } catch (Exception e) { + e.printStackTrace(); + ticketIdCounter = 0; + } + } + + private static void saveCounter() { + try { + Files.write(Paths.get(COUNTER_FILE), String.valueOf(ticketIdCounter).getBytes()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static int getNextId() { + return ++ticketIdCounter; + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 50b60c2..1280924 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -3,6 +3,7 @@ module dev.ksan.travelpathoptimizer { requires javafx.fxml; requires java.desktop; requires javafx.graphics; + requires javafx.base; opens dev.ksan.travelpathoptimizer to javafx.fxml; @@ -38,4 +39,3 @@ module dev.ksan.travelpathoptimizer { opens dev.ksan.travelpathoptimizer.graph to javafx.fxml; } - diff --git a/src/main/resources/dev/ksan/travelpathoptimizer/app/main.fxml b/src/main/resources/dev/ksan/travelpathoptimizer/app/main.fxml index 582e51e..dbb5044 100644 --- a/src/main/resources/dev/ksan/travelpathoptimizer/app/main.fxml +++ b/src/main/resources/dev/ksan/travelpathoptimizer/app/main.fxml @@ -2,7 +2,10 @@ + + + @@ -100,6 +103,7 @@ + @@ -114,7 +118,6 @@ - @@ -153,6 +156,25 @@ + + + + + + + + + + + + + + + @@ -175,6 +197,39 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +