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}