/*
* This file is part of the Illarion project.
*
* Copyright © 2015 - Illarion e.V.
*
* Illarion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Illarion 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.
*/
package illarion.common.util;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
/**
* This utility class is used to track the progress of any action. Its very handy of complex tasks need to be
* monitored.
*
* @author Martin Karing <nitram@illarion.org>
*/
public final class ProgressMonitor implements ProgressMonitorCallback {
/**
* The progress of this monitor. This value does not apply in case there are any children applied to this class.
*/
private float progress;
/**
* The weight of this loading operation. This applies in case this monitor is a child to another progress monitor,
* The weight is compared to the siblings of this monitor. A monitor with a weight of two has twice the effect to
* the resulting progress of the monitor compared to a monitor with a weight of one.
*/
private float weight;
/**
* The list of children of this monitor.
*/
@Nullable
private List<ProgressMonitor> children;
/**
* The callback that is required to receive the updates of the monitor in case there is any.
*/
@Nullable
private ProgressMonitorCallback callback;
/**
* This is set {@code true} while a callback is in progress. This is used to avoid too many active callbacks.
*/
private boolean activeCallback;
/**
* This is set {@code true} in case another callback was requested while there was a active one. If causes the
* callback to be repeated.
*/
private boolean repeatCallback;
/**
* Create a new progress monitor with a custom weight value.
*
* @param weight the weight value of this progress monitor
* @throws IllegalArgumentException in case the weight is set to a value less then {@code 0.f} or to NaN or to
* Infinity
*/
public ProgressMonitor(float weight) {
if (weight < 0.f) {
throw new IllegalArgumentException("weight may not be less then 0");
}
if (Float.isInfinite(weight) || Float.isNaN(weight)) {
throw new IllegalArgumentException("weight may not infinite or NaN");
}
this.weight = weight;
}
/**
* Create a new progress monitor with the default weight of one.
*/
public ProgressMonitor() {
this(1.f);
}
@Override
public void updatedProgress(@Nonnull ProgressMonitor monitor) {
reportProgressChange();
}
/**
* Set the callback that is supposed to receive update information from the progress monitor.
*
* @param callback the callback target or {@code null} in case no more callbacks are required
*/
public void setCallback(@Nullable ProgressMonitorCallback callback) {
this.callback = callback;
if (children != null) {
for (ProgressMonitor child : children) {
if (callback == null) {
child.setCallback(null);
} else {
child.setCallback(this);
}
}
}
}
/**
* Add a children to this progress tracker.
*
* @param childMonitor the children that is supposed to be monitored now
*/
public void addChild(@Nonnull ProgressMonitor childMonitor) {
if (children == null) {
children = new ArrayList<>();
}
children.add(childMonitor);
ProgressMonitorCallback targetCallback = callback;
if (targetCallback != null) {
childMonitor.setCallback(targetCallback);
}
}
/**
* Set the new value of the progress. This operation is only allowed in case there are no children applied to
* this monitor.
*
* @param progress the new progress value, the value is capped to {@code 0.f} to {@code 1.f}
* @throws IllegalStateException in case this monitor has children applied to it
*/
public void setProgress(float progress) {
if (children != null) {
throw new IllegalStateException("Setting the progress of a monitor with children is not allowed.");
}
float oldValue = this.progress;
this.progress = FastMath.clamp(progress, 0.f, 1.f);
if (!FastMath.equals(oldValue, this.progress, FastMath.FLT_EPSILON)) {
reportProgressChange();
}
}
private void reportProgressChange() {
if (activeCallback) {
repeatCallback = true;
return;
}
ProgressMonitorCallback targetCallback = callback;
if (targetCallback != null) {
boolean firstRun = true;
while (firstRun || repeatCallback) {
firstRun = false;
repeatCallback = false;
activeCallback = true;
try {
targetCallback.updatedProgress(this);
} catch (@Nonnull Exception e) {
// nothing
} finally {
activeCallback = false;
}
}
}
}
/**
* Set the weight of this monitor to a new value.
*
* @param weight the new weight of the monitor
*/
public void setWeight(float weight) {
this.weight = weight;
}
/**
* Get the progress of this monitor. This either returns the progress of this monitor or the total weighted
* progress of all the children progress monitors.
*
* @return the progress as a value between {@code 0.f} and {@code 1.f}
*/
public float getProgress() {
if (children == null) {
return progress;
}
float totalProgress = 0.f;
float totalWeight = 0.f;
for (@Nonnull ProgressMonitor childMonitor : children) {
totalProgress += childMonitor.getProgress() * childMonitor.weight;
totalWeight += childMonitor.weight;
}
return totalProgress / totalWeight;
}
}