/***********************************************************************
* mt4j Copyright (c) 2008 - 2009, C.Ruff, Fraunhofer-Gesellschaft All rights reserved.
*
* This program 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 3 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*
***********************************************************************/
package org.mt4j.components;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.HashMap;
import javax.swing.Timer;
import org.mt4j.components.clusters.Cluster;
import org.mt4j.components.clusters.ClusterManager;
import org.mt4j.components.interfaces.IMTComponent3D;
import org.mt4j.input.IHitTestInfoProvider;
import org.mt4j.input.inputData.MTInputEvent;
import org.mt4j.input.inputProcessors.componentProcessors.dragProcessor.DragProcessor;
import org.mt4j.input.inputProcessors.componentProcessors.rotateProcessor.RotateProcessor;
import org.mt4j.input.inputProcessors.componentProcessors.scaleProcessor.ScaleProcessor;
import org.mt4j.input.inputProcessors.componentProcessors.tapProcessor.TapProcessor;
import org.mt4j.util.camera.Icamera;
import org.mt4j.util.math.Matrix;
import processing.core.PApplet;
import processing.core.PGraphics;
import processing.core.PGraphics3D;
/**
* MTCanvas is the root node of the component hierarchy of a MT4j scene.
* To make a mt4j component visible and interactable you have to add it to
* the scene's canvas or to a component which is already attached to the canvas.
* The canvas then recursivly updates and draws all attached components each frame.
*
* @author Christopher Ruff
*/
public class MTCanvas extends MTComponent implements IHitTestInfoProvider{
/** The cluster manager. */
private ClusterManager clusterManager;
/** The last time hit test. */
private long lastTimeHitTest;
/** The cache time delta. */
private long cacheTimeDelta;
/** The cache clear time. */
private int cacheClearTime;
/** The position to component. */
private HashMap<Position, IMTComponent3D> positionToComponent;
/** The timer. */
private Timer timer;
/** The use hit test cache. */
private boolean useHitTestCache;
/** The frustum culling switch. */
private boolean frustumCulling;
private int culledObjects = 0;
private long lastUpdateTime;
/**
* The Constructor.
*
* @param pApplet the applet
* @param attachedCamera the attached camera
*/
public MTCanvas(PApplet pApplet, Icamera attachedCamera) {
this(pApplet, "unnamed MT Canvas", attachedCamera);
}
/**
* The Constructor.
*
* @param pApplet the applet
* @param name the name
* @param attachedCamera the attached camera
*/
public MTCanvas(PApplet pApplet, String name, Icamera attachedCamera) {
super(pApplet, name, attachedCamera);
//Cache settings
lastTimeHitTest = 0;
cacheTimeDelta = 100;
cacheClearTime = 20000;
useHitTestCache = true;
lastUpdateTime = 0;
clusterManager = new ClusterManager(pApplet, this);
positionToComponent = new HashMap<Position, IMTComponent3D>();
//Schedule a Timer task to clear the object cache so it wont
//get filled infinitely
timer = new Timer(cacheClearTime, new ActionListener(){
public void actionPerformed(ActionEvent arg0) {
// System.out.println("Hit test chache entries: " + positionToComponent.size() + " ... cleared!");
positionToComponent.clear();
}
});
// timer.start(); //FIXME TEST
this.useHitTestCache = false; //FIXME TEST DISABLE HIT CACHE
// this.setCollidable(false);
this.setGestureAllowance(RotateProcessor.class, false);
this.setGestureAllowance(ScaleProcessor.class, false);
this.setGestureAllowance(TapProcessor.class, false);
this.setGestureAllowance(DragProcessor.class, false);
this.setPickable(false);
//Frustum culling default
frustumCulling = false;
}
@Override
protected void destroyComponent() {
super.destroyComponent();
if (this.timer != null && timer.isRunning()){
timer.stop();
}
if (positionToComponent != null){
positionToComponent.clear();
}
}
/**
* Method for asking the canvas whether and which object is at the specified
* screen position.
* <p>
* IMPORTANT: this method returns the MTCanvas instance if no other object is hit.
* This means that the MTCanvas instance acts like the background => Gestures that
* are supposed to be performed on the background have to check if they hit the canvas.
* And the gestureevents should then have the canvas as their targetComponent!
* Also, you have to be careful in other gestures, as even when you dont hit an object, you will
* get the mtcanvas returned as the hit component - not null!
* <p>Note: if the hit component is part of a cluster, the cluster is returned!
*
* @param x the screen x coordinate
* @param y the screen y coordinate
*
* @return the object at that position or this MTCanvas instance if no component was hit
*/
public IMTComponent3D getComponentAt(float x, float y) {
IMTComponent3D closest3DComp = null;
try{
long now = System.currentTimeMillis();
if (useHitTestCache){
if (now - lastTimeHitTest > cacheTimeDelta){ //If the time since last check surpassed => do new hit-test!
//Benchmark the picking
// long a = System.nanoTime();
closest3DComp = this.pick(x, y).getNearestPickResult();
//Benchmark the picking
// long b = System.nanoTime();
// System.out.println("Time for picking the scene: " + (b-a));
/*
for (MTBaseComponent c : pickResult.getPickList())
System.out.println(c.getName());
if (closest3DComp != null)
System.out.println("Using: " + closest3DComp.getName());
*/
if (closest3DComp == null){
closest3DComp = this;
}
positionToComponent.put(new Position(x,y), closest3DComp);
}else{
//Check whats in the cache
IMTComponent3D cachedComp = positionToComponent.get(new Position(x,y));
if (cachedComp != null){ //Use cached obj
closest3DComp = cachedComp;
positionToComponent.put(new Position(x,y), closest3DComp);
}else{
closest3DComp = this.pick(x, y).getNearestPickResult();
if (closest3DComp == null){
closest3DComp = this;
}
positionToComponent.put(new Position(x,y), closest3DComp);
}
}
}else{//IF no hittest cache is being used
closest3DComp = this.pick(x, y).getNearestPickResult();
if (closest3DComp == null){
closest3DComp = this;
}
}
lastTimeHitTest = now;
// /*//TODO anders machen..z.b. geclusterte comps einfach als kinder von
//�bergeordnetem clusterpoly machen? aber mit clusterPoly.setComposite(TRUE);
//Clusterpoly pickable machen damit das hier nicht gebraucht wird?
Cluster sel = this.getClusterManager().getCluster(closest3DComp);
if (sel != null){
closest3DComp = sel;
}
// */
// //FIXME TEST for stencil clipped scene windows -> we have to return the scenes canvas for some gestures!
// if (closest3DComp != null && closest3DComp instanceof mtClipSceneWindow)
}catch(Exception e){
System.err.println("Error while trying to pick an object: ");
e.printStackTrace();
}
/*
if (closest3DComp != null)
System.out.println("Picked: '" + closest3DComp.getName() + "' at pos (" + x + "," + y + ")");
else
System.out.println("Picked: '" + closest3DComp + "' at pos (" + x + "," + y + ")");
*/
return closest3DComp;
}
/* (non-Javadoc)
* @see com.jMT.input.IHitTestInfoProvider#isBackGroundAt(float, float)
*/
public boolean isBackGroundAt(float x, float y) {
return this.getComponentAt(x, y).equals(this);
}
//FIXME TEST
@Override
public void updateComponent(long timeDelta) {
super.updateComponent(timeDelta);
this.lastUpdateTime = timeDelta;
}
//FIXME TEST
private boolean calledFromDrawComponent = false;
//FIXME TEST
// /*
// * Actually this canvases drawComponent method should be called
// * ever because the canvas should not be added as a child to any component.
// * Anyway - this code will still make it possible to use it as a child of other components
// * (non-Javadoc)
// * @see org.mt4j.components.MTComponent#drawComponent(processing.core.PGraphics)
// */
// @Override
// public void drawComponent(PGraphics g) { //FIXME this would draw the canvas 2 times..
// super.drawComponent(g);
//
// //Call the canvases scenes draw method to also draw
// //stuff defined in an overrriden scenes draw method
// if (this.getRenderer() instanceof MTApplication){
// MTApplication app = (MTApplication)this.getRenderer();
// Iscene[] scenes = app.getScenes();
// for (int i = 0; i < scenes.length; i++) {
// Iscene iscene = scenes[i];
// if (iscene instanceof AbstractScene){
// AbstractScene as = (AbstractScene)iscene;
// if (as.getCanvas().equals(this)){
// this.calledFromDrawComponent = true;
//// this.drawAndUpdateCanvas(g, this.lastUpdateTime);
// as.drawAndUpdate(g, this.lastUpdateTime);
// this.calledFromDrawComponent = false;
// }
// }
// }
// }
//
//// this.calledFromDrawComponent = true;
//// this.drawAndUpdateCanvas(g, this.lastUpdateTime);
//// this.calledFromDrawComponent = false;
// }
/**
* Updates and then draws every visible object in the canvas.
* First calls the <code>updateComponent(long timeDelta)</code> method. Then
* the <code>drawComponent()</code> method of each object in the scene graph.
* Also handles the setting of cameras attached to the objects.
* @param graphics
*
* @param updateTime the time passed since the last update (in ms)
*/
public void drawAndUpdateCanvas(PGraphics graphics, long updateTime){
this.culledObjects = 0;
//FIXME THIS IS A HACK! WE SHOULD REPLACE CLUSTERS WITH NORMAL COMPONENTS INSTEAD!
//Update cluster components
Cluster[] clusters = getClusterManager().getClusters();
for (int i = 0; i < clusters.length; i++) {
Cluster cluster = clusters[i];
cluster.updateComponent(updateTime);
}
this.drawUpdateRecursive(this, updateTime, graphics);
// System.out.println("Culled objects: " + culledObjects);
}
/**
* Draw the whole canvas update recursive.
*
* @param currentcomp the currentcomp
* @param updateTime the update time
* @param graphics the renderer
*/
private void drawUpdateRecursive(MTComponent currentcomp, long updateTime, PGraphics graphics){
if (currentcomp.isVisible()){
//Update current component
currentcomp.updateComponent(updateTime);
if (currentcomp.getAttachedCamera() != null){
//Saves transformations up to this object
graphics.pushMatrix();
//Resets the modelview completely with a new camera matrix
currentcomp.getAttachedCamera().update();
if (currentcomp.getParent() != null){
//Applies all transforms up to this components parent
//because the new camera wiped out all previous transforms
Matrix m = currentcomp.getParent().getGlobalMatrix();
PGraphics3D pgraphics3D = (PGraphics3D)graphics;
pgraphics3D.modelview.apply(
m.m00, m.m01, m.m02, m.m03,
m.m10, m.m11, m.m12, m.m13,
m.m20, m.m21, m.m22, m.m23,
m.m30, m.m31, m.m32, m.m33
);
}
//Apply local transform etc
currentcomp.preDraw(graphics);
//Check visibility with camera frustum
if (frustumCulling){
if (currentcomp.isContainedIn(currentcomp.getViewingCamera().getFrustum())){
if (!this.calledFromDrawComponent){ //FIXME TEST
// DRAW THE COMPONENT \\
currentcomp.drawComponent(graphics);
}
}else{
culledObjects++;
//System.out.println("Not visible: " + currentcomp.getName());
}
}else{
if (!this.calledFromDrawComponent){ //FIXME TEST
// DRAW THE COMPONENT \\
currentcomp.drawComponent(graphics);
}
}
currentcomp.postDraw(graphics);
//Draw Children
for (MTComponent child : currentcomp.getChildList())
drawUpdateRecursive(child, updateTime, graphics);
currentcomp.postDrawChildren(graphics);
//Restores the transforms of the previous camera etc
graphics.popMatrix();
}else{//If no custom camera was set
//TODO in abstactvisiblecomp wird outine �ber gradients und clips
//gezeichnet obwohl hier invisble war! FIXME!
//evtl applymatrix unapply in eigene methode? dann nur das ausf�hren, kein pre/post draw!
//TODO vater an kinder listener -> resize - new geometry -> resize own
currentcomp.preDraw(graphics);
if (frustumCulling){
//Check visibility with camera frustum
if (currentcomp.isContainedIn(currentcomp.getViewingCamera().getFrustum())){
// DRAW THE COMPONENT \\
currentcomp.drawComponent(graphics);
}else{
culledObjects++;
//System.out.println("Not visible: " + currentcomp.getName());
}
}else{
// DRAW THE COMPONENT \\
currentcomp.drawComponent(graphics);
}
currentcomp.postDraw(graphics);
for (MTComponent child : currentcomp.getChildList())
drawUpdateRecursive(child, updateTime, graphics);
currentcomp.postDrawChildren(graphics);
}
}//if visible end
}
/* (non-Javadoc)
* @see org.mt4j.components.MTComponent#processInputEvent(org.mt4j.input.inputData.MTInputEvent)
*/
@Override
public boolean processInputEvent(MTInputEvent inEvt) {
//TODO not very elegant - better approach??
if (inEvt.hasTarget()){
if (!inEvt.getTargetComponent().equals(this)){ //Avoid recursion
//Send directed event to the target component
return inEvt.getTargetComponent().processInputEvent(inEvt);
}
}
// return true; //this.handleEvent
//handle in superclass
//The MTCanvas get events targeted at him AND events that have no target!
return super.processInputEvent(inEvt);
}
/**
* Gets the cluster manager.
*
* @return the cluster manager
*/
public ClusterManager getClusterManager() {
return clusterManager;
}
/**
* Sets the cluster manager.
*
* @param selectionManager the new cluster manager
*/
public void setClusterManager(ClusterManager selectionManager) {
this.clusterManager = selectionManager;
}
/**
* Gets the cache time delta.
*
* @return the cache time delta
*/
public long getCacheTimeDelta() {
return cacheTimeDelta;
}
/**
* If repeated calls to getObjectAt(float x, float y) in MTCanvas class
* are called during the provided cacheTimeDelta, the Canvas looks into his
* cache instead of querying all objects again
* Default value is: 80.
*
* @param cacheTimeDelta the cache time delta
*/
public void setCacheTimeDelta(long cacheTimeDelta) {
this.cacheTimeDelta = cacheTimeDelta;
}
/**
* Checks if is use hit test cache.
*
* @return true, if is use hit test cache
*/
public boolean isUseHitTestCache() {
return useHitTestCache;
}
/**
* The canvas can be set to look into a hit test cache if
* repeated calls to getComponentAt() with the same coordinates
* during a short period of time are made.
* This period of time can be set with
* <code>setCacheTimeDelta(long cacheTimeDelta)</code>
* <p>
* This is useful for example when a click is made many gestureanalyzers
* call getObjectAt() almost concurrently.
*
* @param useHitTestCache the use hit test cache
*/
public void setUseHitTestCache(boolean useHitTestCache) {
if (useHitTestCache && !timer.isRunning())
timer.start();
else if (!useHitTestCache && timer.isRunning())
timer.stop();
this.useHitTestCache = useHitTestCache;
}
/**
* Gets the cache clear time.
*
* @return the cache clear time
*/
public int getCacheClearTime() {
return cacheClearTime;
}
/**
* Sets the time intervals in ms in which the canvas clears its hit test cache
* Default value is: 20000 ms
* <p>
* This is important to prevent the hit test cache from growing indefinitely.
*
* @param cacheClearTime the cache clear time
*/
public void setCacheClearTime(int cacheClearTime) {
timer.setDelay(cacheClearTime);
this.cacheClearTime = cacheClearTime;
}
public boolean isFrustumCulling() {
return frustumCulling;
}
public void setFrustumCulling(boolean frustumCulling) {
this.frustumCulling = frustumCulling;
}
/**
* Class used for the pickobject cache.
*/
private class Position{
/** The y. */
float x,y;
/**
* Instantiates a new position.
*
* @param x the x
* @param y the y
*/
public Position(float x, float y){
this.x = x;
this.y = y;
}
/**
* Gets the x.
*
* @return the x
*/
public float getX() {return x;}
/**
* Sets the x.
*
* @param x the new x
*/
public void setX(float x) {this.x = x;}
/**
* Gets the y.
*
* @return the y
*/
public float getY() {return y;}
/**
* Sets the y.
*
* @param y the new y
*/
public void setY(float y) {this.y = y;}
/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object arg0) {
return (arg0 instanceof Position && ((Position)arg0).getX() == this.getX() && ((Position)arg0).getY() == this.getY());
}
/* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return ((int)x+(int)y);
}
}
}