/*
* Copyright 2003-2010 Tufts University Licensed under the
* Educational Community 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.osedu.org/licenses/ECL-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 tufts.vue.ds;
import tufts.Util;
import tufts.vue.DEBUG;
import tufts.vue.gui.GUI;
import tufts.vue.MetaMap;
import tufts.vue.LWComponent;
import tufts.vue.LWNode;
import tufts.vue.LWLink;
import tufts.vue.LWMap;
import tufts.vue.MapDropTarget;
import static tufts.vue.MapDropTarget.*;
import tufts.vue.ds.DataTree.Criteria;
import tufts.vue.ds.DataTree.DataNode;
import tufts.vue.ds.DataTree.RowNode;
import tufts.vue.ds.DataTree.AllRowsNode;
import tufts.vue.ds.DataTree.SmartSearch;
import java.util.List;
import java.util.Collection;
import java.util.Collections;
import java.util.ArrayList;
import com.google.common.collect.Multiset;
/**
* Once a Row, Field, or Value has been selected for dragging from the DataTree,
* this handles what happens when it's dropped on the map. What happends depends
* on what it's dropped on.
*
* @version $Revision: 1.8 $ / $Date: 2010-02-03 19:13:16 $ / $Author: mike $
* @author Scott Fraize
*/
class DataDropHandler extends MapDropTarget.DropHandler
{
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(DataDropHandler.class);
/** the dropping DataNode -- note that these are not VUE nodes, they're nodes from the VUE DataTree JTree impl */
private final DataNode droppingDataItem;
private final DataTree dataTree;
@Override public String toString() {
return droppingDataItem.toString();
}
DataDropHandler(DataNode n, DataTree tree) {
droppingDataItem = n;
dataTree =tree;
}
/** DropHandler */
@Override public DropIndication getIndication(LWComponent target, int requestAction) {
final int acceptAction;
if (target instanceof LWNode && target.isDataNode()) {
acceptAction = java.awt.dnd.DnDConstants.ACTION_LINK; // indicate data-action
}
else if (target == null) {
// default map drop
acceptAction = requestAction;
}
else {
//return DnDConstants.ACTION_COPY; // copy-only (e.g., no resource-link creation for "regular" nodes)
// we're attempt to drop onto a non-data node:
return DropIndication.rejected();
}
return new DropIndication(DROP_ACCEPT_DATA, acceptAction, target);
}
static final boolean AUTO_FIT = false;
/** DropHandler */
@Override public boolean handleDrop(final DropContext drop)
{
final List<LWComponent> clusteringTargets = new ArrayList(); // nodes already on the map
final List<LWComponent> newNodes; // nodes created
if (drop.hit != null && drop.hit.isDataNode()) {
newNodes = produceRelatedNodes(droppingDataItem, drop, clusteringTargets);
// clearing hit/hitParent this will prevent MapDropTarget from
// adding the nodes to the target node, and will then add them
// directly to the map. Todo: something cleaner (allow an
// override in the DropHandler for what to do w/parenting)
drop.hit = drop.hitParent = null;
if (DEBUG.Enabled) Log.debug("FILTER EXTRACTED: " + Util.tags(newNodes));
} else {
if (DEBUG.Enabled) Log.debug("PRODUCING NODES ON THE MAP (no drop target for filtering)");
newNodes = produceAllDroppedNodes(droppingDataItem);
// note: clusteringTargets will always be empty here
}
if (newNodes == null || newNodes.size() == 0)
return false;
// We might as well run in right in the drop action and at least get a chance at having the drop cursor:
// GUI.invokeAfterAWT(new Runnable() { public void run() {
// GUI.activateWaitCursor(); // *** STILL isn't working even though the drop is complete & drag/drop cursor should be cleared
// try {
Log.info("servicing the drop: " + drop);
serviceDrop(DataDropHandler.this, drop, newNodes, clusteringTargets);
if (drop.items != null && drop.items.size() > 0)
MapDropTarget.addNodesToMap(drop);
MapDropTarget.completeDrop(drop);
String undoName = "Data Drop";
if (droppingDataItem.getField() != null)
undoName += " (" + droppingDataItem.getField().getName() + ")";
drop.viewer.getMap().getUndoManager().mark(undoName);
//DEAL WITH MATRIX DATA AND ADD LINK WHEN APPROPRIATE
if (droppingDataItem.getSchema().isMatrixDataSet)
{
GUI.invokeAfterAWT(new Runnable() { public void run() {
dataTree.applyMatrixRelations(newNodes);
}});
}
return true;
}
private static void serviceDrop
(DataDropHandler handler,
DropContext drop,
List<LWComponent> newNodes,
List<LWComponent> clusteringTargets)
{
final boolean doZoomFit = handler.createLinksAndLayoutNodes(drop, newNodes, clusteringTargets);
//-------------------------------------------------------
// TODO: handle selection manually (not in MapDropTarget)
// so we can add the field style to the selection in case
// what was dropped is immediately styled
//-------------------------------------------------------
drop.items = newNodes; // tells MapDropTarget to add these to the map
// tell MapDropTarget what to select:
// todo: either make this always handled by the drop handler or make
// a clean API for this (MapDropTargets has standard things to do
// regarding application focus it is going to handle after we return,
// and we don't want to be worrying about that in DropHandler's)
// if (clusteringTargets.size() > 0) {
// drop.select = new ArrayList(newNodes);
// drop.select.addAll(clusteringTargets);
// } else {
// drop.select = newNodes;
// }
drop.select = null; // tells MapDropTarget to skip selection handling (we do it here)
final tufts.vue.LWSelection s = drop.viewer.getSelection();
s.clear();
s.setSource(drop.viewer);
s.setSelectionSourceFocal(drop.viewer.getFocal());
// USER USE-CASE CONFLICT:
//
// If what the user wants to do next after this drop would be to style all the row nodes that
// appeared (say, change the fill color), what we'd like is to select only the NEW NODES,
// and set the selection with the style for those nodes.
//
// If what the user wants to do is drag the new nodes created along with their center clustering
// node to somewhere else on the map, we *do* want the center cluster node in the selection, but then
// we can't set the selection style, as both the row-node selection style and the center-clutering
// value-node style are in play.
//
// For now, we're prioritizing the first case: dragging the new cluster somewhere.
final DataNode droppingDataItem = handler.droppingDataItem; // todo: need to refactor these methods to all be statics
if (clusteringTargets.size() > 0) {
if (DEBUG.Enabled) Log.debug("SELECTING based on both new nodes and clustering targets " + Util.tags(clusteringTargets));
s.add(new Util.GroupIterator(newNodes, clusteringTargets));
} else {
if (DEBUG.Enabled) Log.debug("SELECTING based on " + droppingDataItem + "; hasStyle=" + droppingDataItem.hasStyle());
if (droppingDataItem.hasStyle()) {
s.setWithStyle(newNodes, "", droppingDataItem.getStyle());
} else {
s.setTo(newNodes);
}
}
if (doZoomFit) {
drop.viewer.setZoomFit();
} else {
// the various layout actions can completely screw up the map view for some reason,
// so we always make sure the map is at least SOMEWHAT visible at the end.
// TODO: debug what the layout actions are doing to the view.
drop.viewer.ensureMapVisible();
}
// can't do this yet: MapDropTarget still has yet to do the actual add-to-map,
// and then it will generate the generic "Drop" undo-mark.
//drop.viewer.getMap().getUndoManager().mark("Data Drop: " + droppingDataItem);
}
private static List<LWComponent> produceAllDroppedNodes(final DataNode treeNode)
{
final Field field = treeNode.getField();
final Schema schema = treeNode.getSchema();
Log.debug("PRODUCING NODES FOR FIELD: " + field);
Log.debug(" IN SCHEMA: " + schema);
final java.util.List<LWComponent> nodes;
LWNode n = null;
if (treeNode instanceof RowNode) {
Log.debug("PRODUCING SINGLE ROW NODE");
nodes = DataAction.makeSingleRowNode(schema, treeNode.getRow());
//} else if (treeNode.isRowNode()) {
} else if (treeNode instanceof AllRowsNode) {
List<LWComponent> _nodes = null;
Log.debug("PRODUCING ALL DATA NODES");
try {
_nodes = DataAction.makeRowNodes(schema);
Log.debug("PRODUCED ALL DATA NODES; nodeCount="+_nodes.size());
} catch (Throwable t) {
Util.printStackTrace(t);
}
nodes = _nodes;
} else if (treeNode.isValue()) {
Log.debug("PRODUCING A SINGLE VALUE NODE");
// is a single value from a column
nodes = Collections.singletonList(DataAction.makeValueNode(field, treeNode.getValue()));
} else {
Log.debug("PRODUCING ALL VALUE NODES FOR FIELD: " + field);
nodes = new ArrayList();
// handle all the enumerated values for a column
for (String value : field.getValues()) {
nodes.add(DataAction.makeValueNode(field, value));
}
}
return nodes;
}
/**
* Combine the given tree node with the drop target (e.g., search/filter) to find
* the new nodes to create
*/
private static List<LWComponent> produceRelatedNodes
(final DataNode treeNode,
final DropContext drop,
final List<LWComponent> clusteringTargets)
{
final List<LWComponent> dropTargets = new ArrayList();
final Field dragField = treeNode.getField();
Schema dragSchema = null;
boolean draggingAllRows = false;
if (treeNode instanceof AllRowsNode) {
dragSchema = treeNode.getSchema();
draggingAllRows = true;
}
if (drop.hit.isSelected()) {
dropTargets.addAll(drop.viewer.getSelection());
} else {
dropTargets.add(drop.hit);
}
Log.debug("DATA ACTION ON " + drop.hit
+ "\n\tdropTargets: " + Util.tags(dropTargets)
+ "\n\t dragField: " + dragField
+ "\n\t dragSchema: " + dragSchema);
//-----------------------------------------------------------------------------
// TODO: "merge" action for VALUE nodes.
// For value nodes with the same value, delete one, merge all links
// For value nodes with with DIFFERENT keys and/or values, create a COMPOUND VALUE
// node that either has multiple values, or multiple keys and values.
// This will complicate the hell out of the search code tho.
//-----------------------------------------------------------------------------
final List <LWComponent> newNodes = new ArrayList();
for (LWComponent dropTarget : dropTargets) {
final MetaMap dropTargetData;
if (dropTarget.isDataNode()) {
dropTargetData = dropTarget.getRawData();
clusteringTargets.add(dropTarget);
if (draggingAllRows) {
// TODO: dropTargetData instead of dropTarget?
newNodes.addAll(DataAction.makeRelatedRowNodes(dragSchema, dropTarget));
}
else {
newNodes.addAll(DataAction.makeRelatedNodes(dragField, dropTarget));
}
// else if (dropTarget.isDataRowNode()) {
// // TODO: if dropTarget is a single value node, this makes no sense
// newNodes.addAll(DataAction.makeRelatedValueNodes(dragField, dropTargetData));
// }
// else { // if (dropTarget.isDataValueNode())
// Log.debug("UNIMPLEMENTED: hierarchy use case? relate linked to of "
// + dropTarget + " based on " + dragField,
// new Throwable("HERE"));
// // if a value node, find all ROW nodes connected to it, and color
// // them based on the VALUES from the dragged Field?
// // Or, add all the values nodes and recluster all of of the linked
// // items based on that -- this is the HIERARCHY USE CASE.
// return false;
// }
} else if (dropTarget.hasResource()) {
// TODO: what is dragField going to be? Can we drag from the meta-data pane?
dropTargetData = dropTarget.getResource().getProperties();
newNodes.addAll(DataAction.makeRelatedValueNodes(dragField, dropTargetData));
}
}
return newNodes;
}
private boolean createLinksAndLayoutNodes
(final DropContext drop,
final List<LWComponent> newNodes,
final List<LWComponent> clusteringTargets)
{
//-----------------------------------------------------------------------------
// Currently, we must set node locations before adding any links, as when
// the link-add events happen, the viewer may adjust the canvas size
// to include room for the new links, which will all be linking to 0,0
// unless the nodes have had their locations set, even if the nodes
// are about to be re-laid out via a group clustering.
//
// TODO: can we re-work all the layout code so the nodes and links don't
// have to first be added to the map? It would really clean some things up.
// [I think this has already been done...]
//-----------------------------------------------------------------------------
boolean zoomFit = false;
if (DEBUG.Enabled) {
Log.debug("createLinksAndLayoutNodes:"
+ "\n\tnewNodes: " + Util.tags(newNodes)
+ "\n\tclusteringTargets: " + clusteringTargets.size());
Util.dump(clusteringTargets);
}
//-----------------------------------------------------------------------------
// First, locate all the nodes at the drop location -- so that any that
// aren't later laid out elsewhere will at least appear there.
//-----------------------------------------------------------------------------
MapDropTarget.setCenterAt(newNodes, drop.location);
final Object[] result =
DataAction.addDataLinksForNodes(drop.viewer.getMap(),
newNodes,
droppingDataItem.getField());
final Multiset<LWComponent> targetsUsed = (Multiset) result[0];
final List<LWLink> linksAdded = (List) result[1];
if (DEBUG.Enabled) {
// TODO: targetsUsed is empty for targets found in cross-schema joins...
Log.debug("targetsUsed: " + Util.tags(targetsUsed));
Log.debug(" linksAdded: " + Util.tags(linksAdded));
//Util.dump(targetsUsed.entrySet());
}
if (clusteringTargets.size() > 0) {
//tufts.vue.Actions.MakeCluster.doClusterAction(clusterNode, newNodes);
for (LWComponent center : clusteringTargets) {
tufts.vue.Actions.MakeCluster.doClusterAction(center, center.getClustered());
}
}
// else if (drop.isLinkAction) {
// //tufts.vue.Actions.MakeCluster.act(newNodes); // TODO: GET THIS WORKING -- needs to work w/out a center
// tufts.vue.LayoutAction.filledCircle.act(newNodes, AUTO_FIT); // TODO: this goes into infinite loops sometimes!
// }
else {
// TODO: pass isLinkAction to clusterNodes and sort out there
zoomFit = clusterNodes(drop, newNodes, linksAdded.size() > 0);
}
return zoomFit;
}
private boolean clusterNodes
(final DropContext drop,
final List<LWComponent> nodes,
final boolean newLinksAvailable)
{
if (DEBUG.Enabled) Log.debug("clusterNodes: " + Util.tags(nodes) + "; addedLinks=" + newLinksAvailable);
if (nodes.size() <= 1) {
if (DEBUG.Enabled) Log.debug("clusterNodes: skipping: not enough nodes");
return false;
}
boolean zoomFit = false;
final LWMap map = drop.viewer.getMap();
final boolean didFullReorganization = map.hasState(LWMap.State.HAS_AUTO_CLUSTERED);
if (DEBUG.Enabled) Log.debug("clusterNodes: map has already re-organized: " + didFullReorganization);
if (newLinksAvailable) {
// now we set this state ANY time new links are created: is screwing up too
// many maps and disabling other times when we actually want a map-deformation
// to happen.
map.setState(LWMap.State.HAS_AUTO_CLUSTERED);
}
try {
if (droppingDataItem.isRowNode()) {
tufts.vue.LayoutAction.random.act(nodes, AUTO_FIT);
}
else if (newLinksAvailable && !didFullReorganization) {
// // TODO: Use the fast clustering code if we can -- filledCircle can
// // be VERY slow, and sometimes hangs!
// // TODO: the center nodes still need to be laid out in the big grid!
// for (LWComponent center : nodes) {
// tufts.vue.Actions.MakeCluster.doClusterAction(center, center.getLinked());
// }
// TODO: cluster will currently fail (NPE) if no data-links exist
// Note: this action will re-arrange all the data-nodes on the map
try {
tufts.vue.LayoutAction.cluster.act(nodes, AUTO_FIT);
map.setState(LWMap.State.HAS_AUTO_CLUSTERED);
} catch (Throwable t) {
Log.warn("clustering failure on " + Util.tags(nodes), t);
}
//tufts.vue.Actions.MakeCluster.act(new tufts.vue.LWSelection(nodes));
zoomFit = true;
}
else if (newLinksAvailable) {
final boolean DEFORM_MAP_FOR_NEW_NODES = !drop.isLinkAction;
DataAction.centroidCluster(map, nodes, DEFORM_MAP_FOR_NEW_NODES);
} else {
// may want to do this any way for anything that didn't have a centroid
tufts.vue.LayoutAction.filledCircle.act(nodes, AUTO_FIT);
}
} catch (Throwable t) {
Log.error("clustering failure: " + Util.tags(nodes), t);
zoomFit = false;
}
return zoomFit;
}
}