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