package com.android.droidgraph.scene;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import javax.microedition.khronos.opengles.GL10;
import android.view.MotionEvent;
import com.android.droidgraph.event.GraphNodeEvent;
import com.android.droidgraph.geom.BoundingBox;
import com.android.droidgraph.geom.Bounds;
import com.android.droidgraph.geom.Transform3D;
import com.android.droidgraph.util.PrintLogUtil;
import com.android.droidgraph.vecmath.Point3d;
public abstract class SGNode {
protected PrintLogUtil log = new PrintLogUtil();
private Object parent;
private Map<String, Object> attributeMap;
private List<SGMotionListener> motionListeners = null;
//
private HashSet<GraphNodeEvent> translationEvents = null;
private HashSet<GraphNodeEvent> rotationEvents = null;
private HashSet<GraphNodeEvent> scaleEvents = null;
//
private boolean visible = true;
private String id;
private Bounds cachedAccumBounds;
private Transform3D cachedAccumXform;
private boolean isTouchBlocker = false;
public final boolean isVisible() {
return visible;
}
public void setVisible(boolean visible) {
if (this.visible != visible) {
if (visible) {
this.visible = true;
} else {
this.visible = false;
}
}
}
/**
* @return {@code true} if mouse event should not be dispatched to the nodes
* underneath this one.
*/
public final boolean isTouchBlocker() {
return isTouchBlocker;
}
public final void setTouchBlocker(boolean value) {
if (value != isTouchBlocker) {
isTouchBlocker = value;
}
}
public final String getID() {
return id;
}
public final void setID(String id) {
this.id = id;
}
@Override
public String toString() {
return id + " " + super.toString();
}
public SGParent getParent() {
return (parent instanceof SGView) ? null : (SGParent) parent;
}
final void setParent(Object parent) {
assert (parent == null || parent instanceof SGParent || parent instanceof SGView);
this.parent = parent;
}
public HashSet<GraphNodeEvent> getPendingTranslationEvents() {
return translationEvents;
}
public void addTranslationEvent(GraphNodeEvent e) {
if (translationEvents == null) {
translationEvents = new HashSet<GraphNodeEvent>();
}
translationEvents.add(e);
}
public void addTranslationEvent(HashSet<GraphNodeEvent> list) {
if (translationEvents == null) {
translationEvents = new HashSet<GraphNodeEvent>();
}
translationEvents.addAll(list);
}
public HashSet<GraphNodeEvent> getPendingRotationEvents() {
return rotationEvents;
}
public void addRotationEvent(GraphNodeEvent e) {
if (rotationEvents == null) {
rotationEvents = new HashSet<GraphNodeEvent>();
}
rotationEvents.add(e);
}
public void addRotationEvent(HashSet<GraphNodeEvent> list) {
if (rotationEvents == null) {
rotationEvents = new HashSet<GraphNodeEvent>();
}
rotationEvents.addAll(list);
}
public HashSet<GraphNodeEvent> getPendingScaleEvents() {
return scaleEvents;
}
public void addScaleEvent(GraphNodeEvent e) {
if (scaleEvents == null) {
scaleEvents = new HashSet<GraphNodeEvent>();
}
scaleEvents.add(e);
}
public void addScaleEvent(HashSet<GraphNodeEvent> list) {
if (scaleEvents == null) {
scaleEvents = new HashSet<GraphNodeEvent>();
}
scaleEvents.addAll(list);
}
public SGView getPanel() {
Object node = parent;
while (node != null) {
if (node instanceof SGView) {
return (SGView) node;
} else {
node = ((SGNode) node).parent;
}
}
return null;
}
public SGView getView() {
Object node = parent;
while (node != null) {
if (node instanceof SGView) {
return (SGView) node;
} else {
node = ((SGNode) node).parent;
}
}
return null;
}
/**
* Returns the bounding box of this node in the coordinate space inherited
* from the parent. This is a convenience method, equivalent to calling
* {@code getBounds(null)}.
*/
public final Bounds getBounds() {
return getBounds(null);
}
/**
* Returns the bounding box of this node relative to the specified
* coordinate space.
*
* @param transform
* the transform applied to the geometry
*/
public abstract Bounds getBounds(Transform3D transform);
/**
* Transforms the bounds of this node by the "cumulative transform", and
* then returns the bounding box of that transformed shape.
*/
final Bounds getTransformedBoundsRelativeToRoot() {
if (cachedAccumBounds == null) {
cachedAccumBounds = calculateAccumBounds();
}
return ((Bounds) cachedAccumBounds);
}
/**
* Calculate the accumulated bounds object representing the global bounds
* relative to the root of the tree. The default implementation calculates
* new bounds based on the accumulated transform, but SGFilter nodes
* override this method to return a shared accumulated bounds object from
* their child.
*/
Bounds calculateAccumBounds() {
return getBounds(getCumulativeTransform());
}
/**
* Returns the "cumulative transform", which is the concatenation of all
* ancestor transforms plus the transform of this node (if present).
*/
final Transform3D getCumulativeTransform() {
if (cachedAccumXform == null) {
cachedAccumXform = calculateCumulativeTransform();
}
return cachedAccumXform;
}
/**
* Calculates the accumulated product of all transforms back to the root of
* the tree. The default implementation simply returns a shared value from
* the parent, but SGTransform nodes will override this method to return a
* new modified transform.
*/
Transform3D calculateCumulativeTransform() {
SGNode parent = getParent();
if (parent == null) {
return new Transform3D();
} else {
return parent.getCumulativeTransform();
}
}
/**
* Transforms a point from the global coordinate system of the root node
* (typically a {@link JSGPanel}) into the local coordinate space of this
* SGLNode. The {@code global} parameter must not be null. If the
* {@code local} parameter is null then a new {@link Point3d} object will be
* created and returned after transforming the point. The {@code global} and
* {@code local} parameters may be the same object and the coordinates will
* be correctly updated with the transformed coordinates.
*
* @param global
* the coordinates in the global coordinate system to be
* transformed
* @param local
* a {@code Point3d} object to store the results in
* @return a {@code Point3d} object containig the transformed coordinates
*/
public Point3d globalToLocal(Point3d global, Point3d local) {
Transform3D t = (Transform3D) getCumulativeTransform();
// return .inverseTransform(global, local);
return new Point3d();
}
/**
* Transforms a point from the local coordinate space of this SGNode into
* the global coordinate system of the root node (typically a {@link SGView}
* ). The {@code local} parameter must not be null. If the {@code global}
* parameter is null then a new {@link Point3d} object will be created and
* returned after transforming the point. The {@code local} and
* {@code global} parameters may be the same object and the coordinates will
* be correctly updated with the transformed coordinates.
*
* @param local
* the coordinates in the local coordinate system to be
* transformed
* @param global
* a {@code Point3d} object to store the results in
* @return a {@code Point3d} object containig the transformed coordinates
*/
public Point3d localToGlobal(Point3d local, Point3d global) {
// return getCumulativeTransform().transform(global, local);
return new Point3d();
}
/**
* Process MotionEvent
*
* @param e
*/
final void processMotionEvent(MotionEvent e) {
if ((motionListeners != null) && (motionListeners.size() > 0)) {
for (SGMotionListener tl : motionListeners) {
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
tl.actionDown(e, this);
break;
case MotionEvent.ACTION_UP:
tl.actionUp(e, this);
break;
case MotionEvent.ACTION_MOVE:
tl.actionMove(e, this);
break;
case MotionEvent.ACTION_POINTER_DOWN:
tl.actionPointerDown(e, this);
break;
case MotionEvent.ACTION_POINTER_UP:
tl.actionPointerUp(e, this);
break;
}
}
}
}
public void addMotionEventListener(SGMotionListener listener) {
if (listener == null) {
throw new IllegalArgumentException("null listener");
}
if (motionListeners == null) {
motionListeners = new ArrayList<SGMotionListener>(1);
}
motionListeners.add(listener);
}
public void removeMotionEventListener(SGMotionListener listener) {
if (listener == null) {
throw new IllegalArgumentException("null listener");
}
if (motionListeners != null) {
motionListeners.remove(listener);
}
}
/*
* Attribute-related methods below...
*/
public final Object getAttribute(String key) {
if (key == null) {
throw new IllegalArgumentException("null key");
}
return (attributeMap == null) ? null : attributeMap.get(key);
}
public final void putAttribute(String key, Object value) {
if (attributeMap == null) {
attributeMap = new HashMap<String, Object>(1);
}
attributeMap.put(key, value);
}
public boolean contains(Point3d point) {
if (point == null) {
throw new IllegalArgumentException("null point");
}
Bounds bounds = getBounds(null);
return bounds.intersect(point);
}
/**
* This node is completely clean, and so are all of its descendents.
*/
static final int DIRTY_NONE = (0 << 0);
/**
* This node has changed its overall visual state.
*/
static final int DIRTY_VISUAL = (1 << 0);
/**
* This node has changed only a subregion of its overall visual state. (Only
* applicable to SGLeaf nodes.)
*/
static final int DIRTY_SUBREGION = (1 << 1);
/**
* This node has changed its bounds, so it is important to account for both
* the former bounds and its new, updated bounds.
*/
static final int DIRTY_BOUNDS = (1 << 2);
/**
* One or more of this node's descendents has changed its visual state.
* (Only applicable to SGGroup and SGFilter nodes.)
*/
static final int DIRTY_CHILDREN_VISUAL = (1 << 3);
/**
* One or more of this node's descendents has had a change in bounds, which
* means that the overall bounds of this group will need recalculation.
* (Only applicable to SGGroup and SGFilter nodes.)
*/
static final int DIRTY_CHILDREN_BOUNDS = (1 << 4);
/**
* The dirty state of this node. This is initialized to DIRTY_VISUAL so that
* this node is painted for the very first paint cycle.
*/
private int dirtyState = DIRTY_VISUAL;
/**
* The most recently painted bounds of this node (transformed relative to
* the root node, i.e., in device space). This field is initially set to
* null and is updated everytime the node is actually rendered to the
* destination. It is later used in the case of DIRTY_BOUNDS for the
* purposes of accumulating the former (dirty) bounds of a particular node.
*/
private Bounds lastRenderedBounds;
static Bounds accumulate(Bounds accumulator, Bounds newBox) {
return accumulate(accumulator, newBox, false);
}
static Bounds accumulate(Bounds accumulator, Bounds newBox,
boolean newBoxShareable) {
if (newBox == null || newBox.isEmpty()) {
return accumulator;
}
if (accumulator == null) {
// TODO: We really shouldn't be so trusting of the incoming
// Rectangle type - we should instantiate a (platform sensitive)
// specific type like R2D.Double (desktop) or R2D.Float (phone)
return (newBoxShareable ? newBox : (BoundingBox) newBox.clone());
}
accumulator.combine(newBox);
return accumulator;
}
/*
* Rendering code below...
*/
/**
* Render the tree of nodes to the specified {@link Graphics2D} object
* descending from this node as the root. The {@code dirtyRegion} parameter
* can be used to cull the rendering operations on the tree so that only
* parts of the tree that intersect the indicated rectangle (in device
* space) will be visited and rendered. If the {@code dirtyRegion} parameter
* is null then all parts of the tree will be visited and rendered whether
* they will eventually be visible or not.
*
* @param g
* the {@code Graphics2D} object to render into
* @param dirtyRegion
* a Rectangle to cull which parts of the tree to operate on, or
* null if the full tree should be visited and rendered
*
*
*
* Note: not sure yet if I want to attempt render clipping in
* opengl es... --- ignore the dirtyRegion for now ---
*/
public final void render(GL10 gl) {
if (!isVisible()) {
return;
}
gl.glPushMatrix();
/*
* run scale events
*/
if (scaleEvents != null) {
final HashSet<GraphNodeEvent> events = scaleEvents;
for (GraphNodeEvent scale : events) {
scale.run();
}
}
/*
* run translation events
*/
if (translationEvents != null) {
final HashSet<GraphNodeEvent> events = translationEvents;
for (GraphNodeEvent translation : events) {
translation.run();
}
}
/*
* run rotation events
*/
if (rotationEvents != null) {
final HashSet<GraphNodeEvent> events = rotationEvents;
for (GraphNodeEvent rotation : events) {
rotation.run();
}
}
/*
* render children if a parent
*/
if (this instanceof SGParent) {
for (SGNode child : ((SGParent) this).getChildren()) {
child.render(gl);
}
/*
* else, paint if you are a shape
*/
} else if (this instanceof SGAbstractShape) {
((SGAbstractShape) this).paint(gl);
}
/*
* pop out of this nodes matrix
*/
gl.glPopMatrix();
}
public void renderColorID(GL10 gl) {
gl.glPushMatrix();
/*
* run scale events
*/
if (scaleEvents != null) {
final HashSet<GraphNodeEvent> events = scaleEvents;
for (GraphNodeEvent scale : events) {
scale.run();
}
}
/*
* run translation events
*/
if (translationEvents != null) {
final HashSet<GraphNodeEvent> events = translationEvents;
for (GraphNodeEvent translation : events) {
translation.run();
}
}
/*
* run rotation events
*/
if (rotationEvents != null) {
final HashSet<GraphNodeEvent> events = rotationEvents;
for (GraphNodeEvent rotation : events) {
rotation.run();
}
}
/*
* render children if a parent
*/
if (this instanceof SGParent) {
for (SGNode child : ((SGParent) this).getChildren()) {
child.renderColorID(gl);
}
/*
* else, paint if you are a shape
*/
} else if (this instanceof SGAbstractShape) {
((SGAbstractShape) this).paintColorID(gl);
}
gl.glPopMatrix();
}
}