001package jmri.web.servlet.operations;
002
003import java.io.IOException;
004import java.util.*;
005
006import org.apache.commons.text.StringEscapeUtils;
007import org.slf4j.Logger;
008import org.slf4j.LoggerFactory;
009
010import jmri.InstanceManager;
011import jmri.jmrit.operations.locations.Track;
012import jmri.jmrit.operations.rollingstock.cars.Car;
013import jmri.jmrit.operations.rollingstock.cars.CarManager;
014import jmri.jmrit.operations.rollingstock.engines.Engine;
015import jmri.jmrit.operations.rollingstock.engines.EngineManager;
016import jmri.jmrit.operations.routes.RouteLocation;
017import jmri.jmrit.operations.setup.Setup;
018import jmri.jmrit.operations.trains.Train;
019import jmri.jmrit.operations.trains.TrainCommon;
020import jmri.util.FileUtil;
021
022/**
023 *
024 * @author Randall Wood
025 */
026public class HtmlConductor extends HtmlTrainCommon {
027
028    private final static 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                    StringEscapeUtils.escapeHtml4(train.getComment().replaceAll("\n", "<br>")), 
043                    Setup.isPrintRouteCommentsEnabled() ? train.getRoute().getComment() : "", 
044                    strings.getProperty("Terminated"), 
045                    "", // terminated train has nothing to do // NOI18N
046                    "", // engines in separate section
047                    "", // pickup=true, local=false
048                    "", // pickup=false, local=false
049                    "", // pickup=false, local=true
050                    "", // engines in separate section
051                    "", // terminate with null string, use empty string to indicate terminated
052                    strings.getProperty("Terminated"),  // NOI18N
053                    train.getStatusCode());
054        }
055
056        List<Engine> engineList = InstanceManager.getDefault(EngineManager.class).getByTrainBlockingList(train);
057        List<Car> carList = InstanceManager.getDefault(CarManager.class).getByTrainDestinationList(train);
058        log.debug("Train has {} cars assigned to it", carList.size());
059
060        String pickups = performWork(true, false); // pickup=true, local=false
061        String setouts = performWork(false, false); // pickup=false, local=false
062        String localMoves = performWork(false, true); // pickup=false, local=true
063
064        return String.format(locale, 
065                FileUtil.readURL(FileUtil.findURL(Bundle.getMessage(locale,"ConductorSnippet.html"))), 
066                train.getIconName(), 
067                StringEscapeUtils.escapeHtml4(train.getDescription()),
068                StringEscapeUtils.escapeHtml4(train.getComment().replaceAll("\n", "<br>")), 
069                Setup.isPrintRouteCommentsEnabled() ? train.getRoute().getComment() : "", 
070                getCurrentAndNextLocation(),
071                getLocationComments().replaceAll("\n", "<br>"),
072                pickupEngines(engineList, location), // engines in separate section
073                pickups, setouts, localMoves,
074                dropEngines(engineList, location), // engines in separate section
075                (train.getNextRouteLocation(train.getCurrentRouteLocation()) != null) ? train.getNextLocationName() : null,
076                getMoveButton(),
077                train.getStatusCode());
078    }
079
080    private String getCurrentAndNextLocation() {
081        if (train.getCurrentRouteLocation() != null && train.getNextRouteLocation(train.getCurrentRouteLocation()) != null) {
082            return String.format(locale, strings.getProperty("CurrentAndNextLocation"), // NOI18N
083                    StringEscapeUtils.escapeHtml4(splitString(train.getCurrentLocationName())),
084                    StringEscapeUtils.escapeHtml4(splitString(train.getNextLocationName())));
085        } else if (train.getCurrentRouteLocation() != null) {
086            return StringEscapeUtils.escapeHtml4(splitString(train.getCurrentLocationName()));
087        }
088        return strings.getProperty("Terminated"); // NOI18N
089    }
090
091    private String getMoveButton() {
092        if (train.getNextRouteLocation(train.getCurrentRouteLocation()) != null) {
093            return String.format(locale, strings.getProperty("MoveTo"), // NOI18N
094                    StringEscapeUtils.escapeHtml4(splitString(train.getNextLocationName())));
095        } else if (train.getCurrentRouteLocation() != null) {
096            return strings.getProperty("Terminate");  // NOI18N
097        }
098        return strings.getProperty("Terminated");  // NOI18N
099    }
100
101    // needed for location comments, not yet in formatter
102    private String getEngineChanges(RouteLocation rl) {
103        // engine change or helper service?
104        if (train.getSecondLegOptions() != Train.NO_CABOOSE_OR_FRED) {
105            if (rl == train.getSecondLegStartRouteLocation()) {
106                return engineChange(rl, train.getSecondLegOptions());
107            }
108            if (rl == train.getSecondLegEndRouteLocation() && train.getSecondLegOptions() == Train.HELPER_ENGINES) {
109                return String.format(strings.getProperty("RemoveHelpersAt"), rl.getSplitName()); // NOI18N
110            }
111        }
112        if (train.getThirdLegOptions() != Train.NO_CABOOSE_OR_FRED) {
113            if (rl == train.getThirdLegStartRouteLocation()) {
114                return engineChange(rl, train.getSecondLegOptions());
115            }
116            if (rl == train.getThirdLegEndRouteLocation() && train.getThirdLegOptions() == Train.HELPER_ENGINES) {
117                return String.format(strings.getProperty("RemoveHelpersAt"), rl.getSplitName()); // NOI18N
118            }
119        }
120        return "";
121    }
122
123    private String getLocationComments() {
124        List<Car> carList = InstanceManager.getDefault(CarManager.class).getByTrainDestinationList(train);
125        StringBuilder builder = new StringBuilder();
126        RouteLocation routeLocation = train.getCurrentRouteLocation();
127        boolean work = isThereWorkAtLocation(train, routeLocation.getLocation());
128
129        // print info only if new location
130        String routeLocationName = StringEscapeUtils.escapeHtml4(routeLocation.getSplitName());
131        if (work) {
132            if (!train.isShowArrivalAndDepartureTimesEnabled()) {
133                builder.append(String.format(locale, strings.getProperty("ScheduledWorkAt"), routeLocationName)); // NOI18N
134            } else if (routeLocation == train.getTrainDepartsRouteLocation()) {
135                builder.append(String.format(locale, strings.getProperty("WorkDepartureTime"), routeLocationName, train  // NOI18N
136                        .getFormatedDepartureTime())); // NOI18N
137            } else if (!routeLocation.getDepartureTime().equals("")) {
138                builder.append(String.format(locale, strings.getProperty("WorkDepartureTime"), routeLocationName,  // NOI18N
139                        routeLocation.getFormatedDepartureTime())); // NOI18N
140            } else if (Setup.isUseDepartureTimeEnabled()
141                    && routeLocation != train.getTrainTerminatesRouteLocation()
142                    && !train.getExpectedDepartureTime(routeLocation).equals(Train.ALREADY_SERVICED)) {
143                builder.append(String.format(locale, strings.getProperty("WorkDepartureTime"), routeLocationName, train  // NOI18N
144                        .getExpectedDepartureTime(routeLocation)));
145            } else if (!train.getExpectedArrivalTime(routeLocation).equals(Train.ALREADY_SERVICED)) {
146                builder.append(String.format(locale, strings.getProperty("WorkArrivalTime"), routeLocationName, train  // NOI18N
147                        .getExpectedArrivalTime(routeLocation))); // NOI18N
148            } else {
149                builder.append(String.format(locale, strings.getProperty("ScheduledWorkAt"), routeLocationName)); // NOI18N
150            }
151            // add route comment
152            if (!routeLocation.getComment().trim().equals("")) {
153                builder.append(String.format(locale, strings.getProperty("RouteLocationComment"), StringEscapeUtils  // NOI18N
154                        .escapeHtml4(routeLocation.getComment())));
155            }
156
157            builder.append(getTrackComments(routeLocation, carList));
158
159            // add location comment
160            if (Setup.isPrintLocationCommentsEnabled() && !routeLocation.getLocation().getComment().isEmpty()) {
161                builder.append(String.format(locale, strings.getProperty("LocationComment"), StringEscapeUtils  // NOI18N
162                        .escapeHtml4(routeLocation.getLocation().getComment())));
163            }
164        }
165
166        // engine change or helper service?
167        builder.append(this.getEngineChanges(routeLocation));
168
169        if (routeLocation != train.getTrainTerminatesRouteLocation()) {
170            if (work) {
171                if (!Setup.isPrintLoadsAndEmptiesEnabled()) {
172                    // Message format: Train departs Boston Westbound with 12 cars, 450 feet, 3000 tons
173                    builder.append(String.format(strings.getProperty("TrainDepartsCars"), routeLocationName,  // NOI18N
174                            routeLocation.getTrainDirectionString(), train.getTrainLength(routeLocation), Setup
175                            .getLengthUnit().toLowerCase(), train.getTrainWeight(routeLocation), train
176                            .getNumberCarsInTrain(routeLocation)));
177                } else {
178                    // Message format: Train departs Boston Westbound with 4 loads, 8 empties, 450 feet, 3000 tons
179                    int emptyCars = train.getNumberEmptyCarsInTrain(routeLocation);
180                    builder.append(String.format(strings.getProperty("TrainDepartsLoads"), routeLocationName,  // NOI18N
181                            routeLocation.getTrainDirectionString(), train.getTrainLength(routeLocation), Setup
182                            .getLengthUnit().toLowerCase(), train.getTrainWeight(routeLocation), train
183                            .getNumberCarsInTrain(routeLocation)
184                            - emptyCars, emptyCars));
185                }
186            } else {
187                log.debug("No work ({})", routeLocation.getComment());               
188                if (routeLocation.getComment().trim().isEmpty()) {
189                    // no route comment, no work at this location
190                    if (train.isShowArrivalAndDepartureTimesEnabled()) {
191                        if (routeLocation == train.getTrainDepartsRouteLocation()) {
192                            builder.append(String.format(locale, strings
193                                    .getProperty("NoScheduledWorkAtWithDepartureTime"), routeLocationName, train  // NOI18N
194                                    .getFormatedDepartureTime()));
195                        } else if (!routeLocation.getDepartureTime().isEmpty()) {
196                            builder.append(String.format(locale, strings
197                                    .getProperty("NoScheduledWorkAtWithDepartureTime"), routeLocationName,  // NOI18N
198                                    routeLocation.getFormatedDepartureTime()));
199                        } else {
200                            builder.append(String.format(locale, strings.getProperty("NoScheduledWorkAt"),  // NOI18N
201                                    routeLocationName));
202                        }
203                    } else {
204                        builder.append(String.format(locale, strings.getProperty("NoScheduledWorkAt"),  // NOI18N
205                                routeLocationName));
206                    }
207                } else {
208                    // if a route comment, then only use location name and route comment, useful for passenger
209                    // trains
210                    if (!routeLocation.getComment().equals(RouteLocation.NONE)) {
211                        if (routeLocation.getComment().trim().length() > 0) {
212                            builder.append(String.format(locale, strings.getProperty("CommentAt"), // NOI18N
213                                    routeLocationName, StringEscapeUtils
214                                    .escapeHtml4(routeLocation.getComment())));
215                        }
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.getComment())));
223                        } else if (!routeLocation.getDepartureTime().equals(RouteLocation.NONE)) {
224                            builder.append(String.format(locale, strings
225                                    .getProperty("CommentAtWithDepartureTime"), routeLocationName, // NOI18N
226                                    routeLocation.getFormatedDepartureTime(), StringEscapeUtils
227                                    .escapeHtml4(routeLocation.getComment())));
228                        } else if (Setup.isUseDepartureTimeEnabled() &&
229                                !routeLocation.getComment().equals(RouteLocation.NONE)) {
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().getComment())));
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}