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 public boolean equals(Object obj) { 334 if (obj == this) { 335 return true; 336 } 337 if (obj == null) { 338 return false; 339 } 340 341 if (!(getClass() == obj.getClass())) { 342 return false; 343 } else { 344 Path p = (Path) obj; 345 346 if (!Float.valueOf(p._length).equals(this._length)) { 347 return false; 348 } 349 350 if (p._toBlockDirection != this._toBlockDirection) { 351 return false; 352 } 353 if (p._fromBlockDirection != this._fromBlockDirection) { 354 return false; 355 } 356 357 if (p._block == null && this._block != null) { 358 return false; 359 } 360 if (p._block != null && this._block == null) { 361 return false; 362 } 363 if (p._block != null && this._block != null && !p._block.equals(this._block)) { 364 return false; 365 } 366 367 if (p._beans.size() != this._beans.size()) { 368 return false; 369 } 370 for (int i = 0; i < p._beans.size(); i++) { 371 if (!p._beans.get(i).equals(this._beans.get(i))) { 372 return false; 373 } 374 } 375 } 376 return this.hashCode() == obj.hashCode(); 377 } 378 379 @Override 380 public String toString() { 381 StringBuilder result = new StringBuilder(); 382 String separator = ""; // no separator on first item // NOI18N 383 for (BeanSetting beanSetting : this.getSettings()) { 384 result.append(separator).append(MessageFormat.format("{0} with state {1}", beanSetting.getBean().getDisplayName(), beanSetting.getBean().describeState(beanSetting.getSetting()))); // NOI18N 385 separator = ", "; // NOI18N 386 } 387 if (getBlock() != null) 388 return MessageFormat.format("Path: \"{0}\" ({1}): {2}", getBlock().getDisplayName(), decodeDirection(getToBlockDirection()), result); // NOI18N 389 else 390 return MessageFormat.format("Path: <no block>: {0}", result); // NOI18N 391 } 392 393 @Override 394 public int compareTo(Path obj) { 395 if (obj == this) { 396 return 0; 397 } 398 if (obj == null) { 399 throw new NullPointerException("null argument to compareTo"); 400 } 401 402 if (!(getClass() == obj.getClass())) { 403 throw new IllegalArgumentException("argument of improper type"); 404 } else { 405 406 int retval; 407 408 if (obj.getBlock() != null && getBlock() != null) { 409 retval = getBlock().compareTo(obj.getBlock()); 410 if (retval != 0) return retval; 411 } 412 413 if ( (int)this._length - (int)obj._length != 0.) return (int)this._length - (int)obj._length; 414 415 if (this._toBlockDirection != obj._toBlockDirection) 416 return this._toBlockDirection - obj._toBlockDirection; 417 418 if (this._fromBlockDirection != obj._fromBlockDirection) 419 return this._fromBlockDirection - obj._fromBlockDirection; 420 421 422 if (this._beans.size() != obj._beans.size()) { 423 return this._beans.size() - obj._beans.size(); 424 } 425 426 for (int i = 0; i < obj._beans.size(); i++) { 427 BeanSetting bs1 = this._beans.get(i); 428 BeanSetting bs2 = obj._beans.get(i); 429 retval = bs1.getBean().compareTo(bs2.getBean()); 430 if (retval != 0) return retval; 431 432 if ( bs1.getSetting() != bs2.getSetting() ) { 433 return bs1.getSetting() - bs2.getSetting(); 434 } 435 } 436 } 437 return this.hashCode()- obj.hashCode(); // this is truly an act of desparation 438 } 439 440 // Can't include _toBlockDirection, _fromBlockDirection, or block information as they can change 441 @Override 442 public int hashCode() { 443 int hash = 7; 444 hash = 89 * hash + Objects.hashCode(this._beans); 445 hash = 89 * hash + Objects.hashCode(this._block); 446 hash = 89 * hash + this._toBlockDirection; 447 hash = 89 * hash + this._fromBlockDirection; 448 hash = 89 * hash + Float.floatToIntBits(this._length); 449 return hash; 450 } 451 452 private final ArrayList<BeanSetting> _beans = new ArrayList<>(); 453 private Block _block; 454 private int _toBlockDirection; 455 private int _fromBlockDirection; 456 private float _length = 0.0f; // always stored in millimeters 457 458 /** 459 * Compute octagonal direction of vector from p1 to p2. 460 * <p> 461 * Note: the octagonal (8) directions are: North, North-East, East, 462 * South-East, South, South-West, West and North-West 463 * 464 * @param p1 the first point 465 * @param p2 the second point 466 * @return the octagonal direction from p1 to p2 467 */ 468 public static int computeDirection(Point2D p1, Point2D p2) { 469 log.trace("Path.computeDirection({}, {})", p1, p2); 470 471 double angleDEG = MathUtil.computeAngleDEG(p2, p1); 472 angleDEG = MathUtil.wrap360(angleDEG); // don't want to deal with negative numbers here... 473 474 // convert the angleDEG into an octant index (ccw from south) 475 // note: because we use round here, the octants are offset by half (+/-22.5 deg) 476 // so SOUTH isn't from 0-45 deg; it's from -22.5 deg to +22.5 deg; etc. for other octants. 477 // (and this is what we want!) 478 int octant = (int) Math.round(angleDEG / 45.0); 479 480 // use the octant index to lookup its direction 481 final int dirs[] = {SOUTH, SOUTH_EAST, EAST, NORTH_EAST, 482 NORTH, NORTH_WEST, WEST, SOUTH_WEST, SOUTH}; 483 484 if (log.isTraceEnabled()) log.trace(" returns {} ({})", dirs[octant], decodeDirection(dirs[octant])); 485 486 return dirs[octant]; 487 } // computeOctagonalDirection 488 489 /** 490 * Get the reverse octagonal direction. 491 * 492 * @param inDir the direction 493 * @return the reverse direction or {@value #NONE} if inDir is not a 494 * direction 495 */ 496 public static int reverseDirection(int inDir) { 497 switch (inDir) { 498 case NORTH: 499 return SOUTH; 500 case NORTH_EAST: 501 return SOUTH_WEST; 502 case EAST: 503 return WEST; 504 case SOUTH_EAST: 505 return NORTH_WEST; 506 case SOUTH: 507 return NORTH; 508 case SOUTH_WEST: 509 return NORTH_EAST; 510 case WEST: 511 return EAST; 512 case NORTH_WEST: 513 return SOUTH_EAST; 514 default: 515 return NONE; 516 } 517 } 518 519 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Path.class); 520}