tufts.vue.ds.DataAction.java Source code

Java tutorial

Introduction

Here is the source code for tufts.vue.ds.DataAction.java

Source

/*
* 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.Util.Picker;
import tufts.vue.DEBUG;
import tufts.vue.LWMap;
import tufts.vue.LWComponent;
import tufts.vue.LWLink;
import tufts.vue.LWNode;
import tufts.vue.VueResources;
import tufts.vue.Resource;
import tufts.vue.MetaMap;

import static tufts.vue.ds.Schema.*;
import static tufts.vue.ds.Relation.*;

import static tufts.vue.LWComponent.Flag;

//import edu.tufts.vue.metadata.VueMetadataElement;

import java.awt.Color;
import java.awt.Font;

import java.util.*;

import com.google.common.collect.Multiset;

import org.apache.commons.lang.StringEscapeUtils;

/**
 * @version $Revision: 1.36 $ / $Date: 2010-02-03 19:13:16 $ / $Author: mike $
 * @author  Scott Fraize
 */

public final class DataAction {
    private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(DataAction.class);

    public static final Object ClusterTimeKey = new tufts.vue.LWComponent.PersistClientDataKey("clusterTime");

    /** note that if deformMap is true, the clustering can take a quite a long time for large maps */
    public static void centroidCluster(final LWMap map, final Collection<LWComponent> nodes,
            final boolean deformMap) {
        if (DEBUG.Enabled)
            Log.debug("centroid custering of " + Util.tags(nodes));

        // anything linked will be placed based on centroid:
        final Collection<LWComponent> pushable = map.getTopLevelItems(tufts.vue.LWComponent.ChildKind.EDITABLE);
        final Collection<LWComponent> toPush = new ArrayList(pushable.size() + nodes.size());
        toPush.addAll(nodes); // also push other new nodes (they're not on the map yet)
        for (LWComponent c : map.getTopLevelItems(tufts.vue.LWComponent.ChildKind.EDITABLE)) {
            if (c instanceof tufts.vue.LWLink)
                ; // performance filter
            else
                toPush.add(c);
        }

        final List<LWComponent> unpositioned = new ArrayList();
        final List<LWComponent> pushers = new ArrayList();

        for (LWComponent node : nodes) {

            if (moveToCentroid(node, node.getLinked())) { // could also use getClustered(), which will leave out any count links
                // node was able to be moved:
                if (deformMap)
                    pushers.add(node);
            } else {
                unpositioned.add(node);
            }

        }

        if (unpositioned.size() > 0)
            tufts.vue.LayoutAction.filledCircle.act(unpositioned, DataDropHandler.AUTO_FIT);

        if (deformMap && pushers.size() > 0) {
            // we push everything last, so even the newly added nodes will be pushed
            for (LWComponent node : pushers) {
                tufts.vue.Actions.projectNodes(toPush, node, 12);
            }
        }
    }

    /** @return true if the given mover-node was able to be positioned based on related */
    public static boolean moveToCentroid(LWComponent mover, Collection<LWComponent> related) {
        if (related == null || related.isEmpty()) {
            return false;
        } else if (related.size() == 1) {
            // If only one item, centroid will be center of the one item, and if we set
            // a node *exactly* on center of another node, and theres a link between
            // them (as there is here), the link will be exactly zero length, which is
            // currently a condition that is mostly handled, but generates warnings
            // which we don't want to wholesale turn off -- e.g., you'll see "bad
            // paintBounds" or "bar projection points".  So we never set a node
            // exaclty centered on another node.

            // This also produces an interesting vertical stacking when
            // adding a bunch of value nodes on top of a single row node:
            // e.g.: categories on top of an RSS news item row-node.

            final LWComponent onTopOf = Util.getFirst(related);
            mover.setCenterAt(onTopOf.getMapCenterX() + (2 - Math.random() * 4),
                    onTopOf.getMapCenterY() - (mover.getHeight() + 12));
            return true;
        } else {
            java.awt.geom.Point2D.Float centroid = tufts.vue.VueUtil.computeCentroid(related);
            if (centroid != null) {
                //if (DEBUG.Enabled) Log.debug("CENTROID " + Util.fmt(centroid) + " for " + node + " with " + Util.tags(linked));

                // randomly add +/- 4 coordinate units to prevent the exact-on-center problem mentioned above
                // and provide some helpful off-center visual noise for nodes with exactly two links
                // (less of a "straight line" appearance through the node)
                centroid.x += (4 - Math.random() * 8);
                centroid.y += (4 - Math.random() * 8);

                mover.setCenterAt(centroid);

                return true;
            } else {
                return false;
            }
        }
    }

    public static String valueText(Object value) {
        return StringEscapeUtils.escapeHtml(Field.valueText(value));
    }

    //     private static List<LWLink> makeLinks(LWComponent node)
    //     {
    //         if (node.isDataRowNode())
    //             return makeRowNodeLinks(getLinkTargets(node.getMap()), node);
    //         else if (node.isDataValueNode())
    //             return makeValueNodeLinks(getLinkTargets(node.getMap()), node, node.getDataValueField());
    //         else
    //             return Collections.EMPTY_LIST;
    //     }

