/**************************************************************************
* Copyright (c) 2007, 2008 Gregory Jordan
*
* This file is part of PhyloWidget.
*
* PhyloWidget 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.
*
* PhyloWidget 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 PhyloWidget. If not, see <http://www.gnu.org/licenses/>.
*/
package org.phylowidget.ui;
import java.awt.Cursor;
import java.awt.Rectangle;
import java.awt.event.FocusEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.List;
import org.andrewberman.ui.AbstractUIObject;
import org.andrewberman.ui.Point;
import org.andrewberman.ui.UIRectangle;
import org.andrewberman.ui.UIUtils;
import org.andrewberman.ui.tools.Tool;
import org.andrewberman.ui.tween.Tween;
import org.andrewberman.ui.tween.TweenListener;
import org.andrewberman.ui.tween.TweenQuad;
import org.phylowidget.PWContext;
import org.phylowidget.PWPlatform;
import org.phylowidget.PhyloTree;
import org.phylowidget.render.BasicTreeRenderer;
import org.phylowidget.render.NodeRange;
import org.phylowidget.render.RenderConstants;
import org.phylowidget.tree.PhyloNode;
import org.phylowidget.tree.RootedTree;
import processing.core.PApplet;
public class NodeTraverser extends AbstractUIObject implements TweenListener, KeyListener
{
public static final int LEFT = 0, RIGHT = 1, UP = 2, DOWN = 3;
private NodeRange curNodeRange;
public Tween glowTween;
boolean isGlowing;
Point mousePt = new Point();
ArrayList nearNodes = new ArrayList(10);
private PApplet p;
private PWContext context;
Point pt = new Point();
UIRectangle rect = new UIRectangle();
Point tempPt = new Point();
public NodeTraverser(PApplet p)
{
this.p = p;
this.context = PWPlatform.getInstance().getThisAppContext();
glowTween = new Tween(this, TweenQuad.tween, Tween.INOUT, 1f, .75f, 30);
context.event().add(this);
p.addKeyListener(this);
}
public void destroy()
{
if (context != null && context.event() != null)
{
context.event().remove(this);
}
p.removeKeyListener(this);
glowTween = null;
}
public boolean containsPoint(NodeRange r, Point pt)
{
tempPt.setLocation(getX(r), getY(r));
if (r == null || r.render == null)
return false;
float radius = r.render.getNodeRadius();
radius = Math.max(radius, 2);
float distance = (float) pt.distance(tempPt);
// System.out.println("rad:" + radius + " dist:" + distance);
if (distance < radius)
{
return true;
}
Rectangle rc = new Rectangle();
rc.setFrameFromDiagonal(r.loX, r.loY, r.hiX, r.hiY);
return rc.contains(pt);
}
private boolean glow = true;
public void setGlow(boolean glow)
{
this.glow = glow;
}
public synchronized void draw()
{
/*
* Update the glowing circle's radius.
*/
if (isGlowing && glow)
{
glowTween.update();
float glowRadius = context.trees().getRenderer().getTextSize() / 2;
glowRadius *= glowTween.getPosition();
p.noFill();
p.strokeWeight(glowRadius / 10f);
int color = RenderConstants.hoverColor.getRGB();
p.stroke(color);
NodeRange r = getCurRange();
float cX = getX(r);
float cY = getY(r);
p.ellipse(cX, cY, glowRadius, glowRadius);
}
}
public void focusEvent(FocusEvent e)
{
}
public NodeRange getCurRange()
{
if (curNodeRange == null)
{
/*
* If we haven't set a "focused" NodeRange yet, set it to the root
* node.
*/
BasicTreeRenderer render = context.trees().getRenderer();
if (render == null)
return null;
RootedTree t = render.getTree();
PhyloNode n = (PhyloNode) t.getRoot();
curNodeRange = rangeForNode(render, n);
}
return curNodeRange;
}
public PhyloNode getCurrentNode()
{
NodeRange nr = getCurRange();
return nr.node;
}
Rectangle2D.Float tempRect = new Rectangle2D.Float();
private NodeRange getNearestNode(float x, float y)
{
getWithinRange(x, y, 100);
pt.setLocation(x, y);
float nearestDist = Float.MAX_VALUE;
NodeRange temp = null;
for (int i = 0; i < nearNodes.size(); i++)
{
NodeRange r = (NodeRange) nearNodes.get(i);
PhyloNode n = r.node;
switch (r.type)
{
case (NodeRange.NODE):
float dist = (float) pt.distance(getX(r), getY(r));
if (containsPoint(r, pt))
{
nearestDist = dist;
return r;
}
if (dist < nearestDist)
{
temp = r;
nearestDist = dist;
}
break;
}
}
return temp;
}
private void getWithinRange(float x, float y, float radius)
{
// float ratio = TreeManager.camera.getZ();
float ratio = 1;
float rad = radius * ratio;
pt.setLocation(x, y);
rect.x = (float) (pt.getX() - rad);
rect.y = (float) (pt.getY() - rad);
rect.width = rad * 2;
rect.height = rad * 2;
UIUtils.screenToModel(pt);
UIUtils.screenToModel(rect);
nearNodes.clear();
context.trees().nodesInRange(nearNodes, rect);
}
float getX(NodeRange r)
{
return r.node.getX();
}
float getY(NodeRange r)
{
return r.node.getY();
}
public void keyEvent(KeyEvent e)
{
if (e.getID() != KeyEvent.KEY_PRESSED)
return;
if (context.ui().contextMenu.isOpen())
return;
if (context.focus().getFocusedObject() != null)
return;
int code = e.getKeyCode();
Tool t = context.event().getToolManager().getCurrentTool();
switch (code)
{
case (KeyEvent.VK_LEFT):
navigate(LEFT);
break;
case (KeyEvent.VK_RIGHT):
navigate(RIGHT);
break;
case (KeyEvent.VK_UP):
navigate(UP);
break;
case (KeyEvent.VK_DOWN):
navigate(DOWN);
break;
case (KeyEvent.VK_ENTER):
if (t.respondToOtherEvents() && isGlowing)
{
openContextMenu();
isGlowing = false;
}
break;
}
}
private PhyloNode previousHoveredNode;
boolean pressedWithinNode;
public void mouseEvent(MouseEvent e, Point screen, Point model)
{
mousePt.setLocation(screen);
pt.setLocation(screen);
if (context.event().getToolManager() == null)
return;
Tool t = context.event().getToolManager().getCurrentTool();
if (t == null)
return;
if (context.ui().context == null || context.ui().contextMenu.isOpen())
return;
if (getCurRange() == null)
return;
if (context.focus().getFocusedObject() != null)
return;
context.trees().getRenderer().setMouseLocation(pt);
setCurRange(getNearestNode((float) pt.getX(), (float) pt.getY()));
boolean containsPoint = containsPoint(getCurRange(), pt);
switch (e.getID())
{
case (MouseEvent.MOUSE_DRAGGED):
case (MouseEvent.MOUSE_PRESSED):
if (containsPoint && t.respondToOtherEvents())
{
pressedWithinNode = true;
glowTween.stop();
}
case (MouseEvent.MOUSE_MOVED):
PhyloTree tree = (PhyloTree) context.trees().getTree();
if (containsPoint && t.respondToOtherEvents())
{
UIUtils.setCursor(this, p, Cursor.HAND_CURSOR);
tree.setHoveredNode(getCurRange().node);
PhyloNode hoveredNode = getCurRange().node;
if (hoveredNode != previousHoveredNode)
{
fireEvent(NODE_OVER_EVENT); // GJ 2009-02-15 adding hover and glow event firing.
previousHoveredNode = hoveredNode;
}
} else
{
UIUtils.releaseCursor(this, p);
tree.setHoveredNode(null);
}
break;
case (MouseEvent.MOUSE_CLICKED):
// case (MouseEvent.MOUSE_RELEASED):
if (containsPoint && t.respondToOtherEvents() && pressedWithinNode)
{
openContextMenu();
isGlowing = false;
}
break;
}
if (!containsPoint)
pressedWithinNode = false;
if (!t.respondToOtherEvents())
{
PhyloTree tree = (PhyloTree) context.trees().getTree();
tree.setHoveredNode(null);
setCurRange(null);
}
}
private void navigate(int dir)
{
/*
* Ok, our strategy for navigation will be as follows:
*
* 1. Get all nodes within a reasonable range. 2. Score the closest
* nodes, adding points for closeness but deducting points for being
* off-axis.
*/
Point base = new Point();
NodeRange cur = getCurRange();
/*
* The LEFT or RIGHT directions should always go IN or OUT of the tree,
* while the up and down will use the score-based search.
*/
if (dir == LEFT || dir == RIGHT)
{
RootedTree t = cur.render.getTree();
PhyloNode curNode = null;
if (dir == LEFT)
{
if (t.getParentOf(cur.node) != null)
{
curNode = (PhyloNode) t.getParentOf(cur.node);
setCurRange(rangeForNode(cur.render, curNode));
return;
}
} else if (dir == RIGHT)
{
if (!t.isLeaf(cur.node))
{
List kids = t.getChildrenOf(cur.node);
curNode = (PhyloNode) kids.get(kids.size() - 1);
setCurRange(rangeForNode(cur.render, curNode));
return;
}
}
}
switch (dir)
{
case (LEFT):
base.setLocation(-1, .1);
break;
case (RIGHT):
base.setLocation(1, -.1);
break;
case (UP):
base.setLocation(0, -1);
break;
case (DOWN):
base.setLocation(0, 1);
break;
}
pt.setLocation(cur.node.getLayoutX(), cur.node.getLayoutY());
getWithinRange(cur.node.getLayoutX(), cur.node.getLayoutY(), 200);
Point pt2 = new Point();
float maxScore = -Float.MAX_VALUE;
NodeRange maxRange = null;
for (int i = 0; i < nearNodes.size(); i++)
{
NodeRange r = (NodeRange) nearNodes.get(i);
if (r.type == NodeRange.LABEL)
continue;
if (r.node == cur.node)
continue;
pt2.setLocation(r.node.getLayoutX(), r.node.getLayoutY());
float score = score(pt, pt2, base);
if (score > maxScore)
{
maxScore = score;
maxRange = r;
}
}
if (maxRange == null)
{
System.out.println("Nothing near found");
} else
{
setCurRange(maxRange);
}
}
void openContextMenu()
{
context.ui().contextMenu.open(getCurRange());
}
private NodeRange rangeForNode(BasicTreeRenderer tr, PhyloNode n)
{
return n.range;
}
private void resetGlow()
{
glowTween.rewind();
glowTween.start();
}
float score(Point me, Point him, Point dirV)
{
him.translate((float) -me.getX(), (float) -me.getY());
float len = him.length();
float dot = dirV.dotProd(him);
if (dot / len < 0.1)
return -Float.MAX_VALUE;
return dot / len - len / 30;
}
public static final int NODE_GLOW_EVENT = 23987325;
public static final int NODE_OVER_EVENT = 23987326;
public void setCurRange(NodeRange r)
{
if (r != null && r != curNodeRange)
{
// This is a "new" hovered node. Trigger an event!
fireEvent(this.NODE_GLOW_EVENT);
Tool t = context.event().getToolManager().getCurrentTool();
if (t.respondToOtherEvents())
{
RootedTree tree = r.render.getTree();
}
}
/*
* Logic for the glow resetting and whatnot.
*/
if (r == null)
{
isGlowing = false;
} else
{
if (isGlowing == false)
resetGlow();
if (r != curNodeRange)
{
curNodeRange = r;
resetGlow();
}
isGlowing = true;
}
}
public void tweenEvent(Tween source, int eventType)
{
if (eventType == Tween.FINISHED)
source.yoyo();
}
public void keyPressed(KeyEvent e)
{
keyEvent(e);
}
public void keyReleased(KeyEvent e)
{
keyEvent(e);
}
public void keyTyped(KeyEvent e)
{
keyEvent(e);
}
}