001package jmri.jmrit.display; 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.Image; 010import java.awt.Polygon; 011import java.awt.event.ActionEvent; 012import java.awt.event.ActionListener; 013import java.awt.geom.AffineTransform; 014import java.util.Date; 015 016import javax.swing.ButtonGroup; 017import javax.swing.JMenu; 018import javax.swing.JMenuItem; 019import javax.swing.JPopupMenu; 020import javax.swing.JRadioButtonMenuItem; 021 022import jmri.*; 023import jmri.jmrit.catalog.NamedIcon; 024import jmri.util.swing.JmriColorChooser; 025import jmri.util.swing.JmriMouseEvent; 026 027import org.slf4j.Logger; 028import org.slf4j.LoggerFactory; 029 030/** 031 * An Analog Clock for displaying in a panel. 032 * <p> 033 * Time code copied in part from code for the Nixie clock by Bob Jacobsen 034 * 035 * @author Howard G. Penny - Copyright (C) 2005 036 */ 037public class AnalogClock2Display extends PositionableJComponent implements LinkingObject { 038 039 Timebase clock; 040 double rate; 041 double minuteAngle; 042 double hourAngle; 043 String amPm; 044 Color color = Color.black; 045 046 // Define common variables 047 Image logo; 048 Image scaledLogo; 049 Image clockFace; 050 NamedIcon jmriIcon; 051 NamedIcon scaledIcon; 052 NamedIcon clockIcon; 053 054 int[] hourX = { 055 -12, -11, -25, -10, -10, 0, 10, 10, 25, 11, 12}; 056 int[] hourY = { 057 -31, -163, -170, -211, -276, -285, -276, -211, -170, -163, -31}; 058 int[] minuteX = { 059 -12, -11, -24, -11, -11, 0, 11, 11, 24, 11, 12}; 060 int[] minuteY = { 061 -31, -261, -266, -314, -381, -391, -381, -314, -266, -261, -31}; 062 int[] scaledHourX = new int[hourX.length]; 063 int[] scaledHourY = new int[hourY.length]; 064 int[] scaledMinuteX = new int[minuteX.length]; 065 int[] scaledMinuteY = new int[minuteY.length]; 066 int[] rotatedHourX = new int[hourX.length]; 067 int[] rotatedHourY = new int[hourY.length]; 068 int[] rotatedMinuteX = new int[minuteX.length]; 069 int[] rotatedMinuteY = new int[minuteY.length]; 070 071 Polygon hourHand; 072 Polygon scaledHourHand; 073 Polygon minuteHand; 074 Polygon scaledMinuteHand; 075 int minuteHeight; 076 int hourHeight; 077 double scaleRatio; 078 int faceSize; 079 int panelWidth; 080 int panelHeight; 081 int size; 082 int logoWidth; 083 int logoHeight; 084 085 // centreX, centreY are the coordinates of the centre of the clock 086 int centreX; 087 int centreY; 088 089 String _url; 090 091 public AnalogClock2Display(Editor editor) { 092 super(editor); 093 clock = InstanceManager.getDefault(jmri.Timebase.class); 094 095 rate = (int) clock.userGetRate(); 096 097 init(); 098 } 099 100 public AnalogClock2Display(Editor editor, String url) { 101 this(editor); 102 _url = url; 103 } 104 105 @Override 106 public Positionable deepClone() { 107 AnalogClock2Display pos; 108 if (_url == null || _url.trim().length() == 0) { 109 pos = new AnalogClock2Display(_editor); 110 } else { 111 pos = new AnalogClock2Display(_editor, _url); 112 } 113 return finishClone(pos); 114 } 115 116 protected Positionable finishClone(AnalogClock2Display pos) { 117 pos.setScale(getScale()); 118 return super.finishClone(pos); 119 } 120 121 final void init() { 122 // Load the JMRI logo and clock face 123 // Icons are the original size version kept for to allow for mulitple resizing 124 // and scaled Icons are the version scaled for the panel size 125 jmriIcon = new NamedIcon("resources/logo.gif", "resources/logo.gif"); 126 scaledIcon = new NamedIcon("resources/logo.gif", "resources/logo.gif"); 127 clockIcon = new NamedIcon("resources/clock2.gif", "resources/clock2.gif"); 128 logo = jmriIcon.getImage(); 129 clockFace = clockIcon.getImage(); 130 131 // Create an unscaled set of hands to get the original size (height)to use 132 // in the scaling calculations 133 hourHand = new Polygon(hourX, hourY, 11); 134 hourHeight = hourHand.getBounds().getSize().height; 135 minuteHand = new Polygon(minuteX, minuteY, 11); 136 minuteHeight = minuteHand.getBounds().getSize().height; 137 138 amPm = "AM"; 139 140 // request callback to update time 141 clock.addMinuteChangeListener(e -> update()); 142 // request callback to update changes in properties 143 clock.addPropertyChangeListener(e -> update()); 144 setSize(clockIcon.getIconHeight()); // set to default size 145 } 146 147 ButtonGroup colorButtonGroup = null; 148 ButtonGroup rateButtonGroup = null; 149 JMenuItem runMenu = null; 150 151 public int getFaceWidth() { 152 return faceSize; 153 } 154 155 public int getFaceHeight() { 156 return faceSize; 157 } 158 159 @Override 160 public boolean setScaleMenu(JPopupMenu popup) { 161 162 popup.add(new JMenuItem(Bundle.getMessage("FastClock"))); 163 JMenu rateMenu = new JMenu("Clock rate"); 164 rateButtonGroup = new ButtonGroup(); 165 addRateMenuEntry(rateMenu, 1); 166 addRateMenuEntry(rateMenu, 2); 167 addRateMenuEntry(rateMenu, 4); 168 addRateMenuEntry(rateMenu, 8); 169 popup.add(rateMenu); 170 runMenu = new JMenuItem(getRun() ? "Stop" : "Start"); 171 runMenu.addActionListener(e -> { 172 setRun(!getRun()); 173 update(); 174 }); 175 popup.add(runMenu); 176 popup.add(CoordinateEdit.getScaleEditAction(this)); 177 popup.addSeparator(); 178 JMenuItem colorMenuItem = new JMenuItem(Bundle.getMessage("Color")); 179 colorMenuItem.addActionListener((ActionEvent event) -> { 180 Color desiredColor = JmriColorChooser.showDialog(this, 181 Bundle.getMessage("DefaultTextColor", ""), 182 color); 183 if (desiredColor!=null && !color.equals(desiredColor)) { 184 setColor(desiredColor); 185 } 186 }); 187 popup.add(colorMenuItem); 188 189 return true; 190 } 191 192 @Override 193 public String getNameString() { 194 return "Clock"; 195 } 196 197 @Override 198 public void setScale(double scale) { 199 if (scale == 1.0) { 200 init(); 201 return; 202 } 203 AffineTransform t = AffineTransform.getScaleInstance(scale, scale); 204 clockIcon = new NamedIcon("resources/clock2.gif", "resources/clock2.gif"); 205 int w = (int) Math.ceil(scale * clockIcon.getIconWidth()); 206 int h = (int) Math.ceil(scale * clockIcon.getIconHeight()); 207 clockIcon.transformImage(w, h, t, null); 208 scaledIcon = new NamedIcon("resources/logo.gif", "resources/logo.gif"); 209 w = (int) Math.ceil(scale * scaledIcon.getIconWidth()); 210 h = (int) Math.ceil(scale * scaledIcon.getIconHeight()); 211 scaledIcon.transformImage(w, h, t, null); 212 jmriIcon = new NamedIcon("resources/logo.gif", "resources/logo.gif"); 213 w = (int) Math.ceil(scale * jmriIcon.getIconWidth()); 214 h = (int) Math.ceil(scale * jmriIcon.getIconHeight()); 215 jmriIcon.transformImage(w, h, t, null); 216 logo = jmriIcon.getImage(); 217 clockFace = clockIcon.getImage(); 218 setSize(clockIcon.getIconHeight()); 219 super.setScale(scale); 220 } 221 222 @SuppressFBWarnings(value="FE_FLOATING_POINT_EQUALITY", justification="fixed number of possible values") 223 void addRateMenuEntry(JMenu menu, final int newrate) { 224 JRadioButtonMenuItem button = new JRadioButtonMenuItem("" + newrate + ":1"); 225 button.addActionListener(new ActionListener() { 226 final int r = newrate; 227 228 @Override 229 public void actionPerformed(ActionEvent e) { 230 try { 231 clock.userSetRate(r); 232 rate = r; 233 } catch (TimebaseRateException t) { 234 log.error("TimebaseRateException for rate= {}", r, t); 235 } 236 } 237 }); 238 rateButtonGroup.add(button); 239 240 // next line is the FE_FLOATING_POINT_EQUALITY annotated above 241 if (rate == newrate) { 242 button.setSelected(true); 243 } else { 244 button.setSelected(false); 245 } 246 menu.add(button); 247 } 248 249 public Color getColor() { 250 return this.color; 251 } 252 253 public void setColor(Color color) { 254 this.color = color; 255 update(); 256 JmriColorChooser.addRecentColor(color); 257 } 258 259 @Override 260 public void paint(Graphics g) { 261 // overridden Paint method to draw the clock 262 g.setColor(color); 263 g.translate(centreX, centreY); 264 265 // Draw the clock face 266 g.drawImage(clockFace, -faceSize / 2, -faceSize / 2, faceSize, faceSize, this); 267 268 // Draw the JMRI logo 269 g.drawImage(scaledLogo, -logoWidth / 2, -faceSize / 4, logoWidth, 270 logoHeight, this); 271 272 // Draw hour hand rotated to appropriate angle 273 // Calculation mimics the AffineTransform class calculations in Graphics2D 274 // Grpahics2D and AffineTransform not used to maintain compatabilty with Java 1.1.8 275 double minuteAngleRadians = Math.toRadians(minuteAngle); 276 for (int i = 0; i < scaledMinuteX.length; i++) { 277 rotatedMinuteX[i] = (int) (scaledMinuteX[i] * Math.cos(minuteAngleRadians) 278 - scaledMinuteY[i] * Math.sin(minuteAngleRadians)); 279 rotatedMinuteY[i] = (int) (scaledMinuteX[i] * Math.sin(minuteAngleRadians) 280 + scaledMinuteY[i] * Math.cos(minuteAngleRadians)); 281 } 282 scaledMinuteHand = new Polygon(rotatedMinuteX, rotatedMinuteY, rotatedMinuteX.length); 283 double hourAngleRadians = Math.toRadians(hourAngle); 284 for (int i = 0; i < scaledHourX.length; i++) { 285 rotatedHourX[i] = (int) (scaledHourX[i] * Math.cos(hourAngleRadians) 286 - scaledHourY[i] * Math.sin(hourAngleRadians)); 287 rotatedHourY[i] = (int) (scaledHourX[i] * Math.sin(hourAngleRadians) 288 + scaledHourY[i] * Math.cos(hourAngleRadians)); 289 } 290 scaledHourHand = new Polygon(rotatedHourX, rotatedHourY, 291 rotatedHourX.length); 292 293 g.fillPolygon(scaledHourHand); 294 g.fillPolygon(scaledMinuteHand); 295 296 // Draw AM/PM indicator in slightly smaller font than hour digits 297 int amPmFontSize = (int) (faceSize * .075); 298 if (amPmFontSize < 1) { 299 amPmFontSize = 1; 300 } 301 Font amPmSizedFont = new Font("Serif", Font.BOLD, amPmFontSize); 302 g.setFont(amPmSizedFont); 303 FontMetrics amPmFontM = g.getFontMetrics(amPmSizedFont); 304 305 g.drawString(amPm, -amPmFontM.stringWidth(amPm) / 2, faceSize / 5); 306 } 307 308 // Method to provide the cartesian x coordinate given a radius and angle (in degrees) 309 int dotX(double radius, double angle) { 310 int xDist; 311 xDist = (int) Math.round(radius * Math.cos(Math.toRadians(angle))); 312 return xDist; 313 } 314 315 // Method to provide the cartesian y coordinate given a radius and angle (in degrees) 316 int dotY(double radius, double angle) { 317 int yDist; 318 yDist = (int) Math.round(radius * Math.sin(Math.toRadians(angle))); 319 return yDist; 320 } 321 322 // Method called on resizing event - sets various sizing variables 323 // based on the size of the resized panel and scales the logo/hands 324 private void scaleFace() { 325 panelHeight = this.getSize().height; 326 panelWidth = this.getSize().width; 327 if (panelHeight > 0 && panelWidth > 0) { 328 size = Math.min(panelHeight, panelWidth); 329 } 330 faceSize = size; 331 if (faceSize <= 12) { 332 return; 333 } 334 335 // Had trouble getting the proper sizes when using Images by themselves so 336 // use the NamedIcon as a source for the sizes 337 int logoScaleWidth = faceSize / 6; 338 int logoScaleHeight = (int) ((float) logoScaleWidth 339 * (float) jmriIcon.getIconHeight() 340 / jmriIcon.getIconWidth()); 341 scaledLogo = logo.getScaledInstance(logoScaleWidth, logoScaleHeight, 342 Image.SCALE_SMOOTH); 343 scaledIcon.setImage(scaledLogo); 344 logoWidth = scaledIcon.getIconWidth(); 345 logoHeight = scaledIcon.getIconHeight(); 346 347 scaleRatio = faceSize / 2.7 / minuteHeight; 348 for (int i = 0; i < minuteX.length; i++) { 349 scaledMinuteX[i] = (int) (minuteX[i] * scaleRatio); 350 scaledMinuteY[i] = (int) (minuteY[i] * scaleRatio); 351 scaledHourX[i] = (int) (hourX[i] * scaleRatio); 352 scaledHourY[i] = (int) (hourY[i] * scaleRatio); 353 } 354 scaledHourHand = new Polygon(scaledHourX, scaledHourY, 355 scaledHourX.length); 356 scaledMinuteHand = new Polygon(scaledMinuteX, scaledMinuteY, 357 scaledMinuteX.length); 358 359 if (panelHeight > 0 && panelWidth > 0) { 360 centreX = panelWidth / 2; 361 centreY = panelHeight / 2; 362 } else { 363 centreX = centreY = size / 2; 364 } 365 } 366 367 public void setSize(int x) { 368 size = x; 369 setSize(x, x); 370 scaleFace(); 371 } 372 373 /* This needs to be updated if resizing becomes an option 374 public void resize() { 375 int panelHeight = this.getSize().height; 376 int panelWidth = this.getSize().width; 377 size = Math.min(panelHeight, panelWidth); 378 scaleFace(); 379 } 380 */ 381 @SuppressWarnings("deprecation") // Date.getTime 382 public void update() { 383 Date now = clock.getTime(); 384 if (runMenu != null) { 385 runMenu.setText(getRun() ? "Stop" : "Start"); 386 } 387 int hours = now.getHours(); 388 int minutes = now.getMinutes(); 389 minuteAngle = minutes * 6.; 390 hourAngle = hours * 30. + 30. * minuteAngle / 360.; 391 if (hours < 12) { 392 amPm = Bundle.getMessage("ClockAM"); 393 } else { 394 amPm = Bundle.getMessage("ClockPM"); 395 } 396 if (hours == 12 && minutes == 0) { 397 amPm = Bundle.getMessage("ClockNoon"); 398 } 399 if (hours == 0 && minutes == 0) { 400 amPm = Bundle.getMessage("ClockMidnight"); 401 } 402 403 // show either "Stopped" or rate, depending on state 404 if (! clock.getRun()) { 405 amPm = amPm + " "+Bundle.getMessage("ClockStopped"); 406 } else { 407 // running, display rate 408 String rate = ""+(int)clock.userGetRate(); 409 if (Math.floor(clock.userGetRate()) != clock.userGetRate()) { 410 var format = new java.text.DecimalFormat("0.###"); // no trailing zeros 411 rate = format.format(clock.userGetRate()); 412 } 413 414 // add rate to amPm string for display 415 amPm = amPm + " " + rate + ":1"; 416 } 417 repaint(); 418 } 419 420 public boolean getRun() { 421 return clock.getRun(); 422 } 423 424 public void setRun(boolean next) { 425 clock.setRun(next); 426 } 427 428 @Override 429 void cleanup() { 430 } 431 432 public void dispose() { 433 rateButtonGroup = null; 434 runMenu = null; 435 } 436 437 @Override 438 public String getURL() { 439 return _url; 440 } 441 442 @Override 443 public void setULRL(String u) { 444 _url = u; 445 } 446 447 @Override 448 public boolean setLinkMenu(JPopupMenu popup) { 449 if (_url == null || _url.trim().length() == 0) { 450 return false; 451 } 452 popup.add(CoordinateEdit.getLinkEditAction(this, "EditLink")); 453 return true; 454 } 455 456 @Override 457 public void doMouseClicked(JmriMouseEvent event) { 458 log.debug("click to {}", _url); 459 if (_url == null || _url.trim().length() == 0) { 460 return; 461 } 462 try { 463 if (_url.startsWith("frame:")) { 464 // locate JmriJFrame and push to front 465 String frame = _url.substring(6); 466 final jmri.util.JmriJFrame jframe = jmri.util.JmriJFrame.getFrame(frame); 467 java.awt.EventQueue.invokeLater(() -> { 468 jframe.toFront(); 469 jframe.repaint(); 470 }); 471 } else { 472 jmri.util.HelpUtil.openWebPage(_url); 473 } 474 } catch (JmriException t) { 475 log.error("Error handling link", t); 476 } 477 super.doMouseClicked(event); 478 } 479 480 private static final Logger log = LoggerFactory.getLogger(AnalogClock2Display.class); 481}