    //     private static List<LWLink> makeLinks
    //         (LWComponent node, Collection<LWComponent> linkTargets) {
    //         if (node.isDataRowNode())
    //             return makeRowNodeLinks(linkTargets, node);
    //         else if (node.isDataValueNode())
    //             return makeValueNodeLinks(linkTargets, node, node.getDataValueField());
    //         else
    //             return Collections.EMPTY_LIST;
    //     }

    private static List<LWLink> makeDataLinksForNode(final LWComponent node,
            final Collection<? extends LWComponent> linkTargets, final Multiset<LWComponent> targetsUsed) {
        if (linkTargets.size() > 0) {
            return makeLinks(linkTargets, node, node.getDataValueField(), targetsUsed); // seems wrong to be providing this last argumen
        } else {
            return Collections.EMPTY_LIST;
        }

    }

    private static final Picker DataNodePicker = new Picker<LWComponent>() {
        public boolean match(LWComponent c) {
            return c.getClass() == LWNode.class;
            //return c.getClass() == LWNode.class && c.isDataNode();
        }
    };

    /** @return a list of all *possible* targets we may want to be linking to */
    private static List<? extends LWComponent> getLinkTargets(LWMap map) {
        return Util.extract(map.getAllDescendents(), DataNodePicker);

    }

    /** @field -- if null, will make exaustive row-node links
     * @return double result -- [0] is Multiset of LWComponent targetUsed w/counts, [1] is List of LWLink's created */
    //public static Multiset<LWComponent> addDataLinksForNodes
    public static Object[] addDataLinksForNodes(final LWMap map, final List<? extends LWComponent> nodes,
            final Field field) {
        if (DEBUG.Enabled)
            Log.debug("addDataLinksForNodes; field=" + quoteKey(field));

        final Multiset<LWComponent> targetsUsed = com.google.common.collect.HashMultiset.create();
        final List<LWLink> links = makeDataLinksForNodes(map, nodes, field, targetsUsed);

        if (links.size() > 0)
            map.getInternalLayer("*Data Links*").addChildren(links);

        Object[] result = new Object[2];
        result[0] = targetsUsed;
        result[1] = links;

        return result;
    }

    private static List<LWLink> makeDataLinksForNodes(final LWMap map, final List<? extends LWComponent> nodes,
            final Field field, final Multiset<LWComponent> targetsUsed) {
        final Collection linkTargets = getLinkTargets(map);

        Log.debug("LINK-TARGETS: " + Util.tags(linkTargets));

        List<LWLink> links = Collections.EMPTY_LIST;

        if (linkTargets.size() > 0) {
            links = new ArrayList();
            for (LWComponent newNode : nodes) {
                links.addAll(makeLinks(linkTargets, newNode, field, targetsUsed));
            }
        }

        return links;
    }

    /** @param field -- if null, will defer to makeRowNodeLinks, and assume the given node is a row node */
    private static List<LWLink> makeLinks(final Collection<? extends LWComponent> linkTargets,
            final LWComponent node, final Field field, final Multiset<LWComponent> targetsUsed) {
        //Log.debug("makeLinks: " + field + "; " + node);

        if (field == null)
            return makeRowNodeLinks(linkTargets, node, targetsUsed);
        else
            return makeValueNodeLinks(linkTargets, node, field, targetsUsed);
    }

    public static List<LWComponent> makeRelatedNodes(Field dragField, LWComponent dropTarget) {
        if (DEBUG.Enabled)
            Log.debug("makeRelatedNodes: " + quoteKey(dragField) + "; target=" + dropTarget);
        return makeRelatedValueNodes(dragField, dropTarget.getRawData());
    }

    // Now that this takes a MetaMap, it could allow us to simply substitue a batch of
    // Resource meta-data instead, Tho there's no schema in that case, so we'd have to
    // deal with that...

    /**
     * Used for dragging a schema Field (column) onto an on-map row-node.  This
     * will use the Field to extract any values matching it from the row-node
     * (e.g., 7 different "category" values), or values pulled from a joined schema.
     */
    static List<LWComponent> makeRelatedValueNodes(Field dragField, MetaMap onMapRowData) {

        if (DEBUG.Enabled)
            Log.debug("makeRelatedValueNodes: " + quoteKey(dragField) + "; row=" + onMapRowData);

        // TODO: if rowData is from a single value node, this makes no sense -- should move
        // the methods for identifying the type of "row" (MetaMap) it is to MetaMap itself
        // (instead of LWComponent)

        final List<LWComponent> nodes = new ArrayList();

        for (String value : onMapRowData.getValues(dragField.getName())) {
            //-----------------------------------------------------------------------------
            // Note: We pull ALL values for the given Field: e.g., there may be 20 different
            // "category" values.
            //-----------------------------------------------------------------------------
            Log.debug("makeRelatedValueNodes: " + quoteKV(dragField, value));
            nodes.add(makeValueNode(dragField, value));
        }

        //         for (String joinedValue : Relation.getCrossSchemaJoinedValues(dragField, onMapRowData, ALL_VALUES)) {
        //             LWComponent newNode = makeValueNode(dragField, joinedValue);
        //             //newNode.addDataValue("@index", String.format("%s=%s", indexKey, indexValue));
        //             nodes.add(newNode);
        //         }
        for (Relation join : Relation.getCrossSchemaJoinedValues(dragField, onMapRowData, ALL_VALUES)) {
            LWComponent newNode = makeValueNode(dragField, join.value);
            //newNode.addDataValue("@index", String.format("%s=%s", indexKey, indexValue));
            nodes.add(newNode);
        }

        return nodes;
    }

