edu.ku.brc.services.mapping.LocalityMapper.java Source code

Java tutorial

Introduction

Here is the source code for edu.ku.brc.services.mapping.LocalityMapper.java

Source

/* Copyright (C) 2015, University of Kansas Center for Research
 * 
 * Specify Software Project, specify@ku.edu, Biodiversity Institute,
 * 1345 Jayhawk Boulevard, Lawrence, Kansas, 66045, USA
 * 
 * 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.
*/
package edu.ku.brc.services.mapping;

import java.awt.Color;
import java.awt.Component;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.io.IOException;
import java.util.List;
import java.util.Vector;

import javax.swing.Icon;
import javax.swing.ImageIcon;

import org.apache.commons.httpclient.HttpException;
import org.apache.log4j.Logger;
import org.jdesktop.animation.timing.Animator;
import org.jdesktop.animation.timing.TimingTarget;
import org.jdesktop.animation.timing.Animator.RepeatBehavior;

import edu.ku.brc.specify.datamodel.Locality;
import edu.ku.brc.ui.GraphicsUtils;
import edu.ku.brc.ui.SimpleCircleIcon;
import edu.ku.brc.util.Pair;
import edu.ku.brc.util.services.MapGrabber;

/**
 * Maps a collection of <code>Locality</code> objects.
 *
 * @author jstewart
 * @code_status Beta
 */
public class LocalityMapper implements TimingTarget {
    /** Logger for all messages emitted from this class. */
    protected static final Logger log = Logger.getLogger(LocalityMapper.class);

    /** List of {@link MapLocationIFace} objects to be mapped. */
    protected List<MapLocationIFace> mapLocations;
    /** A member of {@link #mapLocations} to be considered the 'current' item. */
    protected MapLocationIFace currentLoc;
    /** Labels to apply to the members of {@link #mapLocations}. */
    protected List<String> labels;
    /** Pixel locations of markers to apply to members of {@link #mapLocations}. */
    protected List<Point> markerLocations;
    /** Smallest latitude in the set of {@link MapLocationIFace}s. */
    protected double minLat;
    /** Largest latitude in the set of {@link MapLocationIFace}s. */
    protected double maxLat;
    /** Smallest longitude in the set of {@link MapLocationIFace}s. */
    protected double minLong;
    /** Largest longitude in the set of {@link MapLocationIFace}s. */
    protected double maxLong;
    /**locationslatitude in the set of {@link MapLocationIFace}s after adding a 5% buffer region. */
    protected double mapMinLat;
    /** Largest latitude in the set of {@link MapLocationIFace}s after adding a 5% buffer region. */
    protected double mapMaxLat;
    /** Smallest longitude in the set of {@link MapLocationIFace}s after adding a 5% buffer region. */
    protected double mapMinLong;
    /** Largest longitude in the set of {@link MapLocationIFace}s after adding a 5% buffer region. */
    protected double mapMaxLong;
    /** Range of latitude covered by the map. */
    protected double mapLatRange;
    /** Range of longitude convered by the map. */
    protected double mapLongRange;
    /** Ratio of image pixels to latitude degrees. */
    protected double pixelPerLatRatio;
    /** Ratio of image pixels to longitude degrees. */
    protected double pixelPerLongRatio;
    /** Maximum width of retrieved maps. */
    protected Integer maxMapWidth;
    /** Maximum height of retrieved maps. */
    protected Integer maxMapHeight;
    /** Actual width of a retrieved map. */
    protected int mapWidth;
    /** Actual height of a retrieved map. */
    protected int mapHeight;
    /** The minimum aspect ratio of the returned map (as a float length/width) */
    protected double minAspectRatio;
    /** The maximum aspect ratio of the returned map (as a float length/width) */
    protected double maxAspectRatio;
    /** Indicator as to whether the minimum and maximum aspect ratios are enforced. */
    protected boolean enforceAspectRatios;
    /** The most recent screen coordinate used for painting the map. */
    protected int mostRecentPaintedX;
    /** The most recent screen coordinate used for painting the map. */
    protected int mostRecentPaintedY;

    /** Icon to use for painting locations on the map. */
    protected SimpleCircleIcon marker;
    /** Icon to use for painting the 'current' location on the map. */
    protected SimpleCircleIcon currentLocMarker;
    /** Toggle switch for enabling/disabling arrow painting between map locations. */
    protected boolean showArrows;
    /** Toggle switch for enabling/disabling arrow animating between map locations. */
    protected boolean showArrowAnimations = true;
    /** Toggle switch for enabling/disabling label display near map locations. */
    protected boolean showLabels;
    /** Color of the arrows. */
    protected Color arrowColor;
    /** Color of the labels. */
    protected Color labelColor;
    /** Indicates if an animation is currently in progress. */
    protected boolean animationInProgress = false;
    /** Percentage of animation that is completed. */
    protected float percent;
    /** Animation manager. */
    protected Animator animator;
    /** Start location of the current animation. */
    protected MapLocationIFace animStartLoc;
    /** End location of the current animation. */
    protected MapLocationIFace animEndLoc;
    /** An <code>Icon</code> of the map. */
    protected Icon mapIcon;
    /** ??? Rod? ???*/
    protected Icon overlayIcon = null;
    /** Indicator as to whether the currently cached map image is still valid. */
    protected boolean cacheValid;

