001package jmri.jmrix.bachrus;
002
003import java.awt.BasicStroke;
004import java.awt.Color;
005import java.awt.Font;
006import java.awt.Graphics;
007import java.awt.Graphics2D;
008import java.awt.RenderingHints;
009import java.awt.font.FontRenderContext;
010import java.awt.font.LineMetrics;
011import java.awt.geom.Ellipse2D;
012import java.awt.geom.Line2D;
013import java.awt.print.PageFormat;
014import java.awt.print.Printable;
015import java.awt.print.PrinterException;
016import java.awt.print.PrinterJob;
017import javax.swing.JPanel;
018import org.slf4j.Logger;
019import org.slf4j.LoggerFactory;
020
021/**
022 * Frame for graph of loco speed curves
023 *
024 * @author Andrew Crosland Copyright (C) 2010
025 * @author Dennis Miller Copyright (C) 2015
026 */
027public class GraphPane extends JPanel implements Printable {
028
029    final int PAD = 40;
030
031    protected String xLabel;
032    protected String yLabel;
033    // array to hold the speed curves
034    protected DccSpeedProfile[] _sp;
035    protected String annotate;
036    protected Color[] colors = {Color.RED, Color.BLUE, Color.BLACK};
037
038    protected boolean _grid = false;
039
040    // Use a default 28 step profile
041    public GraphPane() {
042        super();
043        _sp = new DccSpeedProfile[1];
044        _sp[0] = new DccSpeedProfile(28);
045    }
046
047    public GraphPane(DccSpeedProfile sp) {
048        super();
049        _sp = new DccSpeedProfile[1];
050        _sp[0] = sp;
051    }
052
053    public GraphPane(DccSpeedProfile sp0, DccSpeedProfile sp1) {
054        super();
055        _sp = new DccSpeedProfile[2];
056        _sp[0] = sp0;
057        _sp[1] = sp1;
058    }
059
060    public GraphPane(DccSpeedProfile sp0, DccSpeedProfile sp1, DccSpeedProfile ref) {
061        super();
062        _sp = new DccSpeedProfile[3];
063        _sp[0] = sp0;
064        _sp[1] = sp1;
065        _sp[2] = ref;
066    }
067
068    public void setXLabel(String s) {
069        xLabel = s;
070    }
071
072    public void setYLabel(String s) {
073        yLabel = s;
074    }
075
076    public void showGrid(boolean b) {
077        _grid = b;
078    }
079
080    Speed.Unit unit = Speed.Unit.MPH;
081//    String unitString = "Speed (MPH)";
082
083    void setUnitsMph() {
084        unit = Speed.Unit.MPH;
085        setYLabel(Bundle.getMessage("SpeedMPH"));
086    }
087
088    void setUnitsKph() {
089         unit = Speed.Unit.KPH;
090        setYLabel(Bundle.getMessage("SpeedKPH"));
091    }
092
093    public Speed.Unit getUnits() {
094        return unit;
095    }
096
097    @Override
098    protected void paintComponent(Graphics g) {
099        super.paintComponent(g);
100        drawGraph(g);
101    }
102
103    protected void drawGraph(Graphics g) {
104        if (!(g instanceof Graphics2D) ) {
105              throw new IllegalArgumentException("Graphics object passed is not the correct type");
106        }
107
108        Graphics2D g2 = (Graphics2D) g;
109        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
110                RenderingHints.VALUE_ANTIALIAS_ON);
111        int w = getWidth();
112        int h = getHeight();
113
114        // Draw ordinate (y-axis).
115        g2.draw(new Line2D.Double(PAD, PAD, PAD, h - PAD));
116        // Draw abcissa (x-axis).
117        g2.draw(new Line2D.Double(PAD, h - PAD, w - PAD, h - PAD));
118
119        // Draw labels.
120        Font font = g2.getFont();
121        FontRenderContext frc = g2.getFontRenderContext();
122        LineMetrics lm = font.getLineMetrics("0", frc);
123
124        float dash1[] = {1.0f};
125        BasicStroke dashed = new BasicStroke(1.0f,
126                BasicStroke.CAP_BUTT,
127                BasicStroke.JOIN_MITER,
128                10.0f, dash1, 0.0f);
129        BasicStroke plain = new BasicStroke(1.0f);
130
131        float sh = lm.getAscent() + lm.getDescent();
132        // Ordinate (y-axis) label.
133        float sy = PAD + ((h - 2 * PAD) - yLabel.length() * sh) / 2 + lm.getAscent();
134        g2.setPaint(Color.green.darker());
135        for (int i = 0; i < yLabel.length(); i++) {
136            String letter = String.valueOf(yLabel.charAt(i));
137            float sw = (float) font.getStringBounds(letter, frc).getWidth();
138            float sx = (PAD / 2 - sw) / 2;
139            g2.drawString(letter, sx, sy);
140            sy += sh;
141        }
142        // Abcissa (x-axis) label.
143        sy = h - PAD / 2 + (PAD / 2 - sh) / 2 + lm.getAscent();
144        float sw = (float) font.getStringBounds(xLabel, frc).getWidth();
145        float sx = (w - sw) / 2;
146        g2.drawString(xLabel, sx, sy);
147
148        // find the maximum of all profiles
149        float maxSpeed = 0;
150        for (int i = 0; i < _sp.length; i++) {
151            maxSpeed = Math.max(_sp[i].getMax(), maxSpeed);
152        }
153
154        // Used to scale values into drawing area
155        float scale = (h - 2 * PAD) / maxSpeed;
156        // space between values along the ordinate (y-axis)
157        // start with an increment of 1
158        // Plot a grid line every two
159        // Plot a label every ten
160        float yInc = scale;
161        int yMod = 10;
162        int gridMod = 2;
163        if (unit == Speed.Unit.MPH) {
164            // need inverse transform here
165            yInc = Speed.mphToKph(yInc);
166        }
167        if ((unit == Speed.Unit.KPH) && (maxSpeed > 100) || (unit == Speed.Unit.MPH) && (maxSpeed > 160)) {
168            log.debug("Adjusting Y axis spacing for max speed");
169            yMod *= 2;
170            gridMod *= 2;
171        }
172        String ordString;
173        // Draw lines
174        for (int i = 0; i <= (h - 2 * PAD) / yInc; i++) {
175            g2.setPaint(Color.green.darker());
176            g2.setStroke(plain);
177            float y1 = h - PAD - i * yInc;
178            if ((i % yMod) == 0) {
179                g2.draw(new Line2D.Double(7 * PAD / 8, y1, PAD, y1));
180                ordString = Integer.toString(i);
181                sw = (float) font.getStringBounds(ordString, frc).getWidth();
182                sx = 7 * PAD / 8 - sw;
183                sy = y1 + lm.getAscent() / 2;
184                g2.drawString(ordString, sx, sy);
185            }
186            if (_grid && (i > 0) && ((i % gridMod) == 0)) {
187                // Horizontal grid lines
188                g2.setPaint(Color.LIGHT_GRAY);
189                if ((i % yMod) != 0) {
190                    g2.setStroke(dashed);
191                }
192                g2.draw(new Line2D.Double(PAD, y1, w - PAD, y1));
193            }
194        }
195        if (_grid) {
196            // Close the top
197            g2.setPaint(Color.LIGHT_GRAY);
198            g2.setStroke(dashed);
199            g2.draw(new Line2D.Double(PAD, PAD, w - PAD, PAD));
200        }
201
202        // The space between values along the abcissa (x-axis).
203        float xInc = (float) (w - 2 * PAD) / (_sp[0].getLength() - 1);
204        String abString;
205        // Draw lines between data points.
206        // for each point in a profile
207        for (int i = 0; i < _sp[0].getLength(); i++) {
208            g2.setPaint(Color.green.darker());
209            g2.setStroke(plain);
210            float x1 = 0.0F;
211            // for each profile in the array
212            for (int j = 0; j < _sp.length; j++) {
213                x1 = PAD + i * xInc;
214                float y1 = h - PAD - scale * _sp[j].getPoint(i);
215                float x2 = PAD + (i + 1) * xInc;
216                float y2 = h - PAD - scale * _sp[j].getPoint(i + 1);
217                // if it's a valid data point
218                if (i <= _sp[j].getLast() - 1) {
219                    g2.draw(new Line2D.Double(x1, y1, x2, y2));
220                }
221            }
222            // tick marks along abcissa
223            g2.draw(new Line2D.Double(x1, h - 7 * PAD / 8, x1, h - PAD));
224            if (((i % 5) == 0) || (i == _sp[0].getLength() - 1)) {
225                // abcissa labels every 5 ticks
226                abString = Integer.toString(i);
227                sw = (float) font.getStringBounds(abString, frc).getWidth();
228                sx = x1 - sw / 2;
229                sy = h - PAD + (PAD / 2 - sh) / 2 + lm.getAscent();
230                g2.drawString(abString, sx, sy);
231            }
232            if (_grid && (i > 0)) {
233                // Verical grid line
234                g2.setPaint(Color.LIGHT_GRAY);
235                if ((i % 5) != 0) {
236                    g2.setStroke(dashed);
237                }
238                g2.draw(new Line2D.Double(x1, PAD, x1, h - PAD));
239            }
240        }
241        g2.setStroke(plain);
242
243        // Mark data points.
244        // for each point in a profile
245        for (int i = 0; i <= _sp[0].getLength(); i++) {
246            // for each profile in the array
247            for (int j = 0; j < _sp.length; j++) {
248                g2.setPaint(colors[j]);
249                float x = PAD + i * xInc;
250                float y = h - PAD - scale * _sp[j].getPoint(i);
251                // if it's a valid data point
252                if (i <= _sp[j].getLast()) {
253                    g2.fill(new Ellipse2D.Double(x - 2, y - 2, 4, 4));
254                }
255            }
256        }
257    }
258
259    @Override
260    public int print(Graphics g, PageFormat pf, int page) throws
261            PrinterException {
262
263        if (page > 0) { /* We have only one page, and 'page' is zero-based */
264
265            return Printable.NO_SUCH_PAGE;
266        }
267
268        if (!(g instanceof Graphics2D) ) {
269              throw new IllegalArgumentException("Graphics object passed is not the correct type");
270        }
271
272        Graphics2D g2 = (Graphics2D) g;
273        /* User (0,0) is typically outside the imageable area, so we must
274         * translate by the X and Y values in the PageFormat to avoid clipping.
275         */
276        g2.translate(pf.getImageableX(), pf.getImageableY());
277
278        // Scale to fit the width and height if neccessary
279        double scale = 1.0;
280        if (this.getWidth() > pf.getImageableWidth()) {
281            scale *= pf.getImageableWidth() / this.getWidth();
282        }
283        if (this.getHeight() > pf.getImageableHeight()) {
284            scale *= pf.getImageableHeight() / this.getHeight();
285        }
286        g2.scale(scale, scale);
287
288        // Draw the graph
289        drawGraph(g);
290
291        // Add annotation
292        g2.setPaint(Color.BLACK);
293        g2.drawString(annotate, 0, Math.round(this.getHeight() + 2 * PAD * scale));
294
295        /* tell the caller that this page is part of the printed document */
296        return Printable.PAGE_EXISTS;
297    }
298
299    public void printProfile(String s) {
300        annotate = s;
301        PrinterJob job = PrinterJob.getPrinterJob();
302        job.setPrintable(this);
303        boolean ok = job.printDialog();
304        if (ok) {
305            try {
306                job.print();
307            } catch (PrinterException ex) {
308                log.error("Exception whilst printing profile", ex);
309            }
310        }
311    }
312
313    private final static Logger log = LoggerFactory.getLogger(GraphPane.class);
314}