/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 1999-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library 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
* Lesser General Public License for more details.
*/
package org.geotoolkit.gui.headless;
import java.util.Locale;
import java.util.Objects;
import java.io.PrintWriter;
import java.text.NumberFormat;
import java.text.BreakIterator;
import org.opengis.util.InternationalString;
import org.apache.sis.util.CharSequences;
import org.geotoolkit.resources.Vocabulary;
import org.geotoolkit.nio.IOUtilities;
import org.geotoolkit.process.ProgressController;
/**
* Prints progress report of a lengthly operation to an output stream. Progress are reported
* as percentage on a single line. This class can also prints warning, which is useful for
* notifications without stopping the lengthly task.
*
* @author Martin Desruisseaux (MPO, IRD, Geomatys)
* @author Guilhem Legal (Geomatys)
* @version 3.20
*
* @since 1.0
* @module
*/
public class ProgressPrinter extends ProgressController {
/**
* The stream where to write progress reports.
*/
private final PrintWriter out;
/**
* {@code true} if {@code '\r'} brings the cursor back to the beginning of current line.
* We assume {@code true} if the system do not uses the {@code "\r\n"} pair for going
* to the next line (like VAX-VMS systems).
*/
private final boolean CR_supported;
/**
* The maximal line length. Note that the amount of spaces really available is slightly
* less since some spaces will be used at the beginning of the line.
*/
private final int maxLength;
/**
* Amount of characters used last time a line was sent to the output stream.
* This field is updated by the {@link #carriageReturn} method.
*/
private int lastLength;
/**
* Position where to write the percentage.
* This field is updated by the {@link #progress} method.
*/
private int percentPosition;
/**
* The last percentage written, or {@code null} if none. This is used in order to avoid
* sending many time the same line with invisible change.
*/
private String lastPercent;
/**
* The format to use for writing the percentage.
*/
private NumberFormat format;
/**
* Object used for breaking a long warning message on many lines.
*/
private BreakIterator breaker;
/**
* {@code true} if this printer has written at least one warning.
*/
private boolean hasPrintedWarning;
/**
* The source of the last warnings, or {@code null} if unknown.
* Used in order to avoid repeating this information many time.
*/
private String lastSource;
/**
* Constructs a new object sending progress reports to the
* {@linkplain java.lang.System#out standard output stream}.
* The maximal line length is assumed to be 80 characters.
*/
public ProgressPrinter() {
this(IOUtilities.standardPrintWriter());
}
/**
* Constructs a new object sending progress reports to the specified stream.
* The maximal line length is assumed 80 characters.
*
* @param out The output stream.
*/
public ProgressPrinter(final PrintWriter out) {
this(out, 80);
}
/**
* Constructs a new object sending progress reports to the specified stream.
*
* @param out The output stream.
* @param maxLength The maximal line length. This is used by {@link #warningOccurred}
* for splitting longer lines into many lines.
*/
public ProgressPrinter(final PrintWriter out, final int maxLength) {
this.out = out;
this.maxLength = maxLength;
final String lineSeparator = System.lineSeparator();
CR_supported = lineSeparator.equals("\r\n") || lineSeparator.equals("\n");
}
/**
* Erases the remainder of current line (if needed), then moves the cursor to the beginning
* of current line. If carriage returns are not supported, then this method will rather move
* to the next line. If every cases, the cursor will be at the beginning of a line and the
* {@link #lastLength} field will have the {@code length} value.
*
* @param length Number of characters which have been written on the current line.
*/
private void carriageReturn(final int length) {
if (CR_supported && length<maxLength) {
for (int i=length; i<lastLength; i++) {
out.print(' ');
}
out.print('\r');
out.flush();
} else {
out.println();
}
lastLength = length;
}
/**
* Adds dots at the end of current line, just before to write the progress percentage.
* This method is used only for terminals that do not support carriage returns.
*
* @param percent Progress percentage between 0 and 100.
*/
private void completeBar(final float percent) {
final int end = (int) ((percent/100) * ((maxLength-2) - percentPosition)); // Round toward 0.
while (lastLength < end) {
out.print('.');
lastLength++;
}
}
/**
* Notifies this controller that the operation begins.
*/
@Override
public synchronized void started() {
int length = 0;
final InternationalString task = getTask();
if (task != null) {
final String asString = task.toString(getLocale());
out.print(asString);
length = asString.length();
}
if (CR_supported) {
carriageReturn(length);
}
out.flush();
percentPosition = length;
lastPercent = null;
lastSource = null;
hasPrintedWarning = false;
}
/**
* Notifies this controller that the operation is suspended.
*/
@Override
public synchronized void paused() {
final String message = Vocabulary.getResources(getLocale()).getString(Vocabulary.Keys.Paused);
out.print(message);
carriageReturn(message.length());
}
/**
* Notifies this controller that the operation is resumed.
*/
@Override
public synchronized void resumed() {
final String message = Vocabulary.getResources(getLocale()).getString(Vocabulary.Keys.Resumed);
out.print(message);
carriageReturn(message.length());
}
/**
* Notifies this controller of progress in the lengthly operation. Progress are reported
* as a value between 0 and 100 inclusive. Values out of bounds will be clamped.
*
* @param percent The progress as a value between 0 and 100 inclusive.
*/
@Override
public synchronized void setProgress(float percent) {
if (percent < 0 ) percent = 0;
if (percent > 100) percent = 100;
if (CR_supported) {
/*
* Si le périphérique de sortie supporte les retours chariot,
* on écrira l'état d'avancement comme un pourcentage après
* la description, comme dans "Lecture des données (38%)".
*/
if (lastPercent == null || percent != super.getProgress()) {
if (format == null) {
format = NumberFormat.getPercentInstance();
}
final String text = format.format(percent / 100.0);
if (!text.equals(lastPercent)) {
int length = text.length();
percentPosition = 0;
final InternationalString task = getTask();
if (task != null) {
final String asString = task.toString(getLocale());
out.print(asString);
out.print(' ');
length += (percentPosition = asString.length()) + 1;
}
out.print('(');
out.print(text);
out.print(')');
length += 2;
carriageReturn(length);
lastPercent = text;
}
super.setProgress(percent);
}
} else {
/*
* Si le périphérique ne supporte par les retours chariots, on
* écrira l'état d'avancement comme une série de points placés
* après la description, comme dans "Lecture des données......"
*/
completeBar(percent);
super.setProgress(percent);
out.flush();
}
}
/**
* Prints a warning. The first time this method is invoked, the localized word "WARNING" will
* be printed in the middle of a box. If a source is specified, it will be printed only if it
* is not the same one than the source of the last warning. If a margin is specified, it will
* be printed of the left side of the first line of the warning message.
*
* @param source The source of the warning, or {@code null} if none. This is typically the
* filename in process of being parsed.
* @param margin Text to write on the left side of the warning message, or {@code null} if none.
* This is typically the line number where the error occurred in the {@code source} file.
* @param warning The warning message. If this string is longer than the maximal length
* specified at construction time (80 characters by default), then it will be splitted
* in as many lines as needed and indented according the margin width.
*/
@Override
public synchronized void warningOccurred(final String source, String margin, final String warning) {
carriageReturn(0);
final Locale locale = getLocale();
if (!hasPrintedWarning) {
printInBox(Vocabulary.getResources(locale).getString(Vocabulary.Keys.Warning));
hasPrintedWarning = true;
}
if (!Objects.equals(source, lastSource)) {
out.println();
out.println(source != null ? source : Vocabulary.getResources(locale).getString(Vocabulary.Keys.Untitled));
lastSource = source;
}
/*
* Procède à l'écriture de l'avertissement avec (de façon optionnelle)
* quelque chose dans la marge (le plus souvent un numéro de ligne).
*/
String prefix = " ";
String second = prefix;
if (margin != null) {
margin = trim(margin);
if (!margin.isEmpty()) {
prefix = prefix + '(' + margin + ") ";
second = CharSequences.spaces(prefix.length()).toString();
}
}
int width = maxLength - prefix.length() - 1;
if (breaker == null) {
breaker = BreakIterator.getLineInstance();
}
breaker.setText(warning);
int start = breaker.first(), end = start, nextEnd;
while ((nextEnd = breaker.next()) != BreakIterator.DONE) {
while (nextEnd - start > width) {
if (end <= start) {
end = Math.min(nextEnd, start + width);
}
out.print(prefix);
out.println(warning.substring(start, end));
prefix = second;
start = end;
}
end=Math.min(nextEnd, start + width);
}
if (end > start) {
out.print(prefix);
out.println(warning.substring(start, end));
}
final InternationalString task = getTask();
if (!CR_supported && task != null) {
out.print(task.toString(locale));
completeBar(super.getProgress());
}
out.flush();
}
/**
* Prints an exception stack trace in a box.
*/
@Override
public synchronized void exceptionOccurred(final Throwable exception) {
carriageReturn(0);
printInBox(Vocabulary.getResources(getLocale()).getString(Vocabulary.Keys.Exception));
exception.printStackTrace(out);
hasPrintedWarning = false;
out.flush();
}
/**
* Returns the {@code margin} string without parenthesis.
*/
private static String trim(String margin) {
margin = margin.trim();
int lower = 0;
int upper = margin.length();
while (lower<upper && margin.charAt(lower+0) == '(') lower++;
while (lower<upper && margin.charAt(upper-1) == ')') upper--;
return margin.substring(lower, upper);
}
/**
* Writes the given text in a box. The given string shall be a single line.
* The box dimension will be adjusted automatically.
*/
private void printInBox(String text) {
int length = text.length();
for (int pass=-2; pass<=2; pass++) {
switch (Math.abs(pass)) {
case 2: {
for (int j=-10; j<length; j++) {
out.print('*');
}
out.println();
break;
}
case 1: {
out.print("**");
out.print(CharSequences.spaces(length + 6));
out.println("**");
break;
}
case 0: {
out.print("** ");
out.print(text);
out.println(" **");
break;
}
}
}
}
/**
* Notifies this listener that the operation has finished. The progress indicator will
* shows 100% or disappears. If warning messages were pending, they will be printed now.
*/
@Override
public synchronized void completed() {
if (!CR_supported) {
completeBar(100);
}
carriageReturn(0);
out.flush();
}
}