    /**
     * Constructs an instance using default parameters.
     */
    public LocalityMapper() {
        minLat = -90;
        minLong = -180;
        maxLat = 90;
        maxLong = 180;
        minAspectRatio = 1;
        maxAspectRatio = 1;
        enforceAspectRatios = false;

        this.mapLocations = new Vector<MapLocationIFace>();
        this.labels = new Vector<String>();
        this.markerLocations = new Vector<Point>();

        showArrows = true;
        showLabels = true;
        labelColor = Color.BLACK;
        arrowColor = Color.BLACK;
        cacheValid = false;

        marker = new SimpleCircleIcon(8, Color.BLACK);
        currentLocMarker = new SimpleCircleIcon(8, Color.BLACK);

        // setup the animation
        int duration = 750;
        int repeatCount = 1;
        animator = new Animator(duration, repeatCount, RepeatBehavior.REVERSE, this);
        animator.setAcceleration(0.45f);
        animator.setDeceleration(0.45f);
    }

    /**
     * Constructs an instance to map the given set of locations.
     * 
     * @param locations a set of locations to map
     */
    public LocalityMapper(List<MapLocationIFace> locations) {
        this();
        this.mapLocations.addAll(locations);
        for (int i = 0; i < locations.size(); ++i) {
            labels.add(null);
        }
        for (int i = 0; i < locations.size(); ++i) {
            markerLocations.add(null);
        }
        cacheValid = false;
    }

    /**
     * Constructs an instance to map the given locations and
     * applying the given lables.
     * 
     * @param locations the locations to map
     * @param labels the labels to display
     */
    public LocalityMapper(List<MapLocationIFace> locations, List<String> labels) {
        this();

        // check the sizes
        if (locations.size() != labels.size()) {
            throw new IllegalArgumentException("Locations and labels list must be the same size"); //$NON-NLS-1$
        }

        this.mapLocations.addAll(mapLocations);
        this.labels.addAll(labels);
        for (int i = 0; i < mapLocations.size(); ++i) {
            markerLocations.add(null);
        }
        cacheValid = false;
    }

    /**
     * Attach another <code>TimingTarget</code> to the underlying animator.
     * 
     * @param target the new <code>TimingTarget</code>
     */
    public void addTimingTarget(TimingTarget target) {
        animator.addTarget(target);
    }

    /**
     * Returns the 'current' locality.
     * 
     * @see #setCurrentLoc(Locality)
     * @return the 'current' locality
     */
    public MapLocationIFace getCurrentLoc() {
        return currentLoc;
    }

    /**
     * Sets the 'current' locality.
     * 
     * @see #getCurrentLoc()
     * @param currentLoc the 'current' locality
     */
    public void setCurrentLoc(MapLocationIFace currentLoc) {
        if (this.showArrowAnimations) {
            this.animStartLoc = this.currentLoc;
            this.animEndLoc = currentLoc;

            // normalize the arrow speed by calculating the appropriate cycle duration
            // find the longest distance an arrow might have to cover
            double mapDiagDist = Math.sqrt(Math.pow(mapWidth, 2) + Math.pow(mapHeight, 2));

            // set the arrow speed to cover the longest possible route in 2 seconds
            // dist unit is pixels/millisec
            double arrowSpeed = mapDiagDist / 2000;

            // calculate the length of the arrow to animate
            int startIndex = mapLocations.indexOf(animStartLoc);
            int endIndex = mapLocations.indexOf(animEndLoc);
            if (startIndex != -1 && endIndex != -1) {
                Point startPoint = markerLocations.get(startIndex);
                Point endPoint = markerLocations.get(endIndex);
                double arrowLength = GraphicsUtils.distance(startPoint, endPoint);
                int duration = (int) (arrowLength / arrowSpeed);
                animator.setDuration(duration);

                // normalize the acceleration to be 0->full_speed in 500 ms
                // deceleration is the same (full_speed->0 in 500 ms)
                if (duration <= 1000) {
                    animator.setAcceleration(0.5f);
                    animator.setDeceleration(0.5f);
                } else {
                    float acc = 500 / duration;
                    animator.setAcceleration(acc);
                    animator.setDeceleration(acc);
                }
            }

            animator.start();
        }
        this.currentLoc = currentLoc;
    }

    /**
     * Adds a location and label to the set to be mapped/displayed.
     * 
     * @param loc the location
     * @param label the label
     */
    public void addLocationAndLabel(MapLocationIFace loc, String label) {
        mapLocations.add(loc);
        labels.add(label);
        markerLocations.add(null);
        cacheValid = false;
    }

    /**
     * Removes a location (and associated label) from the set to be mapped/displayed. 
     * 
     * @param loc the location
     */
    public void removeLocationAndLabel(MapLocationIFace loc) {
        int index = mapLocations.indexOf(loc);
        mapLocations.remove(index);
        labels.remove(index);
        markerLocations.remove(index);
        cacheValid = false;
    }

    /**
     * Returns the arrowColor.
     *
     * @see #setArrowColor(Color)
     * @return the arrowColor
     */
    public Color getArrowColor() {
        return arrowColor;
    }

    /**
     * Sets the arrowColor.
     *
     * @see #getArrowColor()
     * @param arrowColor the arrowColor
     */
    public void setArrowColor(Color arrowColor) {
        this.arrowColor = arrowColor;
    }

