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}