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}