Skip to content
Snippets Groups Projects
MetaDate.class.php 26.1 KiB
Newer Older
<?php
# Lifter002: TODO
# Lifter007: TODO
# Lifter003: TODO
# Lifter010: TODO
// +--------------------------------------------------------------------------+
// This file is part of Stud.IP
// MetaDate.class.php
//
// Repräsentiert die Zeit- und Turnusdaten einer Veranstaltung
//
// +--------------------------------------------------------------------------+
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or 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 General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
// +--------------------------------------------------------------------------+


/**
 * MetaDate.class.php
 *
 *
 * @author      Till Glöggler <tgloeggl@uos.de>
 * @version     28. Juni 2007
 * @access      protected
 * @package     raumzeit
 */
class MetaDate
{
    var $seminar_id          = '';
    var $seminarStartTime    = 0;
    var $seminarDurationTime = 0;
    var $cycles              = [];

    /**
     * Constructor
     * @param string $seminar_id
     */
    function __construct($seminar_id = '')
    {
        if ($seminar_id != '') {
            $this->seminar_id = $seminar_id;
            $this->restore();
        }

    }

    /**
     * returns start week (Semesterwoche) for a cycledate
     * for compatibility the first cycledate is chosen if no one is specified
     *
     * @deprecated
     * @param string id of cycledate
     * @return int
     */
    function getStartWoche($metadate_id = null)
    {
        if ($metadate_id) {
            return $this->cycles[$metadate_id]->week_offset;
        } else {
            $first_metadate = $this->getFirstMetadate();
            return $first_metadate ? $first_metadate->week_offset : null;
        }
    }

    /**
     * sets start week (Semesterwoche) for a cycledate
     * for compatibility the first cycledate is chosen if no one is specified
     *
     * @deprecated
     * @param int    $start_woche
     * @param string $metadate_id
     * @return null|Ambigous <NULL, unknown>
     */
    function setStartWoche($start_woche, $metadate_id = null)
    {
        if ($metadate_id) {
            return $this->cycles[$metadate_id]->week_offset = $start_woche;
        } else {
            $first_metadate = $this->getFirstMetadate();
            return $first_metadate ? $first_metadate->week_offset = $start_woche : null;
        }
    }

    /**
     * returns first cycledate
     *
     * @return CycleData
     */
    function getFirstMetadate()
    {
        $cycles = array_keys($this->cycles);
        $first_metadate_id = array_shift($cycles);
        return $first_metadate_id ? $this->cycles[$first_metadate_id] : null;
    }

    /**
     * returns the cycle for a cycledate
     * for compatibility the first cycledate is chosen if no one is specified
     *
     * @deprecated
     * @param string $metadate_id
     * @return int 0,1,2 for weekly, biweekly ...
     */
    function getTurnus($metadate_id = null)
    {
        if ($metadate_id) {
            return $this->cycles[$metadate_id]->cycle;
        } else {
            $first_metadate = $this->getFirstMetadate();
            return $first_metadate ? $first_metadate->cycle : null;
        }
    }

    /**
     * set the cycle for a cycledate
     * for compatibility the first cycledate is chosen if no one is specified
     *
     * @deprecated
     * @param        int 0,1,2 for weekly, biweekly ...
     * @param string $metadate_id
     * @return int
     */
    function setTurnus($turnus, $metadate_id = null)
    {
        if ($metadate_id) {
            return $this->cycles[$metadate_id]->cycle = $turnus;
        } else {
            $first_metadate = $this->getFirstMetadate();
            return $first_metadate ? $first_metadate->cycle = $turnus : null;
        }
    }

    function setSeminarStartTime($start)
    {
        $this->seminarStartTime = $start;
    }

    function setSeminarDurationTime($duration)
    {
        $this->seminarDurationTime = $duration;
    }

    function getSeminarID()
    {
        return $this->seminar_id;
    }

