package ch.ethz.karto.map3d;
import java.awt.Insets;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Point2D;
import java.awt.geom.Point2D.Float;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Iterator;
import javax.media.opengl.GLAutoDrawable;
import javax.swing.Timer;
import javax.swing.event.MouseInputAdapter;
/**
* Mouse event support for a Map3DViewer. Rotate and zoom for a 3D model.
* @author Benrhard Jenny, Institute of Cartography, ETH Zurich.
*/
public final class Map3DMouseHandler extends MouseInputAdapter implements MouseWheelListener {
/**
* Divide the horizontal mouse movemement by this factor to convert to a
* rotation angle around the z axis.
*/
private static final float Z_MOVEMENT_SCALE = 20;
/**
* Divide the vertical mouse movemement by this factor to convert to a
* rotation angle around the x axis.
*/
private static final float X_MOVEMENT_SCALE = 10;
/**
* Divide the number of ticks the scroll wheel was turned by this factor to
* convert to the zoom factor of the map.
*/
private static final float SCROLL_WHEEL_SCALE = 20;
/**
* Scale the size of the shear effect produced when manually shearing.
*/
private static final float SHEAR_COEFFICIENT = 1f;
/**
* Animation rate in (frames/second).
*/
private static final int ANIMATION_FRAMERATE = 30;
/**
* Used by spring to control shearing. Fraction of velocity maintained each step.
*/
private static final float FRICTION_CONSTANT = 0.75f;
/**
* Used by spring to control shearing. Fraction of distance between points to impart as velocity.
*/
private static final float SPRING_CONSTANT = 0.05f;
/**
* Used to control the rate of speed when animating zoom (z-per-second).
*/
private static final float ZOOM_SPEED = 2f;
/**
* The map being controlled.
*/
private Map3DViewer map3DViewer;
/**
* Remembers the position of the last mouse event.
*/
private Point lastPoint = null;
/**
* Remembers the position of the mouse at the beginning of the last press.
*/
private Point startPoint = null;
/**
* Remembers the elevation (in model z) at the beginning of the last press.
*/
private float startZ;
/**
* Remembers the X and Y shift of the map at the beginning of a the last press.
*/
private float startShiftX;
private float startShiftY;
/**
* Remembers the current velocity in X and Y when animating a shear/pan.
*/
private float currentVelocityX = 0;
private float currentVelocityY = 0;
private float currentAnimatedShiftX = 0;
private float currentAnimatedShiftY = 0;
/**
* Remember the state, start and end distances when animating zoom;
*/
private boolean zoomIsAnimating = false;
private float zoomEndDistance = 0;
/**
* Is zoom currently in the process of animating.
*/
private boolean shearIsAnimating = false;
/**
* Controls whether the shear-and-pan animation is run.
*/
private boolean shearAnimationOn = false;
/**
* Controls whether zoom animation is run.
*/
private boolean zoomAnimationOn = true;
/**
* Controls whether shear and shear-and-pan should be reversed (e.g. for low points).
*/
private boolean shearReversed = false;
/**
* Construct a new Map3DMouseHandler.
* @param map3DViewer The map to rotate and zoom.
*/
public Map3DMouseHandler(Map3DViewer map3DViewer) {
this.map3DViewer = map3DViewer;
//FIXME: Animation routines should really live outside the handler.
//Animate shearing
ActionListener animationPolling = new ActionListener() {
public void actionPerformed(ActionEvent evt) {
if(shearIsAnimating) animateShearWithSpring();
if(zoomAnimationOn && zoomIsAnimating) animateZoom();
}
};
new Timer(1000/ANIMATION_FRAMERATE, animationPolling).start();
}
public boolean isDragging() {
return lastPoint != null;
}
/**
* Mouse movements pan by default.
* Shift key causes rotation around the vertical z axis and around the horizontal x axis.
* Meta key causes the shear to change.
* @param e
*/
@Override
public void mouseDragged(MouseEvent e) {
if (lastPoint == null || map3DViewer == null) {
return;
}
//Holding shift rotates the view
if (e.isShiftDown()) {
if (this.map3DViewer.is2D()) {
rotate2D(e);
} else {
rotate3D(e);
}
}
//Holding alt forces a pan (already the default in most modes)
else if (e.isAltDown()){
pan(e);
}
//Holding down the meta (or executing a right click or two-finger press) shears in plan oblique
else if (e.isMetaDown() && map3DViewer.getCamera() == Map3DViewer.Camera.planOblique) {
shear(e);
}
//Otherwise, pan in all modes except plan oblique (where shearing animation is used instead)
else {//if(map3DViewer.getCamera() != Map3DViewer.Camera.planOblique){
pan(e);
}
this.lastPoint = e.getPoint();
}
/**
* Remember the point where the dragging starts.
* @param e
*/
@Override
public void mousePressed(MouseEvent e) {
this.startPoint = e.getPoint();
this.lastPoint = e.getPoint();
this.shearIsAnimating = false;
if(shearAnimationOn &&
map3DViewer.getCamera() == Map3DViewer.Camera.planOblique &&
!e.isMetaDown() && !e.isAltDown() && !e.isShiftDown()){
this.shearIsAnimating = true;
this.currentVelocityX = 0;
this.currentVelocityY = 0;
}
this.startShiftX = map3DViewer.getShiftX();
this.startShiftY = map3DViewer.getShiftY();
//FIXME: A few shearing experiments
if(map3DViewer.getCamera() == Map3DViewer.Camera.planOblique && e.isMetaDown()){
Point2D.Float startPosition = this.map3DViewer.mouseXYtoModelXY(lastPoint.x, lastPoint.y);
startZ = map3DViewer.getModel().z(startPosition.x, startPosition.y);
//(1) shearing centered around a set elevation
//((Map3DModelVBOShader) map3DViewer.getModel()).setShearBaseline(startZ);
//(2) shear reversal based on local max/min detector
//shearReversed = isLocalMinMax(this.map3DViewer.getModel(), startPosition,100,0.6f) != -1;
Rectangle2D.Float view = this.map3DViewer.getViewBounds();
shearReversed = (localRelativeHeight(this.map3DViewer.getModel(), view, startPosition) <= 0);
if(shearReversed)
System.out.println("Using reverse shear.");
else
System.out.println("Using normal shear.");
}
}
/**
* Release the last position of the mouse.
* @param e
*/
@Override
public void mouseReleased(MouseEvent e) {
boolean mouseMoved = (this.startPoint.x != this.lastPoint.x) ||
(this.startPoint.y != this.lastPoint.y);
//this.startPoint = null;
//this.lastPoint = null;
// Reset shear if meta is down
if (e.isMetaDown()) {
resetShear();
}
// redraw with antialiasing enabled
if (map3DViewer.isAntialiasing())
this.map3DViewer.display();
}
/**
* Zoom in and out.
* @param e
*/
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
float viewDist = this.map3DViewer.getViewDistance();
float viewDistChange = e.getWheelRotation() / SCROLL_WHEEL_SCALE;
if(zoomAnimationOn){
if(!zoomIsAnimating){
zoomEndDistance = viewDist + viewDistChange;
zoomIsAnimating = true;
}
else zoomEndDistance += viewDistChange;
}
else this.map3DViewer.setViewDistance(viewDist + viewDistChange);
}
private void rotate2D(MouseEvent e) {
int w = this.map3DViewer.getComponent().getWidth();
int h = this.map3DViewer.getComponent().getHeight();
float zAngle = this.map3DViewer.getZAngle();
double lastAngle = Math.atan2(lastPoint.y - h / 2, lastPoint.x - w / 2);
double newAngle = Math.atan2(e.getY() - h / 2, e.getX() - w / 2);
zAngle += (float) Math.toDegrees(newAngle - lastAngle);
this.map3DViewer.setZAngle(zAngle);
}
private void pan(MouseEvent e) {
int h = this.map3DViewer.getComponent().getHeight();
float dx = (float) (lastPoint.x - e.getX()) / h * map3DViewer.viewDistance;
float dy = (float) (lastPoint.y - e.getY()) / h * map3DViewer.viewDistance;
map3DViewer.setShift(map3DViewer.getShiftX() - dx, map3DViewer.getShiftY() - dy);
}
private void shear(MouseEvent e){
Point2D.Float startPosition = this.map3DViewer.mouseXYtoModelXY(startPoint.x, startPoint.y);
Point2D.Float currentPosition = this.map3DViewer.mouseXYtoModelXY(e.getX(),e.getY());
float distanceX = currentPosition.x - startPosition.x;
float distanceY = currentPosition.y - startPosition.y;
//Shear in X and Y so the clicked point stays under the mouse.
if(startZ > 0 && startZ < 1){
if(!shearReversed){
//shear in the positive direction for high points
map3DViewer.setShearX(SHEAR_COEFFICIENT * distanceX / startZ);
map3DViewer.setShearY(SHEAR_COEFFICIENT * distanceY / startZ);
}
else{
//shear in the negative direction for other points
map3DViewer.setShearX(-SHEAR_COEFFICIENT * distanceX / startZ);
map3DViewer.setShearY(-SHEAR_COEFFICIENT * distanceY / startZ);
map3DViewer.setShift(startShiftX + 2*distanceX,
startShiftY + 2*distanceY);
}
}
}
private void animateZoom(){
float viewDist = this.map3DViewer.getViewDistance();
float zoomAmt = ZOOM_SPEED / ANIMATION_FRAMERATE;
if(Math.abs(this.zoomEndDistance - viewDist) <= zoomAmt){
this.map3DViewer.setViewDistance(this.zoomEndDistance);
zoomIsAnimating = false;
}
else{
float dir = viewDist < zoomEndDistance ? 1f : -1f;
this.map3DViewer.setViewDistance(viewDist + dir*zoomAmt);
}
}
private void animateShearWithSpring(){
int w = this.map3DViewer.getComponent().getWidth();
int h = this.map3DViewer.getComponent().getHeight();
if(startPoint == null) return;
//Find distance between current mouse position and point being animated
Point2D.Float startPosition = this.map3DViewer.mouseXYtoModelXY(startPoint.x, startPoint.y);
Point2D.Float currentPosition = this.map3DViewer.mouseXYtoModelXY(lastPoint.x,lastPoint.y);
Point2D.Float animatingPosition = new Point2D.Float(startPosition.x + currentAnimatedShiftX - startShiftX,
startPosition.y + currentAnimatedShiftY - startShiftY);
float distanceX = currentPosition.x - animatingPosition.x;
float distanceY = currentPosition.y - animatingPosition.y;
//Compute velocities - modeling the interaction between the current
// mouse position and the start position as a simple spring with length 0
// (elastic force between the two, but no resistive force, only friction).
currentVelocityX *= FRICTION_CONSTANT;
currentVelocityY *= FRICTION_CONSTANT;
currentVelocityX += distanceX * SPRING_CONSTANT;
currentVelocityY += distanceY * SPRING_CONSTANT;
if(Math.abs(currentVelocityX) < 0.0001) currentVelocityX = 0;
if(Math.abs(currentVelocityY) < 0.0001) currentVelocityY = 0;
//Shift the viewer by the new velocity
currentAnimatedShiftX += currentVelocityX;
currentAnimatedShiftY += currentVelocityY;
//Recompute distances after the new velocity is applied.
animatingPosition = new Point2D.Float(startPosition.x + currentAnimatedShiftX - startShiftX,
startPosition.y + currentAnimatedShiftY - startShiftY);
distanceX = currentPosition.x - animatingPosition.x;
distanceY = currentPosition.y - animatingPosition.y;
//Shear in X and Y so the clicked point stays under the mouse.
if(startZ > 0 && startZ < 1){
if(!shearReversed){
map3DViewer.setShearX(distanceX / startZ);
map3DViewer.setShearY(distanceY / startZ);
map3DViewer.setShift(currentAnimatedShiftX, currentAnimatedShiftY);
}
else{
map3DViewer.setShearX(-1 * distanceX / startZ);
map3DViewer.setShearY(-1 * distanceY / startZ);
map3DViewer.setShift(currentAnimatedShiftX + 2*distanceX,
currentAnimatedShiftY + 2*distanceY);
}
}
else map3DViewer.setShift(currentAnimatedShiftX, currentAnimatedShiftY);
}
private void resetShear(){
this.map3DViewer.setShearY(0);
this.map3DViewer.setShearX(0);
this.map3DViewer.setShiftX(this.startShiftX);
this.map3DViewer.setShiftY(this.startShiftY);
}
private void rotate3D(MouseEvent e) {
float zAngle = this.map3DViewer.getZAngle();
float xAngle = this.map3DViewer.getXAngle();
zAngle += (lastPoint.x - e.getX()) / Z_MOVEMENT_SCALE;
xAngle += (lastPoint.y - e.getY()) / X_MOVEMENT_SCALE;
this.map3DViewer.setZAngle(zAngle);
this.map3DViewer.setXAngle(xAngle);
}
/**
* FIXME: Hackish method for determining whether the specified point is
* a local minimum or maximum. Samples z values for all points in a radius
* around some position. If more than the fraction specified in 'threshold'
* are greater than the z value at the point, return -1 (local min). If more
* than that fraction are below the point, return 1 (local max). Otherwise
* return 0;
* @param position
* @param radius
* @param threshold
* @return
*/
public static int isLocalMinMax(Map3DModel model, Point2D.Float position, int radius, float threshold) {
//FIXME: Naive first stab - ACTUALLY DOESN'T WORK
float s = (Math.max(model.getCols(), model.getRows()) - 1);
int col = (int)Math.round(position.x * s);
int row = (int)Math.round(position.y * s);
float positionZ = model.z(col, row);
int sampledPoints = 0;
int gtPoints = 0;
int ltPoints = 0;
for(int i=col - radius; i < col + radius; i++){
for(int j=row - radius; j < row + radius; j++){
float sampleZ = model.z(i,j);
if(sampleZ == 0) continue; //typically out-of-bounds
if(sampleZ > positionZ) gtPoints++;
if(sampleZ < positionZ) ltPoints++;
sampledPoints++;
}
}
float gtFraction = (float) gtPoints / sampledPoints;
float ltFraction = (float) ltPoints / sampledPoints;
//System.out.println(gtFraction + " : " + ltFraction);
if(ltFraction >= threshold && gtFraction < threshold) return 1;
if(gtFraction >= threshold && ltFraction < threshold) return -1;
else return 0;
}
public static float localRelativeHeight(Map3DModel model, Rectangle2D.Float view, Point2D.Float position) {
//number of samples across the current view
int effectiveResolution = 100;
//radius around which to sample for current view
int radius = 10;
float stepSizeX = view.width / effectiveResolution;
float stepSizeY = view.height / effectiveResolution;
float positionZ = model.z(position.x, position.y);
int sampledPoints = 0;
float sampledDiff = 0;
for(int i = -radius; i <= radius; i++){
for(int j = -radius; j <= radius; j++){
if(Math.sqrt(Math.pow(i,2) + Math.pow(j,2)) > radius) continue;
float sampleZ = model.z(position.x + stepSizeX * i,
position.y + stepSizeY * j);
if(sampleZ == 0) continue; //typically out-of-bounds
sampledDiff += (positionZ - sampleZ);
sampledPoints++;
}
}
return (float) sampledDiff / sampledPoints;
}
public static float localRelativeHeight(Map3DModel model, Point2D.Float position, int radius) {
float s = (Math.max(model.getCols(), model.getRows()) - 1);
int col = (int)Math.round(position.x * s);
int row = (int)Math.round(position.y * s);
float positionZ = model.z(col, row);
int sampledPoints = 0;
float sampledDiff = 0;
int gtPoints = 0;
int ltPoints = 0;
for(int i=col - radius; i < col + radius; i++){
for(int j=row - radius; j < row + radius; j++){
if(Math.sqrt(Math.pow(col - i,2) + Math.pow(row - j,2)) > radius) continue;
float sampleZ = model.z(i,j);
if(sampleZ == 0) continue; //typically out-of-bounds
if(sampleZ > positionZ) gtPoints++;
if(sampleZ < positionZ) ltPoints++;
sampledDiff += (positionZ - sampleZ);
sampledPoints++;
}
}
return (float) sampledDiff / sampledPoints;
}
}