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