    /**
     * internal method to apply cycledate data from assoc array to a given
     * CycleData object. checks the start and endtime and retruns false if wrong
     *
     * @deprecated
     * @param           array assoc, see CycleData, metadate_id must be in $data['cycle_id']
     * @param CycleData $cycle
     * @return boolean
     */
    function setCycleData($data, $cycle)
    {
        $cycle->seminar_id = $this->getSeminarId();
        $cycles = array_keys($this->cycles);
        if ($last_one = array_pop($cycles)) {
            $cycle->sorter = $this->cycles[$last_one]->sorter > 0 ? $this->cycles[$last_one]->sorter + 1 : 0;
        }
        if ($cycle->getDescription() != $data['description']) {
            $cycle->setDescription($data['description']);
        }

        if (isset($data['weekday'])) $cycle->weekday = (int)$data['weekday'];
        if (isset($data['week_offset'])) $cycle->week_offset = (int)$data['week_offset'];
        if (isset($data['cycle'])) $cycle->cycle = (int)$data['cycle'];
        if (isset($data['sws'])) $cycle->sws = $data['sws'];
        if (isset($data['endWeek'])) $cycle->end_offset = (int)$data['endWeek'];
        if (isset($data['day']) && isset($data['start_stunde']) && isset($data['start_minute']) && isset($data['end_stunde']) && isset($data['end_minute'])) {

            if (
                ($data['start_stunde'] > 23) || ($data['start_stunde'] < 0) ||
                ($data['end_stunde'] > 23) || ($data['end_stunde'] < 0) ||
                ($data['start_minute'] > 59) || ($data['start_minute'] < 0) ||
                ($data['end_minute'] > 59) || ($data['end_minute'] < 0)
            ) {
                return FALSE;
            }

            if (mktime((int)$data['start_stunde'], (int)$data['start_minute']) < mktime((int)$data['end_stunde'], (int)$data['end_minute'])) {
                $cycle->setDay($data['day']);
                $cycle->setStart($data['start_stunde'], $data['start_minute']);
                $cycle->setEnd($data['end_stunde'], $data['end_minute']);
                return TRUE;
            }
        }

        return FALSE;
    }


    /**
     * adds a new cycledate, single dates are created if second param is true
     *
     * @param      array assoc, see CycleData, metadate_id must be in $data['cycle_id']
     * @param bool $create_single_dates
     * @return string|boolean metadate_id of created cycle
     */
    function addCycle($data = [], $create_single_dates = true)
    {
        $data['day'] = (int)$data['day'];
        $data['start_stunde'] = (int)$data['start_stunde'];
        $data['start_minute'] = (int)$data['start_minute'];
        $data['end_stunde'] = (int)$data['end_stunde'];
        $data['end_minute'] = (int)$data['end_minute'];

        $cycle = new CycleData();

        if ($this->setCycleData($data, $cycle)) {
            $this->cycles[$cycle->getMetadateID()] = $cycle;
            $this->sortCycleData();
            if ($create_single_dates) $this->createSingleDates($cycle->getMetadateID());
            return $cycle->getMetadateID();
        }
        return FALSE;
    }

