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 }