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}