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}