001package jmri.jmrit.vsdecoder;
002
003import java.io.File;
004import java.util.ArrayList;
005import java.util.List;
006import java.util.Set;
007import java.util.HashMap;
008import java.util.Iterator;
009import jmri.jmrit.XmlFile;
010import jmri.Scale;
011import jmri.Reporter;
012import jmri.Block;
013import jmri.BlockManager;
014import jmri.InstanceManager;
015import jmri.jmrit.display.layoutEditor.*;
016import jmri.jmrit.display.EditorManager;
017import jmri.util.FileUtil;
018import jmri.util.PhysicalLocation;
019import org.jdom2.Element;
020import org.slf4j.Logger;
021import org.slf4j.LoggerFactory;
022
023/**
024 * Load parameter from XML for the Advanced Location Following.
025 *
026 * <hr>
027 * This file is part of JMRI.
028 * <p>
029 * JMRI is free software; you can redistribute it and/or modify it under
030 * the terms of version 2 of the GNU General Public License as published
031 * by the Free Software Foundation. See the "COPYING" file for a copy
032 * of this license.
033 * <p>
034 * JMRI is distributed in the hope that it will be useful, but WITHOUT
035 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
036 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
037 * for more details.
038 *
039 * @author Klaus Killinger Copyright (C) 2018-2022
040 */
041public class VSDGeoFile extends XmlFile {
042
043    static final String VSDGeoDataFileName = "VSDGeoData.xml"; // NOI18N
044    protected Element root;
045    private float blockParameter[][][];
046    private List<List<PhysicalLocation>> blockPositionlists; // Two-dimensional ArrayList
047    private List<PhysicalLocation>[] blockPositionlist;
048    private List<List<Integer>> reporterlists; // Two-dimensional ArrayList
049    private List<Integer>[] reporterlist;
050    private List<Boolean> circlelist;
051    private int setup_index;
052    private int num_issues;
053    boolean geofile_ok;
054    private int num_setups;
055    private Scale _layout_scale;
056    float layout_scale;
057    int check_time; // Time interval in ms for track following updates
058    private ArrayList<LayoutEditor> panels;
059    private ArrayList<LayoutEditor> panelsFinal;
060    HashMap<Block, LayoutEditor> possibleStartBlocks;
061    ArrayList<Block> blockList;
062    private LayoutEditor models;
063    PhysicalLocation models_origin;
064    int lf_version;  // location following
065    int alf_version; // advanced location following
066
067    /**
068     * Looking for additional parameter for train tracking
069     */
070    @SuppressWarnings("unchecked") // ArrayList[n] is not detected as the coded generics
071    public VSDGeoFile() {
072
073        // Setup lists for Reporters and Positions
074        reporterlists = new ArrayList<>();
075        reporterlist = new ArrayList[VSDecoderManager.max_decoder]; // Limit number of supported VSDecoders
076        blockPositionlists = new ArrayList<>();
077        blockPositionlist = new ArrayList[VSDecoderManager.max_decoder];
078        for (int i = 0; i < VSDecoderManager.max_decoder; i++) {
079            reporterlist[i] = new ArrayList<>();
080            blockPositionlist[i] = new ArrayList<>();
081        }
082
083        // Another list to provide a flag for circling or non-circling routes
084        circlelist = new ArrayList<>();
085
086        models = null;
087        geofile_ok = false;
088
089        File file = new File(FileUtil.getUserFilesPath() + VSDGeoDataFileName);
090        if (!file.exists()) {
091            log.debug("File {} for train tracking is not available", VSDGeoDataFileName);
092            lf_version = 1; // assume "location following"
093            return;
094        }
095
096        // Try to load data from the file
097        try {
098            root = rootFromFile(file);
099        } catch (Exception e) {
100            log.error("Exception while loading file {}", VSDGeoDataFileName, e);
101            return;
102        }
103
104        // Get some layout parameters and route geometric data
105        String n;
106        n = root.getChildText("layout-scale");
107        if (n != null) {
108            _layout_scale = jmri.ScaleManager.getScale(n);
109            if (_layout_scale == null) {
110                _layout_scale = jmri.ScaleManager.getScale("N"); // default
111                log.info("File {}: Element layout-scale '{}' unknown, defaulting to N", VSDGeoDataFileName, n);
112            }
113        } else {
114            _layout_scale = jmri.ScaleManager.getScale("N"); // default
115            log.info("File {}: Element layout-scale missing, defaulting to N", VSDGeoDataFileName);
116        }
117        layout_scale = (float) _layout_scale.getScaleRatio(); // Take this for further calculations
118        log.debug("layout-scale: {}, used for further calculations: {}", _layout_scale.toString(), layout_scale);
119
120        n = root.getChildText("check-time");
121        if (n != null) {
122            check_time = Integer.parseInt(n);
123            // Process some limitations; values in milliseconds
124            if (check_time < 500 || check_time > 5000) {
125                check_time = 2000; // default
126                log.info("File {}: Element check-time not in range, defaulting to {} ms", VSDGeoDataFileName, check_time);
127            }
128        } else {
129            check_time = 2000; // default
130            log.info("File {}: Element check-time missing, defaulting to {} ms", VSDGeoDataFileName, check_time);
131        }
132        log.debug("check-time: {} ms", check_time);
133
134        // Now look if the file contains "setup" data or "panel" data
135        n = root.getChildText("setup");
136        if ((n != null) && (!n.isEmpty())) {
137            log.debug("A setup found for ALF version 1");
138            alf_version = 1;
139            readGeoInfos();
140
141        } else {
142
143            // Looking for the "panel" data
144            n = root.getChildText("models");
145            if ((n == null) || (n.isEmpty())) {
146                // cannot continue
147                log.warn("No Panel specified in {}", VSDGeoDataFileName);
148            } else {
149                // An existing (loaded) panel is expected
150                panels = new ArrayList<>(InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class));
151                if (panels.isEmpty()) {
152                    log.warn("No Panel loaded. Please restart PanelPro and load Panel \"{}\" first", n);
153                    return;
154                } else {
155                    // There is at least one panel;
156                    // does it must match with the specified panel?
157                    for (LayoutEditor panel : panels) {
158                        log.debug("checking panel \"{}\" ... looking for \"{}\"", panel.getTitle(), n);
159                        if (n.equals(panel.getTitle())) {
160                            models = panel;
161                            break;
162                        }
163                    }
164                }
165                if (models == null) {
166                    log.error("Loaded Panel \"{}\" does not match with specified Panel \"{}\". Please correct and restart PanelPro", panels, n);
167                } else {
168                    log.debug("selected panel: {}", models.getTitle());
169                    n = root.getChildText("models-origin");
170                    if ((n != null) && (!n.isEmpty())) {
171                        models_origin = PhysicalLocation.parse(n);
172                        log.debug("models-origin: {}", models_origin);
173                    } else {
174                        models_origin = new PhysicalLocation(346f, 260f, 0f); // default
175                    }
176                    alf_version = 2;
177                    log.debug("ALF version: {}", alf_version);
178                    readPanelInfos(); // good to go
179                }
180            }
181        }
182    }
183
184    private void readGeoInfos() {
185        // Detect number of "setup" tags and maximal number of "geodataset" tags
186
187        Element c, c0, c1;
188        String n, np;
189        num_issues = 0;
190
191        num_setups = 0; // # setup
192        int num_geodatasets = 0; // # geodataset
193        int max_geodatasets = 0; // helper
194        Iterator<Element> ix = root.getChildren("setup").iterator(); // NOI18N
195        while (ix.hasNext()) {
196            c = ix.next();
197            num_geodatasets = c.getChildren("geodataset").size();
198            log.debug("setup {} has {} geodataset(s)", num_setups + 1, num_geodatasets);
199            if (num_geodatasets > max_geodatasets) {
200                max_geodatasets = num_geodatasets; // # geodatasets can vary; take highest value
201            }
202            num_setups++;
203        }
204        log.debug("counting setups: {}, maximum geodatasets: {}", num_setups, max_geodatasets);
205        // Limitation check is done by the schema validation, but a XML schema is not yet in place
206        if (num_setups == 0 || num_geodatasets == 0 || num_setups > VSDecoderManager.max_decoder) {
207            log.warn("File {}: Invalid number of setups or geodatasets", VSDGeoDataFileName);
208            geofile_ok = false;
209            return;
210        }
211
212        // Setup array to save the block parameters
213        blockParameter = new float[num_setups][max_geodatasets][5];
214
215        // Go through all setups and their geodatasets
216        //  - get the PhysicalLocation (position) from the parameter file
217        //  - make checks which are not covered by the schema validation
218        //  - make some basic checks for not validated VSDGeoData.xml files (avoid NPEs)
219        setup_index = 0;
220        Iterator<Element> i0 = root.getChildren("setup").iterator(); // NOI18N
221        while (i0.hasNext()) {
222            c0 = i0.next();
223            log.debug("--- SETUP: {}", setup_index + 1);
224
225            boolean is_end_position_set = false; // Need one end-position per setup
226            int j = 0;
227            Iterator<Element> i1 = c0.getChildren("geodataset").iterator(); // NOI18N
228            while (i1.hasNext()) {
229                c1 = i1.next();
230                int rep_int = 0;
231                if (c1.getChildText("reporter-systemname") != null) {
232                    np = c1.getChildText("reporter-systemname");
233                    Reporter rep = jmri.InstanceManager.getDefault(jmri.ReporterManager.class).getBySystemName(np);
234                    if (rep != null) {
235                        try {
236                            rep_int = Integer.parseInt(jmri.Manager.getSystemSuffix(rep.getSystemName()));
237                        } catch (java.lang.NumberFormatException e) {
238                            log.warn("File {}: Reporter System Name '{}' is not valid for VSD", VSDGeoDataFileName, np);
239                            num_issues++;
240                        }
241                        reporterlist[setup_index].add(rep_int);
242                        n = c1.getChildText("position");
243                        // An element "position" is required and a XML schema and a XML schema is not yet in place
244                        if (n != null) {
245                            PhysicalLocation pl = PhysicalLocation.parse(n);
246                            blockPositionlist[setup_index].add(pl);
247                            // Establish relationship Reporter-PhysicalLocation (see window Manage VSD Locations)
248                            PhysicalLocation.setBeanPhysicalLocation(pl, rep);
249                            log.debug("Reporter: {}, position set to: {}", rep, pl);
250                        } else {
251                            log.warn("File {}: Element position not found", VSDGeoDataFileName);
252                            num_issues++;
253                        }
254                    } else {
255                        log.warn("File {}: No Reporter available for system name = {}", VSDGeoDataFileName, np);
256                        num_issues++;
257                    }
258                } else {
259                    log.warn("File {}: Reporter system name missing", VSDGeoDataFileName);
260                    num_issues++;
261                }
262
263                if (num_issues == 0) {
264                    n = c1.getChildText("radius");
265                    if (n != null) {
266                        blockParameter[setup_index][j][0] = Float.parseFloat(n);
267                        log.debug(" radius: {}", n);
268                    } else {
269                        log.warn("File {}: Element radius not found", VSDGeoDataFileName);
270                        num_issues++;
271                    }
272                    n = c1.getChildText("slope");
273                    if (n != null) {
274                        blockParameter[setup_index][j][1] = Float.parseFloat(n);
275                        log.debug(" slope: {}", n);
276                    } else {
277                        // If a radius is not defined (radius = 0), slope must exist!
278                        if (blockParameter[setup_index][j][0] == 0.0f) {
279                            log.warn("File {}: Element slope not found", VSDGeoDataFileName);
280                            num_issues++;
281                        }
282                    }
283                    n = c1.getChildText("rotate-xpos");
284                    if (n != null) {
285                        blockParameter[setup_index][j][2] = Float.parseFloat(n);
286                        log.debug(" rotate-xpos: {}", n);
287                    } else {
288                        // If a radius is defined (radius > 0), rotate-xpos must exist!
289                        if (blockParameter[setup_index][j][0] > 0.0f) {
290                            log.warn("File {}: Element rotate-xpos not found", VSDGeoDataFileName);
291                            num_issues++;
292                        }
293                    }
294                    n = c1.getChildText("rotate-ypos");
295                    if (n != null) {
296                        blockParameter[setup_index][j][3] = Float.parseFloat(n);
297                        log.debug(" rotate-ypos: {}", n);
298                    } else {
299                        // If a radius is defined (radius > 0), rotate-ypos must exist!
300                        if (blockParameter[setup_index][j][0] > 0.0f) {
301                            log.warn("File {}: Element rotate-ypos not found", VSDGeoDataFileName);
302                                num_issues++;
303                            }
304                    }
305                    n = c1.getChildText("length");
306                    if (n != null) {
307                        blockParameter[setup_index][j][4] = Float.parseFloat(n);
308                        log.debug(" length: {}", n);
309                    } else {
310                        log.warn("File {}: Element length not found", VSDGeoDataFileName);
311                        num_issues++;
312                    }
313                    n = c1.getChildText("end-position");
314                    if (n != null) {
315                        if (!is_end_position_set) {
316                            blockPositionlist[setup_index].add(PhysicalLocation.parse(n));
317                            is_end_position_set = true;
318                            log.debug("end-position for location {} set to {}", j,
319                                    blockPositionlist[setup_index].get(blockPositionlist[setup_index].size() - 1));
320                        } else {
321                            log.warn("File {}: Only the last geodataset should have an end-position", VSDGeoDataFileName);
322                            num_issues++;
323                        }
324                    }
325                }
326                j++;
327            }
328
329            if (!is_end_position_set) {
330                log.warn("File {}: End-position missing for setup {}", VSDGeoDataFileName, setup_index + 1);
331                num_issues++;
332            }
333            addLists();
334            setup_index++;
335        }
336        finishRead();
337    }
338
339    // Gather infos about the LayoutEditor panel(s)
340    private void readPanelInfos() {
341        int max_geodatasets = 0;
342        possibleStartBlocks = new HashMap<>();
343        blockList = new ArrayList<>();
344
345        log.debug("Found panel: {}", models);
346
347        // Look for panels with an Edge Connector
348        panels = new ArrayList<>(InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class));
349        panelsFinal = new ArrayList<>();
350        for (LayoutEditor p : panels) {
351            for (LayoutTrack lt : p.getLayoutTracks()) {
352                if (lt instanceof PositionablePoint) {
353                    PositionablePoint pp = (PositionablePoint) lt;
354                    if (pp.getType() == PositionablePoint.PointType.EDGE_CONNECTOR) {
355                        if (!panelsFinal.contains(p)) {
356                            panelsFinal.add(p);
357                        }
358                    }
359                }
360            }
361        }
362        log.debug("edge panels: {}", panelsFinal);
363
364        if (panelsFinal.isEmpty()) {
365            panelsFinal.add(models);
366        }
367        log.debug("final panels: {}", panelsFinal);
368
369        // ALL LAYOUT TRACKS; count turnouts and track segments only
370        int max_ts = 0;
371        for (LayoutEditor p : panelsFinal) {
372            for (LayoutTrack lt : p.getLayoutTracks()) {
373                if (lt instanceof LayoutTurnout) {
374                    max_geodatasets++;
375                } else if (lt instanceof TrackSegment) {
376                    max_geodatasets++;
377                    max_ts++;
378                } else if (lt instanceof LevelXing) {
379                    max_geodatasets++;
380                    max_geodatasets++; // LevelXing contains 2 blocks, AC and BD
381                } else {
382                    log.debug("no LayoutTurnout, no TrackSegment, no PositionablePoint, but: {}", lt);
383                }
384            }
385        }
386        log.debug("number of turnouts and track segments: {}", max_geodatasets);
387
388        // minimal 1 layout track
389        if (max_geodatasets == 0) {
390            log.warn("Panel must have minimum one layout track");
391            return;
392        }
393
394        // minimal 1 track segment
395        if (max_ts == 0) {
396            log.warn("Panel must have minimum one track segment");
397            return;
398        }
399
400        // Find size and setup array to save the block parameters
401        BlockManager bmgr = InstanceManager.getDefault(BlockManager.class);
402        Set<Block> blockSet = bmgr.getNamedBeanSet();
403        if (blockSet.isEmpty()) {
404            log.warn("Panel must have minimum one block");
405            return;
406        }
407
408        LayoutBlockManager lm = InstanceManager.getDefault(LayoutBlockManager.class);
409        LayoutBlock lblk;
410
411        log.debug("panels: {}", panelsFinal);
412
413        // List all blocks and list possible start blocks
414        for (LayoutEditor le : panelsFinal) {
415            log.debug("### panel: {}", le);
416            for (Block bl : blockSet) {
417                if (bl != null) {
418                    String userName2 = bl.getUserName();
419                    if (userName2 != null) {
420                        lblk = lm.getByUserName(userName2);
421                        if (lblk != null) {
422                            log.debug("File {}, block system name: {}, user name: {}", le.getTitle(), bl.getSystemName(), userName2);
423                            int tsInBlock = 0;
424                            // List of all LayoutTracks in the block
425                            ArrayList<LayoutTrack> layoutTracksInBlock = new ArrayList<>();
426                            for (LayoutTrack lt : le.getLayoutTracks()) {
427                                if (lt instanceof LayoutTurnout) {
428                                    LayoutTurnout to = (LayoutTurnout) lt;
429                                    if (to.getLayoutBlock() == lblk) {
430                                        layoutTracksInBlock.add(lt);
431                                        blockList.add(bl);
432                                    }
433                                } else if (lt instanceof TrackSegment) {
434                                    TrackSegment ts = (TrackSegment) lt;
435                                    if (ts.getLayoutBlock() == lblk) {
436                                        layoutTracksInBlock.add(lt);
437                                        blockList.add(bl);
438                                        tsInBlock++;
439                                    }
440                                } else if (lt instanceof LevelXing) {
441                                    LevelXing lx = (LevelXing) lt;
442                                    if (lx.getLayoutBlockAC() == lblk || lx.getLayoutBlockBD() == lblk) {
443                                        layoutTracksInBlock.add(lt); // LevelXing contains 2 blocks, AC and BD; add one more entry here
444                                        blockList.add(bl);
445                                    }
446                                } else if (lt instanceof LayoutTurntable) {
447                                    LayoutTurntable tt = (LayoutTurntable) lt;
448                                    if (tt.getLayoutBlock() == lblk) {
449                                        layoutTracksInBlock.add(lt);
450                                        blockList.add(bl);
451                                    }
452                                }
453                            }
454                            log.debug("layoutTracksInBlock: {}", layoutTracksInBlock);
455                            // A possible start-block is a block with a single TrackSegment
456                            if (tsInBlock == 1 && possibleStartBlocks.get(bl) == null) {
457                                possibleStartBlocks.put(bl, le); // Save a Block together with its LE Panel
458                            }
459                        }
460                    }
461                }
462            }
463        }
464        log.debug("Block list: {}, possible start-blocks: {}", blockList, possibleStartBlocks);
465        geofile_ok = true;
466    }
467
468    private void addLists() {
469        if (num_issues == 0) {
470            // Add lists to their array
471            reporterlists.add(reporterlist[setup_index]);
472            blockPositionlists.add(blockPositionlist[setup_index]);
473
474            // Prove, if the setup has a circling route and add the result to a list
475            //  compare first and last blockPosition without the tunnel attribute
476            //  needed for the Reporter validation check in VSDecoderManager
477            int last_index = blockPositionlist[setup_index].size() - 1;
478            log.debug("first setup position: {}, last setup position: {}", blockPositionlist[setup_index].get(0),
479                    blockPositionlist[setup_index].get(last_index));
480            if (blockPositionlist[setup_index].get(0) != null
481                    && blockPositionlist[setup_index].get(0).x == blockPositionlist[setup_index].get(last_index).x
482                    && blockPositionlist[setup_index].get(0).y == blockPositionlist[setup_index].get(last_index).y
483                    && blockPositionlist[setup_index].get(0).z == blockPositionlist[setup_index].get(last_index).z) {
484                circlelist.add(true);
485            } else {
486                circlelist.add(false);
487            }
488            log.debug("circling: {}", circlelist.get(setup_index));
489        }
490    }
491
492    private void finishRead() {
493        // Some Debug infos
494        if (log.isDebugEnabled()) {
495            log.debug("--- LISTS");
496            log.debug("number of Reporter lists: {}", reporterlists.size());
497            log.debug("Reporter lists with their Reporters (digit only): {}", reporterlists);
498            //log.debug("TEST reporter get 0 list size: {}", reporterlists.get(0).size());
499            //log.debug("TEST reporter [0] list size: {}", reporterlist[0].size());
500            log.debug("number of Position lists: {}", blockPositionlists.size());
501            log.debug("Position lists: {}", blockPositionlists);
502            log.debug("--- COUNTERS");
503            log.debug("number of setups: {}", num_setups);
504            log.debug("number of issues: {}", num_issues);
505        }
506        setGeoFileStatus();
507    }
508
509    private void setGeoFileStatus() {
510        if (num_issues > 0) {
511            geofile_ok = false;
512            log.warn("set geofile to not ok");
513        } else {
514            geofile_ok = true;
515        }
516    }
517
518    // Number of setups
519    public int getNumberOfSetups() {
520        return num_setups;
521    }
522
523    // Reporter lists
524    public List<List<Integer>> getReporterList() {
525        return reporterlists;
526    }
527
528    // Reporter Parameter
529    public float[][][] getBlockParameter() {
530        return blockParameter;
531    }
532
533    // Reporter (Block) Position lists
534    public List<List<PhysicalLocation>> getBlockPosition() {
535        return blockPositionlists;
536    }
537
538    // Circling list
539    public List<Boolean> getCirclingList() {
540        return circlelist;
541    }
542
543    private static final Logger log = LoggerFactory.getLogger(VSDGeoFile.class);
544
545}