    /**
     * change existing cycledate, changes also corresponding single dates
     *
     * @param array assoc, see CycleData, metadate_id must be in $data['cycle_id']
     * @return number|boolean
     */
    function editCycle($data = [])
    {
        $cycle = $this->cycles[$data['cycle_id']];
        $new_start = mktime((int)$data['start_stunde'], (int)$data['start_minute']);
        $new_end = mktime((int)$data['end_stunde'], (int)$data['end_minute']);
        $old_start = mktime($cycle->getStartStunde(), $cycle->getStartMinute());
        $old_end = mktime($cycle->getEndStunde(), $cycle->getEndMinute());

        if (($new_start >= $old_start) && ($new_end <= $old_end) && ($data['day'] == $cycle->day) && ($data['endWeek'] == $cycle->end_offset)) {
            // Zeitraum wurde verkuerzt, Raumbuchungen bleiben erhalten...
            if ($this->setCycleData($data, $cycle)) {
                $termine = $cycle->getSingleDates();
                foreach ($termine as $key => $val) {
                    $tos = $val->getStartTime();
                    $toe = $val->getEndTime();
                    if ($toe > time()) {
                        $t_start = mktime((int)$data['start_stunde'], (int)$data['start_minute'], 0, date('m', $tos), date('d', $tos), date('Y', $tos));
                        $t_end = mktime((int)$data['end_stunde'], (int)$data['end_minute'], 0, date('m', $toe), date('d', $toe), date('Y', $toe));
                        $termine[$key]->setTime($t_start, $t_end);
                        $termine[$key]->store();
                    } else {
                        unset($termine[$key]);
                    }
                }
                $this->sortCycleData();
            }
            return sizeof($termine);
        } else {
            if ($this->setCycleData($data, $cycle)) {
                // collect all existing themes (issues) for this cycle:
                $issues = [];
                $issue_objects = [];
                $singledate_count = 0;

                // loop through the single dates and look for themes (issues)
                $termine = $cycle->getSingleDates();
                foreach ($termine as $key => $termin) {
                    // get all isues of this date ( zero, one, or more, if the expert view is activated)
                    // and store them at the relative position of this single date
                    $issues[$singledate_count] = $termin->getIssueIDs();
                    $singledate_count++;
                }
                // remove all SingleDates in the future for this CycleData
                $count = CycleDataDB::deleteNewerSingleDates($data['cycle_id'], time());
                // create new SingleDates
                $this->createSingleDates(['metadate_id'         => $cycle->getMetaDateId(),
                                               'startAfterTimeStamp' => time()
                ]);

                // clear all loaded SingleDates so no odd ones remain. The Seminar-Class will load them fresh when needed
                $cycle->termine = NULL;

                // read all new single dates
                $termine = $cycle->getSingleDates();

                // new dates counter
                $new_singledate_count = 0;

                // loop through the single dates and add the themes (issues)
                foreach ($termine as $key => $termin) {
                    // check, if there are issues for this single date
                    if ($issues[$new_singledate_count] != NULL) {
                        // add all issues:
                        foreach ($issues[$new_singledate_count] as $issue_key => $issue_id) {
                            $termin->addIssueID($issue_id);
                            $termin->store();
                        }
                    }
                    unset($issues[$new_singledate_count]);
                    $new_singledate_count++;
                }

                // delete issues, that are not assigned to a single date because of to few dates
                // (only if the schedule expert view is off)
                if (!Config::get()->RESOURCES_ENABLES_EXPERT_SCHEDULE_VIEW) {
                    if ($new_singledate_count < $singledate_count) {
                        for ($i = $new_singledate_count; $i < $singledate_count; $i++) {
                            if ($issues[$i] != NULL) {
                                foreach ($issues[$i] as $issue_id) {
                                    // delete this issue
                                    IssueDB::deleteIssue($issue_id);
                                }
                            }
                        }
                    }
                }
                $this->sortCycleData();
                return $count;
            }
        }
        return FALSE;
    }

    /**
     * completey remove cycledate
     * @see CycleData::delete()
     * @param string $cycle_id
     * @return boolean
     */
    function deleteCycle($cycle_id)
    {
        $this->cycles[$cycle_id]->delete();
        unset ($this->cycles[$cycle_id]);
        return TRUE;
    }

    function deleteSingleDate($cycle_id, $date_id, $filterStart, $filterEnd)
    {
        $this->cycles[$cycle_id]->deleteSingleDate($date_id, $filterStart, $filterEnd);
    }

    function unDeleteSingleDate($cycle_id, $date_id, $filterStart, $filterEnd)
    {
        return $this->cycles[$cycle_id]->unDeleteSingleDate($date_id, $filterStart, $filterEnd);
    }

