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