package vnt;
/**
 * VascularNetworkToolkit.java - v1.0
 * Began: June 30, 2005
 * Last Updated: July 28, 2005
 * 
 * Copyright (C) 2005 - Michael D. Miller - mdm162@truman.edu
 * Truman State University
 * 100 E. Normal
 * Kirksville, MO - 63501
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 */

import ij.*;
import ij.gui.*;
import ij.measure.Calibration;
import java.awt.*;
import java.awt.Color.*;
import ij.plugin.filter.PlugInFilter;
import ij.process.*;
import java.io.*;
import ij.io.*;
import java.lang.String.*;
import java.lang.Math.*;
import java.util.Stack;

import ij.plugin.filter.*;
/**
 * <p>Holds a variety of commonly used methods for all
 * plug-ins under this toolkit for ImageJ.</p>
 *
 * <p>This class is meant to assist in the development of related
 * plug-ins for the toolkit. Modification of this class
 * will affect (and possibly break) all other plug-ins in the package.</p>
 *  
 * @author Michael Miller - Truman State University
 * @version 1.0
 * @since 1.0
 */
class VascularNetworkToolkit {

    /**********************************************
     * Inherited variables for global functionality
     **********************************************/
    
    /**
     * The full title filename of the image given to the plug-in.
     * Generally initialized by getFileInformation().
     * @see #getFileInformation
     */
    protected String name;
    /**
     * The filename of the original image for processing.
     * Identifying prefixes are removed.
     * Generally initialized by getFileInformation().
     * @see #getFileInformation
     */
    protected String originalName;
    /**
     * The folder information for the original image given to the plug-in.
     * Generally initialized by getFileInformation().
     * @see #getFileInformation
     */
    protected String directory;
    /**
     * The dimensional information for the original image given to the plug-in. Needed to be
     * Generally initialized by getDimensions().
     * @see #getDimensions
     */
    protected int width, height;
    /**
     * A two-dimensional integer array storing the 8-bit grayscale pixel values of the image. Expected values are between 0 and 255. A pixel is described by pixel[x][y] == color. 
     */
    protected int [][] pixel;
    /**
     * The ImagePlus for the image. Holds, among other things, the filename and folder information.
     * Generally initialized by getFileInformation().
     * @see #getFileInformation
     */
    protected ImagePlus imp;
    /**
     * The calibation information for the image. Holds pixels->real world units information.
     * If the image already contains this information, it can be extracted from the ImagePlus.
     * This is set once getFileInformation() is called. Priority is as follows:
     * 1) If the existing image's calibration information exists, it is used.
     * 2) ElseIf a global settings file exists, its calibration information is used.  
     * 3) Else imageCalibration is left null, and no special calibrations are saved.  
     * @see #getFileInformation
     * @see #getLocalSettings
     */
    protected Calibration imageCalibration;
    /**
     * Used for calibration information. A measured distance in pixels. 
     * @see #setCalibrationInformation
     */
    protected double distanceInPixels;
    /**
     * Used for calibration information. A measured distance in units. 
     * @see #setCalibrationInformation
     */
    protected double distanceInUnits;
    /**
     * The units per pixel: distanceInUnits / distanceInPixels.
     * Use this value when adjusting for real-world measurements.
     * @see #setCalibrationInformation
     */
    protected double distanceInUnitsPerPixel;
    /**
     * Used for calibration information. The image's real-world aspect ratio. This is usually expressed as width divided by height or x:y. 
     * @see #setCalibrationInformation
     */
    protected double imageAspectRatio;
    /**
     * Used for calibration information. The real-world unit of measurement's string name. 
     * @see #setCalibrationInformation
     */
    protected String distanceUnitName;
    
    /**
     * A toggle to draw and update renderings live while generating them.
     * This generally creates an animated, more visually engaging display.
     * Drastically slows down the processing time.
     * @see #getLocalSettings
     */
    protected boolean animatedDisplay = false;
    /**
     * A toggle whether to include the original image in the eyecandy presentation image or not.
     * The default value is true unless specified in a local settings.txt file. 
     * @see #getLocalSettings
     */
    protected boolean generateEyeCandyWithOriginalImage = true;
    /**
     * A toggle whether to perform the initial lighting correction process (true) or to skip it (false).
     * The default value is true unless specified in a local settings.txt file. 
     * @see #getLocalSettings
     */
    protected boolean performPolynomialLightingCorrection = false;
    /**
     * A toggle whether to include the lighting correction surface in the eye candy. This only occurs if lighting correction is performed and an eyecandy image is generated.
     * @see performLightingCorrection
     * @see performEyeCandy
     * @see #getLocalSettings
     */
    protected boolean generateLightingCorrectionSurfaceEyeCandy = false;
    /**
     * Assuming lighting correction will be performed, this is the order of the polynomial that the lighting correction surface will be fitted to it.
     * @see performLightingCorrection
     * @see #getLocalSettings
     */
    protected int lightingCorrectionPolynomialOrder = 6;
    /**
     * Assuming lighting correction will be performed, this is the largest color value which will be treated as equal to black.
     * The default value is -1. This means a variable cutoff will be calculated based off the histogram of the image (as specified in LightingCorrection.java). If a non-negative value is given, it is used as the maximum constant value associated with the color black.
     * @see performLightingCorrection
     * @see #getLocalSettings
     */
    protected int lightingCorrectionCutoff = -1;
    /**
     * A toggle whether to perform the final "eye candy" process (true) or to skip it (false). 
     * The default value is true unless specified in a local settings.txt file. 
     * @see #getLocalSettings
     */
    protected boolean performEyeCandy = true;
    /**
     * A toggle whether to use High/Low Segmentation or to use Find Edges/Variance segmentation.
     * The default value is true unless specified in local settings.txt file.
     * @see #getLocalSettings
     */
    protected boolean useHighLowSegmentation = false;   
    /**
     * If Find Edges/Variances segmentation is being used, then usually an image will contain holes.
     * An algorithm has been created to close these artifacts up to a certain limit. Specify the 
     * maximum area of the holes to fill here.
     * @see fillSegmentedSmallHoles
     * @see #getLocalSettings
     */
    protected int maximumAreaToIgnoreMesh = 100;
    /**
     * A toggle whether to keep temporary image files (true) or delete them (false).
     * The default value is true unless specified in a local settings.txt file. 
     * @see #getLocalSettings
     */
    protected boolean keepTemporaryImageFiles = true;
    /**
     * A toggle whether to keep graph text files (true) or delete them (false).
     * The default value is true unless specified in a local settings.txt file. 
     * @see #getLocalSettings
     */
    protected boolean keepGraphTextFiles = true;
    /**
     * A toggle whether to display debug text to the console or not.
     * @see #getLocalSettings
     */
    protected boolean displayDebugText = false;

    /****************************************
     * STATIC CONSTANTS - change with caution
     ****************************************/
    
    /**
     * The integer value ImageJ associates with the color black.
     */
    public static int BLACK = 0;
    /**
     * The a descriptive constant value that should be set to the same value as BLACK.
     */
    public static int WALL = 0;
    /**
     * The a descriptive constant value that should be set to the same value as BLACK.
     */
    public static int DARK = 0;
    /**
     * The integer value ImageJ associates with the middle grayscale color value gray.
     */
    public static int GRAY = 127;
    /**
     * The descriptive constant value that should be set to the same value as GRAY.
     */
    public static int MATCHING = 127;
    /**
     * The integer value ImageJ associates with the color white.
     */
    public static int WHITE = 255;
    /**
     * The a descriptive constant value that should be set to the same value as WHITE.
     */
    public static int LIGHT = 255;

