001package jmri.jmrit.operations.trains.trainbuilder;
002
003import java.awt.*;
004import java.io.PrintWriter;
005import java.text.MessageFormat;
006import java.text.SimpleDateFormat;
007import java.util.*;
008import java.util.List;
009
010import javax.swing.JLabel;
011
012import org.slf4j.Logger;
013import org.slf4j.LoggerFactory;
014
015import com.fasterxml.jackson.databind.util.StdDateFormat;
016
017import jmri.InstanceManager;
018import jmri.jmrit.operations.locations.*;
019import jmri.jmrit.operations.locations.divisions.DivisionManager;
020import jmri.jmrit.operations.rollingstock.RollingStock;
021import jmri.jmrit.operations.rollingstock.cars.*;
022import jmri.jmrit.operations.rollingstock.engines.*;
023import jmri.jmrit.operations.routes.RouteLocation;
024import jmri.jmrit.operations.setup.Control;
025import jmri.jmrit.operations.setup.Setup;
026import jmri.jmrit.operations.trains.*;
027import jmri.util.ColorUtil;
028
029/**
030 * Common routines for trains
031 *
032 * @author Daniel Boudreau (C) Copyright 2008, 2009, 2010, 2011, 2012, 2013,
033 *         2021, 2025
034 */
035public class TrainCommon {
036
037    protected static final String TAB = "    "; // NOI18N
038    protected static final String NEW_LINE = "\n"; // NOI18N
039    public static final String SPACE = " ";
040    public static final String BLANK_LINE = " ";
041    protected static final char HORIZONTAL_LINE_CHAR = '-';
042    protected static final String BUILD_REPORT_CHAR = "-";
043    public static final String HYPHEN = "-";
044    protected static final char VERTICAL_LINE_CHAR = '|';
045    protected static final String TEXT_COLOR_START = "<FONT color=\"";
046    protected static final String TEXT_COLOR_DONE = "\">";
047    protected static final String TEXT_COLOR_END = "</FONT>";
048
049    // when true a pick up, when false a set out
050    protected static final boolean PICKUP = true;
051    // when true Manifest, when false switch list
052    protected static final boolean IS_MANIFEST = true;
053    // when true local car move
054    public static final boolean LOCAL = true;
055    // when true engine attribute, when false car
056    protected static final boolean ENGINE = true;
057    // when true, two column table is sorted by track names
058    public static final boolean IS_TWO_COLUMN_TRACK = true;
059
060    protected CarManager carManager = InstanceManager.getDefault(CarManager.class);
061    protected EngineManager engineManager = InstanceManager.getDefault(EngineManager.class);
062    protected LocationManager locationManager = InstanceManager.getDefault(LocationManager.class);
063
064    // for switch lists
065    protected boolean _pickupCars; // true when there are pickups
066    protected boolean _dropCars; // true when there are set outs
067
068    /**
069     * Used to generate "Two Column" format for engines.
070     *
071     * @param file       Manifest or Switch List File
072     * @param engineList List of engines for this train.
073     * @param rl         The RouteLocation being printed.
074     * @param isManifest True if manifest, false if switch list.
075     */
076    protected void blockLocosTwoColumn(PrintWriter file, List<Engine> engineList, RouteLocation rl,
077            boolean isManifest) {
078        if (isThereWorkAtLocation(null, engineList, rl)) {
079            printEngineHeader(file, isManifest);
080        }
081        int lineLength = getLineLength(isManifest);
082        for (Engine engine : engineList) {
083            if (engine.getRouteLocation() == rl && !engine.getTrackName().equals(Engine.NONE)) {
084                String pullText = padAndTruncate(pickupEngine(engine).trim(), lineLength / 2);
085                pullText = formatColorString(pullText, Setup.getPickupEngineColor());
086                String s = pullText + VERTICAL_LINE_CHAR + tabString("", lineLength / 2 - 1);
087                addLine(file, s);
088            }
089            if (engine.getRouteDestination() == rl) {
090                String dropText = padAndTruncate(dropEngine(engine).trim(), lineLength / 2 - 1);
091                dropText = formatColorString(dropText, Setup.getDropEngineColor());
092                String s = tabString("", lineLength / 2) + VERTICAL_LINE_CHAR + dropText;
093                addLine(file, s);
094            }
095        }
096    }
097
098    /**
099     * Adds a list of locomotive pick ups for the route location to the output
100     * file. Used to generate "Standard" format.
101     *
102     * @param file       Manifest or Switch List File
103     * @param engineList List of engines for this train.
104     * @param rl         The RouteLocation being printed.
105     * @param isManifest True if manifest, false if switch list
106     */
107    protected void pickupEngines(PrintWriter file, List<Engine> engineList, RouteLocation rl, boolean isManifest) {
108        boolean printHeader = Setup.isPrintHeadersEnabled();
109        for (Engine engine : engineList) {
110            if (engine.getRouteLocation() == rl && !engine.getTrackName().equals(Engine.NONE)) {
111                if (printHeader) {
112                    printPickupEngineHeader(file, isManifest);
113                    printHeader = false;
114                }
115                pickupEngine(file, engine, isManifest);
116            }
117        }
118    }
119
120    private void pickupEngine(PrintWriter file, Engine engine, boolean isManifest) {
121        StringBuffer buf = new StringBuffer(padAndTruncateIfNeeded(Setup.getPickupEnginePrefix(),
122                isManifest ? Setup.getManifestPrefixLength() : Setup.getSwitchListPrefixLength()));
123        String[] format = Setup.getPickupEngineMessageFormat();
124        for (String attribute : format) {
125            String s = getEngineAttribute(engine, attribute, PICKUP);
126            if (!checkStringLength(buf.toString() + s, isManifest)) {
127                addLine(file, buf, Setup.getPickupEngineColor());
128                buf = new StringBuffer(TAB); // new line
129            }
130            buf.append(s);
131        }
132        addLine(file, buf, Setup.getPickupEngineColor());
133    }
134
135    /**
136     * Adds a list of locomotive drops for the route location to the output
137     * file. Used to generate "Standard" format.
138     *
139     * @param file       Manifest or Switch List File
140     * @param engineList List of engines for this train.
141     * @param rl         The RouteLocation being printed.
142     * @param isManifest True if manifest, false if switch list
143     */
144    protected void dropEngines(PrintWriter file, List<Engine> engineList, RouteLocation rl, boolean isManifest) {
145        boolean printHeader = Setup.isPrintHeadersEnabled();
146        for (Engine engine : engineList) {
147            if (engine.getRouteDestination() == rl) {
148                if (printHeader) {
149                    printDropEngineHeader(file, isManifest);
150                    printHeader = false;
151                }
152                dropEngine(file, engine, isManifest);
153            }
154        }
155    }
156
157    private void dropEngine(PrintWriter file, Engine engine, boolean isManifest) {
158        StringBuffer buf = new StringBuffer(padAndTruncateIfNeeded(Setup.getDropEnginePrefix(),
159                isManifest ? Setup.getManifestPrefixLength() : Setup.getSwitchListPrefixLength()));
160        String[] format = Setup.getDropEngineMessageFormat();
161        for (String attribute : format) {
162            String s = getEngineAttribute(engine, attribute, !PICKUP);
163            if (!checkStringLength(buf.toString() + s, isManifest)) {
164                addLine(file, buf, Setup.getDropEngineColor());
165                buf = new StringBuffer(TAB); // new line
166            }
167            buf.append(s);
168        }
169        addLine(file, buf, Setup.getDropEngineColor());
170    }
171
172    /**
173     * Returns the pick up string for a loco. Useful for frames like the train
174     * conductor and yardmaster.
175     *
176     * @param engine The Engine.
177     * @return engine pick up string
178     */
179    public String pickupEngine(Engine engine) {
180        StringBuilder builder = new StringBuilder();
181        for (String attribute : Setup.getPickupEngineMessageFormat()) {
182            builder.append(getEngineAttribute(engine, attribute, PICKUP));
183        }
184        return builder.toString();
185    }
186
187    /**
188     * Returns the drop string for a loco. Useful for frames like the train
189     * conductor and yardmaster.
190     *
191     * @param engine The Engine.
192     * @return engine drop string
193     */
194    public String dropEngine(Engine engine) {
195        StringBuilder builder = new StringBuilder();
196        for (String attribute : Setup.getDropEngineMessageFormat()) {
197            builder.append(getEngineAttribute(engine, attribute, !PICKUP));
198        }
199        return builder.toString();
200    }
201
202    // the next three booleans are used to limit the header to once per location
203    boolean _printPickupHeader = true;
204    boolean _printSetoutHeader = true;
205    boolean _printLocalMoveHeader = true;
206
207    /**
208     * Block cars by track, then pick up and set out for each location in a
209     * train's route. This routine is used for the "Standard" format.
210     *
211     * @param file        Manifest or switch list File
212     * @param train       The train being printed.
213     * @param carList     List of cars for this train
214     * @param rl          The RouteLocation being printed
215     * @param printHeader True if new location.
216     * @param isManifest  True if manifest, false if switch list.
217     */
218    protected void blockCarsByTrack(PrintWriter file, Train train, List<Car> carList, RouteLocation rl,
219            boolean printHeader, boolean isManifest) {
220        if (printHeader) {
221            _printPickupHeader = true;
222            _printSetoutHeader = true;
223            _printLocalMoveHeader = true;
224        }
225        List<Track> tracks = rl.getLocation().getTracksByNameList(null);
226        List<String> trackNames = new ArrayList<>();
227        clearUtilityCarTypes(); // list utility cars by quantity
228        for (Track track : tracks) {
229            if (trackNames.contains(track.getSplitName())) {
230                continue;
231            }
232            trackNames.add(track.getSplitName()); // use a track name once
233
234            // car pick ups
235            blockCarsPickups(file, train, carList, rl, track, isManifest);
236
237            // now do car set outs and local moves
238            // group local moves first?
239            blockCarsSetoutsAndMoves(file, train, carList, rl, track, isManifest, false,
240                    Setup.isGroupCarMovesEnabled());
241            // set outs or both
242            blockCarsSetoutsAndMoves(file, train, carList, rl, track, isManifest, true,
243                    !Setup.isGroupCarMovesEnabled());
244
245            if (!Setup.isSortByTrackNameEnabled()) {
246                break; // done
247            }
248        }
249    }
250
251    private void blockCarsPickups(PrintWriter file, Train train, List<Car> carList, RouteLocation rl,
252            Track track, boolean isManifest) {
253        // block pick up cars, except for passenger cars
254        for (RouteLocation rld : train.getTrainBlockingOrder()) {
255            for (Car car : carList) {
256                if (Setup.isSortByTrackNameEnabled() &&
257                        !track.getSplitName().equals(car.getSplitTrackName())) {
258                    continue;
259                }
260                // Block cars
261                // caboose or FRED is placed at end of the train
262                // passenger cars are already blocked in the car list
263                // passenger cars with negative block numbers are placed at
264                // the front of the train, positive numbers at the end of
265                // the train.
266                if (isNextCar(car, rl, rld)) {
267                    // determine if pick up header is needed
268                    printPickupCarHeader(file, car, isManifest, !IS_TWO_COLUMN_TRACK);
269
270                    // use truncated format if there's a switch list
271                    boolean isTruncate = Setup.isPrintTruncateManifestEnabled() &&
272                            rl.getLocation().isSwitchListEnabled();
273
274                    if (car.isUtility()) {
275                        pickupUtilityCars(file, carList, car, isTruncate, isManifest);
276                    } else if (isManifest && isTruncate) {
277                        pickUpCarTruncated(file, car, isManifest);
278                    } else {
279                        pickUpCar(file, car, isManifest);
280                    }
281                    _pickupCars = true;
282                }
283            }
284        }
285    }
286
287    private void blockCarsSetoutsAndMoves(PrintWriter file, Train train, List<Car> carList, RouteLocation rl,
288            Track track, boolean isManifest, boolean isSetout, boolean isLocalMove) {
289        for (Car car : carList) {
290            if (!car.isLocalMove() && isSetout || car.isLocalMove() && isLocalMove) {
291                if (Setup.isSortByTrackNameEnabled() &&
292                        car.getRouteLocation() != null &&
293                        car.getRouteDestination() == rl) {
294                    // must sort local moves by car's destination track name and not car's track name
295                    // sorting by car's track name fails if there are "similar" location names.
296                    if (!track.getSplitName().equals(car.getSplitDestinationTrackName())) {
297                        continue;
298                    }
299                }
300                if (car.getRouteDestination() == rl && car.getDestinationTrack() != null) {
301                    // determine if drop or move header is needed
302                    printDropOrMoveCarHeader(file, car, isManifest, !IS_TWO_COLUMN_TRACK);
303
304                    // use truncated format if there's a switch list
305                    boolean isTruncate = Setup.isPrintTruncateManifestEnabled() &&
306                            rl.getLocation().isSwitchListEnabled() &&
307                            !train.isLocalSwitcher();
308
309                    if (car.isUtility()) {
310                        setoutUtilityCars(file, carList, car, isTruncate, isManifest);
311                    } else if (isManifest && isTruncate) {
312                        truncatedDropCar(file, car, isManifest);
313                    } else {
314                        dropCar(file, car, isManifest);
315                    }
316                    _dropCars = true;
317                }
318            }
319        }
320    }
321
322    /**
323     * Used to determine if car is the next to be processed when producing
324     * Manifests or Switch Lists. Caboose or FRED is placed at end of the train.
325     * Passenger cars are already blocked in the car list. Passenger cars with
326     * negative block numbers are placed at the front of the train, positive
327     * numbers at the end of the train. Note that a car in train doesn't have a
328     * track assignment.
329     * 
330     * @param car the car being tested
331     * @param rl  when in train's route the car is being pulled
332     * @param rld the destination being tested
333     * @return true if this car is the next one to be processed
334     */
335    public static boolean isNextCar(Car car, RouteLocation rl, RouteLocation rld) {
336        return isNextCar(car, rl, rld, false);
337    }
338        
339    public static boolean isNextCar(Car car, RouteLocation rl, RouteLocation rld, boolean isIgnoreTrack) {
340        Train train = car.getTrain();
341        if (train != null &&
342                (car.getTrack() != null || isIgnoreTrack) &&
343                car.getRouteLocation() == rl &&
344                (rld == car.getRouteDestination() &&
345                        !car.isCaboose() &&
346                        !car.hasFred() &&
347                        !car.isPassenger() ||
348                        rld == train.getTrainDepartsRouteLocation() &&
349                                car.isPassenger() &&
350                                car.getBlocking() < 0 ||
351                        rld == train.getTrainTerminatesRouteLocation() &&
352                                (car.isCaboose() ||
353                                        car.hasFred() ||
354                                        car.isPassenger() && car.getBlocking() >= 0))) {
355            return true;
356        }
357        return false;
358    }
359
360    private void printPickupCarHeader(PrintWriter file, Car car, boolean isManifest, boolean isTwoColumnTrack) {
361        if (_printPickupHeader && !car.isLocalMove()) {
362            printPickupCarHeader(file, isManifest, !IS_TWO_COLUMN_TRACK);
363            _printPickupHeader = false;
364            // check to see if the other headers are needed. If
365            // they are identical, not needed
366            if (getPickupCarHeader(isManifest, !IS_TWO_COLUMN_TRACK)
367                    .equals(getDropCarHeader(isManifest, !IS_TWO_COLUMN_TRACK))) {
368                _printSetoutHeader = false;
369            }
370            if (getPickupCarHeader(isManifest, !IS_TWO_COLUMN_TRACK)
371                    .equals(getLocalMoveHeader(isManifest))) {
372                _printLocalMoveHeader = false;
373            }
374        }
375    }
376
377    private void printDropOrMoveCarHeader(PrintWriter file, Car car, boolean isManifest, boolean isTwoColumnTrack) {
378        if (_printSetoutHeader && !car.isLocalMove()) {
379            printDropCarHeader(file, isManifest, !IS_TWO_COLUMN_TRACK);
380            _printSetoutHeader = false;
381            // check to see if the other headers are needed. If they
382            // are identical, not needed
383            if (getPickupCarHeader(isManifest, !IS_TWO_COLUMN_TRACK)
384                    .equals(getDropCarHeader(isManifest, !IS_TWO_COLUMN_TRACK))) {
385                _printPickupHeader = false;
386            }
387            if (getDropCarHeader(isManifest, !IS_TWO_COLUMN_TRACK).equals(getLocalMoveHeader(isManifest))) {
388                _printLocalMoveHeader = false;
389            }
390        }
391        if (_printLocalMoveHeader && car.isLocalMove()) {
392            printLocalCarMoveHeader(file, isManifest);
393            _printLocalMoveHeader = false;
394            // check to see if the other headers are needed. If they
395            // are identical, not needed
396            if (getPickupCarHeader(isManifest, !IS_TWO_COLUMN_TRACK)
397                    .equals(getLocalMoveHeader(isManifest))) {
398                _printPickupHeader = false;
399            }
400            if (getDropCarHeader(isManifest, !IS_TWO_COLUMN_TRACK).equals(getLocalMoveHeader(isManifest))) {
401                _printSetoutHeader = false;
402            }
403        }
404    }
405
406    /**
407     * Produces a two column format for car pick ups and set outs. Sorted by
408     * track and then by blocking order. This routine is used for the "Two
409     * Column" format.
410     *
411     * @param file        Manifest or switch list File
412     * @param train       The train
413     * @param carList     List of cars for this train
414     * @param rl          The RouteLocation being printed
415     * @param printHeader True if new location.
416     * @param isManifest  True if manifest, false if switch list.
417     */
418    protected void blockCarsTwoColumn(PrintWriter file, Train train, List<Car> carList, RouteLocation rl,
419            boolean printHeader, boolean isManifest) {
420        index = 0;
421        int lineLength = getLineLength(isManifest);
422        List<Track> tracks = rl.getLocation().getTracksByNameList(null);
423        List<String> trackNames = new ArrayList<>();
424        clearUtilityCarTypes(); // list utility cars by quantity
425        if (printHeader) {
426            printCarHeader(file, isManifest, !IS_TWO_COLUMN_TRACK);
427        }
428        for (Track track : tracks) {
429            if (trackNames.contains(track.getSplitName())) {
430                continue;
431            }
432            trackNames.add(track.getSplitName()); // use a track name once
433            // block car pick ups
434            for (RouteLocation rld : train.getTrainBlockingOrder()) {
435                for (int k = 0; k < carList.size(); k++) {
436                    Car car = carList.get(k);
437                    // block cars
438                    // caboose or FRED is placed at end of the train
439                    // passenger cars are already blocked in the car list
440                    // passenger cars with negative block numbers are placed at
441                    // the front of the train, positive numbers at the end of
442                    // the train.
443                    if (isNextCar(car, rl, rld)) {
444                        if (Setup.isSortByTrackNameEnabled() &&
445                                !track.getSplitName().equals(car.getSplitTrackName())) {
446                            continue;
447                        }
448                        _pickupCars = true;
449                        String s;
450                        if (car.isUtility()) {
451                            s = pickupUtilityCars(carList, car, isManifest, !IS_TWO_COLUMN_TRACK);
452                            if (s == null) {
453                                continue;
454                            }
455                            s = s.trim();
456                        } else {
457                            s = pickupCar(car, isManifest, !IS_TWO_COLUMN_TRACK).trim();
458                        }
459                        s = padAndTruncate(s, lineLength / 2);
460                        if (car.isLocalMove()) {
461                            s = formatColorString(s, Setup.getLocalColor());
462                            String sl = appendSetoutString(s, carList, car.getRouteDestination(), car, isManifest,
463                                    !IS_TWO_COLUMN_TRACK);
464                            // check for utility car, and local route with two
465                            // or more locations
466                            if (!sl.equals(s)) {
467                                s = sl;
468                                carList.remove(car); // done with this car, remove from list
469                                k--;
470                            } else {
471                                s = padAndTruncate(s + VERTICAL_LINE_CHAR, getLineLength(isManifest));
472                            }
473                        } else {
474                            s = formatColorString(s, Setup.getPickupColor());
475                            s = appendSetoutString(s, carList, rl, true, isManifest, !IS_TWO_COLUMN_TRACK);
476                        }
477                        addLine(file, s);
478                    }
479                }
480            }
481            if (!Setup.isSortByTrackNameEnabled()) {
482                break; // done
483            }
484        }
485        while (index < carList.size()) {
486            String s = padString("", lineLength / 2);
487            s = appendSetoutString(s, carList, rl, false, isManifest, !IS_TWO_COLUMN_TRACK);
488            String test = s.trim();
489            // null line contains |
490            if (test.length() > 1) {
491                addLine(file, s);
492            }
493        }
494    }
495
496    List<Car> doneCars = new ArrayList<>();
497
498    /**
499     * Produces a two column format for car pick ups and set outs. Sorted by
500     * track and then by destination. Track name in header format, track name
501     * removed from format. This routine is used to generate the "Two Column by
502     * Track" format.
503     *
504     * @param file        Manifest or switch list File
505     * @param train       The train
506     * @param carList     List of cars for this train
507     * @param rl          The RouteLocation being printed
508     * @param printHeader True if new location.
509     * @param isManifest  True if manifest, false if switch list.
510     */
511    protected void blockCarsByTrackNameTwoColumn(PrintWriter file, Train train, List<Car> carList, RouteLocation rl,
512            boolean printHeader, boolean isManifest) {
513        index = 0;
514        List<Track> tracks = rl.getLocation().getTracksByNameList(null);
515        List<String> trackNames = new ArrayList<>();
516        doneCars.clear();
517        clearUtilityCarTypes(); // list utility cars by quantity
518        if (printHeader) {
519            printCarHeader(file, isManifest, IS_TWO_COLUMN_TRACK);
520        }
521        for (Track track : tracks) {
522            String trackName = track.getSplitName();
523            if (trackNames.contains(trackName)) {
524                continue;
525            }
526            // block car pick ups
527            for (RouteLocation rld : train.getTrainBlockingOrder()) {
528                for (Car car : carList) {
529                    if (car.getTrack() != null &&
530                            car.getRouteLocation() == rl &&
531                            trackName.equals(car.getSplitTrackName()) &&
532                            ((car.getRouteDestination() == rld && !car.isCaboose() && !car.hasFred()) ||
533                                    (rld == train.getTrainTerminatesRouteLocation() &&
534                                            (car.isCaboose() || car.hasFred())))) {
535                        if (!trackNames.contains(trackName)) {
536                            printTrackNameHeader(file, trackName, isManifest);
537                        }
538                        trackNames.add(trackName); // use a track name once
539                        _pickupCars = true;
540                        String s;
541                        if (car.isUtility()) {
542                            s = pickupUtilityCars(carList, car, isManifest, IS_TWO_COLUMN_TRACK);
543                            if (s == null) {
544                                continue;
545                            }
546                            s = s.trim();
547                        } else {
548                            s = pickupCar(car, isManifest, IS_TWO_COLUMN_TRACK).trim();
549                        }
550                        s = padAndTruncate(s, getLineLength(isManifest) / 2);
551                        s = formatColorString(s, car.isLocalMove() ? Setup.getLocalColor() : Setup.getPickupColor());
552                        s = appendSetoutString(s, trackName, carList, rl, isManifest, IS_TWO_COLUMN_TRACK);
553                        addLine(file, s);
554                    }
555                }
556            }
557            for (Car car : carList) {
558                if (!doneCars.contains(car) &&
559                        car.getRouteDestination() == rl &&
560                        trackName.equals(car.getSplitDestinationTrackName())) {
561                    if (!trackNames.contains(trackName)) {
562                        printTrackNameHeader(file, trackName, isManifest);
563                    }
564                    trackNames.add(trackName); // use a track name once
565                    String s = padString("", getLineLength(isManifest) / 2);
566                    String so = appendSetoutString(s, carList, rl, car, isManifest, IS_TWO_COLUMN_TRACK);
567                    // check for utility car
568                    if (so.equals(s)) {
569                        continue;
570                    }
571                    String test = so.trim();
572                    if (test.length() > 1) // null line contains |
573                    {
574                        addLine(file, so);
575                    }
576                }
577            }
578        }
579    }
580
581    protected void printTrackComments(PrintWriter file, RouteLocation rl, List<Car> carList, boolean isManifest) {
582        Location location = rl.getLocation();
583        if (location != null) {
584            List<Track> tracks = location.getTracksByNameList(null);
585            for (Track track : tracks) {
586                if (isManifest && !track.isPrintManifestCommentEnabled() ||
587                        !isManifest && !track.isPrintSwitchListCommentEnabled()) {
588                    continue;
589                }
590                // any pick ups or set outs to this track?
591                boolean pickup = false;
592                boolean setout = false;
593                for (Car car : carList) {
594                    if (car.getRouteLocation() == rl && car.getTrack() != null && car.getTrack() == track) {
595                        pickup = true;
596                    }
597                    if (car.getRouteDestination() == rl &&
598                            car.getDestinationTrack() != null &&
599                            car.getDestinationTrack() == track) {
600                        setout = true;
601                    }
602                }
603                // print the appropriate comment if there's one
604                if (pickup && setout && !track.getCommentBothWithColor().equals(Track.NONE)) {
605                    newLine(file, track.getCommentBothWithColor(), isManifest);
606                } else if (pickup && !setout && !track.getCommentPickupWithColor().equals(Track.NONE)) {
607                    newLine(file, track.getCommentPickupWithColor(), isManifest);
608                } else if (!pickup && setout && !track.getCommentSetoutWithColor().equals(Track.NONE)) {
609                    newLine(file, track.getCommentSetoutWithColor(), isManifest);
610                }
611            }
612        }
613    }
614
615    protected void setCarPickupTime(Train train, RouteLocation rl, List<Car> carList) {
616        String expectedDepartureTime = train.getExpectedDepartureTime(rl, true);
617        for (Car car : carList) {
618            if (car.getRouteLocation() == rl) {
619                car.setPickupTime(expectedDepartureTime);
620            }
621        }
622
623    }
624
625    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "SLF4J_FORMAT_SHOULD_BE_CONST",
626            justification = "Only when exception")
627    public static String getTrainMessage(Train train, RouteLocation rl) {
628        String expectedArrivalTime = train.getExpectedArrivalTime(rl);
629        String routeLocationName = rl.getSplitName();
630        String msg = "";
631        String messageFormatText = ""; // the text being formated in case there's an exception
632        try {
633            // Scheduled work at {0}
634            msg = MessageFormat.format(messageFormatText = TrainManifestText
635                    .getStringScheduledWork(),
636                    new Object[]{routeLocationName, train.getName(),
637                            train.getDescription(), rl.getLocation().getDivisionName()});
638            if (train.isShowArrivalAndDepartureTimesEnabled()) {
639                if (rl == train.getTrainDepartsRouteLocation()) {
640                    // Scheduled work at {0}, departure time {1}
641                    msg = MessageFormat.format(messageFormatText = TrainManifestText
642                            .getStringWorkDepartureTime(),
643                            new Object[]{routeLocationName,
644                                    train.getFormatedDepartureTime(), train.getName(),
645                                    train.getDescription(), rl.getLocation().getDivisionName()});
646                } else if (!rl.getDepartureTime().equals(RouteLocation.NONE) &&
647                        rl != train.getTrainTerminatesRouteLocation()) {
648                    // Scheduled work at {0}, departure time {1}
649                    msg = MessageFormat.format(messageFormatText = TrainManifestText
650                            .getStringWorkDepartureTime(),
651                            new Object[]{routeLocationName,
652                                    expectedArrivalTime.equals(Train.ALREADY_SERVICED)
653                                            ? rl.getFormatedDepartureTime() : train.getExpectedDepartureTime(rl),
654                                    train.getName(), train.getDescription(),
655                                    rl.getLocation().getDivisionName()});
656                } else if (Setup.isUseDepartureTimeEnabled() &&
657                        rl != train.getTrainTerminatesRouteLocation() &&
658                        !train.getExpectedDepartureTime(rl).equals(Train.ALREADY_SERVICED)) {
659                    // Scheduled work at {0}, departure time {1}
660                    msg = MessageFormat.format(messageFormatText = TrainManifestText
661                            .getStringWorkDepartureTime(),
662                            new Object[]{routeLocationName,
663                                    train.getExpectedDepartureTime(rl), train.getName(),
664                                    train.getDescription(), rl.getLocation().getDivisionName()});
665                } else if (!expectedArrivalTime.equals(Train.ALREADY_SERVICED)) {
666                    // Scheduled work at {0}, arrival time {1}
667                    msg = MessageFormat.format(messageFormatText = TrainManifestText
668                            .getStringWorkArrivalTime(),
669                            new Object[]{routeLocationName, expectedArrivalTime,
670                                    train.getName(), train.getDescription(),
671                                    rl.getLocation().getDivisionName()});
672                }
673            }
674            return msg;
675        } catch (IllegalArgumentException e) {
676            msg = Bundle.getMessage("ErrorIllegalArgument",
677                    Bundle.getMessage("TitleSwitchListText"), e.getLocalizedMessage()) + NEW_LINE + messageFormatText;
678            log.error(msg);
679            log.error("Illegal argument", e);
680            return msg;
681        }
682    }
683
684    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings(value = "SLF4J_FORMAT_SHOULD_BE_CONST",
685            justification = "Only when exception")
686    public static String getSwitchListTrainStatus(Train train, RouteLocation rl) {
687        String expectedArrivalTime = train.getExpectedArrivalTime(rl);
688        String msg = "";
689        String messageFormatText = ""; // the text being formated in case there's an exception
690        try {
691            if (train.isLocalSwitcher()) {
692                // Use Manifest text for local departure
693                // Scheduled work at {0}, departure time {1}
694                msg = MessageFormat.format(messageFormatText = TrainManifestText.getStringWorkDepartureTime(),
695                        new Object[]{splitString(train.getTrainDepartsName()), train.getFormatedDepartureTime(),
696                                train.getName(), train.getDescription(),
697                                rl.getLocation().getDivisionName()});
698            } else if (rl == train.getTrainDepartsRouteLocation()) {
699                // Departs {0} {1}bound at {2}
700                msg = MessageFormat.format(messageFormatText = TrainSwitchListText.getStringDepartsAt(),
701                        new Object[]{splitString(train.getTrainDepartsName()), rl.getTrainDirectionString(),
702                                train.getFormatedDepartureTime()});
703            } else if (Setup.isUseSwitchListDepartureTimeEnabled() &&
704                    rl != train.getTrainTerminatesRouteLocation() &&
705                    !train.isTrainEnRoute()) {
706                // Departs {0} at {1} expected arrival {2}, arrives {3}bound
707                msg = MessageFormat.format(
708                        messageFormatText = TrainSwitchListText.getStringDepartsAtExpectedArrival(),
709                        new Object[]{splitString(rl.getName()),
710                                train.getExpectedDepartureTime(rl), expectedArrivalTime,
711                                rl.getTrainDirectionString()});
712            } else if (Setup.isUseSwitchListDepartureTimeEnabled() &&
713                    rl == train.getCurrentRouteLocation() &&
714                    rl != train.getTrainTerminatesRouteLocation() &&
715                    !rl.getDepartureTime().equals(RouteLocation.NONE)) {
716                // Departs {0} {1}bound at {2}
717                msg = MessageFormat.format(messageFormatText = TrainSwitchListText.getStringDepartsAt(),
718                        new Object[]{splitString(rl.getName()), rl.getTrainDirectionString(),
719                                rl.getFormatedDepartureTime()});
720            } else if (train.isTrainEnRoute()) {
721                if (!expectedArrivalTime.equals(Train.ALREADY_SERVICED)) {
722                    // Departed {0}, expect to arrive in {1}, arrives {2}bound
723                    msg = MessageFormat.format(messageFormatText = TrainSwitchListText.getStringDepartedExpected(),
724                            new Object[]{splitString(train.getTrainDepartsName()), expectedArrivalTime,
725                                    rl.getTrainDirectionString(), train.getCurrentLocationName()});
726                }
727            } else {
728                // Departs {0} at {1} expected arrival {2}, arrives {3}bound
729                msg = MessageFormat.format(
730                        messageFormatText = TrainSwitchListText.getStringDepartsAtExpectedArrival(),
731                        new Object[]{splitString(train.getTrainDepartsName()),
732                                train.getFormatedDepartureTime(), expectedArrivalTime,
733                                rl.getTrainDirectionString()});
734            }
735            return msg;
736        } catch (IllegalArgumentException e) {
737            msg = Bundle.getMessage("ErrorIllegalArgument",
738                    Bundle.getMessage("TitleSwitchListText"), e.getLocalizedMessage()) + NEW_LINE + messageFormatText;
739            log.error(msg);
740            log.error("Illegal argument", e);
741            return msg;
742        }
743    }
744
745    int index = 0;
746
747    /*
748     * Used by two column format. Local moves (pulls and spots) are lined up
749     * when using this format,
750     */
751    private String appendSetoutString(String s, List<Car> carList, RouteLocation rl, boolean local, boolean isManifest,
752            boolean isTwoColumnTrack) {
753        while (index < carList.size()) {
754            Car car = carList.get(index++);
755            if (local && car.isLocalMove()) {
756                continue; // skip local moves
757            }
758            // car list is already sorted by destination track
759            if (car.getRouteDestination() == rl) {
760                String so = appendSetoutString(s, carList, rl, car, isManifest, isTwoColumnTrack);
761                // check for utility car
762                if (!so.equals(s)) {
763                    return so;
764                }
765            }
766        }
767        // no set out for this line
768        return s + VERTICAL_LINE_CHAR + padAndTruncate("", getLineLength(isManifest) / 2 - 1);
769    }
770
771    /*
772     * Used by two column, track names shown in the columns.
773     */
774    private String appendSetoutString(String s, String trackName, List<Car> carList, RouteLocation rl,
775            boolean isManifest, boolean isTwoColumnTrack) {
776        for (Car car : carList) {
777            if (!doneCars.contains(car) &&
778                    car.getRouteDestination() == rl &&
779                    trackName.equals(car.getSplitDestinationTrackName())) {
780                doneCars.add(car);
781                String so = appendSetoutString(s, carList, rl, car, isManifest, isTwoColumnTrack);
782                // check for utility car
783                if (!so.equals(s)) {
784                    return so;
785                }
786            }
787        }
788        // no set out for this track
789        return s + VERTICAL_LINE_CHAR + padAndTruncate("", getLineLength(isManifest) / 2 - 1);
790    }
791
792    /*
793     * Appends to string the vertical line character, and the car set out
794     * string. Used in two column format.
795     */
796    private String appendSetoutString(String s, List<Car> carList, RouteLocation rl, Car car, boolean isManifest,
797            boolean isTwoColumnTrack) {
798        _dropCars = true;
799        String dropText;
800
801        if (car.isUtility()) {
802            dropText = setoutUtilityCars(carList, car, !LOCAL, isManifest, isTwoColumnTrack);
803            if (dropText == null) {
804                return s; // no changes to the input string
805            }
806        } else {
807            dropText = dropCar(car, isManifest, isTwoColumnTrack).trim();
808        }
809
810        dropText = padAndTruncate(dropText.trim(), getLineLength(isManifest) / 2 - 1);
811        dropText = formatColorString(dropText, car.isLocalMove() ? Setup.getLocalColor() : Setup.getDropColor());
812        return s + VERTICAL_LINE_CHAR + dropText;
813    }
814
815    /**
816     * Adds the car's pick up string to the output file using the truncated
817     * manifest format
818     *
819     * @param file       Manifest or switch list File
820     * @param car        The car being printed.
821     * @param isManifest True if manifest, false if switch list.
822     */
823    protected void pickUpCarTruncated(PrintWriter file, Car car, boolean isManifest) {
824        pickUpCar(file, car,
825                new StringBuffer(padAndTruncateIfNeeded(Setup.getPickupCarPrefix(), Setup.getManifestPrefixLength())),
826                Setup.getPickupTruncatedManifestMessageFormat(), isManifest);
827    }
828
829    /**
830     * Adds the car's pick up string to the output file using the manifest or
831     * switch list format
832     *
833     * @param file       Manifest or switch list File
834     * @param car        The car being printed.
835     * @param isManifest True if manifest, false if switch list.
836     */
837    protected void pickUpCar(PrintWriter file, Car car, boolean isManifest) {
838        if (isManifest) {
839            pickUpCar(file, car,
840                    new StringBuffer(
841                            padAndTruncateIfNeeded(Setup.getPickupCarPrefix(), Setup.getManifestPrefixLength())),
842                    Setup.getPickupManifestMessageFormat(), isManifest);
843        } else {
844            pickUpCar(file, car, new StringBuffer(
845                    padAndTruncateIfNeeded(Setup.getSwitchListPickupCarPrefix(), Setup.getSwitchListPrefixLength())),
846                    Setup.getPickupSwitchListMessageFormat(), isManifest);
847        }
848    }
849
850    private void pickUpCar(PrintWriter file, Car car, StringBuffer buf, String[] format, boolean isManifest) {
851        if (car.isLocalMove()) {
852            return; // print nothing local move, see dropCar
853        }
854        for (String attribute : format) {
855            String s = getCarAttribute(car, attribute, PICKUP, !LOCAL);
856            if (!checkStringLength(buf.toString() + s, isManifest)) {
857                addLine(file, buf, Setup.getPickupColor());
858                buf = new StringBuffer(TAB); // new line
859            }
860            buf.append(s);
861        }
862        addLine(file, buf, Setup.getPickupColor());
863    }
864
865    /**
866     * Returns the pick up car string. Useful for frames like train conductor
867     * and yardmaster.
868     *
869     * @param car              The car being printed.
870     * @param isManifest       when true use manifest format, when false use
871     *                         switch list format
872     * @param isTwoColumnTrack True if printing using two column format sorted
873     *                         by track name.
874     * @return pick up car string
875     */
876    public String pickupCar(Car car, boolean isManifest, boolean isTwoColumnTrack) {
877        StringBuffer buf = new StringBuffer();
878        String[] format;
879        if (isManifest && !isTwoColumnTrack) {
880            format = Setup.getPickupManifestMessageFormat();
881        } else if (!isManifest && !isTwoColumnTrack) {
882            format = Setup.getPickupSwitchListMessageFormat();
883        } else if (isManifest && isTwoColumnTrack) {
884            format = Setup.getPickupTwoColumnByTrackManifestMessageFormat();
885        } else {
886            format = Setup.getPickupTwoColumnByTrackSwitchListMessageFormat();
887        }
888        for (String attribute : format) {
889            buf.append(getCarAttribute(car, attribute, PICKUP, !LOCAL));
890        }
891        return buf.toString();
892    }
893
894    /**
895     * Adds the car's set out string to the output file using the truncated
896     * manifest format. Does not print out local moves. Local moves are only
897     * shown on the switch list for that location.
898     *
899     * @param file       Manifest or switch list File
900     * @param car        The car being printed.
901     * @param isManifest True if manifest, false if switch list.
902     */
903    protected void truncatedDropCar(PrintWriter file, Car car, boolean isManifest) {
904        // local move?
905        if (car.isLocalMove()) {
906            return; // yes, don't print local moves on train manifest
907        }
908        dropCar(file, car, new StringBuffer(Setup.getDropCarPrefix()), Setup.getDropTruncatedManifestMessageFormat(),
909                false, isManifest);
910    }
911
912    /**
913     * Adds the car's set out string to the output file using the manifest or
914     * switch list format
915     *
916     * @param file       Manifest or switch list File
917     * @param car        The car being printed.
918     * @param isManifest True if manifest, false if switch list.
919     */
920    protected void dropCar(PrintWriter file, Car car, boolean isManifest) {
921        boolean isLocal = car.isLocalMove();
922        if (isManifest) {
923            StringBuffer buf = new StringBuffer(
924                    padAndTruncateIfNeeded(Setup.getDropCarPrefix(), Setup.getManifestPrefixLength()));
925            String[] format = Setup.getDropManifestMessageFormat();
926            if (isLocal) {
927                buf = new StringBuffer(padAndTruncateIfNeeded(Setup.getLocalPrefix(), Setup.getManifestPrefixLength()));
928                format = Setup.getLocalManifestMessageFormat();
929            }
930            dropCar(file, car, buf, format, isLocal, isManifest);
931        } else {
932            StringBuffer buf = new StringBuffer(
933                    padAndTruncateIfNeeded(Setup.getSwitchListDropCarPrefix(), Setup.getSwitchListPrefixLength()));
934            String[] format = Setup.getDropSwitchListMessageFormat();
935            if (isLocal) {
936                buf = new StringBuffer(
937                        padAndTruncateIfNeeded(Setup.getSwitchListLocalPrefix(), Setup.getSwitchListPrefixLength()));
938                format = Setup.getLocalSwitchListMessageFormat();
939            }
940            dropCar(file, car, buf, format, isLocal, isManifest);
941        }
942    }
943
944    private void dropCar(PrintWriter file, Car car, StringBuffer buf, String[] format, boolean isLocal,
945            boolean isManifest) {
946        for (String attribute : format) {
947            String s = getCarAttribute(car, attribute, !PICKUP, isLocal);
948            if (!checkStringLength(buf.toString() + s, isManifest)) {
949                addLine(file, buf, isLocal ? Setup.getLocalColor() : Setup.getDropColor());
950                buf = new StringBuffer(TAB); // new line
951            }
952            buf.append(s);
953        }
954        addLine(file, buf, isLocal ? Setup.getLocalColor() : Setup.getDropColor());
955    }
956
957    /**
958     * Returns the drop car string. Useful for frames like train conductor and
959     * yardmaster.
960     *
961     * @param car              The car being printed.
962     * @param isManifest       when true use manifest format, when false use
963     *                         switch list format
964     * @param isTwoColumnTrack True if printing using two column format.
965     * @return drop car string
966     */
967    public String dropCar(Car car, boolean isManifest, boolean isTwoColumnTrack) {
968        StringBuffer buf = new StringBuffer();
969        String[] format;
970        if (isManifest && !isTwoColumnTrack) {
971            format = Setup.getDropManifestMessageFormat();
972        } else if (!isManifest && !isTwoColumnTrack) {
973            format = Setup.getDropSwitchListMessageFormat();
974        } else if (isManifest && isTwoColumnTrack) {
975            format = Setup.getDropTwoColumnByTrackManifestMessageFormat();
976        } else {
977            format = Setup.getDropTwoColumnByTrackSwitchListMessageFormat();
978        }
979        // TODO the Setup.Location doesn't work correctly for the conductor
980        // window due to the fact that the car can be in the train and not
981        // at its starting location.
982        // Therefore we use the local true to disable it.
983        boolean local = false;
984        if (car.getTrack() == null) {
985            local = true;
986        }
987        for (String attribute : format) {
988            buf.append(getCarAttribute(car, attribute, !PICKUP, local));
989        }
990        return buf.toString();
991    }
992
993    /**
994     * Returns the move car string. Useful for frames like train conductor and
995     * yardmaster.
996     *
997     * @param car        The car being printed.
998     * @param isManifest when true use manifest format, when false use switch
999     *                   list format
1000     * @return move car string
1001     */
1002    public String localMoveCar(Car car, boolean isManifest) {
1003        StringBuffer buf = new StringBuffer();
1004        String[] format;
1005        if (isManifest) {
1006            format = Setup.getLocalManifestMessageFormat();
1007        } else {
1008            format = Setup.getLocalSwitchListMessageFormat();
1009        }
1010        for (String attribute : format) {
1011            buf.append(getCarAttribute(car, attribute, !PICKUP, LOCAL));
1012        }
1013        return buf.toString();
1014    }
1015
1016    List<String> utilityCarTypes = new ArrayList<>();
1017    private static final int UTILITY_CAR_COUNT_FIELD_SIZE = 3;
1018
1019    /**
1020     * Add a list of utility cars scheduled for pick up from the route location
1021     * to the output file. The cars are blocked by destination.
1022     *
1023     * @param file       Manifest or Switch List File.
1024     * @param carList    List of cars for this train.
1025     * @param car        The utility car.
1026     * @param isTruncate True if manifest is to be truncated
1027     * @param isManifest True if manifest, false if switch list.
1028     */
1029    protected void pickupUtilityCars(PrintWriter file, List<Car> carList, Car car, boolean isTruncate,
1030            boolean isManifest) {
1031        // list utility cars by type, track, length, and load
1032        String[] format;
1033        if (isManifest) {
1034            format = Setup.getPickupUtilityManifestMessageFormat();
1035        } else {
1036            format = Setup.getPickupUtilitySwitchListMessageFormat();
1037        }
1038        if (isTruncate && isManifest) {
1039            format = Setup.createTruncatedManifestMessageFormat(format);
1040        }
1041        int count = countUtilityCars(format, carList, car, PICKUP);
1042        if (count == 0) {
1043            return; // already printed out this car type
1044        }
1045        pickUpCar(file, car,
1046                new StringBuffer(padAndTruncateIfNeeded(Setup.getPickupCarPrefix(),
1047                        isManifest ? Setup.getManifestPrefixLength() : Setup.getSwitchListPrefixLength()) +
1048                        SPACE +
1049                        padString(Integer.toString(count), UTILITY_CAR_COUNT_FIELD_SIZE)),
1050                format, isManifest);
1051    }
1052
1053    /**
1054     * Add a list of utility cars scheduled for drop at the route location to
1055     * the output file.
1056     *
1057     * @param file       Manifest or Switch List File.
1058     * @param carList    List of cars for this train.
1059     * @param car        The utility car.
1060     * @param isTruncate True if manifest is to be truncated
1061     * @param isManifest True if manifest, false if switch list.
1062     */
1063    protected void setoutUtilityCars(PrintWriter file, List<Car> carList, Car car, boolean isTruncate,
1064            boolean isManifest) {
1065        boolean isLocal = car.isLocalMove();
1066        StringBuffer buf;
1067        String[] format;
1068        if (isLocal && isManifest) {
1069            buf = new StringBuffer(padAndTruncateIfNeeded(Setup.getLocalPrefix(), Setup.getManifestPrefixLength()));
1070            format = Setup.getLocalUtilityManifestMessageFormat();
1071        } else if (!isLocal && isManifest) {
1072            buf = new StringBuffer(padAndTruncateIfNeeded(Setup.getDropCarPrefix(), Setup.getManifestPrefixLength()));
1073            format = Setup.getDropUtilityManifestMessageFormat();
1074        } else if (isLocal && !isManifest) {
1075            buf = new StringBuffer(
1076                    padAndTruncateIfNeeded(Setup.getSwitchListLocalPrefix(), Setup.getSwitchListPrefixLength()));
1077            format = Setup.getLocalUtilitySwitchListMessageFormat();
1078        } else {
1079            buf = new StringBuffer(
1080                    padAndTruncateIfNeeded(Setup.getSwitchListDropCarPrefix(), Setup.getSwitchListPrefixLength()));
1081            format = Setup.getDropUtilitySwitchListMessageFormat();
1082        }
1083        if (isTruncate && isManifest) {
1084            format = Setup.createTruncatedManifestMessageFormat(format);
1085        }
1086
1087        int count = countUtilityCars(format, carList, car, !PICKUP);
1088        if (count == 0) {
1089            return; // already printed out this car type
1090        }
1091        buf.append(SPACE + padString(Integer.toString(count), UTILITY_CAR_COUNT_FIELD_SIZE));
1092        dropCar(file, car, buf, format, isLocal, isManifest);
1093    }
1094
1095    public String pickupUtilityCars(List<Car> carList, Car car, boolean isManifest, boolean isTwoColumnTrack) {
1096        int count = countPickupUtilityCars(carList, car, isManifest);
1097        if (count == 0) {
1098            return null;
1099        }
1100        String[] format;
1101        if (isManifest && !isTwoColumnTrack) {
1102            format = Setup.getPickupUtilityManifestMessageFormat();
1103        } else if (!isManifest && !isTwoColumnTrack) {
1104            format = Setup.getPickupUtilitySwitchListMessageFormat();
1105        } else if (isManifest && isTwoColumnTrack) {
1106            format = Setup.getPickupTwoColumnByTrackUtilityManifestMessageFormat();
1107        } else {
1108            format = Setup.getPickupTwoColumnByTrackUtilitySwitchListMessageFormat();
1109        }
1110        StringBuffer buf = new StringBuffer(SPACE + padString(Integer.toString(count), UTILITY_CAR_COUNT_FIELD_SIZE));
1111        for (String attribute : format) {
1112            buf.append(getCarAttribute(car, attribute, PICKUP, !LOCAL));
1113        }
1114        return buf.toString();
1115    }
1116
1117    public int countPickupUtilityCars(List<Car> carList, Car car, boolean isManifest) {
1118        // list utility cars by type, track, length, and load
1119        String[] format;
1120        if (isManifest) {
1121            format = Setup.getPickupUtilityManifestMessageFormat();
1122        } else {
1123            format = Setup.getPickupUtilitySwitchListMessageFormat();
1124        }
1125        return countUtilityCars(format, carList, car, PICKUP);
1126    }
1127
1128    /**
1129     * For the Conductor and Yardmaster windows.
1130     *
1131     * @param carList    List of cars for this train.
1132     * @param car        The utility car.
1133     * @param isLocal    True if local move.
1134     * @param isManifest True if manifest, false if switch list.
1135     * @return A string representing the work of identical utility cars.
1136     */
1137    public String setoutUtilityCars(List<Car> carList, Car car, boolean isLocal, boolean isManifest) {
1138        return setoutUtilityCars(carList, car, isLocal, isManifest, !IS_TWO_COLUMN_TRACK);
1139    }
1140
1141    protected String setoutUtilityCars(List<Car> carList, Car car, boolean isLocal, boolean isManifest,
1142            boolean isTwoColumnTrack) {
1143        int count = countSetoutUtilityCars(carList, car, isLocal, isManifest);
1144        if (count == 0) {
1145            return null;
1146        }
1147        // list utility cars by type, track, length, and load
1148        String[] format;
1149        if (isLocal && isManifest && !isTwoColumnTrack) {
1150            format = Setup.getLocalUtilityManifestMessageFormat();
1151        } else if (isLocal && !isManifest && !isTwoColumnTrack) {
1152            format = Setup.getLocalUtilitySwitchListMessageFormat();
1153        } else if (!isLocal && !isManifest && !isTwoColumnTrack) {
1154            format = Setup.getDropUtilitySwitchListMessageFormat();
1155        } else if (!isLocal && isManifest && !isTwoColumnTrack) {
1156            format = Setup.getDropUtilityManifestMessageFormat();
1157        } else if (isManifest && isTwoColumnTrack) {
1158            format = Setup.getDropTwoColumnByTrackUtilityManifestMessageFormat();
1159        } else {
1160            format = Setup.getDropTwoColumnByTrackUtilitySwitchListMessageFormat();
1161        }
1162        StringBuffer buf = new StringBuffer(SPACE + padString(Integer.toString(count), UTILITY_CAR_COUNT_FIELD_SIZE));
1163        // TODO the Setup.Location doesn't work correctly for the conductor
1164        // window due to the fact that the car can be in the train and not
1165        // at its starting location.
1166        // Therefore we use the local true to disable it.
1167        if (car.getTrack() == null) {
1168            isLocal = true;
1169        }
1170        for (String attribute : format) {
1171            buf.append(getCarAttribute(car, attribute, !PICKUP, isLocal));
1172        }
1173        return buf.toString();
1174    }
1175
1176    public int countSetoutUtilityCars(List<Car> carList, Car car, boolean isLocal, boolean isManifest) {
1177        // list utility cars by type, track, length, and load
1178        String[] format;
1179        if (isLocal && isManifest) {
1180            format = Setup.getLocalUtilityManifestMessageFormat();
1181        } else if (isLocal && !isManifest) {
1182            format = Setup.getLocalUtilitySwitchListMessageFormat();
1183        } else if (!isLocal && !isManifest) {
1184            format = Setup.getDropUtilitySwitchListMessageFormat();
1185        } else {
1186            format = Setup.getDropUtilityManifestMessageFormat();
1187        }
1188        return countUtilityCars(format, carList, car, !PICKUP);
1189    }
1190
1191    /**
1192     * Scans the car list for utility cars that have the same attributes as the
1193     * car provided. Returns 0 if this car type has already been processed,
1194     * otherwise the number of cars with the same attribute.
1195     *
1196     * @param format   Message format.
1197     * @param carList  List of cars for this train
1198     * @param car      The utility car.
1199     * @param isPickup True if pick up, false if set out.
1200     * @return 0 if the car type has already been processed
1201     */
1202    protected int countUtilityCars(String[] format, List<Car> carList, Car car, boolean isPickup) {
1203        int count = 0;
1204        // figure out if the user wants to show the car's length
1205        boolean showLength = showUtilityCarLength(format);
1206        // figure out if the user want to show the car's loads
1207        boolean showLoad = showUtilityCarLoad(format);
1208        boolean showLocation = false;
1209        boolean showDestination = false;
1210        String carType = car.getTypeName().split(HYPHEN)[0];
1211        String carAttributes;
1212        // Note for car pick up: type, id, track name. For set out type, track
1213        // name, id (reversed).
1214        if (isPickup) {
1215            carAttributes = carType + car.getRouteLocationId() + car.getSplitTrackName();
1216            showDestination = showUtilityCarDestination(format);
1217            if (showDestination) {
1218                carAttributes = carAttributes + car.getRouteDestinationId();
1219            }
1220        } else {
1221            // set outs and local moves
1222            carAttributes = carType + car.getSplitDestinationTrackName() + car.getRouteDestinationId();
1223            showLocation = showUtilityCarLocation(format);
1224            if (showLocation && car.getTrack() != null) {
1225                carAttributes = carAttributes + car.getRouteLocationId();
1226            }
1227        }
1228        if (car.isLocalMove()) {
1229            carAttributes = carAttributes + car.getSplitTrackName();
1230        }
1231        if (showLength) {
1232            carAttributes = carAttributes + car.getLength();
1233        }
1234        if (showLoad) {
1235            carAttributes = carAttributes + car.getLoadName();
1236        }
1237        // have we already done this car type?
1238        if (!utilityCarTypes.contains(carAttributes)) {
1239            utilityCarTypes.add(carAttributes); // don't do this type again
1240            // determine how many cars of this type
1241            for (Car c : carList) {
1242                if (!c.isUtility()) {
1243                    continue;
1244                }
1245                String cType = c.getTypeName().split(HYPHEN)[0];
1246                if (!cType.equals(carType)) {
1247                    continue;
1248                }
1249                if (showLength && !c.getLength().equals(car.getLength())) {
1250                    continue;
1251                }
1252                if (showLoad && !c.getLoadName().equals(car.getLoadName())) {
1253                    continue;
1254                }
1255                if (showLocation && !c.getRouteLocationId().equals(car.getRouteLocationId())) {
1256                    continue;
1257                }
1258                if (showDestination && !c.getRouteDestinationId().equals(car.getRouteDestinationId())) {
1259                    continue;
1260                }
1261                if (car.isLocalMove() ^ c.isLocalMove()) {
1262                    continue;
1263                }
1264                if (isPickup &&
1265                        c.getRouteLocation() == car.getRouteLocation() &&
1266                        c.getSplitTrackName().equals(car.getSplitTrackName())) {
1267                    count++;
1268                }
1269                if (!isPickup &&
1270                        c.getRouteDestination() == car.getRouteDestination() &&
1271                        c.getSplitDestinationTrackName().equals(car.getSplitDestinationTrackName()) &&
1272                        (c.getSplitTrackName().equals(car.getSplitTrackName()) || !c.isLocalMove())) {
1273                    count++;
1274                }
1275            }
1276        }
1277        return count;
1278    }
1279
1280    public void clearUtilityCarTypes() {
1281        utilityCarTypes.clear();
1282    }
1283
1284    private boolean showUtilityCarLength(String[] mFormat) {
1285        return showUtilityCarAttribute(Setup.LENGTH, mFormat);
1286    }
1287
1288    private boolean showUtilityCarLoad(String[] mFormat) {
1289        return showUtilityCarAttribute(Setup.LOAD, mFormat);
1290    }
1291
1292    private boolean showUtilityCarLocation(String[] mFormat) {
1293        return showUtilityCarAttribute(Setup.LOCATION, mFormat);
1294    }
1295
1296    private boolean showUtilityCarDestination(String[] mFormat) {
1297        return showUtilityCarAttribute(Setup.DESTINATION, mFormat) ||
1298                showUtilityCarAttribute(Setup.DEST_TRACK, mFormat);
1299    }
1300
1301    private boolean showUtilityCarAttribute(String string, String[] mFormat) {
1302        for (String s : mFormat) {
1303            if (s.equals(string)) {
1304                return true;
1305            }
1306        }
1307        return false;
1308    }
1309
1310    /**
1311     * Writes a line to the build report file
1312     *
1313     * @param file   build report file
1314     * @param level  print level
1315     * @param string string to write
1316     */
1317    public static void addLine(PrintWriter file, String level, String string) {
1318        log.debug("addLine: {}", string);
1319        if (file != null) {
1320            String[] lines = string.split(NEW_LINE);
1321            for (String line : lines) {
1322                printLine(file, level, line);
1323            }
1324        }
1325    }
1326
1327    // only used by build report
1328    private static void printLine(PrintWriter file, String level, String string) {
1329        int lineLengthMax = getLineLength(Setup.PORTRAIT, Setup.MONOSPACED, Font.PLAIN, Setup.getBuildReportFontSize());
1330        if (string.length() > lineLengthMax) {
1331            String[] words = string.split(SPACE);
1332            StringBuffer sb = new StringBuffer();
1333            for (String word : words) {
1334                if (sb.length() + word.length() < lineLengthMax) {
1335                    sb.append(word + SPACE);
1336                } else {
1337                    file.println(level + BUILD_REPORT_CHAR + SPACE + sb.toString());
1338                    sb = new StringBuffer(word + SPACE);
1339                }
1340            }
1341            string = sb.toString();
1342        }
1343        file.println(level + BUILD_REPORT_CHAR + SPACE + string);
1344    }
1345
1346    /**
1347     * Writes string to file. No line length wrap or protection.
1348     *
1349     * @param file   The File to write to.
1350     * @param string The string to write.
1351     */
1352    protected void addLine(PrintWriter file, String string) {
1353        log.debug("addLine: {}", string);
1354        if (file != null) {
1355            file.println(string);
1356        }
1357    }
1358
1359    /**
1360     * Writes a string to a file. Checks for string length, and will
1361     * automatically wrap lines.
1362     *
1363     * @param file       The File to write to.
1364     * @param string     The string to write.
1365     * @param isManifest set true for manifest page orientation, false for
1366     *                   switch list orientation
1367     */
1368    protected void newLine(PrintWriter file, String string, boolean isManifest) {
1369        String[] lines = string.split(NEW_LINE);
1370        for (String line : lines) {
1371            String[] words = line.split(SPACE);
1372            StringBuffer sb = new StringBuffer();
1373            for (String word : words) {
1374                if (checkStringLength(sb.toString() + word, isManifest)) {
1375                    sb.append(word + SPACE);
1376                } else {
1377                    sb.setLength(sb.length() - 1); // remove last space added to string
1378                    addLine(file, sb.toString());
1379                    sb = new StringBuffer(word + SPACE);
1380                }
1381            }
1382            if (sb.length() > 0) {
1383                sb.setLength(sb.length() - 1); // remove last space added to string
1384            }
1385            addLine(file, sb.toString());
1386        }
1387    }
1388
1389    /**
1390     * Adds a blank line to the file.
1391     *
1392     * @param file The File to write to.
1393     */
1394    protected void newLine(PrintWriter file) {
1395        file.println(BLANK_LINE);
1396    }
1397
1398    /**
1399     * Splits a string (example-number) as long as the second part of the string
1400     * is an integer or if the first character after the hyphen is a left
1401     * parenthesis "(".
1402     *
1403     * @param name The string to split if necessary.
1404     * @return First half of the string.
1405     */
1406    public static String splitString(String name) {
1407        String[] splitname = name.split(HYPHEN);
1408        // is the hyphen followed by a number or left parenthesis?
1409        if (splitname.length > 1 && !splitname[1].startsWith("(")) {
1410            try {
1411                Integer.parseInt(splitname[1]);
1412            } catch (NumberFormatException e) {
1413                // no return full name
1414                return name.trim();
1415            }
1416        }
1417        return splitname[0].trim();
1418    }
1419
1420    /**
1421     * Splits a string if there's a hyphen followed by a left parenthesis "-(".
1422     *
1423     * @return First half of the string.
1424     */
1425    private static String splitStringLeftParenthesis(String name) {
1426        String[] splitname = name.split(HYPHEN);
1427        if (splitname.length > 1 && splitname[1].startsWith("(")) {
1428            return splitname[0].trim();
1429        }
1430        return name.trim();
1431    }
1432
1433    // returns true if there's work at location
1434    protected boolean isThereWorkAtLocation(List<Car> carList, List<Engine> engList, RouteLocation rl) {
1435        if (carList != null) {
1436            for (Car car : carList) {
1437                if (car.getRouteLocation() == rl || car.getRouteDestination() == rl) {
1438                    return true;
1439                }
1440            }
1441        }
1442        if (engList != null) {
1443            for (Engine eng : engList) {
1444                if (eng.getRouteLocation() == rl || eng.getRouteDestination() == rl) {
1445                    return true;
1446                }
1447            }
1448        }
1449        return false;
1450    }
1451
1452    /**
1453     * returns true if the train has work at the location
1454     *
1455     * @param train    The Train.
1456     * @param location The Location.
1457     * @return true if the train has work at the location
1458     */
1459    public static boolean isThereWorkAtLocation(Train train, Location location) {
1460        if (isThereWorkAtLocation(train, location, InstanceManager.getDefault(CarManager.class).getList(train))) {
1461            return true;
1462        }
1463        if (isThereWorkAtLocation(train, location, InstanceManager.getDefault(EngineManager.class).getList(train))) {
1464            return true;
1465        }
1466        return false;
1467    }
1468
1469    private static boolean isThereWorkAtLocation(Train train, Location location, List<? extends RollingStock> list) {
1470        for (RollingStock rs : list) {
1471            if ((rs.getRouteLocation() != null &&
1472                    rs.getTrack() != null &&
1473                    rs.getRouteLocation().getSplitName()
1474                            .equals(location.getSplitName())) ||
1475                    (rs.getRouteDestination() != null &&
1476                            rs.getRouteDestination().getSplitName().equals(location.getSplitName()))) {
1477                return true;
1478            }
1479        }
1480        return false;
1481    }
1482
1483    protected void addCarsLocationUnknown(PrintWriter file, boolean isManifest) {
1484        List<Car> cars = carManager.getCarsLocationUnknown();
1485        if (cars.size() == 0) {
1486            return; // no cars to search for!
1487        }
1488        newLine(file);
1489        newLine(file, Setup.getMiaComment(), isManifest);
1490        if (Setup.isPrintHeadersEnabled()) {
1491            printHorizontalLine(file, isManifest);
1492            newLine(file, SPACE + getHeader(Setup.getMissingCarMessageFormat(), false, false, false), isManifest);
1493            printHorizontalLine(file, isManifest);
1494        }
1495        for (Car car : cars) {
1496            addSearchForCar(file, car, isManifest);
1497        }
1498    }
1499
1500    private void addSearchForCar(PrintWriter file, Car car, boolean isManifest) {
1501        StringBuffer buf = new StringBuffer();
1502        String[] format = Setup.getMissingCarMessageFormat();
1503        for (String attribute : format) {
1504            buf.append(getCarAttribute(car, attribute, false, false));
1505        }
1506        newLine(file, buf.toString(), isManifest);
1507    }
1508
1509    /*
1510     * Gets an engine's attribute String. Returns empty if there isn't an
1511     * attribute and not using the tabular feature. isPickup true when engine is
1512     * being picked up.
1513     */
1514    private String getEngineAttribute(Engine engine, String attribute, boolean isPickup) {
1515        if (!attribute.equals(Setup.BLANK)) {
1516            String s = SPACE + getEngineAttrib(engine, attribute, isPickup);
1517            if (Setup.isTabEnabled() || !s.trim().isEmpty()) {
1518                return s;
1519            }
1520        }
1521        return "";
1522    }
1523
1524    /*
1525     * Can not use String case statement since Setup.MODEL, etc, are not fixed
1526     * strings.
1527     */
1528    private String getEngineAttrib(Engine engine, String attribute, boolean isPickup) {
1529        if (attribute.equals(Setup.MODEL)) {
1530            return padAndTruncateIfNeeded(splitStringLeftParenthesis(engine.getModel()),
1531                    InstanceManager.getDefault(EngineModels.class).getMaxNameLength());
1532        } else if (attribute.equals(Setup.HP)) {
1533            return padAndTruncateIfNeeded(engine.getHp(), 5) +
1534                    (Setup.isPrintHeadersEnabled() ? "" : TrainManifestHeaderText.getStringHeader_Hp());
1535        } else if (attribute.equals(Setup.CONSIST)) {
1536            return padAndTruncateIfNeeded(engine.getConsistName(),
1537                    InstanceManager.getDefault(ConsistManager.class).getMaxNameLength());
1538        } else if (attribute.equals(Setup.DCC_ADDRESS)) {
1539            return padAndTruncateIfNeeded(engine.getDccAddress(),
1540                    TrainManifestHeaderText.getStringHeader_DCC_Address().length());
1541        } else if (attribute.equals(Setup.COMMENT)) {
1542            return padAndTruncateIfNeeded(engine.getComment(), engineManager.getMaxCommentLength());
1543        }
1544        return getRollingStockAttribute(engine, attribute, isPickup, false);
1545    }
1546
1547    /*
1548     * Gets a car's attribute String. Returns empty if there isn't an attribute
1549     * and not using the tabular feature. isPickup true when car is being picked
1550     * up. isLocal true when car is performing a local move.
1551     */
1552    private String getCarAttribute(Car car, String attribute, boolean isPickup, boolean isLocal) {
1553        if (!attribute.equals(Setup.BLANK)) {
1554            String s = SPACE + getCarAttrib(car, attribute, isPickup, isLocal);
1555            if (Setup.isTabEnabled() || !s.trim().isEmpty()) {
1556                return s;
1557            }
1558        }
1559        return "";
1560    }
1561
1562    private String getCarAttrib(Car car, String attribute, boolean isPickup, boolean isLocal) {
1563        if (attribute.equals(Setup.LOAD)) {
1564            return ((car.isCaboose() && !Setup.isPrintCabooseLoadEnabled()) ||
1565                    (car.isPassenger() && !Setup.isPrintPassengerLoadEnabled()))
1566                            ? padAndTruncateIfNeeded("",
1567                                    InstanceManager.getDefault(CarLoads.class).getMaxNameLength())
1568                            : padAndTruncateIfNeeded(car.getLoadName().split(HYPHEN)[0],
1569                                    InstanceManager.getDefault(CarLoads.class).getMaxNameLength());
1570        } else if (attribute.equals(Setup.LOAD_TYPE)) {
1571            return padAndTruncateIfNeeded(car.getLoadType(),
1572                    TrainManifestHeaderText.getStringHeader_Load_Type().length());
1573        } else if (attribute.equals(Setup.HAZARDOUS)) {
1574            return (car.isHazardous() ? Setup.getHazardousMsg()
1575                    : padAndTruncateIfNeeded("", Setup.getHazardousMsg().length()));
1576        } else if (attribute.equals(Setup.DROP_COMMENT)) {
1577            return padAndTruncateIfNeeded(car.getDropComment(),
1578                    InstanceManager.getDefault(CarLoads.class).getMaxLoadCommentLength());
1579        } else if (attribute.equals(Setup.PICKUP_COMMENT)) {
1580            return padAndTruncateIfNeeded(car.getPickupComment(),
1581                    InstanceManager.getDefault(CarLoads.class).getMaxLoadCommentLength());
1582        } else if (attribute.equals(Setup.KERNEL)) {
1583            return padAndTruncateIfNeeded(car.getKernelName(),
1584                    InstanceManager.getDefault(KernelManager.class).getMaxNameLength());
1585        } else if (attribute.equals(Setup.KERNEL_SIZE)) {
1586            if (car.isLead()) {
1587                return padAndTruncateIfNeeded(Integer.toString(car.getKernel().getSize()), 2);
1588            }
1589            return SPACE + SPACE; // assumes that kernel size is 99 or less
1590        } else if (attribute.equals(Setup.RWE)) {
1591            if (!car.getReturnWhenEmptyDestinationName().equals(Car.NONE)) {
1592                // format RWE destination and track name
1593                String rweAndTrackName = car.getSplitReturnWhenEmptyDestinationName();
1594                if (!car.getReturnWhenEmptyDestTrackName().equals(Car.NONE)) {
1595                    rweAndTrackName = rweAndTrackName + "," + SPACE + car.getSplitReturnWhenEmptyDestinationTrackName();
1596                }
1597                return Setup.isPrintHeadersEnabled()
1598                        ? padAndTruncateIfNeeded(rweAndTrackName, locationManager.getMaxLocationAndTrackNameLength())
1599                        : padAndTruncateIfNeeded(
1600                                TrainManifestHeaderText.getStringHeader_RWE() + SPACE + rweAndTrackName,
1601                                locationManager.getMaxLocationAndTrackNameLength() +
1602                                        TrainManifestHeaderText.getStringHeader_RWE().length() +
1603                                        3);
1604            }
1605            return padAndTruncateIfNeeded("", locationManager.getMaxLocationAndTrackNameLength());
1606        } else if (attribute.equals(Setup.FINAL_DEST)) {
1607            return Setup.isPrintHeadersEnabled()
1608                    ? padAndTruncateIfNeeded(car.getSplitFinalDestinationName(),
1609                            locationManager.getMaxLocationNameLength())
1610                    : padAndTruncateIfNeeded(
1611                            TrainManifestText.getStringFinalDestination() +
1612                                    SPACE +
1613                                    car.getSplitFinalDestinationName(),
1614                            locationManager.getMaxLocationNameLength() +
1615                                    TrainManifestText.getStringFinalDestination().length() +
1616                                    1);
1617        } else if (attribute.equals(Setup.FINAL_DEST_TRACK)) {
1618            // format final destination and track name
1619            String FDAndTrackName = car.getSplitFinalDestinationName();
1620            if (!car.getFinalDestinationTrackName().equals(Car.NONE)) {
1621                FDAndTrackName = FDAndTrackName + "," + SPACE + car.getSplitFinalDestinationTrackName();
1622            }
1623            return Setup.isPrintHeadersEnabled()
1624                    ? padAndTruncateIfNeeded(FDAndTrackName, locationManager.getMaxLocationAndTrackNameLength() + 2)
1625                    : padAndTruncateIfNeeded(TrainManifestText.getStringFinalDestination() + SPACE + FDAndTrackName,
1626                            locationManager.getMaxLocationAndTrackNameLength() +
1627                                    TrainManifestText.getStringFinalDestination().length() +
1628                                    3);
1629        } else if (attribute.equals(Setup.DIVISION)) {
1630            return padAndTruncateIfNeeded(car.getDivisionName(),
1631                    InstanceManager.getDefault(DivisionManager.class).getMaxDivisionNameLength());
1632        } else if (attribute.equals(Setup.BLOCKING_ORDER)) {
1633            if (car.isPassenger()) {
1634                return padAndTruncateIfNeeded(Integer.toString(car.getBlocking()), 3);
1635            }
1636            return SPACE + SPACE + SPACE; // assumes that blocking order is +/- 99
1637        } else if (attribute.equals(Setup.COMMENT)) {
1638            return padAndTruncateIfNeeded(car.getComment(), carManager.getMaxCommentLength());
1639        }
1640        return getRollingStockAttribute(car, attribute, isPickup, isLocal);
1641    }
1642
1643    private String getRollingStockAttribute(RollingStock rs, String attribute, boolean isPickup, boolean isLocal) {
1644        try {
1645            if (attribute.equals(Setup.NUMBER)) {
1646                return padAndTruncateIfNeeded(splitString(rs.getNumber()), Control.max_len_string_print_road_number);
1647            } else if (attribute.equals(Setup.ROAD)) {
1648                String road = rs.getRoadName().split(HYPHEN)[0];
1649                return padAndTruncateIfNeeded(road, InstanceManager.getDefault(CarRoads.class).getMaxNameLength());
1650            } else if (attribute.equals(Setup.TYPE)) {
1651                String type = rs.getTypeName().split(HYPHEN)[0];
1652                return padAndTruncateIfNeeded(type, InstanceManager.getDefault(CarTypes.class).getMaxNameLength());
1653            } else if (attribute.equals(Setup.LENGTH)) {
1654                return padAndTruncateIfNeeded(rs.getLength() + Setup.getLengthUnitAbv(),
1655                        InstanceManager.getDefault(CarLengths.class).getMaxNameLength());
1656            } else if (attribute.equals(Setup.WEIGHT)) {
1657                return padAndTruncateIfNeeded(Integer.toString(rs.getAdjustedWeightTons()),
1658                        Control.max_len_string_weight_name) +
1659                        (Setup.isPrintHeadersEnabled() ? "" : TrainManifestHeaderText.getStringHeader_Weight());
1660            } else if (attribute.equals(Setup.COLOR)) {
1661                return padAndTruncateIfNeeded(rs.getColor(),
1662                        InstanceManager.getDefault(CarColors.class).getMaxNameLength());
1663            } else if (((attribute.equals(Setup.LOCATION)) && (isPickup || isLocal)) ||
1664                    (attribute.equals(Setup.TRACK) && isPickup)) {
1665                return Setup.isPrintHeadersEnabled()
1666                        ? padAndTruncateIfNeeded(rs.getSplitTrackName(),
1667                                locationManager.getMaxTrackNameLength())
1668                        : padAndTruncateIfNeeded(
1669                                TrainManifestText.getStringFrom() + SPACE + rs.getSplitTrackName(),
1670                                TrainManifestText.getStringFrom().length() +
1671                                        locationManager.getMaxTrackNameLength() +
1672                                        1);
1673            } else if (attribute.equals(Setup.LOCATION) && !isPickup && !isLocal) {
1674                return Setup.isPrintHeadersEnabled()
1675                        ? padAndTruncateIfNeeded(rs.getSplitLocationName(),
1676                                locationManager.getMaxLocationNameLength())
1677                        : padAndTruncateIfNeeded(
1678                                TrainManifestText.getStringFrom() + SPACE + rs.getSplitLocationName(),
1679                                locationManager.getMaxLocationNameLength() +
1680                                        TrainManifestText.getStringFrom().length() +
1681                                        1);
1682            } else if (attribute.equals(Setup.DESTINATION) && isPickup) {
1683                if (Setup.isPrintHeadersEnabled()) {
1684                    return padAndTruncateIfNeeded(rs.getSplitDestinationName(),
1685                            locationManager.getMaxLocationNameLength());
1686                }
1687                if (Setup.isTabEnabled()) {
1688                    return padAndTruncateIfNeeded(
1689                            TrainManifestText.getStringDest() + SPACE + rs.getSplitDestinationName(),
1690                            TrainManifestText.getStringDest().length() +
1691                                    locationManager.getMaxLocationNameLength() +
1692                                    1);
1693                } else {
1694                    return TrainManifestText.getStringDestination() +
1695                            SPACE +
1696                            rs.getSplitDestinationName();
1697                }
1698            } else if ((attribute.equals(Setup.DESTINATION) || attribute.equals(Setup.TRACK)) && !isPickup) {
1699                return Setup.isPrintHeadersEnabled()
1700                        ? padAndTruncateIfNeeded(rs.getSplitDestinationTrackName(),
1701                                locationManager.getMaxTrackNameLength())
1702                        : padAndTruncateIfNeeded(
1703                                TrainManifestText.getStringTo() +
1704                                        SPACE +
1705                                        rs.getSplitDestinationTrackName(),
1706                                locationManager.getMaxTrackNameLength() +
1707                                        TrainManifestText.getStringTo().length() +
1708                                        1);
1709            } else if (attribute.equals(Setup.DEST_TRACK)) {
1710                // format destination name and destination track name
1711                String destAndTrackName =
1712                        rs.getSplitDestinationName() + "," + SPACE + rs.getSplitDestinationTrackName();
1713                return Setup.isPrintHeadersEnabled()
1714                        ? padAndTruncateIfNeeded(destAndTrackName,
1715                                locationManager.getMaxLocationAndTrackNameLength() + 2)
1716                        : padAndTruncateIfNeeded(TrainManifestText.getStringDest() + SPACE + destAndTrackName,
1717                                locationManager.getMaxLocationAndTrackNameLength() +
1718                                        TrainManifestText.getStringDest().length() +
1719                                        3);
1720            } else if (attribute.equals(Setup.OWNER)) {
1721                return padAndTruncateIfNeeded(rs.getOwnerName(),
1722                        InstanceManager.getDefault(CarOwners.class).getMaxNameLength());
1723            } else if (attribute.equals(Setup.LAST_TRAIN)) {
1724                String lastTrainName = padAndTruncateIfNeeded(rs.getLastTrainName(),
1725                        InstanceManager.getDefault(TrainManager.class).getMaxTrainNameLength());
1726                return Setup.isPrintHeadersEnabled() ? lastTrainName
1727                        : TrainManifestHeaderText.getStringHeader_Last_Train() + SPACE + lastTrainName;
1728            }
1729            // the three utility attributes that don't get printed but need to
1730              // be tabbed out
1731            else if (attribute.equals(Setup.NO_NUMBER)) {
1732                return padAndTruncateIfNeeded("",
1733                        Control.max_len_string_print_road_number - (UTILITY_CAR_COUNT_FIELD_SIZE + 1));
1734            } else if (attribute.equals(Setup.NO_ROAD)) {
1735                return padAndTruncateIfNeeded("", InstanceManager.getDefault(CarRoads.class).getMaxNameLength());
1736            } else if (attribute.equals(Setup.NO_COLOR)) {
1737                return padAndTruncateIfNeeded("", InstanceManager.getDefault(CarColors.class).getMaxNameLength());
1738            } // there are four truncated manifest attributes
1739            else if (attribute.equals(Setup.NO_DEST_TRACK)) {
1740                return Setup.isPrintHeadersEnabled()
1741                        ? padAndTruncateIfNeeded("", locationManager.getMaxLocationAndTrackNameLength() + 1)
1742                        : "";
1743            } else if ((attribute.equals(Setup.NO_LOCATION) && !isPickup) ||
1744                    (attribute.equals(Setup.NO_DESTINATION) && isPickup)) {
1745                return Setup.isPrintHeadersEnabled()
1746                        ? padAndTruncateIfNeeded("", locationManager.getMaxLocationNameLength())
1747                        : "";
1748            } else if (attribute.equals(Setup.NO_TRACK) ||
1749                    attribute.equals(Setup.NO_LOCATION) ||
1750                    attribute.equals(Setup.NO_DESTINATION)) {
1751                return Setup.isPrintHeadersEnabled()
1752                        ? padAndTruncateIfNeeded("", locationManager.getMaxTrackNameLength())
1753                        : "";
1754            } else if (attribute.equals(Setup.TAB)) {
1755                return createTabIfNeeded(Setup.getTab1Length() - 1);
1756            } else if (attribute.equals(Setup.TAB2)) {
1757                return createTabIfNeeded(Setup.getTab2Length() - 1);
1758            } else if (attribute.equals(Setup.TAB3)) {
1759                return createTabIfNeeded(Setup.getTab3Length() - 1);
1760            }
1761            // something isn't right!
1762            return Bundle.getMessage("ErrorPrintOptions", attribute);
1763
1764        } catch (ArrayIndexOutOfBoundsException e) {
1765            if (attribute.equals(Setup.ROAD)) {
1766                return padAndTruncateIfNeeded("", InstanceManager.getDefault(CarRoads.class).getMaxNameLength());
1767            } else if (attribute.equals(Setup.TYPE)) {
1768                return padAndTruncateIfNeeded("", InstanceManager.getDefault(CarTypes.class).getMaxNameLength());
1769            }
1770            // something isn't right!
1771            return Bundle.getMessage("ErrorPrintOptions", attribute);
1772        }
1773    }
1774
1775    /**
1776     * Two column header format. Left side pick ups, right side set outs
1777     *
1778     * @param file       Manifest or switch list File.
1779     * @param isManifest True if manifest, false if switch list.
1780     */
1781    public void printEngineHeader(PrintWriter file, boolean isManifest) {
1782        int lineLength = getLineLength(isManifest);
1783        printHorizontalLine(file, 0, lineLength);
1784        if (!Setup.isPrintHeadersEnabled()) {
1785            return;
1786        }
1787        if (!Setup.getPickupEnginePrefix().trim().isEmpty() || !Setup.getDropEnginePrefix().trim().isEmpty()) {
1788            // center engine pick up and set out text
1789            String s = padAndTruncate(tabString(Setup.getPickupEnginePrefix().trim(),
1790                    lineLength / 4 - Setup.getPickupEnginePrefix().length() / 2), lineLength / 2) +
1791                    VERTICAL_LINE_CHAR +
1792                    tabString(Setup.getDropEnginePrefix(), lineLength / 4 - Setup.getDropEnginePrefix().length() / 2);
1793            s = padAndTruncate(s, lineLength);
1794            addLine(file, s);
1795            printHorizontalLine(file, 0, lineLength);
1796        }
1797
1798        String s = padAndTruncate(getPickupEngineHeader(), lineLength / 2);
1799        s = padAndTruncate(s + VERTICAL_LINE_CHAR + getDropEngineHeader(), lineLength);
1800        addLine(file, s);
1801        printHorizontalLine(file, 0, lineLength);
1802    }
1803
1804    public void printPickupEngineHeader(PrintWriter file, boolean isManifest) {
1805        int lineLength = getLineLength(isManifest);
1806        printHorizontalLine(file, 0, lineLength);
1807        String s = padAndTruncate(createTabIfNeeded(Setup.getManifestPrefixLength() + 1) + getPickupEngineHeader(),
1808                lineLength);
1809        addLine(file, s);
1810        printHorizontalLine(file, 0, lineLength);
1811    }
1812
1813    public void printDropEngineHeader(PrintWriter file, boolean isManifest) {
1814        int lineLength = getLineLength(isManifest);
1815        printHorizontalLine(file, 0, lineLength);
1816        String s = padAndTruncate(createTabIfNeeded(Setup.getManifestPrefixLength() + 1) + getDropEngineHeader(),
1817                lineLength);
1818        addLine(file, s);
1819        printHorizontalLine(file, 0, lineLength);
1820    }
1821
1822    /**
1823     * Prints the two column header for cars. Left side pick ups, right side set
1824     * outs.
1825     *
1826     * @param file             Manifest or Switch List File
1827     * @param isManifest       True if manifest, false if switch list.
1828     * @param isTwoColumnTrack True if two column format using track names.
1829     */
1830    public void printCarHeader(PrintWriter file, boolean isManifest, boolean isTwoColumnTrack) {
1831        int lineLength = getLineLength(isManifest);
1832        printHorizontalLine(file, 0, lineLength);
1833        if (!Setup.isPrintHeadersEnabled()) {
1834            return;
1835        }
1836        // center pick up and set out text
1837        String s = padAndTruncate(
1838                tabString(Setup.getPickupCarPrefix(), lineLength / 4 - Setup.getPickupCarPrefix().length() / 2),
1839                lineLength / 2) +
1840                VERTICAL_LINE_CHAR +
1841                tabString(Setup.getDropCarPrefix(), lineLength / 4 - Setup.getDropCarPrefix().length() / 2);
1842        s = padAndTruncate(s, lineLength);
1843        addLine(file, s);
1844        printHorizontalLine(file, 0, lineLength);
1845
1846        s = padAndTruncate(getPickupCarHeader(isManifest, isTwoColumnTrack), lineLength / 2);
1847        s = padAndTruncate(s + VERTICAL_LINE_CHAR + getDropCarHeader(isManifest, isTwoColumnTrack), lineLength);
1848        addLine(file, s);
1849        printHorizontalLine(file, 0, lineLength);
1850    }
1851
1852    public void printPickupCarHeader(PrintWriter file, boolean isManifest, boolean isTwoColumnTrack) {
1853        if (!Setup.isPrintHeadersEnabled()) {
1854            return;
1855        }
1856        printHorizontalLine(file, isManifest);
1857        String s = padAndTruncate(createTabIfNeeded(Setup.getManifestPrefixLength() + 1) +
1858                getPickupCarHeader(isManifest, isTwoColumnTrack), getLineLength(isManifest));
1859        addLine(file, s);
1860        printHorizontalLine(file, isManifest);
1861    }
1862
1863    public void printDropCarHeader(PrintWriter file, boolean isManifest, boolean isTwoColumnTrack) {
1864        if (!Setup.isPrintHeadersEnabled() || getDropCarHeader(isManifest, isTwoColumnTrack).trim().isEmpty()) {
1865            return;
1866        }
1867        printHorizontalLine(file, isManifest);
1868        String s = padAndTruncate(
1869                createTabIfNeeded(Setup.getManifestPrefixLength() + 1) + getDropCarHeader(isManifest, isTwoColumnTrack),
1870                getLineLength(isManifest));
1871        addLine(file, s);
1872        printHorizontalLine(file, isManifest);
1873    }
1874
1875    public void printLocalCarMoveHeader(PrintWriter file, boolean isManifest) {
1876        if (!Setup.isPrintHeadersEnabled()) {
1877            return;
1878        }
1879        printHorizontalLine(file, isManifest);
1880        String s = padAndTruncate(
1881                createTabIfNeeded(Setup.getManifestPrefixLength() + 1) + getLocalMoveHeader(isManifest),
1882                getLineLength(isManifest));
1883        addLine(file, s);
1884        printHorizontalLine(file, isManifest);
1885    }
1886
1887    public String getPickupEngineHeader() {
1888        return getHeader(Setup.getPickupEngineMessageFormat(), PICKUP, !LOCAL, ENGINE);
1889    }
1890
1891    public String getDropEngineHeader() {
1892        return getHeader(Setup.getDropEngineMessageFormat(), !PICKUP, !LOCAL, ENGINE);
1893    }
1894
1895    public String getPickupCarHeader(boolean isManifest, boolean isTwoColumnTrack) {
1896        if (isManifest && !isTwoColumnTrack) {
1897            return getHeader(Setup.getPickupManifestMessageFormat(), PICKUP, !LOCAL, !ENGINE);
1898        } else if (!isManifest && !isTwoColumnTrack) {
1899            return getHeader(Setup.getPickupSwitchListMessageFormat(), PICKUP, !LOCAL, !ENGINE);
1900        } else if (isManifest && isTwoColumnTrack) {
1901            return getHeader(Setup.getPickupTwoColumnByTrackManifestMessageFormat(), PICKUP, !LOCAL, !ENGINE);
1902        } else {
1903            return getHeader(Setup.getPickupTwoColumnByTrackSwitchListMessageFormat(), PICKUP, !LOCAL, !ENGINE);
1904        }
1905    }
1906
1907    public String getDropCarHeader(boolean isManifest, boolean isTwoColumnTrack) {
1908        if (isManifest && !isTwoColumnTrack) {
1909            return getHeader(Setup.getDropManifestMessageFormat(), !PICKUP, !LOCAL, !ENGINE);
1910        } else if (!isManifest && !isTwoColumnTrack) {
1911            return getHeader(Setup.getDropSwitchListMessageFormat(), !PICKUP, !LOCAL, !ENGINE);
1912        } else if (isManifest && isTwoColumnTrack) {
1913            return getHeader(Setup.getDropTwoColumnByTrackManifestMessageFormat(), !PICKUP, !LOCAL, !ENGINE);
1914        } else {
1915            return getHeader(Setup.getDropTwoColumnByTrackSwitchListMessageFormat(), !PICKUP, !LOCAL, !ENGINE);
1916        }
1917    }
1918
1919    public String getLocalMoveHeader(boolean isManifest) {
1920        if (isManifest) {
1921            return getHeader(Setup.getLocalManifestMessageFormat(), !PICKUP, LOCAL, !ENGINE);
1922        } else {
1923            return getHeader(Setup.getLocalSwitchListMessageFormat(), !PICKUP, LOCAL, !ENGINE);
1924        }
1925    }
1926
1927    private String getHeader(String[] format, boolean isPickup, boolean isLocal, boolean isEngine) {
1928        StringBuffer buf = new StringBuffer();
1929        for (String attribute : format) {
1930            if (attribute.equals(Setup.BLANK)) {
1931                continue;
1932            }
1933            if (attribute.equals(Setup.ROAD)) {
1934                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Road(),
1935                        InstanceManager.getDefault(CarRoads.class).getMaxNameLength()) + SPACE);
1936            } else if (attribute.equals(Setup.NUMBER) && !isEngine) {
1937                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Number(),
1938                        Control.max_len_string_print_road_number) + SPACE);
1939            } else if (attribute.equals(Setup.NUMBER) && isEngine) {
1940                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_EngineNumber(),
1941                        Control.max_len_string_print_road_number) + SPACE);
1942            } else if (attribute.equals(Setup.TYPE)) {
1943                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Type(),
1944                        InstanceManager.getDefault(CarTypes.class).getMaxNameLength()) + SPACE);
1945            } else if (attribute.equals(Setup.MODEL)) {
1946                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Model(),
1947                        InstanceManager.getDefault(EngineModels.class).getMaxNameLength()) + SPACE);
1948            } else if (attribute.equals(Setup.HP)) {
1949                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Hp(),
1950                        5) + SPACE);
1951            } else if (attribute.equals(Setup.CONSIST)) {
1952                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Consist(),
1953                        InstanceManager.getDefault(ConsistManager.class).getMaxNameLength()) + SPACE);
1954            } else if (attribute.equals(Setup.DCC_ADDRESS)) {
1955                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_DCC_Address(),
1956                        TrainManifestHeaderText.getStringHeader_DCC_Address().length()) + SPACE);
1957            } else if (attribute.equals(Setup.KERNEL)) {
1958                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Kernel(),
1959                        InstanceManager.getDefault(KernelManager.class).getMaxNameLength()) + SPACE);
1960            } else if (attribute.equals(Setup.KERNEL_SIZE)) {
1961                buf.append("   "); // assume kernel size is 99 or less
1962            } else if (attribute.equals(Setup.LOAD)) {
1963                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Load(),
1964                        InstanceManager.getDefault(CarLoads.class).getMaxNameLength()) + SPACE);
1965            } else if (attribute.equals(Setup.LOAD_TYPE)) {
1966                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Load_Type(),
1967                        TrainManifestHeaderText.getStringHeader_Load_Type().length()) + SPACE);
1968            } else if (attribute.equals(Setup.COLOR)) {
1969                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Color(),
1970                        InstanceManager.getDefault(CarColors.class).getMaxNameLength()) + SPACE);
1971            } else if (attribute.equals(Setup.OWNER)) {
1972                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Owner(),
1973                        InstanceManager.getDefault(CarOwners.class).getMaxNameLength()) + SPACE);
1974            } else if (attribute.equals(Setup.LENGTH)) {
1975                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Length(),
1976                        InstanceManager.getDefault(CarLengths.class).getMaxNameLength()) + SPACE);
1977            } else if (attribute.equals(Setup.WEIGHT)) {
1978                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Weight(),
1979                        Control.max_len_string_weight_name) + SPACE);
1980            } else if (attribute.equals(Setup.TRACK)) {
1981                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Track(),
1982                        locationManager.getMaxTrackNameLength()) + SPACE);
1983            } else if (attribute.equals(Setup.LOCATION) && (isPickup || isLocal)) {
1984                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Location(),
1985                        locationManager.getMaxTrackNameLength()) + SPACE);
1986            } else if (attribute.equals(Setup.LOCATION) && !isPickup) {
1987                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Location(),
1988                        locationManager.getMaxLocationNameLength()) + SPACE);
1989            } else if (attribute.equals(Setup.DESTINATION) && !isPickup) {
1990                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Destination(),
1991                        locationManager.getMaxTrackNameLength()) + SPACE);
1992            } else if (attribute.equals(Setup.DESTINATION) && isPickup) {
1993                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Destination(),
1994                        locationManager.getMaxLocationNameLength()) + SPACE);
1995            } else if (attribute.equals(Setup.DEST_TRACK)) {
1996                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Dest_Track(),
1997                        locationManager.getMaxLocationAndTrackNameLength() + 2) + SPACE);
1998            } else if (attribute.equals(Setup.FINAL_DEST)) {
1999                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Final_Dest(),
2000                        locationManager.getMaxLocationNameLength()) + SPACE);
2001            } else if (attribute.equals(Setup.FINAL_DEST_TRACK)) {
2002                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Final_Dest_Track(),
2003                        locationManager.getMaxLocationAndTrackNameLength() + 2) + SPACE);
2004            } else if (attribute.equals(Setup.HAZARDOUS)) {
2005                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Hazardous(),
2006                        Setup.getHazardousMsg().length()) + SPACE);
2007            } else if (attribute.equals(Setup.RWE)) {
2008                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_RWE(),
2009                        locationManager.getMaxLocationAndTrackNameLength()) + SPACE);
2010            } else if (attribute.equals(Setup.COMMENT)) {
2011                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Comment(),
2012                        isEngine ? engineManager.getMaxCommentLength() : carManager.getMaxCommentLength()) + SPACE);
2013            } else if (attribute.equals(Setup.DROP_COMMENT)) {
2014                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Drop_Comment(),
2015                        InstanceManager.getDefault(CarLoads.class).getMaxLoadCommentLength()) + SPACE);
2016            } else if (attribute.equals(Setup.PICKUP_COMMENT)) {
2017                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Pickup_Comment(),
2018                        InstanceManager.getDefault(CarLoads.class).getMaxLoadCommentLength()) + SPACE);
2019            } else if (attribute.equals(Setup.DIVISION)) {
2020                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Division(),
2021                        InstanceManager.getDefault(DivisionManager.class).getMaxDivisionNameLength()) + SPACE);
2022            } else if (attribute.equals(Setup.BLOCKING_ORDER)) {
2023                buf.append("    "); // assume blocking order +/- 99
2024            } else if (attribute.equals(Setup.LAST_TRAIN)) {
2025                buf.append(padAndTruncateIfNeeded(TrainManifestHeaderText.getStringHeader_Last_Train(),
2026                        InstanceManager.getDefault(TrainManager.class).getMaxTrainNameLength()) + SPACE);
2027            } else if (attribute.equals(Setup.TAB)) {
2028                buf.append(createTabIfNeeded(Setup.getTab1Length()));
2029            } else if (attribute.equals(Setup.TAB2)) {
2030                buf.append(createTabIfNeeded(Setup.getTab2Length()));
2031            } else if (attribute.equals(Setup.TAB3)) {
2032                buf.append(createTabIfNeeded(Setup.getTab3Length()));
2033            } else {
2034                buf.append(attribute + SPACE);
2035            }
2036        }
2037        return buf.toString().stripTrailing();
2038    }
2039
2040    protected void printTrackNameHeader(PrintWriter file, String trackName, boolean isManifest) {
2041        printHorizontalLine(file, isManifest);
2042        int lineLength = getLineLength(isManifest);
2043        String s = padAndTruncate(tabString(trackName.trim(), lineLength / 4 - trackName.trim().length() / 2),
2044                lineLength / 2) +
2045                VERTICAL_LINE_CHAR +
2046                tabString(trackName.trim(), lineLength / 4 - trackName.trim().length() / 2);
2047        s = padAndTruncate(s, lineLength);
2048        addLine(file, s);
2049        printHorizontalLine(file, isManifest);
2050    }
2051
2052    /**
2053     * Prints a line across the entire page.
2054     *
2055     * @param file       The File to print to.
2056     * @param isManifest True if manifest, false if switch list.
2057     */
2058    public void printHorizontalLine(PrintWriter file, boolean isManifest) {
2059        printHorizontalLine(file, 0, getLineLength(isManifest));
2060    }
2061
2062    public void printHorizontalLine(PrintWriter file, int start, int end) {
2063        StringBuffer sb = new StringBuffer();
2064        while (start-- > 0) {
2065            sb.append(SPACE);
2066        }
2067        while (end-- > 0) {
2068            sb.append(HORIZONTAL_LINE_CHAR);
2069        }
2070        addLine(file, sb.toString());
2071    }
2072
2073    public static String getISO8601Date(boolean isModelYear) {
2074        Calendar calendar = Calendar.getInstance();
2075        // use the JMRI Timebase (which may be a fast clock).
2076        calendar.setTime(jmri.InstanceManager.getDefault(jmri.Timebase.class).getTime());
2077        if (isModelYear && !Setup.getYearModeled().isEmpty()) {
2078            try {
2079                calendar.set(Calendar.YEAR, Integer.parseInt(Setup.getYearModeled().trim()));
2080            } catch (NumberFormatException e) {
2081                return Setup.getYearModeled();
2082            }
2083        }
2084        return (new StdDateFormat()).format(calendar.getTime());
2085    }
2086
2087    public static String getDate(Date date) {
2088        SimpleDateFormat format = new SimpleDateFormat("M/dd/yyyy HH:mm"); // NOI18N
2089        if (Setup.is12hrFormatEnabled()) {
2090            format = new SimpleDateFormat("M/dd/yyyy hh:mm a"); // NOI18N
2091        }
2092        return format.format(date);
2093    }
2094
2095    public static String getDate(boolean isModelYear) {
2096        Calendar calendar = Calendar.getInstance();
2097        // use the JMRI Timebase (which may be a fast clock).
2098        calendar.setTime(jmri.InstanceManager.getDefault(jmri.Timebase.class).getTime());
2099        if (isModelYear && !Setup.getYearModeled().equals(Setup.NONE)) {
2100            try {
2101                calendar.set(Calendar.YEAR, Integer.parseInt(Setup.getYearModeled().trim()));
2102            } catch (NumberFormatException e) {
2103                return Setup.getYearModeled();
2104            }
2105        }
2106        return TrainCommon.getDate(calendar.getTime());
2107    }
2108
2109    public static Date convertStringToDate(String date) {
2110        if (!date.isBlank()) {
2111            // create a date object from the string.
2112            try {
2113                // try MM/dd/yyyy HH:mm:ss.
2114                SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss"); // NOI18N
2115                return formatter.parse(date);
2116            } catch (java.text.ParseException pe1) {
2117                // try the old 12 hour format (no seconds).
2118                try {
2119                    SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy hh:mmaa"); // NOI18N
2120                    return formatter.parse(date);
2121                } catch (java.text.ParseException pe2) {
2122                    try {
2123                        // try 24hour clock.
2124                        SimpleDateFormat formatter = new SimpleDateFormat("MM/dd/yyyy HH:mm"); // NOI18N
2125                        return formatter.parse(date);
2126                    } catch (java.text.ParseException pe3) {
2127                        log.debug("Not able to parse date: {}", date);
2128                    }
2129                }
2130            }
2131        }
2132        return null; // there was no date specified.
2133    }
2134
2135    /*
2136     * Converts String time DAYS:HH:MM and DAYS:HH:MM AM/PM to minutes from
2137     * midnight.
2138     */
2139    protected int convertStringTime(String time) {
2140        int minutes = 0;
2141        boolean hrFormat = false;
2142        String[] splitTimePM = time.split(" ");
2143        if (splitTimePM.length > 1) {
2144            hrFormat = true;
2145            if (splitTimePM[1].equals(Bundle.getMessage("PM"))) {
2146                minutes = 12 * 60;
2147            }
2148        }
2149        String[] splitTime = splitTimePM[0].split(":");
2150
2151        if (splitTime.length > 2) {
2152            // days:hrs:minutes
2153            if (hrFormat && splitTime[1].equals("12")) {
2154                splitTime[1] = "00";
2155            }
2156            minutes += 24 * 60 * Integer.parseInt(splitTime[0]);
2157            minutes += 60 * Integer.parseInt(splitTime[1]);
2158            minutes += Integer.parseInt(splitTime[2]);
2159        } else {
2160            // hrs:minutes
2161            if (hrFormat && splitTime[0].equals("12")) {
2162                splitTime[0] = "00";
2163            }
2164            minutes += 60 * Integer.parseInt(splitTime[0]);
2165            minutes += Integer.parseInt(splitTime[1]);
2166        }
2167        log.debug("convert time {} to minutes {}", time, minutes);
2168        return minutes;
2169    }
2170
2171    /**
2172     * Pads out a string by adding spaces to the end of the string, and will
2173     * remove characters from the end of the string if the string exceeds the
2174     * field size.
2175     *
2176     * @param s         The string to pad.
2177     * @param fieldSize The maximum length of the string.
2178     * @return A String the specified length
2179     */
2180    public static String padAndTruncateIfNeeded(String s, int fieldSize) {
2181        if (Setup.isTabEnabled()) {
2182            return padAndTruncate(s, fieldSize);
2183        }
2184        return s;
2185    }
2186
2187    public static String padAndTruncate(String s, int fieldSize) {
2188        s = padString(s, fieldSize);
2189        if (s.length() > fieldSize) {
2190            s = s.substring(0, fieldSize);
2191        }
2192        return s;
2193    }
2194
2195    /**
2196     * Adjusts string to be a certain number of characters by adding spaces to
2197     * the end of the string.
2198     *
2199     * @param s         The string to pad
2200     * @param fieldSize The fixed length of the string.
2201     * @return A String the specified length
2202     */
2203    public static String padString(String s, int fieldSize) {
2204        StringBuffer buf = new StringBuffer(s);
2205        while (buf.length() < fieldSize) {
2206            buf.append(SPACE);
2207        }
2208        return buf.toString();
2209    }
2210
2211    /**
2212     * Creates a String of spaces to create a tab for text. Tabs must be
2213     * enabled. Setup.isTabEnabled()
2214     * 
2215     * @param tabSize the length of tab
2216     * @return tab
2217     */
2218    public static String createTabIfNeeded(int tabSize) {
2219        if (Setup.isTabEnabled()) {
2220            return tabString("", tabSize);
2221        }
2222        return "";
2223    }
2224
2225    protected static String tabString(String s, int tabSize) {
2226        StringBuffer buf = new StringBuffer();
2227        // TODO this doesn't consider the length of s string.
2228        while (buf.length() < tabSize) {
2229            buf.append(SPACE);
2230        }
2231        buf.append(s);
2232        return buf.toString();
2233    }
2234
2235    /**
2236     * Returns the line length for manifest or switch list printout. Always an
2237     * even number.
2238     * 
2239     * @param isManifest True if manifest.
2240     * @return line length for manifest or switch list.
2241     */
2242    public static int getLineLength(boolean isManifest) {
2243        return getLineLength(isManifest ? Setup.getManifestOrientation() : Setup.getSwitchListOrientation(),
2244                Setup.getFontName(), Font.PLAIN, Setup.getManifestFontSize());
2245    }
2246
2247    public static int getManifestHeaderLineLength() {
2248        return getLineLength(Setup.getManifestOrientation(), "SansSerif", Font.ITALIC, Setup.getManifestFontSize());
2249    }
2250
2251    private static int getLineLength(String orientation, String fontName, int fontStyle, int fontSize) {
2252        Font font = new Font(fontName, fontStyle, fontSize); // NOI18N
2253        JLabel label = new JLabel();
2254        FontMetrics metrics = label.getFontMetrics(font);
2255        int charwidth = metrics.charWidth('m');
2256        if (charwidth == 0) {
2257            log.error("Line length charater width equal to zero. font size: {}, fontName: {}", fontSize, fontName);
2258            charwidth = fontSize / 2; // create a reasonable character width
2259        }
2260        // compute lines and columns within margins
2261        int charLength = getPageSize(orientation).width / charwidth;
2262        if (charLength % 2 != 0) {
2263            charLength--; // make it even
2264        }
2265        return charLength;
2266    }
2267
2268    private boolean checkStringLength(String string, boolean isManifest) {
2269        return checkStringLength(string, isManifest ? Setup.getManifestOrientation() : Setup.getSwitchListOrientation(),
2270                Setup.getFontName(), Setup.getManifestFontSize());
2271    }
2272
2273    /**
2274     * Checks to see if the string fits on the page.
2275     *
2276     * @return false if string length is longer than page width.
2277     */
2278    private boolean checkStringLength(String string, String orientation, String fontName, int fontSize) {
2279        // ignore text color controls when determining line length
2280        if (string.startsWith(TEXT_COLOR_START) && string.contains(TEXT_COLOR_DONE)) {
2281            string = string.substring(string.indexOf(TEXT_COLOR_DONE) + 2);
2282        }
2283        if (string.contains(TEXT_COLOR_END)) {
2284            string = string.substring(0, string.indexOf(TEXT_COLOR_END));
2285        }
2286        Font font = new Font(fontName, Font.PLAIN, fontSize); // NOI18N
2287        JLabel label = new JLabel();
2288        FontMetrics metrics = label.getFontMetrics(font);
2289        int stringWidth = metrics.stringWidth(string);
2290        return stringWidth <= getPageSize(orientation).width;
2291    }
2292
2293    protected static final Dimension PAPER_MARGINS = new Dimension(84, 72);
2294
2295    protected static Dimension getPageSize(String orientation) {
2296        // page size has been adjusted to account for margins of .5
2297        // Dimension(84, 72)
2298        Dimension pagesize = new Dimension(523, 720); // Portrait 8.5 x 11
2299        // landscape has .65 margins
2300        if (orientation.equals(Setup.LANDSCAPE)) {
2301            pagesize = new Dimension(702, 523); // 11 x 8.5
2302        }
2303        if (orientation.equals(Setup.HALFPAGE)) {
2304            pagesize = new Dimension(261, 720); // 4.25 x 11
2305        }
2306        if (orientation.equals(Setup.HANDHELD)) {
2307            pagesize = new Dimension(206, 720); // 3.25 x 11
2308        }
2309        return pagesize;
2310    }
2311
2312    /**
2313     * Produces a string using commas and spaces between the strings provided in
2314     * the array. Does not check for embedded commas in the string array.
2315     *
2316     * @param array The string array to be formated.
2317     * @return formated string using commas and spaces
2318     */
2319    public static String formatStringToCommaSeparated(String[] array) {
2320        StringBuffer sbuf = new StringBuffer("");
2321        for (String s : array) {
2322            if (s != null) {
2323                sbuf = sbuf.append(s + "," + SPACE);
2324            }
2325        }
2326        if (sbuf.length() > 2) {
2327            sbuf.setLength(sbuf.length() - 2); // remove trailing separators
2328        }
2329        return sbuf.toString();
2330    }
2331
2332    private void addLine(PrintWriter file, StringBuffer buf, Color color) {
2333        String s = buf.toString();
2334        if (!s.trim().isEmpty()) {
2335            addLine(file, formatColorString(s, color));
2336        }
2337    }
2338
2339    /**
2340     * Adds HTML like color text control characters around a string. Note that
2341     * black is the standard text color, and if black is requested no control
2342     * characters are added.
2343     * 
2344     * @param text  the text to be modified
2345     * @param color the color the text is to be printed
2346     * @return formated text with color modifiers
2347     */
2348    public static String formatColorString(String text, Color color) {
2349        String s = text;
2350        if (!color.equals(Color.black)) {
2351            s = TEXT_COLOR_START + ColorUtil.colorToColorName(color) + TEXT_COLOR_DONE + text + TEXT_COLOR_END;
2352        }
2353        return s;
2354    }
2355
2356    /**
2357     * Removes the color text control characters around the desired string
2358     * 
2359     * @param string the string with control characters
2360     * @return pure text
2361     */
2362    public static String getTextColorString(String string) {
2363        String text = string;
2364        if (string.contains(TEXT_COLOR_START)) {
2365            text = string.substring(0, string.indexOf(TEXT_COLOR_START)) +
2366                    string.substring(string.indexOf(TEXT_COLOR_DONE) + 2);
2367        }
2368        if (text.contains(TEXT_COLOR_END)) {
2369            text = text.substring(0, text.indexOf(TEXT_COLOR_END)) +
2370                    string.substring(string.indexOf(TEXT_COLOR_END) + TEXT_COLOR_END.length());
2371        }
2372        return text;
2373    }
2374
2375    public static Color getTextColor(String string) {
2376        Color color = Color.black;
2377        if (string.contains(TEXT_COLOR_START)) {
2378            String c = string.substring(string.indexOf(TEXT_COLOR_START) + TEXT_COLOR_START.length());
2379            c = c.substring(0, c.indexOf("\""));
2380            color = ColorUtil.stringToColor(c);
2381        }
2382        return color;
2383    }
2384
2385    public static String getTextColorName(String string) {
2386        return ColorUtil.colorToColorName(getTextColor(string));
2387    }
2388
2389    private static final Logger log = LoggerFactory.getLogger(TrainCommon.class);
2390}