001/* *********************************************************************** * 002 * project: org.matsim.* 003 * * 004 * *********************************************************************** * 005 * * 006 * copyright : (C) 2010 by the members listed in the COPYING, * 007 * LICENSE and WARRANTY file. * 008 * email : info at matsim dot org * 009 * * 010 * *********************************************************************** * 011 * * 012 * This program is free software; you can redistribute it and/or modify * 013 * it under the terms of the GNU General Public License as published by * 014 * the Free Software Foundation; either version 2 of the License, or * 015 * (at your option) any later version. * 016 * See also COPYING, LICENSE and WARRANTY file * 017 * * 018 * *********************************************************************** */ 019 020package org.matsim.pt.utils; 021 022import java.io.IOException; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.HashSet; 028import java.util.Iterator; 029import java.util.List; 030import java.util.Set; 031 032import javax.xml.parsers.ParserConfigurationException; 033 034import org.matsim.api.core.v01.Id; 035import org.matsim.api.core.v01.network.Link; 036import org.matsim.api.core.v01.network.Network; 037import org.matsim.core.config.ConfigUtils; 038import org.matsim.core.network.io.MatsimNetworkReader; 039import org.matsim.core.population.routes.NetworkRoute; 040import org.matsim.core.scenario.MutableScenario; 041import org.matsim.core.scenario.ScenarioUtils; 042import org.matsim.pt.transitSchedule.api.MinimalTransferTimes; 043import org.matsim.pt.transitSchedule.api.TransitLine; 044import org.matsim.pt.transitSchedule.api.TransitRoute; 045import org.matsim.pt.transitSchedule.api.TransitRouteStop; 046import org.matsim.pt.transitSchedule.api.TransitSchedule; 047import org.matsim.pt.transitSchedule.api.TransitScheduleReader; 048import org.matsim.pt.transitSchedule.api.TransitStopFacility; 049import org.xml.sax.SAXException; 050 051/** 052 * An abstract class offering a number of static methods to validate several aspects of transit schedules. 053 * 054 * @author mrieser 055 */ 056public abstract class TransitScheduleValidator { 057 058 private TransitScheduleValidator() { 059 // this class should not be instantiated 060 } 061 062 /** 063 * Checks that the links specified for a network route really builds a complete route that can be driven along. 064 * 065 * @param schedule 066 * @param network 067 * @return 068 */ 069 public static ValidationResult validateNetworkRoutes(final TransitSchedule schedule, final Network network) { 070 ValidationResult result = new ValidationResult(); 071 if (network == null || network.getLinks().size() == 0) { 072 result.addWarning("Cannot validate network routes: No network given!"); 073 return result; 074 } 075 076 for (TransitLine line : schedule.getTransitLines().values()) { 077 for (TransitRoute route : line.getRoutes().values()) { 078 NetworkRoute netRoute = route.getRoute(); 079 if (netRoute == null) { 080 result.addIssue(new ValidationResult.ValidationIssue(ValidationResult.Severity.ERROR, "Transit line " + line.getId() + ", route " + route.getId() + " has no network route.", ValidationResult.Type.OTHER, Collections.singleton(route.getId()))); 081 } else { 082 Link prevLink = network.getLinks().get(netRoute.getStartLinkId()); 083 for (Id<Link> linkId : netRoute.getLinkIds()) { 084 Link link = network.getLinks().get(linkId); 085 if (link == null) { 086 result.addIssue(new ValidationResult.ValidationIssue(ValidationResult.Severity.ERROR, "Transit line " + line.getId() + ", route " + route.getId() + 087 " contains a link that is not part of the network: " + linkId, ValidationResult.Type.OTHER, Collections.singleton(route.getId()))); 088 } else if (prevLink != null && !prevLink.getToNode().equals(link.getFromNode())) { 089 result.addIssue(new ValidationResult.ValidationIssue(ValidationResult.Severity.ERROR, "Transit line " + line.getId() + ", route " + route.getId() + 090 " has inconsistent network route, e.g. between link " + prevLink.getId() + " and " + linkId, ValidationResult.Type.OTHER, Collections.singleton(route.getId()))); 091 } 092 prevLink = link; 093 } 094 } 095 } 096 } 097 return result; 098 } 099 100 /** 101 * Checks that all the listed stops in a route appear in that order when driving along the network route 102 * 103 * @param schedule 104 * @param network 105 * @return 106 */ 107 public static ValidationResult validateStopsOnNetworkRoute(final TransitSchedule schedule, final Network network) { 108 ValidationResult result = new ValidationResult(); 109 if (network == null || network.getLinks().size() == 0) { 110 result.addWarning("Cannot validate stops on network route: No network given!"); 111 return result; 112 } 113 114 for (TransitLine line : schedule.getTransitLines().values()) { 115 for (TransitRoute route : line.getRoutes().values()) { 116 NetworkRoute netRoute = route.getRoute(); 117 if (netRoute == null) { 118 result.addIssue(new ValidationResult.ValidationIssue(ValidationResult.Severity.ERROR, "Transit line " + line.getId() + ", route " + route.getId() + " has no network route.", ValidationResult.Type.OTHER, Collections.singleton(route.getId()))); 119 } else { 120 List<Id<Link>> linkIds = new ArrayList<>(); 121 linkIds.add(netRoute.getStartLinkId()); 122 linkIds.addAll(netRoute.getLinkIds()); 123 linkIds.add(netRoute.getEndLinkId()); 124 Iterator<Id<Link>> linkIdIterator = linkIds.iterator(); 125 Id<Link> nextLinkId = linkIdIterator.next(); 126 boolean error = false; 127 for (TransitRouteStop stop : route.getStops()) { 128 Id<Link> linkRefId = stop.getStopFacility().getLinkId(); 129 130 while (!linkRefId.equals(nextLinkId)) { 131 if (linkIdIterator.hasNext()) { 132 nextLinkId = linkIdIterator.next(); 133 } else { 134 result.addIssue(new ValidationResult.ValidationIssue(ValidationResult.Severity.ERROR, "Transit line " + line.getId() + ", route " + route.getId() + ": Stop " + stop.getStopFacility().getId() + " cannot be reached along network route.", ValidationResult.Type.ROUTE_HAS_UNREACHABLE_STOP, Collections.singletonList(stop.getStopFacility().getId()))); 135 error = true; 136 break; 137 } 138 } 139 if (error) { 140 break; 141 } 142 143 } 144 } 145 } 146 } 147 return result; 148 } 149 150 public static ValidationResult validateUsedStopsHaveLinkId(final TransitSchedule schedule) { 151 ValidationResult result = new ValidationResult(); 152 for (TransitLine line : schedule.getTransitLines().values()) { 153 for (TransitRoute route : line.getRoutes().values()) { 154 for (TransitRouteStop stop : route.getStops()) { 155 Id<Link> linkId = stop.getStopFacility().getLinkId(); 156 if (linkId == null) { 157 result.addIssue(new ValidationResult.ValidationIssue(ValidationResult.Severity.ERROR,"Transit Stop Facility " + stop.getStopFacility().getId() + " has no linkId, but is used by transit line " + line.getId() + ", route " + route.getId(), ValidationResult.Type.HAS_NO_LINK_REF, Collections.singleton(stop.getStopFacility().getId()))); 158 } 159 } 160 } 161 } 162 return result; 163 } 164 165 public static ValidationResult validateAllStopsExist(final TransitSchedule schedule) { 166 ValidationResult result = new ValidationResult(); 167 for (TransitLine line : schedule.getTransitLines().values()) { 168 for (TransitRoute route : line.getRoutes().values()) { 169 for (TransitRouteStop stop : route.getStops()) { 170 if (stop.getStopFacility() == null) { 171 result.addIssue(new ValidationResult.ValidationIssue(ValidationResult.Severity.ERROR, "Transit line " + line.getId() + ", route " + route.getId() + " contains a stop (dep-offset=" + stop.getDepartureOffset() + ") without stop-facility. Most likely, a wrong id was specified in the file.", ValidationResult.Type.HAS_MISSING_STOP_FACILITY, Collections.singletonList(route.getId()))); 172 } else if (schedule.getFacilities().get(stop.getStopFacility().getId()) == null) { 173 result.addIssue(new ValidationResult.ValidationIssue(ValidationResult.Severity.ERROR, "Transit line " + line.getId() + ", route " + route.getId() + " contains a stop (stop-facility " + stop.getStopFacility().getId() + ") that is not contained in the list of all stop facilities.", ValidationResult.Type.HAS_MISSING_STOP_FACILITY, Collections.singletonList(route.getId()))); 174 } 175 } 176 } 177 } 178 return result; 179 } 180 181 public static ValidationResult validateOffsets(final TransitSchedule schedule) { 182 ValidationResult result = new ValidationResult(); 183 184 for (TransitLine line : schedule.getTransitLines().values()) { 185 for (TransitRoute route : line.getRoutes().values()) { 186 ArrayList<TransitRouteStop> stops = new ArrayList<TransitRouteStop>(route.getStops()); 187 int stopCount = stops.size(); 188 189 if (stopCount > 0) { 190 TransitRouteStop stop = stops.get(0); 191 if (stop.getDepartureOffset().isUndefined()) { 192 result.addError("Transit line " + line.getId() + ", route " + route.getId() + ": The first stop does not contain any departure offset."); 193 } 194 195 for (int i = 1; i < stopCount - 1; i++) { 196 stop = stops.get(i); 197 if (stop.getDepartureOffset().isUndefined()) { 198 result.addError("Transit line " + line.getId() + ", route " + route.getId() + ": Stop " + i + " does not contain any departure offset."); 199 } 200 } 201 202 stop = stops.get(stopCount - 1); 203 if (stop.getArrivalOffset().isUndefined()) { 204 result.addError("Transit line " + line.getId() + ", route " + route.getId() + ": The last stop does not contain any arrival offset."); 205 } 206 } else { 207 result.addWarning("Transit line " + line.getId() + ", route " + route.getId() + ": The route has not stops assigned, looks suspicious."); 208 } 209 210 } 211 } 212 213 return result; 214 } 215 216 public static ValidationResult validateTransfers(final TransitSchedule schedule) { 217 ValidationResult result = new ValidationResult(); 218 219 MinimalTransferTimes transferTimes = schedule.getMinimalTransferTimes(); 220 MinimalTransferTimes.MinimalTransferTimesIterator iter = transferTimes.iterator(); 221 Set<Id> missingFromStops = new HashSet<>(); 222 Set<Id> missingToStops = new HashSet<>(); 223 224 while (iter.hasNext()) { 225 iter.next(); 226 Id<TransitStopFacility> fromStopId = iter.getFromStopId(); 227 Id<TransitStopFacility> toStopId = iter.getToStopId(); 228 double transferTime = iter.getSeconds(); 229 230 if (fromStopId == null && toStopId == null) { 231 result.addError("Minimal Transfer Times: both fromStop and toStop are null."); 232 } else if (fromStopId == null) { 233 result.addError("Minimal Transfer Times: fromStop = null, toStop " + toStopId + "."); 234 } else if (toStopId == null) { 235 result.addError("Minimal Transfer Times: fromStop " + fromStopId + ", toStop = null."); 236 } 237 if (transferTime <= 0) { 238 result.addWarning("Minimal Transfer Times: fromStop " + fromStopId + " toStop " + toStopId + " with transferTime = " + transferTime); 239 } 240 if (schedule.getFacilities().get(fromStopId) == null && missingFromStops.add(fromStopId)) { 241 result.addError("Minimal Transfer Times: fromStop " + fromStopId + " does not exist in schedule."); 242 } 243 if (schedule.getFacilities().get(toStopId) == null && missingToStops.add(toStopId)) { 244 result.addError("Minimal Transfer Times: toStop " + toStopId + " does not exist in schedule."); 245 } 246 } 247 248 return result; 249 } 250 251 public static ValidationResult validateAll(final TransitSchedule schedule, final Network network) { 252 ValidationResult v = validateUsedStopsHaveLinkId(schedule); 253 v.add(validateNetworkRoutes(schedule, network)); 254 try { 255 v.add(validateStopsOnNetworkRoute(schedule, network)); 256 } catch (NullPointerException e) { 257 v.addError("Exception during 'validateStopsOnNetworkRoute'. Most likely something is wrong in the file, but it cannot be specified in more detail." + Arrays.toString(e.getStackTrace())); 258 } 259 v.add(validateAllStopsExist(schedule)); 260 v.add(validateOffsets(schedule)); 261 v.add(validateTransfers(schedule)); 262 return v; 263 } 264 265 public static void printResult(final ValidationResult result) { 266 if (result.isValid()) { 267 System.out.println("Schedule appears valid!"); 268 } else { 269 System.out.println("Schedule is NOT valid!"); 270 } 271 if (result.getErrors().size() > 0) { 272 System.out.println("Validation errors:"); 273 for (String e : result.getErrors()) { 274 System.out.println(e); 275 } 276 } 277 if (result.getWarnings().size() > 0) { 278 System.out.println("Validation warnings:"); 279 for (String w : result.getWarnings()) { 280 System.out.println(w); 281 } 282 } 283 } 284 285 /** 286 * @param args [0] path to transitSchedule.xml, [1] path to network.xml (optional) 287 * @throws IOException 288 * @throws SAXException 289 * @throws ParserConfigurationException 290 */ 291 public static void main(String[] args) throws IOException, SAXException, ParserConfigurationException { 292 if (args.length > 2 || args.length < 1) { 293 System.err.println("Usage: TransitScheduleValidator transitSchedule.xml [network.xml]"); 294 return; 295 } 296 297 MutableScenario s = (MutableScenario) ScenarioUtils.createScenario(ConfigUtils.createConfig()); 298 s.getConfig().transit().setUseTransit(true); 299 TransitSchedule ts = s.getTransitSchedule(); 300 Network net = s.getNetwork(); 301 302 if (args.length > 1) { 303 new MatsimNetworkReader(s.getNetwork()).readFile(args[1]); 304 } 305 new TransitScheduleReader(s).readFile(args[0]); 306 307 ValidationResult v = validateAll(ts, net); 308 printResult(v); 309 } 310 311 public static class ValidationResult { 312 313 public enum Severity { 314 WARNING, ERROR; 315 } 316 317 public enum Type { 318 HAS_MISSING_STOP_FACILITY, HAS_NO_LINK_REF, ROUTE_HAS_UNREACHABLE_STOP, OTHER; 319 } 320 321 public static class ValidationIssue<T> { 322 private final Severity severity; 323 private final String message; 324 private final Type errorCode; 325 private final Collection<Id<T>> entities; 326 327 public ValidationIssue(Severity severity, String message, Type errorCode, Collection<Id<T>> entities) { 328 this.severity = severity; 329 this.message = message; 330 this.errorCode = errorCode; 331 this.entities = entities; 332 } 333 334 public Severity getSeverity() { 335 return severity; 336 } 337 338 public String getMessage() { 339 return message; 340 } 341 342 public Type getErrorCode() { 343 return errorCode; 344 } 345 346 public Collection<Id<T>> getEntities() { 347 return entities; 348 } 349 350 } 351 352 private boolean isValid = true; 353 private final List<ValidationIssue> issues = new ArrayList<>(); 354 355 public boolean isValid() { 356 return this.isValid; 357 } 358 359 public List<String> getWarnings() { 360 List<String> result = new ArrayList<>(); 361 for (ValidationIssue issue : this.issues) { 362 if (issue.severity == Severity.WARNING) { 363 result.add(issue.getMessage()); 364 } 365 } 366 return Collections.unmodifiableList(result); 367 } 368 369 public List<String> getErrors() { 370 List<String> result = new ArrayList<>(); 371 for (ValidationIssue issue : this.issues) { 372 if (issue.severity == Severity.ERROR) { 373 result.add(issue.getMessage()); 374 } 375 } 376 return Collections.unmodifiableList(result); 377 } 378 379 public List<ValidationIssue> getIssues() { 380 return Collections.unmodifiableList(this.issues); 381 } 382 383 public void addWarning(final String warning) { 384 this.issues.add(new ValidationIssue(Severity.WARNING, warning, Type.OTHER, Collections.<Id<?>>emptyList())); 385 } 386 387 public void addError(final String error) { 388 this.issues.add(new ValidationIssue(Severity.ERROR, error, Type.OTHER, Collections.<Id<?>>emptyList())); 389 this.isValid = false; 390 } 391 392 public void addIssue(final ValidationIssue issue) { 393 this.issues.add(issue); 394 if (issue.severity == Severity.ERROR) { 395 this.isValid = false; 396 } 397 } 398 399 public void add(final ValidationResult otherResult) { 400 this.issues.addAll(otherResult.getIssues()); 401 this.isValid = this.isValid && otherResult.isValid; 402 } 403 } 404}