001package jmri;
002
003import java.awt.geom.Point2D;
004import java.text.MessageFormat;
005import java.util.ArrayList;
006import java.util.List;
007import java.util.Objects;
008import jmri.util.MathUtil;
009
010/**
011 * Represents a particular set of NamedBean (usually turnout) settings to put a
012 * path through trackwork to a Block.
013 * <p>
014 * Directions are defined for traffic along this path "to" the block, and "from"
015 * the block. Being more specific:
016 * <ul>
017 *   <li>The "to" direction is the direction that a train is going when it
018 *   traverses this path "to" the final block.
019 *   <li>The "from" direction is the direction that a train is going when it
020 *   traverses this path "from" the final block.
021 * </ul>
022 * Although useful constants are defined, you don't have to restrict to those,
023 * and there's no assumption that they have to be opposites; NORTH for "to" does
024 * not imply SOUTH for "from". This allows you to e.g. handle a piece of curved
025 * track where you can be going LEFT at one point and UP at another. The
026 * constants are defined as bits, so you can use more than one at a time, for
027 * example a direction can simultanously be EAST and RIGHT if desired. What that
028 * means needs to be defined by whatever object is using this Path.
029 * <p>
030 * This implementation handles paths with a list of bean settings. This has been
031 * extended from the initial implementation.
032 * <p>
033 * The length of the path may also optionally be entered if desired. This
034 * attribute is for use in automatic running of trains. Length should be the
035 * actual length of model railroad track in the path. It is always stored here
036 * in millimeter units. A length of 0.0 indicates no entry of length by the
037 * user. If there is no entry the length of the block the path is in will be
038 * returned. An Entry is only needed when there are paths of greatly different
039 * lengths in the block.
040 *
041 * @author Bob Jacobsen Copyright (C) 2006, 2008
042 */
043public class Path implements Comparable<Path> {
044
045    /**
046     * Create an object with default directions of NONE, and no setting element.
047     */
048    public Path() {
049    }
050
051    /**
052     * Convenience constructor to set the destination/source block and
053     * directions in one call.
054     *
055     * @param dest               the destination
056     * @param toBlockDirection   direction to next block
057     * @param fromBlockDirection direction from prior block
058     */
059    public Path(Block dest, int toBlockDirection, int fromBlockDirection) {
060        this();
061        _toBlockDirection = toBlockDirection;
062        _fromBlockDirection = fromBlockDirection;
063        Path.this.setBlock(dest);
064    }
065
066    /**
067     * Convenience constructor to set the destination/source block, directions
068     * and a single setting element in one call.
069     *
070     * @param dest               the destination
071     * @param toBlockDirection   direction to next block
072     * @param fromBlockDirection direction from prior block
073     * @param setting            the setting to add
074     */
075    public Path(Block dest, int toBlockDirection, int fromBlockDirection, BeanSetting setting) {
076        this(dest, toBlockDirection, fromBlockDirection);
077        Path.this.addSetting(setting);
078    }
079
080    public void addSetting(BeanSetting t) {
081        _beans.add(t);
082    }
083
084    public List<BeanSetting> getSettings() {
085        return _beans;
086    }
087
088    public void removeSetting(BeanSetting t) {
089        _beans.remove(t);
090    }
091
092    public void clearSettings() {
093        for (int i = _beans.size(); i > 0; i--) {
094            _beans.remove(i - 1);
095        }
096    }
097
098    public void setBlock(Block b) {
099        _block = b;
100    }
101
102    public Block getBlock() {
103        return _block;
104    }
105
106    public int getToBlockDirection() {
107        return _toBlockDirection;
108    }
109
110    public void setToBlockDirection(int d) {
111        _toBlockDirection = d;
112    }
113
114    public int getFromBlockDirection() {
115        return _fromBlockDirection;
116    }
117
118    public void setFromBlockDirection(int d) {
119        _fromBlockDirection = d;
120    }
121
122    /**
123     * Check that the Path can be traversed. This means that any path elements
124     * are set to the proper state, e.g. that the Turnouts on this path are set
125     * to the proper CLOSED or OPEN status.
126     *
127     * @return true if the path can be traversed; always true if no path
128     *         elements (BeanSettings) are defined.
129     */
130    public boolean checkPathSet() {
131        // empty conditions are always set
132        if (_beans.isEmpty()) {
133            return true;
134        }
135        // check the status of all BeanSettings
136        for (BeanSetting bean : _beans) {
137            if (!bean.check()) {
138                return false;
139            }
140        }
141        return true;
142    }
143
144    /**
145     * Direction not known or not specified. May also represent "stopped", in
146     * the sense of not moving in any direction.
147     */
148    public static final int NONE = 0x00000;
149    /**
150     * Northward
151     */
152    public static final int NORTH = 0x00010;
153    /**
154     * Southward
155     */
156    public static final int SOUTH = 0x00020;
157    /**
158     * Eastward
159     */
160    public static final int EAST = 0x00040;
161    /**
162     * Westward
163     */
164    public static final int WEST = 0x00080;
165
166    /**
167     * North-East
168     */
169    public static final int NORTH_EAST = NORTH | EAST;
170    /**
171     * South-East
172     */
173    public static final int SOUTH_EAST = SOUTH | EAST;
174    /**
175     * South-West
176     */
177    public static final int SOUTH_WEST = SOUTH | WEST;
178    /**
179     * North-West
180     */
181    public static final int NORTH_WEST = NORTH | WEST;
182
183    /**
184     * Clockwise
185     */
186    public static final int CW = 0x00100;
187    /**
188     * Counter-clockwise
189     */
190    public static final int CCW = 0x00200;
191    /**
192     * Leftward, e.g. on a schematic diagram or CTC panel
193     */
194    public static final int LEFT = 0x00400;
195    /**
196     * Rightward, e.g. on a schematic diagram or CTC panel
197     */
198    public static final int RIGHT = 0x00800;
199    /**
200     * Upward, e.g. on a schematic diagram or CTC panel
201     */
202    public static final int UP = 0x01000;
203    /**
204     * Downward, e.g. on a schematic diagram or CTC panel
205     */
206    public static final int DOWN = 0x02000;
207
208    /**
209     * Decode the direction constants into a human-readable form.
210     *
211     * @param d the direction
212     * @return the direction description
213     */
214    static public String decodeDirection(int d) {
215        if (d == NONE) {
216            return Bundle.getMessage("None"); // UI strings i18n using NamedBeanBundle.properties
217        }
218        StringBuffer b = new StringBuffer();
219        if (((d & NORTH) != 0) && ((d & EAST) != 0) ) {
220            appendOne(b, Bundle.getMessage("NorthEast"));
221        }
222        else if (((d & NORTH) != 0) && ((d & WEST) != 0) ) {
223            appendOne(b, Bundle.getMessage("NorthWest"));
224        }
225        else if (((d & SOUTH) != 0) && ((d & EAST) != 0) ) {
226            appendOne(b, Bundle.getMessage("SouthEast"));
227        }
228        else if (((d & SOUTH) != 0) && ((d & WEST) != 0) ) {
229            appendOne(b, Bundle.getMessage("SouthWest"));
230        }
231        else {
232            if ((d & NORTH) != 0) {
233                appendOne(b, Bundle.getMessage("North"));
234            }
235            if ((d & SOUTH) != 0) {
236                appendOne(b, Bundle.getMessage("South"));
237            }
238            if ((d & EAST) != 0) {
239                appendOne(b, Bundle.getMessage("East"));
240            }
241            if ((d & WEST) != 0) {
242                appendOne(b, Bundle.getMessage("West"));
243            }
244        }
245        if ((d & CW) != 0) {
246            appendOne(b, Bundle.getMessage("Clockwise"));
247        }
248        if ((d & CCW) != 0) {
249            appendOne(b, Bundle.getMessage("CounterClockwise"));
250        }
251        if ((d & LEFT) != 0) {
252            appendOne(b, Bundle.getMessage("Leftward"));
253        }
254        if ((d & RIGHT) != 0) {
255            appendOne(b, Bundle.getMessage("Rightward"));
256        }
257        if ((d & UP) != 0) {
258            appendOne(b, Bundle.getMessage("ButtonUp")); // reuse "Up" in NBB
259        }
260        if ((d & DOWN) != 0) {
261            appendOne(b, Bundle.getMessage("ButtonDown")); // reuse "Down" in NBB
262        }
263        final int mask = NORTH | SOUTH | EAST | WEST | CW | CCW | LEFT | RIGHT | UP | DOWN;
264        if ((d & ~mask) != 0) {
265            appendOne(b, "Unknown: 0x" + Integer.toHexString(d & ~mask));
266        }
267        return b.toString();
268    }
269
270    /**
271     * Set path length.
272     * Length may override the block length default.
273     *
274     * @param l length in millimeters
275     */
276    public void setLength(float l) {
277        _length = l;
278    }
279
280    /**
281     * Get actual stored length.
282     *
283     * @return length in millimeters or 0
284     */
285    public float getLength() {
286        return _length;
287    }
288
289    /**
290     * Get length in millimeters.
291     *
292     * @return the stored length if greater than 0 or the block length
293     */
294    public float getLengthMm() {
295        if (_length <= 0.0f) {
296            return _block.getLengthMm();
297        }
298        return _length;
299    }
300
301    /**
302     * Get length in centimeters.
303     *
304     * @return the stored length if greater than 0 or the block length
305     */
306    public float getLengthCm() {
307        if (_length <= 0.0f) {
308            return _block.getLengthCm();
309        }
310        return (_length / 10.0f);
311    }
312
313    /**
314     * Get length in inches.
315     *
316     * @return the stored length if greater than 0 or the block length
317     */
318    public float getLengthIn() {
319        if (_length <= 0.0f) {
320            return _block.getLengthIn();
321        }
322        return (_length / 25.4f);
323    }
324
325    static private void appendOne(StringBuffer b, String t) {
326        if (b.length() != 0) {
327            b.append(", ");
328        }
329        b.append(t);
330    }
331
332    @Override
333    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "FE_FLOATING_POINT_EQUALITY", justification = "equals operator should actually check for equality")
334    public boolean equals(Object obj) {
335        if (obj == this) {
336            return true;
337        }
338        if (obj == null) {
339            return false;
340        }
341
342        if (!(getClass() == obj.getClass())) {
343            return false;
344        } else {
345            Path p = (Path) obj;
346
347            if (!Float.valueOf(p._length).equals(this._length)) {
348                return false;
349            }
350
351            if (p._toBlockDirection != this._toBlockDirection) {
352                return false;
353            }
354            if (p._fromBlockDirection != this._fromBlockDirection) {
355                return false;
356            }
357
358            if (p._block == null && this._block != null) {
359                return false;
360            }
361            if (p._block != null && this._block == null) {
362                return false;
363            }
364            if (p._block != null && this._block != null && !p._block.equals(this._block)) {
365                return false;
366            }
367
368            if (p._beans.size() != this._beans.size()) {
369                return false;
370            }
371            for (int i = 0; i < p._beans.size(); i++) {
372                if (!p._beans.get(i).equals(this._beans.get(i))) {
373                    return false;
374                }
375            }
376        }
377        return this.hashCode() == obj.hashCode();
378    }
379
380    @Override
381    public String toString() {
382        StringBuilder result = new StringBuilder();
383        String separator = ""; // no separator on first item // NOI18N
384        for (BeanSetting beanSetting : this.getSettings()) {
385            result.append(separator).append(MessageFormat.format("{0} with state {1}", beanSetting.getBean().getDisplayName(), beanSetting.getBean().describeState(beanSetting.getSetting()))); // NOI18N
386            separator = ", "; // NOI18N
387        }
388        if (getBlock() != null)
389            return MessageFormat.format("Path: \"{0}\" ({1}): {2}", getBlock().getDisplayName(), decodeDirection(getToBlockDirection()), result); // NOI18N
390        else
391            return MessageFormat.format("Path: <no block>: {0}", result); // NOI18N
392    }
393
394    @Override
395    public int compareTo(Path obj) {
396        if (obj == this) {
397            return 0;
398        }
399        if (obj == null) {
400            throw new NullPointerException("null argument to compareTo");
401        }
402
403        if (!(getClass() == obj.getClass())) {
404            throw new IllegalArgumentException("argument of improper type");
405        } else {
406
407            int retval;
408            
409            if (obj.getBlock() != null && getBlock() != null) {
410                retval = getBlock().compareTo(obj.getBlock());
411                if (retval != 0) return retval;
412            }
413            
414            if ( (int)this._length - (int)obj._length != 0.) return (int)this._length - (int)obj._length;
415
416            if (this._toBlockDirection != obj._toBlockDirection) 
417                return this._toBlockDirection - obj._toBlockDirection;
418
419            if (this._fromBlockDirection != obj._fromBlockDirection) 
420                return this._fromBlockDirection - obj._fromBlockDirection;
421
422            
423            if (this._beans.size() != obj._beans.size()) {
424                return this._beans.size() - obj._beans.size();
425            }
426            
427            for (int i = 0; i < obj._beans.size(); i++) {
428                BeanSetting bs1 = this._beans.get(i);
429                BeanSetting bs2 = obj._beans.get(i);
430                retval = bs1.getBean().compareTo(bs2.getBean());
431                if (retval != 0) return retval;
432                
433                if ( bs1.getSetting() != bs2.getSetting() ) {
434                    return bs1.getSetting() - bs2.getSetting();
435                }
436            }
437        }
438        return this.hashCode()- obj.hashCode();  // this is truly an act of desparation
439    }
440    
441    // Can't include _toBlockDirection, _fromBlockDirection, or block information as they can change
442    @Override
443    public int hashCode() {
444        int hash = 7;
445        hash = 89 * hash + Objects.hashCode(this._beans);
446        hash = 89 * hash + Objects.hashCode(this._block);
447        hash = 89 * hash + this._toBlockDirection;
448        hash = 89 * hash + this._fromBlockDirection;
449        hash = 89 * hash + Float.floatToIntBits(this._length);
450        return hash;
451    }
452
453    private final ArrayList<BeanSetting> _beans = new ArrayList<>();
454    private Block _block;
455    private int _toBlockDirection;
456    private int _fromBlockDirection;
457    private float _length = 0.0f;  // always stored in millimeters
458
459    /**
460     * Compute octagonal direction of vector from p1 to p2.
461     * <p>
462     * Note: the octagonal (8) directions are: North, North-East, East,
463     * South-East, South, South-West, West and North-West
464     *
465     * @param p1 the first point
466     * @param p2 the second point
467     * @return the octagonal direction from p1 to p2
468     */
469    public static int computeDirection(Point2D p1, Point2D p2) {
470        log.trace("Path.computeDirection({}, {})", p1, p2);
471        
472        double angleDEG = MathUtil.computeAngleDEG(p2, p1);
473        angleDEG = MathUtil.wrap360(angleDEG);  // don't want to deal with negative numbers here...
474
475        // convert the angleDEG into an octant index (ccw from south)
476        // note: because we use round here, the octants are offset by half (+/-22.5 deg)
477        // so SOUTH isn't from 0-45 deg; it's from -22.5 deg to +22.5 deg; etc. for other octants.
478        // (and this is what we want!)
479        int octant = (int) Math.round(angleDEG / 45.0);
480
481        // use the octant index to lookup its direction
482        final int dirs[] = {SOUTH, SOUTH_EAST, EAST, NORTH_EAST,
483            NORTH, NORTH_WEST, WEST, SOUTH_WEST, SOUTH};
484            
485        if (log.isTraceEnabled()) log.trace("   returns {} ({})", dirs[octant], decodeDirection(dirs[octant]));
486        
487        return dirs[octant];
488    }   // computeOctagonalDirection
489
490    /**
491     * Get the reverse octagonal direction.
492     *
493     * @param inDir the direction
494     * @return the reverse direction or {@value #NONE} if inDir is not a
495     *         direction
496     */
497    public static int reverseDirection(int inDir) {
498        switch (inDir) {
499            case NORTH:
500                return SOUTH;
501            case NORTH_EAST:
502                return SOUTH_WEST;
503            case EAST:
504                return WEST;
505            case SOUTH_EAST:
506                return NORTH_WEST;
507            case SOUTH:
508                return NORTH;
509            case SOUTH_WEST:
510                return NORTH_EAST;
511            case WEST:
512                return EAST;
513            case NORTH_WEST:
514                return SOUTH_EAST;
515            default:
516                return NONE;
517        }
518    }
519
520    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Path.class);
521}