weekly release 3.1dev
[moodle.git] / lib / classes / progress / base.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 namespace core\progress;
19 defined('MOODLE_INTERNAL') || die();
21 /**
22  * Base class for handling progress information.
23  *
24  * Subclasses should generally override the {@link current_progress} function which
25  * summarises all progress information.
26  *
27  * @package core_progress
28  * @copyright 2013 The Open University
29  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30  */
31 abstract class base {
32     /**
33      * @var int Constant indicating that the number of progress calls is unknown.
34      */
35     const INDETERMINATE = -1;
37     /**
38      * This value is set rather high to ensure there are no regressions from
39      * previous behaviour. For testing, it may be useful to set the
40      * frontendservertimeout config option to a lower value, such as 180
41      * seconds (default for some commercial products).
42      *
43      * @var int The number of seconds that can pass without {@link progress()} calls.
44      */
45     const TIME_LIMIT_WITHOUT_PROGRESS = 3600;
47     /**
48      * @var int Time of last progress call.
49      */
50     protected $lastprogresstime;
52     /**
53      * @var int Number of progress calls (restricted to ~ 1/second).
54      */
55     protected $count;
57     /**
58      * @var array Array of progress descriptions for each stack level.
59      */
60     protected $descriptions = array();
62     /**
63      * @var array Array of maximum progress values for each stack level.
64      */
65     protected $maxes = array();
67     /**
68      * @var array Array of current progress values.
69      */
70     protected $currents = array();
72     /**
73      * @var int[] Array of counts within parent progress entry (ignored for first)
74      */
75     protected $parentcounts = array();
77     /**
78      * Marks the start of an operation that will display progress.
79      *
80      * This can be called multiple times for nested progress sections. It must
81      * be paired with calls to end_progress.
82      *
83      * The progress maximum may be {@link self::INDETERMINATE} if the current operation has
84      * an unknown number of steps. (This is default.)
85      *
86      * Calling this function will always result in a new display, so this
87      * should not be called exceedingly frequently.
88      *
89      * When it is complete by calling {@link end_progress()}, each {@link start_progress} section
90      * automatically adds progress to its parent, as defined by $parentcount.
91      *
92      * @param string $description Description to display
93      * @param int $max Maximum value of progress for this section
94      * @param int $parentcount How many progress points this section counts for
95      * @throws \coding_exception If max is invalid
96      */
97     public function start_progress($description, $max = self::INDETERMINATE,
98             $parentcount = 1) {
99         if ($max != self::INDETERMINATE && $max < 0) {
100             throw new \coding_exception(
101                     'start_progress() max value cannot be negative');
102         }
103         if ($parentcount < 1) {
104             throw new \coding_exception(
105                     'start_progress() parent progress count must be at least 1');
106         }
107         if (!empty($this->descriptions)) {
108             $prevmax = end($this->maxes);
109             if ($prevmax !== self::INDETERMINATE) {
110                 $prevcurrent = end($this->currents);
111                 if ($prevcurrent + $parentcount > $prevmax) {
112                     throw new \coding_exception(
113                             'start_progress() parent progress would exceed max');
114                 }
115             }
116         } else {
117             if ($parentcount != 1) {
118                 throw new \coding_exception(
119                         'start_progress() progress count must be 1 when no parent');
120             }
121         }
122         $this->descriptions[] = $description;
123         $this->maxes[] = $max;
124         $this->currents[] = 0;
125         $this->parentcounts[] = $parentcount;
126         $this->update_progress();
127     }
129     /**
130      * Marks the end of an operation that will display progress.
131      *
132      * This must be paired with each {@link start_progress} call.
133      *
134      * If there is a parent progress section, its progress will be increased
135      * automatically to reflect the end of the child section.
136      *
137      * @throws \coding_exception If progress hasn't been started
138      */
139     public function end_progress() {
140         if (!count($this->descriptions)) {
141             throw new \coding_exception('end_progress() without start_progress()');
142         }
143         array_pop($this->descriptions);
144         array_pop($this->maxes);
145         array_pop($this->currents);
146         $parentcount = array_pop($this->parentcounts);
147         if (!empty($this->descriptions)) {
148             $lastmax = end($this->maxes);
149             if ($lastmax != self::INDETERMINATE) {
150                 $lastvalue = end($this->currents);
151                 $this->currents[key($this->currents)] = $lastvalue + $parentcount;
152             }
153         }
154         $this->update_progress();
155     }
157     /**
158      * Indicates that progress has occurred.
159      *
160      * The progress value should indicate the total progress so far, from 0
161      * to the value supplied for $max (inclusive) in {@link start_progress}.
162      *
163      * You do not need to call this function for every value. It is OK to skip
164      * values. It is also OK to call this function as often as desired; it
165      * doesn't update the display if called more than once per second.
166      *
167      * It must be INDETERMINATE if {@link start_progress} was called with $max set to
168      * INDETERMINATE. Otherwise it must not be indeterminate.
169      *
170      * @param int $progress Progress so far
171      * @throws \coding_exception If progress value is invalid
172      */
173     public function progress($progress = self::INDETERMINATE) {
174         // Check we are inside a progress section.
175         $max = end($this->maxes);
176         if ($max === false) {
177             throw new \coding_exception(
178                     'progress() without start_progress');
179         }
181         // Check and apply new progress.
182         if ($progress === self::INDETERMINATE) {
183             // Indeterminate progress.
184             if ($max !== self::INDETERMINATE) {
185                 throw new \coding_exception(
186                         'progress() INDETERMINATE, expecting value');
187             }
188         } else {
189             // Determinate progress.
190             $current = end($this->currents);
191             if ($max === self::INDETERMINATE) {
192                 throw new \coding_exception(
193                         'progress() with value, expecting INDETERMINATE');
194             } else if ($progress < 0 || $progress > $max) {
195                 throw new \coding_exception(
196                         'progress() value out of range');
197             } else if ($progress < $current) {
198                 throw new \coding_exception(
199                         'progress() value may not go backwards');
200             }
201             $this->currents[key($this->currents)] = $progress;
202         }
204         // Don't update progress bar too frequently (more than once per second).
205         $now = $this->get_time();
206         if ($now === $this->lastprogresstime) {
207             return;
208         }
210         // Update progress.
211         $this->count++;
212         $this->lastprogresstime = $now;
214         // Update time limit before next progress display.
215         \core_php_time_limit::raise(self::TIME_LIMIT_WITHOUT_PROGRESS);
216         $this->update_progress();
217     }
219     /**
220      * An alternative to calling progress. This keeps track of the number of items done internally. Call this method
221      * with no parameters to increment the internal counter by one or you can use the $incby parameter to specify a positive
222      * change in progress. The internal progress counter should not exceed $max as passed to {@link start_progress} for this
223      * section.
224      *
225      * If you called {@link start_progress} with parameter INDETERMINATE then you cannot call this method.
226      *
227      * @var int $incby The positive change to apply to the internal progress counter. Defaults to 1.
228      */
229     public function increment_progress($incby = 1) {
230         $current = end($this->currents);
231         $this->progress($current + $incby);
232     }
234     /**
235      * Gets time (this is provided so that unit tests can override it).
236      *
237      * @return int Current system time
238      */
239     protected function get_time() {
240         return time();
241     }
243     /**
244      * Called whenever new progress should be displayed.
245      */
246     protected abstract function update_progress();
248     /**
249      * @return bool True if currently inside a progress section
250      */
251     public function is_in_progress_section() {
252         return !empty($this->descriptions);
253     }
255     /**
256      * Checks max value of current progress section.
257      *
258      * @return int Current max value - may be {@link \core\progress\base::INDETERMINATE}.
259      * @throws \coding_exception If not in a progress section
260      */
261     public function get_current_max() {
262         $max = end($this->maxes);
263         if ($max === false) {
264             throw new \coding_exception('Not inside progress section');
265         }
266         return $max;
267     }
269     /**
270      * @throws \coding_exception
271      * @return string Current progress section description
272      */
273     public function get_current_description() {
274         $description = end($this->descriptions);
275         if ($description === false) {
276             throw new \coding_exception('Not inside progress section');
277         }
278         return $description;
279     }
281     /**
282      * Obtains current progress in a way suitable for drawing a progress bar.
283      *
284      * Progress is returned as a minimum and maximum value. If there is no
285      * indeterminate progress, these values will be identical. If there is
286      * intermediate progress, these values can be different. (For example, if
287      * the top level progress sections is indeterminate, then the values will
288      * always be 0.0 and 1.0.)
289      *
290      * @return array Minimum and maximum possible progress proportions
291      */
292     public function get_progress_proportion_range() {
293         // If there is no progress underway, we must have finished.
294         if (empty($this->currents)) {
295             return array(1.0, 1.0);
296         }
297         $count = count($this->currents);
298         $min = 0.0;
299         $max = 1.0;
300         for ($i = 0; $i < $count; $i++) {
301             // Get max value at that section - if it's indeterminate we can tell
302             // no more.
303             $sectionmax = $this->maxes[$i];
304             if ($sectionmax === self::INDETERMINATE) {
305                 return array($min, $max);
306             }
308             // Special case if current value is max (this should only happen
309             // just before ending a section).
310             $sectioncurrent = $this->currents[$i];
311             if ($sectioncurrent === $sectionmax) {
312                 return array($max, $max);
313             }
315             // Using the current value at that section, we know we are somewhere
316             // between 'current' and the next 'current' value which depends on
317             // the parentcount of the nested section (if any).
318             $newmin = ($sectioncurrent / $sectionmax) * ($max - $min) + $min;
319             $nextcurrent = $sectioncurrent + 1;
320             if ($i + 1 < $count) {
321                 $weight = $this->parentcounts[$i + 1];
322                 $nextcurrent = $sectioncurrent + $weight;
323             }
324             $newmax = ($nextcurrent / $sectionmax) * ($max - $min) + $min;
325             $min = $newmin;
326             $max = $newmax;
327         }
329         // If there was nothing indeterminate, we use the min value as current.
330         return array($min, $min);
331     }
333     /**
334      * Obtains current indeterminate progress in a way suitable for adding to
335      * the progress display.
336      *
337      * This returns the number of indeterminate calls (at any level) during the
338      * lifetime of this progress reporter, whether or not there is a current
339      * indeterminate step. (The number will not be ridiculously high because
340      * progress calls are limited to one per second.)
341      *
342      * @return int Number of indeterminate progress calls
343      */
344     public function get_progress_count() {
345         return $this->count;
346     }