package org.autoplot.pngwalk;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.autoplot.pngwalk.WalkUtil.splitIndex;
import org.das2.datum.DatumRange;
import org.das2.datum.DatumRangeUtil;
import org.das2.datum.Units;
import org.das2.util.filesystem.FileSystem;
import org.autoplot.dom.DebugPropertyChangeSupport;
import org.autoplot.datasource.DataSetURI;
import org.das2.datum.TimeParser;
import org.das2.fsm.FileStorageModel;
import org.das2.qds.QDataSet;
import org.das2.qds.ops.Ops;
import org.das2.util.filesystem.FileSystemUtil;
/**
*
This class maintains a list of WalkImage
s and provides functionality
* for navigating through it. Whenever the current index is changed, whether explicitly
* set or via one of the navigation functions, a PropertyChangeEvent
is fired.
* This allows UI code to control the index and view code to respond.
*
* Access is also provided to the {@link WalkImage} objects so that view
* code can retrieve images, thumbnails, etc.
*
* @author Ed Jackson
*/
public class WalkImageSequence implements PropertyChangeListener {
private static final Logger logger= org.das2.util.LoggerManager.getLogger("autoplot.pngwalk");
// list of all possible images, without limit.
private List existingImages;
// list of the visible images, limited by "Limit range to" gui.
private List displayImages = new ArrayList();
//private List locations;
private boolean showMissing = false;
private boolean useSubRange = false;
private int index;
private DatumRange timeSpan = null;
// list of ranges for each existing image file.
private List datumRanges = null;
// list of ranges, including gaps between files.
private List possibleRanges = null;
private List subRange = null;
/**
* template used to create list. This may be null.
*/
private final String template;
/**
* the location of the base of the pngwalk.
*/
private URI baseURI;
private final PropertyChangeSupport pcs = new DebugPropertyChangeSupport(this);
//public static final String PROP_SHOWMISSING = "showMissing";
public static final String PROP_INDEX = "index";
public static final String PROP_SELECTED_INDECES = "selectedIndeces";
public static final String PROP_IMAGE_LOADED = "imageLoaded";
public static final String PROP_THUMB_LOADED = "thumbLoaded";
public static final String PROP_USESUBRANGE = "useSubRange";
public static final String PROP_SEQUENCE_CHANGED = "sequenceChanged";
public static final String PROP_BADGE_CHANGE = "badgeChange"; // For quality control icon badge
private URI qcFolder = null; //Location for quality control files, if used
private QualityControlSequence qualitySeq;
private String qcFilter=""; // limits what is shown.
private boolean haveThumbs400=true;
private boolean limitWarning= false;
/** Create an image sequence based on a URI template.
*
* @param template a template, or null will produce an empty walk sequence.
*/
public WalkImageSequence( String template ) {
this.template= template;
int i= WalkUtil.splitIndex(template);
if ( i==-1 ) {
throw new IllegalArgumentException("template does not contain /. Hrmph.");
}
try {
this.baseURI= new URI(template.substring(0,i));
} catch (URISyntaxException ex) {
throw new IllegalArgumentException(ex);
}
//call initialLoad before any other methods.
}
private DatumRange timerange = null;
public static final String PROP_TIMERANGE = "timerange";
/**
* constraint for the limit of the time ranges listed. Note timespan
* is what was found. This does not appear to be the current image
* timerange.
* @return
*/
public DatumRange getTimerange() {
return timerange;
}
/**
* constraint for the limit of the time ranges listed. Note timespan
* is what was found.
*
* @param timerange
*/
public void setTimerange(DatumRange timerange) {
DatumRange oldTimerange = this.timerange;
this.timerange = timerange;
pcs.firePropertyChange(PROP_TIMERANGE, oldTimerange, timerange);
}
/**
* return true if the files contain both start and end.
* TODO: this is a quick and dirty implementation that needs to be
* done thoroughly.
* @return
*/
private boolean templateHasExplicitEnd( ) {
if ( template.contains("$(Y;end)") ) return true;
return false;
}
/**
* do the initial listing of the remote filesystem. This should not
* be done on the event thread, and should be done before the
* sequence is used.
* @throws java.io.IOException
*/
public void initialLoad() throws java.io.IOException {
datumRanges = new ArrayList();
subRange = new ArrayList();
List uris;
if ( template==null ) {
throw new IllegalStateException("template was null");
} else {
try {
setStatus( "busy: listing "+template );
uris = WalkUtil.getFilesFor(template, timerange, datumRanges, false, null);
if ( uris.size()>0 ) {
setStatus( "Done listing "+template );
} else {
setStatus( "warning: no files found in "+template );
}
} catch ( IOException | URISyntaxException | ParseException | IllegalArgumentException ex) {
ex.printStackTrace();
logger.log(Level.SEVERE, ex.getMessage(), ex);
setStatus("error: Error listing " + template+", "+ex.getMessage() );
throw new java.io.IOException("Error listing " + template+", "+ex.getMessage() );
} finally {
setStatus( " " );
}
}
if ( template.equals("file:///") ) {
haveThumbs400= false;
} else {
int splitIndex= WalkUtil.splitIndex( template );
URI fsRoot;
fsRoot = DataSetURI.getResourceURI( template.substring(0,splitIndex) );
try {
FileSystem fs= FileSystem.create( fsRoot );
if ( fs.getFileObject("/thumbs400/").exists() ) {
String[] result= fs.listDirectory("/thumbs400/");
if ( result.length<2 ) {
haveThumbs400= false; //TODO: kludge, I expected IOException when dir doesn't exist.
}
} else {
haveThumbs400= false;
}
} catch ( IOException ex ) {
haveThumbs400= false;
}
}
//if ( uris.size()>20 ) {uris= uris.subList(0,30); }
FileStorageModel fsm=null;
{
String sansArgs= template;
int i = splitIndex(sansArgs);
URI surls= DataSetURI.getResourceURI(sansArgs.substring(0, i+1));
FileSystem fs = FileSystem.create( surls );
String spec= sansArgs.substring(i+1);
if ( TimeParser.isSpec(spec) ) fsm= FileStorageModel.create( fs, spec );
}
existingImages = new ArrayList<>();
for (int i=0; i < uris.size(); i++) {
existingImages.add(new WalkImage(uris.get(i),haveThumbs400));
//System.err.println(i + ": " + datumRanges.get(i));
String captionString;
int splitIndex= WalkUtil.splitIndex( template );
if (datumRanges.get(i) != null) {
captionString = datumRanges.get(i).toString();//TODO: consider not formatting these until visible.
if ( ( template.contains("*") || template.contains("$x") ) && fsm!=null ) {
String cs= uris.get(i).toString();
if ( template.startsWith("file:///") && cs.length()>6 && cs.charAt(6)!='/' ) {
splitIndex -= 2;
}
if ( cs.length()>13 && cs.substring(8,13).equals("user@") ) {
splitIndex += 5;
}
String x1= fsm.getField( "x", cs.substring(splitIndex+1) );
captionString = captionString + " " + x1;
}
} else {
captionString = FileSystemUtil.uriDecode(uris.get(i).toString());
if ( template.startsWith("file:///") && captionString.length()>6 && captionString.charAt(6)!='/' ) {
splitIndex-= 2;
}
String cs= captionString;
if ( cs.length()>13 && cs.substring(8,13).equals("user@") ) {
splitIndex+=5;
}
captionString = cs.substring(splitIndex+1);
}
existingImages.get(i).setCaption(captionString);
existingImages.get(i).setDatumRange(datumRanges.get(i));
}
for (DatumRange dr : datumRanges) {
if (timeSpan == null)
timeSpan = dr;
else
timeSpan = DatumRangeUtil.union(timeSpan, dr);
}
if (timeSpan != null) {
if ( templateHasExplicitEnd() ||
timeSpan.width().divide(datumRanges.get(0).width() ).doubleValue(Units.dimensionless) > 100000 ) {
logger.warning("too many spans to indicate gaps.");
possibleRanges = datumRanges;
} else {
possibleRanges = DatumRangeUtil.generateList(timeSpan, datumRanges.get(0));
}
}
// There's a funny bug where things aren't regularly spaced, because we
// realign at year boundaries. Just use the original ranges in this
// case.
if ( possibleRanges!=null && datumRanges.size()>possibleRanges.size() ) {
logger.info("jumps in cadence, just use original ranges");
possibleRanges= datumRanges;
}
for (WalkImage i : existingImages) {
i.addPropertyChangeListener(this);
}
subRange = possibleRanges;
rebuildSequence();
}
/**
* return the index of the datumRange completely containing this
* interval.
* @param dr
* @return -1 if no range contains the subrange, the index of the DatumRange otherwise.
*/
protected int indexOfSubrange( DatumRange dr ) {
int idx= -1;
for ( int i=datumRanges.size()-1; i>=0; i-- ) {
if ( datumRanges.get(i)==null ) {
logger.info("ranges are not available");
return -1;
}
if ( dr.contains( datumRanges.get(i).min() ) ) {
idx= i;
break;
}
if ( datumRanges.get(i).contains(dr) ) {
idx= i;
break;
}
}
return idx;
}
/**
* show the datumRange requested by selecting it. The range that intersects is selected.
* If the datum range is within a gap in time, then select the time range immediately following.
*
* @param ds
*/
void gotoSubrange(DatumRange ds) {
int idx= -1;
if ( datumRanges.isEmpty() || datumRanges.get(0)==null ) {
logger.log(Level.INFO, "pngwalk does not have time ranges for each image ({0})", template);
return;
}
for ( int i=0; i=0; i-- ) {
if ( ds.contains( datumRanges.get(i).min() ) ) {
idx= i;
break;
}
if ( datumRanges.get(i).contains(ds) ) {
idx= i;
break;
}
if ( datumRanges.get(i).min().ge( ds.min() ) ) {
idx=i;
}
}
}
if ( idx==-1 ) {
setIndex( datumRanges.size()-1);
} else {
setIndex(idx);
}
}
private final Lock lock = new ReentrantLock();
/** Rebuilds the image sequence. Should be called on initial load and if
* list content options (showMissing, subrange) are changed.
*/
private void rebuildSequence( ) {
lock.lock();
try {
// remember current image so we can update the index appropriately
WalkImage currentImage = null;
if(displayImages.size() > 0) currentImage = currentImage();
if ( timeSpan != null || qcFilter.length()>0 ) {
List displayRange;
if (isUseSubRange() && subRange.size() > 0) {
displayRange = subRange;
} else {
displayRange = possibleRanges;
}
limitWarning = possibleRanges!=null && possibleRanges.size()==20000;
List statuses=null;
if ( qualitySeq!=null ) {
statuses = new ArrayList<>(this.datumRanges.size());
for ( int i=0; i allowedStatuses= new HashSet<>();
if ( qcFilter.length()>0 ) {
for ( int i=0; i-1 ) {
if ( qualitySeq!=null && qcFilter.length()>0 ) {
assert statuses!=null;
if ( !allowedStatuses.contains( statuses.get(ind) ) ) {
continue;
}
}
if ( hasXLogic ) {
displayImages.add(existingImages.get(i));
i++;
} else {
displayImages.add(existingImages.get(ind));//TODO: suspect this is very inefficient.
}
} else if (showMissing && timeSpan != null) {
if ( qualitySeq!=null && qcFilter.length()>0 ) {
continue;
}
// add missing image placeholder
WalkImage ph = new WalkImage(null,haveThumbs400);
ph.setCaption(dr.toString());
displayImages.add(ph);
} else {
logger.fine("I don't think we should get here (but we do, harmless).");
}
}
} else {
displayImages.clear();
for ( int ind=0; ind0 ) {
assert statuses!=null;
if ( !allowedStatuses.contains( statuses.get(ind) ) ) {
continue;
}
}
displayImages.add(existingImages.get(ind));
}
}
} else {
displayImages.clear();
displayImages = new ArrayList<>( existingImages );
}
if (displayImages.contains(currentImage)) {
index = displayImages.indexOf(currentImage);
} else {
index = 0;
}
} finally {
lock.unlock();
}
//Bogus property has no meaningful value, only event is important
pcs.firePropertyChange(PROP_SEQUENCE_CHANGED, false, true);
}
protected boolean isLimitWarning() {
return this.limitWarning;
}
/**
* initialize the quality control sequence.
* @param qcFolder URI with the password resolved.
*/
protected void initQualitySequence( URI qcFolder ) {
try {
this.qcFolder= qcFolder;
qualitySeq = new QualityControlSequence(WalkImageSequence.this, qcFolder);
for (int i = 0; i < displayImages.size(); i++) {
qualitySeq.getQualityControlRecord(i).addPropertyChangeListener(WalkImageSequence.this); // DANGER
pcs.firePropertyChange(PROP_BADGE_CHANGE, -1, i);
}
} catch (IOException ex) {
logger.log(Level.SEVERE, ex.getMessage(), ex);
setStatus("warning: "+ ex.toString());
throw new RuntimeException(ex);
}
}
//commented until questions are resolved.
// /** Create an image sequence using an explicit list of image locations. Images
// * in a list created via this constructor will not have any date/time information
// * associated with them.
// *
// * @param seq
// */
// // Could we get date/time stamps from files and use those? Do we care?
// public WalkImageSequence(List seq) {
// throw new UnsupportedOperationException("Constructor not implemented.");
// }
public WalkImage currentImage() {
return imageAt(index);
}
/**
* return the WalkImage object for the given URI.
* @param image the location
* @return the object modeling this image.
*/
public WalkImage getImage( URI image ) {
for ( WalkImage i: displayImages ) {
if ( i.getUri().equals(image) ) return i;
}
throw new IllegalStateException("didn't find image for "+image);
}
/**
* return the image of the subrange.
* @param n
* @return
*/
public WalkImage imageAt(int n) {
if (n<0 || n>displayImages.size()-1) {
throw new IndexOutOfBoundsException("index must be within 0-"+(displayImages.size()-1)+": "+n);
} else {
return displayImages.get(n);
}
}
/**
* get the image in the sequence, regardless of the subrange.
* @param n
* @return
*/
public WalkImage imageAtNoSubRange(int n) {
if (n<0 || n>existingImages.size()-1) {
throw new IndexOutOfBoundsException("index must be within 0-"+(displayImages.size()-1)+": "+n);
} else {
return existingImages.get(n);
}
}
/**
* return the location of the PNGWalk, which should contain the image files.
* @return
*/
public URI getBaseUri() {
return baseURI;
}
public URI getQCFolder() {
return qcFolder;
}
public void setQCFolder( URI folder ) {
this.qcFolder= folder;
}
/**
* this may be null if even though we are using QualityControl, if the user
* hasn't logged in yet.
* @return
*/
public QualityControlSequence getQualityControlSequence() {
return this.qualitySeq;
}
public boolean isShowMissing() {
return showMissing;
}
public void setShowMissing(boolean showMissing) {
if (showMissing != this.showMissing) {
this.showMissing = showMissing;
rebuildSequence(); // fires property change
}
}
public boolean isUseSubRange() {
return useSubRange;
}
public void setUseSubRange(boolean useSubRange) {
boolean oldSubRange = this.useSubRange;
this.useSubRange = useSubRange;
pcs.firePropertyChange(PROP_USESUBRANGE, oldSubRange, useSubRange);
if(useSubRange != oldSubRange) rebuildSequence();
}
/** Set the sequence's active subrange to a range from first to last, inclusive.
* First and last are indices into the list obtained from getAllTimes()
.
* @param first
* @param last
*/
public void setActiveSubrange(int first, int last) {
subRange = possibleRanges.subList(first, last+1);
if (isUseSubRange()) rebuildSequence();
}
/**
* Convenience method for to set the subrange to the items intersecting the given range.
* @param range
*/
public void setActiveSubrange(DatumRange range) {
int first=-1;
int last= -1;
for ( int i=0; i
* o okay are shown
* p problem are shown
* i ignore are shown
*
* @param s
*/
public void setQCFilter( String s ) {
if ( s==null ) throw new NullPointerException("qcfilter cannot be null, set to empty string to clear");
String oldQcFilter= this.qcFilter;
this.qcFilter= s;
if ( !qcFilter.equals(oldQcFilter ) ) {
rebuildSequence();
}
}
/**
* returns the current QC filter, where "" means no filter, otherwise
* the characters indicate:
*
* - o okay are shown
*
- p problem are shown
*
- i ignore are shown
*
* @return the filter, for example "" or "op" for okay and problem.
*/
public String getQCFilter() {
return this.qcFilter;
}
/**
* return the active subrange of the sequence. This is the portion of the pngwalk being used.
* @return the subrange of the sequence.
* @see https://sourceforge.net/p/autoplot/feature-requests/493/
*/
public List getActiveSubrange() {
return subRange;
}
/** Return the time range covered by this sequence. This is the total range
* of available images, not any currently displayed subrange. Will be null
* if no date template was used to create the sequence.
* @return the time range covered by this sequence.
*/
public DatumRange getTimeSpan() {
return timeSpan;
}
/** Return a java.awt.List
of the times associated with this sequence.
* This list will include times associated with missing images, and is not restricted
* to any currently active subrange.
* @return
*/
public List getAllTimes() {
return possibleRanges;
}
/** Return the current value of the index, where index is that of the displayImages.
*
* @return
*/
public int getIndex() {
return index;
}
/** Set the index explicitly.
*
* @param index
* @throws IndexOutOfBoundsException when...
*/
public void setIndex(int index) {
if (index == this.index) {
// do nothing and fire no event
return;
}
if ( index<0 ) {
index= 0;
}
if ( index>= displayImages.size() ) {
index= displayImages.size()-1;
}
int oldIndex = this.index;
this.index = index;
pcs.firePropertyChange(PROP_INDEX, oldIndex, this.index);
}
List sel= Collections.emptyList();
public void setSelectedIndeces( List sel ) {
List old= this.sel;
this.sel= sel;
pcs.firePropertyChange( PROP_SELECTED_INDECES, old, sel );
}
public List getSelectedIndeces( ) {
return this.sel;
}
/** Advance the index to the next image in the list. If the index is already
* at the last image, do nothing.
*/
public void next() {
if (index < displayImages.size() -1) {
setIndex(index + 1);
}
}
/** Step the index to the previous image in the list. If the index is already
* at the first image, do nothing.
*/
public void prev() {
if (index > 0) {
setIndex(index - 1);
}
}
/** Move the index to the first image in the list.
*
*/
public void first() {
setIndex(0);
}
/** Move the image to the last image in the list.
*
*/
public void last() {
setIndex(displayImages.size()-1);
}
/** Skip forward or backward by the specified number of images. Positive
* numbers skip forward; negative skip backward. If skipping the requested
* number of frames would put the index out of range, the skip moves to the
* last or first image, as appropriate.
*
* @param n The number of images to skip.
*/
public void skipBy(int n) {
if (index + n > displayImages.size() - 1) {
setIndex(displayImages.size() - 1);
} else if (index + n < 0) {
setIndex(0);
} else {
setIndex(index + n);
}
}
/**
* return the number of images in the sequence.
* @return the number of images in the sequence.
*/
public int size() {
return displayImages.size();
}
///**
// * returns null, True or False.
// * @return
// */
//private Boolean doHaveThumbs400() {
// return this.haveThumbs400;
//}
/**
* things we fire events for:
* PROP_BADGE_CHANGE
* and others
* @param l
*/
public void addPropertyChangeListener(PropertyChangeListener l) {
pcs.addPropertyChangeListener(l);
}
public void removePropertyChangeListener(PropertyChangeListener l) {
pcs.removePropertyChangeListener(l);
}
public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener) {
pcs.removePropertyChangeListener(propertyName, listener);
}
public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener) {
pcs.addPropertyChangeListener(propertyName, listener);
}
/**
* return the template representing the sequence.
* @return the template
*/
public String getTemplate() {
return this.template;
}
protected String status = "idle";
public static final String PROP_STATUS = "status";
/**
* get the current status
* @return
*/
public String getStatus() {
return status;
}
/**
* set the current status, which is echoed back to the scientist.
* @param status the status
*/
protected void setStatus(String status) {
String oldStatus = this.status;
this.status = status;
pcs.firePropertyChange(PROP_STATUS, oldStatus, status);
}
// Get status changes from the images in the list
@Override
public void propertyChange(PropertyChangeEvent e) {
if (e.getNewValue() instanceof WalkImage.Status) {
if ((WalkImage.Status) e.getNewValue() == WalkImage.Status.IMAGE_LOADED ||
(WalkImage.Status) e.getNewValue() == WalkImage.Status.THUMB_LOADED) {
int i = displayImages.indexOf(e.getSource());
if (i == -1) {
if (existingImages.indexOf(e.getSource()) == -1) {
//panic because something is very very wrong
throw new RuntimeException("Status change from unknown image object");
}
/* If we get here, this is just a notification from an image that's no
* longer displayed because of date filter or "show missing" filter, so do nothing
*/
return;
}
// imageLoaded is a bogus property so there's no old value
// passing an illegal negative value in its place assures event is always fired
pcs.firePropertyChange(PROP_THUMB_LOADED, -1, i);
if ((WalkImage.Status) e.getNewValue() == WalkImage.Status.IMAGE_LOADED) {
pcs.firePropertyChange(PROP_IMAGE_LOADED, -1, i);
}
}
} else if ( e.getPropertyName().equals( QualityControlRecord.PROP_STATUS ) ) {
URI imageURI= ((QualityControlRecord)e.getSource()).getImageURI();
WalkImage im= getImage(imageURI);
int i= displayImages.indexOf(im);
pcs.firePropertyChange( PROP_BADGE_CHANGE, -1, i );
}
int loadingCount=0;
int loadedCount=0;
int thumbLoadingCount=0;
//int thumbLoadedCount=0;
//int sizeThumbCount= 0;
int totalCount= 0;
for ( WalkImage i : existingImages ) {
totalCount++;
if ( i.getStatus()==WalkImage.Status.IMAGE_LOADING ) loadingCount++;
if ( i.getStatus()==WalkImage.Status.IMAGE_LOADED ) loadedCount++;
if ( i.getStatus()==WalkImage.Status.THUMB_LOADING ) thumbLoadingCount++;
//if ( i.getStatus()==WalkImage.Status.THUMB_LOADED ) thumbLoadedCount++;
//if ( i.getStatus()==WalkImage.Status.SIZE_THUMB_LOADED ) sizeThumbCount++;
}
/* if ( loadingCount>5 ) {
System.err.println( Thread.currentThread() );
new Exception().printStackTrace();
}
if ( thumbLoadingCount>30 ) {
System.err.println( Thread.currentThread() );
new Exception().printStackTrace();
}*/
//long mem= ( Runtime.getRuntime().freeMemory() ) / (1024 * 1024);
if ( loadingCount==0 && thumbLoadingCount==0) {
if ( limitWarning ) {
setStatus(""+loadedCount+" of "+totalCount + " images loaded. Limitations of the PNG Walk Tool prevent use of the entire series." );
} else {
setStatus(""+loadedCount+" of "+totalCount + " images loaded." );
}
} else {
setStatus("busy: "+loadedCount+" of "+totalCount + " images loaded, "
+ loadingCount + " are loading and "+ thumbLoadingCount + " thumbs are loading.");
}
}
/**
* returns the index of the name, or -1 if the name is not found. This is not the full
* filename, but instead just the part of the name within the walk. For example,
* For example if getTemplate is file:/tmp/$Y$m$d.gif, then the setSelectedName might be 20141111.gif.
* @param name the file name.
* @return the index, or -1 if the name is not found.
*/
public int findIndex( String name ) {
for ( int i=0; i