1 /* 2 * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. 3 * 4 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 5 * 6 * The contents of this file are subject to the terms of either the Universal Permissive License 7 * v 1.0 as shown at http://oss.oracle.com/licenses/upl 8 * 9 * or the following license: 10 * 11 * Redistribution and use in source and binary forms, with or without modification, are permitted 12 * provided that the following conditions are met: 13 * 14 * 1. Redistributions of source code must retain the above copyright notice, this list of conditions 15 * and the following disclaimer. 16 * 17 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of 18 * conditions and the following disclaimer in the documentation and/or other materials provided with 19 * the distribution. 20 * 21 * 3. Neither the name of the copyright holder nor the names of its contributors may be used to 22 * endorse or promote products derived from this software without specific prior written permission. 23 * 24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 25 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 26 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 27 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 28 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 30 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY 31 * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 */ 33 package org.openjdk.jmc.ui.charts; 34 35 import java.awt.Color; 36 import java.awt.Font; 37 import java.awt.FontMetrics; 38 import java.awt.Graphics2D; 39 import java.awt.Point; 40 import java.awt.geom.AffineTransform; 41 import java.util.ArrayList; 42 import java.util.Arrays; 43 import java.util.Collections; 44 import java.util.List; 45 46 public class RendererToolkit { 47 48 private static final IXDataRenderer EMPTY = new IXDataRenderer() { 49 50 @Override 51 public IRenderedRow render(Graphics2D context, SubdividedQuantityRange xRange, int height) { 52 return new RenderedRowBase(height); 53 54 } 55 56 }; 57 58 private static class LayeredRenderer implements IXDataRenderer { 59 private final List<IXDataRenderer> layers; 60 61 LayeredRenderer(List<IXDataRenderer> layers) { 62 this.layers = layers; 63 } 64 65 @Override 66 public IRenderedRow render(Graphics2D context, SubdividedQuantityRange xRange, int height) { 67 final List<IRenderedRow> results = new ArrayList<>(layers.size()); 68 String text = null; 69 String description = null; 70 Object payload = null; 71 List<IRenderedRow> subdivision = Collections.emptyList(); 72 for (IXDataRenderer layer : layers) { 73 IRenderedRow result = layer.render(context, xRange, height); 74 results.add(result); 75 // Beware that this picks the last text, payload and subdivision from its layers. 76 // This could be confusing if more than one layer has those things. 77 if (result.getName() != null && !result.getName().isEmpty()) { 78 text = result.getName(); 79 } 80 if (result.getDescription() != null && !result.getDescription().isEmpty()) { 81 description = result.getDescription(); 82 } 83 if (result.getPayload() != null) { 84 payload = result.getPayload(); 85 } 86 if (!result.getNestedRows().isEmpty()) { 87 subdivision = result.getNestedRows(); 88 } 89 } 90 return new RenderedRowBase(subdivision, height, text, description, payload) { 91 92 @Override 93 public void infoAt(IChartInfoVisitor visitor, int x, int y, Point offset) { 94 for (IRenderedRow rr : results) { 95 rr.infoAt(visitor, x, y, offset); 96 } 97 } 98 }; 99 } 100 101 } 102 103 private static class CompositeRenderer implements IXDataRenderer { 104 105 private static final Color TOO_MUCH_CONTENT_BG = new Color(240, 240, 240, 190); 106 private static final String TOO_MUCH_CONTENT_MSG = Messages 107 .getString(Messages.RendererToolkit_TOO_MUCH_CONTENT); 108 private final List<IXDataRenderer> children; 109 private final List<Double> weights; 110 private final String text; 111 private final double totalWeight; 112 113 CompositeRenderer(List<IXDataRenderer> children, String text, List<Double> weights) { 114 this.children = children; 115 this.text = text; 116 this.weights = weights; 117 if (weights == null) { 118 totalWeight = children.size(); 119 } else { 120 double sum = 0; 121 for (Double w : weights) { 122 sum += w; 123 } 124 totalWeight = sum; 125 } 126 } 127 128 @Override 129 public IRenderedRow render(Graphics2D context, SubdividedQuantityRange xRange, int height) { 130 List<IRenderedRow> result = new ArrayList<>(children.size()); 131 AffineTransform oldTransform = context.getTransform(); 132 int heightLeft = height; 133 double weightLeft = totalWeight; 134 for (int i = 0; i < children.size(); i++) { 135 double rowWeight = weights == null ? 1 : weights.get(i); 136 int rowHeight = (int) Math.round(heightLeft / weightLeft * rowWeight); 137 weightLeft -= rowWeight; 138 if (rowHeight > 0) { 139 heightLeft -= rowHeight; 140 result.add(children.get(i).render(context, xRange, rowHeight)); 141 context.translate(0, rowHeight); 142 } 143 } 144 context.setTransform(oldTransform); 145 if (result.size() != children.size()) { 146 result = Collections.emptyList(); 147 context.setPaint(TOO_MUCH_CONTENT_BG); 148 context.fillRect(0, 0, xRange.getPixelExtent(), height); 149 // FIXME: Draw something nice. 150 Font orgFont = context.getFont(); 151 Font italicFont = orgFont.deriveFont(Font.ITALIC); 152 FontMetrics fm = context.getFontMetrics(italicFont); 153 int msgWidth = fm.stringWidth(TOO_MUCH_CONTENT_MSG); 154 if (height > fm.getHeight() && xRange.getPixelExtent() > msgWidth) { 155 context.setFont(italicFont); 156 context.setPaint(Color.BLACK); 157 context.drawString(TOO_MUCH_CONTENT_MSG, (xRange.getPixelExtent() - msgWidth) / 2, 158 (height - fm.getHeight()) / 2 + fm.getAscent()); 159 context.setFont(orgFont); 160 } 161 } 162 163 return new RenderedRowBase(result, height, text, null, null) { 164 165 @Override 166 public void infoAt(IChartInfoVisitor visitor, int x, int y, Point offset) { 167 boolean notifyLeave = false; 168 if (text != null) { 169 // FIXME: Use stored state for fullyShown? 170 notifyLeave = visitor.enterScope(text, (getHeight() > 20) && (text.length() <= 26)); 171 } 172 int yRowStart = 0; 173 for (IRenderedRow nestedRow : getNestedRows()) { 174 int yRowEnd = yRowStart + nestedRow.getHeight(); 175 if (yRowStart > y) { 176 break; 177 } else if (yRowEnd > y) { 178 Point newOffset = new Point(offset.x, offset.y + yRowStart); 179 nestedRow.infoAt(visitor, x, y - yRowStart, newOffset); 180 } 181 yRowStart = yRowEnd; 182 } 183 if (notifyLeave) { 184 visitor.leaveScope(); 185 } 186 } 187 }; 188 } 189 } 190 191 public static IXDataRenderer layers(IXDataRenderer ... layers) { 192 return layers(Arrays.asList(layers)); 193 } 194 195 public static IXDataRenderer layers(List<IXDataRenderer> layers) { 196 return layers.isEmpty() ? empty() : new LayeredRenderer(layers); 197 } 198 199 public static IXDataRenderer uniformRows(List<IXDataRenderer> rows) { 200 return uniformRows(rows, null); 201 } 202 203 public static IXDataRenderer uniformRows(List<IXDataRenderer> rows, String text) { 204 return weightedRows(rows, text, null); 205 } 206 207 public static IXDataRenderer weightedRows(List<IXDataRenderer> rows, List<Double> weights) { 208 return weightedRows(rows, null, weights); 209 } 210 211 public static IXDataRenderer weightedRows(List<IXDataRenderer> rows, String text, List<Double> weights) { 212 return rows.isEmpty() ? empty() : new CompositeRenderer(rows, text, weights); 213 } 214 215 public static IXDataRenderer empty() { 216 return EMPTY; 217 } 218 219 }