001package jmri.jmrit.analogclock; 002 003import java.awt.Color; 004import java.awt.Font; 005import java.awt.FontMetrics; 006import java.awt.Graphics; 007import java.awt.Image; 008import java.awt.Polygon; 009import java.awt.event.ActionEvent; 010import java.awt.event.ActionListener; 011import java.awt.event.ComponentAdapter; 012import java.awt.event.ComponentEvent; 013import java.beans.PropertyChangeEvent; 014import java.beans.PropertyChangeListener; 015import java.util.Date; 016 017import javax.swing.BoxLayout; 018import javax.swing.JButton; 019import javax.swing.JPanel; 020 021import jmri.InstanceManager; 022import jmri.Timebase; 023import jmri.jmrit.catalog.NamedIcon; 024import jmri.util.JmriJFrame; 025import jmri.util.ThreadingUtil; 026 027/** 028 * Creates a JFrame containing an analog clockface and hands. 029 * <p> 030 * Time code copied from code for the Nixie clock by Bob Jacobsen 031 * 032 * @author Dennis Miller Copyright (C) 2004 033 */ 034public class AnalogClockFrame extends JmriJFrame implements java.beans.PropertyChangeListener { 035 036 // GUI member declarations 037 Timebase clock; 038 private final PropertyChangeListener minuteListener = (PropertyChangeEvent evt) -> update(); 039 double minuteAngle; 040 double hourAngle; 041 String amPm; 042 043 public AnalogClockFrame() { 044 super(Bundle.getMessage("MenuItemAnalogClock")); 045 046 clock = InstanceManager.getDefault(jmri.Timebase.class); 047 048 // init GUI 049 setPreferredSize(new java.awt.Dimension(200, 200)); 050 getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS)); 051 JPanel analogClockPanel = new ClockPanel(); 052 analogClockPanel.setOpaque(true); 053 getContentPane().add(analogClockPanel); 054 055 JPanel buttonPanel = new JPanel(); 056 // Need to put a Box Layout on the panel to ensure the run/stop button is centered 057 // Without it, the button does not center properly 058 buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS)); 059 buttonPanel.add(b = new JButton(Bundle.getMessage("ButtonPauseClock"))); 060 updateButtonText(); 061 b.addActionListener(new ButtonListener()); 062 b.setOpaque(true); 063 b.setVisible(true); 064 getContentPane().add(buttonPanel); 065 // since Run/Stop button is not to evryones taste, user may turn it on in clock prefs 066 buttonPanel.setVisible(clock.getShowStopButton()); // pick up clock prefs choice 067 // get ready to display 068 pack(); 069 070 ThreadingUtil.runOnGUIEventually(() -> { 071 AnalogClockFrame.this.update(); // set proper time 072 }); 073 074 // listen for changes to the Timebase parameters 075 clock.addPropertyChangeListener(AnalogClockFrame.this); 076 077 // request callback to update time 078 clock.addMinuteChangeListener(minuteListener); 079 080 } 081 082 public class ClockPanel extends JPanel { 083 084 // Create a Panel that has clockface drawn on it scaled to the size of the panel 085 // Define common variables 086 Image logo; 087 Image scaledLogo; 088 NamedIcon jmriIcon; 089 NamedIcon scaledIcon; 090 int hourX[] = {-12, -11, -25, -10, -10, 0, 10, 10, 25, 11, 12}; 091 int hourY[] = {-31, -163, -170, -211, -276, -285, -276, -211, -170, -163, -31}; 092 int minuteX[] = {-12, -11, -24, -11, -11, 0, 11, 11, 24, 11, 12}; 093 int minuteY[] = {-31, -261, -266, -314, -381, -391, -381, -314, -266, -261, -31}; 094 int scaledHourX[] = new int[hourX.length]; 095 int scaledHourY[] = new int[hourY.length]; 096 int scaledMinuteX[] = new int[minuteX.length]; 097 int scaledMinuteY[] = new int[minuteY.length]; 098 int rotatedHourX[] = new int[hourX.length]; 099 int rotatedHourY[] = new int[hourY.length]; 100 int rotatedMinuteX[] = new int[minuteX.length]; 101 int rotatedMinuteY[] = new int[minuteY.length]; 102 103 Polygon scaledHourHand; 104 Polygon scaledMinuteHand; 105 int minuteHeight; 106 double scaleRatio; 107 int faceSize; 108 int size; 109 int logoWidth; 110 int logoHeight; 111 112 // centreX, centreY are the coordinates of the centre of the clock 113 int centreX; 114 int centreY; 115 116 public ClockPanel() { 117 // Load the JMRI logo and hands to put on the clock 118 // Icons are the original size version kept for to allow for mulitple resizing 119 // and scaled Icons are the version scaled for the panel size 120 jmriIcon = new NamedIcon("resources/logo.gif", "resources/logo.gif"); 121 scaledIcon = new NamedIcon("resources/logo.gif", "resources/logo.gif"); 122 logo = jmriIcon.getImage(); 123 124 // Create an unscaled minute hand to get the original size (height) to use 125 // in the scaling calculations 126 Polygon minuteHand = new Polygon(minuteX, minuteY, 11); 127 minuteHeight = minuteHand.getBounds().getSize().height; 128 129 amPm = "AM"; 130 131 // Add component listener to handle frame resizing event 132 this.addComponentListener(new ComponentAdapter() { 133 @Override 134 public void componentResized(ComponentEvent e) { 135 scaleFace(); 136 } 137 }); 138 139 } 140 141 @Override 142 public void paint(Graphics g) { 143 144 // overridden Paint method to draw the clock 145 g.translate(centreX, centreY); 146 147 // Draw the clockface outline scaled to the panel size with a dot in the middle and 148 // rings for the hands 149 g.setColor(Color.white); 150 g.fillOval(-faceSize / 2, -faceSize / 2, faceSize, faceSize); 151 g.setColor(Color.black); 152 g.drawOval(-faceSize / 2, -faceSize / 2, faceSize, faceSize); 153 154 int dotSize = faceSize / 40; 155 g.fillOval(-dotSize * 2, -dotSize * 2, 4 * dotSize, 4 * dotSize); 156 g.setColor(Color.white); 157 g.fillOval(-dotSize, -dotSize, 2 * dotSize, 2 * dotSize); 158 g.setColor(Color.black); 159 g.fillOval(-dotSize / 2, -dotSize / 2, dotSize, dotSize); 160 161 // Draw the JMRI logo 162 g.drawImage(scaledLogo, -logoWidth / 2, -faceSize / 4, logoWidth, logoHeight, this); 163 164 // Draw the hour and minute markers 165 int dashSize = size / 60; 166 for (int i = 0; i < 360; i = i + 6) { 167 g.drawLine(dotX(faceSize / 2., i), dotY(faceSize / 2., i), dotX(faceSize / 2. - dashSize, i), dotY(faceSize / 2. - dashSize, i)); 168 } 169 for (int i = 0; i < 360; i = i + 30) { 170 g.drawLine(dotX(faceSize / 2., i), dotY(faceSize / 2., i), dotX(faceSize / 2. - 3 * dashSize, i), dotY(faceSize / 2. - 3 * dashSize, i)); 171 } 172 173 // Add the hour digits, with the fontsize scaled to the clock size 174 int fontSize = faceSize / 10; 175 if (fontSize < 1) { 176 fontSize = 1; 177 } 178 Font sizedFont = new Font("Serif", Font.PLAIN, fontSize); 179 g.setFont(sizedFont); 180 FontMetrics fontM = g.getFontMetrics(sizedFont); 181 182 for (int i = 0; i < 12; i++) { 183 String hour = Integer.toString(i + 1); 184 int xOffset = fontM.stringWidth(hour); 185 int yOffset = fontM.getHeight(); 186 g.drawString(Integer.toString(i + 1), dotX(faceSize / 2 - 6 * dashSize, i * 30 - 60) - xOffset / 2, dotY(faceSize / 2 - 6 * dashSize, i * 30 - 60) + yOffset / 4); 187 } 188 189 // Draw hour hand rotated to appropriate angle 190 // Calculation mimics the AffineTransform class calculations in Graphics2D 191 // Graphics2D and AffineTransform not used to maintain compatabilty with Java 1.1.8 192 double minuteAngleRad = Math.toRadians(minuteAngle); 193 for (int i = 0; i < scaledMinuteX.length; i++) { 194 rotatedMinuteX[i] = (int) (scaledMinuteX[i] * Math.cos(minuteAngleRad) - scaledMinuteY[i] * Math.sin(minuteAngleRad)); 195 rotatedMinuteY[i] = (int) (scaledMinuteX[i] * Math.sin(minuteAngleRad) + scaledMinuteY[i] * Math.cos(minuteAngleRad)); 196 } 197 scaledMinuteHand = new Polygon(rotatedMinuteX, rotatedMinuteY, rotatedMinuteX.length); 198 199 double hourAngleRad = Math.toRadians(hourAngle); 200 for (int i = 0; i < scaledHourX.length; i++) { 201 rotatedHourX[i] = (int) (scaledHourX[i] * Math.cos(hourAngleRad) - scaledHourY[i] * Math.sin(hourAngleRad)); 202 rotatedHourY[i] = (int) (scaledHourX[i] * Math.sin(hourAngleRad) + scaledHourY[i] * Math.cos(hourAngleRad)); 203 } 204 scaledHourHand = new Polygon(rotatedHourX, rotatedHourY, rotatedHourX.length); 205 206 g.fillPolygon(scaledHourHand); 207 g.fillPolygon(scaledMinuteHand); 208 209 // Draw AM/PM indicator in slightly smaller font than hour digits 210 int amPmFontSize = (int) (fontSize * .75); 211 if (amPmFontSize < 1) { 212 amPmFontSize = 1; 213 } 214 Font amPmSizedFont = new Font("Serif", Font.PLAIN, amPmFontSize); 215 g.setFont(amPmSizedFont); 216 FontMetrics amPmFontM = g.getFontMetrics(amPmSizedFont); 217 218 g.drawString(amPm, -amPmFontM.stringWidth(amPm) / 2, faceSize / 5); 219 } 220 221 // Method to provide the cartesian x coordinate given a radius and angle (in degrees) 222 int dotX(double radius, double angle) { 223 int xDist; 224 xDist = (int) Math.round(radius * Math.cos(Math.toRadians(angle))); 225 return xDist; 226 } 227 228 // Method to provide the cartesian y coordinate given a radius and angle (in degrees) 229 int dotY(double radius, double angle) { 230 int yDist; 231 yDist = (int) Math.round(radius * Math.sin(Math.toRadians(angle))); 232 return yDist; 233 } 234 235 // Method called on resizing event - sets various sizing variables 236 // based on the size of the resized panel and scales the logo/hands 237 public void scaleFace() { 238 int panelHeight = this.getSize().height; 239 int panelWidth = this.getSize().width; 240 size = Math.min(panelHeight, panelWidth); 241 faceSize = (int) (size * .97); 242 if (faceSize == 0) { 243 faceSize = 1; 244 } 245 246 // Had trouble getting the proper sizes when using Images by themselves so 247 // use the NamedIcon as a source for the sizes 248 int logoScaleWidth = faceSize / 6; 249 int logoScaleHeight = (int) ((float) logoScaleWidth * (float) jmriIcon.getIconHeight() / jmriIcon.getIconWidth()); 250 scaledLogo = logo.getScaledInstance(Math.max(1, logoScaleWidth), Math.max(1, logoScaleHeight), Image.SCALE_SMOOTH); 251 scaledIcon.setImage(scaledLogo); 252 logoWidth = scaledIcon.getIconWidth(); 253 logoHeight = scaledIcon.getIconHeight(); 254 255 scaleRatio = faceSize / 2.7 / minuteHeight; 256 for (int i = 0; i < minuteX.length; i++) { 257 scaledMinuteX[i] = (int) (minuteX[i] * scaleRatio); 258 scaledMinuteY[i] = (int) (minuteY[i] * scaleRatio); 259 scaledHourX[i] = (int) (hourX[i] * scaleRatio); 260 scaledHourY[i] = (int) (hourY[i] * scaleRatio); 261 } 262 scaledHourHand = new Polygon(scaledHourX, scaledHourY, scaledHourX.length); 263 scaledMinuteHand = new Polygon(scaledMinuteX, scaledMinuteY, scaledMinuteX.length); 264 265 centreX = panelWidth / 2; 266 centreY = panelHeight / 2; 267 268 } 269 } 270 271 @SuppressWarnings("deprecation") // Date.getHours, getMinutes, getSeconds 272 void update() { 273 Date now = clock.getTime(); 274 int hours = now.getHours(); 275 int minutes = now.getMinutes(); 276 minuteAngle = minutes * 6.; 277 hourAngle = hours * 30. + 30. * minuteAngle / 360.; 278 if (hours < 12) { 279 amPm = Bundle.getMessage("ClockAM"); 280 } else { 281 amPm = Bundle.getMessage("ClockPM"); 282 } 283 if (hours == 12 && minutes == 0) { 284 amPm = Bundle.getMessage("ClockNoon"); 285 } 286 if (hours == 0 && minutes == 0) { 287 amPm = Bundle.getMessage("ClockMidnight"); 288 } 289 290 // show either "Stopped" or rate, depending on state 291 if (! clock.getRun()) { 292 amPm = amPm + " "+Bundle.getMessage("ClockStopped"); 293 } else { 294 // running, display rate 295 String rate = ""+(int)clock.userGetRate(); 296 if (Math.floor(clock.userGetRate()) != clock.userGetRate()) { 297 var format = new java.text.DecimalFormat("0.###"); // no trailing zeros 298 rate = format.format(clock.userGetRate()); 299 } 300 301 // add rate to amPm string for display 302 amPm = amPm + " " + rate + ":1"; 303 } 304 repaint(); 305 } 306 307 /** 308 * Handle a change to clock properties. 309 * @param e unused. 310 */ 311 @Override 312 public void propertyChange(java.beans.PropertyChangeEvent e) { 313 updateButtonText(); 314 315 // paint the clock too 316 update(); 317 } 318 319 /** 320 * Update clock button text. 321 */ 322 private void updateButtonText(){ 323 b.setText( Bundle.getMessage( clock.getRun() ? "ButtonPauseClock" : "ButtonRunClock") ); 324 } 325 326 JButton b; 327 328 private class ButtonListener implements ActionListener { 329 @Override 330 public void actionPerformed(ActionEvent a) { 331 clock.setRun(!clock.getRun()); 332 updateButtonText(); 333 } 334 } 335 336 @Override 337 public void dispose() { 338 clock.removeMinuteChangeListener(minuteListener); 339 clock.removePropertyChangeListener(this); 340 super.dispose(); 341 } 342 343}