/* * 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. */ // TODO: see http://publicobject.com/glazedlists/ as a possible baseline for a much // fancier data interface -- tabular, and big, but may have enough flexibility // to still be handy. package tufts.vue.ds; import tufts.vue.LWSelection; import tufts.vue.MapViewer; import tufts.vue.VUE; import tufts.vue.DEBUG; import tufts.vue.Resource; import tufts.vue.VueResources; import tufts.vue.LWComponent; import tufts.vue.LWNode; import tufts.vue.LWLink; import tufts.vue.LWMap; import tufts.vue.LWKey; import tufts.vue.gui.GUI; import tufts.vue.gui.Widget; import tufts.Util; import tufts.vue.VueConstants; import java.util.List; import java.util.*; import java.net.URL; import java.awt.*; import java.awt.event.*; import java.awt.dnd.*; import java.awt.geom.Point2D; import java.awt.geom.RectangularShape; import javax.swing.*; import javax.swing.border.*; import javax.swing.event.*; import javax.swing.tree.*; import com.google.common.collect.*; /** * UI component for browsing the Fields and Rows of a fully loaded * Schema, providing the status of data elements releative to the * currently active map, code for adding new nodes to the current map, * and initiating drags of fields or rows destined for a map. * * @version $Revision: 1.109 $ / $Date: 2010-02-03 19:13:16 $ / $Author: mike $ * @author Scott Fraize */ public class DataTree extends javax.swing.JTree implements DragGestureListener { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(DataTree.class); protected final Schema mSchema; private DataNode mRootNode; protected DataNode mAllRowsNode; private DataNode mSelectedSearchNode; private final JLabel mNewRowsLabel = new JLabel(); private final JLabel mChangedRowsLabel = new JLabel(); private final JCheckBox mNewRowsCheckBox = new JCheckBox(); private final JCheckBox mChangedRowsCheckBox = new JCheckBox(); private final AbstractButton mUpdateButton = new JButton(VueResources.getString("dockWindow.contentPanel.sync.updateMap")); private final AbstractButton mSendToMapButton = new JButton("Send to Map"); private final DefaultTreeModel mTreeModel; private final static Color MEDIUM_DARK_GRAY = new Color(96, 96, 96); private final static boolean DEBUG_LOCAL = false; private Thread mAnnotateThread; private static final MapListener ActiveMapListener = new MapListener(); private static final java.util.concurrent.atomic.AtomicBoolean FirstInstance = new java.util.concurrent.atomic.AtomicBoolean(true); private static final Collection<DataTree> ActiveTrees = new java.util.concurrent.CopyOnWriteArrayList(); private static volatile DataTree ForegroundTree; private static volatile LWMap mActiveMap; private static volatile MapViewer mActiveViewer; private static volatile Collection<LWComponent> ActiveMapDataNodes = Collections.EMPTY_LIST; public static final class MapListener implements LWComponent.Listener, LWSelection.Listener { private static boolean mDataEventWasSeen; /** if the active map changes, we need to wake the annotation thread to re-annotate against the newly active map, * as well as start listening for changes in the active map for running future annotation updates */ public void activeChanged(tufts.vue.ActiveEvent e, final LWMap map) { if (mActiveMap == map) return; if (mActiveMap != null) { mActiveMap.removeLWCListener(this); mActiveViewer.getSelection().removeListener(this); } mActiveMap = map; mActiveViewer = VUE.getActiveViewer(); if (map == null) return; kickOffAnnotations(); mActiveMap.addLWCListener(this); mActiveViewer.getSelection().addListener(this); } public void LWCChanged(tufts.vue.LWCEvent e) { if (mActiveMap == null) { Log.warn("LWCEvent w/no active map: " + e); return; } //if (DEBUG.ANNOTATE) Log.debug("SCANNING DATA EVENT: " + e + "; seenOne=" + mDataEventWasSeen); // TODO: pull one copy of all the active map's descendents, once, in the AWT thread, // before all the annotate's kick off. Will probably need to have a single active map // listener with a list of all active data-tree's instead of each tree listening // to the active map itself. if (e.key == LWKey.UserActionCompleted && mDataEventWasSeen) { // technically, don't need to check after ANY action has been completed: // only if a data node was added/removed from the map. todo: we'll need // a data-changed LWCEvent. if (DEBUG.ANNOTATE) Log.debug("RUNNING ANNOTATE on: " + e); kickOffAnnotations(); mDataEventWasSeen = false; } else if (isDataEvent(e)) { mDataEventWasSeen = true; if (DEBUG.ANNOTATE) Log.debug(" FOUND DATA EVENT: " + e + "; seenOne=" + mDataEventWasSeen); } } private void kickOffAnnotations() { if (DEBUG.ANNOTATE) Log.debug("kicking off annotations for: " + Util.tags(ActiveTrees)); if (ActiveTrees.size() > 0) { loadGlobalDataForAnnotations(); for (DataTree tree : ActiveTrees) tree.kickAnnotate(); } } public void selectionChanged(LWSelection selection) { if (selection.isEmpty()) { if (ActiveTrees.size() > 0) { for (DataTree tree : ActiveTrees) tree.setSelectionPath(null); } } } } // note: this should be called from the AWT thread as it's going to access the main map model private static void loadGlobalDataForAnnotations() { if (mActiveMap == null) { mActiveMap = VUE.getActiveMap(); mActiveViewer = VUE.getActiveViewer(); } if (DEBUG.Enabled) Log.debug("loading global annotation data from map " + mActiveMap); if (mActiveMap != null) { final Collection<LWComponent> allNodes = mActiveMap.getAllDescendents(); final Collection<LWComponent> dataNodes = new ArrayList(allNodes.size()); for (LWComponent c : allNodes) { if (c.isDataNode() && c instanceof LWNode) // leave out data-links for now dataNodes.add(c); } ActiveMapDataNodes = dataNodes; } else { ActiveMapDataNodes = Collections.EMPTY_LIST; } } private static boolean isDataEvent(tufts.vue.LWCEvent e) { // we need to check for any childrenAdded/childrenRemoved right now, just in case ANY of them were data nodes return e.key == LWKey.DataUpdate || e.key == LWKey.HierarchyChanging || e.key == LWKey.Created; } // note: this is usually NOT called from the AWT thread -- is called from a data-source load thread public static JComponent create(Schema schema) { final DataTree tree = new DataTree(schema); //tree.setBorder(new LineBorder(Color.red, 4)); try { tree.restoreAnyExpandedState(); } catch (Throwable t) { Log.warn(t); // an assistave measure only -- non fatal } GUI.invokeOnEDT(new Runnable() { public void run() { if (FirstInstance.getAndSet(false)) { if (DEBUG.ANNOTATE) Log.debug("FIRST RUN"); // Listen for changes to the ActiveMap VUE.addActiveListener(LWMap.class, ActiveMapListener); // Simulate an active-map changed event to make sure we're listening to the active map. ActiveMapListener.activeChanged(null, VUE.getActiveMap()); // ensure the global data is loaded 1st time from the ActiveMap loadGlobalDataForAnnotations(); } tree.kickAnnotate(); ActiveTrees.add(tree); // make sure to do this last -- activeChanged above will kick all 1st time }}); return buildControllerUI(tree); } private void sendSelectedToMap() { sendToMap(getSelectedNode(), mActiveMap); } private void addMissingRowsToMap() { // failsafe: tho the Schema and our tree nodes should already // be updated, make absolutely certian we're current to the // active map by running adding new rows based on our detection // of the rows already in the map. VUE.activateWaitCursor(); try { annotateForMap(mActiveMap); addMissingRowsToMap(mActiveMap); } catch (Throwable t) { Log.warn("addMissingRowsToMap", t); } finally { VUE.clearWaitCursor(); } } private void applyChangesToMap() { // failsafe: tho the Schema and our tree nodes should already // be updated, make absolutely certian we're current to the // active map by running adding new rows based on our detection // of the rows already in the map. VUE.activateWaitCursor(); try { annotateForMap(mActiveMap); applyDataUpdatesToMap(mActiveMap); } catch (Throwable t) { Log.warn("applyChangesToMap", t); } finally { VUE.clearWaitCursor(); } } private void updateMap() { // failsafe: tho the Schema and our tree nodes should already // be updated, make absolutely certian we're current to the // active map by running adding new rows based on our detection // of the rows already in the map. VUE.activateWaitCursor(); try { annotateForMap(mActiveMap); if (mNewRowsCheckBox.isSelected()) { addMissingRowsToMap(mActiveMap); final LWSelection newNodes = VUE.getSelection().clone(); if (mSchema.isMatrixDataSet) { GUI.invokeAfterAWT(new Runnable() { public void run() { applyMatrixRelations(newNodes); }}); } } if (mChangedRowsCheckBox.isSelected()) { applyDataUpdatesToMap(mActiveMap); final LWSelection newNodes = VUE.getSelection().clone(); if (newNodes != null) { VUE.getSelection().add(newNodes); } if (mSchema.isMatrixDataSet) GUI.invokeAfterAWT(new Runnable() { public void run() { applyMatrixRelations(newNodes); }}); } } catch (Throwable t) { Log.warn("updateMap", t); } finally { VUE.clearWaitCursor(); } } int call =0; public synchronized void applyMatrixRelations(List<LWComponent> newNodes) { //System.out.println("APPLY MATRIX RELATIONS : " + call++); List<MatrixRelationship> relations = mSchema.matrixRelations; //for (MatrixRelationship relation: relations) // System.out.println("Relations : " + relation.getFromLabel() + ", " + relation.getToLabel()); for (LWComponent newNode: newNodes) { String trueName = newNode.getRawData().getString(Schema.MATRIX_NAME_FIELD); for (MatrixRelationship relation: relations) { if (relation.getFromLabel().equals(trueName) || relation.getToLabel().equals(trueName)) { for (DataNode n : mAllRowsNode.getChildren()) { RowNode rn = (RowNode)n; if (rn.isMapPresent()) { String potentialTargetName = rn.getRow().getValue(Schema.MATRIX_NAME_FIELD); if (potentialTargetName.equals(relation.getToLabel()) || potentialTargetName.equals(relation.getFromLabel())) { //System.out.println("Relation : " + relation.getFromLabel() + "," + relation.getToLabel()); //try to find a place to draw it. final Collection<LWComponent> searchSet = VUE.getActiveViewer().getMap().getAllDescendents(LWComponent.ChildKind.EDITABLE); final Criteria criteria = dataNodeToSearchCriteria(rn); SmartSearch currentSearch = new SmartSearch(); currentSearch.addCriteria(criteria); List<LWComponent> hits = currentSearch.search(searchSet); for (LWComponent hit: hits) { LWLink link = null; LWLink link2 = null; if (newNode.getLabel().equals(hit.getLabel())) continue; if (relation.getFromLabel().equals(trueName) && !newNode.hasDirectedLinkTo(hit)) link = new LWLink(newNode,hit); if (relation.getToLabel().equals(trueName) && !hit.hasDirectedLinkTo(newNode)) link2 = new LWLink(hit,newNode); LWSelection sel = VUE.getSelection(); VUE.getSelection().clear(); if (link !=null) { link.setLabel(relation.getRelationLabel()); link.setAsDataLink(relation.getRelationLabel()); VUE.getActiveViewer().getMap().add(link); VUE.getSelection().add(link); //System.out.println("Add Link 1 : " + link.toString() + " :: " + call + " :: " + newNode.toString() + " ::: " + hit.toString()); } if (link2 !=null) { link2.setLabel(relation.getRelationLabel()); link2.setAsDataLink(relation.getRelationLabel()); VUE.getActiveViewer().getMap().add(link2); VUE.getSelection().add(link2); //System.out.println("Add Link 2 : " + link2.toString() + " :: " + call + " :: " + newNode.toString() + " ::: " + hit.toString()); } if (hit.hasMultipleLinksTo(newNode) && VUE.getSelection().size() >0) tufts.vue.Actions.LinkMakeQuadCurved.act(); } } } } // for each data node } } } } private void enableUpdateButton() { mUpdateButton.setEnabled((mNewRowsCheckBox.isEnabled() && mNewRowsCheckBox.isSelected()) || (mChangedRowsCheckBox.isEnabled() && mChangedRowsCheckBox.isSelected())); } /* * This will find all nodes on the map where fresher/newer data is in the given * data-set and update those nodes with the new data. When done, it will leave the * selection set to all nodes that have been updated with new data. */ private void applyDataUpdatesToMap(final LWMap map) { final Map<String,DataRow> freshData = new HashMap(); final Field keyField = mSchema.getKeyField(); final String keyFieldName = keyField.getName(); for (DataNode n : mAllRowsNode.getChildren()) { final DataRow row = n.getRow(); if (row.isContextChanged()) { //Log.debug("Context changed: " + Util.tag(row)); String keyValue = row.getValue(keyField); freshData.put(keyValue, row); } } if (DEBUG.Enabled) Log.debug("Found " + freshData.size() + " data rows with newer data for map"); final Collection<LWComponent> nodes = map.getAllDescendents(); final Collection<LWComponent> patched = new ArrayList(); for (LWComponent c : nodes) { if (c.isDataRow(mSchema)) { DataRow newRow = freshData.get(c.getDataValue(keyFieldName)); if (newRow != null) { //Log.debug("patching " + c); c.setDataMap(newRow.getData()); patched.add(c); } } } if (DEBUG.Enabled) Log.debug("Updated " + patched.size() + " nodes with fresh data"); // Note: kicking the annotation may no longer be required, as we also listen for // changes to the map to kick annotations, but that code isn't as smart as it // could be (though actually, it generally does overkill -- annotation happens // more often than it need be). kickAnnotate(); VUE.getSelection().setTo(patched); map.getUndoManager().mark(String.format("Update %d Data Nodes", patched.size())); } private static JComponent buildControllerUI(final DataTree tree) { final Schema schema = tree.mSchema; final JPanel wrap = new JPanel(new BorderLayout()) { @Override public void firePropertyChange(String property, boolean oldVal, boolean newVal) { if (tufts.vue.gui.GUI.FINALIZE.equals(property)) { if (DEBUG.Enabled) Log.debug("firePropertyChange: " + property); tree.destroy(); } else { super.firePropertyChange(property, oldVal, newVal); } } }; tree.mUpdateButton.setOpaque(false); tree.mSendToMapButton.setOpaque(false); tree.mUpdateButton.setFont(tufts.vue.gui.GUI.LabelFace); tree.mSendToMapButton.setFont(tufts.vue.gui.GUI.LabelFace); tree.mNewRowsCheckBox.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { tree.enableUpdateButton(); } }); tree.mChangedRowsCheckBox.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { tree.enableUpdateButton(); } }); tree.mUpdateButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { tree.updateMap(); } }); tree.mSendToMapButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { tree.sendSelectedToMap(); } }); tree.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); tree.setBorder(GUI.makeSpace(3,0,0,0)); JLabel dataSourceLabel = null; String imagePath = schema.getSingletonValue("rss.channel.image.url"); if (imagePath == null) imagePath = schema.getSingletonValue("rdf:RDF.image.url"); if (imagePath != null) { URL imageURL = Resource.makeURL(imagePath); if (imageURL != null) { dataSourceLabel = new JLabel(new ImageIcon(imageURL)); dataSourceLabel.setBorder(GUI.makeSpace(2,2,1,0)); } //addNew.setIcon(new ImageIcon(imageURL)); //addNew.setLabel(imageURL); } // JComboBox keyBox = new JComboBox(schema.getPossibleKeyFieldNames()); // keyBox.setOpaque(false); // keyBox.setSelectedItem(schema.getKeyField().getName()); // keyBox.addItemListener(new ItemListener() { // public void itemStateChanged(ItemEvent e) { // if (e.getStateChange() == ItemEvent.SELECTED) { // String newKey = (String) e.getItem(); // //Log.debug("KEY FIELD SELECTED: " + newKey); // schema.setKeyField(newKey); // tree.refreshRoot(); // } // } // }); // toolbar.add(keyBox, BorderLayout.WEST); // toolbar.add(addNew, BorderLayout.EAST); final int GUTTER = 4; JPanel toolbar = new JPanel(), remainderPanel = new JPanel(), newRowsPanel = new JPanel(), changedRowsPanel = new JPanel(); Insets noInset = new Insets(0, 0, 0, 0), //top, left, bottom, right panelInset = new Insets(0, 0, 0, GUTTER), buttonInset = new Insets(0, 0, GUTTER, GUTTER); GridBagConstraints gbcCheckBox = new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, noInset, 0, 0), gbcTextArea = new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, noInset, 0, 0), gbcPanel = new GridBagConstraints(0, 0, 3, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL, panelInset, 0, 0), gbcRemainder = new GridBagConstraints(0, 2, 1, 1, 1.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.REMAINDER, buttonInset, 0, 0), gbcButton = new GridBagConstraints(1, 2, 1, 1, 0.0, 0.0, GridBagConstraints.LINE_START, GridBagConstraints.NONE, buttonInset, 0, 0); int checkBoxHeight = tree.mNewRowsCheckBox.getPreferredSize().height; toolbar.setLayout(new GridBagLayout()); newRowsPanel.setLayout(new GridBagLayout()); changedRowsPanel.setLayout(new GridBagLayout()); tree.mNewRowsLabel.setFont(tufts.vue.gui.GUI.LabelFace); tree.mNewRowsLabel.setOpaque(false); newRowsPanel.add(tree.mNewRowsCheckBox, gbcCheckBox); newRowsPanel.add(tree.mNewRowsLabel, gbcTextArea); toolbar.add(newRowsPanel, gbcPanel); gbcPanel.gridy = 1; tree.mChangedRowsLabel.setFont(tufts.vue.gui.GUI.LabelFace); tree.mChangedRowsLabel.setOpaque(false); changedRowsPanel.add(tree.mChangedRowsCheckBox, gbcCheckBox); changedRowsPanel.add(tree.mChangedRowsLabel, gbcTextArea); toolbar.add(changedRowsPanel, gbcPanel); toolbar.add(remainderPanel, gbcRemainder); toolbar.add(tree.mUpdateButton, gbcButton); if (DEBUG_LOCAL) { toolbar.setOpaque(true); toolbar.setBackground(Color.CYAN); newRowsPanel.setOpaque(true); newRowsPanel.setBackground(Color.CYAN); changedRowsPanel.setOpaque(true); changedRowsPanel.setBackground(Color.CYAN); remainderPanel.setOpaque(true); remainderPanel.setBackground(Color.YELLOW); tree.mNewRowsCheckBox.setOpaque(true); tree.mNewRowsCheckBox.setBackground(Color.YELLOW); tree.mNewRowsLabel.setOpaque(true); tree.mNewRowsLabel.setBackground(Color.YELLOW); tree.mChangedRowsCheckBox.setOpaque(true); tree.mChangedRowsCheckBox.setBackground(Color.YELLOW); tree.mChangedRowsLabel.setOpaque(true); tree.mChangedRowsLabel.setBackground(Color.YELLOW); tree.mUpdateButton.setOpaque(true); tree.mUpdateButton.setBackground(Color.YELLOW); } if (dataSourceLabel != null) wrap.add(dataSourceLabel, BorderLayout.SOUTH); // if (dataSourceLabel == null) { // // dataSourceLabel = new JLabel(schema.getName()); // // dataSourceLabel.setFont(tufts.vue.VueConstants.SmallFont); // // dataSourceLabel.setBorder(GUI.makeSpace(0,2,0,0)); // // toolbar.add(dataSourceLabel, BorderLayout.WEST); // // toolbar.add(addNew, BorderLayout.EAST); // toolbar.add(addNew, BorderLayout.CENTER); // } else { // toolbar.add(dataSourceLabel, BorderLayout.WEST); // toolbar.add(addNew, BorderLayout.EAST); // } toolbar.setBorder(new MatteBorder(0,0,1,0, Color.gray)); wrap.add(toolbar, BorderLayout.NORTH); // todo: if save entire schema with map, include date of creation (last refresh before save) wrap.add(tree, BorderLayout.CENTER); return wrap; } @Override protected void setExpandedState(final TreePath path, final boolean expanded) { final DataNode treeNode = path == null ? null : (DataNode) path.getLastPathComponent(); if (DEBUG.Enabled) Log.debug("setExpandedState " + path + " = " + expanded + "; " + Util.tags(treeNode)); // we can interrupt tree expansion here on our double-clicks for searches // (which may obviate part of the workaround we needed with the ClearSearchMouseListener, // tho not for the JScrollPane problem if that is really happening) if (treeNode != null && treeNode.isField()) { // we record the expanded state in the visibility bit of the style node // so we can restore it later from the skeletal Schema's saved with maps treeNode.getField().getStyleNode().setVisible(expanded); } GUI.invokeAfterAWT(new Runnable() { public void run() { if (!inDoubleClick) { if (DEBUG.FOCUS) Log.debug("setExpandedState " + path + " = " + expanded + " RELAYING"); DataTree.super.setExpandedState(path, expanded); } else if (DEBUG.FOCUS) Log.debug("setExpandedState " + path + " = " + expanded + " SKIPPING"); inDoubleClick = false; }}); } private boolean inDoubleClick; private class ClickHandler extends tufts.vue.MouseAdapter { private TreePath mClickPath; @Override public void mousePressed(java.awt.event.MouseEvent e) { mClickPath = getPathForLocation(e.getX(), e.getY()); if (DEBUG.Enabled) { //Log.debug("MOUSE PRESSED ON " + Util.tags(mClickPath)); if (mClickPath != null) Log.debug("MOUSE PRESSED ON: " + Util.tags(mClickPath.getLastPathComponent())); else Log.debug("MOUSE PRESSED ON: nothing"); } // it's possible that the node under the mouse changes from the time of the // first press, to the time mouseClicked is called (e.g., due to tree // expansion and/or possible scrolling of the entire tree component), so we // capture it here. Could make this a class a generic subclassable helper // class for JTree's. inDoubleClick = GUI.isDoubleClick(e); if (DEBUG.Enabled) Log.debug("IN DOUBLE CLICK = " + inDoubleClick); } @Override public void mouseClicked(java.awt.event.MouseEvent e) { if (mClickPath == null) return; final DataNode treeNode = (DataNode) mClickPath.getLastPathComponent(); // TODO: below selectMatchingNodes is often a repeat run after the one trigger in TreeSelectionListener.valueChanged // if (GUI.isSingleClick(e)) { // if (mSelectedSearchNode == treeNode) { // // re-run search: we're clicking on already selected, and can't select it again // selectMatchingNodes(treeNode, false); // } // return; // } if (!GUI.isDoubleClick(e)) return; if (DEBUG.Enabled) Log.debug("ACTIONABLE DOUBLE CLICK ON " + Util.tags(treeNode)); // if (treeNode.hasStyle()) { // final tufts.vue.LWSelection selection = VUE.getSelection(); // selection.setSource(DataTree.this); // // prevents from ever drawing through on map: // selection.setSelectionSourceFocal(null); // selection.setTo(treeNode.getStyle()); // } else if (treeNode.isRow() || (treeNode.getField() != null && treeNode.getField().isPossibleKeyField())) { selectMatchingNodes(treeNode, false); } } } private void selectMatchingNodes(final DataNode treeNode, final boolean extendSearch) { if (mActiveMap == null) return; // we search only amongst EDITBALE nodes, so that we ignore hidden/locked layers & nodes, etc final Collection<LWComponent> searchSet = mActiveMap.getAllDescendents(LWComponent.ChildKind.EDITABLE); if (DEBUG.Enabled) Log.debug("SEARCH:\n\nSEARCHING ALL EDITABLE DESCENDENTS of " + mActiveMap + "; count=" + searchSet.size() + "; treeNode=" + Util.tags(treeNode)); //,new Throwable("HERE")); findAndSelectMatchingNodes(searchSet, treeNode, extendSearch); } private SmartSearch mCurrentSearch; private Criteria mLastCriteria; /** * Note: This method also has the side effect of picking an active style record for * the selection if the DataNode represents a Field (the style for all enumerated * values on the map from that Field), as well as setting a description in the * selection of the search that produced it. */ private void findAndSelectMatchingNodes(final Collection<LWComponent> searchSet, final DataNode treeNode, final boolean extendSearch) { Field field = treeNode.getField(); LWComponent styleRecord = null; if (field == null) { if (treeNode == mAllRowsNode || treeNode instanceof RowNode) { field = mSchema.getKeyField(); if (treeNode == mAllRowsNode) styleRecord = mSchema.getRowNodeStyle(); } else { // todo: must be root node: select a row-node items return; } } if (extendSearch) { // only use the style record for single criteria searches; otherwise // makes no sense -- can't hang a style off a search, only single Fields styleRecord = null; } else { if (treeNode.isField() && styleRecord == null) styleRecord = field.getStyleNode(); } final String fieldName = field.getName(); final Criteria criteria = dataNodeToSearchCriteria(treeNode); final List<LWComponent> hits; String desc; if (extendSearch) { if (mCurrentSearch == null) { mCurrentSearch = new SmartSearch(); mCurrentSearch.addCriteria(mLastCriteria); mLastCriteria = null; } mCurrentSearch.addCriteria(criteria); Log.debug("Running search " + mCurrentSearch); hits = mCurrentSearch.search(searchSet); desc = mCurrentSearch.toString(); } else { // if we've got a single Criteria, no need to mess with a SmartSearch mCurrentSearch = null; hits = new ArrayList(); Log.debug("SEARCHING WITH CRITERIA " + criteria); for (LWComponent c : searchSet) if (criteria.matches(c)) hits.add(c); mLastCriteria = criteria; desc = criteria.description(); } desc = "matching<br>" + desc; if (DEBUG.Enabled) { if (hits.size() == 1) Log.debug("hits=" + hits.get(0) + " [single hit]"); else Log.debug("hits=" + hits.size()); Log.debug("styleRecord: " + styleRecord); //if (styleRecord != null) desc += "<p>style: " + styleRecord; } final tufts.vue.LWSelection selection = VUE.getSelection(); // make sure selection bounds are drawn in MapViewer: selection.setSelectionSourceFocal(VUE.getActiveFocal()); // now set the selection, along with a description selection.setWithStyle(hits, desc, styleRecord); } public Criteria dataNodeToSearchCriteria(final DataNode treeNode) { final Field field = treeNode.getField(); final String fieldName = field == null ? null : field.getName(); Criteria criteria = null; if (treeNode == mAllRowsNode) { // search for ANY row-node in the schema if (DEBUG.Enabled) Log.debug("searching for all data records in schema " + mSchema); criteria = new SchemaMatch(mSchema); } else if (treeNode.isRow()) { // search for a particular row-node in the schema based on the key field -- this will // normally only find a single node on the map, unless there are duplicate nodes on // the map referencing the same row final String keyField = ((RowNode)treeNode).getSchema().getKeyFieldName(); final String keyValue = treeNode.getRow().getValue(keyField); criteria = new ValueMatch(keyField, keyValue); } else if (treeNode.isField()) { // search for all nodes anchoring a particular value for the given Field if (DEBUG.Enabled) Log.debug("searching for any enumerated value from a field named " + fieldName); criteria = new FieldMatch(field); //criteria = new KeyMatch(fieldName); } else if (treeNode.isValue()) { // search for a particular key=value final String fieldValue = treeNode.getValue(); if (DEBUG.Enabled) Log.debug(String.format("searching for %s=[%s]", fieldName, fieldValue)); criteria = new ValueMatch(fieldName, fieldValue); } return criteria; } // todo: better to move all this Search stuff to a DataSearch.java or some such. public abstract static class Criteria { boolean matches(LWComponent c) { throw new UnsupportedOperationException("unimplemented matches in " + this); } abstract String description(); String getKey() { return null; } public List<LWComponent> search(final Collection<LWComponent> searchSet) { final List<LWComponent> hits = new ArrayList(); for (LWComponent c : searchSet) if (matches(c)) hits.add(c); return hits; } @Override public String toString() { return String.format("%s[%s]", getClass().getSimpleName(), description()); } } public static final class SchemaMatch extends Criteria { final Schema schema; public SchemaMatch(Schema s) { schema = s; } @Override public boolean matches(LWComponent c) { return c.isDataRow(schema); } @Override public String description() { return String.format("in data set: <i>%s</i>", schema.getName()); } } public static final class FieldMatch extends Criteria { final Field field; public FieldMatch(Field f) { field = f; } @Override public boolean matches(LWComponent c) { return c.isDataValueNode(field); } @Override public String description() { return String.format("enumerated values of: <b>%s</b>", field); } //@Override String getKey() { return key; } @Override public String toString() { return String.format("enumerated values of <i>%s</i>", field); } } public static final class KeyMatch extends Criteria { final String key; public KeyMatch(String fieldName) { key = fieldName; } @Override public boolean matches(LWComponent c) { return c.isDataValueNode(key); } @Override public String description() { return String.format("any enumerated values of: <b>%s</b>", key); } //@Override String getKey() { return key; } @Override public String toString() { return String.format("any enumerated values of <i>%s</i>", key); } } public static final class ValueMatch extends Criteria { final String key; final String value; public ValueMatch(String k, String v) { key = k; value = v; } @Override public boolean matches(LWComponent c) { return c.hasDataValue(key, value); } @Override public String description() { return String.format("<b>%s: <i>%s</i>", key, valueText(value)); } @Override String getKey() { return key; } @Override public String toString() { return String.format("%s=%s", key, value); } } /** * A multiple criteria search that is automagically smart about how to create boolean * AND and OR groups to create reasonable searches. */ public static final class SmartSearch /*extends Crtieria*/ { /** a boolean AND group of OR lists for each key used in any key=value searches present */ final Multimap<String,Criteria> criteriaByKey = Multimaps.newHashMultimap(); /** a special OR group that takes priority: anything matching criteria in this group * is "hit" no matter what */ final Collection<Criteria> globalBooleanOr = new ArrayList(); public void addCriteria(Criteria criteria) { if (DEBUG.Enabled) Log.debug("SmartSearch adding " + criteria); if (criteria == null) return; final String key = criteria.getKey(); if (key != null) { criteriaByKey.put(key, criteria); } else { // note: this impl means any search containing a SchemaMatch will match ALL // rows in the schema, and it will only be meaninful to add Fields to // the search (not values selecting particular rows, as they'll allready // be all selected). globalBooleanOr.add(criteria); } } public List<LWComponent> search(final Collection<LWComponent> searchSet) { final List<LWComponent> hits = new ArrayList(); if (DEBUG.Enabled) { for (Criteria c : globalBooleanOr) Log.debug("GlobalOR: " + c); final Collection<Map.Entry<String,Collection<Criteria>>> allKeyEntries = criteriaByKey.asMap().entrySet(); for (Map.Entry e : allKeyEntries) { Log.debug(String.format("Key: %-12s criteria=%s", e.getKey(), e.getValue())); } } final Collection<Collection<Criteria>> keyBasedCriteria; if (criteriaByKey.size() > 0) keyBasedCriteria = criteriaByKey.asMap().values(); else keyBasedCriteria = null; if (globalBooleanOr.size() > 0) { // search method 1: includes the global OR tests for (LWComponent c : searchSet) { if (anyCriteriaMatches(c, globalBooleanOr)) hits.add(c); else if (keyBasedCriteria != null && allGroupsMatch(c, keyBasedCriteria)) hits.add(c); } } else if (keyBasedCriteria != null) { // search method 2: just the AND'd group of OR tests for (LWComponent c : searchSet) { if (allGroupsMatch(c, keyBasedCriteria)) { hits.add(c); } } } return hits; } /** perform a boolean AND of a bunch of OR groups: at least one criteria must match from each collection of criteria * Note: if the group is empty (no collections of Criteria), this will always return TRUE */ private boolean allGroupsMatch(final LWComponent c, final Collection<Collection<Criteria>> booleanAndGroup) { for (Collection<Criteria> booleanOrGroup : booleanAndGroup) { boolean atLeastOneMatched = false; //Log.debug(c.getUniqueComponentTypeLabel() + "; against " + booleanOrGroup); // todo: could make this even faster by exploiting the underlying // Multimap impl in the LWComponent MetaMap to have it pull all values // for the key, which should be a Set, and just check for set membership // for each of the values we're looking for under that key. Could pull // the key to use from the first criteria in the group, or just pass in // Map.Entry's of the keys and their collections of criteria. We'd // abandon the matches(c) API and manually pull the value from key=value // criteria to check the set membership. for (Criteria criteria : booleanOrGroup) { if (criteria.matches(c)) { //Log.debug(c.getUniqueComponentTypeLabel() + "; ok"); atLeastOneMatched = true; break; } } if (!atLeastOneMatched) { // nothing matched in this or group: entire search immediately fails //Log.debug(c.getUniqueComponentTypeLabel() + " failed; no values matched in key"); return false; // immediate boolean short-circuit } } return true; } /** perform a boolean OR for a group of criteria */ private boolean anyCriteriaMatches(final LWComponent c, final Collection<Criteria> booleanOrGroup) { for (Criteria criteria : booleanOrGroup) { if (criteria.matches(c)) { //Log.debug(criteria + " hit " + c); return true; // immediate boolean short-circuit } } return false; } @Override public String toString() { final Collection<Collection<Criteria>> groupedByKey = criteriaByKey.asMap().values(); StringBuilder b = new StringBuilder("search terms:<br>"); for (Criteria c : globalBooleanOr) { b.append(c.description()); b.append("<br>"); } for (Collection<Criteria> eachKey : groupedByKey) { for (Criteria c : eachKey) { b.append(c.description()); b.append("<br>"); } } return b.toString(); } } private void debug(String s) { Log.debug(String.format("%s%-20s%s %s", Util.TERM_PURPLE, "[" + mSchema.getName() + "]", Util.TERM_CLEAR, s)); } // For annotations, we create a single handler for listening to user changes to the // current map (UseActionCompleted), that keeps background threads running for the // annotations for each loaded DataTree. It is designed so that the annotate thread // for the currently visible DataTree(s) runs at a higher priority, and the loaded // but not currently displated DataTree's run at a lower priority. If a second // update comes through before the current pass is completed, the prior update can // be aborted. private void kickAnnotate() { GUI.invokeOnEDT(new Runnable() { public void run() { mUpdateButton.setEnabled(false); }}); if (DEBUG.THREAD) debug("WAKING-> ANNOTATION THREAD " + mAnnotateThread); // note: if we're already on a thread that's NOT the AWT EDT, we could // assume we're in a builder thread and instead of waking the annotation // thread just run in the builder thread, tho better to keep all that // done there as sometimes that can take a while to run, and this will // allow initial tree creations to run faster. synchronized (mAnnotateThread) { mAnnotateThread.notify(); } if (DEBUG.THREAD||DEBUG.ANNOTATE) debug("NOTIFIED ANNOTATION THREAD " + mAnnotateThread); } // TODO: add another kind of annotation pass that runs after a search, and greys out enumerated // values that have dropped out of the search set. (?) /** @return true if interrupted */ private boolean annotateForMap() { return annotateForMap(mActiveMap); } /** @return true if interrupted */ private boolean annotateForMap(final LWMap map) { if (DEBUG.THREAD || DEBUG.SCHEMA || DEBUG.ANNOTATE) Log.debug("ANNOTATING against " + map + "; " + Util.tags(ActiveMapDataNodes)); mSchema.annotateFor(ActiveMapDataNodes); // note: the map isn't actually needed by any of the below annotation calls if (map != null) { final String annot = map.getLabel(); for (DataNode n : mRootNode.getChildren()) { if (Thread.interrupted()) return true; n.annotate(map); if (!n.isLeaf()) for (DataNode cn : n.getChildren()) { if (Thread.interrupted()) return true; cn.annotate(map); } } } int _newRowCount = 0; int _changedRowCount = 0; // if (mAllRowsNode.getChildren() !=null) for (DataNode n : mAllRowsNode.getChildren()) { if (!n.isMapPresent()) _newRowCount++; if (n.isContextChanged()) _changedRowCount++; } final int newRowCount = _newRowCount; final int changedRowCount = _changedRowCount; if (DEBUG.THREAD || DEBUG.SCHEMA) Log.debug("annotateForMap: newRows " + newRowCount + "; changedRows " + changedRowCount); if (Thread.interrupted()) return true; GUI.invokeOnEDT(new Runnable() { public void run() { String newRowsMessage = "", changedRowsMessage = ""; newRowsMessage = (newRowCount == 0 ? VueResources.getString("dockWindow.contentPanel.sync.noNewRecords") : (newRowCount == 1 ? VueResources.getString("dockWindow.contentPanel.sync.oneNewRecord") : String.format(VueResources.getString("dockWindow.contentPanel.sync.manyNewRecords"), newRowCount))); changedRowsMessage = (changedRowCount == 0 ? VueResources.getString("dockWindow.contentPanel.sync.noChangedRecords") : (changedRowCount == 1 ? VueResources.getString("dockWindow.contentPanel.sync.oneChangedRecord") : String.format(VueResources.getString("dockWindow.contentPanel.sync.manyChangedRecords"), changedRowCount))); mNewRowsLabel.setText(newRowsMessage); mNewRowsLabel.setToolTipText(newRowsMessage); mNewRowsLabel.setForeground(newRowCount != 0 ? Color.BLACK : MEDIUM_DARK_GRAY); mNewRowsCheckBox.setEnabled(newRowCount != 0); mChangedRowsLabel.setText(changedRowsMessage); mChangedRowsLabel.setToolTipText(changedRowsMessage); mChangedRowsLabel.setForeground(changedRowCount != 0 ? Color.BLACK : MEDIUM_DARK_GRAY); mChangedRowsCheckBox.setEnabled(changedRowCount != 0); enableUpdateButton(); // TODO: don't bother with refresh if annotations didn't change at all refreshAll(); }}); return false; } // private void refreshRoot() { // if (DEBUG.Enabled) Log.debug("REFRESHING " + Util.tags(mRootNode)); // refreshAllChildren(mRootNode); // } private void refreshTopLevel() { refreshAllChildren(mRootNode); } private void refreshAllStyleNodes() { refreshTopLevel(); } private void refreshAll() { //mTreeModel.reload(mRootNode); // using nodesChanged instead of reload preserves the expanded state of nodes in the tree if (DEBUG.THREAD) Log.debug("REFRESHING " + Util.tags(mRootNode.getChildren())); refreshTopLevel(); for (TreeNode n : mRootNode.getChildren()) if (!n.isLeaf()) refreshAllChildren(n); if (DEBUG.THREAD) Log.debug(" REFRESHED " + Util.tags(mRootNode.getChildren())); // This gets close, but doesn't always handle updating NON expanded nodes, plus // it often leaves labels truncated with "..." // invalidate(); // super.treeDidChange(); } private void restoreAnyExpandedState() { int i = 0; for (TreeNode n : mRootNode.getChildren()) { if (n instanceof FieldNode) { //Log.debug("found field node " + Util.tags(n)); Field f = ((FieldNode)n).getField(); if (f != null) { //Log.debug("found field " + f); LWComponent style = ((FieldNode)n).getField().getStyleNode(); if (style != null && style.isVisible()) { final TreePath path = getPathForRow(i); if (DEBUG.Enabled) Log.debug("EXPAND " + Util.tags(n) + " " + Util.tags(path)); GUI.invokeOnEDT(new Runnable() { public void run() { DataTree.super.setExpandedState(path, true); }}); } } } i++; } } private void refreshAllChildren(TreeNode node) { final int[] childIndexes = new int[node.getChildCount()]; for (int i = 0; i < childIndexes.length; i++) childIndexes[i] = i; // why there's isn't an API to do this automatically, i don't know... // using nodesChanged instead of mTreeModel.reload preserves the expanded state of nodes in the tree if (DEBUG.META) Log.debug("refreshing " + childIndexes.length + " children of " + node); mTreeModel.nodesChanged(node, childIndexes); } private void refreshRootNode() { mTreeModel.nodesChanged(mRootNode, new int[] { 0 }); } @Override public String toString() { return String.format("DataTree[%s]", mSchema.toString()); } private static volatile int AnnotationThreadCount = 0; private void destroy() { if (DEBUG.Enabled) Log.debug("destroying w/" + mAnnotateThread); mAnnotateThread.interrupt(); mAnnotateThread = null; // it's crucial to flush the old schema so that if it isn't reloaded with new // data (a new Schema instance is created when/if this schema is reloaded), the // old schema will be empty and will no longer match to any nodes on any map. // todo: the XmlDataSource impl current decides which happens (e.g., re-loaded // for .csv, new instances for XML) -- if we keep the new-instance // functionality, eventually we should actually remove the defunct schema's from // the Schema global instance lists, instead of just leaving them in there but // empty. mSchema.flushData(); ActiveTrees.remove(this); // if (mActiveMap != null) // mActiveMap.removeLWCListener(this); // VUE.removeActiveListener(LWMap.class, this); } private DataTree(final Schema schema) { mSchema = schema; setCellRenderer(new DataRenderer()); //setSelectionModel(null); setModel(mTreeModel = new DefaultTreeModel(buildTree(schema), false)); final int ac = AnnotationThreadCount++; mAnnotateThread = new Thread(String.format("Annotate%d: %-20.20s", ac, schema.getName())) { { setPriority(MAX_PRIORITY); } public synchronized void run() { while (true) { try { // must be careful: if we get a notify before the 1st time we go to // sleep, we'll never wake up! So we start this thread at high // priority, and kick it off ("start()") immediately, because as soon // as the DataTree is done constructing, we're going to get notified // the first time -- still theoretically risky but it appears to // be working reliably. if (DEBUG.THREAD || DEBUG.ANNOTATE) Log.debug("annotation thread sleeping, pri=" + getPriority()); wait(); } catch (InterruptedException e) { Log.error("interrupted; exiting; " + schema); return; } if (DataTree.this == ForegroundTree && !VUE.isApplet()) // referring to NORM_PRIORITY can fail in Applets setPriority(NORM_PRIORITY - 1); else setPriority(MIN_PRIORITY); if (DEBUG.THREAD || DEBUG.ANNOTATE) Log.debug("annotation thread woke, pri=" + getPriority() + "; running..."); final boolean interrupted = annotateForMap(); if (DEBUG.Enabled) { if (interrupted) Log.debug("annotation aborted"); else if (DEBUG.THREAD) Log.debug("annotation completed"); } } } }; if (DEBUG.THREAD) Log.debug("STARTING " + mAnnotateThread + "; (tree constructing)"); mAnnotateThread.start(); setRowHeight(0); setRootVisible(false); setShowsRootHandles(true); java.awt.dnd.DragSource.getDefaultDragSource() .createDefaultDragGestureRecognizer (this, java.awt.dnd.DnDConstants.ACTION_COPY | java.awt.dnd.DnDConstants.ACTION_MOVE | java.awt.dnd.DnDConstants.ACTION_LINK, this); addMouseListener(new ClickHandler()); addTreeSelectionListener(new javax.swing.event.TreeSelectionListener() { public void valueChanged(javax.swing.event.TreeSelectionEvent e) { //final TreePath[] paths = e.getPaths(); final TreePath[] paths = getSelectionModel().getSelectionPaths(); if (DEBUG.Enabled) Log.debug("valueChanged: isAddedPath=" + e.isAddedPath() + "; PATHS:"); if (DEBUG.Enabled) Util.dump(paths); //if (DEBUG.Enabled) Log.debug("OLD LeadPath: " + e.getOldLeadSelectionPath()); //if (DEBUG.Enabled) Log.debug("NEW LeadPath: " + e.getNewLeadSelectionPath()); // TODO: change from checking getPaths to model.getSelectionPaths & ignoring isAddedPath // if (!e.isAddedPath() || e.getPath().getLastPathComponent() == null) // return; final DataNode treeNode = (DataNode) e.getPath().getLastPathComponent(); if (treeNode instanceof RowNode) { VUE.setActive(tufts.vue.MetaMap.class, DataTree.this, treeNode.getRow().getData()); } else if (treeNode instanceof ValueNode && treeNode.getField().isPossibleKeyField()) { DataRow row = mSchema.findRow(treeNode.getField(), treeNode.getValue()); if (row!=null) { VUE.setActive(tufts.vue.MetaMap.class, DataTree.this, row.getData()); } else { Log.warn("Row is NULL while trying to set active in tree selection value changed"); } } // else if (treeNode.hasStyle()) { // final tufts.vue.LWSelection selection = VUE.getSelection(); // selection.setSource(DataTree.this); // // prevents from ever drawing through on map: // selection.setSelectionSourceFocal(null); // selection.setTo(treeNode.getStyle()); // } else if (paths != null) { boolean multipleSearchTerms = false; DataNode node = null; for (TreePath path : paths) { node = (DataNode) path.getLastPathComponent(); selectMatchingNodes(node, multipleSearchTerms); multipleSearchTerms = true; } if (paths.length == 1) mSelectedSearchNode = node; else mSelectedSearchNode = null; } else { if (DEBUG.Enabled) Log.warn("null search path from selection model"); } // else if (treeNode instanceof ValueNode) { // if (treeNode.getField().isPossibleKeyField()) { // DataRow row = mSchema.findRow(treeNode.getField(), treeNode.getValue()); // VUE.setActive(tufts.vue.MetaMap.class, // DataTree.this, // row.getData()); // } else { // selectMapForNode(treeNode, false); // } // } //VUE.setActive(LWComponent.class, this, node.styleNode); } }); } @Override public void addNotify() { ForegroundTree = this; super.addNotify(); } @Override public void removeNotify() { if (ForegroundTree == this) ForegroundTree = null; super.removeNotify(); } private static String HTML(String s) { //if (true) return s; final StringBuilder b = new StringBuilder(s.length() + 6); //b.append("<html>"); // we add space before and after to widen the painted background selection around the text a bit b.append("<html> "); b.append(s); b.append(" "); return b.toString(); } private static int sortPriority(Field f) { if (f.isSingleton()) return -4; else if (f.isSingleValue()) return -3; else if (f.isUntrackedValue()) return -2; else if (f.getName().contains(":") && !f.getName().startsWith("dc:")) return -1; else return 0; } /** build the model and return the root node */ private TreeNode buildTree(final Schema schema) { mAllRowsNode = new AllRowsNode(schema, this); final DataNode root = new DataNode("Data Set: " + schema.getName()); //new VauleNode("Data Set: " + schema.getName()); // new DataNode(null, null, // String.format("%s [%d %s]", // schema.getName(), // schema.getRowCount(), // "items"//isCSV ? "rows" : "items")); final Field keyField = schema.getKeyField(); Field labelField = schema.getField("title"); if (labelField == null) labelField = keyField; for (DataRow row : schema.getRows()) { mAllRowsNode.add(new RowNode(row, labelField)); //String label = row.getValue(labelField); //rowNodeTemplate.add(new ValueNode(keyField, row.getValue(keyField), label)); } root.add(mAllRowsNode); mRootNode = root; final Field sortedFields[] = new Field[schema.getFieldCount()]; schema.getFields().toArray(sortedFields); Arrays.sort(sortedFields, new Comparator<Field>() { public int compare(Field f1, Field f2) { return sortPriority(f2) - sortPriority(f1); } }); final LWComponent.Listener styleRepainter = new LWComponent.Listener() { // todo: schema style nodes are currently parentless, which means // their property change events don't go up through the map to // the undo-manager, making changes to them not undoable -- either // manually relay style property change events up through the appropriate // map, or have a way for a map to have hidden list of style children. public void LWCChanged(tufts.vue.LWCEvent e) { if (DEBUG.EVENTS) Log.debug("REPAINTER UPDATE ON " + e); if (e.getKey() == LWKey.FillColor) { DataTree.this.refreshAllStyleNodes(); } } }; //The current data model doesn't make sense for matrix data and so it's not going to make a ton of sense if (!DataTree.this.mSchema.isMatrixDataSet) for (Field field : sortedFields) { // if (field.isSingleton()) // continue; DataNode fieldNode = new FieldNode(field, styleRepainter, null); root.add(fieldNode); // if (field.uniqueValueCount() == schema.getRowCount()) { // //Log.debug("SKIPPING " + f); // continue; // } final Set values = field.getValues(); // could add all style nodes to the schema node to be put in an internal layer for // persistance: either that or store them with the datasources, which // probably makes more sense. if (values.size() > 1) { try { buildValueChildren(field, fieldNode); } catch (Throwable t) { Log.error("building child values for: " + Util.tags(field) + "; " + Util.tags(fieldNode), t); } } } return root; } private static final boolean SORT_BY_COUNT = false; private static final boolean SORT_BY_VALUE = !SORT_BY_COUNT; private static void buildValueChildren(Field field, DataNode fieldNode) { final Multiset<String> valueCounts = field.getValueSet(); final Set<Multiset.Entry<String>> entrySet = valueCounts.entrySet(); final Iterable<Multiset.Entry<String>> valueEntries; if (field.isQuantile() || (SORT_BY_COUNT && field.isPossibleKeyField())) { // cases we don't need to bother sorting: (1) quantiles, which are // pre-sorted (2) possible key fields: if sorting by counts (frequency) // don't need to bother sorting if field is a possible key field, as all // value counts == 1 valueEntries = entrySet; } else { final ArrayList<Multiset.Entry<String>> sortedValues = new ArrayList(entrySet); Collections.sort(sortedValues, new Comparator<Multiset.Entry<String>>() { public int compare(final Multiset.Entry<String> e1, final Multiset.Entry<String> e2) { // always put any empty value item last, otherwise sort on frequency if (e1.getElement() == Field.EMPTY_VALUE) return 1; else if (e2.getElement() == Field.EMPTY_VALUE) return -1; else if (SORT_BY_COUNT) return e2.getCount() - e1.getCount(); else // SORT_BY_VALUE return tufts.Strings.compareNaturalIgnoreCaseAscii(e1.getElement(), e2.getElement()); } }); valueEntries = sortedValues; } //----------------------------------------------------------------------------- // Add the enumerated values //----------------------------------------------------------------------------- for (Multiset.Entry<String> e : valueEntries) { final String value = e.getElement(); final String display; int nValues = e.getCount(); if (field.isQuantile() && value != Field.EMPTY_VALUE) { // non-empty Quantile values always have an extra count, // which was the "init" count to enforce quantile-order // on the values list. nValues--; } if (field.isPossibleKeyField()) { display = field.valueDisplay(value); } else { final String countTxt = String.format("%3d", nValues).replaceAll(" ", " "); final String color; if (nValues <= 0) display = String.format(HTML("<font color=#AAAAAA><code>%s</code> %s"), countTxt, valueText(value)); else display = String.format(HTML("<code><font color=#888888>%s</font></code> %s"), countTxt, valueText(value)); } final ValueNode valueNode = new ValueNode(field, value, display, nValues); fieldNode.add(valueNode); } for (String comment : field.getDataComments()) { fieldNode.add(new DataNode(HTML("<font color=#AAAAAA>" + comment))); } } public void dragGestureRecognized(DragGestureEvent e) { if (getSelectionPath() == null) { Log.debug("dragGestureRecognized: no selection path; " + e); return; } Log.debug("SELECTED: " + Util.tags(getSelectionPath().getLastPathComponent())); final DataNode treeNode = (DataNode) getSelectionPath().getLastPathComponent(); //if (resource != null) //GUI.startRecognizedDrag(e, resource, this); //tufts.vue.gui.GUI.startRecognizedDrag(e, Resource.instance(node.value), null); final LWComponent dragNode; final Field field = treeNode.getField(); boolean stylesAlreadyApplied = false; if (treeNode.isValue()) { if (treeNode.getCount() <= 0) dragNode = null; else dragNode = DataAction.makeValueNode(field, treeNode.getValue()); } else if (treeNode.isField()) { //if (field.isPossibleKeyField()) //return; dragNode = new LWNode(String.format(" %d unique \n '%s' \n values ", field.uniqueValueCount(), field.getName())); //dragNode.setClientData(java.awt.datatransfer.DataFlavor.stringFlavor, //" ${" + field.getName() + "}"); } else if (treeNode instanceof RowNode) { final DataRow row = ((RowNode)treeNode).getRow(); final List<LWComponent> nodes = DataAction.makeSingleRowNode(treeNode.getSchema(), row); if (DEBUG.Enabled) Log.debug("made row nodes: " + Util.tags(nodes)); if (nodes.isEmpty()) { Log.error("no row node made from row: " + row); dragNode = null; } else dragNode = nodes.get(0); stylesAlreadyApplied = true; } else { //assert treeNode instanceof TemplateNode; final Schema schema = treeNode.getSchema(); dragNode = new LWNode(String.format(" '%s' \n dataset \n (%d items) ", schema.getName(), schema.getRowCount() )); } if (dragNode == null) { Log.warn("Unable to create nodes from drag of " + treeNode); return; } dragNode.copyStyle(treeNode.getStyle(), ~LWKey.Label.bit); //dragNode.setFillColor(null); //dragNode.setStrokeWidth(0); if (!treeNode.isValue()) { dragNode.mFontSize.setTo(24); dragNode.mFontStyle.setTo(java.awt.Font.BOLD); // dragNode.setClientData(LWComponent.ListFactory.class, // new NodeProducer(treeNode)); } dragNode.setFlag(LWComponent.Flag.INTERNAL); dragNode.setClientData(tufts.vue.MapDropTarget.DropHandler.class, new DataDropHandler(treeNode, DataTree.this)); dragNode.setClientData(Field.class, treeNode.getField()); // for associations panel tufts.vue.gui.GUI.startRecognizedDrag(e, dragNode); } private static String valueText(Object value) { return DataAction.valueText(value); } private DataNode getSelectedNode() { return (DataNode) getLastSelectedPathComponent(); } private void sendToMap(final DataNode treeNode, final LWMap map) { if (map == null || treeNode == null) return; Log.debug("SENDING TO MAP: " + treeNode); } private static final Ordering<Multiset.Entry> ByDecreasingFrequency = new Ordering<Multiset.Entry>() { /*@Override*/ public int compare(Multiset.Entry a, Multiset.Entry b) { return b.getCount() - a.getCount(); } }; /* * This will find all rows of data in this given data-set that are NOT in the map, * create row-nodes for them, and send them to the map. This also kicks off an * annotation run to update the tree after the nodes have been added to the map. */ // For NEW DATA CLUSTERING: whenever new nodes are added to the map and there is no // layout specified / going to be applied, we want to place nodes near items their // related to. We do this based on the links. // // There are many versions of this. E.g.: // // 1 - re-clustering around the last clustered nodes as marked by the clustering // time-stamp for row-node additions // // 2 - placing new value-nodes most near the nodes their related to based on links /** adding more than this # of new row-nodes to the map permits a map-reorg */ private static final int NEW_ROW_NODE_MAP_REORG_THRESHOLD = 20; private void addMissingRowsToMap(final LWMap map) { // todo: we'll want to merge some of this code w/DropHandler code, as // this is somewhat of a special case of doing a drop final List<DataRow> newRows = new ArrayList(); for (DataNode n : mAllRowsNode.getChildren()) { if (!n.isMapPresent()) { //Log.debug("ADDING TO MAP: " + n); newRows.add(n.getRow()); } } final List<LWComponent> newRowNodes = DataAction.makeRowNodes(mSchema, newRows); Multiset<LWComponent> targetsUsed = null; List<LWLink> linksAdded = Collections.EMPTY_LIST; try { final Object[] result = DataAction.addDataLinksForNodes(map, newRowNodes, (Field) null); targetsUsed = (Multiset) result[0]; linksAdded = (List) result[1]; } catch (Throwable t) { Log.error("problem creating links on " + map + " for new nodes: " + Util.tags(newRowNodes), t); } if (DEBUG.Enabled && targetsUsed != null) { final Set entries = targetsUsed.entrySet(); Log.debug("TARGETS USED: " + targetsUsed.size() + " / " + entries.size()); Util.dump(entries); } if (newRowNodes.size() > 0) { // we cannot run setXYByClustering before adding to the map w/out refactoring projectNodes // (or for that matter, centroidCluster, which also uses projectNodes). E.g. -- we // can't use this as an initial fallback/failsafe. //tufts.vue.VueUtil.setXYByClustering(map, nodes); //----------------------------------------------------------------------------- // add all the "missing" / newly-arrived rows to the map //----------------------------------------------------------------------------- map.getOrCreateLayer("New Data Nodes").addChildren(newRowNodes); // PROBLEM/BUG: the above add to a special layer appears to be failing (to // the user) somtimes and the nodes wind up in the same layer as the // relating nodes -- this is when ArrangeAction.clusterLinked is then used // below. It does some reparenting which it needs to do in case nodes had // been collected as children, but in some cases, it doesn't need doing and // ends up just pulling the nodes right back out of the "New Data Nodes" // layer after we just moved them there. // ----------------------------------------------------------------------------- if (newRowNodes.size() > NEW_ROW_NODE_MAP_REORG_THRESHOLD) { if (targetsUsed.size() > 0) { // Note: won't currently trigger for cross-schema joins, as targesUsed aren't reported //------------------------------------------------------- // RE-CLUSTER THE ENTIRE MAP //------------------------------------------------------- // If there is was more than one value-node link per row-node created (e.g., // multiple sets of value nodes are already on the map), prioritizing those // targets with the most first spreads the nodes out the most as the targets // with the fewest links would are at least be guaranteed to get some of the // row nodes. Using the push-method in this case would be far too slow -- we'd // have to push based on every row node. final List<Multiset.Entry<LWComponent>> ordered = ByDecreasingFrequency.sortedCopy(targetsUsed.entrySet()); for (Multiset.Entry<LWComponent> e : ordered) { tufts.vue.Actions.ArrangeAction.clusterLinked(e.getElement()); } // note: if we wished, we could also decide here // what to cluster on based on what targets are // selected (currently have the selection bit set) } else { // fallback: randomly layout anything that isn't first XY clustered: tufts.vue.LayoutAction.random.act (tufts.vue.VueUtil.setXYByClustering(newRowNodes)); } } else { //------------------------------------------------------- // Centroid cluster //------------------------------------------------------- DataAction.centroidCluster(map, newRowNodes, true); //------------------------------------------------------- } VUE.getSelection().setTo(newRowNodes); } map.getUndoManager().mark("Add New Data Nodes"); } private static String makeFieldLabel(final Field field) { if (field.isQuantile()) return HTML(field.getName()); //return HTML("<font color=gray>" + field.getName()); final Set values = field.getValues(); //Log.debug("EXPANDING " + colNode); //LWComponent schemaNode = new LWNode(schema.getName() + ": " + schema.getSource()); // add all style nodes to the schema node to be put in an internal layer for // persistance: either that or store them with the datasources, which // probably makes more sense. String label = field.toString(); if (values.size() == 0) { if (field.getMaxValueLength() == 0) { //label = String.format("<html><b><font color=gray>%s", field.getName()); label = String.format(HTML("<font color=gray>%s"), field.getName()); } else { //label = String.format("<html><b>%s (max size: %d bytes)", label = String.format(HTML("%s (max size: %d bytes)"), field.getName(), field.getMaxValueLength()); } } else if (values.size() == 1) { label = String.format(HTML("%s: <font color=green>%s"), field.getName(), field.getValues().toArray()[0]); } else if (values.size() > 1) { //final Map<String,Integer> valueCounts = field.getValueMap(); // if (field.isPossibleKeyField()) // //label = String.format("<html><i><b>%s</b> (%d)", field.getName(), field.uniqueValueCount()); // else // // we add space before and after to widen the painted background selection around the text a bit // label = String.format(HTML(" %s (%d) "), field.getName(), field.uniqueValueCount()); label = String.format(HTML("%s (%d)"), field.getName(), field.uniqueValueCount()); } return label; } static class DataNode extends DefaultMutableTreeNode { String display; protected DataNode(String description) { setDisplay(description); } protected DataNode() {} Vector<DataNode> getChildren() { return super.children; } Schema getSchema() { Util.printStackTrace("getSchema: unimplemented"); return null; } DataRow getRow() { Util.printStackTrace("getRow: unimplemented"); return null; } /** @return false -- override for row nodes */ boolean isRow() { return false; } /** @return null -- override for value nodes */ String getValue() { return null; } /** @return null -- override for field nodes */ Field getField() { return null; } /** @return -1 -- override for value nodes */ int getCount() { return -1; } /** @return true if this node represents the collection of all possible values found in a column of data */ boolean isField() { return false; } /** @return true if this node represents a paricular enumerated value from a given column */ boolean isValue() { return !isField(); } LWComponent getStyle() { return null; } boolean hasStyle() { return false; } /** set the label visually displayed in the tree (unannotated) */ void setDisplay(String s) { display = s; setUserObject(s); // sets display label } public String getDisplay() { return (String) getUserObject(); } /** noop -- override to provide annotations againast the given map */ void annotate(LWMap map) {} void setAnnotation(String s) { setPostfix(s); } void setPostfix(String s) { //Log.debug("postfix " + this + " with [" + s + "]"); if (s == null || s.length() == 0) { setUserObject(display); } else { setUserObject(display + " " + s); } } void setPrefix(String s) { //Log.debug("prefix " + this + " with [" + s + "]"); if (s == null || s.length() == 0) { setUserObject(display); } else { //final String cs = (String) getUserObject(); if (display.startsWith("<html>")) { setUserObject("<html>" + s + " " + display.substring(6)); } else setUserObject(s + " " + display); } } /** @return true if this node is tracked for presence in the active map */ boolean isMapTracked() { return false; //return isValue(); } boolean isRowNode() { return getField() == null; } /** @return false -- override for semantics */ boolean isMapPresent() { return false; } /** @return false -- override for semantics */ boolean isContextChanged() { return false; } } final class RowNode extends DataNode { final DataRow row; boolean isMapPresent; RowNode(DataRow row, Field labelField) { this.row = row; //setDisplay(row.getValue(labelField)); setDisplay(row.toString()); } @Override boolean isRow() { return true; } @Override DataRow getRow() { return row; } @Override boolean isMapPresent() { return isMapPresent; } @Override boolean isContextChanged() { return row.isContextChanged(); } @Override void annotate(LWMap map) { // final Field keyField = getSchema().getKeyField(); // final String keyValue = row.getValue(keyField); // isMapPresent = keyField.countContextValue(keyValue) > 0; isMapPresent = row.getContextCount() > 0; } @Override boolean isMapTracked() { return true; } @Override boolean isField() { return false; } @Override boolean isValue() { return false; } @Override Schema getSchema() { // return row.getSchema() -- row's don't currently encode the schema // if pull a schema stored in the root template node from parent.parent, // could skip making this an inner class, and save 4 bytes per row-node at runtime return mSchema; } } /** * A "field" is really a column from a particular data set, with the additional * semantics that we usually always keep around an enumerated list of all the possible * unique values that appear in that column. A FieldNode node will have a list of ValueNodes * as children to represent these values. */ private static class FieldNode extends DataNode { final Field field; FieldNode(Field field, LWComponent.Listener repainter, String description) { this.field = field; if (description == null) { if (field != null) setDisplay(makeFieldLabel(field)); } else setDisplay(description); //if (field != null && field.isEnumerated() && !field.isPossibleKeyField()) //if (field != null && !field.hasStyleNode() && !field.isSingleValue() && field.isEnumerated()) { if (field != null && field.hasStyleNode() && !field.isSingleValue() && field.isEnumerated()) { // TODO: this means on refresh, the old style node will be pointing via repainter to // an AWT component that is no longer displayed, breaking updates! // DO NOT CREATE THE STYLE NODE HERE: DO SO IN FIELD -- just UPDATE it with // the new repainter here //field.setStyleNode(DataAction.makeStyleNode(field, repainter)); field.getStyleNode().addLWCListener(repainter); } } protected FieldNode(Field field) { this.field = field; } @Override Field getField() { return field; } @Override Schema getSchema() { return field.getSchema(); } @Override LWComponent getStyle() { return field == null ? null : field.getStyleNode(); } @Override boolean hasStyle() { return field != null && field.getStyleNode() != null; } @Override boolean isField() { return field != null; } } private static final class ValueNode extends FieldNode { final String value; final int dataSetCount; boolean isMapPresent; ValueNode(Field field, String value, String label, int dataSetValueCount) { super(field); setDisplay(label); this.value = value; this.dataSetCount = dataSetValueCount; } @Override final boolean isMapTracked() { return true; } @Override String getValue() { return value; } @Override int getCount() { return dataSetCount; } @Override void annotate(LWMap map) { final int mapCount = field.countContextValue(value); if (mapCount > 0) { isMapPresent = true; if (mapCount != dataSetCount) setPostfix(String.format("[%+d]", mapCount - dataSetCount)); else setPostfix(null); //setPrefix("="); } else { isMapPresent = false; setPostfix(null); //setPrefix("-"); } // // TODO: INCLUDE CURRENT MAP COUNTS // if (field.hasContextValue(value)) // setPrefix("="); // //setAnnotation(null); // else // setPrefix("+"); // //setAnnotation("<font color=red>(new)"); } @Override public boolean isField() { return false; } @Override public boolean hasStyle() { return false; } // @Override // public LWComponent getStyle() { return null; } @Override boolean isMapPresent() { return isMapPresent; } // @Override // public String toString() { return "ValueNode[" + super.toString() + "; value=" + getValue() + "]"; } } final class AllRowsNode extends FieldNode { final Schema schema; //AllRowsNode(Schema schema, LWComponent.Listener _repainter_ignored) { AllRowsNode(Schema schema, Object _repainter_ignored) { super(null, /*repainter*/null, "All Rows"); //String.format(HTML("<b><u>All Records in %s (%d)"), schema.getName(), schema.getRowCount())); this.schema = schema; if (schema.getRowNodeStyle() == null) schema.setRowNodeStyle(DataAction.makeStyleNode(schema)); schema.getRowNodeStyle().addLWCListener(new LWComponent.Listener() { public void LWCChanged(tufts.vue.LWCEvent e) { if (DEBUG.EVENTS) Log.debug("ALL-ROWS-NODE UPDATE " + e); DataTree.this.refreshRootNode(); //updateLabel(true); } }); updateLabel(false); } private void updateLabel(boolean refresh) { // as it currently stands, this never actually needs updating setDisplay(String.format(HTML("All Records (%d)"), schema.getRowCount())); // String labelFormat = schema.getRowNodeStyle().getLabel().trim(); // if (labelFormat.startsWith("${") && labelFormat.endsWith("}")) // labelFormat = labelFormat.substring(2, labelFormat.length()-1); // setDisplay(String.format(HTML("<b>All Records</b> (%d) : <b><font color=red>%s"), // schema.getRowCount(), // labelFormat)); if (refresh) DataTree.this.refreshRootNode(); } @Override void annotate(LWMap map) { //if (DEBUG.Enabled) setAnnotation(String.format("[%s]", map.getLabel())); } @Override Schema getSchema() { return schema; } @Override boolean isField() { return false; } @Override boolean isValue() { return false; } @Override boolean hasStyle() { return true; } @Override LWComponent getStyle() { return schema.getRowNodeStyle(); } } private static final int IconWidth = 27; private static final int IconHeight = 20; //private static final Border TopBorder = BorderFactory.createLineBorder(Color.gray); // private static final Border TopBorder = new CompoundBorder(new MatteBorder(3,0,3,0, Color.white), // new CompoundBorder(new LineBorder(Color.gray), // GUI.makeSpace(1,0,1,2))); //private static final Border TopBorder = GUI.makeSpace(3,0,2,0); //private static final Border TopBorder = GUI.makeSpace(0,0,2,0); //private static final Border TopBorder = new CompoundBorder(GUI.makeSpace(0,0,10,0), new MatteBorder(0,0,1,0, Color.gray)); private static final Border TopBorderCollapsed = new CompoundBorder(new CompoundBorder(GUI.makeSpace(0,0,7,0), new MatteBorder(0,0,1,0, Color.gray)), GUI.makeSpace(0,0,7,0)); private static final Border TopBorderExpanded = null; private static final Border TopTierBorder = GUI.makeSpace(0,0,2,0); private static final Border LeafBorder = GUI.makeSpace(0,IconWidth-16,2,0); //private static final Icon IncludedInMapIcon = VueResources.getIcon(VUE.class, "images/data_onmap.png"); //private static final Icon IncludedInMapIcon = GUI.reframeIcon(VueResources.getIcon(VUE.class, "images/data_onmap.png"), 8, 16); //private static final Icon NewToMapIcon = VueResources.getIcon(VUE.class, "images/data_offmap.png"); private static final int RIPS = Util.isMacPlatform() ? 20 : 16; // RowIconPointSize private static final Icon RowHasChangedIcon = makeIcon(0x229B, RIPS, Color.green.darker(), -2, -1); private static final Icon RowOnMapIcon = makeIcon(0x229B, RIPS, VueConstants.COLOR_SELECTION, -2, -1); private static final Icon RowOffMapIcon = makeIcon(0x229B, RIPS, Color.lightGray, -2, -1); private static final Icon ValueOnMapIcon = makeIcon(0x25C9, 12, VueConstants.COLOR_SELECTION, 0, 0); private static final Icon ValueOffMapIcon = makeIcon(0x25C9, 12, Color.lightGray, 0, 0); // private static final Icon RowOnMapIcon = makeIcon(0x25C9, 14, VueConstants.COLOR_SELECTION); // private static final Icon RowOffMapIcon = makeIcon(0x25C9, 14, Color.lightGray); // private static final Icon ValueOnMapIcon = makeIcon(0x229B, 18, VueConstants.COLOR_SELECTION, 0, -1); // private static final Icon ValueOffMapIcon = makeIcon(0x229B, 18, Color.lightGray, 0, -1); //private static final Icon UniqueValueOnMapIcon = makeIcon(0x29BF, 16, VueConstants.COLOR_SELECTION, 0, -1); //private static final Icon UniqueValueOffMapIcon = makeIcon(0x29BF, 16, Color.lightGray, 0, -1); private static final Icon UniqueValueOnMapIcon = makeIcon(0x229A, 16, VueConstants.COLOR_SELECTION, 0, 0); private static final Icon UniqueValueOffMapIcon = makeIcon(0x229A, 16, Color.lightGray, 0, 0); // 29BE: ⦾ // 29BF: ⦿ // 25C9: â—‰ // 25CE: â—Ž // 229A: ⊚ // 25E6: â—¦ // 229D: âŠ� // 229B: ⊛ private static Icon makeIcon(int code, int pointSize, Color color) { return makeIcon(code, pointSize, color, 0, 0); } private static Icon makeIcon(int code, int pointSize, Color color, int xoff, int yoff) { return GUI.makeUnicodeIcon(code, pointSize, color, 16, // fixed width 16, // fixed height 4+xoff, // xoff 4+yoff // yoff ); } // private static final GUI.ResizedIcon NewToMapIcon = // new GUI.ResizedIcon(VueResources.getIcon(GUI.class, "icons/MacSmallCloseIcon.gif"), 16, 16); private static final Color KeyFieldColor = Color.green.darker(); private static final Icon TestIcon = VueResources.getImageIcon("dataSourceRSS"); private class DataRenderer extends DefaultTreeCellRenderer { { //setIconTextGap(2); //setBorder(LeafBorder); setVerticalTextPosition(SwingConstants.CENTER); //setTextSelectionColor(Color.blue); // text color selected //setTextNonSelectionColor(Color.green); // text color normal //setBackgroundSelectionColor(VueConstants.COLOR_SELECTION.brighter()); setBackgroundSelectionColor(VueResources.getColor("dataTree.selected.background", Color.blue)); setBorderSelectionColor(VueConstants.COLOR_SELECTION); //setBackgroundSelectionColor(VueConstants.COLOR_HIGHLIGHT); //setFont(tufts.vue.VueConstants.SmallFixedFont); } //@Override public int getWidth() { return 500; } public Component getTreeCellRendererComponent( final JTree tree, final Object value, final boolean selected, final boolean expanded, final boolean leaf, final int row, final boolean hasFocus) { //Log.debug(Util.tags(value)); //Log.debug(Util.tags(value)); if (!(value instanceof DataNode)) return super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); final DataNode treeNode = (DataNode) value; final Field field = treeNode.getField(); //setIconTextGap(4); // pre   standard HTML setIconTextGap(1); super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus); setForeground(Color.black); // must do every time for some reason, or de-selected text goes invisible if (treeNode.hasStyle()) { //setIconTextGap(4); // Note: icons at top level do not get the background selection color painted // behind them for some reason, whereas leaf node icons do, so this icon // needs to take that into account, and should be careful not to paint over // the selected & focus-active border of the row item. setIcon(FieldIconPainter.load(treeNode.getStyle(), selected ? backgroundSelectionColor : null)); } else { if (field != null && field.isSingleton()) { setIcon(null); } else if (!treeNode.isMapTracked()) { setIcon(null); } else { setIconTextGap(1); if (treeNode.isRow()) { if (treeNode.isContextChanged()) setIcon(RowHasChangedIcon); else if (treeNode.isMapPresent()) setIcon(RowOnMapIcon); else setIcon(RowOffMapIcon); } else { if (field != null && field.isPossibleKeyField()) { if (treeNode.isMapPresent()) setIcon(UniqueValueOnMapIcon); else setIcon(UniqueValueOffMapIcon); // } else if (field != null && field.isQuantile() && treeNode.getCount() <= 0) { // setIcon(null); } else { if (treeNode.isMapPresent()) setIcon(ValueOnMapIcon); else setIcon(ValueOffMapIcon); } } } } if (row == 0) { if (expanded) setBorder(TopBorderExpanded); else setBorder(TopBorderCollapsed); //setBorder(TopBorder); //setBackgroundNonSelectionColor(Color.lightGray); //setFont(EnumFont); } else { //setBackgroundNonSelectionColor(null); //setFont(null); //setBorder(leaf ? LeafBorder : null); if (leaf) { if (treeNode.isField() && treeNode.getField().isSingleton()) setBorder(TopTierBorder); else setBorder(LeafBorder); } else { setBorder(null); } } return this; } } private static final java.awt.geom.Rectangle2D IconSize = new java.awt.geom.Rectangle2D.Float(0,0,IconWidth,IconHeight); // private static final java.awt.geom.Rectangle2D IconInsetSize // = new java.awt.geom.Rectangle2D.Float(1,2,IconWidth-2,IconHeight-4); private static final NodeIconPainter FieldIconPainter = new NodeIconPainter(); private static final Icon EmptyIcon = new GUI.EmptyIcon(IconWidth, IconHeight); private static final double ViewScaleDown = 0.5; private static final double ViewScale = 1 / ViewScaleDown; private static final java.awt.geom.Rectangle2D IconViewSize = new java.awt.geom.Rectangle2D.Double(2*ViewScale, 4*ViewScale+0.5, (IconWidth-4) * ViewScale, (IconHeight-8) * ViewScale); private static final Stroke NodeIconBorder = new BasicStroke(0.5f); private static class NodeIconPainter implements Icon { LWComponent node; Color fill; public Icon load(LWComponent c, Color fill) { if (c == null) Log.error("null node; fill=" + fill, new Throwable("HERE")); this.node = c; this.fill = fill; return this; } public int getIconWidth() { return IconWidth; } public int getIconHeight() { return IconHeight; } public void paintIcon(Component c, Graphics _g, int x, int y) { //Log.debug("x="+x+", y="+y); if (node == null) { if (DEBUG.Enabled) Log.warn("null node in " + getClass().getName()); return; } java.awt.Graphics2D g = (java.awt.Graphics2D) _g; g.setRenderingHint (java.awt.RenderingHints.KEY_ANTIALIASING, java.awt.RenderingHints.VALUE_ANTIALIAS_ON); if (fill != null) { if (DEBUG.BOXES) { g.setColor(Color.red); g.fillRect(x,y,IconWidth,IconHeight); } else { // g.setColor(fill); // g.setColor(Color.red); // // add to width to also fill the IconTextGap // // TODO: this is painting over edge of active selected border color // g.fillRect(0,0,IconWidth+8,IconHeight+8); } } // we should only be seeing LWNode's, which always have RectanularShape final RectangularShape shape = (RectangularShape) node.getZeroShape(); shape.setFrame(IconViewSize); g.setColor(node.getFillColor()); g.scale(ViewScaleDown, ViewScaleDown); g.fill(shape); g.setStroke(NodeIconBorder); g.setColor(Color.gray); g.draw(shape); g.scale(ViewScale, ViewScale); //node.setSize(IconSize); // node.drawFit(new DrawContext(g.create(), node), // IconSize, // 2); // //node.drawFit(g, x, y); } } } // // this type of node was only for intial prototype // private static LWComponent makeDataNodes(Schema schema, Field field) // { // Log.debug("PRODUCING KEY FIELD NODES " + field); // int i = 0; // for (DataRow row : schema.getRows()) { // n = new LWNode(); // n.setClientData(Schema.class, schema); // n.getMetadataList().add(row.entries()); // if (field != null) { // final String value = row.getValue(field); // n.setLabel(makeLabel(field, value)); // } else { // //n.setLabel(treeNode.getStyle().getLabel()); // applies initial style // } // nodes.add(n); // //Log.debug("setting meta-data for row " + (++i) + " [" + value + "]"); // // for (Map.Entry<String,String> e : row.entries()) { // // // todo: this is slow: is updating UI components, setting cursors, etc, every time // // n.addMetaData(e.getKey(), e.getValue()); // // } // } // Log.debug("PRODUCED META-DATA IN " + field); // } // private static LWComponent makeDataNode(Schema schema) // { // int i = 0; // LWNode node; // for (DataRow row : schema.getRows()) { // node = new LWNode(); // node.setClientData(Schema.class, schema); // node.getMetadataList().add(row.entries()); // node.setStyle(schema.getStyleNode()); // must have meta-data set first to pick up label template // nodes.add(n); // //Log.debug("setting meta-data for row " + (++i) + " [" + value + "]"); // // for (Map.Entry<String,String> e : row.entries()) { // // // todo: this is slow: is updating UI components, setting cursors, etc, every time // // n.addMetaData(e.getKey(), e.getValue()); // // } // } // Log.debug("PRODUCED META-DATA IN " + field); // }