    /**
     * The integer ARGB color value ImageJ associates with the color RED.
     * Format is hexadecimal 0xWWXXYYZZ where WW is the one byte hex alpha channel, XX is the one byte hex red channel, YY is the one byte hex green channel, and ZZ is the one byte hex blue channel.
     */
    public static int RED = 0x00FF0000;
    /**
     * The integer ARGB color value ImageJ associates with the color GREEN.
     * Format is hexadecimal 0xWWXXYYZZ where WW is the one byte hex alpha channel, XX is the one byte hex red channel, YY is the one byte hex green channel, and ZZ is the one byte hex blue channel.
     */
    public static int GREEN = 0x0000FF00;
    /**
     * The integer ARGB color value ImageJ associates with the color BLUE.
     * Format is hexadecimal 0xWWXXYYZZ where WW is the one byte hex alpha channel, XX is the one byte hex red channel, YY is the one byte hex green channel, and ZZ is the one byte hex blue channel.
     */
    public static int BLUE = 0x000000FF;
    /**
     * The integer ARGB color value ImageJ associates with the color YELLOW.
     * Format is hexadecimal 0xWWXXYYZZ where WW is the one byte hex alpha channel, XX is the one byte hex red channel, YY is the one byte hex green channel, and ZZ is the one byte hex blue channel.
     */
    public static int YELLOW = 0x00FFFF00;
    /**
     * The integer ARGB color value ImageJ associates with the color MAGENTA.
     * Format is hexadecimal 0xWWXXYYZZ where WW is the one byte hex alpha channel, XX is the one byte hex red channel, YY is the one byte hex green channel, and ZZ is the one byte hex blue channel.
     */
    public static int MAGENTA = 0x00FF00FF;
    /**
     * The integer ARGB color value ImageJ associates with the color CYAN.
     * Format is hexadecimal 0xWWXXYYZZ where WW is the one byte hex alpha channel, XX is the one byte hex red channel, YY is the one byte hex green channel, and ZZ is the one byte hex blue channel.
     */
    public static int CYAN = 0x0000FFFF;

    /******************************
     * static and inherited methods
     ******************************/
    
    
    /**
     * Initializes the plug-in by storing it's imageplus information.
     * Attempts to access and store the given images filename
     * and local directory information.
     * 
     * <p>Pre: Assumes imageplus is not null. Expects this to be called directly from a properly run plug-in's setup() routine.
     * <br />Post: The image data is parsed for filename and local directory information. If this was obtainable, the method returns true. Otherwise false.
     * @param imageplus The ImagePlus object for the image given.
     * @return Returns true if filename and directory were accessed properly. Returns false if there was any problem.
     * @see #imp
     * @see #name
     * @see #directory
     */
    public boolean getFileInformation(ImagePlus imageplus) {
        // make sure there is an image to analyze
        if (imageplus == null)
            return false;

        // an image exists, set defaults
        name = "";
        originalName = "";
        directory = "";
        distanceInPixels = 1.0;
        distanceInUnits = 1.0;
        distanceInUnitsPerPixel = distanceInUnits / distanceInPixels;
        imageAspectRatio = 1.0;
        distanceUnitName = "pixel";

        imp = imageplus;        
        name = imp.getTitle();

        // get the position of the _
        int position = name.indexOf('_');  // \('.')/  :-)
        if (position == -1) {
            originalName = new String (name); 
        } else {
            originalName = name.substring(position+1);
        }
        
        // attempt to gather calibration information
        // not all images have this
        // note: (if it exists) the image calibration will be used instead of a local settings.txt
        imageCalibration = imp.getCalibration();
        if (imageCalibration != null) {
            // is this image spatially calibrated?
            if (imageCalibration.scaled()) {
                distanceInPixels = 1.0/imageCalibration.pixelWidth;
                distanceInUnits = 1.0;
                distanceInUnitsPerPixel = imageCalibration.pixelWidth;
                imageAspectRatio = imageCalibration.pixelHeight/imageCalibration.pixelWidth;
                distanceUnitName = imageCalibration.getUnit();
            }
        }

        if (imp.getOriginalFileInfo() == null) {
            // no local directory information, quit with fail
            return false;
        }
        directory = imp.getOriginalFileInfo().directory;
        
        // Since we have directory information, grab the local settings.txt file.
        getLocalSettings(directory);        

        return true;
    }
    
    /**
     * Attempts to load the local settings information from the text file 'settings.txt' in the given folder.
     * 
     * <p>Pre: This method should generally only be called from getFileInformation.
     * <br />Post: If a settings file is found, the settings are loaded into the VNT (and any inherited plugins can use this information to act accordingly).
     * @param folder The directory containing this image (and consequently the settings file for it).
     * @return True on successful read, false on failure to read the file. 
     */
    private boolean getLocalSettings(String folder) {
        // if we're here, the original image had no calibration information 
        // hence imageCalibration == null
        if (folder == null) { 
            return false;
        }
        if (folder.length() < 1) {
            return false;
        }
        
        File fileIn = new File(folder,"settings.txt");
        if(fileIn.exists() == false) {
            // global file did not exist! no calibration info to retrieve
            return false;
        }
        // get the data and setup the new calibration with it
        BufferedReader input = null;
        String temp = null, unitName = "";
        String[] tokens = null;
        int i, line = 1;
        double aspectRatio=0, pixelDistance=0, unitDistance=0;
        try{
            input = new BufferedReader(new InputStreamReader(new FileInputStream(fileIn)));
            temp = input.readLine();
            while (temp != null) { // temp == null -> end of file
                // don't look at anything commented
                if (temp.indexOf(';') != -1) {
                    temp = temp.substring(0,temp.indexOf(';'));
                }
                // split into tokens by (whitespace ==== whitespace)
                tokens = temp.split("\\s*=+\\s*");
                i = 0;
                while (i<tokens.length-1) { // make sure at least 2 tokens are left
                    if (tokens[i].equals("PerformPolynomialLightingCorrection")) {
                        if (tokens[i+1].equalsIgnoreCase("true") || tokens[i+1].equalsIgnoreCase("t") || tokens[i+1].equalsIgnoreCase("1")) {
                            performPolynomialLightingCorrection = true;                             
                        } else if (tokens[i+1].equalsIgnoreCase("false") || tokens[i+1].equalsIgnoreCase("f") || tokens[i+1].equalsIgnoreCase("0")) {
                            performPolynomialLightingCorrection = false;
                        }
                    } else if (tokens[i].equals("GenerateEyeCandyWithOriginalImage")) {
                        if (tokens[i+1].equalsIgnoreCase("true") || tokens[i+1].equalsIgnoreCase("t") || tokens[i+1].equalsIgnoreCase("1")) {
                            generateEyeCandyWithOriginalImage = true;                               
                        } else if (tokens[i+1].equalsIgnoreCase("false") || tokens[i+1].equalsIgnoreCase("f") || tokens[i+1].equalsIgnoreCase("0")) {
                            generateEyeCandyWithOriginalImage = false;
                        }
                    } else if (tokens[i].equals("GenerateLightingCorrectionSurfaceEyeCandy")) {
                        if (tokens[i+1].equalsIgnoreCase("true") || tokens[i+1].equalsIgnoreCase("t") || tokens[i+1].equalsIgnoreCase("1")) {
                            generateLightingCorrectionSurfaceEyeCandy = true;                               
                        } else if (tokens[i+1].equalsIgnoreCase("false") || tokens[i+1].equalsIgnoreCase("f") || tokens[i+1].equalsIgnoreCase("0")) {
                            generateLightingCorrectionSurfaceEyeCandy = false;
                        }
                    } else if (tokens[i].equals("LightingCorrectionPolynomialOrder")) {
                        try {
                            lightingCorrectionPolynomialOrder = Integer.valueOf(tokens[i+1].trim()).intValue(); 
                        } catch (Exception e) {
                            // invalid numeric token, don't crash over this
                            System.out.println("Settings.txt file in '"+folder+"' -- an invalid numeric token["+i+"] was read on line "+line+".");
                        }
                    } else if (tokens[i].equals("LightingCorrectionBlackCutoff")) {
                        try {
                            lightingCorrectionCutoff = Integer.valueOf(tokens[i+1].trim()).intValue(); 
                        } catch (Exception e) {
                            // invalid numeric token, don't crash over this
                            System.out.println("Settings.txt file in '"+folder+"' -- an invalid numeric token["+i+"] was read on line "+line+".");
                        }
                    } else if (tokens[i].equals("MaxMeshAreaIgnore")) {
                        try {
                            maximumAreaToIgnoreMesh = Integer.valueOf(tokens[i+1].trim()).intValue(); 
                        } catch (Exception e) {
                            // invalid numeric token, don't crash over this
                            System.out.println("Settings.txt file in '"+folder+"' -- an invalid numeric token["+i+"] was read on line "+line+".");
                        }
                    } else if (tokens[i].equals("GenerateEyeCandy")) {
                        if (tokens[i+1].equalsIgnoreCase("true") || tokens[i+1].equalsIgnoreCase("t") || tokens[i+1].equalsIgnoreCase("1")) {
                            performEyeCandy = true;                             
                        } else if (tokens[i+1].equalsIgnoreCase("false") || tokens[i+1].equalsIgnoreCase("f") || tokens[i+1].equalsIgnoreCase("0")) {
                            performEyeCandy = false;
                        }
                    } else if (tokens[i].equals("KeepTemporaryImageFiles")) {
                        if (tokens[i+1].equalsIgnoreCase("true") || tokens[i+1].equalsIgnoreCase("t") || tokens[i+1].equalsIgnoreCase("1")) {
                            keepTemporaryImageFiles = true;                             
                        } else if (tokens[i+1].equalsIgnoreCase("false") || tokens[i+1].equalsIgnoreCase("f") || tokens[i+1].equalsIgnoreCase("0")) {
                            keepTemporaryImageFiles = false;
                        }
                    } else if (tokens[i].equals("UseHighLowSegmentation")) {
                        if (tokens[i+1].equalsIgnoreCase("true") || tokens[i+1].equalsIgnoreCase("t") || tokens[i+1].equalsIgnoreCase("1")) {
                            useHighLowSegmentation = true;                              
                        } else if (tokens[i+1].equalsIgnoreCase("false") || tokens[i+1].equalsIgnoreCase("f") || tokens[i+1].equalsIgnoreCase("0")) {
                            useHighLowSegmentation = false;
                        }
                    } else if (tokens[i].equals("KeepGraphTextFiles")) {
                        if (tokens[i+1].equalsIgnoreCase("true") || tokens[i+1].equalsIgnoreCase("t") || tokens[i+1].equalsIgnoreCase("1")) {
                            keepGraphTextFiles = true;                              
                        } else if (tokens[i+1].equalsIgnoreCase("false") || tokens[i+1].equalsIgnoreCase("f") || tokens[i+1].equalsIgnoreCase("0")) {
                            keepGraphTextFiles = false;
                        }
                    } else if (tokens[i].equals("DisplayJavaCodeDebugConsoleText")) {
                        if (tokens[i+1].equalsIgnoreCase("true") || tokens[i+1].equalsIgnoreCase("t") || tokens[i+1].equalsIgnoreCase("1")) {
                            displayDebugText = true;                                
                        } else if (tokens[i+1].equalsIgnoreCase("false") || tokens[i+1].equalsIgnoreCase("f") || tokens[i+1].equalsIgnoreCase("0")) {
                            displayDebugText = false;
                        }
                    } else if (tokens[i].equals("PerformAnimatedRendering")) {
                        if (tokens[i+1].equalsIgnoreCase("true") || tokens[i+1].equalsIgnoreCase("t") || tokens[i+1].equalsIgnoreCase("1")) {
                            animatedDisplay = true;                             
                        } else if (tokens[i+1].equalsIgnoreCase("false") || tokens[i+1].equalsIgnoreCase("f") || tokens[i+1].equalsIgnoreCase("0")) {
                            animatedDisplay = false;
                        }
                    } else if (tokens[i].equals("LengthUnitName")) {
                        unitName = tokens[i+1]; 
                    } else if (tokens[i].equals("DistanceInPixels")) {
                        try {
                            pixelDistance = Double.valueOf(tokens[i+1].trim()).doubleValue(); 
                        } catch (Exception e) {
                            // invalid numeric token, don't crash over this
                            System.out.println("Settings.txt file in '"+folder+"' -- an invalid numeric token["+i+"] was read on line "+line+".");
                        }
                    } else if (tokens[i].equals("DistanceInUnits")) {
                        try {
                            unitDistance = Double.valueOf(tokens[i+1].trim()).doubleValue();
                        } catch (Exception e) {
                            // invalid numeric token, don't crash over this
                            System.out.println("Settings.txt file in '"+folder+"' -- an invalid numeric token["+i+"] was read on line "+line+".");
                        }
                    } else if (tokens[i].equals("AspectRatio")) {
                        try {
                            aspectRatio = Double.valueOf(tokens[i+1].trim()).doubleValue();
                        } catch (Exception e) {
                            // invalid numeric token, don't crash over this
                            System.out.println("Settings.txt file in '"+folder+"' -- an invalid numeric token["+i+"] was read on line "+line+".");
                        }
                    }
                    i ++;
                }
                // read another line
                temp = input.readLine();
                line ++;
            }
            // close the setup file
            input.close();
        } catch(Exception e) {
            // didn't work for whatever reason. clean up and return failure.
            System.out.println("Settings.txt file in '"+folder+"' -- an invalid unknown exception has occured. The last line read was "+line+".");
            try {
                if (input != null)
                    input.close();
            } catch (Exception error) {
                // error occured inside an error, we better stop poking around :)
            }
            return false;
        }
        boolean storeThisCalibration = false;
        // only use this if we don't already have an existing calibration
        if (imageCalibration == null) {
            storeThisCalibration = true;
        } else {
            // make sure this image isn't already spatially calibrated
            storeThisCalibration = true;
        }
        if (storeThisCalibration) {
            // double check and properly format the new information and store result
            setCalibrationInformation(pixelDistance, unitDistance, aspectRatio, unitName);
        }
        
        // if we made it this far it worked!
        return true;
    }

