ucar.unidata.idv.ui.ImageGenerator.java Source code

Java tutorial

Introduction

Here is the source code for ucar.unidata.idv.ui.ImageGenerator.java

Source

/*
 * This file is part of McIDAS-V
 *
 * Copyright 2007-2019
 * Space Science and Engineering Center (SSEC)
 * University of Wisconsin - Madison
 * 1225 W. Dayton Street, Madison, WI 53706, USA
 * http://www.ssec.wisc.edu/mcidas
 * 
 * All Rights Reserved
 * 
 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
 * some McIDAS-V source code is based on IDV and VisAD source code.  
 * 
 * McIDAS-V is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 * 
 * McIDAS-V 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 Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser Public License
 * along with this program.  If not, see http://www.gnu.org/licenses.
 */

package ucar.unidata.idv.ui;

import edu.wisc.ssec.mcidasv.McIDASV;
import edu.wisc.ssec.mcidasv.util.CollectionHelpers;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;

import org.python.util.PythonInterpreter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;

import ucar.unidata.data.DataChoice;
import ucar.unidata.data.DataSelection;
import ucar.unidata.data.DataSource;
import ucar.unidata.data.GeoLocationInfo;
import ucar.unidata.data.grid.GridDataSource;
import ucar.unidata.geoloc.ProjectionImpl;
import ucar.unidata.geoloc.ProjectionRect;
import ucar.unidata.idv.*;
import ucar.unidata.idv.control.DisplayControlImpl;
import ucar.unidata.ui.ImageUtils;
import ucar.unidata.ui.colortable.ColorTableCanvas;
import ucar.unidata.util.ColorTable;
import ucar.unidata.util.FileManager;
import ucar.unidata.util.GuiUtils;
import ucar.unidata.util.IOUtil;
import ucar.unidata.util.LogUtil;
import ucar.unidata.util.Misc;
import ucar.unidata.util.PatternFileFilter;
import ucar.unidata.util.Range;
import ucar.unidata.util.StringUtil;
import ucar.unidata.util.Trace;
import ucar.unidata.view.geoloc.NavigatedDisplay;
import ucar.unidata.view.geoloc.ViewpointInfo;
import ucar.unidata.xml.XmlUtil;

import ucar.visad.GeoUtils;
import ucar.visad.ProjectionCoordinateSystem;
import ucar.visad.display.Animation;
import ucar.visad.display.AnimationWidget;

import visad.CommonUnit;
import visad.MouseBehavior;
import visad.Real;
import visad.RealType;
import visad.Unit;

import visad.georef.EarthLocation;
import visad.georef.EarthLocationTuple;
import visad.georef.LatLonPoint;
import visad.georef.MapProjection;

import visad.util.BaseRGBMap;
import visad.util.ColorMap;
import visad.util.ColorPreview;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;

import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;
import java.util.TimeZone;

import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;

/**
 * Manages the user interface for the IDV
 *
 *
 * @author IDV development team
 */
public class ImageGenerator extends IdvManager {

    /** attr value */
    public static final String VALUE_TOP = "top";

    /** attr value */
    public static final String VALUE_BOTTOM = "bottom";

    /** attr value */
    public static final String VALUE_RIGHT = "right";

    /** attr value */
    public static final String VALUE_LEFT = "left";

    /** attr value */
    public static final String VALUE_ALL = "all";

    /** attr value */
    public static final String VALUE_NONE = "none";

    /** attr value */
    public static final String VALUE_FIRST = "first";

    /** attr value */
    public static final String VALUE_LAST = "last";

    /** macro property */
    public static final String PROP_LOOPINDEX = "loopindex";

    /** padded loop index */
    public static final String PROP_LOOPINDEX_PAD2 = "loopindex_pad2";

    /** padded loop index */
    public static final String PROP_LOOPINDEX_PAD3 = "loopindex_pad3";

    /** padded loop index */
    public static final String PROP_LOOPINDEX_PAD4 = "loopindex_pad4";

    /** macro property */
    public static final String PROP_VIEWINDEX = "viewindex";

    /** macro property */
    public static final String PROP_VIEWNAME = "viewname";

    /** macro property */
    public static final String PROP_IMAGEINDEX = "imageindex";

    /** macro property */
    public static final String PROP_IMAGEFILE = "imagefile";

    /** macro property */
    public static final String PROP_IMAGEPATH = "imagepath";

    /** file property */
    public static final String PROP_FILE = "file";

    /** filenosuffix property */
    public static final String PROP_FILENOSUFFIX = "filenosuffix";

    /** filetail property */
    public static final String PROP_FILETAIL = "filetail";

    /** filetailnosuffix property */
    public static final String PROP_FILETAILNOSUFFIX = "filetailnosuffix";

    /** fileprefix property */
    public static final String PROP_FILEPREFIX = "fileprefix";

    /** macro property */
    public static final String PROP_CONTENTS = "contents";

    /** macro property */
    public static final String PROP_ANIMATIONTIME = "animationtime";

    /** macro property */
    public static final String PROP_OFFSCREEN = "offscreen";

    /** Macro name */
    private static final String[] DATE_PROPS = { "G", "yy", "yyyy", "MM", "M", "MMM", "MMMMM", "HH", "H", "k", "kk",
            "D", "d", "dd", "K", "KK", "a", "mm", "ss", "s", "S", "EEE", "Z" };

    /** List of SimpleDateFormat objects. One for each DATE_PROPS. */
    private static List DATE_FORMATS;

    /** isl tag */
    public static final String TAG_FILESET = "fileset";

    /** view tag */
    public static final String TAG_VIEW = "view";

    /** isl tag */
    public static final String TAG_TEMPLATE = "template";

    /** isl tag */
    public static final String TAG_APPEND = "append";

    /** isl tag */
    public static final String TAG_SETFILES = "setfiles";

    /** isl tag */
    public static final String TAG_ISL = "isl";

    /** viewpoint tag */
    public static final String TAG_VIEWPOINT = "viewpoint";

    /** isl tag */
    public static final String TAG_PROPERTY = "property";

    /** isl tag */
    public static final String TAG_IMPORT = "import";

    /** isl tag */
    public static final String TAG_IMAGE = "image";

    /** isl tag */
    public static final String TAG_GROUP = "group";

    /** isl tag */
    public static final String TAG_PAUSE = "pause";

    /** isl tag */
    public static final String TAG_MOVIE = "movie";

    /** isl tag */
    public static final String TAG_BUNDLE = "bundle";

    /** isl tag */
    public static final String TAG_ELSE = "else";

    /** isl tag */
    public static final String TAG_THEN = "then";

    /** isl tag */
    public static final String TAG_COLORBAR = "colorbar";

    /** isl tag */
    public static final String TAG_CLIP = "clip";

    /** publish tag */
    public static final String TAG_PUBLISH = "publish";

    /** isl tag */
    public static final String TAG_DISPLAY = "display";

    /** datasource tag */
    public static final String TAG_DATASOURCE = "datasource";

    /** isl tag */
    public static final String TAG_MATTE = "matte";

    /** show tag */
    public static final String TAG_SHOW = "show";

    /** displaylist tag */
    public static final String TAG_DISPLAYLIST = "displaylist";

    /** isl tag */
    public static final String TAG_OUTPUT = "output";

    /** isl tag */
    public static final String TAG_OVERLAY = "overlay";

    /** isl tag */
    public static final String TAG_KML = "kml";

    /** kml colorbar tag */
    public static final String TAG_KML_COLORBAR = "kmlcolorbar";

    /** isl tag */
    public static final String TAG_KMZFILE = "kmzfile";

    /** isl tag */
    public static final String TAG_SPLIT = "split";

    /** isl tag */
    public static final String TAG_RESIZE = "resize";

    /** isl tag */
    public static final String TAG_THUMBNAIL = "thumbnail";

    /** isl tag */
    public static final String TAG_TRANSPARENT = "transparent";

    /** isl tag */
    public static final String TAG_PROJECTION = "projection";

    /** background transparent tag */
    public static final String TAG_BGTRANSPARENT = "backgroundtransparent";

    /** index attribute */
    public static final String ATTR_INDEX = "index";

    /** stride attribute */
    public static final String ATTR_STRIDE = "stride";

    /** x stride attribute */
    public static final String ATTR_STRIDEX = "stridex";

    /** y stride attribute */
    public static final String ATTR_STRIDEY = "stridey";

    /** z stride attribute */
    public static final String ATTR_STRIDEZ = "stridez";

    /** bounding box attribute */
    public static final String ATTR_BBOX = "bbox";

    /** from level attribute */
    public static final String ATTR_LEVEL_FROM = "levelfrom";

    /** to level attribute */
    public static final String ATTR_LEVEL_TO = "levelto";

    /** azimuth attribute */
    public static final String ATTR_AZIMUTH = "azimuth";

    /** tilt attribute */
    public static final String ATTR_TILT = "tilt";

    /** x aspect attribute */
    public static final String ATTR_ASPECTX = "aspectx";

    /** y aspect attribute */
    public static final String ATTR_ASPECTY = "aspecty";

    /** z aspect attribute */
    public static final String ATTR_ASPECTZ = "aspectz";

    /** x rotation attribute */
    public static final String ATTR_ROTX = "rotx";

    /** y rotation attribute */
    public static final String ATTR_ROTY = "roty";

    /** z rotation attribute */
    public static final String ATTR_ROTZ = "rotz";

    /** scale attribute */
    public static final String ATTR_SCALE = "scale";

    /** x translation attribute */
    public static final String ATTR_TRANSX = "transx";

    /** y translation attribute */
    public static final String ATTR_TRANSY = "transy";

    /** z translation attribute */
    public static final String ATTR_TRANSZ = "transz";

    /** suffix attribute */
    public static final String ATTR_SUFFIX = "suffix";

    /** showunit atttribute */
    public static final String ATTR_SHOWUNIT = "showunit";

    /** transparency attribute */
    public static final String ATTR_TRANSPARENCY = "transparency";

    /** top attribute */
    public static final String ATTR_TOP = "top";

    /** left space attribute */
    public static final String ATTR_SPACE_LEFT = "space_left";

    /** right space attribute */
    public static final String ATTR_SPACE_RIGHT = "space_right";

    /** top space attribute */
    public static final String ATTR_SPACE_TOP = "space_top";

    /** bottom space attribute */
    public static final String ATTR_SPACE_BOTTOM = "space_bottom";

    /** isl tag */
    public static final String TAG_WRITE = "write";

    /** isl tag */
    public static final String ATTR_ANCHOR = "anchor";

    /** isl attr */
    public static final String ATTR_FROM = "from";

    /** isl attr */
    public static final String ATTR_TO = "to";

    /** isl attribute */
    public static final String ATTR_GLOBAL = "global";

    /** isl attribute */
    public static final String ATTR_ONERROR = "onerror";

    /** isl attribute */
    public static final String ATTR_SORT = "sort";

    /** isl attribute */
    public static final String ATTR_SORTDIR = "sortdir";

    /** isl attribute */
    public static final String VALUE_TIME = "time";

    /** isl attribute */
    public static final String VALUE_ASCENDING = "ascending";

    /** isl attribute */
    public static final String VALUE_DESCENDING = "descending";

    /** isl attribute */
    public static final String ATTR_FIRST = "first";

    /** isl attribute */
    public static final String ATTR_LAST = "last";

    /** isl tag */
    public static final String ATTR_USEPROJECTION = "useprojection";

    /** isl tag */
    public static final String ATTR_EXPR = "expr";

    /** isl tag */
    public static final String ATTR_COPY = "copy";

    /** the count tag */
    public static final String ATTR_COUNT = "count";

    /** isl tag */
    public static final String ATTR_COLUMNS = "columns";

    /** isl attribute */
    public static final String ATTR_DATASOURCE = "datasource";

    /** isl attribute */
    public static final String ATTR_DESTINATION = "destination";

    /** isl attribute */
    public static final String ATTR_SERVER = "server";

    /** isl attribute */
    public static final String ATTR_PASSWORD = "password";

    /** isl attribute */
    public static final String ATTR_USER = "user";

    /** isl tag */
    public static final String ATTR_ROWS = "rows";

    /** isl tag */
    public static final String ATTR_CLASS = "class";

    /** isl tag */
    public static final String ATTR_ANGLE = "angle";

    /** isl tag */
    public static final String ATTR_WHERE = "where";

    /** isl tag */
    public static final String ATTR_BACKGROUND = "background";

    /** isl attribute */
    public static final String ATTR_BUNDLE = "bundle";

    /** isl tag */
    public static final String ATTR_SHOWLINES = "showlines";

    /** isl tag */
    public static final String ATTR_LINECOLOR = "linecolor";

    /** isl tag */
    public static final String ATTR_COLOR = "color";

    /** isl tag */
    public static final String ATTR_COMMAND = "command";

    /** isl tag */
    public static final String ATTR_FONTFACE = "fontface";

    /** isl attr */
    public static final String ATTR_FORMAT = "format";

    /** that latlonlabels tag */
    public static final String TAG_LATLONLABELS = "latlonlabels";

    /** that latvalues tag */
    public static final String ATTR_LAT_VALUES = "latvalues";

    /** that latlabels tag */
    public static final String ATTR_LAT_LABELS = "latlabels";

    /** that lonvalues tag */
    public static final String ATTR_LON_VALUES = "lonvalues";

    /** that lonlabels tag */
    public static final String ATTR_LON_LABELS = "lonlabels";

    /** the draw lon lines tag */
    public static final String ATTR_DRAWLONLINES = "drawlonlines";

    /** the draw lat lines tag */
    public static final String ATTR_DRAWLATLINES = "drawlatlines";

    /** dashes tag */
    public static final String ATTR_DASHES = "dashes";

    /** linewidth tag */
    public static final String ATTR_LINEWIDTH = "linewidth";

    /** isl tag */
    public static final String ATTR_LINEOFFSET_RIGHT = "lineoffsetright";

    /** isl tag */
    public static final String ATTR_LINEOFFSET_LEFT = "lineoffsetleft";

    /** isl tag */
    public static final String ATTR_LINEOFFSET_TOP = "lineoffsettop";

    /** isl tag */
    public static final String ATTR_LINEOFFSET_BOTTOM = "lineoffsetbottom";

    /** isl tag */
    public static final String ATTR_LABELBACKGROUND = "labelbackground";

    /** isl tag */
    public static final String ATTR_SHOWTOP = "showtop";

    /** isl tag */
    public static final String ATTR_SHOWBOTTOM = "showbottom";

    /** isl tag */
    public static final String ATTR_SHOWLEFT = "showleft";

    /** isl tag */
    public static final String ATTR_SHOWRIGHT = "showright";

    /** isl tag */
    public static final String ATTR_FONTSIZE = "fontsize";

    /** isl tag */
    public static final String ATTR_FRAMERATE = "framerate";

    /** isl tag for ending frame pause for animated gifs */
    public static final String ATTR_ENDFRAMEPAUSE = "endframepause";

    /** isl tag */
    public static final String ATTR_CAPTION = "caption";

    /** isl tag */
    public static final String ATTR_DEBUG = "debug";

    /** isl tag */
    public static final String ATTR_DEFAULT = "default";

    /** isl tag */
    public static final String ATTR_DISPLAY = "display";

    /** isl tag */
    public static final String ATTR_OFFSCREEN = "offscreen";

    /** isl tag */
    public static final String ATTR_TIMES = "times";

    /** isl tag */
    public static final String ATTR_ENSEMBLES = "ensembles";

    /** isl tag */
    public static final String ATTR_DIR = "dir";

    /** isl tag */
    public static final String ATTR_PATTERN = "pattern";

    /** isl attribute */
    public static final String ATTR_WAIT = "wait";

    /** isl tag */
    public static final String ATTR_PROPERTY = "property";

    /** isl tag */
    public static final String ATTR_QUALITY = "quality";

    /** isl tag */
    public static final String ATTR_LOOP = "loop";

    /** isl tag */
    public static final String ATTR_ENTRY = "entry";

    /** isl tag */
    public static final String ATTR_ID = "id";

    /** isl tag */
    public static final String ATTR_IMAGE = "image";

    /** isl tag */
    public static final String ATTR_INTERVAL = "interval";

    /** isl tag */
    public static final String ATTR_LEFT = "left";

    /** isl tag */
    public static final String ATTR_MESSAGE = "message";

    /** isl tag */
    public static final String ATTR_MATTEBG = "mattebg";

    /** isl tag */
    public static final String ATTR_NAME = "name";

    /** isl tag */
    public static final String ATTR_RIGHT = "right";

    /** isl tag */
    public static final String ATTR_TICKMARKS = "tickmarks";

    /** isl tag */
    public static final String ATTR_SPACE = "space";

    /** isl tag */
    public static final String ATTR_HSPACE = "hspace";

    /** isl tag */
    public static final String ATTR_VSPACE = "vspace";

    /** isl tag */
    public static final String ATTR_BOTTOM = "bottom";

    /** the valign attribute */
    public static final String ATTR_VALIGN = "valign";

    /** isl tag */
    public static final String ATTR_TEXT = "text";

    /** isl tag */
    public static final String ATTR_TEMPLATE = "template";

    /** isl tag */
    public static final String ATTR_TYPE = "type";

    /** isl tag */
    public static final String ATTR_EVERY = "every";

    /** isl tag */
    public static final String ATTR_VALUE = "value";

    /** isl tag */
    public static final String ATTR_VALUES = "values";

    /** isl tag */
    public static final String ATTR_ORIENTATION = "orientation";

    /** isl tag */
    public static final String ATTR_PARAM = "param";

    /** isl tag */
    public static final String ATTR_PLACE = "place";

    /** isl tag */
    public static final String ATTR_VIEW = "view";

    /** the view dir attribute */
    public static final String ATTR_VIEWDIR = "viewdir";

    /** isl tag */
    public static final String ATTR_URL = "url";

    /** isl tag */
    public static final String ATTR_FILE = "file";

    /** isl tag */
    public static final String ATTR_FROMFILE = "fromfile";

    /** isl tag */
    public static final String ATTR_NORTH = "north";

    /** isl tag */
    public static final String ATTR_SOUTH = "south";

    /** isl tag */
    public static final String ATTR_EAST = "east";

    /** isl tag */
    public static final String ATTR_WEST = "west";

    /** isl tag */
    public static final String ATTR_WIDTH = "width";

    /** isl tag */
    public static final String ATTR_HEIGHT = "height";

    /** isl tag */
    public static final String ATTR_SLEEP = "sleep";

    /** isl tag */
    public static final String ATTR_SECONDS = "seconds";

    /** isl tag */
    public static final String ATTR_MINUTES = "minutes";

    /** isl tag */
    public static final String ATTR_HOURS = "hours";

    /** isl tag */
    public static final String ATTR_CLEAR = "clear";

    /** isl tag */
    public static final String ATTR_CODE = "code";

    /** isl tag */
    public static final String ATTR_LAT = "lat";

    /** isl tag */
    public static final String ATTR_LON = "lon";

    /** isl attribute */
    public static final String ATTR_WHAT = "what";

    /** isl attribute */
    public static final String ATTR_COMBINE = "combine";

    /** isl attribute */
    public static final String ATTR_ANIMATION_INDEX = "animation_index";

    /** isl attribute */
    private static final String ATTR_SUFFIXFREQUENCY = "suffixfrequency";

    /** Show debug messages */
    private boolean debug = false;

    /** Stack of properties hashtables */
    private List propertiesStack = new ArrayList();

    /** Maps id to data source */
    private Hashtable idToDataSource = new Hashtable();

    /** The interpreter */
    private PythonInterpreter interpreter;

    /** Stack of active OutputInfo objects */
    private List outputStack = new ArrayList();

    /** When we are looping this gets set so we can use in as a macro value */
    private int currentLoopIndex = 0;

    /** Holds the procedure elements. Maps procedure name to element. */
    private Hashtable procs;

    /** Holds the tag name to method */
    private Hashtable methods = new Hashtable();

    /** current xml node we are processing */
    private Element currentNode;

    /** Keep around the last image captured */
    private Image lastImage;

    /** an object map */
    private Hashtable objectMap;

    /**
     * Create me with the IDV
     *
     * @param idv The IDV
     */
    public ImageGenerator(IntegratedDataViewer idv) {
        super(idv);
    }

    /**
     * Create me with the IDV and start processing files
     *
     * @param idv The IDV
     * @param scriptFiles List of isl files
     */
    public ImageGenerator(IntegratedDataViewer idv, List scriptFiles) {
        super(idv);
        processScriptFiles(scriptFiles);
    }

    /**
     * Process the list of isl files
     *
     * @param scriptFiles isl files
     */
    public void processScriptFiles(List scriptFiles) {
        for (int fileIdx = 0; fileIdx < scriptFiles.size(); fileIdx++) {
            String filename = (String) scriptFiles.get(fileIdx);
            if (!processScriptFile(filename)) {
                boolean offScreen = getIdv().getArgsManager().getIsOffScreen();
                boolean isPython = filename.toLowerCase().endsWith(".py");
                if (offScreen && isPython) {
                    ((McIDASV) getIdv()).exitMcIDASV(1);
                } else {
                    return;
                }
            }
        }
    }

    /**
     * Process the  isl files
     *
     * @param islFile file
     *
     * @return Was it successful
     */
    public synchronized boolean processScriptFile(String islFile) {
        return processScriptFile(islFile, new Hashtable());
    }

