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 MISMATCH_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 static final String NO_CONTENT_MSG = Messages
 109                                 .getString(Messages.RendererToolkit_NO_CONTENT);
 110                 private final List<IXDataRenderer> children;
 111                 private final List<Double> weights;
 112                 private final String text;
 113                 private final double totalWeight;
 114 
 115                 CompositeRenderer(List<IXDataRenderer> children, String text, List<Double> weights) {
 116                         this.children = children;
 117                         this.text = text;
 118                         this.weights = weights;
 119                         if (weights == null) {
 120                                 totalWeight = children.size();
 121                         } else {
 122                                 double sum = 0;
 123                                 for (Double w : weights) {
 124                                         sum += w;
 125                                 }
 126                                 totalWeight = sum;
 127                         }
 128                 }
 129 
 130                 @Override
 131                 public IRenderedRow render(Graphics2D context, SubdividedQuantityRange xRange, int height) {
 132                         List<IRenderedRow> result = new ArrayList<>(children.size());
 133                         AffineTransform oldTransform = context.getTransform();
 134                         int heightLeft = height;
 135                         double weightLeft = totalWeight;
 136                         for (int i = 0; i < children.size(); i++) {
 137                                 double rowWeight = weights == null ? 1 : weights.get(i);
 138                                 int rowHeight = (int) Math.round(heightLeft / weightLeft * rowWeight);
 139                                 weightLeft -= rowWeight;
 140                                 if (rowHeight > 0) {
 141                                         heightLeft -= rowHeight;
 142                                         result.add(children.get(i).render(context, xRange, rowHeight));
 143                                         context.translate(0, rowHeight);
 144                                 }
 145                         }
 146                         context.setTransform(oldTransform);
 147                         if (result.size() != children.size()) {
 148                                 String displayMessage = result.size() == 0 ? NO_CONTENT_MSG : TOO_MUCH_CONTENT_MSG;
 149                                 result = Collections.emptyList();
 150                                 context.setPaint(MISMATCH_CONTENT_BG);
 151                                 context.fillRect(0, 0, xRange.getPixelExtent(), height);
 152                                 // FIXME: Draw something nice.
 153                                 Font orgFont = context.getFont();
 154                                 Font italicFont = orgFont.deriveFont(Font.ITALIC);
 155                                 FontMetrics fm = context.getFontMetrics(italicFont);
 156                                 int msgWidth = fm.stringWidth(displayMessage);
 157                                 if (height > fm.getHeight() && xRange.getPixelExtent() > msgWidth) {
 158                                         context.setFont(italicFont);
 159                                         context.setPaint(Color.BLACK);
 160                                         context.drawString(displayMessage, (xRange.getPixelExtent() - msgWidth) / 2,
 161                                                         (height - fm.getHeight()) / 2 + fm.getAscent());
 162                                         context.setFont(orgFont);
 163                                 }
 164                         }
 165 
 166                         return new RenderedRowBase(result, height, text, null, null) {
 167 
 168                                 @Override
 169                                 public void infoAt(IChartInfoVisitor visitor, int x, int y, Point offset) {
 170                                         boolean notifyLeave = false;
 171                                         if (text != null) {
 172                                                 // FIXME: Use stored state for fullyShown?
 173                                                 notifyLeave = visitor.enterScope(text, (getHeight() > 20) && (text.length() <= 26));
 174                                         }
 175                                         int yRowStart = 0;
 176                                         for (IRenderedRow nestedRow : getNestedRows()) {
 177                                                 int yRowEnd = yRowStart + nestedRow.getHeight();
 178                                                 if (yRowStart > y) {
 179                                                         break;
 180                                                 } else if (yRowEnd > y) {
 181                                                         Point newOffset = new Point(offset.x, offset.y + yRowStart);
 182                                                         nestedRow.infoAt(visitor, x, y - yRowStart, newOffset);
 183                                                 }
 184                                                 yRowStart = yRowEnd;
 185                                         }
 186                                         if (notifyLeave) {
 187                                                 visitor.leaveScope();
 188                                         }
 189                                 }
 190                         };
 191                 }
 192         }
 193 
 194         public static IXDataRenderer layers(IXDataRenderer ... layers) {
 195                 return layers(Arrays.asList(layers));
 196         }
 197 
 198         public static IXDataRenderer layers(List<IXDataRenderer> layers) {
 199                 return layers.isEmpty() ? empty() : new LayeredRenderer(layers);
 200         }
 201 
 202         public static IXDataRenderer uniformRows(List<IXDataRenderer> rows) {
 203                 return uniformRows(rows, null);
 204         }
 205 
 206         public static IXDataRenderer uniformRows(List<IXDataRenderer> rows, String text) {
 207                 return weightedRows(rows, text, null);
 208         }
 209 
 210         public static IXDataRenderer weightedRows(List<IXDataRenderer> rows, List<Double> weights) {
 211                 return weightedRows(rows, null, weights);
 212         }
 213 
 214         public static IXDataRenderer weightedRows(List<IXDataRenderer> rows, String text, List<Double> weights) {
 215                 return rows.isEmpty() ? empty() : new CompositeRenderer(rows, text, weights);
 216         }
 217 
 218         public static IXDataRenderer empty() {
 219                 return EMPTY;
 220         }
 221 
 222 }