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