mirror of
https://github.com/sabazadam/Se302.git
synced 2025-12-31 12:21:22 +00:00
introduce exam scheduling feature with CSP algorithm and calendar UI
This commit is contained in:
@@ -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<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
|
||||
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
|
||||
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<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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,15 +59,22 @@ 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) {
|
||||
// Check if student has another exam at the SAME time slot (hard constraint)
|
||||
ValidationResult sameTimeResult = checkNoSameTimeExams(assignment, scheduleState, course);
|
||||
result.merge(sameTimeResult);
|
||||
|
||||
// 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
|
||||
// Check no consecutive exams (soft - skip if allowBackToBack)
|
||||
ValidationResult consecutiveResult = checkNoConsecutiveExams(
|
||||
studentId, assignment, scheduleState);
|
||||
result.merge(consecutiveResult);
|
||||
@@ -65,6 +83,50 @@ public class ConstraintValidator {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +193,8 @@ 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,
|
||||
|
||||
@@ -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<Course> courses, int courseIndex, ScheduleConfiguration config) {
|
||||
private boolean backtrack(ScheduleState scheduleState, List<Course> 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:
|
||||
@@ -320,6 +324,7 @@ public class ScheduleGeneratorService {
|
||||
public interface ProgressListener {
|
||||
/**
|
||||
* Called when progress updates.
|
||||
*
|
||||
* @param progress Value between 0.0 and 1.0
|
||||
* @param message Current status message
|
||||
*/
|
||||
|
||||
@@ -4,64 +4,153 @@
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
|
||||
<BorderPane xmlns="http://javafx.com/javafx"
|
||||
<ScrollPane xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
fx:controller="org.example.se302.controller.ScheduleCalendarController"
|
||||
styleClass="content-pane">
|
||||
fitToWidth="true" fitToHeight="true"
|
||||
hbarPolicy="AS_NEEDED" vbarPolicy="AS_NEEDED">
|
||||
|
||||
<BorderPane styleClass="content-pane">
|
||||
<top>
|
||||
<VBox spacing="10">
|
||||
<VBox spacing="15">
|
||||
<padding>
|
||||
<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"/>
|
||||
|
||||
<!-- Page Title -->
|
||||
<Label text="📅 Calendar View - Exam Schedule Generator" styleClass="section-title"/>
|
||||
<Label text="Configure and generate an optimized exam schedule using CSP algorithm."
|
||||
wrapText="true" styleClass="description-label"/>
|
||||
|
||||
<Separator/>
|
||||
|
||||
<!-- Configuration Panel -->
|
||||
<VBox spacing="15" styleClass="config-panel">
|
||||
<Label text="Schedule Configuration" styleClass="subsection-title"/>
|
||||
|
||||
<!-- Row 1: Days and Slots -->
|
||||
<HBox spacing="20" alignment="CENTER_LEFT">
|
||||
<VBox spacing="5">
|
||||
<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="10" alignment="CENTER">
|
||||
<VBox spacing="15">
|
||||
<padding>
|
||||
<Insets top="50" right="20" bottom="50" left="20"/>
|
||||
<Insets top="20" right="20" bottom="20" 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">
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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 -->
|
||||
<Label text="09:00" GridPane.columnIndex="0" GridPane.rowIndex="1" styleClass="grid-header"/>
|
||||
<Label text="-" GridPane.columnIndex="1" GridPane.rowIndex="1" styleClass="grid-cell"/>
|
||||
<Label text="-" GridPane.columnIndex="2" GridPane.rowIndex="1" styleClass="grid-cell"/>
|
||||
<Label text="-" GridPane.columnIndex="3" GridPane.rowIndex="1" styleClass="grid-cell"/>
|
||||
|
||||
<Label text="11:00" GridPane.columnIndex="0" GridPane.rowIndex="2" styleClass="grid-header"/>
|
||||
<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"/>
|
||||
<Label text="-" GridPane.columnIndex="1" GridPane.rowIndex="3" styleClass="grid-cell"/>
|
||||
<Label text="-" GridPane.columnIndex="2" GridPane.rowIndex="3" styleClass="grid-cell"/>
|
||||
<Label text="-" GridPane.columnIndex="3" GridPane.rowIndex="3" styleClass="grid-cell"/>
|
||||
<!-- 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>
|
||||
|
||||
Reference in New Issue
Block a user