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