    private static final boolean CREATE_COUNT_LINKS = true;

    /**
     * Make links from the given node, which is a value node for the given Field,
     * to any nodes in linkTargets for which a Relation can be found.
     */
    private static List<LWLink> makeValueNodeLinks(final Collection<? extends LWComponent> linkTargets,
            final LWComponent node, final Field field, // todo: remove arg? is not immediately clear this MUST be the Field in node (which MUST be a value node, yes?)
            final Multiset<LWComponent> targetsUsed) {
        if (DEBUG.SCHEMA || DEBUG.WORK) {
            Log.debug("makeValueNodeLinks:" + "\n\t  field: " + quoteKey(field) + "\n\t   node: " + node
                    + "\n\ttargets: " + Util.tags(linkTargets));

            if (node.getDataValueField() != field)
                Util.printStackTrace("field mis-match: nodeField=" + node.getDataValueField() + "; argField="
                        + field + "; node=" + node);
        }

        final List<LWLink> links = Util.skipNullsArrayList();

        final String fieldName = field.getName();
        final String fieldValue = node.getDataValue(fieldName);

        final Schema dragSchema = field.getSchema();

        for (LWComponent target : linkTargets) {

            if (target == node)
                continue;

            if (DEBUG.DATA)
                Log.debug("makeValueNodeLinks: processing " + target);

            try {

                // TODO: NEEDS TO USE ASSOCIATIONS
                // HANDLE VIA RELATIONS???

                // This is where ALSO where a JOIN needs to take place.  We want a way to do
                // that which is generic to schemas instead of just here, so dropping the
                // Rockwell.Medium FIELD on a Rockwell.Painting ROW will extract the right
                // value, as well as be discovered later here to create the link.

                if (target.hasDataValue(fieldName, fieldValue)) {
                    // if the target node c is schematic at all, it should only have
                    // one piece of meta-data, and it should be an exact match already
                    //boolean sameField = fieldName.equals(c.getSchematicFieldName());
                    final boolean sameField = target.isDataValueNode();
                    links.add(makeLink(node, target, fieldName, fieldValue, sameField ? Color.red : null));
                    if (targetsUsed != null)
                        targetsUsed.add(target);
                }

                final Relation relation = Relation.getCrossSchemaRelation(field, target.getRawData(), fieldValue);
                if (relation != null) {
                    if (!CREATE_COUNT_LINKS && relation.type == Relation.COUNT) {
                        // We'll get here if we're ignoring the creation of count links.

                        // This is the kind of link that makes being able to analyize an
                        // iTunes library even possible -- e.g., there are zillions of
                        // row nodes and you really don't want to see any of them --
                        // just the relationships between them.
                    } else {
                        links.add(makeLink(node, target, relation));
                    }
                }
                //                 final String relatedValue = Relation.getCrossSchemaRelation(field, target.getRawData(), fieldValue);
                //                 if (relatedValue != null) {
                //                     final String relation = String.format("matched joined value \"%s\"", relatedValue);
                //                     links.add(makeLink(node, target, null, relation, Color.green));
                //                 }
            } catch (Throwable t) {
                Log.error("exception scanning for links from " + field + " to " + target + ":", t);
            }

        }

        if (DEBUG.SCHEMA || DEBUG.WORK)
            Log.debug("makeValueNodeLinks: returning:\n\t" + Util.tags(links) + "\n\n");

        return links;
    }

    // Auto-add actual associations for fields with the same name from different schemas?

