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> getByLastDateReversedList() {
425        List<T> out = getByLastDateList();
426        Collections.reverse(out);
427        return out;
428    } 
429    
430    public List<T> getByCommentList() {
431        return getByList(getByIdList(), BY_COMMENT);
432    }
433
434    protected List<T> getByList(List<T> sortIn, int attribute) {
435        List<T> out = new ArrayList<>(sortIn);
436        out.sort(getComparator(attribute));
437        return out;
438    }
439
440    // The various sort options for RollingStock
441    // see CarManager and EngineManger for other values
442    protected static final int BY_NUMBER = 0;
443    protected static final int BY_ROAD = 1;
444    protected static final int BY_TYPE = 2;
445    protected static final int BY_COLOR = 3;
446    protected static final int BY_LOCATION = 4;
447    protected static final int BY_DESTINATION = 5;
448    protected static final int BY_TRAIN = 6;
449    protected static final int BY_MOVES = 7;
450    protected static final int BY_BUILT = 8;
451    protected static final int BY_OWNER = 9;
452    protected static final int BY_RFID = 10;
453    protected static final int BY_VALUE = 11;
454    protected static final int BY_LAST = 12;
455    protected static final int BY_BLOCKING = 13;
456    private static final int BY_PICKUP = 14;
457    protected static final int BY_COMMENT = 15;
458
459    protected java.util.Comparator<T> getComparator(int attribute) {
460        switch (attribute) {
461            case BY_NUMBER:
462                return (r1, r2) -> (r1.getNumber().compareToIgnoreCase(r2.getNumber()));
463            case BY_ROAD:
464                return (r1, r2) -> (r1.getRoadName().compareToIgnoreCase(r2.getRoadName()));
465            case BY_TYPE:
466                return (r1, r2) -> (r1.getTypeName().compareToIgnoreCase(r2.getTypeName()));
467            case BY_COLOR:
468                return (r1, r2) -> (r1.getColor().compareToIgnoreCase(r2.getColor()));
469            case BY_LOCATION:
470                return (r1, r2) -> (r1.getStatus() + r1.getLocationName() + r1.getTrackName())
471                        .compareToIgnoreCase(r2.getStatus() + r2.getLocationName() + r2.getTrackName());
472            case BY_DESTINATION:
473                return (r1, r2) -> (r1.getDestinationName() + r1.getDestinationTrackName())
474                        .compareToIgnoreCase(r2.getDestinationName() + r2.getDestinationTrackName());
475            case BY_TRAIN:
476                return (r1, r2) -> (r1.getTrainName().compareToIgnoreCase(r2.getTrainName()));
477            case BY_MOVES:
478                return (r1, r2) -> (r1.getMoves() - r2.getMoves());
479            case BY_BUILT:
480                return (r1,
481                        r2) -> (convertBuildDate(r1.getBuilt()).compareToIgnoreCase(convertBuildDate(r2.getBuilt())));
482            case BY_OWNER:
483                return (r1, r2) -> (r1.getOwnerName().compareToIgnoreCase(r2.getOwnerName()));
484            case BY_RFID:
485                return (r1, r2) -> (r1.getRfid().compareToIgnoreCase(r2.getRfid()));
486            case BY_VALUE:
487                return (r1, r2) -> (r1.getValue().compareToIgnoreCase(r2.getValue()));
488            case BY_LAST:
489                return (r1, r2) -> (r1.getLastMoveDate().compareTo(r2.getLastMoveDate()));
490            case BY_BLOCKING:
491                return (r1, r2) -> (r1.getBlocking() - r2.getBlocking());
492            case BY_PICKUP:
493                return (r1, r2) -> (r1.getPickupTime().compareToIgnoreCase(r2.getPickupTime()));
494            case BY_COMMENT:
495                return (r1, r2) -> (r1.getComment().compareToIgnoreCase(r2.getComment()));
496            default:
497                return (r1, r2) -> ((r1.getRoadName() + r1.getNumber())
498                        .compareToIgnoreCase(r2.getRoadName() + r2.getNumber()));
499        }
500    }
501    
502    protected List<T> sortByTrackPriority(List<T> list) {
503        List<T> out = new ArrayList<>();
504        // sort rolling stock by track priority
505        for (T rs : list) {
506            if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_HIGH)) {
507                out.add(rs);
508            }
509        }
510        for (T rs : list) {
511            if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_MEDIUM)) {
512                out.add(rs);
513            }
514        }
515        for (T rs : list) {
516            if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_NORMAL)) {
517                out.add(rs);
518            }
519        }
520        for (T rs : list) {
521            if (rs.getTrack() != null && rs.getTrack().getTrackPriority().equals(Track.PRIORITY_LOW)) {
522                out.add(rs);
523            }
524        }
525        // rolling stock without a track assignment
526        for (T rs : list) {
527            if (!out.contains(rs)) {
528                out.add(rs);
529            }
530        }
531        return out;
532    }
533
534    /*
535     * Converts build date into consistent String. Three build date formats; Two
536     * digits YY becomes 19YY. MM-YY becomes 19YY. MM-YYYY becomes YYYY.
537     */
538    public static String convertBuildDate(String date) {
539        String[] built = date.split("-");
540        if (built.length == 2) {
541            try {
542                int d = Integer.parseInt(built[1]);
543                if (d < 100) {
544                    d = d + 1900;
545                }
546                return Integer.toString(d);
547            } catch (NumberFormatException e) {
548                log.debug("Unable to parse built date ({})", date);
549            }
550        } else {
551            try {
552                int d = Integer.parseInt(date);
553                if (d < 100) {
554                    d = d + 1900;
555                }
556                return Integer.toString(d);
557            } catch (NumberFormatException e) {
558                log.debug("Unable to parse built date ({})", date);
559            }
560        }
561        return date;
562    }
563
564    /**
565     * Get a list of rolling stocks assigned to a train ordered by location
566     *
567     * @param train The Train.
568     *
569     * @return List of RollingStock assigned to the train ordered by location
570     */
571    public List<T> getByTrainList(Train train) {
572        return getByList(getList(train), BY_LOCATION);
573    }
574
575    /**
576     * Returns a list (no order) of RollingStock in a train.
577     *
578     * @param train The Train.
579     *
580     * @return list of RollingStock
581     */
582    public List<T> getList(Train train) {
583        List<T> out = new ArrayList<>();
584        getList().stream().filter((rs) -> {
585            return rs.getTrain() == train;
586        }).forEachOrdered((rs) -> {
587            out.add(rs);
588        });
589        return out;
590    }
591
592    /**
593     * Returns a list (no order) of RollingStock at a location.
594     *
595     * @param location location to search for.
596     * @return list of RollingStock
597     */
598    public List<T> getList(Location location) {
599        List<T> out = new ArrayList<>();
600        getList().stream().filter((rs) -> {
601            return rs.getLocation() == location;
602        }).forEachOrdered((rs) -> {
603            out.add(rs);
604        });
605        return out;
606    }
607
608    /**
609     * Returns a list (no order) of RollingStock on a track.
610     *
611     * @param track Track to search for.
612     * @return list of RollingStock
613     */
614    public List<T> getList(Track track) {
615        List<T> out = new ArrayList<>();
616        getList().stream().filter((rs) -> {
617            return rs.getTrack() == track;
618        }).forEachOrdered((rs) -> {
619            out.add(rs);
620        });
621        return out;
622    }
623    
624    /**
625     * Returns the rolling stock's last clone if there's one.
626     * 
627     * @param rs The rolling stock searching for a clone
628     * @return Returns the rolling stock's last clone, null if there isn't a
629     *         clone.
630     */
631    public T getClone(RollingStock rs) {
632        List<T> list = getByLastDateReversedList();
633        // clone with the highest creation number will be first in this list
634        for (T clone : list) {
635            if (clone.isClone() &&
636                    clone.getDestinationTrack() == rs.getTrack() &&
637                    clone.getRoadName().equals(rs.getRoadName()) &&
638                    clone.getNumber().split(RollingStock.CLONE_REGEX)[0].equals(rs.getNumber())) {
639                return clone;
640            }
641        }
642        return null; // no clone for this rolling stock
643    }
644
645    int cloneCreationOrder = 0;
646
647    /**
648     * Returns the highest clone creation order given to a clone.
649     * 
650     * @return 1 if the first clone created, otherwise the highest found plus
651     *         one. Automatically increments.
652     */
653    private int getCloneCreationOrder() {
654        if (cloneCreationOrder == 0) {
655            for (RollingStock rs : getList()) {
656                if (rs.isClone()) {
657                    String[] number = rs.getNumber().split(RollingStock.CLONE_REGEX);
658                    int creationOrder = Integer.parseInt(number[1]);
659                    if (creationOrder > cloneCreationOrder) {
660                        cloneCreationOrder = creationOrder;
661                    }
662                }
663            }
664        }
665        return ++cloneCreationOrder;
666    }
667    
668    /**
669     * Creates a clone of rolling stock and places it at the rolling stocks location and track.
670     * @param rs the rolling stock requesting a clone
671     * @return the clone of the rolling stock
672     */
673    protected T createClone(RollingStock rs) {
674        int cloneCreationOrder = getCloneCreationOrder();
675        return createClone(rs, cloneCreationOrder);
676    }
677    
678    protected T createClone(RollingStock rs, int cloneCreationOrder) {
679        @SuppressWarnings("unchecked")
680        T clone = (T) rs.copy();
681        clone.setNumber(rs.getNumber() + RollingStock.CLONE + padNumber(cloneCreationOrder));
682        clone.setClone(true);
683        clone.setMoves(rs.getMoves());
684        // register car before setting location so the car gets logged
685        register(clone);
686        clone.setLocation(rs.getLocation(), rs.getTrack(), RollingStock.FORCE);
687        rs.setCloneOrder(cloneCreationOrder); // for reset
688        return clone;
689    }
690    
691    /**
692     * Moves the rolling stock to the clone's destination track
693     * @param rs rolling stock to be moved
694     * @param track the destination track for the clone
695     * @param train the train that will transport the clone
696     * @param startTime when the rolling stock was moved
697     * @param clone the clone being transported by the train
698     */
699    protected void finshCreateClone(RollingStock rs, Track track, Train train, Date startTime, RollingStock clone) {
700        rs.setMoves(rs.getMoves() + 1); // bump count
701        rs.setLocation(track.getLocation(), track, RollingStock.FORCE);
702        rs.setLastTrain(train);
703        rs.setLastLocationId(clone.getLocationId());
704        rs.setLastTrackId(clone.getTrackId());
705        rs.setLastRouteId(train.getRoute().getId());
706        // this rs was moved during the build process
707        rs.setLastDate(startTime);
708        rs.setDestination(null, null);
709    }
710
711    /**
712     * Pads the number to 4 digits for sorting purposes
713     * 
714     * @param n the number needed leading zeros
715     * @return String "number" with leading zeros if necessary
716     */
717    protected String padNumber(int n) {
718        return String.format("%04d", n);
719    }
720
721    @Override
722    @OverridingMethodsMustInvokeSuper
723    public void propertyChange(PropertyChangeEvent evt) {
724        if (evt.getPropertyName().equals(Xml.ID)) {
725            @SuppressWarnings("unchecked")
726            T rs = (T) evt.getSource(); // unchecked cast to T  
727            _hashTable.remove(evt.getOldValue());
728            if (_hashTable.containsKey(rs.getId())) {
729                log.error("Duplicate rolling stock id: ({})", rs.getId());
730                rs.dispose();
731            } else {
732                _hashTable.put(rs.getId(), rs);
733            }
734            // fire so listeners that rebuild internal lists get signal of change in id, even without change in size
735            firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, _hashTable.size(), _hashTable.size());
736        }
737    }
738
739    private final static Logger log = LoggerFactory.getLogger(RollingStockManager.class);
740
741}