//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 . 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)) if functions.IsLastMonthOfSemester(now) { toTime = functions.GetSemesterStart(now.AddDate(1, 0, 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) } }