Files
htwkalender-pwa/backend/service/room/roomService.go

286 lines
8.6 KiB
Go

//Calendar implementation for the HTWK Leipzig timetable. Evaluation and display of the individual dates in iCal format.
//Copyright (C) 2024 HTWKalender support@htwkalender.de
//This program is free software: you can redistribute it and/or modify
//it under the terms of the GNU Affero General Public License as published by
//the Free Software Foundation, either version 3 of the License, or
//(at your option) any later version.
//This program is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//GNU Affero General Public License for more details.
//You should have received a copy of the GNU Affero General Public License
//along with this program. If not, see <https://www.gnu.org/licenses/>.
package room
import (
"htwkalender/model"
"htwkalender/service/db"
"htwkalender/service/functions"
"math"
"regexp"
"strings"
"time"
"github.com/pocketbase/pocketbase"
"go.mongodb.org/mongo-driver/bson/primitive"
)
// maximum number of blocks is around 6 months with 15 minute granularity (180 * 24 * 4 = 17280)
const MaxNumberOfBlocks = 1728000
func GetRooms(app *pocketbase.PocketBase) ([]string, error) {
rooms, err := db.GetRooms(app)
if err != nil {
return nil, err
} else {
return rooms, nil
}
}
func GetRoomScheduleForDay(app *pocketbase.PocketBase, room string, date string) ([]model.AnonymizedEventDTO, error) {
roomSchedule, err := db.GetRoomScheduleForDay(app, room, date)
if err != nil {
return nil, err
}
anonymizedRoomSchedule := anonymizeRooms(roomSchedule)
return anonymizedRoomSchedule, nil
}
func GetRoomSchedule(app *pocketbase.PocketBase, room string, from string, to string) ([]model.AnonymizedEventDTO, error) {
roomSchedule, err := db.GetRoomSchedule(app, room, from, to)
if err != nil {
return nil, err
}
anonymizedRoomSchedule := anonymizeRooms(roomSchedule)
return anonymizedRoomSchedule, nil
}
/**
* Get the room occupancy for all rooms within a given time range
* @param app pocketbase instance
* @param from start time of the occupancy list
* @param to end time of the occupancy list
* @param granularity number of minutes for one block
* @return room occupancy list
* @return error if the database query fails
*/
func GetRoomOccupancyList(app *pocketbase.PocketBase, granularity int) (model.RoomOccupancyList, error) {
now := time.Now()
fromTime := functions.GetSemesterStart(now)
toTime := functions.GetSemesterStart(now.AddDate(0, 6, 0))
// calculate the number of blocks for the given time range and granularity
timeDifference := toTime.Sub(fromTime)
numberOfBlocks := int(math.Ceil(timeDifference.Minutes() / float64(granularity)))
numberOfBlocks = min(numberOfBlocks, MaxNumberOfBlocks)
roomOccupancyList := emptyRoomOccupancyList(fromTime, granularity, numberOfBlocks)
// get all events within the time range
events, err := db.GetEventsThatCollideWithTimeRange(app, fromTime, toTime)
if err != nil {
return model.RoomOccupancyList{}, err
}
rooms, err := getRelevantRooms(app)
if err != nil {
return model.RoomOccupancyList{}, err
}
for _, room := range rooms {
// get the schedule for only this room
roomEvents := functions.Filter(events, func(event model.Event) bool {
return strings.Contains(event.Rooms, room)
})
// encode the room schedule binary and add it to the list
roomOccupancy, err := createRoomOccupancy(room, roomEvents, fromTime, granularity, numberOfBlocks)
if err != nil {
return model.RoomOccupancyList{}, err
}
roomOccupancyList.Rooms = append(roomOccupancyList.Rooms, roomOccupancy)
}
return roomOccupancyList, nil
}
/**
* Get all rooms from the database and filter them by a regex
* @param app pocketbase instance
* @return all rooms that match the regex
* @return error if the database query fails
*/
func getRelevantRooms(app *pocketbase.PocketBase) ([]string, error) {
// get all rooms
rooms, err := db.GetRooms(app)
if err != nil {
return nil, err
}
// filter by regex for the room name
roomRegex := regexp.MustCompile(".*")
return functions.Filter(rooms, roomRegex.MatchString), nil
}
/*
* Create an empty room occupancy list
* @param from start time of the occupancy list
* @param granularity number of minutes for one block
* @param blockCount number of blocks that can be either occupied or free
*/
func emptyRoomOccupancyList(from time.Time, granularity int, blockCount int) model.RoomOccupancyList {
return model.RoomOccupancyList{
Start: from,
Updated: time.Now(),
Granularity: granularity,
Blocks: blockCount,
Rooms: []model.RoomOccupancy{},
}
}
/*+
* Create the room occupancy for a given room
* @param room room name
* @param events events that could block the room (or be free)
* @param start start time of the schedule
* @param granularity number of minutes for one block
* @param blockCount number of blocks
* @return room occupancy for the given room
* @return error if encoding the room schedule fails
*/
func createRoomOccupancy(room string, events []model.Event, start time.Time, granularity int, blockCount int) (model.RoomOccupancy, error) {
roomSchedule := anonymizeRooms(events)
occupancy, err := encodeRoomSchedule(roomSchedule, start, granularity, blockCount)
if err != nil {
return model.RoomOccupancy{}, err
}
return model.RoomOccupancy{
Name: room,
Occupancy: primitive.Binary{
Subtype: 0,
Data: occupancy,
},
}, nil
}
/*
* Transform the events to anonymized events throwing away all unnecessary information
* @param events events to be anonymized
* @see Event.AnonymizeEvent
* @return anonymized events
*/
func anonymizeRooms(events []model.Event) []model.AnonymizedEventDTO {
var anonymizedEvents []model.AnonymizedEventDTO
for _, event := range events {
anonymizedEvents = append(anonymizedEvents, event.AnonymizeEvent())
}
return anonymizedEvents
}
func GetFreeRooms(app *pocketbase.PocketBase, from time.Time, to time.Time) ([]string, error) {
rooms, err := db.GetRooms(app)
if err != nil {
return nil, err
}
var events model.Events
events, err = db.GetEventsThatCollideWithTimeRange(app, from, to)
if err != nil {
return nil, err
}
freeRooms := removeRoomsThatHaveEvents(rooms, events)
return freeRooms, nil
}
func removeRoomsThatHaveEvents(rooms []string, schedule []model.Event) []string {
var freeRooms []string
for _, room := range rooms {
if !isRoomInSchedule(room, schedule) {
freeRooms = append(freeRooms, room)
}
}
return freeRooms
}
func isRoomInSchedule(room string, schedule []model.Event) bool {
for _, event := range schedule {
if event.Course != "Sport" {
rooms := functions.SeperateRoomString(event.Rooms)
// check if room is in rooms
for _, r := range rooms {
if r == room {
return true
}
}
} else {
if event.Rooms == room {
return true
}
}
}
return false
}
/**
* Encode the room schedule to a byte array
*
* @param roomSchedule events that block the room
* @param start start time of the schedule
* @param granularity number of minutes for one block
* @param blockCount number of blocks
*
* @return byte array of the encoded room schedule
* @return error if encoding fails
*/
func encodeRoomSchedule(roomSchedule []model.AnonymizedEventDTO, start time.Time, granularity int, blockCount int) ([]byte, error) {
// Create empty occupancy array with blockCount bits
byteCount := int(math.Ceil(float64(blockCount) / 8))
occupancy := make([]byte, byteCount)
// Iterate over all events in the schedule
for _, event := range roomSchedule {
// skip if room is not occupied or end time is not after start time
if event.Free || !event.Start.Time().Before(event.End.Time()) {
continue
}
// Calculate the start and end block of the event
startBlock := int(
math.Floor(event.Start.Time().Sub(start).Minutes() / float64(granularity)),
)
endBlock := int(
math.Ceil(event.End.Time().Sub(start).Minutes() / float64(granularity)),
)
occupyBlocks(occupancy, startBlock, endBlock, blockCount)
}
return occupancy, nil
}
/**
* Set the bits of the occupancy array for the given block range
* to 1
*
* @param occupancy byte array of the occupancy
* @param startBlock start block (bit defined by granularity) of the event
* @param endBlock end block of the event
* @param blockCount number of blocks (bits) in the occupancy array
*/
func occupyBlocks(occupancy []byte, startBlock int, endBlock int, blockCount int) {
lowerBound := max(0, min(startBlock, blockCount))
upperBound := min(max(endBlock, lowerBound), blockCount)
// Iterate over all blocks of the event
for i := lowerBound; i < upperBound; i++ {
occupancy[i/8] |= 1 << (7 - i%8)
}
}