From 84391e98658203bfe12b395214ab0532061cca7e Mon Sep 17 00:00:00 2001 From: Giang Vu Date: Sun, 1 Sep 2024 22:31:45 +1000 Subject: [PATCH 01/40] feat: add course information spreadsheet input page --- frontend/src/components/Sidebar.tsx | 10 ++++++++++ frontend/src/pages/spreadsheets/Course.tsx | 11 +++++++++++ frontend/src/routes.tsx | 2 ++ 3 files changed, 23 insertions(+) create mode 100644 frontend/src/pages/spreadsheets/Course.tsx diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index b5ec512..a51587f 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -59,6 +59,15 @@ export default function Sidebar({ marginTop, width }: SidebarProps) { + + + + + + + + + @@ -67,6 +76,7 @@ export default function Sidebar({ marginTop, width }: SidebarProps) { + ); diff --git a/frontend/src/pages/spreadsheets/Course.tsx b/frontend/src/pages/spreadsheets/Course.tsx new file mode 100644 index 0000000..a343adf --- /dev/null +++ b/frontend/src/pages/spreadsheets/Course.tsx @@ -0,0 +1,11 @@ +import Spreadsheet from '../../components/Spreadsheet.tsx' + +export default function Course() { + + return ( + <> +

Course

+ + + ); +}; \ No newline at end of file diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index fdfba8e..a0f2238 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -6,6 +6,7 @@ import Room from './pages/spreadsheets/Room.tsx' import Unit from './pages/spreadsheets/Unit.tsx' import Download from './pages/Download.tsx' import Enrolment from './pages/Enrolment.tsx' +import Course from './pages/spreadsheets/Course.tsx' const routes = [ @@ -20,6 +21,7 @@ const routes = [ { path: "campus", element: }, { path: "building", element: }, { path: "room", element: }, + { path: "course", element: }, { path: "unit", element: }, ], }, From cef0c5877117e4580bfc3b30a00b95d30c2a9794 Mon Sep 17 00:00:00 2001 From: Eden Xu Date: Fri, 6 Sep 2024 21:47:11 +1000 Subject: [PATCH 02/40] feat(database): connect Quarkus with Azure PostgreSQL database, create Campus entity --- backend/pom.xml | 8 +++++ .../acme/schooltimetabling/domain/Campus.java | 21 +++++++++++++ .../domain/CampusResource.java | 30 +++++++++++++++++++ .../src/main/resources/application.properties | 11 ++++++- 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 backend/src/main/java/org/acme/schooltimetabling/domain/Campus.java create mode 100644 backend/src/main/java/org/acme/schooltimetabling/domain/CampusResource.java diff --git a/backend/pom.xml b/backend/pom.xml index 1a815ca..09150b0 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -62,6 +62,14 @@ rest-assured test + + io.quarkus + quarkus-jdbc-postgresql + + + io.quarkus + quarkus-hibernate-orm-panache + diff --git a/backend/src/main/java/org/acme/schooltimetabling/domain/Campus.java b/backend/src/main/java/org/acme/schooltimetabling/domain/Campus.java new file mode 100644 index 0000000..d4c49cc --- /dev/null +++ b/backend/src/main/java/org/acme/schooltimetabling/domain/Campus.java @@ -0,0 +1,21 @@ +package org.acme.schooltimetabling.domain; + +// import java.util.List; +import jakarta.persistence.*; +import io.quarkus.hibernate.orm.panache.PanacheEntity; + +@Entity +public class Campus extends PanacheEntity { + + public String name; + + // empty constructor + public Campus() { + } + + // constructor with name input + public Campus(String name) { + this.name = name; + } + +} \ No newline at end of file diff --git a/backend/src/main/java/org/acme/schooltimetabling/domain/CampusResource.java b/backend/src/main/java/org/acme/schooltimetabling/domain/CampusResource.java new file mode 100644 index 0000000..7b380c9 --- /dev/null +++ b/backend/src/main/java/org/acme/schooltimetabling/domain/CampusResource.java @@ -0,0 +1,30 @@ +package org.acme.schooltimetabling.domain; + +import java.util.List; + +import jakarta.transaction.Transactional; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@Path("/campuses") +public class CampusResource { + + @GET + @Produces(MediaType.APPLICATION_JSON) + public List list() { + return Campus.listAll(); + } + + @POST + @Transactional + @Consumes(MediaType.APPLICATION_JSON) + public Response createCampus(Campus campus) { + campus.persist(); + return Response.status(Response.Status.CREATED).entity(campus).build(); + } +} \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index c99914e..43e49f3 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -4,4 +4,13 @@ quarkus.timefold.solver.termination.spent-limit=5s quarkus.http.cors=true quarkus.http.cors.origins=* quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS -quarkus.http.cors.headers=Content-Type,Authorization \ No newline at end of file +quarkus.http.cors.headers=Content-Type,Authorization + +# datasource config +quarkus.datasource.db-kind = postgresql +quarkus.datasource.username = ${QUARKUS_DATASOURCE_USERNAME} +quarkus.datasource.password = ${QUARKUS_DATASOURCE_PASSWORD} +quarkus.datasource.jdbc.url = ${QUARKUS_DATASOURCE_JDBC_URL} + +# drop and create the database at startup (use `update` to only update the schema) +quarkus.hibernate-orm.database.generation = update \ No newline at end of file From 7dc4beedf737628a55b5daaad46061caab44646f Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Fri, 13 Sep 2024 20:30:01 +1000 Subject: [PATCH 03/40] feat: add Student class --- backend/pom.xml | 20 +++--- backend/src/main/java/org/acme/Student.java | 57 +++++++++++++++ .../acme/schooltimetabling/domain/Lesson.java | 69 ------------------- .../acme/schooltimetabling/domain/Room.java | 23 ------- .../schooltimetabling/domain/Timeslot.java | 38 ---------- .../schooltimetabling/domain/Timetable.java | 52 -------------- .../rest/TimetableResource.java | 35 ---------- .../solver/TimetableConstraintProvider.java | 57 --------------- 8 files changed, 67 insertions(+), 284 deletions(-) create mode 100644 backend/src/main/java/org/acme/Student.java delete mode 100644 backend/src/main/java/org/acme/schooltimetabling/domain/Lesson.java delete mode 100644 backend/src/main/java/org/acme/schooltimetabling/domain/Room.java delete mode 100644 backend/src/main/java/org/acme/schooltimetabling/domain/Timeslot.java delete mode 100644 backend/src/main/java/org/acme/schooltimetabling/domain/Timetable.java delete mode 100644 backend/src/main/java/org/acme/schooltimetabling/rest/TimetableResource.java delete mode 100644 backend/src/main/java/org/acme/schooltimetabling/solver/TimetableConstraintProvider.java diff --git a/backend/pom.xml b/backend/pom.xml index 1a815ca..afaa655 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -53,14 +53,14 @@ timefold-solver-quarkus-jackson - io.quarkus - quarkus-junit5 - test + io.quarkus + quarkus-junit5 + test - io.rest-assured - rest-assured - test + io.rest-assured + rest-assured + test @@ -91,10 +91,10 @@ - native - - native - + native + + native + \ No newline at end of file diff --git a/backend/src/main/java/org/acme/Student.java b/backend/src/main/java/org/acme/Student.java new file mode 100644 index 0000000..7eda440 --- /dev/null +++ b/backend/src/main/java/org/acme/Student.java @@ -0,0 +1,57 @@ +package org.acme; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.lookup.PlanningId; +import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; +import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; +import ai.timefold.solver.core.api.domain.variable.PlanningVariable; + +import java.util.List; +import java.util.Objects; + +//@PlanningEntity +public class Student { + + // String studentID; +// @PlanningId + String name; + +// @PlanningListVariable +// List classes; + + public Student() { + } + + public Student(String name) { + this.name = name; + } + +// public List getClasses() { +// return classes; +// } +// +// public void setClasses(List classes) { +// this.classes = classes; +// } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Student student = (Student) o; + return Objects.equals(name, student.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/acme/schooltimetabling/domain/Lesson.java b/backend/src/main/java/org/acme/schooltimetabling/domain/Lesson.java deleted file mode 100644 index 59ba6ec..0000000 --- a/backend/src/main/java/org/acme/schooltimetabling/domain/Lesson.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.acme.schooltimetabling.domain; - -import ai.timefold.solver.core.api.domain.entity.PlanningEntity; -import ai.timefold.solver.core.api.domain.lookup.PlanningId; -import ai.timefold.solver.core.api.domain.variable.PlanningVariable; - -@PlanningEntity -public class Lesson { - - @PlanningId - private String id; - - private String subject; - private String teacher; - private String studentGroup; - - @PlanningVariable - private Timeslot timeslot; - @PlanningVariable - private Room room; - - public Lesson() { - } - - public Lesson(String id, String subject, String teacher, String studentGroup) { - this.id = id; - this.subject = subject; - this.teacher = teacher; - this.studentGroup = studentGroup; - } - - public String getId() { - return id; - } - - public String getSubject() { - return subject; - } - - public String getTeacher() { - return teacher; - } - - public String getStudentGroup() { - return studentGroup; - } - - public Timeslot getTimeslot() { - return timeslot; - } - - public void setTimeslot(Timeslot timeslot) { - this.timeslot = timeslot; - } - - public Room getRoom() { - return room; - } - - public void setRoom(Room room) { - this.room = room; - } - - @Override - public String toString() { - return subject + "(" + id + ")"; - } - -} \ No newline at end of file diff --git a/backend/src/main/java/org/acme/schooltimetabling/domain/Room.java b/backend/src/main/java/org/acme/schooltimetabling/domain/Room.java deleted file mode 100644 index 84dd324..0000000 --- a/backend/src/main/java/org/acme/schooltimetabling/domain/Room.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.acme.schooltimetabling.domain; - -public class Room { - - private String name; - - public Room() { - } - - public Room(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - @Override - public String toString() { - return name; - } - -} \ No newline at end of file diff --git a/backend/src/main/java/org/acme/schooltimetabling/domain/Timeslot.java b/backend/src/main/java/org/acme/schooltimetabling/domain/Timeslot.java deleted file mode 100644 index dc9bdb3..0000000 --- a/backend/src/main/java/org/acme/schooltimetabling/domain/Timeslot.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.acme.schooltimetabling.domain; - -import java.time.DayOfWeek; -import java.time.LocalTime; - -public class Timeslot { - - private DayOfWeek dayOfWeek; - private LocalTime startTime; - private LocalTime endTime; - - public Timeslot() { - } - - public Timeslot(DayOfWeek dayOfWeek, LocalTime startTime, LocalTime endTime) { - this.dayOfWeek = dayOfWeek; - this.startTime = startTime; - this.endTime = endTime; - } - - public DayOfWeek getDayOfWeek() { - return dayOfWeek; - } - - public LocalTime getStartTime() { - return startTime; - } - - public LocalTime getEndTime() { - return endTime; - } - - @Override - public String toString() { - return dayOfWeek + " " + startTime; - } - -} \ No newline at end of file diff --git a/backend/src/main/java/org/acme/schooltimetabling/domain/Timetable.java b/backend/src/main/java/org/acme/schooltimetabling/domain/Timetable.java deleted file mode 100644 index 16e5500..0000000 --- a/backend/src/main/java/org/acme/schooltimetabling/domain/Timetable.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.acme.schooltimetabling.domain; - -import java.util.List; - -import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; -import ai.timefold.solver.core.api.domain.solution.PlanningScore; -import ai.timefold.solver.core.api.domain.solution.PlanningSolution; -import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty; -import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; -import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; - -@PlanningSolution -public class Timetable { - - @ValueRangeProvider - @ProblemFactCollectionProperty - private List timeslots; - @ValueRangeProvider - @ProblemFactCollectionProperty - private List rooms; - @PlanningEntityCollectionProperty - private List lessons; - - @PlanningScore - private HardSoftScore score; - - public Timetable() { - } - - public Timetable(List timeslots, List rooms, List lessons) { - this.timeslots = timeslots; - this.rooms = rooms; - this.lessons = lessons; - } - - public List getTimeslots() { - return timeslots; - } - - public List getRooms() { - return rooms; - } - - public List getLessons() { - return lessons; - } - - public HardSoftScore getScore() { - return score; - } - -} \ No newline at end of file diff --git a/backend/src/main/java/org/acme/schooltimetabling/rest/TimetableResource.java b/backend/src/main/java/org/acme/schooltimetabling/rest/TimetableResource.java deleted file mode 100644 index 66d8ef9..0000000 --- a/backend/src/main/java/org/acme/schooltimetabling/rest/TimetableResource.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.acme.schooltimetabling.rest; - -import java.util.UUID; -import java.util.concurrent.ExecutionException; -import jakarta.inject.Inject; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; - -import org.acme.schooltimetabling.domain.Timetable; -import ai.timefold.solver.core.api.solver.SolverJob; -import ai.timefold.solver.core.api.solver.SolverManager; - -@Path("/timetables") -public class TimetableResource { - - @Inject - SolverManager solverManager; - - @POST - @Path("/solve") - public Timetable solve(Timetable problem) { - UUID problemId = UUID.randomUUID(); - // Submit the problem to start solving - SolverJob solverJob = solverManager.solve(problemId, problem); - Timetable solution; - try { - // Wait until the solving ends - solution = solverJob.getFinalBestSolution(); - } catch (InterruptedException | ExecutionException e) { - throw new IllegalStateException("Solving failed.", e); - } - return solution; - } - -} \ No newline at end of file diff --git a/backend/src/main/java/org/acme/schooltimetabling/solver/TimetableConstraintProvider.java b/backend/src/main/java/org/acme/schooltimetabling/solver/TimetableConstraintProvider.java deleted file mode 100644 index 6344e9c..0000000 --- a/backend/src/main/java/org/acme/schooltimetabling/solver/TimetableConstraintProvider.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.acme.schooltimetabling.solver; - -import org.acme.schooltimetabling.domain.Lesson; -import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; -import ai.timefold.solver.core.api.score.stream.Constraint; -import ai.timefold.solver.core.api.score.stream.ConstraintFactory; -import ai.timefold.solver.core.api.score.stream.ConstraintProvider; -import ai.timefold.solver.core.api.score.stream.Joiners; - -public class TimetableConstraintProvider implements ConstraintProvider { - - @Override - public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { - return new Constraint[] { - // Hard constraints - roomConflict(constraintFactory), - teacherConflict(constraintFactory), - studentGroupConflict(constraintFactory), - // Soft constraints are only implemented in the timefold-quickstarts code - }; - } - - private Constraint roomConflict(ConstraintFactory constraintFactory) { - // A room can accommodate at most one lesson at the same time. - return constraintFactory - // Select each pair of 2 different lessons ... - .forEachUniquePair(Lesson.class, - // ... in the same timeslot ... - Joiners.equal(Lesson::getTimeslot), - // ... in the same room ... - Joiners.equal(Lesson::getRoom)) - // ... and penalize each pair with a hard weight. - .penalize(HardSoftScore.ONE_HARD) - .asConstraint("Room conflict"); - } - - private Constraint teacherConflict(ConstraintFactory constraintFactory) { - // A teacher can teach at most one lesson at the same time. - return constraintFactory - .forEachUniquePair(Lesson.class, - Joiners.equal(Lesson::getTimeslot), - Joiners.equal(Lesson::getTeacher)) - .penalize(HardSoftScore.ONE_HARD) - .asConstraint("Teacher conflict"); - } - - private Constraint studentGroupConflict(ConstraintFactory constraintFactory) { - // A student can attend at most one lesson at the same time. - return constraintFactory - .forEachUniquePair(Lesson.class, - Joiners.equal(Lesson::getTimeslot), - Joiners.equal(Lesson::getStudentGroup)) - .penalize(HardSoftScore.ONE_HARD) - .asConstraint("Student group conflict"); - } - -} \ No newline at end of file From 2f7bf6e74e3a6e555ee0a679a40caa3bbe575ed3 Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Fri, 13 Sep 2024 20:52:18 +1000 Subject: [PATCH 04/40] feat: add Room class --- .../main/java/org/acme/GreetingResource.java | 2 + backend/src/main/java/org/acme/Room.java | 39 +++++++++++++++++++ backend/src/main/java/org/acme/Student.java | 20 ++++------ 3 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 backend/src/main/java/org/acme/Room.java diff --git a/backend/src/main/java/org/acme/GreetingResource.java b/backend/src/main/java/org/acme/GreetingResource.java index 244f294..9835c9b 100644 --- a/backend/src/main/java/org/acme/GreetingResource.java +++ b/backend/src/main/java/org/acme/GreetingResource.java @@ -8,6 +8,8 @@ @Path("/hello") public class GreetingResource { + + @GET @Produces(MediaType.TEXT_PLAIN) public String hello() { diff --git a/backend/src/main/java/org/acme/Room.java b/backend/src/main/java/org/acme/Room.java new file mode 100644 index 0000000..4d1d4a4 --- /dev/null +++ b/backend/src/main/java/org/acme/Room.java @@ -0,0 +1,39 @@ +package org.acme; + +import ai.timefold.solver.core.api.domain.lookup.PlanningId; + +/** Represents a room. + * @author Jet Edge + */ +public class Room { + @PlanningId + private String id; + private int capacity; + + public Room() {} + + /** Creates a room with its ID and capacity. + * @param id The room’s id. + * @param capacity The room's capacity. + */ + public Room(String id, int capacity) { + this.id = id; + this.capacity = capacity; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public int getCapacity() { + return capacity; + } + + public void setCapacity(int capacity) { + this.capacity = capacity; + } +} \ No newline at end of file diff --git a/backend/src/main/java/org/acme/Student.java b/backend/src/main/java/org/acme/Student.java index 7eda440..7d1ba69 100644 --- a/backend/src/main/java/org/acme/Student.java +++ b/backend/src/main/java/org/acme/Student.java @@ -9,31 +9,25 @@ import java.util.List; import java.util.Objects; -//@PlanningEntity +/** Represents a student. + * @author Jet Edge + */ public class Student { - // String studentID; +// String studentID; // @PlanningId String name; -// @PlanningListVariable -// List classes; - public Student() { } + /** Creates a student with the specified name. + * @param name The student’s name. + */ public Student(String name) { this.name = name; } -// public List getClasses() { -// return classes; -// } -// -// public void setClasses(List classes) { -// this.classes = classes; -// } - public String getName() { return name; } From 0ed9e8224f1667d101de95b059354b425cb9cc2f Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Fri, 13 Sep 2024 21:00:26 +1000 Subject: [PATCH 05/40] feat: add Unit class --- backend/src/main/java/org/acme/Room.java | 13 +- backend/src/main/java/org/acme/Student.java | 17 +- backend/src/main/java/org/acme/Unit.java | 163 ++++++++++++++++++++ 3 files changed, 179 insertions(+), 14 deletions(-) create mode 100644 backend/src/main/java/org/acme/Unit.java diff --git a/backend/src/main/java/org/acme/Room.java b/backend/src/main/java/org/acme/Room.java index 4d1d4a4..ce56196 100644 --- a/backend/src/main/java/org/acme/Room.java +++ b/backend/src/main/java/org/acme/Room.java @@ -2,7 +2,9 @@ import ai.timefold.solver.core.api.domain.lookup.PlanningId; -/** Represents a room. +/** + * Represents a room. + * * @author Jet Edge */ public class Room { @@ -10,10 +12,13 @@ public class Room { private String id; private int capacity; - public Room() {} + public Room() { + } - /** Creates a room with its ID and capacity. - * @param id The room’s id. + /** + * Creates a room with its ID and capacity. + * + * @param id The room’s id. * @param capacity The room's capacity. */ public Room(String id, int capacity) { diff --git a/backend/src/main/java/org/acme/Student.java b/backend/src/main/java/org/acme/Student.java index 7d1ba69..3efb468 100644 --- a/backend/src/main/java/org/acme/Student.java +++ b/backend/src/main/java/org/acme/Student.java @@ -1,27 +1,24 @@ package org.acme; -import ai.timefold.solver.core.api.domain.entity.PlanningEntity; -import ai.timefold.solver.core.api.domain.lookup.PlanningId; -import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; -import ai.timefold.solver.core.api.domain.variable.PlanningListVariable; -import ai.timefold.solver.core.api.domain.variable.PlanningVariable; - -import java.util.List; import java.util.Objects; -/** Represents a student. +/** + * Represents a student. + * * @author Jet Edge */ public class Student { -// String studentID; + // String studentID; // @PlanningId String name; public Student() { } - /** Creates a student with the specified name. + /** + * Creates a student with the specified name. + * * @param name The student’s name. */ public Student(String name) { diff --git a/backend/src/main/java/org/acme/Unit.java b/backend/src/main/java/org/acme/Unit.java new file mode 100644 index 0000000..5502eaf --- /dev/null +++ b/backend/src/main/java/org/acme/Unit.java @@ -0,0 +1,163 @@ +package org.acme; + +import ai.timefold.solver.core.api.domain.entity.PlanningEntity; +import ai.timefold.solver.core.api.domain.lookup.PlanningId; +import ai.timefold.solver.core.api.domain.variable.PlanningVariable; + +import java.time.Duration; +import java.time.LocalTime; +import java.util.List; + + +/** + * Represents a unit. + * + * @author Jet Edge + */ +@PlanningEntity +public class Unit { + + @PlanningId + int unitID; + + String name; + + Duration duration; + + @PlanningVariable + LocalTime start; + List students; + @PlanningVariable + private Room room; + + public Unit() { + } + + ; + + /** + * Creates a unit. + * + * @param unitID The unit’s ID. + * @param name The unit’s ID. + * @param duration The unit’s duration. + * @param students The list of students enrolled in the unit. + */ + public Unit(int unitID, String name, Duration duration, List students) { + this.unitID = unitID; + this.name = name; + this.duration = duration; + this.students = students; + } + + /** + * Creates a unit. + * + * @param unitID The unit’s ID. + * @param name The unit’s ID. + * @param duration The unit’s duration. + * @param students The list of students enrolled in the unit. + * @param room The room assigned to the unit. + */ + public Unit(int unitID, String name, Duration duration, List students, Room room) { + this.unitID = unitID; + this.name = name; + this.duration = duration; + this.students = students; + this.room = room; + } + + public int getUnitID() { + return unitID; + } + + public void setUnitID(int unitID) { + this.unitID = unitID; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Duration getDuration() { + return duration; + } + + public void setDuration(Duration duration) { + this.duration = duration; + } + + public LocalTime getStart() { + return start; + } + + public void setStart(LocalTime start) { + this.start = start; + } + + public LocalTime getEnd() { + return start.plus(duration); + } + + public List getStudents() { + return students; + } + + public void setStudents(List students) { + this.students = students; + } + + /** + * Check if the unit has a common student with the other unit. + * + * @param otherUnit The other unit. + * @return A boolean representing whether there is a common student. + */ + public boolean hasSameStudent(Unit otherUnit) { + for (Student student : students) { + if (otherUnit.getStudents().contains(student)) { + return true; + } + } + + return false; + } + + /** + * Get the number of common students between 2 units. + * + * @param otherUnit The other unit. + * @return An int representing the number of common students. + */ + public int numSameStudent(Unit otherUnit) { + int num = 0; + + for (Student student : students) { + if (otherUnit.getStudents().contains(student)) { + num++; + } + } + return num; + } + + /** + * Get the number of students enrolled in the unit. + * + * @return An int representing the number of students. + */ + public int getStudentSize() { + return students.size(); + } + + public Room getRoom() { + return room; + } + + public void setRoom(Room room) { + this.room = room; + } +} \ No newline at end of file From f75f06ff1463be04a61aee13c58e3554fccc3e55 Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Fri, 13 Sep 2024 21:03:10 +1000 Subject: [PATCH 06/40] feat: add ConflictingUnit class --- .../main/java/org/acme/ConflictingUnit.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 backend/src/main/java/org/acme/ConflictingUnit.java diff --git a/backend/src/main/java/org/acme/ConflictingUnit.java b/backend/src/main/java/org/acme/ConflictingUnit.java new file mode 100644 index 0000000..050f617 --- /dev/null +++ b/backend/src/main/java/org/acme/ConflictingUnit.java @@ -0,0 +1,62 @@ +package org.acme; + +/** + * Represents a pair of conflicting units. + * + * @author Jet Edge + */ +public class ConflictingUnit { + Unit unit1; + + Unit unit2; + + int numStudent; + + /** + * Creates a pair of conflicting units. + * + * @param first The first unit. + * @param second The second unit. + */ + public ConflictingUnit(Unit first, Unit second) { + this.unit1 = first; + this.unit2 = second; + } + + /** + * Creates a pair of conflicting units. + * + * @param first The first unit. + * @param second The second unit. + * @param numStudent The number of common students. + */ + public ConflictingUnit(Unit first, Unit second, int numStudent) { + this.unit1 = first; + this.unit2 = second; + this.numStudent = numStudent; + } + + public Unit getUnit1() { + return unit1; + } + + public void setUnit1(Unit unit1) { + this.unit1 = unit1; + } + + public Unit getUnit2() { + return unit2; + } + + public void setUnit2(Unit unit2) { + this.unit2 = unit2; + } + + public int getNumStudent() { + return numStudent; + } + + public void setNumStudent(int numStudent) { + this.numStudent = numStudent; + } +} \ No newline at end of file From 1a346cef0c69706da86d06e2fec19b74ac603812 Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Fri, 13 Sep 2024 21:16:04 +1000 Subject: [PATCH 07/40] feat: add Timetable class --- backend/src/main/java/org/acme/Timetable.java | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 backend/src/main/java/org/acme/Timetable.java diff --git a/backend/src/main/java/org/acme/Timetable.java b/backend/src/main/java/org/acme/Timetable.java new file mode 100644 index 0000000..946afca --- /dev/null +++ b/backend/src/main/java/org/acme/Timetable.java @@ -0,0 +1,120 @@ +package org.acme; + +import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; +import ai.timefold.solver.core.api.domain.solution.PlanningScore; +import ai.timefold.solver.core.api.domain.solution.PlanningSolution; +import ai.timefold.solver.core.api.domain.solution.ProblemFactCollectionProperty; +import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; + +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents a timetable, the solution from the program. + * + * @author Jet Edge + */ +@PlanningSolution +public class Timetable { + + @PlanningEntityCollectionProperty + List units; + + @ValueRangeProvider + List startTimes; + @PlanningScore + HardSoftScore score; + @ProblemFactCollectionProperty + @ValueRangeProvider + private List rooms; + + public Timetable() { + + } + + /** + * Creates a timetable. + * + * @param units The list of units to be allocated. + * @param startTimes The list of available starting times. + */ + public Timetable(List units, List startTimes) { + this.units = units; + this.startTimes = startTimes; + } + + /** + * Creates a timetable. + * + * @param units The list of units to be allocated. + * @param startTimes The list of available starting times. + * @param rooms The list of available rooms. + */ + public Timetable(List units, List startTimes, List rooms) { + this.units = units; + this.startTimes = startTimes; + this.rooms = rooms; + } + + public List getStartTimes() { + return startTimes; + } + + public void setStartTimes(List startTimes) { + this.startTimes = startTimes; + } + + public List getRooms() { + return rooms; + } + + public void setRooms(List rooms) { + this.rooms = rooms; + } + + public List getUnits() { + return units; + } + + public void setUnits(List units) { + this.units = units; + } + + public HardSoftScore getScore() { + return score; + } + + public void setScore(HardSoftScore score) { + this.score = score; + } + + /** + * Identify conflicting units having common students at the same time. + * + * @return A list of conflicting units. + */ + @ProblemFactCollectionProperty + public List calculateSoftUnitConflictList() { + ArrayList out = new ArrayList(); + for (var first : units) { + for (var second : units) { + if (first.getUnitID() >= second.getUnitID()) { + continue; + } + int numStudents = 0; + for (Student firstStudent : first.getStudents()) { + if (second.getStudents().contains(firstStudent)) { + numStudents++; + } + } + if (numStudents > 0) { + out.add(new ConflictingUnit(first, second, numStudents)); + } + } + } + return out; + } + +} \ No newline at end of file From 4b46ebe350a879d235fa5b15958f725947fe8773 Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Fri, 13 Sep 2024 21:26:34 +1000 Subject: [PATCH 08/40] feat: add TimetableResource to provide the problem and get the solution from TimefoldAI --- .../main/java/org/acme/GreetingResource.java | 18 ----- .../main/java/org/acme/TimetableResource.java | 69 +++++++++++++++++++ ...sourceIT.java => TimetableResourceIT.java} | 2 +- ...ceTest.java => TimetableResourceTest.java} | 2 +- 4 files changed, 71 insertions(+), 20 deletions(-) delete mode 100644 backend/src/main/java/org/acme/GreetingResource.java create mode 100644 backend/src/main/java/org/acme/TimetableResource.java rename backend/src/test/java/org/acme/{GreetingResourceIT.java => TimetableResourceIT.java} (72%) rename backend/src/test/java/org/acme/{GreetingResourceTest.java => TimetableResourceTest.java} (93%) diff --git a/backend/src/main/java/org/acme/GreetingResource.java b/backend/src/main/java/org/acme/GreetingResource.java deleted file mode 100644 index 9835c9b..0000000 --- a/backend/src/main/java/org/acme/GreetingResource.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.acme; - -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; - -@Path("/hello") -public class GreetingResource { - - - - @GET - @Produces(MediaType.TEXT_PLAIN) - public String hello() { - return "Hello from Quarkus REST"; - } -} diff --git a/backend/src/main/java/org/acme/TimetableResource.java b/backend/src/main/java/org/acme/TimetableResource.java new file mode 100644 index 0000000..47112f0 --- /dev/null +++ b/backend/src/main/java/org/acme/TimetableResource.java @@ -0,0 +1,69 @@ +package org.acme; + +import ai.timefold.solver.core.api.solver.SolverManager; +import jakarta.inject.Inject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +import java.time.Duration; +import java.time.LocalTime; +import java.util.List; +import java.util.concurrent.ExecutionException; + +/** + * Entry to the timetabling program. + * Receives a timetabling problem and outputs the solution + * with the best optimised scores according to the provided constraints. + * + * @author Jet Edge + */ +@Path("/timetabling") +public class TimetableResource { + + @Inject + SolverManager solverManager; + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Timetable hello() throws ExecutionException, InterruptedException { + + Student a = new Student("a"); + Student b = new Student("b"); + Student c = new Student("c"); + Student d = new Student("d"); + Student e = new Student("e"); + Student f = new Student("f"); + Student g = new Student("g"); + Student h = new Student("h"); + Student i = new Student("i"); + + Room r1 = new Room("Room1", 1); + Room r2 = new Room("Room2", 2); + Room r3 = new Room("Room3", 3); + + var problem = new Timetable( + List.of( + new Unit(1, "1", Duration.ofHours(2), List.of(a, b)), + new Unit(2, "2", Duration.ofHours(2), List.of(c, d, e)), + new Unit(3, "3", Duration.ofHours(2), List.of(f, g, h, i)), + new Unit(4, "4", Duration.ofHours(2), List.of(a, b)), + new Unit(5, "5", Duration.ofHours(2), List.of(c, d, e)), + new Unit(6, "6", Duration.ofHours(2), List.of(f, g, h, i)) + ), + List.of( + LocalTime.of(15, 0), + LocalTime.of(17, 0) +// LocalTime.of(16,0), +// LocalTime.of(23,0) + ), + List.of(r1, r2, r3) + ); + + + Timetable solution = solverManager.solve("job 1", problem).getFinalBestSolution(); + return solution; + } + +} \ No newline at end of file diff --git a/backend/src/test/java/org/acme/GreetingResourceIT.java b/backend/src/test/java/org/acme/TimetableResourceIT.java similarity index 72% rename from backend/src/test/java/org/acme/GreetingResourceIT.java rename to backend/src/test/java/org/acme/TimetableResourceIT.java index cfa9d1b..511c713 100644 --- a/backend/src/test/java/org/acme/GreetingResourceIT.java +++ b/backend/src/test/java/org/acme/TimetableResourceIT.java @@ -3,6 +3,6 @@ import io.quarkus.test.junit.QuarkusIntegrationTest; @QuarkusIntegrationTest -class GreetingResourceIT extends GreetingResourceTest { +class TimetableResourceIT extends TimetableResourceTest { // Execute the same tests but in packaged mode. } diff --git a/backend/src/test/java/org/acme/GreetingResourceTest.java b/backend/src/test/java/org/acme/TimetableResourceTest.java similarity index 93% rename from backend/src/test/java/org/acme/GreetingResourceTest.java rename to backend/src/test/java/org/acme/TimetableResourceTest.java index 18332e4..52218bd 100644 --- a/backend/src/test/java/org/acme/GreetingResourceTest.java +++ b/backend/src/test/java/org/acme/TimetableResourceTest.java @@ -7,7 +7,7 @@ import static org.hamcrest.CoreMatchers.is; @QuarkusTest -class GreetingResourceTest { +class TimetableResourceTest { @Test void testHelloEndpoint() { given() From a0b1208727ca34e43ece8b65e50d982a4e3d5d64 Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Fri, 13 Sep 2024 21:52:12 +1000 Subject: [PATCH 09/40] feat: add student constraint: no students have overlapping units --- .../main/java/org/acme/ConflictingUnit.java | 2 +- .../org/acme/TimetableConstraintProvider.java | 47 +++++++++++++++++++ .../main/java/org/acme/TimetableResource.java | 10 ++-- 3 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 backend/src/main/java/org/acme/TimetableConstraintProvider.java diff --git a/backend/src/main/java/org/acme/ConflictingUnit.java b/backend/src/main/java/org/acme/ConflictingUnit.java index 050f617..1ac16d5 100644 --- a/backend/src/main/java/org/acme/ConflictingUnit.java +++ b/backend/src/main/java/org/acme/ConflictingUnit.java @@ -1,7 +1,7 @@ package org.acme; /** - * Represents a pair of conflicting units. + * Represents a pair of conflicting units, which are units with common students. * * @author Jet Edge */ diff --git a/backend/src/main/java/org/acme/TimetableConstraintProvider.java b/backend/src/main/java/org/acme/TimetableConstraintProvider.java new file mode 100644 index 0000000..f1f6c63 --- /dev/null +++ b/backend/src/main/java/org/acme/TimetableConstraintProvider.java @@ -0,0 +1,47 @@ +package org.acme; + +import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import ai.timefold.solver.core.api.score.stream.Constraint; +import ai.timefold.solver.core.api.score.stream.ConstraintFactory; +import ai.timefold.solver.core.api.score.stream.ConstraintProvider; +import ai.timefold.solver.core.api.score.stream.Joiners; + +import java.util.function.Function; + +/** + * Provides the constraints for the timetabling problem. + * + * @author Jet Edge + */ +public class TimetableConstraintProvider implements ConstraintProvider { + + /** + * Enable the various implemented constraints. + * + * @return A list of constraints. + */ + @Override + public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { + return new Constraint[]{ + studentConflict(constraintFactory) + + }; + } + + /** + * Penalize 1 hard score for each student with overlapping units. + */ + private Constraint studentConflict(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(ConflictingUnit.class) + .join(Unit.class, Joiners.equal(ConflictingUnit::getUnit1, Function.identity())) + .join(Unit.class, Joiners.equal((conflictingUnit, unit1) -> conflictingUnit.getUnit2(), Function.identity()), + Joiners.overlapping((conflictingUnit, unit1) -> unit1.getStart(), + (conflictingUnit, unit1) -> unit1.getEnd(), + Unit::getStart, Unit::getEnd)) + .penalize(HardSoftScore.ofHard(1), (conflictingUnit, unit1, unit2) -> conflictingUnit.getNumStudent()) + .asConstraint("Student conflict"); + + } + + +} \ No newline at end of file diff --git a/backend/src/main/java/org/acme/TimetableResource.java b/backend/src/main/java/org/acme/TimetableResource.java index 47112f0..9c046e1 100644 --- a/backend/src/main/java/org/acme/TimetableResource.java +++ b/backend/src/main/java/org/acme/TimetableResource.java @@ -46,11 +46,11 @@ public Timetable hello() throws ExecutionException, InterruptedException { var problem = new Timetable( List.of( new Unit(1, "1", Duration.ofHours(2), List.of(a, b)), - new Unit(2, "2", Duration.ofHours(2), List.of(c, d, e)), - new Unit(3, "3", Duration.ofHours(2), List.of(f, g, h, i)), - new Unit(4, "4", Duration.ofHours(2), List.of(a, b)), - new Unit(5, "5", Duration.ofHours(2), List.of(c, d, e)), - new Unit(6, "6", Duration.ofHours(2), List.of(f, g, h, i)) + new Unit(2, "2", Duration.ofHours(2), List.of(a, c, d, e)), + new Unit(3, "3", Duration.ofHours(2), List.of(a, f, g, h, i)) +// new Unit(4, "4", Duration.ofHours(2), List.of(a, b)), +// new Unit(5, "5", Duration.ofHours(2), List.of(c, d, e)), +// new Unit(6, "6", Duration.ofHours(2), List.of(f, g, h, i)) ), List.of( LocalTime.of(15, 0), From 5957dba722abb9a92250e5186d1d370046e56c3d Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Fri, 13 Sep 2024 22:06:02 +1000 Subject: [PATCH 10/40] feat: add room constraint, no overlapping units are in the same room --- .../org/acme/TimetableConstraintProvider.java | 24 ++++++++++++++++--- .../main/java/org/acme/TimetableResource.java | 4 ++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/backend/src/main/java/org/acme/TimetableConstraintProvider.java b/backend/src/main/java/org/acme/TimetableConstraintProvider.java index f1f6c63..dc1dec0 100644 --- a/backend/src/main/java/org/acme/TimetableConstraintProvider.java +++ b/backend/src/main/java/org/acme/TimetableConstraintProvider.java @@ -8,6 +8,8 @@ import java.util.function.Function; +import static ai.timefold.solver.core.api.score.stream.Joiners.overlapping; + /** * Provides the constraints for the timetabling problem. * @@ -23,8 +25,8 @@ public class TimetableConstraintProvider implements ConstraintProvider { @Override public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { return new Constraint[]{ - studentConflict(constraintFactory) - + studentConflict(constraintFactory), + roomConflict(constraintFactory) }; } @@ -35,7 +37,7 @@ private Constraint studentConflict(ConstraintFactory constraintFactory) { return constraintFactory.forEach(ConflictingUnit.class) .join(Unit.class, Joiners.equal(ConflictingUnit::getUnit1, Function.identity())) .join(Unit.class, Joiners.equal((conflictingUnit, unit1) -> conflictingUnit.getUnit2(), Function.identity()), - Joiners.overlapping((conflictingUnit, unit1) -> unit1.getStart(), + overlapping((conflictingUnit, unit1) -> unit1.getStart(), (conflictingUnit, unit1) -> unit1.getEnd(), Unit::getStart, Unit::getEnd)) .penalize(HardSoftScore.ofHard(1), (conflictingUnit, unit1, unit2) -> conflictingUnit.getNumStudent()) @@ -43,5 +45,21 @@ private Constraint studentConflict(ConstraintFactory constraintFactory) { } + /** + * Penalize 1 hard score for each room with overlapping units. + */ + Constraint roomConflict(ConstraintFactory constraintFactory) { + // A room can accommodate at most one lesson at the same time. + return constraintFactory + // Select each pair of 2 different lessons ... + .forEachUniquePair(Unit.class, + // ... in the same timeslot ... + overlapping(Unit::getStart, Unit::getEnd), + // ... in the same room ... + Joiners.equal(Unit::getRoom)) + // ... and penalize each pair with a hard weight. + .penalize(HardSoftScore.ofHard(1)) + .asConstraint("Room conflict"); + } } \ No newline at end of file diff --git a/backend/src/main/java/org/acme/TimetableResource.java b/backend/src/main/java/org/acme/TimetableResource.java index 9c046e1..e305d87 100644 --- a/backend/src/main/java/org/acme/TimetableResource.java +++ b/backend/src/main/java/org/acme/TimetableResource.java @@ -47,7 +47,7 @@ public Timetable hello() throws ExecutionException, InterruptedException { List.of( new Unit(1, "1", Duration.ofHours(2), List.of(a, b)), new Unit(2, "2", Duration.ofHours(2), List.of(a, c, d, e)), - new Unit(3, "3", Duration.ofHours(2), List.of(a, f, g, h, i)) + new Unit(3, "3", Duration.ofHours(2), List.of(f, g, h, i)) // new Unit(4, "4", Duration.ofHours(2), List.of(a, b)), // new Unit(5, "5", Duration.ofHours(2), List.of(c, d, e)), // new Unit(6, "6", Duration.ofHours(2), List.of(f, g, h, i)) @@ -58,7 +58,7 @@ public Timetable hello() throws ExecutionException, InterruptedException { // LocalTime.of(16,0), // LocalTime.of(23,0) ), - List.of(r1, r2, r3) + List.of(r1, r2) ); From 645497891df053885200a605b4df2c86e025b1b4 Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Fri, 13 Sep 2024 22:12:09 +1000 Subject: [PATCH 11/40] feat: add room capacity constraint, ensure room capacity is not less than allocated unit size --- .../java/org/acme/TimetableConstraintProvider.java | 13 ++++++++++++- .../src/main/java/org/acme/TimetableResource.java | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/org/acme/TimetableConstraintProvider.java b/backend/src/main/java/org/acme/TimetableConstraintProvider.java index dc1dec0..078cbe1 100644 --- a/backend/src/main/java/org/acme/TimetableConstraintProvider.java +++ b/backend/src/main/java/org/acme/TimetableConstraintProvider.java @@ -26,7 +26,8 @@ public class TimetableConstraintProvider implements ConstraintProvider { public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { return new Constraint[]{ studentConflict(constraintFactory), - roomConflict(constraintFactory) + roomConflict(constraintFactory), + roomCapacity(constraintFactory) }; } @@ -62,4 +63,14 @@ Constraint roomConflict(ConstraintFactory constraintFactory) { .asConstraint("Room conflict"); } + /** + * Penalize 1 soft score for each student overflowing the capacity of the room. + */ + Constraint roomCapacity(ConstraintFactory constraintFactory) { + return constraintFactory.forEach(Unit.class) + .filter(unit -> unit.getStudentSize() > unit.getRoom().getCapacity()) + .penalize(HardSoftScore.ofSoft(1), unit -> unit.getStudentSize() - unit.getRoom().getCapacity()) + .asConstraint("Room capacity conflict"); + } + } \ No newline at end of file diff --git a/backend/src/main/java/org/acme/TimetableResource.java b/backend/src/main/java/org/acme/TimetableResource.java index e305d87..0e9a385 100644 --- a/backend/src/main/java/org/acme/TimetableResource.java +++ b/backend/src/main/java/org/acme/TimetableResource.java @@ -39,8 +39,8 @@ public Timetable hello() throws ExecutionException, InterruptedException { Student h = new Student("h"); Student i = new Student("i"); - Room r1 = new Room("Room1", 1); - Room r2 = new Room("Room2", 2); + Room r1 = new Room("Room1", 2); + Room r2 = new Room("Room2", 3); Room r3 = new Room("Room3", 3); var problem = new Timetable( From b2fe2f5e9c5e3531ffc637a3a1a6601eb785c4a1 Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Fri, 13 Sep 2024 22:18:28 +1000 Subject: [PATCH 12/40] refactor: move classes to domain package --- .../main/java/org/acme/TimetableConstraintProvider.java | 2 ++ backend/src/main/java/org/acme/TimetableResource.java | 8 ++++++-- .../main/java/org/acme/{ => domain}/ConflictingUnit.java | 2 +- backend/src/main/java/org/acme/{ => domain}/Room.java | 2 +- backend/src/main/java/org/acme/{ => domain}/Student.java | 2 +- .../src/main/java/org/acme/{ => domain}/Timetable.java | 2 +- backend/src/main/java/org/acme/{ => domain}/Unit.java | 2 +- 7 files changed, 13 insertions(+), 7 deletions(-) rename backend/src/main/java/org/acme/{ => domain}/ConflictingUnit.java (98%) rename backend/src/main/java/org/acme/{ => domain}/Room.java (96%) rename backend/src/main/java/org/acme/{ => domain}/Student.java (97%) rename backend/src/main/java/org/acme/{ => domain}/Timetable.java (99%) rename backend/src/main/java/org/acme/{ => domain}/Unit.java (99%) diff --git a/backend/src/main/java/org/acme/TimetableConstraintProvider.java b/backend/src/main/java/org/acme/TimetableConstraintProvider.java index 078cbe1..560ebe8 100644 --- a/backend/src/main/java/org/acme/TimetableConstraintProvider.java +++ b/backend/src/main/java/org/acme/TimetableConstraintProvider.java @@ -5,6 +5,8 @@ import ai.timefold.solver.core.api.score.stream.ConstraintFactory; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; import ai.timefold.solver.core.api.score.stream.Joiners; +import org.acme.domain.ConflictingUnit; +import org.acme.domain.Unit; import java.util.function.Function; diff --git a/backend/src/main/java/org/acme/TimetableResource.java b/backend/src/main/java/org/acme/TimetableResource.java index 0e9a385..11e28f4 100644 --- a/backend/src/main/java/org/acme/TimetableResource.java +++ b/backend/src/main/java/org/acme/TimetableResource.java @@ -6,6 +6,10 @@ import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; +import org.acme.domain.Room; +import org.acme.domain.Student; +import org.acme.domain.Timetable; +import org.acme.domain.Unit; import java.time.Duration; import java.time.LocalTime; @@ -41,7 +45,7 @@ public Timetable hello() throws ExecutionException, InterruptedException { Room r1 = new Room("Room1", 2); Room r2 = new Room("Room2", 3); - Room r3 = new Room("Room3", 3); + Room r3 = new Room("Room3", 4); var problem = new Timetable( List.of( @@ -58,7 +62,7 @@ public Timetable hello() throws ExecutionException, InterruptedException { // LocalTime.of(16,0), // LocalTime.of(23,0) ), - List.of(r1, r2) + List.of(r1, r2, r3) ); diff --git a/backend/src/main/java/org/acme/ConflictingUnit.java b/backend/src/main/java/org/acme/domain/ConflictingUnit.java similarity index 98% rename from backend/src/main/java/org/acme/ConflictingUnit.java rename to backend/src/main/java/org/acme/domain/ConflictingUnit.java index 1ac16d5..20444ff 100644 --- a/backend/src/main/java/org/acme/ConflictingUnit.java +++ b/backend/src/main/java/org/acme/domain/ConflictingUnit.java @@ -1,4 +1,4 @@ -package org.acme; +package org.acme.domain; /** * Represents a pair of conflicting units, which are units with common students. diff --git a/backend/src/main/java/org/acme/Room.java b/backend/src/main/java/org/acme/domain/Room.java similarity index 96% rename from backend/src/main/java/org/acme/Room.java rename to backend/src/main/java/org/acme/domain/Room.java index ce56196..ef74552 100644 --- a/backend/src/main/java/org/acme/Room.java +++ b/backend/src/main/java/org/acme/domain/Room.java @@ -1,4 +1,4 @@ -package org.acme; +package org.acme.domain; import ai.timefold.solver.core.api.domain.lookup.PlanningId; diff --git a/backend/src/main/java/org/acme/Student.java b/backend/src/main/java/org/acme/domain/Student.java similarity index 97% rename from backend/src/main/java/org/acme/Student.java rename to backend/src/main/java/org/acme/domain/Student.java index 3efb468..2d7cd6d 100644 --- a/backend/src/main/java/org/acme/Student.java +++ b/backend/src/main/java/org/acme/domain/Student.java @@ -1,4 +1,4 @@ -package org.acme; +package org.acme.domain; import java.util.Objects; diff --git a/backend/src/main/java/org/acme/Timetable.java b/backend/src/main/java/org/acme/domain/Timetable.java similarity index 99% rename from backend/src/main/java/org/acme/Timetable.java rename to backend/src/main/java/org/acme/domain/Timetable.java index 946afca..b340c92 100644 --- a/backend/src/main/java/org/acme/Timetable.java +++ b/backend/src/main/java/org/acme/domain/Timetable.java @@ -1,4 +1,4 @@ -package org.acme; +package org.acme.domain; import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty; import ai.timefold.solver.core.api.domain.solution.PlanningScore; diff --git a/backend/src/main/java/org/acme/Unit.java b/backend/src/main/java/org/acme/domain/Unit.java similarity index 99% rename from backend/src/main/java/org/acme/Unit.java rename to backend/src/main/java/org/acme/domain/Unit.java index 5502eaf..8c662af 100644 --- a/backend/src/main/java/org/acme/Unit.java +++ b/backend/src/main/java/org/acme/domain/Unit.java @@ -1,4 +1,4 @@ -package org.acme; +package org.acme.domain; import ai.timefold.solver.core.api.domain.entity.PlanningEntity; import ai.timefold.solver.core.api.domain.lookup.PlanningId; From 44ff683429cd73f3eb67eca8fa97936cbdd3aaf4 Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Fri, 13 Sep 2024 22:19:12 +1000 Subject: [PATCH 13/40] refactor: move constraints to solver package --- .../java/org/acme/{ => solver}/TimetableConstraintProvider.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename backend/src/main/java/org/acme/{ => solver}/TimetableConstraintProvider.java (99%) diff --git a/backend/src/main/java/org/acme/TimetableConstraintProvider.java b/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java similarity index 99% rename from backend/src/main/java/org/acme/TimetableConstraintProvider.java rename to backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java index 560ebe8..393120c 100644 --- a/backend/src/main/java/org/acme/TimetableConstraintProvider.java +++ b/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java @@ -1,4 +1,4 @@ -package org.acme; +package org.acme.solver; import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; import ai.timefold.solver.core.api.score.stream.Constraint; From fe949ae433cb07256875cb0a5368cda00313a9ff Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Fri, 13 Sep 2024 22:24:25 +1000 Subject: [PATCH 14/40] refactor: remove redundant methods from Unit class --- .../src/main/java/org/acme/domain/Unit.java | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/backend/src/main/java/org/acme/domain/Unit.java b/backend/src/main/java/org/acme/domain/Unit.java index 8c662af..96314e2 100644 --- a/backend/src/main/java/org/acme/domain/Unit.java +++ b/backend/src/main/java/org/acme/domain/Unit.java @@ -111,39 +111,6 @@ public void setStudents(List students) { this.students = students; } - /** - * Check if the unit has a common student with the other unit. - * - * @param otherUnit The other unit. - * @return A boolean representing whether there is a common student. - */ - public boolean hasSameStudent(Unit otherUnit) { - for (Student student : students) { - if (otherUnit.getStudents().contains(student)) { - return true; - } - } - - return false; - } - - /** - * Get the number of common students between 2 units. - * - * @param otherUnit The other unit. - * @return An int representing the number of common students. - */ - public int numSameStudent(Unit otherUnit) { - int num = 0; - - for (Student student : students) { - if (otherUnit.getStudents().contains(student)) { - num++; - } - } - return num; - } - /** * Get the number of students enrolled in the unit. * From 1b14fb452d11aa9bb2508aa4477ca2f31c495d9e Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Sat, 14 Sep 2024 20:10:20 +1000 Subject: [PATCH 15/40] feat: add the day-of-week to Unit --- .../src/main/java/org/acme/domain/Unit.java | 36 +++++++++++-------- .../solver/TimetableConstraintProvider.java | 6 ++-- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/backend/src/main/java/org/acme/domain/Unit.java b/backend/src/main/java/org/acme/domain/Unit.java index 96314e2..4be353a 100644 --- a/backend/src/main/java/org/acme/domain/Unit.java +++ b/backend/src/main/java/org/acme/domain/Unit.java @@ -4,6 +4,7 @@ import ai.timefold.solver.core.api.domain.lookup.PlanningId; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; +import java.time.DayOfWeek; import java.time.Duration; import java.time.LocalTime; import java.util.List; @@ -17,24 +18,21 @@ @PlanningEntity public class Unit { + List students; @PlanningId - int unitID; - - String name; - - Duration duration; - + private int unitID; + private String name; + private Duration duration; @PlanningVariable - LocalTime start; - List students; + private LocalTime startTime; + @PlanningVariable + private DayOfWeek dayOfWeek; @PlanningVariable private Room room; public Unit() { } - ; - /** * Creates a unit. * @@ -91,16 +89,16 @@ public void setDuration(Duration duration) { this.duration = duration; } - public LocalTime getStart() { - return start; + public LocalTime getStartTime() { + return startTime; } - public void setStart(LocalTime start) { - this.start = start; + public void setStartTime(LocalTime startTime) { + this.startTime = startTime; } public LocalTime getEnd() { - return start.plus(duration); + return startTime.plus(duration); } public List getStudents() { @@ -127,4 +125,12 @@ public Room getRoom() { public void setRoom(Room room) { this.room = room; } + + public DayOfWeek getDayOfWeek() { + return dayOfWeek; + } + + public void setDayOfWeek(DayOfWeek dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } } \ No newline at end of file diff --git a/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java b/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java index 393120c..3d301fa 100644 --- a/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java +++ b/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java @@ -40,9 +40,9 @@ private Constraint studentConflict(ConstraintFactory constraintFactory) { return constraintFactory.forEach(ConflictingUnit.class) .join(Unit.class, Joiners.equal(ConflictingUnit::getUnit1, Function.identity())) .join(Unit.class, Joiners.equal((conflictingUnit, unit1) -> conflictingUnit.getUnit2(), Function.identity()), - overlapping((conflictingUnit, unit1) -> unit1.getStart(), + overlapping((conflictingUnit, unit1) -> unit1.getStartTime(), (conflictingUnit, unit1) -> unit1.getEnd(), - Unit::getStart, Unit::getEnd)) + Unit::getStartTime, Unit::getEnd)) .penalize(HardSoftScore.ofHard(1), (conflictingUnit, unit1, unit2) -> conflictingUnit.getNumStudent()) .asConstraint("Student conflict"); @@ -57,7 +57,7 @@ Constraint roomConflict(ConstraintFactory constraintFactory) { // Select each pair of 2 different lessons ... .forEachUniquePair(Unit.class, // ... in the same timeslot ... - overlapping(Unit::getStart, Unit::getEnd), + overlapping(Unit::getStartTime, Unit::getEnd), // ... in the same room ... Joiners.equal(Unit::getRoom)) // ... and penalize each pair with a hard weight. From 39756dceed43680b1818f5eb24fa059c9db180ad Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Sat, 14 Sep 2024 20:16:43 +1000 Subject: [PATCH 16/40] feat: add the day-of-week to Timetable --- .../main/java/org/acme/domain/Timetable.java | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/backend/src/main/java/org/acme/domain/Timetable.java b/backend/src/main/java/org/acme/domain/Timetable.java index b340c92..147edf5 100644 --- a/backend/src/main/java/org/acme/domain/Timetable.java +++ b/backend/src/main/java/org/acme/domain/Timetable.java @@ -7,6 +7,7 @@ import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore; +import java.time.DayOfWeek; import java.time.LocalTime; import java.util.ArrayList; import java.util.List; @@ -20,12 +21,14 @@ public class Timetable { @PlanningEntityCollectionProperty - List units; + private List units; @ValueRangeProvider - List startTimes; + private List daysOfWeek; + @ValueRangeProvider + private List startTimes; @PlanningScore - HardSoftScore score; + private HardSoftScore score; @ProblemFactCollectionProperty @ValueRangeProvider private List rooms; @@ -58,6 +61,29 @@ public Timetable(List units, List startTimes, List rooms) this.rooms = rooms; } + /** + * Creates a timetable. + * + * @param units The list of units to be allocated. + * @param daysOfWeek The list of available days of the week. + * @param startTimes The list of available starting times. + * @param rooms The list of available rooms. + */ + public Timetable(List units, List daysOfWeek, List startTimes, List rooms) { + this.units = units; + this.daysOfWeek = daysOfWeek; + this.startTimes = startTimes; + this.rooms = rooms; + } + + public List getDaysOfWeek() { + return daysOfWeek; + } + + public void setDaysOfWeek(List daysOfWeek) { + this.daysOfWeek = daysOfWeek; + } + public List getStartTimes() { return startTimes; } From 2e1d2e50dd054581b356c854bb7d58efd7b8ca3c Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Sat, 14 Sep 2024 20:28:53 +1000 Subject: [PATCH 17/40] feat: add day-of-week to constraints, add comments to explain the constraints --- .../main/java/org/acme/domain/Student.java | 2 +- .../solver/TimetableConstraintProvider.java | 28 ++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/backend/src/main/java/org/acme/domain/Student.java b/backend/src/main/java/org/acme/domain/Student.java index 2d7cd6d..42093dd 100644 --- a/backend/src/main/java/org/acme/domain/Student.java +++ b/backend/src/main/java/org/acme/domain/Student.java @@ -10,7 +10,7 @@ public class Student { // String studentID; -// @PlanningId + String name; public Student() { diff --git a/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java b/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java index 3d301fa..af0f1b8 100644 --- a/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java +++ b/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java @@ -10,6 +10,7 @@ import java.util.function.Function; +import static ai.timefold.solver.core.api.score.stream.Joiners.equal; import static ai.timefold.solver.core.api.score.stream.Joiners.overlapping; /** @@ -37,12 +38,21 @@ public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { * Penalize 1 hard score for each student with overlapping units. */ private Constraint studentConflict(ConstraintFactory constraintFactory) { - return constraintFactory.forEach(ConflictingUnit.class) - .join(Unit.class, Joiners.equal(ConflictingUnit::getUnit1, Function.identity())) - .join(Unit.class, Joiners.equal((conflictingUnit, unit1) -> conflictingUnit.getUnit2(), Function.identity()), + // A student can be in at most one unit at the same time. + return constraintFactory + // Select each pair of conflicting units. + .forEach(ConflictingUnit.class) + // Find the first unit. + .join(Unit.class, equal(ConflictingUnit::getUnit1, Function.identity())) + // Find the second unit. + .join(Unit.class, equal((conflictingUnit, unit1) -> conflictingUnit.getUnit2(), Function.identity()), + // Check if the 2 units are on the same weekday ... + equal((conflictingUnit, unit1) -> unit1.getDayOfWeek(), Unit::getDayOfWeek), + // ... in the same timeslot ... overlapping((conflictingUnit, unit1) -> unit1.getStartTime(), (conflictingUnit, unit1) -> unit1.getEnd(), Unit::getStartTime, Unit::getEnd)) + // ... and penalize each pair with a hard weight. .penalize(HardSoftScore.ofHard(1), (conflictingUnit, unit1, unit2) -> conflictingUnit.getNumStudent()) .asConstraint("Student conflict"); @@ -52,14 +62,16 @@ private Constraint studentConflict(ConstraintFactory constraintFactory) { * Penalize 1 hard score for each room with overlapping units. */ Constraint roomConflict(ConstraintFactory constraintFactory) { - // A room can accommodate at most one lesson at the same time. + // A room can accommodate at most one unit at the same time. return constraintFactory - // Select each pair of 2 different lessons ... + // Select each pair of 2 different units ... .forEachUniquePair(Unit.class, + // ... on the same weekday ... + equal(Unit::getDayOfWeek), // ... in the same timeslot ... overlapping(Unit::getStartTime, Unit::getEnd), // ... in the same room ... - Joiners.equal(Unit::getRoom)) + equal(Unit::getRoom)) // ... and penalize each pair with a hard weight. .penalize(HardSoftScore.ofHard(1)) .asConstraint("Room conflict"); @@ -69,7 +81,9 @@ Constraint roomConflict(ConstraintFactory constraintFactory) { * Penalize 1 soft score for each student overflowing the capacity of the room. */ Constraint roomCapacity(ConstraintFactory constraintFactory) { - return constraintFactory.forEach(Unit.class) + // A room cannot accommodate more students than its capacity. + return constraintFactory + .forEach(Unit.class) .filter(unit -> unit.getStudentSize() > unit.getRoom().getCapacity()) .penalize(HardSoftScore.ofSoft(1), unit -> unit.getStudentSize() - unit.getRoom().getCapacity()) .asConstraint("Room capacity conflict"); From bab1a2eec4f8ec393007d15befe1a93fa5c5174f Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Sat, 14 Sep 2024 20:42:24 +1000 Subject: [PATCH 18/40] feat: add day-of-week to problem in TimetableResource --- .../main/java/org/acme/TimetableResource.java | 16 +++++++++++++--- .../java/org/acme/domain/ConflictingUnit.java | 6 +++--- .../src/main/java/org/acme/domain/Timetable.java | 12 +++++++----- backend/src/main/java/org/acme/domain/Unit.java | 6 +++--- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/backend/src/main/java/org/acme/TimetableResource.java b/backend/src/main/java/org/acme/TimetableResource.java index 11e28f4..2202575 100644 --- a/backend/src/main/java/org/acme/TimetableResource.java +++ b/backend/src/main/java/org/acme/TimetableResource.java @@ -11,6 +11,7 @@ import org.acme.domain.Timetable; import org.acme.domain.Unit; +import java.time.DayOfWeek; import java.time.Duration; import java.time.LocalTime; import java.util.List; @@ -56,17 +57,26 @@ public Timetable hello() throws ExecutionException, InterruptedException { // new Unit(5, "5", Duration.ofHours(2), List.of(c, d, e)), // new Unit(6, "6", Duration.ofHours(2), List.of(f, g, h, i)) ), + + List.of( + DayOfWeek.MONDAY, + DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY +// DayOfWeek.THURSDAY, +// DayOfWeek.FRIDAY + ), + List.of( - LocalTime.of(15, 0), - LocalTime.of(17, 0) + LocalTime.of(15, 0) +// LocalTime.of(17, 0) // LocalTime.of(16,0), // LocalTime.of(23,0) ), List.of(r1, r2, r3) ); - Timetable solution = solverManager.solve("job 1", problem).getFinalBestSolution(); + return solution; } diff --git a/backend/src/main/java/org/acme/domain/ConflictingUnit.java b/backend/src/main/java/org/acme/domain/ConflictingUnit.java index 20444ff..14be70b 100644 --- a/backend/src/main/java/org/acme/domain/ConflictingUnit.java +++ b/backend/src/main/java/org/acme/domain/ConflictingUnit.java @@ -6,11 +6,11 @@ * @author Jet Edge */ public class ConflictingUnit { - Unit unit1; + private Unit unit1; - Unit unit2; + private Unit unit2; - int numStudent; + private int numStudent; /** * Creates a pair of conflicting units. diff --git a/backend/src/main/java/org/acme/domain/Timetable.java b/backend/src/main/java/org/acme/domain/Timetable.java index 147edf5..b6f9826 100644 --- a/backend/src/main/java/org/acme/domain/Timetable.java +++ b/backend/src/main/java/org/acme/domain/Timetable.java @@ -20,19 +20,21 @@ @PlanningSolution public class Timetable { - @PlanningEntityCollectionProperty - private List units; - @ValueRangeProvider private List daysOfWeek; @ValueRangeProvider private List startTimes; - @PlanningScore - private HardSoftScore score; + @ProblemFactCollectionProperty @ValueRangeProvider private List rooms; + @PlanningEntityCollectionProperty + private List units; + + @PlanningScore + private HardSoftScore score; + public Timetable() { } diff --git a/backend/src/main/java/org/acme/domain/Unit.java b/backend/src/main/java/org/acme/domain/Unit.java index 4be353a..e09a274 100644 --- a/backend/src/main/java/org/acme/domain/Unit.java +++ b/backend/src/main/java/org/acme/domain/Unit.java @@ -18,16 +18,16 @@ @PlanningEntity public class Unit { - List students; + private List students; @PlanningId private int unitID; private String name; private Duration duration; @PlanningVariable - private LocalTime startTime; - @PlanningVariable private DayOfWeek dayOfWeek; @PlanningVariable + private LocalTime startTime; + @PlanningVariable private Room room; public Unit() { From d960b1a644f27490a800df78c98ba3d36dbeb1b7 Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Sat, 14 Sep 2024 20:52:25 +1000 Subject: [PATCH 19/40] feat: add isLab attribute to Room, whether it is a laboratory --- .../src/main/java/org/acme/TimetableResource.java | 6 +++--- backend/src/main/java/org/acme/domain/Room.java | 14 +++++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/backend/src/main/java/org/acme/TimetableResource.java b/backend/src/main/java/org/acme/TimetableResource.java index 2202575..0efb26b 100644 --- a/backend/src/main/java/org/acme/TimetableResource.java +++ b/backend/src/main/java/org/acme/TimetableResource.java @@ -44,9 +44,9 @@ public Timetable hello() throws ExecutionException, InterruptedException { Student h = new Student("h"); Student i = new Student("i"); - Room r1 = new Room("Room1", 2); - Room r2 = new Room("Room2", 3); - Room r3 = new Room("Room3", 4); + Room r1 = new Room("Room1", 2, true); + Room r2 = new Room("Room2", 3, false); + Room r3 = new Room("Room3", 4, false); var problem = new Timetable( List.of( diff --git a/backend/src/main/java/org/acme/domain/Room.java b/backend/src/main/java/org/acme/domain/Room.java index ef74552..57c7bbd 100644 --- a/backend/src/main/java/org/acme/domain/Room.java +++ b/backend/src/main/java/org/acme/domain/Room.java @@ -10,7 +10,9 @@ public class Room { @PlanningId private String id; + private int capacity; + private boolean isLab; public Room() { } @@ -20,10 +22,12 @@ public Room() { * * @param id The room’s id. * @param capacity The room's capacity. + * @param isLab Whether the room is a laboratory. */ - public Room(String id, int capacity) { + public Room(String id, int capacity, boolean isLab) { this.id = id; this.capacity = capacity; + this.isLab = isLab; } public String getId() { @@ -41,4 +45,12 @@ public int getCapacity() { public void setCapacity(int capacity) { this.capacity = capacity; } + + public boolean isLab() { + return isLab; + } + + public void setLab(boolean lab) { + isLab = lab; + } } \ No newline at end of file From b82d52f0ee73dd2a8c1a600a01c83c9ff7dd0c37 Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Sat, 14 Sep 2024 20:57:31 +1000 Subject: [PATCH 20/40] feat: add wantsLab attribute to Unit, whether it prefers a laboratory room --- .../main/java/org/acme/TimetableResource.java | 6 ++--- .../src/main/java/org/acme/domain/Unit.java | 24 +++++++++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/backend/src/main/java/org/acme/TimetableResource.java b/backend/src/main/java/org/acme/TimetableResource.java index 0efb26b..dc1ffd9 100644 --- a/backend/src/main/java/org/acme/TimetableResource.java +++ b/backend/src/main/java/org/acme/TimetableResource.java @@ -50,9 +50,9 @@ public Timetable hello() throws ExecutionException, InterruptedException { var problem = new Timetable( List.of( - new Unit(1, "1", Duration.ofHours(2), List.of(a, b)), - new Unit(2, "2", Duration.ofHours(2), List.of(a, c, d, e)), - new Unit(3, "3", Duration.ofHours(2), List.of(f, g, h, i)) + new Unit(1, "1", Duration.ofHours(2), List.of(a, b), true), + new Unit(2, "2", Duration.ofHours(2), List.of(a, c, d, e), true), + new Unit(3, "3", Duration.ofHours(2), List.of(f, g, h, i), false) // new Unit(4, "4", Duration.ofHours(2), List.of(a, b)), // new Unit(5, "5", Duration.ofHours(2), List.of(c, d, e)), // new Unit(6, "6", Duration.ofHours(2), List.of(f, g, h, i)) diff --git a/backend/src/main/java/org/acme/domain/Unit.java b/backend/src/main/java/org/acme/domain/Unit.java index e09a274..570c9b7 100644 --- a/backend/src/main/java/org/acme/domain/Unit.java +++ b/backend/src/main/java/org/acme/domain/Unit.java @@ -30,6 +30,8 @@ public class Unit { @PlanningVariable private Room room; + private boolean wantsLab; + public Unit() { } @@ -55,14 +57,14 @@ public Unit(int unitID, String name, Duration duration, List students) * @param name The unit’s ID. * @param duration The unit’s duration. * @param students The list of students enrolled in the unit. - * @param room The room assigned to the unit. + * @param wantsLab Whether the unit wants a laboratory room. */ - public Unit(int unitID, String name, Duration duration, List students, Room room) { + public Unit(int unitID, String name, Duration duration, List students, boolean wantsLab) { this.unitID = unitID; this.name = name; this.duration = duration; this.students = students; - this.room = room; + this.wantsLab = wantsLab; } public int getUnitID() { @@ -89,6 +91,14 @@ public void setDuration(Duration duration) { this.duration = duration; } + public DayOfWeek getDayOfWeek() { + return dayOfWeek; + } + + public void setDayOfWeek(DayOfWeek dayOfWeek) { + this.dayOfWeek = dayOfWeek; + } + public LocalTime getStartTime() { return startTime; } @@ -126,11 +136,11 @@ public void setRoom(Room room) { this.room = room; } - public DayOfWeek getDayOfWeek() { - return dayOfWeek; + public boolean isWantsLab() { + return wantsLab; } - public void setDayOfWeek(DayOfWeek dayOfWeek) { - this.dayOfWeek = dayOfWeek; + public void setWantsLab(boolean wantsLab) { + this.wantsLab = wantsLab; } } \ No newline at end of file From a23e2a7497667ac25227b697cf29e2745b91a831 Mon Sep 17 00:00:00 2001 From: tungkhanhh Date: Sat, 14 Sep 2024 21:15:13 +1000 Subject: [PATCH 21/40] feat: add lab preference constraint, lab units prefer lab rooms --- .../main/java/org/acme/TimetableResource.java | 6 ++--- .../solver/TimetableConstraintProvider.java | 25 ++++++++++++++++--- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/org/acme/TimetableResource.java b/backend/src/main/java/org/acme/TimetableResource.java index dc1ffd9..93cd1e3 100644 --- a/backend/src/main/java/org/acme/TimetableResource.java +++ b/backend/src/main/java/org/acme/TimetableResource.java @@ -45,15 +45,15 @@ public Timetable hello() throws ExecutionException, InterruptedException { Student i = new Student("i"); Room r1 = new Room("Room1", 2, true); - Room r2 = new Room("Room2", 3, false); + Room r2 = new Room("Room2", 4, false); Room r3 = new Room("Room3", 4, false); var problem = new Timetable( List.of( new Unit(1, "1", Duration.ofHours(2), List.of(a, b), true), new Unit(2, "2", Duration.ofHours(2), List.of(a, c, d, e), true), - new Unit(3, "3", Duration.ofHours(2), List.of(f, g, h, i), false) -// new Unit(4, "4", Duration.ofHours(2), List.of(a, b)), + new Unit(3, "3", Duration.ofHours(2), List.of(f, g, h, i), false), + new Unit(4, "4", Duration.ofHours(2), List.of(a, b), false) // new Unit(5, "5", Duration.ofHours(2), List.of(c, d, e)), // new Unit(6, "6", Duration.ofHours(2), List.of(f, g, h, i)) ), diff --git a/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java b/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java index af0f1b8..4e06a01 100644 --- a/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java +++ b/backend/src/main/java/org/acme/solver/TimetableConstraintProvider.java @@ -4,7 +4,6 @@ import ai.timefold.solver.core.api.score.stream.Constraint; import ai.timefold.solver.core.api.score.stream.ConstraintFactory; import ai.timefold.solver.core.api.score.stream.ConstraintProvider; -import ai.timefold.solver.core.api.score.stream.Joiners; import org.acme.domain.ConflictingUnit; import org.acme.domain.Unit; @@ -30,7 +29,8 @@ public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { return new Constraint[]{ studentConflict(constraintFactory), roomConflict(constraintFactory), - roomCapacity(constraintFactory) + roomCapacity(constraintFactory), + labPreference(constraintFactory) }; } @@ -53,7 +53,8 @@ private Constraint studentConflict(ConstraintFactory constraintFactory) { (conflictingUnit, unit1) -> unit1.getEnd(), Unit::getStartTime, Unit::getEnd)) // ... and penalize each pair with a hard weight. - .penalize(HardSoftScore.ofHard(1), (conflictingUnit, unit1, unit2) -> conflictingUnit.getNumStudent()) + .penalize(HardSoftScore.ofHard(1), + (conflictingUnit, unit1, unit2) -> conflictingUnit.getNumStudent()) .asConstraint("Student conflict"); } @@ -85,8 +86,24 @@ Constraint roomCapacity(ConstraintFactory constraintFactory) { return constraintFactory .forEach(Unit.class) .filter(unit -> unit.getStudentSize() > unit.getRoom().getCapacity()) - .penalize(HardSoftScore.ofSoft(1), unit -> unit.getStudentSize() - unit.getRoom().getCapacity()) + .penalize(HardSoftScore.ofSoft(1), + unit -> unit.getStudentSize() - unit.getRoom().getCapacity()) .asConstraint("Room capacity conflict"); } + /** + * Penalize 1 soft score for each laboratory unit not assigned to a laboratory. + */ + Constraint labPreference(ConstraintFactory constraintFactory) { + // Some units prefer to have a laboratory room. + return constraintFactory + .forEach(Unit.class) + // Select a laboratory unit ... + .filter(Unit::isWantsLab) + // ... in a non-lab room ... + .filter(unit -> !unit.getRoom().isLab()) + .penalize(HardSoftScore.ofSoft(1)) + .asConstraint("Unit laboratory preference"); + } + } \ No newline at end of file From 0e4a78f60784040b037845423effbd35ac95052c Mon Sep 17 00:00:00 2001 From: Giang Vu Date: Sun, 15 Sep 2024 13:08:01 +1000 Subject: [PATCH 22/40] feat: store user input data in indexedDB; remove sessionStorage --- frontend/package-lock.json | 183 ++++++++++++++++++- frontend/package.json | 5 +- frontend/src/components/ProceedButton.tsx | 16 +- frontend/src/components/Sidebar.tsx | 17 -- frontend/src/components/Spreadsheet.tsx | 55 +++--- frontend/src/components/UploadButton.tsx | 36 ++-- frontend/src/pages/TimetableMod.tsx | 2 +- frontend/src/pages/spreadsheets/Building.tsx | 2 +- frontend/src/pages/spreadsheets/Campus.tsx | 11 -- frontend/src/pages/spreadsheets/Course.tsx | 11 -- frontend/src/pages/spreadsheets/Room.tsx | 4 +- frontend/src/pages/spreadsheets/Unit.tsx | 2 +- frontend/src/routes.tsx | 4 - frontend/src/scripts/persistence.ts | 118 ++++++++++++ 14 files changed, 354 insertions(+), 112 deletions(-) delete mode 100644 frontend/src/pages/spreadsheets/Campus.tsx delete mode 100644 frontend/src/pages/spreadsheets/Course.tsx create mode 100644 frontend/src/scripts/persistence.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index edf8bbb..faf991f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,10 +13,13 @@ "@mui/base": "^5.0.0-beta.40", "@mui/icons-material": "^5.16.7", "@mui/material": "^5.16.7", + "dexie": "^4.0.8", + "dexie-react-hooks": "^1.1.7", "jspreadsheet-ce": "^4.2.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.26.1" + "react-router-dom": "^6.26.1", + "read-excel-file": "^5.8.5" }, "devDependencies": { "@eslint/js": "^9.9.0", @@ -2025,6 +2028,15 @@ "vite": "^4.2.0 || ^5.0.0" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", + "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -2126,6 +2138,12 @@ "dev": true, "license": "MIT" }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2273,6 +2291,12 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -2334,6 +2358,23 @@ "dev": true, "license": "MIT" }, + "node_modules/dexie": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.8.tgz", + "integrity": "sha512-1G6cJevS17KMDK847V3OHvK2zei899GwpDiqfEXHP1ASvme6eWJmAp9AU4s1son2TeGkWmC0g3y8ezOBPnalgQ==", + "license": "Apache-2.0" + }, + "node_modules/dexie-react-hooks": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/dexie-react-hooks/-/dexie-react-hooks-1.1.7.tgz", + "integrity": "sha512-Lwv5W0Hk+uOW3kGnsU9GZoR1er1B7WQ5DSdonoNG+focTNeJbHW6vi6nBoX534VKI3/uwHebYzSw1fwY6a7mTw==", + "license": "Apache-2.0", + "peerDependencies": { + "@types/react": ">=16", + "dexie": "^3.2 || ^4.0.1-alpha", + "react": ">=16" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2357,6 +2398,15 @@ "csstype": "^3.0.2" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.13", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", @@ -2748,6 +2798,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2818,6 +2874,20 @@ "dev": true, "license": "ISC" }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2899,6 +2969,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -2978,6 +3054,12 @@ "node": ">=0.8.19" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -3042,6 +3124,12 @@ "node": ">=8" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3120,6 +3208,18 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jspreadsheet-ce": { "version": "4.13.4", "resolved": "https://registry.npmjs.org/jspreadsheet-ce/-/jspreadsheet-ce-4.13.4.tgz", @@ -3278,6 +3378,12 @@ "dev": true, "license": "MIT" }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -3467,6 +3573,12 @@ "node": ">= 0.8.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -3604,6 +3716,32 @@ "react-dom": ">=16.6.0" } }, + "node_modules/read-excel-file": { + "version": "5.8.5", + "resolved": "https://registry.npmjs.org/read-excel-file/-/read-excel-file-5.8.5.tgz", + "integrity": "sha512-KDDcSsI3VzXTNUBs8q7RwTYrGRE8RZgNwGUivYq13bQtMp1KJmocyBs/EiPTJaFk4I8Ri9iDF+ht2A4GUrudMg==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.8.2", + "fflate": "^0.7.3", + "unzipper": "^0.12.2" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -3707,6 +3845,12 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -3778,6 +3922,15 @@ "node": ">=0.10.0" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -3927,6 +4080,28 @@ } } }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unzipper": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz", + "integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==", + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "^11.2.0", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", @@ -3968,6 +4143,12 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, "node_modules/vite": { "version": "5.4.2", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 36456be..6614815 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,10 +15,13 @@ "@mui/base": "^5.0.0-beta.40", "@mui/icons-material": "^5.16.7", "@mui/material": "^5.16.7", + "dexie": "^4.0.8", + "dexie-react-hooks": "^1.1.7", "jspreadsheet-ce": "^4.2.1", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.26.1" + "react-router-dom": "^6.26.1", + "read-excel-file": "^5.8.5" }, "devDependencies": { "@eslint/js": "^9.9.0", diff --git a/frontend/src/components/ProceedButton.tsx b/frontend/src/components/ProceedButton.tsx index e096e6a..8630d0c 100644 --- a/frontend/src/components/ProceedButton.tsx +++ b/frontend/src/components/ProceedButton.tsx @@ -5,11 +5,11 @@ interface ProceedButtonProps { fileChosen: File | null; } -export default function ProceedButton({ fileChosen }:ProceedButtonProps) { - const navigate = useNavigate(); - if (fileChosen === null) { - return - } else { - return ; - } -}` ` \ No newline at end of file +export default function ProceedButton({ fileChosen }: ProceedButtonProps) { + const navigate = useNavigate(); + if (fileChosen === null) { + return + } else { + return ; + } +} \ No newline at end of file diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index a51587f..759ace2 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -32,14 +32,6 @@ export default function Sidebar({ marginTop, width }: SidebarProps) { anchor="left" > - - - - - - - - @@ -59,15 +51,6 @@ export default function Sidebar({ marginTop, width }: SidebarProps) { - - - - - - - - - diff --git a/frontend/src/components/Spreadsheet.tsx b/frontend/src/components/Spreadsheet.tsx index 7af472f..20ce1f8 100644 --- a/frontend/src/components/Spreadsheet.tsx +++ b/frontend/src/components/Spreadsheet.tsx @@ -1,18 +1,15 @@ import { useRef, useEffect } from "react"; -import jspreadsheet, { CellValue, JspreadsheetInstance, JspreadsheetInstanceElement, JSpreadsheetOptions } from "jspreadsheet-ce"; +import jspreadsheet, { JspreadsheetInstance, JspreadsheetInstanceElement } from "jspreadsheet-ce"; import "../../node_modules/jspreadsheet-ce/dist/jspreadsheet.css"; import "../styles/spreadsheet.css" import Button from "@mui/material/Button"; +import { getSpreadsheetData, storeSpreadsheetData } from "../scripts/persistence"; interface SpreadsheetProps { headers: string[]; storageKey: string; } -type SavedSpreadsheetOpt = { - cellvalues: CellValue[][]; -} - export default function Spreadsheet({ headers, storageKey }: SpreadsheetProps) { const jRef = useRef(null); @@ -70,7 +67,7 @@ export default function Spreadsheet({ headers, storageKey }: SpreadsheetProps) { } } }); - + return items; }; @@ -107,44 +104,44 @@ export default function Spreadsheet({ headers, storageKey }: SpreadsheetProps) { // ], }; - // mount: create spreadsheet using data from sessionStorage (if exist), + // mount: create spreadsheet using data from indexedDB (if exist), // otherwise create a blank default. useEffect(() => { // console.log(`Mount ${storageKey}`); - if (jRef.current && !jRef.current.jspreadsheet) { - const savedSpreadsheetData = sessionStorage.getItem(storageKey); - - if (savedSpreadsheetData) { - const ssd: SavedSpreadsheetOpt = JSON.parse(savedSpreadsheetData); - options.data = ssd.cellvalues; - } - - jspreadsheet(jRef.current, options); - } + getSpreadsheetData(storageKey) + .then((data) => { + if (data && jRef.current && !jRef.current.jspreadsheet) { + options.data = data; + jspreadsheet(jRef.current, options); + } + }); }); - // unmount: save spreadsheet data to sessionStorage + // unmount: save spreadsheet data to indexedDB useEffect(() => { const instanceElem: JspreadsheetInstanceElement | null = jRef.current; function cleanup() { // console.log(`Unmount ${storageKey}`); - if (instanceElem) { - const newOpts: SavedSpreadsheetOpt = { - cellvalues: instanceElem.jspreadsheet.getData(), - }; - sessionStorage.setItem(storageKey, JSON.stringify(newOpts)); - } - else { - throw new Error( - "JspreadsheetInstanceElement is null" - ) + if (instanceElem && instanceElem.jspreadsheet) { + const data = instanceElem.jspreadsheet.getJson(false); + storeSpreadsheetData(data, storageKey); } } - return cleanup; }) + // page refresh: save spreadsheet data to indexedDB + useEffect(() => { + window.addEventListener("beforeunload", () => { + const instanceElem: JspreadsheetInstanceElement | null = jRef.current; + if (instanceElem && instanceElem.jspreadsheet) { + const data = instanceElem.jspreadsheet.getJson(false); + storeSpreadsheetData(data, storageKey); + } + }); + }) + const addRow = () => { if (jRef.current && jRef.current.jexcel) { jRef.current.jexcel.insertRow(); diff --git a/frontend/src/components/UploadButton.tsx b/frontend/src/components/UploadButton.tsx index 7340f08..d2767b5 100644 --- a/frontend/src/components/UploadButton.tsx +++ b/frontend/src/components/UploadButton.tsx @@ -2,6 +2,7 @@ import React, { useState } from "react"; import { styled } from "@mui/material/styles"; import Button from "@mui/material/Button"; import UploadFileIcon from "@mui/icons-material/UploadFile"; +import { getFile, storeFile } from "../scripts/persistence"; interface InputFileUploadProps { setFileChosen: (file: File | null) => void; @@ -20,39 +21,24 @@ const VisuallyHiddenInput = styled("input")({ }); export default function UploadButton ({ setFileChosen }: InputFileUploadProps) { - const [file, setFile] = useState(null); // Handler for file selection const handleFileChange = (event: React.ChangeEvent) => { if (event.target.files && event.target.files.length > 0) { - setFile(event.target.files[0]); - setFileChosen(event.target.files[0]); + storeFile(event.target.files[0]) + .then(() => { + return getFile(); + }) + .then((file) => { + setFileChosen(file); + }) + .catch((error) => { + alert("Upload failed. Please try again. " + error); + }) console.log("File selected:", event.target.files[0]); } }; - // Example of sending the file to a server - const handleUpload = () => { - if (!file) { - console.error("No file selected for upload."); - return; - } - - const formData = new FormData(); - formData.append("file", file); - - fetch("http://url.com", { - method: "POST", - body: formData, - }) - .then((response) => response.json()) - .then((data) => { - console.log("File uploaded successfully:", data); - }) - .catch((error) => { - console.error("Error uploading file:", error); - }); - }; return ( +
+ + +
+ + + ) +} \ No newline at end of file diff --git a/frontend/src/pages/TimetableMod.tsx b/frontend/src/pages/TimetableMod.tsx index f9f44ad..9872287 100644 --- a/frontend/src/pages/TimetableMod.tsx +++ b/frontend/src/pages/TimetableMod.tsx @@ -4,7 +4,7 @@ export default function TimetableMod() { return ( <>

This is the page to modify generated timetable

- Go Back + Go Back Go to Next ) diff --git a/frontend/src/pages/spreadsheets/Building.tsx b/frontend/src/pages/spreadsheets/Building.tsx index 0dc8de4..d195d23 100644 --- a/frontend/src/pages/spreadsheets/Building.tsx +++ b/frontend/src/pages/spreadsheets/Building.tsx @@ -1,11 +1,15 @@ import Spreadsheet from '../../components/Spreadsheet.tsx' +import { DB_BUILDINGS } from '../../scripts/persistence.ts'; export default function Building() { return ( <>

Building

- + ); }; \ No newline at end of file diff --git a/frontend/src/pages/spreadsheets/Room.tsx b/frontend/src/pages/spreadsheets/Room.tsx index 1b130af..42b78bd 100644 --- a/frontend/src/pages/spreadsheets/Room.tsx +++ b/frontend/src/pages/spreadsheets/Room.tsx @@ -1,4 +1,5 @@ import Spreadsheet from '../../components/Spreadsheet.tsx' +import { DB_ROOMS } from '../../scripts/persistence.ts'; export default function Room() { @@ -6,8 +7,9 @@ export default function Room() { <>

Room

); diff --git a/frontend/src/pages/spreadsheets/Unit.tsx b/frontend/src/pages/spreadsheets/Unit.tsx index 237a4fe..876cd71 100644 --- a/frontend/src/pages/spreadsheets/Unit.tsx +++ b/frontend/src/pages/spreadsheets/Unit.tsx @@ -1,11 +1,16 @@ import Spreadsheet from '../../components/Spreadsheet.tsx' +import { DB_UNITS } from '../../scripts/persistence.ts'; export default function Unit() { return ( <>

Unit

- + ); }; \ No newline at end of file diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index ab961be..14b1270 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -5,6 +5,7 @@ import Room from './pages/spreadsheets/Room.tsx' import Unit from './pages/spreadsheets/Unit.tsx' import Download from './pages/Download.tsx' import Enrolment from './pages/Enrolment.tsx' +import SendData from './pages/SendData.tsx' const routes = [ @@ -21,6 +22,10 @@ const routes = [ { path: "unit", element: }, ], }, + { + path: "senddata", + element: , + }, { path: "timetablemod", element: , diff --git a/frontend/src/scripts/api.ts b/frontend/src/scripts/api.ts new file mode 100644 index 0000000..dfd3357 --- /dev/null +++ b/frontend/src/scripts/api.ts @@ -0,0 +1,68 @@ +const API_URL = 'http://localhost:8080/timetable'; + +export type TimetableProblem = TimetableBase & { + units: Unit[], +} + +export type TimetableSolution = TimetableBase & { + units: Required, +} + +export type TimetableBase = { + daysOfWeek: Weekday[], + startTimes: Time[], + rooms: Room[] +} + +export type Unit = { + unitID: number, + name: string, + duration: number, + students: Student[], + wantsLab: boolean, + // fields to be assigned by backend's algorithm + room?: Room, + studentSize?: number + dayOfWeek?: Weekday, + start?: Time, + end?: Time, +}; + +export type Student = { + name: string +}; + +export type Room = { + id: string, + capacity: number, + lab: boolean +} + +export type Weekday = "MONDAY" | "TUESDAY" | "WEDNESDAY" | "THURSDAY" | "FRIDAY" + +export type Time = string; + + +// API function(s) +export async function fetchTimetableSolution(problem: TimetableProblem): Promise { + try { + const response = await fetch(API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(problem) + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`); + } + + const solution: TimetableSolution = await response.json(); + return solution; + } + catch (error) { + console.log(error); + return null; + } +} \ No newline at end of file diff --git a/frontend/src/scripts/handleInput.ts b/frontend/src/scripts/handleInput.ts new file mode 100644 index 0000000..dcf08c6 --- /dev/null +++ b/frontend/src/scripts/handleInput.ts @@ -0,0 +1,135 @@ +import readXlsxFile, { Row } from 'read-excel-file'; +import { CellValue } from 'jspreadsheet-ce'; +import { TimetableProblem, Unit, Room, Student, Weekday } from './api'; +import { DB_UNITS, storeSpreadsheetData } from './persistence'; +import { duration } from '@mui/material'; + +function isExcelFile(file: File) { + const fileExtension = file.name.split('.').pop(); + if (fileExtension === undefined || !['xlsx', 'xls'].includes(fileExtension)) { + alert("Wrong file type, file type must be .xlsx or .xls"); + return false; + } + return true; +} + +function validateEnrolmentHeader(inputHeader: Row) { + const header = ['StudentID', 'Student Name', 'Personal Email', 'University Email', + 'Student Type', 'Offer Type', 'Course Name', 'Campus', 'Original COE Start Date', + 'Course Start Date', 'Course End Date', 'COE Status', 'Specialisation', 'Pathway Indicator']; + + if (inputHeader.length >= header.length && JSON.stringify(header) === JSON.stringify(inputHeader.slice(0, header.length))) { + return true; + } + else { + alert("Enrolment data header row is invalid"); + return false; + } +} + +export async function getUnitsList(enrolmentExcel: File) { + if (!isExcelFile(enrolmentExcel)) { + throw new Error( + "File is not .xlsx or .xls" + ) + } + + const [header] = await readXlsxFile(enrolmentExcel); + + if (!validateEnrolmentHeader(header)) { + throw new Error( + "Enrolment data has wrong headers" + ) + } + + // console.log(header.slice(14)); + const unitsList = header.slice(14).map(elem => elem.toString()); + const unitsData: Record[] = unitsList.map((u) => { + return { 0: u }; + }); + + storeSpreadsheetData(unitsData, DB_UNITS); + + return enrolmentExcel; +} + +export async function getTimetableProblem(enrolmentExcel: File, roomSpreadsheet: Record[], unitSpreadsheet: Record[]) { + if (!isExcelFile(enrolmentExcel)) { + throw new Error( + "File is not .xlsx or .xls" + ) + } + + const [header, ...body] = await readXlsxFile(enrolmentExcel); + + if (!validateEnrolmentHeader(header)) { + throw new Error( + "Enrolment data has wrong headers" + ) + } + + const unitsList = header.slice(14); + const units: Unit[] = unitsList.map((value, index) => { + return { + unitID: index, + name: value.toString(), + duration: 0, + students: [], + wantsLab: false + } + }); + + unitSpreadsheet.map((record, index) => { + const totalDuration = (parseInt(record['1'].toString()) + parseInt(record['2'].toString()) + parseInt(record['3'].toString())) * 60; + const wantsLab = parseInt(record['3'].toString()) > 0; + units[index].duration = totalDuration; + units[index].wantsLab = wantsLab; + }) + + // check each row and add students to each unit they're enrolled in + for (let i = 0; i < body.length; i++) { + const enrolments = body[i].slice(14); + for (let j = 0; j < enrolments.length; j++) { + if (enrolments[j] === "ENRL") { + units[j].students.push({ + name: body[i][0].toString() + }) + } + } + } + + const rooms: Room[] = roomSpreadsheet + .filter((record) => record['5'] as boolean) + .map((record) => { + return { + id: record['2'] as string, + capacity: record['3'] as number, + lab: record['4'] as boolean + } + }); + + + const problem: TimetableProblem = { + units: units, + daysOfWeek: [ + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY" + ], + startTimes: [ + "08:00:00", + "09:00:00", + "10:00:00", + "11:00:00", + "12:00:00", + "13:00:00", + ], + rooms: rooms + } + + console.log(problem); + + return problem; +} diff --git a/frontend/src/scripts/persistence.ts b/frontend/src/scripts/persistence.ts index b62d2a8..1ac5808 100644 --- a/frontend/src/scripts/persistence.ts +++ b/frontend/src/scripts/persistence.ts @@ -1,7 +1,10 @@ import Dexie, { EntityTable } from "dexie"; -import { CellValue, JspreadsheetInstanceElement } from "jspreadsheet-ce"; +import { CellValue } from "jspreadsheet-ce"; const DB_NAME = 'TimetableInput'; +const DB_BUILDINGS = 'buildings'; +const DB_ROOMS = 'rooms'; +const DB_UNITS = 'units'; interface FileRecord { @@ -30,35 +33,26 @@ db.version(1).stores({ }); export async function storeFile(file: File): Promise { - try { - await db.files.clear(); - const id = await db.files.add({ - id: 0, - file: file - }); - return id; - } - catch (error) { - throw error; - } + await db.files.clear(); + const id = await db.files.add({ + id: 0, + file: file + }); + return id; } -export async function getFile(): Promise { - try { - const file = await db.files.orderBy('id').first(); - if (file === undefined) { - return null; - } - return file.file; - } - catch (error) { - console.log(error); - return null; +export async function getFile(): Promise { + const file = await db.files.orderBy('id').first(); + + if (file === undefined) { + throw new Error( + "getFile() failed" + ); } + return file.file; } export async function storeSpreadsheetData(data: Record[], storageObject: string) { - if (!data) { return; } @@ -71,13 +65,13 @@ export async function storeSpreadsheetData(data: Record[], st }); try { - if (storageObject === "buildings") { + if (storageObject === DB_BUILDINGS) { await db.buildings.bulkPut(records); } - else if (storageObject === "rooms") { + else if (storageObject === DB_ROOMS) { await db.rooms.bulkPut(records); } - else if (storageObject === "units") { + else if (storageObject === DB_UNITS) { await db.units.bulkPut(records); } else { @@ -115,4 +109,6 @@ export async function getSpreadsheetData(storageObject: string): Promise Date: Sun, 15 Sep 2024 19:41:59 +1000 Subject: [PATCH 25/40] Revert "Revert "Merge pull request #8 from hotungkhanh/kan-41/database"" --- backend/pom.xml | 8 +++++ .../acme/schooltimetabling/domain/Campus.java | 21 +++++++++++++ .../domain/CampusResource.java | 30 +++++++++++++++++++ .../src/main/resources/application.properties | 11 ++++++- 4 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 backend/src/main/java/org/acme/schooltimetabling/domain/Campus.java create mode 100644 backend/src/main/java/org/acme/schooltimetabling/domain/CampusResource.java diff --git a/backend/pom.xml b/backend/pom.xml index afaa655..dd29581 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -62,6 +62,14 @@ rest-assured test + + io.quarkus + quarkus-jdbc-postgresql + + + io.quarkus + quarkus-hibernate-orm-panache + diff --git a/backend/src/main/java/org/acme/schooltimetabling/domain/Campus.java b/backend/src/main/java/org/acme/schooltimetabling/domain/Campus.java new file mode 100644 index 0000000..d4c49cc --- /dev/null +++ b/backend/src/main/java/org/acme/schooltimetabling/domain/Campus.java @@ -0,0 +1,21 @@ +package org.acme.schooltimetabling.domain; + +// import java.util.List; +import jakarta.persistence.*; +import io.quarkus.hibernate.orm.panache.PanacheEntity; + +@Entity +public class Campus extends PanacheEntity { + + public String name; + + // empty constructor + public Campus() { + } + + // constructor with name input + public Campus(String name) { + this.name = name; + } + +} \ No newline at end of file diff --git a/backend/src/main/java/org/acme/schooltimetabling/domain/CampusResource.java b/backend/src/main/java/org/acme/schooltimetabling/domain/CampusResource.java new file mode 100644 index 0000000..7b380c9 --- /dev/null +++ b/backend/src/main/java/org/acme/schooltimetabling/domain/CampusResource.java @@ -0,0 +1,30 @@ +package org.acme.schooltimetabling.domain; + +import java.util.List; + +import jakarta.transaction.Transactional; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@Path("/campuses") +public class CampusResource { + + @GET + @Produces(MediaType.APPLICATION_JSON) + public List list() { + return Campus.listAll(); + } + + @POST + @Transactional + @Consumes(MediaType.APPLICATION_JSON) + public Response createCampus(Campus campus) { + campus.persist(); + return Response.status(Response.Status.CREATED).entity(campus).build(); + } +} \ No newline at end of file diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index c99914e..43e49f3 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -4,4 +4,13 @@ quarkus.timefold.solver.termination.spent-limit=5s quarkus.http.cors=true quarkus.http.cors.origins=* quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS -quarkus.http.cors.headers=Content-Type,Authorization \ No newline at end of file +quarkus.http.cors.headers=Content-Type,Authorization + +# datasource config +quarkus.datasource.db-kind = postgresql +quarkus.datasource.username = ${QUARKUS_DATASOURCE_USERNAME} +quarkus.datasource.password = ${QUARKUS_DATASOURCE_PASSWORD} +quarkus.datasource.jdbc.url = ${QUARKUS_DATASOURCE_JDBC_URL} + +# drop and create the database at startup (use `update` to only update the schema) +quarkus.hibernate-orm.database.generation = update \ No newline at end of file From 4eb7c18aab0f02a0e55e3c6e9eaac90bbba9b6ae Mon Sep 17 00:00:00 2001 From: Giang Vu Date: Sun, 15 Sep 2024 20:55:32 +1000 Subject: [PATCH 26/40] feat: send + test display response --- backend/src/main/java/org/acme/TimetableResource.java | 8 ++++++++ frontend/src/pages/SendData.tsx | 11 +++++++---- frontend/src/scripts/api.ts | 3 ++- frontend/src/scripts/handleInput.ts | 3 +-- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/backend/src/main/java/org/acme/TimetableResource.java b/backend/src/main/java/org/acme/TimetableResource.java index 93cd1e3..72c1943 100644 --- a/backend/src/main/java/org/acme/TimetableResource.java +++ b/backend/src/main/java/org/acme/TimetableResource.java @@ -3,6 +3,7 @@ import ai.timefold.solver.core.api.solver.SolverManager; import jakarta.inject.Inject; import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; @@ -30,6 +31,13 @@ public class TimetableResource { @Inject SolverManager solverManager; + @POST + public Timetable handleRequest(Timetable problem) throws ExecutionException, InterruptedException { + + Timetable solution = solverManager.solve("job 1", problem).getFinalBestSolution(); + return solution; + } + @GET @Produces(MediaType.APPLICATION_JSON) public Timetable hello() throws ExecutionException, InterruptedException { diff --git a/frontend/src/pages/SendData.tsx b/frontend/src/pages/SendData.tsx index 27256e6..d35e43e 100644 --- a/frontend/src/pages/SendData.tsx +++ b/frontend/src/pages/SendData.tsx @@ -10,10 +10,10 @@ import { fetchTimetableSolution } from "../scripts/api"; export default function SendData() { - const [isGenerated, setIsGenerated] = useState(false); + const [isGenerated, setIsGenerated] = useState(""); function generateTimetable() { - setIsGenerated(false); + setIsGenerated(""); Promise.all([getFile(), getSpreadsheetData(DB_ROOMS), getSpreadsheetData(DB_UNITS)]) .then((responses) => { const [enrolment, roomData, unitData] = [...responses]; @@ -23,12 +23,15 @@ export default function SendData() { else if (!unitData) { throw new Error("Error: unit data not available"); } - setIsGenerated(true); // this should be after a solution has been obtained return getTimetableProblem(enrolment, roomData, unitData); }) .then((problem) => { return fetchTimetableSolution(problem); }) + .then((solution) => { + console.log(solution); + setIsGenerated(JSON.stringify(solution, null, 2)); + }) .catch((error) => { alert(error); }) @@ -37,7 +40,7 @@ export default function SendData() { return ( <>
-
+
{isGenerated.toString()}