    /**
     * Sets the calibration information from the given data.
     * Note: Invalid input will result in the default calibration setting (ImageJ interprets this as no calibration information).
     * 
     * <p>Pre: This method should generally only be called from getLocalSettings.
     * <br />Post: On success, the variable imageCalibration will be filled with a new Calibration. On failure, imageCalibration will be unchanged.
     * @param pixels The distance in pixels.
     * @param units The same distance, but in # of units.
     * @param aspectRatio The aspect ratio of real-world units. Negative values are automatically made positive. An aspect ratio of 0 is interpretted as 1.
     * @param unitName The string containing the name of the unit. Ex: "cm", "microns" etc.
     * @return True on success, false on failure. On true, imageCalibration should have a valid new Calibration.
     */
    protected boolean setCalibrationInformation(double pixels, double units, double aspectRatio, String unitName) {
        if (unitName == null)
            unitName = new String("");
        if (aspectRatio < 0.0)
            aspectRatio = -aspectRatio;
        
        Calibration myCalibration = new Calibration();
        
        if (unitName.equals("um"))
            unitName = IJ.micronSymbol+"m";
        else if (unitName.equals("A"))
            unitName = ""+IJ.angstromSymbol;

        if (pixels <= 0.0 || units <= 0.0 || unitName.startsWith("pixel") || unitName.startsWith("Pixel") || unitName.equals("")) {
            myCalibration.pixelWidth = 1.0;
            myCalibration.pixelHeight = 1.0;
            myCalibration.setUnit("pixel");
        } else {
            myCalibration.pixelWidth = units/pixels;
            if (aspectRatio != 0.0)
                myCalibration.pixelHeight = myCalibration.pixelWidth*aspectRatio;
            else
                myCalibration.pixelHeight = myCalibration.pixelWidth;
            myCalibration.setUnit(unitName);
        }

        // make sure there was actually a change from the current setting
        if (distanceInPixels == pixels && units == distanceInUnits && aspectRatio == imageAspectRatio && distanceUnitName.equals(unitName)) {
            // exactly the same! abort with failure
            return false;
        }
        
        // If we made it this far, it worked! Store values and save the calibration.
        distanceInPixels = pixels;
        distanceInUnits = units;
        if (pixels > 0)
            distanceInUnitsPerPixel = units/pixels;
        else
            distanceInUnitsPerPixel = units;
        imageAspectRatio = aspectRatio;
        distanceUnitName = unitName;
        imageCalibration = myCalibration;
        return true;
    }
    
    /**
     * Saves the currently active window. An underscore ('_') is inserted between
     * the prefix and the original filename.  
     * 
     * <p>Pre: Assumes the desired file to be saved is active. Assumes getFileInformation has already been called and completed successfully.
     * <br />Post: The active window is saved with the folder, prefix, and original filename. The full path is directory_path+prefix+"_"+original_file_name.
     * @param prefix The prefix to save before the beginning of the file. Examples include "lightcorrected" - "segmented" - "skeletonized" - "pruned".
     * @return True on successful save. False if information was missing.
     */
    public boolean saveFile(String prefix) {
        if ((directory == null) || (prefix == null) || (originalName == null)) {
            IJ.showMessage("VNT saveFile() Error", "directory, prefix, or originalName was null.");
            return false; // failure - storage information missing
        }
        if ((directory.length() < 4) || (prefix.length() < 1) || (originalName.length() <= 4)) {
            IJ.showMessage("VNT saveFile() Error", "directory: '"+directory+"', prefix: '"+prefix+"', or originalName: '"+originalName+"' was too short.");
            return false; // failure, storage information invalid (too short)
        }
        String fullpath = directory+prefix+"_"+originalName;
        IJ.save(fullpath);
        return true;
    }

