// **********************************************************************
//
// <copyright>
//
// BBN Technologies
// 10 Moulton Street
// Cambridge, MA 02138
// (617) 873-8000
//
// Copyright (C) BBNT Solutions LLC. All rights reserved.
//
// </copyright>
// **********************************************************************
//
// $Source: /cvs/distapps/openmap/src/openmap/com/bbn/openmap/proj/ProjectionStack.java,v $
// $RCSfile: ProjectionStack.java,v $
// $Revision: 1.8 $
// $Date: 2009/01/21 01:24:41 $
// $Author: dietrick $
//
// **********************************************************************
package com.bbn.openmap.proj;
import java.awt.Container;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Point2D;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.bbn.openmap.MapBean;
import com.bbn.openmap.OMComponent;
import com.bbn.openmap.event.ProjectionEvent;
import com.bbn.openmap.event.ProjectionListener;
/**
* Provides Projection Stack, to listen for projection changes and remember them
* as they pass by. As a Tool, it provides a GUI so that past projections can be
* retrieved, and, if a past projection is being displayed, a forward projection
* stack is activated to provide a path to get to the last projection set in the
* MapBean. ProjectionStackTriggers should hook themselves up to the
* ProjectionStack. The ProjectionStack is responsible for finding and
* connecting to the MapBean.
*/
public class ProjectionStack extends OMComponent implements ActionListener,
ProjectionListener {
private final static Logger logger = Logger
.getLogger("com.bbn.openmap.proj.ProjectionStack");
public final static int DEFAULT_MAX_SIZE = 10;
public final static int REMEMBER_ALL = -1;
/**
* The currentProjection should be the top item on the backStack.
*/
protected transient ProjHolder currentProjection;
protected transient String currentProjectionID;
protected transient Container face;
protected transient MapBean mapBean;
protected int stackSize = DEFAULT_MAX_SIZE;
public final static transient String BackProjCmd = "backProjection";
public final static transient String ForwardProjCmd = "forwardProjection";
public final static transient String ClearBackStackCmd = "clearBackStack";
public final static transient String ClearForwardStackCmd = "clearForwardStack";
public final static transient String ClearStacksCmd = "clearStacks";
protected Stack<ProjHolder> backStack;
protected Stack<ProjHolder> forwardStack;
protected ProjectionStackSupport triggers;
/**
* Create the projection submenu.
*/
public ProjectionStack() {
}
public void setMapBean(MapBean map) {
if (mapBean != null) {
mapBean.removeProjectionListener(this);
}
if (map != null) {
map.addProjectionListener(this);
}
mapBean = map;
}
public MapBean getMapBean() {
return mapBean;
}
public void actionPerformed(ActionEvent ae) {
String command = ae.getActionCommand().intern();
logger.fine("Received command: " + command);
boolean changeProjection = false;
// This is important. We need to set the current projection
// before setting the projection in the MapBean. That way,
// the projectionChanged method actions won't get fired
if (command == BackProjCmd && backStack != null && backStack.size() > 1) {
pop();
currentProjection = backStack.peek();
changeProjection = true;
} else if (command == ForwardProjCmd && forwardStack != null
&& !forwardStack.empty()) {
currentProjection = backPop();
changeProjection = true;
} else {
clearStacks(
(command == ClearBackStackCmd || command == ClearStacksCmd),
(command == ClearForwardStackCmd || command == ClearStacksCmd));
// fireStackStatus is called in clearStacks
}
if (changeProjection && mapBean != null) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("changing mapbean projection to : "
+ currentProjection);
}
Projection currProj = currentProjection.create(mapBean.getWidth(),
mapBean.getHeight());
mapBean.setProjection(currProj);
fireStackStatus();
}
}
// ------------------------------------------------------------
// ProjectionListener interface
// ------------------------------------------------------------
protected ProjectionFactory getProjectionFactory(ProjectionEvent e) {
Object obj = e.getSource();
if (obj instanceof MapBean) {
return ((MapBean) obj).getProjectionFactory();
} else if (mapBean != null) {
return mapBean.getProjectionFactory();
}
return ProjectionFactory.loadDefaultProjections();
}
/**
* The Map projection has changed.
*
* @param e
* ProjectionEvent
*/
public void projectionChanged(ProjectionEvent e) {
logger.fine("ProjectionStack.projectionChanged()");
Projection newProj = e.getProjection();
// If the ProjectionStack doesn't already know about the
// projection change, that means that it didn't instigate it,
// and the new projection needs to get added to the stack,
// with the forwardStack cleared.
if (currentProjection == null || !currentProjection.equals(newProj)) {
logger.fine("pushing projection on backStack");
// push on the backStack, clear the forwardStack;
currentProjection = push(new ProjHolder(newProj,
getProjectionFactory(e)));
if (forwardStack != null) {
forwardStack.clear();
}
fireStackStatus();
} else {
logger
.fine("new projection matches current projection, no action.");
}
}
/**
* Clear out the chosen projection stacks and fire an event to update the
* triggers on stack status.
*
* @param clearBackStack
* clear out the backward projection stack.
* @param clearForwardStack
* clear out the forward projection stack.
*/
public synchronized void clearStacks(boolean clearBackStack,
boolean clearForwardStack) {
if (clearBackStack && backStack != null) {
ProjHolder currentProj = pop(); // current projection
backStack.clear();
push(currentProj);
}
if (clearForwardStack && forwardStack != null) {
forwardStack.clear();
}
fireStackStatus();
}
/**
* Take a ProjHolder off the backStack, and push it on the forward stack.
*
* @return the ProjHolder pushed onto the forwardStack.
*/
protected synchronized ProjHolder pop() {
ProjHolder proj = backStack.pop();
if (forwardStack == null) {
forwardStack = new Stack<ProjHolder>();
}
while (forwardStack.size() >= stackSize) {
forwardStack.removeElementAt(0);
}
forwardStack.push(proj);
return proj;
}
/**
* Take a ProjHolder off the forwardStack, and push it on the backStack.
*
* @return the ProjHolder pushed on the backStack.
*/
protected synchronized ProjHolder backPop() {
ProjHolder proj = forwardStack.pop();
// This has almost no chance of happening...
if (backStack == null) {
backStack = new Stack<ProjHolder>();
}
while (backStack.size() >= stackSize) {
backStack.removeElementAt(0);
}
backStack.push(proj);
return proj;
}
/**
* Put a new ProjHolder on the backStack, to remember for later in case we
* need to back up.
*
* @param proj
* ProjHolder.
* @return the ProjHolder pushed on the backStack.
*/
protected synchronized ProjHolder push(ProjHolder proj) {
if (backStack == null) {
backStack = new Stack<ProjHolder>();
}
if (backStack.size() >= stackSize) {
backStack.removeElementAt(0);
}
return backStack.push(proj);
}
public void fireStackStatus() {
fireStackStatus((backStack != null && backStack.size() > 1),
(forwardStack != null && !forwardStack.empty()));
}
public void fireStackStatus(boolean enableBackButton,
boolean enableForwardButton) {
if (triggers != null) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("back enabled: " + enableBackButton
+ ", forward enabled: " + enableForwardButton);
}
triggers.fireStackStatus(enableBackButton, enableForwardButton);
}
}
/**
* ProjectionStackTriggers should call this method, and all will be well.
*/
public void addProjectionStackTrigger(ProjectionStackTrigger trigger) {
trigger.addActionListener(this);
if (triggers == null) {
triggers = new ProjectionStackSupport();
}
triggers.add(trigger);
trigger.updateProjectionStackStatus((backStack != null && backStack
.size() > 1), (forwardStack != null && !forwardStack.empty()));
}
/**
* ProjectionStackTriggers should call this method, and all will be well.
*/
public void removeProjectionStackTrigger(ProjectionStackTrigger trigger) {
trigger.removeActionListener(this);
if (triggers != null) {
triggers.remove(trigger);
if (triggers.size() == 0) {
triggers = null;
}
}
}
// ------------------------------------------------------------
// BeanContextMembershipListener and BeanContextChild interface
// ------------------------------------------------------------
/**
* Look at the object received in a MapHandler status message and disconnect
* from it if necessary.
*/
public void findAndUndo(Object someObj) {
if (someObj instanceof com.bbn.openmap.MapBean) {
logger.fine("ProjectionStack removing a MapBean.");
MapBean map = getMapBean();
if (map != null && map == (MapBean) someObj) {
setMapBean(null);
}
}
}
/**
* Look at the object received in a MapHandler status message and connect to
* it if necessary.
*/
public void findAndInit(Object someObj) {
if (someObj instanceof com.bbn.openmap.MapBean) {
logger.fine("ProjectionStack found a MapBean.");
setMapBean((MapBean) someObj);
}
}
public class ProjHolder {
public Class<? extends Projection> projClass;
public float scale;
public Point2D center;
protected Point tmpPoint1;
protected Point tmpPoint2;
protected ProjectionFactory projFactory;
public ProjHolder(Projection proj, ProjectionFactory projectionFactory) {
projClass = proj.getClass();
scale = proj.getScale();
center = proj.getCenter();
projFactory = projectionFactory;
}
public boolean equals(Projection proj) {
// For some reason, the ProjectionFactory can mess up the
// center lat/lons, so that the center isn't EXACTLY what
// they were when the projection was created. It's almost
// like it decides what map it can draw, and then figures
// out what the coordinate of the center pixel of the
// projection it created was. Doing this projection hack
// seems to accurately determine what projections are
// actually identical visually, which is what you want to
// know anyway.
Point2D tmpPoint1 = proj.forward(proj.getCenter());
Point2D tmpPoint2 = proj.forward(center);
boolean same = (projClass == proj.getClass()
&& scale == proj.getScale() &&
// NOT GOOD ENOUGH! Sometimes, the
// slighest difference causes a false
// false.
// MoreMath.approximately_equal(center.getLatitude(),
// proj.getCenter().getLatitude(),
// .00001f) &&
// MoreMath.approximately_equal(center.getLongitude(),
// proj.getCenter().getLongitude(),
// .00001f)
// This seems to work...
tmpPoint1.getX() == tmpPoint2.getX() && tmpPoint1.getY() == tmpPoint2
.getY());
return same;
}
public Projection create(int width, int height) {
return projFactory.makeProjection(projClass, center, scale, width,
height);
}
public String toString() {
return ("[ProjHolder: class(" + projClass.getName() + "), scale("
+ scale + "), center(" + center + ")]");
}
}
}