    /**
     * store all changes to cycledates for the course, removed cycles are deleted from database
     * @return int > 0 if changes where made
     */
    function store()
    {
        $old_cycle_dates = [];
        $changed = 0;
        foreach (SeminarCycleDate::findBySeminar($this->seminar_id) as $c) {
            $old_cycle_dates[$c->getId()] = $c;
        }
        $removed = array_diff(array_keys($old_cycle_dates), array_keys($this->cycles));
        foreach ($removed as $one) {
            $changed += $old_cycle_dates[$one]->delete();
        }
        foreach ($this->cycles as $one) {
            $changed += $one->storeCycleDate();
        }
        $this->sortCycleData();
        return $changed;
    }


    /**
     * load all cycledates from database
     */
    function restore()
    {
        $this->cycles = [];
        foreach (SeminarCycleDate::findBySeminar($this->seminar_id) as $c) {
            $this->cycles[$c->getId()] = new CycleData($c);
        }
        $this->sortCycleData();
    }

    function delete($removeSingleDates = TRUE)
    {
        //TODO: Löschen eines MetaDate-Eintrages (CycleData);
    }

    /**
     * sort cycledates by sorter column and date
     */
    function sortCycleData()
    {
        uasort($this->cycles, function ($a, $b) {
            return $a->sorter - $b->sorter
                ?: $a->weekday - $b->weekday
                ?: $a->start_hour - $b->start_hour;
        });
    }

    /**
     * returns cycledates as arrays
     *
     * @param bool $show_invisibles if cycles without dates should
     *                              be in the array, defaults to false
     * @return array assoc of cycledate data arrays
     */
    function getCycleData($show_invisibles = false)
    {
        $ret = [];

        foreach ($this->cycles as $val) {
            if ($val->is_visible || $show_invisibles) {
                $ret[$val->getMetaDateID()] = $val->toArray();
            }
        }
        return $ret;
    }

    /**
     * returns the cycledate objects
     * @return array of CycleData objects
     */
    function getCycles()
    {
        return $this->cycles;
    }

    /**
     * returns an array of SingleDate objects corresponding to the given cycle id
     * use the optional params to specify a timerange
     *
     * @param string a cycle id
     * @param int    unix timestamp
     * @param int    unix timestamp
     * @return array of SingleDate objects
     */
    function getSingleDates($metadate_id, $filterStart = 0, $filterEnd = 0)
    {
        if (!$this->cycles[$metadate_id]->termine) {
            $this->readSingleDates($metadate_id, $filterStart, $filterEnd);
        }

        return $this->cycles[$metadate_id]->termine;
    }

    /**
     * reload SingleDate objects for a given cycle id
     *
     * @param string $metadate_id
     * @param int    $start
     * @param int    $end
     * @return bool
     */
    function readSingleDates($metadate_id, $start = 0, $end = 0)
    {
        return $this->cycles[$metadate_id]->readSingleDates($start, $end);
    }

    /**
     * returns true if a given cycle has at least one date at all or in the given time range
     *
     * @param string $metadate_id
     * @param int $filterStart
     * @param int $filterEnd
     * @return bool
     */
    function hasDates($metadate_id, $filterStart = 0, $filterEnd = 0)
    {
        if (!isset($this->hasDatesTmp[$metadate_id])) {
            $this->hasDatesTmp[$metadate_id] = MetaDateDB::has_dates($metadate_id, $this->getSeminarID(), $filterStart, $filterEnd);
        }

        return $this->hasDatesTmp[$metadate_id];
    }

    /**
     * create single dates for one cycle and all semester and store them in database, deleting obsolete ones
     *
     * @param mixed cycle id (string) or array with 'metadate_id' => string cycle id, 'startAfterTimeStamp' => int timestamp to override semester start
     */
    function createSingleDates($data)
    {
        foreach ($this->getVirtualSingleDates($data) as $semester_id => $dates_for_semester) {
            list($dates, $dates_to_delete) = array_values($dates_for_semester);
            foreach ($dates_to_delete as $d) $d->delete();
            foreach ($dates as $d) {
                if ($d->isUpdate()) continue; //vorhandene Termine nicht speichern wg. chdate
                $d->store();
            }
        }
        //das sollte nicht nötig sein, muss aber erst genauer untersucht werden
        $this->store();
        $this->restore();
    }

