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