    /**
     * Process the script file
     *
     * @param islFile  the ISL file
     * @param properties optional properties
     *
     * @return true if successful
     */
    public synchronized boolean processScriptFile(String islFile, Hashtable properties) {
        procs = new Hashtable();
        idToDataSource = new Hashtable();
        propertiesStack = new ArrayList();
        pushProperties();

        if (islFile.endsWith(".jy") || islFile.endsWith(".py")) {
            try {
                String islPath = IOUtil.getFileRoot(islFile);
                putProperty("islpath", islPath);
                String jythonCode = IOUtil.readContents(islFile);
                PythonInterpreter interp = getInterpreter();
                if (getIdv().getJythonManager().getInError()) {
                    return false;
                }

                interp.exec(jythonCode);
                popProperties();
                return true;
            } catch (Exception exc) {
                String msg = String.format("Error running jython script: %s", islFile);
                logger.error(msg, exc);
                return error(msg + "\n" + exc);
            }

        }

        Element root = null;
        try {
            InputStream is = null;
            try {
                if (islFile.startsWith("xml:")) {
                    String xml = islFile.substring(4);
                    is = new ByteArrayInputStream(xml.getBytes());
                    islFile = "Inline isl";
                } else if (islFile.startsWith("b64:")) {
                    is = new ByteArrayInputStream(XmlUtil.decodeBase64(islFile.substring(4)));
                    islFile = "Inline base 64 encoded isl";
                } else {
                    is = IOUtil.getInputStream(islFile, getClass());
                }
            } catch (FileNotFoundException fnfe) {
            } catch (IOException ioe) {
            }
            if (is == null) {
                return error("Given script file does not exist or could not be read: " + islFile);
            }
            root = XmlUtil.getRoot(is);
        } catch (Exception exc) {
            String msg = String.format("Could not load script file: %s", islFile);
            logger.error(msg, exc);
            return error(msg + "\n" + exc);
        }
        if (root == null) {
            return error("Could not load script file:" + islFile);
        }
        try {
            String islPath = IOUtil.getFileRoot(islFile);
            putProperty("islpath", islPath);
            objectMap = properties;
            processNode(root);
            popProperties();
        } catch (InvocationTargetException ite) {
            Throwable inner = ite.getTargetException();
            while (inner instanceof InvocationTargetException) {
                inner = ((InvocationTargetException) inner).getTargetException();
            }
            return handleError(inner);
        } catch (Throwable exc) {
            return handleError(exc);
        } finally {
            objectMap = null;
        }
        return true;
    }

    /**
     * Handle the error
     *
     * @param exc The error
     *
     * @return Just return false
     */
    private boolean handleError(Throwable exc) {
        if (!(exc instanceof IllegalStateException) && !(exc instanceof IllegalArgumentException)) {
            logger.error("An error occurred", exc);
        } else {
            logger.error("An error occurred", exc);
        }
        return error("An error occurred:" + exc);
    }

    /**
     * Find the inner most non InvocationTargetException exception
     *
     * @param ite The exception
     *
     * @return First non InvocationTargetException exception
     */
    private Throwable getInnerException(InvocationTargetException ite) {
        Throwable inner = ite.getTargetException();
        while (inner instanceof InvocationTargetException) {
            inner = ((InvocationTargetException) inner).getTargetException();
        }
        return inner;
    }

    /**
     * Process the node
     *
     * @param node The node
     * @return ok
     * @throws Throwable On badness
     */
    private boolean processNode(Element node) throws Throwable {
        String tagName = node.getTagName();
        //        System.err.println("tag:" + tagName);

        String methodName = "processTag" + tagName.substring(0, 1).toUpperCase()
                + tagName.substring(1).toLowerCase();

        //Look to see if this is a isl procedure
        Element procNode = (Element) procs.get(tagName);
        if (procNode != null) {
            return processTagCall(node, procNode);
        }

        Object tmp = methods.get(methodName);
        if (tmp == null) {
            try {
                tmp = getClass().getDeclaredMethod(methodName, new Class[] { Element.class });
            } catch (Exception exc) {
            }
            if (tmp == null) {
                tmp = "no method";
            }
            methods.put(methodName, tmp);
        }

        if (tmp instanceof Method) {
            try {
                currentNode = node;
                Object result = ((Method) tmp).invoke(this, new Object[] { node });
                if (result.equals(Boolean.TRUE)) {
                    return true;
                }
            } catch (InvocationTargetException ite) {
                Throwable inner = getInnerException(ite);
                if (inner instanceof BadIslException) {
                    return error("Error handling ISL node:" + XmlUtil.toStringNoChildren(currentNode) + "\t"
                            + inner.toString());
                }
                throw inner;
            }
            return false;
        }
        return error("Unknown tag:" + tagName);
    }

    /**
     * Process the "for" tag
     * 
     * @param node  the tag node
     * @return true if successful
     * @throws Throwable an error
     */
    protected boolean processTagFor(Element node) throws Throwable {
        pushProperties();
        int start = XmlUtil.getAttribute(node, "start", 0);
        int end = XmlUtil.getAttribute(node, "end", 0);
        int inc = XmlUtil.getAttribute(node, "increment", 1);

        for (int i = start; i < end; i += inc) {
            putProperty("index", "" + i);
            try {
                if (!processChildren(node)) {
                    return false;
                }
            } catch (MyBreakException be) {
                break;
            } catch (MyContinueException ce) {
            }
        }
        popProperties();
        return true;
    }

