001package jmri.jmrit.operations.routes;
002
003import java.awt.Color;
004import java.awt.Point;
005
006import org.jdom2.Attribute;
007import org.jdom2.Element;
008import org.slf4j.Logger;
009import org.slf4j.LoggerFactory;
010
011import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
012import jmri.InstanceManager;
013import jmri.beans.PropertyChangeSupport;
014import jmri.jmrit.operations.locations.Location;
015import jmri.jmrit.operations.locations.LocationManager;
016import jmri.jmrit.operations.setup.Control;
017import jmri.jmrit.operations.setup.Setup;
018import jmri.jmrit.operations.trains.TrainCommon;
019import jmri.util.ColorUtil;
020
021/**
022 * Represents a location in a route, a location can appear more than once in a
023 * route.
024 *
025 * @author Daniel Boudreau Copyright (C) 2008, 2013
026 */
027public class RouteLocation extends PropertyChangeSupport implements java.beans.PropertyChangeListener {
028
029    public static final String NONE = "";
030
031    protected String _id = NONE;
032    protected Location _location = null; // the location in the route
033    protected String _locationId = NONE; // the location's id
034    protected int _trainDir = (Setup.getTrainDirection() == Setup.EAST + Setup.WEST) ? EAST : NORTH; // train direction
035    protected int _maxTrainLength = Setup.getMaxTrainLength();
036    protected int _maxCarMoves = Setup.getCarMoves();
037    protected String _randomControl = DISABLED;
038    protected boolean _drops = true; // when true set outs allowed at this location
039    protected boolean _pickups = true; // when true pick ups allowed at this location
040    protected boolean _localMoves = true; // when true local moves allowed at this location
041    protected int _sequenceNum = 0; // used to determine location order in a route
042    protected double _grade = 0; // maximum grade between locations
043    protected int _wait = 0; // wait time at this location
044    protected String _departureTime = NONE; // departure time from this location
045    protected int _trainIconX = 0; // the x & y coordinates for the train icon
046    protected int _trainIconY = 0;
047    protected int _blockingOrder = 0;
048    protected String _comment = NONE;
049    protected Color _commentColor = Color.black;
050
051    protected int _carMoves = 0; // number of moves at this location
052    protected int _trainWeight = 0; // total car weight departing this location
053    protected int _trainLength = 0; // train length departing this location
054
055    public static final int EAST = 1; // train direction
056    public static final int WEST = 2;
057    public static final int NORTH = 4;
058    public static final int SOUTH = 8;
059
060    public static final String EAST_DIR = Setup.EAST_DIR; // train directions text
061    public static final String WEST_DIR = Setup.WEST_DIR;
062    public static final String NORTH_DIR = Setup.NORTH_DIR;
063    public static final String SOUTH_DIR = Setup.SOUTH_DIR;
064
065    public static final String DISPOSE = "routeLocationDispose"; // NOI18N
066    public static final String DELETED = Bundle.getMessage("locationDeleted");
067
068    public static final String DROP_CHANGED_PROPERTY = "dropChange"; // NOI18N
069    public static final String PICKUP_CHANGED_PROPERTY = "pickupChange"; // NOI18N
070    public static final String LOCAL_MOVES_CHANGED_PROPERTY = "localMovesChange"; // NOI18N
071    public static final String MAX_MOVES_CHANGED_PROPERTY = "maxMovesChange"; // NOI18N
072    public static final String TRAIN_DIRECTION_CHANGED_PROPERTY = "trainDirectionChange"; // NOI18N
073    public static final String DEPARTURE_TIME_CHANGED_PROPERTY = "routeDepartureTimeChange"; // NOI18N
074    public static final String MAX_LENGTH_CHANGED_PROPERTY = "maxLengthChange"; // NOI18N
075
076    public static final String DISABLED = "Off";
077
078    public RouteLocation(String id, Location location) {
079        log.debug("New route location ({}) id: {}", location.getName(), id);
080        _location = location;
081        _id = id;
082        // listen for name change or delete
083        location.addPropertyChangeListener(this);
084    }
085
086    // for combo boxes
087    @Override
088    public String toString() {
089        return getName();
090    }
091
092    public String getId() {
093        return _id;
094    }
095
096    public String getName() {
097        if (getLocation() != null) {
098            return getLocation().getName();
099        }
100        return DELETED;
101    }
102    
103    public String getSplitName() {
104        if (getLocation() != null) {
105            return getLocation().getSplitName();
106        }
107        return DELETED;
108    }
109
110    private String getNameId() {
111        if (_location != null) {
112            return _location.getId();
113        }
114        return _locationId;
115    }
116
117    public Location getLocation() {
118        return _location;
119    }
120
121    public int getSequenceNumber() {
122        return _sequenceNum;
123    }
124
125    public void setSequenceNumber(int sequence) {
126        // property change not needed
127        _sequenceNum = sequence;
128    }
129
130    public int getBlockingOrder() {
131        return _blockingOrder;
132    }
133
134    public void setBlockingOrder(int order) {
135        _blockingOrder = order;
136    }
137
138    public void setComment(String comment) {
139        String old = _comment;
140        _comment = comment;
141        if (!old.equals(_comment)) {
142            setDirtyAndFirePropertyChange("RouteLocationComment", old, comment); // NOI18N
143        }
144    }
145
146    public String getComment() {
147        return _comment;
148    }
149
150    /**
151     * Sets the text color for the route comment
152     * @param color The color of the text
153     */
154    public void setCommentColor(Color color) {
155        Color old = _commentColor;
156        _commentColor = color;
157        if (!old.equals(_commentColor)) {
158            setDirtyAndFirePropertyChange("RouteLocationCommentColor", old, color); // NOI18N
159        }
160    }
161
162    public Color getCommentColor() {
163        return _commentColor;
164    }
165
166    public String getCommentWithColor() {
167        return TrainCommon.formatColorString(getComment(), getCommentColor());
168    }
169
170    public void setCommentTextColor(String color) {
171        setCommentColor(ColorUtil.stringToColor(color));
172    }
173
174    public String getCommentTextColor() {
175        return ColorUtil.colorToColorName(getCommentColor());
176    }
177
178    public void setTrainDirection(int direction) {
179        int old = _trainDir;
180        _trainDir = direction;
181        if (old != direction) {
182            setDirtyAndFirePropertyChange(TRAIN_DIRECTION_CHANGED_PROPERTY, Integer.toString(old), Integer
183                    .toString(direction));
184        }
185    }
186
187    /**
188     * Gets the binary representation of the train's direction at this location
189     *
190     * @return int representing train direction EAST WEST NORTH SOUTH
191     */
192    public int getTrainDirection() {
193        return _trainDir;
194    }
195
196    /**
197     * Gets the String representation of the train's direction at this location
198     *
199     * @return String representing train direction at this location
200     */
201    public String getTrainDirectionString() {
202        return Setup.getDirectionString(getTrainDirection());
203    }
204
205    public void setMaxTrainLength(int length) {
206        int old = _maxTrainLength;
207        _maxTrainLength = length;
208        if (old != length) {
209            setDirtyAndFirePropertyChange(MAX_LENGTH_CHANGED_PROPERTY, Integer.toString(old), Integer.toString(length)); // NOI18N
210        }
211    }
212
213    public int getMaxTrainLength() {
214        return _maxTrainLength;
215    }
216
217    /**
218     * Set the train length departing this location when building a train
219     * @param length The train's current length.
220     *
221     */
222    public void setTrainLength(int length) {
223        int old = _trainLength;
224        _trainLength = length;
225        if (old != length) {
226            firePropertyChange("trainLength", Integer.toString(old), Integer.toString(length)); // NOI18N
227        }
228    }
229
230    public int getTrainLength() {
231        return _trainLength;
232    }
233
234    /**
235     * Set the train weight departing this location when building a train
236     * @param weight The train's current weight.
237     *
238     */
239    public void setTrainWeight(int weight) {
240        int old = _trainWeight;
241        _trainWeight = weight;
242        if (old != weight) {
243            firePropertyChange("trainWeight", Integer.toString(old), Integer.toString(weight)); // NOI18N
244        }
245    }
246
247    public int getTrainWeight() {
248        return _trainWeight;
249    }
250
251    public void setMaxCarMoves(int moves) {
252        int old = _maxCarMoves;
253        _maxCarMoves = moves;
254        if (old != moves) {
255            setDirtyAndFirePropertyChange(MAX_MOVES_CHANGED_PROPERTY, Integer.toString(old), Integer.toString(moves));
256        }
257    }
258
259    /**
260     * Get the maximum number of moves for this location
261     *
262     * @return maximum number of moves
263     */
264    public int getMaxCarMoves() {
265        return _maxCarMoves;
266    }
267
268    public void setRandomControl(String value) {
269        String old = _randomControl;
270        _randomControl = value;
271        if (!old.equals(value)) {
272            setDirtyAndFirePropertyChange("randomControl", old, value); // NOI18N
273        }
274    }
275
276    public String getRandomControl() {
277        return _randomControl;
278    }
279
280    /**
281     * When true allow car drops at this location
282     *
283     * @param drops when true drops allowed at this location
284     */
285    public void setDropAllowed(boolean drops) {
286        boolean old = _drops;
287        _drops = drops;
288        if (old != drops) {
289            setDirtyAndFirePropertyChange(DROP_CHANGED_PROPERTY, old ? "true" : "false", drops ? "true" : "false"); // NOI18N
290        }
291    }
292
293    public boolean isDropAllowed() {
294        return _drops;
295    }
296
297    /**
298     * When true allow car pick ups at this location
299     *
300     * @param pickups when true pick ups allowed at this location
301     */
302    public void setPickUpAllowed(boolean pickups) {
303        boolean old = _pickups;
304        _pickups = pickups;
305        if (old != pickups) {
306            setDirtyAndFirePropertyChange(PICKUP_CHANGED_PROPERTY, old ? "true" : "false", pickups ? "true" : "false"); // NOI18N
307        }
308    }
309
310    public boolean isPickUpAllowed() {
311        return _pickups;
312    }
313    
314    /**
315     * When true allow local car moves at this location
316     *
317     * @param local when true local moves allowed at this location
318     */
319    public void setLocalMovesAllowed(boolean local) {
320        boolean old = _localMoves;
321        _localMoves = local;
322        if (old != local) {
323            setDirtyAndFirePropertyChange(LOCAL_MOVES_CHANGED_PROPERTY, old ? "true" : "false", local ? "true" : "false"); // NOI18N
324        }
325    }
326
327    public boolean isLocalMovesAllowed() {
328        return _localMoves;
329    }
330
331    /**
332     * Set the number of moves completed when building a train
333     * @param moves An integer representing the amount of moves completed.
334     *
335     */
336    public void setCarMoves(int moves) {
337        int old = _carMoves;
338        _carMoves = moves;
339        if (old != moves) {
340            firePropertyChange("carMoves", Integer.toString(old), Integer.toString(moves)); // NOI18N
341        }
342    }
343
344    public int getCarMoves() {
345        return _carMoves;
346    }
347
348    public void setWait(int time) {
349        int old = _wait;
350        _wait = time;
351        if (old != time) {
352            setDirtyAndFirePropertyChange("waitTime", Integer.toString(old), Integer.toString(time)); // NOI18N
353        }
354    }
355
356    public int getWait() {
357        return _wait;
358    }
359
360    /**
361     * Sets the formated departure time from this location
362     * @param time format hours:minutes
363     */
364    public void setDepartureTime(String time) {
365        String old = _departureTime;
366        _departureTime = time;
367        if (!old.equals(time)) {
368            setDirtyAndFirePropertyChange(DEPARTURE_TIME_CHANGED_PROPERTY, old, time);
369        }
370    }
371
372    public void setDepartureTime(String hour, String minute) {
373        String old = _departureTime;
374        int h = Integer.parseInt(hour);
375        if (h < 10) {
376            hour = "0" + h;
377        }
378        int m = Integer.parseInt(minute);
379        if (m < 10) {
380            minute = "0" + m;
381        }
382        String time = hour + ":" + minute;
383        _departureTime = time;
384        if (!old.equals(time)) {
385            setDirtyAndFirePropertyChange(DEPARTURE_TIME_CHANGED_PROPERTY, old, time);
386        }
387    }
388
389    public String getDepartureTime() {
390        return _departureTime;
391    }
392
393    public String getDepartureTimeHour() {
394        String[] time = getDepartureTime().split(":");
395        return time[0];
396    }
397
398    public String getDepartureTimeMinute() {
399        String[] time = getDepartureTime().split(":");
400        return time[1];
401    }
402
403    public String getFormatedDepartureTime() {
404        if (getDepartureTime().equals(NONE) || !Setup.is12hrFormatEnabled()) {
405            return _departureTime;
406        }
407        String AM_PM = " " + Bundle.getMessage("AM");
408        String[] time = getDepartureTime().split(":");
409        int hour = Integer.parseInt(time[0]);
410        if (hour >= 12) {
411            AM_PM = " " + Bundle.getMessage("PM");
412            hour = hour - 12;
413        }
414        if (hour == 0) {
415            hour = 12;
416        }
417        time[0] = Integer.toString(hour);
418        return time[0] + ":" + time[1] + AM_PM;
419    }
420
421    @SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY", justification = "firing property change doesn't matter")
422    public void setGrade(double grade) {
423        double old = _grade;
424        _grade = grade;
425        if (old != grade) {
426            setDirtyAndFirePropertyChange("grade", Double.toString(old), Double.toString(grade)); // NOI18N
427        }
428    }
429
430    public double getGrade() {
431        return _grade;
432    }
433
434    public void setTrainIconX(int x) {
435        int old = _trainIconX;
436        _trainIconX = x;
437        if (old != x) {
438            setDirtyAndFirePropertyChange("trainIconX", Integer.toString(old), Integer.toString(x)); // NOI18N
439        }
440    }
441
442    public int getTrainIconX() {
443        return _trainIconX;
444    }
445
446    public void setTrainIconY(int y) {
447        int old = _trainIconY;
448        _trainIconY = y;
449        if (old != y) {
450            setDirtyAndFirePropertyChange("trainIconY", Integer.toString(old), Integer.toString(y)); // NOI18N
451        }
452    }
453
454    public int getTrainIconY() {
455        return _trainIconY;
456    }
457
458    /**
459     * Gets the X range for detecting the manual movement of a train icon.
460     * @return the range for detection
461     */
462    public int getTrainIconRangeX() {
463        return getLocation().getTrainIconRangeX();
464    }
465
466    /**
467     * Gets the Y range for detecting the manual movement of a train icon.
468     * @return the range for detection
469     */
470    public int getTrainIconRangeY() {
471        return getLocation().getTrainIconRangeY();
472    }
473
474    /**
475     * Set the train icon panel coordinates to the location defaults.
476     * Coordinates are dependent on the train's departure direction.
477     */
478    public void setTrainIconCoordinates() {
479        Location l = InstanceManager.getDefault(LocationManager.class).getLocationByName(getName());
480        if ((getTrainDirection() & Location.EAST) == Location.EAST) {
481            setTrainIconX(l.getTrainIconEast().x);
482            setTrainIconY(l.getTrainIconEast().y);
483        }
484        if ((getTrainDirection() & Location.WEST) == Location.WEST) {
485            setTrainIconX(l.getTrainIconWest().x);
486            setTrainIconY(l.getTrainIconWest().y);
487        }
488        if ((getTrainDirection() & Location.NORTH) == Location.NORTH) {
489            setTrainIconX(l.getTrainIconNorth().x);
490            setTrainIconY(l.getTrainIconNorth().y);
491        }
492        if ((getTrainDirection() & Location.SOUTH) == Location.SOUTH) {
493            setTrainIconX(l.getTrainIconSouth().x);
494            setTrainIconY(l.getTrainIconSouth().y);
495        }
496    }
497
498    public Point getTrainIconCoordinates() {
499        return new Point(getTrainIconX(), getTrainIconY());
500    }
501
502    public void dispose() {
503        if (_location != null) {
504            _location.removePropertyChangeListener(this);
505        }
506        firePropertyChange(DISPOSE, null, DISPOSE);
507    }
508
509    /**
510     * Construct this Entry from XML. This member has to remain synchronized
511     * with the detailed DTD in operations-config.xml
512     *
513     * @param e Consist XML element
514     */
515    public RouteLocation(Element e) {
516        Attribute a;
517        if ((a = e.getAttribute(Xml.ID)) != null) {
518            _id = a.getValue();
519        } else {
520            log.warn("no id attribute in route location element when reading operations");
521        }
522        if ((a = e.getAttribute(Xml.LOCATION_ID)) != null) {
523            _locationId = a.getValue();
524            _location = InstanceManager.getDefault(LocationManager.class).getLocationById(a.getValue());
525            if (_location != null) {
526                _location.addPropertyChangeListener(this);
527            }
528        } // old way of storing a route location
529        else if ((a = e.getAttribute(Xml.NAME)) != null) {
530            _location = InstanceManager.getDefault(LocationManager.class).getLocationByName(a.getValue());
531            if (_location != null) {
532                _location.addPropertyChangeListener(this);
533            }
534            // force rewrite of route file
535            InstanceManager.getDefault(RouteManagerXml.class).setDirty(true);
536        }
537        if ((a = e.getAttribute(Xml.TRAIN_DIRECTION)) != null) {
538            // early releases had text for train direction
539            if (Setup.getTrainDirectionList().contains(a.getValue())) {
540                _trainDir = Setup.getDirectionInt(a.getValue());
541                log.debug("found old train direction {} new direction {}", a.getValue(), _trainDir);
542            } else {
543                try {
544                    _trainDir = Integer.parseInt(a.getValue());
545                } catch (NumberFormatException ee) {
546                    log.error("Route location ({}) direction ({}) is unknown", getName(), a.getValue());
547                }
548            }
549        }
550        if ((a = e.getAttribute(Xml.MAX_TRAIN_LENGTH)) != null) {
551            try {
552                _maxTrainLength = Integer.parseInt(a.getValue());
553            } catch (NumberFormatException ee) {
554                log.error("Route location ({}) maximum train length ({}) isn't a valid number", getName(), a.getValue());
555            }
556        }
557        if ((a = e.getAttribute(Xml.GRADE)) != null) {
558            try {
559                _grade = Double.parseDouble(a.getValue());
560            } catch (NumberFormatException ee) {
561                log.error("Route location ({}) grade ({}) isn't a valid number", getName(), a.getValue());
562            }
563        }
564        if ((a = e.getAttribute(Xml.MAX_CAR_MOVES)) != null) {
565            try {
566                _maxCarMoves = Integer.parseInt(a.getValue());
567            } catch (NumberFormatException ee) {
568                log.error("Route location ({}) maximum car moves ({}) isn't a valid number", getName(), a.getValue());
569            }
570        }
571        if ((a = e.getAttribute(Xml.RANDOM_CONTROL)) != null) {
572            _randomControl = a.getValue();
573        }
574        if ((a = e.getAttribute(Xml.PICKUPS)) != null) {
575            _pickups = a.getValue().equals(Xml.YES);
576        }
577        if ((a = e.getAttribute(Xml.DROPS)) != null) {
578            _drops = a.getValue().equals(Xml.YES);
579        }
580        if ((a = e.getAttribute(Xml.LOCAL_MOVES)) != null) {
581            _localMoves = a.getValue().equals(Xml.YES);
582        } else {
583            if (!isPickUpAllowed() || !isDropAllowed()) {
584                _localMoves = false; // disable local moves
585            }
586        }
587        if ((a = e.getAttribute(Xml.WAIT)) != null) {
588            try {
589                _wait = Integer.parseInt(a.getValue());
590            } catch (NumberFormatException ee) {
591                log.error("Route location ({}) wait ({}) isn't a valid number", getName(), a.getValue());
592            }
593        }
594        if ((a = e.getAttribute(Xml.DEPART_TIME)) != null) {
595            _departureTime = a.getValue();
596        }
597        if ((a = e.getAttribute(Xml.BLOCKING_ORDER)) != null) {
598            try {
599                _blockingOrder = Integer.parseInt(a.getValue());
600            } catch (NumberFormatException ee) {
601                log.error("Route location ({}) blocking order ({}) isn't a valid number", getName(), a.getValue());
602            }
603        }
604        if ((a = e.getAttribute(Xml.TRAIN_ICON_X)) != null) {
605            try {
606                _trainIconX = Integer.parseInt(a.getValue());
607            } catch (NumberFormatException ee) {
608                log.error("Route location ({}) icon x ({}) isn't a valid number", getName(), a.getValue());
609            }
610        }
611        if ((a = e.getAttribute(Xml.TRAIN_ICON_Y)) != null) {
612            try {
613                _trainIconY = Integer.parseInt(a.getValue());
614            } catch (NumberFormatException ee) {
615                log.error("Route location ({}) icon y ({}) isn't a valid number", getName(), a.getValue());
616            }
617        }
618        if ((a = e.getAttribute(Xml.SEQUENCE_ID)) != null) {
619            try {
620                _sequenceNum = Integer.parseInt(a.getValue());
621            } catch (NumberFormatException ee) {
622                log.error("Route location ({}) sequence id isn't a valid number {}", getName(), a.getValue());
623            }
624        }
625        if ((a = e.getAttribute(Xml.COMMENT_COLOR)) != null) {
626            setCommentTextColor(a.getValue());
627        }
628
629        if ((a = e.getAttribute(Xml.COMMENT)) != null) {
630            _comment = a.getValue();
631        }
632    }
633
634    /**
635     * Create an XML element to represent this Entry. This member has to remain
636     * synchronized with the detailed DTD in operations-config.xml.
637     *
638     * @return Contents in a JDOM Element
639     */
640    public Element store() {
641        Element e = new Element(Xml.LOCATION);
642        e.setAttribute(Xml.ID, getId());
643        e.setAttribute(Xml.NAME, getName());
644        e.setAttribute(Xml.LOCATION_ID, getNameId());
645        e.setAttribute(Xml.SEQUENCE_ID, Integer.toString(getSequenceNumber()));
646        e.setAttribute(Xml.TRAIN_DIRECTION, Integer.toString(getTrainDirection()));
647        e.setAttribute(Xml.MAX_TRAIN_LENGTH, Integer.toString(getMaxTrainLength()));
648        e.setAttribute(Xml.GRADE, Double.toString(getGrade()));
649        e.setAttribute(Xml.MAX_CAR_MOVES, Integer.toString(getMaxCarMoves()));
650        e.setAttribute(Xml.RANDOM_CONTROL, getRandomControl());
651        e.setAttribute(Xml.PICKUPS, isPickUpAllowed() ? Xml.YES : Xml.NO);
652        e.setAttribute(Xml.DROPS, isDropAllowed() ? Xml.YES : Xml.NO);
653        e.setAttribute(Xml.LOCAL_MOVES, isLocalMovesAllowed() ? Xml.YES : Xml.NO);
654        e.setAttribute(Xml.WAIT, Integer.toString(getWait()));
655        e.setAttribute(Xml.DEPART_TIME, getDepartureTime());
656        e.setAttribute(Xml.BLOCKING_ORDER, Integer.toString(getBlockingOrder()));
657        e.setAttribute(Xml.TRAIN_ICON_X, Integer.toString(getTrainIconX()));
658        e.setAttribute(Xml.TRAIN_ICON_Y, Integer.toString(getTrainIconY()));
659        e.setAttribute(Xml.COMMENT_COLOR, getCommentTextColor());
660        e.setAttribute(Xml.COMMENT, getComment());
661
662        return e;
663    }
664
665    @Override
666    public void propertyChange(java.beans.PropertyChangeEvent e) {
667        if (Control.SHOW_PROPERTY) {
668            log.debug("Property change: ({}) old: ({}) new: ({})", e.getPropertyName(), e.getOldValue(), e
669                    .getNewValue());
670        }
671        if (e.getPropertyName().equals(Location.DISPOSE_CHANGED_PROPERTY)) {
672            if (_location != null) {
673                _location.removePropertyChangeListener(this);
674            }
675            _location = null;
676        }
677        // forward property name change
678        if (e.getPropertyName().equals(Location.NAME_CHANGED_PROPERTY)) {
679            firePropertyChange(e.getPropertyName(), e.getOldValue(), e.getNewValue());
680        }
681    }
682
683    protected void setDirtyAndFirePropertyChange(String p, Object old, Object n) {
684        InstanceManager.getDefault(RouteManagerXml.class).setDirty(true);
685        firePropertyChange(p, old, n);
686    }
687
688    private final static Logger log = LoggerFactory.getLogger(RouteLocation.class);
689
690}