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}