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