/* * Copyright (C) 2010 Brockmann Consult GmbH (info@brockmann-consult.de) * * This program is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) * any later version. * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, see http://www.gnu.org/licenses/ */ package org.esa.snap.ui.diagram; import org.esa.snap.core.util.Guardian; import org.esa.snap.core.util.ObjectUtils; import org.esa.snap.core.util.math.Range; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.Rectangle; import java.awt.Stroke; import java.awt.geom.AffineTransform; import java.awt.geom.Line2D; import java.awt.geom.NoninvertibleTransformException; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.List; /** * The <code>Diagram</code> class is used to plot simple X/Y graphs. Instances of this class are composed of * <code>{@link DiagramGraph}</code> and two <code>{@link DiagramAxis}</code> objects for the X and Y axes. */ public class Diagram { public final static String DEFAULT_FONT_NAME = "Verdana"; public final static int DEFAULT_FONT_SIZE = 9; public static final Color DEFAULT_FOREGROUND_COLOR = Color.BLACK; public static final Color DEFAULT_BACKGROUND_COLOR = new Color(210, 210, 255); // Main components: graphs + axes private List<DiagramGraph> graphs; private DiagramAxis xAxis; private DiagramAxis yAxis; // Visual properties private boolean drawGrid; private Font font; private int textGap; private Color textColor; private int majorTickLength; private int minorTickLength; private Color majorGridColor; private Color minorGridColor; private Color foregroundColor; private Color backgroundColor; // Change management private ArrayList<DiagramChangeListener> changeListeners; private int numMergedChangeEvents; // Internal properties private boolean valid; private double xMinAccum; private double xMaxAccum; private double yMinAccum; private double yMaxAccum; // Computed internal properties private FontMetrics fontMetrics; private Rectangle graphArea; private String[] yTickTexts; private String[] xTickTexts; private int maxYTickTextWidth; private RectTransform transform; public Diagram() { graphs = new ArrayList<DiagramGraph>(3); font = new Font(DEFAULT_FONT_NAME, Font.PLAIN, DEFAULT_FONT_SIZE); textGap = 3; majorTickLength = 5; minorTickLength = 3; drawGrid = true; foregroundColor = DEFAULT_FOREGROUND_COLOR; backgroundColor = DEFAULT_BACKGROUND_COLOR; minorGridColor = DEFAULT_BACKGROUND_COLOR.brighter(); majorGridColor = DEFAULT_BACKGROUND_COLOR.darker(); textColor = DEFAULT_FOREGROUND_COLOR; changeListeners = new ArrayList<DiagramChangeListener>(3); disableChangeEventMerging(); resetMinMaxAccumulators(); } public Diagram(DiagramAxis xAxis, DiagramAxis yAxis, DiagramGraph graph) { this(); setXAxis(xAxis); setYAxis(yAxis); addGraph(graph); resetMinMaxAccumulatorsFromAxes(); } public void enableChangeEventMerging() { numMergedChangeEvents = 0; } public void disableChangeEventMerging() { final boolean changeEventsMerged = numMergedChangeEvents > 0; numMergedChangeEvents = -1; if (changeEventsMerged) { invalidate(); } } public RectTransform getTransform() { return transform; } public boolean getDrawGrid() { return drawGrid; } public void setDrawGrid(boolean drawGrid) { if (this.drawGrid != drawGrid) { this.drawGrid = drawGrid; invalidate(); } } public DiagramAxis getXAxis() { return xAxis; } public void setXAxis(DiagramAxis xAxis) { Guardian.assertNotNull("xAxis", xAxis); if (this.xAxis != xAxis) { if (this.xAxis != null) { this.xAxis.setDiagram(null); } this.xAxis = xAxis; this.xAxis.setDiagram(this); invalidate(); } } public DiagramAxis getYAxis() { return yAxis; } public void setYAxis(DiagramAxis yAxis) { Guardian.assertNotNull("yAxis", yAxis); if (this.yAxis != yAxis) { if (this.yAxis != null) { this.yAxis.setDiagram(null); } this.yAxis = yAxis; this.yAxis.setDiagram(this); invalidate(); } } public DiagramGraph[] getGraphs() { return graphs.toArray(new DiagramGraph[0]); } public int getGraphCount() { return graphs.size(); } public DiagramGraph getGraph(int index) { return graphs.get(index); } public void addGraph(DiagramGraph graph) { Guardian.assertNotNull("graph", graph); if (graphs.add(graph)) { graph.setDiagram(this); invalidate(); } } public void removeGraph(DiagramGraph graph) { Guardian.assertNotNull("graph", graph); if (graphs.remove(graph)) { graph.setDiagram(null); invalidate(); } } public void removeAllGraphs() { if (getGraphCount() > 0) { for (DiagramGraph graph : graphs) { graph.setDiagram(null); } graphs.clear(); invalidate(); } } public Font getFont() { return font; } public void setFont(Font font) { if (!ObjectUtils.equalObjects(this.font, font)) { this.font = font; invalidate(); } } public Color getMajorGridColor() { return majorGridColor; } public void setMajorGridColor(Color majorGridColor) { if (!ObjectUtils.equalObjects(this.majorGridColor, majorGridColor)) { this.majorGridColor = majorGridColor; invalidate(); } } public Color getMinorGridColor() { return minorGridColor; } public void setMinorGridColor(Color minorGridColor) { if (!ObjectUtils.equalObjects(this.minorGridColor, minorGridColor)) { this.minorGridColor = minorGridColor; invalidate(); } } public Color getForegroundColor() { return foregroundColor; } public void setForegroundColor(Color foregroundColor) { if (!ObjectUtils.equalObjects(this.foregroundColor, foregroundColor)) { this.foregroundColor = foregroundColor; invalidate(); } } public Color getBackgroundColor() { return backgroundColor; } public void setBackgroundColor(Color backgroundColor) { this.backgroundColor = backgroundColor; if (!ObjectUtils.equalObjects(this.backgroundColor, backgroundColor)) { this.backgroundColor = backgroundColor; invalidate(); } } public int getTextGap() { return textGap; } public void setTextGap(int textGap) { if (!ObjectUtils.equalObjects(this.font, font)) { this.textGap = textGap; invalidate(); } } public boolean isValid() { return valid; } public void setValid(boolean valid) { this.valid = valid; } public void invalidate() { setValid(false); fireDiagramChanged(); } public Rectangle getGraphArea() { return new Rectangle(graphArea); } public void render(Graphics2D g2d, int x, int y, int width, int height) { Font oldFont = g2d.getFont(); g2d.setFont(font); if (!isValid()) { validate(g2d, x, y, width, height); } if (isValid()) { drawAxes(g2d, x, y, width, height); drawGraphs(g2d); } g2d.setFont(oldFont); } private void validate(Graphics2D g2d, int x, int y, int width, int height) { fontMetrics = g2d.getFontMetrics(); xTickTexts = xAxis.createTickmarkTexts(); yTickTexts = yAxis.createTickmarkTexts(); // define y-Axis _values final int fontAscent = fontMetrics.getAscent(); maxYTickTextWidth = 0; for (String yTickText : yTickTexts) { int sw = fontMetrics.stringWidth(yTickText); maxYTickTextWidth = Math.max(maxYTickTextWidth, sw); } final int widthMaxX = fontMetrics.stringWidth(xTickTexts[xTickTexts.length - 1]); int x1 = textGap + fontAscent + textGap + maxYTickTextWidth + textGap + majorTickLength; int y1 = textGap + fontAscent / 2; int x2 = x + width - (textGap + widthMaxX / 2); int y2 = y + height - (textGap + fontAscent + textGap + fontAscent + textGap + majorTickLength); final int w = x2 - x1 + 1; final int h = y2 - y1 + 1; graphArea = new Rectangle(x1, y1, w, h); transform = null; if (w > 0 && h > 0) { transform = new RectTransform(new Range(xAxis.getMinValue(), xAxis.getMaxValue()), new Range(yAxis.getMinValue(), yAxis.getMaxValue()), new Range(graphArea.x, graphArea.x + graphArea.width), new Range(graphArea.y + graphArea.height, graphArea.y)); } setValid(w > 0 && h > 0); } private void drawGraphs(Graphics2D g2d) { final Stroke oldStroke = g2d.getStroke(); final Color oldColor = g2d.getColor(); final Rectangle oldClip = g2d.getClipBounds(); g2d.setClip(graphArea.x, graphArea.y, graphArea.width, graphArea.height); Point2D.Double a; Point2D.Double b1; Point2D.Double b2; DiagramGraph[] graphs = getGraphs(); for (DiagramGraph graph : graphs) { a = new Point2D.Double(); b1 = new Point2D.Double(); b2 = new Point2D.Double(); g2d.setStroke(graph.getStyle().getOutlineStroke()); g2d.setColor(graph.getStyle().getOutlineColor()); int n = graph.getNumValues(); for (int i = 0; i < n; i++) { double xa = graph.getXValueAt(i); double ya = graph.getYValueAt(i); if (!Double.isNaN(ya)) { a.setLocation(xa, ya); if (b2.equals(new Point2D.Double())) { transform.transformA2B(a, b1); b2.setLocation(b1); } else { b1.setLocation(b2); transform.transformA2B(a, b2); } if (i > 0 && !b1.equals(b2)) { g2d.draw(new Line2D.Double(b1, b2)); } } } g2d.setStroke(new BasicStroke(0.5f)); if (graph.getStyle().isShowingPoints()) { for (int i = 0; i < n; i++) { double xa = graph.getXValueAt(i); double ya = graph.getYValueAt(i); if (!Double.isNaN(ya)) { a.setLocation(xa, ya); transform.transformA2B(a, b1); Rectangle2D.Double r = new Rectangle2D.Double(b1.getX() - 1.5, b1.getY() - 1.5, 3.0, 3.0); g2d.setPaint(graph.getStyle().getFillPaint()); g2d.fill(r); g2d.setColor(graph.getStyle().getOutlineColor()); g2d.draw(r); } } } } g2d.setStroke(oldStroke); g2d.setColor(oldColor); g2d.setClip(oldClip); } private void drawAxes(Graphics2D g2d, int xOffset, int yOffset, int width, int height) { final Stroke oldStroke = g2d.getStroke(); final Color oldColor = g2d.getColor(); g2d.setStroke(new BasicStroke(1.0f)); g2d.setColor(backgroundColor); g2d.fillRect(graphArea.x, graphArea.y, graphArea.width, graphArea.height); int tw; int x0, y0, x1, x2, y1, y2, xMin, xMax, yMin, yMax, n, n1, n2; String text; final int th = fontMetrics.getAscent(); // draw X major tick lines xMin = graphArea.x; xMax = graphArea.x + graphArea.width; yMin = graphArea.y; yMax = graphArea.y + graphArea.height; y1 = graphArea.y + graphArea.height; n1 = xAxis.getNumMajorTicks(); n2 = xAxis.getNumMinorTicks(); n = (n1 - 1) * (n2 + 1) + 1; for (int i = 0; i < n; i++) { x0 = xMin + (i * (xMax - xMin)) / (n - 1); if (i % (n2 + 1) == 0) { y2 = y1 + majorTickLength; text = xTickTexts[i / (n2 + 1)]; tw = fontMetrics.stringWidth(text); g2d.setColor(textColor); g2d.drawString(text, x0 - tw / 2, y2 + textGap + fontMetrics.getAscent()); if (drawGrid) { g2d.setColor(majorGridColor); g2d.drawLine(x0, y1, x0, yMin); } } else { y2 = y1 + minorTickLength; if (drawGrid) { g2d.setColor(minorGridColor); g2d.drawLine(x0, y1, x0, yMin); } } g2d.setColor(foregroundColor); g2d.drawLine(x0, y1, x0, y2); } // draw Y major tick lines x1 = graphArea.x; n1 = yAxis.getNumMajorTicks(); n2 = yAxis.getNumMinorTicks(); n = (n1 - 1) * (n2 + 1) + 1; for (int i = 0; i < n; i++) { if(yAxis.isMinToMax()) y0 = yMin + (i * (yMax - yMin)) / (n - 1); else y0 = yMax - (i * (yMax - yMin)) / (n - 1); if (i % (n2 + 1) == 0) { x2 = x1 - majorTickLength; text = yTickTexts[n1 - 1 - (i / (n2 + 1))]; tw = fontMetrics.stringWidth(text); g2d.setColor(textColor); g2d.drawString(text, x2 - textGap - tw, y0 + th / 2); if (drawGrid) { g2d.setColor(majorGridColor); g2d.drawLine(x1, y0, xMax, y0); } } else { x2 = x1 - minorTickLength; if (drawGrid) { g2d.setColor(minorGridColor); g2d.drawLine(x1, y0, xMax, y0); } } g2d.setColor(foregroundColor); g2d.drawLine(x1, y0, x2, y0); } g2d.setColor(foregroundColor); g2d.drawRect(graphArea.x, graphArea.y, graphArea.width, graphArea.height); // draw X axis name and unit text = getAxisText(xAxis); tw = fontMetrics.stringWidth(text); x1 = graphArea.x + graphArea.width / 2 - tw / 2; y1 = yOffset + height - textGap; g2d.setColor(textColor); g2d.drawString(text, x1, y1); // draw Y axis name and unit text = getAxisText(yAxis); tw = fontMetrics.stringWidth(text); x1 = graphArea.x - majorTickLength - textGap - maxYTickTextWidth - textGap; y1 = graphArea.y + graphArea.height / 2 + tw / 2; final AffineTransform oldTransform = g2d.getTransform(); g2d.translate(x1, y1); g2d.rotate(-Math.PI / 2); g2d.setColor(textColor); g2d.drawString(text, 0, 0); g2d.setTransform(oldTransform); g2d.setStroke(oldStroke); g2d.setColor(oldColor); } private String getAxisText(DiagramAxis axis) { StringBuilder sb = new StringBuilder(37); if (axis.getName() != null && axis.getName().length() > 0) { sb.append(axis.getName()); } if (axis.getUnit() != null && axis.getUnit().length() > 0) { sb.append(" ("); sb.append(axis.getUnit()); sb.append(")"); } return sb.toString(); } public DiagramGraph getClosestGraph(int x, int y) { double minDist = Double.MAX_VALUE; Point2D.Double a = new Point2D.Double(); Point2D.Double b1 = new Point2D.Double(); Point2D.Double b2 = new Point2D.Double(); DiagramGraph closestGraph = null; for (DiagramGraph graph : getGraphs()) { double minDistGraph = Double.MAX_VALUE; int n = graph.getNumValues(); for (int i = 0; i < n; i++) { a.setLocation(graph.getXValueAt(i), graph.getYValueAt(i)); b1.setLocation(b2); transform.transformA2B(a, b2); if (i > 0) { Line2D.Double segment = new Line2D.Double(b1, b2); double v = segment.ptSegDist(x, y); if (v < minDistGraph) { minDistGraph = v; } } } if (minDistGraph < minDist) { minDist = minDistGraph; closestGraph = graph; } } return closestGraph; } public void adjustAxes(boolean reset) { if (reset) { resetMinMaxAccumulators(); } for (DiagramGraph graph : graphs) { adjustAxes(graph); } } protected void adjustAxes(DiagramGraph graph) { try { enableChangeEventMerging(); final DiagramAxis xAxis = getXAxis(); xMinAccum = Math.min(xMinAccum, graph.getXMin()); xMaxAccum = Math.max(xMaxAccum, graph.getXMax()); boolean xRangeValid = xMaxAccum > xMinAccum; if (xRangeValid) { xAxis.setValueRange(xMinAccum, xMaxAccum); xAxis.setOptimalSubDivision(4, 6, 5); } final DiagramAxis yAxis = getYAxis(); yMinAccum = Math.min(yMinAccum, graph.getYMin()); yMaxAccum = Math.max(yMaxAccum, graph.getYMax()); boolean yRangeValid = yMaxAccum > yMinAccum; if (yRangeValid) { yAxis.setValueRange(yMinAccum, yMaxAccum); yAxis.setOptimalSubDivision(3, 6, 5); } } finally { disableChangeEventMerging(); } } public void resetMinMaxAccumulators() { xMinAccum = +Double.MAX_VALUE; xMaxAccum = -Double.MAX_VALUE; yMinAccum = +Double.MAX_VALUE; yMaxAccum = -Double.MAX_VALUE; } public void resetMinMaxAccumulatorsFromAxes() { xMinAccum = getXAxis().getMinValue(); xMaxAccum = getXAxis().getMaxValue(); yMinAccum = getYAxis().getMinValue(); yMaxAccum = getYAxis().getMaxValue(); } private void fireDiagramChanged() { if (numMergedChangeEvents == -1) { final DiagramChangeListener[] listeners = getChangeListeners(); for (DiagramChangeListener listener : listeners) { listener.diagramChanged(this); } } else { numMergedChangeEvents++; } } public DiagramChangeListener[] getChangeListeners() { return changeListeners.toArray(new DiagramChangeListener[0]); } public void addChangeListener(DiagramChangeListener listener) { if (listener != null) { changeListeners.add(listener); } } public void removeChangeListener(DiagramChangeListener listener) { if (listener != null) { changeListeners.remove(listener); } } public static class RectTransform { private AffineTransform transformA2B; private AffineTransform transformB2A; public RectTransform(Range ax, Range ay, Range bx, Range by) { double ax1 = ax.getMin(); double ax2 = ax.getMax(); double ay1 = ay.getMin(); double ay2 = ay.getMax(); double bx1 = bx.getMin(); double bx2 = bx.getMax(); double by1 = by.getMin(); double by2 = by.getMax(); transformA2B = new AffineTransform(); transformA2B.translate(bx1 - ax1 * (bx2 - bx1) / (ax2 - ax1), by1 - ay1 * (by2 - by1) / (ay2 - ay1)); transformA2B.scale((bx2 - bx1) / (ax2 - ax1), (by2 - by1) / (ay2 - ay1)); try { transformB2A = transformA2B.createInverse(); } catch (NoninvertibleTransformException e) { throw new IllegalArgumentException(); } } public Point2D transformA2B(Point2D a, Point2D b) { return transformA2B.transform(a, b); } public Point2D transformB2A(Point2D b, Point2D a) { return transformB2A.transform(b, a); } } public void dispose() { // first disable listening to what will happen next! changeListeners.clear(); // remove main components removeAllGraphs(); if (xAxis != null) { xAxis.setDiagram(null); xAxis = null; } if (yAxis != null) { yAxis.setDiagram(null); yAxis = null; } } }