    /**
     * Receives an ImageProcessor for an image and returns
     * the image in the form of a two dimensional integer array.
     *
     * <p>Pre: Accepts a valid ImageProcessor to an image given by ImageJ.
     * <br />Post: The image is loaded into a double integer array. If the given ImageProcessor is null, the array is null. 
     * @param ip The ImageProcessor for the image to load.
     * @return A new int [][] array that holds a copy of the image given.
     */
    public static int[][] LoadImage(ImageProcessor ip) {
        if (ip == null) {
            return null;
        }
        int width = ip.getWidth(), height = ip.getHeight(), x, y;
        int [][] pixel = new int[width][height];
        
        for(y=0; y<height; y++) {
            for(x=0; x<width; x++) {
                pixel[x][y] = ip.getPixel(x,y); 
            }
        }
        return pixel;
        
    }

    /**
     * A partial version of LoadImage(ImageProcessor).
     * Receives a two dimensional integer array, an ImageProcessor for
     * a valid ImageJ image, and rectangle boundary information, and
     * returns the two dimensional integer array with the rectangular
     * region replaced by the corresponding parts of the image.
     *
     * <p>Pre: Accepts a valid ImageProcessor to an image given by ImageJ. The array must be instantiated and the same size as the given image.
     * <br />Post: The image within the boundaries is copied into a double integer array. If the given ImageProcessor is null, the array is null. If the given array is not the same dimensions as the image, the returned array is null. 
     * @param ip The ImageProcessor for the image to load. Contains the region which will be copied over the array.
     * @param data The two dimensional integer array. (The existing picture.) Must be the same size as the given ImageProcessor. Null will also be returned if any of the given coordinates are out of bounds or malformed (left>right, etc.) 
     * @return A new int [][] array that holds a copy of the image given. Null if an error occured.
     */
    public static int[][] LoadImage(ImageProcessor ip, int[][] data, int xLeft, int xRight, int yTop, int yBottom) {
        if (ip == null) {
            return null;
        }
        int width = ip.getWidth(), height = ip.getHeight(), x, y;
        if (data.length != height || data[0].length != width) {
            return null;
        }
        if (xLeft>xRight || yTop>yBottom || xRight > width || yBottom > height) {
            return null;
        }
        
        for(y=yTop; y<=yBottom; y++) {
            for(x=xLeft; x<=xRight; x++) {
                data[x][y] = ip.getPixel(x,y); 
            }
        }
        return data;
    }
    
    /**
     * Gets the width and heigh information of the given image.
     * 
     * <p>Pre: A valid ImageProcessor from an image given from ImageJ is provided.
     * <br />Post: The width and height of the image are stored in local members.
     * @param ip The ImageProcessor for the given image.
     * @see #width
     * @see #height
     */
    public void getDimensions(ImageProcessor ip) {
        if (ip == null) {
            return;
        }
        width = ip.getWidth();
        height = ip.getHeight();
    }

    /**
     * Linearly converts a given amount into a fractional total.
     * Useful for coordinate transforms.
     * 
     * <p>Pre: Assumes total != 0. If it is, this function returns 0.
     * <br />Post: Returns amount/total.
     * @param amount The amount to convert.
     * @param total The total possible amount.
     * @return Returns amount/total, meant for converting coordinates from very large values to basically a percentage% between [0,1].
     */
    public static double convertPercentage(int amount, int total) {
        double top = (double)amount;
        double bottom = (double)total;
        if (bottom == 0) {
            return 0;
        }
        return top/bottom;
    }
   
    /**
     * Displays information about the plug-in through 
     * ImageJ Plug-in's "About" display.
     * 
     * <p>Pre: None.
     * <br />Post: No changes.
     * @param title The title of the About display.
     * @param content The message body of the About display. 
     */
    public void showAbout(String title, String content) {
        IJ.showMessage(title, content);
    }

    /**
     * Draws an outline of a circle on the image.
     * The circle is centered on coordinates (x,y), with radius.
     * The color of the circle is specified as a single integer value. 
     * 
     * <p>Pre: Assumes a valid ImageProcessor from an image given from ImageJ is provided. Assumes the given color value is appropriate for the type of image.
     * <br />Post: A circle centered at (x,y) with radius is drawn on the specified image.
     * @param ip The image processor for the image on which to draw.
     * @param x The x coordinate of the center of the circle.
     * @param y The y coordinate of the center of the circle.
     * @param radius The radius for the circle.
     * @param color The integer color value for the circle.
     */
    public static void drawCircle(ImageProcessor ip, int x, int y, int radius, int color) {
        if ((ip == null) || (x<0) || (y<0) || (radius <= 0)) {
            return;
        }
        radius --; // The reason for this is because ImageJ's selection box is exclusive and not inclusive.
        OvalRoi myCircle = new OvalRoi(x-radius, y-radius, radius*2, radius*2);
        ip.setValue(color);
        myCircle.drawPixels(ip);
        
        /*
        int width = ip.getWidth(), height = ip.getHeight(), xOffset, yOffset;
        double angle;
        
        for (angle=0;angle<6.30;angle+=0.1) {
            xOffset = (int)(radius*Math.cos(angle));
            yOffset = (int)(radius*Math.sin(angle));
            ip.putPixel(x+xOffset,y+yOffset,color);
        }
        */
    }

    /**
     * Draws an outline of a circle on the image.
     * The circle is centered on coordinates (x,y), with radius.
     * The color of the circle is specified as a single integer value. 
     * 
     * <p>Pre: Assumes a valid ImageProcessor from an image given from ImageJ is provided. Assumes the given color value is appropriate for the type of image.
     * <br />Post: A circle centered at (x,y) with radius is drawn on the specified image.
     * @param ip The image processor for the image on which to draw.
     * @param x The x coordinate of the center of the circle.
     * @param y The y coordinate of the center of the circle.
     * @param radius The radius for the circle.
     * @param red The integer color value for the red channel. Must be between 0 and 255.
     * @param green The integer color value for the green channel. Must be between 0 and 255.
     * @param blue The integer color value for the blue channel. Must be between 0 and 255.
     */
    public static void drawCircle(ImageProcessor ip, int x, int y, int radius, int red, int green, int blue) {
        if ((ip == null) || (x<0) || (y<0) || (radius <= 0) || (red < 0) || (red > 255) || (green < 0) || (green > 255) || (blue < 0) || (blue > 255)) {
            return;
        }
        radius --; // The reason for this is because ImageJ's selection box is exclusive and not inclusive.
        OvalRoi myCircle = new OvalRoi(x-radius, y-radius, radius*2, radius*2);
        int color = (((red << 8) + green) << 8) + blue;
        ip.setValue(color);
        myCircle.drawPixels(ip);
    }
    
    /**
     * Draws a line on the image.
     * The line is drawn from (x1,y1) to (x2,y2).
     * The color specified for the line is a single integer value.
     * 
     * <p>Pre: Assumes a valid ImageProcessor from an image given from ImageJ is provided. Assumes the integer color is correct for the given image.
     * <br />Post: A line is drawn from (x1,y1) to (x2,y2) on the given image..
     * @param ip The image processor for the image on which to draw.
     * @param x1 The x coordinate of the first endpoint of the line.
     * @param y1 The y coordinate of the first endpoint of the line.
     * @param x2 The x coordinate of the second endpoint of the line.
     * @param y2 The y coordinate of the second endpoint of the line.
     * @param color The integer color value for the line.
     */
    public static void drawLine(ImageProcessor ip, int x1, int y1, int x2, int y2, int color) {
        if ((ip == null) || (x1<0) || (y1<0) || (x2<0) || (y2<0)) {
            return;
        }
        ip.setValue(color);
        ip.drawLine(x1,y1,x2,y2);
    }

    /**
     * Draws a line on the image.
     * The line is drawn from (x1,y1) to (x2,y2).
     * The color specified for the line is supplied in red, green, and blue channels.
     * 
     * <p>Pre: Assumes a valid ImageProcessor from an image given from ImageJ is provided. Assumes the integer color is correct for the given image.
     * <br />Post: A line is drawn from (x1,y1) to (x2,y2) on the given image..
     * @param ip The image processor for the image on which to draw.
     * @param x1 The x coordinate of the first endpoint of the line.
     * @param y1 The y coordinate of the first endpoint of the line.
     * @param x2 The x coordinate of the second endpoint of the line.
     * @param y2 The y coordinate of the second endpoint of the line.
     * @param red The integer color value for the red channel. Must be between 0 and 255.
     * @param green The integer color value for the green channel. Must be between 0 and 255.
     * @param blue The integer color value for the blue channel. Must be between 0 and 255.
     */
    public static void drawLine(ImageProcessor ip, int x1, int y1, int x2, int y2, int red, int green, int blue) {
        if ((ip == null) || (x1<0) || (y1<0) || (x2<0) || (y2<0) || (red < 0) || (red > 255) || (green < 0) || (green > 255) || (blue < 0) || (blue > 255)) {
            return;
        }
        int color = (((red << 8) + green) << 8) + blue;
        ip.setValue(color);
        ip.drawLine(x1,y1,x2,y2);
    }

    
    /**
     * Merges the given four channels.
     * 
     * <p>Pre: Assumes alpha, red, green, and blue are all [0, 255].
     * <br />Post: Returns the channel combination: ARGB in one int.
     * @param alpha The one byte [0, 255] alpha channel.
     * @param red The one byte [0, 255] red channel.
     * @param green The one byte [0, 255] green channel.
     * @param blue The one byte [0, 255] blue channel.
     * @return Returns the ARGB integer.
     */
    public static int mergeARGBChannels(int alpha, int red, int green, int blue) {
        int color = alpha;
        color = (color << 8) + red;
        color = (color << 8) + green;
        color = (color << 8) + blue;
        return color;
    }
    
