package haxby.map;

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.io.BufferedOutputStream;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.text.NumberFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

import javax.imageio.ImageIO;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.undo.UndoManager;

import org.geomapapp.credit.Credit;
import org.geomapapp.gis.shape.ESRIShapefile;
import org.geomapapp.grid.Grid2DOverlay;
import org.geomapapp.grid.GridDialog;
import org.geomapapp.util.XML_Menu;

import haxby.proj.CylindricalProjection;
import haxby.proj.Mercator;
import haxby.proj.Projection;
import haxby.util.ScaledComponent;

//import com.sun.image.codec.jpeg.JPEGCodec;
//import com.sun.image.codec.jpeg.JPEGImageEncoder;

/**
 	XMap loads and manipulates the map.
 */
public class XMap extends ScaledComponent implements Zoomable,
				Scrollable, Printable, AdjustmentListener, MouseListener, MouseMotionListener, KeyListener {

	Projection proj;

	// GMA 1.6.4: Variables added to record values when mouse button is pressed (for panning)
	/**
	 * Pressed x value
	 */
	protected int pressedX = 0;

	/**
	 * Pressed y value
	 */
	protected int pressedY = 0;

	/**
	 * Pressed x value on screen
	 */
	protected int screenPressedX = 0;

	/**
	 * Pressed y value on screen
	 */
	protected int screenPressedY = 0;
	// GMA 1.6.4

	/**
	 * Base cursor, Used to override defaultCursor settings
	 */
	protected Cursor baseCursor = Cursor.getDefaultCursor();

	/**
	 * Base map; Optional.
	 */
	protected BufferedImage image = null;

	/**
	 * Overlay objects added to the map.
	 */
	protected Vector<Overlay> overlays;

	/**
	 * A mapping of Overlay to Floats for the alpha values to draw each overlay with
	 */
	protected Map<Overlay, Float> overlayAlphas;

	/**
	 * Not implemented.
	 */
	protected Vector mapInsets;

	protected MapTools tools;
	/**
	 * Scale factor
	 */
	protected double zoom;

	/**
	 * Base units of grid
	 */
	protected String units = "m";

	/**
	 * Alternative z value to be displayed
	 */
	protected double alternateZ = Double.NaN;

	/**
	 * Alternative units to be displayed (associated with alternateZ)
	 */
	protected String alternateUnits = "m";

	/**
	 * Alternative z value to be displayed
	 */
	protected float alternate2Z = Float.NaN;

	/**
	 * Alternative units to be displayed (associated with alternateZ)
	 */
	protected String alternate2Units = "m";

	/**
	 * Angle of ratation; Not implemented.
	 */
	protected int rotation;

	/**
	 * Dimension of map when zoom = 0;
	 */
	protected int width, height;

	/**
	 	Used to scroll the map.
	 */
	protected JScrollPane scrollPane = null;

	/**
	 * MapBorder draws Latitude/Longitude annotations on the border.
	 */
	protected MapBorder mapBorder = null;

	/**
	 * Plot the Latitude/Longitude lines.
	 */
	protected boolean graticule = true;

	/**
	 * For CylindricalProjection, wrap = nodes per 360 degrees; otherwise wrap = -1.
	 */
	protected double wrap;
	Object app;
	Grid2DOverlay focus = null;

	/**
	 * Paint insets on map if true, do not paint insets on map if false.
	 */
	protected boolean includeInsets = true;
	protected boolean spaceBarDown = false;
	protected String pastZoom = "0", nextZoom;

	/**
	 * UndoManager to manage zoom actions
	 */
	protected static UndoManager undoManager;
	public static JTextField zoomActionTrack = new JTextField(80);

	public XMap(Object app, Projection proj, BufferedImage image) {
		this.app = app;
		this.proj = proj;
		this.image = image;
		width = image.getWidth();
		height = image.getHeight();
		overlays = new Vector();
		overlayAlphas = new HashMap<Overlay, Float>();
		mapInsets = null;
		zoom = 1d;
		rotation = 0;
		try {
			CylindricalProjection p = (CylindricalProjection) proj;
			wrap = Math.rint(360.*(p.getX(10.)-p.getX(9.)));
		} catch (ClassCastException ex) {
			wrap = -1.;
		}

		// GMA 1.6.4: Add mouse motion listener to perform pan, mouse listener to perform center
		addMouseMotionListener(this);
		addMouseListener(this);
		undoManager = new UndoManager();
		undoManager.setLimit(8);
		zoomActionTrack.getDocument().addUndoableEditListener(undoManager);
	}

	public XMap(Object app, Projection proj, int width, int height) {
		this.app = app;
		this.proj = proj;
		this.width = width;
		this.height = height;
		overlays = new Vector();
		overlayAlphas = new HashMap<Overlay, Float>();
		mapInsets = null;
		zoom = 1d;
		setLayout( null );
	//	addMapInset( new haxby.image.Logo(this) );
		try {
			CylindricalProjection p = (CylindricalProjection) proj;
			wrap = Math.rint(360.*(p.getX(10.)-p.getX(9.)));
		} catch (ClassCastException ex) {
			wrap = -1.;
		}

		// GMA 1.6.4: Add mouse motion listener to perform pan, mouse listener to perform center
		addMouseMotionListener(this);
		addMouseListener(this);
		addKeyListener(this);
		undoManager = new UndoManager();
		undoManager.setLimit(8);
		zoomActionTrack.getDocument().addUndoableEditListener(undoManager);
	}

	/**
	 *	Assigns the scrollPane.
	 */
	public void addNotify() {
		super.addNotify();
		Container c = getParent();
		while(c != null) {
			if( c instanceof JScrollPane ) {
				scrollPane = (JScrollPane) c;

//				***** GMA 1.6.4: Add adjustment listeners to scroll bars to perform pan
				scrollPane.getHorizontalScrollBar().addAdjustmentListener(this);
				scrollPane.getVerticalScrollBar().addAdjustmentListener(this);

				return;
			}
			c = c.getParent();
		}
	}

	/**
	Get wrap value.
	 * @return wrap value.
	 */
	public double getWrap() {
		return wrap;
	}

	public MapTools getMapTools() {
		if( app instanceof MapApp ) {
			tools = ((MapApp)app).tools;
		}
		return tools;
	}

	public BufferedImage getBaseMap(){
		if( app instanceof MapApp ) {
			return ((MapApp)app).getBaseMap();
		}
		return null;
	}

	public Credit getCredit() {
		if( app instanceof MapApp ) {
			return ((MapApp)app).credit;
		}
		return null;
	}

	/**
	 *	Sets the map border.
	 *	@param border map border to set.
	 */
	public void setMapBorder( MapBorder border ) {
		mapBorder = border;
	}

	/**
		Get the map border.
		@return the map border.
	 */
	public MapBorder getMapBorder() {
		return mapBorder;
	}

	/**
	 	Gets the default height/width.
	 	@return the default height/width in a dimension.
	 */
	public Dimension getDefaultSize() {
		return new Dimension( width, height );
	}

	/**
	 	Gets the Preferred Size of map.
	 	@return the perferred size of the map.
	 */
	public Dimension getPreferredSize() {
		Dimension dim = new Dimension( (int) Math.ceil( zoom*(double)width ),
					(int) Math.ceil( zoom*(double)height ));
		if(rotation%2==1) {
			int tmp = dim.width;
			dim.width = dim.height;
			dim.height = tmp;
		}
		if(mapBorder != null) {
			Insets ins = mapBorder.getBorderInsets(this);
			dim.width += ins.left + ins.right;
			dim.height += ins.top + ins.bottom;
		}
		return dim;
	}

	/**
	 	If there is an overlay.
	 	@return If there is an overlay.
	 */
	public boolean hasOverlay( Overlay overlay ) {
		return overlays.contains( overlay );
	}

	/**
	 	Adds Overlay object to wrap.
	 	@param overlay Overlay object to add.
	 */
	public void addOverlay( int index, Overlay overlay ) {
		addOverlay("Overlay", overlay, index, null, true, null);
	}

	public void addOverlay( int index, Overlay overlay, boolean fireChange) {
		addOverlay("Overlay", overlay, index, null, fireChange, null);
	}

	public void addOverlay(Overlay overlay, boolean fireChange) {
		addOverlay("Overlay", overlay, fireChange, null);
	}

	public int getOverlaysSize() {
		return overlays.size();
	}

	public float getOverlayAlpha(Overlay overlay) {
		if (!overlayAlphas.containsKey(overlay))
			return Float.NaN;
		return overlayAlphas.get(overlay);
	}

	public void setOverlayAlpha(Overlay overlay, float alpha) {
		if (overlayAlphas.containsKey(overlay))
			overlayAlphas.put(overlay, alpha);
	}

	public int getOverlayIndex(Overlay overlay) {
		return overlays.indexOf(overlay);
	}

	public void addOverlay( Overlay overlay ) {
		addOverlay("Overlay", overlay, overlays.size(), null, true, null);
	}

	public void addOverlay( String name, String infoURLString, Overlay overlay ) {
		addOverlay(name, overlay, overlays.size(), infoURLString, true, null);
	}

	public void addOverlay( String name, String infoURLString, Overlay overlay, XML_Menu xml_item ) {
		addOverlay(name, overlay, overlays.size(), infoURLString, true, xml_item);
	}

	public void addOverlay( String name, Overlay overlay ) {
		addOverlay(name, overlay, overlays.size(), null, true, null);
	}

	public void addOverlay(String name, Overlay overlay, XML_Menu xml_item){
		addOverlay(name, overlay, overlays.size(), null, true, xml_item);
	}

	public void addOverlay( String name, Overlay overlay, int index ) {
		addOverlay(name, overlay, index, null, true, null);
	}

	public void addOverlay( String name, Overlay overlay, boolean fireChange) {
		addOverlay(name, overlay, overlays.size(), null, fireChange, null);
	}

	public void addOverlay( String name, Overlay overlay, boolean fireChange, XML_Menu xml_item ) {
		addOverlay(name, overlay, overlays.size(), null, fireChange, xml_item);
	}

	public void addOverlay( String name, Overlay overlay, int index, String infoURLString, boolean fireChange) {
		addOverlay(name, overlay, index, infoURLString, true, null);
	}

	public void addOverlay( String name, Overlay overlay, int index, String infoURLString, boolean fireChange, XML_Menu xml_item) {
		if( overlays.contains(overlay) )return;
		if ( index > (overlays.size() - 1) ) {
			overlays.add(overlay);
		} else {
			overlays.add(index,overlay);
		}
		overlayAlphas.put(overlay, 1f);
		if( overlay instanceof Grid2DOverlay ) {
			focus=(Grid2DOverlay)overlay;
		} else if (overlay instanceof ESRIShapefile && ((ESRIShapefile) overlay).getMultiGrid() != null) {
			focus = ((ESRIShapefile) overlay).getMultiGrid().getGrid2DOverlay();
		}
		
		if (fireChange){
			if(xml_item == null){
				// xml null
				this.firePropertyChange(name, infoURLString, overlay);
			}
			if(xml_item != null){
				// xml not null
				if(infoURLString == null){
					//if infoURLString is null, then use the name parameter
					this.firePropertyChange(name, xml_item, overlay);
				} else {
					this.firePropertyChange(infoURLString, xml_item, overlay);
				}
			}
		}
	}

	public void moveOverlayToFront(Overlay overlay) {
		overlays.remove(overlay);
		overlays.add(overlay);
	}

	public Grid2DOverlay getFocus() {
		return focus;
	}

	public void setIncludeInsets( boolean input ) {
		includeInsets = input;
	}

	public void setFocus( Grid2DOverlay newFocus ) {
		focus = newFocus;
	}

	/**
	 * Removes Overlay object from wrap.
	 * @param overlay Overlay object to remove.
	 */
	public int removeOverlay( Overlay overlay ) {
		return removeOverlay(overlay, true);
	}

	public int removeOverlay( Overlay overlay, boolean removeFlag ) {
		int index = overlays.indexOf(overlay);
		overlays.remove( overlay );
		overlayAlphas.remove(overlay);

		if (overlay == focus) {
			for( int i=overlays.size()-1 ; i>-1 ; i--) {
				if( overlays.get(i) instanceof Grid2DOverlay) {
					focus = (Grid2DOverlay)overlays.get(i);
					break;
				}
			}
		}
		this.firePropertyChange("overlays", overlay, removeFlag);
		return index;
	}

	/**
	 * Not Implemented.
	 */
	public void rotate(int direction) {
		Dimension dim = getPreferredSize();
		Rectangle rect = getVisibleRect();
		if(dim.width>rect.width) rect.width = dim.width;
		if(dim.height>rect.height) rect.height = dim.height;
		if(proj instanceof CylindricalProjection ) return;
		rotation += (direction>0) ? 1:3;
		rotation &= 0x3;
		JScrollBar hsb = scrollPane.getHorizontalScrollBar();
		int x = hsb.getValue();
		JScrollBar vsb = scrollPane.getVerticalScrollBar();
		int y = vsb.getValue();
		if(direction>0) {
			hsb.setValue(dim.height - rect.height - y);
			vsb.setValue(x);
		} else {
			hsb.setValue(y);
			vsb.setValue(dim.width -rect.width - x);
		}
		revalidate();
		repaint();
	}

	/**
	 * Zoom in to point p.
	 * @param point to zoom to.
	 */
	public void zoomIn( Point p ) {
		doZoom( p, 2d );
	}
	
	/**
	 * Zoom in to point p.with speed
	 * @param point to zoom to.
	 * @param double zoom speed.
	 */
	public void zoomSpeed( Point p, Double d ) {
		doZoom( p, d );
	}

	/**
	 	Zoom out from point p.
	 	@param point to zoom from.
	 */
	public void zoomOut( Point p ) {
		doZoom( p, .5d );
	}

	/**
	 * Set if the graticule should be displayed.
	 * @param tf True if graticul should be displayed.
	 */
	public void setGraticule( boolean tf ) {
		if( graticule==tf )return;
		graticule = tf;
		repaint();
	}

	public void zoomToWESN(double[] wesn) {
		if (wesn[0] >= wesn[1] ||
				wesn[2] >= wesn[3]) return;

		if (wesn[1] - wesn[0] >= 300) { // Zoom to World
			Point2D.Double p = (Point2D.Double) 
				proj.getMapXY(180, (wesn[2] + wesn[3]) / 2);

			p.x *= zoom;
			p.y *= zoom;
			Insets insets = getInsets();
			p.x += insets.left;
			p.y += insets.top;
			doZoom(p, 1 / zoom);
			return;
		}

		if (proj instanceof Mercator) {
			wesn[2] = Math.max(wesn[2], -80);
			wesn[3] = Math.min(wesn[3], 80);
		}

		Point2D[] pts = new Point2D[] {
				proj.getMapXY(wesn[0],wesn[3]),
				proj.getMapXY(wesn[1],wesn[3]),
				proj.getMapXY(wesn[0],wesn[2]),
				proj.getMapXY(wesn[1],wesn[2])};

		if (wrap > 0) {
			while (pts[0].getX() > pts[1].getX())
				pts[0].setLocation(pts[0].getX() - wrap, pts[0].getY());

			while (pts[2].getX() > pts[3].getX())
				pts[2].setLocation(pts[2].getX() - wrap, pts[2].getY());
		}

		double minX, minY;
		double maxX, maxY;
		minX = minY = Double.MAX_VALUE;
		maxX = maxY = -Double.MAX_VALUE;
		for (int i = 0; i < pts.length; i++) {
			minX = Math.min(minX, pts[i].getX());
			minY = Math.min(minY, pts[i].getY());
			maxX = Math.max(maxX, pts[i].getX());
			maxY = Math.max(maxY, pts[i].getY());
		}

		while (minX < 0 && wrap > 0) {
			minX += wrap;
			maxX += wrap;
		}

		Rectangle2D.Double bounds = new Rectangle2D.Double();
		bounds.x = minX;
		bounds.y = minY;

		bounds.width = maxX - minX;
		bounds.height = maxY - minY;

		bounds.x -= bounds.width * .1;
		bounds.y -= bounds.height* .1;

		bounds.width += bounds.width * .2;
		bounds.height -= bounds.height * .1;

		zoomToRect(bounds);
	}

	public void zoomToRect( Rectangle2D rect ) {
		Point2D.Double p = new Point2D.Double( rect.getX() + rect.getWidth()/2., rect.getY() + rect.getHeight()/2. );
		p.x *= zoom;
		p.y *= zoom;
		Insets insets = new Insets(0,0,0,0);
		if(mapBorder!=null) insets = mapBorder.getBorderInsets(this);
		p.x += insets.left;
		p.y += insets.top;
		Rectangle r = getVisibleRect();
		r.width -= insets.left + insets.right;
		r.height -= insets.top + insets.bottom;
		double factor = Math.min( r.getWidth()/(zoom*rect.getWidth()),
					r.getHeight()/(zoom*rect.getHeight()) );
		// default min zoom is one.
		if(factor < 1) {
			factor = 1;
		}
		doZoom( p, factor );
	}
	/**
	 	Zoom to a rectangle defined by a mouse
	 	@param rect Rectangled defined by the mouse
	 */
	public void zoomTo( Rectangle rect ) {
		if(rect.width<10 || rect.height<10) return;
		Point p = new Point( rect.x + rect.width/2, rect.y + rect.height/2 );
		Rectangle r = getVisibleRect();
		Insets insets = new Insets(0,0,0,0);
		if(mapBorder!=null) insets = mapBorder.getBorderInsets(this);
		r.width -= insets.left + insets.right;
		r.height -= insets.top + insets.bottom;
		double factor = Math.min( r.getWidth()/rect.getWidth(),
					r.getHeight()/rect.getHeight() );
		doZoom( p, factor );
	}

	/**
	 	Zoom by factor and center on point p.
	 	@param p point to center on.
	 	@param factor Factor to zoom by.
	 */
	public void doZoom( Point2D p, double factor ) {
		AffineTransform ATrans = new AffineTransform();
		ATrans.scale( zoom, zoom );
		Insets insets = getInsets();
		Rectangle rect = getVisibleRect();
		Dimension dim = getParent().getSize();
		if( dim.width>rect.width) rect.width = dim.width;
		if( dim.height>rect.height) rect.height = dim.height;
		rect.width -= insets.left + insets.right;
		rect.height -= insets.top + insets.bottom;
		double nX = (p.getX() - insets.left - (rect.width/(2.*factor)));
		double nY = ( p.getY() - insets.top - rect.height/(2.*factor));
		Point2D newP = null;
		try {
			newP = ATrans.inverseTransform( new Point2D.Double(nX, nY), null );
		} catch (Exception ex ) {
			return;
		}
		ATrans.scale( factor, factor );
		zoom *= factor;
		newP = ATrans.transform( newP, null );
		int newX = (int)newP.getX(); // + insets.left;
		int newY = (int)newP.getY(); // + insets.top;
		invalidate();
		scrollPane.validate();
		JScrollBar sb = scrollPane.getHorizontalScrollBar();
		sb.setValue(newX);
		sb = scrollPane.getVerticalScrollBar();
		sb.setValue(newY);
		revalidate();

		//If this is MapApp Auto Focus
		if (app instanceof MapApp) {
			((MapApp) app).autoFocus();
		}
	}

	public void setZoomHistoryPast(XMap mapSource) {
		Rectangle2D r = null;
		r = mapSource.getClipRect2D();
		Point2D.Double p2 = new Point2D.Double(
				r.getX()+.5*r.getWidth(),
				r.getY()+.5*r.getHeight() );
		p2 = (Point2D.Double)mapSource.getProjection().getRefXY(p2);

		double zoom = mapSource.getZoom();
		NumberFormat fmtZoom1 = NumberFormat.getInstance();
		fmtZoom1.setMinimumFractionDigits(1);
		fmtZoom1.format(zoom);
		// set zoom
		if (getZoomHistoryPast().contentEquals("0")) {
			pastZoom = "past, " + p2.getX() + ", " + p2.getY() + ", " + zoom;
			zoomActionTrack.selectAll();
			zoomActionTrack.replaceSelection(pastZoom);
			//System.out.println("jtext " + zoomActionTrack.getText());
		}
	}

	public String getZoomHistoryPast() {
		if(pastZoom.contentEquals("0")) {
			pastZoom = "0";
		}
		return pastZoom;
	}

	public void setZoomHistoryNext(XMap mapSource) {
		Rectangle2D r = null;
		r = mapSource.getClipRect2D();
		Point2D.Double p2 = new Point2D.Double(
				r.getX()+.5*r.getWidth(),
				r.getY()+.5*r.getHeight() );
		p2 = (Point2D.Double)mapSource.getProjection().getRefXY(p2);

		double zoom = mapSource.getZoom();
		NumberFormat fmtZoom1 = NumberFormat.getInstance();
		fmtZoom1.setMinimumFractionDigits(1);
		fmtZoom1.format(zoom);

		nextZoom = "next, " + p2.getX() + ", " + p2.getY() + ", " + zoom;
		zoomActionTrack.selectAll();
		zoomActionTrack.replaceSelection(nextZoom);
		//System.out.println("track: " + zoomActionTrack.getText());
	}

	public String getZoomHistoryNext() {
		return nextZoom;
	}

	public void updateZoomHistory(String past, String next) throws IOException {
		doZoomHistory(past, next);
	}

	protected void doZoomHistory(String past, String next) throws IOException {

		if (app instanceof MapApp) {
			if(((MapApp) app).historyFile.exists()) {
				((MapApp) app).startNewZoomHistory();
			}

			if(((MapApp) app).historyFile.canWrite()) {
				BufferedWriter bw = new BufferedWriter(new FileWriter(((MapApp) app).historyFile.getAbsoluteFile(), false));
				bw.write(past + "\n");
				bw.write(next);
				bw.close();
			}
		}
	}

	public double[] getScales() {
		return new double[] { zoom, zoom};
	}

	public Insets getInsets() {
		if(mapBorder != null) {
			return mapBorder.getBorderInsets(this);
		}
		return super.getInsets();
	}

	/**
	 	Gets the zoom factor;
	 	@return the zoom factor.
	 */
	public double getZoom() {
		return zoom;
	}

	/**
	 	Rectangle to zoom to.
	 */
	Rectangle zoomRect = null;
	
	/**
	 	Sets the rectangle to zoom to.
	 	@param Rectangle to zoom to.
	 */
	public void setRect(Rectangle rect) {
		synchronized (getTreeLock()) {
			Graphics2D g = (Graphics2D)getGraphics();
			g.setXORMode(Color.white);
			if(zoomRect!=null) g.draw(zoomRect);
			zoomRect = rect;
			if(zoomRect!=null) g.draw(zoomRect);
		}
	}

	public JLabel getInfoLabel() {
		if( app instanceof MapApp ) {
			try {
				return ((MapApp)app).tools.info;
			} catch(Exception ex) {
				return null;
			}
		} else {
			return null;
		}
	}

	public boolean isSelectable() {
		return getMapTools().isSelectable();
	}

	/**
	 * Sets the units displayed above the map
	 * @param newUnits
	 */
	// 1.3.5: Sets the units displayed above the map next to the zoom factor
	public void setUnits( String newUnits )	{
		units = newUnits;
	}

	// GMA 1.6.2: Functions to change alternative units and z value
	public void setAlternateZValue( double inputAlternateZ ) {
		alternateZ = inputAlternateZ;
	}

	public void setAlternateUnits( String inputAlternateUnits ) {
		alternateUnits = inputAlternateUnits;
	}

	public void setAlternate2ZValue( float inputAlternateZ ) {
		alternate2Z = inputAlternateZ;
	}

	public void setAlternate2Units( String inputAlternateUnits ) {
		alternate2Units = inputAlternateUnits;
	}

	/**
	 	Sets the coordinates of mouse to be displayed.
	 	@param p mouse location.
	 */
	public void setXY( Point p ) {
		if( p==null ) {
			if( app instanceof MapApp ) {
				((MapApp)app).tools.info.setText("");
			} else if(app instanceof PolarMapApp ) {
				((PolarMapApp)app).tools.info.setText("");
			}
			return;
		}
		Point2D.Double pt = (Point2D.Double)proj.getRefXY( getScaledPoint(p));
		setLonLat( pt );
	}
	public void setLonLat( double lon, double lat) {
		setLonLat( new Point2D.Double(lon, lat) );
	}
	void setLonLat( Point2D.Double pt ) {
		Point2D.Double pt0 = new Point2D.Double(pt.x, pt.y);
		while( pt.x>180. ) pt.x-=360.;
		String ew = "E";
		String ns = "N";
		if(pt.x<0.) {
			pt.x = -pt.x;
			ew = "W";
		}
		if(pt.y<0.) {
			pt.y = -pt.y;
			ns = "S";
		}
		NumberFormat fmt = NumberFormat.getInstance();
		fmt.setMaximumFractionDigits(3);
		fmt.setMinimumFractionDigits(3);
		NumberFormat fmt1 = NumberFormat.getInstance();
		fmt1.setMaximumFractionDigits(1);
		fmt1.setMinimumFractionDigits(1);

//		***** GMA 1.6.4: Allow as many digits as needed for XMCS two-way travel time
		NumberFormat fmt2 = NumberFormat.getInstance();
		fmt2.setMaximumFractionDigits(3);
		fmt2.setMinimumFractionDigits(3);

//		GMA 1.4.8: Format for minutes and for degrees with minutes listed
		NumberFormat fmtMins = NumberFormat.getInstance();
		fmtMins.setMaximumFractionDigits(0);
		fmtMins.setMinimumFractionDigits(0);
		NumberFormat fmtDegsMins = NumberFormat.getInstance();
		fmtDegsMins.setMaximumFractionDigits(0);
		fmtDegsMins.setMinimumFractionDigits(0);

		if( app instanceof MapApp ) {
			Grid2DOverlay focus = null;
			for( int i=overlays.size()-1 ; i>-1 ; i--) {
				if (overlays.get(i) instanceof ESRIShapefile && ((ESRIShapefile) overlays.get(i)).getMultiGrid() != null) {
					focus = ((ESRIShapefile) overlays.get(i)).getMultiGrid().getGrid2DOverlay();
					break;
				}
				if( overlays.get(i) instanceof Grid2DOverlay) {
					focus = (Grid2DOverlay)overlays.get(i);
					break;
				}
			}

//			GMA 1.4.8: TESTING
//			System.out.println( "Longitude: " + pt.getX() + "\t " + "Latitude: " + pt.getY() + "\t " + "Zoom: " + zoom );
			double minLon = ( pt.getX() % 1 ) * 60;
			double minLat = ( pt.getY() % 1 ) * 60;
//			System.out.println( "Longitude minutes: " + minLon + "\t " + "Latitude minutes: " + minLat );
//			fmt.setMaximumFractionDigits(7);
			fmt.setMinimumIntegerDigits(3);
			fmtDegsMins.setMinimumIntegerDigits(3);
			fmtMins.setMinimumIntegerDigits(2);
			if ( zoom < 4 ) {
				fmt.setMaximumFractionDigits(2);
				fmt.setMinimumFractionDigits(2);
				fmtMins.setMaximumFractionDigits(0);
				fmtMins.setMinimumFractionDigits(0);
			}
			else if ( zoom >= 4 && zoom < 16 ) {
				fmt.setMaximumFractionDigits(2);
				fmt.setMinimumFractionDigits(2);
				fmtMins.setMaximumFractionDigits(0);
				fmtMins.setMinimumFractionDigits(0);
			}
			else if ( zoom >= 16 && zoom < 256 ) {
				fmt.setMaximumFractionDigits(3);
				fmt.setMinimumFractionDigits(3);
				fmtMins.setMaximumFractionDigits(1);
				fmtMins.setMinimumFractionDigits(1);
			}
			else if ( zoom >= 256 && zoom < 2048 ) {
				fmt.setMaximumFractionDigits(4);
				fmt.setMinimumFractionDigits(4);
				fmtMins.setMaximumFractionDigits(2);
				fmtMins.setMinimumFractionDigits(2);
			}
			else if ( zoom >= 2048 && zoom < 4096 ) {
				fmt.setMaximumFractionDigits(4);
				fmt.setMinimumFractionDigits(4);
				fmtMins.setMaximumFractionDigits(3);
				fmtMins.setMinimumFractionDigits(3);
			}
			else if ( zoom >= 4096 && zoom < 32768 ) {
				fmt.setMaximumFractionDigits(5);
				fmt.setMinimumFractionDigits(5);
				fmtMins.setMaximumFractionDigits(3);
				fmtMins.setMinimumFractionDigits(3);
			}
			else if ( zoom >= 32768 ) {
				fmt.setMaximumFractionDigits(6);
				fmt.setMinimumFractionDigits(6);
				fmtMins.setMaximumFractionDigits(4);
				fmtMins.setMinimumFractionDigits(4);
			}
			MapApp mApp = ((MapApp)app);
			GridDialog gridDialog = mApp.getMapTools().getGridDialog();
			float z = Float.NaN; 
			String str = null;
			if (focus != null) {
				z = focus.getZ(pt0);
				str = gridDialog.getUnits(focus.toString());
			
				if (str != null)
					setUnits(str);
			} else {
				// NSS 04/07/17 - this part may now be deprecated, since I set focus even for ESRIShape files in line 914 above
				if ( mApp.tools.suite != null && gridDialog != null) {
					Overlay topOverlay = mApp.layerManager.getOverlays().get(0);
					if (topOverlay instanceof ESRIShapefile && ((ESRIShapefile) topOverlay).getMultiGrid() != null) {
						Grid2DOverlay grid = ((ESRIShapefile) topOverlay).getMultiGrid().getGrid2DOverlay();
						z = grid.getZ(pt0);
						units = ((ESRIShapefile) topOverlay).getGridUnits();
					}	
				}
			}

//			***** GMA 1.4.8: Add a listing of the latitude and longitude that includes minutes
//			((MapApp)app).tools.info.setText( "("+fmt.format(pt.getX()) 
//				+" "+ew+", "+ fmt.format(pt.getY()) +" "+ns);

			//GMA 1.6.2: If decimal is zero for alternate values take it out
			String alternateZString = fmt2.format( (double)alternateZ );
			String alternate2ZString = fmt1.format( (double)alternate2Z );
			if ( alternateZString.indexOf(".") != -1 ) {
				if ( alternateZString.substring( alternateZString.indexOf(".") + 1, alternateZString.length() ).equals("0") ) {
					alternateZString = alternateZString.substring( 0, alternateZString.indexOf(".") );
				}
			}
			if ( alternate2ZString.indexOf(".") != -1 ) {
				if ( alternate2ZString.substring( alternate2ZString.indexOf(".") + 1, alternate2ZString.length() ).equals("0") ) {
					alternate2ZString = alternate2ZString.substring( 0, alternate2ZString.indexOf(".") );
				}
			}

			// Displays location and zoom on main MapApp
			if (((MapApp)app).tools.info != null) {
				((MapApp)app).tools.info.setText("(" + fmtDegsMins.format( Math.floor(pt.getX()) ) + "\u00B0" + 
					fmtMins.format( minLon ) + "\u0027" + ew + ", " + fmtDegsMins.format( Math.floor(pt.getY()) ) + "\u00B0" +
					fmtMins.format( minLat ) + "\u0027" + ns + ") (" + fmt.format( pt.getX() ) + "\u00B0" + ew + ", " + 
					fmt.format( pt.getY() ) + "\u00B0" + ns + ")"

					//1.3.5: Display appropriate units depending on the grid being displayed
					+( Float.isNaN(z) ? "" : ", "+ fmt1.format( (double)z ) +" " + units )

					// GMA 1.6.2: Display alternate values if there are alternate values
					+( Double.isNaN(alternateZ) ? "" : ", "+ alternateZString +" " + alternateUnits )
					+( Float.isNaN(alternate2Z) ? "" : ", "+ alternate2ZString +" " + alternate2Units )

	//				GMA 1.4.8: Dropped "factor" from "zoom factor"
	//				+"), zoom factor = "+ fmt1.format(zoom) );
					+", zoom = "+ fmt1.format(zoom) );
			}
		} else if( app instanceof PolarMapApp ) {
			((PolarMapApp)app).tools.info.setText( "("+fmt.format(pt.getX()) 
				+" "+ew+", "+ fmt.format(pt.getY()) +" "+ns+")");
		}
	}

	public String getLonLat() {
		if (((MapApp)app).tools.info == null) return null;
		String lonLat = "";
		lonLat = ((MapApp)app).tools.info.getText();
		return lonLat;
	}

	/**
	 	Gets the projection.
	 	@return the projection.
	 */
	public Projection getProjection() {
		return proj;
	}

	/**
	 * Not implemented.
	 */
	public void addMapInset( MapInset inset ) {
		if( mapInsets==null ) mapInsets = new Vector();
		mapInsets.add( inset );
	}

	/**
	 * Not implemented.
	 */
	public void removeMapInset( MapInset inset ) {
		if(mapInsets==null) {
			return;
		}
		mapInsets.remove( inset );
	}
//	public boolean isFocusTraversable() { return true; }

	/**
	 * Not Implemented.
	 */
	public double[] getWESN() {
		double[] wesn = new double[4];
		Rectangle r = getVisibleRect();
		if(mapBorder != null) {
			r = mapBorder.getInteriorRectangle(this, r.x, r.y, r.width, r.height);
		}
		Point2D.Double pt = new Point2D.Double( r.getX(), r.getY() );
		pt = (Point2D.Double)proj.getRefXY( getScaledPoint(pt));
		wesn[0] = pt.x;
		wesn[3] = pt.y;
		pt = new Point2D.Double( r.getX()+r.getWidth(),
					 r.getY()+r.getHeight() );
		pt = (Point2D.Double)proj.getRefXY( getScaledPoint(pt));
		wesn[1] = pt.x;
		wesn[2] = pt.y;
		return wesn;
	}

	/**
		Gets the scaled and offset graphics object.
		<pre>
		<b>Caution</b> - Must always be put in the code block:
			XMap map;
			Shape stuff;
			synchronized(map.getTreeLock()) {
				Graphics2D g = map.getGraphics2D();
				g.draw(stuff);
			}
		This ensures that the recorded graphics are syncronized.
		</pre>
		@return the scaled and offset graphics object.
	 */
	public Graphics2D getGraphics2D() {
		Graphics2D g = (Graphics2D) getGraphics();
		Rectangle r = getVisibleRect();
		if(mapBorder != null) {
			g.clip(mapBorder.getInteriorRectangle(this, r.x, r.y, r.width, r.height));
			Insets ins = mapBorder.getBorderInsets(this);
			g.translate((double)ins.left, (double)ins.top);
		} else {
			g.clip(r);
		}
		g.scale(zoom, zoom);
		return g;
	}

	/**
	 	Gets the map coordinates (Projection object) at the curso location.
	 	@param mousePoint location of mouse.
	 	@return the coordinates of the mouse releative to the map.
	 */
	public Point2D getScaledPoint( Point2D mousePoint ) {
		double x = mousePoint.getX();
		double y = mousePoint.getY();
		if(mapBorder != null) {
			Insets ins = mapBorder.getBorderInsets(this);
			x -= (double) ins.left;
			y -= ins.top;
		}
		x /= zoom;
		y /= zoom;
		return new Point2D.Double(x, y);
	}

	/**
	 * Gets the units of the current map overlay
	 * @return String representation of the units of the current map overlay
	 */
	public String getUnits() {
		return units;
	}

	/**
	 * Saves current image.
	 */
	public void saveBaseMap() throws IOException {
		JFileChooser chooser = new JFileChooser(System.getProperty("user.dir"));
		int ok = chooser.showSaveDialog(null);
		if(ok==chooser.CANCEL_OPTION) return;

		Insets ins = new Insets(0,0,0,0);
		Rectangle r = getVisibleRect();
		int w = width;
		int h = height;
		Font font = null;
		if(mapBorder != null) {
			ins = mapBorder.getBorderInsets(this);
			float scale = (float)width / (float)r.width;
			font = mapBorder.getFont();
			Font font1 = font.deriveFont(scale*(float)font.getSize());
			mapBorder.setFont(font1);
			ins = mapBorder.getBorderInsets(this);
			w += ins.left+ins.right;
			h += ins.bottom+ins.top;
		}
		BufferedImage im = new BufferedImage(w, h,
					BufferedImage.TYPE_INT_RGB);
		Graphics2D g2 = im.createGraphics();
		if(mapBorder != null) {
			// GMA 1.4.8: TESTING - Has no affect on borders
			mapBorder.paintBorder(this, g2, 0, 0, w, h);
			
			g2.clip( mapBorder.getInteriorRectangle(this, 
						0, 0, w, h) );
			g2.translate(ins.left, ins.top);
			mapBorder.setFont(font);
		}
		for( int i=0 ; i<overlays.size() ; i++) {
			if( overlays.get(i) instanceof MapOverlay ) {
				((MapOverlay)overlays.get(i)).draw(g2);
				break;
			}
		}
		BufferedOutputStream out = new BufferedOutputStream(
					new FileOutputStream( chooser.getSelectedFile()));
		//JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);
		//encoder.encode(im);
		ImageIO.write(im, "JPEG", out);

		out.flush();
		out.close();
	}

	/**
	 * Saves current image.as a JPEG.
	 */
	public void saveJPEGImage(File file) throws IOException {
		Rectangle r = getVisibleRect();
		BufferedImage im = new BufferedImage( r.width, r.height, BufferedImage.TYPE_INT_RGB );
		Graphics2D g2d = im.createGraphics();
		g2d.translate(-r.getX(), -r.getY());
		paint(g2d);		
		int s_idx = file.getName().indexOf(".");
		String suffix = s_idx<0
				? "jpg"
				: file.getName().substring(s_idx+1);
		if( !javax.imageio.ImageIO.getImageWritersBySuffix(suffix).hasNext() )suffix = "jpg";
		javax.imageio.ImageIO.write( im, suffix, file);
	//	BufferedOutputStream out = new BufferedOutputStream(
	//				new FileOutputStream( file ));
	//	JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);
	//	encoder.encode(im);
	}

//	***** GMA 1.6.6: Add option to save as .tiff image for high detail
	public void savePMGImage(File file) throws IOException {
		Rectangle r = getVisibleRect();
		BufferedImage im = new BufferedImage( r.width, r.height, BufferedImage.TYPE_INT_BGR );
		Graphics2D g2d = im.createGraphics();
		g2d.translate(-r.getX(), -r.getY());
		paint(g2d);
		int s_idx = file.getName().indexOf(".");
		String suffix = s_idx<0
				? "png"
				: file.getName().substring(s_idx+1);
		if( !javax.imageio.ImageIO.getImageWritersBySuffix(suffix).hasNext() ) {
			suffix = "png";
		}
		javax.imageio.ImageIO.write( (RenderedImage)im, suffix, file );
	}

	public void saveJPEGImage() throws IOException {
		JFileChooser chooser = MapApp.getFileChooser();
		int ok = chooser.showSaveDialog(getTopLevelAncestor());
		if(ok==chooser.CANCEL_OPTION) return;
		saveJPEGImage( chooser.getSelectedFile() );
	}

	/**
	 * Loads a JPEG.
	 */
	public byte[] getJPEGImage() throws IOException {
		Rectangle r = getVisibleRect();
		BufferedImage im = new BufferedImage(r.width, r.height, BufferedImage.TYPE_INT_RGB);
		Graphics2D g2d = im.createGraphics();
		g2d.translate(-r.getX(), -r.getY());
		paint(g2d);
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		//JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);
		//encoder.encode(im);
		ImageIO.write(im, "JPEG", out);
		return out.toByteArray();
	}

	/**
	 * Gets the region currently displayed in Projection coordinates.
	 * @return the region currently displayed in Projection coordinates.
	 */
	public Rectangle2D getClipRect2D() {
		Rectangle r = getVisibleRect();
		Dimension dim = getPreferredSize();
		r.width = Math.min(r.width, dim.width);
		r.height = Math.min(r.height, dim.height);
		AffineTransform at = new AffineTransform();
		if(rotation==1) {
			at.translate( 0., dim.getHeight() );
			at.rotate(-.5*Math.PI);
		} else if( rotation==2 ) {
			at.translate( dim.getWidth(), dim.getHeight() );
			at.rotate( Math.PI );
		} else if( rotation == 3) {
			at.translate( dim.getWidth(), 0. );
			at.rotate( .5*Math.PI );
		}
		if(rotation != 0) {
			Point2D p1 = at.transform(new Point(r.x,r.y), null);
			Point2D p2 = at.transform(new Point(r.x+r.width,r.y+r.width), null);
			r.x = (int) Math.min(p1.getX(), p2.getX());
			r.width = (int) Math.max(p1.getX(), p2.getX()) - r.x;
			r.y = (int) Math.min(p1.getY(), p2.getY());
			r.height = (int) Math.max(p1.getY(), p2.getY()) - r.y;
		}

		if(mapBorder != null) {
			Insets ins = mapBorder.getBorderInsets(this);
			r.width -= ins.left + ins.right;
			r.height -= ins.top + ins.bottom;
		}
		Rectangle2D.Double r2d = new Rectangle2D.Double(
				r.getX()/zoom, r.getY()/zoom,
				r.getWidth()/zoom, r.getHeight()/zoom);
		return r2d;
	}
	public Dimension getPreferredScrollableViewportSize() {
		return getPreferredSize();
	}
	public int getScrollableUnitIncrement(Rectangle visibleRect,
				int orientation, int direction) {
		if( orientation == SwingConstants.VERTICAL) return 10;
		int newX = visibleRect.x + (direction>0 ? 10 : -10);
		if( wrap>0. ) {
			int dx = (int) (wrap*zoom);
			int test = getPreferredSize().width - visibleRect.width;
			while( newX<0 ) newX += dx;
			while( newX>test ) newX -= dx;
		}
		return (direction>0) ? (newX-visibleRect.x) : -(newX-visibleRect.x);
	}
	public int getScrollableBlockIncrement(Rectangle visibleRect,
				int orientation, int direction) {
		if( orientation == SwingConstants.VERTICAL) return visibleRect.height/2;
		int dx = visibleRect.width/2;
		int newX = visibleRect.x + (direction>0 ? dx : -dx);
		if( wrap>0. ) {
			dx = (int) (wrap*zoom);
			int test = getPreferredSize().width - visibleRect.width;
			while( newX<0 ) newX += dx;
			while( newX>test ) newX -= dx;
		}
		return (direction>0) ? (newX-visibleRect.x) : -(newX-visibleRect.x);
	}
	public boolean getScrollableTracksViewportWidth() { return false; }
	public boolean getScrollableTracksViewportHeight() { return false; }

	/**
	 	Paints designated graphics.
	 	@param g what to paint.
	 */
	public void paintComponent( Graphics g ) {
		MapApp mApp = (MapApp) app;
		Graphics2D g2 = (Graphics2D)g;
		Rectangle clip = g.getClipBounds();
		Dimension dim = getPreferredSize();
		Rectangle r = getVisibleRect();
		if(r.width>dim.width) r.width = dim.width;
		if(r.height>dim.height) r.height = dim.height;
		AffineTransform at = g2.getTransform();
		Rectangle interior = new Rectangle();
		if(mapBorder != null) {
			mapBorder.paintBorder(this, g, r.x, r.y, r.width, r.height);
			interior = mapBorder.getInteriorRectangle(this,
					r.x, r.y, r.width, r.height);
			g2.clip( interior );
			Insets ins = mapBorder.getBorderInsets(this);
			g2.translate(ins.left, ins.top);
		}
// Mac fix?
		BufferedImage im = new BufferedImage(r.width, r.height, BufferedImage.TYPE_INT_ARGB);
		Graphics2D g2d = im.createGraphics();
		g2d.setRenderingHint( RenderingHints.KEY_INTERPOLATION,
				RenderingHints.VALUE_INTERPOLATION_BILINEAR);

		// GMA 1.4.8: TESTING
		g2d.setColor( Color.white );

		g2d.fillRect( 0, 0, r.width, r.height);
		g2d.translate( -r.x, -r.y );
		g2d.scale(zoom, zoom);
		int i=0;
		if(image != null && mApp.isBaseMapVisible())
			g2d.drawImage(image, 0, 0, this);

		while( i<overlays.size() && overlays.get(i) instanceof MapOverlay ) {
			Overlay overlay = overlays.get(i);
			if (overlay == mApp.baseMap) {
				i++;
				continue;
			}

			if ( mApp.isLayerVisible(overlay) ) {
				float alpha = getAlpha(overlay);
				g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));;
				
				if (overlay == mApp.baseMapFocus) {
					mApp.baseMap.draw(g2d);
				}
				overlay.draw(g2d);
			}
			i++;
		}
		g2.setPaintMode();
		if( image != null || i!=0 ) {
			g2.translate( r.x, r.y);
			g2.drawImage( im, 0, 0, this);
			g2.translate( -r.x, -r.y);
		}
		g2.scale(zoom, zoom);
		int i0=i;

		for( i=i0 ; i<overlays.size() ; i++) {
			Overlay overlay = overlays.get(i);
			if (overlay == mApp.baseMap) continue;

			if (mApp.isLayerVisible( overlay)) {
				float alpha = getAlpha(overlay);
				g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));

				if (overlay == mApp.baseMapFocus) {
					mApp.baseMap.draw(g2);
				}
				overlay.draw(g2);
			}
		}
		g2.setPaintMode();
		if( graticule && mapBorder instanceof PolarMapBorder) ((PolarMapBorder)mapBorder).draw(g2);
		if( mapInsets==null || mapInsets.size()==0 || !includeInsets ) {
			return;
		}	
		g2.setTransform(at);
		g2.setClip( getVisibleRect() );
		g2.translate( r.x, r.y );
		interior = getVisibleRect();
		for( i=0 ; i<mapInsets.size() ; i++) {
			((MapInset)mapInsets.get(i)).draw( g2, interior.width, interior.height );
		}
	}

	/**
	 * Prints the current view.
	 * @param g What to print.
	 * @param fmt Page format.
	 * @param pageNo Page Number.
	 * @return if the page exists.
	 */
	public int print( Graphics g, PageFormat fmt, int pageNo ) {
		if( pageNo>0 ) return NO_SUCH_PAGE;
		Graphics2D g2 = (Graphics2D)g;
		Dimension dim = getPreferredSize();
		Rectangle r = getVisibleRect();
	//	r.height += 20;
		if(r.width>dim.width) r.width = dim.width;
		if(r.height>dim.height) r.height = dim.height;
		org.geomapapp.util.DateFmt df = new org.geomapapp.util.DateFmt();
		int secs = (int)(System.currentTimeMillis()/1000L);
		String date = df.format(secs);
		Font font = new Font("SansSerif", Font.PLAIN, 8);
		g.setFont( font );
		Rectangle2D r2d = font.getStringBounds(date, g2.getFontRenderContext());
	//	g2.translate( r.getWidth()-20.-r2d.getWidth(), r.getHeight()+18. );
		g2.setColor( Color.black);

	//	g.setClip( new Rectangle( 0, 0, r.width, r.height) );
		double w = fmt.getImageableWidth();
		double h = fmt.getImageableHeight();
		double x = fmt.getImageableX();
		double y = fmt.getImageableY();
		g2.translate(x, y);
		double scale = Math.min( w / r.getWidth(), h / r.getHeight());
		int xd = (int)(scale*r.getWidth()-10.-r2d.getWidth());
		int yd = (int)(scale*r.getHeight()+18.);
		g2.drawString( date, xd, yd);
		g2.translate( -r.getX()*scale, -r.getY()*scale );
		g2.scale( scale, scale);
//	System.out.println(x +"\t"+ y +"\t"+ w +"\t"+ h  +"\t"+ zoom
//			 +"\t"+ scale +"\t"+ r.getWidth() +"\t"+ r.getHeight());
		if(mapBorder != null) {
//			GMA 1.4.8: TESTING - Commenting this out does not affect border
			mapBorder.paintBorder(this, g, r.x, r.y, r.width, r.height);

			g2.clip( mapBorder.getInteriorRectangle(this, 
						r.x, r.y, r.width, r.height));
			Insets ins = mapBorder.getBorderInsets(this);
			g2.translate(ins.left, ins.top);
		}
		g2.scale(zoom, zoom);
		if(image != null)g2.drawImage(image, 0, 0, this);
		for(int i=0 ; i<overlays.size() ; i++) {
			Overlay overlay = (Overlay)overlays.get(i);
			boolean isMap = overlay instanceof MapOverlay;
			if( !isMap )continue;
			overlay.draw(g2);
		}
		if( graticule && mapBorder instanceof PolarMapBorder) ((PolarMapBorder)mapBorder).draw(g2);
		for(int i=0 ; i<overlays.size() ; i++) {
			Overlay overlay = (Overlay)overlays.get(i);
			boolean isMap = overlay instanceof MapOverlay;
			if( isMap )continue;
			overlay.draw(g2);
		}
		return PAGE_EXISTS;
	}

	private float getAlpha(Overlay overlay) {
		if (overlay == null) return 0;

		// This ties the baseMap and the baseMapFocus together
		if (overlay == ((MapApp) app).baseMap)
			return overlayAlphas.get( 
					((MapApp) app).baseMapFocus);

		Float alpha = overlayAlphas.get(overlay);
		return alpha == null ? 0 : alpha;
	}

	public Object getApp() {
		return app;
	}

	public void setBaseCursor(Cursor cursor) {
		baseCursor = cursor;
	}

	public void setCursor(Cursor cursor) {
		if (cursor.equals(Cursor.getDefaultCursor()))
			super.setCursor( ((MapApp)app).getMapTools().getCurrentCursor() );
		else
		super.setCursor(cursor);
	}

	public void adjustmentValueChanged(AdjustmentEvent e) {
	}

	// Pans the map if user holds space bar down while navigating with pointer.
	public void keyPressed(KeyEvent e) {
		if ( e.getKeyCode() == KeyEvent.VK_SPACE) {
			setSpaceBarPress(true);
		}

		if(getSpaceBarPress()) {
			// Set locations
			screenPressedX = MouseInfo.getPointerInfo().getLocation().x;
			screenPressedY = MouseInfo.getPointerInfo().getLocation().y;
			pressedX = scrollPane.getHorizontalScrollBar().getValue();
			pressedY = scrollPane.getVerticalScrollBar().getValue();

			// Set cursor to closed hand
			Toolkit toolkit = Toolkit.getDefaultToolkit();
			ClassLoader loader = org.geomapapp.util.Icons.class.getClassLoader();
			String path = "org/geomapapp/resources/icons/close_hand.png";
			java.net.URL url = loader.getResource(path);
			try {
				BufferedImage im = ImageIO.read(url);
				Cursor closeHandCursor = toolkit.createCustomCursor( im, new Point(0,0), "close_hand");
				setCursor(closeHandCursor);
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		}
	}

	public void keyReleased(KeyEvent e) {
		if ( e.getKeyCode() == KeyEvent.VK_SPACE) {
			setSpaceBarPress(false);
		}
		// If pan button in toolbar is selected replace with open hand cursor
		if(((MapApp)app).tools.panB.isSelected() ){
			Toolkit toolkit = Toolkit.getDefaultToolkit();
			ClassLoader loader = org.geomapapp.util.Icons.class.getClassLoader();
			String pathB = "org/geomapapp/resources/icons/open_hand.png";
			java.net.URL url = loader.getResource(pathB);
			setSpaceBarPress(false);
			try {
				BufferedImage im = ImageIO.read(url);
				Cursor openHandCursor = toolkit.createCustomCursor( im, new Point(0,0), "close_hand");
				setCursor(openHandCursor);
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		}
		else if(!getSpaceBarPress()){
			setCursor(Cursor.getDefaultCursor());
			setSpaceBarPress(false);
		}
	}

	public void keyTyped(KeyEvent e) {
	}

	public void mouseDragged(MouseEvent e) {

		// GMA 1.6.4: Perform pan while "Pan" button is selected by performing a centering on the point that is dragged to.
		// Pan with right click of mouse equivalent of modifier 4
		if ( (((MapApp)app).tools.panB.isSelected() && e.getModifiers()==16) ||
				((e.getModifiers()==4)) ) {
			// Set cursor to closed hand while drag
			Toolkit toolkit = Toolkit.getDefaultToolkit();
			ClassLoader loader = org.geomapapp.util.Icons.class.getClassLoader();
			String path = "org/geomapapp/resources/icons/close_hand.png";
			java.net.URL url = loader.getResource(path);
			try {
				BufferedImage im = ImageIO.read(url);
				Cursor closeHandCursor = toolkit.createCustomCursor( im, new Point(0,0), "close_hand");
				setCursor(closeHandCursor);
			} catch (IOException e1) {
				e1.printStackTrace();
			}
			int newX = pressedX - ( MouseInfo.getPointerInfo().getLocation().x - screenPressedX );
			if (wrap > 0) {
				double dx = wrap * zoom;
				while (newX < 0) {
					newX += dx;
				}
				int test = getPreferredSize().width - getVisibleRect().width;
				while (newX > test)
					newX -= dx;
			}
			scrollPane.getHorizontalScrollBar().setValue( newX );
			/*if ( screenPressedX < MouseInfo.getPointerInfo().getLocation().x ) {
				scrollPane.getHorizontalScrollBar().setValue( newX );
			}
			else {
				scrollPane.getHorizontalScrollBar().setValue( newX );
			} */
			if ( screenPressedY < MouseInfo.getPointerInfo().getLocation().y ) {
				scrollPane.getVerticalScrollBar().setValue( pressedY - ( MouseInfo.getPointerInfo().getLocation().y - screenPressedY ) );
			}
			else {
				scrollPane.getVerticalScrollBar().setValue( pressedY + ( screenPressedY - MouseInfo.getPointerInfo().getLocation().y ) );
			}
			revalidate();
			repaint();
		} 
		else {
			setSpaceBarPress(false);
		}
		// Request focus on so that KeyEvents work again
		requestFocusInWindow();
	}

	public void mouseMoved(MouseEvent e) {
		//check the profile button in the main tool bar
		if (((MapApp)app).tools.profileB.isSelected()) {
			setCursor(Cursor.getDefaultCursor());			
		}

		spaceBarDown = getSpaceBarPress();
		// Do actions if space bar is down and the mouse is moving.
		if(spaceBarDown) {
			int newX = pressedX - ( MouseInfo.getPointerInfo().getLocation().x - screenPressedX );
			if (wrap > 0) {
				double dx = wrap * zoom;
				while (newX < 0) {
					newX += dx;
				}
				int test = getPreferredSize().width - getVisibleRect().width;
				while (newX > test)
					newX -= dx;
			}
			scrollPane.getHorizontalScrollBar().setValue( newX );
			if ( screenPressedY < MouseInfo.getPointerInfo().getLocation().y ) {
				scrollPane.getVerticalScrollBar().setValue( pressedY - ( MouseInfo.getPointerInfo().getLocation().y - screenPressedY ) );
			}
			else {
				scrollPane.getVerticalScrollBar().setValue( pressedY + ( screenPressedY - MouseInfo.getPointerInfo().getLocation().y ) );
			}
			revalidate();
			repaint();
			// Request focus on click so that KeyEvents work again
			requestFocusInWindow();
		}
	}
	public void mouseClicked(MouseEvent e) {
		// GMA 1.6.4: Center on spot where user double-clicks (but not if Digitizer is open)
		if ( e.getClickCount() >= 2 && e.getModifiers()==16 && !((MapApp)app).digitizer.isEnabled()) {
			Point p = e.getPoint();
			doZoom( p, 1 );
		}
	}

	public void mouseEntered(MouseEvent e) {
		requestFocusInWindow(); // To activate Key Events
	}

	public void mouseExited(MouseEvent e) {
	}
	public void mousePressed(MouseEvent e) {
//		***** GMA 1.6.4: Record screen position when user presses mouse button
//		System.out.println("Pressed value: " + e.getX());
//		System.out.println("Pressed value: " + e.getY());
//		pressedX = e.getX();
//		pressedY = e.getY();
		pressedX = scrollPane.getHorizontalScrollBar().getValue();
		pressedY = scrollPane.getVerticalScrollBar().getValue();
		screenPressedX = MouseInfo.getPointerInfo().getLocation().x;
		screenPressedY = MouseInfo.getPointerInfo().getLocation().y;

		// Set cursor to closed hand while right button pressed
		if(((MapApp)app).tools.panB.isSelected() ) {
			if (e.getModifiers()==4 || e.getModifiers()==16) {
				Toolkit toolkit = Toolkit.getDefaultToolkit();
				ClassLoader loader = org.geomapapp.util.Icons.class.getClassLoader();
				String path = "org/geomapapp/resources/icons/close_hand.png";
				java.net.URL url = loader.getResource(path);
				try {
					BufferedImage im = ImageIO.read(url);
					Cursor closeHandCursor = toolkit.createCustomCursor( im, new Point(0,0), "close_hand");
					setCursor(closeHandCursor);
				} catch (IOException e1) {
					e1.printStackTrace();
				}
			} else {
				setSpaceBarPress(false);
				setCursor(Cursor.getDefaultCursor());
			}
		}
		
		//check Profile button in the main tool bar
		if (((MapApp)app).tools.profileB.isSelected()) {
			setCursor(Cursor.getDefaultCursor());
		}
		
		// Request focus on click so that KeyEvents work again
		requestFocusInWindow();
	}
	public void mouseReleased(MouseEvent e) {
		
		// If pan button in toolbar is selected replace with open hand cursor
		if(((MapApp)app).tools.panB.isSelected() ){
			Toolkit toolkit = Toolkit.getDefaultToolkit();
			ClassLoader loader = org.geomapapp.util.Icons.class.getClassLoader();
			String pathB = "org/geomapapp/resources/icons/open_hand.png";
			java.net.URL url = loader.getResource(pathB);
			try {
				BufferedImage im = ImageIO.read(url);
				Cursor openHandCursor = toolkit.createCustomCursor( im, new Point(0,0), "close_hand");
				setCursor(openHandCursor);
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		} // Otherwise set to the default cursor
		else {
			setCursor(Cursor.getDefaultCursor());
		}
	}

	public void setSpaceBarPress(boolean a){
		spaceBarDown = a;
	}

	public boolean getSpaceBarPress(){
		return spaceBarDown;
	}

	public Double getZoomValue() {
		Double zv = getZoom();
		return zv;
	}

	public Double getZoomValueX() {
		return null;
	}

	public Double getZoomValueY() {
		return null;
	}
	
	public Vector<Overlay> getOverlays() {
		return overlays;
	}
}