001package jmri.jmrit.timetable.swing; 002 003import java.awt.*; 004import java.awt.geom.*; 005import java.awt.print.*; 006import java.util.*; 007import java.util.List; 008 009import jmri.jmrit.timetable.*; 010 011/** 012 * The left column has the layout information along with the station names next to the diagram box. 013 * The column width is dynamic based on the width of the items. 014 * Across the top, lined up with the diagram box, are the throttle lines. 015 * The main section is the diagram box. 016 * Across the bottom, lined up with the diagram box, is the hours section. 017 * <pre> 018 * +--------- canvas -------------+ 019 * | info | throttle lines | 020 * | |+------------------+| 021 * | station || || 022 * | station || diagram box || 023 * | station || || 024 * | |+------------------+| 025 * | | hours | 026 * +------------------------------+ 027 * </pre> 028 * A normal train line will be "a-b-c-d-e" for a through train, or "a-b-c-b-a" for a turn. 029 * <p> 030 * A multi-segment train will be "a1-b1-c1-x2-y2-z2" where c is the junction. The 031 * reverse will be "z2-y2-z2-c2-b1-a1". Notice: While c is in both segments, for 032 * train stop purposes, the arrival "c" is used and the departure "c" is skipped. 033 */ 034public class TimeTableGraphCommon { 035 036 /** 037 * Initialize the data used by paint() and supporting methods when the 038 * panel is displayed. 039 * @param segmentId The segment to be displayed. For multiple segment 040 * layouts separate graphs are required. 041 * @param scheduleId The schedule to be used for this graph. 042 * @param showTrainTimes When true, include the minutes portion of the 043 * train times at each station. 044 * @param height Display height 045 * @param width Display width 046 * @param displayType (not currently used) 047 */ 048 void init(int segmentId, int scheduleId, boolean showTrainTimes, double height, double width, boolean displayType) { 049 _segmentId = segmentId; 050 _scheduleId = scheduleId; 051 _showTrainTimes = showTrainTimes; 052 053 _dataMgr = TimeTableDataManager.getDataManager(); 054 _segment = _dataMgr.getSegment(_segmentId); 055 _layout = _dataMgr.getLayout(_segment.getLayoutId()); 056 _throttles = _layout.getThrottles(); 057 _schedule = _dataMgr.getSchedule(_scheduleId); 058 _startHour = _schedule.getStartHour(); 059 _duration = _schedule.getDuration(); 060 _stations = _dataMgr.getStations(_segmentId, true); 061 _trains = _dataMgr.getTrains(_scheduleId, 0, true); 062 _dimHeight = height; 063 _dimWidth = width; 064 } 065 066 final Font _stdFont = new Font(Font.SANS_SERIF, Font.PLAIN, 10); 067 final Font _smallFont = new Font(Font.SANS_SERIF, Font.PLAIN, 8); 068 final static BasicStroke gridstroke = new BasicStroke(0.5f); 069 final static BasicStroke stroke = new BasicStroke(2.0f); 070 071 TimeTableDataManager _dataMgr; 072 int _segmentId; 073 int _scheduleId; 074 075 Layout _layout; 076 int _throttles; 077 078 Segment _segment; 079 080 Schedule _schedule; 081 int _startHour; 082 int _duration; 083 084 List<Station> _stations; 085 List<Train> _trains; 086 List<Stop> _stops; 087 088 // ------------ global variables ------------ 089 HashMap<Integer, Double> _stationGrid = new HashMap<>(); 090 HashMap<Integer, Double> _hourMap = new HashMap<>(); 091 ArrayList<Double> _hourGrid = new ArrayList<>(); 092 int _infoColWidth = 0; 093 double _hourOffset = 0; 094 double _graphHeight = 0; 095 double _graphWidth = 0; 096 double _graphTop = 0; 097 double _graphBottom = 0; 098 double _graphLeft = 0; 099 double _graphRight = 0; 100 Graphics2D _g2; 101 boolean _showTrainTimes; 102 PageFormat _pf; 103 double _dimHeight = 0; 104 double _dimWidth = 0; 105 106 // ------------ train variables ------------ 107 ArrayList<Rectangle2D> _textLocation = new ArrayList<>(); 108 109 // Train 110 String _trainName; 111 int _trainThrottle; 112 Color _trainColor; 113 Path2D _trainLine; 114 115 // Stop 116 int _stopCnt; 117 int _stopIdx; 118 int _arriveTime; 119 int _departTime; 120 121 // Stop processing 122 double _maxDistance; 123 String _direction; 124// int _baseTime; 125 boolean _firstStop; 126 boolean _lastStop; 127 128 double _firstX; 129 double _lastX; 130 131 double _sizeMinute; 132 double _throttleX; 133 134 public void doPaint(Graphics g) { 135 if (g instanceof Graphics2D) { 136 _g2 = (Graphics2D) g; 137 } else { 138 throw new IllegalArgumentException(); 139 } 140 _g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); 141 _stationGrid.clear(); 142 _hourGrid.clear(); 143 _textLocation.clear(); 144 145// Dimension dim = getSize(); 146// double dimHeight = _pf.getImageableHeight(); 147// double dimWidth = _pf.getImageableWidth() * 2; 148// double dimHeight = _dimHeight; 149// double dimWidth = _dimWidth; 150 151 // Get the height of the throttle section and set the graph top 152 _graphTop = 70.0; 153 if (_layout.getThrottles() > 4) { 154 _graphTop = _layout.getThrottles() * 15.0; 155 } 156 _graphHeight = _dimHeight - _graphTop - 30.0; 157 _graphBottom = _graphTop + _graphHeight; 158 159 // Draw the left column components 160 drawInfoSection(); 161 drawStationSection(); 162 163 // Set the horizontal graph dimensions based on the width of the left column 164 _graphLeft = _infoColWidth + 50.0; 165 _graphWidth = _dimWidth - _infoColWidth - 65.0; 166 _graphRight = _graphLeft + _graphWidth; 167 168 drawHours(); 169 drawThrottleNumbers(); 170 drawGraphGrid(); 171 drawTrains(); 172 } 173 174 void drawInfoSection() { 175 // Info section 176 _g2.setFont(_stdFont); 177 _g2.setColor(Color.BLACK); 178 String layoutName = String.format("%s %s", Bundle.getMessage("LabelLayoutName"), _layout.getLayoutName()); // NOI18N 179 String segmentName = String.format("%s %s", Bundle.getMessage("LabelSegmentName"), _segment.getSegmentName()); // NOI18N 180 String scheduleName = String.format("%s %s", Bundle.getMessage("LabelScheduleName"), _schedule.getScheduleName()); // NOI18N 181 String effDate = String.format("%s %s", Bundle.getMessage("LabelEffDate"), _schedule.getEffDate()); // NOI18N 182 183 _infoColWidth = Math.max(_infoColWidth, _g2.getFontMetrics().stringWidth(layoutName)); 184 _infoColWidth = Math.max(_infoColWidth, _g2.getFontMetrics().stringWidth(scheduleName)); 185 _infoColWidth = Math.max(_infoColWidth, _g2.getFontMetrics().stringWidth(effDate)); 186 187 _g2.drawString(layoutName, 10, 20); 188 _g2.drawString(segmentName, 10, 40); 189 _g2.drawString(scheduleName, 10, 60); 190 _g2.drawString(effDate, 10, 80); 191 } 192 193 void drawStationSection() { 194 _maxDistance = _stations.get(_stations.size() - 1).getDistance(); 195 _g2.setFont(_stdFont); 196 _g2.setColor(Color.BLACK); 197 _stationGrid.clear(); 198 for (Station station : _stations) { 199 String stationName = station.getStationName(); 200 _infoColWidth = Math.max(_infoColWidth, _g2.getFontMetrics().stringWidth(stationName) + 5); 201 double distance = station.getDistance(); 202 double stationY = ((_graphHeight - 50) / _maxDistance) * distance + _graphTop + 30; // calculate the Y offset 203 _g2.drawString(stationName, 15.0f, (float) stationY); 204 _stationGrid.put(station.getStationId(), stationY); 205 } 206 } 207 208 void drawHours() { 209 int currentHour = _startHour; 210 double hourWidth = _graphWidth / (_duration + 1); 211 _hourOffset = hourWidth / 2; 212 _g2.setFont(_stdFont); 213 _g2.setColor(Color.BLACK); 214 _hourGrid.clear(); 215 for (int i = 0; i <= _duration; i++) { 216 String hourString = Integer.toString(currentHour); 217 double hourX = (hourWidth * i) + _hourOffset + _graphLeft; 218 int hOffset = _g2.getFontMetrics().stringWidth(hourString) / 2; 219 _g2.drawString(hourString, (float) hourX - hOffset, (float) _graphBottom + 20); 220 if (i < _duration) { 221 _hourMap.put(currentHour, hourX); 222 } 223 _hourGrid.add(hourX); 224 if (i == 0) { 225 _firstX = hourX - hOffset; 226 } 227 if (i == _duration) { 228 _lastX = hourX - hOffset; 229 } 230 currentHour++; 231 if (currentHour > 23) { 232 currentHour -= 24; 233 } 234 } 235 } 236 237 void drawThrottleNumbers() { 238 _g2.setFont(_smallFont); 239 _g2.setColor(Color.BLACK); 240 for (int i = 1; i <= _throttles; i++) { 241 _g2.drawString(Integer.toString(i), (float) _graphLeft, (float) i * 14); 242 } 243 } 244 245 void drawGraphGrid() { 246 // Print the graph box 247 _g2.draw(new Rectangle2D.Double(_graphLeft, _graphTop, _graphWidth, _graphHeight)); 248 249 // Print the grid lines 250 _g2.setStroke(gridstroke); 251 _g2.setColor(Color.GRAY); 252 _stationGrid.forEach((i, y) -> { 253 _g2.draw(new Line2D.Double(_graphLeft, y, _graphRight, y)); 254 }); 255 _hourGrid.forEach((x) -> { 256 _g2.draw(new Line2D.Double(x, _graphTop, x, _graphBottom)); 257 }); 258 } 259 260 /** 261 * Create the train line for each train with labels. Include times if 262 * selected. 263 * <p> 264 * All defined trains their stops are processed. If a stop has a station 265 * in the segment, it is included. Most trains only use a single segment. 266 */ 267 void drawTrains() { 268// _baseTime = _startHour * 60; 269 _sizeMinute = _graphWidth / ((_duration + 1) * 60); 270 _throttleX = 0; 271 for (Train train : _trains) { 272 _trainName = train.getTrainName(); 273 _trainThrottle = train.getThrottle(); 274 String typeColor; 275 if (train.getTypeId() == 0) { 276 typeColor = "#000000"; 277 } else { 278 typeColor = _dataMgr.getTrainType(train.getTypeId()).getTypeColor(); 279 } 280 _trainColor = Color.decode(typeColor); 281 _trainLine = new Path2D.Double(); 282 283 boolean activeSeg = false; 284 285 _stops = _dataMgr.getStops(train.getTrainId(), 0, true); 286 _stopCnt = _stops.size(); 287 _firstStop = true; 288 _lastStop = false; 289 290 for (_stopIdx = 0; _stopIdx < _stopCnt; _stopIdx++) { 291 Stop stop = _stops.get(_stopIdx); 292 293 // Set basic values 294 _arriveTime = stop.getArriveTime(); 295 _departTime = stop.getDepartTime(); 296 Station stopStation = _dataMgr.getStation(stop.getStationId()); 297 int stopSegmentId = stopStation.getSegmentId(); 298 if (_stopIdx > 0) _firstStop = false; 299 if (_stopIdx == _stopCnt - 1) _lastStop = true; 300 301 if (!activeSeg) { 302 if (stopSegmentId != _segmentId) { 303 continue; 304 } 305 activeSeg = true; 306 setBegin(stop); 307 if (_lastStop) { 308 // One stop route or only one stop in current segment 309 setEnd(stop, false); 310 break; 311 } 312 continue; 313 } 314 315 // activeSeg always true here 316 if (stopSegmentId != _segmentId) { 317 // No longer in active segment, do the end process 318 setEnd(stop, true); 319 activeSeg = false; 320 continue; 321 } else { 322 drawLine(stop); 323 if (_lastStop) { 324 // At the end, do the end process 325 setEnd(stop, false); 326 break; 327 } 328 } 329 } 330 } 331 } 332 333 /** 334 * Draw a train name on the graph. 335 * <p> 336 * The base location is provided by x and y. justify is used to offset 337 * the x axis. invert is used to flip the y offsets. 338 * @param x The x coordinate. 339 * @param y The y coordinate. 340 * @param justify "Center" moves the string left half of the distance. "Right" 341 * moves the string left the full width of the string. 342 * @param invert If true, the y coordinate offset is flipped. 343 * @param throttle If true, a throttle line item. 344 */ 345 void drawTrainName(double x, double y, String justify, boolean invert, boolean throttle) { 346 Rectangle2D textRect = _g2.getFontMetrics().getStringBounds(_trainName, _g2); 347 348 // Position train name 349 if (justify.equals("Center")) { // NOI18N 350 x = x - textRect.getWidth() / 2; 351 } else if (justify.equals("Right")) { // NOI18N 352 x = x - textRect.getWidth(); 353 } 354 355 if (invert) { 356 y = y + ((_direction.equals("down") || throttle) ? -7 : 13); // NOI18N 357 } else { 358 y = y + ((_direction.equals("down") || throttle) ? 13 : -7); // NOI18N 359 } 360 361 textRect.setRect( 362 x, 363 y, 364 textRect.getWidth(), 365 textRect.getHeight() 366 ); 367 textRect = adjustText(textRect); 368 x = textRect.getX(); 369 370 _g2.setFont(_stdFont); 371 _g2.setColor(Color.BLACK); 372 _g2.drawString(_trainName, (float) x, (float) y); 373 _textLocation.add(textRect); 374 } 375 376 /** 377 * Draw the minutes value on the graph if enabled. 378 * @param time The time in total minutes. Converted to remainder minutes. 379 * @param mode Used to set the x and y offsets based on type of time. 380 * @param x The base x coordinate. 381 * @param y The base y coordinate. 382 */ 383 void drawTrainTime(int time, String mode, double x, double y) { 384 if (!_showTrainTimes) { 385 return; 386 } 387 String minutes = String.format("%02d", time % 60); // NOI18N 388 Rectangle2D textRect = _g2.getFontMetrics().getStringBounds(minutes, _g2); 389 switch (mode) { 390 case "begin": // NOI18N 391 x = x + ((_direction.equals("down")) ? 2 : 2); // NOI18N 392 y = y + ((_direction.equals("down")) ? 10 : -1); // NOI18N 393 break; 394 case "arrive": // NOI18N 395 x = x + ((_direction.equals("down")) ? 2 : 3); // NOI18N 396 y = y + ((_direction.equals("down")) ? -2 : 10); // NOI18N 397 break; 398 case "depart": // NOI18N 399 x = x + ((_direction.equals("down")) ? 2 : 2); // NOI18N 400 y = y + ((_direction.equals("down")) ? 10 : -2); // NOI18N 401 break; 402 case "end": // NOI18N 403 x = x + ((_direction.equals("down")) ? 0 : 0); // NOI18N 404 y = y + ((_direction.equals("down")) ? 0 : 0); // NOI18N 405 break; 406 default: 407 log.error("drawTrainTime mode {} is unknown",mode); // NOI18N 408 return; 409 } 410 411 textRect.setRect( 412 x, 413 y, 414 textRect.getWidth(), 415 textRect.getHeight() 416 ); 417 textRect = adjustText(textRect); 418 x = textRect.getX(); 419 420 _g2.setFont(_smallFont); 421 _g2.setColor(Color.GRAY); 422 _g2.drawString(minutes, (float) x, (float) y); 423 _textLocation.add(textRect); 424 } // TODO End? 425 426 /** 427 * Move text that overlaps existing text. 428 * @param textRect The proposed text rectangle. 429 * @return The resulting rectangle 430 */ 431 Rectangle2D adjustText(Rectangle2D textRect) { 432 double xLoc = textRect.getX(); 433 double yLoc = textRect.getY(); 434 double xLen = textRect.getWidth(); 435 436 double wrkX = xLoc; 437 double xMin; 438 double xMax; 439 boolean chgX = false; 440 441 for (Rectangle2D workRect : _textLocation) { 442 if (workRect.getY() == yLoc) { 443 xMin = workRect.getX(); 444 xMax = xMin + workRect.getWidth(); 445 446 if (xLoc > xMin && xLoc < xMax) { 447 wrkX = xMax + 2; 448 chgX = true; 449 } 450 451 if (xLoc + xLen > xMin && xLoc + xLen < xMax) { 452 wrkX = xMin - xLen -2; 453 chgX = true; 454 } 455 } 456 } 457 458 if (chgX) { 459 textRect.setRect( 460 wrkX, 461 yLoc, 462 textRect.getWidth(), 463 textRect.getHeight() 464 ); 465 } 466 467 return textRect; 468 } 469 470 /** 471 * Determine direction of travel on the graph: up or down 472 */ 473 void setDirection() { 474 if (_stopCnt == 1) { 475 // Single stop train, default to down 476 _direction = "down"; // NOI18N 477 return; 478 } 479 480 Stop stop = _stops.get(_stopIdx); 481 Station currStation = _dataMgr.getStation(stop.getStationId()); 482 Station nextStation; 483 Station prevStation; 484 double currDistance = currStation.getDistance(); 485 486 if (_firstStop) { 487 // For the first stop, use the next stop to set the direction 488 nextStation = _dataMgr.getStation(_stops.get(_stopIdx + 1).getStationId()); 489 _direction = (nextStation.getDistance() > currDistance) ? "down" : "up"; // NOI18N 490 return; 491 } 492 493 prevStation = _dataMgr.getStation(_stops.get(_stopIdx - 1).getStationId()); 494 if (_lastStop) { 495 // For the last stop, use the previous stop to set the direction 496 // Last stop may also be only stop after segment change; if so wait for next "if" 497 if (prevStation.getSegmentId() == _segmentId) { 498 _direction = (prevStation.getDistance() < currDistance) ? "down" : "up"; // NOI18N 499 return; 500 } 501 } 502 503 if (prevStation.getSegmentId() != _segmentId) { 504 // For the first stop after segment change, use the transfer point to set the direction 505 String prevName = prevStation.getStationName(); 506 507 // Find the corresponding station in the current Segment 508 for (Station segStation : _stations) { 509 if (segStation.getStationName().equals(prevName)) { 510 _direction = (segStation.getDistance() < currDistance) ? "down" : "up"; // NOI18N 511 return; 512 } 513 } 514 } 515 516 // For all other stops in the active segment, use the next stop. 517 if (!_lastStop) { 518 nextStation = _dataMgr.getStation(_stops.get(_stopIdx + 1).getStationId()); 519 if (nextStation.getSegmentId() == _segmentId) { 520 _direction = (nextStation.getDistance() > currDistance) ? "down" : "up"; // NOI18N 521 return; 522 } 523 } 524 525 // At this point, don't change anything. 526 } 527 528 /** 529 * Set the starting point for the _trainLine path. 530 * The normal case will be the first stop (aka start) for the train. 531 * <p> 532 * The other case is a multi-segment train. The first stop in the current 533 * segment will be the station AFTER the junction. That means the start 534 * will actually be at the junction station. 535 * @param stop The current stop. 536 */ 537 void setBegin(Stop stop) { 538 double x; 539 double y; 540 boolean segmentChange = false; 541 542 if (_stopIdx > 0) { 543 // Begin after segment change 544 segmentChange = true; 545 Stop prevStop = _stops.get(_stopIdx - 1); 546 Station prevStation = _dataMgr.getStation(prevStop.getStationId()); 547 String prevName = prevStation.getStationName(); 548 549 // Find matching station in the current segment for the last station in the other segment 550 for (Station segStation : _stations) { 551 if (segStation.getStationName().equals(prevName)) { 552 // x is based on previous depart time, y is based on corresponding station position 553 x = calculateX(prevStop.getDepartTime()); 554 y = _stationGrid.get(segStation.getStationId()); 555 _trainLine.moveTo(x, y); 556 _throttleX = x; // save for drawing the throttle line at setEnd 557 558 setDirection(); 559 drawTrainName(x, y, "Center", true, false); // NOI18N 560 drawTrainTime(prevStop.getDepartTime(), "begin", x, y); // NOI18N 561 break; 562 } 563 } 564 } 565 x = calculateX(stop.getArriveTime()); 566 y = _stationGrid.get(stop.getStationId()); 567 568 if (segmentChange) { 569 _trainLine.lineTo(x, y); 570 setDirection(); 571 drawTrainTime(stop.getArriveTime(), "arrive", x, y); // NOI18N 572 } else { 573 _trainLine.moveTo(x, y); 574 _throttleX = x; // save for drawing the throttle line at setEnd 575 576 setDirection(); 577 drawTrainName(x, y, "Center", true, false); // NOI18N 578 drawTrainTime(stop.getArriveTime(), "begin", x, y); // NOI18N 579 } 580 581 // Check for stop duration before depart 582 if (stop.getDuration() > 0) { 583 x = calculateX(stop.getDepartTime()); 584 _trainLine.lineTo(x, y); 585 drawTrainTime(stop.getDepartTime(), "depart", x, y); // NOI18N 586 } 587 } 588 589 /** 590 * Extend the train line with additional stops. 591 * @param stop The current stop. 592 */ 593 void drawLine(Stop stop) { 594 double x = calculateX(_arriveTime); 595 double y = _stationGrid.get(stop.getStationId()); 596 _trainLine.lineTo(x, y); 597 drawTrainTime(_arriveTime, "arrive", x, y); // NOI18N 598 599 setDirection(); 600 // Check for duration after arrive 601 if (stop.getDuration() > 0) { 602 x = calculateX(_departTime); 603 if (x < _trainLine.getCurrentPoint().getX()) { 604 // The line wraps around to the beginning, do the line in two pieces 605 _trainLine.lineTo(_graphRight - _hourOffset, y); 606 drawTrainName(_graphRight - _hourOffset, y, "Right", false, false); // NOI18N 607 _trainLine.moveTo(_graphLeft + _hourOffset, y); 608 _trainLine.lineTo(x, y); 609 drawTrainName(_graphLeft + _hourOffset, y, "Left", true, false); // NOI18N 610 drawTrainTime(_departTime, "depart", x, y); // NOI18N 611 } else { 612 _trainLine.lineTo(x, y); 613 drawTrainTime(_departTime, "depart", x, y); // NOI18N 614 } 615 } 616 } 617 618 /** 619 * Finish the train line, draw it, the train name and the throttle line if used. 620 * @param stop The current stop. 621 * @param endSegment final segment 622 */ 623 void setEnd(Stop stop, boolean endSegment) { 624 double x; 625 double y; 626 boolean skipLine = false; 627 628 if (_stops.size() == 1 || endSegment) { 629 x = _trainLine.getCurrentPoint().getX(); 630 y = _trainLine.getCurrentPoint().getY(); 631 skipLine = true; 632 } else { 633 x = calculateX(_arriveTime); 634 y = _stationGrid.get(stop.getStationId()); 635 } 636 637 drawTrainName(x, y, "Center", false, false); // NOI18N 638 _g2.setColor(_trainColor); 639 _g2.setStroke(stroke); 640 if (!skipLine) { 641 _trainLine.lineTo(x, y); 642 } 643 _g2.draw(_trainLine); 644 645 // Process throttle line 646 if (_trainThrottle > 0) { 647 _g2.setFont(_smallFont); 648 double throttleY = (_trainThrottle * 14); 649 if (x < _throttleX) { 650 _g2.draw(new Line2D.Double(_throttleX, throttleY, _graphRight - _hourOffset, throttleY)); 651 _g2.draw(new Line2D.Double(_graphLeft + _hourOffset, throttleY, x, throttleY)); 652 drawTrainName(_throttleX + 10, throttleY + 5, "Left", true, true); // NOI18N 653 drawTrainName(_graphLeft + _hourOffset + 10, throttleY + 5, "Left", true, true); // NOI18N 654 } else { 655 _g2.draw(new Line2D.Double(_throttleX, throttleY, x, throttleY)); 656 drawTrainName(_throttleX + 10, throttleY + 5, "Left", true, true); // NOI18N 657 } 658 } 659 } 660 661 /** 662 * Convert the time value, 0 - 1439 to the x graph position. 663 * @param time The time value. 664 * @return the x value. 665 */ 666 double calculateX(int time) { 667 if (time < 0) time = 0; 668 if (time > 1439) time = 1439; 669 670 int hour = time / 60; 671 int min = time % 60; 672 673 return _hourMap.get(hour) + (min * _sizeMinute); 674 } 675 676 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TimeTableGraphCommon.class); 677 678}