001package jmri.jmrix.bachrus; 002 003import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 004 005import java.awt.Color; 006import java.awt.Font; 007import java.awt.FontMetrics; 008import java.awt.Graphics; 009import java.awt.Graphics2D; 010import java.awt.Image; 011import java.awt.Polygon; 012import java.awt.event.ComponentAdapter; 013import java.awt.event.ComponentEvent; 014import javax.swing.JPanel; 015import jmri.jmrit.catalog.NamedIcon; 016 017/** 018 * Creates a JPanel containing an Dial type speedo display. 019 * 020 * <p> 021 * Based on analogue clock frame by Dennis Miller 022 * 023 * @author Andrew Crosland Copyright (C) 2010 024 * @author Dennis Miller Copyright (C) 2015 025 * 026 */ 027public class SpeedoDial extends JPanel { 028 029 // GUI member declarations 030 float speedAngle = 0.0F; 031 int speedDigits = 0; 032 033 // Create a Panel that has a dial drawn on it scaled to the size of the panel 034 // Define common variables 035 Image logo; 036 Image scaledLogo; 037 NamedIcon jmriIcon; 038 NamedIcon scaledIcon; 039 int minuteX[] = {-12, -11, -24, -11, -11, 0, 11, 11, 24, 11, 12}; 040 int minuteY[] = {-31, -261, -266, -314, -381, -391, -381, -314, -266, -261, -31}; 041 int scaledMinuteX[] = new int[minuteX.length]; 042 int scaledMinuteY[] = new int[minuteY.length]; 043 int rotatedMinuteX[] = new int[minuteX.length]; 044 int rotatedMinuteY[] = new int[minuteY.length]; 045 046 Polygon minuteHand; 047 Polygon scaledMinuteHand; 048 int minuteHeight; 049 float scaleRatio; 050 int faceSize; 051 int size; 052 int logoWidth; 053 int logoHeight; 054 055 // centreX, centreY are the coordinates of the centre of the dial 056 int centreX; 057 int centreY; 058 059 Speed.Unit unit = Speed.Unit.MPH; 060 061 int baseMphLimit = 80; 062 int baseKphLimit = 140; 063 int mphLimit = baseMphLimit; 064 int mphInc = 40; 065 int kphLimit = baseKphLimit; 066 int kphInc = 70; 067 float priMajorTick; 068 float priMinorTick; 069 float secTick; 070 String priString = "MPH"; 071 String secString = "KPH"; 072 073 public SpeedoDial() { 074 super(); 075 076 // Load the JMRI logo and pointer for the dial 077 // Icons are the original size version kept for to allow for mulitple resizing 078 // and scaled Icons are the version scaled for the panel size 079 jmriIcon = new NamedIcon("resources/logo.gif", "resources/logo.gif"); 080 scaledIcon = new NamedIcon("resources/logo.gif", "resources/logo.gif"); 081 logo = jmriIcon.getImage(); 082 083 // Create an unscaled pointer to get the original size (height)to use 084 // in the scaling calculations 085 minuteHand = new Polygon(minuteX, minuteY, 11); 086 minuteHeight = minuteHand.getBounds().getSize().height; 087 088 // Add component listener to handle frame resizing event 089 this.addComponentListener(new ComponentAdapter() { 090 @Override 091 public void componentResized(ComponentEvent e) { 092 scaleFace(); 093 } 094 }); 095 } 096 097 @Override 098 @SuppressFBWarnings(value = "FL_FLOATS_AS_LOOP_COUNTERS", 099 justification = "Major refactor required to unwind the float in loops." 100 +"Speeds above max speed, along with Mph / Kmph need to be considered.") 101 public void paint(Graphics g) { 102 super.paint(g); 103 if (!(g instanceof Graphics2D) ) { 104 throw new IllegalArgumentException("Graphics object passed is not the correct type"); 105 } 106 107 Graphics2D g2 = (Graphics2D) g; 108 109 // overridden Paint method to draw the speedo dial 110 g2.translate(centreX, centreY); 111 112 // Draw the dial outline scaled to the panel size with a dot in the middle and 113 // center ring for the pointer 114 g2.setColor(Color.white); 115 g2.fillOval(-faceSize / 2, -faceSize / 2, faceSize, faceSize); 116 g2.setColor(Color.black); 117 g2.drawOval(-faceSize / 2, -faceSize / 2, faceSize, faceSize); 118 119 int dotSize = faceSize / 40; 120 g2.fillOval(-dotSize * 2, -dotSize * 2, 4 * dotSize, 4 * dotSize); 121 122 // Draw the JMRI logo 123 g2.drawImage(scaledLogo, -logoWidth / 2, -faceSize / 4, logoWidth, logoHeight, this); 124 125 // Currently selected units are plotted every 10 units with major and minor 126 // tick marks around the outer edge of the dial 127 // Other units are plotted in a differrent color, smaller font with dots 128 // in an inner ring 129 // Scaled font size for primary units 130 int fontSize = faceSize / 10; 131 if (fontSize < 1) { 132 fontSize = 1; 133 } 134 Font sizedFont = new Font("Serif", Font.PLAIN, fontSize); 135 g2.setFont(sizedFont); 136 FontMetrics fontM = g2.getFontMetrics(sizedFont); 137 138 // Draw the speed markers for the primary units 139 int dashSize = size / 60; 140 setTicks(); 141 // i is degrees clockwise from the X axis 142 // Add minor tick marks 143 for (float i = 150; i < 391; i = i + priMinorTick) { 144 g2.drawLine(dotX((float) faceSize / 2, i), dotY((float) faceSize / 2, i), 145 dotX((float) faceSize / 2 - dashSize, i), dotY((float) faceSize / 2 - dashSize, i)); 146 } 147 // Add major tick marks and digits 148 int j = 0; 149 for (float i = 150; i < 391; i = i + priMajorTick) { 150 g2.drawLine(dotX((float) faceSize / 2, i), dotY((float) faceSize / 2, i), 151 dotX((float) faceSize / 2 - 3 * dashSize, i), dotY((float) faceSize / 2 - 3 * dashSize, i)); 152 String speed = Integer.toString(10 * j); 153 int xOffset = fontM.stringWidth(speed); 154 int yOffset = fontM.getHeight(); 155 // offset by 210 degrees to start in lower left quadrant and work clockwise 156 g2.drawString(speed, dotX((float) faceSize / 2 - 6 * dashSize, j * priMajorTick - 210) - xOffset / 2, 157 dotY((float) faceSize / 2 - 6 * dashSize, j * priMajorTick - 210) + yOffset / 4); 158 j++; 159 } 160 161 // Add dots and digits for secondary units 162 // First make a smaller font 163 fontSize = faceSize / 15; 164 if (fontSize < 1) { 165 fontSize = 1; 166 } 167 sizedFont = new Font("Serif", Font.PLAIN, fontSize); 168 g2.setFont(sizedFont); 169 fontM = g2.getFontMetrics(sizedFont); 170 g2.setColor(Color.green); 171 j = 0; 172 for (float i = 150; i < 391; i = i + secTick) { 173 g2.fillOval(dotX((float) faceSize / 2 - 10 * dashSize, i), dotY((float) faceSize / 2 - 10 * dashSize, i), 174 5, 5); 175 if (((j & 1) == 0) || (unit == Speed.Unit.KPH)) { 176 // kph are plotted every 20 when secondary, mph every 10 177 String speed = Integer.toString(10 * j); 178 int xOffset = fontM.stringWidth(speed); 179 int yOffset = fontM.getHeight(); 180 // offset by 210 degrees to start in lower left quadrant and work clockwise 181 g2.drawString(speed, dotX((float) faceSize / 2 - 13 * dashSize, j * secTick - 210) - xOffset / 2, 182 dotY((float) faceSize / 2 - 13 * dashSize, j * secTick - 210) + yOffset / 4); 183 } 184 j++; 185 } 186 // Draw secondary units string 187 g2.drawString(secString, dotX((float) faceSize / 2 - 5 * dashSize, 45) - fontM.stringWidth(secString) / 2, 188 dotY((float) faceSize / 2 - 5 * dashSize, 45) + fontM.getHeight() / 4); 189 g2.setColor(Color.black); 190 191 // Draw pointer rotated to appropriate angle 192 // Calculation mimics the AffineTransform class calculations in Graphics2D 193 // Graphics2D and AffineTransform not used to maintain compatabilty with Java 1.1.8 194 double speedAngleRadians = Math.toRadians(speedAngle); 195 for (int i = 0; i < scaledMinuteX.length; i++) { 196 rotatedMinuteX[i] = (int) (scaledMinuteX[i] * Math.cos(speedAngleRadians) 197 - scaledMinuteY[i] * Math.sin(speedAngleRadians)); 198 rotatedMinuteY[i] = (int) (scaledMinuteX[i] * Math.sin(speedAngleRadians) 199 + scaledMinuteY[i] * Math.cos(speedAngleRadians)); 200 } 201 scaledMinuteHand = new Polygon(rotatedMinuteX, rotatedMinuteY, rotatedMinuteX.length); 202 g2.fillPolygon(scaledMinuteHand); 203 204 // Draw primary units indicator in slightly smaller font than speed digits 205 int unitsFontSize = (int) ((float) faceSize / 10 * .75); 206 if (unitsFontSize < 1) { 207 unitsFontSize = 1; 208 } 209 Font unitsSizedFont = new Font("Serif", Font.PLAIN, unitsFontSize); 210 g2.setFont(unitsSizedFont); 211 FontMetrics unitsFontM = g2.getFontMetrics(unitsSizedFont); 212// g2.drawString(unitsString, -amPmFontM.stringWidth(unitsString)/2, faceSize/5 ); 213 g2.drawString(priString, dotX((float) faceSize / 2 - 5 * dashSize, -225) - unitsFontM.stringWidth(priString) / 2, 214 dotY((float) faceSize / 2 - 5 * dashSize, -225) + unitsFontM.getHeight() / 4); 215 216 // Show numeric speed 217 String speedString = Integer.toString(speedDigits); 218 int digitsFontSize = (int) (fontSize * 1.5); 219 Font digitsSizedFont = new Font("Serif", Font.PLAIN, digitsFontSize); 220 g2.setFont(digitsSizedFont); 221 FontMetrics digitsFontM = g2.getFontMetrics(digitsSizedFont); 222 223 // draw a box around the digital speed 224 int pad = (int) (digitsFontSize * 0.2); 225 int h = (int) (digitsFontM.getAscent() * 0.8); 226 int w = digitsFontM.stringWidth("999"); 227 if (pad < 2) { 228 pad = 2; 229 } 230 g2.setColor(Color.LIGHT_GRAY); 231 g2.fillRect(-w / 2 - pad, 2 * faceSize / 5 - h - pad, w + pad * 2, h + pad * 2); 232 g2.setColor(Color.DARK_GRAY); 233 g2.drawRect(-w / 2 - pad, 2 * faceSize / 5 - h - pad, w + pad * 2, h + pad * 2); 234 235 g2.setColor(Color.BLACK); 236 g2.drawString(speedString, -digitsFontM.stringWidth(speedString) / 2, 2 * faceSize / 5); 237 } 238 239 // Method to provide the cartesian x coordinate given a radius and angle (in degrees) 240 int dotX(float radius, float angle) { 241 int xDist; 242 xDist = (int) Math.round(radius * Math.cos(Math.toRadians(angle))); 243 return xDist; 244 } 245 246 // Method to provide the cartesian y coordinate given a radius and angle (in degrees) 247 int dotY(float radius, float angle) { 248 int yDist; 249 yDist = (int) Math.round(radius * Math.sin(Math.toRadians(angle))); 250 return yDist; 251 } 252 253 // Method called on resizing event - sets various sizing variables 254 // based on the size of the resized panel and scales the logo/hands 255 public void scaleFace() { 256 int panelHeight = this.getSize().height; 257 int panelWidth = this.getSize().width; 258 size = Math.min(panelHeight, panelWidth); 259 faceSize = (int) (size * .97); 260 if (faceSize == 0) { 261 faceSize = 1; 262 } 263 264 // Had trouble getting the proper sizes when using Images by themselves so 265 // use the NamedIcon as a source for the sizes 266 int logoScaleWidth = faceSize / 6; 267 int logoScaleHeight = (int) ((float) logoScaleWidth * (float) jmriIcon.getIconHeight() / jmriIcon.getIconWidth()); 268 scaledLogo = logo.getScaledInstance(logoScaleWidth, logoScaleHeight, Image.SCALE_SMOOTH); 269 scaledIcon.setImage(scaledLogo); 270 logoWidth = scaledIcon.getIconWidth(); 271 logoHeight = scaledIcon.getIconHeight(); 272 273 scaleRatio = faceSize / 2.7F / minuteHeight; 274 for (int i = 0; i < minuteX.length; i++) { 275 scaledMinuteX[i] = (int) (minuteX[i] * scaleRatio); 276 scaledMinuteY[i] = (int) (minuteY[i] * scaleRatio); 277 } 278 scaledMinuteHand = new Polygon(scaledMinuteX, scaledMinuteY, scaledMinuteX.length); 279 280 centreX = panelWidth / 2; 281 centreY = panelHeight / 2; 282 } 283 284 void update(float speed) { 285 // hand rotation starts at 12 o'clock position so offset it by 120 degrees 286 // scale by the angle between major tick marks divided by 10 287 if (unit == Speed.Unit.MPH) { 288 if (Speed.kphToMph(speed) > mphLimit) { 289 mphLimit += mphInc; 290 kphLimit += kphInc; 291 } 292 setTicks(); 293 speedDigits = Math.round(Speed.kphToMph(speed)); 294 speedAngle = -120 + Speed.kphToMph(speed * priMajorTick / 10); 295 } else { 296 if (speed > kphLimit) { 297 mphLimit += mphInc; 298 kphLimit += kphInc; 299 } 300 setTicks(); 301 speedDigits = Math.round(speed); 302 speedAngle = -120 + speed * priMajorTick / 10; 303 } 304 repaint(); 305 } 306 307 void setTicks() { 308 if (unit == Speed.Unit.MPH) { 309 priMajorTick = 240 / ((float) mphLimit / 10); 310 priMinorTick = priMajorTick / 5; 311 secTick = 240 / (Speed.mphToKph(mphLimit) / 10); 312 } else { 313 priMajorTick = 240 / ((float) kphLimit / 10); 314 priMinorTick = priMajorTick / 5; 315 secTick = 240 / (Speed.kphToMph(kphLimit) / 10); 316 } 317 } 318 319 void setUnitsMph() { 320 unit = Speed.Unit.MPH; 321 priString = "MPH"; 322 secString = "KPH"; 323 } 324 325 void setUnitsKph() { 326 unit = Speed.Unit.KPH; 327 priString = "KPH"; 328 secString = "MPH"; 329 } 330 331 public void reset() { 332 mphLimit = baseMphLimit; 333 kphLimit = baseKphLimit; 334 update(0.0f); 335 } 336}