introduce exam scheduling feature with CSP algorithm and calendar UI

This commit is contained in:
feyzagereme
2025-12-14 18:12:30 +03:00
parent 57b80a63b2
commit dbdb46e5de
4 changed files with 709 additions and 110 deletions

View File

@@ -1,33 +1,475 @@
package org.example.se302.controller; package org.example.se302.controller;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Alert; import javafx.geometry.Insets;
import javafx.scene.control.DatePicker; 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. * Controller for the Calendar/Day schedule view.
* Handles schedule configuration and generation using CSP algorithm.
*/ */
public class ScheduleCalendarController { public class ScheduleCalendarController {
@FXML private DatePicker startDatePicker; // Configuration controls
@FXML private DatePicker endDatePicker; @FXML
private Spinner<Integer> numDaysSpinner;
@FXML
private Spinner<Integer> slotsPerDaySpinner;
@FXML
private DatePicker startDatePicker;
@FXML
private Spinner<Integer> slotDurationSpinner;
@FXML
private ComboBox<String> strategyComboBox;
@FXML
private ComboBox<String> 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 @FXML
public void initialize() { 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<Integer> daysFactory = new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 30, 5);
numDaysSpinner.setValueFactory(daysFactory);
// Slots per day spinner (1-10)
SpinnerValueFactory<Integer> slotsFactory = new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 10, 4);
slotsPerDaySpinner.setValueFactory(slotsFactory);
// Slot duration spinner (30-240 minutes)
SpinnerValueFactory<Integer> durationFactory = new SpinnerValueFactory.IntegerSpinnerValueFactory(30, 240, 120,
30);
slotDurationSpinner.setValueFactory(durationFactory);
}
private void initializeComboBoxes() {
// Optimization strategies
List<String> 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<String> 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 @FXML
private void onGenerateSchedule() { private void onGenerateSchedule() {
Alert alert = new Alert(Alert.AlertType.INFORMATION); // Validate data
alert.setTitle("Coming Soon"); if (dataManager.getTotalCourses() == 0) {
alert.setHeaderText("Schedule Generation"); showAlert(Alert.AlertType.WARNING, "No Data",
alert.setContentText("The CSP-based schedule generation algorithm will be implemented in Phase 2.\n\n" + "Please import course data before generating a schedule.");
"It will automatically create an exam schedule that satisfies all constraints:\n" + return;
"- No consecutive exams for students\n" + }
"- Max 2 exams per day per student\n" +
"- Classroom capacity limits\n" + if (dataManager.getTotalClassrooms() == 0) {
"- No double-booking"); 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<String> 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<ExamAssignment> 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(); alert.showAndWait();
} }
} }

View File

