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