    /**
     * Returns the mapHeight.
     *
     * @see #setMapHeight(int)
     * @return the mapHeight
     */
    public int getMapHeight() {
        return mapHeight;
    }

    /**
     * Returns the mapWidth.
     *
     * @see #setMapWidth(int)
     * @return the mapWidth
     */
    public int getMapWidth() {
        return mapWidth;
    }

    /**
     * Returns the labelColor.
     *
     * @see #setLabelColor(Color)
     * @return the labelColor
     */
    public Color getLabelColor() {
        return labelColor;
    }

    /**
     * Sets the labelColor.
     *
     * @see #getLabelColor()
     * @param labelColor the labelColor
     */
    public void setLabelColor(Color labelColor) {
        this.labelColor = labelColor;
    }

    /**
     * Returns the maxMapHeight.
     *
     * @see #setMaxMapHeight(Integer)
     * @return the maxMapHeight
     */
    public Integer getMaxMapHeight() {
        return maxMapHeight;
    }

    /**
     * Sets the maxMapHeight.
     *
     * @see #getMaxMapHeight()
     * @param maxMapHeight the maxMapHeight
     */
    public void setMaxMapHeight(Integer maxMapHeight) {
        this.maxMapHeight = maxMapHeight;
        cacheValid = false;
    }

    /**
     * Returns the maxMapWidth.
     *
     * @see #setMaxMapWidth(Integer)
     * @return the maxMapWidth
     */
    public Integer getMaxMapWidth() {
        return maxMapWidth;
    }

    /**
     * Sets the maxMapWidth.
     *
     * @see #getMaxMapWidth()
     * @param maxMapWidth the maxMapWidth
     */
    public void setMaxMapWidth(Integer maxMapWidth) {
        this.maxMapWidth = maxMapWidth;
        cacheValid = false;
    }

    /**
      * Returns the maximum allowed aspect ratio of the returned maps.
      * 
      * @see #setMaxAspectRatio(float)
     * @return the maximum allowed aspect ratio
     */
    public double getMaxAspectRatio() {
        return maxAspectRatio;
    }

    /**
     * Sets the maximum allowed aspect ratio of the returned maps.  This limit is enforced
     * as much as possible, but under no circumstances will the returned map be too small to
     * contain the provided {@link MapLocationIFace}s, nor will the map have a longitude range greater than
     * 180 to -180 or a latitude range greater than 90 to -90.
     * 
     * @see #getMaxAspectRatio()
     * @param maxAspectRatio the maximum allowed aspect ratio
     */
    public void setMaxAspectRatio(double maxAspectRatio) {
        this.maxAspectRatio = maxAspectRatio;
        if (this.enforceAspectRatios) {
            cacheValid = false;
        }
    }

    /**
     * Returns the minimum allowed aspect ratio of the returned maps.
     * 
     * @see #setMinAspectRatio(float)
     * @return the minimum allowed aspect ratio
     */
    public double getMinAspectRatio() {
        return minAspectRatio;
    }

    /**
     * Sets the minimum allowed aspect ratio of the returned maps.  This limit is enforced
     * as much as possible, but under no circumstances will the returned map be too small to
     * contain the provided {@link MapLocationIFace}s, nor will the map have a longitude range greater than
     * 180 to -180 or a latitude range greater than 90 to -90.
     * 
     * @see #getMinAspectRatio()
     * @param maxAspectRatio the minimum allowed aspect ratio
     */
    public void setMinAspectRatio(double minAspectRatio) {
        this.minAspectRatio = minAspectRatio;
        if (this.enforceAspectRatios) {
            cacheValid = false;
        }
    }

    /**
     * Returns true if the aspect ratio limits are enforced.
     * 
     * @return true if the aspect ratio limits are enforced
     */
    public boolean isEnforceAspectRatios() {
        return enforceAspectRatios;
    }

    /**
     * Sets the value of {@link #enforceAspectRatios}.
     * 
     * @param enforceAspectRatios whether or not to enforce the aspect ratio limits
     */
    public void setEnforceAspectRatios(boolean enforceAspectRatios) {
        this.enforceAspectRatios = enforceAspectRatios;

        if (this.enforceAspectRatios) {
            cacheValid = false;
        }
    }

    /**
    * Returns the showArrowAnimations.
    *
    * @see #setShowArrowAnimations(boolean)
    * @return the showArrowAnimations
    */
    public boolean isShowArrowAnimations() {
        return showArrowAnimations;
    }

    /**
     * Sets the showArrowAnimations.
     *
     * @see #getShowArrowAnimations()
     * @param showArrowAnimations the showArrowAnimations
     */
    public void setShowArrowAnimations(boolean showArrowAnimations) {
        this.showArrowAnimations = showArrowAnimations;
    }

    /**
     * Returns the showArrows.
     *
     * @see #setShowArrows(boolean)
     * @return the showArrows
     */
    public boolean isShowArrows() {
        return showArrows;
    }

    /**
     * Sets the showArrows.
     *
     * @see #getShowArrows()
     * @param showArrows the showArrows
     */
    public void setShowArrows(boolean showArrows) {
        this.showArrows = showArrows;
    }

    /**
     * Returns the showLabels.
     *
     * @see #setShowLabels(boolean)
     * @return the showLabels
     */
    public boolean isShowLabels() {
        return showLabels;
    }