    /** make links from row nodes (full data nodes) to any schematic field nodes found in the link targets,
     or between row nodes from different schema's that are considered "auto-joined" (e.g., a matching key field appears) */
    private static List<LWLink> makeRowNodeLinks(final Collection<? extends LWComponent> linkTargets,
            final LWComponent rowNode, final Multiset<LWComponent> targetsUsed) {
        if (!rowNode.isDataRowNode())
            Log.warn("making row links to non-row node: " + rowNode, new Throwable("FYI"));

        final Schema sourceSchema = rowNode.getDataSchema();
        final MetaMap sourceRow = rowNode.getRawData();

        if (DEBUG.Enabled) {
            String targets;
            if (linkTargets.size() == 1)
                targets = Util.getFirst(linkTargets).toString();
            else
                targets = Util.tags(linkTargets);
            Log.debug("makeRowNodeLinks: " + rowNode + "; " + rowNode.getRawData() + "; " + targets);
        }

        final List<LWLink> links = Util.skipNullsArrayList();

        final List<LWComponent> singletonTargetList = new ArrayList(2);
        singletonTargetList.add(null);

        for (LWComponent target : linkTargets) {

            if (target == rowNode) // never link to ourself
                continue;

            try {

                final Schema targetSchema = target.getDataSchema();

                if (targetSchema == null) {
                    //-----------------------------------------------------------------------------
                    // CHECK FOR RESOURCE META-DATA AND LABEL META-DATA
                    //-----------------------------------------------------------------------------
                    continue;
                }

                final Field singleValueField = target.getDataValueField();

                if (singleValueField != null) {
                    singletonTargetList.set(0, rowNode);
                    final List<LWLink> valueLinks = makeValueNodeLinks(singletonTargetList, target,
                            singleValueField, null); // do NOT accrue reverse targets!
                    //= makeValueNodeLinks(singletonTargetList, target, singleValueField, targetsUsed);
                    if (valueLinks.size() > 0) {
                        targetsUsed.add(target);
                        if (valueLinks.size() > 1)
                            Log.warn("more than 1 link added for single value node: " + Util.tags(valueLinks),
                                    new Throwable("HERE"));
                    }
                    links.addAll(valueLinks);

                }

                //                 final String singleValueFieldName = c.getDataValueFieldName();
                //                 if (singleValueFieldName != null) {
                //                     //-----------------------------------------------------------------------------
                //                     // The target being inspected is a value node - create a link
                //                     // if there's any matching value in the row node.  We don't
                //                     // currently care if it's from the same schema or not: identical
                //                     // field names currently always provide a match (sort of a weak auto-join)
                //                     //-----------------------------------------------------------------------------
                //                     final String fieldValue = c.getDataValue(singleValueFieldName);
                //                     // TODO: USE ASSOCIATIONS
                //                     if (rowNode.hasDataValue(singleValueFieldName, fieldValue)) {
                //                         links.add(makeLink(c, rowNode, singleValueFieldName, fieldValue, null));
                //                     }
                //                 }

                else if (sourceSchema == targetSchema) {

                    final MetaMap targetRow = target.getRawData();

                    if (Relation.isSameRow(targetSchema, targetRow, sourceRow)) {
                        links.add(makeLink(rowNode, target, null, null, Color.orange));
                        targetsUsed.add(target);
                    }
                }

                else { // if (sourceSchema != targetSchema) {

                    final MetaMap targetRow = target.getRawData();

                    //Log.debug("looking for x-schema relation: " + sourceRow + "; " + targetRow);

                    final Relation relation = Relation.getRelation(sourceRow, targetRow);

                    if (relation != null) {
                        links.add(makeLink(rowNode, target, relation));
                        targetsUsed.add(target);
                    }

                    //makeCrossSchemaRowNodeLinks(links, sourceSchema, targetSchema, rowNode, c);
                }

            } catch (Throwable t) {
                Log.warn("makeRowNodeLinks: processing target: " + target, t);
            }
        }

        return links;
    }

    private static LWLink makeLink(final LWComponent src, final LWComponent dest, final Relation r) {
        if (DEBUG.Enabled)
            Log.debug("makeLink: " + r);

        final Color color;

        final LWLink link = makeLink(src, dest, null, r.getDescription(), null);

        if (link == null) {
            Log.error("link=null " + r);
            return null;
        }

        if (r.isCrossSchema()) {
            link.mStrokeStyle.setTo(LWComponent.StrokeStyle.DASH3);
            link.setStrokeWidth(2);
        }

        // todo: count style priority over join style

        if (r.type == Relation.AUTOMATIC) {
            color = Color.lightGray;
        } else if (r.type == Relation.USER) {
            color = Color.black;
        } else if (r.type == Relation.COUNT) {
            //if (true) return null;
            color = Color.lightGray;
            if (r.count == 1)
                link.setStrokeWidth(0.3f);
            else
                link.setStrokeWidth((float) Math.log(r.count));
            link.setTextColor(color);
            link.setLabel(String.format(" %d ", r.getCount()));
        } else if (r.type == Relation.JOIN) {
            color = Color.orange;
        } else
            color = Color.magenta; // unknown type!

        link.setStrokeColor(color);

        //         if (r.type == Relation.JOIN)
        //         else if (r.type == Relation.COUNT)

        return link;
    }

    private static LWLink makeLink(final LWComponent src, final LWComponent dest, final String fieldName, // if null, puts raw fieldValue as the entire relationship name
            final String fieldValue, final Color specialColor) {
        if (src.hasLinkTo(dest)) {
            // don't create a link if there already is one of any kind
            return null;
        }

        LWLink link = new LWLink(src, dest);
        link.setArrowState(0);
        if (specialColor != null) {
            link.mStrokeStyle.setTo(LWComponent.StrokeStyle.DASH3);
            link.setStrokeWidth(3);
            link.setStrokeColor(specialColor);
            if (specialColor == Color.red) {
                // complate hack for now till specialColor is a more semantically meaningful argument.
                // Disabling label on a "same" link so we can double-click it to collapse it.
                link.disableProperty(tufts.vue.LWKey.Label);
            }
        } else {
            link.setStrokeColor(Color.lightGray);
        }

        final String relationship;

        if (fieldName == null)
            relationship = fieldValue;
        else
            relationship = String.format("%s=%s", fieldName, StringEscapeUtils.escapeCsv(fieldValue));

        link.setAsDataLink(relationship);

        return link;

    }

