Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
 
17
namespace core\progress;
18
 
19
defined('MOODLE_INTERNAL') || die();
20
 
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;
36
 
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;
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
    /**
73
     * @var int[] Array of counts within parent progress entry (ignored for first)
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
     *
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
    }
128
 
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
    }
156
 
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
        }
180
 
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
        }
203
 
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
 
210
        // Update progress.
211
        $this->count++;
212
        $this->lastprogresstime = $now;
213
 
214
        // Update time limit before next progress display.
215
        \core_php_time_limit::raise(self::TIME_LIMIT_WITHOUT_PROGRESS);
216
        $this->update_progress();
217
    }
218
 
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
 
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
    abstract protected 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
 
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
    }
268
 
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
    }
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
}