/*******************************************************************************
* Copyright 2012 University of Southern California
*
* Licensed under the Apache 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.apache.org/licenses/LICENSE-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.
*
* This code was developed by the Information Integration Group as part
* of the Karma project at the Information Sciences Institute of the
* University of Southern California. For more information, publications,
* and related projects, please see: http://www.isi.edu/integration
******************************************************************************/
/**
*
*/
package edu.isi.karma.view.tableheadings;
import static edu.isi.karma.controller.update.WorksheetHierarchicalHeadersUpdate.JsonKeys.cellType;
import static edu.isi.karma.controller.update.WorksheetHierarchicalHeadersUpdate.JsonKeys.colSpan;
import static edu.isi.karma.controller.update.WorksheetHierarchicalHeadersUpdate.JsonKeys.columnNameFull;
import static edu.isi.karma.controller.update.WorksheetHierarchicalHeadersUpdate.JsonKeys.columnNameShort;
import static edu.isi.karma.controller.update.WorksheetHierarchicalHeadersUpdate.JsonKeys.fillId;
import static edu.isi.karma.controller.update.WorksheetHierarchicalHeadersUpdate.JsonKeys.hNodeId;
import static edu.isi.karma.controller.update.WorksheetHierarchicalHeadersUpdate.JsonKeys.heading;
import static edu.isi.karma.controller.update.WorksheetHierarchicalHeadersUpdate.JsonKeys.headingPadding;
import static edu.isi.karma.controller.update.WorksheetHierarchicalHeadersUpdate.JsonKeys.leftBorder;
import static edu.isi.karma.controller.update.WorksheetHierarchicalHeadersUpdate.JsonKeys.rightBorder;
import static edu.isi.karma.controller.update.WorksheetHierarchicalHeadersUpdate.JsonKeys.topBorder;
import static edu.isi.karma.view.Stroke.StrokeStyle.inner;
import static edu.isi.karma.view.Stroke.StrokeStyle.none;
import static edu.isi.karma.view.Stroke.StrokeStyle.outer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONStringer;
import org.json.JSONWriter;
import edu.isi.karma.rep.HNode;
import edu.isi.karma.rep.HNodePath;
import edu.isi.karma.rep.Node;
import edu.isi.karma.rep.Row;
import edu.isi.karma.rep.TablePager;
import edu.isi.karma.util.JSONUtil;
import edu.isi.karma.view.Border;
import edu.isi.karma.view.Margin;
import edu.isi.karma.view.Stroke;
import edu.isi.karma.view.Stroke.StrokeStyle;
import edu.isi.karma.view.VTableCssTags;
import edu.isi.karma.view.VWorksheet;
import edu.isi.karma.view.VWorkspace;
import edu.isi.karma.view.ViewPreferences.ViewPreference;
import edu.isi.karma.view.tabledata.VDIndexTable;
import edu.isi.karma.view.tabledata.VDRow;
import edu.isi.karma.view.tabledata.VDTreeNode;
import edu.isi.karma.view.tabledata.VDVerticalSeparator;
import edu.isi.karma.view.tabledata.VDVerticalSeparators;
/**
* @author szekely
*
*/
public class VHTreeNode {
/**
* The hNode for this node, it can have nested subtables.
*/
private final HNode hNode;
/**
* If the hNode has a nested table, the id of this nested table. Otherwise a
* special string "leaf" to mark leaf nodes.
*/
private final String hTableId;
/**
* Position of a node within it's parent.
*/
private boolean isFirst, isLast;
/**
* The (left/right) index within it's parent.
*/
private int index;
/**
* Number of leaf children.
*/
private int width = 0;
/**
* Number of margins defined by this node and all children below.
*/
private int numSubtreeMargins = 0;
/**
* Depth of the node in the tree, root has depth 0;
*/
private int depth = 0;
/**
* Left and right strokes defined by the position of this node within the
* parent. It can be the stroke for a nested table.
*/
private Stroke leftStroke, rightStroke;
/**
* The strokes that will be drawn before the margins are drawn.
*/
private Stroke leftInnerStroke, rightInnerStroke;
/**
* Strokes are propagated down the tree as they must be drawn around nested
* tables.
*/
private List<Stroke> leftSubtreeStrokes = new LinkedList<Stroke>(),
rightSubtreeStrokes = new LinkedList<Stroke>();
/**
* Margins are the spaces around tables.
*/
private Margin leftMargin, rightMargin;
/**
* Bottom up accumulation of margins.
*/
private List<Margin> leftSubtreeMargins = new LinkedList<Margin>(),
rightSubtreeMargins = new LinkedList<Margin>();
/**
* Top down accumulation of subtreeMargins.
*/
private List<Margin> leftFullMargins = new LinkedList<Margin>(),
rightFullMargins = new LinkedList<Margin>();
/**
* Borders listed in the order they should appear in the html table, ie in
* painting order from left to right.
*/
private List<Border> leftBorders, rightBorders;
/**
* The children are empty if the node is a leaf node.
*/
private final ArrayList<VHTreeNode> children = new ArrayList<VHTreeNode>();
VHTreeNode(HNode hNode, String hTableId) {
this.hNode = hNode;
this.hTableId = hTableId;
}
VHTreeNode(String hTableId) {
this(null, hTableId);
}
public HNode getHNode() {
return hNode;
}
boolean isLeaf() {
return children.isEmpty();
}
boolean hasChildren() {
return !children.isEmpty();
}
boolean isRoot() {
return hNode == null;
}
public int getDepth() {
return depth;
}
String getHNodeId() {
if (isRoot()) {
return "root";
} else {
return hNode.getId();
}
}
ArrayList<VHTreeNode> getChildren() {
return children;
}
/**
* Find a VHTreeNode for a given HNode among the children.
*
* @param aHNode
* @return the VHTreeNode. It will be created if needed.
*/
private VHTreeNode getVHNode(HNode aHNode) {
// If we find it, then return it.
for (VHTreeNode child : children) {
if (child.hNode.getId().equals(aHNode.getId())) {
return child;
}
}
// Couldn't find it, so create a new one.
VHTreeNode newVHNode = new VHTreeNode(aHNode, aHNode.getNestedTable()
.getId());
children.add(newVHNode);
return newVHNode;
}
public boolean isFirst() {
return isFirst;
}
public boolean isLast() {
return isLast;
}
public boolean isMiddle() {
return !isFirst && !isLast;
}
boolean isFirstAndLast() {
return isFirst() && isLast();
}
int getIndex() {
return index;
}
/**
* Add a path of HNodes to the tree, creating nodes as needed.
*
* @param path
*/
void addColumn(HNodePath path) {
HNode hNode = path.getFirst();
HNodePath rest = path.getRest();
// hNode is a leaf.
if (rest.isEmpty()) {
children.add(new VHTreeNode(hNode, "leaf"));
}
// hNode is an internal node.
else {
VHTreeNode frontierVHNode = getVHNode(hNode);
frontierVHNode.addColumn(rest);
}
}
void addColumns(List<HNodePath> paths) {
for (HNodePath p : paths) {
addColumn(p);
}
}
/**
* @return the number of children at the leaves of the tree.
*/
int assignWidths() {
if (isLeaf()) {
width = 1;
} else {
int result = 0;
for (VHTreeNode vHNode : children) {
result += vHNode.assignWidths();
}
width = result;
}
return width;
}
void assignDepths(int depth) {
this.depth = depth;
for (VHTreeNode n : children) {
n.assignDepths(depth + 1);
}
}
/**
* Convenient to explicitly store the positions of each node among it's
* siblings given the pain to deal with this using while or for loops.
*/
void assignPositions() {
if (isRoot()) {
isFirst = true;
isLast = true;
index = 0;
}
boolean isFirstChild = true;
int currentIndex = 0;
Iterator<VHTreeNode> it = children.iterator();
while (it.hasNext()) {
VHTreeNode child = it.next();
child.index = currentIndex;
currentIndex++;
child.isFirst = isFirstChild;
isFirstChild = false;
child.isLast = !it.hasNext();
child.assignPositions();
}
}
/**
* Record the immediate strokes around each node.
*/
void assignLeftRightStrokes() {
if (isRoot()) {
leftStroke = Stroke.getRootStroke();
rightStroke = Stroke.getRootStroke();
leftMargin = Margin.getRootMargin();
rightMargin = Margin.getRootMargin();
}
if (hasChildren()) {
for (VHTreeNode child : children) {
if (child.isFirst()) {
child.leftStroke = new Stroke(outer, hTableId, depth);
} else {
child.leftStroke = new Stroke(inner, hTableId, depth);
}
if (child.isLast()) {
child.rightStroke = new Stroke(outer, hTableId, depth);
} else {
child.rightStroke = new Stroke(none, hTableId, depth);
}
child.assignLeftRightStrokes();
}
}
}
/**
* Assign the margins that go around individual nodes
*/
void assignLeftRightMargins() {
if (isRoot()) {
leftMargin = Margin.getRootMargin();
rightMargin = Margin.getRootMargin();
}
if (hasChildren()) {
for (VHTreeNode child : children) {
if (child.isLeaf()) {
child.leftMargin = Margin.getleafMargin();
child.rightMargin = Margin.getleafMargin();
} else {
child.leftMargin = new Margin(hTableId, depth);
child.rightMargin = new Margin(hTableId, depth);
}
child.assignLeftRightMargins();
}
}
}
private void addMargin(Collection<Margin> container, Margin margin) {
if (!margin.isRootMargin(margin)) {
container.add(margin);
}
}
private void addAllMargins(Collection<Margin> container,
Collection<Margin> listToAdd) {
for (Margin m : listToAdd) {
addMargin(container, m);
}
}
void bottomUpPropagation() {
if (hasChildren()) {
int numChildrenMargins = 0;
for (VHTreeNode child : children) {
child.bottomUpPropagation();
if (child.isFirst()) {
addMargin(leftSubtreeMargins, leftMargin);
addAllMargins(leftSubtreeMargins, child.leftSubtreeMargins);
} else {
}
if (child.isLast()) {
addMargin(rightSubtreeMargins, rightMargin);
addAllMargins(rightSubtreeMargins,
child.rightSubtreeMargins);
} else {
}
numChildrenMargins += child.numSubtreeMargins;
}
numSubtreeMargins = numChildrenMargins + 2;
}
else {
// leftSubtreeMargins = rightSubtreeMargins = empty;
numSubtreeMargins = 0;
}
}
void topDownPropagation() {
if (isRoot()) {
// leftSubtreeStrokes, rightSubtreeStrokes remain unmodified.
leftFullMargins.addAll(leftSubtreeMargins);
rightFullMargins.addAll(rightSubtreeMargins);
}
if (hasChildren()) {
for (VHTreeNode child : children) {
if (child.isFirst()) {
child.leftSubtreeStrokes.add(child.leftStroke);
child.leftSubtreeStrokes.addAll(leftSubtreeStrokes);
child.leftFullMargins.addAll(leftFullMargins);
} else {
child.leftSubtreeStrokes.add(child.leftStroke);
child.leftFullMargins.addAll(child.leftSubtreeMargins);
}
if (child.isLast()) {
child.rightSubtreeStrokes.add(child.rightStroke);
child.rightSubtreeStrokes.addAll(rightSubtreeStrokes);
child.rightFullMargins.addAll(rightFullMargins);
} else {
child.rightSubtreeStrokes.add(child.rightStroke);
child.rightFullMargins.addAll(child.rightSubtreeMargins);
}
child.topDownPropagation();
}
}
}
void assignInnerStrokesAndBorders() {
if (isRoot()) {
leftInnerStroke = new Stroke(outer, hTableId, 0);
rightInnerStroke = new Stroke(outer, hTableId, 0);
leftBorders = Border.getRootBorderList();
rightBorders = Border.getRootBorderList();
} else {
if (hasChildren()) {
leftInnerStroke = new Stroke(none, hNode.getHTableId(), depth);
rightInnerStroke = new Stroke(none, hNode.getHTableId(), depth);
} else {
leftInnerStroke = leftStroke;
rightInnerStroke = rightStroke;
}
leftBorders = Border.constructBorderList(leftFullMargins,
leftSubtreeStrokes, depth, true);
rightBorders = Border.constructBorderList(rightFullMargins,
rightSubtreeStrokes, depth, false);
}
for (VHTreeNode n : children) {
n.assignInnerStrokesAndBorders();
}
}
void assignTopBorderStrokes() {
for (Border b : leftBorders) {
boolean sameColor = b.getMargin().getHTableId()
.equals(hNode.getHTableId());
b.setHasTopStroke(sameColor);
}
for (Border b : rightBorders) {
boolean sameColor = b.getMargin().getHTableId()
.equals(hNode.getHTableId());
b.setHasTopStroke(sameColor);
}
for (VHTreeNode n : children) {
n.assignTopBorderStrokes();
}
}
void computeDerivedInformation() {
if (isRoot()) {
assignPositions();
assignWidths();
assignDepths(0);
assignLeftRightStrokes();
assignLeftRightMargins();
bottomUpPropagation();
topDownPropagation();
assignInnerStrokesAndBorders();
assignTopBorderStrokes();
} else {
throw new Error("Should be called on the tree root.");
}
}
void generateJson(JSONWriter jw, int levelDepth, VWorksheet vWorksheet,
VWorkspace vWorkspace) throws JSONException {
for (Border b : leftBorders) {
b.generateJson(jw, true, vWorksheet, vWorkspace);
}
if (levelDepth > depth) {
generateHeadingPaddingJson(jw, vWorksheet, vWorkspace);
} else {
generateHeadingJson(jw, vWorksheet, vWorkspace);
}
for (Border b : rightBorders) {
b.generateJson(jw, false, vWorksheet, vWorkspace);
}
}
private String shorten(String string, int maxLength) {
String result = string;
if (string.length() > maxLength) {
result = JSONUtil.truncateForHeader(string, maxLength);
}
return result;
}
private void generateHeadingJson(JSONWriter jw, VWorksheet vWorksheet,
VWorkspace vWorkspace) throws JSONException {
VTableCssTags css = vWorkspace.getViewFactory().getTableCssTags();
String topBorderCss = isRoot() ? css.getCssTag("root", 0) : css
.getCssTag(hNode.getHTableId(), depth-1);
jw.object()
//
.key(cellType.name())
.value(heading.name())
//
.key(hNodeId.name())
.value(hNode.getId())
//
.key(columnNameFull.name())
.value(hNode.getColumnName())
//
.key(columnNameShort.name())
.value(shorten(
hNode.getColumnName(),
vWorkspace.getPreferences().getIntViewPreferenceValue(
ViewPreference.maxCharactersInHeader)))
//
.key(topBorder.name())
.value(Border.encodeBorder(StrokeStyle.outer, topBorderCss))//
;
generateHeadingCommonField(jw, vWorksheet, vWorkspace);
jw.endObject();
}
private void generateHeadingPaddingJson(JSONWriter jw,
VWorksheet vWorksheet, VWorkspace vWorkspace) throws JSONException {
VTableCssTags css = vWorkspace.getViewFactory().getTableCssTags();
jw.object()
//
.key(cellType.name()).value(headingPadding.name())
//
.key(topBorder.name())
.value(Border.encodeBorder(StrokeStyle.none,
css.getCssTag(hTableId, depth-1)))
//
;
generateHeadingCommonField(jw, vWorksheet, vWorkspace);
jw.endObject();
}
private void generateHeadingCommonField(JSONWriter jw,
VWorksheet vWorksheet, VWorkspace vWorkspace) throws JSONException {
VTableCssTags css = vWorkspace.getViewFactory().getTableCssTags();
String hTableId = isRoot() ? "root" : hNode.getHTableId();
String fillCssTag = css.getCssTag(hTableId, depth-1);
int span = 1;
if (hasChildren()) {
span = numSubtreeMargins + width - leftSubtreeMargins.size()
- rightSubtreeMargins.size();
}
jw.key(fillId.name())
.value(fillCssTag)
//
.key(colSpan.name())
.value(span)
//
.key(leftBorder.name())
.value(Border.encodeBorder(leftInnerStroke.getStyle(),
css.getCssTag(leftInnerStroke.getHTableId(), depth-1)))
//
.key(rightBorder.name())
.value(Border.encodeBorder(rightInnerStroke.getStyle(),
css.getCssTag(rightInnerStroke.getHTableId(), depth-1)))//
//
.key("_hTableId").value(hTableId);
;
}
/**
* For each row in the TablePager, create a VDRow and fill it in. Deals with
* nested tables.
*
* @param vDRows
* @param tablePager
*/
public void populateVDRows(VDTreeNode parent, List<VDRow> vDRows,
TablePager tablePager, VWorksheet vWorksheet) {
boolean isFirst = true;
Iterator<Row> it = tablePager.getRows().iterator();
while (it.hasNext()) {
Row r = it.next();
VDRow vdRow = new VDRow(r, this, parent, isFirst, !it.hasNext());
isFirst = false;
vDRows.add(vdRow);
populateVDDataRow(vdRow, r, vWorksheet);
}
}
private void populateVDDataRow(VDRow vdRow, Row dataRow,
VWorksheet vWorksheet) {
Iterator<VHTreeNode> it = children.iterator();
while (it.hasNext()) {
VHTreeNode vhNode = it.next();
Node n = dataRow.getNode(vhNode.hNode.getId());
VDTreeNode vdNode = new VDTreeNode(n, vhNode, vdRow);
vdRow.add(vdNode);
if (vhNode.hasChildren()) {
vhNode.populateVDRows(vdNode, vdNode.getNestedTableRows(),
vWorksheet.getTablePager(n.getNestedTable().getId()),
vWorksheet);
}
}
}
/**
* Collect all the leaves of the tree in left to right order.
*
* @param result
*/
public void collectLeaves(List<VHTreeNode> result) {
if (isLeaf()) {
result.add(this);
} else {
for (VHTreeNode n : children) {
n.collectLeaves(result);
}
}
}
public void populateVDIndexTable(VDIndexTable vdIndexTable) {
for (VHTreeNode n : children) {
if (!n.isLeaf()) {
List<VHTreeNode> list = new LinkedList<VHTreeNode>();
n.collectLeaves(list);
vdIndexTable.addIndex(n.getHNode(), list);
}
n.populateVDIndexTable(vdIndexTable);
}
}
public void populateVDVerticalSeparators(
VDVerticalSeparators vdVerticalSeparators) {
VDVerticalSeparator s = vdVerticalSeparators.get(getHNodeId());
for (VHTreeNode n : children) {
VDVerticalSeparator vs = new VDVerticalSeparator();
if (n.isFirst) {
vs.addLeft(s.getLeftStrokes());
} else {
vs.addLeft(depth, hTableId);
}
if (n.isLast) {
vs.addRight(s.getRightStrokes());
} else {
vs.addRight(depth, hTableId);
}
if (!n.isLeaf()) {
vs.add(n.depth, n.hTableId);
}
vdVerticalSeparators.put(n.hNode.getId(), vs);
n.populateVDVerticalSeparators(vdVerticalSeparators);
}
}
/*****************************************************************
*
* Debugging Support
*
*****************************************************************/
@SuppressWarnings("unused")
private String getPositionString() {
if (isFirst() && isLast()) {
return "first/last";
} else if (isFirst()) {
return "first";
} else if (isLast()) {
return "last";
} else {
return "BAD_POSITION_STRING";
}
}
private String getStrokesString() {
return leftStroke.toString() + "||" + rightStroke.toString();
}
private String getInnerStrokesString() {
return leftInnerStroke.toString() + "||" + rightInnerStroke.toString();
}
private String getSubtreeStrokesString() {
return Stroke.toString(leftSubtreeStrokes) + "||"
+ Stroke.toString(rightSubtreeStrokes);
}
@SuppressWarnings("unused")
private String getMarginsString() {
return leftMargin.toString() + "||" + rightMargin.toString();
}
private String getSubtreeMarginsString() {
return Margin.toString(leftSubtreeMargins) + "||"
+ Margin.toString(rightSubtreeMargins);
}
private String getFullMarginsString() {
return Margin.toString(leftFullMargins) + "||"
+ Margin.toString(rightFullMargins);
}
private String getBordersString() {
return Border.getBordersString(leftBorders) + "||"
+ Border.getBordersString(rightBorders);
}
public void prettyPrintJson(JSONWriter w, boolean printChildren,
boolean verbose) {
try {
String hNodeId = "NH__";
String containingTable = "root";
String columnName = "root";
if (hNode != null) {
hNodeId = hNode.getId();
columnName = hNode.getColumnName();
containingTable = hNode.getHTableId();
}
JSONWriter a = w.object().key("hNode")
.value(hNodeId + "(" + containingTable + ")")//
.key("hTableId").value(hTableId)//
.key("column").value(columnName)//
.key("width").value(width)//
.key("depth").value(depth)//
;
if (verbose) {
a//
// .key("position").value(getPositionString())//
.key("strokes").value(getStrokesString())//
.key("subtreeStrokes").value(getSubtreeStrokesString())//
// .key("margins").value(getMarginsString())//
.key("subtreeMargins").value(getSubtreeMarginsString())//
.key("fullMargins").value(getFullMarginsString())//
.key("innerStrokes").value(getInnerStrokesString())//
.key("borders").value(getBordersString())//
.key("numMargins").value(numSubtreeMargins)//
;
}
if (!isLeaf() && printChildren) {
a.key("xchildren").array();
for (VHTreeNode n : children) {
n.prettyPrintJson(w, printChildren, verbose);
}
a.endArray();
}
a.endObject();
} catch (JSONException e) {
e.printStackTrace();
}
}
String prettyPrint() {
JSONStringer js = new JSONStringer();
prettyPrintJson(js, true, true);
try {
JSONObject o = new JSONObject(js.toString());
return o.toString(3);
} catch (JSONException e) {
e.printStackTrace();
return "error";
}
}
}