    /**
     * Extracts and returns the alpha transparency channel from a 4 byte ARGB color.
     * 
     * <p>Pre: None.
     * <br />Post: The 1-byte transparency channel is returned.
     * @param color The ARGB 4 byte color value. 
     * @return The 1-byte alpha channel is returned. The value will be [0, 255].
     */
    public static int getAlphaChannel(int color) {
        int alpha = color & 0xFF000000;
        alpha = alpha >> 24;
        return alpha;
    }

    /**
     * Extracts and returns the red color channel from a 4 byte ARGB color.
     * 
     * <p>Pre: None.
     * <br />Post: The 1-byte color channel is returned.
     * @param color The ARGB 4 byte color value. 
     * @return The 1-byte red color channel is returned. The value will be [0, 255].
     */
    public static int getRedChannel(int color) {
        int red = color & 0x00FF0000;
        red = red >> 16;
        return red;
    }

    /**
     * Extracts and returns the green color channel from a 4 byte ARGB color.
     * 
     * <p>Pre: None.
     * <br />Post: The 1-byte color channel is returned.
     * @param color The ARGB 4 byte color value. 
     * @return The 1-byte green color channel is returned. The value will be [0, 255].
     */
    public static int getGreenChannel(int color) {
        int green = color & 0x0000FF00;
        green = green >> 8;
        return green;
    }

    /**
     * Extracts and returns the blue color channel from a 4 byte ARGB color.
     * 
     * <p>Pre: None.
     * <br />Post: The 1-byte color channel is returned.
     * @param color The ARGB 4 byte color value. 
     * @return The 1-byte blue color channel is returned. The value will be [0, 255].
     */
    public static int getBlueChannel(int color) {
        int blue = color & 0x000000FF;
        return blue;
    }

    /**
     * Breaks the given ARGB colors into four seperate channels and averages each channel, and returns the
     * recombination of the average.
     *
     * <p>Pre: None.
     * <br />Post: No changes to the outside class are made.
     * @param colorA The first ARGB color.
     * @param colorB The second ARGB color.
     * @return The recombination of the averages of the four channels of the given colors.
     * @see #getAlphaChannel
     * @see #getRedChannel
     * @see #getGreenChannel
     * @see #getBlueChannel
     * @see #mergeARGBChannels
     */
    public static int averageARGBcolor(int colorA, int colorB) {
        int alphaA = getAlphaChannel(colorA);
        int alphaB = getAlphaChannel(colorB);
        int redA = getRedChannel(colorA);
        int redB = getRedChannel(colorB);
        int greenA = getGreenChannel(colorA);
        int greenB = getGreenChannel(colorB);
        int blueA = getBlueChannel(colorA);
        int blueB = getBlueChannel(colorB);
        
        int alpha = (alphaA+alphaB)/2;
        int red = (redA+redB)/2;
        int green = (greenA+greenB)/2;
        int blue = (blueA+blueB)/2;
        return mergeARGBChannels(alpha, red, green, blue);
    }

    /**
     * Grabs the grayscale color of the pixel requested from the local
     * member pixel array. Has built in checks to make sure it is not
     * out of bounds.
     *
     * <p>Pre: The given values are within bounds of the image. Requires that the pixel array is loaded with an image.
     * <br />Post: No changes to the outside class are made.
     * @param x The x coordinate of the requested pixel.
     * @param y The y coordinate of the requested pixel.
     * @return The color of the requested pixel if the coordinates given fall within the images boundaries. If the coordinates do not, WHITE is returned.
     * @see #pixel
     * @see #WHITE
     */
    public int getColor(int x, int y) {
        if (pixel == null) {
            return WHITE;
        }
        if ((x >= 0) && (x <= (width-1))) {
            if ((y >= 0) && (y <= (height-1))) {
                return pixel[x][y];
            }
        }
        return WHITE;
    }

    /**
     * Returns the color of an 8-neighbor to the given coordinate pixel.
     * The specified pixel is the coordinate data given with the direction modification.
     * The direction corresponds to the 8 directions around a pixel.
     * 
     * Please keep in mind the upper-left coordinate of the image is (0, 0)
     * and the lower-right coordinate of the image is (width, height).
     * The pixels .., p-8, p0, p8, etc. are all the same. (the right x+1 neighbor)
     * The pixels .., p-7, p1, p9, etc. are all the same. (the upper-right x+1 y-1 neighbor)
     * This continues in a counter-clockwise fashion.
     * 
     * If the pixel coordinates are out of bounds, returns WHITE (255).
     * 
     * <p>Pre: Requires the local member pixel array to be properly loaded with an image. The width and height dimension data must be initialized.
     * <br />Post: No changes to the outside class are made.
     * @param location The x and y coordinate of the center pixel.
     * @param direction The 8-neighborhood direction of the target color pixel to return.
     * @return Either the color of the pixel requested, or WHITE (255) if the bounds are invalid.
     * @see #Coordinate
     * @see #pixel
     * @see #WHITE
     */
    public int getColor(Coordinate location, int direction) {
        if ((pixel == null) || (width < 1) || (height < 1)) {
            return WHITE;
        }
        int modDirection = direction % 8;
        int x=location.getX(), y=location.getY();
        
        // directions 0, 1, and 7 increase x by 1 (right)
        // directions 3, 4, and 5 decrease x by 1 (left) 
        switch(modDirection) {
            case 0:
            case 1:
            case 7:
                x ++;
                break;
            case 3:
            case 4:
            case 5:
                x --;
                break;
        }
        // directions 1, 2, and 3 decrease y by 1 (up)
        // directions 5, 6, and 7 increase y by 1 (down)
        switch(modDirection) {
            case 1:
            case 2:
            case 3:
                y --;
                break;
            case 5:
            case 6:
            case 7:
                y ++;
                break;
        }
        
        if ((x >= 0) && (x <= (width-1))) {
            if ((y >= 0) && (y <= (height-1))) {
                return pixel[x][y];
            }
        }
        return WHITE; // WHITE (see AngiogenesisToolkit constants)
    }

    /**
     * Sets the grayscale color of the specified pixel.
     * Allowable color values are in the range [0, 255].
     * If the pixel coordinates are out of bounds, no change is made.
     * 
     * <p>Pre: Requires the local member pixel array to be properly loaded with an image. The width and height dimension data must be initialized.
     * <br />Post: If no error is encountered, the target pixel is changed to the specified color.
     * @param x The x-coordinate of the pixel specified for color change.
     * @param y The y-coordinate of the pixel specified for color change.
     * @param color The new color for the specified pixel.
     * @see #pixel
     */
    public void setColor(int x, int y, int color) {
        if ((pixel == null) || (width < 1) || (height < 1) || (color < 0) ||(color > 255)) {
            return;
        }

        if ((x >= 0) && (x <= (width-1))) {
            if ((y >= 0) && (y <= (height-1))) {
                pixel[x][y] = color;
            }
        }
    }

    /**
     * Searches the pixel[][] array for the darkest (smallest) non-BLACK (assumed 0) color.
     * 
     * <p>Pre: Assumes that the image is not all WHITE (255). Assumes BLACK is the numerically smallest color. Searches the local member pixel for the darkest pixel. Consequently the pixel array must be initialized appropriately. Width and height dimension information must also be specified.
     * <br />Post: No changes to the outside class are made.
     * @return A Coordinate class which contains the location and color of the darkest pixel in the image. Returns null if there was an error processing the image (width, height, or pixel[][] not setup appropriately).
     * @see #Coordinate
     * @see #pixel 
     */
    public Coordinate getDarkestPixel() {
        if ((pixel == null) || (width < 1) || (height < 1)) {
            return null;
        }
        // temporary values
        int darkestX=0,darkestY=0,darkestColor=WHITE,x,y;
        
        // search for the darkest color
        for (x=0; x<width; x++) {
            for (y=0; y<height; y++) {
                if ((pixel[x][y] > BLACK) && (pixel[x][y] < darkestColor)) {
                    darkestX = x;
                    darkestY = y;
                    darkestColor=pixel[x][y];
                }
            }
        }
        
        return new Coordinate(darkestX, darkestY, darkestColor);
    }

