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}