/* * 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; } }