weekly release 3.1dev
[moodle.git] / lib / classes / progress / base.php
CommitLineData
16cd7088 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/>.
16
809fdb83
JP
17namespace core\progress;
18
19defined('MOODLE_INTERNAL') || die();
20
16cd7088 21/**
809fdb83 22 * Base class for handling progress information.
16cd7088 23 *
eb50c6e1 24 * Subclasses should generally override the {@link current_progress} function which
16cd7088 25 * summarises all progress information.
26 *
809fdb83 27 * @package core_progress
16cd7088 28 * @copyright 2013 The Open University
29 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30 */
809fdb83 31abstract class base {
16cd7088 32 /**
33 * @var int Constant indicating that the number of progress calls is unknown.
34 */
35 const INDETERMINATE = -1;
36
37 /**
6edd90cc 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 *
eb50c6e1 43 * @var int The number of seconds that can pass without {@link progress()} calls.
16cd7088 44 */
6edd90cc 45 const TIME_LIMIT_WITHOUT_PROGRESS = 3600;
16cd7088 46
47 /**
48 * @var int Time of last progress call.
49 */
50 protected $lastprogresstime;
51
52 /**
53 * @var int Number of progress calls (restricted to ~ 1/second).
54 */
55 protected $count;
56
57 /**
58 * @var array Array of progress descriptions for each stack level.
59 */
60 protected $descriptions = array();
61
62 /**
63 * @var array Array of maximum progress values for each stack level.
64 */
65 protected $maxes = array();
66
67 /**
68 * @var array Array of current progress values.
69 */
70 protected $currents = array();
71
72 /**
809fdb83 73 * @var int[] Array of counts within parent progress entry (ignored for first)
16cd7088 74 */
75 protected $parentcounts = array();
76
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 *
eb50c6e1 83 * The progress maximum may be {@link self::INDETERMINATE} if the current operation has
16cd7088 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 *
eb50c6e1 89 * When it is complete by calling {@link end_progress()}, each {@link start_progress} section
16cd7088 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
809fdb83 95 * @throws \coding_exception If max is invalid
16cd7088 96 */
97 public function start_progress($description, $max = self::INDETERMINATE,
98 $parentcount = 1) {
6a0189eb 99 if ($max != self::INDETERMINATE && $max < 0) {
809fdb83 100 throw new \coding_exception(
6a0189eb 101 'start_progress() max value cannot be negative');
16cd7088 102 }
103 if ($parentcount < 1) {
809fdb83 104 throw new \coding_exception(
16cd7088 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) {
809fdb83 112 throw new \coding_exception(
16cd7088 113 'start_progress() parent progress would exceed max');
114 }
115 }
116 } else {
117 if ($parentcount != 1) {
809fdb83 118 throw new \coding_exception(
16cd7088 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();
16cd7088 127 }
128
129 /**
130 * Marks the end of an operation that will display progress.
131 *
eb50c6e1 132 * This must be paired with each {@link start_progress} call.
16cd7088 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 *
809fdb83 137 * @throws \coding_exception If progress hasn't been started
16cd7088 138 */
139 public function end_progress() {
140 if (!count($this->descriptions)) {
809fdb83 141 throw new \coding_exception('end_progress() without start_progress()');
16cd7088 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 }
156
157 /**
158 * Indicates that progress has occurred.
159 *
160 * The progress value should indicate the total progress so far, from 0
eb50c6e1 161 * to the value supplied for $max (inclusive) in {@link start_progress}.
16cd7088 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
eb50c6e1 165 * doesn't update the display if called more than once per second.
16cd7088 166 *
eb50c6e1 167 * It must be INDETERMINATE if {@link start_progress} was called with $max set to
16cd7088 168 * INDETERMINATE. Otherwise it must not be indeterminate.
169 *
170 * @param int $progress Progress so far
809fdb83 171 * @throws \coding_exception If progress value is invalid
16cd7088 172 */
173 public function progress($progress = self::INDETERMINATE) {
16cd7088 174 // Check we are inside a progress section.
175 $max = end($this->maxes);
176 if ($max === false) {
809fdb83 177 throw new \coding_exception(
16cd7088 178 'progress() without start_progress');
179 }
180
181 // Check and apply new progress.
182 if ($progress === self::INDETERMINATE) {
183 // Indeterminate progress.
184 if ($max !== self::INDETERMINATE) {
809fdb83 185 throw new \coding_exception(
16cd7088 186 'progress() INDETERMINATE, expecting value');
187 }
188 } else {
189 // Determinate progress.
190 $current = end($this->currents);
191 if ($max === self::INDETERMINATE) {
809fdb83 192 throw new \coding_exception(
16cd7088 193 'progress() with value, expecting INDETERMINATE');
194 } else if ($progress < 0 || $progress > $max) {
809fdb83 195 throw new \coding_exception(
16cd7088 196 'progress() value out of range');
197 } else if ($progress < $current) {
809fdb83 198 throw new \coding_exception(
16cd7088 199 'progress() value may not go backwards');
200 }
201 $this->currents[key($this->currents)] = $progress;
202 }
203
eb50c6e1
JP
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 }
209
16cd7088 210 // Update progress.
211 $this->count++;
212 $this->lastprogresstime = $now;
6edd90cc 213
214 // Update time limit before next progress display.
ef81a7c3 215 \core_php_time_limit::raise(self::TIME_LIMIT_WITHOUT_PROGRESS);
16cd7088 216 $this->update_progress();
217 }
218
eb50c6e1
JP
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 }
233
16cd7088 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 }
242
243 /**
244 * Called whenever new progress should be displayed.
245 */
246 protected abstract function update_progress();
247
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 }
254
37ff843d 255 /**
256 * Checks max value of current progress section.
257 *
eb50c6e1 258 * @return int Current max value - may be {@link \core\progress\base::INDETERMINATE}.
809fdb83 259 * @throws \coding_exception If not in a progress section
37ff843d 260 */
261 public function get_current_max() {
262 $max = end($this->maxes);
263 if ($max === false) {
809fdb83 264 throw new \coding_exception('Not inside progress section');
37ff843d 265 }
266 return $max;
267 }
268
16cd7088 269 /**
809fdb83 270 * @throws \coding_exception
16cd7088 271 * @return string Current progress section description
272 */
273 public function get_current_description() {
274 $description = end($this->descriptions);
275 if ($description === false) {
809fdb83 276 throw new \coding_exception('Not inside progress section');
16cd7088 277 }
278 return $description;
279 }
280
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 }
307
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 }
314
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 }
328
329 // If there was nothing indeterminate, we use the min value as current.
330 return array($min, $min);
331 }
332
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 }
347}