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