    /**
     * Checks the 8-neighbors of the given pixel for colors numerically equal to it.
     * 
     * <p>Pre: Requires the local member pixel array to be properly loaded with an image. The width and height dimension data must be initialized.
     * <br />Post: No changes to the outside class are made.
     * @param location A Coordinate class that holds an X, Y, and color position.
     * @return Returns the number of 8-neighbors that match the color of the pixel.
     * @see #Coordinate
     * @see #pixel 
     */
    public int matching8Neighbors(Coordinate location) {
        if ((pixel == null) || (width < 1) || (height < 1)) {
            return 0;
        }
        int count=0,radius=1,x,y,color=location.getColor();
        // check the 8 directions
        for (x=location.getX()-radius; x<=location.getX()+radius; x++) {
            for (y=location.getY()-radius; y<=location.getY()+radius; y++) {
                if (getColor(x,y) == color) {
                    count ++;
                }               
            }
        }
        // don't count the center pixel
        return count-1;
    }

    /**
     * Checks the 8-neighbors of the given pixel for colors numerically equal to it.
     * Returns the number of corners which match the color.
     * Example:
     * ABC
     * DEF
     * GHI
     * The count is incremented if E matches BCF, BAD, DGH, or HIF.
     * If count is every gre 
     * 
     * <p>Pre: Requires the local member pixel array to be properly loaded with an image. The width and height dimension data must be initialized.
     * <br />Post: No changes to the outside class are made.
     * @param location A Coordinate class that holds an X, Y, and color position.
     * @return Returns the number of 4-squares in the 8-neighborhood that match the color of the pixel.
     * @see #Coordinate
     * @see #pixel 
     */
    public int matching8Neighborhood4Squares(Coordinate location) {
        if ((pixel == null) || (width < 1) || (height < 1)) {
            return 0;
        }
        int count=0,x=location.getX(),y=location.getY(),color=location.getColor();
        // I'm going to be lazy and use cases.
        if (getColor(x+1,y) == color && getColor(x+1,y-1) == color && getColor(x,y-1) == color) {
            count ++; // BCF case
        }
        if (getColor(x-1,y) == color && getColor(x-1,y-1) == color && getColor(x,y-1) == color) {
            count ++; // ABD case
        }
        if (getColor(x-1,y) == color && getColor(x-1,y+1) == color && getColor(x,y+1) == color) {
            count ++; // DGH case
        }
        if (getColor(x+1,y) == color && getColor(x+1,y+1) == color && getColor(x,y+1) == color) {
            count ++; // FHI case
        }
        return count;
    }

    /**
     * Checks the 8-neighbors of the given pixel for colors numerically different from it.
     * Returns the maximum difference.
     * 
     * <p>Pre: Requires the local member pixel array to be properly loaded with an image. The width and height dimension data must be initialized.
     * <br />Post: No changes to the outside class are made.
     * @param location A Coordinate class that holds an X, Y, and color position.
     * @return Returns the maximum color value difference in the 8-neighborhood.
     * @see #Coordinate
     * @see #pixel 
     */
    public int max8NeighborhoodDelta(Coordinate location) {
        if ((pixel == null) || (width < 1) || (height < 1)) {
            return 0;
        }
        int max=0,radius=1,x,y,color=location.getColor();
        // check the 8 directions
        for (x=location.getX()-radius; x<=location.getX()+radius; x++) {
            for (y=location.getY()-radius; y<=location.getY()+radius; y++) {
                if (Math.abs(getColor(x,y)-color) > max) {
                    max = Math.abs(getColor(x,y)-color);
                }               
            }
        }
        return max;
    }
    
    /**
     * Checks the 8-neighbors of the given pixel for colors not matching WHITE (!=255).
     * 
     * <p>Pre: Requires the local member pixel array to be properly loaded with an image. The width and height dimension data must be initialized.
     * <br />Post: No changes to the outside class are made.
     * @param centerX The center x-coordinate whos neihbors need checked. 
     * @param centerY The center y-coordinate whos neihbors need checked. 
     * @return Returns the number of 8-neighbors that are non-white (!= 255).
     * @see #Coordinate
     * @see #pixel 
     * @see #BLACK 
     */
    public int skeletalNeighbors(int centerX, int centerY) {
        int count=0, x, y;
        // check the 8 directions
        for (x=centerX-1;x<=centerX+1;x++) {
            for (y=centerY-1;y<=centerY+1;y++) {
                if (getColor(x,y) != WHITE) {
                    count ++;
                }               
            }
        }
        // don't count the center pixel
        return count-1;
    }

    /**
     * Checks the 8-neighbors of the given pixel for colors matching it or darker.
     * 
     * <p>Pre: Requires the local member pixel array to be properly loaded with an image. The width and height dimension data must be initialized.
     * <br />Post: No changes to the class outside this method are made.
     * @param location A Coordinate class that holds an X, Y, and color position.
     * @return Returns the number of 8-neighbors that are as dark or darker than the color of the pixel.
     * @see #Coordinate
     * @see #pixel 
     */
    public int dark8Neighbors(Coordinate location) {
        if ((pixel == null) || (width < 1) || (height < 1)) {
            return 0;
        }
        int count=0,radius=1,x,y,color=location.getColor();
        // check the 8 directions
        for (x=location.getX()-radius; x<=location.getX()+radius; x++) {
            for (y=location.getY()-radius; y<=location.getY()+radius; y++) {
                if (getColor(x,y) <= color) {
                    count ++;
                }               
            }
        }
        // don't count the center pixel
        return count-1;
    }

    /**
     * Checks the 8-neighbors of the given pixel for colors lighter than it.
     * 
     * <p>Pre: Requires the local member pixel array to be properly loaded with an image. The width and height dimension data must be initialized.
     * <br />Post: No changes to the class outside this method are made.
     * @param location A Coordinate class that holds an X, Y, and color position.
     * @return Returns the number of 8-neighbors that are lighter than the color of the pixel.
     * @see #Coordinate
     * @see #pixel 
     */
    public int light8Neighbors(Coordinate location) {
        if ((pixel == null) || (width < 1) || (height < 1)) {
            return 0;
        }
        int count=0,radius=1,x,y,color=location.getColor();
        // check the 8 directions
        for (x=location.getX()-radius; x<=location.getX()+radius; x++) {
            for (y=location.getY()-radius; y<=location.getY()+radius; y++) {
                if (getColor(x,y) > color) {
                    count ++;
                }               
            }
        }
        return count;
    }

    /**
     * Checks the 24-neighbors of the given pixel for colors darking than it.
     * 
     * <p>Pre: Requires the local member pixel array to be properly loaded with an image. The width and height dimension data must be initialized.
     * <br />Post: No changes to the class outside this method are made.
     * @param location A Coordinate class that holds an X, Y, and color position.
     * @return Returns the number of 24-neighbors that are as dark or darker than the color of the given pixel.
     * @see #Coordinate
     * @see #pixel 
     */
    public int dark24Neighbors(Coordinate location) {
        if ((pixel == null) || (width < 1) || (height < 1)) {
            return 0;
        }
        int count=0,radius=2,x,y,color=location.getColor();
        // check the surroundings
        for (x=location.getX()-radius; x<=location.getX()+radius; x++) {
            for (y=location.getY()-radius; y<=location.getY()+radius; y++) {
                if (getColor(x,y) <= color) {
                    count ++;
                }               
            }
        }
        // don't count the center pixel
        return count-1;
    }

    /**
     * Checks the 24-neighbors of the given pixel for colors lighter than it.
     * 
     * <p>Pre: Requires the local member pixel array to be properly loaded with an image. The width and height dimension data must be initialized.
     * <br />Post: No changes to the class outside this method are made.
     * @param location A Coordinate class that holds an X, Y, and color position.
     * @return Returns the number of 24-neighbors that are lighter than the color of the given pixel.
     * @see #Coordinate
     * @see #pixel 
     */
    public int light24Neighbors(Coordinate location) {
        if ((pixel == null) || (width < 1) || (height < 1)) {
            return 0;
        }
        int count=0,radius=2,x,y,color=location.getColor();
        // check the surroundings
        for (x=location.getX()-radius; x<=location.getX()+radius; x++) {
            for (y=location.getY()-radius; y<=location.getY()+radius; y++) {
                if (getColor(x,y) > color) {
                    count ++;
                }               
            }
        }
        return count;
    }

