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;
009
010import jmri.jmrit.XmlFile;
011import jmri.Scale;
012import jmri.Reporter;
013import jmri.Block;
014import jmri.BlockManager;
015import jmri.InstanceManager;
016import jmri.jmrit.display.layoutEditor.*;
017import jmri.jmrit.display.EditorManager;
018import jmri.util.FileUtil;
019import jmri.util.PhysicalLocation;
020
021import org.jdom2.Element;
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, 2025
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    private ArrayList<LayoutEditor> panels;
058    private ArrayList<LayoutEditor> panelsFinal;
059    HashMap<Block, LayoutEditor> possibleStartBlocks;
060    ArrayList<Block> blockList;
061    private LayoutEditor models;
062    PhysicalLocation models_origin;
063    int lf_version;  // location following
064    int alf_version; // advanced location following
065    private String check_time_str;
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, layout_scale);
119
120        check_time_str = "2000"; // string with default value; see getCheckTime() below
121        n = root.getChildText("check-time");
122        if (n != null) {
123            check_time_str = n.trim();
124        }
125        log.debug("check time: {}", check_time_str);
126
127        // Now look if the file contains "setup" data or "panel" data
128        n = root.getChildText("setup");
129        if ((n != null) && (!n.isEmpty())) {
130            log.debug("A setup found for ALF version 1");
131            alf_version = 1;
132            jmri.util.ThreadingUtil.runOnGUI(() -> {
133                readGeoInfos();
134            });
135
136        } else {
137
138            // Looking for the "panel" data
139            n = root.getChildText("models");
140            if ((n == null) || (n.isEmpty())) {
141                // cannot continue
142                log.warn("No Panel specified in {}", VSDGeoDataFileName);
143            } else {
144                // An existing (loaded) panel is expected
145                panels = new ArrayList<>(InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class));
146                if (panels.isEmpty()) {
147                    log.warn("No Panel loaded. Please restart PanelPro and load Panel \"{}\" first", n);
148                    return;
149                } else {
150                    // There is at least one panel;
151                    // does it must match with the specified panel?
152                    for (LayoutEditor panel : panels) {
153                        log.debug("checking panel \"{}\" ... looking for \"{}\"", panel.getTitle(), n);
154                        if (n.equals(panel.getTitle())) {
155                            models = panel;
156                            break;
157                        }
158                    }
159                }
160                if (models == null) {
161                    log.error("Loaded Panel \"{}\" does not match with specified Panel \"{}\". Please correct and restart PanelPro", panels, n);
162                } else {
163                    log.debug("selected panel: {}", models.getTitle());
164                    n = root.getChildText("models-origin");
165                    if ((n != null) && (!n.isEmpty())) {
166                        models_origin = PhysicalLocation.parse(n);
167                        log.debug("models-origin: {}", models_origin);
168                    } else {
169                        models_origin = new PhysicalLocation(346f, 260f, 0f); // default
170                    }
171                    alf_version = 2;
172                    log.debug("ALF version: {}", alf_version);
173                    readPanelInfos(); // good to go
174                }
175            }
176        }
177    }
178
179    private void readGeoInfos() {
180        // Detect number of "setup" tags and maximal number of "geodataset" tags
181
182        Element c, c0, c1;
183        String n, np;
184        num_issues = 0;
185
186        num_setups = 0; // # setup
187        int num_geodatasets = 0; // # geodataset
188        int max_geodatasets = 0; // helper
189        Iterator<Element> ix = root.getChildren("setup").iterator(); // NOI18N
190        while (ix.hasNext()) {
191            c = ix.next();
192            num_geodatasets = c.getChildren("geodataset").size();
193            log.debug("setup {} has {} geodataset(s)", num_setups + 1, num_geodatasets);
194            if (num_geodatasets > max_geodatasets) {
195                max_geodatasets = num_geodatasets; // # geodatasets can vary; take highest value
196            }
197            num_setups++;
198        }
199        log.debug("counting setups: {}, maximum geodatasets: {}", num_setups, max_geodatasets);
200        // Limitation check is done by the schema validation, but a XML schema is not yet in place
201        if (num_setups == 0 || num_geodatasets == 0 || num_setups > VSDecoderManager.max_decoder) {
202            log.warn("File {}: Invalid number of setups or geodatasets", VSDGeoDataFileName);
203            geofile_ok = false;
204            return;
205        }
206
207        // Setup array to save the block parameters
208        blockParameter = new float[num_setups][max_geodatasets][5];
209
210        // Go through all setups and their geodatasets
211        //  - get the PhysicalLocation (position) from the parameter file
212        //  - make checks which are not covered by the schema validation
213        //  - make some basic checks for not validated VSDGeoData.xml files (avoid NPEs)
214        setup_index = 0;
215        Iterator<Element> i0 = root.getChildren("setup").iterator(); // NOI18N
216        while (i0.hasNext()) {
217            c0 = i0.next();
218            log.debug("--- SETUP: {}", setup_index + 1);
219
220            boolean is_end_position_set = false; // Need one end-position per setup
221            int j = 0;
222            Iterator<Element> i1 = c0.getChildren("geodataset").iterator(); // NOI18N
223            while (i1.hasNext()) {
224                c1 = i1.next();
225                int rep_int = 0;
226                if (c1.getChildText("reporter-systemname") != null) {
227                    np = c1.getChildText("reporter-systemname");
228                    Reporter rep = jmri.InstanceManager.getDefault(jmri.ReporterManager.class).getBySystemName(np);
229                    if (rep != null) {
230                        try {
231                            rep_int = Integer.parseInt(jmri.Manager.getSystemSuffix(rep.getSystemName()));
232                        } catch (java.lang.NumberFormatException e) {
233                            log.warn("File {}: Reporter System Name '{}' is not valid for VSD", VSDGeoDataFileName, np);
234                            num_issues++;
235                        }
236                        reporterlist[setup_index].add(rep_int);
237                        n = c1.getChildText("position");
238                        // An element "position" is required and a XML schema and a XML schema is not yet in place
239                        if (n != null) {
240                            PhysicalLocation pl = PhysicalLocation.parse(n);
241                            blockPositionlist[setup_index].add(pl);
242                            // Establish relationship Reporter-PhysicalLocation (see window Manage VSD Locations)
243                            PhysicalLocation.setBeanPhysicalLocation(pl, rep);
244                            log.debug("Reporter: {}, position set to: {}", rep, pl);
245                        } else {
246                            log.warn("File {}: Element position not found", VSDGeoDataFileName);
247                            num_issues++;
248                        }
249                    } else {
250                        log.warn("File {}: No Reporter available for system name = {}", VSDGeoDataFileName, np);
251                        num_issues++;
252                    }
253                } else {
254                    log.warn("File {}: Reporter system name missing", VSDGeoDataFileName);
255                    num_issues++;
256                }
257
258                if (num_issues == 0) {
259                    n = c1.getChildText("radius");
260                    if (n != null) {
261                        blockParameter[setup_index][j][0] = Float.parseFloat(n);
262                        log.debug(" radius: {}", n);
263                    } else {
264                        log.warn("File {}: Element radius not found", VSDGeoDataFileName);
265                        num_issues++;
266                    }
267                    n = c1.getChildText("slope");
268                    if (n != null) {
269                        blockParameter[setup_index][j][1] = Float.parseFloat(n);
270                        log.debug(" slope: {}", n);
271                    } else {
272                        // If a radius is not defined (radius = 0), slope must exist!
273                        if (blockParameter[setup_index][j][0] == 0.0f) {
274                            log.warn("File {}: Element slope not found", VSDGeoDataFileName);
275                            num_issues++;
276                        }
277                    }
278                    n = c1.getChildText("rotate-xpos");
279                    if (n != null) {
280                        blockParameter[setup_index][j][2] = Float.parseFloat(n);
281                        log.debug(" rotate-xpos: {}", n);
282                    } else {
283                        // If a radius is defined (radius > 0), rotate-xpos must exist!
284                        if (blockParameter[setup_index][j][0] > 0.0f) {
285                            log.warn("File {}: Element rotate-xpos not found", VSDGeoDataFileName);
286                            num_issues++;
287                        }
288                    }
289                    n = c1.getChildText("rotate-ypos");
290                    if (n != null) {
291                        blockParameter[setup_index][j][3] = Float.parseFloat(n);
292                        log.debug(" rotate-ypos: {}", n);
293                    } else {
294                        // If a radius is defined (radius > 0), rotate-ypos must exist!
295                        if (blockParameter[setup_index][j][0] > 0.0f) {
296                            log.warn("File {}: Element rotate-ypos not found", VSDGeoDataFileName);
297                                num_issues++;
298                            }
299                    }
300                    n = c1.getChildText("length");
301                    if (n != null) {
302                        blockParameter[setup_index][j][4] = Float.parseFloat(n);
303                        log.debug(" length: {}", n);
304                    } else {
305                        log.warn("File {}: Element length not found", VSDGeoDataFileName);
306                        num_issues++;
307                    }
308                    n = c1.getChildText("end-position");
309                    if (n != null) {
310                        if (!is_end_position_set) {
311                            blockPositionlist[setup_index].add(PhysicalLocation.parse(n));
312                            is_end_position_set = true;
313                            log.debug("end-position for location {} set to {}", j,
314                                    blockPositionlist[setup_index].get(blockPositionlist[setup_index].size() - 1));
315                        } else {
316                            log.warn("File {}: Only the last geodataset should have an end-position", VSDGeoDataFileName);
317                            num_issues++;
318                        }
319                    }
320                }
321                j++;
322            }
323
324            if (!is_end_position_set) {
325                log.warn("File {}: End-position missing for setup {}", VSDGeoDataFileName, setup_index + 1);
326                num_issues++;
327            }
328            addLists();
329            setup_index++;
330        }
331        finishRead();
332    }
333
334    // Gather infos about the LayoutEditor panel(s)
335    private void readPanelInfos() {
336        int max_geodatasets = 0;
337        possibleStartBlocks = new HashMap<>();
338        blockList = new ArrayList<>();
339
340        log.debug("Found panel: {}", models);
341
342        // Look for panels with an Edge Connector
343        panels = new ArrayList<>(InstanceManager.getDefault(EditorManager.class).getAll(LayoutEditor.class));
344        panelsFinal = new ArrayList<>();
345        for (LayoutEditor p : panels) {
346            for (LayoutTrack lt : p.getLayoutTracks()) {
347                if (lt instanceof PositionablePoint) {
348                    PositionablePoint pp = (PositionablePoint) lt;
349                    if (pp.getType() == PositionablePoint.PointType.EDGE_CONNECTOR) {
350                        if (!panelsFinal.contains(p)) {
351                            panelsFinal.add(p);
352                        }
353                    }
354                }
355            }
356        }
357        log.debug("edge panels: {}", panelsFinal);
358
359        if (panelsFinal.isEmpty()) {
360            panelsFinal.add(models);
361        }
362        log.debug("final panels: {}", panelsFinal);
363
364        // ALL LAYOUT TRACKS; count turnouts and track segments only
365        int max_ts = 0;
366        for (LayoutEditor p : panelsFinal) {
367            for (LayoutTrack lt : p.getLayoutTracks()) {
368                if (lt instanceof LayoutTurnout) {
369                    max_geodatasets++;
370                } else if (lt instanceof TrackSegment) {
371                    max_geodatasets++;
372                    max_ts++;
373                } else if (lt instanceof LevelXing) {
374                    max_geodatasets++;
375                    max_geodatasets++; // LevelXing contains 2 blocks, AC and BD
376                } else {
377                    log.debug("no LayoutTurnout, no TrackSegment, no PositionablePoint, but: {}", lt);
378                }
379            }
380        }
381        log.debug("number of turnouts and track segments: {}", max_geodatasets);
382
383        // minimal 1 layout track
384        if (max_geodatasets == 0) {
385            log.warn("Panel must have minimum one layout track");
386            return;
387        }
388
389        // minimal 1 track segment
390        if (max_ts == 0) {
391            log.warn("Panel must have minimum one track segment");
392            return;
393        }
394
395        // Find size and setup array to save the block parameters
396        BlockManager bmgr = InstanceManager.getDefault(BlockManager.class);
397        Set<Block> blockSet = bmgr.getNamedBeanSet();
398        if (blockSet.isEmpty()) {
399            log.warn("Panel must have minimum one block");
400            return;
401        }
402
403        LayoutBlockManager lm = InstanceManager.getDefault(LayoutBlockManager.class);
404        LayoutBlock lblk;
405
406        log.debug("panels: {}", panelsFinal);
407
408        // List all blocks and list possible start blocks
409        for (LayoutEditor le : panelsFinal) {
410            log.debug("### panel: {}", le);
411            for (Block bl : blockSet) {
412                if (bl != null) {
413                    String userName2 = bl.getUserName();
414                    if (userName2 != null) {
415                        lblk = lm.getByUserName(userName2);
416                        if (lblk != null) {
417                            log.debug("File {}, block system name: {}, user name: {}", le.getTitle(), bl.getSystemName(), userName2);
418                            int tsInBlock = 0;
419                            // List of all LayoutTracks in the block
420                            ArrayList<LayoutTrack> layoutTracksInBlock = new ArrayList<>();
421                            for (LayoutTrack lt : le.getLayoutTracks()) {
422                                if (lt instanceof LayoutTurnout) {
423                                    LayoutTurnout to = (LayoutTurnout) lt;
424                                    if (to.getLayoutBlock() == lblk) {
425                                        layoutTracksInBlock.add(lt);
426                                        blockList.add(bl);
427                                    }
428                                } else if (lt instanceof TrackSegment) {
429                                    TrackSegment ts = (TrackSegment) lt;
430                                    if (ts.getLayoutBlock() == lblk) {
431                                        layoutTracksInBlock.add(lt);
432                                        blockList.add(bl);
433                                        tsInBlock++;
434                                    }
435                                } else if (lt instanceof LevelXing) {
436                                    LevelXing lx = (LevelXing) lt;
437                                    if (lx.getLayoutBlockAC() == lblk || lx.getLayoutBlockBD() == lblk) {
438                                        layoutTracksInBlock.add(lt); // LevelXing contains 2 blocks, AC and BD; add one more entry here
439                                        blockList.add(bl);
440                                    }
441                                } else if (lt instanceof LayoutTurntable) {
442                                    LayoutTurntable tt = (LayoutTurntable) lt;
443                                    if (tt.getLayoutBlock() == lblk) {
444                                        layoutTracksInBlock.add(lt);
445                                        blockList.add(bl);
446                                    }
447                                }
448                            }
449                            log.debug("layoutTracksInBlock: {}", layoutTracksInBlock);
450                            // A possible start-block is a block with a single TrackSegment
451                            if (tsInBlock == 1 && possibleStartBlocks.get(bl) == null) {
452                                possibleStartBlocks.put(bl, le); // Save a Block together with its LE Panel
453                            }
454                        }
455                    }
456                }
457            }
458        }
459        log.debug("Block list: {}, possible start-blocks: {}", blockList, possibleStartBlocks);
460        geofile_ok = true;
461    }
462
463    private void addLists() {
464        if (num_issues == 0) {
465            // Add lists to their array
466            reporterlists.add(reporterlist[setup_index]);
467            blockPositionlists.add(blockPositionlist[setup_index]);
468
469            // Prove, if the setup has a circling route and add the result to a list
470            //  compare first and last blockPosition without the tunnel attribute
471            //  needed for the Reporter validation check in VSDecoderManager
472            int last_index = blockPositionlist[setup_index].size() - 1;
473            log.debug("first setup position: {}, last setup position: {}", blockPositionlist[setup_index].get(0),
474                    blockPositionlist[setup_index].get(last_index));
475            if (blockPositionlist[setup_index].get(0) != null
476                    && blockPositionlist[setup_index].get(0).x == blockPositionlist[setup_index].get(last_index).x
477                    && blockPositionlist[setup_index].get(0).y == blockPositionlist[setup_index].get(last_index).y
478                    && blockPositionlist[setup_index].get(0).z == blockPositionlist[setup_index].get(last_index).z) {
479                circlelist.add(true);
480            } else {
481                circlelist.add(false);
482            }
483            log.debug("circling: {}", circlelist.get(setup_index));
484        }
485    }
486
487    private void finishRead() {
488        // Some Debug infos
489        if (log.isDebugEnabled()) {
490            log.debug("--- LISTS");
491            log.debug("number of Reporter lists: {}", reporterlists.size());
492            log.debug("Reporter lists with their Reporters (digit only): {}", reporterlists);
493            //log.debug("TEST reporter get 0 list size: {}", reporterlists.get(0).size());
494            //log.debug("TEST reporter [0] list size: {}", reporterlist[0].size());
495            log.debug("number of Position lists: {}", blockPositionlists.size());
496            log.debug("Position lists: {}", blockPositionlists);
497            log.debug("--- COUNTERS");
498            log.debug("number of setups: {}", num_setups);
499            log.debug("number of issues: {}", num_issues);
500        }
501        setGeoFileStatus();
502    }
503
504    private void setGeoFileStatus() {
505        if (num_issues > 0) {
506            geofile_ok = false;
507            log.warn("set geofile to not ok");
508        } else {
509            geofile_ok = true;
510        }
511    }
512
513    // Set a range to protect the process
514    int getCheckTime() {
515        int check_time = 2000; // default
516        if (org.apache.commons.lang3.StringUtils.isNumeric(check_time_str)) {
517            int ct = Integer.parseInt(check_time_str);
518            if (ct >= 500 && ct <= 5000) {
519                check_time = ct; // new valid value
520            } else {
521                log.info("Parameter check-time not in range 500 - 5000, defaulting to {} ms", ct);
522            }
523        } else {
524            log.info("Parameter check-time not numeric, defaulting to {} ms", check_time);
525        }
526        return check_time;
527    }
528
529    // Number of setups
530    public int getNumberOfSetups() {
531        return num_setups;
532    }
533
534    // Reporter lists
535    public List<List<Integer>> getReporterList() {
536        return reporterlists;
537    }
538
539    // Reporter Parameter
540    public float[][][] getBlockParameter() {
541        return blockParameter;
542    }
543
544    // Reporter (Block) Position lists
545    public List<List<PhysicalLocation>> getBlockPosition() {
546        return blockPositionlists;
547    }
548
549    // Circling list
550    public List<Boolean> getCirclingList() {
551        return circlelist;
552    }
553
554    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(VSDGeoFile.class);
555
556}