/*
* Geotoolkit - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2013, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library 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
* Lesser General Public License for more details.
*/
package org.geotoolkit.gui.swing.render3d.control;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import org.geotoolkit.display3d.scene.camera.TrackBallCamera;
/**
*
* @author Johann Sorel (Geomatys)
* @author Thomas Rouby (Geomatys)
*/
public class JRotationControlDecoration extends JComponent implements PropertyChangeListener {
private static final int[] MARGIN = new int[]{5,5,5,5}; // margin left, bottom, right, top
private static final int RESET_RADIUS = 5;
private static final int INNER_RADIUS = 20 + RESET_RADIUS;
private static final int OUTER_RADIUS = 12 + INNER_RADIUS;
private static final int ARROW_SIZE = 8;
private static final int[] CENTER = new int[]{MARGIN[0]+OUTER_RADIUS, MARGIN[3]+OUTER_RADIUS}; // center on x,y
private static final int[] SIZE = new int[]{MARGIN[0] + MARGIN[2] + OUTER_RADIUS*2, MARGIN[1] + MARGIN[3] + OUTER_RADIUS*2}; // width, height
private final TrackBallCamera camera;
private final Color north = Color.RED;
private final Color teinteDark;
private final Color teinteLight;
private final Color text1;
private final Color text2;
private final Color trans = new Color(1f, 1f, 1f, 0f);
private double rotation = 0.0;
private final Shape innerCercle;
private final Shape outerCercle;
private final Shape resetCercle;
private final Shape arrow;
private final BufferedImage buffer = new BufferedImage(SIZE[0], SIZE[1], BufferedImage.TYPE_INT_ARGB);
private boolean mustUpdate = true;
/**
* 0 = drag scale
* 1 = drag rotation
* -1 = no drag
*/
private short actionFlag = -1;
/**
* -1 = no button
* 0 = top
* 1 = right
* 2 = down
* 3 = left
*/
private short overButton = -1;
public JRotationControlDecoration(TrackBallCamera camera) {
this.camera = camera;
this.camera.addPropertyChangeListener(this);
teinteDark = Color.LIGHT_GRAY;
teinteLight = Color.WHITE;
text1 = Color.DARK_GRAY;
text2 = Color.GRAY;
addMouseListener(mouseListener);
addMouseMotionListener(mouseMotionListener);
setOpaque(false);
outerCercle = new java.awt.geom.Ellipse2D.Float(CENTER[0]-OUTER_RADIUS, CENTER[1]-OUTER_RADIUS, OUTER_RADIUS*2, OUTER_RADIUS*2);
innerCercle = new java.awt.geom.Ellipse2D.Float(CENTER[0]-INNER_RADIUS, CENTER[1]-INNER_RADIUS, INNER_RADIUS*2, INNER_RADIUS*2);
resetCercle = new java.awt.geom.Ellipse2D.Float(CENTER[0]-RESET_RADIUS, CENTER[1]-RESET_RADIUS, RESET_RADIUS*2, RESET_RADIUS*2);
arrow = new Polygon(new int[]{0, ARROW_SIZE, ARROW_SIZE / 2 }, new int[]{ARROW_SIZE , ARROW_SIZE , 0}, 3);
}
@Override
public void propertyChange(PropertyChangeEvent evt) {
//event from camera
this.setRotation(Math.toRadians(360.0f-camera.getRotateZ()), false);
}
@Override
protected void paintComponent(final Graphics g) {
super.paintComponent(g);
final Graphics2D g2d = (Graphics2D) g;
final Rectangle clip = g2d.getClipBounds();
if(!clip.intersects(0,0,SIZE[0],SIZE[1])) return;
if(mustUpdate){
final Graphics2D g2 = buffer.createGraphics();
g2.setStroke(new BasicStroke(1));
g2.setFont(g2d.getFont().deriveFont(Font.BOLD));
g2.setBackground(new Color(0f,0f,0f,0f));
g2.clearRect(0, 0, buffer.getWidth(), buffer.getHeight());
g2.setRenderingHints(g2d.getRenderingHints());
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
final int justOuterRadius = OUTER_RADIUS-INNER_RADIUS;
final int justInnerRadius = INNER_RADIUS-RESET_RADIUS;
final Point2D center = new Point2D.Float(CENTER[0], CENTER[1]);
final float[] dist = {0.5f, 1.0f};
final Color[] colors = {teinteLight, teinteDark};
final RadialGradientPaint paint = new RadialGradientPaint(center, INNER_RADIUS, dist, colors);
g2.setPaint(paint);
g2.fillOval(CENTER[0]-INNER_RADIUS, CENTER[1]-INNER_RADIUS, INNER_RADIUS*2, INNER_RADIUS*2);
g2.setPaint(teinteDark);
g2.drawOval(CENTER[0]-INNER_RADIUS-justOuterRadius/2, CENTER[1]-INNER_RADIUS-justOuterRadius/2, INNER_RADIUS*2+justOuterRadius-1, INNER_RADIUS*2+justOuterRadius-1);
//draw arrows
final int topX = CENTER[0] - ARROW_SIZE/2;
final int topY = CENTER[1] - (int)(0.8*INNER_RADIUS);
g2.translate(topX,topY);
g2.setColor((overButton==0) ? text1 : text2);
g2.fill(arrow);
g2.translate( -topX, -topY);
g2.rotate(Math.PI/2, CENTER[0], CENTER[1]);
g2.translate(topX, topY);
g2.setColor((overButton==1) ? text1 : text2);
g2.fill(arrow);
g2.translate( -topX, -topY);
g2.rotate(Math.PI/2, CENTER[0], CENTER[1]);
g2.translate(topX, topY);
g2.setColor((overButton==2) ? text1 : text2);
g2.fill(arrow);
g2.translate( -topX, -topY);
g2.rotate(Math.PI/2, CENTER[0], CENTER[1]);
g2.translate(topX, topY);
g2.setColor((overButton==3) ? text1 : text2);
g2.fill(arrow);
g2.translate( -topX, -topY);
g2.rotate(Math.PI/2, CENTER[0], CENTER[1]);
//draw reset button ------------------------------------------------
g2.setPaint((overButton==4) ? text1 : text2);
g2.fill(resetCercle);
//draw the rotation cercle ---------------------------------------------
g2.rotate(rotation, CENTER[0], CENTER[1]);
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1f));
final int posX = CENTER[0] - justOuterRadius/4;
final int posY = CENTER[1] - OUTER_RADIUS + justOuterRadius/4 ;
g2.setColor(north);
g2.fillOval(posX, posY, justOuterRadius/2, justOuterRadius/2);
g2.rotate(-rotation, CENTER[0], CENTER[1]);
mustUpdate = false;
}
g2d.drawImage(buffer, 0, 0, this);
g2d.dispose();
}
private void setRotation(final double r, boolean updateMap){
rotation = r%(2.0*Math.PI);
if (updateMap){
camera.setRotateZ(360.0f-(float)Math.toDegrees(r));
}
mustUpdate = true;
repaint(MARGIN[0],MARGIN[3],OUTER_RADIUS*2,OUTER_RADIUS*2);
}
private double getRotation(){
return rotation;
}
private void moveUp(){
camera.rotateUp(1.0f);
}
private void moveDown(){
camera.rotateDown(1.0f);
}
private void moveLeft(){
setRotation((float) getRotation() + Math.toRadians(1.0), true);
}
private void moveRight(){
setRotation((float)getRotation()-Math.toRadians(1.0), true);
}
private void mapRotate(final double d){
setRotation((float)d, true);
}
private double calculateAngle(final int mouseX, final int mouseY){
final Point pa = new Point( CENTER[0] ,0);
final Point pb = new Point( CENTER[0] , CENTER[1] );
final Point pc = new Point(mouseX,mouseY);
final double a = Math.pow( Math.pow( pc.x - pb.x , 2) + Math.pow( pc.y - pb.y , 2) ,0.5d);
final double b = Math.pow( Math.pow( pa.x - pc.x , 2) + Math.pow( pa.y - pc.y , 2) ,0.5d);
final double c = Math.pow( Math.pow( pa.x - pb.x , 2) + Math.pow( pa.y - pb.y , 2) ,0.5d);
// double angleA = Math.acos( ( Math.pow(b, 2) + Math.pow(c, 2) - Math.pow(a, 2) )/(2*b*c) );
double angleB = Math.acos( ( Math.pow(a, 2) + Math.pow(c, 2) - Math.pow(b, 2) )/(2*a*c) );
// double angleC = Math.acos( ( Math.pow(a, 2) + Math.pow(b, 2) - Math.pow(c, 2) )/(2*a*b) );
if(mouseX < CENTER[1] ){
angleB = 2* Math.PI - angleB;
}
return angleB;
}
@Override
public boolean contains(final int x, final int y) {
return outerCercle.contains(x,y);
}
private final MouseListener mouseListener = new MouseListener() {
@Override
public void mouseClicked(final MouseEvent e) {
final Point mouse = e.getPoint();
if(resetCercle.contains(mouse)){
mapRotate(0);
}
}
@Override
public void mousePressed(final MouseEvent e) {
final Point mouse = e.getPoint();
if(resetCercle.contains(mouse)) return;
final double tx = mouse.x - CENTER[0] ;
final double ty = mouse.y - CENTER[1] ;
final double distance = Math.hypot(tx, ty);
if(distance >= INNER_RADIUS &&
distance <= OUTER_RADIUS){
actionFlag = 1;
mapRotate(calculateAngle(mouse.x, mouse.y));
} else if (distance < INNER_RADIUS){
actionFlag = -1;
if(Math.abs(tx)!=Math.abs(ty)) {
if (tx>ty && -tx>ty) {
moveUp();
} else if (tx<ty && -tx<ty) {
moveDown();
} else if (tx<ty && -tx>ty) {
moveLeft();
} else if (tx>ty && -tx<ty) {
moveRight();
}
}
}
}
@Override
public void mouseReleased(MouseEvent e) {
actionFlag = -1;
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
if(overButton !=-1){
overButton = -1;
mustUpdate = true;
repaint(innerCercle.getBounds());
}
}
};
private final MouseMotionListener mouseMotionListener = new MouseMotionListener() {
@Override
public void mouseDragged(final MouseEvent e) {
if (actionFlag == 1) {
mapRotate(calculateAngle(e.getX(),e.getY()));
}
}
@Override
public void mouseMoved(final MouseEvent e) {
final Point mouse = e.getPoint();
//we repaint inner buttons
if(innerCercle.contains(mouse) ){
final int oldOver = overButton;
final double tx = mouse.x - CENTER[0];
final double ty = mouse.y - CENTER[1];
if(resetCercle.contains(mouse)){
overButton = 4;
} else if (tx>ty && -tx>ty) {
overButton = 0;
} else if (tx<ty && -tx<ty) {
overButton = 2;
} else if (tx<ty && -tx>ty) {
overButton = 3;
} else if (tx>ty && -tx<ty) {
overButton = 1;
}
if(oldOver != overButton){
mustUpdate = true;
repaint(innerCercle.getBounds());
}
}else if(overButton != -1){
overButton = -1;
mustUpdate = true;
repaint(innerCercle.getBounds());
}
}
};
@Override
public Dimension getPreferredSize() {
return new Dimension(buffer.getWidth(),buffer.getHeight());
}
@Override
public Dimension getSize() {
return getPreferredSize();
}
@Override
public Dimension getSize(final Dimension rv) {
if(rv != null){
rv.height = buffer.getHeight();
rv.width = buffer.getWidth();
return rv;
}else{
return getSize();
}
}
@Override
public int getWidth() {
return this.getSize().width;
}
@Override
public int getHeight() {
return this.getSize().height;
}
}