    private static String makeLabel(Field f, Object value) {

        return f.valueDisplay(value);

        //Log.debug("*** makeLabel " + f + " [" + value + "] emptyValue=" + (value == Field.EMPTY_VALUE));
        // // This will be overriden by the label-style: this could would need to go there to work
        // //         if (value == Field.EMPTY_VALUE)
        // //             return String.format("(no [%s] value)", f.getName());
        // //         else
        //             return Field.valueName(value);
    }

    private static final int DataNodeLabelLength = VueResources.getInt("dataNode.labelLength", 30);

    public static LWComponent makeValueNode(Field field, String value) {
        final String displayLabel = makeLabel(field, value);

        final LWComponent node = new LWNode(displayLabel);

        node.setDataInstanceValue(field, value);

        if (field.hasStyleNode())
            node.setStyle(field.getStyleNode());

        // The set-style may have re-set the label, so we must wrap after / in case of that

        node.wrapLabelToWidth(DataNodeLabelLength);

        return node;
    }

    //     public static LWComponent makeValueNode(Field field, String value)
    //     {
    //         final String displayLabel = makeLabel(field, value);

    //         //Log.debug("DISPLAY LABEL: " + Util.tags(displayLabel) + " in field " + field + " with style " + field.getStyleNode());

    //         //final String wrappedLabel = Util.formatLines(displayLabel, VueResources.getInt("dataNode.labelLength"));

    //         //Log.debug("WRAPPED LABEL: " + Util.tags(wrappedLabel));

    //         final LWComponent node = new LWNode(displayLabel);

    // //         Log.debug("MADE VALUE NODE0: " + node);
    // //         Log.debug("WITH LABEL0: " + Util.tags(node.getLabel()));

    //         node.setDataInstanceValue(field, value);

    //         if (field.hasStyleNode())
    //             node.setStyle(field.getStyleNode());

    //         // The set-style may have re-set the label, so we must wrap after / in case of that

    //         // do need to set label afterwords to ensure any templating?

    // //         Log.debug("MADE VALUE NODE1: " + node);
    // //         Log.debug("WITH LABEL1: " + Util.tags(node.getLabel()));

    //         node.wrapLabelToWidth(DataNodeLabelLength);

    //         return node;

    //     }

    public static List<LWComponent> makeSingleRowNode(Schema schema, DataRow singleRow) {

        Log.debug("PRODUCING SINGLE ROW NODE FOR " + schema + "; row=" + singleRow);

        List<DataRow> rows = Collections.singletonList(singleRow);

        return makeRowNodes(schema, rows);

    }

    public static List<LWComponent> makeRowNodes(Schema schema) {

        Log.debug(" " + schema + "; rowCount=" + schema.getRows().size());

        return makeRowNodes(schema, schema.getRows());

    }

    //     public static List<LWComponent> makeRelatedRowNodes(Schema schema, MetaMap rowFilter) {
    //     }

    public static List<LWComponent> makeRelatedRowNodes(Schema schema, LWComponent dropTargetFilter) {

        Log.debug("makeRelatedRowNodes " + schema + "; filter=" + dropTargetFilter);

        return makeRowNodes(schema, Relation.findRelatedRows(schema, dropTargetFilter));
    }

