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}