001package jmri.jmrit.operations.locations.schedules;
002
003import java.util.*;
004
005import org.jdom2.Element;
006import org.slf4j.Logger;
007import org.slf4j.LoggerFactory;
008
009import jmri.InstanceManager;
010import jmri.beans.PropertyChangeSupport;
011import jmri.jmrit.operations.locations.*;
012import jmri.jmrit.operations.rollingstock.cars.*;
013import jmri.jmrit.operations.setup.Control;
014import jmri.jmrit.operations.trains.schedules.TrainSchedule;
015import jmri.jmrit.operations.trains.schedules.TrainScheduleManager;
016
017/**
018 * Represents a car delivery schedule for a location
019 *
020 * @author Daniel Boudreau Copyright (C) 2009, 2011, 2013
021 */
022public class Schedule extends PropertyChangeSupport implements java.beans.PropertyChangeListener {
023
024    protected String _id = "";
025    protected String _name = "";
026    protected String _comment = "";
027
028    // stores ScheduleItems for this schedule
029    protected Hashtable<String, ScheduleItem> _scheduleHashTable = new Hashtable<String, ScheduleItem>();
030    protected int _IdNumber = 0; // each item in a schedule gets its own id
031    protected int _sequenceNum = 0; // each item has a unique sequence number
032
033    public static final String LISTCHANGE_CHANGED_PROPERTY = "scheduleListChange"; // NOI18N
034    public static final String DISPOSE = "scheduleDispose"; // NOI18N
035
036    public static final String SCHEDULE_OKAY = ""; // NOI18N
037
038    public Schedule(String id, String name) {
039        log.debug("New schedule ({}) id: {}", name, id);
040        _name = name;
041        _id = id;
042    }
043
044    public String getId() {
045        return _id;
046    }
047
048    public void setName(String name) {
049        String old = _name;
050        _name = name;
051        if (!old.equals(name)) {
052            setDirtyAndFirePropertyChange("ScheduleName", old, name); // NOI18N
053        }
054    }
055
056    // for combo boxes
057    @Override
058    public String toString() {
059        return _name;
060    }
061
062    public String getName() {
063        return _name;
064    }
065
066    public int getSize() {
067        return _scheduleHashTable.size();
068    }
069
070    public void setComment(String comment) {
071        String old = _comment;
072        _comment = comment;
073        if (!old.equals(comment)) {
074            setDirtyAndFirePropertyChange("ScheduleComment", old, comment); // NOI18N
075        }
076    }
077
078    public String getComment() {
079        return _comment;
080    }
081
082    public void dispose() {
083        setDirtyAndFirePropertyChange(DISPOSE, null, DISPOSE);
084    }
085
086    public void resetHitCounts() {
087        for (ScheduleItem si : getItemsByIdList()) {
088            si.setHits(0);
089        }
090    }
091
092    /**
093     * Adds a car type to the end of this schedule
094     * 
095     * @param type The string car type to add.
096     * @return ScheduleItem created for the car type added
097     */
098    public ScheduleItem addItem(String type) {
099        _IdNumber++;
100        _sequenceNum++;
101        String id = _id + "c" + Integer.toString(_IdNumber);
102        log.debug("Adding new item to ({}) id: {}", getName(), id);
103        ScheduleItem si = new ScheduleItem(id, type);
104        si.setSequenceId(_sequenceNum);
105        Integer old = Integer.valueOf(_scheduleHashTable.size());
106        _scheduleHashTable.put(si.getId(), si);
107
108        setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, old, Integer.valueOf(_scheduleHashTable.size()));
109        // listen for set out and pick up changes to forward
110        si.addPropertyChangeListener(this);
111        return si;
112    }
113
114    /**
115     * Add a schedule item at a specific place (sequence) in the schedule
116     * Allowable sequence numbers are 0 to max size of schedule. 0 = start of
117     * list.
118     * 
119     * @param carType  The string car type name to add.
120     * @param sequence Where in the schedule to add the item.
121     * @return schedule item
122     */
123    public ScheduleItem addItem(String carType, int sequence) {
124        ScheduleItem si = addItem(carType);
125        if (sequence < 0 || sequence > _scheduleHashTable.size()) {
126            return si;
127        }
128        for (int i = 0; i < _scheduleHashTable.size() - sequence - 1; i++) {
129            moveItemUp(si);
130        }
131        return si;
132    }
133
134    /**
135     * Remember a NamedBean Object created outside the manager.
136     * 
137     * @param si The schedule item to add.
138     */
139    public void register(ScheduleItem si) {
140        Integer old = Integer.valueOf(_scheduleHashTable.size());
141        _scheduleHashTable.put(si.getId(), si);
142
143        // find last id created
144        String[] getId = si.getId().split("c");
145        int id = Integer.parseInt(getId[1]);
146        if (id > _IdNumber) {
147            _IdNumber = id;
148        }
149        // find highest sequence number
150        if (si.getSequenceId() > _sequenceNum) {
151            _sequenceNum = si.getSequenceId();
152        }
153        setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, old, Integer.valueOf(_scheduleHashTable.size()));
154        // listen for set out and pick up changes to forward
155        si.addPropertyChangeListener(this);
156    }
157
158    /**
159     * Delete a ScheduleItem
160     * 
161     * @param si The scheduleItem to delete.
162     */
163    public void deleteItem(ScheduleItem si) {
164        if (si != null) {
165            si.removePropertyChangeListener(this);
166            // subtract from the items's available track length
167            String id = si.getId();
168            si.dispose();
169            Integer old = Integer.valueOf(_scheduleHashTable.size());
170            _scheduleHashTable.remove(id);
171            resequenceIds();
172            setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, old, Integer.valueOf(_scheduleHashTable.size()));
173        }
174    }
175
176    /**
177     * Reorder the item sequence numbers for this schedule
178     */
179    private void resequenceIds() {
180        List<ScheduleItem> scheduleItems = getItemsBySequenceList();
181        for (int i = 0; i < scheduleItems.size(); i++) {
182            scheduleItems.get(i).setSequenceId(i + 1); // start sequence numbers
183                                                       // at 1
184            _sequenceNum = i + 1;
185        }
186    }
187
188    /**
189     * Get item by car type (gets last schedule item with this type)
190     * 
191     * @param carType The string car type to search for.
192     * @return schedule item
193     */
194    public ScheduleItem getItemByType(String carType) {
195        List<ScheduleItem> scheduleSequenceList = getItemsBySequenceList();
196        ScheduleItem si;
197
198        for (int i = scheduleSequenceList.size() - 1; i >= 0; i--) {
199            si = scheduleSequenceList.get(i);
200            if (si.getTypeName().equals(carType)) {
201                return si;
202            }
203        }
204        return null;
205    }
206
207    /**
208     * Get a ScheduleItem by id
209     * 
210     * @param id The string id of the ScheduleItem.
211     * @return schedule item
212     */
213    public ScheduleItem getItemById(String id) {
214        return _scheduleHashTable.get(id);
215    }
216
217    private List<ScheduleItem> getItemsByIdList() {
218        String[] arr = new String[_scheduleHashTable.size()];
219        List<ScheduleItem> out = new ArrayList<ScheduleItem>();
220        Enumeration<String> en = _scheduleHashTable.keys();
221        int i = 0;
222        while (en.hasMoreElements()) {
223            arr[i++] = en.nextElement();
224        }
225        Arrays.sort(arr);
226        for (i = 0; i < arr.length; i++) {
227            out.add(getItemById(arr[i]));
228        }
229        return out;
230    }
231
232    /**
233     * Get a list of ScheduleItems sorted by schedule order
234     *
235     * @return list of ScheduleItems ordered by sequence
236     */
237    public List<ScheduleItem> getItemsBySequenceList() {
238        // first get id list
239        List<ScheduleItem> sortList = getItemsByIdList();
240        // now re-sort
241        List<ScheduleItem> out = new ArrayList<ScheduleItem>();
242
243        for (ScheduleItem si : sortList) {
244            for (int j = 0; j < out.size(); j++) {
245                if (si.getSequenceId() < out.get(j).getSequenceId()) {
246                    out.add(j, si);
247                    break;
248                }
249            }
250            if (!out.contains(si)) {
251                out.add(si);
252            }
253        }
254        return out;
255    }
256
257    /**
258     * Places a ScheduleItem earlier in the schedule
259     * 
260     * @param si The ScheduleItem to move.
261     */
262    public void moveItemUp(ScheduleItem si) {
263        int sequenceId = si.getSequenceId();
264        if (sequenceId - 1 <= 0) {
265            si.setSequenceId(_sequenceNum + 1); // move to the end of the list
266            resequenceIds();
267        } else {
268            // adjust the other item taken by this one
269            ScheduleItem replaceSi = getItemBySequenceId(sequenceId - 1);
270            if (replaceSi != null) {
271                replaceSi.setSequenceId(sequenceId);
272                si.setSequenceId(sequenceId - 1);
273            } else {
274                resequenceIds(); // error the sequence number is missing
275            }
276        }
277        setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, Integer.toString(sequenceId));
278    }
279
280    /**
281     * Places a ScheduleItem later in the schedule
282     * 
283     * @param si The ScheduleItem to move.
284     */
285    public void moveItemDown(ScheduleItem si) {
286        int sequenceId = si.getSequenceId();
287        if (sequenceId + 1 > _sequenceNum) {
288            si.setSequenceId(0); // move to the start of the list
289            resequenceIds();
290        } else {
291            // adjust the other item taken by this one
292            ScheduleItem replaceSi = getItemBySequenceId(sequenceId + 1);
293            if (replaceSi != null) {
294                replaceSi.setSequenceId(sequenceId);
295                si.setSequenceId(sequenceId + 1);
296            } else {
297                resequenceIds(); // error the sequence number is missing
298            }
299        }
300        setDirtyAndFirePropertyChange(LISTCHANGE_CHANGED_PROPERTY, null, Integer.toString(sequenceId));
301    }
302
303    public ScheduleItem getItemBySequenceId(int sequenceId) {
304        for (ScheduleItem si : getItemsByIdList()) {
305            if (si.getSequenceId() == sequenceId) {
306                return si;
307            }
308        }
309        return null;
310    }
311
312    /**
313     * Check to see if schedule is valid for the track.
314     * 
315     * @param track The track associated with this schedule
316     * @return SCHEDULE_OKAY if schedule okay, otherwise an error message.
317     */
318    public String checkScheduleValid(Track track) {
319        String status = SCHEDULE_OKAY;
320        List<ScheduleItem> scheduleItems = getItemsBySequenceList();
321        if (scheduleItems.size() == 0) {
322            return Bundle.getMessage("empty");
323        }
324        for (ScheduleItem si : scheduleItems) {
325            // check train schedules
326            if (!si.getSetoutTrainScheduleId().equals(ScheduleItem.NONE) &&
327                    InstanceManager.getDefault(TrainScheduleManager.class)
328                            .getScheduleById(si.getSetoutTrainScheduleId()) == null) {
329                status = Bundle.getMessage("NotValid", si.getSetoutTrainScheduleId());
330                break;
331            }
332            if (!si.getPickupTrainScheduleId().equals(ScheduleItem.NONE) &&
333                    InstanceManager.getDefault(TrainScheduleManager.class)
334                            .getScheduleById(si.getPickupTrainScheduleId()) == null) {
335                status = Bundle.getMessage("NotValid", si.getPickupTrainScheduleId());
336                break;
337            }
338            if (!track.getLocation().acceptsTypeName(si.getTypeName())) {
339                status = Bundle.getMessage("NotValid", si.getTypeName());
340                break;
341            }
342            if (!track.isTypeNameAccepted(si.getTypeName())) {
343                status = Bundle.getMessage("NotValid", si.getTypeName());
344                break;
345            }
346            // check roads, accepted by track, valid road, and there's at least
347            // one car with
348            // that road
349            if (!si.getRoadName().equals(ScheduleItem.NONE) &&
350                    (!track.isRoadNameAccepted(si.getRoadName()) ||
351                            !InstanceManager.getDefault(CarRoads.class).containsName(si.getRoadName()) ||
352                            InstanceManager.getDefault(CarManager.class).getByTypeAndRoad(si.getTypeName(),
353                                    si.getRoadName()) == null)) {
354                status = Bundle.getMessage("NotValid", si.getRoadName());
355                break;
356            }
357            // check loads
358            List<String> loads = InstanceManager.getDefault(CarLoads.class).getNames(si.getTypeName());
359            if (!si.getReceiveLoadName().equals(ScheduleItem.NONE) &&
360                    (!track.isLoadNameAndCarTypeAccepted(si.getReceiveLoadName(), si.getTypeName()) ||
361                            !loads.contains(si.getReceiveLoadName()))) {
362                status = Bundle.getMessage("NotValid", si.getReceiveLoadName());
363                break;
364            }
365            if (!si.getShipLoadName().equals(ScheduleItem.NONE) && !loads.contains(si.getShipLoadName())) {
366                status = Bundle.getMessage("NotValid", si.getShipLoadName());
367                break;
368            }
369            // check destination
370            if (si.getDestination() != null &&
371                    (!si.getDestination().acceptsTypeName(si.getTypeName()) ||
372                            InstanceManager.getDefault(LocationManager.class)
373                                    .getLocationById(si.getDestination().getId()) == null)) {
374                status = Bundle.getMessage("NotValid", si.getDestination());
375                break;
376            }
377            // check destination track
378            if (si.getDestination() != null && si.getDestinationTrack() != null) {
379                if (!si.getDestination().isTrackAtLocation(si.getDestinationTrack())) {
380                    status = Bundle.getMessage("NotValid",
381                            si.getDestinationTrack() + " (" + Bundle.getMessage("Track") + ")");
382                    break;
383                }
384                if (!si.getDestinationTrack().isTypeNameAccepted(si.getTypeName())) {
385                    status = Bundle.getMessage("NotValid",
386                            si.getDestinationTrack() + " (" + Bundle.getMessage("Type") + ")");
387                    break;
388                }
389                if (!si.getRoadName().equals(ScheduleItem.NONE) &&
390                        !si.getDestinationTrack().isRoadNameAccepted(si.getRoadName())) {
391                    status = Bundle.getMessage("NotValid",
392                            si.getDestinationTrack() + " (" + Bundle.getMessage("Road") + ")");
393                    break;
394                }
395                if (!si.getShipLoadName().equals(ScheduleItem.NONE) &&
396                        !si.getDestinationTrack().isLoadNameAndCarTypeAccepted(si.getShipLoadName(),
397                                si.getTypeName())) {
398                    status = Bundle.getMessage("NotValid",
399                            si.getDestinationTrack() + " (" + Bundle.getMessage("Load") + ")");
400                    break;
401                }
402            }
403        }
404        return status;
405    }
406
407    private static boolean debugFlag = false;
408
409    /*
410     * Match mode search
411     */
412    public String searchSchedule(Car car, Track track) {
413        if (debugFlag) {
414            log.debug("Search match for car ({}) type ({}) load ({})", car.toString(), car.getTypeName(),
415                    car.getLoadName());
416        }
417        // has the car already been assigned a schedule item? Then verify that
418        // its still okay
419        if (!car.getScheduleItemId().equals(Track.NONE)) {
420            ScheduleItem si = getItemById(car.getScheduleItemId());
421            if (si != null) {
422                String status = checkScheduleItem(si, car, track);
423                if (status.equals(Track.OKAY)) {
424                    track.setScheduleItemId(si.getId());
425                    return Track.OKAY;
426                }
427                log.debug("Car ({}) with schedule id ({}) failed check, status: {}", car.toString(),
428                        car.getScheduleItemId(), status);
429            }
430        }
431        // first check to see if the schedule services car type
432        if (!checkScheduleAttribute(Track.TYPE, car.getTypeName(), car)) {
433            return Track.SCHEDULE + " " + Bundle.getMessage("scheduleNotType", getName(), car.getTypeName());
434        }
435
436        // search schedule for a match
437        for (int i = 0; i < getSize(); i++) {
438            ScheduleItem si = track.getNextScheduleItem();
439            if (debugFlag) {
440                log.debug("Item id: ({}) requesting type ({}) load ({}) final dest ({}, {})", si.getId(),
441                        si.getTypeName(), si.getReceiveLoadName(), si.getDestinationName(),
442                        si.getDestinationTrackName()); // NOI18N
443            }
444            String status = checkScheduleItem(si, car, track);
445            if (status.equals(Track.OKAY)) {
446                log.debug("Found item match ({}) car ({}) type ({}) load ({}) ship ({}) destination ({}, {})",
447                        si.getId(), car.toString(), car.getTypeName(), si.getReceiveLoadName(), si.getShipLoadName(),
448                        si.getDestinationName(), si.getDestinationTrackName()); // NOI18N
449                car.setScheduleItemId(si.getId()); // remember which item was a
450                                                   // match
451                return Track.OKAY;
452            } else {
453                if (debugFlag) {
454                    log.debug("Item id: ({}) status ({})", si.getId(), status);
455                }
456            }
457        }
458        if (debugFlag) {
459            log.debug("No Match");
460        }
461        car.setScheduleItemId(Car.NONE); // clear the car's schedule id
462        return Track.SCHEDULE + " " + Bundle.getMessage("matchMessage", getName());
463    }
464
465    public String checkScheduleItem(ScheduleItem si, Car car, Track track) {
466        // if car is already assigned to this schedule item allow it to be
467        // dropped off
468        // on the wrong day (car arrived late)
469        if (!car.getScheduleItemId().equals(si.getId()) &&
470                !si.getSetoutTrainScheduleId().equals(ScheduleItem.NONE) &&
471                !InstanceManager.getDefault(TrainScheduleManager.class).getTrainScheduleActiveId()
472                        .equals(si.getSetoutTrainScheduleId())) {
473            TrainSchedule trainSch = InstanceManager.getDefault(TrainScheduleManager.class)
474                    .getScheduleById(si.getSetoutTrainScheduleId());
475            if (trainSch != null) {
476                return Track.SCHEDULE +
477                        " (" +
478                        getName() +
479                        ") " +
480                        Bundle.getMessage("requestCarOnly") +
481                        " (" +
482                        trainSch.getName() +
483                        ")";
484            }
485        }
486        // Check for correct car type, road, load
487        if (!car.getTypeName().equals(si.getTypeName())) {
488            return Track.SCHEDULE +
489                    " (" +
490                    getName() +
491                    ") " +
492                    Bundle.getMessage("requestCar") +
493                    " " +
494                    Track.TYPE +
495                    " (" +
496                    si.getTypeName() +
497                    ")";
498        }
499        if (!si.getRoadName().equals(ScheduleItem.NONE) && !car.getRoadName().equals(si.getRoadName())) {
500            return Track.SCHEDULE +
501                    " (" +
502                    getName() +
503                    ") " +
504                    Bundle.getMessage("requestCar") +
505                    " " +
506                    Track.TYPE +
507                    " (" +
508                    si.getTypeName() +
509                    ") " +
510                    Track.ROAD +
511                    " (" +
512                    si.getRoadName() +
513                    ")";
514        }
515        if (!si.getReceiveLoadName().equals(ScheduleItem.NONE) && !car.getLoadName().equals(si.getReceiveLoadName())) {
516            return Track.SCHEDULE +
517                    " (" +
518                    getName() +
519                    ") " +
520                    Bundle.getMessage("requestCar") +
521                    " " +
522                    Track.TYPE +
523                    " (" +
524                    si.getTypeName() +
525                    ") " +
526                    Track.LOAD +
527                    " (" +
528                    si.getReceiveLoadName() +
529                    ")";
530        }
531        // don't try the random feature if car is already assigned to this
532        // schedule item
533        if (car.getFinalDestinationTrack() != track &&
534                !si.getRandom().equals(ScheduleItem.NONE) &&
535                !car.getScheduleItemId().equals(si.getId())) {
536            if (!si.doRandom()) {
537                return Bundle.getMessage("scheduleRandom", Track.SCHEDULE, getName(), si.getId(), si.getRandom(), si.getCalculatedRandom());
538            }
539        }
540        return Track.OKAY;
541    }
542
543    public boolean checkScheduleAttribute(String attribute, String carType, Car car) {
544        List<ScheduleItem> scheduleItems = getItemsBySequenceList();
545        for (ScheduleItem si : scheduleItems) {
546            if (si.getTypeName().equals(carType)) {
547                // check to see if schedule services car type
548                if (attribute.equals(Track.TYPE)) {
549                    return true;
550                }
551                // check to see if schedule services car type and load
552                if (attribute.equals(Track.LOAD) &&
553                        (si.getReceiveLoadName().equals(ScheduleItem.NONE) ||
554                                car == null ||
555                                si.getReceiveLoadName().equals(car.getLoadName()))) {
556                    return true;
557                }
558                // check to see if schedule services car type and road
559                if (attribute.equals(Track.ROAD) &&
560                        (si.getRoadName().equals(ScheduleItem.NONE) ||
561                                car == null ||
562                                si.getRoadName().equals(car.getRoadName()))) {
563                    return true;
564                }
565                // check to see if train schedule allows delivery
566                if (attribute.equals(Track.TRAIN_SCHEDULE) &&
567                        (si.getSetoutTrainScheduleId().isEmpty() ||
568                                InstanceManager.getDefault(TrainScheduleManager.class).getTrainScheduleActiveId()
569                                        .equals(si.getSetoutTrainScheduleId()))) {
570                    return true;
571                }
572                // check to see if at least one schedule item can service car
573                if (attribute.equals(Track.ALL) &&
574                        (si.getReceiveLoadName().equals(ScheduleItem.NONE) ||
575                                car == null ||
576                                si.getReceiveLoadName().equals(car.getLoadName())) &&
577                        (si.getRoadName().equals(ScheduleItem.NONE) ||
578                                car == null ||
579                                si.getRoadName().equals(car.getRoadName())) &&
580                        (si.getSetoutTrainScheduleId().equals(ScheduleItem.NONE) ||
581                                InstanceManager.getDefault(TrainScheduleManager.class).getTrainScheduleActiveId()
582                                        .equals(si.getSetoutTrainScheduleId()))) {
583                    return true;
584                }
585            }
586        }
587        return false;
588    }
589
590    /**
591     * Construct this Entry from XML. This member has to remain synchronized
592     * with the detailed DTD in operations-config.xml
593     *
594     * @param e Consist XML element
595     */
596    public Schedule(Element e) {
597        org.jdom2.Attribute a;
598        if ((a = e.getAttribute(Xml.ID)) != null) {
599            _id = a.getValue();
600        } else {
601            log.warn("no id attribute in schedule element when reading operations");
602        }
603        if ((a = e.getAttribute(Xml.NAME)) != null) {
604            _name = a.getValue();
605        }
606        if ((a = e.getAttribute(Xml.COMMENT)) != null) {
607            _comment = a.getValue();
608        }
609        if (e.getChildren(Xml.ITEM) != null) {
610            List<Element> eScheduleItems = e.getChildren(Xml.ITEM);
611            log.debug("schedule: {} has {} items", getName(), eScheduleItems.size());
612            for (Element eScheduleItem : eScheduleItems) {
613                register(new ScheduleItem(eScheduleItem));
614            }
615        }
616    }
617
618    /**
619     * Create an XML element to represent this Entry. This member has to remain
620     * synchronized with the detailed DTD in operations-config.xml.
621     *
622     * @return Contents in a JDOM Element
623     */
624    public org.jdom2.Element store() {
625        Element e = new org.jdom2.Element(Xml.SCHEDULE);
626        e.setAttribute(Xml.ID, getId());
627        e.setAttribute(Xml.NAME, getName());
628        e.setAttribute(Xml.COMMENT, getComment());
629        for (ScheduleItem si : getItemsBySequenceList()) {
630            e.addContent(si.store());
631        }
632
633        return e;
634    }
635
636    @Override
637    public void propertyChange(java.beans.PropertyChangeEvent e) {
638        if (Control.SHOW_PROPERTY) {
639            log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(), e
640                    .getNewValue());
641        }
642        // forward all schedule item changes
643        setDirtyAndFirePropertyChange(e.getPropertyName(), e.getOldValue(), e.getNewValue());
644    }
645
646    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
647        // set dirty
648        InstanceManager.getDefault(LocationManagerXml.class).setDirty(true);
649        firePropertyChange(p, old, n);
650    }
651
652    private final static Logger log = LoggerFactory.getLogger(Schedule.class);
653
654}