    public static List<LWComponent> makeRowNodes(final Schema schema, final Collection<DataRow> rows) {
        if (rows.isEmpty())
            return Collections.EMPTY_LIST;

        if (schema.getRowNodeStyle() == null) {
            schema.setRowNodeStyle(makeStyleNode(schema));
            Log.info("auto-applied row-style to " + schema + ": " + schema.getRowNodeStyle());
        }

        final java.util.List<LWComponent> nodes = new ArrayList();

        // TODO: findField should find case-independed values -- wasn't our key hack supposed to handle that?

        final Field linkField = schema.findField("Link");
        final Field descField = schema.findField("Description");
        final Field titleField = schema.findField("Title");

        // Note: these fields are RSS specific -- just using them as defaults for now.
        // Not a big deal if their not found.
        //final Field mediaField = schema.findField("media:group.media:content.media:url");
        final Field mediaField = schema.findField("media:content@url");
        Field mediaDescription = schema.findField("media:description");
        if (mediaDescription == null)
            mediaDescription = schema.findField("media:content.media:description");
        final Field imageField = schema.getImageField();

        //final int maxLabelLineLength = VueResources.getInt("dataNode.labelLength", 50);
        //         final Collection<DataRow> rows;
        //         if (singleRow != null) {
        //             Log.debug("PRODUCING SINGLE ROW NODE FOR " + schema + "; row=" + singleRow);
        //             rows = Collections.singletonList(singleRow);
        //         } else {
        //             Log.debug("PRODUCING ALL DATA NODES FOR " + schema + "; rowCount=" + schema.getRows().size());
        //             rows = schema.getRows();
        //         }

        if (DEBUG.Enabled) {
            Log.debug("makeRowNodes " + schema + "; " + Util.tags(rows));
            Log.debug("IMAGE FIELD: " + imageField);
            Log.debug("MEDIA FIELD: " + mediaField);
        }

        final boolean singleRow = (rows.size() == 1);

        int i = 0;
        LWNode node;
        for (DataRow row : rows) {

            try {

                node = LWNode.createRaw();
                node.takeAllDataValues(row.getData());
                node.setStyle(schema.getRowNodeStyle()); // must have meta-data set first to pick up label template

                //                 if (singleRow) {
                //                     // if handling a single node (e.g., probably a single drag),
                //                     // also apply & override with the current on-map creation style
                //                     tufts.vue.EditorManager.targetAndApplyCurrentProperties(node);
                //                 }

                String link = null;

                if (linkField != null) {
                    link = row.getValue(linkField);
                    if ("n/a".equals(link))
                        link = null; // TEMP HACK
                }

                if (link != null) {
                    node.setResource(link);
                    final tufts.vue.Resource r = node.getResource();
                    //                 if (descField != null) // now redundant with data fields, may want to leave out for brevity
                    //                     r.setProperty("Description", row.getValue(descField));
                    if (titleField != null) {
                        String title = row.getValue(titleField);
                        r.setTitle(title);
                        r.setProperty("Title", title);
                    }

                    if (mediaField != null) {
                        // todo: if no per-item media field, use any per-schema media field found
                        // (e.g., RSS content provider icon image)
                        // todo: refactor so cast not required
                        String media = row.getValue(mediaField);
                        if (DEBUG.WORK)
                            Log.debug("attempting to set thumbnail " + Util.tags(media));
                        ((tufts.vue.URLResource) r).setURL_Thumb(media);
                    }
                }

                Resource IR = null;

                if (imageField != null) {
                    String image = row.getValue(imageField);
                    if (Resource.looksLikeURLorFile(image)) {
                        if (!image.equals("n/a")) // tmp hack for one of our old test data sets
                            IR = Resource.instance(image);
                    }
                }

                if (IR == null && mediaField != null) {
                    String media = row.getValue(mediaField);
                    if (Resource.looksLikeURLorFile(media))
                        IR = Resource.instance(media);
                }

                if (IR != null) {

                    String mediaDesc = null;

                    if (mediaDescription != null)
                        mediaDesc = row.getValue(mediaDescription);

                    if (mediaDesc == null && titleField != null)
                        mediaDesc = row.getValue(titleField);

                    if (mediaDesc != null) {
                        IR.setTitle(mediaDesc);
                        IR.setProperty("Title", mediaDesc);
                    }

                    if (DEBUG.WORK)
                        Log.debug("image resource: " + IR);
                    tufts.vue.LWImage image = tufts.vue.LWImage.createNodeIcon(IR);
                    node.addChild(image);

                    // HACK FOR NOW: set the ICON bit.  This will have been cleared
                    // during the addChild when it is discovered that the resource for
                    // the image is NOT the same as the resource for the node.  Setting
                    // this here will force the image to keep icon-sizing when it's size
                    // arrives, which fixes the sizing part of VUE-1637 for now, and
                    // also half-fixes another outstanding bug which is that this images
                    // can be dragged out (as they're not really node-icons).
                    //
                    // So as long as this bit is set, the image can't be dragged out.
                    // But the bit is not persisted, so after saving / opening the map,
                    // the drag-out bug will still exist.  To fix that, we'd need to
                    // redesign all the code dealing with node-icons so that it isn't
                    // triggered by having the same resource, which raises other issues
                    // -- e.g., what, really, is a node-icon?

                    image.setFlag(Flag.ICON);
                }

                // This is now handled by LWComponent.fillLabelFormat for all data value replacements
                //String label = node.getLabel();
                //label = Util.formatLines(label, maxLabelLineLength);
                //node.setLabel(label);

                nodes.add(node);
            } catch (Throwable t) {
                Log.error("failed to create node for row " + row, t);
            }

        }

        Log.debug("makeRowNodes: PRODUCED NODE(S) FOR " + schema + "; count=" + nodes.size());

        return nodes;
    }

    private static final Color DataNodeColor = VueResources.getColor("node.dataRow.color", Color.gray);
    private static final float DataNodeStrokeWidth = VueResources.getInt("node.dataRow.stroke.width", 0);
    private static final Color DataNodeStrokeColor = VueResources.getColor("node.dataRow.stroke.color",
            Color.black);
    private static final Font DataNodeFont = VueResources.getFont("node.dataRow.font");

    private static final Color ValueNodeTextColor = VueResources.getColor("node.dataValue.text.color", Color.black);
    private static final Font ValueNodeFont = VueResources.getFont("node.dataValue.font");
    private static final Color[] ValueNodeDataColors = VueResources.getColorArray("node.dataValue.color.cycle");
    private static int NextColor = 0;

