001package jmri.jmrit.operations.rollingstock;
002
003import java.beans.PropertyChangeEvent;
004import java.beans.PropertyChangeListener;
005import java.util.*;
006
007import javax.annotation.OverridingMethodsMustInvokeSuper;
008
009import org.slf4j.Logger;
010import org.slf4j.LoggerFactory;
011
012import jmri.beans.PropertyChangeSupport;
013import jmri.jmrit.operations.locations.Location;
014import jmri.jmrit.operations.locations.Track;
015import jmri.jmrit.operations.trains.Train;
016import jmri.jmrit.operations.trains.trainbuilder.TrainCommon;
017
018/**
019 * Base class for rolling stock managers car and engine.
020 *
021 * @author Daniel Boudreau Copyright (C) 2010, 2011
022 * @param <T> the type of RollingStock managed by this manager
023 */
024public abstract class RollingStockManager<T extends RollingStock> extends PropertyChangeSupport implements PropertyChangeListener {
025
026    public static final String NONE = "";
027
028    // RollingStock
029    protected Hashtable<String, T> _hashTable = new Hashtable<>();
030
031    public static final String LISTLENGTH_CHANGED_PROPERTY = "RollingStockListLength"; // NOI18N
032    
033    abstract public RollingStock newRS(String road, String number);
034
035    public RollingStockManager() {
036    }
037
038    /**
039     * Get the number of items in the roster
040     *
041     * @return Number of rolling stock in the Roster
042     */
043    public int getNumEntries() {
044        return _hashTable.size();
045    }
046
047    public void dispose() {
048        deleteAll();
049    }
050
051    /**
052     * Get rolling stock by id
053     *
054     * @param id The string id.
055     *
056     * @return requested RollingStock object or null if none exists
057     */
058    public T getById(String id) {
059        return _hashTable.get(id);
060    }
061
062    /**
063     * Get rolling stock by road and number
064     *
065     * @param road   RollingStock road
066     * @param number RollingStock number
067     * @return requested RollingStock object or null if none exists
068     */
069    public T getByRoadAndNumber(String road, String number) {
070        String id = RollingStock.createId(road, number);
071        return getById(id);
072    }
073
074    /**
075     * Get a rolling stock by type and road. Used to test that rolling stock
076     * with a specific type and road exists.
077     *
078     * @param type RollingStock type.
079     * @param road RollingStock road.
080     * @return the first RollingStock found with the specified type and road.
081     */
082    public T getByTypeAndRoad(String type, String road) {
083        Enumeration<String> en = _hashTable.keys();
084        while (en.hasMoreElements()) {
085            T rs = getById(en.nextElement());
086            if (rs.getTypeName().equals(type) && rs.getRoadName().equals(road)) {
087                return rs;
088            }
089        }
090        return null;
091    }
092
093    /**
094     * Get a rolling stock by Radio Frequency Identification (RFID)
095     *
096     * @param rfid RollingStock's RFID.
097     * @return the RollingStock with the specific RFID, or null if not found
098     */
099    public T getByRfid(String rfid) {
100        Enumeration<String> en = _hashTable.keys();
101        while (en.hasMoreElements()) {
102            T rs = getById(en.nextElement());
103            if (rs.getRfid().equals(rfid)) {
104                return rs;
105            }
106        }
107        return null;
108    }
109
110    /**
111     * Load RollingStock.
112     *
113     * @param rs The RollingStock to load.
114     */
115    public void register(T rs) {
116        if (!_hashTable.containsKey(rs.getId())) {
117            int oldSize = _hashTable.size();
118            rs.addPropertyChangeListener(this);
119            _hashTable.put(rs.getId(), rs);
120            firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize, _hashTable.size());
121        } else {
122            log.error("Duplicate rolling stock id: ({})", rs.getId());
123            rs.dispose();
124        }
125    }
126
127    /**
128     * Unload RollingStock.
129     *
130     * @param rs The RollingStock to delete.
131     */
132    public void deregister(T rs) {
133        rs.removePropertyChangeListener(this);
134        rs.dispose();
135        int oldSize = _hashTable.size();
136        _hashTable.remove(rs.getId());
137        firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize, _hashTable.size());
138    }
139
140    /**
141     * Remove all RollingStock from roster
142     */
143    public void deleteAll() {
144        int oldSize = _hashTable.size();
145        Enumeration<String> en = _hashTable.keys();
146        while (en.hasMoreElements()) {
147            T rs = getById(en.nextElement());
148            rs.dispose();
149            _hashTable.remove(rs.getId());
150        }
151        firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize, _hashTable.size());
152    }
153
154    public void resetMoves() {
155        resetMoves(getList());
156    }
157
158    public void resetMoves(List<T> list) {
159        for (RollingStock rs : list) {
160            rs.setMoves(0);
161        }
162    }
163
164    /**
165     * Returns a list (no order) of RollingStock.
166     *
167     * @return list of RollingStock
168     */
169    public List<T> getList() {
170        return new ArrayList<>(_hashTable.values());
171    }
172
173    /**
174     * Sort by rolling stock id
175     *
176     * @return list of RollingStock ordered by id
177     */
178    public List<T> getByIdList() {
179        Enumeration<String> en = _hashTable.keys();
180        String[] arr = new String[_hashTable.size()];
181        List<T> out = new ArrayList<>();
182        int i = 0;
183        while (en.hasMoreElements()) {
184            arr[i++] = en.nextElement();
185        }
186        Arrays.sort(arr);
187        for (i = 0; i < arr.length; i++) {
188            out.add(getById(arr[i]));
189        }
190        return out;
191    }
192
193    /**
194     * Sort by rolling stock road name
195     *
196     * @return list of RollingStock ordered by road name
197     */
198    public List<T> getByRoadNameList() {
199        return getByList(getByIdList(), BY_ROAD);
200    }
201
202    private static final int PAGE_SIZE = 64;
203    private static final int NOT_INTEGER = -999999999; // flag when RS number isn't an Integer
204
205    /**
206     * Sort by rolling stock number, number can be alphanumeric. RollingStock
207     * number can also be in the format of nnnn-N, where the "-N" allows the
208     * user to enter RollingStock with similar numbers.
209     *
210     * @return list of RollingStock ordered by number
211     */
212    public List<T> getByNumberList() {
213        // first get by road list
214        List<T> sortIn = getByRoadNameList();
215        // now re-sort
216        List<T> out = new ArrayList<>();
217        int rsNumber = 0;
218        int outRsNumber = 0;
219
220        for (T rs : sortIn) {
221            boolean rsAdded = false;
222            try {
223                rsNumber = Integer.parseInt(rs.getNumber());
224                rs.number = rsNumber;
225            } catch (NumberFormatException e) {
226                // maybe rolling stock number in the format nnnn-N
227                try {
228                    String[] number = rs.getNumber().split(TrainCommon.HYPHEN);
229                    rsNumber = Integer.parseInt(number[0]);
230                    rs.number = rsNumber;
231                } catch (NumberFormatException e2) {
232                    rs.number = NOT_INTEGER;
233                    // sort alphanumeric numbers at the end of the out list
234                    String numberIn = rs.getNumber();
235                    // log.debug("rolling stock in road number ("+numberIn+") isn't a number");
236                    for (int k = (out.size() - 1); k >= 0; k--) {
237                        String numberOut = out.get(k).getNumber();
238                        try {
239                            Integer.parseInt(numberOut);
240                            // done, place rolling stock with alphanumeric
241                            // number after rolling stocks with real numbers.
242                            out.add(k + 1, rs);
243                            rsAdded = true;
244                            break;
245                        } catch (NumberFormatException e3) {
246                            if (numberIn.compareToIgnoreCase(numberOut) >= 0) {
247                                out.add(k + 1, rs);
248                                rsAdded = true;
249                                break;
250                            }
251                        }
252                    }
253                    if (!rsAdded) {
254                        out.add(0, rs);
255                    }
256                    continue;
257                }
258            }
259
260            int start = 0;
261            // page to improve sort performance.
262            int divisor = out.size() / PAGE_SIZE;
263            for (int k = divisor; k > 0; k--) {
264                outRsNumber = out.get((out.size() - 1) * k / divisor).number;
265                if (outRsNumber == NOT_INTEGER) {
266                    continue;
267                }
268                if (rsNumber >= outRsNumber) {
269                    start = (out.size() - 1) * k / divisor;
270                    break;
271                }
272            }
273            for (int j = start; j < out.size(); j++) {
274                outRsNumber = out.get(j).number;
275                if (outRsNumber == NOT_INTEGER) {
276                    try {
277                        outRsNumber = Integer.parseInt(out.get(j).getNumber());
278                    } catch (NumberFormatException e) {
279                        try {
280                            String[] number = out.get(j).getNumber().split(TrainCommon.HYPHEN);
281                            outRsNumber = Integer.parseInt(number[0]);
282                        } catch (NumberFormatException e2) {
283                            // force add
284                            outRsNumber = rsNumber + 1;
285                        }
286                    }
287                }
288                if (rsNumber < outRsNumber) {
289                    out.add(j, rs);
290                    rsAdded = true;
291                    break;
292                }
293            }
294            if (!rsAdded) {
295                out.add(rs);
296            }
297        }
298        // log.debug("end rolling stock sort by number list");
299        return out;
300    }
301
302    /**
303     * Sort by rolling stock type names
304     *
305     * @return list of RollingStock ordered by RollingStock type
306     */
307    public List<T> getByTypeList() {
308        return getByList(getByRoadNameList(), BY_TYPE);
309    }
310
311    /**
312     * Return rolling stock of a specific type
313     *
314     * @param type type of rolling stock
315     * @return list of RollingStock that are specific type
316     */
317    public List<T> getByTypeList(String type) {
318        List<T> typeList = getByTypeList();
319        List<T> out = new ArrayList<>();
320        for (T rs : typeList) {
321            if (rs.getTypeName().equals(type)) {
322                out.add(rs);
323            }
324        }
325        return out;
326    }
327
328    /**
329     * Sort by rolling stock color names
330     *
331     * @return list of RollingStock ordered by RollingStock color
332     */
333    public List<T> getByColorList() {
334        return getByList(getByTypeList(), BY_COLOR);
335    }
336
337    /**
338     * Sort by rolling stock location
339     *
340     * @return list of RollingStock ordered by RollingStock location
341     */
342    public List<T> getByLocationList() {
343        return getByList(getByNumberList(), BY_LOCATION);
344    }
345
346    /**
347     * Sort by rolling stock destination
348     *
349     * @return list of RollingStock ordered by RollingStock destination
350     */
351    public List<T> getByDestinationList() {
352        return getByList(getByLocationList(), BY_DESTINATION);
353    }
354
355    /**
356     * Sort by rolling stocks in trains
357     *
358     * @return list of RollingStock ordered by trains
359     */
360    public List<T> getByTrainList() {
361        List<T> byDest = getByList(getByIdList(), BY_DESTINATION);
362        List<T> byLoc = getByList(byDest, BY_LOCATION);
363        return getByList(byLoc, BY_TRAIN);
364    }
365
366    /**
367     * Sort by rolling stock moves
368     *
369     * @return list of RollingStock ordered by RollingStock moves
370     */
371    public List<T> getByMovesList() {
372        return getByList(getList(), BY_MOVES);
373    }
374
375    /**
376     * Sort by when rolling stock was built
377     *
378     * @return list of RollingStock ordered by RollingStock built date
379     */
380    public List<T> getByBuiltList() {
381        return getByList(getByIdList(), BY_BUILT);
382    }
383
384    /**
385     * Sort by rolling stock owner
386     *
387     * @return list of RollingStock ordered by RollingStock owner
388     */
389    public List<T> getByOwnerList() {
390        return getByList(getByIdList(), BY_OWNER);
391    }
392
393    /**
394     * Sort by rolling stock value
395     *
396     * @return list of RollingStock ordered by value
397     */
398    public List<T> getByValueList() {
399        return getByList(getByIdList(), BY_VALUE);
400    }
401
402    /**
403     * Sort by rolling stock RFID
404     *
405     * @return list of RollingStock ordered by RFIDs
406     */
407    public List<T> getByRfidList() {
408        return getByList(getByIdList(), BY_RFID);
409    }
410    
411    public List<T> getByPickupList() {
412        return getByList(getByDestinationList(), BY_PICKUP);
413    }
414
415    /**
416     * Get a list of all rolling stock sorted last date used
417     *
418     * @return list of RollingStock ordered by last date
419     */
420    public List<T> getByLastDateList() {
421        return getByList(getByIdList(), BY_LAST);
422    }
423    
424    public List<T> getByCommentList() {
425        return getByList(getByIdList(), BY_COMMENT);
426    }
427
428    /**
429     * Sort a specific list of rolling stock last date used
430     *
431     * @param inList list of rolling stock to sort.
432     * @return list of RollingStock ordered by last date
433     */
434    public List<T> getByLastDateList(List<T> inList) {
435        return getByList(inList, BY_LAST);
436    }
437
438    protected List<T> getByList(List<T> sortIn, int attribute) {
439        List<T> out = new ArrayList<>(sortIn);
440        out.sort(getComparator(attribute));
441        return out;
442    }
443
444    // The various sort options for RollingStock
445    // see CarManager and EngineManger for other values
446    protected static final int BY_NUMBER = 0;
447    protected static final int BY_ROAD = 1;
448    protected static final int BY_TYPE = 2;
449    protected static final int BY_COLOR = 3;
450    protected static final int BY_LOCATION = 4;
451    protected static final int BY_DESTINATION = 5;
452    protected static final int BY_TRAIN = 6;
453    protected static final int BY_MOVES = 7;
454    protected static final int BY_BUILT = 8;
455    protected static final int BY_OWNER = 9;
456    protected static final int BY_RFID = 10;
457    protected static final int BY_VALUE = 11;
458    protected static final int BY_LAST = 12;
459    protected static final int BY_BLOCKING = 13;
460    private static final int BY_PICKUP = 14;
461    protected static final int BY_COMMENT = 15;
462
463    protected java.util.Comparator<T> getComparator(int attribute) {
464        switch (attribute) {
465            case BY_NUMBER:
466                return (r1, r2) -> (r1.getNumber().compareToIgnoreCase(r2.getNumber()));
467            case BY_ROAD:
468                return (r1, r2) -> (r1.getRoadName().compareToIgnoreCase(r2.getRoadName()));
469            case BY_TYPE:
470                return (r1, r2) -> (r1.getTypeName().compareToIgnoreCase(r2.getTypeName()));
471            case BY_COLOR:
472                return (r1, r2) -> (r1.getColor().compareToIgnoreCase(r2.getColor()));
473            case BY_LOCATION:
474                return (r1, r2) -> (r1.getStatus() + r1.getLocationName() + r1.getTrackName())
475                        .compareToIgnoreCase(r2.getStatus() + r2.getLocationName() + r2.getTrackName());
476            case BY_DESTINATION:
477                return (r1, r2) -> (r1.getDestinationName() + r1.getDestinationTrackName())
478                        .compareToIgnoreCase(r2.getDestinationName() + r2.getDestinationTrackName());
479            case BY_TRAIN:
480                return (r1, r2) -> (r1.getTrainName().compareToIgnoreCase(r2.getTrainName()));
481            case BY_MOVES:
482                return (r1, r2) -> (r1.getMoves() - r2.getMoves());
483            case BY_BUILT:
484                return (r1,
485                        r2) -> (convertBuildDate(r1.getBuilt()).compareToIgnoreCase(convertBuildDate(r2.getBuilt())));
486            case BY_OWNER:
487                return (r1, r2) -> (r1.getOwnerName().compareToIgnoreCase(r2.getOwnerName()));
488            case BY_RFID:
489                return (r1, r2) -> (r1.getRfid().compareToIgnoreCase(r2.getRfid()));
490            case BY_VALUE:
491                return (r1, r2) -> (r1.getValue().compareToIgnoreCase(r2.getValue()));
492            case BY_LAST:
493                return (r1, r2) -> (r1.getLastMoveDate().compareTo(r2.getLastMoveDate()));
494            case BY_BLOCKING:
495                return (r1, r2) -> (r1.getBlocking() - r2.getBlocking());
496            case BY_PICKUP:
497                return (r1, r2) -> (r1.getPickupTime().compareToIgnoreCase(r2.getPickupTime()));
498            case BY_COMMENT:
499                return (r1, r2) -> (r1.getComment().compareToIgnoreCase(r2.getComment()));
500            default:
501                return (r1, r2) -> ((r1.getRoadName() + r1.getNumber())
502                        .compareToIgnoreCase(r2.getRoadName() + r2.getNumber()));
503        }
504    }
505    
506    protected List<T> sortByTrackPriority(List<T> list) {
507        List<T> out = new ArrayList<>();
508        // sort rolling stock by track priority
509        for (T rs : list) {
510            if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_HIGH)) {
511                out.add(rs);
512            }
513        }
514        for (T rs : list) {
515            if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_MEDIUM)) {
516                out.add(rs);
517            }
518        }
519        for (T rs : list) {
520            if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_NORMAL)) {
521                out.add(rs);
522            }
523        }
524        for (T rs : list) {
525            if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_LOW)) {
526                out.add(rs);
527            }
528        }
529        // rolling stock without a track assignment
530        for (T rs : list) {
531            if (!out.contains(rs)) {
532                out.add(rs);
533            }
534        }
535        return out;
536    }
537
538    /*
539     * Converts build date into consistent String. Three build date formats; Two
540     * digits YY becomes 19YY. MM-YY becomes 19YY. MM-YYYY becomes YYYY.
541     */
542    public static String convertBuildDate(String date) {
543        String[] built = date.split("-");
544        if (built.length == 2) {
545            try {
546                int d = Integer.parseInt(built[1]);
547                if (d < 100) {
548                    d = d + 1900;
549                }
550                return Integer.toString(d);
551            } catch (NumberFormatException e) {
552                log.debug("Unable to parse built date ({})", date);
553            }
554        } else {
555            try {
556                int d = Integer.parseInt(date);
557                if (d < 100) {
558                    d = d + 1900;
559                }
560                return Integer.toString(d);
561            } catch (NumberFormatException e) {
562                log.debug("Unable to parse built date ({})", date);
563            }
564        }
565        return date;
566    }
567
568    /**
569     * Get a list of rolling stocks assigned to a train ordered by location
570     *
571     * @param train The Train.
572     *
573     * @return List of RollingStock assigned to the train ordered by location
574     */
575    public List<T> getByTrainList(Train train) {
576        return getByList(getList(train), BY_LOCATION);
577    }
578
579    /**
580     * Returns a list (no order) of RollingStock in a train.
581     *
582     * @param train The Train.
583     *
584     * @return list of RollingStock
585     */
586    public List<T> getList(Train train) {
587        List<T> out = new ArrayList<>();
588        getList().stream().filter((rs) -> {
589            return rs.getTrain() == train;
590        }).forEachOrdered((rs) -> {
591            out.add(rs);
592        });
593        return out;
594    }
595
596    /**
597     * Returns a list (no order) of RollingStock at a location.
598     *
599     * @param location location to search for.
600     * @return list of RollingStock
601     */
602    public List<T> getList(Location location) {
603        List<T> out = new ArrayList<>();
604        getList().stream().filter((rs) -> {
605            return rs.getLocation() == location;
606        }).forEachOrdered((rs) -> {
607            out.add(rs);
608        });
609        return out;
610    }
611
612    /**
613     * Returns a list (no order) of RollingStock on a track.
614     *
615     * @param track Track to search for.
616     * @return list of RollingStock
617     */
618    public List<T> getList(Track track) {
619        List<T> out = new ArrayList<>();
620        getList().stream().filter((rs) -> {
621            return rs.getTrack() == track;
622        }).forEachOrdered((rs) -> {
623            out.add(rs);
624        });
625        return out;
626    }
627    
628    /**
629     * Returns the rolling stock's last clone rolling stock if there's one.
630     * 
631     * @param rs The rolling stock searching for a clone
632     * @return Returns the rolling stock's last clone rolling stock, null if
633     *         there isn't a clone rolling stock.
634     */
635    public T getClone(RollingStock rs) {
636        List<T> list = getByLastDateList();
637        // clone with the highest creation number will be last in the list
638        for (int i = list.size() - 1; i >= 0; i--) {
639            T kar = list.get(i);
640            if (kar.isClone() &&
641                    kar.getDestinationTrack() == rs.getTrack() &&
642                    kar.getRoadName().equals(rs.getRoadName()) &&
643                    kar.getNumber().split(RollingStock.CLONE_REGEX)[0].equals(rs.getNumber())) {
644                return kar;
645            }
646        }
647        return null; // no clone for this rolling stock
648    }
649
650    int cloneCreationOrder = 0;
651
652    /**
653     * Returns the highest clone creation order given to a clone.
654     * 
655     * @return 1 if the first clone created, otherwise the highest found plus
656     *         one. Automatically increments.
657     */
658    protected int getCloneCreationOrder() {
659        if (cloneCreationOrder == 0) {
660            for (RollingStock rs : getList()) {
661                if (rs.isClone()) {
662                    String[] number = rs.getNumber().split(RollingStock.CLONE_REGEX);
663                    int creationOrder = Integer.parseInt(number[1]);
664                    if (creationOrder > cloneCreationOrder) {
665                        cloneCreationOrder = creationOrder;
666                    }
667                }
668            }
669        }
670        return ++cloneCreationOrder;
671    }
672    
673    /**
674     * Adds 4 leading zeros to the number for sorting purposes
675     * 
676     * @param n the number needed leading zeros
677     * @return String "number" with 4 leading zeros
678     */
679    protected String padNumber(int n) {
680        return String.format("%04d", n);
681    }
682
683    @Override
684    @OverridingMethodsMustInvokeSuper
685    public void propertyChange(PropertyChangeEvent evt) {
686        if (evt.getPropertyName().equals(Xml.ID)) {
687            @SuppressWarnings("unchecked")
688            T rs = (T) evt.getSource(); // unchecked cast to T  
689            _hashTable.remove(evt.getOldValue());
690            if (_hashTable.containsKey(rs.getId())) {
691                log.error("Duplicate rolling stock id: ({})", rs.getId());
692                rs.dispose();
693            } else {
694                _hashTable.put(rs.getId(), rs);
695            }
696            // fire so listeners that rebuild internal lists get signal of change in id, even without change in size
697            firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, _hashTable.size(), _hashTable.size());
698        }
699    }
700
701    private final static Logger log = LoggerFactory.getLogger(RollingStockManager.class);
702
703}