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        } else if (!si.getPickupTrainScheduleId().equals(ScheduleItem.NONE) &&
350                InstanceManager.getDefault(TrainScheduleManager.class)
351                        .getScheduleById(si.getPickupTrainScheduleId()) == null) {
352            status = Bundle.getMessage("NotValid", si.getPickupTrainScheduleId());
353        } else if (!track.getLocation().acceptsTypeName(si.getTypeName())) {
354            status = Bundle.getMessage("NotValid", si.getTypeName());
355        } else if (!track.isTypeNameAccepted(si.getTypeName())) {
356            status = Bundle.getMessage("NotValid", si.getTypeName());
357        }
358        // check roads, accepted by track, valid road, and there's at least
359        // one car with that road
360        else if (!si.getRoadName().equals(ScheduleItem.NONE) &&
361                (!track.isRoadNameAccepted(si.getRoadName()) ||
362                        !InstanceManager.getDefault(CarRoads.class).containsName(si.getRoadName()) ||
363                        InstanceManager.getDefault(CarManager.class).getByTypeAndRoad(si.getTypeName(),
364                                si.getRoadName()) == null)) {
365            status = Bundle.getMessage("NotValid", si.getRoadName());
366        }
367        // check loads
368        else if (!si.getReceiveLoadName().equals(ScheduleItem.NONE) &&
369                (!track.isLoadNameAndCarTypeAccepted(si.getReceiveLoadName(), si.getTypeName()) ||
370                        !InstanceManager.getDefault(CarLoads.class).getNames(si.getTypeName())
371                                .contains(si.getReceiveLoadName()))) {
372            status = Bundle.getMessage("NotValid", si.getReceiveLoadName());
373        } else if (!si.getShipLoadName().equals(ScheduleItem.NONE) &&
374                !InstanceManager.getDefault(CarLoads.class).getNames(si.getTypeName()).contains(si.getShipLoadName())) {
375            status = Bundle.getMessage("NotValid", si.getShipLoadName());
376        }
377        // check destination
378        else if (si.getDestination() != null &&
379                (!si.getDestination().acceptsTypeName(si.getTypeName()) ||
380                        InstanceManager.getDefault(LocationManager.class)
381                                .getLocationById(si.getDestination().getId()) == null)) {
382            status = Bundle.getMessage("NotValid", si.getDestination());
383        }
384        // check destination track
385        else if (si.getDestination() != null && si.getDestinationTrack() != null) {
386            if (!si.getDestination().isTrackAtLocation(si.getDestinationTrack())) {
387                status = Bundle.getMessage("NotValid",
388                        si.getDestinationTrack() + " (" + Bundle.getMessage("Track") + ")");
389
390            } else if (!si.getDestinationTrack().isTypeNameAccepted(si.getTypeName())) {
391                status = Bundle.getMessage("NotValid",
392                        si.getDestinationTrack() + " (" + Bundle.getMessage("Type") + ")");
393
394            } else if (!si.getRoadName().equals(ScheduleItem.NONE) &&
395                    !si.getDestinationTrack().isRoadNameAccepted(si.getRoadName())) {
396                status = Bundle.getMessage("NotValid",
397                        si.getDestinationTrack() + " (" + Bundle.getMessage("Road") + ")");
398            } else if (!si.getShipLoadName().equals(ScheduleItem.NONE) &&
399                    !si.getDestinationTrack().isLoadNameAndCarTypeAccepted(si.getShipLoadName(),
400                            si.getTypeName())) {
401                status = Bundle.getMessage("NotValid",
402                        si.getDestinationTrack() + " (" + Bundle.getMessage("Load") + ")");
403            }
404        }
405        return status;
406    }
407
408    private static boolean debugFlag = false;
409
410    /*
411     * Match mode search
412     */
413    public String searchSchedule(Car car, Track track) {
414        if (debugFlag) {
415            log.debug("Search match for car ({}) type ({}) load ({})", car.toString(), car.getTypeName(),
416                    car.getLoadName());
417        }
418        // has the car already been assigned a schedule item? Then verify that
419        // its still okay
420        if (!car.getScheduleItemId().equals(Track.NONE)) {
421            ScheduleItem si = getItemById(car.getScheduleItemId());
422            if (si != null) {
423                String status = checkScheduleItem(si, car, track, false);
424                if (status.equals(Track.OKAY)) {
425                    track.setScheduleItemId(si.getId());
426                    return Track.OKAY;
427                }
428                log.debug("Car ({}) with schedule id ({}) failed check, status: {}", car.toString(),
429                        car.getScheduleItemId(), status);
430            }
431        }
432        // first check to see if the schedule services car type
433        if (!checkScheduleAttribute(Track.TYPE, car.getTypeName(), car)) {
434            return Bundle.getMessage("scheduleNotType", Track.SCHEDULE, getName(), car.getTypeName());
435        }
436
437        // search schedule for a match
438        for (int i = 0; i < getSize(); i++) {
439            ScheduleItem si = track.getNextScheduleItem();
440            if (debugFlag) {
441                log.debug("Item id: ({}) requesting type ({}) load ({}) final dest ({}, {})", si.getId(),
442                        si.getTypeName(), si.getReceiveLoadName(), si.getDestinationName(),
443                        si.getDestinationTrackName()); // NOI18N
444            }
445            String status = checkScheduleItem(si, car, track, true);
446            if (status.equals(Track.OKAY)) {
447                log.debug("Found item match ({}) car ({}) type ({}) load ({}) ship ({}) destination ({}, {})",
448                        si.getId(), car.toString(), car.getTypeName(), si.getReceiveLoadName(), si.getShipLoadName(),
449                        si.getDestinationName(), si.getDestinationTrackName()); // NOI18N
450                // remember which item was a match
451                car.setScheduleItemId(si.getId());
452                return Track.OKAY;
453            } else {
454                if (debugFlag) {
455                    log.debug("Item id: ({}) status ({})", si.getId(), status);
456                }
457            }
458        }
459        if (debugFlag) {
460            log.debug("No Match");
461        }
462        car.setScheduleItemId(Car.NONE); // clear the car's schedule id
463        return Bundle.getMessage("matchMessage", Track.SCHEDULE, getName(),
464                hasRandomItem() ? Bundle.getMessage("Random") : "");
465    }
466
467    public String checkScheduleItem(ScheduleItem si, Car car, Track track, boolean isRandomChecked) {
468        // if car is already assigned to this schedule item allow it to be
469        // dropped off on the wrong day (car arrived late)
470        if (!car.getScheduleItemId().equals(si.getId()) &&
471                !si.getSetoutTrainScheduleId().equals(ScheduleItem.NONE) &&
472                !InstanceManager.getDefault(TrainScheduleManager.class).getTrainScheduleActiveId()
473                        .equals(si.getSetoutTrainScheduleId())) {
474            TrainSchedule trainSch = InstanceManager.getDefault(TrainScheduleManager.class)
475                    .getScheduleById(si.getSetoutTrainScheduleId());
476            if (trainSch != null) {
477                return Bundle.getMessage("requestCarOnly", Track.SCHEDULE, getName(), Track.TYPE, si.getTypeName(),
478                        trainSch.getName());
479            }
480        }
481        // Check for correct car type
482        if (!car.getTypeName().equals(si.getTypeName())) {
483            return Bundle.getMessage("requestCarType", Track.SCHEDULE, getName(), Track.TYPE, si.getTypeName());
484        }
485        // Check for correct car road
486        if (!si.getRoadName().equals(ScheduleItem.NONE) && !car.getRoadName().equals(si.getRoadName())) {
487            return Bundle.getMessage("requestCar", Track.SCHEDULE, getName(), Track.TYPE, si.getTypeName(), Track.ROAD,
488                    si.getRoadName());
489        }
490        // Check for correct car load
491        if (!si.getReceiveLoadName().equals(ScheduleItem.NONE) && !car.getLoadName().equals(si.getReceiveLoadName())) {
492            return Bundle.getMessage("requestCar", Track.SCHEDULE, getName(), Track.TYPE, si.getTypeName(), Track.LOAD,
493                    si.getReceiveLoadName());
494        }
495        // don't try the random feature if car is already assigned to this
496        // schedule item
497        if (isRandomChecked &&
498                car.getFinalDestinationTrack() != track &&
499                !si.getRandom().equals(ScheduleItem.NONE) &&
500                !car.getScheduleItemId().equals(si.getId()) &&
501                !si.doRandom()) {
502            return Bundle.getMessage("scheduleRandom", Track.SCHEDULE, getName(), si.getId(), si.getRandom(),
503                    si.getCalculatedRandom());
504        }
505        return Track.OKAY;
506    }
507
508    public boolean checkScheduleAttribute(String attribute, String carType, Car car) {
509        List<ScheduleItem> scheduleItems = getItemsBySequenceList();
510        for (ScheduleItem si : scheduleItems) {
511            if (si.getTypeName().equals(carType)) {
512                // check to see if schedule services car type
513                if (attribute.equals(Track.TYPE)) {
514                    return true;
515                }
516                // check to see if schedule services car type and load
517                if (attribute.equals(Track.LOAD) &&
518                        (si.getReceiveLoadName().equals(ScheduleItem.NONE) ||
519                                car == null ||
520                                si.getReceiveLoadName().equals(car.getLoadName()))) {
521                    return true;
522                }
523                // check to see if schedule services car type and road
524                if (attribute.equals(Track.ROAD) &&
525                        (si.getRoadName().equals(ScheduleItem.NONE) ||
526                                car == null ||
527                                si.getRoadName().equals(car.getRoadName()))) {
528                    return true;
529                }
530                // check to see if train schedule allows delivery
531                if (attribute.equals(Track.TRAIN_SCHEDULE) &&
532                        (si.getSetoutTrainScheduleId().isEmpty() ||
533                                InstanceManager.getDefault(TrainScheduleManager.class).getTrainScheduleActiveId()
534                                        .equals(si.getSetoutTrainScheduleId()))) {
535                    return true;
536                }
537                // check to see if at least one schedule item can service car
538                if (attribute.equals(Track.ALL) &&
539                        (si.getReceiveLoadName().equals(ScheduleItem.NONE) ||
540                                car == null ||
541                                si.getReceiveLoadName().equals(car.getLoadName())) &&
542                        (si.getRoadName().equals(ScheduleItem.NONE) ||
543                                car == null ||
544                                si.getRoadName().equals(car.getRoadName())) &&
545                        (si.getSetoutTrainScheduleId().equals(ScheduleItem.NONE) ||
546                                InstanceManager.getDefault(TrainScheduleManager.class).getTrainScheduleActiveId()
547                                        .equals(si.getSetoutTrainScheduleId()))) {
548                    return true;
549                }
550            }
551        }
552        return false;
553    }
554
555    /**
556     * Construct this Entry from XML. This member has to remain synchronized
557     * with the detailed DTD in operations-config.xml
558     *
559     * @param e Consist XML element
560     */
561    public Schedule(Element e) {
562        org.jdom2.Attribute a;
563        if ((a = e.getAttribute(Xml.ID)) != null) {
564            _id = a.getValue();
565        } else {
566            log.warn("no id attribute in schedule element when reading operations");
567        }
568        if ((a = e.getAttribute(Xml.NAME)) != null) {
569            _name = a.getValue();
570        }
571        if ((a = e.getAttribute(Xml.COMMENT)) != null) {
572            _comment = a.getValue();
573        }
574        if (e.getChildren(Xml.ITEM) != null) {
575            List<Element> eScheduleItems = e.getChildren(Xml.ITEM);
576            log.debug("schedule: {} has {} items", getName(), eScheduleItems.size());
577            for (Element eScheduleItem : eScheduleItems) {
578                register(new ScheduleItem(eScheduleItem));
579            }
580        }
581    }
582
583    /**
584     * Create an XML element to represent this Entry. This member has to remain
585     * synchronized with the detailed DTD in operations-config.xml.
586     *
587     * @return Contents in a JDOM Element
588     */
589    public org.jdom2.Element store() {
590        Element e = new org.jdom2.Element(Xml.SCHEDULE);
591        e.setAttribute(Xml.ID, getId());
592        e.setAttribute(Xml.NAME, getName());
593        e.setAttribute(Xml.COMMENT, getComment());
594        for (ScheduleItem si : getItemsBySequenceList()) {
595            e.addContent(si.store());
596        }
597
598        return e;
599    }
600
601    @Override
602    public void propertyChange(java.beans.PropertyChangeEvent e) {
603        if (Control.SHOW_PROPERTY) {
604            log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(), e
605                    .getNewValue());
606        }
607        // forward all schedule item changes
608        setDirtyAndFirePropertyChange(e.getPropertyName(), e.getOldValue(), e.getNewValue());
609    }
610
611    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
612        // set dirty
613        InstanceManager.getDefault(LocationManagerXml.class).setDirty(true);
614        firePropertyChange(p, old, n);
615    }
616
617    private final static Logger log = LoggerFactory.getLogger(Schedule.class);
618
619}