    static LWComponent initNewStyleNode(LWComponent style) {

        //style.setID(style.getURI().toString());

        //style.setFlag(Flag.DATA_STYLE); // must set before setting label, or template will atttempt to resolve

        Schema.runtimeInitStyleNode(style);

        // we use the persisted visible bit to store a bit for DataTree node expanded state
        // -- the actual visibility of the style node will never come into play as it's never
        // on a map
        style.setVisible(false);
        return style;
    }

    /** @return an empty styling node (appearance values to be set elsewhere) */
    private static LWComponent makeStyleNode() {
        return initNewStyleNode(new LWNode());
    }

    /** @return a row-styling node for the given schema */
    public static LWComponent makeStyleNode(Schema schema) {
        final LWComponent style = makeStyleNode();

        String titleField;

        // Make a guess at what might be the best field to use for the node label text
        if (schema.getRowCount() <= 42 && (schema.hasField("title") || schema.hasField("Title"))) {
            // if we have hundreds of nodes, title may be too long to use -- the key
            // field may well be shorter.
            titleField = "title";
        } else {
            titleField = schema.getKeyFieldGuess().getName();
        }

        style.setLabel(String.format("${%s}", titleField));

        style.setFont(DataNodeFont);
        style.setTextColor(Color.black);
        style.setFillColor(DataNodeColor);
        style.setStrokeWidth(DataNodeStrokeWidth);
        style.setStrokeColor(DataNodeStrokeColor);
        //style.disableProperty(LWKey.Notes);
        //String notes = String.format("Style for all %d data items in %s",
        String notes = String.format("Style for row nodes in Schema '%s'", schema.getName());

        //if (DEBUG.Enabled) notes += ("\n\nSchema: " + schema.getDump());
        style.setNotes(notes);
        //style.setFlag(Flag.STYLE); // do last

        return style;
    }

    //     public static LWComponent makeStyleNode(final Field field) {
    //         return makeStyleNode(field, null);
    //     }
    //     public static LWComponent makeStyleNode(final Field field, LWComponent.Listener repainter)
    public static LWComponent makeStyleNode(final Field field) {
        final LWComponent style;

        if (field.isPossibleKeyField()) {

            style = new LWNode(); // creates a rectangular node
            //style.setLabel(" ---");
            style.setFillColor(Color.lightGray);
            style.setFont(DataNodeFont);
        } else {
            //style = new LWNode(" ---"); // creates a round-rect node
            style = new LWNode(""); // creates a round-rect node
            //style.setFillColor(Color.blue);
            style.setFillColor(ValueNodeDataColors[NextColor]);
            if (++NextColor >= ValueNodeDataColors.length)
                NextColor = 0;
            //             NextColor += 8;
            //             if (NextColor >= DataColors.length) {
            //                 if (FirstRotation) {
            //                     NextColor = SecondRotationColor;
            //                     FirstRotation = false;
            //                 } else {
            //                     NextColor = FirstRotationColor;
            //                     FirstRotation = true;
            //                 }
            //             }
            style.setFont(ValueNodeFont);
        }
        initNewStyleNode(style); // must set before setting label, or template will atttempt to resolve
        //style.setLabel(String.format("%.9s: \n${%s} ", field.getName(),field.getName()));
        //         if (field.isQuantile())
        //             style.setLabel(String.format("%s\n${%s}", field.getName(), field.getName()));
        //         else
        style.setLabel(String.format("${%s}", field.getName()));
        // holy crap: when did single quotes in strings stop persisting? below was causing a failure
        // due to single quotes (where brackets are now) -- CHIRST -- it's failing no matter what,
        // complaining of single-quotes even when there are none -- what the hell...
        //         style.setNotes(String.format
        //                        ("Style node for field [%s] in data-set [%s]\n\nSource: %s\n\n%s\n\nvalues=%d; unique=%d; type=%s",
        //                         field.getName(),
        //                         field.getSchema().getName(),
        //                         field.getSchema().getResource(),
        //                         field.valuesDebug(),
        //                         field.valueCount(),
        //                         field.uniqueValueCount(),
        //                         field.getType()
        //                        ));
        style.setTextColor(ValueNodeTextColor);
        //style.disableProperty(LWKey.Label);
        //         if (repainter != null)
        //             style.addLWCListener(repainter);
        //style.setFlag(Flag.STYLE); // set last so creation property sets don't attempt updates

        return style;
    }

}

//         final Schema dragSchema = dragField.getSchema();
//         final Schema dropSchema = onMapRowData.getSchema();
//         Log.debug("Looking for joins between:"
//                   + "\n\t" + dragSchema
//                   + "\n\t" + dropSchema);
//         //for (DataRow row : dragSchema.getJoinedMatchingRows(join, dragField) {

//         int i = 0;
//         for (Association join : Association.getBetweens(dragSchema, dropSchema)) {

//             Log.debug("JOIN #" + i + ": " + Util.tags(join));
//             i++;

//             // This works for the Rockwell-Mediums case, tho only for initial node
//             // creation of course -- NEED TO GENERALIZE

//             final Field indexKey = join.getFieldForSchema(dragSchema);
//             final String indexValue = onMapRowData.getString(join.getKeyForSchema(dropSchema)); // todo: multi-values