    /**
     * Sets the showLabels.
     *
     * @see #getShowLabels()
     * @param showLabels the showLabels
     */
    public void setShowLabels(boolean showLabels) {
        this.showLabels = showLabels;
    }

    /**
     * Returns the color of the 'current' locality marker.
     *
     * @see #setCurrentLocColor(Color)
     * @return the color
     */
    public Color getCurrentLocColor() {
        return currentLocMarker.getColor();
    }

    /**
     * Sets the color of the 'current' locality marker.
     *
     * @see #getCurrentLocColor()
     * @param currentLocColor the color
     */
    public void setCurrentLocColor(Color currentLocColor) {
        currentLocMarker.setColor(currentLocColor);
    }

    /**
     * Gets the size of the locality marker dot.
     *
     * @see #setDotSize(int)
     * @return the size
     */
    public int getDotSize() {
        return marker.getSize();
    }

    /**
     * Sets the size of the locality marker dot.
     *
     * @see #getDotSize()
     * @param dotSize the size
     */
    public void setDotSize(int dotSize) {
        marker.setSize(dotSize);
        currentLocMarker.setSize(dotSize);
    }

    /**
     * Returns the color of the locality marker icon.
     *
     * @see edu.ku.brc.ui.SimpleCircleIcon#getColor()
     * @see #setDotColor(Color)
     * @return the color
     */
    public Color getDotColor() {
        return marker.getColor();
    }

    /**
     * Sets the color of the locality marker icon.
     *
     * @see edu.ku.brc.ui.SimpleCircleIcon#setColor(java.awt.Color)
     * @see #getDotColor();
     * @param color the color
     */
    public void setDotColor(Color color) {
        marker.setColor(color);
    }

    /**
     * Returns the pixel locations of the markers.
     *
     * @return the <code>List</code> of marker locations
     */
    public List<Point> getMarkerLocations() {
        return markerLocations;
    }

    /**
     * Zooms the current map by the given percentage.
     *
     * @param percentZoom the zoom ratio
     */
    public void zoom(float percentZoom) {
        if (percentZoom == 1 || percentZoom <= 0) {
            // don't waste any time
            return;
        }
        double longRangeChange = mapLongRange * 1 / percentZoom;
        double longChange = .5 * (mapLongRange - longRangeChange);
        mapMinLong += longChange;
        mapMaxLong -= longChange;
        double latRangeChange = mapLatRange * 1 / percentZoom;
        double latChange = .5 * (mapLatRange - latRangeChange);
        mapMinLat += latChange;
        mapMaxLat -= latChange;
        cacheValid = false;
    }

    /**
     * Pans the current map the given number of degrees in each direction.
     *
     * @param latChange the amount of pan in the N/S direction
     * @param longChange the amount of pan in the E/W direction
     */
    public void pan(double latChange, double longChange) {
        double latChg = latChange;
        double longChg = longChange;
        if (mapMinLat + latChange < -90) {
            latChg = -90 - mapMinLat;
        }
        if (mapMaxLat + latChange > 90) {
            latChg = 90 - mapMaxLat;
        }
        if (mapMinLong + longChange < -180) {
            longChg = -180 - mapMinLong;
        }
        if (mapMaxLong + longChange > 180) {
            longChg = 180 - mapMaxLong;
        }

        mapMinLat += latChg;
        mapMaxLat += latChg;
        mapMinLong += longChg;
        mapMaxLong += longChg;
        cacheValid = false;
    }

    /**
     * Determines if the given map bounding box is valid.
     *
     * @param minimumLat min lat
     * @param minimumLong min long
     * @param maximumLat max lat
     * @param maximumLong max long
     * @return true if valid
     */
    protected boolean boxIsValid(double minimumLat, double minimumLong, double maximumLat, double maximumLong) {
        if (-90 <= minimumLat && minimumLat < maximumLat && maximumLat <= 90) {
            if (-180 <= minimumLong && minimumLong < maximumLong && maximumLong <= 180) {
                return true;
            }
        }
        return false;
    }

    /**
     * Gets the lat/long of a locality.  If the locality specifies a bounding box,
     * the center of the box is returned.
     *
     * @param loc the locality
     * @return the lat/long
     */
    private Pair<Double, Double> getLatLong(MapLocationIFace loc) {
        Double lat1 = loc.getLat1();
        Double long1 = loc.getLong1();
        Double lat2 = loc.getLat2();
        Double long2 = loc.getLong2();

        if (lat2 != null && long2 != null) {
            return centerOfBBox(lat1, lat2, long1, long2);
        }

        return new Pair<Double, Double>(lat1, long1);
    }

    /**
     * Returns the center of the bounding box described by the parameters.
     *
     * @param lat1 a latitude
     * @param lat2 a latitude
     * @param long1 a longitude
     * @param long2 a longitude
     * @return the lat/long center of the bounding box
     */
    private Pair<Double, Double> centerOfBBox(Double lat1, Double lat2, Double long1, Double long2) {
        Pair<Double, Double> center = new Pair<Double, Double>();
        center.first = (lat1 + lat2) / 2;
        center.second = (long1 + long2) / 2;
        return center;
    }