    /**
     * Checks the 24-neighbors of the given pixel for colors matching it.
     * 
     * <p>Pre: Requires the local member pixel array to be properly loaded with an image. The width and height dimension data must be initialized.
     * <br />Post: No changes to the class outside this method are made.
     * @param location A Coordinate class that holds an X, Y, and color position.
     * @return Returns the number of 24-neighbors that are the same color as the given pixel.
     * @see #Coordinate
     * @see #pixel 
     */
    public int matching24Neighbors(Coordinate location) {
        if ((pixel == null) || (width < 1) || (height < 1)) {
            return 0;
        }
        int count=0,radius=2,x,y,color=location.getColor();
        // check the surroundings
        for (x=location.getX()-radius; x<=location.getX()+radius; x++) {
            for (y=location.getY()-radius; y<=location.getY()+radius; y++) {
                if (getColor(x,y) == color) {
                    count ++;
                }               
            }
        }
        // don't count the center pixel
        return count-1;
    }
    /**
     * Looks at the 8-neighborhood of a pixel.
     * Finds the number of 4-connected components
     * that are white.
     * 
     * Suppose p is part of a forward-slash
     *  ---p--- ridge. Here p3 and p7 will be black.
     * 
     * This means (p0,p1,p2) and (p4,p5,p6)
     * will be white 4-connected components.
     * In this case the method will return 2. 
     * 
     * (sorry of this looks really weird with a different font)
     * _____________
     * | p3 | p2 | p1 |
     * |------|------|-----|
     * | p4 |  p  | p0 |
     * |------|------|-----|
     * | p5 | p6 | p7 |
     * ---------------------
     * 
     * <p>Pre: Requires the local member pixel array to be properly loaded with an image. The width and height dimension data must be initialized.
     * <br />Post: No changes to the outside class are made.
     * @param location A Coordinate class that holds an X, Y, and color position.
     * @return The number of 4-connected lighter components in the 8-neighborhood.
     * @see #Coordinate
     * @see #pixel
     * @see #BLACK 
     */ 
    public int getWhiteComponents(Coordinate location) {
        int count = 0, direction = 0, color=location.getColor(), seeking, end;
        // a wall is a darker or matching pixel
        // white is a lighter pixel
        
        end = 8;
        seeking = BLACK;
        // find the first wall.. if no wall is found, quit and return 1 component
        while (direction < end) {
            if (getColor(location, direction) != WHITE) {
                if (seeking == BLACK) {                 
                    if (count == 0) {
                        end = direction + 8;
                    }
                    seeking = WHITE;
                } // else .. no need
                    // seeking LIGHT and found a wall.. ignore                  
            } else if (getColor(location, direction) == WHITE) {
                if (seeking == WHITE) {
                    count ++;
                    seeking = BLACK;
                } // else
                    // seeking BLACK and found WHITE.. ignore
            }           
            direction ++;
        }
        if ((count == 0) && (seeking == BLACK)) {
            // all white
            return 1;
        }
        // else..
        return count;
    }

    /**
     * Looks at the 8-neighborhood of a pixel.
     * Finds the number of 4-connected components
     * that are lighter than the given pixel.
     * 
     * Suppose p is part of a \ ---p--- ridge.
     * p3 and p7 will be darker or matching.
     * 
     * This means (p0,p1,p2) and (p4,p5,p6)
     * will be white 4-connected components.
     * In this case the method will return 2. 
     * 
     * (sorry of this looks really weird with a different font)
     * _____________
     * | p3 | p2 | p1 |
     * |------|------|-----|
     * | p4 |  p  | p0 |
     * |------|------|-----|
     * | p5 | p6 | p7 |
     * ---------------------
     * 
     * <p>Pre: Requires the local member pixel array to be properly loaded with an image. The width and height dimension data must be initialized.
     * <br />Post: No changes to the class outside this method are made.
     * @param location A Coordinate class that holds an X, Y, and color position.
     * @return The number of 4-connected lighter components in the 8-neighborhood.
     * @see #Coordinate
     * @see #pixel 
     * @see #BLACK
     */ 
    public int getLightComponents(Coordinate location) {
        if ((pixel == null) || (width < 1) || (height < 1)) {
            return 0;
        }
        int count = 0, direction = 0, color=location.getColor(), seeking, end;
        // a wall is a darker or matching pixel
        // white is a lighter pixel
        
        end = 8;
        seeking = WALL;
        // find the first wall.. if no wall is found, quit and return 1 component
        while (direction < end) {

            if (getColor(location, direction) <= color) { // found a wall
                if (seeking == WALL) {                  
                    if (count == 0) {
                        end = direction + 8;
                    }
                    seeking = LIGHT;
                } // else .. no need
                    // seeking LIGHT and found a wall.. ignore                  
            } else if (getColor(location, direction) > color) { // found light
                if (seeking == LIGHT) {
                    count ++;
                    seeking = WALL;
                } // else
                    // seeking WALL and found LIGHT.. ignore
            }           
            direction ++;
        }
        if ((count == 0) && (seeking == WALL)) {
            // all white
            return 1;
        }
        // else..
        return count;
    }
    
    /**
     * Looks at the 5x5 area centered on the given pixel.
     * Finds the number of 4-connected components
     * that are lighter/darker/matching the given pixel.
     * 
     * <p>Pre: Requires the local member pixel array to be properly loaded with an image. The width and height dimension data must be initialized.
     * <br />Post: No changes to the class outside this method are made.
     * @param type Must be LIGHT, MATCHING, or DARK for lighter, equal, or strictly darker components. This changes what this method returns. 
     * @param location A Coordinate class that holds an X, Y, and color position.
     * @return The number of relevant 4-connected components in the 5x5 area centerred at the given pixel. Returns -1 if the type given was invalid or if the preconditions were not satisfied.
     * @see #Coordinate
     * @see #pixel 
     * @see #LIGHT
     * @see #MATCHING
     * @see #DARK
     */ 
    public int get5x5Components(int type, Coordinate location) {
        if (!(type == LIGHT || type == MATCHING || type == DARK)) {
            return -1;
        }
        if ((pixel == null) || (width < 1) || (height < 1)) {
            return -1;
        }
        int color=location.getColor(), searchColor, radius=2,seeking,x,y,xMod,yMod;
        int darkComponents=0, lightComponents=0, matchingComponents=0;
        // create a (radius X radius) memory space for searching
        int [][] region = new int[radius*2+1][radius*2+1];
        int baseX = location.getX()-radius;
        int baseY = location.getY()-radius;
        // copy the image memory into the memory space
        for (x=location.getX()-radius;x<=location.getX()+radius;x++) {
            for (y=location.getY()-radius;y<=location.getY()+radius;y++) {
                region[x-baseX][y-baseY] = getColor(x,y);
            }
        }
        // create a search stack for our new memory space
        Stack searchStack = new Stack();
        Coordinate seeker;
        // begin the search. exhaust all pixels.
        for (x=0;x<=radius*2;x++) {
            for (y=0;y<=radius*2;y++) {
                if (region[x][y] != BLACK) {
                    if (region[x][y] < color) {
                        seeking = DARK;
                        darkComponents ++;
                    } else if (region[x][y] == color) {
                        seeking = MATCHING;
                        matchingComponents ++;
                    } else { // must be greater than
                        seeking = LIGHT; 
                        lightComponents ++;
                    }
                    // push onto search stack
                    searchStack.push(new Coordinate(x, y, color));
                    // begin search
                    while (!searchStack.isEmpty()) {
                        seeker = (Coordinate)searchStack.pop();
                        region[seeker.getX()][seeker.getY()] = WALL;
                        // search left
                        xMod = -1; yMod = 0;
                        if ((seeker.getX()+xMod >= 0 && seeker.getX()+xMod <= radius*2) && (seeker.getY()+yMod >= 0 && seeker.getY()+yMod <= radius*2)) {
                            searchColor = region[seeker.getX()+xMod][seeker.getY()+yMod];
                            if (searchColor != WALL && ((seeking == LIGHT && searchColor > color) || (seeking == MATCHING && searchColor == color) || (seeking == DARK && searchColor < color))) {
                                searchStack.push(new Coordinate(seeker.getX()+xMod, seeker.getY()+yMod, color));
                            }
                        }
                        // search right
                        xMod = 1; yMod = 0;
                        if ((seeker.getX()+xMod >= 0 && seeker.getX()+xMod <= radius*2) && (seeker.getY()+yMod >= 0 && seeker.getY()+yMod <= radius*2)) {
                            searchColor = region[seeker.getX()+xMod][seeker.getY()+yMod];
                            if (searchColor != WALL && ((seeking == LIGHT && searchColor > color) || (seeking == MATCHING && searchColor == color) || (seeking == DARK && searchColor < color))) {
                                searchStack.push(new Coordinate(seeker.getX()+xMod, seeker.getY()+yMod, color));
                            }
                        }
                        // search up
                        xMod = 0; yMod = -1;
                        if ((seeker.getX()+xMod >= 0 && seeker.getX()+xMod <= radius*2) && (seeker.getY()+yMod >= 0 && seeker.getY()+yMod <= radius*2)) {
                            searchColor = region[seeker.getX()+xMod][seeker.getY()+yMod];
                            if (searchColor != WALL && ((seeking == LIGHT && searchColor > color) || (seeking == MATCHING && searchColor == color) || (seeking == DARK && searchColor < color))) {
                                searchStack.push(new Coordinate(seeker.getX()+xMod, seeker.getY()+yMod, color));
                            }
                        }
                        // search down
                        xMod = 0; yMod = 1;
                        if ((seeker.getX()+xMod >= 0 && seeker.getX()+xMod <= radius*2) && (seeker.getY()+yMod >= 0 && seeker.getY()+yMod <= radius*2)) {
                            searchColor = region[seeker.getX()+xMod][seeker.getY()+yMod];
                            if (searchColor != WALL && ((seeking == LIGHT && searchColor > color) || (seeking == MATCHING && searchColor == color) || (seeking == DARK && searchColor < color))) {
                                searchStack.push(new Coordinate(seeker.getX()+xMod, seeker.getY()+yMod, color));
                            }
                        }
                    }
                }
            }
        }

        if (type == LIGHT) {
            return lightComponents;
        } else if (type == MATCHING) {
            return matchingComponents;
        } else if (type == DARK) {
            return darkComponents;
        }
        // else... type was invalid. either it got changed or your computer is broken
        return -1;
    }
    
