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}