    /**
     * Returns the lat/long ratio of the current map parameters.
     *
     * @return the ratio
     */
    protected double getLatLongRatio() {
        double longRange = maxLong - minLong;
        double latRange = maxLat - minLat;
        return latRange / longRange;
    }

    /**
     * Calculates the proper bounding box to enclose the current set of
     * {@link MapLocationIFace}s.
     */
    protected void recalculateBoundingBox() {
        if (mapLocations.isEmpty()) {
            minLat = -90;
            minLong = -180;
            maxLat = 90;
            maxLong = 180;
            mapMinLat = -90;
            mapMinLong = -180;
            mapMaxLat = 90;
            mapMaxLong = 180;
            return;
        }

        // setup the minimums so they are guaranteed to be changed
        minLong = 180;
        maxLong = -180;
        minLat = 90;
        maxLat = -90;
        for (MapLocationIFace loc : mapLocations) {
            Pair<Double, Double> latLong = getLatLong(loc);
            if (latLong.first < minLat) {
                minLat = latLong.first;
            }
            if (latLong.second < minLong) {
                minLong = latLong.second;
            }
            if (latLong.first > maxLat) {
                maxLat = latLong.first;
            }
            if (latLong.second > maxLong) {
                maxLong = latLong.second;
            }
        }

        createBoundingBoxBufferRegion();

        expandMapRegionToFillUsableSpace();

        if (enforceAspectRatios) {
            ensureMinMaxAspectRatio();
        }
    }

    /**
     * Increases the size of the current bounding box in order to create
     * a bit of a buffer (and ensure the bounding box isn't a single point).
     */
    protected void createBoundingBoxBufferRegion() {
        //double minSpread = .025;
        double minSpread = .032;
        double latSpread = maxLat - minLat;
        if (latSpread < minSpread) {
            // expand the range to at least be .5 degrees
            double diff = minSpread - latSpread;
            latSpread = minSpread;
            mapMinLat = minLat - diff / 2;
            mapMaxLat = maxLat + diff / 2;
        } else {
            // just add 5% to each side
            mapMinLat = minLat - (.05 * latSpread);
            mapMaxLat = maxLat + (.05 * latSpread);
        }

        double longSpread = maxLong - minLong;
        if (longSpread < minSpread) {
            // expand the range to at least be .5 degrees
            double diff = minSpread - longSpread;
            longSpread = minSpread;
            mapMinLong = minLong - diff / 2;
            mapMaxLong = maxLong + diff / 2;
        } else {
            // just add 5% to each side
            mapMinLong = minLong - (.05 * longSpread);
            mapMaxLong = maxLong + (.05 * longSpread);
        }
    }

    /**
     * Modifies the bounding box to ensure that the minimum and maximum
     * aspect ratio limits are met.
     */
    protected void ensureMinMaxAspectRatio() {
        double currentWidth = mapMaxLong - mapMinLong;
        double currentHeight = mapMaxLat - mapMinLat;
        double currentAspectRatio = currentWidth / currentHeight;

        if (currentAspectRatio < minAspectRatio) {
            // increase aspect ratio to the minimum
            // we do this by increasing the current width (if possible)
            double newWidth = minAspectRatio * currentHeight;
            double amtOfIncr = newWidth - currentWidth;
            mapMinLong = mapMinLong - .5 * amtOfIncr;
            mapMaxLong = mapMaxLong + .5 * amtOfIncr;
        } else if (currentAspectRatio > maxAspectRatio) {
            // decrease aspect ratio to the maximum
            // we do this by increasing the current height (if possible)
            double newHeight = currentWidth / maxAspectRatio;
            double amtOfIncr = newHeight - currentHeight;
            mapMinLat = mapMinLat - .5 * amtOfIncr;
            mapMaxLat = mapMaxLat + .5 * amtOfIncr;
        }

        // At this point we have an expanded map that meets the aspect ratio requirements.
        // However, the map might not fit inside the [-180,180] / [-90,90] max bounding box.
        // So, we'll do some panning around, then cropping

        // pan right if needed, then left if needed, then crop the width
        if (mapMinLong < -180) {
            double panRightAmt = -180 - mapMinLong;
            mapMinLong += panRightAmt;
            mapMaxLong += panRightAmt;
        }

        if (mapMaxLong > 180) {
            double panLeftAmt = mapMaxLong - 180;
            mapMinLong -= panLeftAmt;
            mapMaxLong -= panLeftAmt;
        }

        mapMinLong = Math.max(mapMinLong, -180);
        mapMaxLong = Math.min(mapMaxLong, 180);

        // pan up if needed, then down if needed, then crop the height
        if (mapMinLat < -90) {
            double panUpAmt = -90 - mapMinLat;
            mapMinLat += panUpAmt;
            mapMaxLat += panUpAmt;
        }

        if (mapMaxLat > 90) {
            double panDownAmt = mapMaxLat - 90;
            mapMinLat -= panDownAmt;
            mapMaxLat -= panDownAmt;
        }

        mapMinLat = Math.max(mapMinLat, -90);
        mapMaxLat = Math.min(mapMaxLat, 90);

        // Now we have a bounding box that fits within the maximum bounding box, contains
        // all the provided georeferences, and is as close to the ratio limits as possible
    }

