001    /**
002     * iFish - An iRiver iHP jukebox database creation tool
003     *
004     * Copyright (C) 2009 Richard "Shred" Körber
005     *   http://ifish.shredzone.org
006     *
007     * This program is free software: you can redistribute it and/or modify
008     * it under the terms of the GNU General Public License as published by
009     * the Free Software Foundation, either version 3 of the License, or
010     * (at your option) any later version.
011     *
012     * This program is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
015     * GNU General Public License for more details.
016     *
017     * You should have received a copy of the GNU General Public License
018     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
019     */
020    package net.shredzone.ifish.gui;
021    
022    import net.shredzone.ifish.db.*;
023    import javax.swing.*;
024    import java.io.File;
025    
026    /**
027     * This is an extension of the NaviDb class to make it usable in JTables.
028     *
029     * @author    Richard Körber &lt;dev@shredzone.de&gt;
030     * @version   $Id: StatusProgressBar.java 291 2009-04-28 21:29:27Z shred $
031     */
032    public final class StatusProgressBar extends JProgressBar implements StatusCallback, Runnable {
033      private static final long serialVersionUID = 3258416123057027379L;
034    
035      private int max;
036      private int index;
037      private String action;
038      private long starttime;
039      private Boolean changed = Boolean.FALSE;
040      private boolean countdown = false;
041      
042      /**
043       * Create a new StatusProgressBar.
044       */
045      public StatusProgressBar() {
046        super();
047        setString( " " );
048        setStringPainted( true );
049        Thread th = new Thread( this, "StatusProgressBar" );
050        th.setDaemon( true );
051        th.start();
052      }
053      
054      /**
055       * Set the current action that is being done.
056       *
057       * @param   action      Action string
058       */
059      public void setAction( String action ) {
060        if( action==null || action.equals("") )
061          this.action = " ";
062        else 
063          this.action = action;
064      }
065      
066      /**
067       * Show that the process is finished. This will also disable the
068       * countdown mode automatically.
069       */
070      public void done() {
071        synchronized( changed ) {
072          changed = Boolean.FALSE;
073        }
074        setCountdown( false );
075        setMinimum( 0 );
076        setMaximum( 1 );
077        setValue( 0 );
078        setString( " " );
079        setStringPainted( true );
080        setIndeterminate( false );
081      }
082    
083      /**
084       * Enable the countdown. The StatusProgressBar will now try to estimate
085       * the time until the index reaches the maximum. 
086       * <p>
087       * If countdown is enabled, you must make sure
088       * to call <code>setMaxEntries()</code> only once at the beginning,
089       * and you must ensure that <code>setCurrentIndex()</code> is always
090       * only incrementing!
091       *
092       * @param   cntdown       true: activate countdown, false: disactivate
093       */
094      public void setCountdown( boolean cntdown ) {
095        starttime = System.currentTimeMillis();
096        this.countdown = cntdown;
097      }
098      
099    
100      /**
101       * Check if countdown is enabled.
102       *
103       * @return    true: countdown activated, false: disactivated
104       */
105      public boolean isCountdown() {
106        return countdown;
107      }
108    
109      /**
110       * Set the maximum number of entries to be processed by the current
111       * action. If max is negative, then the maximum number is not known.
112       * Anyhow, during the operation setMaxEntries could be invoked multiple
113       * times.
114       *
115       * @param     max       Maximum number of entries.
116       */
117      @Override
118      public void setMaxEntries( int max ) {
119        synchronized( changed ) {
120          this.max = max;
121          starttime = System.currentTimeMillis();
122          changed = Boolean.TRUE;
123        }
124      }
125      
126      /**
127       * Set the current entry number that is being processed by the current
128       * action. If the number is negative, then the current entry number is
129       * not known.
130       *
131       * @param     index     Current entry.
132       */
133      @Override
134      public void setCurrentIndex( int index ) {
135        synchronized( changed ) {
136          this.index = index;
137          changed = Boolean.TRUE;
138        }
139      }
140    
141      /**
142       * Set the current entry that is being processed by the current
143       * action. If null is passed, then no Entry is currently processed,
144       * or there are no Entries being processed.
145       *
146       * @param     entry     Entry currently being processed, or null.
147       */
148      @Override
149      public void setCurrentEntry( Entry entry ) {
150        /*TODO: nothing for now... We could show filenames later. */
151      }
152      
153      /**
154       * Set the current directory being processed. If there are no
155       * directories to be processed, dir will be null. Note that this
156       * method might be invoked several times, even without any changes.
157       *
158       * @param     base      Base directory
159       * @param     dir       Current directory
160       */
161      @Override
162      public void setCurrentDir( File base, File dir ) {
163        /*TODO: nothing for now... We could show filenames later. */
164      }
165      
166      /**
167       * Runnable implementation. All StatusCallback data is cached. The
168       * progress bar will only be updated every 500ms, to gain some
169       * performance. In this thread, all the text concatenations and
170       * countdown calculations are done as well, so the CPU does not
171       * need to bother too much with this time consuming calculations.
172       */
173      @Override
174      public void run() {
175        try {
176          while(true) {
177            Thread.sleep( 500 );
178            synchronized( changed ) {
179              if( changed.booleanValue() ) {
180                if( index>=0 ) {
181                  StringBuffer text = new StringBuffer( action );
182                  
183                  //--- Percent ---
184                  if( max>0 && index>=0 ) {
185                    text.append( ": " );
186                    if( index<=max ) {
187                      text.append( index*100 / max );
188                    }else {
189                      text.append("100");
190                    }
191                    text.append( '%' );
192                  }
193                  
194                  //--- ETA ---
195                  if( countdown ) {
196                    int eta = estimateTime();
197                    if( eta>=0 ) {
198                      text.append( " - " );
199                      text.append( eta/60 );
200                      text.append( ':' );
201                      if( eta%60<10 )
202                        text.append( '0' );
203                      text.append( eta%60 );
204                    }
205                  }
206                  
207                  //--- Set the progress bar ---
208                  setMinimum( 0 );
209                  setMaximum( max );
210                  setValue( index );
211                  setString( text.toString() );
212                  setIndeterminate( false );
213                }else {
214                  //--- Set to indeterminate state ---
215                  setString( action );
216                  setMinimum( 0 );
217                  setMaximum( 1 );
218                  setValue( 0 );
219                  setIndeterminate( true );
220                }
221                changed = Boolean.FALSE;
222              }
223            }
224          }
225        }catch( Exception e ) {}
226      }
227    
228      /**
229       * When countdown is enabled, this method will try to estimate the time
230       * until index reaches maximum. The estimated time in seconds will be
231       * returned. If an estimation is not possible yet, -1 will be returned.
232       *
233       * @return    Estimated Time of Arrival
234       */
235      public int estimateTime() {
236        int cMax   = max;
237        int cIndex = index;
238        
239        //--- Check prerequisites ---
240        if( !countdown || cMax<=0 || cIndex<=0 ) return -1;
241    
242        //--- Compute elapsed time ---
243        long current = System.currentTimeMillis();
244        long elapsed = current - starttime;
245        
246        // Wait at least 3 seconds before estimating. Before that period,
247        // the guesswork is rather utopian than reasonable.
248        if( elapsed<3000 ) return -1;
249        
250        //--- Compute ETA and required time ---
251        // The ETA is extrapolated from the elapsed time and the index
252        // in relation to the given maximum.
253        long eta = ( (elapsed * cMax) / cIndex ) + starttime;
254        long required = (eta - current) / 1000;     // floored, in seconds
255        
256        //--- Out of range? ---
257        if( required<0 || required>Integer.MAX_VALUE ) return -1;
258        
259        //--- Return the time ---
260        return (int) required;
261      }
262    
263    }