    /**
     * generate single date objects for one cycle and all semester, existing dates are merged in
     *
     * @param mixed cycle id (string) or array with 'metadate_id' => string cycle id, 'startAfterTimeStamp' => int timestamp to override semester start
     * @return array array of arrays, for each semester id  an array of two arrays of SingleDate objects: 'dates' => all new and surviving dates, 'dates_to_delete' => obsolete dates
     */
    function getVirtualSingleDates($data)
    {
        if (is_array($data)) {
            $metadate_id = $data['metadate_id'];
            $startAfterTimeStamp = $data['startAfterTimeStamp'];
        } else {
            $metadate_id = $data;
            $startAfterTimeStamp = 0;
        }

        $ret = [];

        $all_semester = Semester::findAllVisible(false);
        $sem_begin = null;
        $sem_end = null;
        // get the starting-point for creating singleDates for the choosen cycleData
        foreach ($all_semester as $val) {
            if (($this->seminarStartTime >= $val["beginn"]) && ($this->seminarStartTime <= $val["ende"])) {
                $sem_begin = mktime(0, 0, 0, date("n", $val["vorles_beginn"]), date("j", $val["vorles_beginn"]), date("Y", $val["vorles_beginn"]));
            }
        }

        // get the end-point
        if ($this->seminarDurationTime == -1) {
            foreach ($all_semester as $val) {
                $sem_end = $val['vorles_ende'];
            }
        } else {
            $i = 0;
            foreach ($all_semester as $val) {
                $i++;
                $timestamp = $this->seminarDurationTime + $this->seminarStartTime;
                if (($timestamp >= $val['beginn']) && ($timestamp <= $val['ende'])) {
                    $sem_end = $val["vorles_ende"];
                }
            }
        }

        $passed = false;
        foreach ($all_semester as $val) {
            if ($sem_begin <= $val['vorles_beginn']) {
                $passed = true;
            }
            if ($passed && ($sem_end >= $val['vorles_ende']) && ($startAfterTimeStamp <= $val['ende'])) {
                // correction calculation, if the semester does not start on monday
                $dow = date("w", $val['vorles_beginn']);
                if ($dow <= 5)
                    $corr = ($dow - 1) * -1;
                elseif ($dow == 6)
                    $corr = 2;
                elseif ($dow == 0)
                    $corr = 1;
                else
                    $corr = 0;
                $ret[$val['semester_id']] = $this->getVirtualSingleDatesForSemester($metadate_id, $val['vorles_beginn'], $val['vorles_ende'], $startAfterTimeStamp, $corr);
            }
        }

        return $ret;
    }

