Mercurial > hg > release > thermostat-1.6
view client/swing/src/main/java/com/redhat/thermostat/client/swing/components/experimental/SquarifiedTreeMap.java @ 2049:a92d602216ad
Update copyright license headers for 2017
PR3290
Reviewed-by: jerboaa
Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2017-January/021974.html
author | Andrew Azores <aazores@redhat.com> |
---|---|
date | Tue, 17 Jan 2017 12:19:56 -0500 |
parents | ec503206ccf5 |
children |
line wrap: on
line source
/* * Copyright 2012-2017 Red Hat, Inc. * * This file is part of Thermostat. * * Thermostat is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published * by the Free Software Foundation; either version 2, or (at your * option) any later version. * * Thermostat is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Thermostat; see the file COPYING. If not see * <http://www.gnu.org/licenses/>. * * Linking this code with other modules is making a combined work * based on this code. Thus, the terms and conditions of the GNU * General Public License cover the whole combination. * * As a special exception, the copyright holders of this code give * you permission to link this code with independent modules to * produce an executable, regardless of the license terms of these * independent modules, and to copy and distribute the resulting * executable under terms of your choice, provided that you also * meet, for each linked independent module, the terms and conditions * of the license of that module. An independent module is a module * which is not derived from or based on this code. If you modify * this code, you may extend this exception to your version of the * library, but you are not obligated to do so. If you do not wish * to do so, delete this exception statement from your version. */ package com.redhat.thermostat.client.swing.components.experimental; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Objects; /** * This class implements the Squarified algorithm for TreeMaps. Using it, it is * possible to associate a rectangle to a {@link TreeMapNode} element and its * children. * <p> * @see TreeMapNode * @see TreMapBuilder */ public class SquarifiedTreeMap { /* * The algorithm this implements is described in detail here: * * https://bitbucket.org/Ammirate/thermostat-treemap/src/tip/TreeMap%20documentation.pdf * * Which is an improvement on: * * Mark Bruls, Kees Huizing and Jarke J. van Wijk, "Squarified * Treemaps" in Data Visualization 2000: Proceedings of the Joint * EUROGRAPHICS and IEEE TCVG Symposium on Visualization in Amsterdam, * The Netherlands, May 29–30, 2000. Berlin, Germany: Springer Science * & Business Media, 2012 * * The paper itself is also available online at: * https://www.win.tue.nl/~vanwijk/stm.pdf */ /** * List of node to represent as TreeMap. */ private LinkedList<TreeMapNode> elements; /** * Represent the area in which draw nodes. */ private Rectangle2D.Double container; private enum DIRECTION { LEFT_RIGHT, TOP_BOTTOM } /** * Indicates the drawing direction. */ private DIRECTION drawingDir; /** * The rectangles area available for drawing. */ private Rectangle2D.Double availableArea; /** * List of the calculated rectangles. */ private List<TreeMapNode> squarifiedNodes; /** * List of the current rectangles under processing. */ private List<TreeMapNode> currentRow; /** * Coordinates on which to draw. */ private double lastX = 0; private double lastY = 0; /** * Constructor. * * @param d the dimension of the total area in which draw elements. * @param list the list of elements to draw as TreeMap. * * @throws a NullPointerException if one of the arguments is null. */ public SquarifiedTreeMap(Rectangle2D.Double bounds, List<TreeMapNode> list) { this.elements = new LinkedList<>(); elements.addAll(Objects.requireNonNull(list)); this.container = Objects.requireNonNull(bounds); } /** * Invoke this method to calculate the rectangles for the TreeMap. * * @return a list of node having a rectangle built in percentage to the * available area. */ public List<TreeMapNode> squarify() { initializeArea(); prepareData(elements); List<TreeMapNode> row = new ArrayList<>(); double w = getPrincipalSide(); squarify(elements, row, w); return getSquarifiedNodes(); } /** * Calculate recursively the rectangles to draw and their size. * * @param nodes the list of elements to draw. * @param row the list of current rectangles to process. * @param w the side against which to calculate the rectangles. */ private void squarify(LinkedList<TreeMapNode> nodes, List<TreeMapNode> row, double w) { if (nodes.isEmpty() && row.isEmpty()) { // work done return; } if (nodes.isEmpty()) { // no more element to process, just draw current row finalizeRow(row); return; } if (row.isEmpty()) { // add the first element to the row and iterate the process over it row.add(nodes.getFirst()); nodes.removeFirst(); squarify(nodes, row, w); return; } /* Greedy step: calculate the best aspect ratio of actual row and the * best aspect ratio given by adding another rectangle to the row. * If the current row can not be improved then finalize it * else add the next element, to improve the global aspect ratio */ List<TreeMapNode> expandedRow = new ArrayList<TreeMapNode>(row); expandedRow.add(nodes.getFirst()); double actualAspectRatio = bestAspectRatio(row, w); double expandedAspectRatio = bestAspectRatio(expandedRow, w); if (!willImprove(actualAspectRatio, expandedAspectRatio)) { finalizeRow(row); squarify(nodes, new ArrayList<TreeMapNode>(), getPrincipalSide()); } else { nodes.removeFirst(); squarify(nodes, expandedRow, w); } } /** * Return the rectangles list. * @return a list of rectangles. */ public List<TreeMapNode> getSquarifiedNodes() { return squarifiedNodes; } /** * Initialize the available area used to create the tree map */ private void initializeArea() { availableArea = new Rectangle2D.Double(container.getX(), container.getY(), container.getWidth(), container.getHeight()); lastX = 0; lastY = 0; squarifiedNodes = new ArrayList<>(); currentRow = new ArrayList<>(); updateDirection(); } /** * Recalculate the drawing direction. */ private void updateDirection() { drawingDir = availableArea.getWidth() > availableArea.getHeight() ? DIRECTION.TOP_BOTTOM : DIRECTION.LEFT_RIGHT; } /** * Invert the drawing direction. */ private void invertDirection() { drawingDir = drawingDir == DIRECTION.LEFT_RIGHT ? DIRECTION.TOP_BOTTOM : DIRECTION.LEFT_RIGHT; } /** * Keep the current list of nodes which produced the best aspect ratio * in the available area, draw their respective rectangles and reinitialize * the current row to draw. * <p> * @param nodes the list of numbers which represent the rectangles' area. * @return the number of Rectangles created. */ private void finalizeRow(List<TreeMapNode> nodes) { if (nodes == null || nodes.isEmpty()) { return; } // get the total weight of nodes in order to calculate their percentages double sum = getSum(nodes); // greedy optimization step: get the best aspect ratio for nodes drawn // on the longer and on the smaller side, to evaluate the best. double actualAR = bestAspectRatio(nodes, getPrincipalSide()); double alternativeAR = bestAspectRatio(nodes, getSecondarySide()); if (willImprove(actualAR, alternativeAR)) { invertDirection(); } for (TreeMapNode node: nodes) { // assign a rectangle calculated as percentage of the total weight Rectangle2D.Double r = createRectangle(sum, node.getWeight()); node.setRectangle(r); // recalculate coordinates to draw next rectangle updateXY(r); // add the node to the current list of rectangles in processing currentRow.add(node); } // recalculate the area in which new rectangles will be drawn and // reinitialize the current list of node to represent. reduceAvailableArea(); newRow(); } /** * Create a rectangle having area = @param area in percentage of @param sum. * <p> * For example: assume @param area = 4 and @param sum = 12 and the * drawing direction is top to bottom. <br> * <p> * __ __ __ __ * | | | * |__ __| | * |__ __ __ __| * * <br>the internal rectangle will be calculated as follow:<br> * {@code height = (4 / 9) * 3} <--note that the principal side for actual * drawing direction is 3. * <br>Now it is possible to calculate the width:<br> * {@code width = 4 / 1.3} <-- note this is the height value * * <p> * @param sum the total size of all rectangles in the actual row. * @param area this Rectangle's area. * @return the Rectangle which correctly fill the available area. */ private Rectangle2D.Double createRectangle(Double sum, Double area) { double side = getPrincipalSide(); double w = 0; double h = 0; //don't want division by 0 if (validate(area) == 0 || validate(sum) == 0 || validate(side) == 0) { return new Rectangle2D.Double(lastX, lastY, 0, 0); } // calculate the rectangle's principal side relatively to the container // rectangle's principal side. if (drawingDir == DIRECTION.TOP_BOTTOM) { h = (area / sum) * side; w = area / h; } else { w = (area / sum) * side; h = area / w; } return new Rectangle2D.Double(lastX, lastY, w, h); } /** * Check if a double value is defined as Not a Number and sets it to 0. * @param d the value to check. * @return the checked value: 0 if the given number is NaN, else the number * itself. */ private double validate(double d) { if (d == Double.NaN) { d = 0; } return d; } /** * Check in which direction the rectangles have to be drawn. * @return the side on which rectangles will be created. */ private double getPrincipalSide() { return drawingDir == DIRECTION.LEFT_RIGHT ? availableArea.getWidth() : availableArea.getHeight(); } /** * * @return the secondary available area's side. */ private double getSecondarySide() { return drawingDir == DIRECTION.LEFT_RIGHT ? availableArea.getHeight() : availableArea.getWidth(); } /** * Sum the elements in the list. * @param nodes the list which contains elements to sum. * @return the sum of the elements. */ private double getSum(List<TreeMapNode> nodes) { double sum = 0; for (TreeMapNode n : nodes) { sum += n.getWeight(); } return sum; } /** * Recalculate the origin to draw next rectangles. * @param r the rectangle from which recalculate the origin. */ private void updateXY(Rectangle2D.Double r) { if (drawingDir == DIRECTION.LEFT_RIGHT) { //lastY doesn't change lastX += r.width; } else { //lastX doesn't change lastY += r.height; } } /** * Initialize the origin at the rectangle's origin. * @param r the rectangle used as origin source. */ private void initializeXY(Rectangle2D.Double r) { lastX = r.x; lastY = r.y; } /** * Reduce the size of the available rectangle. Use it after the current * row's closure. */ private void reduceAvailableArea() { if (drawingDir == DIRECTION.LEFT_RIGHT) { // all rectangles inside the row have the same height availableArea.height -= currentRow.get(0).getRectangle().height; availableArea.y = lastY + currentRow.get(0).getRectangle().height; availableArea.x = currentRow.get(0).getRectangle().x; } else { // all rectangles inside the row have the same width availableArea.width -= currentRow.get(0).getRectangle().width; availableArea.x = lastX + currentRow.get(0).getRectangle().width; availableArea.y = currentRow.get(0).getRectangle().y; } updateDirection(); initializeXY(availableArea); } /** * Close the current row and initialize a new one. */ private void newRow() { squarifiedNodes.addAll(currentRow); currentRow = new ArrayList<>(); } /** * Calculate the aspect ratio for all the rectangles in the list and * return the max of them. * @param row the list of rectangles. * @param side the side against which to calculate the the aspect ratio. * @return the max aspect ratio calculated for the row. */ private double bestAspectRatio(List<TreeMapNode> row, double side) { if (row == null || row.isEmpty()) { return Double.MAX_VALUE; } double sum = getSum(row); double max = 0; // calculate the aspect ratio against the main side, and also its inverse. // this is because aspect ratio of rectangle 6x4 can be calculated as // 6/4 but also 4/6. Here the aspect ratio has been calculated as // indicated in the Squarified algorithm. for (TreeMapNode node : row) { double m1 = (Math.pow(side, 2) * node.getWeight()) / Math.pow(sum, 2); double m2 = Math.pow(sum, 2) / (Math.pow(side, 2) * node.getWeight()); double m = Math.max(m1, m2); if (m > max) { max = m; } } return max; } /** * Prepare the elements in the list, sorting them and transforming them * proportionally the given dimension. * @param dim the dimension in which rectangles will be drawn. * @param elements the list of elements to draw. * @return the list sorted and proportioned to the dimension. */ private void prepareData(List<TreeMapNode> elements) { if (elements == null || elements.isEmpty()) { return; } TreeMapNode.sort(elements); double totArea = availableArea.width * availableArea.height; double sum = getSum(elements); // recalculate weights in percentage of their sum for (TreeMapNode node : elements) { double w = (node.getWeight()/sum) * totArea; node.setWeight(w); } } /** * This method check which from the values in input, that represent * rectangles' aspect ratio, produces more approximatively a square. * It checks if one of the aspect ratio values gives a value nearest to 1 * against the other, which means that width and height are similar. * @param actualAR the actual aspect ratio * @param expandedAR the aspect ratio to evaluate * @return false if the actual aspect ratio is better than the new one, * else true. */ private boolean willImprove(double actualAR, double expandedAR) { if (actualAR == 0) { return true; } if (expandedAR == 0) { return false; } // check which value is closer to 1, the square's aspect ratio double v1 = Math.abs(actualAR - 1); double v2 = Math.abs(expandedAR - 1); return v1 > v2; } }