001package jmri.jmrix.loconet.duplexgroup.swing;
002
003import java.awt.BasicStroke;
004import java.awt.Dimension;
005import java.awt.Font;
006import java.awt.event.ActionEvent;
007import java.awt.event.ActionListener;
008import javax.swing.BoxLayout;
009import javax.swing.JLabel;
010import javax.swing.JPanel;
011import javax.swing.JSeparator;
012import jmri.jmrix.loconet.LnConstants;
013import jmri.jmrix.loconet.LnTrafficController;
014import jmri.jmrix.loconet.LocoNetListener;
015import jmri.jmrix.loconet.LocoNetMessage;
016import jmri.jmrix.loconet.LocoNetSystemConnectionMemo;
017import jmri.jmrix.loconet.duplexgroup.LnDplxGrpInfoImplConstants;
018import org.slf4j.Logger;
019import org.slf4j.LoggerFactory;
020
021/**
022 * Defines a GUI and associated logic to perform energy scan operations on
023 * Duplex radio channels. Displays energy scan data in a graphical form.
024 * <p>
025 * This tool works equally well with UR92 and UR92CE devices. The UR92 and
026 * UR92CE behave identically with respect to this tool. For the purpose of
027 * clarity, only the term UR92 is used herein.
028 *
029 * @author B. Milhaupt Copyright 2010, 2011
030 */
031public class DuplexGroupScanPanel extends jmri.jmrix.loconet.swing.LnPanel
032        implements LocoNetListener, javax.swing.event.ChangeListener {
033
034    DuplexChannelInfo dci[] = new DuplexChannelInfo[LnDplxGrpInfoImplConstants.DPLX_MAX_CH - LnDplxGrpInfoImplConstants.DPLX_MIN_CH + 1];
035    private javax.swing.Timer tmr;
036    DuplexGroupScanPanel safe;
037
038    private final static int DEFAULT_SCAN_COUNT = 25;
039    private boolean isInitialized = false;
040
041    public DuplexGroupScanPanel() {
042        super();
043        memo = null;
044        safe = this;
045    }
046
047    javax.swing.JButton scanLoopButton = null;
048    javax.swing.JLabel scanLoopLabel = null;
049    javax.swing.JButton clearButton = null;
050    javax.swing.JLabel grStatusValue = null;
051    boolean stopRequested;
052    Integer scanLoopDelay;
053    boolean waitingForPreviousGroupChannel;
054    int previousGroupChannel;
055//    Dimension channelTextSize;
056
057    /**
058     * {@inheritDoc}
059     */
060    @Override
061    public void initComponents() {
062        int i;
063        int j;
064        int minWindowWidth = 0;
065        JPanel p;
066        j = 0;
067
068        for (i = LnDplxGrpInfoImplConstants.DPLX_MIN_CH; i <= LnDplxGrpInfoImplConstants.DPLX_MAX_CH; ++i) {
069            dci[j] = new DuplexChannelInfo();
070            dci[j].channel = i;
071            dci[j].numSamples = 0;
072            dci[j].maxSigValue = -1;
073            dci[j].minSigValue = 256;
074            dci[j].sumSamples = 0;
075            dci[j].avgSamples = 0;
076            dci[j].mostRecentSample = -1;
077            j++;
078        }
079
080        grStatusValue = new javax.swing.JLabel(" ");
081        clearButton = new javax.swing.JButton(Bundle.getMessage("ButtonClearScanData"));
082        scanLoopButton = new javax.swing.JButton(Bundle.getMessage("ButtonScanChannelsLoop"));
083        clearButton.setToolTipText(Bundle.getMessage("ToolTipButtonClearScanData"));
084        scanLoopButton.setToolTipText(Bundle.getMessage("ToolTipButtonScanChannelsLoop"));
085        setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
086
087        p = new JPanel();
088        graphicArea = new DuplexGroupChannelScanGuiCanvas();
089        p.add(graphicArea);
090        add(p);
091
092        p = new JPanel();
093        p.setLayout(new java.awt.GridLayout(4, 1));
094
095        JLabel graphicAreaLabel1 = new JLabel(Bundle.getMessage("LabelGraphicArea1"));
096        graphicAreaLabel1.setFont(new Font("Dialog", Font.PLAIN, 10));
097        p.add(graphicAreaLabel1);
098
099        JLabel graphicAreaLabel2 = new JLabel(Bundle.getMessage("LabelGraphicArea2"));
100        graphicAreaLabel2.setFont(new Font("Dialog", Font.PLAIN, 10));
101        p.add(graphicAreaLabel2);
102
103        JLabel graphicAreaLabel3 = new JLabel(Bundle.getMessage("LabelGraphicArea3"));
104        graphicAreaLabel3.setFont(new Font("Dialog", Font.PLAIN, 10));
105        p.add(graphicAreaLabel3);
106        add(p);
107
108        JLabel graphicAreaLabel4 = new JLabel(Bundle.getMessage("LabelGraphicArea4"));
109        graphicAreaLabel4.setFont(new Font("Dialog", Font.PLAIN, 10));
110        p.add(graphicAreaLabel4);
111        add(p);
112
113        p = new JPanel();
114        p.setLayout(new java.awt.FlowLayout());
115        p.add(clearButton);
116        p.add(scanLoopButton);
117        stopRequested = false;
118        add(p);
119
120        p = new JPanel();
121        p.setLayout(new java.awt.FlowLayout());
122        add(new JSeparator());
123        p.add(grStatusValue);
124        add(p);
125        p = new JPanel();
126        // Apply a rigid area with a width that is wide enough to display the longest status message
127        try {
128            minWindowWidth = Integer.parseInt(Bundle.getMessage("MinimumWidthForWindow"), 10);
129        } catch (Exception e) {
130            minWindowWidth = 400;
131        }
132
133        p.add(javax.swing.Box.createRigidArea(new java.awt.Dimension(minWindowWidth, 0)));
134        add(p);
135
136        scanLoopButton.addActionListener(new java.awt.event.ActionListener() {
137            @Override
138            public void actionPerformed(java.awt.event.ActionEvent e) {
139                if (scanLoopButton.getText().equals(Bundle.getMessage("ButtonScanChannelsStop"))) {
140                    scanLoopStopButtonActionPerformed();
141                } else {
142                    scanLoopButton.setText(Bundle.getMessage("ButtonScanChannelsStop"));
143                    scanLoopButtonActionPerformed();
144                }
145            }
146        });
147
148        clearButton.addActionListener(new java.awt.event.ActionListener() {
149            @Override
150            public void actionPerformed(java.awt.event.ActionEvent e) {
151                scanLoopStopButtonActionPerformed();
152                clearButtonActionPerformed();
153                graphicArea.repaint();
154            }
155        });
156
157        // send message to get current Duplex Channel number
158        try {
159            scanLoopDelay = Integer.parseInt(Bundle.getMessage("SetupDefaultChannelDelayInMilliSec"));
160        } catch (Exception e) {
161            log.error("Bad value in prop files for SetupDefaultChannelDelayInMilliSec.");
162            scanLoopDelay = 200;
163        }
164        if (memo != null) {
165            isInitialized = true;
166        }
167
168    }
169
170    /**
171     * {@inheritDoc}
172     */
173    @Override
174    public String getHelpTarget() {
175        return "package.jmri.jmrix.loconet.duplexgroup.DuplexGroupTabbedPanel"; // NOI18N replacement UR92
176    } // NOI18N
177
178    /**
179     * {@inheritDoc}
180     */
181    @Override
182    public String getTitle() {
183        return Bundle.getMessage("ScanTitle");
184    }
185
186    /**
187     * {@inheritDoc}
188     */
189    @Override
190    public void initComponents(LocoNetSystemConnectionMemo memo) {
191        super.initComponents(memo);
192
193        // connect to the LnTrafficController
194        connect(memo.getLnTrafficController());
195        waitingForPreviousGroupChannel = true;
196        memo.getLnTrafficController().sendLocoNetMessage(createGetGroupChannelPacketInt());
197        if (grStatusValue != null) {
198            isInitialized = true;
199        }
200    }
201
202    public boolean isInitialized() {
203        return isInitialized;
204    }
205
206    /**
207     * Process all incoming LocoNet messages to look for Duplex Group
208     * information operations. Only pays attention to LocoNet report of Duplex
209     * Group Name/password/channel/groupID, and ignores all other LocoNet
210     * messages.
211     * <p>
212     * If tool has sent a query for Duplex group information and has not yet
213     * received a Duplex group report, the method updates the GUI with the
214     * received information.
215     * <p>
216     * If the tool is not currently waiting for a response to a query, then the
217     * method compares the received information against the information
218     * currently displayed in the GUI. If the received information does not
219     * match, a message is displayed on the status line in the GUI, else nothing
220     * is displayed in the GUI status line.
221     */
222    @Override
223    public void message(LocoNetMessage m) {
224        if (stopRequested == true) {
225            return;
226        }
227        if (handleMessageDuplexScanReport(m)) {
228            return;
229        }
230        if (handleMessageDuplexChannelReport(m)) {
231            return;
232        }
233        return;
234    }
235
236    /**
237     * Examines incoming LocoNet messages to see if the message is a Duplex
238     * Group Channel Report. If so, captures the group number.
239     *
240     * @param m  incoming LocoNetMessage
241     * @return true if message m is a Duplex Group Channel Report
242     */
243    private boolean handleMessageDuplexChannelReport(LocoNetMessage m) {
244        if ((m.getOpCode() != LnConstants.OPC_PEER_XFER)
245                || (m.getElement(1) != LnConstants.RE_DPLX_OP_LEN)
246                || (m.getElement(2) != LnConstants.RE_DPLX_GP_CHAN_TYPE)
247                || (m.getElement(3) != LnConstants.RE_DPLX_SCAN_REPORT_B3)) {
248            return false;
249        }
250        if (waitingForPreviousGroupChannel) {
251            waitingForPreviousGroupChannel = false;
252            previousGroupChannel = m.getElement(5);  // capture Group Channel Number
253        }
254        return true;
255    }
256
257    /**
258     * Interprets a received LocoNet message. If message is an IPL report of
259     * attached IPL-capable equipment, check to see if it reports a UR92 device
260     * as attached. If so, increment count of UR92 devices. Else ignore.
261     *
262     * @return true if message is an IPL device report indicating a UR92
263     *         present, else return false.
264     */
265    private boolean handleMessageDuplexScanReport(LocoNetMessage m) {
266        if ((m.getOpCode() != LnConstants.OPC_PEER_XFER)
267                || (m.getElement(1) != LnConstants.RE_DPLX_SCAN_OP_LEN)
268                || (m.getElement(2) != LnConstants.RE_DPLX_SCAN_REPORT_B2)
269                || (m.getElement(3) != LnConstants.RE_DPLX_SCAN_REPORT_B3)) {
270            return false;
271        }
272        handleChannelSignalReport(m.getElement(4), m.getElement(5), m.getElement(6));
273        return true;
274    }
275
276    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value="SLF4J_SIGN_ONLY_FORMAT",
277                                                        justification="I18N of log message")
278    private void handleChannelSignalReport(int extendedVal, int channelNum, int signalValue) {
279        int index = -1;
280        int fullSignal;
281        fullSignal = signalValue + 128 * (((extendedVal & 0x2) == 2) ? 1 : 0);
282        for (int i = 0; i < dci.length; i++) {
283            if (dci[i].channel == channelNum) {
284                index = i;
285            }
286        }
287        if (index != -1) {
288            if (index == 16) {
289                log.error("{}\n", Bundle.getMessage("ErrorLogUnexpectedChannelNumber", channelNum));
290
291            }
292            dci[index].numSamples++;
293            dci[index].mostRecentSample = fullSignal;
294            if (fullSignal > dci[index].maxSigValue) {
295                dci[index].maxSigValue = fullSignal;
296            }
297            if (fullSignal < dci[index].minSigValue) {
298                dci[index].minSigValue = fullSignal;
299            }
300            dci[index].sumSamples += fullSignal;
301            dci[index].avgSamples = dci[index].sumSamples / dci[index].numSamples;
302
303            graphicArea.repaint();
304
305        } else {
306            log.error("{}",Bundle.getMessage("ErrorLogUnexpectedChannelNumber", channelNum));
307        }
308    }
309
310    /**
311     * Creates a LocoNet message containing a channel-specific query for signal
312     * information from UR92 device(s).
313     *
314     * @param channelNum  integer between 11 and 26, inclusive
315     * @return LocoNetMessage - query for Dulpex Channel Scan information
316     */
317    private LocoNetMessage createDuplexScanQueryPacket(int channelNum) {
318        int i = 0;
319        LocoNetMessage m = new LocoNetMessage(LnConstants.RE_DPLX_OP_LEN);
320
321        m.setElement(i++, LnConstants.OPC_PEER_XFER);
322        m.setElement(i++, LnConstants.RE_DPLX_OP_LEN);   // 20-byte message
323        m.setElement(i++, LnConstants.RE_DPLX_SCAN_QUERY_B2);   // Duplex Group Scan Query type
324        m.setElement(i++, LnConstants.RE_DPLX_SCAN_QUERY_B3);   // Query Operation
325        m.setElement(i++, LnConstants.RE_DPLX_SCAN_QUERY_B4);
326        m.setElement(i++, channelNum);                                  // Duplex Channel Number
327        for (; i < (LnConstants.RE_DPLX_OP_LEN - 1); i++) {
328            m.setElement(i, 0);   // always 0 for duplex group ID write
329        }
330        // LocoNet send process will compute and add checksum byte in correct location
331        return m;
332    }
333
334    /**
335     * Create a LocoNet packet to get the current Duplex group channel number.
336     *
337     * @return The packet which writes the Group Channel Number to the UR92
338     *         device(s)
339     */
340    private LocoNetMessage createGetGroupChannelPacketInt() {
341        int i;
342
343        // format packet
344        LocoNetMessage m = new LocoNetMessage(LnConstants.RE_DPLX_OP_LEN);
345
346        i = 0;
347        m.setElement(i++, LnConstants.OPC_PEER_XFER);
348        m.setElement(i++, LnConstants.RE_DPLX_OP_LEN);   // 20-byte message
349        m.setElement(i++, LnConstants.RE_DPLX_GP_CHAN_TYPE);   // Group Channel Operation
350        m.setElement(i++, LnConstants.RE_DPLX_OP_TYPE_QUERY);   // Write Operation
351        for (; i < (LnConstants.RE_DPLX_OP_LEN - 1); i++) {
352            m.setElement(i, 0);   // always 0 for duplex group channel query
353        }
354        // LocoNet send process will compute and add checksum byte in correct location
355        return m;
356    }
357
358    int channelIndexToScan;
359    int maxChannelIndexToScan;
360    int loopNum;
361    Integer whenToStop;
362
363    private void updateScanLoopCountStatus(int current, int total) {
364//        String countStatus = Bundle.getMessage("StatusCurrentLoopCounter"); // much easier using Bundle.getMessage variables
365//        String begin = countStatus.substring(0, countStatus.indexOf("%count")); // NOI18N
366//
367//        String middle = countStatus.substring(begin.length() + 6, countStatus.indexOf("%loops")); // NOI18N
368//        String end = countStatus.substring(countStatus.indexOf("%loops") + 6); // NOI18N
369//        countStatus = begin + Integer.toString(current) + middle + Integer.toString(total) + end;
370        grStatusValue.setText(Bundle.getMessage("StatusCurrentLoopCounter", current, total));
371    }
372
373    private void scanLoopButtonActionPerformed() {
374        loopNum = 1;
375        try {
376            whenToStop = Integer.parseInt(Bundle.getMessage("SetupNumberOfLoops"));
377        } catch (Exception e) {
378            whenToStop = DEFAULT_SCAN_COUNT;
379        }
380        if ((whenToStop <= 0) || (whenToStop > 1000)) {
381            grStatusValue.setText(Bundle.getMessage("ErrorBadLoopCount"));
382            return;
383        }
384        grStatusValue.setText(" ");
385        stopRequested = false;
386
387        channelIndexToScan = 0;
388        maxChannelIndexToScan = LnDplxGrpInfoImplConstants.DPLX_MAX_CH - LnDplxGrpInfoImplConstants.DPLX_MIN_CH;
389        updateScanLoopCountStatus(loopNum, whenToStop);
390
391        tmr = new javax.swing.Timer(scanLoopDelay, new ActionListener() {
392            @Override
393            public void actionPerformed(ActionEvent e) {
394                tmr.stop();
395                if (stopRequested == true) {
396                    stopRequested = false;
397                    showOnlyMaxAvgValues();
398                } else if (channelIndexToScan <= maxChannelIndexToScan) {
399
400                    graphicArea.setChannelBeingScanned(dci[channelIndexToScan].channel);
401                    graphicArea.repaint();
402                    memo.getLnTrafficController().sendLocoNetMessage(createDuplexScanQueryPacket(dci[channelIndexToScan].channel));
403                    tmr.setInitialDelay(scanLoopDelay);
404                    tmr.setRepeats(false);
405                    tmr.start();
406                    channelIndexToScan++;
407                } else if (loopNum < whenToStop) {
408                    loopNum++;
409                    // update displayed Loop Count
410                    updateScanLoopCountStatus(loopNum, whenToStop);
411
412                    channelIndexToScan = 0;
413                    graphicArea.setChannelBeingScanned(dci[channelIndexToScan].channel);
414                    graphicArea.repaint();
415
416                    memo.getLnTrafficController().sendLocoNetMessage(createDuplexScanQueryPacket(dci[channelIndexToScan].channel));
417                    tmr.setInitialDelay(scanLoopDelay);
418                    tmr.setRepeats(false);
419                    tmr.start();
420                    channelIndexToScan++;
421                } else {
422                    // must be done with all channels and all loops.
423                    showOnlyMaxAvgValues();
424                    scanLoopButton.setText(Bundle.getMessage("ButtonScanChannelsLoop"));
425                    scanLoopStopButtonActionPerformed();
426                    grStatusValue.setText(" ");
427                    graphicArea.setChannelBeingScanned(-1);
428                    graphicArea.repaint();
429                }
430            }
431        });
432        // need to trigger first delay to get first channel to be scanned
433        tmr.setInitialDelay(scanLoopDelay);
434        tmr.setRepeats(false);
435        tmr.start();
436        return;
437    }
438
439    private void scanLoopStopButtonActionPerformed() {
440        scanLoopButton.setText(Bundle.getMessage("ButtonScanChannelsLoop"));
441        graphicArea.setChannelBeingScanned(-1);
442        graphicArea.repaint();
443        grStatusValue.setText(" ");
444        stopRequested = true;
445    }
446
447    private void clearButtonActionPerformed() {
448        int index;
449        int maxIndex;
450        maxIndex = LnDplxGrpInfoImplConstants.DPLX_MAX_CH - LnDplxGrpInfoImplConstants.DPLX_MIN_CH;
451        for (index = 0; index <= maxIndex; ++index) {
452            dci[index].numSamples = 0;
453            dci[index].maxSigValue = -1;
454            dci[index].minSigValue = 256;
455            dci[index].sumSamples = 0;
456            dci[index].avgSamples = 0;
457            dci[index].mostRecentSample = -1;
458        }
459        return;
460    }
461
462    public void connect(LnTrafficController t) {
463        if (t != null) {
464            // connect to the LnTrafficController if the connection is a valid LocoNet connection
465            t.addLocoNetListener(~0, this);
466        }
467    }
468
469    /**
470     * Break connection with the LnTrafficController and stop timers.
471     */
472    @Override
473    public void dispose() {
474        javax.swing.Timer exitTmr;
475
476        stopRequested = true;
477        if (tmr != null) {
478            tmr.stop();
479        }
480        tmr = null;
481
482        if (waitingForPreviousGroupChannel == false) {
483            exitTmr = new javax.swing.Timer(200, new ActionListener() {
484                @Override
485                public void actionPerformed(ActionEvent e) {
486                    if (memo.getLnTrafficController() != null) {
487                        memo.getLnTrafficController().removeLocoNetListener(~0, safe);
488                    }
489                    safe.dispose();
490                }
491            });
492            exitTmr.setInitialDelay(200);
493            exitTmr.setRepeats(false);
494            exitTmr.start();
495            while (exitTmr.isRunning()) {
496                // wait for timer to run out before releasing LocoNet traffic controller listener
497            }
498            exitTmr.stop();
499        }
500        if (memo.getLnTrafficController() != null) {
501            memo.getLnTrafficController().removeLocoNetListener(~0, this);
502        }
503        super.dispose();
504    }
505
506    private final static Logger log = LoggerFactory.getLogger(DuplexGroupScanPanel.class);
507
508    @Override
509    public void stateChanged(javax.swing.event.ChangeEvent e) {
510        graphicArea.repaint();
511    }
512
513    private void showOnlyMaxAvgValues() {
514        for (int i = 0; i < (LnDplxGrpInfoImplConstants.DPLX_MAX_CH - LnDplxGrpInfoImplConstants.DPLX_MIN_CH) + 1; ++i) {
515            dci[i].mostRecentSample = -1;
516        }
517        graphicArea.repaint();
518    }
519
520    private DuplexGroupChannelScanGuiCanvas graphicArea;
521
522    private class DuplexGroupChannelScanGuiCanvas extends java.awt.Canvas {
523
524        private int barWidth = 7;
525        private int barSpace = barWidth + 8;
526        private int barOffset = (barSpace - barWidth) / 2;
527        private final static int channelCount = 26 - 11 + 1;
528        private final static int barGraphScale = 2;
529        private final static int maxScanValue = 255;
530        private final static int maxScaledBarValue = ((maxScanValue + 1) / barGraphScale);
531        private int baseline = maxScaledBarValue + 5;
532
533        public int requiredMinWindowWidth = (channelCount * barSpace);
534        public int requiredMinWindowHeight = (baseline + 10);
535        private static final int HORIZ_PADDING = 12;
536        private static final int VERT_PADDING = 4;
537        private int indexBeingScanned = -1;
538        private Dimension channelTextSize;
539
540        Font signalBarsFont;
541        private final java.awt.Color foregroundColor = java.awt.Color.WHITE;
542        private final java.awt.Color backgroundColor = java.awt.Color.BLACK;
543        private final java.awt.Color recommendationLineColor = java.awt.Color.YELLOW;
544        private final java.awt.Color valueBarColor = java.awt.Color.CYAN;
545        private final java.awt.Color maxLineColor = java.awt.Color.RED;
546        private final java.awt.Color averageLineColor = java.awt.Color.GREEN;
547        private final java.awt.Color lowerLimitLineColor = java.awt.Color.LIGHT_GRAY;
548
549        public DuplexGroupChannelScanGuiCanvas() {
550            super();
551            setBackground(backgroundColor);
552            setForeground(foregroundColor);
553            // create a smaller font
554            signalBarsFont = new Font("Dialog", Font.PLAIN, 8);
555
556            int textHeight = 0;
557            int textWidth = 0;
558
559            // get metrics from the graphics
560            java.awt.FontMetrics metrics = getFontMetrics(signalBarsFont);
561            // get the height of a line of text in this font and render context
562            textHeight = metrics.getHeight();
563            // get the advance of my text in this font and render context
564            textWidth = metrics.stringWidth("38");  // representative (but not accurate) example text string // NOI18N
565            // calculate the size of a box to hold the text with some padding.
566            channelTextSize = new Dimension(textWidth + HORIZ_PADDING, textHeight + VERT_PADDING);
567            requiredMinWindowWidth = channelCount * channelTextSize.width;
568            requiredMinWindowHeight += (2 * channelTextSize.height);
569            baseline += channelTextSize.height;
570            barSpace = channelTextSize.width;
571            barWidth = textWidth;
572            barOffset = (barSpace - barWidth) / 2;
573            textWidth = 0;
574            setSize(requiredMinWindowWidth, requiredMinWindowHeight);
575        }
576
577        /**
578         * Used by this class to specify a channel number to highlight in the
579         * GUI. An invalid channel number may be used to cause the class to
580         * clear the channel highlight. After invoking this method, a call to
581         * this class' repaint() method is required to cause the GUI update.
582         *
583     * @param channelNum  integer representing a Duplex Group channel
584         *                   number.
585         */
586        public void setChannelBeingScanned(int channelNum) {
587            if ((channelNum < 11) || (channelNum > 26)) {
588                indexBeingScanned = -1;
589                return;
590            }
591            indexBeingScanned = channelNum - 11;
592        }
593
594        final float dash1[] = {7.0f, 3.0f};
595        final BasicStroke dashedStroke = new BasicStroke(1.0f,
596                BasicStroke.CAP_BUTT,
597                BasicStroke.JOIN_MITER,
598                10.0f, dash1, 0.0f);
599
600        final BasicStroke plainStroke = new BasicStroke(1.0f);
601
602        @Override
603        public void paint(java.awt.Graphics g) {
604            int channelIndex;
605            java.awt.Graphics2D g2;
606            if (g instanceof java.awt.Graphics2D) {
607                g2 = (java.awt.Graphics2D) g;
608            } else {
609                log.error("paint() cannot cast object g to Graphics2D.  Aborting paint().");
610                return;
611            }
612            for (int i = 11; i <= 26; ++i) {
613                g2.drawString(Integer.toString(i), (i - 11) * channelTextSize.width, channelTextSize.height);
614                g2.drawString(Integer.toString(i), (i - 11) * channelTextSize.width, requiredMinWindowHeight - 1);
615            }
616
617            // draw a simple line to act as a bottom line in the graphic block
618            g2.setColor(lowerLimitLineColor);
619            g2.draw(new java.awt.geom.Line2D.Float(0, baseline + 1,
620                    requiredMinWindowWidth - 1, baseline + 1));
621
622            // draw a bar, average line and max line for each channel
623            for (channelIndex = 0; channelIndex < 16; ++channelIndex) {
624                redrawSignalBar(g2, dci[channelIndex]);
625            }
626            // draw a diamond for the area showing the channel being scanned
627            redrawChannelAtIndicator(g2, indexBeingScanned);
628
629            // draw the recommended limit line
630            g2.setColor(recommendationLineColor);
631            g2.setStroke(dashedStroke);
632            g2.draw(new java.awt.geom.Line2D.Float(1, baseline - (96 / barGraphScale),
633                    requiredMinWindowWidth - 1, baseline - (96 / barGraphScale)));
634            g2.setStroke(plainStroke);
635        }
636
637        private void redrawSignalBar(java.awt.Graphics2D g2, DuplexChannelInfo dci) {
638            int index = dci.channel - 11;
639            int current = dci.mostRecentSample;
640            int max = dci.maxSigValue;
641            int avg = dci.avgSamples;
642            if (avg < 0) {
643                avg = 0;
644            }
645            int numSamples = dci.numSamples;
646
647            if (current > 0) {
648                int upperX;
649                int upperY;
650                int width;
651                int height;
652
653                upperX = (barSpace * index);
654                width = barSpace;
655
656                // clear anything above the "bottoms line"
657                upperY = baseline - maxScaledBarValue;
658                height = (maxScaledBarValue - 1);
659                g2.setColor(backgroundColor);
660                g2.fillRect(upperX, upperY,
661                        width, height - 1);
662
663                // draw the filled rectangle for the current value.
664                upperY = baseline - (current / barGraphScale);
665                g2.setColor(valueBarColor);
666                g2.fillRect(upperX + barOffset, upperY,
667                        barWidth, (current / barGraphScale));
668
669            } else {
670                // clear anything above the "bottoms line"
671                g2.setColor(backgroundColor);
672                g2.fillRect(
673                        (barOffset + (barSpace * index)), ((baseline - maxScaledBarValue) - 1),
674                        barWidth, maxScaledBarValue);
675            }
676
677            if (numSamples > 1) {
678                // draw the line for the average value.
679                g2.setColor(averageLineColor);
680                g2.draw(new java.awt.geom.Line2D.Float(
681                        (barSpace * index) + 1, ((baseline - (avg / barGraphScale)) - 1),
682                        (barSpace * (index + 1)) - 2, (baseline - (avg / barGraphScale)) - 1));
683            }
684
685            // draw the line for the max value.
686            if (max >= 0) {
687                g2.setColor(maxLineColor);
688                g2.draw(new java.awt.geom.Line2D.Float(
689                        (barSpace * index) + 1, ((baseline - (max / barGraphScale)) - 1),
690                        (barSpace * (index + 1)) - 2, (baseline - (max / barGraphScale)) - 1));
691            }
692        }
693
694        private void redrawChannelAtIndicator(java.awt.Graphics2D g2, int channelIndex) {
695            int upperX;
696            int upperY;
697            int width;
698            int height;
699            // clear anything below the "bottoms line"
700            upperY = baseline + 2;
701            height = requiredMinWindowHeight - upperY - channelTextSize.height - 2;
702            upperX = 0;
703            width = requiredMinWindowWidth;
704            g2.setColor(backgroundColor);
705            g2.fillRect(upperX, upperY,
706                    width, height - 1);
707
708            // show the highlight only if a valid channel index (1-16) is specified
709            if ((channelIndex >= 0) && (channelIndex < 16)) {
710                // draw a diamond in black using polyline mechanisms
711                g2.setColor(foregroundColor);
712                int x2Points[] = {(channelIndex * barSpace) + (barSpace / 2),
713                    (channelIndex * barSpace) + barOffset,
714                    channelIndex * barSpace + (barSpace / 2),
715                    (channelIndex * barSpace) + (barSpace - barOffset)};
716                int y2Points[] = {baseline + 2, baseline + 5, baseline + 8, baseline + 5};
717                java.awt.geom.GeneralPath polygon
718                        = new java.awt.geom.GeneralPath(java.awt.geom.GeneralPath.WIND_EVEN_ODD,
719                                x2Points.length);
720
721                polygon.moveTo(x2Points[0], y2Points[0]);
722
723                for (int index = 1; index < x2Points.length; index++) {
724                    polygon.lineTo(x2Points[index], y2Points[index]);
725                }
726                polygon.closePath();
727                g2.draw(polygon);
728            }
729        }
730    }
731
732    /**
733     * Implements a basic structure for tracking Duplex Radio channel energy
734     * scan information.
735     *
736     * @author B. Milhaupt Copyright 2010, 2011
737     */
738    private static class DuplexChannelInfo {
739
740        public int channel;
741        public int numSamples;
742        public int maxSigValue;
743        public int minSigValue;
744        public int sumSamples;
745        public int avgSamples;
746        public int mostRecentSample;
747
748        public DuplexChannelInfo() {
749            channel = -1;
750            numSamples = 0;
751            maxSigValue = -1;
752            minSigValue = 1000;
753            sumSamples = 0;
754            avgSamples = 0;
755            mostRecentSample = -1;
756        }
757    }
758
759}