package nl.tudelft.lifetiles.tree.view; import javafx.geometry.Bounds; import javafx.geometry.Point2D; import javafx.scene.control.Control; import javafx.scene.input.MouseButton; import nl.tudelft.lifetiles.tree.controller.TreeController; import nl.tudelft.lifetiles.tree.model.PhylogeneticTreeItem; /** * A View to display a tree. * The tree will be displayed in a circle. * * @author Albert Smit * */ public class SunburstView extends Control { /** * The root of the tree this view will show. */ private PhylogeneticTreeItem rootItem; /** * the current node we use as the center of the view. */ private PhylogeneticTreeItem currentItem; /** * The center coordinates of this view. */ private Point2D centerPoint; /** * the {@link TreeController} controlling this SunburstView. */ private TreeController controller; /** * the scaling factor to calculate coordinates, starts at 1. */ private double scale = 1d; /** * The bounds for this view, used to scale content to fit. */ private Bounds layoutBounds; /** * Creates a new SunburstView. */ public SunburstView() { super(); centerPoint = new Point2D(getWidth() / 2d, getHeight() / 2d); } /** * changes the currently selected node. * * @param selected * the new selected node. */ public void selectNode(final PhylogeneticTreeItem selected) { if (selected != null) { currentItem = selected; scale = calculateScale(); update(); } } /** * Changes the displayed tree. * * @param root * the new root */ public void setRoot(final PhylogeneticTreeItem root) { rootItem = root; selectNode(rootItem); } /** * @param controller * the {@link TreeController} controlling this tree */ public void setController(final TreeController controller) { this.controller = controller; } /** * stores a reference to this nodes' parents bounds * because the nodes' own bounds are not accurate. * * @param bounds * The bounds of the parent node */ public void setBounds(final Bounds bounds) { layoutBounds = bounds; if (rootItem != null) { scale = calculateScale(); update(); } } /** * updates the view by redrawing all elements. */ private void update() { // remove the old elements getChildren().clear(); centerPoint = new Point2D(getWidth() / 2d, getHeight() / 2d); // add a center unit SunburstCenter center = new SunburstCenter(currentItem, scale); center.setOnMouseClicked(mouseEvent -> { if (mouseEvent.getButton() == MouseButton.PRIMARY) { selectNode(currentItem.getParent()); controller.shoutVisible(currentItem.getChildSequences()); } }); getChildren().add(center); // add the ring units double totalDescendants = currentItem.numberDescendants(); double degreeStart = 0d; for (PhylogeneticTreeItem child : currentItem.getChildren()) { double sectorSize = (child.numberDescendants() + 1) / totalDescendants; double degreeEnd = degreeStart + (AbstractSunburstNode.CIRCLEDEGREES * sectorSize); DegreeRange angle = new DegreeRange(degreeStart, degreeEnd); drawRingRecursive(child, 0, angle); degreeStart = degreeEnd; } } /** * draws all ringUnits. * * @param node * the {@link PhylogeneticTreeItem} that this * SunburstRingSegment will represent. * @param layer * the layer on which this SunburstRingSegment is located * @param angle * the start and end point of this ring, in degrees */ private void drawRingRecursive(final PhylogeneticTreeItem node, final int layer, final DegreeRange angle) { // generate ring SunburstRingSegment ringUnit = new SunburstRingSegment(node, layer, angle, centerPoint, scale); ringUnit.setOnMouseClicked(mouseEvent -> { if (mouseEvent.getButton() == MouseButton.PRIMARY) { selectNode(node); controller.shoutVisible(currentItem.getChildSequences()); } }); getChildren().add(ringUnit); double totalDescendants = node.numberDescendants(); double start = angle.getStartAngle(); double sectorAngle = angle.angle(); // generate rings for child nodes for (PhylogeneticTreeItem child : node.getChildren()) { double sectorSize = (child.numberDescendants() + 1) / totalDescendants; double end = start + (sectorAngle * sectorSize); DegreeRange childAngle = new DegreeRange(start, end); drawRingRecursive(child, layer + 1, childAngle); start = end; } } /** * Calculate the scaling factor needed for rendering the full tree * in the available space. * * @return a double between 0 and 1 */ private double calculateScale() { int depth = currentItem.maxDepth(); double minSize = Math.min(layoutBounds.getWidth(), layoutBounds.getHeight()); double maxRadius = AbstractSunburstNode.CENTER_RADIUS; maxRadius += depth * AbstractSunburstNode.RING_WIDTH; return Math.min(1, minSize / (maxRadius * 2)); } }