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}