/* * Copyright 2014-2016 Groupon, Inc * Copyright 2014-2016 The Billing Project, LLC * * The Billing Project licenses this file to you 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. */ package org.killbill.billing.invoice.tree; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicInteger; public class TreePrinter { public static String print(final ItemsNodeInterval root) { return print(buildCoordinates(root)); } private static String print(final SortedMap<XY, ItemsNodeInterval> tree) { // Make left most node start at X=0 translate(tree); final AtomicInteger totalOrdering = new AtomicInteger(64); final Map<String, ItemsNodeInterval> legend = new LinkedHashMap<String, ItemsNodeInterval>(); final List<StringBuilder> builders = new LinkedList<StringBuilder>(); for (int level = 0; level >= maxOffset(tree).Y; level--) { builders.add(new StringBuilder()); // Draw edges for that level drawLevel(true, level, tree, builders, totalOrdering, legend); // Draw nodes for that level builders.add(new StringBuilder()); drawLevel(false, level, tree, builders, totalOrdering, legend); } final StringBuilder builder = new StringBuilder(); for (final StringBuilder levelBuilder : builders) { builder.append(levelBuilder.toString()); } builder.append("\n"); for (final String key : legend.keySet()) { builder.append(key).append(": "); appendNodeDetails(legend.get(key), builder); builder.append("\n"); } return builder.toString(); } private static void drawLevel(final boolean drawEdges, final int level, final SortedMap<XY, ItemsNodeInterval> tree, final List<StringBuilder> builders, final AtomicInteger totalOrdering, final Map<String, ItemsNodeInterval> legend) { if (drawEdges && level == 0) { // Nothing to do for root return; } final StringBuilder builder = builders.get(builders.size() - 1); int posX = 0; boolean sibling; for (final XY levelXY : tree.keySet()) { if (levelXY.Y > level) { // Sorted - we haven't reached that level yet continue; } else if (levelXY.Y < level) { // Sorted - we are done for that level break; } sibling = levelXY.parent == null; while (posX < levelXY.X) { if (drawEdges || !sibling || level == 0) { builder.append(" "); } else { // Link siblings builder.append("-"); } posX++; } if (drawEdges) { if (sibling) { builder.append(" "); } else { builder.append("/"); } } else { if (sibling && level != 0) { builder.append(""); } // Print this node String node = Character.toString((char) totalOrdering.incrementAndGet()); if (sibling && level != 0) { node = node.toLowerCase(); } legend.put(node, tree.get(levelXY)); builder.append(node); } posX++; } builder.append("\n"); } private static void appendNodeDetails(final ItemsNodeInterval interval, final StringBuilder builder) { builder.append("[") .append(interval.getStart()) .append(",") .append(interval.getEnd()) .append("]"); if (interval.getItems().isEmpty()) { return; } builder.append("("); final List<Item> items = interval.getItems(); for (int i = 0; i < items.size(); i++) { final Item item = items.get(i); if (i > 0) { builder.append(","); } builder.append(item.getAction().name().charAt(0)); } builder.append(")"); } public static SortedMap<XY, ItemsNodeInterval> buildCoordinates(final ItemsNodeInterval root) { final XY reference = new XY(0, 0); final SortedMap<XY, ItemsNodeInterval> result = new TreeMap<XY, ItemsNodeInterval>(); result.put(reference, root); result.putAll(buildCoordinates(root, reference)); return result; } public static Map<XY, ItemsNodeInterval> buildCoordinates(final ItemsNodeInterval root, final XY initialCoords) { final Map<XY, ItemsNodeInterval> result = new HashMap<XY, ItemsNodeInterval>(); if (root == null) { return result; } // Compute the coordinate of the left most child ItemsNodeInterval curChild = (ItemsNodeInterval) root.getLeftChild(); if (curChild == null) { return result; } XY curXY = leftChildXY(initialCoords); result.put(curXY, curChild); // Compute the coordinates of the tree below that child result.putAll(buildCoordinates(curChild, curXY)); curChild = (ItemsNodeInterval) curChild.getRightSibling(); while (curChild != null) { curXY = rightSiblingXY(curXY); // Compute the coordinates of the tree below that child final Map<XY, ItemsNodeInterval> subtree = buildCoordinates(curChild, curXY); final XY offset = translate(subtree); translate(offset, curXY); result.put(curXY, curChild); result.putAll(subtree); curChild = (ItemsNodeInterval) curChild.getRightSibling(); } return result; } private static XY translate(final Map<XY, ItemsNodeInterval> subtree) { final XY offset = maxOffset(subtree); for (final XY xy : subtree.keySet()) { translate(offset, xy); } return offset; } private static void translate(final XY offset, final XY xy) { xy.X = xy.X - offset.X; } private static XY maxOffset(final Map<XY, ItemsNodeInterval> tree) { final XY res = new XY(0, 0); for (final XY xy : tree.keySet()) { if (xy.X < res.X) { res.X = xy.X; } if (xy.Y < res.Y) { res.Y = xy.Y; } } return res; } private static XY leftChildXY(final XY parent) { return new XY(parent.X - 1, parent.Y - 1, parent); } private static XY rightSiblingXY(final XY leftSibling) { return new XY(leftSibling.X + 1, leftSibling.Y); } static class XY implements Comparable<XY> { int X; int Y; XY parent; public XY(final int x, final int y) { this(x, y, null); } public XY(final int x, final int y, final XY parent) { X = x; Y = y; this.parent = parent; } @Override public String toString() { final StringBuilder sb = new StringBuilder("("); sb.append(X); sb.append(",").append(Y); sb.append(')'); return sb.toString(); } @Override public boolean equals(final Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } final XY xy = (XY) o; if (X != xy.X) { return false; } return Y == xy.Y; } @Override public int hashCode() { int result = X; result = 31 * result + Y; return result; } @Override public int compareTo(final XY o) { return Y == o.Y ? X < o.X ? -1 : X == o.X ? 0 : 1 : Y < o.Y ? 1 : -1; } } }