MATSIM
TransitScheduleValidator.java
Go to the documentation of this file.
1 /* *********************************************************************** *
2  * project: org.matsim.*
3  * *
4  * *********************************************************************** *
5  * *
6  * copyright : (C) 2010 by the members listed in the COPYING, *
7  * LICENSE and WARRANTY file. *
8  * email : info at matsim dot org *
9  * *
10  * *********************************************************************** *
11  * *
12  * This program is free software; you can redistribute it and/or modify *
13  * it under the terms of the GNU General Public License as published by *
14  * the Free Software Foundation; either version 2 of the License, or *
15  * (at your option) any later version. *
16  * See also COPYING, LICENSE and WARRANTY file *
17  * *
18  * *********************************************************************** */
19 
20 package org.matsim.pt.utils;
21 
22 import java.io.IOException;
23 import java.util.*;
24 
25 import javax.xml.parsers.ParserConfigurationException;
26 
27 import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
28 import it.unimi.dsi.fastutil.doubles.DoubleList;
29 import org.matsim.api.core.v01.Id;
40 import org.xml.sax.SAXException;
41 
47 public abstract class TransitScheduleValidator {
48 
50  // this class should not be instantiated
51  }
52 
60  public static ValidationResult validateNetworkRoutes(final TransitSchedule schedule, final Network network) {
61  ValidationResult result = new ValidationResult();
62  if (network == null || network.getLinks().size() == 0) {
63  result.addWarning("Cannot validate network routes: No network given!");
64  return result;
65  }
66 
67  for (TransitLine line : schedule.getTransitLines().values()) {
68  for (TransitRoute route : line.getRoutes().values()) {
69  NetworkRoute netRoute = route.getRoute();
70  if (netRoute == null) {
71  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())));
72  } else {
73  Link prevLink = network.getLinks().get(netRoute.getStartLinkId());
74  for (Id<Link> linkId : netRoute.getLinkIds()) {
75  Link link = network.getLinks().get(linkId);
76  if (link == null) {
77  result.addIssue(new ValidationResult.ValidationIssue(ValidationResult.Severity.ERROR, "Transit line " + line.getId() + ", route " + route.getId() +
78  " contains a link that is not part of the network: " + linkId, ValidationResult.Type.OTHER, Collections.singleton(route.getId())));
79  } else if (prevLink != null && !prevLink.getToNode().equals(link.getFromNode())) {
80  result.addIssue(new ValidationResult.ValidationIssue(ValidationResult.Severity.ERROR, "Transit line " + line.getId() + ", route " + route.getId() +
81  " has inconsistent network route, e.g. between link " + prevLink.getId() + " and " + linkId, ValidationResult.Type.OTHER, Collections.singleton(route.getId())));
82  }
83  prevLink = link;
84  }
85  }
86  }
87  }
88  return result;
89  }
90 
98  public static ValidationResult validateStopsOnNetworkRoute(final TransitSchedule schedule, final Network network) {
99  ValidationResult result = new ValidationResult();
100  if (network == null || network.getLinks().size() == 0) {
101  result.addWarning("Cannot validate stops on network route: No network given!");
102  return result;
103  }
104 
105  for (TransitLine line : schedule.getTransitLines().values()) {
106  for (TransitRoute route : line.getRoutes().values()) {
107  NetworkRoute netRoute = route.getRoute();
108  if (netRoute == null) {
109  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())));
110  } else {
111  List<Id<Link>> linkIds = new ArrayList<>();
112  linkIds.add(netRoute.getStartLinkId());
113  linkIds.addAll(netRoute.getLinkIds());
114  linkIds.add(netRoute.getEndLinkId());
115  Iterator<Id<Link>> linkIdIterator = linkIds.iterator();
116  Id<Link> nextLinkId = linkIdIterator.next();
117  boolean error = false;
118  for (TransitRouteStop stop : route.getStops()) {
119  Id<Link> linkRefId = stop.getStopFacility().getLinkId();
120 
121  while (!linkRefId.equals(nextLinkId)) {
122  if (linkIdIterator.hasNext()) {
123  nextLinkId = linkIdIterator.next();
124  } else {
125  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())));
126  error = true;
127  break;
128  }
129  }
130  if (error) {
131  break;
132  }
133 
134  }
135  }
136  }
137  }
138  return result;
139  }
140 
142  ValidationResult result = new ValidationResult();
143  for (TransitLine line : schedule.getTransitLines().values()) {
144  for (TransitRoute route : line.getRoutes().values()) {
145  for (TransitRouteStop stop : route.getStops()) {
146  Id<Link> linkId = stop.getStopFacility().getLinkId();
147  if (linkId == null) {
148  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())));
149  }
150  }
151  }
152  }
153  return result;
154  }
155 
157  ValidationResult result = new ValidationResult();
158  for (TransitLine line : schedule.getTransitLines().values()) {
159  for (TransitRoute route : line.getRoutes().values()) {
160  for (TransitRouteStop stop : route.getStops()) {
161  if (stop.getStopFacility() == null) {
162  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())));
163  } else if (schedule.getFacilities().get(stop.getStopFacility().getId()) == null) {
164  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())));
165  }
166  }
167  }
168  }
169  return result;
170  }
171 
172  public static ValidationResult validateOffsets(final TransitSchedule schedule) {
173  ValidationResult result = new ValidationResult();
174 
175  for (TransitLine line : schedule.getTransitLines().values()) {
176  for (TransitRoute route : line.getRoutes().values()) {
177  ArrayList<TransitRouteStop> stops = new ArrayList<TransitRouteStop>(route.getStops());
178  int stopCount = stops.size();
179 
180  if (stopCount > 0) {
181  TransitRouteStop stop = stops.get(0);
182  if (stop.getDepartureOffset().isUndefined()) {
183  result.addError("Transit line " + line.getId() + ", route " + route.getId() + ": The first stop does not contain any departure offset.");
184  }
185 
186  for (int i = 1; i < stopCount - 1; i++) {
187  stop = stops.get(i);
188  if (stop.getDepartureOffset().isUndefined()) {
189  result.addError("Transit line " + line.getId() + ", route " + route.getId() + ": Stop " + i + " does not contain any departure offset.");
190  }
191  }
192 
193  stop = stops.get(stopCount - 1);
194  if (stop.getArrivalOffset().isUndefined()) {
195  result.addError("Transit line " + line.getId() + ", route " + route.getId() + ": The last stop does not contain any arrival offset.");
196  }
197  } else {
198  result.addWarning("Transit line " + line.getId() + ", route " + route.getId() + ": The route has not stops assigned, looks suspicious.");
199  }
200 
201  }
202  }
203 
204  return result;
205  }
206 
207  public static ValidationResult validateTransfers(final TransitSchedule schedule) {
208  ValidationResult result = new ValidationResult();
209 
210  MinimalTransferTimes transferTimes = schedule.getMinimalTransferTimes();
212  Set<Id> missingFromStops = new HashSet<>();
213  Set<Id> missingToStops = new HashSet<>();
214 
215  while (iter.hasNext()) {
216  iter.next();
217  Id<TransitStopFacility> fromStopId = iter.getFromStopId();
218  Id<TransitStopFacility> toStopId = iter.getToStopId();
219  double transferTime = iter.getSeconds();
220 
221  if (fromStopId == null && toStopId == null) {
222  result.addError("Minimal Transfer Times: both fromStop and toStop are null.");
223  } else if (fromStopId == null) {
224  result.addError("Minimal Transfer Times: fromStop = null, toStop " + toStopId + ".");
225  } else if (toStopId == null) {
226  result.addError("Minimal Transfer Times: fromStop " + fromStopId + ", toStop = null.");
227  }
228  if (transferTime <= 0) {
229  result.addWarning("Minimal Transfer Times: fromStop " + fromStopId + " toStop " + toStopId + " with transferTime = " + transferTime);
230  }
231  if (schedule.getFacilities().get(fromStopId) == null && missingFromStops.add(fromStopId)) {
232  result.addError("Minimal Transfer Times: fromStop " + fromStopId + " does not exist in schedule.");
233  }
234  if (schedule.getFacilities().get(toStopId) == null && missingToStops.add(toStopId)) {
235  result.addError("Minimal Transfer Times: toStop " + toStopId + " does not exist in schedule.");
236  }
237  }
238 
239  return result;
240  }
241 
243  ValidationResult result = new ValidationResult();
244  for (TransitLine line : schedule.getTransitLines().values()) {
245  for (TransitRoute route : line.getRoutes().values()) {
246  if (route.getDepartures().isEmpty())
247  result.addError("No departures defined for line %s, route %s".formatted(line.getId(), route.getId()));
248 
249  }
250  }
251 
252  return result;
253  }
254 
259 
260  ValidationResult result = new ValidationResult();
261 
262  // List of stops to the collected suspicious stops
263  Map<TransitStopFacility, DoubleList> suspiciousStops = new TreeMap<>(Comparator.comparing(TransitStopFacility::getName));
264 
265  for (TransitLine line : schedule.getTransitLines().values()) {
266 
267  for (TransitRoute route : line.getRoutes().values()) {
268 
269  List<TransitRouteStop> routeStops = route.getStops();
270 
271  // For too short routes, we can not detect outliers
272  if (routeStops.size() <= 4)
273  continue;
274 
275  double lastDepartureOffset = routeStops.getFirst().getDepartureOffset().or(routeStops.getFirst().getArrivalOffset()).seconds();
276 
277  DoubleList speeds = new DoubleArrayList();
278  DoubleList dists = new DoubleArrayList();
279 
280  for (int i = 1; i < routeStops.size(); i++) {
281  TransitRouteStop routeStop = routeStops.get(i);
282 
283  if (routeStop.getStopFacility().getCoord() == null)
284  break;
285 
286  double departureOffset = routeStop.getArrivalOffset().or(routeStop.getDepartureOffset()).orElse(0);
287  double travelTime = departureOffset - lastDepartureOffset;
288  double length = CoordUtils.calcEuclideanDistance(routeStop.getStopFacility().getCoord(),
289  routeStops.get(i - 1).getStopFacility().getCoord());
290 
291  dists.add(length);
292 
293  // Short distances are not checked, because here high speeds are not so problematic and arise from few seconds difference
294  if (length <= 20) {
295  speeds.add(-1);
296  continue;
297  }
298 
299  if (travelTime == 0) {
300  speeds.add(Double.POSITIVE_INFINITY);
301  continue;
302  }
303 
304  double speed = length / travelTime;
305  speeds.add(speed);
306  lastDepartureOffset = departureOffset;
307  }
308 
309  // If all speeds are valid, the stops and speeds can be checked
310  if (speeds.size() == routeStops.size() - 1) {
311 
312  // First check for suspicious stops
313  // These are stops with very high speed, and also high distance between stops
314  for (int i = 0; i < speeds.size() - 1; i++) {
315  TransitRouteStop stop = routeStops.get(i + 1);
316  double toStop = speeds.getDouble(i);
317  double fromStop = speeds.getDouble(i + 1);
318 
319  double both = (toStop + fromStop) / 2;
320 
321  double dist = (dists.getDouble(i) + dists.getDouble(i + 1)) / 2;
322 
323  // Only if the distance is large, we assume a mapping error might have occurred
324  if (dist < 5_000)
325  continue;
326 
327  // Remove the considered speeds from the calculation
328  DoubleList copy = new DoubleArrayList(speeds);
329  copy.removeDouble(i);
330  copy.removeDouble(i);
331  copy.removeIf(s -> s == -1 || s == Double.POSITIVE_INFINITY);
332 
333  double mean = copy.doubleStream().average().orElse(-1);
334 
335  // If no mean is known, use a high value to avoid false positives
336  if (mean == -1) {
337  mean = 70;
338  }
339 
340  // Some hard coded rules to detect suspicious stops, these are speed m/s, so quite high values
341  if (((toStop > 3 * mean && both > 50) || toStop > 120) && (((fromStop > 3 * mean && both > 50) || fromStop > 120))) {
342  DoubleList suspiciousSpeeds = suspiciousStops.computeIfAbsent(stop.getStopFacility(), (k) -> new DoubleArrayList());
343  suspiciousSpeeds.add(toStop);
344  suspiciousSpeeds.add(fromStop);
345  }
346  }
347 
348  // Then check for implausible travel times
349  for (int i = 0; i < speeds.size(); i++) {
350  double speed = speeds.getDouble(i);
351  TransitStopFacility from = routeStops.get(i).getStopFacility();
352  TransitStopFacility to = routeStops.get(i + 1).getStopFacility();
353  if (speed > 230) {
354  result.addWarning("Suspicious high speed from stop %s (%s) to %s (%s) on line %s, route %s, index: %d: %.2f m/s, %.2fm"
355  .formatted(from.getName(), from.getId(), to.getName(), to.getId(), line.getId(), route.getId(), i, speed, dists.getDouble(i)));
356  }
357  }
358  }
359  }
360  }
361 
362  for (Map.Entry<TransitStopFacility, DoubleList> e : suspiciousStops.entrySet()) {
363  TransitStopFacility stop = e.getKey();
364  double speed = e.getValue().doubleStream().average().orElse(-1);
365  result.addWarning("Suspicious location for stop %s (%s) at stop area %s: %s, avg. speed: %.2f m/s".formatted(stop.getName(), stop.getId(), stop.getStopAreaId(), stop.getCoord(), speed));
366  }
367 
368  return result;
369  }
370 
371  public static ValidationResult validateAll(final TransitSchedule schedule, final Network network) {
373  v.add(validateNetworkRoutes(schedule, network));
374  try {
375  v.add(validateStopsOnNetworkRoute(schedule, network));
376  } catch (NullPointerException e) {
377  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()));
378  }
379  v.add(validateAllStopsExist(schedule));
380  v.add(validateOffsets(schedule));
381  v.add(validateTransfers(schedule));
382  v.add(validateStopCoordinates(schedule));
383  v.add(validateDepartures(schedule));
384  return v;
385  }
386 
387  public static void printResult(final ValidationResult result) {
388  if (result.isValid()) {
389  System.out.println("Schedule appears valid!");
390  } else {
391  System.out.println("Schedule is NOT valid!");
392  }
393  if (result.getErrors().size() > 0) {
394  System.out.println("Validation errors:");
395  for (String e : result.getErrors()) {
396  System.out.println(e);
397  }
398  }
399  if (result.getWarnings().size() > 0) {
400  System.out.println("Validation warnings:");
401  for (String w : result.getWarnings()) {
402  System.out.println(w);
403  }
404  }
405  }
406 
413  public static void main(String[] args) throws IOException, SAXException, ParserConfigurationException {
414  if (args.length > 2 || args.length < 1) {
415  System.err.println("Usage: TransitScheduleValidator transitSchedule.xml [network.xml]");
416  return;
417  }
418 
420  s.getConfig().transit().setUseTransit(true);
422  Network net = s.getNetwork();
423 
424  if (args.length > 1) {
425  new MatsimNetworkReader(s.getNetwork()).readFile(args[1]);
426  }
427  new TransitScheduleReader(s).readFile(args[0]);
428 
429  ValidationResult v = validateAll(ts, net);
430  printResult(v);
431  }
432 
433  public static class ValidationResult {
434 
435  public enum Severity {
436  WARNING, ERROR
437  }
438 
439  public enum Type {
440  HAS_MISSING_STOP_FACILITY, HAS_NO_LINK_REF, ROUTE_HAS_UNREACHABLE_STOP, OTHER
441  }
442 
443  public static class ValidationIssue<T> {
444  private final Severity severity;
445  private final String message;
446  private final Type errorCode;
447  private final Collection<Id<T>> entities;
448 
449  public ValidationIssue(Severity severity, String message, Type errorCode, Collection<Id<T>> entities) {
450  this.severity = severity;
451  this.message = message;
452  this.errorCode = errorCode;
453  this.entities = entities;
454  }
455 
457  return severity;
458  }
459 
460  public String getMessage() {
461  return message;
462  }
463 
464  public Type getErrorCode() {
465  return errorCode;
466  }
467 
468  public Collection<Id<T>> getEntities() {
469  return entities;
470  }
471 
472  }
473 
474  private boolean isValid = true;
475  private final List<ValidationIssue> issues = new ArrayList<>();
476 
477  public boolean isValid() {
478  return this.isValid;
479  }
480 
481  public List<String> getWarnings() {
482  List<String> result = new ArrayList<>();
483  for (ValidationIssue issue : this.issues) {
484  if (issue.severity == Severity.WARNING) {
485  result.add(issue.getMessage());
486  }
487  }
488  return Collections.unmodifiableList(result);
489  }
490 
491  public List<String> getErrors() {
492  List<String> result = new ArrayList<>();
493  for (ValidationIssue issue : this.issues) {
494  if (issue.severity == Severity.ERROR) {
495  result.add(issue.getMessage());
496  }
497  }
498  return Collections.unmodifiableList(result);
499  }
500 
501  public List<ValidationIssue> getIssues() {
502  return Collections.unmodifiableList(this.issues);
503  }
504 
505  public void addWarning(final String warning) {
506  this.issues.add(new ValidationIssue(Severity.WARNING, warning, Type.OTHER, Collections.<Id<?>>emptyList()));
507  }
508 
509  public void addError(final String error) {
510  this.issues.add(new ValidationIssue(Severity.ERROR, error, Type.OTHER, Collections.<Id<?>>emptyList()));
511  this.isValid = false;
512  }
513 
514  public void addIssue(final ValidationIssue issue) {
515  this.issues.add(issue);
516  if (issue.severity == Severity.ERROR) {
517  this.isValid = false;
518  }
519  }
520 
521  public void add(final ValidationResult otherResult) {
522  this.issues.addAll(otherResult.getIssues());
523  this.isValid = this.isValid && otherResult.isValid;
524  }
525  }
526 }
Map< Id< TransitStopFacility >, TransitStopFacility > getFacilities()
ValidationIssue(Severity severity, String message, Type errorCode, Collection< Id< T >> entities)
static double calcEuclideanDistance(Coord coord, Coord other)
static ValidationResult validateDepartures(TransitSchedule schedule)
static ValidationResult validateAllStopsExist(final TransitSchedule schedule)
static ValidationResult validateOffsets(final TransitSchedule schedule)
TransitConfigGroup transit()
Definition: Config.java:451
static void printResult(final ValidationResult result)
static ValidationResult validateAll(final TransitSchedule schedule, final Network network)
static ValidationResult validateUsedStopsHaveLinkId(final TransitSchedule schedule)
static ValidationResult validateNetworkRoutes(final TransitSchedule schedule, final Network network)
Map< Id< Link >, ? extends Link > getLinks()
abstract TransitStopFacility getStopFacility()
boolean equals(Object obj)
Definition: Id.java:139
Map< Id< TransitLine >, TransitLine > getTransitLines()
static ValidationResult validateTransfers(final TransitSchedule schedule)
static Scenario createScenario(final Config config)
static ValidationResult validateStopCoordinates(final TransitSchedule schedule)
static Config createConfig(final String context)
static ValidationResult validateStopsOnNetworkRoute(final TransitSchedule schedule, final Network network)
OptionalTime or(OptionalTime optionalTime)