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.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.contains(rs)) {
117            int oldSize = _hashTable.size();
118            rs.addPropertyChangeListener(this);
119            _hashTable.put(rs.getId(), rs);
120            firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize, _hashTable.size());
121        }
122    }
123
124    /**
125     * Unload RollingStock.
126     *
127     * @param rs The RollingStock to delete.
128     */
129    public void deregister(T rs) {
130        rs.removePropertyChangeListener(this);
131        rs.dispose();
132        int oldSize = _hashTable.size();
133        _hashTable.remove(rs.getId());
134        firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize, _hashTable.size());
135    }
136
137    /**
138     * Remove all RollingStock from roster
139     */
140    public void deleteAll() {
141        int oldSize = _hashTable.size();
142        Enumeration<String> en = _hashTable.keys();
143        while (en.hasMoreElements()) {
144            T rs = getById(en.nextElement());
145            rs.dispose();
146            _hashTable.remove(rs.getId());
147        }
148        firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, oldSize, _hashTable.size());
149    }
150
151    public void resetMoves() {
152        Enumeration<String> en = _hashTable.keys();
153        while (en.hasMoreElements()) {
154            T rs = getById(en.nextElement());
155            rs.setMoves(0);
156        }
157    }
158
159    /**
160     * Returns a list (no order) of RollingStock.
161     *
162     * @return list of RollingStock
163     */
164    public List<T> getList() {
165        return new ArrayList<>(_hashTable.values());
166    }
167
168    /**
169     * Sort by rolling stock id
170     *
171     * @return list of RollingStock ordered by id
172     */
173    public List<T> getByIdList() {
174        Enumeration<String> en = _hashTable.keys();
175        String[] arr = new String[_hashTable.size()];
176        List<T> out = new ArrayList<>();
177        int i = 0;
178        while (en.hasMoreElements()) {
179            arr[i++] = en.nextElement();
180        }
181        Arrays.sort(arr);
182        for (i = 0; i < arr.length; i++) {
183            out.add(getById(arr[i]));
184        }
185        return out;
186    }
187
188    /**
189     * Sort by rolling stock road name
190     *
191     * @return list of RollingStock ordered by road name
192     */
193    public List<T> getByRoadNameList() {
194        return getByList(getByIdList(), BY_ROAD);
195    }
196
197    private static final int PAGE_SIZE = 64;
198    private static final int NOT_INTEGER = -999999999; // flag when RS number isn't an Integer
199
200    /**
201     * Sort by rolling stock number, number can be alphanumeric. RollingStock
202     * number can also be in the format of nnnn-N, where the "-N" allows the
203     * user to enter RollingStock with similar numbers.
204     *
205     * @return list of RollingStock ordered by number
206     */
207    public List<T> getByNumberList() {
208        // first get by road list
209        List<T> sortIn = getByRoadNameList();
210        // now re-sort
211        List<T> out = new ArrayList<>();
212        int rsNumber = 0;
213        int outRsNumber = 0;
214
215        for (T rs : sortIn) {
216            boolean rsAdded = false;
217            try {
218                rsNumber = Integer.parseInt(rs.getNumber());
219                rs.number = rsNumber;
220            } catch (NumberFormatException e) {
221                // maybe rolling stock number in the format nnnn-N
222                try {
223                    String[] number = rs.getNumber().split(TrainCommon.HYPHEN);
224                    rsNumber = Integer.parseInt(number[0]);
225                    rs.number = rsNumber;
226                } catch (NumberFormatException e2) {
227                    rs.number = NOT_INTEGER;
228                    // sort alphanumeric numbers at the end of the out list
229                    String numberIn = rs.getNumber();
230                    // log.debug("rolling stock in road number ("+numberIn+") isn't a number");
231                    for (int k = (out.size() - 1); k >= 0; k--) {
232                        String numberOut = out.get(k).getNumber();
233                        try {
234                            Integer.parseInt(numberOut);
235                            // done, place rolling stock with alphanumeric
236                            // number after rolling stocks with real numbers.
237                            out.add(k + 1, rs);
238                            rsAdded = true;
239                            break;
240                        } catch (NumberFormatException e3) {
241                            if (numberIn.compareToIgnoreCase(numberOut) >= 0) {
242                                out.add(k + 1, rs);
243                                rsAdded = true;
244                                break;
245                            }
246                        }
247                    }
248                    if (!rsAdded) {
249                        out.add(0, rs);
250                    }
251                    continue;
252                }
253            }
254
255            int start = 0;
256            // page to improve sort performance.
257            int divisor = out.size() / PAGE_SIZE;
258            for (int k = divisor; k > 0; k--) {
259                outRsNumber = out.get((out.size() - 1) * k / divisor).number;
260                if (outRsNumber == NOT_INTEGER) {
261                    continue;
262                }
263                if (rsNumber >= outRsNumber) {
264                    start = (out.size() - 1) * k / divisor;
265                    break;
266                }
267            }
268            for (int j = start; j < out.size(); j++) {
269                outRsNumber = out.get(j).number;
270                if (outRsNumber == NOT_INTEGER) {
271                    try {
272                        outRsNumber = Integer.parseInt(out.get(j).getNumber());
273                    } catch (NumberFormatException e) {
274                        try {
275                            String[] number = out.get(j).getNumber().split(TrainCommon.HYPHEN);
276                            outRsNumber = Integer.parseInt(number[0]);
277                        } catch (NumberFormatException e2) {
278                            // force add
279                            outRsNumber = rsNumber + 1;
280                        }
281                    }
282                }
283                if (rsNumber < outRsNumber) {
284                    out.add(j, rs);
285                    rsAdded = true;
286                    break;
287                }
288            }
289            if (!rsAdded) {
290                out.add(rs);
291            }
292        }
293        // log.debug("end rolling stock sort by number list");
294        return out;
295    }
296
297    /**
298     * Sort by rolling stock type names
299     *
300     * @return list of RollingStock ordered by RollingStock type
301     */
302    public List<T> getByTypeList() {
303        return getByList(getByRoadNameList(), BY_TYPE);
304    }
305
306    /**
307     * Return rolling stock of a specific type
308     *
309     * @param type type of rolling stock
310     * @return list of RollingStock that are specific type
311     */
312    public List<T> getByTypeList(String type) {
313        List<T> typeList = getByTypeList();
314        List<T> out = new ArrayList<>();
315        for (T rs : typeList) {
316            if (rs.getTypeName().equals(type)) {
317                out.add(rs);
318            }
319        }
320        return out;
321    }
322
323    /**
324     * Sort by rolling stock color names
325     *
326     * @return list of RollingStock ordered by RollingStock color
327     */
328    public List<T> getByColorList() {
329        return getByList(getByTypeList(), BY_COLOR);
330    }
331
332    /**
333     * Sort by rolling stock location
334     *
335     * @return list of RollingStock ordered by RollingStock location
336     */
337    public List<T> getByLocationList() {
338        return getByList(getByNumberList(), BY_LOCATION);
339    }
340
341    /**
342     * Sort by rolling stock destination
343     *
344     * @return list of RollingStock ordered by RollingStock destination
345     */
346    public List<T> getByDestinationList() {
347        return getByList(getByLocationList(), BY_DESTINATION);
348    }
349
350    /**
351     * Sort by rolling stocks in trains
352     *
353     * @return list of RollingStock ordered by trains
354     */
355    public List<T> getByTrainList() {
356        List<T> byDest = getByList(getByIdList(), BY_DESTINATION);
357        List<T> byLoc = getByList(byDest, BY_LOCATION);
358        return getByList(byLoc, BY_TRAIN);
359    }
360
361    /**
362     * Sort by rolling stock moves
363     *
364     * @return list of RollingStock ordered by RollingStock moves
365     */
366    public List<T> getByMovesList() {
367        return getByList(getList(), BY_MOVES);
368    }
369
370    /**
371     * Sort by when rolling stock was built
372     *
373     * @return list of RollingStock ordered by RollingStock built date
374     */
375    public List<T> getByBuiltList() {
376        return getByList(getByIdList(), BY_BUILT);
377    }
378
379    /**
380     * Sort by rolling stock owner
381     *
382     * @return list of RollingStock ordered by RollingStock owner
383     */
384    public List<T> getByOwnerList() {
385        return getByList(getByIdList(), BY_OWNER);
386    }
387
388    /**
389     * Sort by rolling stock value
390     *
391     * @return list of RollingStock ordered by value
392     */
393    public List<T> getByValueList() {
394        return getByList(getByIdList(), BY_VALUE);
395    }
396
397    /**
398     * Sort by rolling stock RFID
399     *
400     * @return list of RollingStock ordered by RFIDs
401     */
402    public List<T> getByRfidList() {
403        return getByList(getByIdList(), BY_RFID);
404    }
405
406    /**
407     * Get a list of all rolling stock sorted last date used
408     *
409     * @return list of RollingStock ordered by last date
410     */
411    public List<T> getByLastDateList() {
412        return getByList(getByIdList(), BY_LAST);
413    }
414    
415    public List<T> getByCommentList() {
416        return getByList(getByIdList(), BY_COMMENT);
417    }
418
419    /**
420     * Sort a specific list of rolling stock last date used
421     *
422     * @param inList list of rolling stock to sort.
423     * @return list of RollingStock ordered by last date
424     */
425    public List<T> getByLastDateList(List<T> inList) {
426        return getByList(inList, BY_LAST);
427    }
428
429    protected List<T> getByList(List<T> sortIn, int attribute) {
430        List<T> out = new ArrayList<>(sortIn);
431        out.sort(getComparator(attribute));
432        return out;
433    }
434
435    // The various sort options for RollingStock
436    // see CarManager and EngineManger for other values
437    protected static final int BY_NUMBER = 0;
438    protected static final int BY_ROAD = 1;
439    protected static final int BY_TYPE = 2;
440    protected static final int BY_COLOR = 3;
441    protected static final int BY_LOCATION = 4;
442    protected static final int BY_DESTINATION = 5;
443    protected static final int BY_TRAIN = 6;
444    protected static final int BY_MOVES = 7;
445    protected static final int BY_BUILT = 8;
446    protected static final int BY_OWNER = 9;
447    protected static final int BY_RFID = 10;
448    protected static final int BY_VALUE = 11;
449    protected static final int BY_LAST = 12;
450    protected static final int BY_BLOCKING = 13;
451    protected static final int BY_COMMENT = 14;
452
453    protected java.util.Comparator<T> getComparator(int attribute) {
454        switch (attribute) {
455            case BY_NUMBER:
456                return (r1, r2) -> (r1.getNumber().compareToIgnoreCase(r2.getNumber()));
457            case BY_ROAD:
458                return (r1, r2) -> (r1.getRoadName().compareToIgnoreCase(r2.getRoadName()));
459            case BY_TYPE:
460                return (r1, r2) -> (r1.getTypeName().compareToIgnoreCase(r2.getTypeName()));
461            case BY_COLOR:
462                return (r1, r2) -> (r1.getColor().compareToIgnoreCase(r2.getColor()));
463            case BY_LOCATION:
464                return (r1, r2) -> (r1.getStatus() + r1.getLocationName() + r1.getTrackName())
465                        .compareToIgnoreCase(r2.getStatus() + r2.getLocationName() + r2.getTrackName());
466            case BY_DESTINATION:
467                return (r1, r2) -> (r1.getDestinationName() + r1.getDestinationTrackName())
468                        .compareToIgnoreCase(r2.getDestinationName() + r2.getDestinationTrackName());
469            case BY_TRAIN:
470                return (r1, r2) -> (r1.getTrainName().compareToIgnoreCase(r2.getTrainName()));
471            case BY_MOVES:
472                return (r1, r2) -> (r1.getMoves() - r2.getMoves());
473            case BY_BUILT:
474                return (r1,
475                        r2) -> (convertBuildDate(r1.getBuilt()).compareToIgnoreCase(convertBuildDate(r2.getBuilt())));
476            case BY_OWNER:
477                return (r1, r2) -> (r1.getOwnerName().compareToIgnoreCase(r2.getOwnerName()));
478            case BY_RFID:
479                return (r1, r2) -> (r1.getRfid().compareToIgnoreCase(r2.getRfid()));
480            case BY_VALUE:
481                return (r1, r2) -> (r1.getValue().compareToIgnoreCase(r2.getValue()));
482            case BY_LAST:
483                return (r1, r2) -> (r1.getLastMoveDate().compareTo(r2.getLastMoveDate()));
484            case BY_BLOCKING:
485                return (r1, r2) -> (r1.getBlocking() - r2.getBlocking());
486            case BY_COMMENT:
487                return (r1, r2) -> (r1.getComment().compareToIgnoreCase(r2.getComment()));
488            default:
489                return (r1, r2) -> ((r1.getRoadName() + r1.getNumber())
490                        .compareToIgnoreCase(r2.getRoadName() + r2.getNumber()));
491        }
492    }
493
494    /*
495     * Converts build date into consistent String. Three build date formats; Two
496     * digits YY becomes 19YY. MM-YY becomes 19YY. MM-YYYY becomes YYYY.
497     */
498    public static String convertBuildDate(String date) {
499        String[] built = date.split("-");
500        if (built.length == 2) {
501            try {
502                int d = Integer.parseInt(built[1]);
503                if (d < 100) {
504                    d = d + 1900;
505                }
506                return Integer.toString(d);
507            } catch (NumberFormatException e) {
508                log.debug("Unable to parse built date ({})", date);
509            }
510        } else {
511            try {
512                int d = Integer.parseInt(date);
513                if (d < 100) {
514                    d = d + 1900;
515                }
516                return Integer.toString(d);
517            } catch (NumberFormatException e) {
518                log.debug("Unable to parse built date ({})", date);
519            }
520        }
521        return date;
522    }
523
524    /**
525     * Get a list of rolling stocks assigned to a train ordered by location
526     *
527     * @param train The Train.
528     *
529     * @return List of RollingStock assigned to the train ordered by location
530     */
531    public List<T> getByTrainList(Train train) {
532        return getByList(getList(train), BY_LOCATION);
533    }
534
535    /**
536     * Returns a list (no order) of RollingStock in a train.
537     *
538     * @param train The Train.
539     *
540     * @return list of RollingStock
541     */
542    public List<T> getList(Train train) {
543        List<T> out = new ArrayList<>();
544        _hashTable.values().stream().filter((rs) -> {
545            return rs.getTrain() == train;
546        }).forEachOrdered((rs) -> {
547            out.add(rs);
548        });
549        return out;
550    }
551
552    /**
553     * Returns a list (no order) of RollingStock at a location.
554     *
555     * @param location location to search for.
556     * @return list of RollingStock
557     */
558    public List<T> getList(Location location) {
559        List<T> out = new ArrayList<>();
560        _hashTable.values().stream().filter((rs) -> {
561            return rs.getLocation() == location;
562        }).forEachOrdered((rs) -> {
563            out.add(rs);
564        });
565        return out;
566    }
567
568    /**
569     * Returns a list (no order) of RollingStock on a track.
570     *
571     * @param track Track to search for.
572     * @return list of RollingStock
573     */
574    public List<T> getList(Track track) {
575        List<T> out = new ArrayList<>();
576        _hashTable.values().stream().filter((rs) -> {
577            return rs.getTrack() == track;
578        }).forEachOrdered((rs) -> {
579            out.add(rs);
580        });
581        return out;
582    }
583
584    @Override
585    @OverridingMethodsMustInvokeSuper
586    public void propertyChange(PropertyChangeEvent evt) {
587        if (evt.getPropertyName().equals(Xml.ID)) {
588            @SuppressWarnings("unchecked")
589            T rs = (T) evt.getSource(); // unchecked cast to T  
590            _hashTable.remove(evt.getOldValue());
591            _hashTable.put(rs.getId(), rs);
592            // fire so listeners that rebuild internal lists get signal of change in id, even without change in size
593            firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, _hashTable.size(), _hashTable.size());
594        }
595    }
596
597    private final static Logger log = LoggerFactory.getLogger(RollingStockManager.class);
598
599}