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