    /**
     * In order to ensure that we grab the largest map allowed, we need to expand either
     * then width or height to fill in the extra space.  So, we figure out which way the
     * map needs to be expanded, then adjust the bounding box to accomplish that.
     */
    protected void expandMapRegionToFillUsableSpace() {
        double degToPixelLat = (mapMaxLat - mapMinLat) / maxMapHeight;
        double degToPixelLon = (mapMaxLong - mapMinLong) / maxMapWidth;

        // we need a uniform deg/pixel ratio for both lat and long, so find the largest one and use it
        double uniformDegToPixel = Math.max(degToPixelLat, degToPixelLon);

        double correctedLatRange = uniformDegToPixel * maxMapHeight;
        double correctedLonRange = uniformDegToPixel * maxMapWidth;

        double latRangeDiff = correctedLatRange - (mapMaxLat - mapMinLat);
        double lonRangeDiff = correctedLonRange - (mapMaxLong - mapMinLong);

        // apply the corrections evenly
        mapMaxLat += latRangeDiff / 2;
        mapMinLat -= latRangeDiff / 2;
        mapMaxLong += lonRangeDiff / 2;
        mapMinLong -= lonRangeDiff / 2;
    }

    /**
     * Configures the {@link MapGrabber} with the given parameters and requests
     * a new map.
     *
     * @param host the map service host
     * @param defaultPathAndParams the URL path and default parameters
     * @param layers the map layers
     * @param minLat min map lat
     * @param minLong min map long
     * @param maxLat max map lat
     * @param maxLong max map long
     * @return the map image
     * @throws HttpException a network error occurred while grabbing a new map
     * @throws IOException a network error occurred while grabbing a new map
     */
    protected Image getMapFromService(final String host, final String defaultPathAndParams, final String layers,
            double miniLat, double miniLong, double maxiLat, double maxiLong, int height, int width)
            throws HttpException, IOException {
        MapGrabber mapGrabber = new MapGrabber();
        mapGrabber.setHost(host);
        mapGrabber.setDefaultPathAndParams(defaultPathAndParams);
        mapGrabber.setLayers(layers);

        mapGrabber.setMinLat(miniLat);
        mapGrabber.setMaxLat(maxiLat);
        mapGrabber.setMinLong(miniLong);
        mapGrabber.setMaxLong(maxiLong);

        mapGrabber.setHeight(height);
        mapGrabber.setWidth(width);
        return mapGrabber.getMap();
    }

    /**
     * Finds the pixel location on the map of the given map location.
     *
     * @param loc the map location
     * @return the pixel location
     */
    protected Point determinePixelCoordsOfMapLocationIFace(MapLocationIFace loc) {
        Pair<Double, Double> latLong = getLatLong(loc);
        double y = latLong.first - mapMinLat;
        double x = latLong.second - mapMinLong;
        y = mapHeight - y * pixelPerLatRatio;
        x = x * pixelPerLongRatio;
        return new Point((int) x, (int) y);
    }

    /**
     * Determines if a given pixel location falls on the map icon.
     *
     * @param x the x coordinate
     * @param y the y coordinate
     * @return true if (x,y) is on the map icon
     */
    protected boolean pointIsOnMapIcon(int x, int y) {
        if (mostRecentPaintedX > x || mostRecentPaintedX + mapWidth < x) {
            return false;
        }
        if (mostRecentPaintedY > y || mostRecentPaintedY + mapHeight < y) {
            return false;
        }
        return true;
    }

    /**
     * Returns the lat/long associated with the given pixel location.
     *
     * @param x the x coordinate
     * @param y the y coordinate
     * @return the lat/long
     */
    public Pair<Double, Double> getLatLongForPointOnMapIcon(int x, int y) {
        if (!pointIsOnMapIcon(x, y)) {
            return null;
        }
        // calculate the latitude
        double lat = -1;
        int relativeY = y - mostRecentPaintedY;
        lat = mapMaxLat - relativeY / pixelPerLatRatio;
        // calculate the longitude
        double lon = -1;
        int relativeX = x - mostRecentPaintedX;
        lon = relativeX / pixelPerLongRatio + mapMinLong;
        return new Pair<Double, Double>(lat, lon);
    }

    /**
     * Starts a thread to grab a new map.  When the process succeeds
     * or fails, <code>callback</code> will be notified.
     *
     * @param callback the object to notify when completed
     */
    public void getMap(final MapperListener callback) {
        Thread mapGrabberThread = new Thread("Map Grabber") //$NON-NLS-1$
        {
            @Override
            public void run() {
                try {
                    Icon map = grabNewMap();
                    if (callback != null) {
                        callback.mapReceived(map);
                    }
                } catch (Exception e) {
                    if (callback != null) {
                        callback.exceptionOccurred(e);
                    }
                }
            }
        };
        mapGrabberThread.setDaemon(true);
        mapGrabberThread.start();
    }

