001package jmri.jmrix.bachrus;
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.Graphics2D;
010import java.awt.Image;
011import java.awt.Polygon;
012import java.awt.event.ComponentAdapter;
013import java.awt.event.ComponentEvent;
014import javax.swing.JPanel;
015import jmri.jmrit.catalog.NamedIcon;
016
017/**
018 * Creates a JPanel containing an Dial type speedo display.
019 *
020 * <p>
021 * Based on analogue clock frame by Dennis Miller
022 *
023 * @author Andrew Crosland Copyright (C) 2010
024 * @author Dennis Miller Copyright (C) 2015
025 *
026 */
027public class SpeedoDial extends JPanel {
028
029    // GUI member declarations
030    float speedAngle = 0.0F;
031    int speedDigits = 0;
032
033    // Create a Panel that has a dial drawn on it scaled to the size of the panel
034    // Define common variables
035    Image logo;
036    Image scaledLogo;
037    NamedIcon jmriIcon;
038    NamedIcon scaledIcon;
039    int minuteX[] = {-12, -11, -24, -11, -11, 0, 11, 11, 24, 11, 12};
040    int minuteY[] = {-31, -261, -266, -314, -381, -391, -381, -314, -266, -261, -31};
041    int scaledMinuteX[] = new int[minuteX.length];
042    int scaledMinuteY[] = new int[minuteY.length];
043    int rotatedMinuteX[] = new int[minuteX.length];
044    int rotatedMinuteY[] = new int[minuteY.length];
045
046    Polygon minuteHand;
047    Polygon scaledMinuteHand;
048    int minuteHeight;
049    float scaleRatio;
050    int faceSize;
051    int size;
052    int logoWidth;
053    int logoHeight;
054
055    // centreX, centreY are the coordinates of the centre of the dial
056    int centreX;
057    int centreY;
058
059    int units = Speed.MPH;
060
061    int baseMphLimit = 80;
062    int baseKphLimit = 140;
063    int mphLimit = baseMphLimit;
064    int mphInc = 40;
065    int kphLimit = baseKphLimit;
066    int kphInc = 70;
067    float priMajorTick;
068    float priMinorTick;
069    float secTick;
070    String priString = "MPH";
071    String secString = "KPH";
072
073    public SpeedoDial() {
074        super();
075
076        // Load the JMRI logo and pointer for the dial
077        // Icons are the original size version kept for to allow for mulitple resizing
078        // and scaled Icons are the version scaled for the panel size
079        jmriIcon = new NamedIcon("resources/logo.gif", "resources/logo.gif");
080        scaledIcon = new NamedIcon("resources/logo.gif", "resources/logo.gif");
081        logo = jmriIcon.getImage();
082
083        // Create an unscaled pointer to get the original size (height)to use
084        // in the scaling calculations
085        minuteHand = new Polygon(minuteX, minuteY, 11);
086        minuteHeight = minuteHand.getBounds().getSize().height;
087
088        // Add component listener to handle frame resizing event
089        this.addComponentListener(new ComponentAdapter() {
090            @Override
091            public void componentResized(ComponentEvent e) {
092                scaleFace();
093            }
094        });
095    }
096
097    @Override
098    @SuppressFBWarnings(value = "FL_FLOATS_AS_LOOP_COUNTERS",
099        justification = "Major refactor required to unwind the float in loops."
100        +"Speeds above max speed, along with Mph / Kmph need to be considered.")
101    public void paint(Graphics g) {
102        super.paint(g);
103        if (!(g instanceof Graphics2D) ) {
104              throw new IllegalArgumentException("Graphics object passed is not the correct type");
105        }
106
107        Graphics2D g2 = (Graphics2D) g;
108
109        // overridden Paint method to draw the speedo dial
110        g2.translate(centreX, centreY);
111
112        // Draw the dial outline scaled to the panel size with a dot in the middle and
113        // center ring for the pointer
114        g2.setColor(Color.white);
115        g2.fillOval(-faceSize / 2, -faceSize / 2, faceSize, faceSize);
116        g2.setColor(Color.black);
117        g2.drawOval(-faceSize / 2, -faceSize / 2, faceSize, faceSize);
118
119        int dotSize = faceSize / 40;
120        g2.fillOval(-dotSize * 2, -dotSize * 2, 4 * dotSize, 4 * dotSize);
121
122        // Draw the JMRI logo
123        g2.drawImage(scaledLogo, -logoWidth / 2, -faceSize / 4, logoWidth, logoHeight, this);
124
125        // Currently selected units are plotted every 10 units with major and minor
126        // tick marks around the outer edge of the dial
127        // Other units are plotted in a differrent color, smaller font with dots
128        // in an inner ring
129        // Scaled font size for primary units
130        int fontSize = faceSize / 10;
131        if (fontSize < 1) {
132            fontSize = 1;
133        }
134        Font sizedFont = new Font("Serif", Font.PLAIN, fontSize);
135        g2.setFont(sizedFont);
136        FontMetrics fontM = g2.getFontMetrics(sizedFont);
137
138        // Draw the speed markers for the primary units
139        int dashSize = size / 60;
140        setTicks();
141        // i is degrees clockwise from the X axis
142        // Add minor tick marks
143        for (float i = 150; i < 391; i = i + priMinorTick) {
144            g2.drawLine(dotX((float) faceSize / 2, i), dotY((float) faceSize / 2, i),
145                    dotX((float) faceSize / 2 - dashSize, i), dotY((float) faceSize / 2 - dashSize, i));
146        }
147        // Add major tick marks and digits
148        int j = 0;
149        for (float i = 150; i < 391; i = i + priMajorTick) {
150            g2.drawLine(dotX((float) faceSize / 2, i), dotY((float) faceSize / 2, i),
151                    dotX((float) faceSize / 2 - 3 * dashSize, i), dotY((float) faceSize / 2 - 3 * dashSize, i));
152            String speed = Integer.toString(10 * j);
153            int xOffset = fontM.stringWidth(speed);
154            int yOffset = fontM.getHeight();
155            // offset by 210 degrees to start in lower left quadrant and work clockwise
156            g2.drawString(speed, dotX((float) faceSize / 2 - 6 * dashSize, j * priMajorTick - 210) - xOffset / 2,
157                    dotY((float) faceSize / 2 - 6 * dashSize, j * priMajorTick - 210) + yOffset / 4);
158            j++;
159        }
160
161        // Add dots and digits for secondary units
162        // First make a smaller font
163        fontSize = faceSize / 15;
164        if (fontSize < 1) {
165            fontSize = 1;
166        }
167        sizedFont = new Font("Serif", Font.PLAIN, fontSize);
168        g2.setFont(sizedFont);
169        fontM = g2.getFontMetrics(sizedFont);
170        g2.setColor(Color.green);
171        j = 0;
172        for (float i = 150; i < 391; i = i + secTick) {
173            g2.fillOval(dotX((float) faceSize / 2 - 10 * dashSize, i), dotY((float) faceSize / 2 - 10 * dashSize, i),
174                    5, 5);
175            if (((j & 1) == 0) || (units == Speed.KPH)) {
176                // kph are plotted every 20 when secondary, mph every 10
177                String speed = Integer.toString(10 * j);
178                int xOffset = fontM.stringWidth(speed);
179                int yOffset = fontM.getHeight();
180                // offset by 210 degrees to start in lower left quadrant and work clockwise
181                g2.drawString(speed, dotX((float) faceSize / 2 - 13 * dashSize, j * secTick - 210) - xOffset / 2,
182                        dotY((float) faceSize / 2 - 13 * dashSize, j * secTick - 210) + yOffset / 4);
183            }
184            j++;
185        }
186        // Draw secondary units string
187        g2.drawString(secString, dotX((float) faceSize / 2 - 5 * dashSize, 45) - fontM.stringWidth(secString) / 2,
188                dotY((float) faceSize / 2 - 5 * dashSize, 45) + fontM.getHeight() / 4);
189        g2.setColor(Color.black);
190
191        // Draw pointer rotated to appropriate angle
192        // Calculation mimics the AffineTransform class calculations in Graphics2D
193        // Graphics2D and AffineTransform not used to maintain compatabilty with Java 1.1.8
194        double speedAngleRadians = Math.toRadians(speedAngle);
195        for (int i = 0; i < scaledMinuteX.length; i++) {
196            rotatedMinuteX[i] = (int) (scaledMinuteX[i] * Math.cos(speedAngleRadians)
197                    - scaledMinuteY[i] * Math.sin(speedAngleRadians));
198            rotatedMinuteY[i] = (int) (scaledMinuteX[i] * Math.sin(speedAngleRadians)
199                    + scaledMinuteY[i] * Math.cos(speedAngleRadians));
200        }
201        scaledMinuteHand = new Polygon(rotatedMinuteX, rotatedMinuteY, rotatedMinuteX.length);
202        g2.fillPolygon(scaledMinuteHand);
203
204        // Draw primary units indicator in slightly smaller font than speed digits
205        int unitsFontSize = (int) ((float) faceSize / 10 * .75);
206        if (unitsFontSize < 1) {
207            unitsFontSize = 1;
208        }
209        Font unitsSizedFont = new Font("Serif", Font.PLAIN, unitsFontSize);
210        g2.setFont(unitsSizedFont);
211        FontMetrics unitsFontM = g2.getFontMetrics(unitsSizedFont);
212//        g2.drawString(unitsString, -amPmFontM.stringWidth(unitsString)/2, faceSize/5 );
213        g2.drawString(priString, dotX((float) faceSize / 2 - 5 * dashSize, -225) - unitsFontM.stringWidth(priString) / 2,
214                dotY((float) faceSize / 2 - 5 * dashSize, -225) + unitsFontM.getHeight() / 4);
215
216        // Show numeric speed
217        String speedString = Integer.toString(speedDigits);
218        int digitsFontSize = (int) (fontSize * 1.5);
219        Font digitsSizedFont = new Font("Serif", Font.PLAIN, digitsFontSize);
220        g2.setFont(digitsSizedFont);
221        FontMetrics digitsFontM = g2.getFontMetrics(digitsSizedFont);
222
223        // draw a box around the digital speed
224        int pad = (int) (digitsFontSize * 0.2);
225        int h = (int) (digitsFontM.getAscent() * 0.8);
226        int w = digitsFontM.stringWidth("999");
227        if (pad < 2) {
228            pad = 2;
229        }
230        g2.setColor(Color.LIGHT_GRAY);
231        g2.fillRect(-w / 2 - pad, 2 * faceSize / 5 - h - pad, w + pad * 2, h + pad * 2);
232        g2.setColor(Color.DARK_GRAY);
233        g2.drawRect(-w / 2 - pad, 2 * faceSize / 5 - h - pad, w + pad * 2, h + pad * 2);
234
235        g2.setColor(Color.BLACK);
236        g2.drawString(speedString, -digitsFontM.stringWidth(speedString) / 2, 2 * faceSize / 5);
237    }
238
239    // Method to provide the cartesian x coordinate given a radius and angle (in degrees)
240    int dotX(float radius, float angle) {
241        int xDist;
242        xDist = (int) Math.round(radius * Math.cos(Math.toRadians(angle)));
243        return xDist;
244    }
245
246    // Method to provide the cartesian y coordinate given a radius and angle (in degrees)
247    int dotY(float radius, float angle) {
248        int yDist;
249        yDist = (int) Math.round(radius * Math.sin(Math.toRadians(angle)));
250        return yDist;
251    }
252
253    // Method called on resizing event - sets various sizing variables
254    // based on the size of the resized panel and scales the logo/hands
255    public void scaleFace() {
256        int panelHeight = this.getSize().height;
257        int panelWidth = this.getSize().width;
258        size = Math.min(panelHeight, panelWidth);
259        faceSize = (int) (size * .97);
260        if (faceSize == 0) {
261            faceSize = 1;
262        }
263
264        // Had trouble getting the proper sizes when using Images by themselves so
265        // use the NamedIcon as a source for the sizes
266        int logoScaleWidth = faceSize / 6;
267        int logoScaleHeight = (int) ((float) logoScaleWidth * (float) jmriIcon.getIconHeight() / jmriIcon.getIconWidth());
268        scaledLogo = logo.getScaledInstance(logoScaleWidth, logoScaleHeight, Image.SCALE_SMOOTH);
269        scaledIcon.setImage(scaledLogo);
270        logoWidth = scaledIcon.getIconWidth();
271        logoHeight = scaledIcon.getIconHeight();
272
273        scaleRatio = faceSize / 2.7F / minuteHeight;
274        for (int i = 0; i < minuteX.length; i++) {
275            scaledMinuteX[i] = (int) (minuteX[i] * scaleRatio);
276            scaledMinuteY[i] = (int) (minuteY[i] * scaleRatio);
277        }
278        scaledMinuteHand = new Polygon(scaledMinuteX, scaledMinuteY, scaledMinuteX.length);
279
280        centreX = panelWidth / 2;
281        centreY = panelHeight / 2;
282    }
283
284    void update(float speed) {
285        // hand rotation starts at 12 o'clock position so offset it by 120 degrees
286        // scale by the angle between major tick marks divided by 10
287        if (units == Speed.MPH) {
288            if (Speed.kphToMph(speed) > mphLimit) {
289                mphLimit += mphInc;
290                kphLimit += kphInc;
291            }
292            setTicks();
293            speedDigits = Math.round(Speed.kphToMph(speed));
294            speedAngle = -120 + Speed.kphToMph(speed * priMajorTick / 10);
295        } else {
296            if (speed > kphLimit) {
297                mphLimit += mphInc;
298                kphLimit += kphInc;
299            }
300            setTicks();
301            speedDigits = Math.round(speed);
302            speedAngle = -120 + speed * priMajorTick / 10;
303        }
304        repaint();
305    }
306
307    void setTicks() {
308        if (units == Speed.MPH) {
309            priMajorTick = 240 / ((float) mphLimit / 10);
310            priMinorTick = priMajorTick / 5;
311            secTick = 240 / (Speed.mphToKph(mphLimit) / 10);
312        } else {
313            priMajorTick = 240 / ((float) kphLimit / 10);
314            priMinorTick = priMajorTick / 5;
315            secTick = 240 / (Speed.kphToMph(kphLimit) / 10);
316        }
317    }
318
319    void setUnitsMph() {
320        units = Speed.MPH;
321        priString = "MPH";
322        secString = "KPH";
323    }
324
325    void setUnitsKph() {
326        units = Speed.KPH;
327        priString = "KPH";
328        secString = "MPH";
329    }
330
331    public void reset() {
332        mphLimit = baseMphLimit;
333        kphLimit = baseKphLimit;
334        update(0.0f);
335    }
336}