/*
* Copyright 2015 JBoss, by Red Hat, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.uberfire.ext.wires.core.trees.client.shapes;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.ait.lienzo.client.core.animation.AnimationProperties;
import com.ait.lienzo.client.core.animation.AnimationTweener;
import com.ait.lienzo.client.core.animation.IAnimation;
import com.ait.lienzo.client.core.animation.IAnimationCallback;
import com.ait.lienzo.client.core.animation.IAnimationHandle;
import com.ait.lienzo.client.core.event.NodeDragMoveEvent;
import com.ait.lienzo.client.core.event.NodeDragMoveHandler;
import com.ait.lienzo.client.core.shape.Group;
import com.ait.lienzo.client.core.types.Point2D;
import org.uberfire.commons.data.Pair;
import org.uberfire.ext.wires.core.api.layout.LayoutManager;
import org.uberfire.ext.wires.core.api.layout.RequiresLayoutManager;
import org.uberfire.ext.wires.core.api.shapes.RequiresShapesManager;
import org.uberfire.ext.wires.core.api.shapes.ShapesManager;
import org.uberfire.ext.wires.core.api.shapes.WiresBaseShape;
import org.uberfire.ext.wires.core.trees.client.canvas.WiresTreeNodeConnector;
import org.uberfire.ext.wires.core.trees.client.layout.WiresLayoutUtilities;
import org.uberfire.ext.wires.core.trees.client.layout.treelayout.Rectangle2D;
import org.uberfire.mvp.Command;
public abstract class WiresBaseTreeNode extends WiresBaseShape implements RequiresShapesManager,
RequiresLayoutManager {
private static final int ANIMATION_DURATION = 250;
protected ShapesManager shapesManager;
protected LayoutManager layoutManager;
private WiresBaseTreeNode parent;
private List<WiresBaseTreeNode> children = new ArrayList<WiresBaseTreeNode>();
private List<WiresTreeNodeConnector> connectors = new ArrayList<WiresTreeNodeConnector>();
private IAnimationHandle animationHandle;
private int collapsed = 0;
public WiresBaseTreeNode() {
//Update connectors when this Node moves
addNodeDragMoveHandler(new NodeDragMoveHandler() {
@Override
public void onNodeDragMove(final NodeDragMoveEvent nodeDragMoveEvent) {
for (WiresTreeNodeConnector connector : connectors) {
connector.getPoints().get(0).set(getLocation());
}
getLayer().batch();
}
});
}
@Override
public void setShapesManager(final ShapesManager shapesManager) {
this.shapesManager = shapesManager;
}
@Override
public void setLayoutManager(final LayoutManager layoutManager) {
this.layoutManager = layoutManager;
}
@Override
public boolean contains(final double cx,
final double cy) {
return false;
}
@Override
public void destroy() {
//Remove children
final List<WiresBaseTreeNode> cloneChildren = new ArrayList<WiresBaseTreeNode>(children);
for (WiresBaseTreeNode child : cloneChildren) {
shapesManager.forceDeleteShape(child);
}
children.clear();
//Remove connectors to children
final List<WiresTreeNodeConnector> cloneConnectors = new ArrayList<WiresTreeNodeConnector>(connectors);
for (WiresTreeNodeConnector connector : cloneConnectors) {
getLayer().remove(connector);
}
connectors.clear();
//Remove from its parent
if (parent != null) {
parent.removeChildNode(this);
}
super.destroy();
}
public WiresBaseTreeNode getParentNode() {
return this.parent;
}
public void setParentNode(final WiresBaseTreeNode parent) {
this.parent = parent;
}
/**
* TreeNodes can decide to accept child TreeNodes when being dragged from the Palette to a prospective parent
* @param child TreeNode that will be added to this TreeNode as a child
* @return true if the child can be added to this TreeNode
*/
public boolean acceptChildNode(final WiresBaseTreeNode child) {
//Accept all types of WiresBaseTreeNode by default
return true;
}
/**
* Add a TreeNode as a child to this TreeNode. A connector is automatically created and maintained for the child.
* Connectors are "outgoing" from the parent to a child.
* @param child
*/
public void addChildNode(final WiresBaseTreeNode child) {
final WiresTreeNodeConnector connector = new WiresTreeNodeConnector();
connector.getPoints().get(0).set(getLocation());
connector.getPoints().get(1).set(child.getLocation());
getLayer().add(connector);
connector.moveToBottom();
final int index = getChildIndex(connector);
children.add(index,
child);
connectors.add(index,
connector);
child.setParentNode(this);
//Update connectors when child Node moves
child.addNodeDragMoveHandler(new NodeDragMoveHandler() {
@Override
public void onNodeDragMove(final NodeDragMoveEvent nodeDragMoveEvent) {
connector.getPoints().get(1).set(child.getLocation());
}
});
}
//Get the index of the new child connector by determining the angle of existing connectors it lays in between
private int getChildIndex(final WiresTreeNodeConnector newConnector) {
final double newConnectorTheta = getConnectorAngle(newConnector);
for (int index = 0; index < connectors.size(); index++) {
final WiresTreeNodeConnector existingConnector = connectors.get(index);
final double existingConnectorTheta = getConnectorAngle(existingConnector);
if (newConnectorTheta > existingConnectorTheta) {
return index;
}
}
return connectors.size();
}
private double getConnectorAngle(final WiresTreeNodeConnector connector) {
final double cdx = connector.getPoints().get(1).getX() - connector.getPoints().get(0).getX();
final double cdy = connector.getPoints().get(1).getY() - connector.getPoints().get(0).getY();
final double theta = Math.atan2(cdy,
cdx) + Math.PI / 2;
return (theta < 0 ? theta + (2 * Math.PI) : theta);
}
/**
* Remove a child TreeNode from this TreeNode. Connectors are automatically cleared up.
* @param child
*/
public void removeChildNode(final WiresBaseTreeNode child) {
child.setParentNode(null);
final int index = children.indexOf(child);
final WiresTreeNodeConnector connector = connectors.get(index);
children.remove(child);
connectors.remove(connector);
getLayer().remove(connector);
}
public List<WiresBaseTreeNode> getChildren() {
return this.children;
}
public abstract double getWidth();
public abstract double getHeight();
private void childMoved(final WiresBaseTreeNode child,
final double nx,
final double ny) {
final int index = children.indexOf(child);
final WiresTreeNodeConnector connector = connectors.get(index);
connector.getPoints().get(1).setX(nx);
connector.getPoints().get(1).setY(ny);
}
/**
* Collapse this TreeNode and all descendants.
* @param callback The callback is invoked when the animation completes.
*/
public void collapse(final Command callback) {
//This TreeNode is already collapsed
if (!hasChildren() || hasCollapsedChildren()) {
return;
}
if (animationHandle != null) {
animationHandle.stop();
}
animationHandle = animate(AnimationTweener.EASE_OUT,
new AnimationProperties(),
ANIMATION_DURATION,
new IAnimationCallback() {
private List<WiresBaseTreeNode> descendants;
private Map<WiresBaseShape, Pair<Point2D, Point2D>> transformations = new HashMap<WiresBaseShape, Pair<Point2D, Point2D>>();
private Map<WiresBaseShape, Point2D> layout;
private Rectangle2D canvasBounds;
@Override
public void onStart(final IAnimation iAnimation,
final IAnimationHandle iAnimationHandle) {
//Mark all descendants as collapsed, which affects the layout information
descendants = getDescendants(WiresBaseTreeNode.this);
for (WiresBaseTreeNode descendant : descendants) {
descendant.collapsed++;
}
//Get new layout information
layout = layoutManager.getLayoutInformation(getTreeRoot());
canvasBounds = WiresLayoutUtilities.alignLayoutInCanvas(layout);
//Store required transformations: Shape, Current location, Target location
transformations.clear();
for (Map.Entry<WiresBaseShape, Point2D> e : layout.entrySet()) {
final Point2D origin = e.getKey().getLocation();
final Point2D destination = e.getValue();
transformations.put(e.getKey(),
new Pair<Point2D, Point2D>(origin,
destination));
}
//Allow subclasses to change their appearance
onCollapseStart();
}
@Override
public void onFrame(final IAnimation iAnimation,
final IAnimationHandle iAnimationHandle) {
//Lienzo's IAnimation.getPercent() passes values > 1.0
final double pct = iAnimation.getPercent() > 1.0 ? 1.0 : iAnimation.getPercent();
//Move each descendant along the line between its origin and the target destination
for (Map.Entry<WiresBaseShape, Pair<Point2D, Point2D>> e : transformations.entrySet()) {
final Point2D descendantOrigin = e.getValue().getK1();
final Point2D descendantTarget = e.getValue().getK2();
final double dx = (descendantTarget.getX() - descendantOrigin.getX()) * pct;
final double dy = (descendantTarget.getY() - descendantOrigin.getY()) * pct;
e.getKey().setX(descendantOrigin.getX() + dx);
e.getKey().setY(descendantOrigin.getY() + dy);
}
for (WiresBaseTreeNode descendant : descendants) {
descendant.setAlpha(1.0 - pct);
}
//Allow subclasses to change their appearance
onCollapseProgress(pct);
//Without this call Lienzo doesn't update the Canvas for sub-classes of WiresBaseTreeNode
WiresBaseTreeNode.this.getLayer().batch();
}
@Override
public void onClose(final IAnimation iAnimation,
final IAnimationHandle iAnimationHandle) {
//Hide connectors, descendants and descendants' connectors when complete
for (WiresTreeNodeConnector connector : connectors) {
connector.setVisible(false);
}
for (WiresBaseTreeNode descendant : descendants) {
descendant.setVisible(false);
for (WiresTreeNodeConnector connector : descendant.connectors) {
connector.setVisible(false);
}
}
//Allow subclasses to change their appearance
onCollapseEnd();
//Invoke callback if one was provided
if (callback != null) {
callback.execute();
}
WiresLayoutUtilities.resizeViewPort(canvasBounds,
WiresBaseTreeNode.this.getViewport());
}
});
getLayer().batch();
}
/**
* Called when the TreeNode is about to be collapsed. Default implementation does nothing.
*/
public void onCollapseStart() {
//Do nothing by default
}
/**
* Called while the TreeNode is being collapsed. Default implementation does nothing.
* @param pct 0.0 to 1.0 where 1.0 is collapsed
*/
public void onCollapseProgress(final double pct) {
//Do nothing by default
}
/**
* Called when the TreeNode has been collapsed. Default implementation does nothing.
*/
public void onCollapseEnd() {
//Do nothing by default
}
/**
* Expand this TreeNode and all descendants. Nested collapsed TreeNodes are not expanded.
* @param callback The callback is invoked when the animation completes.
*/
public void expand(final Command callback) {
//This TreeNode is already expanded
if (!hasCollapsedChildren()) {
return;
}
if (animationHandle != null) {
animationHandle.stop();
}
animationHandle = animate(AnimationTweener.EASE_OUT,
new AnimationProperties(),
ANIMATION_DURATION,
new IAnimationCallback() {
private List<WiresBaseTreeNode> descendants;
private Map<WiresBaseShape, Pair<Point2D, Point2D>> transformations = new HashMap<WiresBaseShape, Pair<Point2D, Point2D>>();
@Override
public void onStart(final IAnimation iAnimation,
final IAnimationHandle iAnimationHandle) {
//Show connectors to this node's immediate children
for (WiresTreeNodeConnector connector : connectors) {
connector.setVisible(true);
}
//Show child nodes and connectors if they are not still collapsed
descendants = getDescendants(WiresBaseTreeNode.this);
for (WiresBaseTreeNode descendant : descendants) {
descendant.collapsed--;
if (descendant.collapsed == 0) {
descendant.setVisible(true);
}
}
for (WiresBaseTreeNode descendant : descendants) {
for (WiresTreeNodeConnector connector : descendant.connectors) {
connector.setVisible(!descendant.hasCollapsedChildren());
}
}
//Get new layout information
final Map<WiresBaseShape, Point2D> layout = layoutManager.getLayoutInformation(getTreeRoot());
final Rectangle2D canvasBounds = WiresLayoutUtilities.alignLayoutInCanvas(layout);
//Store required transformations: Shape, Current location, Target location
transformations.clear();
for (Map.Entry<WiresBaseShape, Point2D> e : layout.entrySet()) {
final Point2D origin = e.getKey().getLocation();
final Point2D destination = e.getValue();
transformations.put(e.getKey(),
new Pair<Point2D, Point2D>(origin,
destination));
}
//Allow subclasses to change their appearance
onExpandStart();
WiresLayoutUtilities.resizeViewPort(canvasBounds,
WiresBaseTreeNode.this.getViewport());
}
@Override
public void onFrame(final IAnimation iAnimation,
final IAnimationHandle iAnimationHandle) {
//Lienzo's IAnimation.getPercent() passes values > 1.0
final double pct = iAnimation.getPercent() > 1.0 ? 1.0 : iAnimation.getPercent();
//Move each descendant along the line between its origin and the target destination
for (Map.Entry<WiresBaseShape, Pair<Point2D, Point2D>> e : transformations.entrySet()) {
final Point2D descendantOrigin = e.getValue().getK1();
final Point2D descendantTarget = e.getValue().getK2();
final double dx = (descendantTarget.getX() - descendantOrigin.getX()) * pct;
final double dy = (descendantTarget.getY() - descendantOrigin.getY()) * pct;
e.getKey().setX(descendantOrigin.getX() + dx);
e.getKey().setY(descendantOrigin.getY() + dy);
}
for (WiresBaseTreeNode descendant : descendants) {
descendant.setAlpha(pct);
}
//Allow subclasses to change their appearance
onExpandProgress(pct);
//Without this call Lienzo doesn't update the Canvas for sub-classes of WiresBaseTreeNode
WiresBaseTreeNode.this.getLayer().batch();
}
@Override
public void onClose(final IAnimation iAnimation,
final IAnimationHandle iAnimationHandle) {
//Allow subclasses to change their appearance
onExpandEnd();
//Invoke callback if one was provided
if (callback != null) {
callback.execute();
}
}
});
getLayer().batch();
}
/**
* Get the root node for the tree in which this node exists
* @return The root
*/
private WiresBaseTreeNode getTreeRoot() {
WiresBaseTreeNode root = this;
while (root.parent != null) {
root = root.parent;
}
return root;
}
/**
* Called when the TreeNode is about to be expanded. Default implementation does nothing.
*/
public void onExpandStart() {
//Do nothing by default
}
/**
* Called while the TreeNode is being expanded. Default implementation does nothing.
* @param pct 0.0 to 1.0 where 1.0 is expanded
*/
public void onExpandProgress(final double pct) {
//Do nothing by default
}
/**
* Called when the TreeNode has been expanded. Default implementation does nothing.
*/
public void onExpandEnd() {
//Do nothing by default
}
protected List<WiresBaseTreeNode> getDescendants(final WiresBaseTreeNode node) {
final List<WiresBaseTreeNode> descendants = new ArrayList<WiresBaseTreeNode>();
descendants.addAll(node.children);
for (WiresBaseTreeNode child : node.children) {
descendants.addAll(getDescendants(child));
}
return descendants;
}
public boolean hasChildren() {
return children.size() > 0;
}
public boolean hasCollapsedChildren() {
for (WiresBaseTreeNode child : children) {
if (child.collapsed > 0) {
return true;
}
}
return false;
}
//Move the Connector end-points to match where the descendant has been moved
private void updateConnectorsEndPoints() {
if (connectors == null) {
return;
}
for (WiresTreeNodeConnector connector : connectors) {
connector.getPoints().get(0).setX(getX());
connector.getPoints().get(0).setY(getY());
}
if (parent != null) {
parent.childMoved(this,
getX(),
getY());
}
}
@Override
public Group setX(final double x) {
final Group g = super.setX(x);
updateConnectorsEndPoints();
return g;
}
@Override
public Group setY(final double y) {
final Group g = super.setY(y);
updateConnectorsEndPoints();
return g;
}
}