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 Track.SCHEDULE + " " + Bundle.getMessage("scheduleNotType", 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 Track.SCHEDULE +
471                " " +
472                Bundle.getMessage("matchMessage", getName(), hasRandomItem() ? Bundle.getMessage("Random") : "");
473    }
474
475    public String checkScheduleItem(ScheduleItem si, Car car, Track track) {
476        // if car is already assigned to this schedule item allow it to be
477        // dropped off on the wrong day (car arrived late)
478        if (!car.getScheduleItemId().equals(si.getId()) &&
479                !si.getSetoutTrainScheduleId().equals(ScheduleItem.NONE) &&
480                !InstanceManager.getDefault(TrainScheduleManager.class).getTrainScheduleActiveId()
481                        .equals(si.getSetoutTrainScheduleId())) {
482            TrainSchedule trainSch = InstanceManager.getDefault(TrainScheduleManager.class)
483                    .getScheduleById(si.getSetoutTrainScheduleId());
484            if (trainSch != null) {
485                return Track.SCHEDULE +
486                        " (" +
487                        getName() +
488                        ") " +
489                        Bundle.getMessage("requestCarOnly") +
490                        " (" +
491                        trainSch.getName() +
492                        ")";
493            }
494        }
495        // Check for correct car type, road, load
496        if (!car.getTypeName().equals(si.getTypeName())) {
497            return Track.SCHEDULE +
498                    " (" +
499                    getName() +
500                    ") " +
501                    Bundle.getMessage("requestCar") +
502                    " " +
503                    Track.TYPE +
504                    " (" +
505                    si.getTypeName() +
506                    ")";
507        }
508        if (!si.getRoadName().equals(ScheduleItem.NONE) && !car.getRoadName().equals(si.getRoadName())) {
509            return Track.SCHEDULE +
510                    " (" +
511                    getName() +
512                    ") " +
513                    Bundle.getMessage("requestCar") +
514                    " " +
515                    Track.TYPE +
516                    " (" +
517                    si.getTypeName() +
518                    ") " +
519                    Track.ROAD +
520                    " (" +
521                    si.getRoadName() +
522                    ")";
523        }
524        if (!si.getReceiveLoadName().equals(ScheduleItem.NONE) && !car.getLoadName().equals(si.getReceiveLoadName())) {
525            return Track.SCHEDULE +
526                    " (" +
527                    getName() +
528                    ") " +
529                    Bundle.getMessage("requestCar") +
530                    " " +
531                    Track.TYPE +
532                    " (" +
533                    si.getTypeName() +
534                    ") " +
535                    Track.LOAD +
536                    " (" +
537                    si.getReceiveLoadName() +
538                    ")";
539        }
540        // don't try the random feature if car is already assigned to this
541        // schedule item
542        if (car.getFinalDestinationTrack() != track &&
543                !si.getRandom().equals(ScheduleItem.NONE) &&
544                !car.getScheduleItemId().equals(si.getId())) {
545            if (!si.doRandom()) {
546                return Bundle.getMessage("scheduleRandom", Track.SCHEDULE, getName(), si.getId(), si.getRandom(), si.getCalculatedRandom());
547            }
548        }
549        return Track.OKAY;
550    }
551
552    public boolean checkScheduleAttribute(String attribute, String carType, Car car) {
553        List<ScheduleItem> scheduleItems = getItemsBySequenceList();
554        for (ScheduleItem si : scheduleItems) {
555            if (si.getTypeName().equals(carType)) {
556                // check to see if schedule services car type
557                if (attribute.equals(Track.TYPE)) {
558                    return true;
559                }
560                // check to see if schedule services car type and load
561                if (attribute.equals(Track.LOAD) &&
562                        (si.getReceiveLoadName().equals(ScheduleItem.NONE) ||
563                                car == null ||
564                                si.getReceiveLoadName().equals(car.getLoadName()))) {
565                    return true;
566                }
567                // check to see if schedule services car type and road
568                if (attribute.equals(Track.ROAD) &&
569                        (si.getRoadName().equals(ScheduleItem.NONE) ||
570                                car == null ||
571                                si.getRoadName().equals(car.getRoadName()))) {
572                    return true;
573                }
574                // check to see if train schedule allows delivery
575                if (attribute.equals(Track.TRAIN_SCHEDULE) &&
576                        (si.getSetoutTrainScheduleId().isEmpty() ||
577                                InstanceManager.getDefault(TrainScheduleManager.class).getTrainScheduleActiveId()
578                                        .equals(si.getSetoutTrainScheduleId()))) {
579                    return true;
580                }
581                // check to see if at least one schedule item can service car
582                if (attribute.equals(Track.ALL) &&
583                        (si.getReceiveLoadName().equals(ScheduleItem.NONE) ||
584                                car == null ||
585                                si.getReceiveLoadName().equals(car.getLoadName())) &&
586                        (si.getRoadName().equals(ScheduleItem.NONE) ||
587                                car == null ||
588                                si.getRoadName().equals(car.getRoadName())) &&
589                        (si.getSetoutTrainScheduleId().equals(ScheduleItem.NONE) ||
590                                InstanceManager.getDefault(TrainScheduleManager.class).getTrainScheduleActiveId()
591                                        .equals(si.getSetoutTrainScheduleId()))) {
592                    return true;
593                }
594            }
595        }
596        return false;
597    }
598
599    /**
600     * Construct this Entry from XML. This member has to remain synchronized
601     * with the detailed DTD in operations-config.xml
602     *
603     * @param e Consist XML element
604     */
605    public Schedule(Element e) {
606        org.jdom2.Attribute a;
607        if ((a = e.getAttribute(Xml.ID)) != null) {
608            _id = a.getValue();
609        } else {
610            log.warn("no id attribute in schedule element when reading operations");
611        }
612        if ((a = e.getAttribute(Xml.NAME)) != null) {
613            _name = a.getValue();
614        }
615        if ((a = e.getAttribute(Xml.COMMENT)) != null) {
616            _comment = a.getValue();
617        }
618        if (e.getChildren(Xml.ITEM) != null) {
619            List<Element> eScheduleItems = e.getChildren(Xml.ITEM);
620            log.debug("schedule: {} has {} items", getName(), eScheduleItems.size());
621            for (Element eScheduleItem : eScheduleItems) {
622                register(new ScheduleItem(eScheduleItem));
623            }
624        }
625    }
626
627    /**
628     * Create an XML element to represent this Entry. This member has to remain
629     * synchronized with the detailed DTD in operations-config.xml.
630     *
631     * @return Contents in a JDOM Element
632     */
633    public org.jdom2.Element store() {
634        Element e = new org.jdom2.Element(Xml.SCHEDULE);
635        e.setAttribute(Xml.ID, getId());
636        e.setAttribute(Xml.NAME, getName());
637        e.setAttribute(Xml.COMMENT, getComment());
638        for (ScheduleItem si : getItemsBySequenceList()) {
639            e.addContent(si.store());
640        }
641
642        return e;
643    }
644
645    @Override
646    public void propertyChange(java.beans.PropertyChangeEvent e) {
647        if (Control.SHOW_PROPERTY) {
648            log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(), e
649                    .getNewValue());
650        }
651        // forward all schedule item changes
652        setDirtyAndFirePropertyChange(e.getPropertyName(), e.getOldValue(), e.getNewValue());
653    }
654
655    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
656        // set dirty
657        InstanceManager.getDefault(LocationManagerXml.class).setDirty(true);
658        firePropertyChange(p, old, n);
659    }
660
661    private final static Logger log = LoggerFactory.getLogger(Schedule.class);
662
663}