Java tutorial
/* * The MIT License (MIT) * * Copyright (c) 2018 PikeTec GmbH * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and * associated documentation files (the "Software"), to deal in the Software without restriction, * including without limitation the rights to use, copy, modify, merge, publish, distribute, * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or * substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package com.piketec.jenkins.plugins.tpt.publisher; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Polygon; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.font.FontRenderContext; import java.awt.geom.Arc2D; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import javax.annotation.Nullable; import javax.swing.ImageIcon; import org.apache.commons.lang.ArrayUtils; import jenkins.model.Jenkins; /** * TPT pie chart. * * @author FInfantino, PikeTec GmbH * */ class PieChart { private static final Color BRIGHT_GRAY = new Color(240, 242, 240); private static final Font NORMALFONT = new Font("Dialog", Font.PLAIN, 12); private static void checkLegendSegmentOrder(List<Segment> segments, int[] legendSegmentOrder) { if (legendSegmentOrder == null) { return; } assert legendSegmentOrder.length == segments.size(); for (int i = 0; i < legendSegmentOrder.length; ++i) { assert ArrayUtils.contains(legendSegmentOrder, i); } } final int centerX = 296; final int centerY = 287; final int radius = 263; final int totalHeight = 616; final int totalWidth = 1232; double zoom = 1; private final List<Segment> segments; private final boolean showTotalInLegend; private final double total; private final boolean withSubSegments; private final String subTotalTextOrNull; private final double subTotal; private final DecimalFormat legendPortionFormat; private final int[] legendSegmentOrder; private final String totalText = "in total"; private ImageIcon pieShadow; private ImageIcon keyShadow; public PieChart(List<Segment> segments, int fractionalDigits, boolean showTotalInLegend) throws MalformedURLException, IOException { this(segments, null, fractionalDigits, showTotalInLegend, false, null); Jenkins jenkinsInstance = Jenkins.getInstance(); if (jenkinsInstance == null) { throw new IOException("No Jenkins instance found."); } pieShadow = new ImageIcon(new File(jenkinsInstance.getRootDir(), File.separator + "plugins" + File.separator + "piketec-tpt" + File.separator + "PieChart" + File.separator + "Shadow.png").toURI().toURL()); keyShadow = new ImageIcon(new File(jenkinsInstance.getRootDir(), File.separator + "plugins" + File.separator + "piketec-tpt" + File.separator + "PieChart" + File.separator + "Shadow2.png").toURI().toURL()); } public PieChart(List<Segment> segments, @Nullable int[] legendSegmentOrder, int fractionalDigits, boolean showTotalInLegend, boolean withSubSegments, String subTotalTextOrNull) { checkLegendSegmentOrder(segments, legendSegmentOrder); // Pruefung der Konsistenz zwischen // segments and legendSegmentOrder this.segments = segments; this.showTotalInLegend = showTotalInLegend; this.legendSegmentOrder = legendSegmentOrder != null ? legendSegmentOrder.clone() : null; this.withSubSegments = withSubSegments; this.subTotalTextOrNull = subTotalTextOrNull; double sum = 0; double subSum = 0; if (showTotalInLegend) { for (Segment seg : segments) { sum += seg.getPortion(); subSum += seg.getSubPortion(); } } this.total = sum; this.subTotal = subSum; StringBuilder pattern = new StringBuilder("#,##0"); if (fractionalDigits > 0) { pattern.append('.'); for (int i = 0; i < fractionalDigits; ++i) { pattern.append('0'); } } legendPortionFormat = new DecimalFormat(pattern.toString()); } /** * Render the pie chart with the given height * * @param height * The height of the resulting image * @return The pie chart rendered as an image */ public BufferedImage render(int height) { BufferedImage image = new BufferedImage(totalWidth, totalHeight, BufferedImage.TYPE_INT_RGB); Graphics2D g2 = image.createGraphics(); g2.scale(zoom, zoom); // fill background to white g2.setColor(Color.WHITE); g2.fill(new Rectangle(totalWidth, totalHeight)); // prepare render hints g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2.setStroke(new BasicStroke(4, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); // draw shadow image g2.drawImage(pieShadow.getImage(), 0, 0, pieShadow.getImageObserver()); double start = 0; List<Arc2D> pies = new ArrayList<>(); // pie segmente erzeugen und fuellen if (total == 0) { g2.setColor(BRIGHT_GRAY); g2.fillOval(centerX - radius, centerY - radius, 2 * radius, 2 * radius); g2.setColor(Color.WHITE); g2.drawOval(centerX - radius, centerY - radius, 2 * radius, 2 * radius); } else { for (Segment s : segments) { double portionDegrees = s.getPortion() / total; Arc2D pie = paintPieSegment(g2, start, portionDegrees, s.getColor()); if (withSubSegments) { double smallRadius = radius * s.getSubSegmentRatio(); paintPieSegment(g2, start, portionDegrees, smallRadius, s.getColor().darker()); } start += portionDegrees; // portion degree jetzt noch als String (z.B. "17.3%" oder "20%" zusammenbauen) String p = String.format(Locale.ENGLISH, "%.1f", Math.rint(portionDegrees * 1000) / 10.0); p = removeSuffix(p, ".0"); // evtl. ".0" bei z.B. "25.0" abschneiden (-> "25") s.setPercent(p + "%"); pies.add(pie); } // weissen Rahmen um die pie segmente zeichen g2.setColor(Color.WHITE); for (Arc2D pie : pies) { g2.draw(pie); } } // Legende zeichnen renderLegend(g2); // "xx%" Label direkt auf die pie segmente zeichen g2.setColor(Color.WHITE); float fontSize = 32f; g2.setFont(NORMALFONT.deriveFont(fontSize).deriveFont(Font.BOLD)); start = 0; for (Segment s : segments) { if (s.getPortion() < 1E-6) { continue; // ignore segments with portions that are extremely small } double portionDegrees = s.getPortion() / total; double angle = start + portionDegrees / 2; // genau in der Mitte des Segments double xOffsetForCenteredTxt = 8 * s.getPercent().length(); // assume roughly 8px per char int x = (int) (centerX + 0.6 * radius * Math.sin(2 * Math.PI * angle) - xOffsetForCenteredTxt); int y = (int) (centerY - 0.6 * radius * Math.cos(2 * Math.PI * angle) + fontSize / 2); g2.drawString(s.getPercent(), x, y); start += portionDegrees; } return image; } private void renderLegend(Graphics2D g2) { g2.setFont(NORMALFONT.deriveFont(32f).deriveFont(Font.BOLD)); // erst die Breite der Zahlen fuer die Einrueckung berechnen Font font = g2.getFont(); FontRenderContext fontRenderContext = g2.getFontRenderContext(); Map<Segment, Double> numberWidths = new HashMap<>(); double maxNumberWidth = 0; for (Segment seg : segments) { double numberWidth = font .getStringBounds(legendPortionFormat.format(seg.getPortion()), fontRenderContext).getWidth(); numberWidths.put(seg, numberWidth); maxNumberWidth = Math.max(maxNumberWidth, numberWidth); } if (showTotalInLegend) { double numberWidth = font.getStringBounds(legendPortionFormat.format(total), fontRenderContext) .getWidth(); numberWidths.put(null, numberWidth); maxNumberWidth = Math.max(maxNumberWidth, numberWidth); } // jetzt die Zeilen in die Legende malen int verticalOffset = 0; for (int row = 0; row < segments.size(); ++row) { Segment seg = getLegendSegment(row); // Zahlen rechtsbuendig double indentation = maxNumberWidth - numberWidths.get(seg); String segmentPortionNumber = legendPortionFormat.format(seg.getPortion()); String segmentText = seg.getText(); Color segmentColor = seg.getColor(); String subSegmentText = seg.getSubSegmentText(); String subNumberText = legendPortionFormat.format(seg.subPortion); drawLegendLine(g2, verticalOffset, indentation, segmentColor, segmentText, segmentPortionNumber, seg.getPortion() != 1d, seg.getSubPortion() > 0, subSegmentText, subNumberText, seg.getSubPortion() != 1d); verticalOffset += 85; } if (showTotalInLegend) { double indentation = maxNumberWidth - numberWidths.get(null); String subNumberText = legendPortionFormat.format(subTotal); drawLegendLine(g2, verticalOffset, indentation, null, totalText, legendPortionFormat.format(total), total != 1, subTotalTextOrNull != null, subTotalTextOrNull, subNumberText, subTotal != 1); } } private Segment getLegendSegment(int row) { if (legendSegmentOrder == null) { return segments.get(row); } else { return segments.get(legendSegmentOrder[row]); } } private void drawLegendLine(Graphics2D g2, int verticalOffset, double horizontalNumberOffset, Color col, String txt, String numberText, boolean textIsPlural, boolean withSubSegment, String subSegmentText, String subNumberText, boolean subTextIsPlural) { int left = 620; // col == null --> total --> kein Rechteck if (col != null) { g2.drawImage(keyShadow.getImage(), left, 30 + verticalOffset, keyShadow.getImageObserver()); g2.setColor(col); g2.fillRect(left + 13, 37 + verticalOffset, 45, 45); if (withSubSegment) { Polygon p = new Polygon(new int[] { left + 13 + 45, left + 13 + 45, left + 13 }, new int[] { verticalOffset + 37, verticalOffset + 37 + 45, verticalOffset + 37 + 45 }, 3); g2.setColor(col.darker()); g2.fillPolygon(p); } } g2.setColor(Color.BLACK); StringBuffer sb = new StringBuffer(numberText); sb.append(" ").append(plural(textIsPlural, txt)); if (withSubSegment) { sb.append(" with "); sb.append(subNumberText); sb.append(" "); sb.append(plural(subTextIsPlural, subSegmentText)); } g2.drawString(sb.toString(), (int) (left + 80 + horizontalNumberOffset), 30 + 41 + verticalOffset); } private Arc2D paintPieSegment(Graphics2D g2, double startRatio, double endRatio, Color fillColor) { return paintPieSegment(g2, startRatio, endRatio, radius, fillColor); } private Arc2D paintPieSegment(Graphics2D g2, double startRatio, double endRatio, double radius, Color fillColor) { Arc2D pie = new Arc2D.Double(Arc2D.PIE); double startAngle = 90 - 360 * startRatio; // top (= north) double endAngle = -360 * endRatio; // mit dem Uhrzeigersinn pie.setArcByCenter(centerX, centerY, radius, startAngle, endAngle, Arc2D.PIE); g2.setColor(fillColor); g2.fill(pie); return pie; } private static final String removeSuffix(String text, String suffix) { if (text.endsWith(suffix)) { return text.substring(0, text.length() - suffix.length()); } return text; } private static final String plural(boolean plural, CharSequence text) { StringBuilder b = new StringBuilder(); int mode = 0; for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); switch (mode) { case 0: // regular mode { if (c == '{') { mode = 1; } else { b.append(c); // always append } break; } case 1: // singular mode { if (c == '|') { mode = 2; } else if (!plural) { b.append(c); // append if singular } break; } case 2: // plural mode { if (c == '}') { mode = 0; } else if (plural) { b.append(c); // append if plural } break; } default: throw new RuntimeException(); } } return b.toString(); } // ------------------------------------------------------------------------------------------ public static class Segment { private final String text; private final String subSegmentText; private final Color color; private final double portion; private final double subPortion; private final double subSegmentRatio; private String percent = null; public Segment(String text, double portion, Color color) { this(text, null, portion, 0, color); } public Segment(String text, String subSegmentText, double portion, double subPortion, Color color) throws IllegalArgumentException { assert portion >= 0 && subPortion >= 0 && portion >= subPortion; this.text = text; this.subSegmentText = subSegmentText; this.portion = portion; this.subPortion = subPortion; this.subSegmentRatio = portion > 0 ? subPortion / portion : 0; this.color = color; } public String getText() { return text; } public String getSubSegmentText() { return subSegmentText; } public double getPortion() { return portion; } public double getSubPortion() { return subPortion; } public double getSubSegmentRatio() { return subSegmentRatio; } public Color getColor() { return color; } public String getPercent() { return percent; } public void setPercent(String percent) { this.percent = percent; } } }