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