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    int units = Speed.MPH;
081//    String unitString = "Speed (MPH)";
082
083    void setUnitsMph() {
084        units = Speed.MPH;
085        setYLabel(Bundle.getMessage("SpeedMPH"));
086    }
087
088    void setUnitsKph() {
089        units = Speed.KPH;
090        setYLabel(Bundle.getMessage("SpeedKPH"));
091    }
092
093    public int getUnits() {
094        return units;
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 (units == Speed.MPH) {
164            // need inverse transform here
165            yInc = Speed.mphToKph(yInc);
166        }
167        if ((units == Speed.KPH) && (maxSpeed > 100)
168                || (units == Speed.MPH) && (maxSpeed > 160)) {
169            log.debug("Adjusting Y axis spacing for max speed");
170            yMod *= 2;
171            gridMod *= 2;
172        }
173        String ordString;
174        // Draw lines
175        for (int i = 0; i <= (h - 2 * PAD) / yInc; i++) {
176            g2.setPaint(Color.green.darker());
177            g2.setStroke(plain);
178            float y1 = h - PAD - i * yInc;
179            if ((i % yMod) == 0) {
180                g2.draw(new Line2D.Double(7 * PAD / 8, y1, PAD, y1));
181                ordString = Integer.toString(i);
182                sw = (float) font.getStringBounds(ordString, frc).getWidth();
183                sx = 7 * PAD / 8 - sw;
184                sy = y1 + lm.getAscent() / 2;
185                g2.drawString(ordString, sx, sy);
186            }
187            if (_grid && (i > 0) && ((i % gridMod) == 0)) {
188                // Horizontal grid lines
189                g2.setPaint(Color.LIGHT_GRAY);
190                if ((i % yMod) != 0) {
191                    g2.setStroke(dashed);
192                }
193                g2.draw(new Line2D.Double(PAD, y1, w - PAD, y1));
194            }
195        }
196        if (_grid) {
197            // Close the top
198            g2.setPaint(Color.LIGHT_GRAY);
199            g2.setStroke(dashed);
200            g2.draw(new Line2D.Double(PAD, PAD, w - PAD, PAD));
201        }
202
203        // The space between values along the abcissa (x-axis).
204        float xInc = (float) (w - 2 * PAD) / (_sp[0].getLength() - 1);
205        String abString;
206        // Draw lines between data points.
207        // for each point in a profile
208        for (int i = 0; i < _sp[0].getLength(); i++) {
209            g2.setPaint(Color.green.darker());
210            g2.setStroke(plain);
211            float x1 = 0.0F;
212            // for each profile in the array
213            for (int j = 0; j < _sp.length; j++) {
214                x1 = PAD + i * xInc;
215                float y1 = h - PAD - scale * _sp[j].getPoint(i);
216                float x2 = PAD + (i + 1) * xInc;
217                float y2 = h - PAD - scale * _sp[j].getPoint(i + 1);
218                // if it's a valid data point
219                if (i <= _sp[j].getLast() - 1) {
220                    g2.draw(new Line2D.Double(x1, y1, x2, y2));
221                }
222            }
223            // tick marks along abcissa
224            g2.draw(new Line2D.Double(x1, h - 7 * PAD / 8, x1, h - PAD));
225            if (((i % 5) == 0) || (i == _sp[0].getLength() - 1)) {
226                // abcissa labels every 5 ticks
227                abString = Integer.toString(i);
228                sw = (float) font.getStringBounds(abString, frc).getWidth();
229                sx = x1 - sw / 2;
230                sy = h - PAD + (PAD / 2 - sh) / 2 + lm.getAscent();
231                g2.drawString(abString, sx, sy);
232            }
233            if (_grid && (i > 0)) {
234                // Verical grid line
235                g2.setPaint(Color.LIGHT_GRAY);
236                if ((i % 5) != 0) {
237                    g2.setStroke(dashed);
238                }
239                g2.draw(new Line2D.Double(x1, PAD, x1, h - PAD));
240            }
241        }
242        g2.setStroke(plain);
243
244        // Mark data points.
245        // for each point in a profile
246        for (int i = 0; i <= _sp[0].getLength(); i++) {
247            // for each profile in the array
248            for (int j = 0; j < _sp.length; j++) {
249                g2.setPaint(colors[j]);
250                float x = PAD + i * xInc;
251                float y = h - PAD - scale * _sp[j].getPoint(i);
252                // if it's a valid data point
253                if (i <= _sp[j].getLast()) {
254                    g2.fill(new Ellipse2D.Double(x - 2, y - 2, 4, 4));
255                }
256            }
257        }
258    }
259
260    @Override
261    public int print(Graphics g, PageFormat pf, int page) throws
262            PrinterException {
263
264        if (page > 0) { /* We have only one page, and 'page' is zero-based */
265
266            return Printable.NO_SUCH_PAGE;
267        }
268
269        if (!(g instanceof Graphics2D) ) {
270              throw new IllegalArgumentException("Graphics object passed is not the correct type");
271        }
272
273        Graphics2D g2 = (Graphics2D) g;
274        /* User (0,0) is typically outside the imageable area, so we must
275         * translate by the X and Y values in the PageFormat to avoid clipping.
276         */
277        g2.translate(pf.getImageableX(), pf.getImageableY());
278
279        // Scale to fit the width and height if neccessary
280        double scale = 1.0;
281        if (this.getWidth() > pf.getImageableWidth()) {
282            scale *= pf.getImageableWidth() / this.getWidth();
283        }
284        if (this.getHeight() > pf.getImageableHeight()) {
285            scale *= pf.getImageableHeight() / this.getHeight();
286        }
287        g2.scale(scale, scale);
288
289        // Draw the graph
290        drawGraph(g);
291
292        // Add annotation
293        g2.setPaint(Color.BLACK);
294        g2.drawString(annotate, 0, Math.round(this.getHeight() + 2 * PAD * scale));
295
296        /* tell the caller that this page is part of the printed document */
297        return Printable.PAGE_EXISTS;
298    }
299
300    public void printProfile(String s) {
301        annotate = s;
302        PrinterJob job = PrinterJob.getPrinterJob();
303        job.setPrintable(this);
304        boolean ok = job.printDialog();
305        if (ok) {
306            try {
307                job.print();
308            } catch (PrinterException ex) {
309                log.error("Exception whilst printing profile", ex);
310            }
311        }
312    }
313
314    private final static Logger log = LoggerFactory.getLogger(GraphPane.class);
315}