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