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