From dbdb46e5deb890c2f943bfd463ef211569946be5 Mon Sep 17 00:00:00 2001 From: feyzagereme Date: Sun, 14 Dec 2025 18:12:30 +0300 Subject: [PATCH] introduce exam scheduling feature with CSP algorithm and calendar UI --- .../ScheduleCalendarController.java | 470 +++++++++++++++++- .../se302/service/ConstraintValidator.java | 105 +++- .../service/ScheduleGeneratorService.java | 41 +- .../se302/view/schedule-calendar-view.fxml | 203 +++++--- 4 files changed, 709 insertions(+), 110 deletions(-) diff --git a/src/main/java/org/example/se302/controller/ScheduleCalendarController.java b/src/main/java/org/example/se302/controller/ScheduleCalendarController.java index f625cf4..9899fc1 100644 --- a/src/main/java/org/example/se302/controller/ScheduleCalendarController.java +++ b/src/main/java/org/example/se302/controller/ScheduleCalendarController.java @@ -1,33 +1,475 @@ package org.example.se302.controller; +import javafx.application.Platform; +import javafx.collections.FXCollections; import javafx.fxml.FXML; -import javafx.scene.control.Alert; -import javafx.scene.control.DatePicker; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.*; +import org.example.se302.model.*; +import org.example.se302.service.DataManager; +import org.example.se302.service.ScheduleGeneratorService; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.*; /** * Controller for the Calendar/Day schedule view. + * Handles schedule configuration and generation using CSP algorithm. */ public class ScheduleCalendarController { - @FXML private DatePicker startDatePicker; - @FXML private DatePicker endDatePicker; + // Configuration controls + @FXML + private Spinner numDaysSpinner; + @FXML + private Spinner slotsPerDaySpinner; + @FXML + private DatePicker startDatePicker; + @FXML + private Spinner slotDurationSpinner; + @FXML + private ComboBox strategyComboBox; + @FXML + private ComboBox startTimeComboBox; + @FXML + private CheckBox allowBackToBackCheckBox; + @FXML + private Label summaryLabel; + + // Action controls + @FXML + private Button generateButton; + @FXML + private Button cancelButton; + @FXML + private Label statusLabel; + + // Progress controls + @FXML + private VBox progressContainer; + @FXML + private ProgressBar progressBar; + @FXML + private Label progressLabel; + + // Schedule display + @FXML + private ScrollPane scheduleScrollPane; + @FXML + private GridPane scheduleGrid; + + // Statistics labels + @FXML + private Label totalCoursesLabel; + @FXML + private Label scheduledCoursesLabel; + @FXML + private Label classroomsUsedLabel; + @FXML + private Label generationTimeLabel; + + // Services + private final DataManager dataManager = DataManager.getInstance(); + private ScheduleGeneratorService generatorService; + private ScheduleState currentSchedule; + private Thread generationThread; @FXML public void initialize() { - // Initialize with default date range if needed + // Initialize spinners + initializeSpinners(); + + // Initialize combo boxes + initializeComboBoxes(); + + // Set default date to next week + startDatePicker.setValue(LocalDate.now().plusDays(7)); + + // Set default checkbox - allow back-to-back for easier scheduling + allowBackToBackCheckBox.setSelected(true); + + // Add listeners to update summary + numDaysSpinner.valueProperty().addListener((obs, oldVal, newVal) -> updateSummary()); + slotsPerDaySpinner.valueProperty().addListener((obs, oldVal, newVal) -> updateSummary()); + + // Initial summary update + updateSummary(); + + // Update status with data info + updateDataStatus(); + } + + private void initializeSpinners() { + // Number of days spinner (1-30) + SpinnerValueFactory daysFactory = new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 30, 5); + numDaysSpinner.setValueFactory(daysFactory); + + // Slots per day spinner (1-10) + SpinnerValueFactory slotsFactory = new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 10, 4); + slotsPerDaySpinner.setValueFactory(slotsFactory); + + // Slot duration spinner (30-240 minutes) + SpinnerValueFactory durationFactory = new SpinnerValueFactory.IntegerSpinnerValueFactory(30, 240, 120, + 30); + slotDurationSpinner.setValueFactory(durationFactory); + } + + private void initializeComboBoxes() { + // Optimization strategies + List strategies = Arrays.asList( + "Default (Balanced)", + "Minimize Days", + "Balanced Distribution", + "Minimize Classrooms", + "Balance Classrooms", + "Student Friendly"); + strategyComboBox.setItems(FXCollections.observableArrayList(strategies)); + strategyComboBox.getSelectionModel().selectFirst(); + + // Start times (8:00 - 14:00) + List times = Arrays.asList( + "08:00", "08:30", "09:00", "09:30", "10:00"); + startTimeComboBox.setItems(FXCollections.observableArrayList(times)); + startTimeComboBox.getSelectionModel().select("09:00"); + } + + private void updateSummary() { + int days = numDaysSpinner.getValue(); + int slots = slotsPerDaySpinner.getValue(); + int total = days * slots; + summaryLabel.setText(String.format("%d days × %d slots = %d total time slots", days, slots, total)); + } + + private void updateDataStatus() { + int courses = dataManager.getTotalCourses(); + int classrooms = dataManager.getTotalClassrooms(); + int students = dataManager.getTotalStudents(); + + if (courses == 0 || classrooms == 0) { + statusLabel + .setText("⚠️ Please import data first (Courses: " + courses + ", Classrooms: " + classrooms + ")"); + statusLabel.setStyle("-fx-text-fill: #e74c3c;"); + } else { + statusLabel.setText( + "✓ Ready - " + courses + " courses, " + classrooms + " classrooms, " + students + " students"); + statusLabel.setStyle("-fx-text-fill: #27ae60;"); + } + + totalCoursesLabel.setText(String.valueOf(courses)); } @FXML private void onGenerateSchedule() { - Alert alert = new Alert(Alert.AlertType.INFORMATION); - alert.setTitle("Coming Soon"); - alert.setHeaderText("Schedule Generation"); - alert.setContentText("The CSP-based schedule generation algorithm will be implemented in Phase 2.\n\n" + - "It will automatically create an exam schedule that satisfies all constraints:\n" + - "- No consecutive exams for students\n" + - "- Max 2 exams per day per student\n" + - "- Classroom capacity limits\n" + - "- No double-booking"); + // Validate data + if (dataManager.getTotalCourses() == 0) { + showAlert(Alert.AlertType.WARNING, "No Data", + "Please import course data before generating a schedule."); + return; + } + + if (dataManager.getTotalClassrooms() == 0) { + showAlert(Alert.AlertType.WARNING, "No Classrooms", + "Please import classroom data before generating a schedule."); + return; + } + + // Validate configuration + if (startDatePicker.getValue() == null) { + showAlert(Alert.AlertType.WARNING, "Missing Date", + "Please select a start date for the exam period."); + return; + } + + // Build configuration + ScheduleConfiguration config = buildConfiguration(); + + // Validate total slots + int totalSlots = config.getTotalSlots(); + int totalCourses = dataManager.getTotalCourses(); + int totalClassrooms = dataManager.getTotalClassrooms(); + + if (totalSlots * totalClassrooms < totalCourses) { + showAlert(Alert.AlertType.WARNING, "Insufficient Capacity", + String.format( + "Not enough time slots! You have %d courses but only %d total capacity (%d slots × %d classrooms).\n\nPlease increase days or slots per day.", + totalCourses, totalSlots * totalClassrooms, totalSlots, totalClassrooms)); + return; + } + + // Start generation + startGeneration(config); + } + + private ScheduleConfiguration buildConfiguration() { + ScheduleConfiguration config = new ScheduleConfiguration(); + + config.setNumDays(numDaysSpinner.getValue()); + config.setSlotsPerDay(slotsPerDaySpinner.getValue()); + config.setStartDate(startDatePicker.getValue()); + config.setSlotDurationMinutes(slotDurationSpinner.getValue()); + config.setAllowBackToBackExams(allowBackToBackCheckBox.isSelected()); + + // Parse start time + String timeStr = startTimeComboBox.getValue(); + if (timeStr != null) { + String[] parts = timeStr.split(":"); + config.setDayStartTime(LocalTime.of(Integer.parseInt(parts[0]), Integer.parseInt(parts[1]))); + } + + // Set optimization strategy + String strategyStr = strategyComboBox.getValue(); + ScheduleConfiguration.OptimizationStrategy strategy = ScheduleConfiguration.OptimizationStrategy.DEFAULT; + + if (strategyStr != null) { + if (strategyStr.contains("Minimize Days")) { + strategy = ScheduleConfiguration.OptimizationStrategy.MINIMIZE_DAYS; + } else if (strategyStr.contains("Balanced Distribution")) { + strategy = ScheduleConfiguration.OptimizationStrategy.BALANCED_DISTRIBUTION; + } else if (strategyStr.contains("Minimize Classrooms")) { + strategy = ScheduleConfiguration.OptimizationStrategy.MINIMIZE_CLASSROOMS; + } else if (strategyStr.contains("Balance Classrooms")) { + strategy = ScheduleConfiguration.OptimizationStrategy.BALANCE_CLASSROOMS; + } else if (strategyStr.contains("Student Friendly")) { + strategy = ScheduleConfiguration.OptimizationStrategy.STUDENT_FRIENDLY; + } + } + config.setOptimizationStrategy(strategy); + + return config; + } + + private void startGeneration(ScheduleConfiguration config) { + // Update UI for generation + generateButton.setDisable(true); + cancelButton.setDisable(false); + progressContainer.setVisible(true); + progressContainer.setManaged(true); + progressBar.setProgress(0); + progressLabel.setText("Initializing schedule generation..."); + statusLabel.setText("⏳ Generating schedule..."); + statusLabel.setStyle("-fx-text-fill: #3498db;"); + + long startTime = System.currentTimeMillis(); + + // Create generator service + generatorService = new ScheduleGeneratorService(); + generatorService.setProgressListener((progress, message) -> { + Platform.runLater(() -> { + progressBar.setProgress(progress); + progressLabel.setText(message); + }); + }); + + // Run generation in background thread + generationThread = new Thread(() -> { + ScheduleGeneratorService.ScheduleResult result = generatorService.generateSchedule(config); + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + + Platform.runLater(() -> { + onGenerationComplete(result, config, duration); + }); + }); + + generationThread.setDaemon(true); + generationThread.start(); + } + + private void onGenerationComplete(ScheduleGeneratorService.ScheduleResult result, + ScheduleConfiguration config, long durationMs) { + // Reset UI + generateButton.setDisable(false); + cancelButton.setDisable(true); + progressContainer.setVisible(false); + progressContainer.setManaged(false); + + if (result.isSuccess()) { + currentSchedule = result.getScheduleState(); + + // Update status + statusLabel.setText("✓ Schedule generated successfully!"); + statusLabel.setStyle("-fx-text-fill: #27ae60;"); + + // Update statistics + updateStatistics(currentSchedule, durationMs); + + // Display schedule in grid + displayScheduleGrid(currentSchedule, config); + + showAlert(Alert.AlertType.INFORMATION, "Success", + "Exam schedule generated successfully!\n\n" + + "Scheduled: " + currentSchedule.getAssignedCourses() + "/" + + currentSchedule.getTotalCourses() + " courses\n" + + "Time: " + durationMs + "ms"); + } else if (result.wasCancelled()) { + statusLabel.setText("⚠️ Generation cancelled"); + statusLabel.setStyle("-fx-text-fill: #f39c12;"); + } else { + statusLabel.setText("❌ Generation failed"); + statusLabel.setStyle("-fx-text-fill: #e74c3c;"); + + showAlert(Alert.AlertType.ERROR, "Generation Failed", + result.getMessage() + + "\n\nTry:\n• Increasing number of days\n• Increasing slots per day\n• Adding more classrooms"); + } + } + + private void updateStatistics(ScheduleState schedule, long durationMs) { + scheduledCoursesLabel.setText(String.valueOf(schedule.getAssignedCourses())); + + // Count unique classrooms used + Set usedClassrooms = new HashSet<>(); + for (ExamAssignment assignment : schedule.getAssignments().values()) { + if (assignment.isAssigned() && assignment.getClassroomId() != null) { + usedClassrooms.add(assignment.getClassroomId()); + } + } + classroomsUsedLabel.setText(String.valueOf(usedClassrooms.size())); + + // Format generation time + if (durationMs < 1000) { + generationTimeLabel.setText(durationMs + "ms"); + } else { + generationTimeLabel.setText(String.format("%.2fs", durationMs / 1000.0)); + } + } + + private void displayScheduleGrid(ScheduleState schedule, ScheduleConfiguration config) { + scheduleGrid.getChildren().clear(); + scheduleGrid.getColumnConstraints().clear(); + scheduleGrid.getRowConstraints().clear(); + + int numDays = config.getNumDays(); + int slotsPerDay = config.getSlotsPerDay(); + + // Column constraints - make columns wider for better visibility + ColumnConstraints headerCol = new ColumnConstraints(); + headerCol.setMinWidth(100); + headerCol.setPrefWidth(120); + scheduleGrid.getColumnConstraints().add(headerCol); + + for (int day = 0; day < numDays; day++) { + ColumnConstraints dayCol = new ColumnConstraints(); + dayCol.setMinWidth(180); + dayCol.setPrefWidth(220); + dayCol.setHgrow(Priority.ALWAYS); + scheduleGrid.getColumnConstraints().add(dayCol); + } + + // Row constraints - make rows taller for better visibility + for (int slot = 0; slot <= slotsPerDay; slot++) { + RowConstraints rowConstraint = new RowConstraints(); + rowConstraint.setMinHeight(80); + rowConstraint.setPrefHeight(100); + rowConstraint.setVgrow(Priority.SOMETIMES); + scheduleGrid.getRowConstraints().add(rowConstraint); + } + + // Header row - Days + Label cornerLabel = createHeaderLabel(""); + scheduleGrid.add(cornerLabel, 0, 0); + + for (int day = 0; day < numDays; day++) { + Label dayLabel = createHeaderLabel("Day " + (day + 1) + "\n" + + config.getStartDate().plusDays(day).toString()); + scheduleGrid.add(dayLabel, day + 1, 0); + } + + // Time slot rows + for (int slot = 0; slot < slotsPerDay; slot++) { + // Time slot label + TimeSlot timeSlot = config.getTimeSlot(0, slot); + String timeText = timeSlot != null ? timeSlot.getStartTime() + "\n-\n" + timeSlot.getEndTime() + : "Slot " + (slot + 1); + Label slotLabel = createHeaderLabel(timeText); + scheduleGrid.add(slotLabel, 0, slot + 1); + + // Cells for each day + for (int day = 0; day < numDays; day++) { + VBox cellContent = createScheduleCell(schedule, day, slot); + scheduleGrid.add(cellContent, day + 1, slot + 1); + } + } + } + + private Label createHeaderLabel(String text) { + Label label = new Label(text); + label.setStyle("-fx-font-weight: bold; -fx-background-color: #34495e; -fx-text-fill: white; " + + "-fx-padding: 10; -fx-alignment: center;"); + label.setMaxWidth(Double.MAX_VALUE); + label.setMaxHeight(Double.MAX_VALUE); + label.setAlignment(Pos.CENTER); + return label; + } + + private VBox createScheduleCell(ScheduleState schedule, int day, int slot) { + VBox cell = new VBox(3); + cell.setPadding(new Insets(5)); + cell.setAlignment(Pos.TOP_LEFT); + cell.setStyle("-fx-background-color: #ecf0f1; -fx-border-color: #bdc3c7; -fx-border-width: 1;"); + + // Find all exams at this day/slot + List examsAtSlot = new ArrayList<>(); + for (ExamAssignment assignment : schedule.getAssignments().values()) { + if (assignment.isAssigned() && + assignment.getDay() == day && + assignment.getTimeSlotIndex() == slot) { + examsAtSlot.add(assignment); + } + } + + if (examsAtSlot.isEmpty()) { + Label emptyLabel = new Label("-"); + emptyLabel.setStyle("-fx-text-fill: #95a5a6;"); + cell.getChildren().add(emptyLabel); + } else { + // Sort by classroom + examsAtSlot.sort(Comparator.comparing(ExamAssignment::getClassroomId)); + + for (ExamAssignment exam : examsAtSlot) { + HBox examBox = new HBox(5); + examBox.setAlignment(Pos.CENTER_LEFT); + examBox.setStyle("-fx-background-color: #3498db; -fx-background-radius: 3; -fx-padding: 3 6;"); + + Label courseLabel = new Label(exam.getCourseCode()); + courseLabel.setStyle("-fx-text-fill: white; -fx-font-weight: bold; -fx-font-size: 11;"); + + Label roomLabel = new Label("(" + exam.getClassroomId() + ")"); + roomLabel.setStyle("-fx-text-fill: #d4e6f1; -fx-font-size: 10;"); + + examBox.getChildren().addAll(courseLabel, roomLabel); + cell.getChildren().add(examBox); + } + + // Change cell color based on load + if (examsAtSlot.size() >= 3) { + cell.setStyle("-fx-background-color: #fadbd8; -fx-border-color: #e74c3c; -fx-border-width: 1;"); + } else if (examsAtSlot.size() >= 2) { + cell.setStyle("-fx-background-color: #fcf3cf; -fx-border-color: #f39c12; -fx-border-width: 1;"); + } + } + + return cell; + } + + @FXML + private void onCancelGeneration() { + if (generatorService != null) { + generatorService.cancel(); + } + + cancelButton.setDisable(true); + progressLabel.setText("Cancelling..."); + } + + private void showAlert(Alert.AlertType type, String title, String message) { + Alert alert = new Alert(type); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); alert.showAndWait(); } } diff --git a/src/main/java/org/example/se302/service/ConstraintValidator.java b/src/main/java/org/example/se302/service/ConstraintValidator.java index 46946cb..abbd03d 100644 --- a/src/main/java/org/example/se302/service/ConstraintValidator.java +++ b/src/main/java/org/example/se302/service/ConstraintValidator.java @@ -35,8 +35,19 @@ public class ConstraintValidator { /** * Validate a single assignment against the current schedule state. + * Uses default strict validation (no back-to-back allowed). */ public ValidationResult validateAssignment(ExamAssignment assignment, ScheduleState scheduleState) { + return validateAssignment(assignment, scheduleState, false); + } + + /** + * Validate a single assignment against the current schedule state. + * + * @param allowBackToBack If true, consecutive exams are allowed + */ + public ValidationResult validateAssignment(ExamAssignment assignment, ScheduleState scheduleState, + boolean allowBackToBack) { ValidationResult result = new ValidationResult(); if (!assignment.isAssigned()) { @@ -48,23 +59,74 @@ public class ConstraintValidator { ValidationResult capacityResult = checkClassroomCapacity(assignment); result.merge(capacityResult); - // Check no double-booking + // Check no double-booking (always required) ValidationResult doubleBookingResult = checkNoDoubleBooking(assignment, scheduleState); result.merge(doubleBookingResult); - // Check student constraints + // Check student constraints - only check same time slot conflicts (always + // required) Course course = dataManager.getCourse(assignment.getCourseCode()); if (course != null) { - for (String studentId : course.getEnrolledStudents()) { - // Check no consecutive exams - ValidationResult consecutiveResult = checkNoConsecutiveExams( - studentId, assignment, scheduleState); - result.merge(consecutiveResult); + // Check if student has another exam at the SAME time slot (hard constraint) + ValidationResult sameTimeResult = checkNoSameTimeExams(assignment, scheduleState, course); + result.merge(sameTimeResult); - // Check max 2 exams per day - ValidationResult maxPerDayResult = checkMaxTwoExamsPerDay( - studentId, assignment, scheduleState); - result.merge(maxPerDayResult); + // Only check consecutive and max per day if back-to-back is NOT allowed + if (!allowBackToBack) { + for (String studentId : course.getEnrolledStudents()) { + // Check no consecutive exams (soft - skip if allowBackToBack) + ValidationResult consecutiveResult = checkNoConsecutiveExams( + studentId, assignment, scheduleState); + result.merge(consecutiveResult); + + // Check max 2 exams per day + ValidationResult maxPerDayResult = checkMaxTwoExamsPerDay( + studentId, assignment, scheduleState); + result.merge(maxPerDayResult); + + // If already invalid, no need to check more students + if (!result.isValid()) { + break; + } + } + } + } + + return result; + } + + /** + * Check that no student has two exams at the exact same time slot. + * This is a hard constraint that must always be satisfied. + */ + private ValidationResult checkNoSameTimeExams(ExamAssignment newAssignment, + ScheduleState scheduleState, + Course course) { + ValidationResult result = new ValidationResult(); + + int newDay = newAssignment.getDay(); + int newSlot = newAssignment.getTimeSlotIndex(); + + for (ExamAssignment existing : scheduleState.getAssignments().values()) { + if (!existing.isAssigned() || existing.getCourseCode().equals(newAssignment.getCourseCode())) { + continue; + } + + // Check if same day and slot + if (existing.getDay() == newDay && existing.getTimeSlotIndex() == newSlot) { + // Check if any student is enrolled in both courses + Course otherCourse = dataManager.getCourse(existing.getCourseCode()); + if (otherCourse != null) { + for (String studentId : course.getEnrolledStudents()) { + if (otherCourse.getEnrolledStudents().contains(studentId)) { + result.addError(String.format( + "Student %s has two exams at the same time: %s and %s (Day %d, Slot %d)", + studentId, newAssignment.getCourseCode(), existing.getCourseCode(), + newDay + 1, newSlot + 1)); + return result; // One conflict is enough + } + } + } } } @@ -111,10 +173,10 @@ public class ConstraintValidator { // Check if any other assignment uses the same classroom at the same time for (ExamAssignment existing : scheduleState.getAssignments().values()) { if (existing.isAssigned() && - !existing.getCourseCode().equals(assignment.getCourseCode()) && - existing.getClassroomId().equals(assignment.getClassroomId()) && - existing.getDay() == assignment.getDay() && - existing.getTimeSlotIndex() == assignment.getTimeSlotIndex()) { + !existing.getCourseCode().equals(assignment.getCourseCode()) && + existing.getClassroomId().equals(assignment.getClassroomId()) && + existing.getDay() == assignment.getDay() && + existing.getTimeSlotIndex() == assignment.getTimeSlotIndex()) { result.addError(String.format( "Classroom double-booking: %s already has %s at Day %d, Slot %d", @@ -131,11 +193,12 @@ public class ConstraintValidator { /** * Check that a student has no consecutive exams. - * Consecutive means exams in adjacent time slots on the same day (back-to-back). + * Consecutive means exams in adjacent time slots on the same day + * (back-to-back). */ public ValidationResult checkNoConsecutiveExams(String studentId, - ExamAssignment newAssignment, - ScheduleState scheduleState) { + ExamAssignment newAssignment, + ScheduleState scheduleState) { ValidationResult result = new ValidationResult(); // Get all courses this student is enrolled in @@ -161,7 +224,7 @@ public class ConstraintValidator { // Check if consecutive (adjacent slots on the same day) boolean isConsecutive = (existingDay == newDay) && - (Math.abs(existingSlot - newSlot) == 1); + (Math.abs(existingSlot - newSlot) == 1); if (isConsecutive) { result.addError(String.format( @@ -180,8 +243,8 @@ public class ConstraintValidator { * Check that a student has at most 2 exams per day. */ public ValidationResult checkMaxTwoExamsPerDay(String studentId, - ExamAssignment newAssignment, - ScheduleState scheduleState) { + ExamAssignment newAssignment, + ScheduleState scheduleState) { ValidationResult result = new ValidationResult(); // Get all courses this student is enrolled in diff --git a/src/main/java/org/example/se302/service/ScheduleGeneratorService.java b/src/main/java/org/example/se302/service/ScheduleGeneratorService.java index 07d6391..c36df21 100644 --- a/src/main/java/org/example/se302/service/ScheduleGeneratorService.java +++ b/src/main/java/org/example/se302/service/ScheduleGeneratorService.java @@ -6,7 +6,8 @@ import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; /** - * Service for generating exam schedules using Constraint Satisfaction Problem (CSP) solving. + * Service for generating exam schedules using Constraint Satisfaction Problem + * (CSP) solving. * Implements backtracking algorithm with MRV and LCV heuristics. * Works with the day/timeSlotIndex based ExamAssignment architecture. */ @@ -57,7 +58,8 @@ public class ScheduleGeneratorService { updateProgress(coursesToSchedule.size(), coursesToSchedule.size(), "Schedule generated successfully!"); return ScheduleResult.success(scheduleState); } else { - return ScheduleResult.failure("No valid schedule found. Try increasing days/slots or relaxing constraints."); + return ScheduleResult + .failure("No valid schedule found. Try increasing days/slots or relaxing constraints."); } } @@ -83,7 +85,8 @@ public class ScheduleGeneratorService { /** * Backtracking algorithm core. */ - private boolean backtrack(ScheduleState scheduleState, List courses, int courseIndex, ScheduleConfiguration config) { + private boolean backtrack(ScheduleState scheduleState, List courses, int courseIndex, + ScheduleConfiguration config) { // Check cancellation if (cancelled.get()) { return false; @@ -122,9 +125,9 @@ public class ScheduleGeneratorService { assignment.setTimeSlotIndex(timeSlot.slot); assignment.setClassroomId(classroom.getClassroomId()); - // Validate assignment - ConstraintValidator.ValidationResult validationResult = - validator.validateAssignment(assignment, scheduleState); + // Validate assignment - pass allowBackToBack from config + ConstraintValidator.ValidationResult validationResult = validator.validateAssignment(assignment, + scheduleState, config.isAllowBackToBackExams()); if (validationResult.isValid()) { // Assignment is valid, try to assign remaining courses @@ -154,8 +157,7 @@ public class ScheduleGeneratorService { // More students = more constrained courses.sort((c1, c2) -> Integer.compare( c2.getEnrolledStudentsCount(), - c1.getEnrolledStudentsCount() - )); + c1.getEnrolledStudentsCount())); return courses; } @@ -181,14 +183,16 @@ public class ScheduleGeneratorService { break; case BALANCED_DISTRIBUTION: - // Round-robin across days: day 0 slot 0, day 1 slot 0, day 2 slot 0, ... day 0 slot 1, ... + // Round-robin across days: day 0 slot 0, day 1 slot 0, day 2 slot 0, ... day 0 + // slot 1, ... timeSlots.sort(Comparator.comparingInt((DaySlotPair p) -> p.slot) .thenComparingInt(p -> p.day)); break; case STUDENT_FRIENDLY: // Try to space out exams - prefer later slots on same day to avoid consecutive - // (This is a simple heuristic - more sophisticated would track student conflicts) + // (This is a simple heuristic - more sophisticated would track student + // conflicts) break; default: @@ -204,10 +208,10 @@ public class ScheduleGeneratorService { * ordered according to optimization strategy. */ private List getSuitableClassroomsOrdered(Course course, - int day, - int timeSlotIndex, - ScheduleState scheduleState, - ScheduleConfiguration config) { + int day, + int timeSlotIndex, + ScheduleState scheduleState, + ScheduleConfiguration config) { List suitable = new ArrayList<>(); for (Classroom classroom : scheduleState.getAvailableClassrooms()) { @@ -220,9 +224,9 @@ public class ScheduleGeneratorService { boolean isAvailable = true; for (ExamAssignment assignment : scheduleState.getAssignments().values()) { if (assignment.isAssigned() && - assignment.getClassroomId().equals(classroom.getClassroomId()) && - assignment.getDay() == day && - assignment.getTimeSlotIndex() == timeSlotIndex) { + assignment.getClassroomId().equals(classroom.getClassroomId()) && + assignment.getDay() == day && + assignment.getTimeSlotIndex() == timeSlotIndex) { isAvailable = false; break; } @@ -320,8 +324,9 @@ public class ScheduleGeneratorService { public interface ProgressListener { /** * Called when progress updates. + * * @param progress Value between 0.0 and 1.0 - * @param message Current status message + * @param message Current status message */ void onProgress(double progress, String message); } diff --git a/src/main/resources/org/example/se302/view/schedule-calendar-view.fxml b/src/main/resources/org/example/se302/view/schedule-calendar-view.fxml index 50d5c28..1df0dab 100644 --- a/src/main/resources/org/example/se302/view/schedule-calendar-view.fxml +++ b/src/main/resources/org/example/se302/view/schedule-calendar-view.fxml @@ -4,64 +4,153 @@ - - - - - - - -