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}