    /**
     * Looks at the 7x7 area centered on the given pixel.
     * Finds the number of 4-connected components
     * that are lighter/darker/matching the given pixel.
     * 
     * <p>Pre: Requires the local member pixel array to be properly loaded with an image. The width and height dimension data must be initialized.
     * <br />Post: No changes to the class outside this method are made.
     * @param type Must be LIGHT, MATCHING, or DARK for lighter, equal, or strictly darker components. This changes what this method returns. 
     * @param location A Coordinate class that holds an X, Y, and color position.
     * @return The number of relevant 4-connected components in the 7x7 area centerred at the given pixel. Returns -1 if the type given was invalid or if the preconditions were not satisfied.
     * @see #Coordinate
     * @see #pixel 
     * @see #LIGHT
     * @see #MATCHING
     * @see #DARK
     */ 
    public int get7x7Components(int type, Coordinate location) {
        if (!(type == LIGHT || type == MATCHING || type == DARK)) {
            return -1;
        }
        if ((pixel == null) || (width < 1) || (height < 1)) {
            return -1;
        }
        int color=location.getColor(), searchColor, radius=3,seeking,x,y,xMod,yMod;
        int darkComponents=0, lightComponents=0, matchingComponents=0;
        // create a (radius X radius) memory space for searching
        int [][] region = new int[radius*2+1][radius*2+1];
        int baseX = location.getX()-radius;
        int baseY = location.getY()-radius;
        // copy the image memory into the memory space
        for (x=location.getX()-radius;x<=location.getX()+radius;x++) {
            for (y=location.getY()-radius;y<=location.getY()+radius;y++) {
                region[x-baseX][y-baseY] = getColor(x,y);
            }
        }
        // create a search stack for our new memory space
        Stack searchStack = new Stack();
        Coordinate seeker;
        // begin the search. exhaust all pixels.
        for (x=0;x<=radius*2;x++) {
            for (y=0;y<=radius*2;y++) {
                if (region[x][y] != BLACK) {
                    if (region[x][y] < color) {
                        seeking = DARK;
                        darkComponents ++;
                    } else if (region[x][y] == color) {
                        seeking = MATCHING;
                        matchingComponents ++;
                    } else { // must be greater than
                        seeking = LIGHT; 
                        lightComponents ++;
                    }
                    // push onto search stack
                    searchStack.push(new Coordinate(x, y, color));
                    // begin search
                    while (!searchStack.isEmpty()) {
                        seeker = (Coordinate)searchStack.pop();
                        region[seeker.getX()][seeker.getY()] = WALL;
                        // search left
                        xMod = -1; yMod = 0;
                        if ((seeker.getX()+xMod >= 0 && seeker.getX()+xMod <= radius*2) && (seeker.getY()+yMod >= 0 && seeker.getY()+yMod <= radius*2)) {
                            searchColor = region[seeker.getX()+xMod][seeker.getY()+yMod];
                            if (searchColor != WALL && ((seeking == LIGHT && searchColor > color) || (seeking == MATCHING && searchColor == color) || (seeking == DARK && searchColor < color))) {
                                searchStack.push(new Coordinate(seeker.getX()+xMod, seeker.getY()+yMod, color));
                            }
                        }
                        // search right
                        xMod = 1; yMod = 0;
                        if ((seeker.getX()+xMod >= 0 && seeker.getX()+xMod <= radius*2) && (seeker.getY()+yMod >= 0 && seeker.getY()+yMod <= radius*2)) {
                            searchColor = region[seeker.getX()+xMod][seeker.getY()+yMod];
                            if (searchColor != WALL && ((seeking == LIGHT && searchColor > color) || (seeking == MATCHING && searchColor == color) || (seeking == DARK && searchColor < color))) {
                                searchStack.push(new Coordinate(seeker.getX()+xMod, seeker.getY()+yMod, color));
                            }
                        }
                        // search up
                        xMod = 0; yMod = -1;
                        if ((seeker.getX()+xMod >= 0 && seeker.getX()+xMod <= radius*2) && (seeker.getY()+yMod >= 0 && seeker.getY()+yMod <= radius*2)) {
                            searchColor = region[seeker.getX()+xMod][seeker.getY()+yMod];
                            if (searchColor != WALL && ((seeking == LIGHT && searchColor > color) || (seeking == MATCHING && searchColor == color) || (seeking == DARK && searchColor < color))) {
                                searchStack.push(new Coordinate(seeker.getX()+xMod, seeker.getY()+yMod, color));
                            }
                        }
                        // search down
                        xMod = 0; yMod = 1;
                        if ((seeker.getX()+xMod >= 0 && seeker.getX()+xMod <= radius*2) && (seeker.getY()+yMod >= 0 && seeker.getY()+yMod <= radius*2)) {
                            searchColor = region[seeker.getX()+xMod][seeker.getY()+yMod];
                            if (searchColor != WALL && ((seeking == LIGHT && searchColor > color) || (seeking == MATCHING && searchColor == color) || (seeking == DARK && searchColor < color))) {
                                searchStack.push(new Coordinate(seeker.getX()+xMod, seeker.getY()+yMod, color));
                            }
                        }
                    }
                }
            }
        }

        if (type == LIGHT) {
            return lightComponents;
        } else if (type == MATCHING) {
            return matchingComponents;
        } else if (type == DARK) {
            return darkComponents;
        }
        // else... type was invalid. either it got changed or your computer is broken
        return -1;
    }   
    
    /**
     * Looks at the 8-neighbors of a pixel.
     * Determines if any of these pixels are
     * branching points (3 or more 4-connected white components).
     * 
     * <p>Pre: Requires the local member pixel array to be properly loaded with an image. The width and height dimension data must be initialized.
     * <br />Post: No changes to the class outside this method are made.
     * @param location A Coordinate class that holds an X, Y, and color position.
     * @return True if there is an 8-neighbor of the given pixel that is a branching point. False if there are none.
     * @see #Coordinate
     * @see #pixel 
     * @see #WHITE
     */ 
    public boolean isNeighborBranchingPoint(Coordinate location) {
        if ((pixel == null) || (width < 1) || (height < 1)) {
            return false;
        }
        int x,y;
        
        Coordinate point;
        for (x=location.getX()-1; x<=location.getX()+1; x++) {
            for (y=location.getY()-1; y<=location.getY()+1; y++) {
                if (!(x == location.getX() && y == location.getY()) && (getColor(x,y) != WHITE)) {
                    point = new Coordinate(x,y,getColor(x,y));
                    if (getWhiteComponents(point) >= 3)
                        return true;
                }
            }
        }
        
        return false;
    }   
}