Merge branch 'subtree'
[fanfix.git] / src / be / nikiroo / utils / Progress.java
1 package be.nikiroo.utils;
2
3 import java.util.ArrayList;
4 import java.util.EventListener;
5 import java.util.HashMap;
6 import java.util.List;
7 import java.util.Map;
8 import java.util.Map.Entry;
9
10 /**
11 * Progress reporting system, possibly nested.
12 * <p>
13 * A {@link Progress} can have a name, and that name will be reported through
14 * the event system (it will report the first non-null name in the stack from
15 * the {@link Progress} from which the event originated to the parent the event
16 * is listened on).
17 * <p>
18 * The {@link Progress} also has a table of keys/values shared amongst all the
19 * hierarchy (note that when adding a {@link Progress} to others, its values
20 * will be prioritized if some with the same keys were already present in the
21 * hierarchy).
22 *
23 * @author niki
24 */
25 public class Progress {
26 /**
27 * This event listener is designed to report progress events from
28 * {@link Progress}.
29 *
30 * @author niki
31 */
32 public interface ProgressListener extends EventListener {
33 /**
34 * A progression event.
35 *
36 * @param progress
37 * the {@link Progress} object that generated it, not
38 * necessarily the same as the one where the listener was
39 * attached (it could be a child {@link Progress} of this
40 * {@link Progress}).
41 * @param name
42 * the first non-null name of the {@link Progress} step that
43 * generated this event
44 */
45 public void progress(Progress progress, String name);
46 }
47
48 private Map<Object, Object> map = new HashMap<Object, Object>();
49 private Progress parent = null;
50 private Object lock = new Object();
51 private String name;
52 private Map<Progress, Double> children;
53 private List<ProgressListener> listeners;
54 private int min;
55 private int max;
56 private double relativeLocalProgress;
57 private double relativeProgress; // children included
58
59 /**
60 * Create a new default unnamed {@link Progress}, from 0 to 100.
61 */
62 public Progress() {
63 this(null);
64 }
65
66 /**
67 * Create a new default {@link Progress}, from 0 to 100.
68 *
69 * @param name
70 * the name of this {@link Progress} step
71 */
72 public Progress(String name) {
73 this(name, 0, 100);
74 }
75
76 /**
77 * Create a new unnamed {@link Progress}, from min to max.
78 *
79 * @param min
80 * the minimum progress value (and starting value) -- must be
81 * non-negative
82 * @param max
83 * the maximum progress value
84 */
85 public Progress(int min, int max) {
86 this(null, min, max);
87 }
88
89 /**
90 * Create a new {@link Progress}, from min to max.
91 *
92 * @param name
93 * the name of this {@link Progress} step
94 * @param min
95 * the minimum progress value (and starting value) -- must be
96 * non-negative
97 * @param max
98 * the maximum progress value
99 */
100 public Progress(String name, int min, int max) {
101 this.name = name;
102 this.children = new HashMap<Progress, Double>();
103 this.listeners = new ArrayList<Progress.ProgressListener>();
104 setMinMax(min, max);
105 setProgress(min);
106 }
107
108 /**
109 * The name of this {@link Progress} step.
110 *
111 * @return the name, can be NULL
112 */
113 public String getName() {
114 return name;
115 }
116
117 /**
118 * The name of this {@link Progress} step.
119 *
120 * @param name
121 * the new name
122 */
123 public void setName(String name) {
124 this.name = name;
125 changed(this, name);
126 }
127
128 /**
129 * The minimum progress value.
130 *
131 * @return the min
132 */
133 public int getMin() {
134 return min;
135 }
136
137 /**
138 * The minimum progress value.
139 *
140 * @param min
141 * the min to set
142 *
143 *
144 * @throws RuntimeException
145 * if min &lt; 0 or if min &gt; max
146 */
147 public void setMin(int min) {
148 if (min < 0) {
149 throw new RuntimeException("negative values not supported");
150 }
151
152 synchronized (lock) {
153 if (min > max) {
154 throw new RuntimeException(
155 "The minimum progress value must be <= the maximum progress value");
156 }
157
158 this.min = min;
159 }
160 }
161
162 /**
163 * The maximum progress value.
164 *
165 * @return the max
166 */
167 public int getMax() {
168 return max;
169 }
170
171 /**
172 * The maximum progress value (must be >= the minimum progress value).
173 *
174 * @param max
175 * the max to set
176 *
177 *
178 * @throws RuntimeException
179 * if max &lt; min
180 */
181 public void setMax(int max) {
182 synchronized (lock) {
183 if (max < min) {
184 throw new Error(
185 "The maximum progress value must be >= the minimum progress value");
186 }
187
188 this.max = max;
189 }
190 }
191
192 /**
193 * Set both the minimum and maximum progress values.
194 *
195 * @param min
196 * the min
197 * @param max
198 * the max
199 *
200 * @throws RuntimeException
201 * if min &lt; 0 or if min &gt; max
202 */
203 public void setMinMax(int min, int max) {
204 if (min < 0) {
205 throw new RuntimeException("negative values not supported");
206 }
207
208 if (min > max) {
209 throw new RuntimeException(
210 "The minimum progress value must be <= the maximum progress value");
211 }
212
213 synchronized (lock) {
214 this.min = min;
215 this.max = max;
216 }
217 }
218
219 /**
220 * Get the total progress value (including the optional children
221 * {@link Progress}) on a {@link Progress#getMin()} to
222 * {@link Progress#getMax()} scale.
223 *
224 * @return the progress the value
225 */
226 public int getProgress() {
227 return (int) Math.round(relativeProgress * (max - min));
228 }
229
230 /**
231 * Set the local progress value (not including the optional children
232 * {@link Progress}), on a {@link Progress#getMin()} to
233 * {@link Progress#getMax()} scale.
234 *
235 * @param progress
236 * the progress to set
237 */
238 public void setProgress(int progress) {
239 synchronized (lock) {
240 double childrenProgress = relativeProgress - relativeLocalProgress;
241
242 relativeLocalProgress = ((double) progress) / (max - min);
243
244 setRelativeProgress(this, name,
245 relativeLocalProgress + childrenProgress);
246 }
247 }
248
249 /**
250 * Get the total progress value (including the optional children
251 * {@link Progress}) on a 0.0 to 1.0 scale.
252 *
253 * @return the progress
254 */
255 public double getRelativeProgress() {
256 return relativeProgress;
257 }
258
259 /**
260 * Set the total progress value (including the optional children
261 * {@link Progress}), on a 0 to 1 scale.
262 * <p>
263 * Will generate a changed event from this very {@link Progress}.
264 *
265 * @param relativeProgress
266 * the progress to set
267 */
268 public void setRelativeProgress(double relativeProgress) {
269 setRelativeProgress(this, name, relativeProgress);
270 }
271
272 /**
273 * Set the total progress value (including the optional children
274 * {@link Progress}), on a 0 to 1 scale.
275 *
276 * @param pg
277 * the {@link Progress} to report as the progression emitter (can
278 * be NULL, will then be considered the same as <tt>this</tt>)
279 * @param name
280 * the current name (if it is NULL, the first non-null name in
281 * the hierarchy will overwrite it) of the {@link Progress} who
282 * emitted this change
283 * @param relativeProgress
284 * the progress to set
285 */
286 private void setRelativeProgress(Progress pg, String name,
287 double relativeProgress) {
288 synchronized (lock) {
289 relativeProgress = Math.max(0, relativeProgress);
290 relativeProgress = Math.min(1, relativeProgress);
291 this.relativeProgress = relativeProgress;
292
293 changed(pg, name);
294 }
295 }
296
297 /**
298 * Get the total progress value (including the optional children
299 * {@link Progress}) on a 0 to 1 scale.
300 *
301 * @return the progress the value
302 */
303 private int getLocalProgress() {
304 return (int) Math.round(relativeLocalProgress * (max - min));
305 }
306
307 /**
308 * Add some value to the current progression of this {@link Progress}.
309 *
310 * @param step
311 * the amount to add
312 */
313 public void add(int step) {
314 synchronized (lock) {
315 setProgress(getLocalProgress() + step);
316 }
317 }
318
319 /**
320 * Check if the action corresponding to this {@link Progress} is done (i.e.,
321 * if its progress value == its max value).
322 *
323 * @return TRUE if it is
324 */
325 public boolean isDone() {
326 return getProgress() == max;
327 }
328
329 /**
330 * Mark the {@link Progress} as done by setting its value to max.
331 */
332 public void done() {
333 synchronized (lock) {
334 double childrenProgress = relativeProgress - relativeLocalProgress;
335 relativeLocalProgress = 1 - childrenProgress;
336 setRelativeProgress(this, name, 1d);
337 }
338 }
339
340 /**
341 * Return the list of direct children of this {@link Progress}.
342 * <p>
343 * Can return an empty list, but never NULL.
344 *
345 * @return the children (Who will think of the children??)
346 */
347 public List<Progress> getChildren() {
348 synchronized (lock) {
349 return new ArrayList<Progress>(children.keySet());
350 }
351 }
352
353 /**
354 * The weight of this children if it is actually a child of this.
355 *
356 * @param child
357 * the child to get the weight of
358 *
359 * @return NULL if this is not a child of this
360 */
361 public Double getWeight(Progress child) {
362 synchronized (lock) {
363 return children.get(child);
364 }
365 }
366
367 /**
368 * Notify the listeners that this {@link Progress} changed value.
369 *
370 * @param pg
371 * the emmiter, that is, the (sub-){link Progress} that just
372 * reported some change, not always the same as <tt>this</tt>
373 * @param name
374 * the current name (if it is NULL, the first non-null name in
375 * the hierarchy will overwrite it) of the {@link Progress} who
376 * emitted this change
377 */
378 private void changed(Progress pg, String name) {
379 if (pg == null) {
380 pg = this;
381 }
382
383 if (name == null) {
384 name = this.name;
385 }
386
387 synchronized (lock) {
388 for (ProgressListener l : listeners) {
389 l.progress(pg, name);
390 }
391 }
392 }
393
394 /**
395 * Add a {@link ProgressListener} that will trigger on progress changes.
396 * <p>
397 * Note: the {@link Progress} that will be reported will be the active
398 * progress, not necessarily the same as the current one (it could be a
399 * child {@link Progress} of this {@link Progress}).
400 *
401 * @param l
402 * the listener
403 */
404 public void addProgressListener(ProgressListener l) {
405 synchronized (lock) {
406 this.listeners.add(l);
407 }
408 }
409
410 /**
411 * Remove a {@link ProgressListener} that would trigger on progress changes.
412 *
413 * @param l
414 * the listener
415 *
416 * @return TRUE if it was found (and removed)
417 */
418 public boolean removeProgressListener(ProgressListener l) {
419 synchronized (lock) {
420 return this.listeners.remove(l);
421 }
422 }
423
424 /**
425 * Add a child {@link Progress} of the given weight.
426 *
427 * @param progress
428 * the child {@link Progress} to add
429 * @param weight
430 * the weight (on a {@link Progress#getMin()} to
431 * {@link Progress#getMax()} scale) of this child
432 * {@link Progress} in relation to its parent
433 *
434 * @throws RuntimeException
435 * if weight exceed {@link Progress#getMax()} or if progress
436 * already has a parent
437 */
438 public void addProgress(Progress progress, double weight) {
439 if (weight < min || weight > max) {
440 throw new RuntimeException(String.format(
441 "Progress object %s cannot have a weight of %f, "
442 + "it is outside of its parent (%s) range (%d)",
443 progress.name, weight, name, max));
444 }
445
446 if (progress.parent != null) {
447 throw new RuntimeException(String.format(
448 "Progress object %s cannot be added to %s, "
449 + "as it already has a parent (%s)",
450 progress.name, name, progress.parent.name));
451 }
452
453 ProgressListener progressListener = new ProgressListener() {
454 @Override
455 public void progress(Progress pg, String name) {
456 synchronized (lock) {
457 double total = relativeLocalProgress;
458 for (Entry<Progress, Double> entry : children.entrySet()) {
459 total += (entry.getValue() / (max - min))
460 * entry.getKey().getRelativeProgress();
461 }
462
463 setRelativeProgress(pg, name, total);
464 }
465 }
466 };
467
468 synchronized (lock) {
469 // Should not happen but just in case
470 if (this.map != progress.map) {
471 this.map.putAll(progress.map);
472 }
473 progress.map = this.map;
474 progress.parent = this;
475 this.children.put(progress, weight);
476 progress.addProgressListener(progressListener);
477 }
478 }
479
480 /**
481 * Set the given value for the given key on this {@link Progress} and it's
482 * children.
483 *
484 * @param key
485 * the key
486 * @param value
487 * the value
488 */
489 public void put(Object key, Object value) {
490 map.put(key, value);
491 }
492
493 /**
494 * Return the value associated with this key as a {@link String} if any,
495 * NULL if not.
496 * <p>
497 * If the value is not NULL but not a {@link String}, it will be converted
498 * via {@link Object#toString()}.
499 *
500 * @param key
501 * the key to check
502 *
503 * @return the value or NULL
504 */
505 public String getString(Object key) {
506 Object value = map.get(key);
507 if (value == null) {
508 return null;
509 }
510
511 return value.toString();
512 }
513
514 /**
515 * Return the value associated with this key if any, NULL if not.
516 *
517 * @param key
518 * the key to check
519 *
520 * @return the value or NULL
521 */
522 public Object get(Object key) {
523 return map.get(key);
524 }
525
526 @Override
527 public String toString() {
528 return "[Progress]" //
529 + (name == null || name.isEmpty() ? "" : " " + name) //
530 + ": " + getProgress() + " / " + getMax() //
531 + (children.isEmpty() ? ""
532 : " (with " + children.size() + " children)") //
533 ;
534 }
535 }