001package jmri.web.servlet.operations;
002
003import java.io.IOException;
004import java.util.*;
005
006import jmri.InstanceManager;
007import jmri.jmrit.operations.locations.Track;
008import jmri.jmrit.operations.rollingstock.cars.Car;
009import jmri.jmrit.operations.rollingstock.cars.CarManager;
010import jmri.jmrit.operations.rollingstock.engines.Engine;
011import jmri.jmrit.operations.rollingstock.engines.EngineManager;
012import jmri.jmrit.operations.routes.RouteLocation;
013import jmri.jmrit.operations.setup.Setup;
014import jmri.jmrit.operations.trains.Train;
015import jmri.jmrit.operations.trains.trainbuilder.TrainCommon;
016import jmri.util.FileUtil;
017
018import org.apache.commons.text.StringEscapeUtils;
019import org.slf4j.Logger;
020import org.slf4j.LoggerFactory;
021
022/**
023 *
024 * @author Randall Wood
025 */
026public class HtmlConductor extends HtmlTrainCommon {
027
028    private static final Logger log = LoggerFactory.getLogger(HtmlConductor.class);
029
030    public HtmlConductor(Locale locale, Train train) throws IOException {
031        super(locale, train);
032        this.resourcePrefix = "Conductor";  // NOI18N
033    }
034
035    public String getLocation() throws IOException {
036        RouteLocation location = train.getCurrentRouteLocation();
037        if (location == null) {
038            return String.format(locale, 
039                    FileUtil.readURL(FileUtil.findURL(Bundle.getMessage(locale,"ConductorSnippet.html"))), 
040                    train.getIconName(), 
041                    StringEscapeUtils.escapeHtml4(train.getDescription()), 
042                    convertToHTMLColor(
043                            StringEscapeUtils
044                                    .escapeHtml4(TrainCommon.getTextSizeString(train.getCommentCurrentWithColor()))),
045                    Setup.isPrintRouteCommentsEnabled() ? train.getRoute().getComment() : "",
046                    strings.getProperty("Terminated"), 
047                    "", // terminated train has nothing to do // NOI18N
048                    "", // engines in separate section
049                    "", // pickup=true, local=false
050                    "", // pickup=false, local=false
051                    "", // pickup=false, local=true
052                    "", // engines in separate section
053                    "", // terminate with null string, use empty string to indicate terminated
054                    strings.getProperty("Terminated"),  // NOI18N
055                    train.getStatusCode());
056        }
057
058        List<Engine> engineList = InstanceManager.getDefault(EngineManager.class).getByTrainBlockingList(train);
059        List<Car> carList = InstanceManager.getDefault(CarManager.class).getByTrainDestinationList(train);
060        log.debug("Train has {} cars assigned to it", carList.size());
061
062        String pickups = performWork(true, false); // pickup=true, local=false
063        String setouts = performWork(false, false); // pickup=false, local=false
064        String localMoves = performWork(false, true); // pickup=false, local=true
065
066        return String.format(locale, 
067                FileUtil.readURL(FileUtil.findURL(Bundle.getMessage(locale,"ConductorSnippet.html"))), 
068                train.getIconName(), 
069                StringEscapeUtils.escapeHtml4(train.getDescription()),
070                convertToHTMLColor(StringEscapeUtils.escapeHtml4(train.getCommentCurrentWithColor())),
071                Setup.isPrintRouteCommentsEnabled() ? train.getRoute().getComment() : "", 
072                getCurrentAndNextLocation(),
073                convertToHTMLColor(getLocationComments()),
074                pickupEngines(engineList, location), // engines in separate section
075                pickups, setouts, localMoves,
076                dropEngines(engineList, location), // engines in separate section
077                (train.getNextRouteLocation(train.getCurrentRouteLocation()) != null) ? train.getNextLocationName() : null,
078                getMoveButton(),
079                train.getStatusCode());
080    }
081
082    private String getCurrentAndNextLocation() {
083        if (train.getCurrentRouteLocation() != null && train.getNextRouteLocation(train.getCurrentRouteLocation()) != null) {
084            return String.format(locale, strings.getProperty("CurrentAndNextLocation"), // NOI18N
085                    StringEscapeUtils.escapeHtml4(splitString(train.getCurrentLocationName())),
086                    StringEscapeUtils.escapeHtml4(splitString(train.getNextLocationName())));
087        } else if (train.getCurrentRouteLocation() != null) {
088            return StringEscapeUtils.escapeHtml4(splitString(train.getCurrentLocationName()));
089        }
090        return strings.getProperty("Terminated"); // NOI18N
091    }
092
093    private String getMoveButton() {
094        if (train.getNextRouteLocation(train.getCurrentRouteLocation()) != null) {
095            return String.format(locale, strings.getProperty("MoveTo"), // NOI18N
096                    StringEscapeUtils.escapeHtml4(splitString(train.getNextLocationName())));
097        } else if (train.getCurrentRouteLocation() != null) {
098            return strings.getProperty("Terminate");  // NOI18N
099        }
100        return strings.getProperty("Terminated");  // NOI18N
101    }
102
103    // needed for location comments, not yet in formatter
104    private String getEngineChanges(RouteLocation rl) {
105        // engine change or helper service?
106        if (train.getSecondLegOptions() != Train.NO_CABOOSE_OR_FRED) {
107            if (rl == train.getSecondLegStartRouteLocation()) {
108                return engineChange(rl, train.getSecondLegOptions());
109            }
110            if (rl == train.getSecondLegEndRouteLocation() && train.getSecondLegOptions() == Train.HELPER_ENGINES) {
111                return String.format(strings.getProperty("RemoveHelpersAt"), rl.getSplitName()); // NOI18N
112            }
113        }
114        if (train.getThirdLegOptions() != Train.NO_CABOOSE_OR_FRED) {
115            if (rl == train.getThirdLegStartRouteLocation()) {
116                return engineChange(rl, train.getSecondLegOptions());
117            }
118            if (rl == train.getThirdLegEndRouteLocation() && train.getThirdLegOptions() == Train.HELPER_ENGINES) {
119                return String.format(strings.getProperty("RemoveHelpersAt"), rl.getSplitName()); // NOI18N
120            }
121        }
122        return "";
123    }
124
125    private String getLocationComments() {
126        List<Car> carList = InstanceManager.getDefault(CarManager.class).getByTrainDestinationList(train);
127        StringBuilder builder = new StringBuilder();
128        RouteLocation routeLocation = train.getCurrentRouteLocation();
129        boolean work = isThereWorkAtLocation(train, routeLocation.getLocation());
130
131        // print info only if new location
132        String routeLocationName = StringEscapeUtils.escapeHtml4(routeLocation.getSplitName());
133        if (work) {
134            if (!train.isShowArrivalAndDepartureTimesEnabled()) {
135                builder.append(String.format(locale, strings.getProperty("ScheduledWorkAt"), routeLocationName)); // NOI18N
136            } else if (routeLocation == train.getTrainDepartsRouteLocation()) {
137                builder.append(String.format(locale, strings.getProperty("WorkDepartureTime"), routeLocationName, train  // NOI18N
138                        .getFormatedDepartureTime())); // NOI18N
139            } else if (!routeLocation.getDepartureTimeHourMinutes().equals(RouteLocation.NONE)) {
140                builder.append(String.format(locale, strings.getProperty("WorkDepartureTime"), routeLocationName,  // NOI18N
141                        routeLocation.getFormatedDepartureTime())); // NOI18N
142            } else if (Setup.isUseDepartureTimeEnabled()
143                    && routeLocation != train.getTrainTerminatesRouteLocation()
144                    && !train.getExpectedArrivalTime(routeLocation).equals(Train.ALREADY_SERVICED)) {
145                builder.append(String.format(locale, strings.getProperty("WorkDepartureTime"), routeLocationName, train  // NOI18N
146                        .getExpectedDepartureTime(routeLocation)));
147            } else if (!train.getExpectedArrivalTime(routeLocation).equals(Train.ALREADY_SERVICED)) {
148                builder.append(String.format(locale, strings.getProperty("WorkArrivalTime"), routeLocationName, train  // NOI18N
149                        .getExpectedArrivalTime(routeLocation))); // NOI18N
150            } else {
151                builder.append(String.format(locale, strings.getProperty("ScheduledWorkAt"), routeLocationName)); // NOI18N
152            }
153            // add route comment
154            if (!routeLocation.getComment().isBlank()) {
155                builder.append(String.format(locale, strings.getProperty("RouteLocationComment"), StringEscapeUtils  // NOI18N
156                        .escapeHtml4(routeLocation.getCommentWithColor())));
157            }
158
159            builder.append(getTrackComments(routeLocation, carList));
160
161            // add location comment
162            if (Setup.isPrintLocationCommentsEnabled() && !routeLocation.getLocation().getComment().isEmpty()) {
163                builder.append(String.format(locale, strings.getProperty("LocationComment"), StringEscapeUtils  // NOI18N
164                        .escapeHtml4(routeLocation.getLocation().getCommentWithColor())));
165            }
166        }
167
168        // engine change or helper service?
169        builder.append(this.getEngineChanges(routeLocation));
170
171        if (routeLocation != train.getTrainTerminatesRouteLocation()) {
172            if (work) {
173                if (!Setup.isPrintLoadsAndEmptiesEnabled()) {
174                    // Message format: Train departs Boston Westbound with 12 cars, 450 feet, 3000 tons
175                    builder.append(String.format(strings.getProperty("TrainDepartsCars"), routeLocationName,  // NOI18N
176                            routeLocation.getTrainDirectionString(), train.getTrainLength(routeLocation), Setup
177                            .getLengthUnit().toLowerCase(), train.getTrainWeight(routeLocation), train
178                            .getNumberCarsInTrain(routeLocation)));
179                } else {
180                    // Message format: Train departs Boston Westbound with 4 loads, 8 empties, 450 feet, 3000 tons
181                    int emptyCars = train.getNumberEmptyCarsInTrain(routeLocation);
182                    builder.append(String.format(strings.getProperty("TrainDepartsLoads"), routeLocationName,  // NOI18N
183                            routeLocation.getTrainDirectionString(), train.getTrainLength(routeLocation), Setup
184                            .getLengthUnit().toLowerCase(), train.getTrainWeight(routeLocation), train
185                            .getNumberCarsInTrain(routeLocation)
186                            - emptyCars, emptyCars));
187                }
188            } else {
189                log.debug("No work ({})", routeLocation.getComment());               
190                if (routeLocation.getComment().isBlank()) {
191                    // no route comment, no work at this location
192                    if (train.isShowArrivalAndDepartureTimesEnabled()) {
193                        if (routeLocation == train.getTrainDepartsRouteLocation()) {
194                            builder.append(String.format(locale, strings
195                                    .getProperty("NoScheduledWorkAtWithDepartureTime"), routeLocationName, train  // NOI18N
196                                    .getFormatedDepartureTime()));
197                        } else if (!routeLocation.getDepartureTimeHourMinutes().isEmpty()) {
198                            builder.append(String.format(locale, strings
199                                    .getProperty("NoScheduledWorkAtWithDepartureTime"), routeLocationName,  // NOI18N
200                                    routeLocation.getFormatedDepartureTime()));
201                        } else {
202                            builder.append(String.format(locale, strings.getProperty("NoScheduledWorkAt"),  // NOI18N
203                                    routeLocationName));
204                        }
205                    } else {
206                        builder.append(String.format(locale, strings.getProperty("NoScheduledWorkAt"),  // NOI18N
207                                routeLocationName));
208                    }
209                } else {
210                    // if a route comment, then only use location name and route comment, useful for passenger
211                    // trains
212                    if (!routeLocation.getComment().isBlank()) {
213                        builder.append(String.format(locale, strings.getProperty("CommentAt"), // NOI18N
214                                routeLocationName, StringEscapeUtils
215                                        .escapeHtml4(routeLocation.getCommentWithColor())));
216                    }
217                    if (train.isShowArrivalAndDepartureTimesEnabled()) {
218                        if (routeLocation == train.getTrainDepartsRouteLocation()) {
219                            builder.append(String.format(locale, strings
220                                    .getProperty("CommentAtWithDepartureTime"), routeLocationName, train // NOI18N
221                                    .getFormatedDepartureTime(), StringEscapeUtils
222                                            .escapeHtml4(routeLocation.getCommentWithColor())));
223                        } else if (!routeLocation.getDepartureTimeHourMinutes().equals(RouteLocation.NONE)) {
224                            builder.append(String.format(locale, strings
225                                    .getProperty("CommentAtWithDepartureTime"), routeLocationName, // NOI18N
226                                    routeLocation.getFormatedDepartureTime(), StringEscapeUtils
227                                            .escapeHtml4(routeLocation.getCommentWithColor())));
228                        } else if (Setup.isUseDepartureTimeEnabled() &&
229                                !routeLocation.getComment().isBlank()) {
230                            builder.append(String.format(locale, strings
231                                    .getProperty("NoScheduledWorkAtWithDepartureTime"), routeLocationName, // NOI18N
232                                    train.getExpectedDepartureTime(routeLocation)));
233                        }
234                    }                           
235                }
236                // add location comment
237                if (Setup.isPrintLocationCommentsEnabled() && !routeLocation.getLocation().getComment().isEmpty()) {
238                    builder.append(String.format(locale, strings.getProperty("LocationComment"), StringEscapeUtils  // NOI18N
239                            .escapeHtml4(routeLocation.getLocation().getCommentWithColor())));
240                }
241            }
242        } else {
243            builder.append(String.format(strings.getProperty("TrainTerminatesIn"), routeLocationName));  // NOI18N
244        }
245        return builder.toString();
246    }
247
248    private String performWork(boolean pickup, boolean local) {
249        if (pickup) {
250           return pickupCars();
251        } else {
252            return dropCars(local);
253        }
254    }
255
256    private String pickupCars() {
257        StringBuilder builder = new StringBuilder();
258        RouteLocation rlocation = train.getCurrentRouteLocation();
259        List<Car> carList = InstanceManager.getDefault(CarManager.class).getByTrainDestinationList(train);
260        List<Track> tracks = rlocation.getLocation().getTracksByNameList(null);
261        List<String> trackNames = new ArrayList<>();
262        List<String> pickedUp = new ArrayList<>();
263        this.clearUtilityCarTypes();
264        for (Track track : tracks) {
265            if (trackNames.contains(track.getSplitName())) {
266                continue;
267            }
268            trackNames.add(track.getSplitName()); // use a track name once
269            // block cars by destination
270            for (RouteLocation rld : train.getRoute().getLocationsBySequenceList()) {
271                for (Car car : carList) {
272                    if (pickedUp.contains(car.getId())
273                            || (Setup.isSortByTrackNameEnabled() && !track.getSplitName().equals(
274                                    car.getSplitTrackName()))) {
275                        continue;
276                    }
277                    if (car.isLocalMove() && rlocation == rld) {
278                        continue;
279                    }
280                    // block pick up cars
281                    // caboose or FRED is placed at end of the train
282                    // passenger cars are already blocked in the car list
283                    // passenger cars with negative block numbers are placed at
284                    // the front of the train, positive numbers at the end of
285                    // the train.
286                    // note that a car in train doesn't have a track assignment
287                    if (isNextCar(car, rlocation, rld)) {
288                        pickedUp.add(car.getId());
289                        if (car.isUtility()) {
290                            builder.append(pickupUtilityCars(carList, car, TrainCommon.IS_MANIFEST));
291                         // use truncated format if there's a switch list
292                        } else if (Setup.isPrintTruncateManifestEnabled() && rlocation.getLocation().isSwitchListEnabled()) {
293                            builder.append(pickUpCar(car, Setup.getPickupTruncatedManifestMessageFormat()));
294                        } else {
295                            builder.append(pickUpCar(car, Setup.getPickupManifestMessageFormat()));
296                        }
297                    }
298                }
299            }
300        }
301        return builder.toString();
302    }
303
304    private String dropCars(boolean local) {
305        StringBuilder builder = new StringBuilder();
306        RouteLocation location = train.getCurrentRouteLocation();
307        List<Car> carList = InstanceManager.getDefault(CarManager.class).getByTrainDestinationList(train);
308        List<Track> tracks = location.getLocation().getTracksByNameList(null);
309        List<String> trackNames = new ArrayList<>();
310        List<String> dropped = new ArrayList<>();
311        for (Track track : tracks) {
312            if (trackNames.contains(track.getSplitName())) {
313                continue;
314            }
315            trackNames.add(track.getSplitName()); // use a track name once
316            for (Car car : carList) {
317                if (dropped.contains(car.getId())
318                        || (Setup.isSortByTrackNameEnabled() && !track.getSplitName().equals(
319                                car.getSplitDestinationTrackName()))) {
320                    continue;
321                }
322                if (car.isLocalMove() == local
323                        && (car.getRouteDestination() == location && car.getDestinationTrack() != null)) {
324                    dropped.add(car.getId());
325                    if (car.isUtility()) {
326                        builder.append(setoutUtilityCars(carList, car, local));
327                    } else {
328                        String[] format = (!local) ? Setup.getDropManifestMessageFormat() : Setup
329                                .getLocalManifestMessageFormat();
330                        builder.append(dropCar(car, format, local));
331                    }
332                }
333            }
334        }
335        return builder.toString();
336    }
337}