001package jmri.jmrit.etcs.dmi.swing; 002 003import jmri.jmrit.etcs.TrackCondition; 004import jmri.jmrit.etcs.ResourceUtil; 005import jmri.jmrit.etcs.TrackSection; 006import jmri.jmrit.etcs.MovementAuthority; 007 008import java.awt.Color; 009import java.awt.Font; 010import java.awt.FontMetrics; 011import java.awt.Graphics; 012import java.awt.Graphics2D; 013import java.awt.RenderingHints; 014import java.awt.event.ActionEvent; 015import java.awt.image.BufferedImage; 016import java.util.*; 017 018import javax.annotation.Nonnull; 019import javax.swing.*; 020 021/** 022 * Class to demonstrate features of ERTMS DMI Panel D, the Planning Area. 023 * @author Steve Young Copyright (C) 2024 024 */ 025public class DmiPanelD extends JPanel { 026 027 private static final Color PASP_DARK = new Color(33,49,74); 028 private static final Color PASP_LIGHT = new Color(41,74,107); 029 030 private final DmiPanel dmiPanel; 031 private final JPanel trackAheadFreeQuestion; 032 private final JPanel planningPanel; 033 034 private final JButton plusButton; 035 private final JButton minusButton; 036 037 private static final int[] scaleLineYPx = new int[]{284,283,250,206,182,164,150,149,107,64,21,20}; 038 private static final boolean[] scaleLineLight = new boolean[]{true, true, false, false, false, false, 039 true, true, false, false, true, true}; 040 041 private static final int[] scaleDistanceBase = new int[]{0, 125, 250, 500, 1000}; 042 private static final int[] scaleycords = new int[]{287, 155, 111, 68, 25}; 043 private static final int[] scales = new int[]{ 1, 2, 4, 8, 16, 32}; 044 045 private final List<MovementAuthority> maList = new ArrayList<>(); 046 private boolean loopGradientLimitReached = false; 047 private int nextAdviceChange = -1; 048 private int indicationDistance = -1; 049 private int indicationSpeedChange = -1; 050 051 private static final BufferedImage speedDownImage = ResourceUtil.getTransparentImage("PL_22.bmp"); 052 private static final BufferedImage speedDownImageTargetIndication = ResourceUtil.getTransparentImage("PL_23.bmp"); 053 private static final BufferedImage speedDownImageTargetIndicationAto = ResourceUtil.getTransparentImage("PL_37.bmp"); 054 private static final BufferedImage speedUpImage = ResourceUtil.getTransparentImage("PL_21.bmp"); 055 056 private int currentScale = 0; 057 058 public DmiPanelD(@Nonnull DmiPanel mainPanel){ 059 super(); 060 setLayout(null); 061 dmiPanel = mainPanel; 062 trackAheadFreeQuestion = this.trackAheadFreeQuestionPanel(); 063 064 plusButton = new TransparentButton( true ); 065 minusButton = new TransparentButton( false ); 066 067 setButtonsToState(); // set initial state 068 069 planningPanel = getPlanningPanel(); 070 planningPanel.setLayout(null); 071 072 setBackground(DmiPanel.BACKGROUND_COLOUR); 073 setBounds(334, 15, 246, 300); 074 075 add(trackAheadFreeQuestion); 076 add(planningPanel); 077 078 DmiPanelD.this.setTrackAheadFreeQuestionVisible(false); 079 } 080 081 private JPanel getPlanningPanel(){ 082 JPanel p = new JPanel() { 083 @Override 084 protected void paintComponent(Graphics g) { 085 086 if (trackAheadFreeQuestion.isVisible() || maList.isEmpty() ) { 087 plusButton.setVisible(false); 088 minusButton.setVisible(false); 089 return; 090 } 091 if (!(g instanceof Graphics2D) ) { 092 throw new IllegalArgumentException("Graphics object passed is not the correct type"); 093 } 094 Graphics2D g2 = (Graphics2D) g; 095 096 RenderingHints hints =new RenderingHints(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); 097 hints.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 098 hints.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); 099 g2.setRenderingHints(hints); 100 101 drawBackground(g2); 102 drawScale(g2); 103 loopGradientLimitReached = false; 104 drawGradientBar(g2); 105 drawSpeedChanges(g2); 106 drawAnnouncementsAndOrders(g2); 107 drawNextAdviceChange(g2); 108 drawIndicationMarkerLine(g2); 109 } 110 }; 111 p.setLayout(null); 112 p.setBounds(0, 0, 246, 300); // includes top and bottom margins 113 p.setOpaque(false); 114 115 p.add(plusButton); 116 p.add(minusButton); 117 118 return p; 119 } 120 121 private void drawAnnouncementsAndOrders( Graphics2D g2 ) { 122 int metresInPreviousSections = 0; 123 int nextColumn = 2; 124 Comparator<TrackCondition> lengthComparator = Comparator.comparingInt(TrackCondition::getDistanceFromStart); 125 for (MovementAuthority ma : maList ) { 126 for ( TrackSection section : ma.getTrackSections() ) { 127 List<TrackCondition> l = section.getAnnouncements(); 128 Collections.sort(l, lengthComparator ); 129 for ( TrackCondition da : l) { 130 log.debug("found trackCondition {}", da ); 131 int distance = metresInPreviousSections+da.getDistanceFromStart(); 132 // no need to render all of the icons 133 if ( distance <= ( scaleDistanceBase[4]*scales[currentScale] *1.5 ) ) { 134 nextColumn = ensureSlotNumber(da, nextColumn); 135 int startPx = ( calculatePositionOnScale(distance)); 136 log.debug("marker dist:{} px:{}", distance, startPx); 137 log.debug("drawing image {} at x:{} y:{} ", 138 da.getSmlImage(), getAnnouncementColumnPx(da.getColumnNum()),265-startPx); 139 g2.drawImage(da.getSmlImage(), getAnnouncementColumnPx(da.getColumnNum()), 265-startPx, this); 140 } 141 } 142 metresInPreviousSections += section.getLength(); 143 } 144 } 145 } 146 147 private static int getAnnouncementColumnPx(int column){ 148 switch (column) { 149 case 1: 150 return 42; 151 case 2: 152 return 67; 153 case 3: 154 return 92; 155 default: 156 throw new IllegalArgumentException(); 157 } 158 } 159 160 private static int ensureSlotNumber(TrackCondition da, int nextColumn){ 161 int tempCol = nextColumn; 162 if ( da.getColumnNum() == 0 ) { 163 da.setColumnNum(tempCol); 164 } else { 165 tempCol = da.getColumnNum(); 166 } 167 tempCol++; 168 if ( tempCol == 4 ) { 169 tempCol = 1; 170 } 171 return tempCol; 172 } 173 174 private void drawSpeedChanges( Graphics2D g2 ) { 175 176 List<TrackSection> speedChangeList = MovementAuthority.getTrackSectionList(maList, true); 177 speedChangeList.forEach(ts -> log.debug("track section speed {} length {}", ts.getSpeed(), ts.getLength())); 178 179 Font unitsSizedFont = new Font(DmiPanel.FONT_NAME, Font.PLAIN, 14); 180 g2.setFont(unitsSizedFont); 181 g2.setColor(DmiPanel.GREY); 182 183 int loopMetres = 0; 184 int stopPx = 0; 185 boolean increaseDisplayed = false; 186 int reductionsDisplayed = 0; 187 188 for (int i = 0; i < speedChangeList.size(); i++){ 189 int startPx = calculatePositionOnScale(loopMetres); 190 loopMetres += speedChangeList.get(i).getLength(); 191 stopPx = calculatePositionOnScale(loopMetres); 192 if ( i == 0 ){ 193 continue; 194 } 195 int speed = speedChangeList.get(i).getSpeed(); 196 int change = speed - speedChangeList.get(i-1).getSpeed(); 197 198 if ( change < 0 ) { 199 g2.setColor(getIndicationColor(i)); 200 if ( !increaseDisplayed && reductionsDisplayed < 4 ) { 201 g2.drawImage(getIndicationImg(i), 136, 280-startPx, this); 202 g2.drawString(String.valueOf(speed), 155, 295-startPx); 203 reductionsDisplayed++; 204 } 205 } else { 206 g2.setColor(DmiPanel.GREY); 207 g2.drawImage(speedUpImage, 136, 266-startPx, this); 208 g2.drawString(String.valueOf(speed), 155, 281-startPx); 209 // TODO check in v4 - increaseDisplayed rule 8.3.7.9 clarification 210 // increaseDisplayed = true; 211 } 212 } 213 214 g2.setColor(getIndicationColor(0)); 215 // add 0 stop speed marker 216 g2.drawImage(getIndicationImg(0), 136, 281-stopPx, this); 217 g2.drawString(String.valueOf("0"), 155, 295-stopPx); 218 } 219 220 private BufferedImage getIndicationImg(int order){ 221 if ( indicationSpeedChange != order ) { 222 return speedDownImage; 223 } 224 if ( dmiPanel.getMode() == DmiPanel.MODE_AUTOMATIC_DRIVING ){ 225 return DmiPanelD.speedDownImageTargetIndicationAto; 226 } else { 227 return DmiPanelD.speedDownImageTargetIndication; 228 } 229 } 230 231 private Color getIndicationColor(int order){ 232 if ( indicationSpeedChange != order ) { 233 return DmiPanel.GREY; 234 } 235 if ( dmiPanel.getMode() == DmiPanel.MODE_AUTOMATIC_DRIVING ){ 236 return DmiPanel.WHITE; 237 } else { 238 return DmiPanel.YELLOW; 239 } 240 } 241 242 private void drawBackground( Graphics2D g2 ) { 243 g2.setColor( PASP_DARK ); 244 g2.fillRect(147, 0, 99, 270+15); 245 246 List<TrackSection> speedChangeList = MovementAuthority.getTrackSectionList(maList, true); 247 speedChangeList.forEach(ts -> log.debug("track section speed {} length {}", ts.getSpeed(), ts.getLength())); 248 249 int loopSpeedPlanningMetresFromStart = 0; 250 int loopWidth = 4; 251 252 for (int i = 0; i < speedChangeList.size(); i++){ 253 int startPx = calculatePositionOnScale(loopSpeedPlanningMetresFromStart); 254 loopSpeedPlanningMetresFromStart += speedChangeList.get(i).getLength(); 255 int stopPx = calculatePositionOnScale(loopSpeedPlanningMetresFromStart); 256 257 if (speedChangeList.get(i) != speedChangeList.get(0)){ 258 double percentageOfFirstSection = (double) 259 speedChangeList.get(i).getSpeed() / speedChangeList.get(0).getSpeed() * 100; 260 if (percentageOfFirstSection < 50 ){ 261 loopWidth = Math.min(loopWidth, 1); 262 } else if (percentageOfFirstSection < 75 ){ 263 loopWidth = Math.min(loopWidth, 2); 264 } else if ( percentageOfFirstSection < 100 ){ 265 loopWidth = Math.min(loopWidth, 3); 266 } 267 } 268 269 int w = (94 * loopWidth / 4); 270 log.debug("draw rect y:{} width:{} height:{}", 282-stopPx, w, stopPx-startPx ); 271 g2.setColor( PASP_LIGHT ); 272 g2.fillRect(147, 282-stopPx, w, stopPx-startPx); 273 } 274 } 275 276 private void drawScale(Graphics2D g2){ 277 for ( int i = 0; i< scaleLineYPx.length; i++ ){ 278 g2.setColor(scaleLineLight[i] ? DmiPanel.MEDIUM_GREY: DmiPanel.DARK_GREY ); 279 g2.drawLine(40, scaleLineYPx[i], 240, scaleLineYPx[i]); 280 } 281 Font unitsSizedFont = new Font(DmiPanel.FONT_NAME, Font.PLAIN, 12); 282 g2.setFont(unitsSizedFont); 283 FontMetrics unitsFontM = g2.getFontMetrics(unitsSizedFont); 284 g2.setColor(DmiPanel.MEDIUM_GREY ); 285 286 for ( int i = 0; i< scaleDistanceBase.length; i++ ){ 287 String s = String.valueOf(scaleDistanceBase[i]*(scales[currentScale])); 288 int width = 38- unitsFontM.stringWidth(s); 289 g2.drawString(s, width, scaleycords[i]); 290 } 291 292 plusButton.setVisible(true); 293 minusButton.setVisible(true); 294 } 295 296 private void drawNextAdviceChange(Graphics2D g2) { 297 if ( nextAdviceChange < 0 ) { 298 return; 299 } 300 g2.setColor(DmiPanel.GREY); 301 int startPx = 282 - calculatePositionOnScale(nextAdviceChange); 302 303 g2.fillRect(147, startPx, 10, 2); 304 g2.fillRect(167, startPx, 10, 2); 305 g2.fillRect(187, startPx, 10, 2); 306 g2.fillRect(207, startPx, 10, 2); 307 g2.fillRect(227, startPx, 10, 2); 308 } 309 310 private void drawIndicationMarkerLine(Graphics2D g2) { 311 if ( indicationDistance < 0 ) { 312 return; 313 } 314 g2.setColor(dmiPanel.getMode() == DmiPanel.MODE_AUTOMATIC_DRIVING 315 ? DmiPanel.WHITE : DmiPanel.YELLOW); 316 int startPx = 282 - calculatePositionOnScale(indicationDistance); 317 g2.fillRect(147, startPx, 93, 2); 318 } 319 320 // Calculate the position on the scale for a given length in meters 321 private int calculatePositionOnScale(int lengthInMeters) { 322 323 int linearScaleMaxDistance = (scaleDistanceBase[1]*(scales[currentScale])/5); 324 double logScaleMaxDistance = (scaleDistanceBase[4]*(scales[currentScale])); 325 final int TOTAL_PIXELS = 262; 326 final long FIRST_LINEAR_SCALE_PIXELS = 33; 327 long logScaleWidthInPixels = TOTAL_PIXELS-FIRST_LINEAR_SCALE_PIXELS; 328 329 int position; 330 331 if ( lengthInMeters <= linearScaleMaxDistance ) { 332 position = (int)((lengthInMeters) / (float)linearScaleMaxDistance * FIRST_LINEAR_SCALE_PIXELS ); 333 } else { 334 // Logarithmic scale for lengths beyond 100 meters 335 double logScaleFactor = logScaleWidthInPixels / (Math.log(logScaleMaxDistance) 336 - Math.log(linearScaleMaxDistance)); 337 position = (int)(FIRST_LINEAR_SCALE_PIXELS + (Math.log(lengthInMeters) 338 - Math.log(linearScaleMaxDistance)) * logScaleFactor); 339 } 340 log.debug("at distance {} px: {}",lengthInMeters,position); 341 return position; 342 } 343 344 protected void extendMovementAuthorities( MovementAuthority a ){ 345 maList.add(a); 346 repaint(); 347 } 348 349 protected void resetMovementAuthorities( final List<MovementAuthority> a ){ 350 maList.clear(); 351 maList.addAll(a); 352 repaint(); 353 } 354 355 /** 356 * Get Unmodifiable List of Movement Authorities. 357 * @return List of movement Authorities. 358 */ 359 protected List<MovementAuthority> getMovementAuthorities() { 360 return Collections.unmodifiableList(maList); 361 } 362 363 protected void advance(int distance) { 364 MovementAuthority.advanceForward(maList, distance); 365 nextAdviceChange -= distance; 366 repaint(); 367 } 368 369 private void drawGradientBar( Graphics2D g2 ){ 370 List<TrackSection> gradientList = MovementAuthority.getTrackSectionList(maList, false); 371 // gradientList.forEach(ts -> log.debug("track section gradient {} length {}", ts.getGradient(), ts.getLength())); 372 373 int drawingMetresFromStart = 0; 374 for ( TrackSection gradientTs: gradientList) { 375 376 int startMetres = drawingMetresFromStart; 377 int endMetres = drawingMetresFromStart + gradientTs.getLength(); 378 379 int startPx = calculatePositionOnScale(startMetres); 380 int stopPx = calculatePositionOnScale(endMetres); 381 382 g2.setColor( gradientTs.getGradient() < 0 ? DmiPanel.DARK_GREY : DmiPanel.GREY); 383 g2.fillRect(116, 283-stopPx, 18-1, stopPx-startPx-2); 384 385 g2.setColor( gradientTs.getGradient() < 0 ? DmiPanel.GREY : DmiPanel.WHITE); 386 g2.drawLine(115+1, 283-stopPx, 115+17-1, 283-stopPx); 387 g2.drawLine(115, 283-startPx, 115, 283-stopPx); 388 389 g2.setColor(DmiPanel.BLACK); 390 g2.drawLine(115, 283-startPx-1, 115+17, 283-startPx-1); 391 392 drawIconsOnGradient(g2, gradientTs, startPx, stopPx); 393 drawingMetresFromStart = endMetres; 394 } 395 } 396 397 private void drawIconsOnGradient( Graphics2D g2, TrackSection gradientTs, int startPx, int stopPx ){ 398 if (loopGradientLimitReached) { 399 return; 400 } 401 402 int stopHeight = 283-stopPx+12; 403 if ( stopHeight < 0 ){ 404 stopHeight = 12; 405 loopGradientLimitReached = true; 406 } 407 408 int usableHeight = stopPx-startPx-4; 409 if ( usableHeight > 14 ) { 410 411 Font unitsSizedFont = new Font(DmiPanel.FONT_NAME, Font.PLAIN, 13); 412 g2.setFont(unitsSizedFont); 413 FontMetrics unitsFontM = g2.getFontMetrics(unitsSizedFont); 414 415 g2.setColor(gradientTs.getGradient()<0 ? DmiPanel.WHITE : DmiPanel.BLACK ); 416 417 String toDraw = gradientTs.getGradient()<0 ? " - " : " + "; 418 int width = 124 - (unitsFontM.stringWidth(toDraw)/2); 419 log.debug("stopHeight:{}",stopHeight); 420 g2.drawString(toDraw,width, stopHeight); 421 422 if (usableHeight> 20 ) { 423 g2.drawString(toDraw,width, 283-startPx-2); 424 } 425 426 if (usableHeight> 30 ) { 427 428 unitsSizedFont = new Font(DmiPanel.FONT_NAME, Font.PLAIN, 12); 429 g2.setFont(unitsSizedFont); 430 unitsFontM = g2.getFontMetrics(unitsSizedFont); 431 432 toDraw = String.valueOf(gradientTs.getGradient()).replace(String.valueOf("-"), ""); 433 width = 124 - (unitsFontM.stringWidth(toDraw)/2); 434 int centre = (startPx + stopPx)/2; 435 int drawHeight = 283-centre+4; 436 g2.drawString(toDraw,width, drawHeight); 437 } 438 } 439 } 440 441 private void setButtonsToState(){ 442 plusButton.setEnabled(currentScale != 0); 443 minusButton.setEnabled(currentScale != 5); 444 } 445 446 /** 447 * Set the Scale on the Planning Area. 448 * 0 : 0 - 1000 449 * 1 : 0 - 2000 450 * 2 : 0 - 4000 451 * 3 : 0 - 8000 452 * 4 : 0 - 16000 453 * 5 : 0 - 32000 454 * @param scale the scale to use. 455 */ 456 protected void setScale(int scale){ 457 currentScale = scale; 458 setButtonsToState(); 459 repaint(); 460 } 461 462 protected void setTrackAheadFreeQuestionVisible(boolean newVal) { 463 trackAheadFreeQuestion.setVisible(newVal); 464 } 465 466 protected void setNextAdviceChange(int distance) { 467 nextAdviceChange = distance; 468 repaint(); 469 } 470 471 // Section 8.3.8 472 protected void setIndicationMarkerLine(int distance, int whichSpeedChange ) { 473 indicationDistance = distance; 474 indicationSpeedChange = whichSpeedChange; 475 repaint(); 476 } 477 478 private JPanel trackAheadFreeQuestionPanel() { 479 JPanel p = new JPanel(); 480 p.setLayout(null); 481 p.setBounds(0,50,246,50); 482 p.setBackground(DmiPanel.DARK_GREY); 483 JLabel trackAheadFreeQuestionLogo = new JLabel( 484 ResourceUtil.getImageIcon( "DR_02.bmp") 485 ); 486 487 trackAheadFreeQuestionLogo.setBounds(0,0,162,50); 488 trackAheadFreeQuestionLogo.setBackground(DmiPanel.DARK_GREY); 489 trackAheadFreeQuestionLogo.setBorder(javax.swing.BorderFactory.createLineBorder(DmiPanel.MEDIUM_GREY, 1)); 490 491 p.add(trackAheadFreeQuestionLogo); 492 493 JButton trackAheadFreeButton = new JButton(Bundle.getMessage("ButtonYes")); 494 trackAheadFreeButton.setFocusable(false); 495 trackAheadFreeButton.setBounds(162, 0, 246-162, 50); 496 trackAheadFreeButton.setBackground(DmiPanel.MEDIUM_GREY); 497 trackAheadFreeButton.setForeground(DmiPanel.BLACK); 498 trackAheadFreeButton.setActionCommand(DmiPanel.PROP_CHANGE_TRACK_AHEAD_FREE_TRUE); 499 trackAheadFreeButton.addActionListener(this::buttonClicked); 500 501 p.add(trackAheadFreeButton); 502 return p; 503 } 504 505 void buttonClicked(ActionEvent e){ 506 dmiPanel.firePropertyChange(e.getActionCommand(), false, true); 507 setTrackAheadFreeQuestionVisible(false); 508 } 509 510 private class TransparentButton extends JButton { 511 512 private final boolean plus; 513 514 private TransparentButton(boolean plusIcon) { 515 super(); 516 setOpaque(false); 517 setBorderPainted(false); 518 setFocusable(false); 519 plus = plusIcon; 520 521 if (plus){ 522 setIcon(ResourceUtil.getImageIcon( "NA_03.bmp")); 523 setDisabledIcon(ResourceUtil.getImageIcon( "NA_05.bmp")); 524 } else { 525 setIcon(ResourceUtil.getImageIcon( "NA_04.bmp")); 526 setDisabledIcon(ResourceUtil.getImageIcon( "NA_06.bmp")); 527 } 528 529 setBounds(0, (plus ? 246 : 0 ), 50, 50); 530 addActionListener(this::changeScale); 531 } 532 533 private void changeScale(ActionEvent e){ 534 log.debug("scale changed {}", e.paramString()); 535 currentScale+= ( plus ? -1 : 1 ); 536 setButtonsToState(); 537 planningPanel.repaint(); 538 } 539 540 @Override 541 protected void paintComponent(Graphics g) { 542 Icon i = ( isEnabled() ? getIcon() : getDisabledIcon()); 543 if (i != null) { 544 i.paintIcon(this, g, 15, plus ? 36 : 0); 545 } 546 } 547 } 548 549 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DmiPanelD.class); 550 551}