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