    /**
     * Grabs a new map from the web service and appropriately adorns it
     * with labels and markers.
     *
     * @return a map image as an icon
     * @throws HttpException a network error occurred while grabbing the map from the service
     * @throws IOException a network error occurred while grabbing the map from the service
     */
    protected Icon grabNewMap() throws HttpException, IOException {
        recalculateBoundingBox();

        if (!cacheValid) {
            //            Image mapImage = getMapFromService("mapus.jpl.nasa.gov",
            //                    "/wms.cgi?request=GetMap&srs=EPSG:4326&format=image/png&styles=visual",
            //                    "global_mosaic",
            //                    mapMinLat, mapMinLong, mapMaxLat, mapMaxLong, maxMapHeight, maxMapWidth);
            //
            //            Image overlayImage = getMapFromService("129.237.201.132",
            //                    "/cgi-bin/mapserv?map=/var/www/maps/specify.map&service=WMS&request=GetMap&srs=EPSG:4326&version=1.3.1&format=image/png&transparent=true",
            //                    "states,rivers",
            //                    mapMinLat, mapMinLong, mapMaxLat, mapMaxLong, maxMapHeight, maxMapWidth);

            Image mapImage = getMapFromService("lifemapper.org", //$NON-NLS-1$
                    "/ogc?map=specify.map&service=WMS&request=GetMap&srs=EPSG:4326&version=1.3.1&STYLES=&format=image/png&transparent=TRUE", //$NON-NLS-1$
                    "global_mosaic,states,rivers", //$NON-NLS-1$
                    mapMinLat, mapMinLong, mapMaxLat, mapMaxLong, maxMapHeight, maxMapWidth);

            mapIcon = new ImageIcon(mapImage);
            //            overlayIcon = new ImageIcon(overlayImage);
            cacheValid = true;

            mapWidth = mapIcon.getIconWidth();
            mapHeight = mapIcon.getIconHeight();

            if (mapWidth < 0 || mapHeight < 0) {
                throw new IOException("Request for map failed.  Received map has negative width or height."); //$NON-NLS-1$
            }

            mapLatRange = mapMaxLat - mapMinLat;
            mapLongRange = mapMaxLong - mapMinLong;

            pixelPerLatRatio = mapHeight / mapLatRange;
            pixelPerLongRatio = mapWidth / mapLongRange;

            for (int i = 0; i < mapLocations.size(); ++i) {
                MapLocationIFace loc = mapLocations.get(i);
                Point iconLoc = determinePixelCoordsOfMapLocationIFace(loc);
                markerLocations.set(i, iconLoc);
            }

            cacheValid = true;
        }

        Icon icon = new Icon() {
            public void paintIcon(Component c, Graphics g, int x, int y) {
                // this helps keep the labels inside the map
                g.setClip(x, y, mapWidth, mapHeight);
                // log the x and y for the MouseMotionListener
                mostRecentPaintedX = x;
                mostRecentPaintedY = y;
                Point currentLocPoint = null;
                if (currentLoc != null) {
                    currentLocPoint = determinePixelCoordsOfMapLocationIFace(currentLoc);
                }

                mapIcon.paintIcon(c, g, x, y);
                //                overlayIcon.paintIcon(c, g, x, y);

                Point lastLoc = null;
                for (int i = 0; i < mapLocations.size(); ++i) {
                    Point markerLoc = markerLocations.get(i);
                    String label = labels.get(i);
                    boolean current = (currentLoc != null) && markerLoc.equals(currentLocPoint);

                    if (markerLoc == null) {
                        log.error("A marker location is null"); //$NON-NLS-1$
                        continue;
                    }
                    if (!pointIsOnMapIcon(x + markerLoc.x, y + markerLoc.y)) {
                        log.error("A marker location is off the map"); //$NON-NLS-1$
                        continue;
                    }
                    if (showArrows && lastLoc != null) {
                        int x1 = x + lastLoc.x;
                        int y1 = y + lastLoc.y;
                        int x2 = x + markerLoc.x;
                        int y2 = y + markerLoc.y;
                        Color origColor = g.getColor();
                        if (current && !animationInProgress) {
                            g.setColor(getCurrentLocColor());
                        } else {
                            g.setColor(arrowColor);
                        }
                        GraphicsUtils.drawArrow(g, x1, y1, x2, y2, 2, 2);
                        g.setColor(origColor);
                    }
                    if (current) {
                        currentLocMarker.paintIcon(c, g, markerLoc.x + x, markerLoc.y + y);
                    } else {
                        marker.paintIcon(c, g, markerLoc.x + x, markerLoc.y + y);
                    }
                    if (label != null) {
                        Color origColor = g.getColor();
                        FontMetrics fm = g.getFontMetrics();
                        int length = fm.stringWidth(label);
                        g.setColor(Color.WHITE);
                        g.fillRect(markerLoc.x + x - (length / 2), markerLoc.y + y - (fm.getHeight() / 2), length,
                                fm.getHeight());
                        g.setColor(labelColor);
                        GraphicsUtils.drawCenteredString(label, g, markerLoc.x + x, markerLoc.y + y);
                        g.setColor(origColor);
                    }

                    lastLoc = markerLoc;
                }
                if (showArrowAnimations && animationInProgress) {
                    int startIndex = mapLocations.indexOf(animStartLoc);
                    int endIndex = mapLocations.indexOf(animEndLoc);
                    if (startIndex != -1 && endIndex != -1) {
                        Point startPoint = markerLocations.get(startIndex);
                        Point endPoint = markerLocations.get(endIndex);
                        Point arrowEnd = GraphicsUtils.getPointAlongLine(startPoint, endPoint, percent);
                        Color orig = g.getColor();
                        g.setColor(getCurrentLocColor());
                        GraphicsUtils.drawArrow(g, startPoint.x + x, startPoint.y + y, arrowEnd.x + x,
                                arrowEnd.y + y, 5, 3);
                        g.setColor(orig);
                    }
                }
            }

            public int getIconWidth() {
                return mapWidth;
            }

            public int getIconHeight() {
                return mapHeight;
            }
        };

        return icon;
    }