    /**
     * Recursively call processNode on each of the children elements
     *
     * @param node The parent node.
     *
     * @return Success.
     *
     * @throws Throwable On badness
     */
    private boolean processChildren(Element node) throws Throwable {
        NodeList elements = XmlUtil.getElements(node);
        for (int childIdx = 0; childIdx < elements.getLength(); childIdx++) {
            Element child = (Element) elements.item(childIdx);
            try {
                if (!processNode(child)) {
                    return false;
                }
            } catch (Throwable thr) {
                String onerror = applyMacros(node, ATTR_ONERROR, (String) null);
                if (onerror == null) {
                    throw thr;
                }
                if (onerror.equals("ignore")) {
                } else {
                    logger.error("Problem processing child nodes", thr);
                }
            }
        }
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagFtp(Element node) throws Throwable {
        String file = applyMacros(node, ATTR_FILE);
        String server = applyMacros(node, ATTR_SERVER);
        String destination = applyMacros(node, ATTR_DESTINATION);
        String user = applyMacros(node, ATTR_USER, "anonymous");
        String password = applyMacros(node, ATTR_PASSWORD, "idvuser@unidata.ucar.edu");
        byte[] bytes = IOUtil.readBytes(IOUtil.getInputStream(file));
        ftpPut(server, user, password, destination, bytes);
        return true;
    }

    /**
     * Process the export tag. This allows one to export data from a display. It requires
     * a display identifier
     *
     * @param node isl xml node
     *
     * @return everything is cool
     *
     * @throws Throwable On badness
     */
    protected boolean processTagExport(Element node) throws Throwable {
        DisplayControlImpl display = findDisplayControl(node);
        if (display == null) {
            throw new IllegalArgumentException("Could not find display:" + XmlUtil.toString(node));
        }
        String what = applyMacros(node, ATTR_WHAT, (String) null);

        String filename = XmlUtil.getAttribute(node, ATTR_FILE);
        display.doExport(what, filename);
        return true;
    }

    /**
     * Process the tag trace
     *
     * @param node  the node
     *
     * @return true if processed
     *
     * @throws Throwable  on badness
     */
    protected boolean processTagTrace(Element node) throws Throwable {
        String pattern = applyMacros(node, ATTR_PATTERN);
        debug("turning trace on for:" + pattern);
        Trace.startTrace();
        Trace.clearOnly();
        Trace.addOnly(pattern);
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagRemovedisplays(Element node) throws Throwable {

        if (XmlUtil.hasAttribute(node, ATTR_DISPLAY)) {
            debug("Removing display");
            DisplayControlImpl display = findDisplayControl(node);
            if (display == null) {
                return error("Could not find display:" + XmlUtil.toString(node));
            }
            display.doRemove();
            return true;
        }
        debug("Removing all displays");
        getIdv().removeAllDisplays(true);
        return true;
    }

    /**
     * Process the print cache tag
     *
     * @param node  the XML node
     *
     * @return  true if successful
     *
     * @throws Throwable  on badness
     */
    protected boolean processTagPrintcache(Element node) throws Throwable {
        visad.data.DataCacheManager.getCacheManager().printStats();
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagRemoveall(Element node) throws Throwable {
        debug("Removing all displays and data");
        getIdv().removeAllDisplays(false);
        getIdv().removeAllDataSources();
        idToDataSource = new Hashtable();
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagSetfiles(Element node) throws Throwable {
        DataSource dataSource = findDataSource(node);
        if (dataSource == null) {
            return error("Could not find data source");
        }
        List files = new ArrayList();
        if (XmlUtil.hasAttribute(node, ATTR_FILE)) {
            files.add(applyMacros(node, ATTR_FILE));
        } else {
            List filesetFiles = findFiles(node);
            if (filesetFiles != null) {
                files.addAll(filesetFiles);
            }
        }
        if (files.size() == 0) {
            return error("Could not find files");
        }
        dataSource.setNewFiles(files);
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagExists(Element node) throws Throwable {
        List files = findFiles(node);
        boolean exists = ((files != null) && (files.size() > 0));
        putProperty(applyMacros(node, ATTR_PROPERTY), (exists ? "1" : "0"));
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagAsk(Element node) throws Throwable {
        String property = applyMacros(node, ATTR_PROPERTY);
        boolean result;
        if (getIdv().getArgsManager().getIsOffScreen()) {
            if (!XmlUtil.hasAttribute(node, ATTR_DEFAULT)) {
                throw new IllegalStateException(
                        "Running in offscreen mode and the 'ask' node does not have a 'default' attribute");
            }
            result = applyMacros(node, ATTR_DEFAULT, true);
        } else {
            result = GuiUtils.showYesNoDialog(null, applyMacros(node, ATTR_MESSAGE), "");
        }
        putProperty(property, (result ? "1" : "0"));
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagEcho(Element node) throws Throwable {
        String message = applyMacros(node, ATTR_MESSAGE, (String) null);
        if (message == null) {
            message = applyMacros(XmlUtil.getChildText(node));
        }
        System.out.println(message);
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagAsktocontinue(Element node) throws Throwable {
        String message = applyMacros(node, ATTR_MESSAGE, (String) null);
        if (message == null) {
            message = applyMacros(XmlUtil.getChildText(node));
        }
        return GuiUtils.askOkCancel("ISL", message);
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagGc(Element node) throws Throwable {
        Runtime.getRuntime().gc();
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagBreak(Element node) throws Throwable {
        throw new MyBreakException();
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagContinue(Element node) throws Throwable {
        throw new MyContinueException();
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagReturn(Element node) throws Throwable {
        throw new MyReturnException();
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagProcedure(Element node) throws Throwable {
        procs.put(applyMacros(node, ATTR_NAME), node);
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagMkdir(Element node) throws Throwable {
        IOUtil.makeDir(applyMacros(node, ATTR_FILE));
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagStop(Element node) throws Throwable {
        return false;
    }

    /**
     * Get the property value
     *
     * @param node the XML node
     *
     * @return  the values
     *
     * @throws Throwable on badness
     */
    protected String[] getPropertyValue(Element node) throws Throwable {
        String name = (String) applyMacros(node, ATTR_NAME);
        String value = null;
        if (XmlUtil.hasAttribute(node, ATTR_VALUE)) {
            value = (String) applyMacros(node, ATTR_VALUE);
        } else if (XmlUtil.hasAttribute(node, ATTR_FROMFILE)) {
            String filename = applyMacros(node, ATTR_FROMFILE);
            value = applyMacros(IOUtil.readContents(filename));
        } else {
            value = XmlUtil.getChildText(node);
            if ((value == null) || (value.trim().length() == 0)) {
                throw new IllegalArgumentException("No value in property tag: " + XmlUtil.toString(node));
            }
            value = applyMacros(value);
        }
        return new String[] { name, value };
    }

    /**
     * Process IDV property tag
     *
     * @param node  the XML node
     *
     * @return  true if successful
     *
     * @throws Throwable  on badness
     */
    protected boolean processTagIdvproperty(Element node) throws Throwable {
        String[] tuple = getPropertyValue(node);
        debug("setting idv property: " + tuple[0] + " =" + tuple[1]);
        getIdv().getStateManager().putProperty(applyMacros(tuple[0]), applyMacros(tuple[1]));
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagProperty(Element node) throws Throwable {
        String[] tuple = getPropertyValue(node);
        putProperty(applyMacros(tuple[0]), tuple[1], applyMacros(node, ATTR_GLOBAL, false));
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagMove(Element node) throws Throwable {
        List files = findFiles(node);
        if (files != null) {
            File dir = new File(applyMacros(node, ATTR_DIR));
            debug("moving files to: " + dir + " files=" + files);
            for (int i = 0; i < files.size(); i++) {
                IOUtil.moveFile(new File(files.get(i).toString()), dir);
            }
        }
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagRename(Element node) throws Throwable {
        String from = applyMacros(node, ATTR_FROM);
        String to = applyMacros(node, ATTR_TO);
        IOUtil.moveFile(new File(from), new File(to));
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagDelete(Element node) throws Throwable {
        List files = findFiles(node);
        if (files != null) {
            debug("deleting files:" + files);
            for (int i = 0; i < files.size(); i++) {
                ((File) files.get(i)).delete();
            }
        }
        return true;
    }

    /**
     * Handle the clear tag
     *
     * @param node node
     *
     * @return ok
     *
     * @throws Throwable On badness
     */
    protected boolean processTagClear(Element node) throws Throwable {
        String name = applyMacros(node, ATTR_NAME);
        Hashtable ht = (Hashtable) propertiesStack.get(0);
        ht.remove(name);
        return true;
    }

    /**
     * Handle the append tag
     *
     * @param node node
     *
     * @return ok
     *
     * @throws Throwable On badness
     */
    protected boolean processTagAppend(Element node) throws Throwable {
        String name = applyMacros(node, ATTR_NAME);
        Hashtable ht = findTableFor(name);
        String value = (String) ht.get(name);
        if (value == null) {
            value = "";
        }
        if (XmlUtil.hasAttribute(node, ATTR_VALUE)) {
            value = value + applyMacros(node, ATTR_VALUE);
        } else if (XmlUtil.hasAttribute(node, ATTR_FROMFILE)) {
            String filename = applyMacros(node, ATTR_FROMFILE);
            value = value + applyMacros(IOUtil.readContents(filename));
        } else {
            value = value + applyMacros(XmlUtil.getChildText(node)).trim();
        }
        ht.put(name, value);
        return true;
    }

    /**
     * Handle the append tag
     *
     * @param node node
     *
     * @return ok
     *
     * @throws Throwable On badness
     */
    protected boolean processTagIncrement(Element node) throws Throwable {
        String name = applyMacros(node, ATTR_NAME);
        Hashtable ht = findTableFor(name);
        String value = (String) ht.get(name);
        if (value == null) {
            value = "0";
        }
        String by = "1";
        if (XmlUtil.hasAttribute(node, ATTR_VALUE)) {
            by = applyMacros(node, ATTR_VALUE);
        }
        double num = new Double(value).doubleValue() + new Double(by).doubleValue();
        ht.put(name, "" + num);
        return true;
    }

    /**
     * Handle the append tag
     *
     * @param node node
     *
     * @return ok
     *
     * @throws Throwable On badness
     */
    protected boolean processTagReplace(Element node) throws Throwable {
        String name = applyMacros(node, ATTR_NAME);
        Hashtable ht = findTableFor(name);
        ht.put(name, applyMacros(node, ATTR_VALUE));
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagCopy(Element node) throws Throwable {
        List files = findFiles(node);
        if (files != null) {
            File dir = new File(applyMacros(node, ATTR_DIR));
            IOUtil.makeDir(dir);
            if (!dir.isDirectory()) {
                return error("Specified file:" + dir + " is not a directory");
            }
            debug("copying files to: " + dir + " files=" + files);
            for (int i = 0; i < files.size(); i++) {
                IOUtil.copyFile(new File(files.get(i).toString()), dir);
            }
        }
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagReload(Element node) throws Throwable {
        List dataSources = getIdv().getDataSources();
        for (int i = 0; i < dataSources.size(); i++) {
            DataSource dataSource = (DataSource) dataSources.get(i);
            dataSource.reloadData();
        }
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagExec(Element node) throws Throwable {
        String command = applyMacros(node, ATTR_COMMAND);
        debug("Calling exec:" + command);
        Process process = Runtime.getRuntime().exec(command);
        //This seems to hang?
        process.waitFor();
        if (process.exitValue() != 0) {
            String result = IOUtil.readContents(process.getInputStream());
            System.err.println("Exec:\n\t" + command + "\nreturned:" + process.exitValue() + "\n" + result);
        }
        return true;

    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagJython(Element node) throws Throwable {
        String jythonFile = applyMacros(node, ATTR_FILE, (String) null);
        if (jythonFile != null) {
            InputStream is = IOUtil.getInputStream(jythonFile, getClass());
            if (is == null) {
                return error("Could not open jython file:" + jythonFile);
            } else {
                getInterpreter().execfile(is, jythonFile);
            }
        } else {
            String jython = applyMacros(node, ATTR_CODE, (String) null);
            if (jython == null) {
                jython = XmlUtil.getChildText(node);
            }
            if (jython != null) {
                getInterpreter().exec(jython);
            }
        }
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagFileset(Element node) throws Throwable {
        List files = findFiles(Misc.newList(node));
        pushProperties();
        for (int i = 0; i < files.size(); i++) {
            try {
                String filePath = files.get(i).toString();
                String tail = IOUtil.getFileTail(filePath);
                putProperty(PROP_FILE, filePath);
                putProperty(PROP_FILEPREFIX, IOUtil.stripExtension(filePath));
                putProperty(PROP_FILENOSUFFIX, IOUtil.stripExtension(filePath));
                putProperty(PROP_FILETAIL, tail);
                putProperty(PROP_FILETAILNOSUFFIX, IOUtil.stripExtension(tail));
                if (!processChildren(node)) {
                    return false;
                }
            } catch (MyBreakException be) {
                break;
            } catch (MyContinueException ce) {
            }
        }
        popProperties();
        return true;

    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagImport(Element node) throws Throwable {
        Element parent = (Element) node.getParentNode();
        String file = applyMacros(node, ATTR_FILE);
        Element root = XmlUtil.findRoot(node);
        Element newRoot = XmlUtil.getRoot(file, getClass());
        newRoot = (Element) root.getOwnerDocument().importNode(newRoot, true);
        parent.insertBefore(newRoot, node);
        parent.removeChild(node);
        if (!processNode(newRoot)) {
            return false;
        }
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagDatasource(Element node) throws Throwable {
        debug("Creating data source");

        DataSource dataSource = null;
        String id = applyMacros(node, ATTR_ID, (String) null);
        if ((id != null) && (objectMap != null)) {
            dataSource = (DataSource) objectMap.get(id);
        }

        if (dataSource == null) {
            Object dataObject = applyMacros(node, ATTR_URL, (String) null);
            if (dataObject == null) {
                dataObject = StringUtil.toString(findFiles(node));
            }
            String bundle = applyMacros(node, ATTR_BUNDLE, (String) null);
            String type = applyMacros(node, ATTR_TYPE, (String) null);
            if ((bundle == null) && (dataObject == null)) {
                return error("datasource tag requires either a url, fileset or a bundle");
            }

            if (dataObject != null) {
                dataSource = getIdv().makeOneDataSource(dataObject, type, null);
                if (dataSource == null) {
                    return error("Failed to create data source:" + dataObject + " " + type);
                }
            } else {
                try {
                    String bundleXml = IOUtil.readContents(bundle);
                    Object obj = getIdv().getEncoderForRead().toObject(bundleXml);
                    if (!(obj instanceof DataSource)) {
                        return error("datasource bundle is not a DataSource:" + obj.getClass().getName());
                    }
                    dataSource = (DataSource) obj;
                } catch (Exception exc) {
                    return error("Error loading data source bundle: " + bundle, exc);
                }
            }
        }

        if (XmlUtil.hasAttribute(node, ATTR_TIMES)) {
            List timesList = StringUtil.parseIntegerListString(applyMacros(node, ATTR_TIMES, (String) null));
            dataSource.setDateTimeSelection(timesList);
        }

        if (XmlUtil.hasAttribute(node, ATTR_ENSEMBLES)) {
            List ensList = StringUtil.parseIntegerListString(applyMacros(node, ATTR_ENSEMBLES, (String) null));
            if (dataSource instanceof GridDataSource) {
                ((GridDataSource) dataSource).setEnsembleSelection(ensList);
            }
        }

        processGeoSelectionTags(node, dataSource.getDataSelection());

        Hashtable properties = getProperties(node);
        dataSource.setObjectProperties(properties);
        if (id != null) {
            idToDataSource.put(id, dataSource);
        }

        NodeList elements = XmlUtil.getElements(node);
        for (int childIdx = 0; childIdx < elements.getLength(); childIdx++) {
            Element child = (Element) elements.item(childIdx);
            if (child.getTagName().equals(TAG_DISPLAY)) {
                if (!processDisplayNode(child, dataSource)) {
                    return false;
                }
            }
        }
        //        getIdv().getVMManager().setDisplayMastersActive();
        updateViewManagers();

        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagJoin(Element node) throws Throwable {
        List files = findFiles(node);
        if (files != null) {
            List images = new ArrayList();
            int cols = applyMacros(node, ATTR_COLUMNS, 0);
            int rows = applyMacros(node, ATTR_ROWS, 0);
            if ((cols == 0) && (rows == 0)) {
                cols = 1;
            }
            if ((cols != 0) && (rows != 0)) {
                cols = 0;
            }
            int colNum = 0;
            int rowNum = 0;
            for (int i = 0; i < files.size(); i++) {
                Image theImage = ImageUtils.readImage(files.get(i).toString());
                if (theImage == null) {
                    continue;
                }
                images.add(theImage);
            }
            if (images.size() > 0) {
                if (cols == 0) {
                    cols = images.size() / rows;
                } else {
                    rows = images.size() / cols;
                }

                int maxWidth = 0;
                int maxHeight = 0;
                int colCnt = 0;
                for (int i = 0; i < images.size(); i++) {
                    Image theImage = (Image) images.get(i);
                    int width = theImage.getWidth(null);
                    int height = theImage.getHeight(null);
                }
            }
        }
        return true;
    }

    /**
     * Process the view tag
     *
     * @param node the element
     *
     * @return  true if it was processed
     *
     * @throws Throwable  problems
     */
    protected boolean processTagView(Element node) throws Throwable {
        List vms = getViewManagers(node);

        String width = applyMacros(node, ATTR_WIDTH, (String) null);
        String height = applyMacros(node, ATTR_HEIGHT, (String) null);
        if ((width != null) && (height != null)) {
            getIdv().getStateManager()
                    .setViewSize(new Dimension(new Integer(width).intValue(), new Integer(height).intValue()));
        }

        if (vms.size() == 0) {
            StringBuffer properties = new StringBuffer();
            List nodes = XmlUtil.findChildren(node, TAG_PROPERTY);
            for (int childIdx = 0; childIdx < nodes.size(); childIdx++) {
                Element child = (Element) nodes.get(childIdx);
                properties.append(applyMacros(child, ATTR_NAME) + "=" + applyMacros(child, ATTR_VALUE) + ";");
            }
            vms.add(getIdv().getViewManager(ViewDescriptor.LASTACTIVE, false, properties.toString()));
            return true;
        }

        for (int i = 0; i < vms.size(); i++) {
            ViewManager vm = (ViewManager) vms.get(i);
            List nodes = XmlUtil.findChildren(node, TAG_PROPERTY);
            for (int childIdx = 0; childIdx < nodes.size(); childIdx++) {
                Element child = (Element) nodes.get(childIdx);
                vm.setProperty(applyMacros(child, ATTR_NAME), applyMacros(child, ATTR_VALUE), false);
            }
        }
        return true;
    }

    /**
     * Handle the animation tag. The index attribute can either be a number or be "end"
     *
     * @param node  the node
     *
     * @return  true if successful
     *
     * @throws Throwable  problems
     */
    protected boolean processTagAnimation(Element node) throws Throwable {
        String indexString = applyMacros(node, ATTR_INDEX, "0");
        int index = -1;
        boolean end = indexString.equals("end");
        boolean step = indexString.equals("step");
        if (!end && !step) {
            index = new Integer(indexString).intValue();
        }
        for (ViewManager viewManager : getViewManagers(node)) {
            AnimationWidget animationWidget = viewManager.getAnimationWidget();
            if (animationWidget == null) {
                continue;
            }
            if (end) {
                animationWidget.gotoEnd();
            } else if (step) {
                animationWidget.stepForward();
            } else {
                animationWidget.gotoIndex(index);
            }
        }
        return true;
    }

    /**
     * Process the viewpoint tag
     *
     * @param node  the node
     *
     * @return  true if successful
     *
     * @throws Throwable  problems
     */
    protected boolean processTagViewpoint(Element node) throws Throwable {

        List vms = getViewManagers(node);
        if (vms.size() == 0) {
            debug("Could not find view managers processing:" + XmlUtil.toString(node));
        }
        ViewpointInfo viewpointInfo = null;

        for (int i = 0; i < vms.size(); i++) {
            ViewManager vm = (ViewManager) vms.get(i);
            if (XmlUtil.hasAttribute(node, ATTR_VIEWDIR)) {
                vm.setView(XmlUtil.getAttribute(node, ATTR_VIEWDIR));
            }

            if (XmlUtil.hasAttribute(node, ATTR_AZIMUTH) || XmlUtil.hasAttribute(node, ATTR_TILT)) {
                viewpointInfo = new ViewpointInfo(toDouble(node, ATTR_AZIMUTH, 0.0),
                        toDouble(node, ATTR_TILT, 0.0));
                if (!(vm instanceof MapViewManager)) {
                    continue;
                }
                MapViewManager mvm = (MapViewManager) vm;
                mvm.setViewpointInfo(viewpointInfo);
            }

            if (XmlUtil.hasAttribute(node, ATTR_ASPECTX) || XmlUtil.hasAttribute(node, ATTR_ASPECTY)
                    || XmlUtil.hasAttribute(node, ATTR_ASPECTZ)) {
                double[] a = vm.getMaster().getDisplayAspect();
                a = new double[] { toDouble(node, ATTR_ASPECTX, a[0]), toDouble(node, ATTR_ASPECTY, a[1]),
                        toDouble(node, ATTR_ASPECTZ, a[2]) };
                vm.getMaster().setDisplayAspect(a);
                vm.setAspectRatio(a);
            }

            if (XmlUtil.hasAttribute(node, ATTR_ROTX) || XmlUtil.hasAttribute(node, ATTR_ROTY)
                    || XmlUtil.hasAttribute(node, ATTR_ROTZ) || XmlUtil.hasAttribute(node, ATTR_TRANSX)
                    || XmlUtil.hasAttribute(node, ATTR_TRANSY) || XmlUtil.hasAttribute(node, ATTR_TRANSZ)
                    || XmlUtil.hasAttribute(node, ATTR_SCALE)) {
                double[] a = vm.getMaster().getDisplayAspect();
                double[] currentMatrix = vm.getDisplayMatrix();
                double[] trans = { 0.0, 0.0, 0.0 };
                double[] rot = { 0.0, 0.0, 0.0 };
                double[] scale = { 0.0, 0.0, 0.0 };

                MouseBehavior mb = vm.getMaster().getMouseBehavior();
                mb.instance_unmake_matrix(rot, scale, trans, currentMatrix);
                double scaleValue = applyMacros(node, ATTR_SCALE, 0.0);
                if (scaleValue != 0) {
                    double scaleX = scaleValue * a[0];
                    double scaleY = scaleValue * a[1];
                    double scaleZ = scaleValue * a[2];
                    double[] scaleMatrix = mb.make_matrix(0.0, 0.0, 0.0, scaleX / scale[0], scaleY / scale[1],
                            scaleZ / scale[2], 0.0, 0.0, 0.0);
                    currentMatrix = mb.multiply_matrix(scaleMatrix, currentMatrix);
                    mb.instance_unmake_matrix(rot, scale, trans, currentMatrix);
                }
                double[] tmp;
                double dummy = 0.0;
                //TODO: the rotation maybe doesn't work
                if (XmlUtil.hasAttribute(node, ATTR_ROTX)) {
                    tmp = mb.make_matrix(applyMacros(node, ATTR_ROTX, dummy), 0.0, 0.0, 1.0, 0.0, 0.0, 0.0);
                    currentMatrix = mb.multiply_matrix(tmp, currentMatrix);
                    mb.instance_unmake_matrix(rot, scale, trans, currentMatrix);
                }
                if (XmlUtil.hasAttribute(node, ATTR_ROTY)) {
                    tmp = mb.make_matrix(0.0, applyMacros(node, ATTR_ROTY, dummy) - rot[1], 0.0, 1.0, 0.0, 0.0,
                            0.0);

                    currentMatrix = mb.multiply_matrix(tmp, currentMatrix);
                    mb.instance_unmake_matrix(rot, scale, trans, currentMatrix);
                }
                if (XmlUtil.hasAttribute(node, ATTR_ROTZ)) {
                    tmp = mb.make_matrix(0.0, 0.0, applyMacros(node, ATTR_ROTZ, dummy) - rot[2], 1.0, 0.0, 0.0,
                            0.0);

                    currentMatrix = mb.multiply_matrix(tmp, currentMatrix);
                    mb.instance_unmake_matrix(rot, scale, trans, currentMatrix);
                }

                if (XmlUtil.hasAttribute(node, ATTR_TRANSX)) {
                    tmp = mb.make_translate(applyMacros(node, ATTR_TRANSX, dummy) - trans[0], 0.0, 0.0);
                    currentMatrix = mb.multiply_matrix(tmp, currentMatrix);
                    mb.instance_unmake_matrix(rot, scale, trans, currentMatrix);
                }
                if (XmlUtil.hasAttribute(node, ATTR_TRANSY)) {
                    tmp = mb.make_translate(0.0, applyMacros(node, ATTR_TRANSY, dummy) - trans[1], 0.0);
                    currentMatrix = mb.multiply_matrix(tmp, currentMatrix);
                    mb.instance_unmake_matrix(rot, scale, trans, currentMatrix);
                }

                if (XmlUtil.hasAttribute(node, ATTR_TRANSZ)) {
                    tmp = mb.make_translate(0.0, 0.0, applyMacros(node, ATTR_TRANSZ, dummy) - trans[2]);
                    currentMatrix = mb.multiply_matrix(tmp, currentMatrix);
                    mb.instance_unmake_matrix(rot, scale, trans, currentMatrix);
                }

                vm.setDisplayMatrix(currentMatrix);

            }
        }
        return true;

    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagCenter(Element node) throws Throwable {
        List vms = getViewManagers(node);
        if (XmlUtil.hasAttribute(node, ATTR_LAT)) {
            getVMManager().center(
                    ucar.visad.Util.makeEarthLocation(toDouble(node, ATTR_LAT), toDouble(node, ATTR_LON)), vms);
            return true;
        }

        if (XmlUtil.hasAttribute(node, ATTR_NORTH)) {
            ProjectionRect projRect = new ProjectionRect(toDouble(node, ATTR_WEST), toDouble(node, ATTR_NORTH),
                    toDouble(node, ATTR_EAST), toDouble(node, ATTR_SOUTH));
            getVMManager().center(projRect, vms);
            return true;
        }

        if (XmlUtil.hasAttribute(node, ATTR_DISPLAY)) {
            DisplayControlImpl display = findDisplayControl(node);
            if (display == null) {
                throw new IllegalArgumentException("Could not find display:" + XmlUtil.toString(node));
            }
            if (XmlUtil.getAttribute(node, ATTR_USEPROJECTION, false)) {
                MapProjection mp = display.getDataProjection();
                if (mp != null) {
                    getVMManager().center(mp, vms);
                }
            } else {
                LatLonPoint llp = display.getDisplayCenter();
                if (llp != null) {
                    getVMManager().center(ucar.visad.Util.makeEarthLocation(llp), vms);
                }
            }
            return true;
        }

        getVMManager().center(vms);
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagProjection(Element node) throws Throwable {
        if (XmlUtil.hasAttribute(node, ATTR_NAME)) {
            String projName = applyMacros(node, ATTR_NAME);
            ProjectionImpl projection = getIdv().getIdvProjectionManager().findProjectionByName(projName);

            if (projection == null) {
                throw new IllegalArgumentException("Could not find projection: " + projName);
            } else {
                MapProjection mp = new ProjectionCoordinateSystem(projection);
                getVMManager().center(mp);
            }

            return true;
        }
        return true;
    }

    /**
     * Find the data source that is identified by the given xml node
     *
     * @param node node
     *
     * @return The data source or null
     */
    private DataSource findDataSource(Element node) {
        String id = XmlUtil.getAttribute(node, ATTR_DATASOURCE);
        return findDataSource(id);
    }

    /**
     * Find the data source with the given id
     *
     * @param id the id we pass to datasource.identifiedByName
     *
     * @return The data source or null if none found
     */
    private DataSource findDataSource(String id) {
        List dataSources = getIdv().getDataSources();
        DataSource dataSource = (DataSource) idToDataSource.get(id);
        if (dataSource != null) {
            return dataSource;
        }
        for (int i = 0; i < dataSources.size(); i++) {
            dataSource = (DataSource) dataSources.get(i);
            if (dataSource.identifiedByName(id)) {
                return dataSource;
            }
        }
        return null;
    }

    /**
     * Find the display control that is identified by the given xml node
     *
     * @param node node
     *
     * @return The display control source or null
     */
    private DisplayControlImpl findDisplayControl(Element node) {
        String id = XmlUtil.getAttribute(node, ATTR_DISPLAY);
        return findDisplayControl(id);
    }

    /**
     * Find the display control identified by the given id
     *
     * @param id The id of the display control. This can be the id or it can be a 'class:class name'
     *
     * @return The display control or null
     */
    public DisplayControlImpl findDisplayControl(String id) {
        List controls = getIdv().getDisplayControls();
        return findDisplayControl(id, controls);
    }

    /**
     * Find the display control
     *
     * @param id  the control id
     * @param controls  the list of controls
     *
     * @return  the control or null
     */
    public DisplayControlImpl findDisplayControl(String id, List<DisplayControlImpl> controls) {
        for (int i = 0; i < controls.size(); i++) {
            DisplayControlImpl control = (DisplayControlImpl) controls.get(i);

            if (id.startsWith("class:")) {
                if (StringUtil.stringMatch(control.getClass().getName(), id.substring(6), true, true)) {
                    return control;
                }
            }
            if (control.getId() != null) {
                if (StringUtil.stringMatch(control.getId(), id, true, true)) {
                    return control;
                }
            }
        }
        return null;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagBundle(Element node) throws Throwable {

        List timesList = null;
        List ensList = null;
        if (XmlUtil.hasAttribute(node, ATTR_TIMES)) {
            timesList = StringUtil.parseIntegerListString(applyMacros(node, ATTR_TIMES, (String) null));
        }
        if (XmlUtil.hasAttribute(node, ATTR_ENSEMBLES)) {
            ensList = StringUtil.parseIntegerListString(applyMacros(node, ATTR_ENSEMBLES, (String) null));
        }

        List nodes = XmlUtil.findChildren(node, TAG_SETFILES);
        List ids = new ArrayList();
        List fileList = new ArrayList();
        getPersistenceManager().clearFileMapping();
        for (int i = 0; i < nodes.size(); i++) {
            Element child = (Element) nodes.get(i);
            String dataSourceId = XmlUtil.getAttribute(child, ATTR_DATASOURCE);
            ids.add(dataSourceId);
            List files = new ArrayList();
            if (XmlUtil.hasAttribute(child, ATTR_FILE)) {
                String file = applyMacros(child, ATTR_FILE);
                debug("For data source: " + dataSourceId + " Using file: " + file);
                files.add(file);
            } else {
                List filesetFiles = findFiles(child);
                if (filesetFiles != null) {
                    debug("For data source: " + dataSourceId + " Using file: " + filesetFiles);
                    files.addAll(filesetFiles);
                } else {
                    debug("For data source: " + dataSourceId + " Could not find any files");
                }
            }
            fileList.add(files);
            debug("Adding a file override id=" + dataSourceId + " files=" + files);
        }
        if (ids.size() > 0) {
            getPersistenceManager().setFileMapping(ids, fileList);
        }

        String width = applyMacros(node, ATTR_WIDTH, (String) null);
        String height = applyMacros(node, ATTR_HEIGHT, (String) null);
        if ((width != null) && (height != null)) {
            getIdv().getStateManager()
                    .setViewSize(new Dimension(new Integer(width).intValue(), new Integer(height).intValue()));
        }
        String bundleFile = applyMacros(node, ATTR_FILE, (String) null);
        boolean doRemove = applyMacros(node, ATTR_CLEAR, true);
        if (doRemove) {
            //            try {
            cleanup();
            //            } catch(Exception exc) {
            //                System.err.println ("Error cleanup");
            //                System.exit(1);
            //            }
        }
        getIdv().getStateManager().setAlwaysLoadBundlesSynchronously(true);
        Hashtable bundleProperties = new Hashtable();
        if (timesList != null) {
            bundleProperties.put(IdvPersistenceManager.PROP_TIMESLIST, timesList);
        }
        if (ensList != null) {
            bundleProperties.put(IdvPersistenceManager.PROP_ENSLIST, ensList);
        }

        if (bundleFile != null) {
            debug("Loading bundle: " + bundleFile);
            if (bundleFile.endsWith(".jnlp")) {
                String xml = getPersistenceManager().extractBundleFromJnlp(bundleFile);
                getPersistenceManager().decodeXml(xml, false, bundleFile, null, false, true, bundleProperties, true,
                        false);
            } else if (getArgsManager().isZidvFile(bundleFile)) {
                Hashtable properties = new Hashtable();
                boolean ask = getStore().get(PREF_ZIDV_ASK, true);
                getStore().put(PREF_ZIDV_ASK, false);
                getPersistenceManager().decodeXmlFile(bundleFile, "", false, false, properties);
                getStore().put(PREF_ZIDV_ASK, ask);
            } else {
                String xml = IOUtil.readContents(bundleFile);
                xml = applyMacros(xml, null, false);
                getPersistenceManager().decodeXml(xml, false, bundleFile, null, false, true, bundleProperties, true,
                        false);
                //                getPersistenceManager().decodeXmlFile(bundleFile, false,
                //                        timesList);
            }
        } else {
            String b64Bundle = XmlUtil.getChildText(node).trim();
            if (b64Bundle.length() == 0) {
                return error("Could not bundle");
            }
            String xml = new String(XmlUtil.decodeBase64(b64Bundle));
            getPersistenceManager().decodeXml(xml, false, "", null, false, true, bundleProperties, true, false);

        }

        if (applyMacros(node, ATTR_WAIT, true)) {
            getIdv().getIdvUIManager().waitUntilDisplaysAreDone(getIdv().getIdvUIManager());
        }
        getPersistenceManager().clearFileMapping();
        Color c = applyMacros(node, ATTR_COLOR, (Color) null);
        List viewManagers = getVMManager().getViewManagers();
        for (int i = 0; i < viewManagers.size(); i++) {
            ViewManager viewManager = (ViewManager) viewManagers.get(i);
            if (c != null) {
                viewManager.setColors(null, c);
            }
            viewManager.updateDisplayList();
        }

        //One more pause for the display lists
        updateViewManagers();
        getIdv().getIdvUIManager().waitUntilDisplaysAreDone(getIdv().getIdvUIManager());
        return true;
    }

    /**
     * remove data and displays, etc
     */
    private void cleanup() {
        getIdv().removeAllDisplays(false);
        getIdv().removeAllDataSources();
        idToDataSource = new Hashtable();
        ucar.unidata.util.CacheManager.clearCache();

        //        getIdv().getIdvUIManager().disposeAllWindows();
        if (getIdv().getArgsManager().getIsOffScreen()) {
            getIdv().getVMManager().removeAllViewManagers(true);
        }
        getIdv().getIdvUIManager().clearWaitCursor();

        double totalMemory = (double) Runtime.getRuntime().maxMemory();
        double highWaterMark = (double) Runtime.getRuntime().totalMemory();
        double freeMemory = (double) Runtime.getRuntime().freeMemory();
        double usedMemory = (highWaterMark - freeMemory);
        totalMemory = totalMemory / 1000000.0;
        usedMemory = usedMemory / 1000000.0;
        highWaterMark = highWaterMark / 1000000.0;

        /*            System.err.println(
                  "MEM:" + ((int) usedMemory) + "/" + ((int) highWaterMark)
                  + " vms:" + getIdv().getVMManager().getViewManagers().size());
        */

    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagCall(Element node) throws Throwable {
        String name = applyMacros(node, ATTR_NAME);
        Element procNode = (Element) procs.get(name);
        return processTagCall(node, procNode);
    }

    /**
     * process the given node
     *
     * @param node Node to process
     * @param procNode The procedure node
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagCall(Element node, Element procNode) throws Throwable {
        if (procNode == null) {
            return error("Could not find procedure node for call:" + XmlUtil.toString(node));
        }

        pushProperties();
        String cdata = XmlUtil.getChildText(node);
        if ((cdata != null) && (cdata.trim().length() > 0)) {
            putProperty("paramtext", cdata);
        } else {
            putProperty("paramtext", "");
        }

        NamedNodeMap procnnm = procNode.getAttributes();
        if (procnnm != null) {
            for (int i = 0; i < procnnm.getLength(); i++) {
                Attr attr = (Attr) procnnm.item(i);
                if (!ATTR_NAME.equals(attr.getNodeName())) {
                    putProperty(attr.getNodeName(), applyMacros(attr.getNodeValue()));
                }
            }
        }

        NamedNodeMap nnm = node.getAttributes();
        if (nnm != null) {
            for (int i = 0; i < nnm.getLength(); i++) {
                Attr attr = (Attr) nnm.item(i);
                if (!ATTR_NAME.equals(attr.getNodeName())) {
                    putProperty(attr.getNodeName(), applyMacros(attr.getNodeValue()));
                }
            }
        }
        try {
            if (!processChildren(node)) {
                return false;
            }
            try {
                if (!processChildren(procNode)) {
                    return false;
                }
            } catch (MyReturnException mre) {
                //noop
            }
        } catch (Throwable throwable) {
            popProperties();
            throw throwable;
        }
        popProperties();
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagIf(Element node) throws Throwable {
        String expr = applyMacros(node, ATTR_EXPR, (String) null);
        if (expr == null) {
            expr = applyMacros(XmlUtil.getChildText(node));
        }
        if ((expr == null) || (expr.trim().length() == 0)) {
            return error("Could not find if expression");
        }
        expr = expr.trim();
        boolean result = getInterpreter().eval(expr).toString().equals("1");

        Element thenNode = XmlUtil.findChild(node, TAG_THEN);
        Element elseNode = XmlUtil.findChild(node, TAG_ELSE);
        Element statementNode = (result ? thenNode : elseNode);
        if (statementNode == null) {
            if (result && (thenNode == null) && (elseNode == null)) {
                statementNode = node;
            } else {
                return true;
            }
        }
        if (statementNode != null) {
            //            pushProperties();
            try {
                if (!processChildren(statementNode)) {
                    return false;
                }
            } catch (Throwable throwable) {
                //                popProperties();
                throw throwable;
            }
            //            popProperties();
        }
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagOutput(Element node) throws Throwable {
        if (!XmlUtil.hasAttribute(node, ATTR_FILE)) {
            for (int i = 0; i < outputStack.size(); i++) {
                OutputInfo oi = (OutputInfo) outputStack.get(i);
                oi.process(node);
            }
            return true;
        }
        OutputInfo outputInfo = new OutputInfo(node);
        outputStack.add(outputInfo);
        pushProperties();
        try {
            if (!processChildren(node)) {
                return false;
            }
        } catch (Throwable throwable) {
            popProperties();
            throw throwable;
        }
        popProperties();
        outputStack.remove(outputStack.size() - 1);
        outputInfo.write();
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagIsl(Element node) throws Throwable {
        debug = applyMacros(node, ATTR_DEBUG, false);
        boolean offScreen = applyMacros(node, ATTR_OFFSCREEN, true);
        //        System.err.println ("offscreen:" + offScreen);
        if (!getIdv().getArgsManager().getIslInteractive()) {
            //            System.err.println ("setting offscreen:" + offScreen);
            getIdv().getArgsManager().setIsOffScreen(offScreen);
        }
        putProperty(PROP_OFFSCREEN, (getIdv().getArgsManager().getIsOffScreen() ? "1" : "0"));

        //        System.err.println("setting offScreen " +         getIdv().getArgsManager().getIsOffScreen());

        return processTagGroup(node);
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagGroup(Element node) throws Throwable {
        pushProperties();
        int loopTimes = applyMacros(node, ATTR_LOOP, 1);
        String sleepString = applyMacros(node, ATTR_SLEEP, (String) null);
        long sleepTime = 0;
        if (sleepString != null) {
            sleepString = sleepString.trim();
            long multiplier = 1000;
            String unit = StringUtil.findPattern(sleepString, "[0-9.]+(.*)$");

            if ((unit != null) && (unit.trim().length() > 0)) {
                sleepString = sleepString.substring(0, sleepString.length() - unit.length());
                if (unit.equals("s")) {
                } else if (unit.equals("seconds")) {
                } else if (unit.equals("minutes")) {
                    multiplier = 60 * 1000;
                } else if (unit.equals("m")) {
                    multiplier = 60 * 1000;
                } else if (unit.equals("hours")) {
                    multiplier = 60 * 60 * 1000;
                } else if (unit.equals("h")) {
                    multiplier = 60 * 60 * 1000;
                } else {
                    return error("Unknown sleep time unit:" + unit);
                }
            }
            sleepTime = (long) (multiplier * new Double(sleepString).doubleValue());
        }
        for (int i = 0; i < loopTimes; i++) {
            currentLoopIndex = i;
            try {
                if (!processChildren(node)) {
                    return false;
                }
            } catch (MyBreakException be) {
                break;
            } catch (MyContinueException ce) {
            }
            if ((loopTimes > 1) && (sleepTime > 0)) {
                Misc.sleep(sleepTime);
            }
        }
        popProperties();

        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagForeach(Element node) throws Throwable {
        pushProperties();
        List allValues = new ArrayList();
        int numElements = 0;
        int cnt = 1;
        NamedNodeMap attrs = node.getAttributes();
        if (attrs == null) {
            return error("No values in foreach tag");
        }

        for (int i = 0; i < attrs.getLength(); i++) {
            Attr attr = (Attr) attrs.item(i);
            String var = attr.getNodeName();
            String values = applyMacros(attr.getNodeValue());
            List tokens;
            //Check if it starts with file:, if so read the contents and split on new line
            if (values.startsWith("file:")) {
                String filename = applyMacros(values.substring("file:".length()));
                values = IOUtil.readContents(filename, getClass()).trim();
                tokens = StringUtil.split(values, "\n");
            } else {
                tokens = StringUtil.split(values, ",");
            }

            if (allValues.size() == 0) {
                numElements = tokens.size();
            } else if (numElements != tokens.size()) {
                return error("Bad number of tokens (" + tokens.size() + " should be:" + numElements
                        + ") in foreach argument:\n" + var + "=" + tokens);
            }
            allValues.add(new Object[] { var, tokens });
            cnt++;
        }
        for (int tokIdx = 0; tokIdx < numElements; tokIdx++) {
            for (int valueIdx = 0; valueIdx < allValues.size(); valueIdx++) {
                Object[] tuple = (Object[]) allValues.get(valueIdx);
                putProperty(tuple[0], ((List) tuple[1]).get(tokIdx));
            }
            try {
                if (!processChildren(node)) {
                    return false;
                }
            } catch (MyBreakException be) {
                break;
            } catch (MyContinueException ce) {
            }
        }
        popProperties();
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagMovie(Element node) throws Throwable {
        pushProperties();
        captureMovie(null, node);
        popProperties();
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagHtml(Element node) throws Throwable {

        String html = null;
        if (XmlUtil.hasAttribute(node, ATTR_FROMFILE)) {
            html = IOUtil.readContents(applyMacros(node, ATTR_FROMFILE));
        } else {
            html = XmlUtil.getChildText(node);
        }
        html = applyMacros(html);
        int width = XmlUtil.getAttribute(node, ATTR_WIDTH, -1);
        Image image = ImageUtils.renderHtml(html, width, null, null);
        image = processImage(ImageUtils.toBufferedImage(image), XmlUtil.getAttribute(node, ATTR_FILE), node,
                getAllProperties(), null, new Hashtable());

        //        writeImageToFile(image, XmlUtil.getAttribute(node, ATTR_FILE));
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagPanel(Element node) throws Throwable {
        pushProperties();
        captureMovie(null, node);
        popProperties();
        return true;
    }

    /**
     * Parse the xml
     *
     * @param xml the xml
     *
     * @return the root
     *
     * @throws Exception On badness
     */
    private Element makeElement(String xml) throws Exception {
        return XmlUtil.getRoot(xml);
    }

    /**
     * Capture a movie and write it out. This is typically called by the jython scripting
     *
     * @param filename Movie filename
     * @param params xml parameters of the the form:  "task arg=val arg2=val; task2 arg3=val"
     *
     * @throws Exception On badness
     */
    public void writeMovie(String filename, String params) throws Exception {
        writeMovie(filename, false, params);
    }

    /**
     * Capture a movie and write it out. This is typically called from Jython.
     *
     * @param filename Movie filename.
     * @param fps Frames per second.
     * @param endFramePause Number of seconds to pause before restarting. 
     * Only applicable when {@code filename} is a GIF.
     * @param globalPalette Whether or not to use a {@literal "global"} color
     * palette for animated GIF output.
     * @param params XML parameters of the the form:
     * {@literal "task arg=val arg2=val; task2 arg3=val"}.
     *
     * @throws Exception On badness
     */
    public void writeMovie(String filename, double fps, double endFramePause, boolean globalPalette, String params)
            throws Exception {
        String isl = makeXmlFromString(params);
        String fmtString = "%s\n" + "<movie file=\"%s\" imagesuffix=\".png\" framerate=\"%s\" endframepause=\"%s\">"
                + "%s</movie>";
        String xml = String.format(fmtString, XmlUtil.getHeader(), filename, fps, endFramePause, isl);
        captureMovie(applyMacros(filename), globalPalette, makeElement(xml));
    }

    /**
     * Capture a movie and write it out. This is typically called by the jython scripting
     *
     * @param filename Movie filename.
     * @param globalPalette Whether or not to use a {@literal "global"} color
     * palette for animated GIF output.
     * @param params XML parameters of the the form:
     * {@literal "task arg=val arg2=val; task2 arg3=val"}.
     *
     * @throws Exception On badness
     */
    public void writeMovie(String filename, boolean globalPalette, String params) throws Exception {
        writeMovie(filename, 2, 2, globalPalette, params);
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagImage(Element node) throws Throwable {
        captureImage(XmlUtil.getAttribute(node, ATTR_FILE), node);
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagWait(Element node) throws Throwable {
        File f = null;
        if (XmlUtil.hasAttribute(node, ATTR_FILE)) {
            f = new File(applyMacros(node, ATTR_FILE));
        }
        double seconds = applyMacros(node, ATTR_SECONDS, 60.0);
        if ((f != null) && f.isDirectory()) {
            String patternStr = applyMacros(applyMacros(node, ATTR_PATTERN, (String) null));
            IOUtil.wait(f, patternStr, seconds);
        } else {
            if (f != null) {
                IOUtil.wait(Misc.newList(f), seconds);
            } else {
                IOUtil.wait(findFiles(node), seconds);
            }
        }
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     *
     * @throws Throwable On badness
     */
    protected boolean processTagPause(Element node) throws Throwable {
        if (XmlUtil.hasAttribute(node, ATTR_EVERY)) {
            Misc.pauseEvery((int) (60 * toDouble(node, ATTR_EVERY)));
            return true;
        }
        if (XmlUtil.hasAttribute(node, ATTR_SECONDS)) {
            Misc.sleep((long) (1000 * toDouble(node, ATTR_SECONDS)));
        } else if (XmlUtil.hasAttribute(node, ATTR_MINUTES)) {
            Misc.sleep((long) (60 * 1000 * toDouble(node, ATTR_MINUTES)));
        } else if (XmlUtil.hasAttribute(node, ATTR_HOURS)) {
            Misc.sleep((long) (60 * 60 * 1000 * toDouble(node, ATTR_HOURS)));
        } else {
            updateViewManagers();
            getIdv().getIdvUIManager().waitUntilDisplaysAreDone(getIdv().getIdvUIManager());
        }
        return true;

    }

    /**
     * Update the view managers
     */
    protected void updateViewManagers() {
        try {
            List viewManagers = getVMManager().getViewManagers();
            for (int i = 0; i < viewManagers.size(); i++) {
                ViewManager viewManager = (ViewManager) viewManagers.get(i);
                viewManager.updateDisplayIfNeeded();
            }
        } catch (Exception exc) {
            logException("Updating view manager", exc);
        }
    }

    /**
     * Process tag display properties
     *
     * @param node  the node
     *
     * @return true if successful
     */
    protected boolean processTagDisplayproperties(Element node) {
        DisplayControlImpl display = findDisplayControl(node);
        if (display == null) {
            throw new IllegalArgumentException("Could not find display:" + XmlUtil.toString(node));
        }
        Hashtable properties = getProperties(node);
        display.applyProperties(properties);
        return true;
    }

    /**
     * process the given node
     *
     * @param node Node to process
     *
     * @return keep going
     */

    protected boolean processTagDisplay(Element node) {
        if (!processDisplayNode(node, null)) {
            return false;
        }
        if (applyMacros(node, ATTR_WAIT, true)) {
            pause();
        }
        updateViewManagers();
        return true;
    }

    /**
     * Process the display node. If data source is not null then use that
     * to find data choices. If null then use all loaded data sources to find
     * data choice.
     *
     * @param node Node to process
     * @param dataSource The data source. May be null.
     *
     * @return keep going
     */
    private boolean processDisplayNode(Element node, DataSource dataSource) {

        //TODO:
        String type = applyMacros(node, ATTR_TYPE, (String) null);
        String param = applyMacros(node, ATTR_PARAM, (String) null);
        DataChoice dataChoice = null;
        debug("Creating display: " + type + " param:" + param);

        if ((dataSource == null) && XmlUtil.hasAttribute(node, ATTR_DATASOURCE)) {
            dataSource = findDataSource(node);
            if (dataSource == null) {
                return error("Failed to to find data source for display tag:" + XmlUtil.toString(node));
            }
        }

        if (param != null) {
            if (dataSource != null) {
                dataChoice = dataSource.findDataChoice(param);
            } else {
                List dataSources = getIdv().getDataSources();
                for (int i = 0; (i < dataSources.size()) && (dataChoice == null); i++) {
                    dataSource = (DataSource) dataSources.get(i);
                    dataChoice = dataSource.findDataChoice(param);
                }
            }
            if (dataChoice == null) {
                return error("Failed to find parameter:" + param);
            }
        }
        List dataChoices = new ArrayList();
        if (dataChoice != null) {
            dataChoice = dataChoice.createClone();
            DataSelection dataSelection = new DataSelection(applyMacros(node, ATTR_LEVEL_FROM, (String) null),
                    applyMacros(node, ATTR_LEVEL_TO, (String) null));

            processGeoSelectionTags(node, dataSelection);

            String timeString = applyMacros(node, ATTR_TIMES, (String) null);
            if (timeString != null) {
                List times = new ArrayList();
                for (String tok : StringUtil.split(timeString, ",", true, true)) {
                    times.add(new Integer(tok));
                }
                dataSelection.setTimes(times);
            }
            if (XmlUtil.hasAttribute(node, ATTR_ENSEMBLES)) {
                List ensList = StringUtil.parseIntegerListString(applyMacros(node, ATTR_ENSEMBLES, (String) null));
                dataSelection.putProperty(GridDataSource.PROP_ENSEMBLEMEMBERS, ensList);
            }
            dataChoice.setDataSelection(dataSelection);
            dataChoices.add(dataChoice);
        }

        if (type == null) {
            String bundleXml = null;
            if (XmlUtil.hasAttribute(node, ATTR_TEMPLATE)) {
                String filename = applyMacros(node, ATTR_TEMPLATE);
                try {
                    bundleXml = IOUtil.readContents(filename);
                } catch (IOException exc) {
                    return error("Could not find file: " + filename);
                }
            } else {
                Element templateNode = XmlUtil.findChild(node, TAG_TEMPLATE);
                if (templateNode != null) {
                    bundleXml = XmlUtil.getChildText(templateNode);
                }
            }
            if (bundleXml == null) {
                return error("<display> tag does not contain type attribute or template attribute/tag");
            }
            try {
                Object obj = getIdv().getEncoderForRead().toObject(bundleXml);

                if (!(obj instanceof DisplayControl)) {
                    return error("display template is not a DisplayControl:" + obj.getClass().getName());
                }
                DisplayControl displayControl = (DisplayControl) obj;
                displayControl.initAfterUnPersistence(getIdv(), getProperties(node), dataChoices);
                getIdv().addDisplayControl(displayControl);
            } catch (Exception exc) {
                return error("Creating display", exc);
            }
        } else {
            ControlDescriptor cd = getIdv().getControlDescriptor(type);
            if (cd == null) {
                return error("Failed to find display control:" + type);
            }
            Trace.call1("ImageGenerator making display");
            getIdv().doMakeControl(dataChoices, cd, getProperties(node), null, false);
            Trace.call2("ImageGenerator making display");
        }
        return true;

    }

    /**
     * Process geo selection tags.
     *
     * @param node the node
     * @param dataSelection the data selection
     * @return true, if successful
     */
    private boolean processGeoSelectionTags(Element node, DataSelection dataSelection) {
        String strideString = applyMacros(node, ATTR_STRIDE, (String) null);
        if (strideString != null) {
            dataSelection.getGeoSelection(true).setXStride(applyMacros(node, ATTR_STRIDE, 1));
            dataSelection.getGeoSelection(true).setYStride(applyMacros(node, ATTR_STRIDE, 1));

        }

        String strideXString = applyMacros(node, ATTR_STRIDEX, (String) null);
        if (strideXString != null) {
            dataSelection.getGeoSelection(true).setXStride(applyMacros(node, ATTR_STRIDEX, 1));

        }

        String strideYString = applyMacros(node, ATTR_STRIDEY, (String) null);
        if (strideYString != null) {
            dataSelection.getGeoSelection(true).setYStride(applyMacros(node, ATTR_STRIDEY, 1));

        }
        String strideZString = applyMacros(node, ATTR_STRIDEZ, (String) null);
        if (strideZString != null) {
            dataSelection.getGeoSelection(true).setZStride(applyMacros(node, ATTR_STRIDEX, 1));

        }

        String bboxString = applyMacros(node, ATTR_BBOX, (String) null);
        if (bboxString != null) {
            List toks = StringUtil.split(bboxString, ",", true, true);
            if (toks.size() != 4) {
                return error("Bad idv.data.geosubset property:" + bboxString);
            } else {
                GeoLocationInfo boundingBox = new GeoLocationInfo(Misc.decodeLatLon((String) toks.get(0)),
                        Misc.decodeLatLon((String) toks.get(1)), Misc.decodeLatLon((String) toks.get(2)),
                        Misc.decodeLatLon((String) toks.get(3)));
                dataSelection.getGeoSelection(true).setBoundingBox(boundingBox);
            }
        }
        return true;
    }

    /**
     * Process the property tag children of the given node
     *
     * @param node parent node that holds property tags
     *
     * @return properties
     */
    private Hashtable getProperties(Element node) {
        Hashtable properties = new Hashtable();
        List nodes = XmlUtil.findChildren(node, TAG_PROPERTY);
        for (int i = 0; i < nodes.size(); i++) {
            Element child = (Element) nodes.get(i);
            properties.put(applyMacros(child, ATTR_NAME), applyMacros(child, ATTR_VALUE));
        }
        return properties;
    }

    /**
     * Utility to print a message and return false.
     *
     * @param msg message
     *
     * @return false
     */
    protected boolean error(String msg) {
        if (!getIdv().getArgsManager().getIsOffScreen()) {
            LogUtil.userErrorMessage(msg);
        } else {
            System.err.println(msg);
        }
        return false;
    }

    /**
     * Utility to print a message and return false.
     *
     * @param msg message
     * @param exc exception
     *
     * @return false
     */
    protected boolean error(String msg, Exception exc) {
        if (!getIdv().getArgsManager().getIsOffScreen()) {
            LogUtil.logException(msg, exc);
        } else {
            logger.error("An error occurred", exc);
        }
        return false;
    }

    /**
     * Find all of the files that are defined by contained fileset nodes.
     *
     * @param parentNode Node to process
     *
     * @return List of files
     */
    private List findFiles(Element parentNode) {
        List resultFiles = null;
        List filesets = XmlUtil.findChildren(parentNode, TAG_FILESET);
        if (filesets.size() > 0) {
            if (resultFiles == null) {
                resultFiles = new ArrayList();
            }
            resultFiles.addAll(findFiles(filesets));
        }
        if (resultFiles == null) {
            return null;
        }
        return resultFiles;
    }

    /**
     * Find all of the files that are defined by any fileset nodes in the nodes list..
     *
     *
     * @param nodes List of nodes
     *
     * @return List of files
     */
    private List findFiles(List nodes) {
        List files = new ArrayList();
        for (int i = 0; i < nodes.size(); i++) {
            Element node = (Element) nodes.get(i);
            if (node.getTagName().equals(TAG_FILESET)) {
                String filename = applyMacros(node, ATTR_FILE, (String) null);
                if (filename != null) {
                    files.add(new File(filename));
                    continue;
                }
                File dir = new File(applyMacros(node, ATTR_DIR, "."));
                String pattern = applyMacros(applyMacros(node, ATTR_PATTERN, (String) null));
                File[] allFiles = ((pattern == null) ? dir.listFiles()
                        : dir.listFiles((java.io.FileFilter) new PatternFileFilter(pattern)));
                if (allFiles == null) {
                    continue;
                }
                List tmpFiles = new ArrayList();
                for (int fileIdx = 0; fileIdx < allFiles.length; fileIdx++) {
                    if (!allFiles[fileIdx].isDirectory()) {
                        if (!files.contains(allFiles[fileIdx])) {
                            tmpFiles.add(allFiles[fileIdx]);
                        }
                    }
                }

                String sort = applyMacros(node, ATTR_SORT, (String) null);
                String sortDir = applyMacros(node, ATTR_SORTDIR, VALUE_ASCENDING);
                if (sort != null) {
                    if (sort.equals(VALUE_TIME)) {
                        if (sortDir.equals(VALUE_ASCENDING)) {
                            tmpFiles = Misc.toList(IOUtil.sortFilesOnAge(IOUtil.toFiles(tmpFiles), false));
                        } else if (sortDir.equals(VALUE_DESCENDING)) {
                            tmpFiles = Misc.toList(IOUtil.sortFilesOnAge(IOUtil.toFiles(tmpFiles), true));
                        } else {
                            System.err.println("unknown sort direction:" + sortDir);
                        }
                    } else {
                        System.err.println("unknown sort type:" + sort);
                    }
                }

                if (XmlUtil.hasAttribute(node, ATTR_FIRST)) {
                    int first = applyMacros(node, ATTR_FIRST, 0);
                    if (first < tmpFiles.size()) {
                        List tmp = new ArrayList();
                        for (int fileIdx = 0; fileIdx < first; fileIdx++) {
                            tmp.add(tmpFiles.get(fileIdx));
                        }
                        tmpFiles = tmp;

                    }
                } else if (XmlUtil.hasAttribute(node, ATTR_LAST)) {
                    int last = applyMacros(node, ATTR_LAST, 0);
                    if (last < tmpFiles.size()) {
                        List tmp = new ArrayList();
                        for (int fileIdx = tmpFiles.size() - 1; fileIdx >= 0; fileIdx--) {
                            tmp.add(0, tmpFiles.get(fileIdx));
                            if (tmp.size() >= last) {
                                break;
                            }
                        }
                        tmpFiles = tmp;
                    }
                }
                files.addAll(tmpFiles);

            }
        }
        return files;
    }

    /**
     * Put the property in the current properties hashtable
     *
     * @param key key
     * @param value value
     */
    private void putProperty(Object key, Object value) {
        putProperty(key, value, false);
    }

    /**
     * Put the property in the current properties hashtable
     *
     * @param key key
     * @param value value
     * @param global If true put it in the base stack frame
     */
    private void putProperty(Object key, Object value, boolean global) {
        Hashtable properties = (global ? (Hashtable) propertiesStack.get(0) : getProperties());
        properties.put(key, value);
    }

    /**
     * Find the property table for the given key
     *
     * @param key The key
     *
     * @return The properties table. If none found then it returns the top of the stack
     */
    private Hashtable findTableFor(Object key) {
        for (int i = propertiesStack.size() - 1; i >= 0; i--) {
            Hashtable properties = (Hashtable) propertiesStack.get(i);
            if (properties.get(key) != null) {
                return properties;
            }
        }
        return getProperties();
    }

    /**
     * Find the table that contains the given property and replace it with the new value
     *
     * @param key key
     * @param value new value
     */
    private void replaceProperty(Object key, Object value) {
        findTableFor(key).put(key, value);
    }

    /**
     * Get the top most hashtable in the properties stack.
     *
     * @return Current properties hashtable.
     */
    private Hashtable getProperties() {
        if (propertiesStack.size() == 0) {
            return new Hashtable();
        }
        return (Hashtable) propertiesStack.get(propertiesStack.size() - 1);
    }

    /**
     * Add a Hashtable to the properties stack.
     *
     * @return The newly created hashtable.
     */
    private Hashtable pushProperties() {
        Hashtable properties = new Hashtable();
        propertiesStack.add(properties);
        return properties;
    }

    /**
     * Remove the top most hashtable in the properties stack
     */
    private void popProperties() {
        propertiesStack.remove(propertiesStack.size() - 1);
    }

    /**
     * utility to convert a string to a double. If the string ends with '%'
     * then return the percentage of the given base value
     *
     *
     * @param node Node to process
     * @param attr The attribute to look up
     * @param baseValue Used to handle '%'
     * @return The value
     */
    private double toDouble(Element node, String attr, double baseValue) {
        String s = applyMacros(node, attr);
        return toDouble(s, baseValue);
    }

    /**
     * utility to make a double. If the string begins with '%' then we take a percentage of the baseValue
     *
     * @param s string
     * @param baseValue Used if s is a percentage
     *
     * @return double value
     */
    private double toDouble(String s, double baseValue) {
        if (s.endsWith("%")) {
            double percent = Misc.toDouble(s.substring(0, s.length() - 1));
            return (percent / 100.0) * baseValue;
        }
        return new Double(s).doubleValue();
    }

    /**
     * Convert the attribute value of the given node to a double
     *
     * @param node Node to process
     * @param attr Attribute name
     *
     * @return double value
     */
    private double toDouble(Element node, String attr) {
        return Misc.toDouble(applyMacros(node, attr));
    }

    /**
     * Find the attribute  value of the given node. Apply the macros to it.
     *
     * @param node Node to process
     * @param attr Attribute name
     *
     * @return The value
     */
    public String applyMacros(Element node, String attr) {
        return applyMacros(XmlUtil.getAttribute(node, attr));
    }

    /**
     * If the attribute does not exist return the dflt. Else return the value.
     *
     * @param node Node to process
     * @param attr Attribute name
     * @param dflt The default value to use if the attribute does not exist
     *
     * @return The value
     */
    public String applyMacros(Element node, String attr, String dflt) {
        return applyMacros(XmlUtil.getAttribute(node, attr, dflt));
    }

    /**
     * If the attribute does not exist return the dflt. Else return the value.
     *
     * @param node Node to process
     * @param attr Attribute name
     * @param dflt The default value to use if the attribute does not exist
     *
     * @return The value
     */
    public int applyMacros(Element node, String attr, int dflt) {
        String value = XmlUtil.getAttribute(node, attr, (String) null);
        if (value == null) {
            return dflt;
        }
        return (int) Misc.toDouble(applyMacros(value));
    }

    /**
     * If the attribute does not exist return the dflt. Else return the value.
     *
     * @param node Node to process
     * @param attr Attribute name
     * @param dflt The default value to use if the attribute does not exist
     *
     * @return The value
     */
    public boolean applyMacros(Element node, String attr, boolean dflt) {
        String value = XmlUtil.getAttribute(node, attr, (String) null);
        if (value == null) {
            return dflt;
        }
        return new Boolean(applyMacros(value)).booleanValue();
    }

    /**
     * If the attribute does not exist return the dflt. Else return the value.
     *
     * @param node Node to process
     * @param attr Attribute name
     * @param dflt The default value to use if the attribute does not exist
     *
     * @return The value
     */
    public Color applyMacros(Element node, String attr, Color dflt) {
        String value = XmlUtil.getAttribute(node, attr, (String) null);
        if (value == null) {
            return dflt;
        }
        String result = applyMacros(value);
        if (result.equals("none")) {
            return null;
        }
        return GuiUtils.decodeColor(result, dflt);
    }

    /**
     * If the attribute does not exist return the dflt. Else return the value.
     *
     * @param node Node to process
     * @param attr Attribute name
     * @param dflt The default value to use if the attribute does not exist
     *
     * @return The value
     */
    public double applyMacros(Element node, String attr, double dflt) {
        String value = XmlUtil.getAttribute(node, attr, (String) null);
        if (value == null) {
            return dflt;
        }
        return Misc.toDouble(applyMacros(value));
    }

    /**
     * Do the macro substitution
     *
     * @param s The string
     *
     * @return The expanded string
     */
    public String applyMacros(String s) {
        return applyMacros(s, null);
    }

    /**
     * Merge all of the proeprties together
     *
     * @return The properties
     */
    private Hashtable getAllProperties() {
        Hashtable props = new Hashtable();
        for (int i = 0; i < propertiesStack.size(); i++) {
            Hashtable properties = (Hashtable) propertiesStack.get(i);
            props.putAll(properties);
        }
        return props;
    }

    /**
     * Do the macro substitution
     *
     * @param s The string
     * @param props Properties
     *
     * @return The expanded string
     */
    private String applyMacros(String s, Hashtable props) {
        return applyMacros(s, props, true);
    }

    /**
     * Do the macro substitution
     *
     * @param s The string
     * @param props Properties
     * @param doTime  process time macros
     *
     * @return The expanded string
     */
    private String applyMacros(String s, Hashtable props, boolean doTime) {
        if (s == null) {
            return null;
        }
        if (props == null) {
            props = new Hashtable();
        } else {
            Hashtable tmp = props;
            props = new Hashtable();
            props.putAll(tmp);
        }
        props.putAll(getAllProperties());

        putIndex(props, PROP_LOOPINDEX, currentLoopIndex);
        props.put(PROP_LOOPINDEX_PAD2, StringUtil.padLeft("" + currentLoopIndex, 2, "0"));
        props.put(PROP_LOOPINDEX_PAD3, StringUtil.padLeft("" + currentLoopIndex, 3, "0"));
        props.put(PROP_LOOPINDEX_PAD4, StringUtil.padLeft("" + currentLoopIndex, 4, "0"));

        Date now = new Date(Misc.getCurrentTime());

        if (DATE_FORMATS == null) {
            TimeZone timeZone = TimeZone.getTimeZone("GMT");
            DATE_FORMATS = new ArrayList();
            for (int i = 0; i < DATE_PROPS.length; i++) {
                SimpleDateFormat sdf = new SimpleDateFormat(DATE_PROPS[i]);
                sdf.setTimeZone(timeZone);
                DATE_FORMATS.add(sdf);
            }
        }

        for (int i = 0; i < DATE_FORMATS.size(); i++) {
            SimpleDateFormat sdf = (SimpleDateFormat) DATE_FORMATS.get(i);
            props.put(DATE_PROPS[i], sdf.format(now));
        }

        props.put("memory", "" + Misc.usedMemory());

        /*
        if (s.indexOf("${anim:") >= 0) {
        now = getAnimationTime();
        if (now != null) {
            for (int i = 0; i < DATE_FORMATS.size(); i++) {
                SimpleDateFormat sdf =
                    (SimpleDateFormat) DATE_FORMATS.get(i);
                props.put("anim:" + DATE_PROPS[i], sdf.format(now));
            }
            
        }
        }*/

        TimeZone tz = getIdv().getPreferenceManager().getDefaultTimeZone();

        s = StringUtil.replaceDate(s, "now:", now, tz);

        if ((s.indexOf("anim:") >= 0) || (s.indexOf("time:") >= 0)) {
            Date animationTime = getAnimationTime();
            if (animationTime == null) {
                animationTime = now;
            }
            if (doTime) {
                s = StringUtil.replaceDate(s, "anim:", animationTime, tz);
                s = StringUtil.replaceDate(s, "time:", animationTime, tz);
                s = StringUtil.replaceDate(s, "now:", now, tz);
            }
        }
        s = StringUtil.applyMacros(s, props, false);
        //Now use the idv properties
        s = StringUtil.applyMacros(s, getStateManager().getProperties(), false);
        if (s.indexOf("${") >= 0) {
            throw new BadIslException("Undefined macro in: " + s);
        }

        if (s.startsWith("jython:")) {
            Object result = getInterpreter().eval(s.substring(7));
            s = result.toString();
        }

        if (s.startsWith("interp.")) {
            Object result = getInterpreter().eval(s);
            s = result.toString();
        }
        if (s.startsWith("islInterpreter.")) {
            Object result = getInterpreter().eval(s);
            s = result.toString();
        }
        s = s.replace("\\n", "\n");
        return s;
    }

    /**
     * Capture an image from the first active view managers
     *
     * @param filename The image filename
     */
    public void captureImage(String filename) {
        try {
            captureImage(filename, null);
        } catch (Throwable exc) {
            logException("Capturing image", exc);
        }
    }

    /**
     * Put the index
     *
     * @param props  the properties
     * @param name the name
     * @param v  the index
     */
    public void putIndex(Hashtable props, String name, int v) {
        props.put(name, new Integer(v));
        props.put(name + "_alpha", getLetter(v).toLowerCase());
        props.put(name + "_ALPHA", getLetter(v).toUpperCase());

        props.put(name + "_ROMAN", getRoman(v).toUpperCase());
        props.put(name + "_roman", getRoman(v).toLowerCase());

    }

    /**
     * Find all view managers that are identified by the given xml node. If the node
     * does not have a "view" attribute return all view managers. Else use the view
     * attribute to find the vms. The view can be class:class name or just a name. If a name can
     * also be a regular expression
     *
     * @param node node
     *
     * @return List of view managers
     */
    private List<ViewManager> getViewManagers(Element node) {
        List<ViewManager> viewManagers = (List<ViewManager>) getVMManager().getViewManagers();
        if ((node == null) || !XmlUtil.hasAttribute(node, ATTR_VIEW)) {
            return viewManagers;
        }

        List goodOnes = new ArrayList();
        String viewId = applyMacros(node, ATTR_VIEW);
        //        System.err.println ("viewManagers:" + viewManagers);
        if (viewId.startsWith("name:")) {
            viewId = viewId.substring(5);
        }

        for (int i = 0; i < viewManagers.size(); i++) {
            ViewManager viewManager = (ViewManager) viewManagers.get(i);
            if (viewId.startsWith("#")) {
                int viewIndex = new Integer(viewId.substring(1)).intValue();
                if (viewIndex == i) {
                    goodOnes.add(viewManager);
                    //                    System.err.println("\tskipping index");
                }
                continue;
            }
            if (viewId.startsWith("class:")) {
                if (StringUtil.stringMatch(viewManager.getClass().getName(), viewId.substring(6), true, true)) {
                    goodOnes.add(viewManager);
                }
                continue;
            }
            String name = viewManager.getName();
            if (name == null) {
                name = "";
            }
            if (StringUtil.stringMatch(name, viewId, true, true)) {
                goodOnes.add(viewManager);
            }
        }
        if (goodOnes.size() == 0) {
            warning("Unable to find any views with id:" + viewId);
        } else {
            //            System.err.println(viewId + " " + goodOnes);
        }
        return goodOnes;
    }

    /**
     * Wait until all displays are built
     */
    public void pause() {
        getIdv().waitUntilDisplaysAreDone();
    }

    /**
     * Toggle debug
     *
     * @param v debug
     */
    public void setDebug(boolean v) {
        debug = v;
    }

    /**
     * Evaluate the given isl
     *
     * @param isl The isl
     *
     * @return success
     *
     * @throws Throwable On badness
     */
    public boolean evaluateIsl(String isl) throws Throwable {
        isl = XmlUtil.tag(TAG_GROUP, "", isl);
        return processNode(XmlUtil.getRoot(isl));
    }

    /**
     * Load the given bundle file
     *
     * @param bundleFile The bundle
     * @param setFiles This is a list, which may be null, of datasource patterns and file names to change
     *
     * @throws Throwable     On badness
     */
    public void loadBundle(String bundleFile, List setFiles) throws Throwable {
        loadBundle(bundleFile, setFiles, -1, -1, "", true);
    }

    /**
     * Load the given bundle file, list of datasets, width and height
     *
     * @param bundleFile The bundle
     * @param setFiles This is a list, which may be null, of datasource patterns and file names to change
     * @param width The width of the display area to use
     * @param height The height of the display are to use
     *
     * @throws Throwable  an exception
     */
    public void loadBundle(String bundleFile, List setFiles, int width, int height) throws Throwable {
        loadBundle(bundleFile, setFiles, width, height, "", true);
    }

    /**
     * Load the given bundle file, list of datasets, width and height
     *
     * @param bundleFile The bundle
     * @param setFiles This is a list, which may be null, of datasource patterns and file names to change
     * @param width The width of the display area to use
     * @param height The height of the display are to use
     * @param times A string of times to use from the bundle file
     * @param clear If false then do not clear out the data sources and displays (which is otherwise the default)
     *
     * @throws Throwable  an exception
     */
    public void loadBundle(String bundleFile, List setFiles, int width, int height, String times, boolean clear)
            throws Throwable {

        StringBuffer extra = new StringBuffer();
        if (setFiles != null) {
            for (int i = 0; i < setFiles.size(); i += 2) {
                String datasource = (String) setFiles.get(i);
                String files = (String) setFiles.get(i + 1);
                if ((datasource != null) && (files != null)) {
                    extra.append(XmlUtil.tag(TAG_SETFILES, XmlUtil.attrs("datasource", datasource, "file", files)));
                }
                extra.append("\n");
            }
        }
        StringBuffer attrs = new StringBuffer();
        attrs.append(" ");
        attrs.append(ATTR_FILE + "=" + quote(bundleFile));
        attrs.append(" ");
        if ((width > 0) && (height > 0)) {
            attrs.append(" ");
            attrs.append(ATTR_WIDTH + "=" + quote("" + width));
            attrs.append(" ");
            attrs.append(ATTR_HEIGHT + "=" + quote("" + height));
            attrs.append(" ");
        }
        if ((times != null) && !times.isEmpty()) {
            attrs.append(" ");
            attrs.append(ATTR_TIMES + "=" + quote("" + times));
        }
        if (!clear) {
            attrs.append(" ");
            attrs.append(ATTR_CLEAR + "=" + quote("false"));
        }

        String xml = "<bundle " + attrs + ">" + extra + "</bundle>";
        System.err.println(xml);
        processTagBundle(makeElement(xml));
    }

    /**
     * Write an Image to the specified file
     *
     * @param image Image to be written
     * @param file Name of output file (may use macros)
     *
     * @throws Exception On badness
     */
    public void writeImageToFile(Image image, String file) throws Exception {
        ImageUtils.writeImageToFile(image, applyMacros(getImageFileName(file)));
    }

    /**
     * Create XML from the input String
     *
     * @param s in the form:  "task arg=val arg2=val; task2 arg3=val"
     *
     * @return <task arg="val" arg2="val"/> <task2 arg3="val" />
     *
     */
    protected static String makeXmlFromString(String s) {
        if ((s == null) || (s.length() == 0)) {
            return "";
        }
        StringTokenizer st = new StringTokenizer(s, ";");
        StringBuffer sb = new StringBuffer();

        while (st.hasMoreTokens()) {
            String so = st.nextToken();
            StringTokenizer sot = new StringTokenizer(so, "=");
            int k = sot.countTokens();

            for (int i = 0; i < k; i++) {

                StringTokenizer sbt = new StringTokenizer(sot.nextToken().trim(), " ");

                if (i == 0) {
                    sb.append("<" + sbt.nextToken().trim());
                }

                int n = sbt.countTokens();
                boolean gotone = false;
                while (n > 1) {
                    if (gotone) {
                        sb.append(" ");
                    }
                    sb.append(sbt.nextToken().trim());
                    n = n - 1;
                    gotone = true;
                }

                // now deal with the last value
                if (sbt.hasMoreTokens()) {
                    if (i != k - 1) {
                        if (gotone) {
                            sb.append("\" " + sbt.nextToken().trim() + "=\"");
                        } else {
                            sb.append(" " + sbt.nextToken().trim() + "=\"");
                        }
                    } else {
                        if (gotone) {
                            sb.append(" " + sbt.nextToken().trim() + "\"");
                        } else {
                            sb.append(sbt.nextToken().trim() + "\"");
                        }
                    }
                }
            }

            sb.append(" />");
        }

        return sb.toString();
    }

    /**
     * Quote a string
     *
     * @param s  the string
     *
     * @return  the quotated string
     */
    private static String quote(String s) {
        return "\"" + s + "\"";
    }

    /**
     * Get the image of the current display and write to file. Image
     * may be modified by the params given in the form:
     *    tag1 arg=val arg2=val2; tag2 arg=val
     * where 'tag' are ISL tags.
     *
     * @param filename Output filename (may be modified by macros)
     * @param params String of parameters
     * @param qual Quality (def=1.0)
     *
     *
     * @throws Exception On badness
     * @throws Throwable On badness
     */
    public void writeImage(String filename, String params, float qual) throws Exception, Throwable {
        String isl = makeXmlFromString(params);
        String xml = XmlUtil.getHeader() + "\n<image file=\"" + filename + "\" quality=\"" + qual + "\">" + isl
                + "</image>";
        captureImage(applyMacros(filename), makeElement(xml));
    }

    /**
     * Get the Image of the current display
     *
     * @return the Image
     *
     * @throws Exception On badness
     */
    public Image getImage() throws Exception {
        //        updateViewManagers();
        logger.trace("trying to get image from viewManagers...");
        List viewManagers = getVMManager().getViewManagers();
        for (int i = 0; i < viewManagers.size(); i++) {
            ViewManager viewManager = (ViewManager) viewManagers.get(i);
            if (!getIdv().getArgsManager().getIsOffScreen()) {
                IdvWindow window = viewManager.getDisplayWindow();
                if (window != null) {
                    window.setLocation(50, 50);
                    viewManager.toFront();
                }
            }
            logger.trace("getting image from viewManager={}", viewManager);
            Image img = viewManager.getMaster().getImage(false);
            logger.trace("result: {}", img);
            return img;
            //            return viewManager.getMaster().getImage(false);
        }
        logger.trace("returning null");
        return null;
    }

    private static final Logger logger = LoggerFactory.getLogger(ImageGenerator.class);

    /**
     * Capture the image
     *
     * @param filename file
     * @param scriptingNode THe node from the isl. Possibly null.
     *
     * @throws Throwable On badness
     */
    private void captureImage(String filename, Element scriptingNode) throws Throwable {

        Hashtable imageProperties = new Hashtable();

        //See if we're in test mode
        if ((scriptingNode != null) && XmlUtil.hasAttribute(scriptingNode, "test")) {
            BufferedImage tmpImage = new BufferedImage(applyMacros(scriptingNode, ATTR_WIDTH, 300),
                    applyMacros(scriptingNode, ATTR_HEIGHT, 300), BufferedImage.TYPE_INT_RGB);
            String loopFilename = applyMacrosFilename(filename);
            lastImage = processImage((BufferedImage) tmpImage, loopFilename, scriptingNode, getAllProperties(),
                    null, imageProperties);
            return;
        }

        List<ViewManager> viewManagers = null;
        if ((scriptingNode != null) && XmlUtil.hasAttribute(scriptingNode, ATTR_DISPLAY)) {
            DisplayControlImpl display = findDisplayControl(scriptingNode);
            if (display == null) {
                throw new IllegalArgumentException("Could not find display:" + XmlUtil.toString(scriptingNode));
            }
            String loopFilename = applyMacrosFilename(filename);
            String what = applyMacros(scriptingNode, ATTR_WHAT, (String) null);

            ViewManager viewManager = display.getViewManagerForCapture(what);
            if (viewManager != null) {
                viewManager.updateDisplayIfNeeded();
                viewManagers = (List<ViewManager>) Misc.newList(viewManager);
            } else {
                logger.trace("calling display control getImage ('{}')...", what);
                lastImage = display.getImage(what);
                logger.trace("result: {}; passing to processImage...", lastImage);
                lastImage = processImage((BufferedImage) lastImage, loopFilename, scriptingNode, getAllProperties(),
                        null, imageProperties);
                logger.trace("result: {}", lastImage);
                return;
            }
        }

        if (viewManagers == null) {
            viewManagers = (List<ViewManager>) getViewManagers(scriptingNode);
        }

        if (viewManagers.size() == 0) {
            debug("No views to capture");
        }
        pushProperties();

        List<Integer> indices;
        boolean hasIndices = XmlUtil.hasAttribute(scriptingNode, ATTR_ANIMATION_INDEX);
        if (hasIndices) {
            indices = StringUtil
                    .parseIntegerListString(XmlUtil.getAttribute(scriptingNode, ATTR_ANIMATION_INDEX, "1"));
        } else {
            indices = CollectionHelpers.arrList();
            indices.add(-1);
        }

        int idx = 0;
        for (int j = 0; j < indices.size(); j++) {
            List<Image> images = new ArrayList<Image>();
            String fname = (indices.size() > 1) ? fixFileName(filename, indices.get(j)) : filename;
            for (int i = 0; i < viewManagers.size(); i++) {
                ViewManager viewManager = (ViewManager) viewManagers.get(i);
                if (viewManager.getAnimation() != null) {
                    // if the indices have been specified, set the animation
                    // to the relevant index. otherwise, simply capture
                    // the current index.
                    if (hasIndices) {
                        viewManager.getAnimation().setCurrent(indices.get(j));
                    }
                }
                putIndex(getProperties(), PROP_VIEWINDEX, idx);
                String name = viewManager.getName();
                if (name == null) {
                    name = "view" + idx;
                }
                getProperties().put(PROP_VIEWNAME, name);
                if (!getIdv().getArgsManager().getIsOffScreen()) {
                    IdvWindow window = viewManager.getDisplayWindow();
                    if (window != null) {
                        window.setLocation(50, 50);
                        viewManager.toFront();
                        //                    Misc.sleep(100);
                    }
                }
                String loopFilename = applyMacrosFilename(fname);
                if (scriptingNode == null) {
                    File imageFile = null;
                    if (loopFilename != null) {
                        imageFile = new File(getImageFileName(loopFilename));
                    }
                    viewManager.writeImage(imageFile, true, false);
                } else if ((loopFilename != null) && ViewManager.isVectorGraphicsFile(loopFilename)) {
                    VectorGraphicsRenderer vectorRenderer = new VectorGraphicsRenderer(viewManager);
                    vectorRenderer.renderTo(loopFilename);
                } else {
                    logger.trace("waiting until displays are 'done'...");
                    IdvUIManager.waitUntilDisplaysAreDone(getIdv().getIdvUIManager(), 1000);
                    logger.trace("done; calling viewManager getImage... (sync={})", !hasIndices);
                    BufferedImage bufferedImage = viewManager.getMaster().getImage(!hasIndices);
                    if (!isNotBlank(bufferedImage)) {
                        boolean success = false;
                        int k;
                        for (k = 0; k < 10; k++) {
                            bufferedImage = viewManager.getMaster().getImage(!hasIndices);
                            if (isNotBlank(bufferedImage)) {
                                success = true;
                                break;
                            }
                        }
                        int width = bufferedImage.getWidth();
                        int height = bufferedImage.getHeight();
                        logger.trace("initial capture was blank; retries: {} success: {}; total iterations: {}", k,
                                success, width * height * k);
                    } else {
                        logger.trace("initial capture worked");
                    }
                    lastImage = bufferedImage;
                    logger.trace("done (next step is processImage); result: {}", lastImage);
                    imageProperties = new Hashtable();
                    lastImage = processImage((BufferedImage) lastImage, loopFilename, scriptingNode,
                            getAllProperties(), viewManager, imageProperties);
                    logger.trace("processImage done; result: {}", lastImage);
                }
                images.add(lastImage);
                idx++;
            }

            boolean combine = XmlUtil.getAttribute(scriptingNode, ATTR_COMBINE, false);
            if (combine) {
                String combineFilename = applyMacrosFilename(filename);
                List<Component> components = new LinkedList<Component>();
                for (ViewManager vm : viewManagers) {
                    components.add(vm.getComponent());
                }
                int cols = 2;
                if (!getIdv().getArgsManager().getIsOffScreen()) {
                    cols = ImageUtils.getColumnCountFromComps(components);
                } else {
                    cols = XmlUtil.getAttribute(scriptingNode, ATTR_COLUMNS, cols);
                }

                if (ViewManager.isVectorGraphicsFile(combineFilename)) {
                    VectorGraphicsRenderer vectorRenderer = new VectorGraphicsRenderer(viewManagers, cols);
                    vectorRenderer.renderTo(combineFilename);
                } else {
                    Image i = ImageUtils.gridImages2(images, 0, Color.GRAY, cols);
                    ImageUtils.writeImageToFile(i, combineFilename);
                }
            }
        }

        popProperties();
    }

    private static boolean isNotBlank(BufferedImage image) {
        // yes, i know this is bonkers. yes, the next step is to try sampling
        // to avoid iterating over each pixel.
        // tests on my linux machine typically find a non-blank pixel within the
        // first 100 iterations, and fewer than 5 retries to get a non-blank
        // *image* (typically 1-2 retries though)
        DataBuffer buf = image.getRaster().getDataBuffer();
        ColorModel model = image.getColorModel();
        boolean result = false;
        int i;
        for (i = 0; i < buf.getSize(); i++) {
            // it's apparently not sufficient to simply grab the value directly;
            // Linux seems to store the "blank" value as -16777216 (-2^24, which
            // makes a lot of sense for images), while on OS X the blank value
            // is simply 0. i suspect the getRGB stuff is a candidate for
            // inlining by the JIT, but profiling is needed.
            int rgb = model.getRGB(buf.getElem(i));
            if (rgb != -16777216) {
                logger.trace("found non-blank value '{}' at index {}", rgb, i);
                result = true;
                break;
            }
        }
        logger.trace("{} iterations to return {}", i, result);
        return result;
    }

    /**
     * Fixing file name for animation indices.
     *
     * @param filename
     * @param integer
     *
     * @return The file name with an appended index.
     */
    private String fixFileName(String filename, Integer integer) {
        String[] tokens = filename.split("\\.(?=[^\\.]+$)");
        return tokens[0] + integer + "." + tokens[1];
    }

    /**
     * Resize the image
     *
     * @param image The image
     * @param node Node to process. This may contain a width or a height attribute.
     *
     * @return The resized image
     */
    protected Image resize(Image image, Element node) {
        int imageWidth = image.getWidth(null);
        int imageHeight = image.getHeight(null);
        int width = -1;
        int height = -1;
        if (XmlUtil.hasAttribute(node, ATTR_WIDTH)) {
            width = (int) toDouble(node, ATTR_WIDTH, imageWidth);
        }
        if (XmlUtil.hasAttribute(node, ATTR_HEIGHT)) {
            height = (int) toDouble(node, ATTR_HEIGHT, imageWidth);
        }
        if ((width == -1) && (height == -1)) {
            return image;
        }

        return image.getScaledInstance(width, height, Image.SCALE_AREA_AVERAGING);
    }

    /**
     * Resize the image
     *
     * @param image The image
     * @param widthStr width of desired image (pixels)
     * @param heightStr height of desired image (pixels)
     *
     * @return The resized image
     */
    public BufferedImage resizeImage(BufferedImage image, String widthStr, String heightStr) {
        int imageWidth = image.getWidth(null);
        int imageHeight = image.getHeight(null);
        int width = -1;
        int height = -1;
        if (!widthStr.equals("-1")) {
            width = (int) toDouble(widthStr, imageWidth);
        }
        if (!heightStr.equals("-1")) {
            height = (int) toDouble(heightStr, imageHeight);
        }
        if ((width == -1) && (height == -1)) {
            return image;
        }

        BufferedImage resizedImage = ImageUtils.toBufferedImage(
                image.getScaledInstance(width, height, Image.SCALE_AREA_AVERAGING), BufferedImage.TYPE_INT_RGB);
        return resizedImage;

    }

    /**
     * Matte the image
     *
     * @param image The image
     * @param bgString color for the matte ("red", "green", etc)
     * @param top number of lines for the top (north) matte
     * @param left number of pixels for the left (west) matte
     * @param bottom number of lines for the bottom (south) matte
     * @param right number of pixels for the right (east) matte
     *
     * @return The matte'd image
     */
    public BufferedImage matteImage(BufferedImage image, String bgString, int top, int left, int bottom,
            int right) {
        Color bg = GuiUtils.decodeColor(bgString, (Color) null);
        return ImageUtils.matte(image, top, bottom, left, right, bg);
    }

    /**
     * Process the image
     *
     * @param image The image
     * @param filename File to write the image to
     * @param node Node to process
     * @param props Extra properties
     * @param viewManager The viewmanager this image came from
     * @param imageProps  the image properties
     *
     *
     * @return The processed image
     * @throws Throwable On badness
     */
    protected BufferedImage processImage(BufferedImage image, String filename, Element node, Hashtable props,
            ViewManager viewManager, Hashtable imageProps) throws Throwable {

        if (node == null) {
            return image;
        }

        if (props == null) {
            props = new Hashtable();
        }
        if (viewManager != null) {
            Animation animation = viewManager.getAnimation();
            props.put(PROP_ANIMATIONTIME, "");
            if (animation != null) {
                if (animation.getAniValue() != null) {
                    props.put(PROP_ANIMATIONTIME, animation.getAniValue());
                }
            }
        }
        getProperties().putAll(props);

        NodeList elements = XmlUtil.getElements(node);
        Hashtable seenColorTable = new Hashtable();
        for (int childIdx = 0; childIdx < elements.getLength(); childIdx++) {
            boolean shouldIterateChildren = true;
            BufferedImage newImage = null;
            int imageWidth = image.getWidth(null);
            int imageHeight = image.getHeight(null);
            Element child = (Element) elements.item(childIdx);
            String tagName = child.getTagName();

            if (tagName.equals(TAG_RESIZE)) {
                newImage = ImageUtils.toBufferedImage(resize(image, child));
            } else if (tagName.equals(TAG_FILESET)) {
                //ignore
            } else if (tagName.equals(TAG_OUTPUT)) {
                processTagOutput(child);
            } else if (tagName.equals(TAG_DISPLAYLIST)) {
                if (viewManager != null) {
                    newImage = ImageUtils.toBufferedImage(image, true);
                    Graphics g = newImage.getGraphics();
                    String valign = applyMacros(child, ATTR_VALIGN, VALUE_BOTTOM);
                    Font font = getFont(child);
                    if (XmlUtil.hasAttribute(child, ATTR_MATTEBG)) {
                        int height = viewManager.paintDisplayList((Graphics2D) g, null, imageWidth, imageHeight,
                                valign.equals(VALUE_BOTTOM), null, font);

                        int top = (valign.equals(VALUE_TOP) ? height : 0);
                        int bottom = (valign.equals(VALUE_BOTTOM) ? height : 0);
                        newImage = ImageUtils.matte(image, top, bottom, 0, 0,
                                applyMacros(child, ATTR_MATTEBG, Color.white));
                        g = newImage.getGraphics();
                        imageHeight += height;
                    }

                    Color c = applyMacros(child, ATTR_COLOR, (Color) null);
                    viewManager.paintDisplayList((Graphics2D) g, null, imageWidth, imageHeight,
                            valign.equals(VALUE_BOTTOM), c, font);
                }
            } else if (tagName.equals(TAG_COLORBAR) || tagName.equals(TAG_KML_COLORBAR)) {
                // only do one colorbar if we are writing to kml
                Integer index = (Integer) props.get(PROP_IMAGEINDEX);
                if ((index != null) && (index.intValue() > 0) && tagName.equals(TAG_KML_COLORBAR)) {
                    continue;
                }

                boolean showLines = applyMacros(child, ATTR_SHOWLINES, false);

                List<DisplayControlImpl> controls = (List<DisplayControlImpl>) ((viewManager != null)
                        ? viewManager.getControls()
                        : new ArrayList());

                if (XmlUtil.hasAttribute(child, ATTR_DISPLAY)) {
                    DisplayControlImpl display = ((controls.size() > 0)
                            ? findDisplayControl(XmlUtil.getAttribute(child, ATTR_DISPLAY), controls)
                            : findDisplayControl(child));
                    if (display == null) {
                        error("Could not find display:" + XmlUtil.toString(node));
                        return null;
                    }
                    controls = Misc.newList(display);
                }

                int width = applyMacros(child, ATTR_WIDTH, 150);
                int height = applyMacros(child, ATTR_HEIGHT, 20);
                int ticks = applyMacros(child, ATTR_TICKMARKS, 0);
                double interval = applyMacros(child, ATTR_INTERVAL, -1.0);
                String valuesStr = applyMacros(child, ATTR_VALUES, (String) null);
                Color c = applyMacros(child, ATTR_COLOR, Color.black);

                Color lineColor = applyMacros(child, ATTR_LINECOLOR, c);

                Rectangle imageRect = new Rectangle(0, 0, imageWidth, imageHeight);

                Point pp = ImageUtils.parsePoint(applyMacros(child, ATTR_PLACE, "ll,10,-10"), imageRect);
                Point ap = ImageUtils.parsePoint(applyMacros(child, ATTR_ANCHOR, "ll"),
                        new Rectangle(0, 0, width, height));

                String orientation = applyMacros(child, ATTR_ORIENTATION, VALUE_BOTTOM);
                boolean vertical = orientation.equals(VALUE_RIGHT) || orientation.equals(VALUE_LEFT);
                int baseY = pp.y - ap.y + (vertical ? 0 : height);
                int baseX = pp.x - ap.x;

                List colorTables = new ArrayList();
                List ranges = new ArrayList();
                List units = new ArrayList();

                boolean forKml = tagName.equals(TAG_KML_COLORBAR);

                for (int i = 0; i < controls.size(); i++) {
                    DisplayControlImpl control = (DisplayControlImpl) controls.get(i);
                    ColorTable colorTable = control.getColorTable();
                    if (colorTable == null) {
                        continue;
                    }
                    Range range = control.getRangeForColorTable();
                    //only do unique color tables
                    Object[] key = { colorTable, range };
                    if (seenColorTable.get(key) != null) {
                        continue;
                    }
                    seenColorTable.put(key, key);
                    colorTables.add(colorTable);
                    ranges.add(range);
                    units.add(control.getDisplayUnit());
                }

                for (int i = 0; i < colorTables.size(); i++) {
                    ColorTable colorTable = (ColorTable) colorTables.get(i);
                    Range range = (Range) ranges.get(i);
                    Unit unit = (Unit) units.get(i);
                    Image imageToDrawIn;
                    if (forKml) {
                        if (vertical) {
                            baseX = 0;
                            baseY = 0;
                        } else {
                            baseX = 0;
                            baseY = height;
                        }
                        int space = applyMacros(child, ATTR_SPACE, (vertical ? width : height));
                        imageToDrawIn = new BufferedImage(width + (vertical ? space : 0),
                                height + (vertical ? 0 : space), BufferedImage.TYPE_INT_RGB);
                    } else {
                        imageToDrawIn = newImage = ImageUtils.toBufferedImage(image);
                    }
                    Graphics g = imageToDrawIn.getGraphics();
                    if (forKml) {
                        Color bgColor = applyMacros(child, ATTR_BACKGROUND, Color.white);
                        g.setColor(bgColor);
                        g.fillRect(0, 0, imageToDrawIn.getWidth(null), imageToDrawIn.getHeight(null));
                    }
                    boolean includeAlpha = applyMacros(child, ATTR_TRANSPARENCY, true);

                    float[][] ctValues;

                    if (includeAlpha) {
                        ctValues = colorTable.getAlphaTable();
                    } else {
                        ctValues = colorTable.getNonAlphaTable();
                    }
                    ColorMap colorMap = new BaseRGBMap(ctValues);
                    ColorPreview preview = new ColorPreview(colorMap, (vertical ? width : height));
                    if (vertical) {
                        preview.setSize(new Dimension(height, width));
                    } else {
                        preview.setSize(new Dimension(width, height));
                    }
                    Image previewImage = ColorTableCanvas.getImage(colorTable, (vertical ? height : width),
                            (vertical ? width : height), includeAlpha);

                    if (vertical) {
                        int imageType = includeAlpha ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB;

                        BufferedImage tmpImage = new BufferedImage(width, height, imageType);

                        Graphics2D tmpG = (Graphics2D) tmpImage.getGraphics();
                        tmpG.rotate(Math.toRadians(90.0));
                        tmpG.drawImage(previewImage, 0, 0 - width, null);
                        previewImage = tmpImage;
                    }
                    if (forKml) {
                        g.drawImage(previewImage, 0, 0, null);
                    } else {
                        g.drawImage(previewImage, baseX, (vertical ? baseY : baseY - height), null);
                    }
                    if (showLines) {
                        g.setColor(lineColor);
                        g.drawRect(baseX, (vertical ? baseY : baseY - height), width - 1,
                                height - (vertical ? 1 : 0));
                    }
                    setFont(g, child);
                    FontMetrics fm = g.getFontMetrics();
                    List values = new ArrayList();
                    String suffixFrequency = XmlUtil.getAttribute(child, ATTR_SUFFIXFREQUENCY,
                            XmlUtil.getAttribute(child, ATTR_SHOWUNIT, "false")).toLowerCase();
                    String unitDefault = (!suffixFrequency.equals("false")) ? " %unit%" : "";
                    String labelSuffix = applyMacros(child, ATTR_SUFFIX, unitDefault);
                    if (unit != null) {
                        labelSuffix = labelSuffix.replace("%unit%", "" + unit);
                    } else {
                        labelSuffix = labelSuffix.replace("%unit%", "");
                    }
                    if (valuesStr != null) {
                        double[] valueArray = Misc.parseDoubles(valuesStr, ",");
                        for (int valueIdx = 0; valueIdx < valueArray.length; valueIdx++) {
                            values.add(new Double(valueArray[valueIdx]));
                        }
                    } else if (ticks > 0) {
                        int spacing = ((ticks == 1) ? 0 : (vertical ? height : width) / (ticks - 1));
                        for (int tickIdx = 0; tickIdx < ticks; tickIdx++) {
                            double percent = ((ticks > 1) ? (double) tickIdx / (double) (ticks - 1) : 0.0);
                            values.add(new Double(range.getValueOfPercent(percent)));
                        }
                    } else if (interval > 0) {
                        double value = range.getMin();
                        double max = range.getMax();
                        while (value <= max) {
                            values.add(new Double(value));
                            value += interval;
                        }
                    }
                    for (int valueIdx = 0; valueIdx < values.size(); valueIdx++) {
                        double value = ((Double) values.get(valueIdx)).doubleValue();
                        int x;
                        int y;
                        if (vertical) {
                            if (orientation.equals(VALUE_RIGHT)) {
                                x = baseX + width;
                            } else {
                                x = baseX;
                            }
                            y = baseY + (int) (range.getPercent(value) * height);
                            if (y > baseY + height) {
                                break;
                            }
                        } else {
                            if (orientation.equals(VALUE_BOTTOM)) {
                                y = baseY;
                            } else {
                                y = baseY - height;
                            }

                            if (range != null) {
                                x = baseX + (int) (range.getPercent(value) * width);
                            } else {
                                x = baseX;
                            }

                            if (x > baseX + width) {
                                break;
                            }
                        }
                        String tickLabel = getIdv().getDisplayConventions().format(value);
                        if (suffixFrequency.equals(VALUE_LAST) && (valueIdx == values.size() - 1)) {
                            tickLabel += labelSuffix;
                        } else if (suffixFrequency.equals(VALUE_FIRST) && (valueIdx == 0)) {
                            tickLabel += labelSuffix;
                        } else if (suffixFrequency.equals(VALUE_ALL) || suffixFrequency.equals("true")) {
                            tickLabel += labelSuffix;
                        }

                        Rectangle2D rect = fm.getStringBounds(tickLabel, g);
                        g.setColor(lineColor);
                        if (orientation.equals(VALUE_RIGHT)) {
                            g.drawLine(x + 1, y, x, y);
                            if (showLines) {
                                g.drawLine(x, y, x - width, y);
                            }
                        } else if (orientation.equals(VALUE_LEFT)) {
                            g.drawLine(x - 1, y, x, y);
                            if (showLines) {
                                g.drawLine(x, y, x + width, y);
                            }
                        } else if (orientation.equals(VALUE_BOTTOM)) {
                            g.drawLine(x, y + 1, x, y);
                            if (showLines) {
                                g.drawLine(x, y, x, y - height);
                            }
                        } else {
                            g.drawLine(x, y - 1, x, y);
                            if (showLines) {
                                g.drawLine(x, y, x, y + height);
                            }
                        }
                        g.setColor(c);
                        if (orientation.equals(VALUE_RIGHT)) {
                            int yLoc = y + (int) (rect.getHeight() / 2) - 2;
                            if (forKml) {
                                if (valueIdx == 0) {
                                    yLoc = y + (int) (rect.getHeight()) - 2;
                                } else if (valueIdx == values.size() - 1) {
                                    yLoc = y - (int) (rect.getHeight()) + 6;
                                }
                            }
                            g.drawString(tickLabel, x + 2, yLoc);
                        } else if (orientation.equals(VALUE_LEFT)) {
                            int xLoc = x - 2 - (int) rect.getWidth();
                            g.drawString(tickLabel, xLoc, y + (int) (rect.getHeight() / 2) - 2);
                        } else if (orientation.equals(VALUE_BOTTOM)) {
                            int xLoc = x - (int) (rect.getWidth() / 2);
                            if (forKml) {
                                if (valueIdx == 0) {
                                    xLoc = x + 2;
                                } else if (valueIdx == values.size() - 1) {
                                    xLoc = x - (int) rect.getWidth() + 2;
                                }
                            }
                            g.drawString(tickLabel, xLoc, y + (int) rect.getHeight() + 2);
                        } else {
                            g.drawString(tickLabel, x - (int) (rect.getWidth() / 2), y - 2);
                        }
                    }
                    if (vertical) {
                        baseX += width + 30;
                    } else {
                        baseY += height + 30;
                    }
                    if (forKml) {
                        String tmpImageFile = applyMacros(child, ATTR_FILE,
                                getIdv().getStore().getTmpFile("testcolorbar${viewindex}.png"));
                        String template = "<ScreenOverlay><name>${kml.name}</name><Icon><href>${icon}</href></Icon>\n"
                                + "<overlayXY x=\"${kml.overlayXY.x}\" y=\"${kml.overlayXY.y}\" xunits=\"${kml.overlayXY.xunits}\" yunits=\"${kml.overlayXY.yunits}\"/>\n"
                                + "<screenXY x=\"${kml.screenXY.x}\" y=\"${kml.screenXY.y}\" xunits=\"${kml.screenXY.xunits}\" yunits=\"${kml.screenXY.yunits}\"/>\n"
                                + "<size x=\"${kml.size.x}\" y=\"${kml.size.y}\" xunits=\"${kml.size.xunits}\" yunits=\"${kml.size.yunits}\"/>\n"
                                + "</ScreenOverlay>\n";
                        String[] macros = { "kml.name", "kml.overlayXY.x", "kml.overlayXY.y",
                                "kml.overlayXY.xunits", "kml.overlayXY.yunits", "kml.screenXY.x", "kml.screenXY.y",
                                "kml.screenXY.xunits", "kml.screenXY.yunits", "kml.size.x", "kml.size.y",
                                "kml.size.xunits", "kml.size.yunits" };
                        String[] macroValues = { "", "0", "1", "fraction", "fraction", "0", "1", "fraction",
                                "fraction", "-1", "-1", "pixels", "pixels" };

                        for (int macroIdx = 0; macroIdx < macros.length; macroIdx++) {
                            template = template.replace("${" + macros[macroIdx] + "}",
                                    applyMacros(child, macros[macroIdx], macroValues[macroIdx]));
                        }
                        template = template.replace("${icon}", IOUtil.getFileTail(tmpImageFile));
                        imageProps.put("kml", template);
                        List kmlFiles = (List) imageProps.get("kmlfiles");
                        //TODO: Only do the first one for now
                        if (kmlFiles == null) {
                            kmlFiles = new ArrayList();
                            imageProps.put("kmlfiles", kmlFiles);
                        }
                        kmlFiles.add(tmpImageFile);

                        //                        System.out.println(template);
                        ImageUtils.writeImageToFile(imageToDrawIn, tmpImageFile);
                    }
                }

            } else if (tagName.equals(TAG_TRANSPARENT) || tagName.equals(TAG_BGTRANSPARENT)) {
                Color c = null;
                if (tagName.equals(TAG_BGTRANSPARENT)) {
                    c = viewManager.getBackground();
                } else {
                    c = applyMacros(child, ATTR_COLOR, (Color) null);
                }
                //                System.err.println ("c:" + c);
                int[] redRange = { 0, 0 };
                int[] greenRange = { 0, 0 };
                int[] blueRange = { 0, 0 };
                if (c != null) {
                    //                    System.err.println("got color");
                    redRange[0] = redRange[1] = c.getRed();
                    greenRange[0] = greenRange[1] = c.getGreen();
                    blueRange[0] = blueRange[1] = c.getBlue();
                } else {
                }
                newImage = ImageUtils.makeColorTransparent(image, redRange, greenRange, blueRange);
            } else if (tagName.equals(TAG_SHOW)) {
                JComponent contents = new JLabel(new ImageIcon(image));
                String message = applyMacros(child, ATTR_MESSAGE, (String) null);
                if (message != null) {
                    contents = GuiUtils.topCenter(new JLabel(message), contents);
                }
                if (!GuiUtils.askOkCancel("Continue?", contents)) {
                    throw new MyQuitException();
                }
            } else if (tagName.equals(TAG_MATTE)) {
                newImage = doMatte(image, child, 0);
            } else if (tagName.equals(TAG_LATLONLABELS)) {
                newImage = doLatLonLabels(child, viewManager, image, imageProps);
            } else if (tagName.equals(TAG_WRITE)) {
                ImageUtils.writeImageToFile(image, getImageFileName(applyMacros(child, ATTR_FILE)));

            } else if (tagName.equals(TAG_PUBLISH)) {
                getIdv().getPublishManager().publishIslImage(this, node, image);
            } else if (tagName.equals(TAG_CLIP)) {
                int[] ul;
                int[] lr;
                if (XmlUtil.hasAttribute(child, ATTR_DISPLAY)) {
                    //                    System.err.println("Clipping from display");
                    DisplayControlImpl dc = findDisplayControl(child);
                    if (dc == null) {
                        throw new IllegalArgumentException("Could not find display:" + XmlUtil.toString(node));
                    }
                    NavigatedDisplay display = (NavigatedDisplay) viewManager.getMaster();
                    MapProjection mapProjection = dc.getDataProjection();
                    java.awt.geom.Rectangle2D rect = mapProjection.getDefaultMapArea();
                    LatLonPoint llplr = mapProjection.getLatLon(new double[][] { { rect.getX() + rect.getWidth() },
                            { rect.getY() + rect.getHeight() } });
                    LatLonPoint llpul = mapProjection
                            .getLatLon(new double[][] { { rect.getX() }, { rect.getY() } });
                    EarthLocation ulEl = new EarthLocationTuple(llpul, new Real(RealType.Altitude, 0));
                    EarthLocation lrEl = new EarthLocationTuple(llplr, new Real(RealType.Altitude, 0));
                    ul = display.getScreenCoordinates(display.getSpatialCoordinates(ulEl, null));
                    lr = display.getScreenCoordinates(display.getSpatialCoordinates(lrEl, null));
                    //System.err.println("ul:" + ulEl + " lr:" + lrEl);
                    if (ul[0] > lr[0]) {
                        int tmp = ul[0];
                        ul[0] = lr[0];
                        lr[0] = tmp;
                    }
                    if (ul[1] > lr[1]) {
                        int tmp = ul[1];
                        ul[1] = lr[1];
                        lr[1] = tmp;
                    }
                    imageProps.put(ATTR_NORTH, new Double(ulEl.getLatitude().getValue()));
                    imageProps.put(ATTR_WEST, new Double(ulEl.getLongitude().getValue()));
                    imageProps.put(ATTR_SOUTH, new Double(lrEl.getLatitude().getValue()));
                    imageProps.put(ATTR_EAST, new Double(lrEl.getLongitude().getValue()));
                } else if ((viewManager != null) && XmlUtil.hasAttribute(child, ATTR_NORTH)) {
                    NavigatedDisplay display = (NavigatedDisplay) viewManager.getMaster();
                    EarthLocation el1 = DisplayControlImpl.makeEarthLocation(toDouble(child, ATTR_NORTH),
                            toDouble(child, ATTR_WEST), 0);
                    EarthLocation el2 = DisplayControlImpl.makeEarthLocation(toDouble(child, ATTR_SOUTH),
                            toDouble(child, ATTR_EAST), 0);
                    ul = display.getScreenCoordinates(display.getSpatialCoordinates(el1, null));
                    lr = display.getScreenCoordinates(display.getSpatialCoordinates(el2, null));
                    imageProps.put(ATTR_NORTH, new Double(el1.getLatitude().getValue()));
                    imageProps.put(ATTR_WEST, new Double(el1.getLongitude().getValue()));
                    imageProps.put(ATTR_SOUTH, new Double(el2.getLatitude().getValue()));
                    imageProps.put(ATTR_EAST, new Double(el2.getLongitude().getValue()));
                } else if (XmlUtil.hasAttribute(child, ATTR_LEFT)) {
                    ul = new int[] { (int) toDouble(child, ATTR_LEFT, imageWidth),
                            (int) toDouble(child, ATTR_TOP, imageHeight) };
                    lr = new int[] { (int) toDouble(child, ATTR_RIGHT, imageWidth),
                            (int) toDouble(child, ATTR_BOTTOM, imageHeight) };
                } else if (viewManager != null) {
                    //TODO: Clip on visad coordinates
                    NavigatedDisplay display = (NavigatedDisplay) viewManager.getMaster();
                    ul = display.getScreenCoordinates(new double[] { -1, 1, 0 });
                    lr = display.getScreenCoordinates(new double[] { 1, -1, 0 });
                    int space = applyMacros(child, ATTR_SPACE, 0);
                    int hspace = applyMacros(child, ATTR_HSPACE, space);
                    int vspace = applyMacros(child, ATTR_VSPACE, space);
                    ul[0] -= applyMacros(child, ATTR_SPACE_LEFT, hspace);
                    ul[1] -= applyMacros(child, ATTR_SPACE_TOP, vspace);
                    lr[0] += applyMacros(child, ATTR_SPACE_RIGHT, hspace);
                    lr[1] += applyMacros(child, ATTR_SPACE_BOTTOM, vspace);
                } else {
                    continue;
                }

                for (String attr : (List<String>) Misc.newList(ATTR_NORTH, ATTR_SOUTH, ATTR_EAST, ATTR_WEST)) {
                    String kmlAttr = "kml." + attr;
                    if (XmlUtil.hasAttribute(child, kmlAttr)) {
                        imageProps.put(attr, new Double(applyMacros(child, kmlAttr, 0.0)));
                    }
                }

                ul[0] = Math.max(0, ul[0]);
                ul[1] = Math.max(0, ul[1]);

                lr[0] = Math.min(lr[0], imageWidth);
                lr[1] = Math.min(lr[1], imageHeight);

                newImage = ImageUtils.clip(image, ul, lr);
            } else if (tagName.equals(TAG_SPLIT)) {
                shouldIterateChildren = false;
                int width = image.getWidth(null);
                int height = image.getHeight(null);
                int cols = applyMacros(child, ATTR_COLUMNS, 2);
                int rows = applyMacros(child, ATTR_ROWS, 2);
                String file = applyMacros(child, ATTR_FILE);
                int cnt = 0;
                int hSpace = width / cols;
                int vSpace = height / rows;
                for (int row = 0; row < rows; row++) {
                    for (int col = 0; col < cols; col++) {
                        pushProperties();
                        Hashtable myprops = new Hashtable();
                        putProperty("row", new Integer(row));
                        putProperty("column", new Integer(col));
                        putProperty("count", new Integer(++cnt));
                        String realFile = applyMacros(file, myprops);
                        Image splitImage = image.getSubimage(hSpace * col, vSpace * row, hSpace, vSpace);
                        processImage(ImageUtils.toBufferedImage(splitImage), realFile, child, myprops, viewManager,
                                new Hashtable());
                        popProperties();
                    }
                }
            } else if (tagName.equals(TAG_THUMBNAIL)) {
                shouldIterateChildren = false;
                BufferedImage thumbImage = ImageUtils.toBufferedImage(resize(image, child));
                String thumbFile = applyMacros(child, ATTR_FILE, (String) null);
                if (thumbFile == null) {
                    thumbFile = IOUtil.stripExtension(filename) + "_thumb" + IOUtil.getFileExtension(filename);
                }
                processImage(thumbImage, thumbFile, child, null, viewManager, new Hashtable());
            } else if (tagName.equals(TAG_KML)) {
                //NOOP
            } else if (tagName.equals(TAG_KMZFILE)) {
                //NOOP
            } else if (tagName.equals(TAG_OVERLAY)) {
                double transparency = applyMacros(child, ATTR_TRANSPARENCY, 0.0);
                Graphics2D g = image.createGraphics();
                String imagePath = applyMacros(child, ATTR_IMAGE, (String) null);

                float scale = (float) applyMacros(child, ATTR_SCALE, 1.0);

                Rectangle imageRect = new Rectangle(0, 0, imageWidth, imageHeight);
                Point pp = ImageUtils.parsePoint(applyMacros(child, ATTR_PLACE, "lr,-10,-10"), imageRect);
                String text = applyMacros(child, ATTR_TEXT, (String) null);
                Color bg = applyMacros(child, ATTR_BACKGROUND, (Color) null);
                if (text != null) {
                    double angle = Math.toRadians(applyMacros(child, ATTR_ANGLE, 0.0));
                    text = applyMacros(text);
                    Color c = applyMacros(child, ATTR_COLOR, Color.white);
                    if ((c != null) && (transparency > 0)) {
                        c = new Color(c.getRed(), c.getGreen(), c.getBlue(), ImageUtils.toAlpha(transparency));
                    }
                    //Color bg = applyMacros(child, ATTR_BACKGROUND,
                    //                       (Color) null);
                    if ((bg != null) && (transparency > 0)) {
                        bg = new Color(bg.getRed(), bg.getGreen(), bg.getBlue(), ImageUtils.toAlpha(transparency));
                    }
                    setFont(g, child);
                    FontMetrics fm = g.getFontMetrics();
                    Rectangle2D rect = fm.getStringBounds(text, g);
                    int width = (int) rect.getWidth();
                    int height = (int) (rect.getHeight());

                    Point ap = ImageUtils.parsePoint(applyMacros(child, ATTR_ANCHOR, "lr,-10,-10"),
                            new Rectangle(0, 0, width, height));

                    g.rotate(angle);

                    if (bg != null) {
                        g.setColor(bg);
                        g.fillRect(pp.x - ap.x - 1, pp.y - ap.y - 1, (int) width + 2, (int) height + 2);
                    }
                    g.setColor(c);
                    g.drawString(text, pp.x - ap.x, pp.y - ap.y + height);
                }

                if (imagePath != null) {
                    Image overlay = ImageUtils.readImage(imagePath);
                    if (overlay != null) {
                        if (transparency > 0) {
                            overlay = ImageUtils.setAlpha(overlay, transparency);
                        }

                        int width = overlay.getWidth(null);
                        int height = overlay.getHeight(null);
                        int scaledWidth = Math.round(width * scale);
                        int scaledHeight = Math.round(height * scale);

                        Image scaled = getScaledImage(overlay, scaledWidth, scaledHeight);
                        Rectangle overlayRect = new Rectangle(0, 0, scaledWidth, scaledHeight);
                        Point ap = ImageUtils.parsePoint(applyMacros(child, ATTR_ANCHOR, "lr,-10,-10"),
                                overlayRect);
                        g.drawImage(scaled, pp.x - ap.x, pp.y - ap.y, bg, null);
                    }
                }
            } else {
                error("Unknown tag:" + tagName);
            }
            if (newImage != null) {
                String newFileName = applyMacros(child, ATTR_FILE, (String) null);
                if (shouldIterateChildren) {
                    logger.trace("newFileName='{}' viewManager={} newImage={}", newFileName, viewManager, newImage);
                    newImage = processImage(newImage, newFileName, child, null, viewManager, new Hashtable());
                    logger.trace("finished processImage; result: {}", newImage);
                }
                if (newFileName != null) {
                    logger.trace("calling writeImageToFile...");
                    ImageUtils.writeImageToFile(newImage, getImageFileName(newFileName));
                    logger.trace("finished writeImageToFile");
                    debug("Writing image:" + newFileName);
                }
                if (!applyMacros(child, ATTR_COPY, false)) {
                    image = newImage;
                }
            }
        }

        if (filename != null) {
            float quality = (float) applyMacros(node, ATTR_QUALITY, 1.0);
            List<String> fileToks = StringUtil.split(filename, ",", true, true);
            for (String file : fileToks) {
                file = getImageFileName(file);
                debug("Writing image:" + file);
                if (file.endsWith(FileManager.SUFFIX_KMZ) || file.endsWith(FileManager.SUFFIX_KML)) {
                    GeoLocationInfo bounds = null;
                    if (viewManager != null) {
                        bounds = viewManager.getVisibleGeoBounds();
                        ImageSequenceGrabber.subsetBounds(bounds, imageProps);
                        String tmpImageFile = getOutputPath(file);
                        ImageUtils.writeImageToFile(image, tmpImageFile, quality);
                        ImageWrapper imageWrapper = new ImageWrapper(tmpImageFile, null, bounds, null);
                        imageWrapper.setProperties(imageProps);
                        new ImageSequenceGrabber(file, getIdv(), this, node,
                                (List<ImageWrapper>) Misc.newList(imageWrapper), null, 1);
                    }
                } else {
                    logger.trace("another writeImageToFile call...");
                    ImageUtils.writeImageToFile(image, file, quality);
                    logger.trace("and it's done.");
                }
            }
        }
        logger.trace("result: {}", image);
        return image;
    }

    /**
     * Scale the given {@code source} {@link Image}.
     *
     * @param source Source image.
     * @param width New width.
     * @param height New height.
     *
     * @return Scaled {@code source} image (uses bilinear interpolation).
     */
    public static BufferedImage getScaledImage(Image source, int width, int height) {
        // convert the given Image into a BufferedImage if needed--makes things a
        // little easier.
        BufferedImage image;
        if (source instanceof BufferedImage) {
            image = (BufferedImage) source;
        } else {
            image = new BufferedImage(source.getWidth(null), source.getHeight(null), BufferedImage.TYPE_INT_ARGB);
            Graphics2D g = image.createGraphics();
            g.drawImage(source, 0, 0, null);
            g.dispose();
        }

        int imageWidth = image.getWidth();
        int imageHeight = image.getHeight();
        double scaleX = (double) width / imageWidth;
        double scaleY = (double) height / imageHeight;
        AffineTransform scaleTransform = AffineTransform.getScaleInstance(scaleX, scaleY);
        BufferedImageOp op = new AffineTransformOp(scaleTransform, AffineTransformOp.TYPE_BILINEAR);
        return op.filter(image, new BufferedImage(width, height, image.getType()));
    }

    /**
     * Perform macro substitution, much like {@link #applyMacros(String)}. The
     * difference is that this method reverts the strange replacement of
     * {@code \\n} with {@code \n}, and so it is safe to use with Windows file
     * paths.
     *
     * @param filename {@code String} that potentially contains macros.
     *
     * @return The expanded {@code String}.
     */
    private String applyMacrosFilename(String filename) {
        return applyMacros(filename).replace("\n", "\\n");
    }

    /**
     * Return the path to use for KML or KMZ output.
     *
     * @param file Path to KML or KMZ file. Cannot be {@code null}.
     *
     * @return If {@code file} is a KML file, the path will be a PNG file
     * within the same directory. Otherwise the path will be a PNG file in a
     * temporary directory generated by
     * {@link ucar.unidata.xml.XmlObjectStore#getUniqueTmpDirectory getUniqueTmpDirectory}.
     */
    private String getOutputPath(String file) {
        String root = IOUtil.getFileRoot(file);
        String tail = IOUtil.getFileTail(file);
        Path tmpPath = file.endsWith(FileManager.SUFFIX_KML) ? Paths.get(root, tail + ".png")
                : getStore().getUniqueTmpDirectory().toPath().resolve(tail + ".png");
        return tmpPath.toString();
    }

    /**
     * Get the insets
     *
     * @param child  the element
     * @param dflt   the default value
     *
     * @return the Insets
     */
    public Insets getInsets(Element child, int dflt) {
        int space = applyMacros(child, ATTR_SPACE, dflt);
        int hspace = applyMacros(child, ATTR_HSPACE, space);
        int vspace = applyMacros(child, ATTR_VSPACE, space);
        int top = applyMacros(child, ATTR_TOP, vspace);
        int bottom = applyMacros(child, ATTR_BOTTOM, vspace);
        int left = applyMacros(child, ATTR_LEFT, hspace);
        int right = applyMacros(child, ATTR_RIGHT, hspace);
        return new Insets(top, left, bottom, right);
    }

    /**
     * Process the lat/lon labels tag
     *
     * @param child  the XML
     * @param viewManager  the associated view manager
     * @param image  the image to draw on
     * @param imageProps  the image properties
     *
     * @return  a new image
     *
     * @throws Exception  on badness
     */
    public BufferedImage doLatLonLabels(Element child, ViewManager viewManager, BufferedImage image,
            Hashtable imageProps) throws Exception {

        if (viewManager == null) {
            throw new IllegalArgumentException("Tag " + TAG_LATLONLABELS + " requires a view");
        }
        if (!(viewManager instanceof MapViewManager)) {
            throw new IllegalArgumentException("Tag " + TAG_LATLONLABELS + " requires a map view");
        }
        MapViewManager mvm = (MapViewManager) viewManager;
        NavigatedDisplay display = (NavigatedDisplay) viewManager.getMaster();
        DecimalFormat format = new DecimalFormat(applyMacros(child, ATTR_FORMAT, "##0.0"));
        Color color = applyMacros(child, ATTR_COLOR, Color.red);
        Color lineColor = applyMacros(child, ATTR_LINECOLOR, color);
        Color bg = applyMacros(child, ATTR_LABELBACKGROUND, (Color) null);

        double[] latValues = Misc.parseDoubles(applyMacros(child, ATTR_LAT_VALUES, ""));
        List<String> latLabels = StringUtil.split(applyMacros(child, ATTR_LAT_LABELS, ""), ",", true, true);
        double[] lonValues = Misc.parseDoubles(applyMacros(child, ATTR_LON_VALUES, ""));
        List<String> lonLabels = StringUtil.split(applyMacros(child, ATTR_LON_LABELS, ""), ",", true, true);

        boolean drawLonLines = applyMacros(child, ATTR_DRAWLONLINES, false);
        boolean drawLatLines = applyMacros(child, ATTR_DRAWLATLINES, false);
        boolean showTop = applyMacros(child, ATTR_SHOWTOP, false);
        boolean showBottom = applyMacros(child, ATTR_SHOWBOTTOM, true);
        boolean showLeft = applyMacros(child, ATTR_SHOWLEFT, true);
        boolean showRight = applyMacros(child, ATTR_SHOWRIGHT, false);

        int width = image.getWidth(null);
        int height = image.getHeight(null);
        int centerX = width / 2;
        int centerY = height / 2;
        EarthLocation nw, ne, se, sw;

        //don: this  what I added
        Double north = (Double) imageProps.get(ATTR_NORTH);
        Double south = (Double) imageProps.get(ATTR_SOUTH);
        Double east = (Double) imageProps.get(ATTR_EAST);
        Double west = (Double) imageProps.get(ATTR_WEST);
        //Assume if we have one we have them all
        if (north != null) {
            nw = DisplayControlImpl.makeEarthLocation(north.doubleValue(), west.doubleValue(), 0);

            ne = DisplayControlImpl.makeEarthLocation(north.doubleValue(), east.doubleValue(), 0);
            sw = DisplayControlImpl.makeEarthLocation(south.doubleValue(), west.doubleValue(), 0);
            se = DisplayControlImpl.makeEarthLocation(south.doubleValue(), east.doubleValue(), 0);

        } else {
            nw = display.screenToEarthLocation(0, 0);
            ne = display.screenToEarthLocation(width, 0);
            se = display.screenToEarthLocation(0, height);
            sw = display.screenToEarthLocation(width, height);
        }

        double widthDegrees = ne.getLongitude().getValue() - nw.getLongitude().getValue();
        double heightDegrees = ne.getLatitude().getValue() - se.getLatitude().getValue();

        Insets insets = getInsets(child, 0);
        int delta = 2;
        int bgPad = 1;

        image = doMatte(image, child, 0);

        Graphics2D g = (Graphics2D) image.getGraphics();
        g.setFont(getFont(child));
        FontMetrics fm = g.getFontMetrics();

        int lineOffsetRight = applyMacros(child, ATTR_LINEOFFSET_RIGHT, 0);
        int lineOffsetLeft = applyMacros(child, ATTR_LINEOFFSET_LEFT, 0);
        int lineOffsetTop = applyMacros(child, ATTR_LINEOFFSET_TOP, 0);
        int lineOffsetBottom = applyMacros(child, ATTR_LINEOFFSET_BOTTOM, 0);

        Stroke lineStroke;
        if (XmlUtil.hasAttribute(child, ATTR_DASHES)) {
            lineStroke = new BasicStroke((float) applyMacros(child, ATTR_LINEWIDTH, 1.0), BasicStroke.CAP_BUTT,
                    BasicStroke.JOIN_BEVEL, 1.0f, Misc.parseFloats(applyMacros(child, ATTR_DASHES, "8")), 0.0f);
        } else {
            lineStroke = new BasicStroke((float) applyMacros(child, ATTR_LINEWIDTH, 1.0));
        }

        g.setStroke(lineStroke);
        double leftLon = nw.getLongitude().getValue(CommonUnit.degree);
        double rightLon = ne.getLongitude().getValue(CommonUnit.degree);
        Range lonRange = new Range(leftLon, rightLon);

        for (int i = 0; i < lonValues.length; i++) {
            double lon = GeoUtils.normalizeLongitude(lonRange, lonValues[i]);
            double percent = (lon - nw.getLongitude().getValue()) / widthDegrees;
            //            if(percent<0 || percent>1) continue;
            String label;
            if (i < lonLabels.size()) {
                label = lonLabels.get(i);
            } else {
                label = format.format(lonValues[i]);
            }
            Rectangle2D rect = fm.getStringBounds(label, g);
            int baseX = insets.left + (int) (percent * width);
            int x = baseX - (int) rect.getWidth() / 2;
            int topY;
            if (insets.top == 0) {
                topY = (int) rect.getHeight() + delta;
            } else {
                topY = insets.top - delta;
            }
            if (drawLonLines) {
                g.setColor(lineColor);
                g.drawLine(baseX, insets.top + lineOffsetTop, baseX, insets.top + height - lineOffsetBottom);
            }

            if (showTop) {
                if (bg != null) {
                    g.setColor(bg);
                    g.fillRect(x - bgPad, topY - (int) rect.getHeight() - bgPad, (int) rect.getWidth() + bgPad * 2,
                            (int) rect.getHeight() + bgPad * 2);
                }
                g.setColor(color);
                g.drawString(label, x, topY);
            }
            int bottomY;
            if (insets.bottom == 0) {
                bottomY = insets.top + height - delta;
            } else {
                bottomY = insets.top + height + (int) rect.getHeight() + delta;
            }
            if (showBottom) {
                if (bg != null) {
                    g.setColor(bg);
                    g.fillRect(x - bgPad, bottomY - (int) rect.getHeight() - bgPad,
                            (int) rect.getWidth() + bgPad * 2, (int) rect.getHeight() + bgPad * 2);
                }
                g.setColor(color);
                g.drawString(label, x, bottomY);
            }
        }

        for (int i = 0; i < latValues.length; i++) {
            double lat = latValues[i];
            double percent = 1.0 - (lat - se.getLatitude().getValue()) / heightDegrees;
            int baseY = insets.top + (int) (percent * height);
            //            if(percent<0 || percent>1) continue;
            String label;
            if (i < latLabels.size()) {
                label = latLabels.get(i);
            } else {
                label = format.format(lat);
            }
            Rectangle2D rect = fm.getStringBounds(label, g);
            int y = baseY + (int) rect.getHeight() / 2;
            int leftX;
            if (insets.left == 0) {
                leftX = 0 + delta;
            } else {
                leftX = insets.left - (int) rect.getWidth() - delta;
            }
            if (drawLonLines) {
                g.setColor(lineColor);
                g.drawLine(insets.left + lineOffsetRight, baseY, insets.left + width - lineOffsetRight, baseY);
            }

            if (showLeft) {
                if (bg != null) {
                    g.setColor(bg);
                    g.fillRect(leftX - bgPad, y - (int) rect.getHeight() - bgPad, (int) rect.getWidth() + bgPad * 2,
                            (int) rect.getHeight() + bgPad * 2);
                }
                g.setColor(color);
                g.drawString(label, leftX, y);
            }

            if (insets.right == 0) {
                leftX = insets.left + width - (int) rect.getWidth() - delta;
            } else {
                leftX = insets.left + width + delta;
            }
            if (showRight) {
                if (bg != null) {
                    g.setColor(bg);
                    g.fillRect(leftX - bgPad, y - (int) rect.getHeight() - bgPad, (int) rect.getWidth() + bgPad * 2,
                            (int) rect.getHeight() + bgPad * 2);
                }
                g.setColor(color);
                g.drawString(label, leftX, y);
            }
        }

        return image;

    }

    /**
     * Matte the image
     *
     * @param image  the image
     * @param child  the XML defining the matting
     * @param dfltSpace  default spacing
     *
     * @return a new image
     */
    public BufferedImage doMatte(BufferedImage image, Element child, int dfltSpace) {
        return doMatte(image, child, getInsets(child, dfltSpace));
    }

    /**
     * Matte the image
     *
     * @param image  the image
     * @param child  the matte specs
     * @param insets the insets
     *
     * @return  a new image
     */
    public BufferedImage doMatte(BufferedImage image, Element child, Insets insets) {
        Color bg = applyMacros(child, ATTR_BACKGROUND, Color.white);
        return ImageUtils.matte(image, insets.top, insets.bottom, insets.left, insets.right, bg);
    }

    /*
      public void setDataSourceFiles(String[] datasource, String[] filenames) {
        
      List dataSources = getIdv().getDataSources();
    for (int k = 0; k < datasource.size(); k++) {
      for (int i = 0; i < dataSources.size(); i++) {
        DataSource theDataSource = (DataSource) dataSources.get(i);
        if (theDataSource.identifiedByName(datasource[k])) {
          theDataSource.setNewFiles(new ArrayList().add(filenames[k]));
        }
      }
    }
      }
        
     */

    /**
     * Get the file name to write images to. If we are in test mode then prepend the test directory
     *
     * @param filename image file name
     *
     * @return filename to use
     */
    private String getImageFileName(String filename) {
        if (LogUtil.getTestMode()) {
            if (getIdv().getArgsManager().testDir != null) {
                filename = IOUtil.joinDir(getIdv().getArgsManager().testDir, filename);
            }
        }
        return filename;
    }

    /**
     * Set the font on the graphics from the font defined on the node.
     *
     * @param g The graphics
     * @param node Node to get font info from
     */
    private void setFont(Graphics g, Element node) {
        int fontSize = applyMacros(node, ATTR_FONTSIZE, 12);
        Font f = new Font(applyMacros(node, ATTR_FONTFACE, "dialog"), Font.PLAIN, fontSize);
        g.setFont(f);
    }

    /**
     * Get the font from the XML
     *
     * @param node  the XML
     *
     * @return  the font or null
     */
    private Font getFont(Element node) {
        if (XmlUtil.hasAttribute(node, ATTR_FONTSIZE) || XmlUtil.hasAttribute(node, ATTR_FONTFACE)) {
            int fontSize = applyMacros(node, ATTR_FONTSIZE, 12);
            return new Font(applyMacros(node, ATTR_FONTFACE, "dialog"), Font.PLAIN, fontSize);
        }
        return null;
    }

    /**
     * Called to notify this object that the movie capture is done
     */
    public synchronized void doneCapturingMovie() {
        this.notify();
    }

    /**
     * Capture a movie from the first view manager
     *
     * @param filename The movie  filename
     */
    public synchronized void captureMovie(String filename) {
        captureMovie(filename, null);
    }

    /**
     * Capture the movie
     *
     * @param filename The file
     * @param scriptingNode Node form isl.
     */
    public synchronized void captureMovie(String filename, Element scriptingNode) {
        captureMovie(filename, false, scriptingNode);
    }

    /**
     * Capture the movie
     *
     * @param filename The file
     * @param globalPalette Whether or not animated GIF output should use a
     * {@literal "global"} color palette.
     * @param scriptingNode Node form isl.
     */
    public synchronized void captureMovie(String filename, boolean globalPalette, Element scriptingNode) {

        if ((filename == null) && (scriptingNode != null)) {
            filename = XmlUtil.getAttribute(scriptingNode, ATTR_FILE);
        }

        if (scriptingNode != null) {
            List files = findFiles(scriptingNode);
            if (files != null) {
                debug("Making movie from existing images " + filename);
                logger.trace("#1: creating '{}'", filename);
                filename = applyMacros(filename);
                Dimension size = new Dimension(applyMacros(scriptingNode, ATTR_WIDTH, 400),
                        applyMacros(scriptingNode, ATTR_HEIGHT, 300));
                ImageSequenceGrabber isg = new ImageSequenceGrabber(filename, getIdv(), this, scriptingNode, files,
                        size, applyMacros(scriptingNode, ATTR_FRAMERATE, 2),
                        applyMacros(scriptingNode, ATTR_ENDFRAMEPAUSE, -1));
                return;
            }
        }

        List<ViewManager> viewManagers = null;
        if ((scriptingNode != null) && XmlUtil.hasAttribute(scriptingNode, ATTR_DISPLAY)) {
            DisplayControlImpl display = findDisplayControl(scriptingNode);
            if (display == null) {
                throw new IllegalArgumentException("Could not find display:" + XmlUtil.toString(scriptingNode));
            }
            String what = applyMacros(scriptingNode, ATTR_WHAT, (String) null);

            ViewManager viewManager = null;
            try {
                viewManager = display.getViewManagerForCapture(what);
                if (viewManager != null) {
                    viewManager.updateDisplayIfNeeded();
                }
            } catch (Exception exc) {
                throw new RuntimeException(exc);
            }

            if (viewManager != null) {
                viewManagers = (List<ViewManager>) Misc.newList(viewManager);
            } else {
                throw new IllegalArgumentException(
                        "Cannot capture a movie with display:" + XmlUtil.toString(scriptingNode));
            }
        }

        if (viewManagers == null) {
            viewManagers = (List<ViewManager>) getViewManagers(scriptingNode);
        }

        boolean combine = XmlUtil.getAttribute(scriptingNode, ImageGenerator.ATTR_COMBINE, false);

        if (combine) {
            ViewManager viewManager = getIdv().getVMManager().getLastActiveViewManager();

            getProperties().put(PROP_VIEWINDEX, new Integer(0));
            String name = viewManager.getName();
            if (name == null) {
                name = "view" + 0;
            }
            getProperties().put(PROP_VIEWNAME, name);

            if (!getIdv().getArgsManager().getIsOffScreen()) {
                JFrame frame = GuiUtils.getFrame(viewManager.getContents());
                if (frame != null) {
                    LogUtil.registerWindow(frame);
                    frame.setVisible(true);
                    GuiUtils.toFront(frame);
                    frame.setLocation(50, 50);
                    Misc.sleep(50);
                }
            }
            String loopFilename = applyMacros(filename);
            debug("Making movie:" + loopFilename);
            logger.trace("#2: creating '{}'", filename);
            ImageSequenceGrabber isg = new ImageSequenceGrabber(viewManager, loopFilename, globalPalette, getIdv(),
                    this, scriptingNode);
            try {
                wait();
            } catch (Exception exc) {
                logException("Doing the captureMovie wait", exc);
            }
            debug("Done making movie:" + loopFilename);

        } else {

            for (int i = 0; i < viewManagers.size(); i++) {
                ViewManager viewManager = viewManagers.get(i);

                getProperties().put(PROP_VIEWINDEX, new Integer(i));
                String name = viewManager.getName();
                if (name == null) {
                    name = "view" + i;
                }
                getProperties().put(PROP_VIEWNAME, name);

                if (!getIdv().getArgsManager().getIsOffScreen()) {
                    JFrame frame = GuiUtils.getFrame(viewManager.getContents());
                    if (frame != null) {
                        LogUtil.registerWindow(frame);
                        frame.setVisible(true);
                        GuiUtils.toFront(frame);
                        frame.setLocation(50, 50);
                        Misc.sleep(50);
                    }
                }
                String loopFilename = applyMacros(filename);
                debug("Making movie:" + loopFilename);
                logger.trace("#3: creating '{}'", filename);
                ImageSequenceGrabber isg = new ImageSequenceGrabber(viewManager, loopFilename, globalPalette,
                        getIdv(), this, scriptingNode);
                try {
                    wait();
                } catch (Exception exc) {
                    logException("Doing the captureMovie wait", exc);
                }
                debug("Done making movie:" + loopFilename);
            }
        }

    }

    /**
     * Find the animation time of the first Animation in a view manager we find
     *
     * @return Animation time
     */
    public Date getAnimationTime() {
        List vms = getViewManagers(currentNode);
        if (vms.size() > 0) {
            ViewManager vm = (ViewManager) vms.get(0);
            Animation animation = vm.getAnimation();
            if (animation != null) {
                Real v = animation.getAniValue();
                if (v != null) {
                    return new Date((long) v.getValue() * 1000);
                }

            }
        }
        return new Date(Misc.getCurrentTime());
    }

    /**
     * Create and instantiate the jython interp.
     *
     * @return The interp
     */
    private PythonInterpreter getInterpreter() {
        if (interpreter == null) {
            interpreter = getIdv().getJythonManager().createInterpreter();
            interpreter.set("ig", this);
            interpreter.set("interp", this);
            interpreter.set("islInterpreter", this);
        }
        return interpreter;
    }

    /**
     * callable by jython to find the data choices that match the given pattern
     *
     * @param datasource data source
     * @param pattern pattern to match
     *
     * @return comma separated list of data choice names
     */
    public String fields(String datasource, String pattern) {
        DataSource dataSource = findDataSource(datasource);
        if (dataSource == null) {
            throw new IllegalArgumentException("Could not find data source:" + datasource);
        }
        List choices;
        if ((pattern == null) || (pattern.length() == 0)) {
            choices = dataSource.getDataChoices();
        } else {
            choices = dataSource.findDataChoices(pattern);
        }

        List names = new ArrayList();
        for (int i = 0; i < choices.size(); i++) {
            DataChoice dataChoice = (DataChoice) choices.get(i);
            names.add(dataChoice.getName());
        }
        return StringUtil.join(",", names);
    }

    /**
     * Class OutputInfo is used for handling output tags
     *
     *
     * @author IDV Development Team
     * @version $Revision: 1.113 $
     */
    private class OutputInfo {

        /** The node */
        Element outputNode;

        /** mapping of where to StringBuffer */
        Hashtable buffers = new Hashtable();

        /** mapping of where to templates */
        Hashtable templates = new Hashtable();

        /**
         * ctor
         *
         * @param node The output node
         */
        public OutputInfo(Element node) {
            this.outputNode = node;
        }

        /**
         * Handle the node.
         *
         * @param node Node to process
         *
         * @throws Throwable On badness
         */
        public void process(Element node) throws Throwable {
            String where = applyMacros(node, ATTR_TEMPLATE, "contents");
            StringBuffer sb = (StringBuffer) buffers.get(where);
            String template = (String) templates.get(where);
            if (sb == null) {
                sb = new StringBuffer();
                template = XmlUtil.getAttribute(outputNode, ATTR_TEMPLATE + ":" + where, "${text}");
                if (template.startsWith("file:")) {
                    template = applyMacros(template);
                    template = IOUtil.readContents(template.substring(5));
                }
                buffers.put(where, sb);
                templates.put(where, template);
            }
            String text = XmlUtil.getAttribute(node, ATTR_TEXT, (String) null);
            if (text == null) {
                if (XmlUtil.hasAttribute(node, ATTR_FROMFILE)) {
                    String filename = applyMacros(node, ATTR_FROMFILE);
                    text = applyMacros(IOUtil.readContents(filename));
                } else {
                    text = XmlUtil.getChildText(node);
                    if ((text != null) && (text.length() == 0)) {
                        text = null;
                    }
                }
            }
            if (text == null) {
                NamedNodeMap nnm = node.getAttributes();
                Hashtable props = new Hashtable();
                if (nnm != null) {
                    for (int i = 0; i < nnm.getLength(); i++) {
                        Attr attr = (Attr) nnm.item(i);
                        if (!ATTR_TEMPLATE.equals(attr.getNodeName())) {
                            props.put(attr.getNodeName(), applyMacros(attr.getNodeValue()));
                        }
                    }
                }
                text = applyMacros(template, props);
            } else {
                text = applyMacros(text);
            }
            sb.append(text);
        }

        /**
         * Write out the output
         *
         * @throws Throwable On badness
         */
        public void write() throws Throwable {
            String outputFile = applyMacros(outputNode, ATTR_FILE);
            String template = applyMacros(outputNode, ATTR_TEMPLATE, (String) null);
            if (template == null) {
                template = "${contents}";
            }
            if (template.startsWith("file:")) {
                template = IOUtil.readContents(template.substring(5));
            }
            for (Enumeration keys = buffers.keys(); keys.hasMoreElements();) {
                String key = (String) keys.nextElement();
                StringBuffer buff = (StringBuffer) buffers.get(key);
                template = applyMacros(template, Misc.newHashtable(key, buff.toString()));
            }
            IOUtil.writeFile(outputFile, template);
        }

    }

    /**
     * Print out a wanring message
     *
     * @param msg message
     */
    private void warning(String msg) {
        System.err.println(new Date() + " WARNING:" + msg);
    }

    /**
     * Print the message if in debug mode
     *
     * @param msg The message
     */
    protected void debug(String msg) {
        if (debug) {
            System.out.println(new Date() + ": " + msg);
        }
    }

    /**
     * Class MyBreakException for handling break tags
     *
     *
     * @author IDV Development Team
     * @version $Revision: 1.113 $
     */
    protected static class MyBreakException extends Exception {
    }

    /**
     * Class MyContinueException for handling continue tags
     *
     *
     * @author IDV Development Team
     * @version $Revision: 1.113 $
     */
    protected static class MyContinueException extends Exception {
    }

    /**
     * Class MyReturnException allows us to return from a isl procedure by throwing an exception.
     * Yes, I know you're not supposed to use exceptions in a non-exceptional way but it works
     *
     *
     * @author IDV Development Team
     * @version $Revision: 1.113 $
     */
    protected static class MyReturnException extends Exception {
    }

    /**
     * Class description
     *
     *
     * @version        Enter version here..., Tue, Jan 12, '10
     * @author         Enter your name here...
     */
    protected static class MyQuitException extends Exception {
    }

    /**
     * Class BadIslException is used to handle bad isl errors
     *
     *
     * @author IDV Development Team
     * @version $Revision: 1.113 $
     */
    private static class BadIslException extends RuntimeException {

        /** message */
        String msg;

        /**
         * ctor
         *
         * @param msg error message
         */
        public BadIslException(String msg) {
            this.msg = msg;
        }

        /**
         * to string
         *
         * @return error message
         */
        public String toString() {
            return msg;
        }

    }

    /**
     * IS the FtpClient in an ok state. If it isn't then disconnect it and throw and IllegalStateException
     *
     * @param f Ftp client
     * @param msg Message to use if in error
     *
     * @throws Exception On badness
     */
    private static void checkFtp(FTPClient f, String msg) throws Exception {
        int replyCode = f.getReplyCode();
        if (!FTPReply.isPositiveCompletion(replyCode)) {
            String reply = f.getReplyString();
            f.disconnect();
            throw new IllegalStateException("Error with ftp: " + replyCode + " " + msg + "\n" + reply);
        }
    }

    /**
     * Do an FTP put of the given bytes
     *
     * @param server server
     * @param userName user name on server
     * @param password password on server
     * @param destination Where to put the bytes
     * @param bytes The bytes
     *
     * @throws Exception On badness
     */
    public static void ftpPut(String server, String userName, String password, String destination, byte[] bytes)
            throws Exception {
        FTPClient f = new FTPClient();

        f.connect(server);
        f.login(userName, password);
        f.setFileType(FTP.BINARY_FILE_TYPE);
        f.enterLocalPassiveMode();
        checkFtp(f, "Connecting to ftp server");
        f.storeFile(destination, new ByteArrayInputStream(bytes));
        checkFtp(f, "Storing file");
        f.logout();
        f.disconnect();
    }

    /** abcdefg... */
    private static String[] alphabet = { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o",
            "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z" };

    /** roman numerals */
    private static String[] roman = { "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII",
            "XIII", "XIV", "XV", "XVI", "XVII", "XVIII", "XX", "XXI", "XXII", "XXIII", "XXIV", "XXV", "XXVI",
            "XXVII", "XXVIII" };

    /**
     * Get the letter for the index
     *
     * @param i  the index
     *
     * @return  the letter
     */
    public String getLetter(int i) {
        if ((i >= 0) && (i < alphabet.length)) {
            return alphabet[i];
        }
        //A hack for now
        return "out of range";

    }

    /**
     * Get the roman numeral
     *
     * @param i the index
     *
     * @return  the corresponding number
     */
    public String getRoman(int i) {
        if ((i >= 0) && (i < roman.length)) {
            return roman[i];
        }
        //A hack for now
        return "out of range";
    }

}