001package jmri.jmrit.logix;
002
003import javax.annotation.Nonnull;
004import java.awt.Color;
005import java.awt.Component;
006import java.awt.Font;
007import java.awt.event.ActionEvent;
008import java.util.ArrayList;
009import java.util.LinkedList;
010import java.util.List;
011
012import javax.swing.AbstractListModel;
013import javax.swing.Box;
014import javax.swing.BoxLayout;
015import javax.swing.JButton;
016import javax.swing.JLabel;
017import javax.swing.JDialog;
018import javax.swing.JList;
019import javax.swing.JOptionPane;
020import javax.swing.JPanel;
021import javax.swing.ListCellRenderer;
022import javax.swing.event.ListSelectionEvent;
023import javax.swing.event.ListSelectionListener;
024
025import jmri.Block;
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029import jmri.JmriException;
030import jmri.jmrit.display.LocoIcon;
031
032/**
033 * Track an occupied block to adjacent blocks becoming occupied.
034 *
035 * @author Pete Cressman Copyright (C) 2013
036 */
037public class Tracker {
038
039    private static final String TRACKER_NO_CURRENT_BLOCK = "TrackerNoCurrentBlock";
040    TrackerTableAction _parent;
041    private final String _trainName;
042    private ArrayList<OBlock> _headRange; // blocks reachable from head block
043    private ArrayList<OBlock> _tailRange; // blocks reachable from tail block
044    private final ArrayList<OBlock> _lostRange = new ArrayList<>(); // blocks that lost detection
045    private final LinkedList<OBlock> _occupies = new LinkedList<>();     // blocks occupied by train
046    long _startTime;
047    String _statusMessage;
048    private final Color _markerForeground;
049    private final Color _markerBackground;
050    private final Font _markerFont;
051    private OBlock _darkBlock = null;
052    enum PathSet {NOWAY, NOTSET, PARTIAL, SET}
053
054    /**
055     *
056     * @param block the starting block to track
057     * @param name  the name of the train being tracked
058     * @param marker icon if LocoIcon was dropped on a block
059     * @param tta TrackerTableAction that manages Trackers
060     */
061    Tracker(OBlock block, String name, LocoIcon marker, TrackerTableAction tta) {
062        _trainName = name;
063        _parent = tta;
064        _markerForeground = block.getMarkerForeground();
065        _markerBackground = block.getMarkerBackground();
066        _markerFont = block.getMarkerFont();
067        block.setState(block.getState() & ~OBlock.RUNNING); // jiggle-jiggle
068        addtoOccupies(block, true);
069        _startTime = System.currentTimeMillis();
070        block._entryTime = _startTime;
071        List<OBlock> occupy = initialRange(_parent);
072        if (!occupy.isEmpty()) {
073            new ChooseStartBlock(block, occupy, this, _parent);
074        } else {
075            _parent.addTracker(this);
076        }
077        if (marker != null) {
078            marker.dock();
079        }
080    }
081
082    private List<OBlock> initialRange(TrackerTableAction parent) {
083        makeRange();
084        if (getHeadBlock().equals(getTailBlock())) {
085            return makeChoiceList(_headRange, parent);
086        } else { // make additional block the tail
087            return makeChoiceList(_tailRange, parent);
088        }
089    }
090    
091    private List<OBlock> makeChoiceList(List<OBlock> range, TrackerTableAction parent) {
092        ArrayList<OBlock> occupy = new ArrayList<>();
093        for (OBlock b : range) {
094            if (!_occupies.contains(b) && 
095                    ((b.getState() & Block.OCCUPIED) != 0 || (b.getState() & Block.UNDETECTED) != 0)
096                    && parent.checkBlock(b)) {
097                occupy.add(b);
098            }
099        }
100        return occupy;
101    }
102
103    /*
104     * Jiggle state so Indicator icons show block value
105     */
106    private void showBlockValue(OBlock block) {
107        block.setValue(_trainName);
108        block.setMarkerBackground(_markerBackground);
109        block.setMarkerForeground(_markerForeground);
110        block.setMarkerFont(_markerFont);
111        block.setState(block.getState() | OBlock.RUNNING);
112    }
113
114    protected String getTrainName() {
115        return _trainName;
116    }
117
118    protected final OBlock getHeadBlock() {
119        return _occupies.peekFirst();
120    }
121
122    protected final OBlock getTailBlock() {
123        return _occupies.peekLast();
124    }
125
126    protected String getStatus() {
127        long et = 0;
128        OBlock block = null;
129        for (OBlock b : _occupies) {
130            long t = System.currentTimeMillis() - b._entryTime;
131            if (t >= et)  {
132                et = t;
133                block = b;
134            }
135        }
136        if (block == null) {
137            return Bundle.getMessage("TrackerLocationLost", _trainName);
138        }
139        et /= 1000;
140        return Bundle.getMessage("TrackerStatus", _trainName, block.getDisplayName(), et / 60, et % 60);
141    }
142
143    /**
144     * Check if there is a path set between blkA and blkB with at most
145     * one dark block between them.  If there is both a path set to exit blkA
146     * and a path set to enter blkB, the path is PathSet.SET. If an exit or
147     * an entry path is set, but not both, the path is PathSet.PARTIAL. If there
148     * is neither an exit path not an entry path set, the path is PathSet.NO.
149     * When NOT PathSet.SET between blkA and blkB, then any dark blocks between 
150     * blkA and blkB are examined. All are examined for the most likely path
151     * through the dark block connecting blkA and blkB.
152     * @param blkA the current Head or Tail block
153     * @param blkB a block from the headRange or tailRange, where entry is possible
154     * @param recurse true if path can be used more than once
155     * @return one of PathSet enum values representing (how much of) a path was set
156     */
157   private PathSet hasPathBetween(@Nonnull OBlock blkA, @Nonnull OBlock blkB, boolean recurse)
158           throws JmriException {
159       // first check if there is an exit path set from blkA, to blkB
160       PathSet pathset = PathSet.NOTSET;
161       boolean hasExitA = false;
162       boolean hasEnterB = false;
163       boolean adjacentBlock = false;
164       ArrayList<OBlock> darkBlocks = new ArrayList<>();
165       for (Portal portal : blkA.getPortals()) {
166           OBlock block = portal.getOpposingBlock(blkA);
167           if (blkB.equals(block)) {
168               adjacentBlock = true;
169               if (!getPathsSet(blkA, portal).isEmpty()) { // set paths of blkA to portal
170                   hasExitA = true;
171                  if (!getPathsSet(blkB, portal).isEmpty()) { // paths of blkB to portal
172                      // done, path through portal is set
173                      pathset = PathSet.SET;
174                      break;
175                  }
176               } else if (!getPathsSet(blkB, portal).isEmpty()) {
177                   hasEnterB = true;
178               }
179           } else if ((block.getState() & Block.UNDETECTED) != 0) {
180               darkBlocks.add(block);
181           }
182       }
183       if (pathset != PathSet.SET) {
184           if (hasExitA || hasEnterB) {
185               pathset = PathSet.PARTIAL;
186           }
187       }
188       if (adjacentBlock || !recurse) {
189           return pathset;
190       }
191       if (darkBlocks.isEmpty()) {
192           return PathSet.NOWAY;
193       }
194       // blkA and blkB not adjacent, so look for a connecting dark block
195       PathSet darkPathSet;
196       for (OBlock block : darkBlocks) {
197           // if more than one dark block, set _darkBlock to the one with best accessing paths
198           darkPathSet = hasDarkBlockPathBetween(blkA, block, blkB);
199           if (darkPathSet == PathSet.SET) {
200               _darkBlock = block;
201               pathset = PathSet.SET;
202               break;
203           }
204           if (darkPathSet == PathSet.PARTIAL) {
205               _darkBlock = block;
206               pathset = PathSet.PARTIAL;
207           }
208       }
209       if (_darkBlock == null) { // _darkBlocks never empty at this point
210           // no good paths, nevertheless there is an intermediate dark block
211           _darkBlock = darkBlocks.get(0);
212       }
213       return pathset;
214   }
215       
216   private PathSet hasDarkBlockPathBetween(OBlock blkA, OBlock block, OBlock blkB)
217       throws JmriException {
218       PathSet pathset = PathSet.NOTSET;
219       PathSet setA = hasPathBetween(blkA, block, false);
220       PathSet setB = hasPathBetween(block, blkB, false);
221       if (setA == PathSet.SET && setB == PathSet.SET) {
222           pathset = PathSet.SET;
223       } else if (setA != PathSet.NOTSET && setB != PathSet.NOTSET) {
224               pathset = PathSet.PARTIAL;
225       }
226       return pathset;
227   }
228
229    protected PathSet hasPathInto(OBlock block) throws JmriException {
230        _darkBlock = null;
231        OBlock blk = getHeadBlock();
232        if (blk != null) {
233            PathSet pathSet = hasPathBetween(blk, block, true);
234            if (pathSet != PathSet.NOWAY) {
235                return pathSet;
236            }
237        }
238        blk = getTailBlock();
239        if (blk == null) {
240            throw new JmriException("No tail block!");
241        }
242        return  hasPathBetween(blk, block, true);
243    }
244
245    /**
246     * Get All paths in OBlock "block" that are set to go to Portal "portal"
247     */
248    private List<OPath> getPathsSet(OBlock block, Portal portal) {
249        List<OPath> paths = portal.getPathsWithinBlock(block);
250        List<OPath> setPaths = new ArrayList<>();
251        for (OPath path : paths) {
252            if (path.checkPathSet()) {
253                setPaths.add(path);
254            }
255        }
256        return setPaths;
257    }
258
259    /**
260     * Important to keep these sets disjoint and without duplicate entries
261     * @param b block to be added
262     */
263    private boolean areDisjoint(OBlock b) {
264        return !(_headRange.contains(b) || _occupies.contains(b) || _tailRange.contains(b));
265    }
266
267    private void addtoHeadRange(OBlock b) {
268        if (b != null) {
269            if (areDisjoint(b)) {
270                _headRange.add(b);
271            }
272        }
273    }
274
275    private void addtoTailRange(OBlock b) {
276        if (b != null) {
277            if (areDisjoint(b)) {
278                _tailRange.add(b);
279            }
280        }
281    }
282
283    private void addtoOccupies(OBlock b, boolean atHead) {
284        if (!_occupies.contains(b)) {
285            if (atHead) {
286                _occupies.addFirst(b);
287            } else {
288                _occupies.addLast(b);
289            }
290            showBlockValue(b);
291            _lostRange.remove(b);
292        }
293    }
294
295    private void removeFromOccupies(OBlock b) {
296        if (b != null) {
297            _occupies.remove(b);
298            _lostRange.remove(b);
299        }
300    }
301    
302    /**
303     * Build array of blocks reachable from head and tail portals
304     * @return range of reachable blocks
305     */
306     protected List<OBlock> makeRange() {
307        _headRange = new ArrayList<>();
308        _tailRange = new ArrayList<>();
309        OBlock headBlock = getHeadBlock();
310        OBlock tailBlock = getTailBlock();
311        if (headBlock != null) {
312            for (Portal portal : headBlock.getPortals()) {
313                OBlock block = portal.getOpposingBlock(headBlock);
314                if (block != null) {
315                    if ((block.getState() & Block.UNDETECTED) != 0) {
316                        for (Portal p : block.getPortals()) {
317                            OBlock blk = p.getOpposingBlock(block);
318                            if (!blk.equals(headBlock)) {
319                                addtoHeadRange(blk);                        
320                            }
321                        }
322                    }  else {
323                        addtoHeadRange(block);
324                    }
325                }
326            }
327        }
328        if (tailBlock != null && !tailBlock.equals(headBlock)) {
329            for (Portal portal : tailBlock.getPortals()) {
330                OBlock block = portal.getOpposingBlock(tailBlock);
331                if (block != null) {
332                    if ((block.getState() & Block.UNDETECTED) != 0) {
333                        for (Portal p : block.getPortals()) {
334                            OBlock blk = p.getOpposingBlock(block);
335                            if (!blk.equals(tailBlock)) {
336                                addtoTailRange(blk);                        
337                            }
338                        }
339                    } else {
340                        addtoTailRange(block);
341                    }
342                 }
343            }
344        }
345        return buildRange();
346    }
347
348     private List<OBlock> buildRange() {
349        // make new list since tracker table is holding the old list
350        ArrayList<OBlock> range = new ArrayList<>();    // total range of train
351        if (_occupies.isEmpty()) {
352            log.warn("{} does not occupy any blocks!", _trainName);
353        }
354        range.addAll(_occupies);
355        range.addAll(_headRange);
356        range.addAll(_tailRange);
357        return range;
358    }
359
360    protected List<OBlock> getBlocksOccupied() { 
361        return _occupies;
362    }
363
364    protected void stop() {
365        for (OBlock b : _occupies) {
366            if ((b.getState() & Block.UNDETECTED) != 0) {
367                removeName(b);
368            }
369        }
370    }
371
372    private void removeBlock(@Nonnull OBlock block) {
373        int size = _occupies.size();
374        int index = _occupies.indexOf(block);
375        if (index > 0 && index < size-1) {
376            // Mid range. Temporary lost of detection?  Don't remove from _occupies
377            log.warn("Tracker {} lost occupancy mid train at block \"{}\"!", _trainName, block.getDisplayName());
378            _statusMessage = Bundle.getMessage("trackerLostBlock", _trainName, block.getDisplayName());
379            return;
380        }
381        removeFromOccupies(block);
382        // remove any adjacent dark block or mid-range lost block
383        for (Portal p : block.getPortals()) {
384            OBlock b = p.getOpposingBlock(block);
385            if ((b.getState() & (Block.UNDETECTED | Block.UNOCCUPIED)) != 0) {
386                removeFromOccupies(b);
387                removeName(b);
388            }
389            
390        }
391        removeName(block);
392    }
393
394    private void removeName(OBlock block) {
395        if (_trainName.equals(block.getValue())) {
396            block.setValue(null);
397            block.setState(block.getState() & ~OBlock.RUNNING);
398        }
399    }
400
401    protected boolean move(OBlock block, int state) {
402        _statusMessage = null;
403        if ((state & Block.OCCUPIED) != 0) {
404            if (_occupies.contains(block)) {
405                if (block.getValue() == null) { // must be a regained lost block
406                    block.setValue(_trainName);
407                    showBlockValue(block);
408                    // don't use _statusMessage, so range listeners get adjusted
409                    _parent.setStatus(Bundle.getMessage("TrackerReentry", _trainName, block.getDisplayName()));
410                    _lostRange.remove(block);
411                } else if (!block.getValue().equals(_trainName)) {
412                    log.error("Block \"{}\" occupied by \"{}\", but block.getValue()= {}!",
413                            block.getDisplayName(),  _trainName, block.getValue());
414                }
415            } else
416                _lostRange.remove(block);
417            Warrant w = block.getWarrant();
418            if (w != null) {
419                String msg = Bundle.getMessage("AllocatedToWarrant", 
420                        w.getDisplayName(), block.getDisplayName(), w.getTrainName());
421                int idx = w.getCurrentOrderIndex();
422                // Was it the warranted train that entered the block?
423                // Can't tell who got notified first - tracker or warrant?
424                // is distance of 1 block OK?
425                if (Math.abs(w.getIndexOfBlock(block, 0) - idx) < 2) {
426                    _statusMessage = msg;
427                    return true;
428                }  // otherwise claim it for tracker
429            }
430            if (_headRange.contains(block)) {
431                if (_darkBlock != null) {
432                    addtoOccupies(_darkBlock, true);
433                }
434                addtoOccupies(block, true);
435            } else if (_tailRange.contains(block)) {
436                if (_darkBlock != null) {
437                    addtoOccupies(_darkBlock, false);
438                }
439                addtoOccupies(block, false);
440            } else if (!_occupies.contains(block)) {
441                log.warn("Block \"{}\" is not within range of  \"{}\"!",block.getDisplayName(),_trainName );
442            }
443            makeRange();
444            return true;
445        } else if ((state & Block.UNOCCUPIED) != 0) {
446            removeBlock(block);
447            int size = _occupies.size();
448            if (size == 0) {    // lost tracker
449                recover(block);
450            } else {    // otherwise head or tail is holding a path fixed through a portal (thrown switch should have derailed train by now)
451                makeRange();
452            }
453            return false;
454        }
455        return true;
456    }
457
458    @Override
459    public String toString() {
460        return _trainName;
461    }
462
463    private void recover(OBlock block) {
464        // make list of possible blocks
465        ArrayList<OBlock> list = new ArrayList<>();
466        list.addAll(_lostRange);
467        list.addAll(_headRange);
468        list.addAll(_tailRange);
469        list.add(block);
470
471        java.awt.Toolkit.getDefaultToolkit().beep();
472        new ChooseRecoverBlock(block, list, this, _parent);
473        _statusMessage = Bundle.getMessage(TRACKER_NO_CURRENT_BLOCK, _trainName,
474                block.getDisplayName()) + "\n" + Bundle.getMessage("TrackingStopped");
475    }
476
477    class ChooseStartBlock extends ChooseBlock {
478
479        ChooseStartBlock(OBlock b, List<OBlock> l, Tracker t, TrackerTableAction tta) {
480            super(b, l, t, tta);
481        }
482
483        @Override
484        JPanel makeBlurb() {
485            JPanel panel = new JPanel();
486            panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
487            panel.add(new JLabel(Bundle.getMessage("MultipleStartBlocks1", getHeadBlock().getDisplayName(), _trainName)));
488            panel.add(new JLabel(Bundle.getMessage("MultipleStartBlocks2")));
489            panel.add(new JLabel(Bundle.getMessage("MultipleStartBlocks3", _trainName)));
490            panel.add(new JLabel(Bundle.getMessage("MultipleStartBlocks4",Bundle.getMessage("ButtonStart"))));
491            return panel;
492        }
493
494        @Override
495        JPanel makeButtonPanel() {
496            JPanel panel = new JPanel();
497            JButton startButton = new JButton(Bundle.getMessage("ButtonStart"));
498            startButton.addActionListener((ActionEvent a) -> {
499                _parent.addTracker(tracker);
500                dispose();
501            });
502            panel.add(startButton);
503            return panel;
504        }
505
506        @Override
507        void doAction() {
508            parent.addTracker(tracker);
509        }
510    }
511
512    private class ChooseRecoverBlock extends ChooseBlock {
513
514        ChooseRecoverBlock(OBlock block, List<OBlock> list, Tracker t, TrackerTableAction tta) {
515            super(block, list, t, tta);
516            _occupies.clear();
517            tta.removeBlockListeners(t);
518        }
519
520        @Override
521        JPanel makeBlurb() {
522            JPanel panel = new JPanel();
523            panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
524            panel.add(new JLabel(Bundle.getMessage(TRACKER_NO_CURRENT_BLOCK, _trainName, block.getDisplayName())));
525            panel.add(new JLabel(Bundle.getMessage("PossibleLocation", _trainName)));
526            return panel;
527        }
528
529        @Override
530        JPanel makeButtonPanel() {
531            JPanel panel = new JPanel();
532            JButton recoverButton = new JButton(Bundle.getMessage("ButtonRecover"));
533            recoverButton.addActionListener((ActionEvent a) -> {
534                if (_occupies.isEmpty()) {
535                    JOptionPane.showMessageDialog(this, 
536                            Bundle.getMessage("RecoverOrExit", _trainName, Bundle.getMessage("ButtonStop")),
537                            Bundle.getMessage("WarningTitle"), JOptionPane.INFORMATION_MESSAGE);                    
538                } else {
539                    doAction();
540                }
541            });
542            panel.add(recoverButton);
543
544            JButton cancelButton = new JButton(Bundle.getMessage("ButtonStop"));
545            cancelButton.addActionListener((ActionEvent a) -> doStopAction());
546            panel.add(cancelButton);
547            return panel;
548        }        
549
550        @Override
551        public void valueChanged(ListSelectionEvent e) {
552            OBlock blk = _jList.getSelectedValue();
553            if (blk != null) {
554                String msg = null;
555                if ((blk.getState() & Block.OCCUPIED) == 0) {
556                    msg = Bundle.getMessage("blockUnoccupied", blk.getDisplayName());
557                } else {
558                    Tracker t = parent.findTrackerIn(blk);
559                    if (t != null && !tracker.getTrainName().equals(blk.getValue())) {
560                        msg = Bundle.getMessage("blockInUse", t.getTrainName(), blk.getDisplayName());
561                    }
562                }
563                if (msg != null) {
564                    JOptionPane.showMessageDialog(this, msg,
565                            Bundle.getMessage("WarningTitle"), JOptionPane.WARNING_MESSAGE);
566                    _jList.removeListSelectionListener(this);
567                    list.remove(blk);
568                    if (list.isEmpty()) {
569                        if (!_occupies.isEmpty()) {
570                            doAction();
571                            dispose();
572                        } else {
573                            doStopAction();
574                        }
575                    }
576                    _jList.setModel(new BlockListModel(list));
577                    _jList.addListSelectionListener(this);
578                } else {
579                    super.valueChanged(e);
580                }
581            }
582        }
583
584        @Override
585        void doAction() {
586            parent.addBlockListeners(tracker);
587            parent.setStatus(Bundle.getMessage("restartTracker",
588                    tracker.getTrainName(), tracker.getHeadBlock().getDisplayName()));
589            dispose();
590        }
591
592        void doStopAction() {
593            parent.stopTracker(tracker, block);
594            parent.setStatus(Bundle.getMessage(TRACKER_NO_CURRENT_BLOCK, _trainName,
595                    block.getDisplayName()) + "\n" + Bundle.getMessage("TrackingStopped"));
596            dispose();
597        }
598
599        @Override
600        public void dispose () {
601            parent.updateStatus();
602            super.dispose();
603        }
604    }
605
606    abstract class ChooseBlock extends JDialog implements ListSelectionListener {
607        OBlock block;
608        TrackerTableAction parent;
609        List<OBlock> list;
610        JList<OBlock> _jList;
611        Tracker tracker;
612         
613        ChooseBlock(OBlock b, List<OBlock> l, Tracker t, TrackerTableAction tta) {
614            super(tta._frame);
615            setTitle(Bundle.getMessage("TrackerTitle"));
616            block = b;
617            list = l;
618            tracker = t;
619            parent = tta;
620
621            JPanel contentPanel = new JPanel();
622            contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
623
624            contentPanel.add(Box.createVerticalStrut(TrackerTableAction.STRUT_SIZE));
625            JPanel p = new JPanel();
626            p.add(makeBlurb());
627            contentPanel.add(p);
628
629            p = new JPanel();
630            p.add(makeListPanel());
631            contentPanel.add(p);
632            
633            contentPanel.add(Box.createVerticalStrut(TrackerTableAction.STRUT_SIZE));
634            contentPanel.add(makeButtonPanel());
635            setContentPane(contentPanel);
636            
637            pack();
638            setLocation(parent._frame.getLocation());
639            setAlwaysOnTop(true);
640            setVisible(true);
641        }
642
643        abstract JPanel makeBlurb();
644        abstract JPanel makeButtonPanel();
645        abstract void doAction();
646
647        protected JPanel makeListPanel() {
648            JPanel panel = new JPanel();
649            panel.setBorder(javax.swing.BorderFactory .createLineBorder(Color.black, 2));
650            _jList = new JList<>();
651            _jList.setModel(new BlockListModel(list));
652            _jList.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
653            _jList.addListSelectionListener(this);
654            _jList.setCellRenderer(new BlockCellRenderer());
655            panel.add(_jList);
656            return panel;
657        }
658
659        @Override
660        public void valueChanged(ListSelectionEvent e) {
661            OBlock b = _jList.getSelectedValue();
662            if (b != null) {
663                b.setState(b.getState() & ~OBlock.RUNNING);
664                addtoOccupies(b, false); // make additional block the tail
665                b._entryTime = System.currentTimeMillis();
666                _jList.removeListSelectionListener(this);
667                List<OBlock> blockList = initialRange(parent);
668                if (blockList.isEmpty()) {
669                    doAction();
670                    dispose();
671                }
672                _jList.setModel(new BlockListModel(blockList));
673                _jList.addListSelectionListener(this);
674            }
675        }
676
677        class BlockCellRenderer extends JLabel implements ListCellRenderer<Object> {
678
679            @Override
680            public Component getListCellRendererComponent(
681              JList<?> list,           // the list
682              Object value,            // value to display
683              int index,               // cell index
684              boolean isSelected,      // is the cell selected
685              boolean cellHasFocus)    // does the cell have focus
686            {
687                String s = ((OBlock)value).getDisplayName();
688                setText(s);
689                if (isSelected) {
690                    setBackground(list.getSelectionBackground());
691                    setForeground(list.getSelectionForeground());
692                } else {
693                    setBackground(list.getBackground());
694                    setForeground(list.getForeground());
695                }
696                setEnabled(list.isEnabled());
697                setFont(list.getFont());
698                setOpaque(true);
699                return this;
700            }
701        }
702
703        class BlockListModel extends AbstractListModel<OBlock> {
704            List<OBlock> blockList;
705
706            BlockListModel(List<OBlock> bl) {
707                blockList = bl;
708            }
709
710            @Override
711            public int getSize() {
712                return blockList.size();
713            }
714
715            @Override
716            public OBlock getElementAt(int index) {
717                return blockList.get(index);
718            }
719        }
720    }
721
722    private static final Logger log = LoggerFactory.getLogger(Tracker.class);
723}