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}