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.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    /**
412     * Get a list of all rolling stock sorted last date used
413     *
414     * @return list of RollingStock ordered by last date
415     */
416    public List<T> getByLastDateList() {
417        return getByList(getByIdList(), BY_LAST);
418    }
419    
420    public List<T> getByCommentList() {
421        return getByList(getByIdList(), BY_COMMENT);
422    }
423
424    /**
425     * Sort a specific list of rolling stock last date used
426     *
427     * @param inList list of rolling stock to sort.
428     * @return list of RollingStock ordered by last date
429     */
430    public List<T> getByLastDateList(List<T> inList) {
431        return getByList(inList, BY_LAST);
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    protected static final int BY_COMMENT = 14;
457
458    protected java.util.Comparator<T> getComparator(int attribute) {
459        switch (attribute) {
460            case BY_NUMBER:
461                return (r1, r2) -> (r1.getNumber().compareToIgnoreCase(r2.getNumber()));
462            case BY_ROAD:
463                return (r1, r2) -> (r1.getRoadName().compareToIgnoreCase(r2.getRoadName()));
464            case BY_TYPE:
465                return (r1, r2) -> (r1.getTypeName().compareToIgnoreCase(r2.getTypeName()));
466            case BY_COLOR:
467                return (r1, r2) -> (r1.getColor().compareToIgnoreCase(r2.getColor()));
468            case BY_LOCATION:
469                return (r1, r2) -> (r1.getStatus() + r1.getLocationName() + r1.getTrackName())
470                        .compareToIgnoreCase(r2.getStatus() + r2.getLocationName() + r2.getTrackName());
471            case BY_DESTINATION:
472                return (r1, r2) -> (r1.getDestinationName() + r1.getDestinationTrackName())
473                        .compareToIgnoreCase(r2.getDestinationName() + r2.getDestinationTrackName());
474            case BY_TRAIN:
475                return (r1, r2) -> (r1.getTrainName().compareToIgnoreCase(r2.getTrainName()));
476            case BY_MOVES:
477                return (r1, r2) -> (r1.getMoves() - r2.getMoves());
478            case BY_BUILT:
479                return (r1,
480                        r2) -> (convertBuildDate(r1.getBuilt()).compareToIgnoreCase(convertBuildDate(r2.getBuilt())));
481            case BY_OWNER:
482                return (r1, r2) -> (r1.getOwnerName().compareToIgnoreCase(r2.getOwnerName()));
483            case BY_RFID:
484                return (r1, r2) -> (r1.getRfid().compareToIgnoreCase(r2.getRfid()));
485            case BY_VALUE:
486                return (r1, r2) -> (r1.getValue().compareToIgnoreCase(r2.getValue()));
487            case BY_LAST:
488                return (r1, r2) -> (r1.getLastMoveDate().compareTo(r2.getLastMoveDate()));
489            case BY_BLOCKING:
490                return (r1, r2) -> (r1.getBlocking() - r2.getBlocking());
491            case BY_COMMENT:
492                return (r1, r2) -> (r1.getComment().compareToIgnoreCase(r2.getComment()));
493            default:
494                return (r1, r2) -> ((r1.getRoadName() + r1.getNumber())
495                        .compareToIgnoreCase(r2.getRoadName() + r2.getNumber()));
496        }
497    }
498
499    /*
500     * Converts build date into consistent String. Three build date formats; Two
501     * digits YY becomes 19YY. MM-YY becomes 19YY. MM-YYYY becomes YYYY.
502     */
503    public static String convertBuildDate(String date) {
504        String[] built = date.split("-");
505        if (built.length == 2) {
506            try {
507                int d = Integer.parseInt(built[1]);
508                if (d < 100) {
509                    d = d + 1900;
510                }
511                return Integer.toString(d);
512            } catch (NumberFormatException e) {
513                log.debug("Unable to parse built date ({})", date);
514            }
515        } else {
516            try {
517                int d = Integer.parseInt(date);
518                if (d < 100) {
519                    d = d + 1900;
520                }
521                return Integer.toString(d);
522            } catch (NumberFormatException e) {
523                log.debug("Unable to parse built date ({})", date);
524            }
525        }
526        return date;
527    }
528
529    /**
530     * Get a list of rolling stocks assigned to a train ordered by location
531     *
532     * @param train The Train.
533     *
534     * @return List of RollingStock assigned to the train ordered by location
535     */
536    public List<T> getByTrainList(Train train) {
537        return getByList(getList(train), BY_LOCATION);
538    }
539
540    /**
541     * Returns a list (no order) of RollingStock in a train.
542     *
543     * @param train The Train.
544     *
545     * @return list of RollingStock
546     */
547    public List<T> getList(Train train) {
548        List<T> out = new ArrayList<>();
549        _hashTable.values().stream().filter((rs) -> {
550            return rs.getTrain() == train;
551        }).forEachOrdered((rs) -> {
552            out.add(rs);
553        });
554        return out;
555    }
556
557    /**
558     * Returns a list (no order) of RollingStock at a location.
559     *
560     * @param location location to search for.
561     * @return list of RollingStock
562     */
563    public List<T> getList(Location location) {
564        List<T> out = new ArrayList<>();
565        _hashTable.values().stream().filter((rs) -> {
566            return rs.getLocation() == location;
567        }).forEachOrdered((rs) -> {
568            out.add(rs);
569        });
570        return out;
571    }
572
573    /**
574     * Returns a list (no order) of RollingStock on a track.
575     *
576     * @param track Track to search for.
577     * @return list of RollingStock
578     */
579    public List<T> getList(Track track) {
580        List<T> out = new ArrayList<>();
581        _hashTable.values().stream().filter((rs) -> {
582            return rs.getTrack() == track;
583        }).forEachOrdered((rs) -> {
584            out.add(rs);
585        });
586        return out;
587    }
588
589    @Override
590    @OverridingMethodsMustInvokeSuper
591    public void propertyChange(PropertyChangeEvent evt) {
592        if (evt.getPropertyName().equals(Xml.ID)) {
593            @SuppressWarnings("unchecked")
594            T rs = (T) evt.getSource(); // unchecked cast to T  
595            _hashTable.remove(evt.getOldValue());
596            if (_hashTable.containsKey(rs.getId())) {
597                log.error("Duplicate rolling stock id: ({})", rs.getId());
598                rs.dispose();
599            } else {
600                _hashTable.put(rs.getId(), rs);
601            }
602            // fire so listeners that rebuild internal lists get signal of change in id, even without change in size
603            firePropertyChange(LISTLENGTH_CHANGED_PROPERTY, _hashTable.size(), _hashTable.size());
604        }
605    }
606
607    private final static Logger log = LoggerFactory.getLogger(RollingStockManager.class);
608
609}