001package jmri.managers; 002 003import java.util.ArrayList; 004import java.util.List; 005import java.util.Objects; 006import java.util.Set; 007 008import javax.annotation.CheckForNull; 009import javax.annotation.Nonnull; 010 011import jmri.Block; 012import jmri.BlockManager; 013import jmri.EntryPoint; 014import jmri.InstanceManager; 015import jmri.JmriException; 016import jmri.NamedBean; 017import jmri.Manager; 018import jmri.Path; 019import jmri.Section; 020import jmri.Sensor; 021import jmri.SensorManager; 022import jmri.SignalHead; 023import jmri.SignalHeadManager; 024 025import jmri.implementation.DefaultSection; 026 027import jmri.jmrit.display.EditorManager; 028import jmri.jmrit.display.layoutEditor.LayoutEditor; 029import jmri.jmrit.display.layoutEditor.LayoutBlockManager; 030import jmri.jmrit.display.layoutEditor.LayoutBlock; 031 032/** 033 * Basic Implementation of a SectionManager. 034 * <p> 035 * This doesn't have a "new" interface, since Sections are independently 036 * implemented, instead of being system-specific. 037 * <p> 038 * Note that Section system names must begin with system prefix and type character, 039 * usually IY, and be followed by a string, usually, but not always, a number. This 040 * is enforced when a Section is created. 041 * <br> 042 * <hr> 043 * This file is part of JMRI. 044 * <p> 045 * JMRI is free software; you can redistribute it and/or modify it under the 046 * terms of version 2 of the GNU General Public License as published by the Free 047 * Software Foundation. See the "COPYING" file for a copy of this license. 048 * <p> 049 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY 050 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 051 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 052 * 053 * @author Dave Duchamp Copyright (C) 2008 054 */ 055public class DefaultSectionManager extends AbstractManager<Section> implements jmri.SectionManager { 056 057 public DefaultSectionManager() { 058 super(); 059 addListeners(); 060 } 061 062 final void addListeners() { 063 InstanceManager.getDefault(SensorManager.class).addVetoableChangeListener(this); 064 InstanceManager.getDefault(BlockManager.class).addVetoableChangeListener(this); 065 } 066 067 @Override 068 public int getXMLOrder() { 069 return Manager.SECTIONS; 070 } 071 072 @Override 073 public char typeLetter() { 074 return 'Y'; 075 } 076 077 @Override 078 public Class<Section> getNamedBeanClass() { 079 return Section.class; 080 } 081 082 /** 083 * Create a new Section if the Section does not exist. 084 * 085 * @param systemName the desired system name 086 * @param userName the desired user name 087 * @return a new Section or 088 * @throws IllegalArgumentException if a Section with the same systemName or 089 * userName already exists, or if there is trouble creating a new 090 * Section. 091 */ 092 @Override 093 @Nonnull 094 public Section createNewSection(@Nonnull String systemName, String userName) throws IllegalArgumentException { 095 Objects.requireNonNull(systemName, "SystemName cannot be null. UserName was " + ((userName == null) ? "null" : userName)); // NOI18N 096 // check system name 097 if (systemName.isEmpty()) { 098 throw new IllegalArgumentException("Section System Name Empty"); 099 // no valid system name entered, return without creating 100 } 101 String sysName = systemName; 102 if (!sysName.startsWith(getSystemNamePrefix())) { 103 sysName = makeSystemName(sysName); 104 } 105 // Check that Section does not already exist 106 Section y; 107 if (userName != null && !userName.isEmpty()) { 108 y = getByUserName(userName); 109 if (y != null) { 110 throw new IllegalArgumentException("Section Already Exists with UserName " + userName); 111 } 112 } 113 y = getBySystemName(sysName); 114 if (y != null) { 115 throw new IllegalArgumentException("Section Already Exists with SystemName " + sysName); 116 } 117 // Section does not exist, create a new Section 118 y = new DefaultSection(sysName, userName); 119 // save in the maps 120 register(y); 121 122 // Keep track of the last created auto system name 123 updateAutoNumber(systemName); 124 125 return y; 126 } 127 128 /** 129 * Create a New Section with Auto System Name. 130 * @param userName UserName for new Section 131 * @return new Section with Auto System Name. 132 * @throws IllegalArgumentException if existing Section, or 133 * unable to create a new Section. 134 */ 135 @Override 136 @Nonnull 137 public Section createNewSection(String userName) throws IllegalArgumentException { 138 return createNewSection(getAutoSystemName(), userName); 139 } 140 141 /** 142 * Remove an existing Section. 143 * 144 * @param y the section to remove 145 */ 146 @Override 147 public void deleteSection(Section y) { 148 // delete the Section 149 deregister(y); 150 y.dispose(); 151 } 152 153 /** 154 * Get an existing Section. First look up assuming that name is a User 155 * Name. If this fails look up assuming that name is a System Name. 156 * 157 * @param name the name to find; user names are searched for a match first, 158 * followed by system names 159 * @return the found section of null if no matching Section found 160 */ 161 @Override 162 @CheckForNull 163 public Section getSection(String name) { 164 Section y = getByUserName(name); 165 if (y != null) { 166 return y; 167 } 168 return getBySystemName(name); 169 } 170 171 /** 172 * Validate all Sections. 173 * 174 * @return number or validation errors; -2 is returned if there are no sections 175 */ 176 @Override 177 public int validateAllSections() { 178 Set<Section> set = getNamedBeanSet(); 179 int numSections = 0; 180 int numErrors = 0; 181 if (set.size() <= 0) { 182 return -2; 183 } 184 for (Section section : set) { 185 String s = section.validate(); 186 if (!s.isEmpty()) { 187 log.error("Validate result for section {}: {}", section.getDisplayName(), s); 188 numErrors++; 189 } 190 numSections++; 191 } 192 log.debug("Validated {} Sections - {} errors or warnings.", numSections, numErrors); 193 return numErrors; 194 } 195 196 /** 197 * Check direction sensors in SSL for signals. 198 * 199 * @return the number or errors; 0 if no errors; -1 if the panel is null; -2 if there are no sections 200 */ 201 @Override 202 public int setupDirectionSensors() { 203 Set<Section> set = getNamedBeanSet(); 204 int numSections = 0; 205 int numErrors = 0; 206 if (set.size() <= 0) { 207 return -2; 208 } 209 for (Section section : set) { 210 int errors = section.placeDirectionSensors(); 211 numErrors += errors; 212 numSections++; 213 } 214 log.debug("Checked direction sensors for {} Sections - {} errors or warnings.", numSections, numErrors); 215 return numErrors; 216 } 217 218 /** 219 * Remove direction sensors from SSL for all signals. 220 * 221 * @return the number or errors; 0 if no errors; -1 if the panel is null; -2 if there are no sections 222 */ 223 @Override 224 public int removeDirectionSensorsFromSSL() { 225 Set<Section> set = getNamedBeanSet(); 226 if (set.size() <= 0) { 227 return -2; 228 } 229 int numErrors = 0; 230 List<String> sensorList = new ArrayList<>(); 231 for (Section s : set) { 232 String name = s.getReverseBlockingSensorName(); 233 if ((name != null) && (!name.isEmpty())) { 234 sensorList.add(name); 235 } 236 name = s.getForwardBlockingSensorName(); 237 if ((name != null) && (!name.isEmpty())) { 238 sensorList.add(name); 239 } 240 } 241 242 var editorManager = InstanceManager.getDefault(EditorManager.class); 243 var shManager = InstanceManager.getDefault(SignalHeadManager.class); 244 245 for (var panel : editorManager.getAll(LayoutEditor.class)) { 246 var cUtil = panel.getConnectivityUtil(); 247 for (SignalHead sh : shManager.getNamedBeanSet()) { 248 if (!cUtil.removeSensorsFromSignalHeadLogic(sensorList, sh)) { 249 numErrors++; 250 } 251 } 252 } 253 return numErrors; 254 } 255 256 /** 257 * Initialize all blocking sensors that exist - set them to 'ACTIVE'. 258 */ 259 @Override 260 public void initializeBlockingSensors() { 261 Sensor sensor; 262 for (Section s : getNamedBeanSet()) { 263 try { 264 sensor = s.getForwardBlockingSensor(); 265 if (sensor != null) { 266 sensor.setState(Sensor.ACTIVE); 267 } 268 sensor = s.getReverseBlockingSensor(); 269 if (sensor != null) { 270 sensor.setState(Sensor.ACTIVE); 271 } 272 } catch (JmriException reason) { 273 log.error("Exception when initializing blocking Sensors for Section {}", s.getDisplayName(NamedBean.DisplayOptions.USERNAME_SYSTEMNAME)); 274 } 275 } 276 } 277 278 /** 279 * Generate Block Sections in stubs/sidings. Called after generating SML based sections. 280 */ 281 282 /** 283 * A list of blocks that will be used to create a block based section. 284 */ 285 private List<Block> blockList; 286 287 /** 288 * Find stub end blocks. 289 */ 290 @Override 291 public void generateBlockSections() { 292 //find blocks with no paths through i.e. stub (siding) 293 LayoutBlockManager layoutBlockManager = InstanceManager.getDefault(LayoutBlockManager.class); 294 295 for (LayoutBlock layoutBlock : layoutBlockManager.getNamedBeanSet()){ 296 if (layoutBlock.getNumberOfThroughPaths() == 0){ 297 if (!blockSectionExists(layoutBlock)){ 298 createBlockSection(layoutBlock); 299 } 300 } 301 } 302 } 303 304 /** 305 * Check if a block based section has a first block that matches. 306 * @param layoutBlock 307 * @return true or false 308 */ 309 private boolean blockSectionExists(LayoutBlock layoutBlock){ 310 for (Section section : getNamedBeanSet()){ 311 if (section.getNumBlocks() > 0 && section.getSectionType() != Section.SIGNALMASTLOGIC) { 312 if (layoutBlock.getBlock().equals(section.getBlockList().get(0))) { 313 return true; 314 } 315 } 316 } 317 return false; 318 } 319 320 /** 321 * Create a block section that has one or more blocks. The initial block is one that has 322 * no through paths, which will normally be track segments that end at an end bumper (EB). 323 * Incomplete track arrangements can also mimmic this behavior. 324 * <p> 325 * The first phase calls a recursive method to build a list of blocks. 326 * The second phase creates the section with an entry point from the next section. 327 * @param layoutBlock The starting layout block. 328 */ 329 private void createBlockSection(LayoutBlock layoutBlock){ 330 blockList = new ArrayList<>(); 331 var block = layoutBlock.getBlock(); 332 createSectionBlockList(block); 333 334 if (blockList.isEmpty()) { 335 log.error("No blocks found for layout block '{}'", layoutBlock.getDisplayName()); 336 return; 337 } 338 339 // Create a new section using the block name(s) as the section name. 340 var sectionName = blockList.get(0).getDisplayName(); 341 if (blockList.size() > 1) { 342 sectionName = sectionName + ":::" + blockList.get(blockList.size() - 1).getDisplayName(); 343 } 344 345 Section section; 346 try { 347 section = createNewSection(sectionName); 348 } 349 catch (IllegalArgumentException ex){ 350 log.error("Could not create Section for layout block '{}'",layoutBlock.getDisplayName()); 351 return; 352 } 353 354 blockList.forEach( blk -> section.addBlock(blk)); 355 356 // Create entry point 357 Block lastBlock = blockList.get(blockList.size() - 1); 358 Block nextBlock = null; 359 String pathDirection = ""; 360 361 for (Path path : lastBlock.getPaths()) { 362 var checkBlock = path.getBlock(); 363 if (!blockList.contains(checkBlock)) { 364 nextBlock = checkBlock; 365 pathDirection = Path.decodeDirection(path.getFromBlockDirection()); 366 break; 367 } 368 } 369 370 if (nextBlock == null) { 371 log.error("Unable to find a next block after block '{}'", lastBlock.getDisplayName()); 372 return; 373 } 374 log.debug("last = {}, next = {}", lastBlock.getDisplayName(), nextBlock.getDisplayName()); 375 376 EntryPoint ep = new EntryPoint(lastBlock, nextBlock, pathDirection); 377 ep.setTypeReverse(); 378 section.addToReverseList(ep); 379 } 380 381 /** 382 * Recursive calls to find a block that is a facing block for SML, a block that has more than 383 * 2 neighbors, or the recursion limit of 100 is reached 384 * @param block The current block being processed. 385 */ 386 private void createSectionBlockList(@Nonnull Block block) { 387 blockList.add(block); 388 if (blockList.size() < 100) { 389 var nextBlock = getNextBlock(block); 390 if (nextBlock != null) { 391 createSectionBlockList(nextBlock); 392 } 393 } 394 } 395 396 /** 397 * Get the next block if this one is not the last block. 398 * The last block is one that is a SML facing block. 399 * The other restriction is only 1 or 2 neighbors. 400 * @param block The block to be checked. 401 * @return the next block or null if it is the last block. 402 */ 403 @CheckForNull 404 private Block getNextBlock(@Nonnull Block block) { 405 var lbmManager = InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class); 406 var smlManager = InstanceManager.getDefault(jmri.SignalMastLogicManager.class); 407 var layoutBlock = lbmManager.getLayoutBlock(block); 408 409 if (layoutBlock == null) { 410 return null; 411 } 412 413 // If the current block is a SML facing block, the next block is not needed. 414 for (jmri.SignalMastLogic sml : smlManager.getSignalMastLogicList()) { 415 if (sml.getFacingBlock().equals(layoutBlock)) { 416 return null; 417 } 418 } 419 420 Block nextBlock = null; 421 switch (layoutBlock.getNumberOfNeighbours()) { 422 case 0: 423 log.debug("No neighbors for layout block '{}'", layoutBlock.getDisplayName()); 424 break; 425 426 case 1: 427 nextBlock = layoutBlock.getNeighbourAtIndex(0); 428 break; 429 430 case 2: 431 nextBlock = layoutBlock.getNeighbourAtIndex(0); 432 if (blockList.contains(nextBlock)) { 433 nextBlock = layoutBlock.getNeighbourAtIndex(1); 434 } 435 break; 436 437 default: 438 log.debug("More than 2 neighbors for layout block '{}'", layoutBlock.getDisplayName()); 439 nextBlock = getNextConnectedBlock(layoutBlock); 440 } 441 return nextBlock; 442 } 443 444 /** 445 * Attempt to find the next block when there are multiple connections. Track segments have 446 * two connections but blocks with turnouts can have any number of connections. 447 * <p> 448 * The checkValidDest method in getLayoutBlockConnectivityTools is used to find the first valid 449 * connection between the current block, its facing block and the possible destination blocks. 450 * @param currentBlock The layout block with more than 2 connections. 451 * @return the next block or null. 452 */ 453 @CheckForNull 454 private Block getNextConnectedBlock(LayoutBlock currentBlock) { 455 var lbmManager = InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class); 456 var lbTools = lbmManager.getLayoutBlockConnectivityTools(); 457 var pathMethod = jmri.jmrit.display.layoutEditor.LayoutBlockConnectivityTools.Routing.NONE; 458 459 // The facing block is the one before the current block or the first block. 460 var index = blockList.size() - 2; 461 if (index < 0) { 462 index = 0; 463 } 464 var facingBlock = lbmManager.getLayoutBlock(blockList.get(index)); 465 if (facingBlock == null) { 466 log.error("The facing block not found for current block '{}'", currentBlock.getDisplayName()); 467 return null; 468 } 469 470 for (int i = 0; i < currentBlock.getNumberOfNeighbours(); i++) { 471 var dest = currentBlock.getNeighbourAtIndex(i); 472 var destBlock = lbmManager.getLayoutBlock(dest); 473 try { 474 boolean valid = lbTools.checkValidDest(facingBlock, 475 currentBlock, destBlock, new ArrayList<>(), pathMethod); 476 if (valid) { 477 return dest; 478 } 479 } catch (JmriException ex) { 480 log.error("getNextConnectedBlock exeption: {}", ex.getMessage()); 481 } 482 } 483 484 return null; 485 } 486 487 @Override 488 @Nonnull 489 public String getBeanTypeHandled(boolean plural) { 490 return Bundle.getMessage(plural ? "BeanNameSections" : "BeanNameSection"); 491 } 492 493 @Override 494 public void dispose(){ 495 InstanceManager.getDefault(SensorManager.class).removeVetoableChangeListener(this); 496 InstanceManager.getDefault(BlockManager.class).removeVetoableChangeListener(this); 497 super.dispose(); 498 } 499 500 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultSectionManager.class); 501 502}