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}