@@ -35,8 +35,19 @@ public class ConstraintValidator {
/** /**
* Validate a single assignment against the current schedule state. * 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) { 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(); ValidationResult result = new ValidationResult();
if (!assignment.isAssigned()) { if (!assignment.isAssigned()) {
@@ -48,23 +59,74 @@ public class ConstraintValidator {
ValidationResult capacityResult = checkClassroomCapacity(assignment); ValidationResult capacityResult = checkClassroomCapacity(assignment);
result.merge(capacityResult); result.merge(capacityResult);
// Check no double-booking // Check no double-booking (always required)
ValidationResult doubleBookingResult = checkNoDoubleBooking(assignment, scheduleState); ValidationResult doubleBookingResult = checkNoDoubleBooking(assignment, scheduleState);
result.merge(doubleBookingResult); result.merge(doubleBookingResult);
// Check student constraints // Check student constraints - only check same time slot conflicts (always
// required)
Course course = dataManager.getCourse(assignment.getCourseCode()); Course course = dataManager.getCourse(assignment.getCourseCode());
if (course != null) { if (course != null) {
for (String studentId : course.getEnrolledStudents()) { // Check if student has another exam at the SAME time slot (hard constraint)
// Check no consecutive exams ValidationResult sameTimeResult = checkNoSameTimeExams(assignment, scheduleState, course);
ValidationResult consecutiveResult = checkNoConsecutiveExams( result.merge(sameTimeResult);
studentId, assignment, scheduleState);
result.merge(consecutiveResult);
// Check max 2 exams per day // Only check consecutive and max per day if back-to-back is NOT allowed
ValidationResult maxPerDayResult = checkMaxTwoExamsPerDay( if (!allowBackToBack) {
studentId, assignment, scheduleState); for (String studentId : course.getEnrolledStudents()) {
result.merge(maxPerDayResult); // 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 // Check if any other assignment uses the same classroom at the same time
for (ExamAssignment existing : scheduleState.getAssignments().values()) { for (ExamAssignment existing : scheduleState.getAssignments().values()) {
if (existing.isAssigned() && if (existing.isAssigned() &&
!existing.getCourseCode().equals(assignment.getCourseCode()) && !existing.getCourseCode().equals(assignment.getCourseCode()) &&
existing.getClassroomId().equals(assignment.getClassroomId()) && existing.getClassroomId().equals(assignment.getClassroomId()) &&
existing.getDay() == assignment.getDay() && existing.getDay() == assignment.getDay() &&
existing.getTimeSlotIndex() == assignment.getTimeSlotIndex()) { existing.getTimeSlotIndex() == assignment.getTimeSlotIndex()) {
result.addError(String.format( result.addError(String.format(
"Classroom double-booking: %s already has %s at Day %d, Slot %d", "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. * 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, public ValidationResult checkNoConsecutiveExams(String studentId,
ExamAssignment newAssignment, ExamAssignment newAssignment,
ScheduleState scheduleState) { ScheduleState scheduleState) {
ValidationResult result = new ValidationResult(); ValidationResult result = new ValidationResult();
// Get all courses this student is enrolled in // Get all courses this student is enrolled in
@@ -161,7 +224,7 @@ public class ConstraintValidator {
// Check if consecutive (adjacent slots on the same day) // Check if consecutive (adjacent slots on the same day)
boolean isConsecutive = (existingDay == newDay) && boolean isConsecutive = (existingDay == newDay) &&
(Math.abs(existingSlot - newSlot) == 1); (Math.abs(existingSlot - newSlot) == 1);
if (isConsecutive) { if (isConsecutive) {
result.addError(String.format( result.addError(String.format(
@@ -180,8 +243,8 @@ public class ConstraintValidator {
* Check that a student has at most 2 exams per day. * Check that a student has at most 2 exams per day.
*/ */
public ValidationResult checkMaxTwoExamsPerDay(String studentId, public ValidationResult checkMaxTwoExamsPerDay(String studentId,
ExamAssignment newAssignment, ExamAssignment newAssignment,
ScheduleState scheduleState) { ScheduleState scheduleState) {
ValidationResult result = new ValidationResult(); ValidationResult result = new ValidationResult();
// Get all courses this student is enrolled in // Get all courses this student is enrolled in

View File

@@ -6,7 +6,8 @@ import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean; 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. * Implements backtracking algorithm with MRV and LCV heuristics.
* Works with the day/timeSlotIndex based ExamAssignment architecture. * Works with the day/timeSlotIndex based ExamAssignment architecture.
*/ */
@@ -57,7 +58,8 @@ public class ScheduleGeneratorService {
updateProgress(coursesToSchedule.size(), coursesToSchedule.size(), "Schedule generated successfully!"); updateProgress(coursesToSchedule.size(), coursesToSchedule.size(), "Schedule generated successfully!");
return ScheduleResult.success(scheduleState); return ScheduleResult.success(scheduleState);
} else { } 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. * Backtracking algorithm core.
*/ */
private boolean backtrack(ScheduleState scheduleState, List<Course> courses, int courseIndex, ScheduleConfiguration config) { private boolean backtrack(ScheduleState scheduleState, List<Course> courses, int courseIndex,
ScheduleConfiguration config) {
// Check cancellation // Check cancellation
if (cancelled.get()) { if (cancelled.get()) {
return false; return false;
@@ -122,9 +125,9 @@ public class ScheduleGeneratorService {
assignment.setTimeSlotIndex(timeSlot.slot); assignment.setTimeSlotIndex(timeSlot.slot);
assignment.setClassroomId(classroom.getClassroomId()); assignment.setClassroomId(classroom.getClassroomId());
// Validate assignment // Validate assignment - pass allowBackToBack from config
ConstraintValidator.ValidationResult validationResult = ConstraintValidator.ValidationResult validationResult = validator.validateAssignment(assignment,
validator.validateAssignment(assignment, scheduleState); scheduleState, config.isAllowBackToBackExams());
if (validationResult.isValid()) { if (validationResult.isValid()) {
// Assignment is valid, try to assign remaining courses // Assignment is valid, try to assign remaining courses
@@ -154,8 +157,7 @@ public class ScheduleGeneratorService {
// More students = more constrained // More students = more constrained
courses.sort((c1, c2) -> Integer.compare( courses.sort((c1, c2) -> Integer.compare(
c2.getEnrolledStudentsCount(), c2.getEnrolledStudentsCount(),
c1.getEnrolledStudentsCount() c1.getEnrolledStudentsCount()));
));
return courses; return courses;
} }
@@ -181,14 +183,16 @@ public class ScheduleGeneratorService {
break; break;
case BALANCED_DISTRIBUTION: 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) timeSlots.sort(Comparator.comparingInt((DaySlotPair p) -> p.slot)
.thenComparingInt(p -> p.day)); .thenComparingInt(p -> p.day));
break; break;
case STUDENT_FRIENDLY: case STUDENT_FRIENDLY:
// Try to space out exams - prefer later slots on same day to avoid consecutive // 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; break;
default: default:
@@ -204,10 +208,10 @@ public class ScheduleGeneratorService {
* ordered according to optimization strategy. * ordered according to optimization strategy.
*/ */
private List<Classroom> getSuitableClassroomsOrdered(Course course, private List<Classroom> getSuitableClassroomsOrdered(Course course,
int day, int day,
int timeSlotIndex, int timeSlotIndex,
ScheduleState scheduleState, ScheduleState scheduleState,
ScheduleConfiguration config) { ScheduleConfiguration config) {
List<Classroom> suitable = new ArrayList<>(); List<Classroom> suitable = new ArrayList<>();
for (Classroom classroom : scheduleState.getAvailableClassrooms()) { for (Classroom classroom : scheduleState.getAvailableClassrooms()) {
@@ -220,9 +224,9 @@ public class ScheduleGeneratorService {
boolean isAvailable = true; boolean isAvailable = true;
for (ExamAssignment assignment : scheduleState.getAssignments().values()) { for (ExamAssignment assignment : scheduleState.getAssignments().values()) {
if (assignment.isAssigned() && if (assignment.isAssigned() &&
assignment.getClassroomId().equals(classroom.getClassroomId()) && assignment.getClassroomId().equals(classroom.getClassroomId()) &&
assignment.getDay() == day && assignment.getDay() == day &&
assignment.getTimeSlotIndex() == timeSlotIndex) { assignment.getTimeSlotIndex() == timeSlotIndex) {
isAvailable = false; isAvailable = false;
break; break;
} }
@@ -320,8 +324,9 @@ public class ScheduleGeneratorService {
public interface ProgressListener { public interface ProgressListener {
/** /**
* Called when progress updates. * Called when progress updates.
*
* @param progress Value between 0.0 and 1.0 * @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); void onProgress(double progress, String message);
} }

View File

@@ -4,64 +4,153 @@
<?import javafx.scene.control.*?> <?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<BorderPane xmlns="http://javafx.com/javafx" <ScrollPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
fx:controller="org.example.se302.controller.ScheduleCalendarController" fx:controller="org.example.se302.controller.ScheduleCalendarController"
styleClass="content-pane"> fitToWidth="true" fitToHeight="true"
hbarPolicy="AS_NEEDED" vbarPolicy="AS_NEEDED">
<top> <BorderPane styleClass="content-pane">
<VBox spacing="10"> <top>
<padding> <VBox spacing="15">
<Insets top="20" right="20" bottom="10" left="20"/>
</padding>
<Label text="Calendar View - Exam Schedule" styleClass="section-title"/>
<HBox spacing="10" alignment="CENTER_LEFT">
<Label text="Exam Period:"/>
<DatePicker fx:id="startDatePicker" promptText="Start Date"/>
<Label text="to"/>
<DatePicker fx:id="endDatePicker" promptText="End Date"/>
<Button text="Generate Schedule" onAction="#onGenerateSchedule" styleClass="primary-button"/>
</HBox>
</VBox>
</top>
<center>
<VBox spacing="10" alignment="CENTER">
<padding>
<Insets top="50" right="20" bottom="50" left="20"/>
</padding>
<Label text="📅" style="-fx-font-size: 48px;"/>
<Label text="Schedule Generation Coming Soon" styleClass="info-title"/>
<Label text="The calendar grid will display exam schedules across days and time slots."
wrapText="true" textAlignment="CENTER" maxWidth="500"/>
<Separator prefWidth="300"/>
<Label text="Preview: Grid Layout" styleClass="subsection-title"/>
<GridPane gridLinesVisible="true" styleClass="schedule-grid">
<padding> <padding>
<Insets top="10" right="10" bottom="10" left="10"/> <Insets top="20" right="20" bottom="10" left="20"/>
</padding> </padding>
<!-- Header row -->
<Label text="" GridPane.columnIndex="0" GridPane.rowIndex="0" styleClass="grid-header"/>
<Label text="Day 1" GridPane.columnIndex="1" GridPane.rowIndex="0" styleClass="grid-header"/>
<Label text="Day 2" GridPane.columnIndex="2" GridPane.rowIndex="0" styleClass="grid-header"/>
<Label text="Day 3" GridPane.columnIndex="3" GridPane.rowIndex="0" styleClass="grid-header"/>
<!-- Time slots --> <!-- Page Title -->
<Label text="09:00" GridPane.columnIndex="0" GridPane.rowIndex="1" styleClass="grid-header"/> <Label text="📅 Calendar View - Exam Schedule Generator" styleClass="section-title"/>
<Label text="-" GridPane.columnIndex="1" GridPane.rowIndex="1" styleClass="grid-cell"/> <Label text="Configure and generate an optimized exam schedule using CSP algorithm."
<Label text="-" GridPane.columnIndex="2" GridPane.rowIndex="1" styleClass="grid-cell"/> wrapText="true" styleClass="description-label"/>
<Label text="-" GridPane.columnIndex="3" GridPane.rowIndex="1" styleClass="grid-cell"/>
<Label text="11:00" GridPane.columnIndex="0" GridPane.rowIndex="2" styleClass="grid-header"/> <Separator/>
<Label text="-" GridPane.columnIndex="1" GridPane.rowIndex="2" styleClass="grid-cell"/>
<Label text="-" GridPane.columnIndex="2" GridPane.rowIndex="2" styleClass="grid-cell"/>
<Label text="-" GridPane.columnIndex="3" GridPane.rowIndex="2" styleClass="grid-cell"/>
<Label text="14:00" GridPane.columnIndex="0" GridPane.rowIndex="3" styleClass="grid-header"/> <!-- Configuration Panel -->
<Label text="-" GridPane.columnIndex="1" GridPane.rowIndex="3" styleClass="grid-cell"/> <VBox spacing="15" styleClass="config-panel">
<Label text="-" GridPane.columnIndex="2" GridPane.rowIndex="3" styleClass="grid-cell"/> <Label text="Schedule Configuration" styleClass="subsection-title"/>
<Label text="-" GridPane.columnIndex="3" GridPane.rowIndex="3" styleClass="grid-cell"/>
</GridPane> <!-- Row 1: Days and Slots -->
</VBox> <HBox spacing="20" alignment="CENTER_LEFT">
</center> <VBox spacing="5">
</BorderPane> <Label text="Number of Days:"/>
<Spinner fx:id="numDaysSpinner" min="1" max="30" initialValue="5"
editable="true" prefWidth="100"/>
</VBox>
<VBox spacing="5">
<Label text="Slots per Day:"/>
<Spinner fx:id="slotsPerDaySpinner" min="1" max="10" initialValue="4"
editable="true" prefWidth="100"/>
</VBox>
<VBox spacing="5">
<Label text="Start Date:"/>
<DatePicker fx:id="startDatePicker" promptText="Select Start Date" prefWidth="150"/>
</VBox>
<VBox spacing="5">
<Label text="Slot Duration (min):"/>
<Spinner fx:id="slotDurationSpinner" min="30" max="240" initialValue="120"
editable="true" prefWidth="100"/>
</VBox>
</HBox>
<!-- Row 2: Strategy and Options -->
<HBox spacing="20" alignment="CENTER_LEFT">
<VBox spacing="5">
<Label text="Optimization Strategy:"/>
<ComboBox fx:id="strategyComboBox" prefWidth="200" promptText="Select Strategy"/>
</VBox>
<VBox spacing="5">
<Label text="Day Start Time:"/>
<ComboBox fx:id="startTimeComboBox" prefWidth="120" promptText="09:00"/>
</VBox>
<VBox spacing="5" alignment="CENTER_LEFT">
<padding>
<Insets top="18"/>
</padding>
<CheckBox fx:id="allowBackToBackCheckBox" text="Allow back-to-back exams" selected="true"/>
</VBox>
</HBox>
<!-- Row 3: Summary -->
<HBox spacing="10" alignment="CENTER_LEFT" styleClass="summary-row">
<Label text="Configuration Summary:"/>
<Label fx:id="summaryLabel" text="5 days × 4 slots = 20 total time slots"
styleClass="summary-value"/>
</HBox>
</VBox>
<Separator/>
<!-- Action Buttons -->
<HBox spacing="15" alignment="CENTER_LEFT">
<Button fx:id="generateButton" text="🚀 Generate Schedule"
onAction="#onGenerateSchedule" styleClass="primary-button" prefWidth="180"/>
<Button fx:id="cancelButton" text="Cancel" onAction="#onCancelGeneration"
disable="true" prefWidth="100"/>
<Region HBox.hgrow="ALWAYS"/>
<Label fx:id="statusLabel" text="Ready" styleClass="status-label"/>
</HBox>
<!-- Progress Bar -->
<VBox fx:id="progressContainer" spacing="5" visible="false" managed="false">
<ProgressBar fx:id="progressBar" prefWidth="400" progress="0"/>
<Label fx:id="progressLabel" text="Initializing..."/>
</VBox>
</VBox>
</top>
<center>
<VBox spacing="15">
<padding>
<Insets top="20" right="20" bottom="20" left="20"/>
</padding>
<!-- Schedule Grid Container -->
<Label text="Generated Schedule" styleClass="subsection-title"/>
<ScrollPane fx:id="scheduleScrollPane" fitToWidth="true" fitToHeight="false"
prefHeight="500" minHeight="300"
hbarPolicy="ALWAYS" vbarPolicy="ALWAYS"
VBox.vgrow="ALWAYS"
style="-fx-background-color: white; -fx-border-color: #bdc3c7; -fx-border-width: 1;">
<GridPane fx:id="scheduleGrid" gridLinesVisible="true" styleClass="schedule-grid"
minWidth="800" minHeight="400">
<padding>
<Insets top="10" right="10" bottom="10" left="10"/>
</padding>
<!-- Grid will be populated dynamically -->
<Label text="Click 'Generate Schedule' to create an exam schedule"
GridPane.columnIndex="0" GridPane.rowIndex="0"
styleClass="placeholder-text" wrapText="true"/>
</GridPane>
</ScrollPane>
<!-- Statistics Panel -->
<HBox spacing="30" alignment="CENTER_LEFT" styleClass="stats-panel">
<VBox spacing="3" alignment="CENTER">
<Label fx:id="totalCoursesLabel" text="0" styleClass="stat-value"/>
<Label text="Total Courses" styleClass="stat-label"/>
</VBox>
<Separator orientation="VERTICAL"/>
<VBox spacing="3" alignment="CENTER">
<Label fx:id="scheduledCoursesLabel" text="0" styleClass="stat-value"/>
<Label text="Scheduled" styleClass="stat-label"/>
</VBox>
<Separator orientation="VERTICAL"/>
<VBox spacing="3" alignment="CENTER">
<Label fx:id="classroomsUsedLabel" text="0" styleClass="stat-value"/>
<Label text="Classrooms Used" styleClass="stat-label"/>
</VBox>
<Separator orientation="VERTICAL"/>
<VBox spacing="3" alignment="CENTER">
<Label fx:id="generationTimeLabel" text="-" styleClass="stat-value"/>
<Label text="Generation Time" styleClass="stat-label"/>
</VBox>
</HBox>
</VBox>
</center>
</BorderPane>
</ScrollPane>