//             Log.debug("JOIN: indexKey=" + indexKey + "; indexValue=" + indexValue);

//             final Collection<DataRow> matchingRows = dragSchema.getMatchingRows(indexKey, indexValue);

//             Log.debug("found rows: " + Util.tags(matchingRows));

//             final String extractKey = dragField.getName();

//             for (DataRow row : matchingRows) {
//                 // todo: use Schema.searchData?
//                 final Collection<String> joinedValues = row.getValues(extractKey);
//                 Log.debug("extracted " + Util.tags(joinedValues));
//                 for (String extractValue : joinedValues) {
//                     final LWComponent newNode = makeValueNode(dragField, extractValue);
//                     newNode.addDataValue("@index", String.format("%s=%s", indexKey, indexValue));
//                     nodes.add(newNode);
//                 }
//             }

//         }

//             //----------------------------------------------------------------------------------------
//             // TODO: below is essentially cut & paste from above makeRelatedValueNodes
//             //----------------------------------------------------------------------------------------

//             final MetaMap onMapRowData = target.getRawData();
//             final Schema dropSchema = onMapRowData.getSchema();
//             final Field dragField = field;

//             if (dragSchema != dropSchema) {

//                 int i = 0;
//                 for (Association join : Association.getBetweens(dragSchema, dropSchema)) {

//                     Log.debug("JOIN #" + i + ": " + Util.tags(join));
//                     i++;

//                     // This works for the Rockwell-Mediums case, tho only for initial node
//                     // creation of course -- NEED TO GENERALIZE

//                     final Field indexKey = join.getFieldForSchema(dragSchema);
//                     final String indexValue = onMapRowData.getString(join.getKeyForSchema(dropSchema)); // todo: multi-values

//                     Log.debug("JOIN: indexKey=" + indexKey + "; indexValue=" + indexValue);

//                     final Collection<DataRow> matchingRows = dragSchema.getMatchingRows(indexKey, indexValue);

//                     Log.debug("found rows: " + Util.tags(matchingRows));

//                     final Field extractKey = dragField;

//                     for (DataRow row : matchingRows) {
//                         // todo: use Schema.searchData?
//                         final Collection<String> joinedValues = row.getValues(extractKey);
//                         Log.debug("extracted " + Util.tags(joinedValues));
//                         for (String extractValue : joinedValues) {

//                             //------------------------------------------------------------------
//                             if (!fieldValue.equals(extractValue)) // ** THE EXCLUSION **
//                                 continue;
//                             //------------------------------------------------------------------

//                             final String relation = String.format("%s=\"%s\"\n%s=\"%s\"",
//                                                                   indexKey, indexValue,
//                                                                   extractKey, extractValue);
//                             links.add(makeLink(node, target, null, relation, Color.green));
//                         }
//                     }

//                 }

//            }

// temp hack: if we keep this, have it populate an existing container
//     public LWMap.Layer createSchematicLayer() {

//         final LWMap.Layer layer = new LWMap.Layer("Schema: " + getName());

//         Field keyField = null;

//         for (Field field : getFields()) {
//             if (field.isPossibleKeyField() && !field.isLenghtyValue()) {
//                 keyField = field;
//                 break;
//             }
//         }
//         // if didn't find a "short" key field, find the shortest
//         if (keyField == null) {
//             for (Field field : getFields()) {
//                 if (field.isPossibleKeyField()) {
//                     keyField = field;
//                     break;
//                 }
//             }
//         }

//         boolean labelGuessed = false;

//         LWNode keyNode = null;
//         if (keyField != null) {
//             keyNode = new LWNode("itemNode");
//             keyNode.setShape(java.awt.geom.Ellipse2D.Float.class);
//             keyNode.setProperty(tufts.vue.LWKey.FontSize, 32);
//             keyNode.setNotes(getDump());
//         }

//         int y = Short.MAX_VALUE;
//         for (Field field : Util.reverse(getFields())) { // reversed to preserve on-map stacking order
//             if (field.isSingleton())
//                 continue;
//             LWNode colNode = new LWNode(field.getName());
//             colNode.setLocation(0, y--);
//             if (field.isPossibleKeyField()) {
//                 if (keyNode != null && !labelGuessed) {
//                     keyNode.setLabel("${" + field.getName() + "}");
//                     labelGuessed = true;
//                 }
//                 colNode.setFillColor(java.awt.Color.red);
//             } else
//                 colNode.setFillColor(java.awt.Color.gray);
//             colNode.setShape(java.awt.geom.Rectangle2D.Float.class);
//             colNode.setNotes(field.valuesDebug());
//             layer.addChild(colNode);

//             if (keyNode != null)
//                 layer.addChild(new tufts.vue.LWLink(keyNode, colNode));

//         }

//         if (keyNode != null) {
//             layer.addChild(keyNode);
//             //tufts.vue.Actions.MakeColumn.act(layer.getChildren());
//             tufts.vue.Actions.MakeCircle.act(Collections.singletonList(keyNode));
//         } else {
//             tufts.vue.Actions.MakeColumn.act(layer.getChildren());
//         }

//         return layer;
//     }