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