    /* (non-Javadoc)
     * @see org.jdesktop.animation.timing.TimingTarget#begin()
     */
    public void begin() {
        this.percent = 0;
        this.animationInProgress = true;
    }

    /* (non-Javadoc)
     * @see org.jdesktop.animation.timing.TimingTarget#end()
     */
    public void end() {
        this.animationInProgress = false;
    }

    /* (non-Javadoc)
     * @see org.jdesktop.animation.timing.TimingTarget#repeat()
     */
    public void repeat() {
        // TODO Auto-generated method stub

    }

    /* (non-Javadoc)
     * @see org.jdesktop.animation.timing.TimingTarget#timingEvent(float)
     */
    public void timingEvent(float fraction) {
        this.percent = fraction;
    }

    /**
     * Defines requirements for objects that receive callbacks from a 
     * <code>LocalityMapper</code> instance about the state of the map
     * generation process.
     * 
     * @author jstewart
      * @code_status Complete
     */
    public interface MapperListener {
        /**
         * Signals that a map was successfully retrieved.
         * 
         * @param map the map
         */
        public void mapReceived(Icon map);

        /**
         * Signals that an exception occurred while grabbing map.
         * 
         * @param e the <code>Exception</code> that occurred during map grabbing operations
         */
        public void exceptionOccurred(Exception e);
    }

    /**
     * This interface defines the minimal information retrievable from any object that can
     * be mapped by a {@link LocalityMapper}.
     * 
     * @author jstewart
     * @code_status Complete
     */
    public static interface MapLocationIFace {
        /**
         * Returns the latitude of the point or the 'upper-left corner' of the bounding box.
         * 
         * @return the latitude of the point or the 'upper-left corner' of the bounding box
         */
        public Double getLat1();

        /**
         * Returns the longitude of the point or the 'upper-left corner' of the bounding box.
         * 
         * @return the longitude of the point or the 'upper-left corner' of the bounding box
         */
        public Double getLong1();

        /**
         * Returns the latitude of the 'lower-right corner' of the bounding box.
         * 
         * @return the latitude of the 'lower-right corner' of the bounding box
         */
        public Double getLat2();

        /**
         * Returns the longitude of the 'lower-right corner' of the bounding box.
         * 
         * @return the longitude of the 'lower-right corner' of the bounding box
         */
        public Double getLong2();

    }

    //    // I commented out this code so it doesn't get compiled in.  However, if I ever need to come back and retest this class, I'll need this again.
    //    public static void main(String[] args)
    //    {
    //        // some test points
    ////        SimpleMapLocation l1 = new SimpleMapLocation(38.877,-94.871,null,null);
    ////        SimpleMapLocation l2 = new SimpleMapLocation(38.875,-94.875,null,null);
    ////        SimpleMapLocation l3 = new SimpleMapLocation(38.879,-94.877,null,null);
    ////        SimpleMapLocation l4 = new SimpleMapLocation(38.871,-94.879,null,null);
    //
    //        SimpleMapLocation l1 = new SimpleMapLocation(38.662,-95.574,null,null);
    ////        SimpleMapLocation l2 = new SimpleMapLocation(38.875,-94.875,null,null);
    ////        SimpleMapLocation l3 = new SimpleMapLocation(38.879,-94.877,null,null);
    ////        SimpleMapLocation l4 = new SimpleMapLocation(38.871,-94.879,null,null);
    //
    //        // this spot gives the WMS server some problems for some reason
    //        // it returns a totally transparent image
    //        SimpleMapLocation l1 = new SimpleMapLocation(39.0247,-95.5,null,null);
    //        LocalityMapper lm = new LocalityMapper();
    //        lm.addLocationAndLabel(l1, null);
    ////        lm.addLocationAndLabel(l2, null);
    ////        lm.addLocationAndLabel(l3, null);
    ////        lm.addLocationAndLabel(l4, null);
    //        lm.setShowArrows(false);
    //        lm.setDotColor(Color.YELLOW);
    //     
    //        JFrame f = new JFrame();
    //        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    //        final JLabel mapLabel = new JLabel();
    //        int w = 500;
    //        int h = 500;
    //        Dimension dimension = new Dimension(w,h);
    //        mapLabel.setSize(dimension);
    //        mapLabel.setPreferredSize(dimension);
    //        f.add(mapLabel);
    //        
    //        lm.setMaxMapWidth(w);
    //        lm.setMaxMapHeight(h);
    //
    //        MapperListener l = new MapperListener()
    //        {
    //            public void exceptionOccurred(Exception e)
    //            {
    //                final StringWriter result = new StringWriter();
    //                final PrintWriter printWriter = new PrintWriter(result);
    //                e.printStackTrace(printWriter);
    //
    //                mapLabel.setText("<html><pre> " + result.toString() + "</pre></html>");
    //            }
    //            public void mapReceived(Icon map)
    //            {
    //                mapLabel.setIcon(map);
    //            }
    //        };
    //        
    //        f.pack();
    //
    //        lm.getMap(l);
    //        
    //        f.setVisible(true);
    //    }
}