    /**
     * generate single date objects for one cycle and one semester, existing dates are merged in
     *
     * @param string cycle id
     * @param int    timestamp of semester start
     * @param int    timestamp of semester end
     * @param int    alternative timestamp to start from
     * @param int    correction calculation, if the semester does not start on monday (number of days?)
     * @return array returns an array of two arrays of SingleDate objects: 'dates' => all new and surviving dates, 'dates_to_delete' => obsolete dates
     */
    function getVirtualSingleDatesForSemester($metadate_id, $sem_begin, $sem_end, $startAfterTimeStamp, $corr)
    {
        $dates = [];
        $dates_to_delete = [];

        // loads the singledates of the by metadate_id denoted regular time-entry into the object
        $this->readSingleDates($metadate_id);

        // The currently existing singledates for the by metadate_id denoted  regular time-entry
        $existingSingleDates =& $this->cycles[$metadate_id]->getSingleDates();

        $start_woche = $this->cycles[$metadate_id]->week_offset;
        $end_woche = $this->cycles[$metadate_id]->end_offset;

        $turnus = $this->cycles[$metadate_id]->cycle;

        // This variable is used to check if a given singledate shall be created in a bi-weekly seminar.
        if ($start_woche == -1) $start_woche = 0;

        $week = 0;

        // get the first presence date after sem_begin
        $day_of_week = date('l', strtotime('Sunday + ' . $this->cycles[$metadate_id]->day . ' days'));
        $stamp = strtotime('this week ' . $day_of_week, $sem_begin);

        $start_time = mktime(
            (int)$this->cycles[$metadate_id]->start_stunde,     // Hour
            (int)$this->cycles[$metadate_id]->start_minute,     // Minute
            0,                                                  // Second
            date("n", $stamp),                                  // Month
            date("j", $stamp),                                  // Day
            date("Y", $stamp));                                 // Year

        $end_time = mktime(
            (int)$this->cycles[$metadate_id]->end_stunde,       // Hour
            (int)$this->cycles[$metadate_id]->end_minute,       // Minute
            0,                                                  // Second
            date("n", $stamp),                                  // Month
            date("j", $stamp),                                  // Day
            date("Y", $stamp));                                 // Year

        // loop through all possible singledates for this regular time-entry
        do {

            // if dateExists is true, the singledate will not be created. Default is of course to create the singledate
            $dateExists = false;

            // do not create singledates if they are earlier than the semester start
            if ($end_time < $sem_begin) $dateExists = true;

            // do not create singledates, if they are earlier then the chosen start-week
            if ($start_woche > $week || (isset($end_woche) && $week > $end_woche)) $dateExists = true;

            // bi-weekly check
            if ($turnus > 0 && ($week - $start_woche) > 0 && (($week - $start_woche) % ($turnus + 1))) {
                $dateExists = true;
            }

            /*
             * We only create dates, which do not already exist, so we do not overwrite existing dates.
             *
             * Additionally, we delete singledates which are not needed any more (bi-weekly, changed start-week, etc.)
             */
            foreach ($existingSingleDates as $key => $val) {
                // take only the singledate into account, that maps the current timepoint
                if ($start_time > $startAfterTimeStamp && ($val->date == $start_time) && ($val->end_time == $end_time)) {

                    // bi-weekly check
                    if ($turnus > 0 && ($week - $start_woche) > 0 && (($week - $start_woche) % ($turnus + 1))) {
                        $dates_to_delete[$key] = $val;
                        unset($existingSingleDates[$key]);
                    }

                    // delete singledates if they are earlier than the chosen start-week
                    if ($start_woche > $week || (isset($end_woche) && $week > $end_woche)) {
                        $dates_to_delete[$key] = $val;
                        unset($existingSingleDates[$key]);
                    }

                    $dateExists = true;
                    if (isset($existingSingleDates[$key])) {
                        $dates[$key] = $val;
                    }
                }
            }

            if ($start_time < $startAfterTimeStamp) {
                $dateExists = true;
            }

            if (!$dateExists) {

                $termin = new SingleDate(['seminar_id' => $this->seminar_id]);

                $all_holiday = SemesterHoliday::getAll(); // fetch all Holidays
                foreach ($all_holiday as $val2) {
                    if (($val2["beginn"] <= $start_time) && ($start_time <= $val2["ende"])) {
                        $termin->setExTermin(true);
                    }
                }

                //check for calculatable holidays
                if (!$termin->isExTermin()) {
                    $holy_type = holiday($start_time);
                    if ($holy_type["col"] == 3) {
                        $termin->setExTermin(true);
                    }
                }

                // fill the singleDate-Object with data
                $termin->setMetaDateID($metadate_id);
                $termin->setTime($start_time, $end_time);
                $termin->setDateType(1); //best guess

                $dates[$termin->getTerminID()] = $termin;
            }

            //inc the week, create timestamps for the next singledate
            $start_time = strtotime('+1 week', $start_time);
            $end_time = strtotime('+1 week', $end_time);

            $week++;

        } while ($end_time < $sem_end);

        return ['dates' => $dates, 'dates_to_delete' => $dates_to_delete];
    }
}